Redis 整数集合(intset)是 Redis 中用于高效存储整数值的底层数据结构,具有内存紧凑和升级灵活的特点。本文深入分析 intset 的内存布局、升级机制和适用边界,帮助开发者在实际应用中做出合理选择。
intset 基本概念与内存布局
intset 是 Redis 中用于存储整数值的集合实现,当集合只包含整数值时,Redis 会使用 intset 作为底层实现。intset 的特点是内存紧凑,能够根据存储的整数大小动态调整编码方式,从而节省内存空间。
intset 的内存布局遵循以下结构:
typedef struct intset {
uint32_t encoding; // 编码方式:INTSET_ENC_INT16, INTSET_ENC_INT32, INTSET_ENC_INT64
uint32_t length; // 元素个数
int8_t contents[]; // 柔性数组,存储整数元素
} intset;
encoding 字段决定了整数的编码方式,有三种可能的取值:
- INTSET_ENC_INT16:16位有符号整数(-32768 到 32767),每个元素占 2 字节
- INTSET_ENC_INT32:32位有符号整数(-2147483648 到 2147483647),每个元素占 4 字节
- INTSET_ENC_INT64:64位有符号整数(-9223372036854775808 到 9223372036854775807),每个元素占 8 字节
length 字段存储当前 intset 中的元素个数。contents 是一个柔性数组,实际存储整数元素,元素按从小到大的顺序排列,使得二分查找成为可能。
// intset 创建示例
intset *is = intsetNew(); // 创建一个空的 intset
intsetAdd(is, 1000, NULL); // 添加元素 1000
intsetAdd(is, 2000, NULL); // 添加元素 2000
intsetAdd(is, 3000, NULL); // 添加元素 3000
// 此时 encoding 为 INTSET_ENC_INT16,因为所有元素都可以用 16 位整数表示
intset 的这种内存布局设计使其在存储小整数时非常高效,但也会受到编码方式的限制。当存储的整数超出当前编码范围时,intset 需要进行升级操作。
intset 升级机制详解
intset 的升级机制是其核心特性之一,它允许 intset 根据存储的整数大小动态调整编码方式,从而在保持有序性的同时最大化内存效率。
升级触发条件
intset 升级会在以下情况下触发:
- 当添加的整数无法被当前编码表示时
- 当添加的整数会导致当前编码无法表示所有元素时
例如,如果一个 intset 当前使用 INTSET_ENC_INT16 编码,当添加一个大于 32767 或小于 -32768 的整数时,intset 将升级为 INTSET_ENC_INT32。
升级过程
升级过程可以分为以下几个步骤:
- 根据新元素的值确定目标编码方式
- 创建一个新的、更大尺寸的 intset
- 将原有元素和新增元素合并排序后存入新 intset
- 释放旧 intset 的内存
// intset 升级示例(伪代码)
void intsetUpgradeAndAdd(intset *is, int64_t value) {
// 1. 确定目标编码
int8_t encoding = intsetGetEncodingForValue(value);
// 2. 创建新 intset
intset *new_is = intsetNew();
new_is->encoding = encoding;
new_is->length = is->length + 1;
// 3. 分配新内存
new_is->contents = malloc(new_is->length * intsetElementSize(encoding));
// 4. 复制元素并合并排序
int i = 0;
while (i < is->length && intsetGet(is, i) < value) {
intsetSet(new_is, i, intsetGet(is, i));
i++;
}
intsetSet(new_is, i, value);
while (i < is->length) {
intsetSet(new_is, i + 1, intsetGet(is, i));
i++;
}
// 5. 释放旧 intset 并替换为新 intset
free(is);
}
升级带来的性能影响
升级操作的时间复杂度为 O(N),其中 N 是 intset 中的元素个数。不过,由于 intset 通常用于存储少量整数,且升级操作不频繁,这种性能影响通常可以忽略不计。
升级后的内存占用会增加:
- 从 INTSET_ENC_INT16 升级到 INTSET_ENC_INT32:内存使用增加 2 倍
- 从 INTSET_ENC_INT32 升级到 INTSET_ENC_INT64:内存使用增加 2 倍
intset 适用场景与边界
适用场景
- 存储少量整数:元素数量较少时,intset 的内存效率优势明显
- 整数值范围跨度不大:当所有整数可以用较小的编码表示时
- 需要有序性:intset 中的元素始终有序,适合有序操作
性能边界
- 元素数量:当元素数量超过一定阈值(通常 512),Redis 会转换为更高效的数据结构
- 整数范围:整数范围过大时会导致频繁升级,影响性能
- 查找操作:由于是有序结构,查找操作的时间复杂度为 O(log N)
与其他数据结构的比较
| 特性 | intset | hashtable | skiplist |
|---|---|---|---|
| 内存占用 | 低(紧凑存储) | 中(需要哈希表结构) | 高(需要多层指针) |
| 查找速度 | O(log N) | O(1) | O(log N) |
| 插入速度 | O(N)(升级时) | O(1) | O(log N) |
| 有序性 | 天然有序 | 无序 | 天然有序 |
intset 在内存占用方面具有明显优势,但频繁的升级操作会影响插入性能。在选择数据结构时,需要根据具体场景进行权衡。
实践示例与注意事项
最小可运行示例
# 创建一个整数集合
> sadd myset 1 2 3 4 5
(integer) 5
# 查看集合类型
> object encoding myset
intset
# 添加一个小整数
> sadd myset 6
(integer) 1
> object encoding myset
intset
# 添加一个大整数,触发升级
> sadd myset 32768
(integer) 1
> object encoding myset
hashtable # 升级为 hashtable
使用注意事项
- 避免频繁升级:尽量避免在 intset 中混合使用跨度极大的整数
- 合理设置初始大小:预估可能存储的整数范围,避免不必要的升级
- 监控内存使用:对于可能包含大量元素的整数集合,考虑使用其他数据结构替代
- 利用有序性:intset 的天然有序性适用于需要有序遍历的场景,如排行榜等
- 避免删除操作:intset 的删除操作可能导致降级(虽然 Redis 当前未实现),频繁删除和添加可能影响性能