面试里这三者经常被混着答,线上排查却必须分清:穿透的压力落在「不存在查询」,击穿集中在单个热点 key,雪崩是大面积同时失效。搞错类型,方案就完全跑偏。

穿透:查询根本不存在的数据
请求用随机 id 打接口,缓存永远不命中,压力全部落到数据库。两种解法,先看成本最低的空值缓存:
$value = $cache->get($key);
if ($value === null) {
$row = $db->find($id);
$cache->set($key, $row ?: '__NULL__', $row ? 600 : 60);
$value = $row;
}
if ($value === '__NULL__') {
return null;
}
空值缓存实现简单,缺点是会占用键空间,适合 id 空间有限的场景。id 随机、量大的场景用布隆过滤器:把存在的 id 预先放进去,不存在的直接拦截,误判率通常可控制在 1% 以内。
击穿:热点 key 过期的那一瞬间
一个 QPS 上万的 key 到期,所有请求同时回源,数据库瞬间被打穿。两条思路:互斥重建,只放一个请求去查库,其余等待或返回旧值;逻辑过期,value 里带过期时间,物理上不设 TTL,发现过期时异步重建。
$lockKey = "lock:{$key}";
if ($cache->add($lockKey, 1, 10)) {
try {
$data = $db->find($id);
$cache->set($key, $data, 600);
} finally {
$cache->del($lockKey);
}
} else {
usleep(50000);
$data = $cache->get($key) ?: $db->find($id);
}
互斥锁的代价是部分请求多等几十毫秒,换来数据库不被打穿;逻辑过期完全不阻塞,但要容忍短暂脏数据。按业务对一致性的要求选。
雪崩:大量 key 同时失效
最常见的成因是「统一在凌晨预热,TTL 都设成一小时」。解法分三层:
- 过期时间加随机抖动,把失效时刻打散;
- 热点数据逻辑上永不过期,后台定时刷新;
- 多级缓存(本地 + Redis)与限流降级作为兜底。
$ttl = 3600 + random_int(-300, 300);
$cache->set($key, $value, $ttl);
一致性:先更新库,再删缓存
「先删缓存再更新库」在并发下会让旧值被重新写回。常用顺序是:更新数据库 → 删除缓存,删除失败加重试或订阅 binlog 补偿。读多写少、要求更高的场景可以再补一次延迟删除,覆盖并发读回填的窗口。
缓存只保证最终一致。余额、库存扣减这类要求强一致的逻辑,不要走「缓存 + 异步回写」,直接在数据库事务里做。
小结
穿透靠「拦住不存在的请求」,击穿靠「只让一个请求回源」,雪崩靠「打散失效时间 + 兜底降级」。数据结构与键设计见Redis 数据结构选型,框架层的缓存配置见ThinkPHP 缓存与 Session 上 Redis。