很多 Redis 入门材料把 List 解释成「链表」,再顺手说一句 LPUSH 和 RPOP 都是 O(1)。这个说法在接口层可以帮助理解,但放到底层就过时了:现代 Redis 的 List 不是一条普通链表,而是 quicklist 串起多个 listpack。

这个差异能解释几类线上现象:为什么两端进出很稳,LRANGE 0 -1 却可能拖住主线程?为什么一个任务队列堆积到百万级以后,删除中间元素会变得很贵?为什么可靠消息最好不要只靠 List 硬撑?

先把机制边界说清楚

这一篇只讨论 List 的物理结构和使用边界,不把它讲成消息队列全家桶。List 适合轻量队列、最新列表和两端访问模型;一旦你需要确认、重试、消费组和堆积观测,就要把目光转向 Stream 或专业 MQ。

整体路径

List 为什么是 quicklist

上面这张图先把主线铺开:链表负责两端伸缩,listpack 负责单节点内存紧凑。读 Redis 这类系统,最重要的是别只停在命令接口,要继续追问它在内存里是什么形状、在主线程上走多远、失败时会留下什么状态。

底层机制

  • quicklist 是双向链表,每个节点里不是单个元素,而是一段紧凑的 listpack。
  • 头尾 push/pop 只需要操作端点节点;当端点 listpack 达到填充限制时,再新增或拆分 quicklistNode。
  • 中间位置访问仍然要沿 quicklist 遍历,再进入 listpack 定位,不能因为名字里有 list 就期待随机访问便宜。
  • listpack 连续内存省掉大量指针,但单个 listpack 太大时会放大移动成本,所以 Redis 用 fill 和压缩深度做折中。

这些机制放在一起看,就能把「这个命令能不能用」改成「这个命令在当前数据规模下还便不便宜」。Redis 的很多坑,不是命令本身错了,而是数据规模和访问方式已经越过了它的舒适区。

取舍与边界

List 的设计是在「纯链表指针太多」和「纯连续数组移动太贵」之间找平衡。两端队列是它的舒适区,深分页、全量遍历、按值删除和中间插入都不是。

典型问题:用机制化例子排查

  • 队列长度要设监控,LLEN 长期上升说明消费者能力或失败重试有问题。
  • 避免在大 List 上执行 LRANGE 0 -1LREMLINSERT 这类会扫大量元素的命令。
  • 任务需要 ack、重试和消费者组时,用 Stream;需要强可靠时,用 Kafka、Pulsar 这类专门队列。
  • 排查延迟时同时看 SLOWLOG、命令分布和单个 key 的长度,不要只盯 CPU。

收束:一句判断

List 的核心判断很简单:两端访问是队列,中间访问就是成本。


关于十三Tech

我是十三,All in AI Agent 方向的架构师,专注 AI 工程实践。

我相信 AI 是程序员的最佳搭档,也希望帮助每一位开发者更好地驾驭 AI。

如果你想继续跟完这套「图解 Redis」,欢迎关注公众号 「十三Tech」。后续会继续按数据结构、底层机制、持久化、高可用和实战排查这条线更新。

十三Tech公众号二维码