基本概述
IntSet是Redis中set集合的一种实现方式,基于
整数数组来实现
,并且具备长度可变、有序
等特征。
结构如下:
typedef struct intset {
uint32_t encoding; /* 编码方式,支持存放16位、32位、64位整数(4字节32比特位)*/
uint32_t length; /* 元素个数 */
int8_t contents[]; /* 整数数组,保存集合数据*/
} intset;
其中的encoding包含三种模式,表示存储的整数大小
不同:
/* 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 */
为了方便查找
,Redis会将intset中所有的整数按照升序依次保存在contents数组中,结构如图:(数组中元素大小要固定,方便寻址、快速定位元素)
上述,数组中每个数字都在int16_t的范围内,因此采用的编码方式是INTSET_ENC_INT16,每部分占用的字节大小
- encoding:4字节
- length:4字节
- contents:2字节 * 3 = 6字节
IntSet升级
现在,假设有一个intset,元素为{5,10,20},采用的编码是INTSET_ENC_INT16,则每个整数占2字节:
此时,向该其中添加一个数字:50000,这个数字超出了int16_t的范围,intset会自动升级编码方式到合适的大小。
升级流程:
① 升级编码
为INTSET_ENC_INT32, 每个整数占4字节,并按照新的编码方式及元素个数扩容数组
② 倒序
依次将数组中的元素拷贝到扩容后的正确位置(元素下标以及元素所占字节,倒序防止覆盖原有数据)
③ 将待添加的元素放入数组末尾
④ 最后,将inset的encoding属性
改为INTSET_ENC_INT32,将length
属性改为4
IntSet新增流程
源码如下:
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)) { // 二分查找
//如果找到了,则无需插入,直接结束并返回失败(set)
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;
}
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(is,length+prepend,_intsetGetEncoded(is,length,curenc));
// value 值大于0,新元素放在末尾;value值小于0,最前面空出一个位置,插入队首(判断元素插入队首还是队尾)
if (prepend)
_intsetSet(is,0,value);
else
_intsetSet(is,intrev32ifbe(is->length),value);
// 修改数组长度
is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
return is;
}
优点
Intset可以看做是特殊的整数数组,具备一些特点:
① Redis会确保Intset中的元素唯一、有序
② 具备类型(整数编码类型)升级机制
,可以节省内存空间
③ 底层采用二分查找
方式来查询(少量数据比较合适,数据量大了,二分查找会影响性能)