0
0
0

Redis 面试复盘:数据类型、Hash 分桶与缓存三大问题

2026-10-28
文章摘要
|

正概念混淆和回答不完整的问题。每节先给出可直接背诵的回答,再解释容易被追问的细节。

一、Redis 常见数据类型怎么回答

背诵版

Redis 常见的五种基础数据类型是 String、Hash、List、Set 和 Sorted Set。
String 适合缓存字符串、JSON、计数器等;Hash 适合存储对象属性,支持单独读写字段;List 适合有序列表和简单队列;Set 适合去重、交并差运算;Sorted Set 适合排行榜和按分数排序。

纠正:String 不只是存“单一属性”

String 也可以保存序列化后的完整对象,例如用户 JSON。选择 String 还是 Hash,主要看访问方式:经常整体读写可以用 String;经常单独修改属性可以考虑 Hash。并不是对象一定用 Hash。

类型

示例

场景

String

user:info:123 → JSON

整体缓存用户信息

Hash

user:profile:123 → name、age

独立修改用户属性

List

task:queue

简单队列;可靠消息另需确认、重试等设计

Set

article:likes:123 → 用户 ID 集合

点赞去重

Sorted Set

game:rank → 用户 ID 和分数

排行榜

Key 可以使用“业务:数据含义:标识”,如 mall:user:profile:123。这是项目约定,不是 Redis 强制要求。命名要清晰,避免无限增长和敏感信息暴露。

二、一个用户一个 Hash,与多个用户共用 Hash

背诵版

如果用 Hash 存用户属性,我通常先考虑一个用户一个 key,例如 user:profile:123,field 是 name、age 等属性。如果确实需要把多个用户聚合到一个 Hash,再设计分桶,field 表示用户标识,value 存该用户的序列化数据。这两种模型不能混为一谈,也不需要因为用户总数多就默认分桶。

模型一:一个用户一个 Hash

HSET user:profile:123 name ZhangSan age 28
HGET user:profile:123 name
EXPIRE user:profile:123 1800

用户多意味着 key 多,不等于某个 key 大。一个用户的属性少且值小,通常没有必要再对这个用户拆桶。对象是否过大,要看实际字段数量与字节大小。

模型二:多个用户聚合进 Hash

下面两种分桶方式,假设 ID 是非负整数、每个用户只占一个 field。

方案

key 后缀

field

特点

按 ID 区间分桶

id / 100,整数除法

id % 100

每桶最多100个用户条目,桶数随 ID 范围增长

固定100个桶

id % 100

完整 ID

桶数固定,单桶条目仍可能不断增长

用户 ID 为12345时:

区间分桶:key=user:bucket:123,field=45
固定分桶:key=user:bucket:45,field=12345

注意:固定桶方案不能仅用同一个余数作为 field,否则同桶用户会互相覆盖。区间方案限制的是条目数,不是总字节数;某个 value 很大时,仍可能形成大 key。

如何查询单个用户:背诵版

已知用户 ID 后,按写入时相同的公式计算桶号和 field,拼出 key,再用 HGET 查询,不需要扫描全部 key。除法和取余的开销通常很小,主要代价在网络往返、跨桶批量操作和分页处理。

分桶的代价

  • 同桶多个用户可以用 HMGET;跨桶时要分组请求,必要时用 Pipeline 减少网络往返,但 Pipeline 不保证原子性。

  • Redis Cluster 中,跨槽多 key 命令、事务和 Lua 有限制;同一 Hash 内的字段仍在同一 key、同一槽。

  • Hash 不提供业务有序分页,HSCAN 是增量遍历,不等于稳定的页码查询。需要排序分页时,应考虑 Sorted Set 或数据库索引。

  • 多用户共用一个 key,会耦合生命周期与热点。不要假设所有版本都支持 Hash 字段独立过期;字段过期能力取决于版本和客户端。普通 EXPIRE 作用于整个 key。

  • 固定桶数量变更会改变映射,需要考虑迁移;分桶也不自动解决热点问题。

三、大 key、热 key、内存不足怎么区分

背诵版

大 key 是单个 key 的值很大,或者集合元素很多;热 key 是单个 key 的访问量很高,两者可以同时存在。大 key 容易增加网络传输、遍历、删除和迁移的成本,某些操作可能阻塞命令处理。我会限制集合规模和单个 value 大小,避免线上全量读取,按需拆分,并结合 MEMORY USAGE、HLEN 等指标定位。

纠正:“内存不够就执行拒绝策略”不够准确

达到 maxmemory 后,行为取决于 maxmemory-policy。配置 noeviction 时,通常是需要新增内存的写命令报错,而不是所有请求都被拒绝;其他策略会尝试淘汰符合条件的 key。如果没有可淘汰的数据,也可能写入失败。

不要把“大 key”直接等同于“内存不足”。前者是单个数据结构的问题,后者是实例整体资源和策略的问题。

线上避免对大 Hash 直接 HGETALL;遍历可考虑 HSCAN,但需要处理重复和遍历期间变更。删除大 key 可评估 UNLINK,让内存回收异步进行,但仍需关注资源消耗,不能认为完全没有成本。

四、穿透、击穿、雪崩:最优先背熟

问题

典型原因

主要措施

穿透

反复查不存在的数据,缓存与数据库都没有

参数校验、缓存空值、布隆过滤器

击穿

一个热点缓存失效,大量并发请求同时回源

互斥重建、提前刷新、允许时返回旧值

雪崩

大量缓存集中失效,或缓存服务不可用

随机 TTL、限流、熔断、降级、高可用

背诵版

