Redis 中 Intset 的内存结构与存储机制详解
Intset(整数集合)是 Redis 为高效存储小整数集合设计的紧凑数据结构,其内存布局和存储方式紧密围绕 "节省空间" 和 "操作高效" 两个核心目标。下面结合结构体定义、内存分配和实际存储案例,完整解析其工作原理:
一、Intset 整体结构与内存布局
Intset 的结构体定义看似简单,实则包含了动态适配不同整数范围的关键设计:
c
typedef struct intset {uint32_t encoding; // 编码方式(决定元素类型)uint32_t length; // 元素数量int8_t contents[]; // 柔性数组(实际存储元素)
} intset;
内存总分配规则
一个 Intset 占用的总内存 = 固定头部(8 字节) + 元素数据区(动态大小):
- 固定头部:
encoding
(4 字节) +length
(4 字节),共 8 字节。 - 元素数据区:由
contents
柔性数组实现,大小 =length × 单个元素字节数
(单个元素字节数由encoding
决定)。
二、encoding
与 contents
的关联:动态适配整数范围
contents
数组的实际类型完全由 encoding
字段决定,这是 Intset 节省内存的核心机制:
encoding 常量 | 对应类型 | 单个元素字节数 | 整数范围 |
---|---|---|---|
INTSET_ENC_INT16 | int16_t | 2 字节 | -32768 ~ 32767 |
INTSET_ENC_INT32 | int32_t | 4 字节 | -2147483648 ~ 2147483647 |
INTSET_ENC_INT64 | int64_t | 8 字节 | -9223372036854775808 ~ 9223372036854775807 |
关键细节:int8_t contents[]
的本质
- 声明为
int8_t
仅为 C 语言语法兼容(柔性数组需声明为字符类型),实际使用时会根据encoding
强制转换为对应类型的数组(如int16_t*
)。 - 例如:当
encoding=INTSET_ENC_INT32
时,contents
会被当作int32_t
数组使用,每个元素占 4 字节。
三、实例解析:从内存布局看元素存储
以具体案例说明 Intset 的内存分配和元素存储方式:
案例 1:存储 [1, 3, 5](使用 INTSET_ENC_INT16 编码)
头部信息:
encoding
= 2(INTSET_ENC_INT16 的宏定义值),占 4 字节 →0x00000002
length
= 3(3 个元素),占 4 字节 →0x00000003
元素数据区:
- 每个元素为 int16_t 类型(2 字节),总大小 = 3×2 = 6 字节。
- 元素按从小到大排序,存储为:
- 1 →
0x0001
(int16_t 表示) - 3 →
0x0003
- 5 →
0x0005
- 1 →
完整内存布局(小端字节序,按字节偏移量排列):
plaintext
偏移量 字节内容(十六进制) 说明 0-3 02 00 00 00 encoding(INTSET_ENC_INT16) 4-7 03 00 00 00 length(3个元素) 8-9 01 00 元素1:1(int16_t) 10-11 03 00 元素2:3(int16_t) 12-13 05 00 元素3:5(int16_t)
总内存:8 + 6 = 14 字节。
案例 2:插入 65535 后升级为 INTSET_ENC_INT32
当插入 65535(超出 int16_t 范围),Intset 会触发升级:
升级后头部信息:
encoding
= 4(INTSET_ENC_INT32),占 4 字节 →0x00000004
length
= 4(新增一个元素),占 4 字节 →0x00000004
元素数据区:
- 每个元素为 int32_t 类型(4 字节),总大小 = 4×4 = 16 字节。
- 原有元素升级为 int32_t 并保持排序,新增元素插入末尾:
- 1 →
0x00000001
(int32_t 表示) - 3 →
0x00000003
- 5 →
0x00000005
- 65535 →
0x0000FFFF
- 1 →
升级后内存布局:
plaintext
偏移量 字节内容(十六进制) 说明 0-3 04 00 00 00 encoding(INTSET_ENC_INT32) 4-7 04 00 00 00 length(4个元素) 8-11 01 00 00 00 元素1:1(int32_t) 12-15 03 00 00 00 元素2:3(int32_t) 16-19 05 00 00 00 元素3:5(int32_t) 20-23 FF FF 00 00 元素4:65535(int32_t)
总内存:8 + 16 = 24 字节。
四、元素访问机制:如何正确解析 contents
数组
Redis 通过宏函数 _intsetGet
按编码解析元素,核心逻辑如下:
c
static int64_t _intsetGet(intset *is, uint32_t pos) {// 根据当前编码解析指定位置的元素if (is->encoding == INTSET_ENC_INT64) {return ((int64_t*)is->contents)[pos]; // 按int64_t访问} else if (is->encoding == INTSET_ENC_INT32) {return ((int32_t*)is->contents)[pos]; // 按int32_t访问} else {return ((int16_t*)is->contents)[pos]; // 按int16_t访问}
}
- 强制类型转换:将
int8_t*
转换为当前编码对应的类型指针(如int32_t*
),直接通过索引访问元素。 - 字节序兼容:通过
memrevXXifbe
函数处理大端 / 小端字节序差异,保证跨平台一致性。
五、总结:Intset 的设计精髓
- 动态编码:根据元素范围自动选择最小可行的编码,避免内存浪费。
- 紧凑存储:柔性数组 + 无额外元数据,内存利用率接近 100%。
- 有序性:元素始终排序,支持二分查找(O (log n) 复杂度)。
- 升级不可逆:只支持从窄编码升级到宽编码(如 int16→int32),保证数据安全。
这种设计让 Intset 在存储小整数集合时,比哈希表节省大量内存(无键值对开销、无指针开销),是 Redis 内存优化的典型案例。当元素数量超过阈值(默认 512)或包含非整数时,Redis 会自动将 Intset 转换为哈希表,平衡内存与性能。