Redis高级之底层源码4——Set数据结构底层源码分析

1 概述

        整数集合(intset)并不是一个基础数据结构,而是Redis自己设计的一种存储结构,是集合键的底层实现之一。当一个集合只包含整数元素并且这个集合的元素数量不多时,Redis就会使用整数集合作为集合键的底层实现。

intset数据结构示意图如下:

        整数集合是有序的。当Redis集合类型的元素都是整数并且它们的值限制在64位(bit)表示的有符号整数范围之内时,使用该结构来存储。

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

typedef struct intset {
    //编码方式
    uint32_t encoding;
    //集合中包含的元素数量
    uint32_t length;
    //保存元素的数组
    int8_t contents[];
} intset;

         contents 数组是整数集合的底层实现,整数集合的每个元素都是contents数组的各数组项,每一项在数组中按值的大小从小到大有序排列,并且数组中不包含任何重复项。

        length 属性记录数组长度

        intset 结构将contents声明为int8_t类型的数组,但实际contents数组并不保存任何int8_t类型的值,contents数组的真正类型取决于encoding属性的值。encoding属性的值为INTSET_ENC_INT16,则数组是uint16_t类型,数组中的每个元素都是int16_t类型的整数值(-32768 ~32767);如果encoding属性的值为INTSET_ENC_INT32,则数组是uint32_t类型,数组中的每个元素都是int32_t类型的整数值。

        INTSET_ENC_INT16:当元素值都位于 INT16_MIN 到 INT16_MAX 之间时使用。每个元素占2个字节。

       INTSET_ENC_INT32: 当元素值都位于 INT16_MAX 到 INT32_MAX 或者 INT32_MIN 到 INT16_MIN之间时使用,每个元素占4字节。

      INTSET_ENC_INT64:  当元素值都位于 INT32_MAX 到 INT64_MAX 或者 INT64_MIN 到 INT32_MIN之间时使用,每个元素占8字节。

2 查询元素

        查询元素的函数是intsetFind,该函数首先进行一些防御性判断,如果没有通过判断则直接返回。intset是按从小到大有序排列的,所有通过防御性判断之后使用二分法进行元素的查找。源码如下:

uint8_t intsetFind(intset *is, int64_t value) {
     //判断数组元素类型
    uint8_t valenc = _intsetValueEncoding(value);
    //编码方式如果大于当前intset的编码方式,就直接返回0,否则调用intsetSearch函数查询
    return valenc <= intrev32ifbe(is->encoding) && intsetSearch(is,value,NULL);
}

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

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中没有元素,直接返回0 */
    if (intrev32ifbe(is->length) == 0) {
        if (pos) *pos = 0;
        return 0;
    } else {
        /*  如果元素大于最大值或者小于最小值,则直接返回0 */
        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;
        }
    }
    //查到则返回1,没查到返回0
    if (value == cur) {
      
        if (pos) *pos = mid;
        return 1;
    } else {
        if (pos) *pos = min;
        return 0;
    }
}

3 插入元素

        插入元素的函数是intsetAdd,该函数根据插入值的编码类型和当前intset的编码类型决定是直接插入还是先进行intset升级在执行插入操作。源码如下:

intset *intsetAdd(intset *is, int64_t value, uint8_t *success) {
    //获取待添加元素的编码值
    uint8_t valenc = _intsetValueEncoding(value);
    uint32_t pos;
    if (success) *success = 1;

    /* 如果大于当前intset的编码,就说明需要进行升级 */
    if (valenc > intrev32ifbe(is->encoding)) {
        /* 进行升级后执行插入操作*/
        return intsetUpgradeAndAdd(is,value);
    } else {
        /* 限制性查重操作,如果已经存在,则直接返回 */
        if (intsetSearch(is,value,&pos)) {
            if (success) *success = 0;
            return is;
        }
        //如果元素不存在,进行插入操作,首先把intset的内存进行扩容
        is = intsetResize(is,intrev32ifbe(is->length)+1);
        //如果插入元素在intset的中间位置,则需要给元素挪出空间
        if (pos < intrev32ifbe(is->length)) intsetMoveTail(is,pos,pos+1);
    }
    //保存元素
    _intsetSet(is,pos,value);
    //修改intset的长度,将其加1
    is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
    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);
    int prepend = value < 0 ? 1 : 0;

    /* 如果待插入元素小于0,就插入intset的头部位置,如果大于0,就插到intset的末尾位置 */
    is->encoding = intrev32ifbe(newenc);
    //将intset内容空间进行扩容
    is = intsetResize(is,intrev32ifbe(is->length)+1);


    while(length--)
        _intsetSet(is,length+prepend,_intsetGetEncoded(is,length,curenc));

    if (prepend)
        //数据依次右移,为插入的数据腾出空间
        _intsetSet(is,0,value);
    else
        //否则插入到intset末尾位置
        _intsetSet(is,intrev32ifbe(is->length),value);
    //修改length长度,将其加1
    is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
    return is;
}

        升级整数集合并插入新元素,主要分以下三步进行:

        1、根据新元素的类型,扩展整数集合底层数组的空间大小,并为新元素分配空间。

        2、将底层数组现有的所有元素都转换成与新元素相同的类型,并将类型转换后的元素放置到正确的位置,而且在放置元素的过程中需要继续维持底层数组的有序性质。

        3、将新元素插入底层数组中。

4 删除元素

        在整数集合中删除元素的函数是intsetRemove。该函数查找需要删除的元素,然后通过内存地址的移动直接将该元素覆盖掉。源码如下:

intset *intsetRemove(intset *is, int64_t value, int *success) {
    //获取待删除元素的编码
    uint8_t valenc = _intsetValueEncoding(value);
    uint32_t pos;
    //待删除元素的编码必须小于等于intset编码并且查找到钙元素才能执行删除操作
    if (success) *success = 0;

    if (valenc <= intrev32ifbe(is->encoding) && intsetSearch(is,value,&pos)) {
        uint32_t len = intrev32ifbe(is->length);
        if (success) *success = 1;
        //如果待删除元素位于中间位置,则直接覆盖
        //如果待删除元素位于intset末尾,则intset缩容内存直接将其丢弃
        if (pos < (len-1)) intsetMoveTail(is,pos+1,pos);
        is = intsetResize(is,len-1);
        //修改intset的长度,将其-1
        is->length = intrev32ifbe(len-1);
    }
    return is;
}

5 intset和HashTable的选择

        set-max-intset-entries的默认值是512,表示当Set对象的键值对数量大于该值时使用HashTable数据结构。

        当Set对象的值出现了非数字时,也会使用HashTable数据结构。

        当插入一个非数字时,数据结构从IntList转变为HashTable。

        当元素数量超过512时,数据结构从IntList转变为HashTable。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

geminigoth

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值