redis源码系列之三-整数集合insert

本文深入探讨Redis中的整数集合(intset)数据结构,包括其内存优化、查找、插入和删除操作。当元素个数不超过512且都在64位整数范围内时,Redis使用intset存储集合。intset使用连续内存空间,根据整数值大小采用不同编码节省空间。插入和删除操作涉及二分查找和动态内存调整,确保集合中元素无重复且有序。
摘要由CSDN通过智能技术生成

本月将开始分析redis源码系列:

同系列文章链接后期更新~~

1.Redis集合类型的元素都是整数并且都处在64位有符号整数范围之内时,并且元素个数不超过512个的时候,使用该结构体insert存储。

整数集合(intset)是Redis用于保存整数值的集合抽象数据结构,它可以保存类型为 int16_t、int32_t或者int64_t的整数值,并且保证集合中不会出现重复元素。

它在内存分配上与ziplist有些类似,是连续的一整块内存空间,而且对于大整数和小整数(按绝对值)采取了不同的编码,尽量对内存的使用进行了优化

127.0.0.1:6379> sadd testSet 1 2 3,
(integer) 4
127.0.0.1:6379> object encoding testSet
"intset"

上面的3个元素,其中存放的每个元素都占用16位,整数集合底层的大小为3*16 = 48位

当添加非整型变量,底层编码会发生转换

127.0.0.1:6379> sadd testSet 'a'
(integer) 1
127.0.0.1:6379> object encoding testSet
"hashtable"

2.insert数据结构

typedef struct intset {
    uint32_t encoding;//编码类型
    uint32_t length;//元素个数
    int8_t contents[];//柔性数组,根据encoding字段决定几个字节表示一个元素,各个项在数组中按值得大小到大有序地排序,并且数组中不包含任何重复项
} intset

  • length: 表示intset中的元素个数。encoding和length两个字段构成了intset的头部(header)。
  • contents: 是一个柔性数组(flexible array member)表示intset的header后面紧跟着数据元素。这个数组的总长度(即总字节数)等于encoding * length。柔性数组在Redis的很多数据结构的定义中都出现过(例如sds, quicklist, skiplist),用于表达一个偏移量。contents需要单独为其分配空间,这部分内存不包含在intset结构当中。

3.元素的查找

uint8_t intsetFind(intset *is, int64_t value) {//第1个参数为待查询的Intset,第2个参数为待查找的值
    uint8_t valenc = _intsetValueEncoding(value);//判断要插入valye的编码方式
    //编码方式如果大于当前intset的编码方式,直接返回0。否则调用intsetSearch函数进行查找
    return valenc <= intrev32ifbe(is->encoding) && intsetSearch(is,value,NULL);
}
    static uint8_t intsetSearch(intset *is, int64_t value, uint32_t *pos) {//可以看到该函数标签只比intsetFind多出第3个参数pos
    //添加元素时会使用该函数进行查找,查找时会使用到pos这个参数,如果未查找到该元素,pos参数会记录需要插入该元素的位置。所以intsetFind函数调用intsetSearch时会将pos参数置为NULL
    int min = 0, max = intrev32ifbe(is->length)-1, mid = -1;
    int64_t cur = -1;
    if (intrev32ifbe(is->length) == 0) { //如果intset中没有元素,直接返回0
        if (pos) *pos = 0;
        return 0;
    } else { //如果元素大于最大值或者小于最小值,直接返回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) {//二分查找该元素
        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;
        }
    }
    if (value == cur) {//查找到返回1,未查找到返回0
        if (pos) *pos = mid;
        return 1;
    } else {
        if (pos) *pos = min;
        return 0;
    }
}

 

4.元素的插入

intset *intsetAdd(intset *is, int64_t value, uint8_t *success) {
    uint8_t valenc = _intsetValueEncoding(value);//获取添加元素的编码值
    uint32_t pos;
    if (success) *success = 1;
    if (valenc > intrev32ifbe(is->encoding)) {//如果大于当前intset的编码,说明需要进行升级
        return intsetUpgradeAndAdd(is,value); //调用intsetUpgradeAndAdd进行升级后添加
    } else {
        if (intsetSearch(is,value,&pos)) {//否则先进行查重,如果已经存在该元素,直接返回,因为集合中不能存在重复的值
             //传递了第3个参数,则插入成功时将第3个参数success的值置为1,如果该元素已经在集合中存在,则将success置为0
            if (success) *success = 0;
            return is;
       }
       //如果元素不存在,则添加元素
       is = intsetResize(is,intrev32ifbe(is->length)+1);//首先将intset占用内存扩容
       //如果插入元素的位置小于insert数据的末尾元素的位置,表示不是尾元素,调用intsetMoveTail给元素挪出空间
       if (pos < intrev32ifbe(is->length)) intsetMoveTail(is,pos,pos+1);
    }
    _intsetSet(is,pos,value);//保存元素
    is->length = intrev32ifbe(intrev32ifbe(is->length)+1);//修改intset的长度,将其加1
    return is;
}

