整数集合使用场景
整数集合(intset)是集合键的底层实现之一: 当一个集合只包含整数值元素, 并且这个集合的元素数量不多时, Redis 就会使用整数集合作为集合键的底层实现。
整数集合(intset)是 Redis 用于保存整数值的集合抽象数据结构, 它可以保存类型为 int16_t 、 int32_t 或者 int64_t 的整数值, 并且保证集合中不会出现重复元素。
整数集合的结构
//每个 intset.h/intset 结构表示一个整数集合:
typedef struct intset {
// 编码方式
uint32_t encoding;
// 集合包含的元素数量
uint32_t length;
// 保存元素的数组
int8_t contents[];
} intset;
注意:
1.contents 数组是整数集合的底层实现: 整数集合的每个元素都是 contents 数组的一个数组项(item), 各个项在数组中按值的大小从小到大有序地排列, 并且数组中不包含任何重复项。
2.contents 数组的真正类型取决于 encoding 属性的值,对应如下
encoding | content类型 | 范围 |
---|---|---|
INTSET_ENC_INT16 | int16_t | -32,768 ~ 32,767 |
INTSET_ENC_INT32 | int32_t | -2,147,483,648 ~ 2,147,483,647 |
INTSET_ENC_INT64 | int64_t | -9,223,372,036,854,775,808 ~ 9,223,372,036,854,775,807 |
升级
因为 C 语言是静态类型语言, 为了避免类型错误, 我们通常不会将两种不同类型的值放在同一个数据结构里面
每当我们要将一个新元素添加到整数集合里面, 并且新元素的类型比整数集合现有所有元素的类型都要长时, 整数集合需要先进行升级(upgrade), 然后才能将新元素添加到整数集合里面。
降级
intset 不支持降级操作,也就是说 对数组进行了升级后,编码就会一直保持升级后的状态。
源码
/* 创建一个intset */
intset *intsetNew(void) { //创建一个空集合
intset *is = zmalloc(sizeof(intset)); //分配空间
is->encoding = intrev32ifbe(INTSET_ENC_INT16); //设置编码方式
is->length = 0; //集合为空
return is;
}
inset写入逻辑
/* Insert an integer in the intset */
intset *intsetAdd(intset *is, int64_t value, uint8_t *success) {//将value添加到is集合中,如果成功success被设置为1否则为0
uint8_t valenc = _intsetValueEncoding(value); //获得value适合的编码类型
uint32_t pos;
if (success) *success = 1; //设置success默认为1
/* Upgrade encoding if necessary. If we need to upgrade, we know that
* this value should be either appended (if > 0) or prepended (if < 0),
* because it lies outside the range of existing values. */
if (valenc > intrev32ifbe(is->encoding)) { //如果value的编码类型大于集合的编码类型,需要进行升级操作
/* This always succeeds, so we don't need to curry *success. */
return intsetUpgradeAndAdd(is,value); //升级集合,并且将value加入集合,一定成功
} else { //不需要升级写入直接操作
/* Abort if the value is already present in the set.
* This call will populate "pos" with the right position to insert
* the value when it cannot be found. */
if (intsetSearch(is,value,&pos)) { //查找value,若果value已经存在,intsetSearch返回1,如果不存在,pos保存value可以插入的位置
if (success) *success = 0; //value存在,success设置为0
return is;
}
//value在集合中不存在,且pos保存可以插入的位置
is = intsetResize(is,intrev32ifbe(is->length)+1); //调整集合大小
if (pos < intrev32ifbe(is->length)) intsetMoveTail(is,pos,pos+1); //如果pos不是在数组末尾则要移动调整集合
}
_intsetSet(is,pos,value); //设置pos下标的值为value
is->length = intrev32ifbe(intrev32ifbe(is->length)+1); //集合节点数量加1
return is;
}
/* Upgrades the intset to a larger encoding and inserts the given integer. */
//插入时候需要升级操作并且写入
static intset *intsetUpgradeAndAdd(intset *is, int64_t value) { //根据value的编码方式,对整数集合is的编码格式升级
uint8_t curenc = intrev32ifbe(is->encoding); //当前集合的编码方式
uint8_t newenc = _intsetValueEncoding(value); //得到value合适的编码方式
int length = intrev32ifbe(is->length); //集合元素数量
int prepend = value < 0 ? 1 : 0; //如果value小于0,则要将value添加到数组最前端,因此为移动1个编码长度
//集合的编码格式要升级,也就是内存增大
//因为 value 的编码比集合原有的其他元素的编码都要大,所以value如果是负数,就是最小值,如果是正数则是最大值
//索引value要么放在数组集合的最前端,要么最后端,根据prepend判断
/* First set new encoding and resize */
is->encoding = intrev32ifbe(newenc); //更新集合is的编码方式
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. */
//_intsetGetEncoded()得到下标为length的值
//_intsetSet设置下标为prepend+length的值为_intsetGetEncoded返回的值
//但是,编码格式已经发生改变,数组元素没变但是内存大小改变
//移动每一个元素的位置从之前旧的位置移动到新的位置
while(length--)
_intsetSet(is,length+prepend,_intsetGetEncoded(is,length,curenc));
/* Set the value at the beginning or the end. */
if (prepend)
_intsetSet(is,0,value); //为负数放在队列的第一个
else
_intsetSet(is,intrev32ifbe(is->length),value); //value为正数,设置最末尾+1的值为value
is->length = intrev32ifbe(intrev32ifbe(is->length)+1); //数组元素加1
return is;
}
参考文件:
http://redisbook.com/preview/intset/upgrade.html
https://github.com/menwengit/redis_source_annotation