压缩列表(ziplist)是列表键和哈希键的底层实现之一,当一个列表只包含少量列表项,并且每个列表想要么是小数,要么是长度比较短的字符串,那么redis就会使用压缩列表。
1. 压缩列表的构成
压缩列表是redis为了节约内存而开发的,是由一系列特殊编码的连续内存块组成的顺序型数据结构。一个压缩列表可以包含任意多个节点,每个节点可以保存一个字节数组或者一个整数值;
- zlbytes:类型uint32_t,长度4字节,记录整个压缩列表占用的内存字节数:在对压缩列表进行内存重分配或者计算zlend的位置时使用;
- zltail:类型uint32_t,长度4字节,记录压缩列表表尾节点距离压缩列表的起始地址有多少字节,通过这个偏移量,程序无须遍历整个压缩列表就可以确定表尾节点的地址;
- zllen:类型uint16_t,长度2字节,记录了压缩列表包含的节点数量:当这个属性的值小于uint16_max(65535)时,这个属性的值就是压缩列表包含节点的数量;当这个值大于uint16_max时,节点的数量需要遍历整个压缩列表才能计算得出;
- entryX:列表节点,长度不定,压缩列表包含的各个节点,节点的长度有节点保存的内容决定;
- zlend:uint8_t,长度1字节,用于标记压缩列表的末端;
压缩列表示例:
压缩列表zlbytes属性的值为0x50,转化成十进制为80,表示压缩列表的总长为80字节;
zltail属性的值为0x3c(十进制60),表示有一个指向压缩列表起始地址的指针p,那么在指针p上加上偏移量60,就可以得出表尾节点entry3的地址;
zllen属性的值为0x3(十进制3),表示压缩列表包含三个节点;
2. 压缩列表节点的构成
每个压缩列表节点可以保存一个字节数组或者一个整数值,其中,字节数组可以时一下三种长度的其中一种:
- 长度小于等于63(26-1)字节的字节数组;
- 长度小于等于163383(214-1)字节的字节数组;
- 长度小于等于4294967295(232-1)字节的字节数组;
而整数值则可以时以下六种长度的其中一种:
- 4位长,介于0-12之间的无符号整数;
- 1字节长的有符号整数;
- 3字节长的有符号整数;
- int16_t类型整数;
- int32_t类型整数;
- int64_t类型整数;
每个压缩列表节点都由previous_entry_length,encoding,content三个部分组成;
2.1 previous_entry_length
节点的previous_entry_length属性以字节位单位,记录了压缩列表中前一个节点的长度,previous_entry_length属性的长度可以是1字节或者5字节:如果前一节点的长度小于254字节,那么previous_entry_length属性的长度为1字节,前一节点的长度就保存在这一字节里面;如果前一字节的长度大于等于254字节,那么previous_entry_length属性的长度为5字节,其中属性的第一字节会被设置为0xFE(十进制值254),而之后的四字节则用于保存前一节点的长度。
如下图所示,表示前一节点的长度为5字节:
如下图所示,属性的值为0xFE00002766,其中值的最高位字节0xFE表示这是五字节长的previous_entry_length属性,而之后的四字节0x00002766(十进制值10086)才是前一节点的实际长度。
previous_entry_length属性记录前一节点的长度,程序可以通过指针运算,根据当前节点的起始地址来计算出前一个节点的起始地址,压缩列表从表尾向表头遍历就是使用的这一原理实现的。
2.2 encoding
encoding属性记录了节点的content属性所保存数据的类型及长度:
一字节,两字节或者五字节长,值的最高位为00,01或者10的是字节数组编码:这种编码表示节点的content属性保存着字节数组,数组的长度由编码出去最高两位之后的其他位记录;
一字节长,值的最高位以11开头的是整数编码:这种编码表示节点的content属性保存着整数,整数值的类型和长度由编码出去最高两位之后的其他位记录;
字节数组编码:
整数编码:
2.3 content
负责保存节点的值。
示例1:encoding的值00001011,前两位00表示是字节数组类型,后六位表示字节数组的长度是11;
示例2:encoding的值11000000,前两位11表示是整数值,后六位则表示content是一个int16_t类型的整数值。
3. 连锁更新
有一种情况,在一个压缩列表中,有多个连续的,长度介于250~253字节之间的节点,当我们将一个长度大于等于254字节的新节点new设置为压缩列表的表头节点,那么new将成为e1的前置节点,因为e1的previous_entry_length属性仅长1字节,所以程序需要将e1节点的previous_entry_length从1字节扩展为5字节,由此导致后面的多个节点的previous_entry_length属性都需要从原来的1字节扩展为5字节,这种情况下产生的连续多次空间扩展操作称为“连锁更新”。
除了添加新节点会引发连锁更新之外,删除节点也可能引发连锁更新;
因为连锁更新在最坏的情况下需要对压缩列表进行N次空间重分配,最坏复杂度为O(N),所以连锁更新的最坏复杂度为O(N 2)。
虽然连锁更新的复杂度较高,但他真正造成性能问题的几率很低:首先压缩列表里正好有多个连续的,长度介于250至253字节之前的节点,才会触发连锁更新;其次,即使出现连锁更新,只要被更新的节点数量不多,也不会对性能造成影响,比如对三五个节点进行连锁更新是绝对不会影响性能的。