Skip to content

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 配置参数

redis

# 配置参数

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 特性

bash

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 的典型队列场景

bash

# 终端 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 类型的内存优化

bash

# 默认配置

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 做队列的选择

  • **内存优化** — 不同类型的内存管理策略

  • ← 返回 **核心数据结构索引**

Knowledge4J — Java 知识库