Redis List 底层实现原理:从压缩列表到快速链表的进化史

118 阅读3分钟

Redis List 底层实现原理:从压缩列表到快速链表的进化史

一、底层数据结构演进

Redis List 的底层实现经历了三个阶段:

  1. Redis 3.2 之前:根据元素大小和数量,在**压缩列表(ziplist)双向链表(linkedlist)**之间切换
  2. Redis 3.2 及以后:统一采用**快速链表(quicklist)**结构

二、压缩列表(ziplist):内存节约大师

结构特点

// ziplist 内存布局
[zlbytes][zltail][zllen][entry1][entry2]...[entryN][zlend]
  • 连续内存块:所有元素紧凑存储,没有指针开销
  • 变长编码:根据元素实际大小动态选择存储空间
  • 逆向遍历:通过zltail可以直接定位到列表末尾

操作特性

  • 插入/删除平均O(N)复杂度(需要内存重分配和数据搬移)
  • 查询操作O(1)(通过计算偏移量直接访问)

触发转换条件(redis.conf配置):

list-max-ziplist-size 512  # 元素数量阈值
list-max-ziplist-value 64  # 单个元素大小阈值(字节)

三、双向链表(linkedlist):传统老将

节点结构

typedef struct listNode {
    struct listNode *prev;
    struct listNode *next;
    void *value;
} listNode;

优缺点

  • 优点:修改操作O(1)时间复杂度
  • 缺点:每个元素需要额外存储前后指针(64位系统每个指针8字节)

四、快速链表(quicklist):集大成者

核心设计

typedef struct quicklist {
    quicklistNode *head;
    quicklistNode *tail;
    unsigned long count;     // 所有ziplist中的元素总数
    unsigned long len;       // quicklist节点数量
    int fill : 16;           // ziplist大小限制
    unsigned int compress : 16; // LZF压缩深度
} quicklist;

typedef struct quicklistNode {
    struct quicklistNode *prev;
    struct quicklistNode *next;
    unsigned char *zl;       // 指向ziplist的指针
    unsigned int sz;         // ziplist字节大小
    unsigned int count : 16; // ziplist元素数量
    unsigned int encoding : 2; // 编码方式
    unsigned int container : 2; // 存储类型
    unsigned int recompress : 1; // 是否被压缩
} quicklistNode;

创新设计

  1. 分片存储:将大列表拆分为多个ziplist节点
  2. LZF压缩:可以对中间节点进行压缩(通过list-compress-depth配置)
  3. 动态平衡
    • 插入时:检查当前ziplist是否超过fill参数限制
    • 删除时:检查相邻ziplist是否可以合并

内存布局示例

quicklist
├── head → quicklistNode(ziplist)[entry1, entry2, entry3]
├── ... 
└── tail → quicklistNode(ziplist)[entryN-2, entryN-1, entryN]

五、关键参数调优

  1. list-max-ziplist-size

    • 正数:表示ziplist最多包含的元素个数
    • 负数:
      • -1:每个ziplist不超过4KB
      • -2:不超过8KB(默认值)
      • -5:不超过64KB
  2. list-compress-depth

    • 0:不压缩(默认)
    • 1:首尾各1个节点不压缩,中间节点压缩
    • 2:首尾各2个节点不压缩,其余压缩

六、性能对比测试

操作类型ziplistlinkedlistquicklist
LPUSHO(1)O(1)O(1)
中间插入O(N)O(1)O(N)但N较小
LRANGE 0 100O(1)O(N)O(N)但局部连续
内存占用最小最大中等

七、底层操作原理图解

LPUSH操作流程

  1. 检查头节点ziplist是否有空间
  2. 有空间:直接插入头ziplist
  3. 无空间:创建新ziplist节点并插入

LTRIM操作优化

  1. 计算需要保留的范围
  2. 直接释放不需要的ziplist节点
  3. 对边界ziplist进行裁剪

八、设计哲学启示

  1. 空间局部性原则:quicklist让相邻元素尽量存储在同一个ziplist中
  2. 时间-空间权衡:用适度的CPU开销换取显著的内存节省
  3. 分治思想:大问题拆分为多个小问题处理

九、特别注意事项

  1. 大元素问题:当单个元素超过ziplist限制时,整个quicklist节点会退化为纯元素存储
  2. 压缩风险:启用压缩后,操作性能会有10%-20%的下降
  3. 监控指标
    • redis-cli --bigkeys可以识别大List
    • MEMORY USAGE key查看具体内存占用

底层实现彩蛋:在Redis 7.0中,quicklist的ziplist实现被替换为listpack,进一步优化了内存使用和修改性能。listpack通过完全消除ziplist的级联更新问题,使得中间插入/删除操作效率提升显著。