[Redis]压缩列表详解

Redis 为了节约内存空间使用,zsethash 容器对象在元素个数较少的时候,采用压缩列表(ziplist)进行存储。压缩列表是一块连续的内存空间,元素之间紧挨着存储,没有任何冗余空隙。

>zadd programmings 1.0 go 2.0 python 3.0 java
(integer)3
>debug object programmings
Value at:0x7fec2de00020 refcount:1 encoding:ziplist serializedlength:36 lru:6022374 lru_seconds_idle:6
>hmset books go fast python slow java fast
OK
>debug object books
Value at:0x7fec2de000c0 refcount:1 encoding:ziplist serializedlength:48 lru:6022478 lru_seconds_idle:1

这里,注意观察 debug object 输出的 encoding 字段都是 ziplist,这就表示内部采用压缩列表结构进行存储。
图 5-6 所示是压缩列表的内部结构示意图。

image

struct ziplist<T>
{
	int32 zlbytes;	//整个压缩列表占用字节数
	int32 zltail_offset;//最后一个元素距离压缩列表起始位置的偏移量,用于快速定位到最后一个节点
	int16 zllength; // 元素个数
	T[] entries;  //元素内容列表,依次紧凑存储
	int8 zlend; //标志压缩列表的结束,值恒为0xFE
}

压缩列表为了支持双向遍历,所以才会有 ztail_offset 这个字段,用来快速定位到最后一个元素,然后倒着遍历

entry块随着容纳的元素类型不同,也会有不一样的结构。

struct entry
{
	int<var> prevlen;//前一个 entry 的字节长度
	int<var> encoding;//元素类型编码
	optional byte[] content;//元素内容
}

如图 5-7 所示是压缩列表内部具体元素的结构示意图。它的 prevlen 字段表示前一个 entry 的字节长度,当压缩列表倒着遍历时,需要通过这个字段来快速定位到下一个元素的位置。它是一个变长的整数,当字符串长度小于254(即0xFE)时,使用1个字节表示;如果达到或超出254 时,就使用5个字节来表示。第一个字节是0xFE,剩余四个字节表示字符串长度。你可能会觉得,用5个字节来表示字符串长度是不是太浪费了,我们可以算一下,当字符串长度比较长的时候,其实5个字节也只占用了不到 2% 的空间。

len_size = 5 #存储字符串长度占用字节数
str_size >= 254 #存储字符串内容占用字节数
#字符串长度存储占用的空间百分比
len_size_ratio = lensize/(lensize+strsize) <= 5/(5+254)=1.93%

image

encoding 字段存储了元素内容的编码类型信息 ziplist 通过这个字段来决定后面content 的形式。

Redis 为了节约存储空间 ,对 encoding 字段进行了相当复杂的设计。 Redis 通过这个字段的前缀位来识别具体存储的数据形式。下面我们来看看 Redis 是如何根据 encoding 的前缀位来区分内容的。

  1. 00xxxxxx 是最大长度位数为 63 的短字符串,后面的 6 个位存储字符串的位数,剩余的字节就是字符串的内容。
  2. 01xxxxxx xxxxxxxx 是中等长度的字符串 后面 14 个位来表示字符串的长度,剩余的字节就是字符串的内容。
  3. 10000000 aaaaaaaa bbbbbbbb cccccccc dddddddd 是特大字符串,需要使用额外4个字节来表示长度。第-个字节前缀是 10 ,剩余 位没有使用,统一置为零。后面跟着字符串内容。不过这样的大字符串是没有机会使用的,压缩列表通常只是用来存储小数据的。
  4. 11000000 表示 int16 ,后跟两个字节表示整数
  5. 11010000 表示 int32 ,后跟四个字节表示整数
  6. 11100000 表示 int64 ,后跟八个字节表示整数。
  7. 11110000 表示 int24 ,后跟三个字节表示整数
  8. 11111110 表示 int8 ,后跟一个字节表示整数
  9. 11111111 表示 ziplist 的结束,也就是 zlend 的值 OxFF
  10. 1111 xxxx 表示极小整数, xxxx 的范围只能是( 0001~1101) ,也就是 1-13,因为 0000 1110 1111 都被占用了。读取到的 value 需要将 xxxx 减一 ,也就是说整0-12 就是最终的 value

注意 content 字段在结构体中定义为 optional 类型,表示这个字段是可选的,对于很小的整数而言,它的内容已经内联到 encoding 字段的尾部了。

