美文网首页工作生活
「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源码解读」—数据结构(四)整数集合

    知识点 整数集合是集合键的底层实现之一 整数集合的底层实现为数组,这个数组以有序、无重复的方式保存集合元素,在有需...

  • 整数集合

    整数集合 整数集合(intset)是 Redis 用于保存整数值的集合抽象数据结构, 它可以保存类型为 >int1...

  • redis数据结构(四):整数集合 intset

    数据结构 整数集合是redis为了保存整数值的集合而抽象出来的数据结构。intset数据结构 看这意思,也是把数组...

  • 6.整数集合

    整数集合 1. 整数集合的实现 整数集合是Redis用于保存整数值的集合抽象数据结构,它可以保存类型为int16_...

  • redis底层数据组织方式

    底层数据结构 redis底层数据结构有:字典、双端链表、压缩链表、整数集合、跳跃表和字典、整数集合、embstr ...

  • Redis源码分析(四)——Redis数据结构-整数集合

    1. 整数集合特点 有序:集合中所有值按照从小到大顺序排列。 不重复 可以存储int16_t、int32_t、in...

  • Redis 整数集合

    整数集合是 Redis 用于保存整数值的有序的集合抽象数据结构,当一个集合只包含整数值元素,并且这个集合的元素数量...

  • intset.c

    Redis中的intset,表示整数集合,用来存储整数,在set数据结构中用到。 intset的数据结构如下: 1...

  • Redis 源码分析(四) :intset

    Redis 源码分析(四) :intset一、什么是intset二、数据结构定义创建集合新增元素查找元素删除元素升...

  • redis数据结构

    redis数据结构列表:简单动态字符串(sds)链表(list)字典(dict)跳跃表(skiplist)整数集合...

网友评论

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

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