Redis设计与实现 笔记 第六章 整数集合

整数集合

整数集合(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进行了数据迁移,虽然可能会出现数量级的整体下降,看上去会有内存浪费的情况发生,但是,既然曾经有过大数量级的数据存储,则后续也有可能再次使用大数量数据,从这个角度,也没有必要进行降级操作.所以当发生了升级操作后,不用也没必要对当前整数集合进行降级.

总结

整数集合结构为我们提供了整数存储的解决方案,底层的升级设计可以让我们在使用的时候,不用考虑需要用那种数据类型进行定义,在需要时,会整体更改所有数据的类型,也可以理解成升级的存在最大限度的节省了内存.
升级是不可逆操作,也是没必要进行逆向降级

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值