美文网首页工作生活
「Redis源码解读」—数据结构(四)整数集合

「Redis源码解读」—数据结构(四)整数集合

作者: wh4763 | 来源:发表于2019-06-30 15:55 被阅读0次

    知识点

    • 整数集合是集合键的底层实现之一
    • 整数集合的底层实现为数组,这个数组以有序、无重复的方式保存集合元素,在有需要时,redis会根据新添加元素的类型,改变这个数组的类型
    • 升级操作为整数集合带来了操作上的灵活性,并且尽可能地节约了内存
    • 整数集合只支持升级操作,不支持降级操作
    typedef struct intset {
        //encoding 指定了编码方式,有 INTSET_ENC_INT16、INTSET_ENC_INT32、INTSET_ENC_INT64 三种。三者表示的范围是 16位整数、32位整数 以及 64位整数。
        uint32_t encoding; 
        //length 存储了整数集合的元素个数     
        uint32_t length;        
        //整数集合中的元素按照从小到大的顺序在 contents 中排列起来
        int8_t contents[];     
    } intset;
    

    编码升级

    • 当前编码方式不足以存储更大位数的整数时,需要升级编码。举个例子,下图所示的四个数字都在 [ -32768, 32767 ] 范围内,所以采用 INTSET_ENC_INT16 编码即可。contents 的数组长度为 sizeof(int16_t) * 4 = 2 * 4 = 8 个字节 ( 即64个二进制位 )。
    • 然后我们插入一个数,它的值为 32768,比 INT16_MAX 大1,所以它需要采用 INTSET_ENC_INT32 编码,而整数集合中所有的数的编码需要保持一致。那么,所有数的编码都需要转为 INTSET_ENC_INT32 编码。这就是 “升级”。
    • 升级完后,contents 数组的长度变为 sizeof(int32_t) * 5 = 4 * 5 = 20 个字节 ( 即160个二进制位 )。而且每个元素占用的内存都扩大一倍,所在的相对位置也发生了变化,导致所有的元素都需要往高位内存迁移。
    • 那我们一开始就把所有的整数集合都用 INTSET_ENC_INT64 来编码不就好了,还省得麻烦。原因是 Redis 设计 intset 的初衷还是为了节省内存,设想一个集合的元素永远都不会超过 16位 整数,那么用 64位整数的话,相当于浪费了 3倍 的内存。

    创建集合

    创建一个整数集合 intsetNew,实现在 intset.c 中:

    • 初始创建的整数集合为空集合,用 zmalloc 进行内存分配后,定义编码为 INTSET_ENC_INT16,这样可以使内存尽量小。
    intset *intsetNew(void) {
        intset *is = zmalloc(sizeof(intset));
        is->encoding = intrev32ifbe(INTSET_ENC_INT16);
        is->length = 0;
        return is;
    }
    

    元素设置

    static void _intsetSet(intset *is, int pos, int64_t value) {
        uint32_t encoding = intrev32ifbe(is->encoding);          /* a */
     
        if (encoding == INTSET_ENC_INT64) {
            ((int64_t*)is->contents)[pos] = value;               /* b */
            memrev64ifbe(((int64_t*)is->contents)+pos);          /* c */
        } else if (encoding == INTSET_ENC_INT32) {
            ((int32_t*)is->contents)[pos] = value;
            memrev32ifbe(((int32_t*)is->contents)+pos);
        } else {
            ((int16_t*)is->contents)[pos] = value;
            memrev16ifbe(((int16_t*)is->contents)+pos);
        }
    }
    

    元素查找

    • 由于整数集合是有序集合,所以查找某个元素是否在整数集合中,Redis 采用的是二分查找。intsetSearch 实现如下:
    static uint8_t intsetSearch(intset *is, int64_t value, uint32_t *pos) {
        int min = 0, max = intrev32ifbe(is->length)-1, mid = -1;
        int64_t cur = -1;
        if (intrev32ifbe(is->length) == 0) {
            //整数集合为空,返回0表示查找失败
            if (pos) *pos = 0;                                        
            return 0;
        } else {                                                      
            //value 的值比整数集合中的最大值还大,或者比最小值还小,则返回0表示查找失败
            if (value > _intsetGet(is,intrev32ifbe(is->length)-1)) {
                if (pos) *pos = intrev32ifbe(is->length);
                return 0;
            } else if (value < _intsetGet(is,0)) {
                if (pos) *pos = 0;
                return 0;
            }
        }
        while(max >= min) {                                          
            //执行二分查找,将找到的值存在 cur 中
            mid = ((unsigned int)min + (unsigned int)max) >> 1;     
            cur = _intsetGet(is,mid);
            if (value > cur) {
                min = mid+1;
            } else if (value < cur) {
                max = mid-1;
            } else {
                break;
            }
        }
        //如果找到则返回1,表示查找成功,并且将 pos 设置为 mid 并返回;如果没找到则返回一个需要插入的位置
        if (value == cur) {                                           
            if (pos) *pos = mid;
            return 1;
        } else {
            if (pos) *pos = min;
            return 0;
        }
    }
    

    内存分配

    static intset *intsetResize(intset *is, uint32_t len) {
        uint32_t size = len*intrev32ifbe(is->encoding);
        is = zrealloc(is,sizeof(intset)+size);
        return is;
    }
    

    编码升级

    static intset *intsetUpgradeAndAdd(intset *is, int64_t value) {
        uint8_t curenc = intrev32ifbe(is->encoding);
        uint8_t newenc = _intsetValueEncoding(value);                             
        int length = intrev32ifbe(is->length);
        // curenc 记录升级前的编码,newenc 记录升级后的编码
        int prepend = value < 0 ? 1 : 0;                                       
        is->encoding = intrev32ifbe(newenc);
        //将整数集合 is 的编码设置成新的编码后,进行内存重分配
        is = intsetResize(is,intrev32ifbe(is->length)+1);                      
        while(length--)
            //获取原先内存中的数据,设置到新内存中
            _intsetSet(is,length+prepend,_intsetGetEncoded(is,length,curenc));   
        if (prepend)
            _intsetSet(is,0,value);
        else
            // 当插入的值 value 为负数的时候,为了保证集合的有序性,需要插入到 contents 的头部;反之,插入到尾部;当 value 为负数时 prepend 为1,这样就可以保证在内存拷贝的时候将第 0 个位置留空
            _intsetSet(is,intrev32ifbe(is->length),value);                       
        is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
        return is;
    }
    

    元素插入

    intset *intsetAdd(intset *is, int64_t value, uint8_t *success) {
        uint8_t valenc = _intsetValueEncoding(value);
        uint32_t pos;
        if (success) *success = 1;
        // 插入的数值 value 的内存编码大于现有集合的编码,直接调用 intsetUpgradeAndAdd 进行编码升级
        if (valenc > intrev32ifbe(is->encoding)) {                               
            return intsetUpgradeAndAdd(is,value);
        } else {
            if (intsetSearch(is,value,&pos)) {                                
                //集合元素是不重复的,如果 intsetSearch 能够找到,则将 success 置为0,表示此次插入失败
                if (success) *success = 0;                                  
                return is;
            }
            //如果 intsetSearch 找不到,将 intset 进行内存重分配,即 长度 加 1
            is = intsetResize(is,intrev32ifbe(is->length)+1);                    
            //pos 为 intsetSearch 过程中找到的 value 将要插入的位置,我们将 pos 以后的内存向后移动1个单位
            if (pos < intrev32ifbe(is->length)) intsetMoveTail(is,pos,pos+1);    
        }
        _intsetSet(is,pos,value);                                                 
        //调用 _intsetSet 将 value 的值设置到 pos 的位置上,然后给成员变量 length 加 1
        is->length = intrev32ifbe(intrev32ifbe(is->length)+1);                  
        return is;
    }
    

    元素删除

    intset *intsetRemove(intset *is, int64_t value, int *success) {
        uint8_t valenc = _intsetValueEncoding(value);
        uint32_t pos;
        if (success) *success = 0;
        //当整数集合中存在 value 这个元素时才能执行删除操作
        if (valenc <= intrev32ifbe(is->encoding) && intsetSearch(is,value,&pos)) { 
            uint32_t len = intrev32ifbe(is->length);
            if (success) *success = 1;
            //如果能通过 intsetSearch 找到元素,那么它的位置就在 pos 上
            if (pos < (len-1)) intsetMoveTail(is,pos+1,pos);            
            //intsetResize 重新分配内存,并且将集合长度减1           
            is = intsetResize(is,len-1);                                            
            is->length = intrev32ifbe(len-1); 
        }
        return is;
    }
    

    相关文章

      网友评论

        本文标题:「Redis源码解读」—数据结构(四)整数集合

        本文链接:https://www.haomeiwen.com/subject/azdbcctx.html