Redis quicklist 实现详解(基于 6.0.10 版本)
Redis 的 List 类型在 3.2 版本后引入了 quicklist 作为底层实现,替代了此前的 “ziplist + linkedlist” 混合策略。quicklist 结合了 ziplist 的内存紧凑性和 linkedlist 的灵活扩展性,是 Redis 对列表结构的一次重要优化。本文结合源码结构和设计思路,详解 quicklist 的实现机制。
quicklist 的设计背景:为何替代旧方案?
在 3.2 版本前,Redis List 采用自适应编码:
- 元素少时用 ziplist(压缩列表):连续内存存储,节省空间,但插入 / 删除大元素时需移动大量数据(O (N) 时间复杂度)。
- 元素多时用 linkedlist(双向链表):节点独立分配内存,插入 / 删除高效(O (1)),但指针开销大(每个节点的 prev/next 指针占 16 字节),且内存碎片化严重。
旧方案的核心问题:
- ziplist 过大时(如存储 10 万个元素),一次插入 / 删除操作可能阻塞主线程(需移动大量内存)。
- linkedlist 节点分散存储,内存利用率低,且缓存命中率差(CPU 缓存难以命中连续数据)。
quicklist 的改进思路:
将 List 拆分为多个小段,每段用 ziplist 存储(保持紧凑性),段之间用双向指针连接(保持灵活性)。既避免了单个 ziplist 过大的性能问题,又减少了 linkedlist 的指针开销和碎片化。
quicklist 核心结构解析
quicklist 的实现依赖两个核心结构体:quicklist(列表本身)和 quicklistNode(列表节点)。
1. quicklist 结构体(列表整体)
1 | typedef struct quicklist { |
关键字段说明:
fill:控制单个quicklistNode中 ziplist 的最大大小(或元素数量),取值来自配置list-max-ziplist-size:- 负数:表示 ziplist 最大字节数(如
-2对应 8KB,-1对应 4KB)。 - 正数:表示 ziplist 最多存储的元素数量(如
5表示每个节点最多 5 个元素)。
- 负数:表示 ziplist 最大字节数(如
compress:控制列表两端节点是否压缩(中间节点默认压缩),减少内存占用:0:不压缩(默认)。1:压缩两端各 1 层节点。2:压缩两端各 2 层节点(以此类推)。
2. quicklistNode 结构体(列表节点)
1 | typedef struct quicklistNode { |
关键字段说明:
zl:核心字段,指向当前节点的 ziplist 内存。若节点被压缩(encoding=2),则zl存储 LZF 压缩后的 ziplist 数据。sz:ziplist 的字节数(无论是否压缩,均记录原始大小),用于快速判断是否需要拆分节点。count:当前 ziplist 中的元素数量,用于快速计算 List 总长度(quicklist.count是所有节点count的总和)。
quicklist 的核心工作机制
1. 元素的存储与分段
- List 的元素被分散存储在多个
quicklistNode的 ziplist 中,每个 ziplist 的大小由fill控制(默认 8KB)。 - 例如:若
fill=-2(8KB),每个 ziplist 最多存储 8KB 的元素,超出则创建新节点(quicklistNode),通过prev/next指针串联。
2. 插入与删除操作
- 插入元素:
- 定位目标节点(如头部、尾部或指定索引)。
- 检查该节点的 ziplist 是否还有空间(未超过fill限制):
- 有空间:直接插入到 ziplist 中(ziplist 内部调整内存)。
- 无空间:创建新节点,插入元素到新节点,并调整
prev/next指针。
- 删除元素:
- 定位元素所在的 ziplist。
- 从 ziplist 中删除元素,若删除后 ziplist 为空,回收该
quicklistNode。 - 若相邻节点的 ziplist 较小,可能合并(减少节点数量,节省指针开销)。
3. 压缩机制(节省内存)
对于不常访问的中间节点,quicklist 会用 LZF 算法压缩其 ziplist(encoding=2):
- 压缩触发:当节点长时间未被访问,且
compress配置大于 0 时,自动压缩。 - 解压访问:当需要访问压缩节点时,临时解压(
recompress=1标记后续需重新压缩)。 - 优势:大幅减少内存占用(尤其长列表),且不影响两端常用节点的访问速度。
4. 与配置的关联(list-max-ziplist-size)
该配置直接决定 fill 字段的值,是平衡性能与内存的关键:
1 | # 配置示例(默认值) |
- 推荐配置:
- 若 List 元素小且访问频繁:
-1(4KB)—— 更小的 ziplist 减少插入 / 删除时的内存移动。 - 若 List 元素大或访问稀疏:
-2(8KB)—— 减少节点数量,降低指针开销。
- 若 List 元素小且访问频繁:
quicklist 与旧方案的对比
| 特性 | quicklist(3.2+) | 旧方案(ziplist + linkedlist) |
|---|---|---|
| 内存效率 | 高(ziplist 紧凑存储 + 分段减少指针开销) | 中(小数据 ziplist 高效,大数据 linkedlist 低效) |
| 插入 / 删除性能 | 优(分段 ziplist 减少内存移动) | 差(大 ziplist 移动成本高,linkedlist 指针开销大) |
| 内存碎片化 | 低(ziplist 连续 + 节点数量可控) | 高(linkedlist 节点分散分配) |
| 配置灵活性 | 高(通过 fill 调整分段大小) | 低(仅通过阈值切换编码) |
| 适用场景 | 所有 List 场景(小列表到大列表均优化) | 仅适合极小列表(ziplist)或纯遍历场景(linkedlist) |
v1.3.10