《Reids 设计与实现》第四章 整数集合和压缩列表

文章目录

  • 《Reids 设计与实现》第四章 整数集合和压缩列表
  • 一、整数集合
    • 1.简介
    • 2.整数集合的实现
    • 3.升级
    • 4.升级的好处
    • 5.降级
    • 6.整数集合 API
    • 7.重点回顾
  • 二、压缩列表
    • 1.简介
    • 2.压缩列表的构成
    • 3.压缩列表节点的构成
    • 4.连锁更新
    • 5.压缩列表 API
    • 6.重点回顾

一、整数集合

1.简介

整数集合(intset)是集合键的底层实现之一,当一个集合只包含整数值元素,并且这个集合的元素数量不多时,Redis 就会使用整数集合作为集合键的底层实现

举个例子,如果我们创建一个只包含五个元素的集合键,并且集合中的所有元素都是整数值,那么这个集合键的底层实现就会是整数集合

2.整数集合的实现

整数集合是 Redis 用于保存整数值的集合抽象数据结构,它可以保存类型为 int16_t、int32_t 或者 int64_t 的整数值,并且保证集合中不会出现重复元素

每个 intset 结构表示一个整数集合:

typedef struct intset{//编码方式uint32_t encoding;//集合包含的元素数量uint32_t length;//保存元素的数组int8_t contents[];
}intset;

contents 数组是整数集合的底层实现:整数集合的每个元素都是 contents 数组的一个数组项(item),各个项在数组中按值的大小从小到大地排列,并且数组中不包含任何重复项

length 属性记录了整数集合包含的元素数量,也即是 contents 数组的长度

虽然 intset 结构将 contents 属性声明为 int8_t 类型的数组,但实际上 contents 数组并没有保存任何 int8_t 类型的值,contents 数组的真正类型取决于 encoding 属性的值

图 6-2 展示了一个整数集合示例:

  • encoding 属性的值为 INTSET_ENC_INT64,表示整数集合的底层实现为 int64_t 类型的数组,而数组中保存的都是 int64_t 类型的整数值
  • length 属性的值为 4,表示整数集合包含四个元素
  • contents 数组按从小到大的顺序保存着集合中的四个元素
  • 因为每个集合元素都是 int64_t 类型的整数值,所以 contents 数组的大小为 sizeof(int64_t) * $ = 64 * 4 = 256 位

虽然 contetns 数组保存的四个整数值中,只有 -2675256175807981027 是真正需要用 int64_t 类型来保存的,而其它的 1、3、5 三个值都可以用 int16_t 类型来保存,不过根据整数集合的升级规则,当向一个底层为 int16_t 数组的整数集合添加一个 int64_t 类型的整数值时,整数集合已有的所有元素都会被转换成 int64_t 类型,所以 contents 数组保存的四个整数值都是 int64_t 类型的

3.升级

每当我们要将一个新元素添加到整数集合里面,并且新元素的类型比整数集合现有所有元素的类型都要长时,整数集合需要先进行升级(upgrade),然后才能将新元素添加到整数集合里面

升级整数集合并添加新元素共分为三步进行:

  1. 根据新元素的类型,扩展整数集合底层数组的空间大小,并为新元素分配空间
  2. 将底层数组现有的所有元素都转换成与新元素相同的类型,并将类型转换后的元素放置到正确的位上,而且在放置元素的过程中,需要继续维持底层数组的有序性质不变
  3. 将新元素添加到底层数组里面

举个例子,假设现在有一个 INTSET_ENC_INT16 编码的整数集合,集合中包含三个 int16_t 类型的元素

因为每个元素都占用 16 位空间,所以整数集合底层数组的大小位 3 * 16 = 48 位,图 6-4 展示了整数集合的三个元素在这 48 位里的位置

现在,假设我们要将类型为 int32_t 的整数值 65535 添加到整数集合里面,因为 65535 的类型 int32_t 比整数集合当前所有元素的类型都要长,所以在将 65535 添加到整数集合之前,程序需要先对整数集合进行升级

升级首先要做的是,根据新类型的长度,以及集合元素的数量(包括要添加的新元素在内),对底层数组进行空间重分配

整数集合目前有三个元素,再加上新元素 65535,整数集合需要分配四个元素的空间,因为每个 int32_t 整数值需要占用 32 为空间,所以在空间重分配之后,底层数组的大小将是 32 * 4 = 128 位,如图 6-5 所示。虽然程序对底层数组进行了空间重分配,但数组原有的三个元素 1、2、3 仍然是 int16_t 类型,这些元素还保存在数组的前 48 位里面,所以程序接下来要做的就是将这三个元素转换成 int32_t 类型,并将转换后的元素放置到正确的位上面,而且在放置元素的过程中,需要维持底层数组的有序性性质不变

