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

Redis 缓存穿透、击穿与雪崩:三种故障的区别与应对 示意图

穿透:查询根本不存在的数据

请求用随机 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。