一 前言
1.1 背景
在阅读本文前,需要了解下 ziplist(压缩列表),因为 listpack 的出现是用来代替 ziplist 的。
Redis 采用 ziplist ,是因为其为一种连续内存空间并且有序的压缩链表 。在数据节点不多的情况下,内存占用和查询复杂度得到一个相对较好的平衡。
但是 zip 有个一个致命的缺陷,就是极端情况下的连锁更新会带来不小的性能消耗。如下图所示:
1.2 方案
Redis 在后续的版本中也采用了 quicklist(快速链表),通过控制 quicklistNode 结构里的压缩列表的大小或者元素个数,来减少连锁更新带来的性能影响,但是并没有完全解决连锁更新的问题,因为 quicklistNode 还是用了压缩列表来保存元素。
所以在 Redis5.0 出现了 listpack,目的是替代压缩列表,其最大特点是 listpack 中每个节点不再包含前一个节点的长度,压缩列表每个节点正因为需要保存前一个节点的长度字段,就会有连锁更新的隐患。
二 源码解读
鉴入 Redis7.0 已经将 listpack 完整替代 ziplist(Redis7.0 新特性) ,所以本文的源码是 7.0版本。
前文提到 listpack 最大特点是 listpack 中每个节点不再包含前一个节点的长度,所以直接对比 listpack 和 ziplist 的结构设计。
2.1 ziplist entry
typedef struct zlentry {
unsigned int prevrawlensize; /* 用于编码前一个节点字节长度*/
unsigned int prevrawlen;
unsigned int lensize; /* 用于编码此节点类型/长度的字节。
例如,字符串有1、2或5个字节标题。
整数总是使用一个字节。
*/
unsigned int len; /* 用于表示节点实际的字节。
对于字符串,这只是字符串长度
而对于整数,它是1、2、3、4、8或
0,具体取决于数字范围。
*/
unsigned int headersize; /* prevrawlensize + lensize. */
unsigned char encoding; /* 设置为ZIP_STR_*或ZIP_INT_*,具体取决于节点编码。*/
unsigned char *p; /* 第一个节点的地址指针,prev-entry-len */
} zlentry;
可以明显看到 prevrawlensize 用于记录前一个节点的大小。
2.2 listpack entry
typedef struct {
/* 当使用string时,它具有长度(slen)。 */
unsigned char *sval;
uint32_t slen;
/* 当使用integer时,“sval”为 NULL,lval 保存该值。*/