首先,因为元素 3 在 1、2、3、65535 四个元素中排名第三,所以它将被移动到 contents 数组的索引 2 位置上,也即是数组 64 位至 95 位的空间内,如图 6-6 所示

接着,因为元素 2 在 1、2、3、65535 四个元素中排名第二,所以它将被移动到 contents 数组的索引 1 位置上,也即是数组的 32 位至 63 位的空间内,如图 6-7 所示

之后,因为元素 1 在 1、2、3、65535 四个元素中排名第一,所以它将被移动到 contents 数组的索引 0 位置上,即数组的 0 位至 31 位的空间内,如图 6-8 所示

然后,因为元素 65535 在 1、2、3、65535 四个元素中排名第四,所以它将被添加到 contetns 数组的索引 3 位置上,也即是数组的 96 位至 127 位的空间内,如图 6-9 所示

最后,程序将整数集合 encoding 属性的值从 INSERT_ENC_INT16 改为 INTSET_ENC_INT32,并将 length 属性的值从 3 改为 4

因为每次向整数集合添加新元素都可能会引发升级,而每次升级都需要对底层数组中已有的所有元素进行类型转换,所以向整数集合添加新元素的时间复杂度为 O(N)

因为引发升级的新元素的长度总是比整数集合现有所有元素的长度都大,所以当这个新元素的值为正时就大于所有现有元素,为负时就小于所有现有元素:

  • 为负时,新元素会被放置在底层数组的最开头(索引 0)
  • 为正时,新元素会被放置在底层数组的最末尾(索引 length - 1)

4.升级的好处

提升灵活性

因为 C 语言是静态类型语言,为了避免类型错误,我们通常不会将两种不同类型的值放在同一个数据结构里面。但是,因为整数集合可以通过自动升级底层数组来适应新元素,所以我们可以随意地将 int16_t、int32_t 或者 int64_t 类型的整数添加到集合中,而不必担心出现类型错误,这种做法非常灵活

节约内存

当然,要让一个数组可以同时保存 int16_t、int32_t、int64_t 三种类型的值,最简单的做法就是直接使用 int64_t 类型的数组作为整数整数集合的底层实现。不过这样依赖,即使添加到整数集合里面的都是 int16_t 类型或者 int32_t 类型的值,数组都需要使用 int64_t 类型的空间去保存它们,从而出现浪费内存的情况

而整数集合现在的做法既可以让集合能同时保存三种不同类型的值,又可以确保升级操作只会在有需要的时候进行,这可以尽量节省内存

5.降级

整数集合不支持降级操作,一旦对数组进行了升级,编码就会一直保持升级后的状态

6.整数集合 API

函数 作用 时间复杂度
intsetNew 创建一个新的整数集合 O(1)
intsetAdd 将给定元素添加到整数集合里面 O(N)
intsetRemove 从整数集合中移除给定元素 O(N)
intsetFind 检查给定值是否存在于集合 因为底层数组有序,查找可以通过二分查找法来进行,所以复杂度为 O(logN)
intsetGet 取出底层数组在给定索引上的元素 O(1)
intsetLen 返回整数集合包含的元素个数 O(1)
intsetBlobLen 返回整数集合占用的内存字节数 O(1)

7.重点回顾

  • 整数集合是集合键的底层实现之一
  • 整数集合的底层实现为数组,这个数组以有序,无重复的方式保存集合元素,在有需要时,程序会根据新添加元素的类型,改变这个数组的类型
  • 升级操作为整数集合带来了操作上的灵活性,并且尽可能地节约了内存
  • 整数集合只支持升级操作,不支持降级操作

二、压缩列表

1.简介

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

另外,当一个哈希键只包含少量键值对,并且每个键值对的键和值要么就是小整数值,要么就是长度比较短的字符串,那么 Redis 就会使用压缩列表来做哈希键的底层实现。哈希键里面包含的所有键和值都是小整数值或者短字符串

2.压缩列表的构成

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

图 7-1 展示了压缩列表的各个组成部分

压缩列表各个组成部分的详细说明:

