Redis数据结构-Hash
一、前言:对象存储的“瑞士军刀”
在 Redis 的五大数据类型中,Hash(哈希) 是最贴近“对象”概念的一个。它允许你将一个对象的多个属性(字段)存储在一个键(key)下,形成一个 field -> value 的映射集合。
从存储用户信息(name, age, email)到管理商品详情(price, stock, category),再到实现购物车(product_id -> quantity),Hash 无处不在。
但你是否想过,Redis 是如何在底层高效地管理这些字段的?为什么对小对象和大对象的处理方式截然不同?
💡 核心价值:
Redis Hash 的底层会根据数据规模,在 ZipList(压缩列表)和 Dict(字典)之间智能切换,以实现内存效率与操作性能的最佳平衡!
本文将带你:
- 拆解 ZipList 的紧凑内存布局
- 揭秘 Dict 如何实现 O(1) 的字段操作
- 理解编码转换背后的阈值逻辑
二、Hash 的双重身份:ZipList 与 Dict
Redis Hash 并非只有一种底层实现,而是拥有两种编码(encoding),由 redisObject 的 encoding 字段决定:
编码 (encoding) |
底层数据结构 | 适用场景 |
|---|---|---|
OBJ_ENCODING_ZIPLIST (Redis < 7.0)OBJ_ENCODING_LISTPACK (Redis >= 7.0) |
ZipList / ListPack | 字段数量少、字段值小 |
OBJ_ENCODING_HT |
Dict (字典/哈希表) | 字段数量多、字段值大 |
这种设计体现了 Redis “因地制宜” 的优化哲学:对简单、规则的数据用最省空间的结构;对复杂、庞大的数据用最高效的结构。
注意:从 Redis 7.0 开始,
ZipList已被更安全的ListPack取代,以解决“连锁更新”问题。但核心思想不变,本文以ZipList为例进行讲解。
三、编码一:ZipList - 小对象的内存奇迹
3.1 诞生背景
当 Hash 中存储的是一个只有几个字段的小对象(如用户ID、姓名、年龄)时,使用通用的哈希表(Dict)会显得“杀鸡用牛刀”。因为哈希表需要为每个字段存储一个完整的 dictEntry 结构(包含 key, value, next 指针等),内存开销较大。
为了极致节省内存,Redis 引入了 ZipList。
3.2 源码结构
ZipList 是一块连续的内存区域,其结构如下:
+----------+--------+-------+--------+------+-+
| zlbytes | zltail | zllen | entry1 | ... |zlend|
+----------+--------+-------+--------+------+-+
- zlbytes:整个 ZipList 占用的字节数。
- zltail:尾节点距离起始地址的偏移量,用于快速定位尾部。
- zllen:节点数量。
- entryX:实际的字段-值对。每个 entry 存储一个
field和一个value,格式为:[prevlen][encoding][data]。 - zlend:结束标记
0xFF。
✅ 关键特性:
- 内存连续:所有字段-值对紧密排列,无任何指针开销。
- 紧凑编码:整数直接存数值,短字符串存长度+内容,极大节省空间。
- 顺序存储:
field1 -> value1 -> field2 -> value2 -> ...
3.3 致命缺陷:连锁更新(Cascading Update)
ZipList 的最大痛点在于其连锁更新问题。
- 场景:假设 ZipList 中有三个节点 A, B, C。节点 A 的
value原本很短,现在被更新为一个很长的值,导致 A 占用的空间变大。 - 后果:节点 B 的
prevlen(记录前一个节点长度的字段)可能不足以存储 A 的新长度,需要扩容。B 扩容后,又可能导致 C 的prevlen不够用,需要扩容... 如此连锁反应,可能引发整个 ZipList 的内存重分配。 - 性能:最坏情况下,一次更新操作的时间复杂度从 O(1) 退化到 O(N²)。
Redis 7.0 的改进:引入
ListPack,移除了prevlen字段,从根本上解决了连锁更新问题。
3.4 内存优势
假设一个 Hash 包含 10 个短字段(如 name: "Alice", age: "30"):
- ZipList:所有数据紧凑存储,无指针,总内存可能只需几百字节。
- Dict:每个
dictEntry至少需要8(key)+8(value)+8(next)= 24字节,加上哈希表本身的桶数组,总内存轻松超过240+ bytes。
内存节省高达 50% 以上!
四、编码二:Dict - 通用的高性能解决方案
一旦 Hash 不再满足 ZipList 的苛刻条件,Redis 会立即将其转换为 Dict(字典)。
4.1 为什么是 Dict?
Dict 是 Redis 的基石数据结构之一,它是一个哈希表,天然具备以下特性:
- O(1) 平均时间复杂度:用于添加 (
HSET)、删除 (HDEL)、查找 (HGET) 操作。 - 支持大量数据:通过渐进式 rehash,可以轻松处理百万级字段。
4.2 Dict 在 Hash 中的用法
在 Hash 的场景下,Dict 的使用非常直接:
- Key:存储 Hash 的
field(字段名)。 - Value:存储 Hash 的
value(字段值)。
// 伪代码示意
dict *d = dictCreate(&hashDictType, NULL);
dictAdd(d, "name", "Alice");
dictAdd(d, "age", "30");
✅ 优势:保证了即使 Hash 包含海量字段,性能依然稳定。
4.3 渐进式 Rehash
Dict 本身也有一套精妙的扩容/缩容机制(渐进式 rehash),确保在数据量巨大时,单次操作的延迟依然很低。
五、编码转换:阈值与触发条件
Redis 通过两个配置项来控制 Hash 何时从 ziplist 转换为 hashtable:
| 配置项 | 默认值 | 说明 |
|---|---|---|
hash-max-ziplist-entries |
512 |
当 Hash 中的字段数量超过此值时,会转换为 Dict。 |
hash-max-ziplist-value |
64 |
当 Hash 中任意一个字段的值长度超过此值(字节)时,会转换为 Dict。 |
设计考量:
- 512 和 64 这两个阈值:是内存效率和操作性能的平衡点。超过此限制后,ZipList 的 O(N) 查找和连锁更新风险开始显现,而 Dict 的 O(1) 优势则愈发明显。
- 即时转换:保证了数据模型的一致性。一旦数据不再是“小而美”,就必须切换到通用模型。
六、动手实验:观察 Hash 的编码变化
6.1 验证 ZipList 编码
# 添加少量短字段
> HSET user:1001 name "Alice" age "30" city "Beijing"
(integer) 3
# 查看编码(Redis 7.0+ 应为 listpack)
> OBJECT ENCODING user:1001
"listpack"
6.2 触发转换:超过字段数量阈值
# 添加第513个字段
> for i in {1..513}; do redis-cli HSET big_hash "field_$i" "value_$i"; done
# 查看编码(应为 hashtable)
> OBJECT ENCODING big_hash
"hashtable"
6.3 触发转换:字段值过长
# 创建一个新Hash,并添加一个超长值
> HSET long_value_hash name "Alice" bio "a...a" # (65个'a')
(integer) 2
# 编码直接为 hashtable
> OBJECT ENCODING long_value_hash
"hashtable"
七、结语
感谢您的阅读!如果你有任何疑问或想要分享的经验,请在评论区留言交流!
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)