Redis学习笔记&源码阅读--压缩列表-概念
申明
- 本文基于Redis源码5.0.8
- 本文内容大量借鉴《Redis设计和实现》和《Redis5设计与源码分析》
zlbytes | zltail | zllen | entry1 | … | entryX | zlend |
---|
- zlbytes:压缩列表的字节长度,占4个字节,因此压缩列表最多有2^32-1个字节。
- zltail:压缩列表尾元素相对于压缩列表起始地址的偏移量,占4个字节。
- zllen:压缩列表的元素个数,占2个字节。zllen无法存储元素个数超过65535(2^16-1)的压缩列表,必须遍历整个压缩列表才能获取到元素个数。
- zlend:压缩列表的结尾,占1个字节,恒为0xFF。
zl中保存的entry也同样有相应的编码结构,如下所示:
previous_entry_length | encoding | content |
---|
假设已知当前元素的首地址为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 |
ziplist也可以作为Hash的底层实现,当需要存储的key-value结构同时满足下面两个条件时,采用ziplist作为底层存储,否则需要转换为字典存储。
- key-value结构的所有键值对的字符串长度都小于hash-max-ziplist-value(默认值64),该值可以通过配置文件配置。
- 散列对象保存的键值对的个数(一个键值对记为1个)小于hash-max-ziplist-entries(默认值512),该值也可以通过配置文件配置。
结构实现 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首元素指针;
推荐阅读
- EffectiveObjective-C2.0|EffectiveObjective-C2.0 笔记 - 第二部分
- 由浅入深理解AOP
- 继续努力,自主学习家庭Day135(20181015)
- python学习之|python学习之 实现QQ自动发送消息
- Android中的AES加密-下
- 一起来学习C语言的字符串转换函数
- 定制一套英文学习方案
- 漫画初学者如何学习漫画背景的透视画法(这篇教程请收藏好了!)
- 《深度倾听》第5天──「RIA学习力」便签输出第16期
- 如何更好的去学习