整数集合是集合键的底层实现之一,当一个集合只包含整数值元素,并且这个集合的元素数量不多时,Redis就会使用整数集合键的底层实现。
typedef struct intset {
//编码方式
uint32_t encoding;
//集合包含的元素数量
uint32_t length;
//保存元素的数组
int8_t contents[];
} intset;
contents数组是整数集合的底层实现:整数集合的每个元素都是contents数组的一个数据项(item),各个项在数组中按值得大小从小到大有序得排列,并且数组中不包含任何重复项。
length属性记录了整数集合包含得元素数量,也即是contents数组得长度。
虽然intset结构将contents属性声明为int8_t类型的数组,但实际上contents并不保存任何int8_t类型的值,contents数组得真正类型取决于encoding属性的值:
intset *intsetNew(void) {
intset *is = zmalloc(sizeof(intset));
is->encoding = intrev32ifbe(INTSET_ENC_INT16);
is->length = 0;
return is;
}
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)) {
//进行升级操作并插入元素
return intsetUpgradeAndAdd(is,value);
} else {
//二分查找当前元素,看是否存在
//如果存在,那么将*success置为0,并返回未经修改的整数集合
//如果不存在,那么可以插入value的位置将被保存在pos指针中
if (intsetSearch(is,value,&pos)) {
if (success) *success = 0;
return is;
}
//重新调整集合的空间大小
is = intsetResize(is,intrev32ifbe(is->length)+1);
//如果新元素不是被添加到底层数组的末尾
//则对现有的元素进行移动,空出pos上的位置,用于设置新值
if (pos < intrev32ifbe(is->length)) intsetMoveTail(is,pos,pos+1);
}
// 将新值设置到底层数组的指定位置中
_intsetSet(is,pos,value);
//修改整数集合的长度
is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
return is;
}
/* Upgrades the intset to a larger encoding and inserts the given integer.
*
* 根据值 value 所使用的编码方式,对整数集合的编码进行升级,
* 并将值 value 添加到升级后的整数集合中。
*
* 返回值:添加新元素之后的整数集合
*
* T = O(N)
*/
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);
//将所有元素从旧的编码集合转换到新的编码集合里
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_ENC_INT16编码的整数集合,集合中包含三个int16_t类型的元素。
因为每个元素都占用16位空间,所以整数集合底层的数组大小为3*16=48位。
现在,假设我们要将类型为int32_t的整数值65535添加到整数集合里面,因为65535的类型int32_t比整数集合当前所有元素的类型都要长,所以在这之前要进行升级。
注意:因为引发升级的新元素的长度总是比整数集合现有所有元素的长度都大,所以这个元素的值要么大于所有现有元素,要么小于所有现有元素(负数)
//重新调整集合的空间大小
static intset *intsetResize(intset *is, uint32_t len) {
uint32_t size = len*intrev32ifbe(is->encoding);
is = zrealloc(is,sizeof(intset)+size);
return is;
}
//根据给定的编码方式,返回集合的底层数组再pos索引上的元素
//T = O(1)
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) {
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;
}
}
//根据集合的编码方式,将底层数组在 pos 位置上的值设为 value
//T = O(1)
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);
}
}