缓存三问是少数"面试题 = 生产事故"高度重合的话题。这篇按"定义 → 解法 → 解法的副作用"把三个问题一次讲清。
先分清三个问题
| 问题 | 触发条件 | 本质 |
|---|---|---|
| 穿透 | 查询不存在的数据,缓存和 DB 都没有 | 恶意/无效流量直达数据库 |
| 击穿 | 某个热点 key 过期瞬间,高并发同时打到 DB | 热点数据的瞬时失效 |
| 雪崩 | 大批 key 同时过期或缓存整体宕机 | 失效的规模效应 |
记忆法:穿透是"无中生有",击穿是"单点爆破",雪崩是"集体塌方"。
flowchart TD
A[请求] --> B{缓存命中?}
B -->|命中| C[返回]
B -->|未命中| D{key 存在过?}
D -->|从未存在·穿透| E[无效流量直捣 DB]
D -->|刚过期·击穿| F[热点并发直捣 DB]
D -->|批量过期·雪崩| G[洪峰直捣 DB]
E & F & G --> H[(数据库)]
穿透:两层防御
- 缓存空值:查无此 key 也写一个短 TTL(30-60s)的空标记。副作用:海量随机 key 会撑爆缓存——所以只对"key 空间可枚举"的业务用
- 入口校验:非法格式(ID 非数字、越界)在网关层直接拒绝。副作用:无,这是最便宜的防御,应该永远做
击穿:互斥重建
热点 key 失效时,只放一个请求去查库重建, others 等待:
value = redis.get(key)
if value is None:
if redis.set(lock_key, 1, nx=True, ex=5):
value = load_from_db(key)
redis.set(key, value, ex=TTL)
redis.delete(lock_key)
else:
time.sleep(0.05)
return get(key) # 重试读缓存
副作用:等待请求堆积;锁持有者挂了要靠锁超时兜底。逻辑过期是替代方案:value 里带过期时间,过期后返回旧值并异步重建——用户看到稍旧的数据,换取零等待。
雪崩:打散 + 兜底
- TTL 加随机抖动:
ttl = base + random(0, 300s),把同时过期摊开 - 多级缓存:本地缓存(L1)挡在 Redis(L2)前面,Redis 整体挂掉时 L1 还能撑一部分
- 熔断降级:DB 侧限流,宁可拒绝部分请求也不被打死——缓存层挂掉时保住数据库,就保住了恢复的希望
事后复盘的检查清单
每次缓存事故后核对:空值策略覆盖了吗 / 热点 key 有没有预热 / TTL 有没有抖动 / 降级预案演练过吗。缓存的问题从来不在缓存层本身,在于失效那一刻系统的行为——设计时永远多问一句:这一刻数据库看到什么?