属性 类型 长度 用途
zlbytes uint32_t 4 字节 记录整个压缩列表占用的内存字节数:在对压缩列表进行内存重分配,或者计算 zlend 的位置时使用
zltail uint32_t 4 字节 记录压缩列表表尾节点距离压缩列表的起始地址有多少个字节:通过这个偏移量,程序无须遍历整个压缩列表就可以确定表尾节点的地址
zllen uint16_t 2 字节 记录了压缩列表包含的节点数量:当这个属性的值小于 UINT16_MAX(65515)时,这个属性的值就是压缩列表包含节点的数量;当这个值等于 UINT16_MAX 时,节点的真实数量需要遍历整个压缩列表才能计算得出
entryX 列表节点 不定 压缩列表包含的各个节点,节点的长度由节点保存的内容决定
zlend uint8_t 1 字节 特殊值 0xFF(十进制 255),用于标记压缩列表的末端

图 7-3 展示了一个压缩列表示例:

  • 列表 zlbytes 属性的值为 0xd2(十进制 210),表示压缩列表的总长为 210 字节
  • 列表 zltail 属性的值为 0xb3(十进制 179),这表示如果我们有一个指向压缩列表起始地址的指针 p,那么只要用指针 p 加上偏移量 179,就可以计算出表尾节点 entry5 的地址
  • 列表 zllen 属性的值为 0x5(十进制 5),表示压缩列表包含五个节点

3.压缩列表节点的构成

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

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

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

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

每个压缩列表节点都由 previous_entry_length、encoding、content 三个部分组成,如图 7-4 所示

previous_entry_length

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

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

图 7-6 展示了一个包含五字节长 previous_entry_length 属性的压缩节点,属性的值为 0xFE00002766,其中值的最高位字节 0xFE 表示这是一个五字节长的 previous_entry_length 属性,而之后的四字节 0x00002766(十进制值 10086)才是前一节点的实际长度

因为节点的 previous_entry_length 属性记录了前一个节点的长度,所以程序可以通过指针运算,根据当前节点的起始地址来计算出前一个节点的起始地址

压缩列表的从表尾向表头遍历操作就是使用这一原理实现的,只要我们拥有了一个指向某个节点起始地址的指针,那么通过这个指针以及这个节点的 previous_entry_length 属性,程序就可以一直向前一个结点回溯,最终到达压缩列表的表头节点

encoding

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

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

下面第一张表记录了所有可用的字节数组编码,而第二张表则记录了所有可用的整数编码。表格中的下划线 “_” 表示留空,而 b、x 等变量则代表实际的二进制数据,为了方便阅读,多个字节之间用空格隔开

字节数组编码:

编码 编码长度 content 属性保存的值
00bbbbbb 1 字节 长度小于等于 63 字节的字节数组
01bbbbbb xxxxxxxx 2 字节 长度小于等于 16383 字节的字节数组
10_ _ _ _ _ _ aaaaaaaa bbbbbbbb cccccccc dddddddd 5 字节 长度小于等于 4294967295 的字节数组

整数编码:

编码 编码长度 content 属性保存的值
11000000 1 字节 int16_t 类型的整数
11010000 1 字节 int32_t 类型的整数
11100000 1 字节 int64_t 类型的整数
11110000 1 字节 24 位有符号整数
11111110 1 字节 8 位有符号整数
1111xxxx 1 字节 使用这一编码的节点没有相应的 content 属性,因为编码本身的 xxxx 四个位已经保存了一个介于 0 和 12 之间的值,所以它无须 content 属性

content

节点的 content 属性负责保存节点的值,节点值可以是一个字节数组或者整数,值的类型和长度由节点的 encoding 属性决定

图 7-9 展示了一个保存字节数组的节点示例:

  • 编码的最高两位 00 表示节点保存的是一个字节数组
  • 编码的后六位 001011 记录了字节数组的长度 11
  • content 属性保存着节点的值 “hello world”

图 7-10 展示了一个保存着整数值的节点示例:

  • 编码 11000000 表示节点保存的是一个 int16_t 类型的整数值
  • content 属性保存着节点的值10086

4.连锁更新

前面说过,每个节点的 previous_entry_length 属性都记录了前一个节点的长度:

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

现在,考虑这样一种情况:在一个压缩列表中,有多个连续的、长度介于 250 字节到 253 字节之间的节点 e1 至 eN,如图 7-11 所示

因为 e1 至 eN 的所有节点的长度都小于 254 字节,所以记录这些节点的长度只需要 1 字节长的 previous_entry_length 属性,换句话说,e1 至 eN 的所有节点的 previous_entry_length 属性都是 1 字节长的