增加元素#

因为 ziplist 都是紧凑存储,没有冗余空间(对比一下 Redis 的字符串结构),意味着插入一个新的元素就需要调用 realloc 扩展内存。取决于内存分配器算法和当前的ziplist 内存大小,realloc 可能会重新分配新的内存空间,并将之前的内容一次性拷贝到新的地址,也有可能在原有的地址上进行扩展,这时就不需要进行旧内容的内存拷贝。如果 ziplist 占据内存太大,重新分配内存和拷贝内存就会有很大的消耗,所以ziplist 不适合存储大型字符串,存储的元素也不宜过多。

级联更新#

/* 压缩列表级联主新*
unsigned char *一 ziplistCascadeUpdate(unsigned char *zl, unsigned 
char *p) { 
size t curlen =工 trev32ifbe (ZIPLIST BYTES (zl)), rawlen, 
rawlensize; 
S 工 ze t offset, noffset, extra; 
uns gned char *np;

zlentry cur, next ; 
while (p[O] != ZIP END) { 
zipEntry(p, &cur); 
rawlen = cur.headers ze + cur.len; 
rawlensize = zipStorePrevEntryLength(NULL,rawlen) ; 
/* Abort if there i 口o next entry . */ 
if (p[rawlen] == ZIP END ) break; 
zipEntry(p+rawlen , &next) ; 
/* Abort when ” prevlen ” has not changed 女/
II prevlen 的长度没有交,中断级联更新
if (口 ext.prevrawlen == rawlen) break; 
(next.prevrawlensize < rawlensize) { 
I* The “ prevlen” field of “ next” needs more bytes to 
the raw length of cur ”. 女/
//级联扩展
offset = p - zl ; 
extra = rawlensize -口 ext prevrawlensize
//扩大内存
zl = ziplistResize(zl,curlen+extra); 
p = zl+offset; 
I* Current pointer and offset for next element. *I 
np = p+rawlen ; 
noffset =口p-zl
I* Update tail offset when next element is not the 
tail element. *I 
//史新 zltail offset 指针
i f ( (zl+intrev32ifbe (ZIPLIST TAIL OFFSET (zl))) != np) { 
ZIPLIST TAIL OFFSET(zl ) = 
intrev32ifbe(intrev32ifbe(ZIPLIST TAIL 
OFFSET(zl))+extra); 
I* Move the tail to the back .女/
//移动内存
memmove(np+rawlens ze
np+next . prevrawlensize , 
curlen-noffset-next.prevrawlensize- 1) ; 
zipStorePrevEntryLength(np,rawlen);

/*Advance the cursor*/
p += rawlen;
curlen +=extra
}else
if(next.prevrawlensize>rawlensize){/*This would result in shrinking,which we want
to avoid.*So,set "rawlen" in the available bytes.*///级联收缩,不过这里可以不用收缩了,因为5个字节也是可以存储 1 个字节的内容的
//虽然有点浪费,但是级联更新实在是太可怕了,所以浪费就浪费
zipStorePrevEntryLengthLarge(p+rawlen,rawlen);}else {
//大小没变,改个长度值就完事了
zipStorePrevEntryLength(p+rawlen,rawlen);
/央Stop here,as the raw length of“next”has not
changed.*/
break;
return zl;
}

前面提到每个 entry 都会有一个 prevlen 字段存储前一个 entry 的长度。如果内容小于 254 字节, prevlen 就用一个字节存储,否则就用五个字节存储。这意昧着如果某个entry 经过了修改操作从 253 字节变成了254字节,那么它的下一个entry的prevlen 字段就要更新,从一个字节扩展到5个字节;如果后面这个 entry 的长度本来也是 253 字节,那么再后面entry prevlen 字段还得继续更新。
如果 ziplist 里面的每个 entry 恰好都存储了 253 字节的内容,那么第1个 entry内容的修改就会导致后续所有 entry的级联更新,这就是1个比较耗费计算资源的操作。

删除中间的某个节点也可能会导致级联更新,读者可以思考一下为什么?

作者:Esofar

出处:https://www.cnblogs.com/DCFV/p/18300481

版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。

posted @   Duancf  阅读(52)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
more_horiz
keyboard_arrow_up dark_mode palette
选择主题
menu
点击右上角即可分享
微信分享提示