这是我参与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 结构体会根据待插入元素的值判断是否需要进行扩容,扩容时会修改 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;
}
}
3.2 插入元素
插入元素的步骤:
-
判断待插入元素的编码是否大于 intset 的编码,如果大于则调用 intsetUpgradeAndAdd 函数进行升级后再添加元素,否则继续添加
-
调用 intsetSearch 函数查找待插入元素的插入位置。如果元素已存在则直接退出 intsetAdd 函数,否则会获取到添加的位置 pos,继续添加
-
调用 intsetResize 函数扩充 intset ,给待添加元素申请内存空间
-
如果插入位置不是数组的尾部,则需要将数组中大于待添加元素的所有元素向后挪动一个位置,给待插入元素腾出空间
-
保存待插入元素到插入位置,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;
}
升级后添加元素的步骤:
-
prepend 标识待插入元素为正数还是负数
-
更改 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;
}
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;
}
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设计与源码分析》