Redis List 底层实现原理:从压缩列表到快速链表的进化史
一、底层数据结构演进
Redis List 的底层实现经历了三个阶段:
- Redis 3.2 之前:根据元素大小和数量,在**压缩列表(ziplist)和双向链表(linkedlist)**之间切换
- 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;
创新设计:
- 分片存储:将大列表拆分为多个ziplist节点
- LZF压缩:可以对中间节点进行压缩(通过
list-compress-depth配置) - 动态平衡:
- 插入时:检查当前ziplist是否超过
fill参数限制 - 删除时:检查相邻ziplist是否可以合并
- 插入时:检查当前ziplist是否超过
内存布局示例:
quicklist
├── head → quicklistNode(ziplist)[entry1, entry2, entry3]
├── ...
└── tail → quicklistNode(ziplist)[entryN-2, entryN-1, entryN]
五、关键参数调优
-
list-max-ziplist-size:- 正数:表示ziplist最多包含的元素个数
- 负数:
- -1:每个ziplist不超过4KB
- -2:不超过8KB(默认值)
- -5:不超过64KB
-
list-compress-depth:- 0:不压缩(默认)
- 1:首尾各1个节点不压缩,中间节点压缩
- 2:首尾各2个节点不压缩,其余压缩
六、性能对比测试
| 操作类型 | ziplist | linkedlist | quicklist |
|---|---|---|---|
| LPUSH | O(1) | O(1) | O(1) |
| 中间插入 | O(N) | O(1) | O(N)但N较小 |
| LRANGE 0 100 | O(1) | O(N) | O(N)但局部连续 |
| 内存占用 | 最小 | 最大 | 中等 |
七、底层操作原理图解
LPUSH操作流程:
- 检查头节点ziplist是否有空间
- 有空间:直接插入头ziplist
- 无空间:创建新ziplist节点并插入
LTRIM操作优化:
- 计算需要保留的范围
- 直接释放不需要的ziplist节点
- 对边界ziplist进行裁剪
八、设计哲学启示
- 空间局部性原则:quicklist让相邻元素尽量存储在同一个ziplist中
- 时间-空间权衡:用适度的CPU开销换取显著的内存节省
- 分治思想:大问题拆分为多个小问题处理
九、特别注意事项
- 大元素问题:当单个元素超过ziplist限制时,整个quicklist节点会退化为纯元素存储
- 压缩风险:启用压缩后,操作性能会有10%-20%的下降
- 监控指标:
redis-cli --bigkeys可以识别大ListMEMORY USAGE key查看具体内存占用
底层实现彩蛋:在Redis 7.0中,quicklist的ziplist实现被替换为listpack,进一步优化了内存使用和修改性能。listpack通过完全消除ziplist的级联更新问题,使得中间插入/删除操作效率提升显著。