• Redis压缩列表详解


     

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

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

    • 不能保存过多的元素,否则查询效率就会降低;

    • 新增或修改某个元素时,压缩列表占用的内存空间需要重新分配,甚至可能引发连锁更新的问题。

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

    接下来,就跟大家详细聊下压缩列表。

    压缩列表结构设计

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

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

    • zlbytes,记录整个压缩列表占用对内存字节数;

    • zltail,记录压缩列表「尾部」节点距离起始地址由多少字节,也就是列表尾的偏移量;

    • zllen,记录压缩列表包含的节点数量;

    • zlend,标记压缩列表的结束点,固定值 0xFF(十进制255)。

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

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

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

    • prevlen,记录了「前一个节点」的长度;

    • encoding,记录了当前节点实际数据的类型以及长度;

    • data,记录了当前节点的实际数据;

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

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

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

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

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

    encoding 属性的空间大小跟数据是字符串还是整数,以及字符串的长度有关:

    • 如果当前节点的数据是整数,则 encoding 会使用 1 字节的空间进行编码。

    • 如果当前节点的数据是字符串,根据字符串的长度大小,encoding 会使用 1 字节/2字节/5字节的空间进行编码。

    连锁更新

    压缩列表除了查找复杂度高的问题,还有一个问题。

    压缩列表新增某个元素或修改某个元素时,如果空间不不够,压缩列表占用的内存空间就需要重新分配。而当新插入的元素较大时,可能会导致后续元素的 prevlen 占用空间都发生变化,从而引起「连锁更新」问题,导致每个元素的空间都要重新分配,造成访问压缩列表性能的下降。

    前面提到,压缩列表节点的 prevlen 属性会根据前一个节点的长度进行不同的空间大小分配:

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

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

    现在假设一个压缩列表中有多个连续的、长度在 250~253 之间的节点,如下图:

    因为这些节点长度值小于 254 字节,所以 prevlen 属性需要用 1 字节的空间来保存这个长度值。

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

    因为 e1 节点的 prevlen 属性只有 1 个字节大小,无法保存新节点的长度,此时就需要对压缩列表的空间重分配操作,并将 e1 节点的 prevlen 属性从原来的 1 字节大小扩展为 5 字节大小。

    多米诺牌的效应就此开始。

    e1 原本的长度在 250~253 之间,因为刚才的扩展空间,此时 e1 的长度就大于等于 254 了,因此原本 e2 保存 e1 的 prevlen 属性也必须从 1 字节扩展至 5 字节大小。

    正如扩展 e1 引发了对 e2 扩展一样,扩展 e2 也会引发对 e3 的扩展,而扩展 e3 又会引发对 e4 的扩展…. 一直持续到结尾。

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

    压缩列表的缺陷

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

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

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

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

  • 相关阅读:
    创建供应商-采购模块
    定义容差组
    前台创建供应商-财务角度
    对供应商账户组分配编号范围
    拓端数据tecdat|R语言建立和可视化混合效应模型mixed effect model
    拓端数据tecdat|R语言建模收入不平等:分布函数拟合及洛伦兹曲线(Lorenz curve)
    拓端数据tecdat|R语言中的多项式回归、局部回归、核平滑和平滑样条回归模型
    拓端数据tecdat|R语言ARIMA,SARIMA预测道路交通流量时间序列:季节性、周期性
    拓端数据tecdat|ARIMA模型预测CO2浓度时间序列
    拓端数据tecdat|R语言基于递归神经网络RNN的温度时间序列预测
  • 原文地址:https://www.cnblogs.com/myf008/p/16201960.html
Copyright © 2020-2023  润新知