Redis 对象底层编码有哪些?为什么会自动转换?
简化版
Redis 会根据数据类型、元素数量和元素大小选择不同底层编码,比如 String 可用 int/SDS,Hash 可用 listpack 或 hashtable,Set 可用 intset 或 hashtable,ZSet 可用 listpack 或 skiplist。自动转换是为了在小数据时省内存、大数据时保性能。
详细版
Redis 的“类型”和“编码”不是一回事。类型是用户看到的 String、List、Hash、Set、ZSet;编码是内部存储方式。
常见编码包括:
- String:int、embstr、raw。
- Hash:listpack、hashtable。
- List:quicklist。
- Set:intset、hashtable。
- ZSet:listpack、skiplist。
小集合用紧凑编码能省内存、提升缓存局部性;元素变多或元素变大后,紧凑结构查找成本上升,就会转换成哈希表或跳表等结构。面试回答要说明转换背后的取舍:空间、时间、CPU cache 和操作复杂度。
完整版教学
一、类型和编码要分开看
Redis 命令层看到的是数据类型,比如 HSET 操作 Hash,ZADD 操作 ZSet。但 Redis 内部还会给同一种类型选择不同编码,用来平衡内存占用和操作效率。
这就像同样叫“通讯录”,联系人少时可以写在一张纸上,联系人多了就需要索引。Redis 自动编码转换也是这个思路:小数据紧凑存,大数据结构化存。
记忆钩子:Redis 类型回答“能做什么”,编码回答“里面怎么放”。
二、String 的 int、embstr、raw
String 是最基础的类型,但内部也不只有一种形态。如果值可以表示为整数,Redis 可以用 int 编码;短字符串可能用 embstr,把对象头和字符串内容连续分配;较长字符串使用 raw,分配更灵活。
SET age 18 -> int 编码可能更省
SET name tom -> embstr 适合短字符串
SET bio 很长文本 -> raw 更适合长字符串
这样做的意义是减少内存分配次数和额外指针开销。比如 embstr 把 redisObject 和 SDS 放在连续内存里,创建和释放都更快。
三、Hash 为什么小的时候不用哈希表
Hash 字段少、字段短时,用 listpack 这类连续紧凑结构更省内存。虽然查找字段可能需要顺序扫描,但元素数量很小时,几十次比较的 CPU 成本比维护哈希表更划算。
假设一个 Hash 只有 4 个字段:name/age/city/vip。如果直接用 hashtable,每个 field 都有哈希桶、指针、对象头开销;如果用 listpack,连续存储可以明显节省内存。
| 场景 | 更适合编码 | 原因 |
|---|---|---|
| 字段少且短 | listpack | 紧凑、省内存 |
| 字段多或字段大 | hashtable | 查找和更新更稳定 |
四、Set 的 intset 到 hashtable
Set 如果只包含整数,且数量不多,可以用 intset。intset 是有序整数数组,内存紧凑,查找可以二分。
一旦加入非整数元素,或者元素数量超过阈值,就会转成 hashtable。因为字符串成员不适合 intset,元素太多时数组插入移动成本也会升高。
SADD ids 1 2 3 -> intset
SADD ids user:4 -> hashtable
这里体现的是典型取舍:小整数集合优先省内存,大规模通用集合优先保证操作效率。
五、ZSet 为什么常说 dict + skiplist
ZSet 同时要支持按 member 查 score,也要支持按 score 范围排序查询。单一结构很难同时做好这两件事。
所以大规模 ZSet 常用 dict + skiplist:dict 负责 ZSCORE member 这类按成员查找,skiplist 负责 ZRANGE、ZRANGEBYSCORE 这类有序范围查询。
member -> score 查询:dict 近似 O(1)
score 范围查询:skiplist O(logN + M)
如果元素很少,Redis 也可能使用 listpack,省掉 skiplist 和 dict 的额外开销。
六、List 的 quicklist 解决什么问题
早期 Redis List 经历过 linkedlist 和 ziplist 等实现演进。现在常见实现是 quicklist,可以理解为“链表 + 紧凑列表”的组合。
纯链表插入删除方便,但每个节点指针开销大;纯连续数组省内存,但中间插入删除移动成本高。quicklist 把多个元素装进一个紧凑块,再用链表串起来,折中内存和修改成本。
| 结构 | 优点 | 缺点 |
|---|---|---|
| 纯链表 | 插入删除方便 | 指针开销大,局部性差 |
| 纯连续结构 | 内存紧凑 | 大规模移动成本高 |
| quicklist | 折中两者 | 参数配置会影响性能 |
七、自动转换的代价和线上注意点
编码转换不是没有成本。比如一个 Hash 从 listpack 转 hashtable,需要重新组织内部结构;如果大批 key 同时跨过阈值,可能带来 CPU 抖动。
实际使用时,不要为了省内存把阈值调得过激,也不要把一个对象做得特别大。一个 Hash 放几十万 field,会带来迁移、复制、删除和访问延迟问题。
小对象 -> 紧凑编码 -> 省内存
持续变大 -> 编码转换 -> 保操作效率
过大对象 -> BigKey 风险 -> 需要拆分
八、常见误区与追问
- 误区:Redis 的 String 就是 C 字符串。 Redis 使用 SDS 等结构,能保存长度并支持二进制安全。
- 误区:Hash 底层永远是哈希表。 小 Hash 可能使用 listpack,达到阈值后才转换。
- 误区:编码转换一定是坏事。 它是 Redis 在空间和时间之间做自适应取舍,但大规模转换需要关注。
- 追问:为什么小集合顺序扫描也可以接受? 因为元素少时 CPU cache 友好,少量比较比维护复杂结构更省。
- 追问:ZSet 为什么不用一个哈希表解决? 哈希表不能高效支持按 score 排序和范围查询。
- 追问:怎么看 key 的编码? 可以用
OBJECT ENCODING key查看,但线上批量执行要谨慎。
九、加强记忆
记住“类型是门面,编码是骨架”。同一个 Redis 类型背后可能有多种编码,小数据省内存,大数据保性能。
复习时把 String、Hash、Set、ZSet、List 分别和 int/embstr/raw、listpack/hashtable、intset、skiplist、quicklist 对上,就能回答大多数底层结构追问。