净植 發表於 2026-1-7 09:00:52

Redis中ziplist与quicklist解析与对比小结

<div id="navCategory"><h5 class="catalogue">目录</h5><ul class="first_class_ul"><li><a href="#_label0">一、背景 &mdash; 为什么要有多种&ldquo;列表(List)&rdquo;编码</a></li><li><a href="#_label1">二、ziplist &mdash;&mdash; 压缩列表(compact list)</a></li><ul class="second_class_ul"><li><a href="#_lab2_1_0">2.1 ziplist 是什么</a></li><li><a href="#_lab2_1_1">2.2 ziplist 的适用条件(默认策略)</a></li><li><a href="#_lab2_1_2">2.3 ziplist 的优点与缺点</a></li></ul><li><a href="#_label2">三、quicklist &mdash;&mdash; 快速列表(QuickList)</a></li><ul class="second_class_ul"><li><a href="#_lab2_2_3">3.1 quicklist 是什么</a></li><li><a href="#_lab2_2_4">3.2 quicklist 的结构与配置参数</a></li><li><a href="#_lab2_2_5">3.3 quicklist 的优点</a></li><li><a href="#_lab2_2_6">3.4 quicklist 的限制与注意事项</a></li></ul><li><a href="#_label3">四、ziplist vs quicklist &mdash;&mdash; 对比总结</a></li><ul class="second_class_ul"></ul></ul></div><p class="maodian"><a name="_label0"></a></p><h2>一、背景 &mdash; 为什么要有多种&ldquo;列表(List)&rdquo;编码</h2>
<ul><li>在内存数据库 Redis 中,列表(List)是常用数据类型之一,用于实现队列、堆栈、消息队列、任务队列等场景。</li><li>不同场景对内存使用与访问效率有不同要求:少量元素 &rarr; 希望节省内存;大量元素或频繁插入/删除 &rarr; 需要高性能操作。</li><li>因此Redis 设计者提供了不同的&ldquo;编码方式&rdquo;(encoding)来存储 List 对象,以兼顾 <strong>空间效率</strong> 与 <strong>操作效率</strong>。</li></ul>
<p>在历史发展中,Redis 对 List 的编码方式经历了如下阶段:</p>
<ul><li>早期:使用 <strong>双向链表(linkedlist)</strong> 或 <strong>压缩列表(ziplist)</strong>,两种可选。</li><li>从 Redis <strong>3.2</strong> 版本起,引入 <strong>quicklist</strong>,作为新的默认结构,取代了 ziplist + linkedlist 的混合策略。</li></ul>
<p>下面分别介绍 ziplist 和 quicklist 的结构与特性,然后比较优缺点。</p>
<p class="maodian"><a name="_label1"></a></p><h2>二、ziplist &mdash;&mdash; 压缩列表(compact list)</h2>
<p class="maodian"><a name="_lab2_1_0"></a></p><h3>2.1 ziplist 是什么</h3>
<ul><li>ziplist 是 Redis 提供的一种&ldquo;紧凑型列表结构(compact list)&rdquo;。它将多个 list 元素连续地存放在一块 <strong>连续内存(contiguous memory block)</strong> 中。</li><li>每个 entry(元素)包含必要的 metadata(如前一个 entry 长度、当前 entry 长度、数据内容等),因此 ziplist 可以存放不同长度的数据项(字符串、整数等),而不需要为每个元素单独分配内存。</li><li>因为内存连续、元素紧凑,ziplist 在内存占用上非常高效 &mdash; 对于存储少量、短字符串/整数列表时,能节省大量内存。</li></ul>
<p>ziplist结构如图所示,参考Redis面试题:</p>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202601/2026010709002740.png" /></p>
<p>下边的图也可加深理解</p>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202601/2026010709002745.png" /></p>
<p>ziplist的各字段含义如下。</p>
<ul><li>zlbytes:压缩列表的字节长度,占4B,因此压缩列表最长 2&sup3;&sup2;‑1B。</li><li>zltail:压缩列表尾元素相对于压缩列表起始地址的偏移量,占4B。</li><li>zllen:压缩列表的元素数目,占2B;当压缩列表的元素数目超过65535(2&sup1;⁶‑1)时,zllen 无法存储,此时必须遍历整个压缩列表才能获取元素数目。</li><li>entry1、entr2y:压缩列表存储的若干个元素,可以是字节数组或者整数,长度不限;</li><li>zlend:压缩列表的结尾,占1B,固定为0xFF。</li></ul>
<p>对于每一个entry元素来讲</p>
<p>entry 结构包含三个字段:</p>
<ol><li>prelen<ul><li>记录前一个元素的长度,用于从后向前遍历。</li><li>前一个元素长度 &lt; 254B &rarr; 占 1B。</li><li>前一个元素长度 &ge; 254B &rarr; 占 5B(首字节为 0xFE,后 4B 为实际长度)。</li></ul></li><li>encoding<ul><li>表示 data 的数据类型(整数或字节数组)。</li><li>长度可变(1B、2B 或 5B),通过开头的比特区分。</li><li>同时可包含字节数组的长度信息(如 6bit、14bit 或 32bit 表示长度)。</li></ul></li></ol>
<p>以下是encoding字段的元素编码</p>
<table><thead><tr><th>编码 (encoding) 标识 / 类型</th><th>encoding长度</th><th>说明 / 含义 (简要)</th></tr></thead><tbody><tr><td>00pppppp</td><td>1B</td><td>表示一个 string,长度 &le; 63 字节。</td></tr><tr><td>01pppppp + qqqqqqqq</td><td>2B</td><td>表示一个 string,长度 &le; 2&sup1;⁴‑1字节。</td></tr><tr><td>10000000 + 4 bytes length</td><td>5B</td><td>表示一个较长 string(长度 &le; 2&sup3;&sup2;‑1字节)。</td></tr><tr><td>11000000</td><td>1B</td><td>表示一个 int16_t(16-bit 整数)元素。</td></tr><tr><td>11010000</td><td>1B</td><td>表示一个 int32_t(32-bit 整数)元素。</td></tr><tr><td>11100000</td><td>1B</td><td>表示一个 24-bit 整数编码(special 24-bit 整数)。</td></tr><tr><td>11111110</td><td>1B</td><td>表示一个 int8_t(8-bit 整数)元素。</td></tr><tr><td>1111xxxx</td><td>1B</td><td>xxxx表示范围为 0&ndash;12 的整数 。</td></tr></tbody></table>
<ol start="3"><li>data<ul><li>实际存储的数据,可以是整数或字节数组,长度由 encoding 决定。</li></ul></li></ol>
<p class="maodian"><a name="_lab2_1_1"></a></p><h3>2.2 ziplist 的适用条件(默认策略)</h3>
<p>在 Redis 配置文件(或编译时默认)中,对于 LIST、HASH、ZSET 等类型,会设置参数来决定是否使用 ziplist,例如:</p>
<ul><li><code>list-max-ziplist-entries</code>:限制 ziplist 中元素(entry)个数</li><li><code>list-max-ziplist-value</code>:限制每个 entry 的最大字节数(value 大小)</li></ul>
<p>当元素数量或元素大小超过这些限制时,Redis 会放弃 ziplist 编码,转为其他更适合的编码方式(在老版本可能是 linkedlist,现在是 quicklist)。</p>
<p class="maodian"><a name="_lab2_1_2"></a></p><h3>2.3 ziplist 的优点与缺点</h3>
<p><strong>优点</strong>:</p>
<ul><li>内存利用率高 &mdash; 非常节省内存空间,适合存储少量、小数据。</li><li>内存连续,有利于 CPU 缓存和遍历效率。</li></ul>
<p><strong>缺点 / 缺陷</strong>:</p>
<ul><li>对修改操作(插入 / 删除 / 更新)不友好 &mdash; 因为要保持连续内存,一旦中间插入或删除,就可能触发内存重新分配和数据搬移(realloc + memcpy),代价高昂。这里会涉及<strong>连锁更新</strong>问题。</li><li>当元素较多或较大时,ziplist 会非常臃肿或重复 realloc,性能和稳定性都不理想。</li><li>因此,ziplist 更适合&ldquo;短小、稳定、不频繁变更&rdquo;的列表数据,不适合大数据量或高频写入场景。</li></ul>
<p class="maodian"><a name="_label2"></a></p><h2>三、quicklist &mdash;&mdash; 快速列表(QuickList)</h2>
<p class="maodian"><a name="_lab2_2_3"></a></p><h3>3.1 quicklist 是什么</h3>
<ul><li>quicklist 是从 Redis 3.2 开始引入的 List 编码方式,用于替代早期的 ziplist /adlist 编码。</li><li>它本质上是 <strong>双向链表 + 压缩子列表</strong> 的混合结构:一个 quicklist 是一个双向链表,而链表的每个节点(<code>quicklistNode</code>)内部持有一个 ziplist来存储一段连续的元素。</li><li>这样的设计兼顾了链表和 ziplist 各自的优点。</li></ul>
<p class="maodian"><a name="_lab2_2_4"></a></p><h3>3.2 quicklist 的结构与配置参数</h3>
<p>quicklist 的主要结构由 <code>quicklist</code> 和 <code>quicklistNode</code> 两个 C 结构体定义,在 Redis 源码中(例如 quicklist.h / quicklist.c)可以查看。</p>
<p>结构图</p>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202601/2026010709002774.png" /></p>
<p>在quicklist中,关键字段包括:</p>
<ul><li><code>head</code> / <code>tail</code>:指向链表头尾节点。</li><li><code>count</code>:List 中所有元素entry的总数。</li><li><code>len</code>:quicklistNode 节点数量。</li><li><code>fill</code>:用于控制每个 ziplist 容量上限entry 数量 或 字节大小,通过 <code>list-max-ziplist-size</code> 配置项设定。</li><li><code>compress</code>:用于控制压缩深度,即链表两端多少个节点不压缩,中间节点可被压缩以节省内存,通过 <code>list-compress-depth</code> 配置项控制。</li><li>每个 quicklistNode 包含指向内部 ziplist(或 listpack)的指针 <code>zl</code>,大小 <code>sz</code>,元素计数 <code>count</code> 等信息。</li></ul>
<blockquote><p>⚠️ 注意:在 Redis 最新版本中(例如 7.x),listpack 已经逐步取代 ziplist 作为内部压缩子列表结构,但 quicklist 的设计思想不变 &mdash;&mdash; 分段 + 链表 + 压缩子列表。</p></blockquote>
<p class="maodian"><a name="_lab2_2_5"></a></p><h3>3.3 quicklist 的优点</h3>
<ul><li><strong>兼顾空间与效率</strong>:通过将列表拆成多个小段(ziplist),既避免了大型链表带来的内存碎片和高 overhead,也避免了大型 ziplist 带来的频繁 realloc 问题。</li><li><strong>快速头尾操作(push/pop)</strong>:因为 quicklist 是链表结构,在头尾插入删除是 O(1) 操作,非常高效,适合队列/栈等操作频繁场景。</li><li><strong>压缩可选</strong>:中间节点可以压缩(LZF 等算法/listpack 压缩),节省内存;而头尾保留原始以保证访问性能。</li><li><strong>灵活适应不同场景</strong>:既适合存储小量短元素,也适合存储大量元素或长列表。</li></ul>
<p class="maodian"><a name="_lab2_2_6"></a></p><h3>3.4 quicklist 的限制与注意事项</h3>
<ul><li>如果配置不当(ziplist 内部过大、压缩设置不合理),可能出现性能开销过重,或内存碎片/压缩开销问题。</li><li>quicklist 内部实现比简单链表或数组复杂,对于某些极端访问模式(大量随机访问中间、频繁跨节点访问)可能不如纯数组或其他结构高效。</li></ul>
<p>quicklist 的两种极端情况如下:</p>
<p>1. 当 ziplist 节点过多时,quicklist 退化为双向链表。最极端的情况是每个 ziplist 节点只包含一个 entry,即一个元素对应一个节点。<br />2. 当 ziplist 节点过少时,quicklist 退化为 ziplist。最极端的情况是整个 quicklist 中只含有一个 ziplist 节点。</p>
<ul><li>对于非常频繁的随机访问或修改,可能需要谨慎评估是否适合使用列表结构。</li></ul>
<p class="maodian"><a name="_label3"></a></p><h2>四、ziplist vs quicklist &mdash;&mdash; 对比总结</h2>
<table><thead><tr><th>特性 / 维度</th><th>ziplist</th><th>quicklist</th></tr></thead><tbody><tr><td>内存布局</td><td>连续内存 block(compact)</td><td>链表 + 多个 ziplist/listpack segments</td></tr><tr><td>内存开销</td><td>低,连续、紧凑</td><td>较高(链表+子列表)但比纯链表低</td></tr><tr><td>适合场景</td><td>元素少、数据小、读不频繁修改</td><td>元素多、队列 / 栈 / 批量 push/pop / 变长频繁</td></tr><tr><td>插入/删除性能</td><td>中间插入删除开销大(可能 memcpy)</td><td>头尾 O(1),节点级调整,较稳定</td></tr><tr><td>内存碎片</td><td>几乎无</td><td>较少(链表 + 子列表)</td></tr><tr><td>压缩 / 节省空间</td><td>默认紧凑</td><td>可配置压缩,兼顾效率</td></tr><tr><td>Redis 中默认支持</td><td>早期版本</td><td>Redis 3.2+ 默认</td></tr></tbody></table>
<p>Redis 之所以在内存数据库 / 缓存系统中表现出色,很大程度上是因为它为不同场景提供了优化良好的底层数据结构。ziplist 与 quicklist 是 Redis List 类型在历史与现在对空间与时间折中的经典设计。</p>
<ul><li>ziplist:为&ldquo;少量、小数据、节省内存&rdquo;而生。</li><li>quicklist:为&ldquo;高性能、灵活、适应大/变动列表&rdquo;而设计。</li></ul>
頁: [1]
查看完整版本: Redis中ziplist与quicklist解析与对比小结