intset 结构体
typedef struct intset {
uint32_t encoding; // 当前结构内所有元素的类型
uint32_t length; // contents 中元素个数,即整数集合中元素个数
int8_t contents[]; // 整数集合中存储的元素
} intset;
intset适用于集合全部是整数且个数不多的情况,如果遇到这两种情况,Redis集合类型的底层结构会由intset转换为hashtable:
(1)当元素个数超过一定数量时,具体数值可通过配置项set-max-intset-entries 来设置
(2)当增加的元素是非整型时
intset查找
/* 查询元素 */
uint8_t intsetFind(intset *is, int64_t value) {
uint8_t valenc = _intsetValueEncoding(value); // 获取元素类型编码
// 如果当前元素的编码大小不超过整数集合的编码 && 存在于该集合
return valenc <= intrev32ifbe(is->encoding) && intsetSearch(is,value,NULL);
}
/*
* 搜索“值”的位置。 找到值后返回1,并将“ pos”设置为该值在整数集中的位置。
* 当整数中不存在该值时,返回0并将“ pos”设置为可以插入“ value”的位置。
*/
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) {
if (pos) *pos = 0;
return 0;
} else {
/*
* 假设这样一种情况,没有找到该值(大于最大值,小于最小值),但是知道插入位置
* 整数集合是默认递增有序的,如果待插入整数大于最后一个整数元素,就应该将其插入到尾部
* 如果小于第一个整数元素,就应该将其放到首部位置
* */
if (value > _intsetGet(is,max)) {
if (pos) *pos = intrev32ifbe(is->length);
return 0;
} else if (value < _intsetGet(is,0)) {
if (pos) *pos = 0;
return 0;
}
}
// 整数集合默认有序的,使用二分法进行查找
while(max >= min) {
mid = ((unsigned int)min + (unsigned int)max) >> 1; // 右移1位即/2
cur = _intsetGet(is,mid); // 获取中间位置的整数
if (value > cur) {
min = mid+1;
} else if (value < cur) {
max = mid-1;
} else {
break;
}
}
// 检查是否找到
if (value == cur) {
if (pos) *pos = mid;
return 1;
} else {
if (pos) *pos = min;
return 0;
}
}
查找主要分为以下几步:
(1)先判断其编码方式是否超过当前intset的编码方式,没有的话,就调用intsetSearch进行查找
(2)intsetSearch中会先判断待查找值是否在最大值和最小值之间,若存在就利用intset集合是从小到大有序(而且不重复)的进行二分查找;
(3)intsetSearch中如果找到该元素,pos就设置为该元素所在的位置,如果没找到就pos就设置为可以在intset中的插入位置,利用return 0或者1进行区分;如果查找元素大于最大值就放在集合尾部,小于最小值就放在集合首部,pos的值分别设置为is->length和0
intset添加
/*
* 向整数集合中插入一个整数
* (1) 查找集合中是否存在待插入的元素
* (2) 若不存在,扩容
* (3) 将待插入元素以后的整数全部后移
* (4) 插入整数*/
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 {
/*
* 查找整数在集合中的位置,如果发现整数已存在集合中的话,就不再插入
* 如果不存在则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);
}
//设置该整数
_intsetSet(is,pos,value);
is->length = intrev32ifbe(intrev32ifbe(is->length)+1); // 调整整数集合长度
return is;
}
/*
* 更新待插入元素的类型编码并插入新节点
* 如果value参数的值超过了当前编码 ,就需要更新类型编码*/
static intset *intsetUpgradeAndAdd(intset *is, int64_t value) {
uint8_t curenc = intrev32ifbe(is->encoding); // 当前编码类型
uint8_t newenc = _intsetValueEncoding(value); // 新的编码类型,根据value值获取
int length = intrev32ifbe(is->length); // 获取集合长度
// 因为value一定超过了编码的限制,所以看value是大于0还是小于0以此决定value放置在content[0]还是content[length]
// 如果进行了升级,那么不是最大值放在最后,就是最小值放在最开始的位置
int prepend = value < 0 ? 1 : 0;
/* First set new encoding and resize */
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--)
//以curenc为编码,倒序取出所有值并赋值给新的位置
_intsetSet(is,length+prepend,_intsetGetEncoded(is,length,curenc));
/* Set the value at the beginning or the end.
* 根据大于0还是小于0,放置在相应的位置 */
if (prepend)
_intsetSet(is,0,value);
else
_intsetSet(is,intrev32ifbe(is->length),value);
// 更新集合的长度
is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
return is;
}
// 移动元素,from节点之后的元素全部移动,直到to节点,底层使用memmove
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);
// 根据不同的编码
// src 记录移动开始的位置
// dst 记录移动结束的位置
// bytes 计算要移动的字节数
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不需要假定两块内存区域没有重叠
memmove(dst,src,bytes);
}
intset 添加主要分为以下几步:
(1)判断待添加元素的编码是否超过当前intset的编码;超过的话就需要升级;
(2)没有超过的话,调用intsetSearch查询是否已存在该元素,并确定其可以放置的位置;
(3)可以插入的话并且待放置的位置在集合元素中间(不是放置在最后),就需要将其之后的元素全部后移,这一步是调用intsetMoveTail来实现的,底层是利用memmove
(4)如果需要升级的话,调用intsetUpgradeAndAdd来实现,会利用新整数的编码对该intset进行扩容和调整编码类型,该intset原先的所有元素均根据新的编码从后向前进行调整,防止被覆盖;如果需要升级则说明待插入的整数要么大于最大值放置在末尾,要么是最小值放置在首部;最后插入该元素并更新intset的长度
intset删除
/* 删除元素,不会引发降级 */
intset *intsetRemove(intset *is, int64_t value, int *success) {
uint8_t valenc = _intsetValueEncoding(value); // 获取待删除整数集合编码
uint32_t pos;
if (success) *success = 0;
// 查找该元素并返回所在位置
// 待删除元素的编码必须小于集合编码并且能找到该元素
if (valenc <= intrev32ifbe(is->encoding) && intsetSearch(is,value,&pos)) {
uint32_t len = intrev32ifbe(is->length);
/* We know we can delete */
if (success) *success = 1; // 可以删除的标记
/* Overwrite value with tail and update length */
if (pos < (len-1)) intsetMoveTail(is,pos+1,pos); // 将删除节点之后的元素全部向前移动一个类型长度
is = intsetResize(is,len-1); // 重置集合空间
is->length = intrev32ifbe(len-1); // 调整整数集合元素个数
}
return is;
}
intset 删除整数时先判断待删除元素的类型编码小于集合并且能在集合中找到该元素,找到的话并且不是位于集合最后,需要将该元素之后的所有元素全部前移一个数据类型
插入可能会引起升级,但删除不会造成降级