thinkphp 8.0 静态缓存不解决缓存击穿,需用 redis + 互斥锁:通过 setnx 加锁、双重检查、lua 安全删锁,并设置锁超时远小于缓存 ttl,配合兜底重试或读过期值。

ThinkPHP 8.0 的静态缓存(如 Cache::store('file') 或 Cache::store('tag') 驱动)本身不解决缓存击穿问题,反而在高并发下更容易触发击穿——因为文件/标签类缓存缺乏原子性操作和分布式锁能力,多个请求同时发现缓存失效,会一窝蜂查库、写缓存,数据库瞬间承压。
要真正应对缓存击穿,必须绕过静态缓存的局限,用带互斥锁的动态缓存策略,核心思路是:让只有一个请求去重建缓存,其余请求等待或回退旧值。
缓存击穿在 ThinkPHP 8.0 静态缓存中的典型表现
- 使用
Cache::get('goods_123')读取商品信息,缓存过期后返回null - 多个并发请求同时进入“查库 → 写缓存”流程
- 数据库被重复查询数十次,缓存中可能写入多个不一致版本
- 文件缓存驱动还会因磁盘 I/O 和文件锁导致响应延迟加剧
必须改用 Redis + 互斥锁机制
ThinkPHP 8.0 支持 Redis 缓存驱动(需安装 predis/predis 或 phpredis),这是实现安全重建的前提:
-
锁 key 要独立命名:比如
"mutex:goods:123",和业务缓存 key"goods:123"分开,避免误删 -
SETNX + 过期时间必须组合使用:
$lockKey = 'mutex:goods:' . $id; $lockValue = uniqid('', true); // 防止误删 $result = $redis->set($lockKey, $lockValue, ['nx', 'ex' => 5]); // 锁最多持5秒 - 获取锁后务必双重检查缓存:防止锁期间已有其他请求写入
-
释放锁必须用 Lua 脚本校验 value:
if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end
实际封装建议(控制器中不要手写锁逻辑)
新建
app/service/Mutex.php,提供acquire()和release()方法-
在业务方法中统一调用:
$cacheKey = 'goods:' . $id; $data = Cache::get($cacheKey); if ($data !== null) { return $data; } if (Mutex::acquire($cacheKey)) { try { $data = Db::name('goods')->find($id); if ($data) { Cache::set($cacheKey, $data, 600); // 缓存10分钟 } } finally { Mutex::release($cacheKey); } } else { // 未抢到锁:等待100ms后重试,或直接返回旧缓存(若支持强制读过期值) usleep(100000); $data = Cache::get($cacheKey, null, true); // 第三个参数 true 表示读取已过期key(需 Redis 驱动支持) }
特别注意兜底细节
- 锁超时(如 5 秒)必须明显短于缓存 TTL(如 600 秒),否则锁释放了缓存还没写上,又进一轮击穿
- 查库失败时,主动删锁并返回旧缓存(
Cache::get($key, null, true)),不能让其他请求无限等待 - 不要用
Cache::tag()管理锁——Tag 在文件驱动下无原子性,Redis 驱动下也非强一致性
不复杂但容易忽略。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











