Intset 解读
数据结构定义
typedef struct intset {
uint32_t encoding; // 编码方式:INTSET_ENC_INT16/32/64
uint32_t length; // 集合中元素个数
int8_t contents[]; // 柔性数组,存储实际的整数数据
} intset;
编码方式
Redis intset支持三种编码方式,根据存储的整数大小自动选择:(只升不降)
#define INTSET_ENC_INT16 (sizeof(int16_t)) // 2字节
#define INTSET_ENC_INT32 (sizeof(int32_t)) // 4字节
#define INTSET_ENC_INT64 (sizeof(int64_t)) // 8字节
编码选择逻辑:
-
如果所有整数都在 [-32768, 32767] 范围内,使用 INT16 编码
-
如果所有整数都在 [-2147483648, 2147483647] 范围内,使用 INT32 编码
-
否则使用 INT64 编码
核心特性
有序存储
-
所有元素按照升序排列
-
支持二分查找,查找时间复杂度 O(log n)
动态编码升级
-
当插入超出当前编码范围的整数时,自动升级编码
-
升级过程:重新分配内存 → 按新编码重新存储所有元素
内存优化
-
根据实际数据范围选择最小编码
-
避免存储大量小整数时浪费内存
核心函数解析
编码判断函数
static uint8_t _intsetValueEncoding(int64_t v) {
if (v < INT32_MIN || v > INT32_MAX)
return INTSET_ENC_INT64;
else if (v < INT16_MIN || v > INT16_MAX)
return INTSET_ENC_INT32;
else
return INTSET_ENC_INT16;
}
二分查找
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;
}
// 二分查找
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 {
if (pos) *pos = min;
return 0; // 未找到,pos为插入位置
}
}
编码升级
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; // 负数插入开头,正数插入末尾
// 设置新编码并调整大小
is->encoding = intrev32ifbe(newenc);
is = intsetResize(is,intrev32ifbe(is->length)+1);
// 从后往前升级,避免覆盖
while(length--)
_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;
}
添加元素
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 {
// 二分查找检查是否已存在
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;
}
添加元素时,只有不需要升级编码时,才需要二分查找确定插入位置
当需要编码升级时,新值一定超出了当前编码的范围:
-
INT16 → INT32:新值 < -32768 或 > 32767
-
INT32 → INT64:新值 < -2147483648 或 > 2147483647
为什么不需要二分查找?
-
数学保证:编码升级意味着新值在现有范围之外
-
位置确定:负数→开头,正数→末尾
-
性能优化:避免不必要的二分查找
-
逻辑简化:直接根据值的符号确定位置
字节序处理
使用 memrev16ifbe、memrev32ifbe、memrev64ifbe 函数处理字节序,确保在不同架构下的一致性
性能特点
-
查找:O(log n) - 二分查找
-
插入:O(n) - 需要移动元素保持有序
-
删除:O(n) - 需要移动元素
-
内存:根据数据范围优化,比哈希表更节省内存
使用场景
-
当Set中只包含整数时,且数量不多(< set-max-intset-entries),Redis底层数据结构优先使用intset
-
当元素数量较少且都是整数时,intset比哈希表更高效
-
适合存储连续的整数范围
限制
-
只能存储整数
-
元素必须有序
-
插入/删除操作较慢(需要移动元素)
-
当元素数量超过阈值或包含非整数时,会转换为哈希表
intset 与 ziplist比较
- ziplist 可存任意二进制串,intset 只能存整数
- ziplist 无序,intset 从小到大有序。ziplist 上查找只能遍历,intset 可进行二分查找,性能更高
- ziplist 可对每个数据项进行不同的变长编码(每个数据项前面都有数据长度字段
len
),intset 只能整体使用一个统一的编码(encoding
)