缓存穿透是查询不存在的数据,缓存和数据库都没有,导致请求反复回源,可以通过参数校验、缓存空值或者布隆过滤器处理。缓存击穿是一个热点 key 失效,大量并发请求同时访问数据库,可以用互斥锁控制缓存重建。缓存雪崩是大量 key 集中失效,或者 Redis 不可用,导致数据库压力骤增;集中失效可以通过随机 TTL 缓解,服务不可用还需要限流、熔断和降级保护数据库。

三个追问

缓存没命中就叫穿透吗?

不是。普通缓存未命中是正常现象;穿透重点在于查询的数据不存在,请求反复绕过缓存。

随机 TTL 能解决击穿吗?

不能可靠解决。它主要打散多个 key 的过期时间;单个热点 key 过期时,仍可能发生大量并发回源。

一直给热点 key 续期可以吗?

如果只是延长旧值 TTL,不重新加载数据,可能导致旧数据长期不更新。应明确采用提前刷新、逻辑过期或更新时失效等策略,并说明业务可接受的陈旧时间。

补充:缓存空值要有合适的较短 TTL,并在数据创建后考虑失效;布隆过滤器可能误判存在,也需要维护更新,不能代替全部数据库查询。

五、用 Redis 互斥锁重建缓存

背诵版

请求先查缓存,命中直接返回。未命中时,针对同一个业务数据,用 SET 加 NX 和过期时间原子抢锁。抢到锁后再次检查缓存,仍未命中才查询数据库,再把结果写回缓存并设置 TTL,最后在 finally 中安全释放锁。没抢到锁的请求做有限次数的延迟重试,或者在业务允许时返回已保留的旧值。释放锁时使用 Lua 原子校验唯一标识并删除,避免误删其他请求的锁。

流程记忆

先查缓存 → 抢锁 → 二次查缓存 → 查数据库 → 写缓存 → 安全解锁。

注意:这里是查询数据库后写回 Redis,不是再次写入数据库;缓存重建本身也不要求开启数据库事务。这里的锁应称为 Redis 分布式互斥锁,和数据库悲观锁区分。

抢锁命令

SET lock:user:123 request-unique-token NX PX 10000
  • NX:仅在 key 不存在时设置。

  • PX:设置毫秒级过期时间;示例10秒不是所有业务通用值。

  • value:每次抢锁生成的唯一标识,例如 Java 8 自带的 UUID。

  • 锁粒度应对应需要重建的数据,比如用户123;不是所有请求共用一把锁。

不要先 SETNX 再单独 EXPIRE:两条命令之间进程故障,可能留下没有 TTL 的锁。

安全解锁脚本

if redis.call('get', KEYS[1]) == ARGV[1] then
    return redis.call('del', KEYS[1])
else
    return 0
end

为什么不能直接 DEL?请求A的锁超时后,请求B可能已经拿到同名锁;A完成后直接删除,就会删掉B的锁。

为什么不能先 GET 校验、再 DEL?两条独立命令之间可能发生锁过期与重新获取,必须让校验和删除原子执行。

这套方案的边界

  • 二次检查可以避免已经有人填好缓存后,后续抢锁者再次回源。

  • TTL 过短会导致查询未完成锁就失效,多个请求可能同时重建;应合理设置并在需要时使用带所有权校验的续期机制。

  • 唯一标识加 Lua 能避免误删,但不能保证锁过期后旧持有者停止执行,也不能杜绝旧查询结果覆盖新缓存。

  • Redis 异步复制和主从切换可能使锁丢失。缓存重建一般用于减少重复回源;资金、库存等强一致操作不能仅靠这个简单锁保证正确。

  • 没抢到锁不能无限循环;要设置等待上限,重试间隔可带抖动。缓存完全失效且没有保留旧值时,不能凭空“返回旧值”。

  • 锁服务故障时也要限制回源,避免所有请求无条件查数据库。

六、面试回答方式:定义、方案、边界

遇到问题先说准确的定义,再说明方案,最后补一个关键风险。不要还没解释清楚模型,就急着讲分片;也不要把主要性能成本归因于简单整数运算。

例如面试官问“如何防止缓存击穿”,可以回答:

击穿是热点缓存失效后,大量请求并发回源。我会按业务 key 加 Redis 互斥锁,抢到后双查缓存,再查数据库、写回缓存。其他请求有限等待,允许时返回旧值。锁使用 SET NX PX,设置唯一标识,最后用 Lua 校验后删除,同时注意锁超时和回源限流。

七、两分钟背诵稿

Redis 常见的五种基础类型是 String、Hash、List、Set 和 Sorted Set。String 不仅可以存简单值,也可以存完整 JSON;Hash 适合独立读写对象字段,选型主要看访问方式。

用 Hash 存用户时,可以一个用户一个 key,field 存属性;也可以多个用户聚合后分桶,field 存用户标识,value 存用户数据,这两种模型不能混淆。

按 ID 区间分桶,可以用 ID 除以100的商作为 key 后缀、余数作为 field,每桶最多100个条目。用余数作为 key 后缀,则是固定100个桶,field 应使用完整 ID,单桶仍可能持续增长。已知 ID 就能直接计算位置,不需要扫描;主要代价在网络、跨桶操作和分页。

穿透是查不存在的数据,可缓存空值或用布隆过滤器;击穿是单个热点缓存失效,可互斥重建;雪崩是大量缓存集中失效或 Redis 不可用,要用随机 TTL、限流、熔断和降级等手段应对。

互斥重建流程是先查缓存、原子抢锁、二次查缓存、查数据库、写回缓存、最后安全解锁。抢锁用 SET NX PX,value 放唯一标识,解锁用 Lua 原子校验后删除。等待要有上限,锁超时和服务故障要有兜底。

大 key 看大小和元素数量,热 key 看访问频率。达到内存上限后的行为取决于淘汰策略,不能简单说所有请求都会被拒绝

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或者给予支持!

评论