整数集合
整数集合的概念
整数集合是redis用来保存整数值的集合的一种数据结构,可以用来保存int类型数据,并且可以保证不会出现重复元素。因此当一个集合中只包含整数元素且数量不多的时候,redis会选择使用整数集合作为底层实现。
整数集合在redis源码中的定义如下
typedef struct intset {
// 编码方式
uint32_t encoding;
// 集合的长度
uint32_t length;
// 保存整数元素的数组
int8_t contents[];
} intset;
contents是整数集合的底层实现,保存了整数集合的每一个元素,每个元素在该数组中从小到大有序排列,并且不重复(如何保证有序性和唯一性我们后面讨论插入的时候在说)。contens数组虽然声明为int8_t类型,但其实真正的类型取决于encoding的值。在操作一个整数集合的时候,会首先获取encoding的值。代码如下
#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;
}
数据类型 | 类型 | 字节数 | 最小值 | 最大值 |
---|---|---|---|---|
int8_t | char | 1 | -128 | 127 |
int16_t | short | 2 | -32,768 | 32,767 |
int32_t | int | 4 | -2,147,483,648 | -2,147,483,648 |
int64_t | long | 8 | -9,223,372,036,854,775,808 | 9,223,372,036,854,775,807 |
整数集合的升级
假设我们有一个整数集合,他现在存储的都是int16_t类型的整数,而现在我们需要向其中添加一个int32_t类型的整数。这个时候因为C语言是静态性语言,我们不能将两种不同类型的值放在同一个数据结构中,这样会带来类型错误,所以我们就需要将原有的数据结构做相应的改变,让他能存储更大的整数,同时将原有的整数类型也改变为更大的类型,这个过程就是整数集合的升级(upgrade)
。代码如下:
// intset.c
/* 升级整数集合,让他能存放更大的数 */
static intset *intsetUpgradeAndAdd(intset *is, int64_t value) {
// 获取当前的编码方式,intrev32ifbe是大小端转换函数
uint8_t curenc = intrev32ifbe(is->encoding);
// 插入的新值需要的编码方式
uint8_t newenc = _intsetValueEncoding(value);
// 当前整数集合的长度
int length = intrev32ifbe(is->length);
// 前置标志,代码能到这一步,证明升级是有必要的,即value的值超出了原有的范围,所以他一定是要么在最后面,要么小于0,在最前面
int prepend = value < 0 ? 1 : 0;
// 设置原来整数集合的编码方式为最新值
is->encoding = intrev32ifbe(newenc);
// 调整原来的整数集合,主要为根据新的长度计算出升级需要的内从空间,然后修改原来整数集合分配到的内存块的大小
is = intsetResize(is,intrev32ifbe(is->length)+1);
// 从后往前升级,保证原来的值不会被覆盖
while(length--)
_intsetSet(is,length+prepend,_intsetGetEncoded(is,length,curenc));
// 如果新值value<0,就插入到第一个位置,否则插入到最后一个位置
if (prepend)
_intsetSet(is,0,value);
else
_intsetSet(is,intrev32ifbe(is->length),value);
// 更新整数集合的长度
is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
return is;
}
static void _intsetSet(intset *is, int pos, int64_t value) {
uint32_t encoding = intrev32ifbe(is->encoding);
if (encoding == INTSET_ENC_INT64) {
((int64_t*)is->contents)[pos] = value;
memrev64ifbe(((int64_t*)is->contents)+pos);
} else if (encoding == INTSET_ENC_INT32) {
((int32_t*)is->contents)[pos] = value;
memrev32ifbe(((int32_t*)is->contents)+pos);
} else {
((int16_t*)is->contents)[pos] = value;
memrev16ifbe(((int16_t*)is->contents)+pos);
}
}
升级的好处:
- 因为整数集合可以自动升级,所以我们可以随意的往里面存放int16_t、int32_t、int64_t等不同类型的整数,而不会发生类型错误。
- 节约内存,因为如果没有升级,我们要在一个数组里面存放不同类型的数,就只能在一开始把数组定义成int64_t类型的,而有时候我们其实只在里面存储了int16_t类型的,就造成了内存浪费。而有了升级功能以后,我们就可以在需要的时候在去扩展内存,节约了资源。整数集合只支持升级,无法降级。
插入操作
intset *intsetAdd(intset *is, int64_t value, uint8_t *success) {
// 获取value需要的编码方式
uint8_t valenc = _intsetValueEncoding(value);
uint32_t pos;
//是否插入成功的标志
if (success) *success = 1;
// 如果编码方式超出了原来的界限,就进行升级
if (valenc > intrev32ifbe(is->encoding)) {
return intsetUpgradeAndAdd(is,value);
} else {
// 插入value前,先查找该值是否已经存在,如果存在,就设置插入成功的标志为失败,结束程序
if (intsetSearch(is,value,&pos)) {
if (success) *success = 0;
return is;
}
// 调整整数集合,修改原来集合所用内存块大小,分配空间
is = intsetResize(is,intrev32ifbe(is->length)+1);
// 确定value要插入的位置后,将原来在这个位置的元素统一向后移
if (pos < intrev32ifbe(is->length)) intsetMoveTail(is,pos,pos+1);
}
// 把value插入到pos标记的位置
_intsetSet(is,pos,value);
// 更新整数集合的长度
is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
return is;
}
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;
// 如果该整数集合是空的,那么设置pos为0,说明value可以插入到第一个位置,结束程序,返回0。
if (intrev32ifbe(is->length) == 0) {
if (pos) *pos = 0;
return 0;
} else {
// 如果value大于集合中最后一个元素,那么也不用去查找,设置pos为集合的长度,说明value可以插入到最后一个位置。
if (value > _intsetGet(is,intrev32ifbe(is->length)-1)) {
if (pos) *pos = intrev32ifbe(is->length);
return 0;
// 如果value小于集合中第一个元素,那么也不用去查找,设置pos为0,说明value可以插入到第一个位置。
} else if (value < _intsetGet(is,0)) {
if (pos) *pos = 0;
return 0;
}
}
// 因为整数集合是有序的,所以这里使用二分查找来定位value的位置
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;
}
}
// 检查是否找到了value,设置pos的位置,结束程序。
if (value == cur) {
if (pos) *pos = mid;
return 1;
} else {
if (pos) *pos = min;
return 0;
}
}
static void intsetMoveTail(intset *is, uint32_t from, uint32_t to) {
void *src, *dst;
uint32_t bytes = intrev32ifbe(is->length)-from;
uint32_t encoding = intrev32ifbe(is->encoding);
if (encoding == INTSET_ENC_INT64) {
src = (int64_t*)is->contents+from;
dst = (int64_t*)is->contents+to;
bytes *= sizeof(int64_t);
} else if (encoding == INTSET_ENC_INT32) {
src = (int32_t*)is->contents+from;
dst = (int32_t*)is->contents+to;
bytes *= sizeof(int32_t);
} else {
src = (int16_t*)is->contents+from;
dst = (int16_t*)is->contents+to;
bytes *= sizeof(int16_t);
}
//批量移动数据
memmove(dst,src,bytes);
}
今天就到这儿了,剩下的闲了在续。
参考资料:《Redis设计与实现》 黄建宏