Hotdry.

Article

Redis 列表类型的内部实现演进:从链表到 quicklist 的技术权衡

从 Redis 作者视角解析数组类型的内部实现演进,深入探讨动态数组与 quicklist 的技术权衡与内存优化策略。

2026-05-04systems

Redis 作为内存数据库的典范,其列表类型的实现经历了从简单链表到紧凑编码再到混合架构的演进过程。这一演进并非随意为之,而是针对不同数据规模和访问模式进行的精细化权衡。理解这一演进历程,不仅有助于更好地使用 Redis,也为分布式系统中的数据结构设计提供了宝贵的参考。

链表时代:简洁但开销巨大

Redis 列表最初采用双向链表实现,这是计算机科学中最基础的数据结构之一。每个节点包含指向前一个节点、后一个节点以及实际值的三个指针,列表本身则维护头尾指针和长度计数。这种设计的优势在于头部和尾部操作的时间复杂度均为 O (1),插入和删除操作只需调整相邻节点的指针即可完成。理论上,这是一种优雅且高效的方案。

然而,当存储大量小元素时,链表的内存开销变得令人难以接受。每个列表节点需要约 40 字节的元数据(三个指针 24 字节,加上 Redis 对象头部的 16 字节),而实际数据可能仅有几个字节。存储 100 万个整数的链表,数据本身仅需 4 MB,但元数据开销超过 40 MB,内存膨胀超过十倍。此外,链表的节点分散在内存各处,严重损害 CPU 缓存局部性,每次遍历都可能触发缓存未命中。

压缩列表:紧凑但存在边界

为了解决链表在小元素场景下的内存浪费问题,Redis 引入了压缩列表(ziplist)这一替代编码。压缩列表将所有元素存储在一块连续的内存区域中,每个元素仅包含指向前一个元素长度的偏移量和实际内容。这种紧密排列的方式大幅减少了元数据开销 —— 每个元素的元数据仅需 1 到 10 字节,取决于元素的大小和类型。相同的 100 万个整数存储在压缩列表中,元数据开销降至约 1 MB,数据结构本身的内存效率提升了数十倍。

压缩列表的另一个优势在于其优秀的缓存局部性。由于所有元素连续存放,遍历操作可以充分利用 CPU 预取机制,显著提升顺序访问性能。Redis 还为压缩列表实现了高效的整数编码,0 到 12 的数字仅占用 4 位,一个字节的数值占用一个字节,两个字节的数值占用两个字节,以此类推。这种变长编码进一步压缩了存储空间。

然而,压缩列表并非完美无缺。由于所有元素共享一块连续内存,在列表中间插入或删除元素时,需要移动后续所有元素的位置。更严重的是,插入操作可能触发内存重分配,导致整个压缩列表被复制到新的内存位置。想象一个存储了数百万元素的压缩列表,每次在头部插入都意味着移动整个数据结构,这将是灾难性的性能灾难。因此,Redis 限制了压缩列表的适用场景,仅在列表元素数量较少时使用这种编码。

Quicklist:融合两种优势的混合架构

Redis 3.2 版本引入的 quicklist 是一种巧妙的混合架构,将链表和压缩列表的优点融为一体。quicklist 本质上是一个双向链表,但链表的每个节点并非存储单个元素,而是一个完整的压缩列表。换言之,数据结构呈现为「压缩列表节点 A <-> 压缩列表节点 B <-> 压缩列表节点 C」的形式,每个压缩列表节点内部仍然紧密排列多个元素。

这种设计带来了显著的优势。头部和尾部操作保持 O (1) 复杂度,因为列表本身仍然维护着头尾指针。内存效率大幅提升,因为每个压缩列表节点内部的元素都采用紧凑编码,而非为每个元素分配独立节点。遍历性能得到改善,因为可以在单个压缩列表内进行高效的顺序扫描,而跨节点的指针跳转相对稀疏。删除大范围元素时,可以直接丢弃整个压缩列表节点,无需逐个遍历。

quicklist 引入了一个关键的可配置参数 list-max-ziplist-entries,用于控制每个内部压缩列表最多存储的元素数量。当一个压缩列表达到这个阈值时,quicklist 会创建一个新的链表节点来存储后续元素。这个参数的默认值经过 Redis 团队的多次调优,在内存效率和操作性能之间取得了良好的平衡。实践表明,将每个压缩列表的元素数量控制在 32 到 128 之间通常能获得最佳效果。

工程实践中的配置调优

在生产环境中,合理配置 quicklist 参数需要对工作负载有清晰的认识。对于存储大量小元素的场景(如时间序列、用户 ID 列表),可以适当增大压缩列表的长度上限,以获得更好的内存效率。对于需要频繁在列表中间进行插入和删除操作的场景,则应选择较小的压缩列表长度,以减少单次操作的内存复制开销。Redis 官方文档建议将 list-max-ziplist-entries 设置为 512 以下,list-max-ziplist-value 设置为 64 字节以下,以确保压缩列表的优势得以发挥。

内存分配器的选择也会显著影响存储效率。Matt Staub 在博客中的基准测试显示,使用 jemalloc 分配器时,quicklist 相比传统链表可节省约 75% 的内存,而使用 libc 的 ptmalloc 分配器时,内存碎片率可能高达 33% 到 39%。这解释了为什么 Redis 官方推荐在生产环境中使用 jemalloc。

从更宏观的视角看,Redis 列表类型的实现演进体现了软件工程中常见的权衡哲学。没有任何数据结构是万能的,优秀的系统会根据不同的使用场景自动切换到最合适的实现。这种「自适应编码」的思想贯穿于 Redis 的许多数据类型设计,也为其他系统提供了值得借鉴的范例。


资料来源:本文技术细节主要参考 Matt Staub 在 matt.sh 发表的「Redis Quicklist - From a More Civilized Age」一文,该文详细记录了 quicklist 的设计思路与基准测试结果。

systems