知识点
- 整数集合是集合键的底层实现之一
- 整数集合的底层实现为数组,这个数组以有序、无重复的方式保存集合元素,在有需要时,redis会根据新添加元素的类型,改变这个数组的类型
- 升级操作为整数集合带来了操作上的灵活性,并且尽可能地节约了内存
- 整数集合只支持升级操作,不支持降级操作
typedef struct intset {
//encoding 指定了编码方式,有 INTSET_ENC_INT16、INTSET_ENC_INT32、INTSET_ENC_INT64 三种。三者表示的范围是 16位整数、32位整数 以及 64位整数。
uint32_t encoding;
//length 存储了整数集合的元素个数
uint32_t length;
//整数集合中的元素按照从小到大的顺序在 contents 中排列起来
int8_t contents[];
} intset;
编码升级
- 当前编码方式不足以存储更大位数的整数时,需要升级编码。举个例子,下图所示的四个数字都在 [ -32768, 32767 ] 范围内,所以采用 INTSET_ENC_INT16 编码即可。contents 的数组长度为 sizeof(int16_t) * 4 = 2 * 4 = 8 个字节 ( 即64个二进制位 )。
- 然后我们插入一个数,它的值为 32768,比 INT16_MAX 大1,所以它需要采用 INTSET_ENC_INT32 编码,而整数集合中所有的数的编码需要保持一致。那么,所有数的编码都需要转为 INTSET_ENC_INT32 编码。这就是 “升级”。
- 升级完后,contents 数组的长度变为 sizeof(int32_t) * 5 = 4 * 5 = 20 个字节 ( 即160个二进制位 )。而且每个元素占用的内存都扩大一倍,所在的相对位置也发生了变化,导致所有的元素都需要往高位内存迁移。
- 那我们一开始就把所有的整数集合都用 INTSET_ENC_INT64 来编码不就好了,还省得麻烦。原因是 Redis 设计 intset 的初衷还是为了节省内存,设想一个集合的元素永远都不会超过 16位 整数,那么用 64位整数的话,相当于浪费了 3倍 的内存。
创建集合
创建一个整数集合 intsetNew,实现在 intset.c 中:
- 初始创建的整数集合为空集合,用 zmalloc 进行内存分配后,定义编码为 INTSET_ENC_INT16,这样可以使内存尽量小。
intset *intsetNew(void) {
intset *is = zmalloc(sizeof(intset));
is->encoding = intrev32ifbe(INTSET_ENC_INT16);
is->length = 0;
return is;
}
元素设置
static void _intsetSet(intset *is, int pos, int64_t value) {
uint32_t encoding = intrev32ifbe(is->encoding); /* a */
if (encoding == INTSET_ENC_INT64) {
((int64_t*)is->contents)[pos] = value; /* b */
memrev64ifbe(((int64_t*)is->contents)+pos); /* c */
} 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);
}
}
元素查找
- 由于整数集合是有序集合,所以查找某个元素是否在整数集合中,Redis 采用的是二分查找。intsetSearch 实现如下:
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) {
//整数集合为空,返回0表示查找失败
if (pos) *pos = 0;
return 0;
} else {
//value 的值比整数集合中的最大值还大,或者比最小值还小,则返回0表示查找失败
if (value > _intsetGet(is,intrev32ifbe(is->length)-1)) {
if (pos) *pos = intrev32ifbe(is->length);
return 0;
} else if (value < _intsetGet(is,0)) {
if (pos) *pos = 0;
return 0;
}
}
while(max >= min) {
//执行二分查找,将找到的值存在 cur 中
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;
}
}
//如果找到则返回1,表示查找成功,并且将 pos 设置为 mid 并返回;如果没找到则返回一个需要插入的位置
if (value == cur) {
if (pos) *pos = mid;
return 1;
} else {
if (pos) *pos = min;
return 0;
}
}
内存分配
static intset *intsetResize(intset *is, uint32_t len) {
uint32_t size = len*intrev32ifbe(is->encoding);
is = zrealloc(is,sizeof(intset)+size);
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);
// curenc 记录升级前的编码,newenc 记录升级后的编码
int prepend = value < 0 ? 1 : 0;
is->encoding = intrev32ifbe(newenc);
//将整数集合 is 的编码设置成新的编码后,进行内存重分配
is = intsetResize(is,intrev32ifbe(is->length)+1);
while(length--)
//获取原先内存中的数据,设置到新内存中
_intsetSet(is,length+prepend,_intsetGetEncoded(is,length,curenc));
if (prepend)
_intsetSet(is,0,value);
else
// 当插入的值 value 为负数的时候,为了保证集合的有序性,需要插入到 contents 的头部;反之,插入到尾部;当 value 为负数时 prepend 为1,这样就可以保证在内存拷贝的时候将第 0 个位置留空
_intsetSet(is,intrev32ifbe(is->length),value);
is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
return is;
}
元素插入
intset *intsetAdd(intset *is, int64_t value, uint8_t *success) {
uint8_t valenc = _intsetValueEncoding(value);
uint32_t pos;
if (success) *success = 1;
// 插入的数值 value 的内存编码大于现有集合的编码,直接调用 intsetUpgradeAndAdd 进行编码升级
if (valenc > intrev32ifbe(is->encoding)) {
return intsetUpgradeAndAdd(is,value);
} else {
if (intsetSearch(is,value,&pos)) {
//集合元素是不重复的,如果 intsetSearch 能够找到,则将 success 置为0,表示此次插入失败
if (success) *success = 0;
return is;
}
//如果 intsetSearch 找不到,将 intset 进行内存重分配,即 长度 加 1
is = intsetResize(is,intrev32ifbe(is->length)+1);
//pos 为 intsetSearch 过程中找到的 value 将要插入的位置,我们将 pos 以后的内存向后移动1个单位
if (pos < intrev32ifbe(is->length)) intsetMoveTail(is,pos,pos+1);
}
_intsetSet(is,pos,value);
//调用 _intsetSet 将 value 的值设置到 pos 的位置上,然后给成员变量 length 加 1
is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
return is;
}
元素删除
intset *intsetRemove(intset *is, int64_t value, int *success) {
uint8_t valenc = _intsetValueEncoding(value);
uint32_t pos;
if (success) *success = 0;
//当整数集合中存在 value 这个元素时才能执行删除操作
if (valenc <= intrev32ifbe(is->encoding) && intsetSearch(is,value,&pos)) {
uint32_t len = intrev32ifbe(is->length);
if (success) *success = 1;
//如果能通过 intsetSearch 找到元素,那么它的位置就在 pos 上
if (pos < (len-1)) intsetMoveTail(is,pos+1,pos);
//intsetResize 重新分配内存,并且将集合长度减1
is = intsetResize(is,len-1);
is->length = intrev32ifbe(len-1);
}
return is;
}
网友评论