redis设计与实现——压缩列表
ziplist压缩列表是有序集合键(另一个是跳表)和哈希键(另一个是字典)的底层实现之一(在3.2之后不再是list的底层实现,被quicklist取代了)。如果一个列表键只包含少量的项,并且每个列表项要么是小整数值,要么是短的字符串,那么Redis就会用ziplist表示列表键。
另外,当一个哈希键只包含少量键值对,并且每个key和value要么是小整数值,要么是短的字符串,那么Redis就会用ziplist表示哈希键。
压缩列表的构成
压缩列表并没有使用一个数据结构去表示。为了节省内存,ziplist是通过特殊编码的连续内存块组成的顺序型结构。我们可以通过其创建步骤来看其结构内容,一个压缩列表包含了多个节点,
1 | /* ziplist头: 2个32位的整数存总共字节数,1个16位的整数存item */ |
因此压缩列表的结构可以表示为(小端序):
1 | /* |
从这个例子中可以看到:
- zlbytes为0x0f,表示压缩列表总长度为15;
- zltail为0x0c,表示如果我们有一个指向压缩列表起始地址的指针p,那么只要指针p加上偏移12,则可以得到entry2的地址;
- zllen为0x02,表示有2个列表节点;
- zlend是特殊值0xFF,即255,用来标记压缩列表末端;
压缩列表节点的组成
Redis使用了一个结构来表示压缩列表的节点,这个结构体并不是真正的编码方式,只是用来做内部函数操作(主要是使用zipEntry函数根据p指针返回一个zlentry),另外还使用了一个函数来创建节点,压缩列表的节点可以保存一个字节数组或者是一个整数值。
1 | typedef struct zlentry { |
1 | // 根据节点指针p返回一个zlentry |
为了节省内存,redis的压缩列表使用了节点的encoding记录了节点所保存的数据类型和长度。以下是不同的编码方式:
1 | /* Different encoding/length possibilities */ |
可以看到,压缩列表的节点保存了整数和字符数组两种类型,并针对不同的长度做了不同的编码解释。
1 | // 检查字符串是否可以被编码成整数 |
因此其编码模式总结就是:
- 字节数组编码
编码 | 编码长度 | 值 |
---|---|---|
00bbbbbb | 1字节 | 小于64字节的字节数组 |
01bbbbbb xxxxxxxx | 2字节 | 小于16383字节的字节数组 |
10______ xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx |
5字节 | 小于等于2^32-1的字节数组,有6个bit留空 |
- 整数编码
编码 | 编码长度 | 值 |
---|---|---|
1111xxxx | 1字节 | 在[0,12]区间的整数 |
11111110 | 1字节 | 8bit有符号整数 |
11110000 | 1字节 | 24bit有符号整数 |
11100000 | 1字节 | int64_t |
11010000 | 1字节 | int32_t |
11000000 | 1字节 | int16_t |
content
虽然redis使用了zlentry作为内部节点的数据结构,但其真实编码表示并不是按照该结构体来计算的。redis对字节数组或者整数的编码方式可以参考节点插入的过程来解释:
1 |
|
节点的插入是通过上面这个函数实现的,分为从头部和尾部进行插入。至于具体的entry表示方式则要看插入节点的具体实现。
1 | /* Insert item at "p". */ |
因此一个节点的完整编码结构包含了prevlen,encoding和content三个部分,下图就是一个保存着整数值10086的编码结构。
级联更新
考虑这样的一种情况,如果目前所有节点的长度都在250-253字节之间,那么意味着记录这些节点只需要1字节长的prevlen。但此时如果将一个长度大于或等于254字节的新节点设置为ziplist的头部节点,那么将直接影响后续所有节点的prevlen编码长度。
Redis将会因此触发级联更新,即遍历所有需要更新的节点进行处理,直到不需要更新为止。
1 | unsigned char *__ziplistCascadeUpdate(unsigned char *zl, unsigned char *p) { |
总结
- 压缩列表是列表键和哈希键的底层实现之一;
- 列表中包含多个节点,每个节点都可以包含一个字节数组或者整数;
- 添加或者删除节点都可以能引发连锁的更新操作;