压缩列表(ziplist)是列表键和哈希键的底层实现之一。
Redis的列表键,哈希键,有序集合的底层实现都用到了ziplist。
当列表键中包含比较少的元素,并且元素都是数字或者比较小的字符串的时候, redis会用压缩列表来作为列表键的底层实现。
当哈希键的键和值都是比较小的整数或者较短的字符的时候,也是用压缩列表来作为底层实现。 因为压缩列表也能够节省内存。
压缩列表的结构如下:
列表头包括三部分内容,分别是zlbytes,zltail,zllen
压缩列表中间一次保存着各个列表项entry。
压缩列表尾部的zlend则表示压缩列表结束,其值固定为0xFF。
先看结点的数据结构:
typedef struct zlentry {
unsigned int prevrawlensize, prevrawlen; // 前置节点长度和编码所需长度
unsigned int lensize, len; // 当前节点长度和编码所需长度
unsigned int headersize; // 头的大小
unsigned char encoding; // 编码类型
unsigned char *p; // 数据部分
} zlentry;
每个压缩列表节点都由 previous_entry_length 、 encoding 、 content 三个部分组成。
节点的 previous_entry_length 记录了压缩列表中前一个节点的长度。
previous_entry_length 属性的长度可以是 1 字节或者 5 字节:
压缩列表zltail和previous_entry_length的存在,我们能够轻松得到一个列表的尾部,然后从尾部实现向前遍历整个压缩列表。
压缩列表能够保存字节数组和整数,当读取压缩列表的时候,如何区分当前的结点存储的是字节数组还是整数呢,就需要靠encoding字段来判断。
保存字节数组的时候,encoding字段可以是一字节、两字节或者五字节长, 值的最高位为 00 、 01 或者 10 ,数组的长度由编码除去最高两位之后的其他位记录。
编码 | 编码长度 | content 属性保存的值 |
---|---|---|
00bbbbbb | 1 字节 | 长度小于等于 63 字节的字节数组。 |
01bbbbbb xxxxxxxx | 2 字节 | 长度小于等于 16383 字节的字节数组。 |
10______ aaaaaaaa bbbbbbbb cccccccc dddddddd | 5 字节 | 长度小于等于 4294967295 的字节数组。 |
如上表所示,三种长度的字节数组分别用不同长度的encoding字段来表示,用来节省空间。 而encoding的前两位用来标记encoding本身的类型。
保存整数的时候,encoding字段为一字节长, 值的最高位以 11 开头。 整数值的类型和长度由编码除去最高两位之后的其他位记录。
编码 | 编码长度 | 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 属性。 |
当encoding最前两位字段为11的时候,表示当前结点为整数。 同时encoding的后几位用来表示不同的整数类型。可以看到后几位中用000000表示int16_t 类型的整数, 用010000表示int32_t 类型的整数, 用100000表示int64_t 类型的整数。
可以注意到,为了进一步节省内存,当编码为1111xxxx时,表示没有内容部分,xxxx已经存放了当前的整数值,包括整数0~12,即xxxx可以表示0000~1101。这样就节省了content的内存空间。这边编码为11111111代表ziplist的结尾。
由于每个压缩列表的结点保存了上一个结点的大小,所以当前结点的变化有可能引起下一个结点的变化。如果前一节点的长度小于 254 字节, 那么 previous_entry_length 属性需要用 1 字节长的空间来保存这个长度值; 如果超过了254字节,这个属性值就需要 5 个字节的长度来保存。
所以最坏的情况下,压缩列表中某一个结点的更新,会引起所有结点的一个更新操作,就是所谓的连锁更新。
此外,插入或者删除结点也有可能引起连锁更新的操作。不过虽然连锁更新带来的消耗很大,但是仍旧可以放心的使用压缩列表,因为连锁更新引起的条件比较苛刻,概率比较小。 首先, 压缩列表里要恰好有多个连续的、长度介于 250 字节至 253 字节之间的节点, 连锁更新才有可能被引发, 在实际中, 这种情况并不多见;
其次, 即使出现连锁更新, 但只要被更新的节点数量不多, 就不会对性能造成任何影响: 比如说, 对三五个节点进行连锁更新是绝对不会影响性能的。
/* Create a new empty ziplist. */
unsigned char *ziplistNew(void) {
// 空ziplist的大小为11个字节,头部10字节,尾部1字节
unsigned int bytes = ZIPLIST_HEADER_SIZE+1;
// 开辟空间
unsigned char *zl = zmalloc(bytes);
// 设定压缩列表的大小
ZIPLIST_BYTES(zl) = intrev32ifbe(bytes);
// 设置尾结点相对头部的偏移量
ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(ZIPLIST_HEADER_SIZE);
// 压缩列表结点数为0
ZIPLIST_LENGTH(zl) = 0;
// 设定尾部一个字节位0xFF
zl[bytes-1] = ZIP_END;
return zl;
}
由于连锁更新的存在,插入结点的复杂度平均 O(N) ,最坏 O(N^2)
// ziplist插入节点只能往头或者尾部插入
// zl: 待插入的ziplist
// s,slen: 待插入节点和其长度
// where: 带插入的位置,0代表头部插入,1代表尾部插入
unsigned char *ziplistPush(unsigned char *zl, unsigned char *s, unsigned int slen, int where) {
unsigned char *p;
// 获取插入的位置
p = (where == ZIPLIST_HEAD) ? ZIPLIST_ENTRY_HEAD(zl) : ZIPLIST_ENTRY_END(zl);
// 执行具体的插入过程
return __ziplistInsert(zl,p,s,slen);
}
/* Insert item at "p". */
unsigned char *__ziplistInsert(unsigned char *zl, unsigned char *p, unsigned char *s, unsigned int slen) {
size_t curlen = intrev32ifbe(ZIPLIST_BYTES(zl)), reqlen;
unsigned int prevlensize, prevlen = 0; // 前置节点长度和编码该长度值所需的长度
size_t offset;
int nextdiff = 0;
unsigned char encoding = 0;
long long value = 123456789; /* 为了防止警告,进行初始化;用一个比较特殊的值以便能够方便的观察到不恰当的使用 */
zlentry tail;
/* Find out prevlen for the entry that is inserted. */
if (p[0] != ZIP_END) {
// 如果不是压缩列表的结束标志,说明p指向了一个已存在的结点
// 解码得到p的前置结点和长度
ZIP_DECODE_PREVLEN(p, prevlensize, prevlen);
} else {
// 如果p指向列表末端,表示列表为空
unsigned char *ptail = ZIPLIST_ENTRY_TAIL(zl);
if (ptail[0] != ZIP_END) {
prevlen = zipRawEntryLength(ptail);
}
}
/* See if the entry can be encoded */
// 判断是否能够编码为整数
if (zipTryEncoding(s,slen,&value,&encoding)) {
/* 'encoding' is set to the appropriate integer encoding */
reqlen = zipIntSize(encoding);
} else {
/* 'encoding' is untouched, however zipStoreEntryEncoding will use the
* string length to figure out how to encode it. */
// 编码为字节数组
reqlen = slen;
}
/* We need space for both the length of the previous entry and
* the length of the payload. */
// 加上前置结点的编码长度和当前结点的编码长度
reqlen += zipStorePrevEntryLength(NULL,prevlen);
reqlen += zipStoreEntryEncoding(NULL,encoding,slen);
/* When the insert position is not equal to the tail, we need to
* make sure that the next entry can hold this entry's length in
* its prevlen field. */
// 如果不是插入到列表的末端,都需要判断下一个结点是否能存放新节点的长度编码
// nextdiff保存新旧编码之间的字节大小差,如果这个值大于0
// 那就说明当前p指向的节点的header进行扩展
int forcelarge = 0;
nextdiff = (p[0] != ZIP_END) ? zipPrevLenByteDiff(p,reqlen) : 0;
if (nextdiff == -4 && reqlen < 4) {
nextdiff = 0;
forcelarge = 1;
}
/* Store offset because a realloc may change the address of zl. */
// 保存偏移量
offset = p-zl;
// 重新分配空间,curlen当前列表的长度
// reqlen 新节点的全部长度
// nextdiff 新节点的后继节点扩展header的长度
zl = ziplistResize(zl,curlen+reqlen+nextdiff);
// 根据新的压缩列表地址得到新的p的地址
p = zl+offset;
/* Apply memory move when necessary and update tail offset. */
if (p[0] != ZIP_END) {
// 如果不是表尾插入,需要更新表尾的偏移地址
/* Subtract one because of the ZIP_END bytes */
memmove(p+reqlen,p-nextdiff,curlen-offset-1+nextdiff);
/* Encode this entry's raw length in the next entry. */
// 编码新结点的长度到下一个结点中
if (forcelarge)
zipStorePrevEntryLengthLarge(p+reqlen,reqlen);
else
zipStorePrevEntryLength(p+reqlen,reqlen);
/* Update offset for tail */
ZIPLIST_TAIL_OFFSET(zl) =
intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+reqlen);
/* When the tail contains more than one entry, we need to take
* "nextdiff" in account as well. Otherwise, a change in the
* size of prevlen doesn't have an effect on the *tail* offset. */
zipEntry(p+reqlen, &tail);
if (p[reqlen+tail.headersize+tail.len] != ZIP_END) {
ZIPLIST_TAIL_OFFSET(zl) =
intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+nextdiff);
}
} else {
/* This element will be the new tail. */
ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(p-zl);
}
// 如果nextdiff不等于0, 下一个结点的头部需要进行扩展
if (nextdiff != 0) {
offset = p-zl;
zl = __ziplistCascadeUpdate(zl,p+reqlen);
p = zl+offset;
}
/* Write the entry */
// 将新节点前置节点的长度写入新节点的header
p += zipStorePrevEntryLength(p,prevlen);
// 编码新结点
p += zipStoreEntryEncoding(p,encoding,slen);
if (ZIP_IS_STR(encoding)) {
memcpy(p,s,slen);
} else {
zipSaveInteger(p,value,encoding);
}
ZIPLIST_INCR_LENGTH(zl,1);
return zl;
}
/* 寻找节点值和 vstr 相等的列表节点,并返回该节点的指针。
* 每次比对之前都跳过 skip 个节点。
* 如果找不到相应的节点,则返回 NULL 。 */
unsigned char *ziplistFind(unsigned char *p, unsigned char *vstr, unsigned int vlen, unsigned int skip) {
int skipcnt = 0;
unsigned char vencoding = 0;
long long vll = 0;
// 循环直到碰到结束标志
while (p[0] != ZIP_END) {
unsigned int prevlensize, encoding, lensize, len;
unsigned char *q;
// 解码得到前置结点的长度
ZIP_DECODE_PREVLENSIZE(p, prevlensize);
// 当前结点的长度
ZIP_DECODE_LENGTH(p + prevlensize, encoding, lensize, len);
q = p + prevlensize + lensize;
if (skipcnt == 0) {
/* Compare current entry with specified entry */
// 如果是字节数组,直接比较
if (ZIP_IS_STR(encoding)) {
if (len == vlen && memcmp(q, vstr, vlen) == 0) {
return p;
}
} else {
/* 查看目标值是否能被编码,只会在第一次循环的时候检查;
* 检查一次之后vencoding会被置为非0 */
if (vencoding == 0) {
if (!zipTryEncoding(vstr, vlen, &vll, &vencoding)) {
/* 如果不能被编码,设置格式为UCHAR_MAX , 下次不会再检查*/
vencoding = UCHAR_MAX;
}
/* Must be non-zero by now */
assert(vencoding);
}
/* Compare current entry with specified entry, do it only
* if vencoding != UCHAR_MAX because if there is no encoding
* possible for the field it can't be a valid integer. */
if (vencoding != UCHAR_MAX) {
long long ll = zipLoadInteger(q, encoding);
if (ll == vll) {
return p;
}
}
}
/* Reset skip count */
skipcnt = skip;
} else {
/* Skip entry */
skipcnt--;
}
/* Move to next entry */
// 后移指针,指向后置节点
p = q + len;
}
return NULL;
}
// 删除给定节点,输入压缩列表zl和指向删除节点的指针p
unsigned char *ziplistDelete(unsigned char *zl, unsigned char **p) {
size_t offset = *p-zl;
// 调用底层函数__ziplistDelete进行删除操作
zl = __ziplistDelete(zl,*p,1);
// 删除操作可能会改变zl,因为会重新分配内存
*p = zl+offset;
return zl;
}
/* 从位置 p 开始,连续删除 num 个节点。
* 函数的返回值为处理删除操作之后的 ziplist */
unsigned char *__ziplistDelete(unsigned char *zl, unsigned char *p, unsigned int num) {
unsigned int i, totlen, deleted = 0;
size_t offset;
int nextdiff = 0;
zlentry first, tail;
zipEntry(p, &first);
// 计算被删除节点的总个数
for (i = 0; p[0] != ZIP_END && i < num; i++) {
p += zipRawEntryLength(p);
deleted++;
}
// totlen 是所有被删除节点总共占用的内存字节数
totlen = p-first.p; /* Bytes taken by the element(s) to delete. */
if (totlen > 0) {
if (p[0] != ZIP_END) {
// 不是尾结点,表示被删除节点之后仍然有节点存在
// 因为位于被删除范围之后的第一个节点的 header 部分的大小
// 可能容纳不了新的前置节点,所以需要计算新旧前置节点之间的字节数差
nextdiff = zipPrevLenByteDiff(p,first.prevrawlen);
/* Note that there is always space when p jumps backward: if
* the new previous entry is large, one of the deleted elements
* had a 5 bytes prevlen header, so there is for sure at least
* 5 bytes free and we need just 4. */
// 如果有需要的话,将指针 p 后退 nextdiff 字节,为新 header 空出空间
// 由于会删除之前的结点,所以肯定会有足够的空间用来扩展
p -= nextdiff;
// 将 first 的前置节点的长度编码至 p 中
zipStorePrevEntryLength(p,first.prevrawlen);
/* Update offset for tail */
ZIPLIST_TAIL_OFFSET(zl) =
intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))-totlen);
/* When the tail contains more than one entry, we need to take
* "nextdiff" in account as well. Otherwise, a change in the
* size of prevlen doesn't have an effect on the *tail* offset. */
// 如果被删除节点之后,有多于一个节点
// 那么程序需要将 nextdiff 记录的字节数也计算到表尾偏移量中
// 这样才能让表尾偏移量正确对齐表尾节点
zipEntry(p, &tail);
if (p[tail.headersize+tail.len] != ZIP_END) {
ZIPLIST_TAIL_OFFSET(zl) =
intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+nextdiff);
}
/* Move tail to the front of the ziplist */
// 从表尾向表头移动数据,覆盖被删除节点的数据
memmove(first.p,p,
intrev32ifbe(ZIPLIST_BYTES(zl))-(p-zl)-1);
} else {
/* The entire tail was deleted. No need to move memory. */
// 执行这里,表示被删除节点之后已经没有其他节点了, 不需要移动结点
ZIPLIST_TAIL_OFFSET(zl) =
intrev32ifbe((first.p-zl)-first.prevrawlen);
}
/* Resize and update length */
// 缩小并更新 ziplist 的长度
offset = first.p-zl;
zl = ziplistResize(zl, intrev32ifbe(ZIPLIST_BYTES(zl))-totlen+nextdiff);
ZIPLIST_INCR_LENGTH(zl,-deleted);
p = zl+offset;
/* When nextdiff != 0, the raw length of the next entry has changed, so
* we need to cascade the update throughout the ziplist */
// 如果 p 所指向的节点的大小已经变更,那么进行级联更新
// 检查 p 之后的所有节点是否符合 ziplist 的编码要求
if (nextdiff != 0)
zl = __ziplistCascadeUpdate(zl,p);
}
return zl;
}