学完这篇,你能分清缓存穿透、击穿、雪崩分别是什么、各自在什么场景下发生,并且拿到一套可以直接抄进项目的 PHP 代码方案。
第一步:先把三个概念分清楚,别混着治
三个词听着像,其实病因完全不同,用错药会更糟:
- 穿透:请求的数据在数据库里根本不存在(比如有人拿 id=-1 疯狂刷),缓存永远不命中,每次都打到 DB。
- 击穿:某个热点 key 恰好过期的那一瞬间,成百上千并发同时没命中,一起冲到 DB。
- 雪崩:大批 key 同时过期,或者 Redis 整台挂了,流量整体压到 DB 上。
一句话记:穿透是"查不存在的",击穿是"一个热点塌了",雪崩是"一片同时塌了"。
第二步:治穿透——空值缓存 + 布隆过滤器 + 入口校验
最省事的做法是空值也缓存,但给个短 TTL,防止被大量随机 key 撑爆内存:
$val = $cache->get($key);
if ($val === null) {
$row = $db->find($id);
if (!$row) {
$cache->set($key, '__NULL__', 60); // 空值只存 60 秒
return null;
}
$cache->set($key, $row, 600);
return $row;
}
return $val === '__NULL__' ? null : $val;
量大的站再加布隆过滤器(Redis 的 BF.ADD / BF.EXISTS),把所有合法 id 预先塞进去,查询前先问一句"这 id 可能存在吗",不存在直接返回,连 Redis 都不用查。
注意:空值缓存的 TTL 一定要短(30~120 秒),否则一旦这条数据后来被创建了,用户会看到"明明有却打不开",排查起来非常费劲。
顺带在入口做一层参数校验:id 必须是正整数、页码不超过上限、搜索关键词长度限制。很多穿透压根不是攻击,是前端分页组件把 page=0 传过来了。
第三步:治击穿——互斥重建,只放一个人去查库
热点 key 过期时,不要让一千个请求一起去查库,只让第一个去重建,其余等待或直接用旧值。
$val = $cache->get($key);
if ($val !== null) return $val;
// 抢锁,只有一个请求能拿到
if ($cache->add($lockKey, 1, 10)) { // SETNX + EX
try {
$row = $db->find($id);
$cache->set($key, $row, 600);
} finally {
$cache->del($lockKey); // 必须放 finally,别让异常把锁焊死
}
} else {
usleep(50000); // 等 50ms 重试
return $cache->get($key) ?? $db->find($id); // 兜底
}
return $row;
更彻底的做法是逻辑过期:key 本身不设 TTL(永不真正过期),在 value 里塞一个 expire_at 字段。读到过期数据时,先返回旧值给用户,同时异步起一个任务去刷新。这样用户端永远是毫秒级响应,没有等待。
注意:抢锁方案里锁的 TTL 一定要大于"查库+回填"的最长耗时,否则锁提前释放,防击穿就变成了击穿。建议 TTL 设为预估耗时的 3 倍。
第四步:治雪崩——打散过期时间 + 多级兜底
三件事按优先级做:
1. 过期时间加随机抖动,这是性价比最高的一招:
$ttl = 600 + mt_rand(0, 120); // 10 分钟 ± 2 分钟
2. 冷启动预热:站点重启或 Redis 重启后,用计划任务把榜单、首页数据提前灌进去,别等用户来触发。像 Clara BBS 这类系统内置了计划任务体系(Cron::register 注册,前台访问懒触发、/cron 入口精准触发),把预热脚本挂上去就行,不用额外装 crontab。
3. 缓存层本身要能降级:Redis 挂了不能等于全站挂了。判断依据是"读缓存失败就退回读库",而不是抛异常。Clara BBS 的 Cache 层就是 Redis 异常时自动降级到文件驱动,键还带表前缀命名空间,多站共用一个 Redis 实例也不会串数据;业务侧统一用 Cache::remember 一行式调用,降级逻辑完全不用自己写。
注意:多级缓存(本地内存 + Redis)能扛住 Redis 短暂抖动,但要处理"本地缓存更新不及时"的问题,一般给本地缓存设 1~5 秒的极短 TTL 就够了,别设太长。
第五步:加监控,别等出事才知道
缓存问题最怕的是"没感觉"。至少盯三个数:命中率(低于 80% 要查原因)、DB QPS 突刺(缓存塌了的直接信号)、Redis 内存与慢查询。
Clara BBS 后台自带容量监控仪表盘,每日快照帖子/用户/附件/数据库/磁盘用量并画增长曲线,还带 MySQL 慢查询 TOP 榜——缓存击穿时慢查询会立刻冒出来,比翻日志快得多。缓存开关在「系统工具 → 缓存设置」,可视化开 Redis,不用改代码。
小结
- 穿透治"不存在":空值缓存(短 TTL)+ 布隆过滤器 + 入口参数校验
- 击穿治"单个热点":互斥锁重建 或 逻辑过期 + 异步刷新,锁放
finally 释放
- 雪崩治"批量失效":TTL 加随机抖动、冷启动预热、缓存层可降级
- 所有方案的前提是缓存失败不能拖垮业务,读不到缓存就退回读库,别抛异常
- 上线前把命中率、DB QPS 突刺、Redis 慢查询纳入监控,出了问题第一时间能定位