Redis数据结构和源码分析——IntSet

128 阅读5分钟

IntSet结构

typedef struct intset {
    uint32_t encoding;    //编码方式 有三种编码方式,分别是int16、int32、int64
    uint32_t length;      //元素个数
    int8_t contents[];    //实际存储数据的数组 指针
} intset;

encoding——编码方式

1、INTSET_ENC_INT16:

使用0来标识INTSET_ENC_INT16,占用2字节,16比特。存储范围为-3276832767

2、INTSET_ENC_INT32:

使用1来标识INTSET_ENC_INT32,占用4字节,32比特。存储范围为--21474836482147483647;

3、INTSET_ENC_INT64:

使用1来标识INTSET_ENC_INT64,占用8字节,64比特。

每个整数都将以对应编码方式的整数占用空间存储

image.png 编码方式为INTSET_ENC_INT32时空间占用图

IntSet有序性

intset 中的整数元素是有序的。虽然它是一个集合数据结构,不允许重复元素,但是元素在 intset 中是有序的。升序排序查找效率更高,范围查询效率更高。

编码升级

初始编码方式选择: 当创建一个新的 intset 时,它会初始选择一种编码方式。一般选择最小的能够容纳所有元素的编码方式,以节省内存。比如{1,2}选择INT16,{40000}选择INT32。

但会遇到一些问题,比如编码方式为int16的intset要新插入一个40000,新元素超出了当前编码方式的表示范围。

选择新的编码方式

1.确定新的编码方式,比如要插入40000编码方式升级为INTSET_ENC_INT32,新的编码方式每个整数占4个字节,数组中每个元素占位都应该改变保持一致。

2.对现有元素重新内存分配,并倒序插入扩容后的正确位置(正序插入会造成数据覆盖,内存地址重叠)。

3.然后再将要新插入的元素40000插入数组末尾,并更新length和encoding。

源码

插入一个新整数

/* Insert an integer in the intset */
intset *intsetAdd(intset *is, int64_t value, uint8_t *success) {
    uint8_t valenc = _intsetValueEncoding(value); //获取整数值的编码方式
    uint32_t pos; //用于保存插入的位置
    if (success) *success = 1;

    /* Upgrade encoding if necessary. If we need to upgrade, we know that
     * this value should be either appended (if > 0) or prepended (if < 0),
     * because it lies outside the range of existing values. */
     /* 如果整数值的编码方式大于当前 intset 的编码方式,需要进行升级 */
    if (valenc > intrev32ifbe(is->encoding)) {
        /* This always succeeds, so we don't need to curry *success. */
        // 编码升级
        return intsetUpgradeAndAdd(is,value);
    } else {
        /* Abort if the value is already present in the set.
         * This call will populate "pos" with the right position to insert
         * the value when it cannot be found. */
         //如果该元素已经存在set中,设置success表示插入失败 注意这个方法,会更新&pos,寻找到该
         //插入的位置
        if (intsetSearch(is,value,&pos)) {
            if (success) *success = 0;
            return is;
        }
        
        /* 调整 intset 的大小,为新元素腾出位置 */
        is = intsetResize(is,intrev32ifbe(is->length)+1);
        if (pos < intrev32ifbe(is->length)) intsetMoveTail(is,pos,pos+1);
    }
    
    /* 在正确的位置插入新的整数值,并更新 intset 的长度 */
    _intsetSet(is,pos,value);
    is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
    return is;
}

intsetSearch

这里会找到该插入的位置,并判断元素是不是已经存在,二分查找

/* Search for the position of an integer in the intset */
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 {
        /* 检查无法找到值的情况,但是知道插入位置 */
        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; // 未找到值
    }
}

编码升级逻辑

/* Upgrades the intset to a larger encoding and inserts the given integer. */
static intset *intsetUpgradeAndAdd(intset *is, int64_t value) {
    // 获取当前编码方式和新的编码方式
    uint8_t curenc = intrev32ifbe(is->encoding);
    uint8_t newenc = _intsetValueEncoding(value);
    // 获取当前 intset 的长度
    int length = intrev32ifbe(is->length);
    // 判断是否需要在开头插入元素
    int prepend = value < 0 ? 1 : 0;

    /* 首先设置新的编码方式并进行扩容 */
    is->encoding = intrev32ifbe(newenc);
    is = intsetResize(is, intrev32ifbe(is->length) + 1);

    /* 从后向前进行升级,以防覆盖值。
     * 注意 "prepend" 变量用于确保在 intset 的开头或结尾有一个空白位置。 */
    while (length--)
        _intsetSet(is, length + prepend, _intsetGetEncoded(is, length, curenc));

    /* 在开头或结尾设置新值 */
    if (prepend)
        _intsetSet(is, 0, value);
    else
        _intsetSet(is, intrev32ifbe(is->length), value);

    is->length = intrev32ifbe(intrev32ifbe(is->length) + 1);
    return is;
}

INTSET只要有不重复元素插入就要扩容,保证了灵活性和内存利用,但也损失了一部分性能