缓存穿透、击穿、雪崩区别与防护
解读
国内高并发业务(电商大促、秒杀、直播带货)中,Redis+MySQL 是 PHP 项目的标配。穿透、击穿、雪崩是 Redis 缓存层最常见的三大“黑天鹅”,面试官想确认:
- 能否精准区分三种场景;
- 能否给出 PHP 可落地的代码级防护,而不仅是“加锁”两个字;
- 能否结合业务成本(QPS、内存、运维复杂度)做权衡。
知识点
- 穿透:key 在 Redis 和 MySQL 都不存在,被高并发恶意或异常请求反复查询,流量直达 DB。
- 击穿:热点 key 突然过期,大量同一 key 的并发线程同时回源 MySQL,造成 DB 瞬时 spike。
- 雪崩:大量 key 因同一批过期时间或 Redis 宕机,集体失效,DB 被打垮,引发级联服务不可用。
- PHP 常用武器:Redis 扩展、Lua 脚本、Setnx 分布式锁、Composer 包(predis/phpredis)、本地 APCu 二级缓存、消息队列(RocketMQ/RabbitMQ)异步重建。
- 国内大厂经验:本地布隆过滤器 + 异步刷新 + 随机 jitter + 哨兵集群 + 降级开关,兼顾成本与 SLA。
答案
一、三场景一句话区分
穿透:Redis 无,MySQL 也无,请求像“穿过”一样。
击穿:Redis 本来有,但过期瞬间被高并发“击穿”。
雪崩:Redis 大面积失效,像“雪崩”压垮 DB。
二、PHP 级防护方案(可直接写进简历)
-
穿透
- 布隆过滤器:PHP 使用 bloom-filter 包,初始化时把全表 ID 同步到 Bloom,误判率 0.1%,内存 50 MB 可扛 5000 万 key。
- 空值缓存:查询 MySQL 返回空时,Redis 写
SET key "NULL" EX 60,防止同一恶意 key 反复打 DB。
-
击穿
- 单飞锁(互斥锁):使用 Redis Lua 脚本保证原子性
local ok = redis.call('setnx',KEYS[1],ARGV[1]); if ok==1 then redis.call('expire',KEYS[1],ARGV[2]) end return ok
锁超时 3 s,防止 PHP-FPM 进程挂掉造成死锁。 - 逻辑过期:value 存 JSON
{data:xxx, expireAt:1672531200},后台异步线程(PHP-cli + Supervisor)提前 30 s 重建,前端永不过期,实现“平滑滚动”。
- 单飞锁(互斥锁):使用 Redis Lua 脚本保证原子性
-
雪崩
- 随机 jitter:PHP 在写入时
EXPIRE = base + mt_rand(0,300),打散过期时间。 - 二级缓存:APCu 本地缓存 3 s,Redis 缓存 10 min,形成“漏斗”。
- 熔断降级:基于阿里 Sentinel-PHP 扩展,监控 Redis 失败率 >50% 时直接返回“排队中”静态页,MQ 异步下单,保证核心链路可用。
- 随机 jitter:PHP 在写入时
三、代码片段(面试手写示范)
// 穿透+空值缓存
function getProduct(int $id) {
$key = "p:$id";
$val = redis()->get($key);
if ($val !== null) {
return $val === 'NULL' ? null : json_decode($val, true);
}
$row = db()->query('SELECT * FROM product WHERE id=?', [$id]);
if (!$row) {
redis()->set($key, 'NULL', 60); // 防穿透
return null;
}
redis()->set($key, json_encode($row), 3600 + mt_rand(0,300));
return $row;
}
// 击穿+互斥锁
function getHotProduct(int $id) {
$key = "p:$id";
$val = redis()->get($key);
if ($val) return json_decode($val, true);
$lockKey = "lock:p:$id";
$identifier = uniqid('', true);
// 原子抢锁
$ok = redis()->eval(
"return redis.call('setnx',KEYS[1],ARGV[1])==1 and redis.call('expire',KEYS[1],ARGV[2]) and 1 or 0",
[$lockKey, $identifier, 3], 1
);
if ($ok) {
$row = db()->query('SELECT * FROM product WHERE id=?', [$id]);
if ($row) {
redis()->set($key, json_encode($row), 3600);
}
redis()->del($lockKey); // 及时释放
return $row;
}
usleep(50000); // 自旋 50 ms
return getHotProduct($id); // 重试
}
四、落地成本
- 布隆过滤器全量同步放在凌晨低峰期,PHP-cli 脚本 5 min 跑完,内存增加 50 MB,QPS 提升 3 倍。
- 互斥锁超时 3 s,极端情况下最多 1 个请求回源,DB QPS 从 2 万降到 1。
- 随机 jitter 后,过期峰值打散,Redis 瞬时失效 key 从 20 万降到 1 千,DB 连接池未再报警。
拓展思考
- 多级缓存一致性:如何防止 APCu 与 Redis 数据不一致?可引入版本号机制,APCu 存
ver=12345,Redis 存ver=12346时强制回源。 - 大 Key 热 Key 问题:直播商品库存 key 成为热 key,可采用 Redis 分片 + 本地缓存 + 消息队列异步扣减,实现“读热写热”分离。
- 容灾演练:国内云厂商 Redis 主从切换 3 s 内完成,PHP 侧需捕获
ConnectionException快速降级,避免用户看到 502。 - 成本优化:布隆过滤器误判导致少量空命中,可接受;若业务对误判 0 容忍,可用 RoaringBitmap 或 RedisBloom 模块,但需评估内存与运维复杂度。
- 合规角度:用户隐私数据(如手机号)不能缓存到 Redis,需走内存加密或 Token 化,面试可主动提及,体现安全意识。