导读:本期聚焦于半夏创作的《Redis 7.2为什么用listpack全面替代ziplist?底层结构变化详解》,敬请观看详情。哈希对象、有序集合等小数据在Redis底层一直依赖ziplist存储,但连锁更新问题始终是性能隐患。Redis 7.2完成了一次重要的底层重构,listpack全面接管了ziplist的工作。本文从ziplist的entry结构讲起,分析previous_entry_length字段引发的级联扩容原理,再对比listpack的backlen设计与 Totlen编码方式,说明它如何从根源上消除连锁更新。同时梳理Quicklist、Hash、ZSet等结构的适配改造过程,并给出升级后的性能表现与使用建议,帮助你理解这次升级背后的工程取舍。

Redis的内存效率一直是它的核心竞争力,而这背后离不开紧凑的列表类编码结构。在Redis 7.2之前,hash、zset等结构在小数据量时底层使用ziplist存储,但ziplist存在一个著名的历史包袱——连锁更新。Redis 7.2完成了一次底层编码的全面替换,listpack正式取代ziplist成为所有小数据量结构的默认编码,本文就来详细分析这次升级的来龙去脉。

Redis 7.2为什么用listpack全面替代ziplist?底层结构变化详解

ziplist的核心缺陷:连锁更新问题

要理解为什么必须替换ziplist,首先要看它的entry结构。ziplist中每个节点由四部分组成:previous_entry_length、encoding、entry-data以及结尾标记。其中previous_entry_length记录前一个节点的总长度,如果前一节点小于254字节,该字段占1字节,否则占5字节。

问题就出在这个字段上。假设一段ziplist中连续存储了多个长度在250到253字节之间的节点,每个节点的previous_entry_length都只需要1字节。此时如果在头部插入一个大于254字节的新节点,第一个原有节点的previous_entry_length就需要从1字节扩展到5字节,导致该节点自身总长超过254字节,进而迫使下一个节点也扩容,如此层层传导,最坏情况下整个ziplist的所有节点都要重新分配内存并修改内容,这就是连锁更新(cascade update)。

虽然实际业务中连续出现大量250到253字节的节点概率不高,但这个问题在理论上导致ziplist的操作复杂度退化为O(N平方)级别,且每次更新都可能触发多次内存realloc,对性能敏感的场景是无法接受的隐患。此外,ziplist的插入删除还涉及大量memmove操作,节点反向遍历时需要不断根据previous_entry_length回溯,实现复杂且容易出错,历史上ziplist相关代码曾多次出现边界bug。

listpack的设计:如何从根源上消除连锁更新

listpack由Redis作者antirez为Streams功能引入,它的设计思路非常直接:每个节点只记录自己的信息,不记录前一个节点的信息。一个listpack entry由三部分组成:encoding编码、entry-data数据以及backlen反向长度。

backlen记录的是当前entry除backlen本身之外的总字节数,也就是encoding加entry-data的长度。它采用一种特殊的变长编码,每个字节的最高位用作标志位,为1表示还有后续字节,为0表示结束。这样设计有两个好处:一是backlen可以从右向左反向读取,支持从尾部向前遍历;二是每个节点占用的空间只取决于自身内容,与前一个节点完全无关。

#include <stdio.h>

/* listpack节点结构示意(伪代码描述):
 * [encoding][entry-data][backlen]
 * backlen记录encoding+entry-data的长度
 * 每个节点独立,不引用前驱节点
 */

unsigned char *lpPrev(unsigned char *lp, unsigned char *p) {
    // 从当前节点指针p向左,逐字节读取backlen
    // backlen高位为1表示未结束,为0表示该字节是最后一个
    // 读出backlen后,p减去backlen再减去backlen自身长度
    // 即可得到前一个节点的起始位置
    return NULL; // 示意
}

插入和删除节点时,listpack只会影响当前位置之后的内存移动,不会引起任何其他节点的结构性修改,彻底消除了连锁更新。同时,listpack的总头部只记录总字节数和元素数量两个字段,比ziplist少了一个尾偏移量字段,结构更加简洁。遍历也不需要依赖额外的索引信息,整体实现代码量比ziplist少了很多,可维护性显著提升。

Redis 7.2中的全面切换与各结构的适配改造

切换并非一蹴而就。Redis 5.0引入listpack用于Streams,Redis 7.0将zset的底层编码从ziplist切换为listpack,hash的切换也在7.0完成。而Redis 7.2则完成了最后的关键一步:Quicklist的底层节点从ziplist替换为listpack,从此ziplist相关代码被彻底移除出核心路径。

Quicklist是双向链表与紧凑节点的复合结构,每个链表节点内部原来是一个ziplist。替换为listpack后,quicknode内部的操作全部改为基于listpack的API,插入、删除、查找逻辑更简单,也顺带修复了quicklist在极端情况下继承自ziplist的连锁更新问题。对应的配置项也发生了变化,原来的list-max-ziplist-entries等参数被list-max-listpack-entries、hash-max-listpack-entries、zset-max-listpack-entries等新参数取代。

对于使用者来说,绝大多数场景无需修改业务代码,因为编码切换是透明的。需要注意的是,如果应用程序或运维脚本依赖OBJECT ENCODING命令检查对象编码,输出结果会从ziplist变为listpack,相关监控和告警逻辑需要同步更新。此外,由于listpack不再有连锁更新,以前为了规避该问题而刻意调小的编码阈值,在7.2中可以适当放宽,以换取更高的内存压缩率。

升级建议与性能表现

从社区公布的benchmark数据看,listpack在日常读写场景下与ziplist性能基本持平,而在频繁的小对象插入删除场景下,由于避免了潜在的级联内存操作,尾部延迟更加稳定。内存占用方面,少了一个尾偏移字段,每个listpack可以节省4到8字节,加上编码实现更紧凑,海量小key场景下的总内存有轻微下降。

如果你的Redis版本还在7.0以前,升级到7.2需要注意几点:第一,升级过程不需要数据迁移,RDB和AOF文件格式中编码的兼容性已在内部处理;第二,检查配置文件中是否残留旧的ziplist相关参数,7.2会忽略这些参数并打印警告;第三,对于使用RESP协议直接解析内部结构的第三方工具,需要确认其已支持listpack编码。

总体来看,listpack对ziplist的替代是一次典型的工程演进:用更简单的结构解决历史遗留问题,同时降低了代码维护成本。理解这次升级背后的设计思想,对我们在自己项目中设计紧凑数据结构也有很好的借鉴意义——节点自包含、避免跨节点引用,往往比复杂的联动机制更可靠。

Redis listpackziplistRedis 7.2修改时间:2026-09-01 13:42:32

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。