Redis 设计与实现 5:压缩列表

压缩列表是 ZSET、HASH和 LIST 类型的其中一种编码的底层实现,是由一系列特殊编码的连续内存块组成的顺序型数据结构,其目的是节省内存。

ziplist 的结构 外层结构

下图展示了压缩列表的组成:

ziplist 的结构

各个字段的含义如下:

zlbytes:是一个无符号 4 字节整数,保存着 ziplist 使用的内存数量。
通过 zlbytes,程序可以直接对 ziplist 的内存大小进行调整,无须为了计算 ziplist 的内存大小而遍历整个列表。

zltail:压缩列表 最后一个 entry 距离起始地址的偏移量,占 4 个字节。
这个偏移量使得对表尾的 pop 操作可以在无须遍历整个列表的情况下进行。

zllen:压缩列表的节点 entry 数目,占 2 个字节。
当压缩列表的元素数目超过 2^16 - 2 的时候,zllen 会设置为2^16-1,当程序查询到值为2^16-1,就需要遍历整个压缩列表才能获取到元素数目。所以 zllen 并不能替代 zltail。

entryX:压缩列表存储数据的节点,可以为字节数组或者整数。

zlend:压缩列表的结尾,占一个字节,恒为 0xFF。

实现的代码 ziplist.c 中,ziplist 定义成了宏属性。

// 相当于 zlbytes,ziplist 使用的内存字节数 #define ZIPLIST_BYTES(zl) (*((uint32_t*)(zl))) // 相当于 zltail,最后一个 entry 距离 ziplist 起始位置的偏移量 #define ZIPLIST_TAIL_OFFSET(zl) (*((uint32_t*)((zl)+sizeof(uint32_t)))) // 相当于 zllen,entry 的数量 #define ZIPLIST_LENGTH(zl) (*((uint16_t*)((zl)+sizeof(uint32_t)*2))) // zlbytes + zltail + zllen 的长度,也就是 4 + 4 + 2 = 10 #define ZIPLIST_HEADER_SIZE (sizeof(uint32_t)*2+sizeof(uint16_t)) // zlend 的长度,1 字节 #define ZIPLIST_END_SIZE (sizeof(uint8_t)) // 指向第一个 entry 起始位置的指针 #define ZIPLIST_ENTRY_HEAD(zl) ((zl)+ZIPLIST_HEADER_SIZE) // 指向最后一个 entry 起始位置的指针 #define ZIPLIST_ENTRY_TAIL(zl) ((zl)+intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))) // 相当于 zlend,指向 ziplist 最后一个字节 #define ZIPLIST_ENTRY_END(zl) ((zl)+intrev32ifbe(ZIPLIST_BYTES(zl))-1)

以下是重建新的空 ziplist 的代码实现,在 ziplist.c 中:

unsigned char *ziplistNew(void) { // ziplist 头加上结尾标志字节数,就是 ziplist 使用内存的字节数了 unsigned int bytes = ZIPLIST_HEADER_SIZE+ZIPLIST_END_SIZE; unsigned char *zl = zmalloc(bytes); ZIPLIST_BYTES(zl) = intrev32ifbe(bytes); // 因为没有 entry 列表,所以尾部偏移量是 ZIPLIST_HEADER_SIZE ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(ZIPLIST_HEADER_SIZE); // entry 节点数量是 0 ZIPLIST_LENGTH(zl) = 0; // 设置尾标识。 // #define ZIP_END 255 zl[bytes-1] = ZIP_END; return zl; } entry 节点的结构 布局

节点的结构一般是:<prevlen> <encoding> <entry-data>

prevlen:前一个 entry 的大小,用于反向遍历。

encoding:编码,由于 ziplist 就是用来节省空间的,所以 ziplist 有多种编码,用来表示不同长度的字符串或整数。

data:用于存储 entry 真实的数据;

prevlen

节点的 prevlen 属性以字节为单位,记录了压缩列表中前一个节点的长度。编码长度可以是 1 字节或者 5 字节。

当前面节点长度小于 254 的时候,长度为 1 个字节。

当前面节点长度大于 254 的时候,1 个字节不够存了。前面第一个字节就设置为 254,后面 4 个字节才是真正的前面节点的长度。

下图展示了 1 字节 和 5 字节 prevlen 的示意图(来源)

不同长度的 prevlen 示意图

prevlen 属性主要的作用是反向遍历。通过 ziplist 的 zltail,我们可以得到最后一个节点的位置,接着可以获取到前一个节点的长度 len,指针向前移动 len,就是指向倒数第二个节点的位置了。以此类推,可以一直往前遍历。

encoding

encoding 记录了节点的 data 属性所保存数据的类型和长度。类型主要有两种:字符串和整数。

类型 1. 字符串

如果 encoding 以 00、01 或者 10 开头,就表示数据类型是字符串

#define ZIP_STR_06B (0 << 6) #define ZIP_STR_14B (1 << 6) #define ZIP_STR_32B (2 << 6)

字符串有三种编码:

长度 < 2^6 时,以 00 开头,后 6 位表示 data 的长度,。

2^6 <= 长度 < 2^14 时,以 01 开头,后续 6 位 + 下一个字节的 8 位 = 14 位表示 data 的长度。

2^14 <= 长度 < 2^32 字节时,以 10 开头,后续 6 位不用,从下一字节起连续 32 位表示 data 的长度。

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zydjgy.html