缓存击穿的本质是热点key过期瞬间多个请求同时发现缓存miss,争抢重建导致数据库压力激增;需用带ex过期的setnx实现分布式互斥锁,锁key与数据key分离,配合双重检查、空值校验、锁续期及合理退避策略。

缓存击穿的本质是并发重建失效
缓存击穿不是Redis本身的问题,而是PHP业务逻辑在key过期瞬间缺乏协调机制导致的——多个请求同时发现$redis->get($key)返回false,又都去调用数据库并写缓存。phpredis扩展本身不提供自动防护,修复必须靠代码层加锁或策略兜底。
用setnx实现轻量互斥锁最直接
别依赖框架封装,直接用phpredis原生命令控制入口。关键点不在“加锁”,而在“谁该去查库、谁该等结果”:
-
setnx必须配合ex参数设置锁超时(如5秒),避免死锁;单独setnx没过期会卡住整个key - 锁key和数据key要分离,比如数据key是
user:1001,锁key建议用lock:user:1001 - 获取锁失败后不能立刻返回错误,得
usleep(50000)再重试几次,否则大量请求直接打库 - 回调函数
$callback()执行完必须del锁,且要用try/finally包裹,防止异常漏删
示例片段:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
$lockKey = 'lock:' . $key;
if ($redis->set($lockKey, 1, ['nx', 'ex' => 5])) {
try {
$data = $callback();
$redis->setex($key, $ttl, $data);
return $data;
} finally {
$redis->del($lockKey);
}
} else {
usleep(50000);
return $this->getData($key, $callback, $ttl); // 递归重试,加最大重试次数防栈溢出
}
永不过期+后台刷新更适合高频热点
对像首页推荐、商品详情这类绝对不能抖动的key,硬加锁反而拖慢首屏。更稳的做法是让缓存“逻辑上永不过期”,靠异步任务维持新鲜度:
- 写缓存时不设
TTL,用$redis->set($key, $data);但值里嵌入时间戳字段,如{"data": {...}, "updated_at": 1720852620} - 每次读取先判断
updated_at是否超过业务容忍阈值(比如10分钟),超了就触发refreshAsync($key)后台更新 - 后台刷新用
setnx抢锁,成功才查库写新值,失败直接退出——避免多个进程重复刷 - 注意:这种模式下
$redis->ttl($key)永远返回-1,监控时别误判为“缓存失效”
phpredis扩展本身不解决击穿,但影响锁可靠性
你用的phpredis版本和连接方式会决定set原子性是否真可靠:
- 低于5.3.0的phpredis,在Redis集群模式下
set(['nx','ex'=>N])可能被拆成多条命令,失去原子性——必须升级到phpredis >= 5.3.4 - 用
connect()直连单节点没问题,但用phpredis的RedisCluster类时,set命令必须确保key落在同一slot,否则nx+ex跨节点失效 - 连接池配置不当(如
timeout太短)会导致set超时返回false,被误判为“锁获取失败”,实际是网络抖动——建议timeout设为2秒以上
真正容易被忽略的点:所有锁操作必须走同一个Redis实例。如果项目混用了主从连接(读走从库、写走主库),setnx在主库成功,但后续get却从从库读到空值,击穿照旧发生。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!










