8.Redis底层数据结构——ziplist和listpack

一、ziplist

1.1 ziplist结构

Redis采用紧凑的字节数组表示一个压缩列表,压缩列表结构示意图如下:

<zlbytes> <zltail> <zllen> <entry> <entry> ... <entry> <zlend>

img

  • 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字段同样长度可变。编码如下表所示:

    image-20250101145933989

[!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的结构如下图

image-20250102092431445

在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的结构也进行了修改,如下所示:

image-20250102093555628

  • 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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值