Redis学习笔记&源码阅读--整数集合

申明

  • 本文基于Redis源码5.0.8
  • 本文内容大量借鉴《Redis设计和实现》和《Redis5设计与源码分析》

概念

整数集合(intset)是一个有序的、存储整型数据的结构,当Redis集合类型的元素都是整数并且都处在64位有符号整数范围之内时,使用该结构体存储。
在两种情况下,底层编码会发生转换。

  • 当元素个数超过一定数量之后(默认值为512),即使元素类型仍然是整型,也会将编码转换为hashtable;
  • 当增加非整型变量时,底层编码从intset转换为hashtable。

整数集合在Redis中可以保存int16_t、int32_t、int64_t类型的整型数据,并且可以保证集合中不会出现重复数据。每个整数集合使用一个intset类型的数据结构表示。intset结构体表示如下:

encodinglengthelement1element2element3

其源码定义如下:

typedef struct intset {
    uint32_t encoding;//编码类型
    uint32_t length;//元素个数
    int8_t contents[];//保存元素的数组
} intset;

encoding:编码类型,决定每个元素占用几个字节。有如下3种类型。

  1. INTSET_ENC_INT16:当元素值都位于INT16_MIN和INT16_MAX之间时使用。该编码方式为每个元素占用2个字节。
  2. INTSET_ENC_INT32:当元素值位于INT16_MAX到INT32_MAX或者INT32_MIN到INT16_MIN之间时使用。该编码方式为每个元素占用4个字节。
  3. INTSET_ENC_INT64:当元素值位于INT32_MAX到INT64_MAX或者INT64_MIN到INT32_MIN之间时使用。该编码方式为每个元素占用8个字节。
    在源码中对应的定义如下:
#define INTSET_ENC_INT16 (sizeof(int16_t))
#define INTSET_ENC_INT32 (sizeof(int32_t))
#define INTSET_ENC_INT64 (sizeof(int64_t))

contents:存储具体元素,根据encoding字段决定多少个字节表示一个元素

基本操作

接下来我们看下整数集合的基本操作。

查询元素

先看代码:

uint8_t intsetFind(intset *is, int64_t value) {
    uint8_t valenc = _intsetValueEncoding(value);
    return valenc <= intrev32ifbe(is->encoding) && intsetSearch(is,value,NULL);
}

整数集合中的所有元素的编码类型都是一样的,所有在查找前我们可以先判断查询value的编码类型是否超过当前编码类型,超过的话说明查询的value不可能在整数集合中,缩短无效查询的耗时。_intsetValueEncoding的逻辑很简单,我们看下源码:

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;
}

这么简单的逻辑直接忽略,如果编码没有超过当前编码则开始查询value,看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;

    /* intset为空*/
    if (intrev32ifbe(is->length) == 0) {
        if (pos) *pos = 0;
        return 0;
    } else {
        /* 有些场景下知道无法找到value,但是希望知道value的插入位置 */
        if (value > _intsetGet(is,max)) {
            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) {
        if (pos) *pos = mid;
        return 1;
    } else {
        if (pos) *pos = min;
        return 0;
    }
}

这段代码的核心其实就是一个二分查找,二分查找前做一些基本判断,intset是否为空,value是否比intset最大值大,是否比intset最小值小,并更新相应的插入位置,如果value的值介于最小值和最大值之间,可使用二分查找的思想来定位value的位置,二分查找我们就不说了吧。然后按照查找结果返回数据。

添加元素

我们知道整数集合中所有元素的编码都是一致的,那么如果集合中的数据都是很小的,比如不超过100,这个时候集合的编码格式就是INTSET_ENC_INT16,但是如果此时存入了一个比较大的值,比如220,或者-220,此时INTSET_ENC_INT16是保存不了,那么集合就会升级编码到INTSET_ENC_INT32,当前所有的元素占用的存储空间都要对齐到4字节,而不是之前的2字节了。

会不会有人问为什么不能只把当前的值用4个字节,其余的依然保留2字节,整数集合的存在不是为了节约内存嘛?如果你这么干了后面查找就没办法按照固定的offset去获取元素值了,可以参考下压缩列表的设计做比对。

