ziplist通常的内存布局一般是<uint32_t zlbytes> <uint32_t zltail> <uint16_t zllen> <entry> <entry> ... <entry> <uint8_t zlend>,zlbytes表示ziplist占用了多少内存,zltail表示尾结点的偏移量,zllen表示结点数量,zlend表示结束标记位。
一个完整的entry内存分布是<prevlen> <encoding> <entry-data>,prevlen表示前驱结点长度,encoding表示字符串或者整数,如果是字符串的话还表示字符串的长度,有些encoding就表示entry-data,所以有时候它的内存分布是<prevlen> <encoding>。
这个数据结构设计巧妙的地方就是设计了几个标记,用最小的内存去存储最大的内容。prevlen如果小于254字节,prevlen就只用占用1个字节的内存,反之,第1个字节就置为254,再用另外4个字节来存储长度。当结点是字符串时,前2位用来存储字符串长度类型,后面就是字符串长度;当结点是整型时,前2位被设置为11,跟着的2位存储整型类型。
-
|00pppppp| - 1 byte,长度小于等于63(6位)
-
|01pppppp|qqqqqqqq| - 2 bytes,长度小于等于16383(14位)
-
|10000000|qqqqqqqq|rrrrrrrr|ssssssss|tttttttt| - 5 bytes,长度小于等于4294967295(32位),大端存储
-
|11000000| - 3 bytes,2字节整型
-
|11010000| - 5 bytes,4字节整型
-
|11100000| - 9 bytes,8字节整型
-
|11110000| - 4 bytes,3字节整型
-
|11111110| - 2 bytes,1字节整型
-
|1111xxxx| - 1 bytes,4位整型
在详细了解了ziplist的内存布局以后,大多数的函数设计思路也就非常的清晰了。
下图是__ziplistInsert函数流程图,唯一有点疑问的就是下面这个条件判断为什么要强制性地让这个节点变成大结点。
if (nextdiff == -4 && reqlen < 4) {
nextdiff = 0;
forcelarge = 1;
}
__ziplistCascadeUpdate这个函数重排了ziplist,增删改查都会用到这个函数重新调整ziplist。
unsigned char *__ziplistCascadeUpdate(unsigned char *zl, unsigned char *p)
让我觉得还不够完美的地方在于ziplist的内存分配用了默认的zmalloc,应该是为了统一方便操作,但是我觉得针对性地对这个结构体进行分配内存还可以更省一些内存,毕竟ziplist的前4字节已经设置了整个ziplist的大小。