这时,如果我们将一个长度大于等于 254 字节的新节点 new 设置为压缩列表的表头节点,那么 new 将成为 e1 的前置节点,如图 7-12 所示

因为 e1 的 previous_entry_length 属性仅长 1 字节,它没办法保存新节点 new 的长度,所以程序将对压缩列表执行空间重分配操作,并将 e1 节点的 previous_entry_length 属性从原来的 1 字节长扩展为 5 字节长

现在,麻烦的事来了,e1 原本的长度介于 250 字节至 253 字节之间,在为 previous_entry_length 属性新增四个字节的空间之后,e1 的长度就变成了介于 254 字节至 257 字节之间,而这种长度使用 1 字节长的 previous_entry_length 属性是没办法保存的

因此,为了让 e2 的 previous_entry_length 属性可以记录下 e1 的长度,程序需要再次对压缩列表执行空间重分配操作,并将 e2 节点的 previous_entry_length 属性从原来的 1 字节长扩展为 5 字节长

正如扩展 e1 引发了对 e2 的扩展一样,扩展 e2 也会引发对 e3 的扩展,而扩展 e3 又会引发对 e4 的扩展……,为了让每个节点的 previous_entry_length 属性都符合压缩列表对节点的要求,程序需要不断地对压缩列表执行空间重分配操作,直到 eN 为止

Redis 将这种在特殊情况下产生的连续多次空间扩展操作称之为 “连锁更新”(cascade update)

除了添加新节点可能会引发连锁更新之外,删除节点也可能会引发连锁更新

因为连锁更新在最坏情况下需要对压缩列表执行 N 次空间重分配操作,而每次空间重分配的最坏复杂度为 O(N),所以连续更新的最坏复杂度为 O(N^2)

要注意的是,尽管连锁更新的复杂度较高,但它真正造成性能问题的几率是很低的:

  • 首先,压缩列表里要恰好有多个连续的、长度介于 250 字节至 253 字节之间的节点,连锁更新才有可能被引发,在实际中,这种情况并不多见
  • 其次,即使出现连锁更新,但只要被更新的节点数量不多,就不会对性能造成任何影响:比如说,对三五个节点进行连锁更新是绝对不会影响性能的

因为以上原因,ziplistPush 等命令的平均复杂度仅为 O(N),在实际中,我们可以放心地使用这些函数,而不必担心连锁更新会影响压缩列表的性能

5.压缩列表 API

函数 作用 算法复杂度
ziplistNew 创建一个新的压缩列表 O(1)
ziplistPush 创建一个包含给定值的新节点,并将这个新节点添加到压缩列表的表头或者表尾 平均 O(N),可能引发连锁更新,最坏 O(N^2)
ziplistInsert 将包含给定值的新节点插入给定节点之后 平均 O(N),可能引发连锁更新,最坏 O(N^2)
ziplistIndex 返回压缩列表给定索引上的节点 O(N)
ziplistFind 在压缩列表中查找并返回包含了给定值的节点 因为节点的值可能是一个字节数组,所以检查节点值和给定值是否相同的复杂度为 O(N),而查找整个列表的复杂度则为 O(N^2)
ziplistNext 返回给定节点的下一个节点 O(1)
ziplistPrev 返回给定节点的下一个节点 O(1)
ziplistGet 获取给定节点所保存的值 O(1)
ziplistDelete 从压缩列表中删除给定的节点 平均 O(N),可能引发连锁更新,最坏 O(N^2)
ziplistDeleteRange 删除压缩列表在给定索引上的连续多个节点 平均 O(N),可能引发连锁更新,最坏 O(N^2)
ziplistBlobLen 返回压缩列表目前占用的内存字节数 O(1)
ziplistLen 返回压缩列表目前包含的节点数量 节点数量小于 65535 是为 O(1),大于 65535 时为 O(N)

6.重点回顾

  • 压缩列表是一种为节约内存而开发的顺序性数据结构
  • 压缩列表被用作列表键和哈希键的底层实现之一
  • 压缩列表可以包含多个节点,每个节点可以保存一个字节数组或者整数值
  • 添加新节点到压缩列表,或者从压缩列表中删除节点,可能会引发连锁更新操作,但这种操作出现的机率并不高

