redis---压缩列表

白昼怎懂夜的黑 提交于 2019-12-18 09:09:10

压缩列表是列表键哈希键的底层实现之一。当一个列表键只包含少量列表项,并且每个列表项要么就是小整数值,要么就是长度比较短的字符串,那么redis就会使用压缩列表来做列表键的底层实现。 另外,当一个哈希键只包含少量键值对,并且每个键值对的键和值要么就是小整数值,要么就是长度比较短的字符串,那么redis就会使用压缩列表来做哈希键的底层实现,例如:

# 后续更新将某些情况改造成了quicklist快速列表
127.0.0.1:6379> rpush lst 1 3 5 10086 "hello" "world"
(integer) 6
127.0.0.1:6379> OBJECT ENCODING lst
"quicklist"
# zip;ist压缩列表
127.0.0.1:6379> HMSET profile "name" "Jack" "age" 28 "job" "Programmer"
OK
127.0.0.1:6379> OBJECT ENCODING profile
"ziplist"

1. 压缩列表的构成

压缩列表是由一些列特殊编码的连续内存块组成的顺序型数据结构,一个压缩列表可包含任意多个节点(entry)每个节点可以保存一个字节数组或者一个整数值。下面两个图分别展示了压缩列表的组成部分和各部分的类型、长度及用途:
在这里插入图片描述
在这里插入图片描述

2. 压缩列表节点的构成

每个压缩列表节点可以保存一个字节数组或者一个整数值,其中字节数组可以是以下三种长度之一:

  • 长度小于等于63(26-1)字节的字节数组
  • 长度小于等于16383(214-1)字节的字节数组
  • 长度小于等于4294967295(232-1)字节的字节数组

而整数值可以是以下六种长度之一:

  • 4位长,介于0到12之间的无符号整数
  • 1字节长的有符号整数
  • 3字节长的有符号整数
  • int15_t类型整数
  • int32_t类型整数
  • int64_t类型整数

每个压缩列表节点都由previous_entry_length、encoding、content三个部分组成,如下图所示,我们分别介绍、
在这里插入图片描述

2.1 previous_entry_length

节点的previous_entry_length属性以字节为单位,记录了压缩列表中前一个节点的长度。previous_entry_length属性的长度可以是1字节或者5字节:

  • 如果前一节点的长度小于254字节,那么previous_entry_length属性的长度为1字节:前一节点的长度就保存在这一个字节里面。
  • 如果前一节点的长度大于等于254字节,那么previous_entry_length属性的长度为5字节:其中属性的第一字节会被设置为0xFE(十进制254),而之后的四个字节则用于保存前一节点的长度

因为节点的previous_entry_length属性记录了前一个节点的长度,所以程序可以通过指针运算,根据当前节点的起始地址来计算出前一个节点的起始地址。而压缩列表的从表尾向表头遍历操作就是使用这一原理实现的,只要我们拥有了一个指向某个节点起始地址的指针,那么通过这个指针以及这个节点的previous_entry_length属性,程序就可以一直向前一个节点回溯,最终到达压缩列表的表头节点。例如下面这个例子:

  • 首先,我们拥有指向压缩列表表尾节点entry4起始地址的指针p1(指向表尾节点的指针可以通过指向压缩列表起始地址的指针加上zltail属性的值得出);
  • 通过用p1减去entry4节点previous_entry_length属性的值,我们得到一个指向entry4前一节点entry3起始地址的指针p2;
  • 通过使用p2减去entry3节点previous_entry_length属性的值,我们得到一个指向entry3前一节点entry2起始地址的指针p3;
  • 通过使用p3减去entry2节点previous_entry_length属性的值,我们得到一个指向entry2前一节点entry1起始地址的指针p4,entry1为压缩列表的表头节点;
  • 最终,我们从表尾节点向表头节点遍历了整个列表。

在这里插入图片描述

2.2 encoding

节点的encoding属性记录了节点的content属性所保存数据的类型长度

  • 一字节、两字节或者五字节长,值的最高位为00、01或者10的是字节数组编码:这种编码表示节点的content属性保存着字节数组,数组的长度由编码去除最高两位之后的其他位记录
  • 一字节长,值得最高位以11开头的是整数编码:这种编码表示节点的content属性保存着整数值,整数值的类型和长度由编码除去最高位之后的其他位记录。

两张表分别记录了一个节点存储字节数组和整数值的情况,其中下划线表示留空不存储信息。

编码 编码长度 content属性保存的值
00bbbbbb 1字节 长度小于等于63(26-1)字节的字节数组
01bbbbbb xxxxxxxx 2字节 长度小于等于16383(214-1)字节的字节数组
00_ _ _ _ _ _ aaaaaaaaa bbbbbbbb cccccccc dddddddd 5字节 长度小于等于(232-1)字节的字节数组
编码 编码长度 content属性保存的值
11000000 1字节 int16_t类型的整数
11010000 1字节 int32_t类型的整数
11100000 1字节 int64_t类型的整数
11110000 1字节 24位有符号整数
11111110 1字节 8位有符号整数
1111xxxx 1字节 使用此编码表示没有相应的content属性,因为编码本身的后四位xxx已经保存了一个0~12的值
2.3 content

节点的content属性负责保存节点的值,节点值可以是一个字节数组或者整数,值的类型和长度由节点的encoding属性决定。例如下面的例子,其中:

  • 编码的最高两位00表示节点保存的是一个字节数组;
  • 编码的后六位001011记录了字节数组的长度11;
  • content属性保存着节点的值"hello world";
    在这里插入图片描述
    再看一个保存整数值的节点,其中:
  • 编码11000000表示节点保存的是一个int16_t类型的整数值
  • content属性保存着节点的值10086
    在这里插入图片描述

3. 连锁更新

前面说过,每个节点的previous_entry_length属性都记录着前一个节点的长度,且前一节点长度是否大于254字节决定了本节点此属性占位情况。而这个属性previous_entry_length也是导致连锁更新的原因

现在假设这种情况:一个压缩列表中所有节点e1至eN的长度都小于254字节,那么previous_entry_length的属性均占用1字节。这时我们将一个长度大于等于254字节的新节点new设置为压缩列表的表头节点,那么因为new后面节点e1的previous_entry_length属性仅为1字节,没办法保存新节点new的长度,所以需要将压缩列表的空间重新分配,并将e1节点的previous_entry_length属性从原来的1字节扩展为5字节。而由于它扩展了以后e1的长度大于254,导致e2也进行同样的操作,依次类推后面所有节点都要进行此操作。

同样进行删除节点也可能出现与此相反的一系列操作,即不断压缩每个节点的previous_entry_length从5字节变为1字节。最坏情况下可能会对压缩列表进行N次空间重分配,而每次空间重分配的最坏时间复杂度也为O(N),所以连锁更新的最坏复杂度为O(N2)

当然出现这种情况的概率是较低的,我们也不用过多的担心。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!