PHP Redis 缓存穿透和击穿防护工作流:从空值缓存到互斥锁
来源:17golang原创
时间:2026-06-18 13:23:25 229浏览 收藏
PHP 后端接入 Redis 缓存后,最常见的两个线上风险是缓存穿透和缓存击穿:一个是不停查询不存在的数据,导致请求绕过缓存直打数据库;另一个是热点 Key 过期瞬间,大量请求同时回源。它们看起来都是“数据库突然变慢”,但修复手段并不一样。
本文用一套完整工作流,把 PHP + Redis 的缓存防护拆成几个阶段:先定义缓存 Key 和命中路径,再处理空值缓存,接着对热点 Key 加互斥锁和 TTL 抖动,最后用压测和日志验证修复效果。
- 目标和边界:要防的是穿透和击穿,不是所有慢查询
- 全流程总览:请求、缓存、数据库和回填
- 阶段 1:统一缓存 Key 和命中路径
- 阶段 2:用空值缓存挡住穿透请求
- 阶段 3:用互斥锁保护热点 Key 回源
- 阶段 4:增加 TTL 抖动和回归指标
- 推荐流程:一次缓存防护怎么落地
- 容易踩坑:为什么加了缓存仍然打爆数据库
- 速查表:场景、做法和检查点
目标和边界:要防的是穿透和击穿,不是所有慢查询
先把边界定清楚。缓存穿透通常发生在“不存在的数据”上,比如恶意或异常请求不断查 product:99999999。缓存击穿通常发生在“热点数据刚好过期”的瞬间,比如首页推荐、商品详情、活动库存等。
这篇文章只讨论 PHP 应用层常用的 Redis 防护方案,不展开数据库索引优化、队列削峰和复杂分布式一致性。目标是让读者能写出一条稳定的查询链路:先查缓存,缓存缺失再回源,回源时避免并发打满数据库。
全流程总览:请求、缓存、数据库和回填
先说结论:缓存防护不能只写一个 get 和 set。完整链路应该包含命中判断、空值缓存、互斥回源、结果回填、TTL 抖动和指标观察。

| 阶段 | 目标 | 关键动作 | 检查点 |
|---|---|---|---|
| Key 设计 | 让缓存入口统一 | 按业务实体生成稳定 Key | 同一资源不会生成多个 Key |
| 命中判断 | 区分命中、空值和缺失 | 约定空值标记 | 不存在数据不会反复查库 |
| 互斥回源 | 保护热点 Key 过期瞬间 | 只有拿到锁的请求查库 | 并发回源数量可控 |
| TTL 抖动 | 避免大量 Key 同时过期 | 给过期时间加随机区间 | 过期曲线不集中尖峰 |
| 回归指标 | 证明方案有效 | 看命中率、回源次数、延迟 | 高峰期间数据库压力下降 |
阶段 1:统一缓存 Key 和命中路径
目标:先把缓存入口收拢到一个函数里,避免不同业务模块各写一套 Key 和过期时间。
工具选择:Key 生成函数负责命名规范,TTL 函数负责抖动。不要把 product_、product:、goods: 混着用,否则缓存命中率会被自己打散。
检查点:同一个资源在代码里只有一种 Key;缓存时间在一个地方定义;日志里能看出命中、缺失和回源。
阶段 2:用空值缓存挡住穿透请求
目标:不存在的数据也要短暂缓存,避免每次都去查数据库。
get($key);
if ($cached === EMPTY_MARK) {
return null;
}
if ($cached !== false && $cached !== null) {
return json_decode($cached, true);
}
$row = queryProductFromDb($id);
if ($row === null) {
$redis->setex($key, 60, EMPTY_MARK);
return null;
}
$redis->setex($key, ttlWithJitter(600), json_encode($row, JSON_UNESCAPED_UNICODE));
return $row;
}
工具选择:空值缓存的 TTL 要短,通常几十秒到几分钟即可。这样既能挡住异常请求,也不会让刚新增的数据长期不可见。
检查点:不存在 ID 的请求第一次会查库,后续短时间内直接返回空;数据库日志里不会持续出现同一个不存在 ID 的查询。
阶段 3:用互斥锁保护热点 Key 回源
目标:热点 Key 过期时,只允许少量请求回源,其余请求等待、短暂重试或返回旧值。

