Redis学习笔记&源码阅读--压缩列表-概念

申明

  • 本文基于Redis源码5.0.8
  • 本文内容大量借鉴《Redis设计和实现》和《Redis5设计与源码分析》
概念 压缩列表存储结构 Redis使用字节数组表示一个压缩列表,压缩列表结构示意如下所示:
zlbytes zltail zllen entry1 entryX zlend
各字段的具体含义如下:
  1. zlbytes:压缩列表的字节长度,占4个字节,因此压缩列表最多有2^32-1个字节。
  2. zltail:压缩列表尾元素相对于压缩列表起始地址的偏移量,占4个字节。
  3. zllen:压缩列表的元素个数,占2个字节。zllen无法存储元素个数超过65535(2^16-1)的压缩列表,必须遍历整个压缩列表才能获取到元素个数。
  4. zlend:压缩列表的结尾,占1个字节,恒为0xFF。
在源码中压缩列表并没有对应的数据结构,它实际上只是一段连续的内存,并通过一个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

压缩列表在Redis中的使用 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作为底层存储,否则需要转换为字典存储。
  1. key-value结构的所有键值对的字符串长度都小于hash-max-ziplist-value(默认值64),该值可以通过配置文件配置。
  2. 散列对象保存的键值对的个数(一个键值对记为1个)小于hash-max-ziplist-entries(默认值512),该值也可以通过配置文件配置。
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中各个字段的含义,我相信现在你已经很清楚了,那么我们来汇总下结论。
  • prevrawlensize: 编码previous_entry_length信息占用的字节数;
  • prevrawlen:previous_entry_length的值;
  • lensize:编码encoding部分占用的字节数;
  • len:content占用的字节数;
  • headersize:如果把整个entry分为header+content两部分,那么headersize就是header长度,header包括previous_entry_length + encoding两个部分;
  • encoding:entry对应数据类型的编码(已经清理了原始encoding部分中content长度的信息);
  • p:entry首元素指针;
【Redis学习笔记&源码阅读--压缩列表-概念】好了,到这里,对ziplist的结构应该从源码层有一定的了解了,后面我们再来看ziplist的增删改查等操作具体如何实现。

    推荐阅读