Redis使用字节数组表示一个压缩列表,压缩列表结构示意如下所示:
zlbytes | zltail | zllen | entry1 | … | entryX | zlend |
---|
各字段的具体含义如下:
在源码中压缩列表并没有对应的数据结构,它实际上只是一段连续的内存,并通过一个unsigned char 类型的指针进行各项操作,所以对ziplist的操作需要大量的编解码,而这些几乎都是通过C语言的宏实现的。
zl中保存的entry也同样有相应的编码结构,如下所示:
previous_entry_length | encoding | content |
---|
previous_entry_length字段表示前一个元素的字节长度,占1个或者5个字节,当前一个元素的长度小于254字节时,用1个字节表示;当前一个元素的长度大于或等于254字节时,用5个字节来表示。而此时previous_entry_length字段的第1个字节是固定的0xFE,后面4个字节才真正表示前一个元素的长度。
假设已知当前元素的首地址为p,那么p-previous_entry_length就是前一个元素的首地址,从而实现压缩列表从尾到头的遍历。
encoding字段表示当前元素的编码,即content字段存储的数据类型(整数或者字节数组),数据内容存储在content字段。为了节约内存,encoding字段同样长度可变。压缩列表元素的编码如下所示。
encoding编码 | encoding长度 | content类型 |
---|---|---|
00 bbbbbb (6比特表示content长度) | 1字节 | 最大长度为63的字节数组 |
01 bbbbbb xxxxxxxx (14比特表示content长度) | 2字节 | 最大长度为2^14-1的字节数组 |
10 ------ aaaaaaaa bbbbbbbb cccccccc dddddddd(32比特表示content长度) | 5字节 | 最大长度为2^32-1的字节数组 |
11 00 0000 | 1字节 | int16整数 |
11 01 0000 | 1字节 | int32整数 |
11 10 0000 | 1字节 | int64整数 |
11 11 0000 | 1字节 | 24位整数 |
11 11 1110 | 1字节 | 8位整数 |
11 11 xxxx | 1字节 | 没有content字段;xxxx表示0~12的整数 |
第四行中的------表示该位置的6个bit值不需要使用
上面表格最后一行,如果你留个心眼计算,会发现后四bits能表示的明明是0~15,怎么就是0~12呢?其实encoding部分能表示的值是在ZIP_INT_24B(11 11 0000)与ZIP_INT_8B(11 11 1110)之间,也就是[1,13],可能是为了能表示0 ,计算结果时会减1(参看zipLoadInteger函数代码)
可以看出,根据encoding字段第1个字节的前2位,可以判断content字段存储的是整数或者字节数组(及其最大长度)。当content存储的是字节数组时,后续字节标识字节数组的实际长度;当content存储的是整数时,可根据第3、第4位判断整数的具体类型。而当encoding字段标识当前元素存储的是0~12的立即数时,数据直接存储在encoding字段的最后4位,此时没有content字段。参照encoding字段的编码表格,Redis预定义了以下常量对应encoding字段的各编码类型:
/* Different encoding/length possibilities */
#define ZIP_STR_06B (0 << 6)
#define ZIP_STR_14B (1 << 6)
#define ZIP_STR_32B (2 << 6)
#define ZIP_INT_16B (0xc0 | 0<<4)
#define ZIP_INT_32B (0xc0 | 1<<4)
#define ZIP_INT_64B (0xc0 | 2<<4)
#define ZIP_INT_24B (0xc0 | 3<<4)
#define ZIP_INT_8B 0xfe
zplist在Redis中的使用大致如下所示:
类型 | 编码 |
---|---|
REDIS_LIST | REDIS_ENCODING_ZIPLIST |
REDIS_HASH | REDIS_ENCODING_ZIPLIST |
REDIS_ZSET | REDIS_ENCODING_ZIPLIST |
目前redis的列表是通过quicklist(以后介绍)实现的,但是quicklist是由ziplist和adlist组合而成。
ziplist也可以作为Hash的底层实现,当需要存储的key-value结构同时满足下面两个条件时,采用ziplist作为底层存储,否则需要转换为字典存储。
ziplist也可以作为有序集合的底层实现,当服务器属性server.zset_max_ziplist_entries的值大于0且元素的member长度小于服务器属性server.zset_max_ziplist_value的值(默认为64)时,使用的是ziplist作为底层存储。
Redis中是如何保存一个ziplist的,我们看下Redis中是如何创建一个ziplist实例
/* Create a new empty ziplist. */
unsigned char *ziplistNew(void) {
unsigned int bytes = ZIPLIST_HEADER_SIZE+ZIPLIST_END_SIZE;
unsigned char *zl = zmalloc(bytes);
ZIPLIST_BYTES(zl) = intrev32ifbe(bytes);
ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(ZIPLIST_HEADER_SIZE);
ZIPLIST_LENGTH(zl) = 0;
zl[bytes-1] = ZIP_END;
return zl;
}
在讲解这段代码前,我们再看下一些用来存取ziplist时使用的宏定义。
//返回ziplist字节长度,对应zlbytes
#define ZIPLIST_BYTES(zl) (*((uint32_t*)(zl)))
//返回ziplist尾元素相对于压缩列表起始地址的偏移量,对应zltail
#define ZIPLIST_TAIL_OFFSET(zl) (*((uint32_t*)((zl)+sizeof(uint32_t))))
//返回ziplist元素个数,对应zllen
#define ZIPLIST_LENGTH(zl) (*((uint16_t*)((zl)+sizeof(uint32_t)*2)))
//ziplist头长度,头包括zlbytes+zltail+zllen
#define ZIPLIST_HEADER_SIZE (sizeof(uint32_t)*2+sizeof(uint16_t))
//返回zlend占用字节数
#define ZIPLIST_END_SIZE (sizeof(uint8_t))
#define ZIP_END 255
了解了这几个宏定义再来看ziplistNew就简单多了,首先计算一个空zllist占用多少字节,申请对应内存,并将大小转为主机字节序保存在zlbytes部分,空ziplist尾元素和ziplist起始位置之间就是Header,所以zltail设置为Header的长度,空ziplist长度是0,将zlend设置为0xFF。
上面介绍了压缩列表的存储结构,对于压缩列表的任意元素,获取前一个元素的长度、判断存储的数据类型、获取数据内容都需要经过复杂的解码运算。解码后的结果应该被缓存起来,为此定义了结构体zlentry,用于表示解码后的压缩列表元素。
/* We use this function to receive information about a ziplist entry.
* Note that this is not how the data is actually encoded, is just what we
* get filled by a function in order to operate more easily. */
typedef struct zlentry {
unsigned int prevrawlensize; /* Bytes used to encode the previous entry len*/
unsigned int prevrawlen; /* Previous entry len. */
unsigned int lensize; /* Bytes used to encode this entry type/len.
For example strings have a 1, 2 or 5 bytes
header. Integers always use a single byte.*/
unsigned int len; /* Bytes used to represent the actual entry.
For strings this is just the string length
while for integers it is 1, 2, 3, 4, 8 or
0 (for 4 bit immediate) depending on the
number range. */
unsigned int headersize; /* prevrawlensize + lensize. */
unsigned char encoding; /* Set to ZIP_STR_* or ZIP_INT_* depending on
the entry encoding. However for 4 bits
immediate integers this can assume a range
of values and must be range-checked. */
unsigned char *p; /* Pointer to the very start of the entry, that
is, this points to prev-entry-len field. */
} zlentry;
以上英文注释是不是看的头大,没关系,接下来我们一一讲解各个字段的含义,当然啦,是结合着源码来讲解,这样即能明白含义,源码层面有点了解,并且你也相信这不是我信口开河。。。
Redis是通过zipEntry函数来解码列表的元素,然后保存在zlentry中,函数的源码完整如下:
/* Return a struct with all information about an entry. */
void zipEntry(unsigned char *p, zlentry *e) {
ZIP_DECODE_PREVLEN(p, e->prevrawlensize, e->prevrawlen);
ZIP_DECODE_LENGTH(p + e->prevrawlensize, e->encoding, e->lensize, e->len);
e->headersize = e->prevrawlensize + e->lensize;
e->p = p;
}
我们来看两个宏定义,首先看ZIP_DECODE_PREVLEN,看它实现了什么功能,其定义如下:
#define ZIP_DECODE_PREVLEN(ptr, prevlensize, prevlen) do { \
ZIP_DECODE_PREVLENSIZE(ptr, prevlensize); \
if ((prevlensize) == 1) { \
(prevlen) = (ptr)[0]; \
} else if ((prevlensize) == 5) { \
assert(sizeof((prevlen)) == 4); \
memcpy(&(prevlen), ((char*)(ptr)) + 1, 4); \
memrev32ifbe(&prevlen); \
} \
} while(0);
说点题外话,为什么宏的定义放在do-while循环中,各位可以自己百度下,还是有点意思的。令人不愉快的是这里再次调用的一个宏,出现了嵌套,文字平述嵌套是很麻烦的,不过我们还是要继续探究下去(读者也自己注意点调用层次),再看下它的定义(自行注意参数的传递,别绕进去了)。
#define ZIP_DECODE_PREVLENSIZE(ptr, prevlensize) do { \
if ((ptr)[0] < ZIP_BIG_PREVLEN) { \
(prevlensize) = 1; \
} else { \
(prevlensize) = 5; \
} \
} while(0);
如果你自己留意了参数的传递应该知道ptr是传递进来需要解码的元素指针,prevlensize是待赋值的变量(当前我们还不知道它到底代表什么意思),总之我们知道这里应该是解码ptr给prevlensize赋值的,看明白这里我们就能知道它代表啥意思了。
判断ptr[0]的值是否小于ZIP_BIG_PREVLEN,这个也是一个宏,值为254,这个值还有印象吗?没有就再回到第一节看,理解了我们就知道这里的prevlensize原来表示用多少个字节来保存前一个元素长度信息。然后我们再回到上一层的宏调用中,当prevlensize=1时ptr[0]就是前一个元素的长度,否则就是后面5个字节保存长度且第一个字节固定为0xfe,代码很直观呈现出逻辑了,最后一个memrev32ifbe(&prevlen)的作用就是将数据的保存改为小端存储,也就是主机字节序。到这里我们也知道了prevlen就表示前一个元素的长度。
小小总结下,走完了ZIP_DECODE_PREVLEN调用的逻辑,我们获取到了保存前一个元素长度使用了多少个字节,并且也获取到了前一个元素的长度。接下来就得看ZIP_DECODE_LENGTH调用了,代码如下所示:
#define ZIP_DECODE_LENGTH(ptr, encoding, lensize, len) do { \
ZIP_ENTRY_ENCODING((ptr), (encoding)); \
if ((encoding) < ZIP_STR_MASK) { \
if ((encoding) == ZIP_STR_06B) { \
(lensize) = 1; \
(len) = (ptr)[0] & 0x3f; \
} else if ((encoding) == ZIP_STR_14B) { \
(lensize) = 2; \
(len) = (((ptr)[0] & 0x3f) << 8) | (ptr)[1]; \
} else if ((encoding) == ZIP_STR_32B) { \
(lensize) = 5; \
(len) = ((ptr)[1] << 24) | \
((ptr)[2] << 16) | \
((ptr)[3] << 8) | \
((ptr)[4]); \
} else { \
panic("Invalid string encoding 0x%02X", (encoding)); \
} \
} else { \
(lensize) = 1; \
(len) = zipIntSize(encoding); \
} \
} while(0);
老规矩,我们先看ZIP_ENTRY_ENCODING的定义。
#define ZIP_ENTRY_ENCODING(ptr, encoding) do { \
(encoding) = (ptr[0]); \
if ((encoding) < ZIP_STR_MASK) (encoding) &= ZIP_STR_MASK; \
} while(0)
注意在zipEntry函数中调用ZIP_DECODE_LENGTH时已经对ptr做移位操作了,移位后是对encoding进行解码,ZIP_STR_MASK的值是0xc0,二进制表示是11000000,如果你还记得encoding中是如何判断保存的是整数还是字符串,你就明白这里的含义了,所有整数的前两个bit都是11,否则就是字符,字符的话对ptr[0]进行一次位运算,将后6bit全部置空,结束处理。
再回到ZIP_DECODE_LENGTH中,第一层的if还是判断encoding保存的是字节数组还是整数,如果是字节数组就进一步判断是哪种情况,具体的判断逻辑对照前面的encoding表一起看代码就能明白,代码也都是些位运算,还是比较容易懂的,这里不展开了。如果是整数的话,是通过函数zipIntSize解码的,代码如下:
unsigned int zipIntSize(unsigned char encoding) {
switch(encoding) {
case ZIP_INT_8B: return 1;
case ZIP_INT_16B: return 2;
case ZIP_INT_24B: return 3;
case ZIP_INT_32B: return 4;
case ZIP_INT_64B: return 8;
}
if (encoding >= ZIP_INT_IMM_MIN && encoding <= ZIP_INT_IMM_MAX)
return 0; /* 4 bit immediate */
panic("Invalid integer encoding 0x%02X", encoding);
return 0;
}
其逻辑结合encoding表也是比较容易理解的。完了我们再来看下我们的起点,ZIP_DECODE_LENGTH(p + e->prevrawlensize, e->encoding, e->lensize, e->len),重点是后面的三个参数,原来encoding表示entry保存数据类型的编码,lensize表示的是encoding本身占用的字节大小,而len表示content内容的字节大小。
看完了两个宏定义,我们再回到函数主体,你们是不是可能已经绕进去了,我们把代码再贴一次(不会有人怀疑我是堆积篇幅)。
/* Return a struct with all information about an entry. */
void zipEntry(unsigned char *p, zlentry *e) {
ZIP_DECODE_PREVLEN(p, e->prevrawlensize, e->prevrawlen);
ZIP_DECODE_LENGTH(p + e->prevrawlensize, e->encoding, e->lensize, e->len);
e->headersize = e->prevrawlensize + e->lensize;
e->p = p;
}
接着是初始化headersize字段了,我们已经明白了prevrawlensize和lensize的含义的,那么headersize表示的就是保存previous_entry_length和encoding两部分使用的存储空间大小,指针p指向entry起始位置。你现在还记得我们为什么看这段源码吗?是为了理解zlentry中各个字段的含义,我相信现在你已经很清楚了,那么我们来汇总下结论。
好了,到这里,对ziplist的结构应该从源码层有一定的了解了,后面我们再来看ziplist的增删改查等操作具体如何实现。