压缩列表(ziplist)是列表键和哈希键的底层实现之一。当一个列表键只包含少量列表项,并且每个列表项要么就是小整数值,要么就是长度较短的字符串,那么Redis就会使用压缩列表来做列表键的底层实现。
1. 结构
Redis使用压缩列表的目的是为了节约内存。压缩列表是由一系列特殊编码的连续内存块组成的顺序型数据结构。一个压缩列表可以包含任意多个节点,每个节点可以保存一个字节数组或者一个整数值。
![digraph {
label = "\n 图 7-1 压缩列表的各个组成部分";
node [shape = record];
ziplist [label = " zlbytes | zltail | zllen | entry1 | entry2 | ... | entryN | zlend "];
}](p3-juejin.byteimg.com/tos-cn-i-k3…)
| 属性 | 类型 | 长度 | 用途 |
|---|---|---|---|
zlbytes | uint32_t | 4 字节 | 记录整个压缩列表占用的内存字节数:在对压缩列表进行内存重分配, 或者计算 zlend 的位置时使用。 |
zltail | uint32_t | 4 字节 | 记录压缩列表表尾节点距离压缩列表的起始地址有多少字节: 通过这个偏移量,程序无须遍历整个压缩列表就可以确定表尾节点的地址。 |
zllen | uint16_t | 2 字节 | 记录了压缩列表包含的节点数量: 当这个属性的值小于 UINT16_MAX (65535)时, 这个属性的值就是压缩列表包含节点的数量; 当这个值等于 UINT16_MAX 时, 节点的真实数量需要遍历整个压缩列表才能计算得出。 |
entryX | 列表节点 | 不定 | 压缩列表包含的各个节点,节点的长度由节点保存的内容决定。 |
zlend | uint8_t | 1 字节 | 特殊值 0xFF (十进制 255 ),用于标记压缩列表的末端。 |
2. 节点
每个压缩列表的节点,保存的是一个字节数组或者一个整数值。
每个节点由三部分组成:previous_entry_length 、encoding 、content 。
2.1. previous_entry_length
记录了前一个节点的长度,以字节为单位。并且它本身的长度可以为1个字节或5个字节
- 如果前一个节点的长度不超过254字节,那么privious_entry_length的长度就为1,前一节点的长度就存在这1个字节中。
- 如果前一个节点的长度超过254字节,那么privious_entry_length的长度就为5,这5个字节的第一个字节被设置为0xFE(十进制的254),然后后面四个字节则保存前一节点的长度。
previous_entry_length属性记录了前一个节点的长度,因此程序可以通过指针运算,根据当前节点的起始地址来计算出前一个节点的起始位置。压缩列表的从表尾向表头遍历操作也是使用这个原理。
2.2. encoding
encoding属性记录了节点的content属性所保存的数据的类型以及长度。
- 1字节、2字节、5字节长,值的最高位为00、01或者10的字节数组编码:表示content保存的是个数组,数组的长度是编码除去最高两位之后的其他位记录。
- 1字节长,并且值最高位以11开头的是整数编码:表示content保存着整数值,整数值的类型和长度由编码出去最高两位后其他位所记录。
举个栗子:
| 编码 | 编码长度 | content 属性保存的值 |
|---|---|---|
00bbbbbb | 1 字节 | 长度小于等于 63 字节的字节数组。 |
01bbbbbb xxxxxxxx | 2 字节 | 长度小于等于 16383 字节的字节数组。 |
10______ aaaaaaaa bbbbbbbb cccccccc dddddddd | 5 字节 | 长度小于等于 4294967295 的字节数组。 |
字节数组编码
| 编码 | 编码长度 | content 属性保存的值 |
|---|---|---|
11000000 | 1 字节 | int16_t 类型的整数。 |
11010000 | 1 字节 | int32_t 类型的整数。 |
11100000 | 1 字节 | int64_t 类型的整数。 |
11110000 | 1 字节 | 24 位有符号整数。 |
11111110 | 1 字节 | 8 位有符号整数。 |
1111xxxx | 1 字节 | 使用这一编码的节点没有相应的 content 属性, 因为编码本身的 xxxx 四个位已经保存了一个介于 0 和 12 之间的值, 所以它无须 content 属性。 |
整数编码
2.3. content
content属性负责保存节点的值,节点值可以是一个字节数组或者整数,值的类型和长度在encoding记录。
3. 连锁更新
之前有说过,如果前一个节点的长度小于254字节,那么previous_entry_length只需要1字节来记录长度,但是如果前一个节点的长度大于等于254字节了,previous_entry_length就需要5个字节来记录长度了。
此时有这么一个情况,有一个压缩列表,它包含了多个连续的,并且长度介于250-253之间的节点。
此时因为所有节点都小于254字节长度,所以都只用了1字节来保存前一节点的长度。
这个时候,我们将一个长度大于254的新节点,插入到它们前面,那么原先的压缩列表的第一个节点的previous_entry_length就要从1变成5了,这样一扩展导致整个的原第一节点的长度从250-253之间直接变成大于等于254了,所以原先的第二节点也需要扩展,同理,后续的所有节点都需要扩展了。
程序需要不断地对压缩列表执行空间重分配操作,Redis将这种特殊情况下产生的连续多次空间扩展操作称为“连锁更新”。除了添加新节点之外,删除节点也可能会引发连锁更新。
4. API
| 函数 | 作用 | 算法复杂度 |
|---|---|---|
ziplistNew | 创建一个新的压缩列表。 | O(1) |
ziplistPush | 创建一个包含给定值的新节点, 并将这个新节点添加到压缩列表的表头或者表尾。 | 平均 O(N) ,最坏 O(N^2) 。 |
ziplistInsert | 将包含给定值的新节点插入到给定节点之后。 | 平均 O(N) ,最坏 O(N^2) 。 |
ziplistIndex | 返回压缩列表给定索引上的节点。 | O(N) |
ziplistFind | 在压缩列表中查找并返回包含了给定值的节点。 | 因为节点的值可能是一个字节数组, 所以检查节点值和给定值是否相同的复杂度为 O(N) , 而查找整个列表的复杂度则为 O(N^2) 。 |
ziplistNext | 返回给定节点的下一个节点。 | O(1) |
ziplistPrev | 返回给定节点的前一个节点。 | O(1) |
ziplistGet | 获取给定节点所保存的值。 | O(1) |
ziplistDelete | 从压缩列表中删除给定的节点。 | 平均 O(N) ,最坏 O(N^2) 。 |
ziplistDeleteRange | 删除压缩列表在给定索引上的连续多个节点。 | 平均 O(N) ,最坏 O(N^2) 。 |
ziplistBlobLen | 返回压缩列表目前占用的内存字节数。 | O(1) |
ziplistLen | 返回压缩列表目前包含的节点数量。 | 节点数量小于 65535 时 O(1) , 大于 65535 时 O(N) 。 |