适用读者:准备架构师/资深后端面试,被问"你的缓存怎么设计"的人。
前置:S12(Redis/NoSQL)、S25-J09(MySQL)、J07(三层骨架)。
覆盖旧文:2699、2816/2830/2832/2834/2842、3466–3470、3572(Redis 面试题,9 域 50+ 问)→ 12 篇 / 约 12 万字符。
扩展点:E-S25-030(五种缓存模式与一致性真实表现)、E-S25-031(命中率与 RT 的非线性关系)、E-S25-032(过期删除 × 内存淘汰 × bigkey)、E-S25-033(Redis 快:单线程与多线程的分工)。
上一篇:S25-J09 · MySQL 题库讲评:索引、MVCC、锁与两阶段提交的机制链
下一篇:题库层第 5 篇 · 消息队列与 Kafka 题库(待写)。
1. 一句话结论
大多数候选人只记得两三种缓存模式,却不知道"很多缓存模式本身就是一致性问题的制造者"。 面试的标准路径是:先定模式 → 再定失效 → 最后定位(穿透/击穿/雪崩)。
出处:
A-3469(缓存模式)、A-3466(一致性方案)、A-3467(淘汰与命中率)、A-3468(过期删除)、A-3572(题库)。
2. 五种缓存模式(E-S25-030)
| 模式 | 读写路径 | 一致性 | 典型问题 |
|---|---|---|---|
| Cache Aside | 读:缓存→miss 查库回填;写:先更库再删缓存 | 中 | 并发下"删缓存后旧值被回填" |
| Read Through | 缓存层代理读库 | 中 | 依赖缓存组件能力 |
| Write Through | 写缓存同时写库(同步) | 高 | 写延迟高 |
| Write Behind | 写缓存后异步批量刷库 | 低(有丢失窗口) | 宕机丢数据 |
| 双删 / 延迟双删 | 更新前删 → 更库 → 延迟再删 | 中高 | 延迟时间难定;靠补偿 |
四种"一致性组合"的真实表现:
先删缓存、再更库 → 并发读会把旧值回填(脏数据长期存在)❌
先更库、再删缓存 → 极端并发下仍可能读到旧值(窗口极短)⚠️ 主流
只更库不删缓存 → 永久不一致 ❌
延迟双删 + 重试 → 把窗口压到毫秒级,仍需兜底 ✅ 工程常用
判据(必答):缓存一致性没有"绝对方案",只有"可接受的不一致窗口 + 兜底对账"。 答"用双删就一致了"会被追问到底。
兜底三件套:① binlog 订阅(Canal)异步删缓存;② 定时对账修复;③ 版本号/时间戳校验(写入带版本,读到旧版本触发回源)。
热点 key 的特殊处理:单 key 打满单分片 → 本地缓存(Caffeine)+ 多级缓存 + key 打散(key:{0..N})。
3. 命中率数学:为什么 10% 的跌幅要命(E-S25-031)
$$T = h \cdot T_{cache} + (1-h)\cdot T_{db}$$
取 $T_{cache}=5,\text{ms}$、$T_{db}=500,\text{ms}$:
| 命中率 h | 平均 RT | 相对 90% |
|---|---|---|
| 95% | 29.75 ms | 0.55× |
| 90% | 54.5 ms | 1.00× |
| 80% | 104 ms | 1.91× |
| 50% | 252.5 ms | 4.63× |
复算:$h=0.9$ → $0.9\times5+0.1\times500=54.5$;$h=0.8$ → $4+100=104$。命中率跌 10 个点,RT 翻倍——这就是"命中率是缓存第一指标"的算术依据。
反向边界:并非所有系统都该追高命中率。GPU 生图(单次 10 s)里 10% 的重复率也值得缓存——收益看"省下的成本 × 命中率",不是命中率本身。
4. 三座大山:三种不同的失败
| 问题 | 触发条件 | 后果 | 解法 |
|---|---|---|---|
| 穿透 | 查不存在的数据(缓存永远 miss) | 请求全打到 DB | 空值缓存(短 TTL)、布隆过滤器、参数校验 |
| 击穿 | 单个热点 key 过期瞬间 | 瞬时并发全打到 DB | 互斥重建(SETNX 锁)、逻辑过期(不真过期,后台续期) |
| 雪崩 | 大量 key 同时过期 或 Redis 宕机 | DB 被打挂 | TTL 加随机抖动、多级缓存、限流降级、高可用集群 |
判据:穿透是"数据不存在",击穿是"单点失效",雪崩是"面失效"。 三者的解法不能互换。
5. 过期删除 × 内存淘汰(E-S25-032)
这是最容易被混淆的一对概念:
| 过期删除 | 内存淘汰 | |
|---|---|---|
| 触发条件 | key 到了 TTL | 内存达 maxmemory |
| 对象 | 单个过期 key | 任意 key |
| 策略 | 惰性删除(访问时查)+ 定期删除(每 100 ms 抽样) | noeviction / allkeys-lru / volatile-lru / allkeys-lfu / volatile-ttl / random |
定期删除如何控制开销:每轮从过期字典随机抽样(默认 20 个),若过期比例 > 25% 则重复本轮,并受时间上限(默认 25% CPU 时间)约束——用"抽样 + 时间片"把开销封顶,代价是"过期了但没被删"。
由此推出四个深水问题:
| 问题 | 原因 | 处理 |
|---|---|---|
| 内存没超但 RSS 高 | 惰性删除 + 未抽样到 | 主动 SCAN 清理、设 maxmemory 兜底 |
| bigkey 过期卡顿 | 删除大 key 是同步释放,阻塞主线程 | UNLINK(异步释放)、拆分 bigkey |
| 主从过期不一致 | 从库不主动过期,等主库的 DEL 传播 |
读到逻辑过期 key 时从库返回空但保留(3.2+ 行为),业务侧容忍 |
| 持久化文件里的过期数据 | RDB/AOF 保存了已过期 key | 加载时过滤(主库);AOF 重写会清理 |
bigkey 判定:String > 10 KB、集合元素 > 5,000(经验值,非标准);redis-cli --bigkeys、MEMORY USAGE key。
6. Redis 为什么快(E-S25-033)
| 原因 | 说明 |
|---|---|
| 纯内存 | 无磁盘 IO(除持久化) |
| 单线程命令执行 | 无锁、无上下文切换、无竞态 |
| IO 多路复用 | epoll 单线程管理万级连接 |
| 高效数据结构 | SDS、跳表、listpack、渐进式 rehash |
| 多线程分工 | 6.0+ 网络 IO 多线程(读写解析),命令执行仍单线程 |
高频追问:为什么命令执行不用多线程?——瓶颈不在 CPU 而在 IO/内存,多线程会引入锁与上下文切换;但删除大 key、AOF fsync 等耗时操作才用后台线程/子进程。
7. 集群与高可用:三句话答清
主从:全量(RDB)+ 增量(repl_backlog 偏移量);异步复制 → 可能丢数据
哨兵:监控 + 主观/客观下线 + Raft 式选主(quorum)
集群:16384 槽分片;客户端按 MOVED/ASK 重定向;槽迁移期间 ASK
一致性边界:Redis 主从/集群是"最终一致";WAIT 只能减少丢失窗口,不是强一致。分布式锁要用 SET key val NX PX + 唯一 value + Lua 释放,且注意锁续期(watchdog)与主从切换丢锁(Redlock 争议)。
8. 反例:四种常见错答
| 说法 | 问题 |
|---|---|
| "先删缓存再更新数据库" | 并发回填脏数据,最经典的错误方案 |
| "用 SETNX 做分布式锁就安全了" | 无唯一 value、无续期、主从切换丢锁 |
| "Redis 6 多线程了,所以命令并发执行" | 只有网络 IO 多线程,命令仍单线程 |
| "过期 key 会被立刻删除" | 惰性 + 定期抽样,存在"已过期未删除"窗口 |
9. 自测
- 五种缓存模式里,哪一种"本身就是一致性问题制造者"?为什么?
- 命中率从 90% 掉到 80%,RT 变化多少倍?公式是什么?
- 穿透 / 击穿 / 雪崩 各对应什么解法?为什么不能互换?
- bigkey 过期为什么会卡顿?怎么缓解?
- Redis 6 的多线程用在哪、不用在哪?
答案
- Cache Aside 的"先删缓存后更库"变体与 Write Behind(异步刷库);前者回填脏数据,后者有丢失窗口。
- 约 1.91 倍;$T=h\cdot T_c+(1-h)\cdot T_{db}$。
- 穿透=空值/布隆;击穿=互斥重建/逻辑过期;雪崩=TTL 抖动/多级/限流。
- 删除是同步释放内存,阻塞单线程;用
UNLINK与拆分。 - 网络读写解析多线程;命令执行仍单线程。
10. 下一篇
题库层第 5 篇 · 消息队列与 Kafka 题库 —— 为什么用 MQ、顺序与幂等、Kafka 高性能的四个原因、延迟消息的三种实现(待写)。
本篇属于系列《S25 · 求职与面试》。旧文来源:A-2699、A-2816/2830/2832/2834/2842、A-3466…3470、A-3572;扩展点 E-S25-030…033。命中率数字为按公式复算,未经线上实测。