Redis 中 ZipList 的级联更新问题

ZipList 的结构

ZipList 是 Redis 中用于实现 ZSet 的压缩数据结构,其元素采用连续存储方式,具有很高的内存紧凑性。

ZipList 结构组成如下:

  • zlbytes:4字节,记录整个ziplist的字节数
  • zltail:4字节,记录最后一个entry的偏移量
  • zllen:2字节,记录entry数量
  • Entry:实际存储的数据项
  • zlend:1字节,结束标记

其中Entry的结构包含:

  • prevlen:记录前一个entry的长度
    • 前一个entry长度 <254:占用1字节
    • 前一个entry长度 ≥254:占用5字节(首字节固定为0xFE)
  • encoding:内容编码(包含类型和长度信息)
  • content:实际数据

需要注意的是,prevlen采用可变长度存储方案,根据前一个entry的长度动态调整存储空间。

ZipList的级联更新问题分析

假设当前ZipList中包含3个Entry,每个Entry的总长度均为253字节。此时在Entry1后插入一个长度为300字节的新Entry,将触发以下连锁反应:

  1. Entry2的prevlen需要更新,因为其前驱节点变为新插入的Entry
  2. 由于新Entry长度超过254字节,Entry2的prevlen需要扩展为5字节
  3. Entry2长度增加可能导致其总长度超过254字节,进而影响Entry3的prevlen存储
  4. 这种连锁反应可能持续传播,最坏情况下需要更新所有后续Entry

这种级联更新机制在极端情况下(如大量连续边缘长度的Entry)会导致显著的性能损耗

ListPack是如何解决的

ListPack为解决这一问题,直接移除了prevlen字段。其核心改动在于Entry结构的设计:

  1. 废弃原有prevlen字段
  2. 引入backlen字段记录整个Entry的字节数
  3. backlen位于元素末尾,采用变长存储(1-5字节)

通过这种设计,ListPack中的每个数据项只需记录自身长度,不再存储前驱节点长度。因此,进行增删操作时仅影响当前元素,无需触发级联更新,从而彻底解决了级联更新的问题。