redis的整数集合实质上是动态的数组。reids的整数集合是可以根据整数的值,自动选择用什么长度来存储的。例如:如果插入的值可以用int16_t类型来保存,那所有的元素都可以用int16_t类型来保存。所以可以看出保存的类型应该有这几种:
#define INTSET_ENC_INT16 (sizeof(int16_t))
#define INTSET_ENC_INT32 (sizeof(int32_t))
#define INTSET_ENC_INT64 (sizeof(int64_t))
其实还有种int8_t,但是redis在创建整数集合的时候就默认最小使用int16_t,所以int8_t就是一个保留的类型。下面来看下整数集合的定义:
typedef struct intset {
// 保存元素所使用的类型的长度
uint32_t encoding;
// 元素个数
uint32_t length;
// 保存元素的数组
int8_t contents[];
} intset;
encoding就是上面说的类型;
有人对对contents的类型为int8_t 有疑惑:下面截取redis设计和实现这本书里的一段话来进行解释:
contents 数组的int8_t 类型声明比较容易让人误解,实际上,intset 并不使用int8_t 类型来保存任何元素,结构中的这个类型声明只是作为一个占位符使用:在对contents 中的元素进行读取或者写入时,程序并不是直接使用contents 来对元素进行索引,而是根据encoding的值,对contents 进行类型转换和指针运算,计算出元素在内存中的正确位置。在添加新元素,进行内存分配时,分配的容量也是由encoding 的值决定。
下面用具体的代码来进行解释下:
static int64_t _intsetGetEncoded(intset *is, int pos, uint8_t enc) {
int64_t v64;
int32_t v32;
int16_t v16;
if (enc == INTSET_ENC_INT64) {
memcpy(&v64,((int64_t*)is->contents)+pos,sizeof(v64));
memrev64ifbe(&v64);
return v64;
} else if (enc == INTSET_ENC_INT32) {
memcpy(&v32,((int32_t*)is->contents)+pos,sizeof(v32));
memrev32ifbe(&v32);
return v32;
} else {
memcpy(&v16,((int16_t*)is->contents)+pos,sizeof(v16));
memrev16ifbe(&v16);
return v16;
}
}
上面的函数就是获取整数集合的某个整数,可以看出获取整数前都是把content的类型转换成相应的encode类型,再通过encode类型和整数在整数集合中的位置(pos)找到内存中地址,再拷贝过来。
整数集合中的encode类型必须一致,也就是说如果新插入的值的类型比较大的话会自动升级到相应的encode类型。例如:新元素的长度为int32_t ,那么这个intset 就会自动进行“升级” :先将集合中现有的所有元素从int16_t 类型转换为int32_t 类型,接着再将新元素加入到集合中。
根据需要,intset 可以自动从int16_t 升级到int32_t 或int64_t ,或者从int32_t 升级到int64_t 。
整数集合的升级是通过intsetUpgradeAndAdd这个函数来进行升级。这个函数最主要的一部分如下:
while(length--)
_intsetSet(is,length+prepend,_intsetGetEncoded(is,length,curenc));
这个就是把原先整数集合中的类型升级到现在的类型,升级的示意图如下:
在升级之前,内存示意图如下:
重新分配空间后的内存示意图:
升级过程的内存示意图:
上面就是整个升级的过程。
注意:整数集合是一个有序的集合,所以redis搜索的时候采用的是二分查找。