跳到主要内容

哈希

2024年04月08日
柏拉文
越努力,越幸运

一、认识


Redis 中,hash(哈希)是一种用于存储 键值对集合 的数据类型,类似于一个小型的 key-value 字典(对象)。它特别适合存储具有多个属性的对象,例如用户信息、产品详情等。在 Redis 中,哈希 是以字段(field)和值(value)的形式存储在一个主键(key)下的。例如,可以使用 hash 来存储用户的信息,如 user:1001,并包含多个字段:nameageemail 等。

Redis Hash 存储结构类似于 JSON 对象或字典。比如: user:1001 → { "name": "Alice", "age": "25", "email": "alice@example.com" }Redis Hash 高效存储, 如果哈希中的字段数量较少(小于 512 个字段,字段和值总大小不超过 64KB),Redis 会使用特殊的 ziplist(压缩列表) 实现,节省内存。Redis 适合存储结构化数据, 可用于存储对象数据,如用户信息、配置项等,避免使用多个字符串键。Redis Hash 支持部分字段操作,避免整体操作, 可以只修改、获取某个字段,而不影响整个哈希数据。Redis 字段增量操作支持, Redis 提供对数值字段的原子递增/递减操作,方便计数操作。

Redis Hash 适用于存储结构化数据,如果你的数据像对象那样包含多个属性,使用 Hash 更合适。比如说: 用户信息存储; 对象属性操作CRUD, 需要频繁更新或获取对象的部分字段,例如商品库存、价格信息等; 动态配置管理, 需要存储多种配置参数并且可能会经常调整,比如应用配置、用户偏好设置等; 排行榜存储, 存储用户游戏统计数据,如分数、等级、胜场等,方便查询和更新; 避免键的爆炸, 如果你的数据非常多且字段固定,使用 Hash 可以减少 Redis 的键数量,避免单独存储多个 String 类型带来的管理负担。在存储大量类似数据时,避免使用大量独立的 String 类型键,而是将数据组织成 Hash 类型,以减少 Redis 管理的键数量,提高查询效率, 降低键的数量,避免“键爆炸”。合理使用哈希字段,避免过大对象, 虽然 Hash 可以存储大量字段,但单个 Hash 过大可能导致:LRU 淘汰策略时,整个 Hash 被一次性移除,影响业务稳定性, 迁移、持久化时增加 Redis 负载, 所以, 设计合理的存储粒度,避免单个 Hash 超过 10 万个字段。

Redis Hash 优化策略: Redis 内部会对小型 Hash 采用特殊的优化机制,即压缩列表(ZipList), 它可以将数据存储在同一个连续的内存块中, 减少内存碎片以减少内存开销, 触发条件为: 字段数量(hash-max-ziplist-entries) ≤ 512(默认值), 每个字段和值的大小 (hash-max-ziplist-value) ≤ 64 字节(默认值)。但是, ZipList 读写有指针位移, 且新增删除有内存重分配。如果数据超出阈值,Redis 将自动切换为标准的 Hash Table 结构,占用更多的内存。

  • 调整参数以适应业务需求:

    CONFIG SET hash-max-ziplist-entries 1024
    CONFIG SET hash-max-ziplist-value 128
  • 检查是否启用 ZipList 优化: 如果返回 ziplist,说明正在使用压缩存储。

    OBJECT ENCODING user:1001

Redis Hash 分片策略: 将大数据拆分到多个 Hash, 每个小段尽可能符合 ZipList 的优化范围, 因为 ZipList 非常节省内存空间。但是 ZipList 读写有指针位移, 且新增删除有内存重分配, 会有性能问题。所以, 我们权衡之后, 选择第三种最好。

// picId => usesrId, 大约有 100 万 数据量, 有如下三种方案:

set pic:{picId} user:{userId} // 全部 string, 浪费内存

hset allPics pic:{pidId} user:{userId} // 一个 hash, hash allPics 元素个数已经有 100 万多个, 是一个超大的 Big Key, 浪费内存。

hset segment:{picId/100} pic:{picId%100} user:{userId} // hash 分片

// 三种方案的结果, hash 分片消耗的内存是最小的, 且比前两种少了好几倍。一个 hahs 消耗的内存是最多的, 是一个超大的 Big Key。