一 介绍
用途
ziplist压缩列表底层实现 是 list对象 与 hash对象 的底层实现之一。
当一个list对象只需要包含少量元素,并且每个元素要么就是小整数值,要么就是长度比较短的字符串,那么Redis就用ziplist来做 list对象 的底层实现。
当一个 hash对象 只包含少量键值对时,并且每个键值对的键和值要么就是小整数要么就是长度比较短的字符串,那么也用 ziplist 作为底层实现。
例如:
rpush lst 1 3 5 'hello'
object encoding lst # 是ziplist
hmset profile name 'fwy' age 18
object encoding profile #是"ziplist"
ziplist 是一种为节约内存而开发的顺序型数据结构。
ziplist 是一系列特殊编码的连续内存块,可以理解成不规则的数组,每个元素的长度是通过元素自身去记录的。
添加新结点到ziplist,或者删除结点,都可能引起连锁更新,但概率不高。
压缩列表可以包含多个结点,每个结点可以保存一个字节数组或者整数值。
源码
ziplist.c
ziplist.h
二 数据结构
以下就是 ziplist 的内存结构
是整个ziplist
所占用的字节数量,
一个无符号整数记录(uint32_t)。
通过保存这个值,可以在不遍历整个 ziplist 的前提下,对整个 ziplist 进行内存重分配。难道一个ziplist可以保存4GB的内容?
是到列表中最后一个节点的偏移量(同样为 uint32_t)。
有了这个偏移量,就可以在常数复杂度内对表尾进行操作,用指针加上偏移量,而不必遍历整个列表。
是entry的数量,为 ``uint16_t`` 。
当这个值大于 2**16-2 时,需要遍历整个列表,才能计算出列表的长度。也就是说,当节点数量小于65535时,zllen就是列表结点的数量,当节点数量大于等于65535时,那么需要遍历才能得知数量。
是一个单字节的特殊值,等于 255 ,它标识了列表的末端。
所以我们看到,一个ziplist,不算各个entry,需要11字节的管理开销。
ziplist里面的entry的结构
/*
节点结构
*/
typedef struct zlentry {
unsigned int prevrawlensize, // 保存前一节点的长度所需的长度,单位字节
prevrawlen; // 前一节点的长度,主要用于从后往前遍历时使用
unsigned int lensize, // 保存节点的长度所需的长度
len; // 节点的长度
unsigned int headersize; // header 长度, 是上面的 prevrawlensize + lensize
unsigned char encoding; // 编码方式
unsigned char *p; // 内容
} zlentry;
整个entry占多少字节呢?
prevrawlensize + lensize + len
prevrawlensize
|
prevrawlen
|
lensize
|
len
|
headersize
|
encoding
|
p
|
* 前一个节点的长度的储存方式如下:
* 1) 如果节点的长度 < 254 字节,那么直接用一个字节保存这个值。
* 2) 如果节点的长度 >= 254 字节,那么将第一个字节设置为 254 ,
* 再在之后用 4 个字节来表示节点的实际长度(共使用 5 个字节)。
* 当前结点的类型与长度
*
* 当节点保存的是字符串时,第2部分的前 2 位用于指示保存内容长度所使用的编码方式,
* 之后跟着的是内容长度的值。
*
* 当节点保存的是整数时,header 的前 2 位都设置为 1 ,
* 之后的 2 位用于指示保存的整数值的类型(这个类型决定了内容所占用的空间)。
三 连锁更新
连锁更新说的是,当前某个 entry 之前的节点 从小于254字节,变成大于等于254字节, 那么当前entry 的 previous_entry_length 从1字节变成5字节。如果因为从1字节变成5字节,使自己跨越了从小于254字节,到过了254字节这条线,就又会引起下一个节点的扩容。
最坏的情况是:所有entry都是刚好处于250-253字节之间,然后在链表头插入一个大于等于254字节的entry,此时会触发全链连锁更新。
除了插入entry会引起连锁更新,删除也可能会,[big][small][entry1][entry2] ,如果把[small]删除,那么entry1可能就因为[big],而需要内存重分配,继而影响后面的entry。
连锁更新最坏情况是执行N次空间重分配操作,每次空间重分配的最坏复杂度是O(N),所以最坏复杂度是O(N^2)。最坏的情况发生几率很低,很难全链每个entry刚好是250-253字节,如果只是引起若干个结点的连锁更新,不会对效率产生很大影响。ziplistPush命令的平均复杂度仅为O(N)
四 主要 API
举例:
/*
* 新创建一个空 ziplist并返回,不包含任何entry。
* 复杂度:O(1)
*/
unsigned char *ziplistNew(void) {
// 分别用于 和
unsigned int bytes = ZIPLIST_HEADER_SIZE+1; // header需要10字节,2 个 32 bit,一个 16 bit,以及一个 8 bit作为最后一个zlend
unsigned char *zl = zmalloc(bytes); // 分配11字节的内存
// 设置长度
ZIPLIST_BYTES(zl) = intrev32ifbe(bytes); //intrev32ifbe是转小端来保存。ZIPLIST_BYTES是将ziplist的第一部分(32bit)位置取出,然后把整个ziplist长度(11字节)存入该位置。
// 设置表尾偏移量
ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(ZIPLIST_HEADER_SIZE); // 设置在 32-63 位的zltail 为列表header的长度,即10(字节),一开始
// 设置列表项数量
ZIPLIST_LENGTH(zl) = 0; //设置zllen,当前还没有任何entry
// 设置表尾标识 // 最后已给字节赋值为十进制255
zl[bytes-1] = ZIP_END;
return zl;
}
注意:ziplistPush,ziplistInsert,ziplistDelete,ziplistDeleteRange都有可能引起连锁更新。