Redis 的整数集合:像分类收纳盒一样的整数专属存储
目录
一、先懂定位:为什么需要整数集合?(衔接哈希表)
二、整数集合的结构:像 “贴了规格标签的收纳盒”
1. encoding:收纳盒的 “规格标签”(核心:决定格子大小)
2. length:收纳盒的 “物品数量”
3. contents:收纳盒的 “有序格子”
三、核心操作:“升级”—— 从小盒子换大盒子(重点)
举个具体例子:从 int16 升级到 int32
步骤 1:换大收纳盒(扩展空间)
步骤 2:把小珠子换成大珠子(转换元素类型)
步骤 3:放进新的大珠子(添加新元素)
3.1、为什么只能升级,不能降级?(Redis 的权衡)
四、适用场景:什么时候用整数集合?(对比哈希表)
五、总结:整数集合的设计逻辑(衔接 Redis 整体哲学)
如果说 SDS 是 “智能价签”、压缩列表是 “紧凑货架”、字典是 “万能储物柜”,那整数集合(intset) 就是 Redis 为 “少量整数” 定制的分类收纳盒—— 专门用来存纯整数、数量不多的集合(比如SADD nums 10 20 30
),核心优势是 “省空间、保有序、无重复”。它和哈希表(hashtable)是集合键的两种底层实现,就像收纳时 “少量小物件用分类盒,大量 / 杂物件用储物柜”。
一、先懂定位:为什么需要整数集合?(衔接哈希表)
之前学的哈希表(hashtable) 虽然能存集合(比如非整数元素SADD fruits "apple"
),但有个 “内存浪费” 的问题:每个元素要存键(元素本身)和值(NULL,因为集合只需存存在性),还要处理哈希冲突。比如存 3 个小整数10、20、30
,哈希表的指针、哈希槽等开销,比整数本身的空间还大。
而整数集合的思路是:用 “统一规格的数组” 存整数,不存额外指针 / 哈希信息,还保持有序无重复—— 就像用 “带格子的收纳盒” 装小珠子,每个格子只放一颗,按大小排好,没有多余的包装,省空间又好查找。
二、整数集合的结构:像 “贴了规格标签的收纳盒”
整数集合的底层是intset
结构体,对应 “分类收纳盒” 的三个核心部分:规格标签、物品数量、收纳格子。我们先看结构定义,再逐个类比拆解:
typedef struct intset {uint32_t encoding; // 收纳盒的“规格标签”(存什么类型的整数)uint32_t length; // 收纳盒里的“物品数量”(元素个数)int8_t contents[]; // 收纳盒的“格子数组”(实际存整数的地方)
} intset;
1. encoding:收纳盒的 “规格标签”(核心:决定格子大小)
encoding
就像收纳盒上贴的 “适用物品规格”,比如 “仅放直径≤5mm 的小珠子”,它决定了contents
数组实际能存什么类型的整数:
- INTSET_ENC_INT16:“小格子规格”—— 每个格子存
int16_t
类型(范围:-32768 ~ 32767),比如 10、20、32767。 - INTSET_ENC_INT32:“中格子规格”—— 每个格子存
int32_t
类型(范围:-2147483648 ~ 2147483647),比如 32768(超过 int16 最大值)、100000。 - INTSET_ENC_INT64:“大格子规格”—— 每个格子存
int64_t
类型(范围极大),比如 2147483648(超过 int32 最大值)。
关键注意:虽然
contents
声明是int8_t
(1 字节小格子),但实际格子大小由encoding
决定!就像收纳盒表面写 “小格子”,但贴的规格标签是 “中格子”,实际就能放大珠子 —— 这是 Redis 的 “灵活声明”,为了兼容不同规格。
2. length:收纳盒的 “物品数量”
length
直接记录收纳盒里有多少个整数(无重复),比如存了 10、20、30,length=3
。和压缩列表的zllen
类似,能快速获取元素数量(O (1) 复杂度),不用数格子。
3. contents:收纳盒的 “有序格子”
contents
是实际存整数的数组,有两个核心特性:
- 有序:按从小到大排列,比如存 10、30、20,最终格子里是
[10,20,30]
—— 方便快速查找(用二分法,O (logN) 复杂度)。 - 无重复:同一个整数不会出现两次,比如再存 20,Redis 会检查格子里已有,不重复添加 —— 就像收纳盒里不会有两颗一样大的珠子。
三、核心操作:“升级”—— 从小盒子换大盒子(重点)
整数集合最特殊的逻辑是 “自动升级”:当要放的整数 “太大”(超过当前encoding
的范围),就必须把整个收纳盒换成更大规格,再统一转换原有元素的 “大小”,最后放进新元素。这就像 “小珠子收纳盒里要放大珠子,必须先换大盒子,把原来的小珠子都换成大珠子,再放大珠子”。
举个具体例子:从 int16 升级到 int32
假设现在有个 “小格子收纳盒”(encoding=INTSET_ENC_INT16),已经放了两个小整数:10、32767(int16 的最大值)。现在要放 32768(超过 int16 的最大值,必须用 int32),升级步骤对应生活场景:
步骤 1:换大收纳盒(扩展空间)
- 原来的小盒子:每个格子占 2 字节(int16),2 个元素共 4 字节。
- 新的大盒子:每个格子占 4 字节(int32),需要存 3 个元素(原有 2 个 + 新 1 个),总空间 = 3×4=12 字节。
- Redis 会申请 12 字节的新空间,代替原来的 4 字节 —— 就像把 2 格的小盒子,换成 3 格的大盒子。
步骤 2:把小珠子换成大珠子(转换元素类型)
- 不能直接把小珠子(int16 的 10、32767)放进大格子,必须先 “放大” 成大珠子(int32 的 10、32767)。
- 转换时要保持顺序:原来小盒子里是
[10,32767]
,转换后大盒子的前两格还是[10,32767]
(只是每个占 4 字节)—— 就像把小珠子装进大托里,位置不变。
步骤 3:放进新的大珠子(添加新元素)
- 把 32768(int32 类型)放进大盒子的最后一格,最终格子里是
[10,32767,32768]
,保持有序。 - 最后更新
encoding
为 INTSET_ENC_INT32,length
为 3—— 收纳盒的规格标签和数量标签同步更新。
3.1、为什么只能升级,不能降级?(Redis 的权衡)
升级后就算删了 “大珠子”(比如把 32768 删掉,只剩 10、32767),Redis 也不会把大盒子换回小盒子 —— 这就是 “无降级” 规则,原因很简单:
- 麻烦且没必要:降级需要先检查所有元素是否都符合小规格,再转换类型、缩小空间 —— 就像把大珠子换回小珠子,再换小盒子,步骤多,浪费时间。
- 效率优先:Redis 认为 “少量内存浪费” 比 “频繁降级的性能损耗” 更划算。比如大盒子里放小珠子,多占的内存不多(每个元素多 2 字节),但省去了降级的麻烦。
- 避免风险:如果降级后又要放大珠子,得再次升级,反复操作反而更慢 —— 就像换了小盒子又要换大的,不如一直用大的。
四、适用场景:什么时候用整数集合?(对比哈希表)
Redis 选择整数集合还是哈希表,遵循 “整数优先、少量优先” 的原则,和压缩列表的 “小数据优先” 思路一致:
集合类型 | 底层实现 | 原因(生活类比) |
---|---|---|
纯整数元素 + 元素少(默认<512 个) | 整数集合 | 分类收纳盒存少量小珠子,省空间、好排序 |
非整数元素(如字符串) | 哈希表 | 万能储物柜存杂物件,不管类型,查找快 |
纯整数元素 + 元素多 | 哈希表 | 收纳盒格子太多不好翻,储物柜(哈希表)更快 |
举例:
SADD nums 1 2 3
(纯整数、少元素)→ 整数集合;
SADD fruits "apple" "banana"
(非整数)→ 哈希表;
SADD big_nums 1 2 ... 1000
(多元素)→ 哈希表。
五、总结:整数集合的设计逻辑(衔接 Redis 整体哲学)
整数集合的所有设计,都和之前学的 SDS、压缩列表、跳跃表一脉相承,体现 Redis“因地制宜、权衡取舍” 的核心思路:
- SDS:用 “len+free” 平衡 “字符串扩展” 和 “内存浪费”—— 取舍:预分配少量空间换扩展效率;
- 压缩列表:用 “连续内存 + 可变标签” 平衡 “内存节省” 和 “连锁更新风险”—— 取舍:接受罕见更新换极致省内存;
- 整数集合:用 “统一规格数组 + 自动升级” 平衡 “内存节省” 和 “类型兼容”—— 取舍:不支持降级换操作简单、效率高。
简单说,整数集合就是 Redis 为 “少量整数集合” 量身定制的 “高效收纳盒”:它没有哈希表的灵活,但胜在省空间、有序;没有压缩列表的复杂,胜在操作简单、只存整数。这种 “专物专用” 的设计,正是 Redis 能兼顾性能和内存的关键。