InSet 是 Redis 中 set集合的一种实现方式,是基于整数数组来实现的,而且具有变换长度和有序等特征。
结构
typedef struct intset {
uint32_t encoding;
uint32_t length;
int8_t contents[];
} intset;
其中分别含义是:
- encoding:编码方式,支持存放16位、32位、64位整数
/* Note that these encodings are ordered, so:
* INTSET_ENC_INT16 < INTSET_ENC_INT32 < INTSET_ENC_INT64. */
#define INTSET_ENC_INT16 (sizeof(int16_t)) /* 2字节整数,范围类似java的short*/
#define INTSET_ENC_INT32 (sizeof(int32_t)) /* 4字节整数,范围类似java的int */
#define INTSET_ENC_INT64 (sizeof(int64_t)) /* 8字节整数,范围类似java的long */
- length:表示集合中的元素个数
- contents:整数数组,用于保存集合数据
为了方便查找,Redis会将intset中所有的整数按照顺序依次排在contents数组中。结构如图
其中数组的下标为encoding中标记的编码大小*前面的个数,便于查询即starPtr * (sizeof(int16) * index)
图中采用的编码为INTSET_ENC_INT16,每个数字都在int16_t,即每个数组元素的大小是2个字节,每部分占用的字节大小如下:
- encoding:4个字节
- length:4个字节
- contents: 2字节 * 3 = 6个字节
编码升级
加入有一个intset,元素为{5,10,20},采用的编码方式为INTSET_ENC_INT16,则每个整数占两个字节。如图
如果我们又向intset中添加了一个整数50000,这个数超出了int16_t 的范围,intset 会自动将编码升级为合适的大小。
流程如下:
- 升级编码为INTSET_ENC_INT32,每个整数占4个字节,并按照新的编码方式以元素扩容数组。
- 倒序依次将数组中元素拷贝到扩容后的正确位置
(即先将20拷贝到8到12 的位置、然后将10拷贝到4到8的位置。倒序是为了防止正序将后面的元素覆盖)
- 将带待添加的数组放到末尾(因为添加的数组编码超过了当前编码,肯定大于当前的所有整数)
- 最后,将encoding 的属性改为 INTSET_ENC_INT32,将length属性改为4
InSet新增元素源码
intset *intsetAdd(intset *is, int64_t value, uint8_t *success) {
uint8_t valenc = _intsetValueEncoding(value);// 获取当前值编码
uint32_t pos; // 要插入的位置
if (success) *success = 1;
// 判断编码是不是超过了当前intset的编码
if (valenc > intrev32ifbe(is->encoding)) {
// 超出编码,需要升级
return intsetUpgradeAndAdd(is,value);
} else {
// 在当前intset中查找值与value一样的元素的角标pos
if (intsetSearch(is,value,&pos)) {
if (success) *success = 0; //如果找到了,则无需插入,直接结束并返回失败
return is;
}
// 数组扩容
is = intsetResize(is,intrev32ifbe(is->length)+1);
// 移动数组中pos之后的元素到pos+1,给新元素腾出空间
if (pos < intrev32ifbe(is->length)) intsetMoveTail(is,pos,pos+1);
}
// 插入新元素
_intsetSet(is,pos,value);
// 重置元素长度
is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
return is;
}
大致步骤如下:
- 获取当前的编码
- 判断当前元素要插入的位置(角标)
- 判断要插入元素的编码是否超过当前的编码
- 超出编码则调用升级编码的intsetUpgradeAndAdd()方法
- 没有超出编码则查找是否有个当前要插入元素一致的角标
- 如果有就直接返回失败,保证元素的唯一性
- 没有则调用数组扩容的intsetResize()方法、然后将角标pos后的元素同一往后移一个位置(pos + 1)
- 调用插入元素方法_intsetSet()
- 重置intset的 length 元素+1
- 其中查找对应位置的方法底层采用了二分查找
InSet升级编码源码
static intset *intsetUpgradeAndAdd(intset *is, int64_t value) {
// 获取当前intset编码
uint8_t curenc = intrev32ifbe(is->encoding);
// 获取新编码
uint8_t newenc = _intsetValueEncoding(value);
// 获取元素个数
int length = intrev32ifbe(is->length);
// 判断新元素是大于0还是小于0 ,小于0插入队首、大于0插入队尾
int prepend = value < 0 ? 1 : 0;
// 重置编码为新编码
is->encoding = intrev32ifbe(newenc);
// 重置数组大小
is = intsetResize(is,intrev32ifbe(is->length)+1);
// 倒序遍历,逐个搬运元素到新的位置,_intsetGetEncoded按照旧编码方式查找旧元素
while(length--) // _intsetSet按照新编码方式插入新元素
_intsetSet(is,length+prepend,_intsetGetEncoded(is,length,curenc));
/* 插入新元素,prepend决定是队首还是队尾*/
if (prepend)
_intsetSet(is,0,value);
else
_intsetSet(is,intrev32ifbe(is->length),value);
// 修改数组长度
is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
return is;
}
步骤如下:
- 获取当前数组的编码和要插入元素的编码大小
- 获取当前数组元素个数
- 判断要插入元素大于零还是小于零来赋值给prepend状态码,其中1 表示插入队首 、0表示插入队尾(因为IntSet 是一个有序的集合,所以调用intsetUpgradeAndAdd()证明要么在队首插入要么在队尾插入)
- 而后倒序遍历,将数组元素拷贝到升级后对应的位置
- 插入新元素
- 修改 intSet 中 length 属性
总结
IntSet可以认为是特殊的整数数组,有如下特点:
- IntSet会保证数组中的元素唯一、有序
- 具备类型升级机制、可以在一定程度上节省内存
- 底层采用二分查找来寻找对应角标、一定程度加快了查找效率