reids底层结构-IntSet集合

InSet 是 Redis 中 set集合的一种实现方式,是基于整数数组来实现的,而且具有变换长度和有序等特征。

结构

typedef struct intset {
    uint32_t encoding; 
    uint32_t length; 
    int8_t contents[]; 
} intset;

其中分别含义是:

  • encoding:编码方式,支持存放16位、32位、64位整数
/* Note that these encodings are ordered, so:
 * INTSET_ENC_INT16 < INTSET_ENC_INT32 < INTSET_ENC_INT64. */
#define INTSET_ENC_INT16 (sizeof(int16_t)) /* 2字节整数,范围类似java的short*/
#define INTSET_ENC_INT32 (sizeof(int32_t)) /* 4字节整数,范围类似java的int */
#define INTSET_ENC_INT64 (sizeof(int64_t)) /* 8字节整数,范围类似java的long */
  • length:表示集合中的元素个数
  • contents:整数数组,用于保存集合数据

为了方便查找,Redis会将intset中所有的整数按照顺序依次排在contents数组中。结构如图

其中数组的下标为encoding中标记的编码大小*前面的个数,便于查询即starPtr * (sizeof(int16) * index)

在这里插入图片描述

图中采用的编码为INTSET_ENC_INT16,每个数字都在int16_t,即每个数组元素的大小是2个字节,每部分占用的字节大小如下:

  • encoding:4个字节
  • length:4个字节
  • contents: 2字节 * 3 = 6个字节

编码升级

加入有一个intset,元素为{5,10,20},采用的编码方式为INTSET_ENC_INT16,则每个整数占两个字节。如图在这里插入图片描述
如果我们又向intset中添加了一个整数50000,这个数超出了int16_t 的范围,intset 会自动将编码升级为合适的大小。

流程如下:

  1. 升级编码为INTSET_ENC_INT32,每个整数占4个字节,并按照新的编码方式以元素扩容数组。
  2. 倒序依次将数组中元素拷贝到扩容后的正确位置
    (即先将20拷贝到8到12 的位置、然后将10拷贝到4到8的位置。倒序是为了防止正序将后面的元素覆盖)
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
  3. 将带待添加的数组放到末尾(因为添加的数组编码超过了当前编码,肯定大于当前的所有整数)
  4. 最后,将encoding 的属性改为 INTSET_ENC_INT32,将length属性改为4
    改完后图片

InSet新增元素源码

intset *intsetAdd(intset *is, int64_t value, uint8_t *success) {
    uint8_t valenc = _intsetValueEncoding(value);// 获取当前值编码
    uint32_t pos; // 要插入的位置
    if (success) *success = 1;
    // 判断编码是不是超过了当前intset的编码
    if (valenc > intrev32ifbe(is->encoding)) {
        // 超出编码,需要升级
        return intsetUpgradeAndAdd(is,value);
    } else {
        // 在当前intset中查找值与value一样的元素的角标pos
        if (intsetSearch(is,value,&pos)) {
            if (success) *success = 0; //如果找到了,则无需插入,直接结束并返回失败
            return is;
        }
        // 数组扩容
        is = intsetResize(is,intrev32ifbe(is->length)+1);
        // 移动数组中pos之后的元素到pos+1,给新元素腾出空间
        if (pos < intrev32ifbe(is->length)) intsetMoveTail(is,pos,pos+1);
    }
    // 插入新元素
    _intsetSet(is,pos,value);
    // 重置元素长度
    is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
    return is;
}

大致步骤如下:

  1. 获取当前的编码
  2. 判断当前元素要插入的位置(角标)
  3. 判断要插入元素的编码是否超过当前的编码
  4. 超出编码则调用升级编码的intsetUpgradeAndAdd()方法
  5. 没有超出编码则查找是否有个当前要插入元素一致的角标
    • 如果有就直接返回失败,保证元素的唯一性
    • 没有则调用数组扩容的intsetResize()方法、然后将角标pos后的元素同一往后移一个位置(pos + 1)
  6. 调用插入元素方法_intsetSet()
  7. 重置intset的 length 元素+1
  8. 其中查找对应位置的方法底层采用了二分查找

InSet升级编码源码

static intset *intsetUpgradeAndAdd(intset *is, int64_t value) {
    // 获取当前intset编码
    uint8_t curenc = intrev32ifbe(is->encoding);
    // 获取新编码
    uint8_t newenc = _intsetValueEncoding(value);
    // 获取元素个数
    int length = intrev32ifbe(is->length); 
    // 判断新元素是大于0还是小于0 ,小于0插入队首、大于0插入队尾
    int prepend = value < 0 ? 1 : 0;
    // 重置编码为新编码
    is->encoding = intrev32ifbe(newenc);
    // 重置数组大小
    is = intsetResize(is,intrev32ifbe(is->length)+1);
    // 倒序遍历,逐个搬运元素到新的位置,_intsetGetEncoded按照旧编码方式查找旧元素
    while(length--) // _intsetSet按照新编码方式插入新元素
        _intsetSet(is,length+prepend,_intsetGetEncoded(is,length,curenc));
    /* 插入新元素,prepend决定是队首还是队尾*/
    if (prepend)
        _intsetSet(is,0,value);
    else
        _intsetSet(is,intrev32ifbe(is->length),value);
    // 修改数组长度
    is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
    return is;
}

步骤如下:

  1. 获取当前数组的编码和要插入元素的编码大小
  2. 获取当前数组元素个数
  3. 判断要插入元素大于零还是小于零来赋值给prepend状态码,其中1 表示插入队首 、0表示插入队尾(因为IntSet 是一个有序的集合,所以调用intsetUpgradeAndAdd()证明要么在队首插入要么在队尾插入)
  4. 而后倒序遍历,将数组元素拷贝到升级后对应的位置
  5. 插入新元素
  6. 修改 intSet 中 length 属性

总结

IntSet可以认为是特殊的整数数组,有如下特点:

  • IntSet会保证数组中的元素唯一、有序
  • 具备类型升级机制、可以在一定程度上节省内存
  • 底层采用二分查找来寻找对应角标、一定程度加快了查找效率
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值