Redis的内存效率一直是它的核心竞争力,而这背后离不开紧凑的列表类编码结构。在Redis 7.2之前,hash、zset等结构在小数据量时底层使用ziplist存储,但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