← 返回题目列表

Redis 对象底层编码有哪些?为什么会自动转换?

高频 中等 第 7 / 36 题 更新于 2026/07/29
Redis底层编码listpack数据结构

简化版

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 负责 ZRANGEZRANGEBYSCORE 这类有序范围查询。

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 对上,就能回答大多数底层结构追问。