02.2 - List 底层——Quicklist
定位: Redis 中的列表(List)——双向链表、快速表、阻塞操作
面试高频度: ⭐⭐⭐⭐
考查方式: Quicklist 结构、Ziplist 压缩原理、LPUSH/LPOP 组合使用
一、这是什么?为什么需要它?
是什么
Redis List 是基于 Quicklist(快速表)实现的有序列表,允许在两端进行 O(1) 的插入/删除操作。
为什么需要 List?
场景:
消息队列 → LPUSH + BRPOP
最新动态/Feed流 → LPUSH + LRANGE
时间线数据 → LPUSH 有序存储
生产-消费模型 → LPUSH + RPOP
没有 List,如果用 String + 序列化来实现队列:
SET messages JSON.stringify([msg1, msg2, ...])
每次追加要:读取 → 反序列化 → 追加 → 序列化 → 写入
数据量大时,读/写整个列表 O(N) —— 极低效演化历史
动机:老方案的 linkedlist 每个节点单独 malloc → 内存碎片多 → quicklist 折中
二、原理拆解
2.1 Quicklist 结构
为什么分段?
不用一个大 ziplist 的问题:
在列表中间插入元素 → 需要移动大量内存 O(N/2)
用纯 linkedlist 的问题:
每个节点 ~40 字节指针开销 + malloc 碎片
Quicklist 折中:
✓ 每个 ziplist 连续内存 → 空间利用率高
✓ 分段 → 中间插入只影响一段(几百字节的移动)
✓ 双向指针 → 两端操作 O(1)2.2 Ziplist(压缩列表)内部结构
每个 ziplist 是一个紧凑的连续内存块:
| 字段 | 大小 | 说明 |
|------|------|------|
| zlbytes | 4B | ziplist 总字节数 |
| zltail | 4B | 尾节点偏移(逆向遍历用) |
| zllen | 2B | 节点数 |
| entry | 变长 | 实际数据节点 |
| zlend | 1B | 结束标记 (0xFF) |
每个 entry:
<prevlen> <encoding> <data>
prevlen: 前一个 entry 的长度(用于逆向遍历)
entry 的 encoding 类型:
字符串编码:长度编码在 encoding 字段中
整数编码:直接存整数(节省空间)Ziplist 的两个核心问题(也是 Quicklist 解决它的原因):
| 问题 | 说明 | Quicklist 如何解决 |
|------|------|------------------|
| 连锁更新 | 插入/删除改变 entry 长度,导致后续所有 entry 的 prevlen 需要更新 | 限制每个 ziplist 大小,连锁范围可控 |
| 大列表性能 | 中间操作 O(N) | 分片后,每个 ziplist 自身很小 |
2.3 Quicklist 配置参数
# 配置参数
list-max-ziplist-size -2 # 每个 ziplist 最大大小
# 取值说明:
# 正数 N:每个 ziplist 最多 N 个 entry
# 负数:限制 ziplist 的大小(字节)
# -1: 4KB -2: 8KB -3: 16KB
# -4: 32KB -5: 64KB
# 默认 -2 (8KB) 是性能与内存的最佳平衡点
list-compress-depth 0 # 是否压缩中间节点
# 0: 不压缩(全部不压缩)
# 1: 首尾各 1 个节点不压缩,中间压缩
# 2: 首尾各 2 个节点不压缩
# ...压缩原理(LZF 算法):列表中部的节点很少被访问,LZF 压缩它们节省内存,两端节点(LPUSH/LPOP 频繁操作)保持不压缩。
三、图解全景
四、实战验证
4.1 验证 Quicklist 特性
redis-cli
# 1. 创建 List
127.0.0.1:6379> LPUSH mylist a b c d e f
(integer) 6
127.0.0.1:6379> TYPE mylist
list
127.0.0.1:6379> OBJECT ENCODING mylist
"listpacks" # Redis 7.x 中 quicklist 被 listpack 替代
# 2. 验证两端操作 O(1)
127.0.0.1:6379> LPUSH mylist z
(integer) 7
127.0.0.1:6379> RPOP mylist
"a"
# 3. 范围读取
127.0.0.1:6379> LRANGE mylist 0 -1
1) "z"
2) "b"
3) "c"
4) "d"
5) "e"
6) "f"
# 4. 阻塞操作
127.0.0.1:6379> BRPOP queue 5 # 阻塞 5 秒
(nil) # 5 秒后超时返回 nil
(5.04s)4.2 验证 List 的典型队列场景
# 终端 1:生产者
redis-cli
LPUSH task:queue "job:1"
LPUSH task:queue "job:2"
# ...
# 终端 2:消费者
redis-cli
BRPOP task:queue 0 # 0 = 无限阻塞
# 立即返回 "job:1"4.3 List 类型的内存优化
# 默认配置
127.0.0.1:6379> CONFIG GET list-*
1) "list-max-ziplist-size"
2) "-2" # 每个 ziplist ≤ 8KB
3) "list-compress-depth"
4) "0" # 不压缩
# 如果你主要操作两端(消息队列):
# 设置压缩深度 = 1,中间节点压缩节省 70-80% 内存
redis-cli CONFIG SET list-compress-depth 1
# 验证效果(压入大量数据)
for i in $(seq 1 10000); do
redis-cli LPUSH biglist "value:$i"
done
# 查看内存变化
redis-cli INFO memory | grep used_memory_human五、面试视角
| 追问 | 答案要点 |
|------|---------|
| 为什么 List 不用普通双向链表? | 双向链表每个节点 2 个指针(16B) + malloc 碎片,Quicklist 用连续 ziplist 分段存储,内存效率高 |
| Quicklist 通过什么配置控制? | list-max-ziplist-size(每段大小)、list-compress-depth(压缩深度)|
| Quicklist 的 ziplist 为什么不会太大? | 默认每段 ≤8KB(list-max-ziplist-size=-2),中间插入只影响一段 |
| LTRIM 命令的时间复杂度? | O(N) 删除范围外的元素,会真正释放内存(不是惰性)|
| List 做队列 vs Stream 做队列的区别? | List:简单的生产消费,ACK 要靠自己;Stream:多消费者组、消息 ID、ACK 机制,功能完整(见 **Stream消息队列**)|
| List 的阻塞操作是用什么实现的? | Redis 有独立的"阻塞队列"机制:客户端挂载到 key 的等待列表,有数据推送时唤醒 |
📚 相关链接
**String 与 SDS** — 另一个基础数据结构
**Stream** — List 做队列 VS Stream 做队列的选择
**内存优化** — 不同类型的内存管理策略
← 返回 **核心数据结构索引**