//虽然contents数组保存的四个整数值中,只有-2675256175807981027 是真正需要用int64_t类型来保存的,而其他 1、3、5三个值都可以用int16_t类型来保存,不过根据整数集合的升级规则,
//当向一个底层int16_t数组的整数集合添加一个int64_t类型的整数值时,整数集合已有的所有元素都会被转换成int64_t类型,所以contents数组保存的四个整数值都是int64_t类型的,
//升级编码格式后插入,返回值:添加新元素之后的集合
static intset *intsetUpgradeAndAdd(intset *is, int64_t value) {
    uint8_t curenc = intrev32ifbe(is->encoding);//获取当前的编码值
    uint8_t newenc = _intsetValueEncoding(value);//获取value需要的编码值
    int length = intrev32ifbe(is->length);//获取元素个数
    //因为value的编码比集合原有的所有的其他元素的编码都要大,所以,value要么大于集合中的所有元素,要么小于集合中的所有元素,因此,value只能添加到底层数组的最前端或最后端
    //如果待插入元素小于0,说明需要插入到intset的头部位置。如果大于0,需要插到intset的末尾位置。
    int prepend = value < 0 ? 1 : 0;
    //更新集合编码方式
    is->encoding = intrev32ifbe(newenc);
    is = intsetResize(is,intrev32ifbe(is->length)+1);//将intset内存空间进行扩容
    while(length--)
        //从最后一个元素逐个往前扩容。将原来存储的数据从后往前重新设置。注意必须从最后一个元素开始,否则有可能会导致元素覆盖
        //举个例子,假设原有的curenc编码的三个元素,它们在数组中排列如下:
        //| x | y | z |
        //当程序对数组进行重分配之后,数组就被扩容了(符号?表示未使用的内容):
        //| x | y | z | ? |   ?   |   ?   |
        //这时程序从数组的后端开始,重新插入元素,预留一个最后的位置给new
        //| 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    |
        _intsetSet(is,length+prepend,_intsetGetEncoded(is,length,curenc));
    if (prepend)//如果待插入元素小于0,插入intset头部位置
        _intsetSet(is,0,value);
    else //否则插入末尾位置
        _intsetSet(is,intrev32ifbe(is->length),value);
    is->length = intrev32ifbe(intrev32ifbe(is->length)+1);//修改intset的长度,将其加1
    return is;
}

 

5.元素的删除

//从整数集合中删除值value,success的值指示删除是否成功:因值不存在时造成删除失败,返回0 删除成功返回1
intset *intsetRemove(intset *is, int64_t value, int *success) {
    uint8_t valenc = _intsetValueEncoding(value);//获取待删除元素编码
    uint32_t pos;
    if (success) *success = 0;//表示删除是否成功,这里表示默认删除失败
    //待删除元素编码必须小于等于intset编码并且查找到该元素,才会执行删除操作
    if (valenc <= intrev32ifbe(is->encoding) && intsetSearch(is,value,&pos)) {
        //使用len记录当前的元素个数
        uint32_t len = intrev32ifbe(is->length);
        if (success) *success = 1;
        //上面的这句话if (valenc <= intrev32ifbe(is->encoding) && intsetSearch(is,value,&pos)) ,调用的intsetSearch函数会更新pos值,找到要删除元素所在的位置
        //如果待删除元素不是末尾元素,则调用intsetMoveTail直接覆盖掉该元素(则通过将position+1和之后位置的数据移动到position来覆盖掉position位置的值)
        if (pos < (len-1)) {
            intsetMoveTail(is,pos+1,pos);
        } else {
            //如果要删除的数据是该intset的最后一个值,假设该intset长度为length,则调用intsetResize分配length-1长度的空间之后会自动丢弃掉position位置的值
            is = intsetResize(is,len-1);
        }
        is->length = intrev32ifbe(len-1);//修改intset的长度,将其减1
    }
    return is;
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值