set($lockKey, "1", ["nx", "ex" => 5]);
if (!$locked) {
usleep(100000);
$retry = $redis->get($key);
if ($retry && $retry !== EMPTY_MARK) {
return json_decode($retry, true);
}
return null;
}
try {
$row = queryProductFromDb($id);
if ($row === null) {
$redis->setex($key, 60, EMPTY_MARK);
return null;
}
$redis->setex($key, ttlWithJitter(600), json_encode($row, JSON_UNESCAPED_UNICODE));
return $row;
} finally {
$redis->del($lockKey);
}
}
工具选择:set nx ex 适合做短时互斥。锁时间要短于业务可接受等待,避免异常请求长期占锁。更复杂的跨进程一致性场景,需要结合业务风险再选择更强方案。
检查点:热点 Key 过期瞬间,数据库回源次数明显下降;请求延迟不会集体冲高;锁 Key 能自动过期,不会卡死链路。
阶段 4:增加 TTL 抖动和回归指标
目标:避免一批 Key 同时过期,并通过指标确认方案真实有效。
900,
"normal" => 600,
default => 300,
};
return $base + random_int(0, 120);
}
检查点:观察缓存命中率、数据库查询次数、慢请求数量和 Redis 锁等待次数。修复是否有效,不看“代码写了”,要看高峰期间回源压力有没有下降。
推荐流程:一次缓存防护怎么落地
- 先统一缓存 Key 命名,收敛到一个入口函数。
- 明确命中、缺失、空值三种状态,不要把空字符串和缺失混为一谈。
- 对不存在的数据写短 TTL 空值缓存。
- 对热点 Key 的回源路径加短时互斥锁。
- 给正常缓存 TTL 加随机抖动,避免同一时间集中失效。
- 记录命中率、回源次数、锁等待和请求耗时。
- 用压测或高峰日志对比修复前后的数据库压力。
容易踩坑:为什么加了缓存仍然打爆数据库
坑 1:把不存在数据当成普通缺失
如果数据库查不到就直接返回,不写空值缓存,攻击或异常参数仍然会绕过缓存持续查库。
坑 2:所有 Key 用同一个过期时间
批量写入时如果 TTL 完全相同,过期也会集中发生。加入小范围随机抖动,可以把回源压力摊开。
坑 3:锁没有过期时间
互斥锁必须设置短过期时间。否则拿锁请求异常退出后,后续请求会一直认为有人在回源。
坑 4:只看 Redis 命中,不看数据库回源
缓存命中率提升是好信号,但数据库回源次数、慢请求数量和高峰延迟才是最终判断依据。
速查表:场景、做法和检查点
| 场景 | 推荐做法 | 检查点 |
|---|---|---|
| 不存在 ID 高频请求 | 短 TTL 空值缓存 | 同一不存在 ID 不再反复查库 |
| 热点 Key 过期 | 短时互斥锁保护回源 | 同时查库请求数量下降 |
| 批量 Key 同时失效 | TTL 加随机抖动 | 过期曲线不再集中尖峰 |
| 锁等待过多 | 缩短回源耗时或返回旧值 | 请求延迟不明显放大 |
| 方案是否有效 | 看命中率、回源、慢请求 | 高峰数据库压力下降 |
总结一下:PHP Redis 缓存防护要把穿透和击穿分开处理。不存在的数据用短 TTL 空值缓存,热点 Key 回源用互斥锁保护,正常缓存用 TTL 抖动分散过期,最后用回源次数和请求耗时验证效果。这样缓存才不只是“加了一层 Redis”,而是真正能保护数据库。
-
286 收藏
-
117 收藏
-
185 收藏
-
426 收藏
-
134 收藏
-
文章 · php教程 | 2小时前 | Cookie · session · php教程 · 登录态 · 后端排查 · php cookie session php-fpm SameSite session_start 登录态丢失484 收藏
-
336 收藏
-
文章 · php教程 | 1天前 | WEB开发 · 登录状态 · Cookie · PHP · session · session_start · php cookie session session_start PHPSESSID 登录态丢失196 收藏
-
227 收藏
-
483 收藏
-
文章 · php教程 | 2天前 | PHP · MD5 · 登录安全 · password_hash · password_verify · password_hash password_verify 登录安全 PHP密码迁移 MD5迁移174 收藏
-
422 收藏
-
420 收藏
-
306 收藏
-
204 收藏
-
322 收藏
-
439 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习