一.概述
整数集合(intset)与hash表,跳跃表等一样是集合键的底层实现之一,一般用于保存数量不多的整数,这是由于其实现机制导致的不能存储过多元素,否则会造成效率问题。集合键的内部结构可以使用Redis命令OBJECT ENCODING获取
二.整数集合结构
整数集合可以用于保存int16_t,int32_t或int64_t的整数值,并且从小到大排序且保证不会出现重复元素。在Redis中的数据结构如下:
typedef struct inset {
// 编码方式,即保存的值类型(int16_t, int32_t, int64_t)
uint32_t encoding;
// 集合包含的元素数量
uint32_t length;
// 保存元素的数值
// 【注】:其实就相当于一个字节数组,再根据编码方式进行存储与读取(c++可以重新解读一块内存)
int8_t contents[]
}intset;
三.升级
假如整数集合中起初存储的都是int16_t类型的元素,而此时要添加一个int32_t类型的元素,那么将进行集合升级,首先进行集合内存的扩展,以便足以容纳新元素与升级后的旧元素(旧元素会被转化为int32_t类型存储)。之后从末至尾将元素进行提升并后移。但不支持降级操作。优点如下:
- 提升灵活性,同一块内存可同时保存int64_t,int32_t和int16_t类型的元素。
- 节约内存,若不能升级内存,想在同一块内存中保存3种元素,则必须以最大的类型来开辟区间。
四.部分源码
1.根据值判断需要采用的编码方式及获取第pos个值
#define INTSET_ENC_INT16 (sizeof(int16_t))
#define INTSET_ENC_INT32 (sizeof(int32_t))
#define INTSET_ENC_INT64 (sizeof(int64_t))
// 根据要存储的数值,判断最小满足的编码方式
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;
}
// 根据编码方式,获取整数集合中第pos个值
static int64_t _intsetGetEncoded(intset *is, int pos, uint8_t enc) {
int64_t v64;
int32_t v32;
int16_t v16;
if (enc == INTSET_ENC_INT64) {
// ((int64_t*)is->contents)+pos 指向第pos个元素的起始位置
memcpy(&v64,((int64_t*)is->contents)+pos,sizeof(v64));
memrev64ifbe(&v64);
return v64;
} else if (enc == INTSET_ENC_INT32) {
memcpy(&v32,((int32_t*)is->contents)+pos,sizeof(v32));
memrev32ifbe(&v32);
return v32;
} else {
memcpy(&v16,((int16_t*)is->contents)+pos,sizeof(v16));
memrev16ifbe(&v16);
return v16;
}
}
2.在整数集合中搜索值
由于整数集合是有序集合,因此进行折半查找,此处不予赘述。
3.在整数集合中添加数据(提升编码)
当向整数集合中添加编码时,若当前编码大小不可容纳,则需要提升编码方式。
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)) {
/* This always succeeds, so we don't need to curry *success. */
return intsetUpgradeAndAdd(is,value);
} else {
// 查找该值是否已存在(整数集合无重复元素),若不存在则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); // 将pos位起的元素都后移一位
}
_intsetSet(is,pos,value); // 将value放至第pos位
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;
// 设置新的编码类型
is->encoding = intrev32ifbe(newenc);
is = intsetResize(is,intrev32ifbe(is->length)+1); // 根据新编码方式扩展整数集合
/* Upgrade back-to-front so we don't overwrite values.
* Note that the "prepend" variable is used to make sure we have an empty
* space at either the beginning or the end of the intset. */
while(length--)
_intsetSet(is,length+prepend,_intsetGetEncoded(is,length,curenc));
/* Set the value at the beginning or the end. */
if (prepend)
_intsetSet(is,0,value);
else
_intsetSet(is,intrev32ifbe(is->length),value);
is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
return is;
}