Redis数据结构 — ZipList

目录

压缩列表结构设计

创建ZipList源码

压缩列表节点(entry)结构

encoding编码属性

字符串

整数

连锁更新问题!!!

压缩列表优缺点


ZipList名叫压缩链表,但本质不是用链表实现的

ZipList中所有存储长度的数值均采用小端字节序,即低位字节在前,高位字节在后。例如:数值0x1234,采用小端字节序后实际存储值为:0x3412

压缩列表的最大特点,就是它被设计成一种内存紧凑型的数据结构,占用一块连续的内存空间,不仅可以利用 CPU 缓存,而且会针对不同长度的数据,进行相应编码,这种方法可以有效地节省内存开销。

压缩列表结构设计

压缩列表是 Redis 为了节约内存而开发的,它是由连续内存块组成的顺序型数据结构,有点类似于数组。可以在任意一端进行压入/弹出操作, 并且该操作的时间复杂度为 O(1)。

属性类型长度用途
zlbytesuint32_t4 字节记录整个压缩列表占用的内存字节数
zltailuint32_t4 字节记录压缩列表表尾节点距离压缩列表的起始地址有多少字节,通过这个偏移量,可以确定表尾节点的地址。
zllenuint16_t2 字节记录了压缩列表包含的节点数量。 最大值为UINT16_MAX (65534),如果超过这个值,此处会记录为65535,但节点的真实数量需要遍历整个压缩列表才能计算得出。
entry列表节点不定压缩列表包含的各个节点,节点的长度由节点保存的内容决定。
zlenduint8_t1 字节特殊值 0xFF (十进制 255 ),用于标记压缩列表的末端。

在压缩列表中,如果我们要查找定位第一个元素和最后一个元素,可以通过表头三个字段(zllen)的长度直接定位,复杂度是 O(1)。而查找其他元素时,就没有这么高效了,只能逐个查找,此时的复杂度就是 O(N) 了,因此压缩列表不适合保存过多的元素

创建ZipList源码

unsigned char *ziplistNew(void) {
    //计算空ziplist的长度并且申请内存
    //zlbytes和zltail的类型是32位无符号整数,zllen是16位无符号整数,共10字节
    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;
}

压缩列表节点(entry)结构

ZipList 中的Entry并不像普通链表那样记录前后节点的指针,因为记录两个指针要占用16个字节,浪费内存。而是采用了下面的结构

  • previous_entry_length:记录前一节点的长度,占1个或5个字节。(造成连续更新的原因
    <1> 如果前一节点的长度小于254字节,则采用1个字节来保存这个长度值
    <2> 如果前一节点的长度大于254字节,则采用5个字节来保存这个长度值,第一个字节为0xfe,后四个字节才是真实长度数据
  • encoding:编码属性,记录content的数据类型(字符串还是整数)以及长度,占用1个、2个或5个字节
  •  contents:负责保存节点的数据,可以是字符串或整数

encoding编码属性

ZipListEntry中的encoding编码分为字符串和整数两种:

  • encoding是以“00”、“01”或者“10”开头,则证明content是字符串
  • encoding是以“11”开头,则证明content是整数,且encoding固定只占用1个字节

字符串

编码编码长度字符串大小
|00pppppp|1 bytes<= 63 bytes
|01pppppp|qqqqqqqq|2 bytes<= 16383 bytes
|10000000|qqqqqqqq|rrrrrrrr|ssssssss|tttttttt|5 bytes<= 4294967295 bytes

例如,我们要保存字符串:“ab”和 “bc”,小端字节序存储

整数

编码编码长度整数类型
110000001 bytesint16_t(2 bytes)
110100001 bytesint32_t(4 bytes)
111000001 bytesint64_t(8 bytes)
111100001 bytes24位有符整数(3 bytes)
111111101 bytes8位有符整数(1 bytes)
1111xxxx1 bytes直接在xxxx位置保存数值,范围从0001~1101,减1后结果为实际值

如果当前节点的数据是整数,则 encoding 会使用 1 字节的空间进行编码,也就是 encoding 长度为 1 字节。通过 encoding 确认了整数类型,就可以确认整数数据的实际大小了,比如如果 encoding 编码确认了数据是 int16 整数,那么 data 的长度就是 int16 的大小。

连锁更新问题!!!

ZipList的每个Entry都包含previous_entry_length来记录上节点的大小,长度是1个或5个字节:
如果前一节点的长度小于254字节,则采用1个字节来保存这个长度值
如果前一节点的长度大于等于254字节,则采用5个字节来保存这个长度值,第一个字节为0xfe,后四个字节才是真实长度数据
问题情景:现在,假设我们有N个连续的、长度为250~253字节之间的entry,因此entry的previous_entry_length属性用1个字节即可表示,如图所示:

 这时,如果将一个长度大于等于 254 字节的新节点加入到压缩列表的表头节点,即新节点将成为 e1 的前置节点,如下图:

因为 e1 节点的 prevlen 属性只有 1 个字节大小,无法保存新节点的长度,此时就需要对压缩列表的空间重分配操作,并将 e1 节点的 prevlen 属性从原来的 1 字节大小扩展为 5 字节大小。此后,多米诺牌的效应就此开始。后面的entry中的prevlen记录前一个entry的大小,都将被迫扩容。

这种在特殊情况下产生的连续多次空间扩展操作就叫做【连锁更新】,就像多米诺牌的效应一样,第一张牌倒下了,推动了第二张牌倒下;第二张牌倒下,又推动了第三张牌倒下...., 

压缩列表优缺点

优点:

  • 压缩列表的可以看做一种连续内存空间,用于保存的节点数量不多的场景,节省内存空间,查询性能也能得到保证,即使发生连锁更新,也是能接受的。

缺点:

  • 如果保存的元素数量增加了,或是元素变大了,会导致内存重新分配,最糟糕的是会有连锁更新的问题
  • 数据过多,导致链表过长,可能影响查询性能
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值