一、intset 介绍
intset,也就是整数集合,是 set 的底层实现之一,当一个集合只包含整数值元素,并且这个集合的元素数量不多时,Redis 就会使用 intset 作为 set 的底层实现。
它的查找是 O(log n) 的,插入和删除都是 O(n) 的。但是由于存储元素相对较少的时候,O(log n) 和 O(n) 差距不是很大,但是用 Redis 的这种 intset,相比红黑树和哈希表来说,可以大大减少内存。
所以,Redis 的 整数集合 intset 的存在主要还是为了节省内存。
二、intset 结构
intset 字段描述:
encoding:编码方式
length:集合包含的元素数量, 也即是 contents 数组的长度
contents:保存元素的数组,intset 的每个元素都是 contents 数组的一个数组项(item), 各个项在数组中按值的大小从小到大有序地排列, 并且数组中不包含任何重复项
虽然 intset 结构将 contents 属性声明为 int8_t 类型的数组, 但实际上 contents 数组并不保存任何 int8_t 类型的值——contents 数组的真正类型取决于 encoding 属性的值。
1、如果 encoding 属性的值为 INTSET_ENC_INT16,那么 contents 就是一个 int16_t 类型的数组,数组里的每个项都是一个 int16_t 类型的整数值(最小值为 -32768,最大值 32767)。
2、如果 encoding 属性的值为 INTSET_ENC_INT32,那么 contents 就是一个 int32_t 类型的数组,数组里的每个项都是一个 int32_t 类型的整数值(最小值为 -2147483648,最大值 2147483647)。
3、如果 encoding 属性的值为 INTSET_ENC_INT64,那么 contents 就是一个 int64_t 类型的数组,数组里的每个项都是一个 int64_t 类型的整数值(最小值为 -9223372036854775808,9223372036854775807)。
三、升级
每当我们要将一个新元素添加到 intset 里面,并且新元素的类型比 intset 现有所有元素的类型都要长时,intset 需要先进行升级(upgrade),然后才能将新元素添加到整数集合里面。
升级 intset 并添加新元素主要分为以下三步进行:
1、根据新元素的类型,扩展 intset 底层数组的空间大小,并为新元素分配空间。(分配空间)
2、将底层数组现有的所有元素都转换成与新元素相同的类型,并将类型转换后的元素放置到正确的位上,而且在放置元素的过程中,需要继续维持底层数组的有序性性质不变。(调整位置类型)
3、将新元素添加到底层数组里面(添加元素)。
注:intset 添加新元素的时间复杂度为O(N)。
举个例子:
假设现在有一个 INTSET_ENC_INT16 编码的 intset,集合中包含三个 int16_t 类型的元素,因为每个元素都占用 16 位空间, 所以 intset 底层数组的大小为 3 * 16 = 48 位, 下图展示了整数集合的三个元素在这 48 位里的位置:
现在, 假设我们要将类型为 int32_t 的整数值 65535 添加到整数集合里面, 因为 65535 的类型 int32_t 比 intset 当前所有元素的类型都要长,所以在将 65535 添加到 intset 之前, 程序需要先对 intset 进行升级。
升级首先要做的是, 根据新类型的长度, 以及集合元素的数量(包括要添加的新元素在内),对底层数组进行空间重分配。
intset 目前有三个元素, 再加上新元素 65535 , intset 需要分配四个元素的空间, 因为每个 int32_t 整数值需要占用 32 位空间, 所以在空间重分配之后, 底层数组的大小将是 32 * 4 = 128 位, 如下图所示:
然后把原数组中的所有元素的类型都转换成新的元素类型,如下图:
最后把新元素添加到数组当中:
因为引发升级的新元素的长度总是比 intset 现有所有元素的长度都大, 所以这个新元素的值要么就大于所有现有元素,要么就小于所有现有元素:
1、在新元素小于所有现有元素的情况下, 新元素会被放置在底层数组的最开头(索引 0 );
2、在新元素大于所有现有元素的情况下, 新元素会被放置在底层数组的最末尾(索引 length-1 )。
升级的好处:
因为 C 语言是静态类型语言,为了避免类型错误,我们通常不会将两种不同类型的值放在同一个数据结构里面。正因为有这一特性,我们可以随意地将 int16_t、int32_t 或者 int64_t 类型的整数添加到集合中,而不必担心出现类型错误。
四、降级
intset 不支持降级操作, 一旦对数组进行了升级,编码就会一直保持升级后的状态。
为什么不实现降级?
1、加入元素超过当前长度我们很容易就知道此时需要进行升级操作,但是当我们删除一个数据时我们如何判断是否需要降级却很困难,我们需要重新遍历一遍剩下的元素是否小于当前长度,实现复杂度 O(N) 。这就是为什么不进行降级原因之一。
2、你可能会说重新遍历一遍很快的,反正在内存中,那么你有没有想过如果降级之后又遇到升级情况,这样来回的升级降级就降低了我们程序的性能了。我们知道升级是必须的所以这里降级 Redis 采取的是忽略的策略。