用 Redis 缓存后,高并发下常遇到穿透、击穿、雪崩三个问题。网上资料多但零散,这里把每个问题的成因、危害、代码级解法一次性讲清楚。
1. 缓存穿透:查了永远不存在的 key
恶意请求反复查一个数据库里不存在的 id,每次都能穿透缓存直达 DB,白名单被拖垮。解法是缓存空值 + 布隆过滤。
// 解法A:缓存空对象(简单可靠)
// 查库为 null 时也写入缓存,TTL 设短一些(如60秒)
if ($data === null) {
$redis->setex($key, 60, serialize(null));
return null;
}
// 解法B:布隆过滤器(能过滤99%不存在请求,需引入bloomfilter扩展或封装)
if (!$bloom->maybeHas($id)) {
return null; // 一定不存在
}
2. 缓存击穿:热点 key 过期瞬间
一个超高并发的热点 key 恰好过期,瞬间请求全打到 DB。解法是互斥锁重建缓存或逻辑过期。
function getWithMutex(string $key, callable $loader, int $ttl) {
$redis = Redis::client();
$data = $redis->get($key);
if ($data !== false) return unserialize($data);
// 加锁:同一时刻只允许一个请求重建缓存
$lockKey = $key . ':lock';
if ($redis->set($lockKey, 1, ['NX', 'EX' => 5])) {
$value = $loader(); // 只有持锁者查库
$redis->setex($key, $ttl, serialize($value));
$redis->del($lockKey);
return $value;
}
// 未抢到锁:短暂等待后重试读缓存
usleep(50 * 1000);
return getWithMutex($key, $loader, $ttl);
}
3. 缓存雪崩:大量 key 同时过期
大量缓存同一时间失效,请求全部落到 DB。解法是过期时间加随机抖动 + 多级缓存。
// 过期时间加随机值(3~8分钟随机),避免同批 key 一起过期
$expire = $baseTtl + random_int(0, 300);
$redis->setex($key, $expire, serialize($data));
// 更稳的兜底:DB 层面做熔断,比如 ORM 查询加开关,异常时返回降级数据
小结
穿透治在"查空值也要缓存",击穿治在"热点 expired 要加锁重建",雪崩治在"过期时间错峰"。三个问题本质都是"缓存失效时如何保护 DB",理解了成因,代码就简单了。