缓存穿透、击穿、雪崩的区别与对策
这三个词经常被混着说,但它们描述的是三种不同的问题:穿透是查不存在的数据,击穿是热 key 过期,雪崩是大面积失效。
穿透:查询不存在的数据
请求的 key 在缓存和数据库里都不存在,每次都打到数据库。攻击者可以构造大量随机 ID 扫库。
对策:
// 1. 空值缓存:查不到也写入短 TTL 的占位
$redis->setex('user:' . $id, 60, json_encode(['__null__' => 1]));
// 2. 布隆过滤器:肯定不存在直接拒绝(有一定误判率)
if (!$bloom->mightContain('user:' . $id)) return null;
另外在入口做参数校验(ID 格式、范围),能挡掉大部分恶意流量。
击穿:热 key 过期瞬间
某个被高频访问的 key 恰好过期,瞬时大量请求同时回源,数据库被打出尖峰。
对策是「只放一个请求去回源」,其余等待:
$lockKey = 'lock:' . $k;
if ($redis->set($lockKey, 1, ['nx', 'ex' => 5])) {
try {
$v = loadFromDb($k);
$redis->setex($k, 3600, $v);
} finally {
$redis->del($lockKey);
}
} else {
usleep(100000);
return $redis->get($k);
}
更省事的做法是逻辑过期:缓存不设 TTL,value 里带过期时间,发现过期由单个线程异步重建,期间返回旧值。
雪崩:大量 key 同时失效
两类成因:一是同一批 key 相同 TTL 集中过期;二是 Redis 实例宕机,全部流量涌向数据库。
对策:
- TTL 加随机抖动:
$ttl = 3600 + mt_rand(0, 600); - 多级缓存:本地缓存挡第一波,Redis 挡第二波;
- 熔断限流:数据库前加限流,宁可降级也不雪崩;
- 高可用:哨兵或集群模式,避免单点。
快速对照
| 问题 | 触发条件 | 核心对策 |
|---|---|---|
| 穿透 | 数据不存在 | 空值缓存 / 布隆过滤器 / 参数校验 |
| 击穿 | 热 key 过期 | 分布式锁 / 逻辑过期 |
| 雪崩 | 批量失效或宕机 | TTL 抖动 / 多级缓存 / 熔断 |
排查线上问题时,先看监控里「缓存未命中率的变化」和「数据库 QPS 的尖峰是否与某个 key 的过期时间对齐」,三个问题自然能区分开。