整数集合
整数集合(intset) 是集合键的底层实现之一,当一个集合只包含整数值元素,并且这个集合的元素数量不多时, Redis 就会使用整数集合作为集合间的底层实现.
6.1 整数集合的实现
typedef struct intset {
// 编码方式
uint32_t encoding;
// 集合包含的元素数量
uint32_t length;
// 保存元素的数组
int8_t contents[];
} intset;
intset 持有 contents 数组, 当前整数集合数量,以及编码方式.
intset 虽然持有 int8_t 数组,可真正存放的元素类型是有 encoding 决定的,而且所有元素的类型是统一的.
6.2 升级
可以知道 intset 可真正存放的元素类型是有 encoding 决定的,而且所有元素的类型是统一的.
那么如果我一开始确定使用 int8_t 的类型,那我插入一个 129 ,那将超过 int8_t 的上限,那将如何处理?
如果让我处理,将大致有下列思路:
1): 整体扩容,额外申请一个同样大小的 int16_t ,然后一个 for 进行整体的迁移.
2): 整体重构,支持单独的一个元素独立扩容.
Redis 实际处理方法和思路 1) 一致,且从尾部开始迁移元素.
static intset *intsetUpgradeAndAdd(intset *is, int64_t value) {
// 当前的编码方式
uint8_t curenc = intrev32ifbe(is->encoding);
// 新值所需的编码方式
uint8_t newenc = _intsetValueEncoding(value);
// 当前集合的元素数量
int length = intrev32ifbe(is->length);
// 根据 value 的值,决定是将它添加到底层数组的最前端还是最后端
// 注意,因为 value 的编码比集合原有的其他元素的编码都要大
// 所以 value 要么大于集合中的所有元素,要么小于集合中的所有元素
// 因此,value 只能添加到底层数组的最前端或最后端
int prepend = value < 0 ? 1 : 0;
/* First set new encoding and resize */
// 更新集合的编码方式
is->encoding = intrev32ifbe(newenc);
// 根据新编码对集合(的底层数组)进行空间调整
// T = O(N)
is = intsetResize(is,intrev32ifbe(is->length)+1);
/* Upgrade back-to-front so we don't overwrite values.
* Note that the "prepend" variable is used to make sure we have an empty
* space at either the beginning or the end of the intset. */
// 根据集合原来的编码方式,从底层数组中取出集合元素
// 然后再将元素以新编码的方式添加到集合中
// 当完成了这个步骤之后,集合中所有原有的元素就完成了从旧编码到新编码的转换
// 因为新分配的空间都放在数组的后端,所以程序先从后端向前端移动元素
// 举个例子,假设原来有 curenc 编码的三个元素,它们在数组中排列如下:
// | x | y | z |
// 当程序对数组进行重分配之后,数组就被扩容了(符号 ? 表示未使用的内存):
// | x | y | z | ? | ? | ? |
// 这时程序从数组后端开始,重新插入元素:
// | x | y | z | ? | z | ? |
// | x | y | y | z | ? |
// | x | y | z | ? |
// 最后,程序可以将新元素添加到最后 ? 号标示的位置中:
// | x | y | z | new |
// 上面演示的是新元素比原来的所有元素都大的情况,也即是 prepend == 0
// 当新元素比原来的所有元素都小时(prepend == 1),调整的过程如下:
// | x | y | z | ? | ? | ? |
// | x | y | z | ? | ? | z |
// | x | y | z | ? | y | z |
// | x | y | x | y | z |
// 当添加新值时,原本的 | x | y | 的数据将被新值代替
// | new | x | y | z |
// T = O(N)
while(length--)
_intsetSet(is,length+prepend,_intsetGetEncoded(is,length,curenc));
/* Set the value at the beginning or the end. */
// 设置新值,根据 prepend 的值来决定是添加到数组头还是数组尾
if (prepend)
_intsetSet(is,0,value);
else
_intsetSet(is,intrev32ifbe(is->length),value);
// 更新整数集合的元素数量
is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
return is;
}
6.3 升级的好处
一是提升整数集合的灵活性.
二是尽可能的节省内存.
6.3.1 提升灵活性
当底层有这种升级方案时,我们在上层使用时,会有更小的负担,不用担心当前的存储结构是否是最优的,因为肯定是最优的.
6.3.2 节约内存
当所有元素可以用 int16_t 进行存放时,我们的 intset 将由 int16_t 构成,如果发生了扩展, 则会用 int32_t 进行存放,不用担心我们只有 -128~128 的使用范围, 却使用了 int64_t 的成员方案
6.4 降级
有升级,理论上也需要降级,但是其实上是不需要的,每一次升级就进行了内存的重新分配,也通过for进行了数据迁移,虽然可能会出现数量级的整体下降,看上去会有内存浪费的情况发生,但是,既然曾经有过大数量级的数据存储,则后续也有可能再次使用大数量数据,从这个角度,也没有必要进行降级操作.所以当发生了升级操作后,不用也没必要对当前整数集合进行降级.
总结
整数集合结构为我们提供了整数存储的解决方案,底层的升级设计可以让我们在使用的时候,不用考虑需要用那种数据类型进行定义,在需要时,会整体更改所有数据的类型,也可以理解成升级的存在最大限度的节省了内存.
升级是不可逆操作,也是没必要进行逆向降级