Redis数据结构-ZipList
一、前言:为了省下每一字节
在 Redis 的世界里,“内存就是金钱”。对于存储大量小对象的场景(如用户标签、会话信息),即使是微小的内存浪费,乘以海量数据后也会成为天文数字。
为此,Redis 在早期版本中引入了一种名为 ZipList(压缩列表) 的精巧数据结构。它并非使用传统压缩算法,而是通过极致紧凑的内存布局,将空间利用率推向了极致。
💡 核心价值:
ZipList 是 Redis 在“小而美”场景下的内存杀手锏,可节省高达 50% 以上的内存!
本文将带你:
- 拆解 ZipList 的连续内存布局
- 揭秘其如何同时拥有数组和链表的优点
- 警示“连锁更新”这一致命缺陷
二、ZipList 是什么?连续内存中的双向链表
2.1 核心思想
ZipList 本质上是一个特殊编码的双向链表,但它不使用指针,而是将所有数据存储在一块连续的内存区域中。
✅ 关键特性:
- 内存连续:像数组一样,无内存碎片。
- 双向遍历:像链表一样,支持从头到尾和从尾到头。
- 变长节点:每个节点(entry)可以存储不同长度的字符串或整数。
2.2 内存布局详解
一个完整的 ZipList 在内存中的布局如下:
+---------+--------+--------+------------------+------------------+------------------+-------+
| zlbytes | zltail | zllen | entry1 | entry2 | ... | zlend |
+---------+--------+--------+------------------+------------------+------------------+-------+
<-- 4B --> <-- 4B -> <-- 2B -> <---- 变长 -----> <---- 变长 -----> <---- 变长 -----> <--1B-->
头部元数据(Header)
zlbytes(4字节):整个 ZipList 占用的总字节数(包括 header 和zlend)。zltail(4字节):最后一个 entry 距离起始地址的偏移量。这使得ZLTAIL(获取尾部元素)操作能在 O(1) 时间内完成。zllen(2字节):ZipList 中包含的 entry 数量。- 注意:由于只有 2 字节,当元素数量超过
UINT16_MAX(65535) 时,此字段不再准确,需要遍历计算。
- 注意:由于只有 2 字节,当元素数量超过
Entry 节点(变长)
每个 entry 由三部分组成:
+----------+----------+---------+
| prevlen | encoding | content |
+----------+----------+---------+
prevlen:前一个 entry 的长度(单位:字节)。- 若前一个 entry 长度 < 254 字节,则用 1 字节 存储。
- 否则,用 5 字节 存储(第1字节为
0xFE,后4字节为真实长度)。
encoding:当前 entry 的内容类型和长度。- 可以表示字符串(不同长度前缀)或整数(多种编码,如 int16, int32 等)。
content:实际存储的数据。
结束标记
zlend(1字节):固定值0xFF,标识 ZipList 结束。
三、ZipList 的双面性:优势与致命缺陷
3.1 优势:博采众长
- 继承数组的优点:
- 内存局部性好:连续内存,CPU 缓存命中率高。
- 极致的空间效率:无指针开销,元数据极小。
- 继承链表的优点:
- 双向遍历:通过
prevlen可以轻松向前回溯。 - 变长存储:灵活适应不同大小的数据。
- 双向遍历:通过
3.2 致命缺陷:连锁更新(Cascade Update)
这是 ZipList 最著名的痛点!
问题根源:prevlen 字段的变长设计。
场景:
- 假设有一系列 entry,每个 entry 的长度都是 253 字节。
- 此时,每个 entry 的
prevlen字段都只需要 1 字节(因为 253 < 254)。 - 现在,在列表头部插入一个 长度为 254 字节 的新 entry。
- 第二个 entry 的
prevlen需要从 1 字节 扩展到 5 字节。 - 这导致第二个 entry 自身的总长度变成了
253 + 4 = 257字节。 - 于是,第三个 entry 的
prevlen也需要从 1 字节扩展到 5 字节... - 雪崩效应:整个列表的后续所有 entry 都需要更新
prevlen字段!
⚠️ 后果:一次 O(1) 的插入操作,可能退化为 O(n^2) 的灾难!虽然平均情况很少发生,但一旦触发,对性能是毁灭性的。
四、ZipList 在 Redis 中的应用与配置
4.1 应用场景(Redis 7.0 之前)
ZipList 曾是以下数据类型的默认底层编码(当数据量小时):
- List:
list-max-ziplist-size(默认 -2,表示最多 8KB) - Hash:
hash-max-ziplist-entries(默认 512)和hash-max-ziplist-value(默认 64 字节) - ZSet:
zset-max-ziplist-entries(默认 128)和zset-max-ziplist-value(默认 64 字节)
4.2 版本演进:从 ZipList 到 ListPack
由于“连锁更新”问题无法根治,Redis 社区最终决定废弃 ZipList。
- Redis 3.2:引入
quicklist作为 List 的底层,它是一个 ZipList 的双向链表,限制了单个 ZipList 的大小,从而将连锁更新的影响控制在局部。 - Redis 7.0:彻底移除 ZipList,引入了全新的
listpack结构。- ListPack 的改进:每个 entry 只记录自身长度,不再记录前一个 entry 的长度,从根本上杜绝了连锁更新!
📌 现状:如果你使用的是 Redis 7.0+,那么 Hash 和 ZSet 的紧凑编码已经变成了
listpack,而 List 的底层是quicklist(内部节点是listpack)。
五、动手实验:观察 ZipList 的行为(Redis < 7.0)
5.1 查看 Hash 的底层编码
# 创建一个小 Hash
> HSET smallhash name "Alice" age "25"
(integer) 2
# 查看编码(应为 ziplist)
> OBJECT ENCODING smallhash
"ziplist"
# 插入一个超长的 value,使其转为 Dict
> HSET smallhash long_field "A...A" # (65个'A')
(integer) 1
# 再次查看(应变为 hashtable)
> OBJECT ENCODING smallhash
"hashtable"
六、结语
感谢您的阅读!如果你有任何疑问或想要分享的经验,请在评论区留言交流!
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐

所有评论(0)