Hotdry.

Article

Redis 数组类型的演进之路:从 antirez 的视角看 quicklist 的诞生

从 Redis 作者 antirez 的视角回顾数组类型的长期开发历程,解析从早期设计到 quicklist 演进背后的工程决策与权衡,为技术实现提供上下文补充。

2026-05-04systems

Redis 作为最受欢迎的内存数据库之一,其内部数据结构的演进历程本身就是一部精彩的技术实践史。数组类型(List)作为 Redis 最核心的数据结构之一,从最初简单的链表实现到后来革命性的 quicklist 结构,经历了数年的迭代与优化。本文从 Redis 作者 antirez(Salvatore Sanfilippo)的视角出发,回顾这段不为人知的开发历程,解析每一次技术决策背后的工程考量。

一、起步阶段:简单链表与内存效率的初次碰撞

Redis 的数组类型在最初版本中采用了最为直接的实现方式:双向链表(doubly linked list)。这种设计的好处是概念清晰、实现简单,并且能够提供常数时间(O (1))的头尾操作性能。每当用户执行 LPUSH 或 RPUSH 命令时,Redis 只需创建新的节点并调整指针即可完成插入操作。然而,这种看似优雅的设计很快就遇到了现实瓶颈。

在真实生产环境中,许多用户的列表会包含数以百万计的元素。当列表长度达到一定规模后,链表结构的内存开销变得难以忽视。每个节点除了存储实际数据外,还需要额外的指针空间来维护前后向链接。在 64 位系统上,一个简单的指针就占用 8 字节,对于存储短字符串的场景,数据本身可能只有几个字节,而指针开销却达到了数据大小的数倍之多。更糟糕的是,链表的节点在内存中是非连续分布的,这严重影响了 CPU 缓存的利用率,导致遍历操作的性能随着列表增长而线性下降。

antirez 在多次公开演讲中提到,Redis 的设计哲学始终以 “简单实用” 为核心。面对链表带来的内存效率问题,他选择了一条渐进式的优化道路,而非激进的重新设计。这种务实态度贯穿了 Redis 整个发展历程,也是其能够在保持代码可维护性的同时不断进化的关键因素。

二、压缩列表登场:小规模数据的内存优化方案

为了解决小规模列表的内存效率问题,Redis 引入了压缩列表(ziplist)这一内部数据结构。ziplist 是一种特殊的双向链表,但它将所有元素紧凑地存储在一段连续的内存区域中,完全消除了节点指针的开销。每个元素由一个变长字段表示,包含前一个元素的长度信息,从而支持从列表尾部向头部的高效遍历。

这种设计在列表规模较小时效果显著。以存储 100 个短字符串为例,相比传统链表,ziplist 可以将内存占用降低 50% 甚至更多。连续的内存布局也带来了更好的缓存局部性,使得 LRANGE 等范围查询操作的性能得到明显提升。Redis 在 List、Hash、Sorted Set 等多种数据结构中都广泛采用了 ziplist 作为小规模数据的默认编码方式。

然而,ziplist 并非万能解决方案。当列表规模持续增长时,其固有的缺陷就开始显现。每次在列表中间位置插入或删除元素时,都需要重新分配内存并移动后续所有元素。假设一个包含 10 万元素的 ziplist,如果要在中间位置插入一个元素,可能需要移动数万个字节的数据,这在高并发写入场景下是难以接受的。此外,过大的 ziplist 还会导致显著的内存碎片化问题。

三、quicklist 的诞生:两全其美的混合架构

面对链表和 ziplist 各自的局限性,Redis 社区开始探索第三条道路。2015 年左右,来自日本的开发者 Matt Stancliff 提出了一种创新的混合数据结构概念,随后在 antirez 的指导下完成了实现,这就是后来广为人知的 quicklist。该数据结构被正式引入 Redis 3.2.0 版本,并在发布公告中被 antirez 特别提及,称其 “大幅降低了大型列表的内存占用”。

quicklist 的核心思想是将一个大列表分解为多个小的 ziplist 块,然后通过双向链表将这些块串联起来。形象地说,它就像是先把一串珍珠(元素)分成若干小串(ziplist 块),再用绳子(链表)把小串连接起来。这种设计既保留了链表在头部和尾部操作上的 O (1) 性能优势,又通过 ziplist 块实现了紧凑的内存布局。每个 ziplist 块默认存储 8 KB 左右的数据,这个大小经过精心调优,能够在内存效率和缓存利用率之间取得良好平衡。

从工程实现的角度看,quicklist 的设计蕴含着深刻的权衡智慧。首先,通过限制单个 ziplist 块的大小,有效避免了过大压缩列表带来的插入性能问题。当需要在列表中间位置插入新元素时,Redis 只需找到对应的块并在该块的 ziplist 中进行局部操作,影响范围被限制在极小的内存区域内。其次,链表结构使得 Redis 可以在 O (1) 时间内定位到任意一个 ziplist 块,这对于 LRANGE 等范围查询操作至关重要 ——Redis 可以快速跳转到起始元素所在的块,然后在块内进行顺序遍历。

四、工程决策的背后:参数调优与社区协作

quicklist 的成功并非一蹴而就,而是经过了大量实验和社区协作才最终成熟。在开发过程中,antirez 和 Matt Stancliff 面临着一个核心问题:如何确定最佳的 ziplist 块大小?块太大会回到原来的内存效率问题,块太小则会增加链表指针的整体开销。经过反复测试和权衡,最终默认的块大小被设定为 8 KB,这个数值在大多数使用场景下都能提供良好的综合性能。

值得注意的是,Redis 并不强制使用固定的块大小。开发者可以通过配置参数 list-max-ziplist-size 来调整这一行为。对于以读为主的工作负载,可以适当增大块大小以获得更好的遍历性能;对于写密集型场景,则可以选择较小的块以保持稳定的插入延迟。这种灵活性体现了 Redis 一贯的设计理念:在提供合理默认值的同时,赋予高级用户足够的自定义能力。

除了块大小,quicklist 还引入了其他可配置参数,如 list-compress-depth 用于控制链表两端未压缩的块数量。这些参数的存在,使得 Redis 能够在不同硬件环境和业务场景下进行针对性优化。antirez 在回顾这段历史时曾表示,quicklist 的设计过程让他深刻认识到 “没有任何数据结构是万能的”,关键在于根据实际工作负载找到最优的平衡点。

结语

从最初的简单链表到革命性的 quicklist,Redis 数组类型的演进历程充分展示了渐进式工程设计的魅力。antirez 并没有追求一步到位的完美方案,而是在保持系统简洁性的前提下,通过持续迭代逐步解决实际问题。quicklist 的成功,既源于 Matt Stancliff 的创新贡献,也离不开整个社区的实践反馈。这种开放协作的模式,正是 Redis 能够历经十多年依然保持活力的根本原因。

对于今天的技术从业者而言,Redis 数组类型的发展史提供了宝贵的参考:技术选型不应是简单的优劣判断,而需要对具体场景进行深入分析,在相互冲突的目标之间找到最适合当前阶段的解决方案。

资料来源:Redis 3.2.0 发布公告(antirez.com/news/104)、Redis 官方 GitHub 仓库(github.com/redis/redis)

systems