《Redis设计与实现》阅读笔记2-数据结构与对象(整数集合,压缩列表)

一 数据结构与对象

5 整数集合

整数集合是集合键的底层实现之一,当一个集合键只包括整数值元素,且数量不多时,Redis就会使用整数集合做为集合的底层实现

5.1 整数集合的实现

typedef struct intset{

    //编码方式
    uint32_t encoding;

    //集合包含的元素数量
    uint32_t length;

    //保存元素的数组
    int8_t contents[];

}intset;
  • contents数组用于保存集合数据,并且按照从小到大的顺序排放,并且数组不包含任何重复项。
  • length记录数据个数,即数组的长度。
  • contents数组输入使用int8_t,但实际的数据类型由encoding来决定。encoding决定底层实现
  • encoding的数据类型,由集合中字节数最大的数来决定,这便使得整数集合有升级。

5.2 升级

当新添加一个数到集合时,数的类型比encoding大时,整数集合需要先进行升级,然后再添加数据。

步骤:

  • 根据新元素类型,拓展整数集合底层数组的空间大小,并为新元素分配空间。
  • 将所有数据转化为新的数据类型,并按顺序放在到正确的位置,一般先移动后面的数,这样移动复杂度为O(N)。
  • 将新元素添加进来
  • 会引起升级操作的数必定小于或大于集合中的所有数,所有引起升级的数必定放在集合开头或末尾。

5.3 升级的好处

整数集合的升级策略有两个好处,一个是提升整数集合的灵活性,另一个是尽可能的节约内存。

5.3.1 提升灵活性

通过集合自动升级底层数组来适应新元素,所以可以将多种类型元素放进去,而不用担心出现类型错误

5.3.2 节约内存

要想保存所有类型的值,最简单的做法就是直接使用int64_t,不过这样就容易出现内存浪费的情况。

5.4 降级

整数集合,暂时不支持降级功能。

6 压缩列表

压缩列表是列表键和哈希键的底层实现之一,当一个列表键只包含少量列表项,并且每个列表项是小整数值,或长度较短的字符串时,那么就会使用压缩列表来做底层实现

6.1 压缩列表的构成

压缩列表是为了节约内存而开发的,由一系列特殊编码的连续内存块组成的顺序性数据结构,一个压缩列表包含任意多个节点,每个节点保存一个字节数组或一个整数值。

压缩列表的组成部分

zlbytes zltail zllen entry1 entry2 entryN zlend

各部分说明:

属性 类型 长度 用途
zlbytes uint32_t 4byte 记录整个压缩列表所占用的字节数,用作内存重分配和计算zlend的位置使用
zltail uint32_t 4byte 记录压缩列表表尾节点距离压缩列表的起始地址有多少字节,通过这个偏移量,无需遍历整个压缩列表就能确定表尾节点的地址。
zllen uint16_t 2byte 记录压缩列表的节点个数,当列表节点数量小于UINT16_MAX(65535)时,这个zllen的值就是节点个数,若大于这个值,你们zllen存放不下,节点的个数小于遍历整个列表
entryX 列表节点 不定 节点长度由节点保存的内容决定
zlend uint8_t 1byte 特殊值0xFF(十进制255),用于标记压缩列表的末尾

6.2压缩列表节点的构成

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

  • 长度小于等于63(2^6-1)字节的字节数组
  • 长度小于等于16383(2^14-1)字节的字节数组
  • 长度小于等于4294967295(2^32-1)字节的字节数组

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

  • 4位长,0-12之间的数
  • 1byte长的有符号数
  • 3byte长的有符号数
  • int16_t类型整数
  • int32_t类型整数
  • int64_t类型整数

每个压缩列表节点由以下三部分组成

previous_entry_length encoding content
6.2.1 previous_entry_length

previous_entry_length属性以字节为单位,可以说1byte,也可以是5byte,记录了压缩列表前一个节点的长度

  • 如果前一个节点的长度小于254byte,那么previous_entry_length属性为1byte,保存前一个节点的具体长度。
  • 如果前一个节点的长度大于等于254byte,那么previous_entry_length属性的长度为5byte,其中previous_entry_length属性的第一个字节被设置为0XFE(十进制254),之后的四个字节保存前一个节点的长度。

压缩列表从表尾向表头遍历,我们只需要拥有一个指向某个节点起始位置的指针,接着凭借previous_entry_length属性就能一直往前遍历。

6.2.2 encoding

encoding属性记录节点中content属性所保存数据的类型及长度,最前方两位用于区分content是自己数组还是整数。

  • 00,01,10开头分别表示encoding的长度为1byte,2byte,5byte长,且content是一个字节数组,除开最前面两位用于标记,后面的剩余位表示content的字节数组的长度。
  • 11开头的表示content是一个整数值,除开前方两位,后面六位用于标记整数值的长度和编码。

字节数组编码

encoding编码 encoding编码长度 content属性的值
00aaaaaa 1byte 长度小于等于63字节的字节数组
01aaaaaa bbbbbbbb 2byte 长度小于等于16386字节的字节数组
10aaaaaa bbbbbbbb cccccccc dddddddd 5byte 长度小于等于4294967295字节的字节数组

整数编码

encoding编码 encoding编码长度 content属性的值
11000000 1byte int16_t类型的整数
11010000 1byte int32_t类型的整数
11100000 1byte int64_t类型的整数
11110000 1byte 24位有符号整数
11111110 1byte 8位有符号整数
1111xxxx(0000-1100) 1byte 并没有对应的content属性,编码本身的xxxx保存的就是值,一个0-12的整数
6.2.3 content

根据encoding确定类型的一个值。

6.3 连锁更新

压缩列表从表尾向表头遍历,利用节点中的previous_entry_length属性

  • 若前一个节点长度小于254byte,那么previous_entry_length长为1byte
  • 若前一个节点长度大于等于254byte,那么previous_entry_length长为5byte
zlbytes zltail zllen entry1 entry2 entryN zlend

现在假设一种情况,entry1到entryN节点长度都介于250byte到253byte之间,则这些节点的previous_entry_length都是1byte。这时一个长度大于等于254byte的新节点new设置到表头。

zlbytes zltail zllen new entry1 entry2 entryN zlend

因为entry1的previous_entry_length属性只有1byte,无法保存new的长度,则需要进行空间重分配,将entry1的previous_entry_length属性变成5字节。
接着entry1的长度也就变得大于等于254byte了,entry2的previous_entry_length属性只有1byte,无法保存entry1,也需要进行空间重分配。如此,后面的的节点全部需要进行内存重分配。

Redis将这种特殊情况下产生的连续多次内存重分配称为“连锁更新”。而类似添加新节点会产生连锁更新,删除节点时,也同样可能引起连续性的previous_entry_length属性的大小需要从5byte变成1byte的情况,这也是连锁更新。

一次空间重分配的复杂度最坏可以达到O(N),所以连锁更新的复杂度最坏可以达到O(N^2),但尽管连锁更新的复杂度很高,但真正造成性能问题的概率很小

  • 一方面,压缩列表要恰好有多个连续,长度在特点范围内的节点,连锁更新才能被触发,实际中,这样的情况并不多见
  • 另一方面,及时出现连锁更新,若连锁更新的节点不多,就不会对性能造成任何影响。

猜你喜欢

转载自blog.csdn.net/maniacxx/article/details/82053081