《Reids 设计与实现》第四章 整数集合和压缩列表相关推荐

  1. Redis的设计与实现之整数集合和压缩列表

    整数集合(intset) 整数集合概念 整数集合是一个集合(set) 整数集合里只包含整数,并且集合元素不能太多 整数集合不会有重复的元素(有重复元素集合就没意义了) 整数集合的实现方式 typede ...

  2. Redis 数据结构 :SDS、链表、字典、跳表、整数集合、压缩列表

    文章目录 SDS 结构分析 内存策略 空间预分配 惰性空间释放 总结 链表 结构分析 总结 字典 结构分析 rehash 渐进式rehash 总结 跳表 结构分析 总结 整数集合 结构分析 升级 降级 ...

  3. redis——数据结构(整数集合,压缩列表)

    4.整数集合 整数集合(intset)是 Redis 用于保存整数值的集合抽象数据结构, 可以保存 int16_t . int32_t . int64_t 的整数值, 并且保证集合中不会出现重复元素. ...

  4. 《Reids 设计与实现》第二章 字典

    <Reids 设计与实现>第二章 字典 文章目录 <Reids 设计与实现>第二章 字典 一.字典 1.简介 2.字典的实现 3.哈希算法 4.解决键冲突 5.rehash 6 ...

  5. 《Reids 设计与实现》第九章 事件

    <Reids 设计与实现>第九章 事件 文章目录 <Reids 设计与实现>第九章 事件 一.简介 二.文件事件 1.文件事件处理器的构成 2.I/O 多路复用程序的实现 3. ...

  6. 《Reids 设计与实现》第一章 简单动态字符串和链表

    <Reids 设计与实现>第一章 简单动态字符串和链表 文章目录 <Reids 设计与实现>第一章 简单动态字符串和链表 一.简单动态字符串 1.简介 2.SDS 的定义 3. ...

  7. 计算机辅助设计capp设计,[高等教育]09第四章 计算机辅助设计与制造技术CAPP.ppt...

    [高等教育]09第四章 计算机辅助设计与制造技术CAPP 4.3 CAD/CAPP/CAM一体化技术 1 计算机辅助工艺设计(CAPP) 2 CAD/CAM集成技术 1 计算机辅助工艺设计CAPP技术 ...

  8. [redis设计与实现][5]基本数据结构——整数集合

    整数集合(intset)用于集合键.当一个集合只包含整数值元素,并且数量不多的时候,会使用整数集合作为集合键的底层实现.相对于直接保存字符串,整数集合能够很好地节约内存,但是由于是数组保存,需要特别关 ...

  9. 【算法设计zxd】第四章蛮力法 1.枚举法 02穷举查找

    目录 蛮力法(brute force): [例4-1]链环数字对  问题分析  计算模型 pair_digital(int n): 代码: [例4-2]解数字迷: 思考题:ACM预测:​ 问题分析 ...

最新文章

  1. Linux查看目录挂载点
  2. Coding:从给定数字集中找到最大的数字
  3. 【SICP练习】57 练习2.27
  4. LeetCode——双指针
  5. TestNG或JUnit
  6. ubuntu 下 php 安装 zip
  7. 50个直击灵魂的问题_直击灵魂的问题:“妈妈,我还能要个哥哥不!”
  8. python 什么是序列_从零起步学Python——什么是序列?
  9. uni-app中v-html中的元素添加样式
  10. ChromeDriver和PhantomJS配置到$PATH
  11. c语言程序设计职工信息管理系统,C语言程序设计-职工信息管理系统.doc
  12. CentOS SSH安装和配置
  13. 如何在线压缩图片?电脑怎么缩小图片kb大小?
  14. 【linux内核分析与应用-陈莉君】文件系统
  15. ajax向后台传递参数为对象实例
  16. 人脸识别入门论文《Deep Facial Expression Recognition: A Survey》学习笔记
  17. 硬路由、软路由、主路由、旁路由对比分析
  18. 2020年有寓意的领证日期_2020领证吉日
  19. Linux操作命令分类详解 - 用户权限(三)
  20. 银河移民PHP面试,移民香港,我真的“后悔死了”

热门文章

  1. mysql5.5二进制安装,mysql5.5.28 通用二进制安装
  2. html5 video在uc不自动播放,uc浏览器无法播放视频怎么办
  3. 云服务器配置(jdk、tomcat、mysql)
  4. Alpha(9/10)
  5. 富文本编辑器 CKeditor 配置使用
  6. YUI3学习笔记 ( 8 )
  7. 管理系统网页模板_档案管理系统应该涵盖一些什么功能?
  8. 河海大学计算机与信息学院 王晶晶,信息学部 计算机与信息学院
  9. MySQL裸机性能测试(2021)
  10. Flume Channel