redis中的intset集合源码阅读探究;基于7.0+版本

1丶什么是intset

Redis 中的 intset(整数集合)是一种高效的数据结构选择。Intset 具有紧凑的内存布局和快速的插入、删除和查找操作,适用于存储大量整数,并且能够节省内存空间。

2丶先说优点

内存布局:Intset 的内存布局非常紧凑。它使用连续的内存块存储整数,并且根据实际存储的整数大小选择适当的编码方式,以节省内存空间。

编码方式:Intset 使用不同的编码方式来存储整数,包括 INT16、INT32 和 INT64。它根据存储的整数值的范围选择最小的编码方式,以便节省内存。编码方式会根据需要进行动态调整。

快速的查找: 因为intset是有序数组,支持logn的查找复杂度. 2分大法.

3丶局限性

Intset 只能存储整数类型的数据,不支持其他数据类型。

频繁的插入和删除 中间元素; 不是最小最大元素.

4丶个人理解的使用场景

1丶整数. 2丶增删改 比较少.(或者插入有序的整数) 3丶主要用于查询 4丶空间资源紧缺
默认在512长度的整数内是使用该结构的,看读者的需求,如果是大量整数的存储且主要用于查询,可以调大这个配置.
redis.config中配置: set-max-intset-entries = 你需要的

5丶源码分析

1丶先看头文件.

redis中的intset集合源码阅读探究;基于7.0+版本_第1张图片
###丶2丶点进创建的方法.
这个没啥难度,就是初始化,分配内存,设置默认值.此时可以知道,他默认内存只是 encoding字段和length字段占用的空间大小.所以比dict结构省很多空间,键值对啊?扩展保留的空间啊,等等.

intset *intsetNew(void) {
	//分配内存
    intset *is = zmalloc(sizeof(intset));
    //指定编码方式;  默认使用了 INTSET_ENC_INT16; 即有符号的
    is->encoding = intrev32ifbe(INTSET_ENC_INT16);
    is->length = 0;
    return is;
}

3丶插入方法

intset *intsetAdd(intset *is, int64_t value, uint8_t *success) {
    //拿到当前元素的编码方式;方法直接放这里了,省的看.就是一些常量的大小比较.
    //    static uint8_t _intsetValueEncoding(int64_t v) {
	//        if (v < INT32_MIN || v > INT32_MAX)
	//            return INTSET_ENC_INT64;
	//        else if (v < INT16_MIN || v > INT16_MAX)
	//            return INTSET_ENC_INT32;
	//        else
	//            return INTSET_ENC_INT16;
	//    }
    uint8_t valenc = _intsetValueEncoding(value);
    //插入的位置.position
    uint32_t pos;
    if (success) *success = 1;
    //如果插入值大于当前编码
    if (valenc > intrev32ifbe(is->encoding)) {
        /* 最后分析,先看符合符合编码要求的情况 */
        return intsetUpgradeAndAdd(is,value);
    } else {
        /* 查找这个元素,如果元素已经存在,则直接返回 */
        if (intsetSearch(is,value,&pos)) {
            if (success) *success = 0;
            return is;
        }
		//说明当前元素不存在content数组中, 肯定需要插入.先扩大数组占用空间
        is = intsetResize(is,intrev32ifbe(is->length)+1);
        //如果插入的位置,不是最后一个位置. intsetMoveTail方法进行移动元素.
        // 如果有5个元素. 1,2, 4 ,5,6.   要插入3. 则需要给 4,5,6 往后移动一位.这就是为什么插入性能低的原因.每次要搬运. 如果插入尾部则不会有这个问题.
        if (pos < intrev32ifbe(is->length)) intsetMoveTail(is,pos,pos+1);
    }
     //执行插入
    _intsetSet(is,pos,value);
    //扩大length值.
    is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
    return is;
}

上面的方法内部基本不复杂, intsetSearch使用了2分查找的方式. intsetResize 进行了分配空间,intsetMoveTail数组的搬运. _intsetSet在下标为pos的位置赋值. 有兴趣的下载源码点进去看一下.这里主要分析 intsetUpgradeAndAdd 方法. 进行集合的升级和添加.

先思考一下,如果由我们去实现升级,该怎么办呢?
1丶扩大内存空间,但是也只是扩大1个元素的位置. 也就是length+1
2丶编码方式修改.
此时,如果插入为负数超编码范围怎么迁移? 如果是正数超出编码范围呢?
如果是正数,即 比较简单,放在最后一位即可. 其它元素如果原来是16位,则下标为0的就是016 + 16 位置,下标为1的就是116 + 16位置. 假设编码改为了32位. 则下标为0的就是032 +32. 从0开始迁移元素占用空间会覆盖下标为1的内存地址,此时如果避免呢? 倒着来! 没错,从下标为1的开始搞, 此时,拿到老的116+16的位置. 放到1*32+32的位置. 因为新的位置是扩大数来的,必然为空. 旧的位置还没被刷新,倒叙来的. 没错,逻辑通了
如果为负数呢? 那么必然新增的元素在下标为0的位置, 其它同正数的迁移,只不过每次迁移后的位置要扩大一位,因为下标为0是新添加元素的占用.

下面看redis的工程师是怎么做的, 是怎么写起来更优雅的呢

static intset *intsetUpgradeAndAdd(intset *is, int64_t value) {
	//拿到旧的编码方式, 为了取历史元素
    uint8_t curenc = intrev32ifbe(is->encoding);
    //新的编码方式
    uint8_t newenc = _intsetValueEncoding(value);
    //当前长度
    int length = intrev32ifbe(is->length);
    // 标识新增的元素是正数还是负数, 果然他有这个来区分.看看他下面怎么使用他的
    int prepend = value < 0 ? 1 : 0;

    //同我们想法, 先赋值新的编码方式,  然后扩大内存占用
    is->encoding = intrev32ifbe(newenc);
    is = intsetResize(is,intrev32ifbe(is->length)+1);

    /* 此时,发现是从大到小进行递归. 避免了覆盖元素的问题,发现他的prepend很巧妙,用起来很优雅,如果为正数则旧元素的pos不变,只是占用内存扩大,编码方式不同.若为负值,则pos+1, 因为0要留给新插入的元素 */
    while(length--)
        _intsetSet(is,length+prepend,_intsetGetEncoded(is,length,curenc));

    /* 插入新元素 */
    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丶迁移旧元素
4丶添加新元素.
然后一步一步的去完善代码.

其它方法,不在赘述,增删改查都差不多.只有增加的扩容个人感觉很巧妙.
思考?为啥不做降级呢?

升级可以根据插入的元素判断是否升级, 但是降级呢? 需要遍历每个元素,且每次移除都要,效率问题吧?

你可能感兴趣的:(redis,java,c++)