一、ziplist
1.1 ziplist结构
Redis采用紧凑的字节数组表示一个压缩列表,压缩列表结构示意图如下:
<zlbytes> <zltail> <zllen> <entry> <entry> ... <entry> <zlend>
- zlbytes:压缩列表的字节长度,占4个字节,因此压缩列表最多有2*32 - 1个字节。
- zltail:压缩列表尾元素相对于压缩列表起始地址的偏移量,占4个字节。
- zllen:压缩列表的元素个数,占2个字节。zllen无法存储元素个数超过65535(2*16-1)的压缩列表,如果超过这个值必须遍历整个压缩列表才能获取到元素个数
- entryX:压缩列表存储的元素,可以是字节数组或整数,长度不限,entry的编码在下面介绍。
- zlend:压缩列表的结尾,占1个字节,恒为0xFF。
1.2 ziplist中entry的结构
<prevlen> <encoding> <entry-data>
当存储较小的整数类型时,encoding中会存储数据,此时结构如下
<prevlen> <encoding>
-
prevlen字段表示前一个元素的字节长度,占1个或者5个字节,当前一个元素的长度大于或等于254字节时,用5个字节表示。而此时prevlen字段的第1个字节是固定的0xFE,后面4个字节才真正表示前一个元素的长度(不能超过zlbytes所表示的最大值)。
-
encoding字段表示当前元素的编码,即entry-data字段存储的数据类型(整数或字节数组),数据存储在entry-data字段。为了节约内存,encoding字段同样长度可变。编码如下表所示:
[!NOTE]
注:Redis为了节省内存并没有存储当前元素的总长度,如果需要从前往后遍历,需要解析encoding再加上prevlen所占字节。
1.3 和Java LinkedList的区别
- ziplist的存储空间是连续的,LinkedList是每个节点单独申请空间,连续的内存空间可以减少内存碎片,以及在遍历的时候更快。
- 因为ziplist的空间是连续的,导致如果在中间的某个节点插入,需要将其之后的节点都移动,类似于ArrayList在中间添加元素。
- LinkedList存储上一个节点时用的是其地址,而ziplist是通过长度确定的,速度上比LinkedList快,但是因为存储了上一个元素的长度,可能会出现上一个元素进行了更新,导致长度发生变化,从而导致当前元素的prevlen也需要改变,当prevlen从1字节变到5字节时,当前元素的总长度就增加了4字节,又会导致下一个元素的prevlen也需要改变…。这个现象被称为连锁更新。
二、listpack
因为ziplist存在连锁更新的问题,所以Redis引入了listpack。
listpack也叫紧凑列表,它的特点就是用一块连续的内存空间来紧凑地保存数据,同时为了节省内存,listpack列表项使用了多种编码方式,来表示不同长度的数据,包括整数和字符串。
listpack的结构如下图
在listpack中没有存储最后一个元素的偏移量,如果想获得最后一个元素,可以用(列表头的位置+总字节数)算出来。
/* Return a pointer to the last element of the listpack, or NULL if the
* listpack has no elements. */
unsigned char *lpLast(unsigned char *lp) {
unsigned char *p = lp+lpGetTotalBytes(lp)-1; /* Seek EOF element. */
return lpPrev(lp,p); /* Will return NULL if EOF is the only element. */
}
为了避免连锁更新的问题,listpack将entry的结构也进行了修改,如下所示:
- entry-encoding中,记录了编码方式以及entry-data的长度
- entry-len中,记录了(entry-coding + entry-data)的总长度
因为entry-len处于entry的末尾,所以我们在p从右往左遍历的时候,可以用p-1拿到entry-len的结束位置,然后拿着这个数通过位运算最终算出entry-len的值,此时就可以做到从右向左遍历。源码如下:
/* If 'p' points to an element of the listpack, calling lpPrev() will return
* the pointer to the previous element (the one on the left), or NULL if 'p'
* already pointed to the first element of the listpack. */
unsigned char *lpPrev(unsigned char *lp, unsigned char *p) {
assert(p);
if (p-lp == LP_HDR_SIZE) return NULL;
p--; /* Seek the first backlen byte of the last element. */
uint64_t prevlen = lpDecodeBacklen(p);
prevlen += lpEncodeBacklen(NULL,prevlen);
p -= prevlen-1; /* Seek the first byte of the previous entry. */
lpAssertValidEntry(lp, lpBytes(lp), p);
return p;
}
参考
https://developer.aliyun.com/article/1252660