击穿是热点key过期瞬间并发请求集体查库所致,php7.4下应采用「互斥锁+短期重试」方案:用$redis->set($lockkey,1,['nx','ex'=>5])原子加锁,失败后usleep(50000)重试3次,并对空值缓存设60秒ttl。

直接上结论:击穿不是缓存没设好,而是单个热点 key 在过期瞬间被并发请求集体“围殴”。PHP7.4 下最稳妥的解法是「互斥锁 + 短期重试」组合,而不是盲目调长 TTL 或全量永不过期。
为什么 setex() 无法防击穿
很多人以为用 $redis->setex($key, 300, $data) 设置 5 分钟过期就万事大吉,但问题出在“过期那一刻”——所有请求同时发现 $redis->get($key) 返回 false,然后全部冲向数据库。Redis 的 setex 不带原子性判断,它只管写,不管“别人是不是也在写”。
- 现象:日志里看到同一秒内数据库收到上百次相同
SELECT * FROM products WHERE id = 1001 - 根本原因:缓存读-查库-写缓存三步非原子,中间有竞态窗口
- PHP7.4 特别要注意:
Redis::set()的['nx','ex'=>5]选项必须显式传数组,旧写法set($key, $val, 5)会忽略nx,锁形同虚设
用 SET key value NX EX 实现可靠互斥锁
核心是让第一个请求获得锁、查库、写缓存;其余请求要么等待后重试,要么直接读新缓存。关键在于锁的设置必须是原子的,且带超时防死锁。
- 锁命令必须用
SET lock:product:1001 1 NX EX 5(NX表示仅当 key 不存在才设置,EX 5是 5 秒自动释放) - 不要用
SETNX单独加锁再EXPIRE,这两步不原子,可能锁设置了但没设过期时间 - PHP7.4 中推荐写法:
$redis->set($lockKey, 1, ['nx', 'ex' => 5]),返回true表示抢锁成功 - 抢锁失败后,建议
usleep(50000)(50ms)再重试,最多循环 3 次,避免线程饿死
空值缓存必须配合击穿防护
击穿常和穿透共存——比如用户猛刷一个刚下架的商品 ID,既触发击穿(缓存刚过期),又触发穿透(数据库也查不到)。这时候只加锁没用,得先拦住无效请求。
- 对
NULL结果也写缓存:$redis->setex($key, 60, 'null'),TTL 设 60 秒足够,太长浪费内存,太短起不到缓冲作用 - 读取时需区分真实数据和空占位符:
if ($data === 'null') { return null; },不能只判false或empty() - 布隆过滤器在 PHP7.4 中实现成本高(需扩展或纯 PHP 模拟),优先用空值缓存+参数校验,比如 ID 小于 1 或非数字直接拒掉
永不过期只适用于极少数场景
把热点 key 设成永不过期($redis->set($key, $data) 不带过期)看似一劳永逸,但 PHP7.4 下隐患明显:
- 数据更新靠后台任务?一旦任务失败或延迟,缓存就脏了,用户看到过期信息 Redis 内存不会自动回收,长期运行后
- 集群环境下,永不过期 key 可能因节点迁移、故障恢复丢失,反而更难保证一致性
- 真正适合永不过期的只有极静态数据,比如国家编码表、APP 版本常量,而非商品、用户等业务实体
MEMORY USAGE 持续上涨,OOM 风险真实存在击穿的本质是“热点 + 过期 + 并发”三者叠加,拆掉任意一环都有效。但最可控、副作用最小的,还是用原子锁守住重建入口——这一步漏了,其他优化都是给洪水修装饰栏杆。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











