• 「Redis数据结构」压缩列表(ZipList)


    「Redis数据结构」压缩列表(ZipList)

    ZipList 是一种特殊的“双端链表” ,由一系列特殊编码的连续内存块组成。可以在任意一端进行压入/弹出操作, 并且该操作的时间复杂度为 O(1)。

    一、概述

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

    但是,压缩列表的缺陷也是有的:

    • 不能保存过多的元素,否则查询效率就会降低;
    • 新增或修改某个元素时,压缩列表占用的内存空间需要重新分配,甚至可能引发连锁更新的问题。

    因此,Redis 对象(List 对象、Hash 对象、Zset 对象)包含的元素数量较少,或者元素值不大的情况才会使用压缩列表作为底层数据结构。


    二、结构

    压缩列表是 Redis 为了节约内存而开发的,它是由连续内存块组成的顺序型数据结构,有点类似于数组。

    img

    压缩列表在表头有三个字段:

    • zlbytes,记录整个压缩列表占用对内存字节数;
    • zltail,记录压缩列表「尾部」节点距离起始地址由多少字节,也就是列表尾的偏移量;
    • zllen,记录压缩列表包含的节点数量;
    • zlend,标记压缩列表的结束点,固定值 0xFF(十进制255)。

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

    另外,压缩列表节点(entry)的构成如下:

    img

    压缩列表节点包含三部分内容:

    • prevlen,记录了「前一个节点」的长度,目的是为了实现从后向前遍历;
    • encoding,记录了当前节点实际数据的「类型和长度」,类型主要有两种:字符串和整数。
    • data,记录了当前节点的实际数据,类型和长度都由 encoding 决定;

    当我们往压缩列表中插入数据时,压缩列表就会根据数据类型是字符串还是整数,以及数据的大小,会使用不同空间大小的 prevlen 和 encoding 这两个元素里保存的信息,这种根据数据大小和类型进行不同的空间大小分配的设计思想,正是 Redis 为了节省内存而采用的

    分别说下,prevlen 和 encoding 是如何根据数据的大小和类型来进行不同的空间大小分配。

    压缩列表里的每个节点中的 prevlen 属性都记录了「前一个节点的长度」,而且 prevlen 属性的空间大小跟前一个节点长度值有关,比如:

    • 如果前一个节点的长度小于 254 字节,那么 prevlen 属性需要用 1 字节的空间来保存这个长度值;
    • 如果前一个节点的长度大于等于 254 字节,那么 prevlen 属性需要用 5 字节的空间来保存这个长度值;

    encoding 属性的空间大小跟数据是字符串还是整数,以及字符串的长度有关,如下图(下图中的 content 表示的是实际数据,即本文的 data 字段):

    img

    • 如果当前节点的数据是整数,则 encoding 会使用 1 字节的空间进行编码,也就是 encoding 长度为 1 字节。通过 encoding 确认了整数类型,就可以确认整数数据的实际大小了,比如如果 encoding 编码确认了数据是 int16 整数,那么 data 的长度就是 int16 的大小。
    • 如果当前节点的数据是字符串,根据字符串的长度大小,encoding 会使用 1 字节/2字节/5字节的空间进行编码,encoding 编码的前两个 bit 表示数据的类型,后续的其他 bit 标识字符串数据的实际长度,即 data 的长度。

    三、连锁更新问题

    ZipList的每个Entry都包含previous_entry_length来记录上一个节点的大小,长度是1个或5个字节:

    • 如果前一节点的长度小于254字节,则采用1个字节来保存这个长度值
    • 如果前一节点的长度大于等于254字节,则采用5个字节来保存这个长度值,第一个字节为0xfe,后四个字节才是真实长度数据

    现在,假设我们有N个连续的、长度为250~253字节之间的entry,因此entry的previous_entry_length属性用1个字节即可表示,如图所示:

    1653986328124

    ZipList这种特殊情况下产生的连续多次空间扩展操作称之为连锁更新(Cascade Update)。新增、删除都可能导致连锁更新的发生。


    四、压缩列表的缺陷

    空间扩展操作也就是重新分配内存,因此连锁更新一旦发生,就会导致压缩列表占用的内存空间要多次重新分配,这就会直接影响到压缩列表的访问性能

    所以说,虽然压缩列表紧凑型的内存布局能节省内存开销,但是如果保存的元素数量增加了,或是元素变大了,会导致内存重新分配,最糟糕的是会有「连锁更新」的问题

    因此,压缩列表只会用于保存的节点数量不多的场景,只要节点数量足够小,即使发生连锁更新,也是能接受的。

    虽说如此,Redis 针对压缩列表在设计上的不足,在后来的版本中,新增设计了两种数据结构:quicklist(Redis 3.2 引入) 和 listpack(Redis 5.0 引入)。这两种数据结构的设计目标,就是尽可能地保持压缩列表节省内存的优势,同时解决压缩列表的「连锁更新」的问题。


    五、小结

    ZipList特性:

    • 压缩列表的可以看做一种连续内存空间的"双向链表"
    • 列表的节点之间不是通过指针连接,而是记录上一节点和本节点长度来寻址,内存占用较低
    • 如果列表数据过多,导致链表过长,可能影响查询性能
    • 增或删较大数据时有可能发生连续更新问题

    参考

    黑马程序员

    小林coding

  • 相关阅读:
    基于Linux的无界面网盘 项目
    数电第一次实验
    第9章 读写文件
    接口测试和性能测试的区别
    砖家预测:腾讯云双11服务器优惠价格表(新鲜出炉)
    数据可视化在智慧医疗中的重要应用
    H3C 7506X版本tftp升级
    MSE和Video标签的关系
    神经网络训练集准确率低,神经网络训练样本个数
    使用ffmpeg将视频转成HLS(m3u8)格式
  • 原文地址:https://blog.csdn.net/u014571143/article/details/127932092