这么看添加元素有两种,一种是不需要做元素升级操作,一种是需要做升级元素操作的,我们先看简单的。

redis中是没有降级元素操作的,因为删除元素时判断是否缩减还需要遍历一次。

无需升级操作的添加元素

我们先看下源码片段:

     if (intsetSearch(is,value,&pos)) {
         if (success) *success = 0;
         return is;
     }

     is = intsetResize(is,intrev32ifbe(is->length)+1);
     if (pos < intrev32ifbe(is->length)) intsetMoveTail(is,pos,pos+1);
      _intsetSet(is,pos,value);
     is->length = intrev32ifbe(intrev32ifbe(is->length)+1);

首先查询value,如果存在,直接返回;否则的话,就先扩容intset,知道为什么intset不用来存在大量元素了吧,每次插入都是要做内存Resize的。
内存申请完了,是不是将插入位置pos后面的数据全部右移一个单位让给value,我们的intsetMoveTail就是干这个事情的,将pos起始的所有数据全部移到以pos+1起始处,然后再讲value插入到pos处,更新intset的长度信息。

需要升级操作的添加元素

既然需要升级存储编码,那么插入value和intset中当前的值之间只会有两种情况,第一,插入的值是正数且比intset中最大值还要大,那么插入到intset最后面;第二,插入的值是负数且比intset最小值还要小,那么插入到intset最前面。只能有这两种情况。带着这个认知我们看下源码:

static intset *intsetUpgradeAndAdd(intset *is, int64_t value) {
    uint8_t curenc = intrev32ifbe(is->encoding);
    uint8_t newenc = _intsetValueEncoding(value);
    int length = intrev32ifbe(is->length);
    //通过value正负判断插入位置是最前还是最后,最前的话在copy时通过prepend控制位置
    int prepend = value < 0 ? 1 : 0;

    /* 修改新的encoding和resize*/
    is->encoding = intrev32ifbe(newenc);
    is = intsetResize(is,intrev32ifbe(is->length)+1);

    /* 倒序逐个赋值 */
    while(length--)
        _intsetSet(is,length+prepend,_intsetGetEncoded(is,length,curenc));

    /* 按照在前在后插入新值 */
    if (prepend)
        _intsetSet(is,0,value);
    else
        _intsetSet(is,intrev32ifbe(is->length),value);
     //更新length
    is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
    return is;
}

当value小于0,说明插入位置在前面,当value大于0,说明插入位置在后面。在resize后进行while循环赋值,当value小于0,prepend=1,length+1说明起始位置是resize后的最后一个元素,因为value小于0,插入位置是前面嘛,后面不用留空间,当时value大于0时就需要预留位置了,所以prepend=0,从length位置开始插入(resize后长度变成length+1了)。后面再按照prepend判断插入位置,更新完值后再更新下length就结束了。

while中倒序赋值的原因我想不用细说都知道原因吗?唯独有一点,在赋值过程中会不会出现旧值还没来得及赋值到新位置就被覆盖了?不会的,我算过了,只有当赋值到最后一个元素,覆盖位置才会追上,所以不用担心。

删除元素

先看下源码:

intset *intsetRemove(intset *is, int64_t value, int *success) {
    uint8_t valenc = _intsetValueEncoding(value);
    uint32_t pos;
    if (success) *success = 0;

    if (valenc <= intrev32ifbe(is->encoding) && intsetSearch(is,value,&pos)) {
        uint32_t len = intrev32ifbe(is->length);

        if (success) *success = 1;

        /* 重写value */
        if (pos < (len-1)) intsetMoveTail(is,pos+1,pos);
        is = intsetResize(is,len-1);
        is->length = intrev32ifbe(len-1);
    }
    return is;
}

删除前也做一次编码格式的检测,如果能够查找到value就讲value所在pos后面的有效内存全部向前移动一个存储单位,这就是intsetMoveTail做的事情,然后通过resize缩减内存,更新length,删除操作就结束了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值