压缩列表,即ziplist,是列表(list),哈希(hash)和有序集合(zset)的底层实现之一,Redis 为了节约内存空间使用,在这些容器对象在元素个数较少的时候,采用压缩列表 (ziplist) 进行存储。
list示例:
zset示例:
hash示例:
压缩列表的构成
压缩列表是由一系列经过特殊编码的连续的内存空间,元素之间紧挨着存储,没有任何冗余空隙。一个压缩列表可以包含任意的多个节点,每个节点可以保存一个字节数组或者整数值。
struct ziplist<T> {
int32 zlbytes;
int32 zltail_offset;
int16 zllength;
T[] entries;
int8 zlend;
}
zlbytes 用于记录整个压缩列表占用字节数
zltail_offset 表示最后一个节点距离压缩列表起始地址有多少个字节,有个这个偏移量,无需遍历就能快速定位到最后一个节点
zllength 表示压缩列表里的节点个数
entries 表示节点内容列表,挨个挨个紧凑存储
zlend 标志压缩列表的结束,值恒为 0xFF
压缩列表节点构成
struct entry {
int<var> previous_entry_length;
int<var> encoding;
optional byte[] content;
}
previous_entry_length 表示前一个 entry 的字节长度,当压缩列表倒着遍历时,需要通过这个字段来快速定位到下一个节点的位置。这个属性长度可以是1字节或者5字节,如果前一个节点的长度大于或等于254字节,那么该属性为5字节,如果前一个节点长度小于254字节,那么该属性为1字节。
encoding 记录了节点的content属性的类型编码,该属性长度为1字节,2字节或者5字节,值的最高位为00,01或者10的是字节数组的编码,数组的长度由该字段除去最高两位后的其他位表示,最高位为11的是整数编码。
content 表示节点的内容,节点值可以是一个字节数组或者整数,类型由encoding属性决定。
增加节点
由于 ziplist 是紧凑存储的,如果想要插入一个新的节点可能就需要调用 realloc 扩展内存,将原有的内容拷贝到新的地址上去,也可能直接在原有的地址上进行扩展,具体会怎么做取决于内存分配器算法和当前的 ziplist 内存大小。
不过如果 ziplist 占据内存太大,重新分配内存和拷贝内存就会有很大的消耗,所以 ziplist 一般不适合存储大型字符串和过多的元素。
级联更新
由于每个节点的previous_entry_length记录了前一个节点的长度信息,而这个属性的长度是不定的,可能是1字节也可能是5字节。
那么如果有某个 entry 经过修改后变成从小于254字节变成了大于254字节,那么这个 entry 的下一个节点的previous_entry_length属性就要跟着更新,从1字节扩展到5字节,那么这个更新可能也会导致当前 entry 变成了大于254 字节,那么它的下一个 entry 的previous_entry_length也要跟着更新,这就导致了级联更新。
不过大家也不用太过于担心,实际上这种情况发生的概率还是比较小的,因为它要求有连续多个 entry 长度介于250字节到253字节之间的节点才可能触发,其次,只要更新的节点数量不是太多,也会对性能造成什么影响。