Redis初识-整数集合| 8月更文挑战

501 阅读6分钟

这是我参与8月更文挑战的第7天,活动详情查看:8月更文挑战

1. 简介

整数集合 intset 是一个有序的、存储整型数据的结构,当 Redis 集合类型的元素都是整数并且都在64位有符号整数范围内时,使用该数据结构

整数集合中,当元素个数超过一定数量(默认为512),或增加了非整型的元素时,集合的底层编码会从 intset 转换成 hashtable

2. 数据存储

整数集合中可以保存 int16_t、int32_t、int64_t 类型的整型数据,并且不会重复

// intset.h
typedef struct intset {
    uint32_t encoding;
    uint32_t length;
    int8_t contents[];
} intset;
  • encoding:编码类型
    • INTSET_ENC_INT16:值为2,元素值位于 [-2^15,2^15-1] 时使用,此时元素占用2个字节
    • INTSET_ENC_INT32:值为4,元素值位于 [-2^31,-2^15) 或 (2^15-1,2^31-1] 时使用,此时元素占用4个字节
    • INTSET_ENC_INT64:值为8,元素值位于 [-2^63,-2^31) 或 (2^31-1,2^63-1] 时使用,此时元素占用8个字节
  • length:元素个数
  • contents:存储元素的数组,根据 encoding 决定几个字节表示一个元素

intset结构

intset 结构体会根据待插入元素的值判断是否需要进行扩容,扩容时会修改 encoding 字段,而 encoding 字段决定了元素在 contents 数组中占用几个字节,所以 encoding 字段变化时,contents 数组中原来保存的元素占用的内存空间也需要扩展

待插入元素导致扩容时,元素的值插入 intset 后不是最大值就是最小值

待插入元素的 encoding 字段大于 intset 的 encoding 字段时,表示需要进行扩容,并且该元素在 intset 中一定不存在

3. 基本操作

3.1 查询元素

查找元素的步骤:

  • 首先判断待查找元素的编码,如果编码大于 intset 的编码,则肯定不存在该元素,否则调用 intsetSearch 函数进一步查找元素
  • intsetSearch 函数首先判断 intset 是否存在元素,不存在直接返回0。如果存在元素再判断待查找元素是否介于 intset 的最大和最小元素之间,不在范围内返回0,否则继续查找
  • 二分法查找元素,找到返回1,未找到返回0
// intset.c
uint8_t intsetFind(intset *is, int64_t value) {
    // 获取待查找元素的编码
    uint8_t valenc = _intsetValueEncoding(value);
    // 如果待查找元素的编码大于 intset 的编码,则返回0
    // 否则进一步查找元素
    return valenc <= intrev32ifbe(is->encoding) && intsetSearch(is,value,NULL);
}
// intset.c
// is:查找的整数集合
// value:待查找元素
// pos:存储元素插入或删除时的位置
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) {
        // 如果 intset 中没有元素,直接返回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;
        }
    }

    if (value == cur) {
        // 查找到元素,返回1
        if (pos) *pos = mid;
        return 1;
    } else {
        // 未查找到元素,返回0
        if (pos) *pos = min;
        return 0;
    }
}

intset查找元素

3.2 插入元素

插入元素的步骤:

  • 判断待插入元素的编码是否大于 intset 的编码,如果大于则调用 intsetUpgradeAndAdd 函数进行升级后再添加元素,否则继续添加

  • 调用 intsetSearch 函数查找待插入元素的插入位置。如果元素已存在则直接退出 intsetAdd 函数,否则会获取到添加的位置 pos,继续添加

  • 调用 intsetResize 函数扩充 intset ,给待添加元素申请内存空间

  • 如果插入位置不是数组的尾部,则需要将数组中大于待添加元素的所有元素向后挪动一个位置,给待插入元素腾出空间

    intsetMoveTail实现原理

  • 保存待插入元素到插入位置,intset 的元素个数加1

// intset.c
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 的编码,则需要进行升级
        // 调用 intsetUpgradeAndAdd 函数对 intset 进行升级后,添加元素
        return intsetUpgradeAndAdd(is,value);
    } else {
        // 如果能在 intset 中查找到待插入元素,则直接返回
        // 否则获取插入位置 pos
        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);
    // intset 的元素个数加1
    is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
    return is;
}

intset插入元素

升级后添加元素的步骤:

  • prepend 标识待插入元素为正数还是负数

  • 更改 intset 的编码,扩充内存空间

  • 从最后一个元素逐步往前,调整扩容后每个元素存储的内存空间。从最后一个元素开始可以避免未处理数据被覆盖

  • 待插入元素为负数时保存到数组的头部,待插入元素为正数时保存到数组的尾部

    intset升级后添加元素位置

  • intset 的元素个数加1

// intset.c
static intset *intsetUpgradeAndAdd(intset *is, int64_t value) {
    uint8_t curenc = intrev32ifbe(is->encoding);
    uint8_t newenc = _intsetValueEncoding(value);
    int length = intrev32ifbe(is->length);
    // prepend 标识待插入元素为正数还是负数
    int prepend = value < 0 ? 1 : 0;

    // 更改 intset 的编码
    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);
    // intset 的元素个数加1
    is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
    return is;
}

intset升级后添加元素

3.3 删除元素

删除元素的步骤:

  • 判断待删除元素的编码是否小于等于 intset 的编码,如果不是表明待删除元素不在 intset 中,直接返回
  • 调用 intsetSearch 函数查看待删除元素是否存在,不存在时直接返回,存在时获取到删除位置 pos
  • 如果删除位置不在数组尾部,则将删除位置后的所有元素向前挪动一个位置,直接覆盖掉待删除元素,否则 intset 收缩内存空间后,直接丢弃待删除元素
// intset.c
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)) {
        uint32_t len = intrev32ifbe(is->length);

        if (success) *success = 1;

        // 如果删除位置不在数组尾部,则将删除位置后的所有元素向前挪动一个位置,直接覆盖掉待删除元素
        // 如果删除位置在数组尾部,则 intset 收缩内存空间后,直接丢弃待删除元素
        if (pos < (len-1)) intsetMoveTail(is,pos+1,pos);
        is = intsetResize(is,len-1);
        // intset 的元素个数减1
        is->length = intrev32ifbe(len-1);
    }
    return is;
}

intset删除元素

4. 常用 API

// 初始化 intset
intset *intsetNew(void);
// 插入元素
intset *intsetAdd(intset *is, int64_t value, uint8_t *success);
// 删除元素
intset *intsetRemove(intset *is, int64_t value, int *success);
// 查找元素是否存在
uint8_t intsetFind(intset *is, int64_t value);
// 随机返回一个元素
int64_t intsetRandom(intset *is);
// 获取指定位置的元素
uint8_t intsetGet(intset *is, uint32_t pos, int64_t *value);
// 获取 intset 的元素个数
uint32_t intsetLen(const intset *is);
// 获取 intset 总共占用的字节数
size_t intsetBlobLen(intset *is);

学自《Redis 5设计与源码分析》