Redis
源码之数据类型解析-IntSet
当前分析 Redis
版本为6.2,需要注意。
整数集合(IntSet
),Redis
用于保存整数值的集合抽象数据结构,可以保存 int16_t
、int32_t
或者 int64_t
的整数值,并且集合满足唯一性(集合不包含重复项)和有序性(集合中的元素按照从小到大有序排序)。
基础结构
typedef struct intset { // 整数集合
// encoding可选值 及其范围
// INTSET_ENC_INT16 int16_t -2^15(min) ~ 2^16-1(max)
// INTSET_ENC_INT32 int32_t -2^31 ~ 2^31-1
// INTSET_ENC_INT64 int64_t -2^63 ~ 2^63-1
// 在 intset.c 中定义
// #define INTSET_ENC_INT16 (sizeof(int16_t))
// #define INTSET_ENC_INT32 (sizeof(int32_t))
// #define INTSET_ENC_INT64 (sizeof(int64_t))
uint32_t encoding; // 编码方式,指定contents数组中元素的类型
uint32_t length; // 集合包含元素数量 contents长度
int8_t contents[]; // 保存元素的数组 虽然这里类型是int8_t,实际上是看encoding属性
} intset;
私有函数
_intsetValueEncoding
获取指定数值的编码方式。最小为 int16_t
。
static uint8_t _intsetValueEncoding(int64_t v) {
if (v < INT32_MIN || v > INT32_MAX)
return INTSET_ENC_INT64; // int64_t
else if (v < INT16_MIN || v > INT16_MAX)
return INTSET_ENC_INT32; // int32_t
else
return INTSET_ENC_INT16; // int16_t
}
_intsetGetEncoded
获取整数集合给定位置处的值,已知编码方式 uint8_t enc
。
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); // 将指针所指的64位无符号整数从小端数切换到大端数
return v64;
} else if (enc == INTSET_ENC_INT32) {
memcpy(&v32,((int32_t*)is->contents)+pos,sizeof(v32));
memrev32ifbe(&v32); // 32位
return v32;
} else {
memcpy(&v16,((int16_t*)is->contents)+pos,sizeof(v16));
memrev16ifbe(&v16); // 16位
return v16;
}
}
_intsetGet
返回整数集合指定位置的值,编码方式由集合指定 is->encoding
(需要调用 intrev32ifbe
对值进行转换,从小端切到大端),然后直接调用 实现 _intsetGetEncoded
。
_intsetSet
设置整数集合指定位置的值,编码方式由集合指定。
static void _intsetSet(intset *is, int pos, int64_t value) {
uint32_t encoding = intrev32ifbe(is->encoding);
if (encoding == INTSET_ENC_INT64) {
((int64_t*)is->contents)[pos] = value;
memrev64ifbe(((int64_t*)is->contents)+pos); // 大小端切换
} else if (encoding == INTSET_ENC_INT32) {
// ...
} else {
// ...
}
}
intsetResize
调整集合大小,采用 zrealloc
。当集合元素类型变动时调用?
static intset *intsetResize(intset *is, uint32_t len) {
uint32_t size = len*intrev32ifbe(is->encoding); // 计算contents长度
is = zrealloc(is,sizeof(intset)+size); // 集合长度+contents长度 的空间
return is;
}
intsetSearch
在整数集合中查找整数值。
static uint8_t intsetSearch(intset *is, int64_t value, uint32_t *pos) {
// value 给定整数值 *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;
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 { // 未找到 pos 为该插入的位置
if (pos) *pos = min;
return 0;
}
}
intsetUpgradeAndAdd
将整数集合的编码方式升级,并插入给定整数值(该值类型比原集合编码方式高,需要升级处理)。
static intset *intsetUpgradeAndAdd(intset *is, int64_t value) {
uint8_t curenc = intrev32ifbe(is->encoding); // 当前集合编码方式
uint8_t newenc = _intsetValueEncoding(value); // 给定值的编码方式
int length = intrev32ifbe(is->length); // 集合长度
int prepend = value < 0 ? 1 : 0; // 判断在前还是在后,该值肯定比集合所有现有值要大或小
// 设置集合新编码方式,并调整集合大小 intsetResize
// length+1 1为新元素 value的空间
is->encoding = intrev32ifbe(newenc); // 设置集合
is = intsetResize(is,intrev32ifbe(is->length)+1);
// 升级原则(不需要重写值)
// 需要添加的元素往往在头或尾,所以我们需要确保contents的头或尾有一个空空间即可
while(length--) // 但从这里来看,是重写了吧(get -> set)? 反向
_intsetSet(is,length+prepend,_intsetGetEncoded(is,length,curenc));
// 在头或尾设置值
if (prepend)
_intsetSet(is,0,value);
else
_intsetSet(is,intrev32ifbe(is->length),value);
is->length = intrev32ifbe(intrev32ifbe(is->length)+1); // 更新集合长度
return is;
}
intsetMoveTail
将整数集合中指定位置 from
以及之后的数据移到 指定位置 to
。整体后移。to-from
中间位置的元素不是存在重复?
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);
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) {
// ...
} else {
// ...
}
memmove(dst,src,bytes);
}
接口函数
intsetNew
创建一个新的整数集合。编码方式默认为 INTSET_ENC_INT16
。
intset *intsetNew(void) {
intset *is = zmalloc(sizeof(intset)); // 分配空间
is->encoding = intrev32ifbe(INTSET_ENC_INT16); // 指定默认编码方式
is->length = 0; // 默认长度
return is;
}
intsetAdd
向整数集合中添加一个整数。
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)) {
// 该操作总是成功的,所以不需要更新 *success
return intsetUpgradeAndAdd(is,value);
} else {
// 检测该值是否已在集合中存在
// 该操作会定位在未找到该元素时需要插入的正确位置
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); // 更新pos处的值
is->length = intrev32ifbe(intrev32ifbe(is->length)+1); // 更新长度
return is;
}
intsetRemove
从整数集合中移除一个整数。
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);
// 可移除
if (success) *success = 1;
// 从尾重写值。整体前移
if (pos < (len-1)) intsetMoveTail(is,pos+1,pos);
is = intsetResize(is,len-1); // 调整和更新长度
is->length = intrev32ifbe(len-1);
}
return is;
}
intsetFind
判断某值在整数集合中是否存在。
uint8_t intsetFind(intset *is, int64_t value) {
uint8_t valenc = _intsetValueEncoding(value);
return valenc <= intrev32ifbe(is->encoding) && intsetSearch(is,value,NULL);
}
intsetRandom
随机返回一个元素。
int64_t intsetRandom(intset *is) {
uint32_t len = intrev32ifbe(is->length);
assert(len); // 避免在损坏的整数集有效负载上被零除
return _intsetGet(is,rand()%len);
}
intsetGet
获取指定索引处的整数值。
uint8_t intsetGet(intset *is, uint32_t pos, int64_t *value) {
if (pos < intrev32ifbe(is->length)) { // 判断索引在正常范围内
*value = _intsetGet(is,pos);
return 1;
}
return 0;
}
intsetLen
获取整数集合的长度。
uint32_t intsetLen(const intset *is) {
return intrev32ifbe(is->length);
}
intsetBlobLen
计算整数集合的字节大小。
size_t intsetBlobLen(intset *is) {
return sizeof(intset)+(size_t)intrev32ifbe(is->length)*intrev32ifbe(is->encoding);
}
intsetValidateIntegrity
验证整数集合结构的完整性。
int intsetValidateIntegrity(const unsigned char *p, size_t size, int deep) {
// deep 是否深度验证
// 0 只验证头部结构
// 1 需要验证集合的唯一性和有序性
intset *is = (intset *)p;
if (size < sizeof(*is)) // 检测头部大小
return 0;
uint32_t encoding = intrev32ifbe(is->encoding);
size_t record_size;
if (encoding == INTSET_ENC_INT64) {
record_size = INTSET_ENC_INT64;
} else if (encoding == INTSET_ENC_INT32) {
record_size = INTSET_ENC_INT32;
} else if (encoding == INTSET_ENC_INT16){
record_size = INTSET_ENC_INT16;
} else { // 非标准编码方式
return 0;
}
uint32_t count = intrev32ifbe(is->length);
if (sizeof(*is) + count*record_size != size) // 整体结构大小检测
return 0;
if (count==0) // 验证空集合
return 0;
if (!deep) // 不需要深度验证,这里即可返回成功标识
return 1;
// 检测唯一性和有序性
int64_t prev = _intsetGet(is,0);
for (uint32_t i=1; i<count; i++) {
int64_t cur = _intsetGet(is,i);
if (cur <= prev)
return 0;
prev = cur;
}
return 1;
}
本章小结
该数据结构需要关注的是升级,这一策略,提高了集合的灵活性,并且节约内存。将不同整型分类放置到对应的整数集合。必要时会触发升级,从范围小的升到范围大的类型。不过,没有降级的策略。