ThinkPHP8需手动实现Redis可重入分布式锁,核心是SET单命令加锁、Lua脚本原子解锁、唯一标识校验及合理过期时间,禁用session GC并确保phpredis扩展与cache配置一致。

ThinkPHP8 用 Redis 实现写入锁,核心不是配个开关就行,而是得手动构造可重入、带自动过期、防死锁的锁逻辑。框架本身不提供开箱即用的“分布式锁”组件,必须自己封装,且要避开常见陷阱。
Redis 锁的配置要点
TP8 不内置 Redis 分布式锁驱动,所以没有 config/xxx.php 里直接填 lock => true 这种配置。所谓“配置”,实际是三件事:
- 确保 Redis 已正确接入:host、port、database、auth(ACL 模式)、timeout、persistent 全部在 config/cache.php 中显式写死,不能依赖 .env;
- 禁用 PHP 原生 session GC(
ini_set('session.gc_probability', 0)),否则锁 key 可能被误删; - 确认项目已启用 phpredis 扩展(非 Predis),因为 TP8 的 Cache::store('redis') 底层调用的是 phpredis,性能和原子性更可靠。
锁的实现方式(推荐)
不要用 SETNX + EXPIRE 两步走——存在竞态窗口。必须用 SET key value EX seconds NX 单命令完成加锁,TP8 可这样写:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- 用原生 phpredis:
$redis = \think\facade\Cache::store('redis')->handler(); $locked = $redis->set('lock:order:'.$orderId, $uniqId, ['nx', 'ex' => 10]); - 锁值设为唯一标识(如 request_id 或 uniqid()),便于后续校验是否自己加的锁;
- 过期时间建议 5–30 秒,需略长于业务主流程耗时,但不能无限长;
- 解锁必须先判断锁归属再 DEL(防止误删别人锁),可用 Lua 脚本保证原子性:
EVAL "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end" 1 lock:order:123 abc123。
典型使用场景示例
比如防止重复提交订单:
立即学习“PHP免费学习笔记(深入)”;
- 用户提交时生成业务唯一 key:
$lockKey = 'lock:submit:' . $userId . ':' . $goodsId; - 尝试加锁,失败则返回“操作太频繁”;
- 加锁成功后,立即查 DB 确认库存、状态等前置条件;
- 所有 DB 写操作包裹在
Db::startTrans()中,锁与事务生命周期解耦(锁只管入口,DB 事务管数据一致性); - 无论成功失败,都执行解锁逻辑(含 try-finally 或 register_shutdown_function 保底)。
避坑提醒
很多项目锁没起作用,是因为:
- 只在 .env 改了 CACHE_STORE=redis,但 config/cache.php 里 host/port/database 缺项,导致
Cache::store('redis')实际走 file 驱动,锁 key 根本没进 Redis; - 用
Cache::set('lock:xxx', 1, 10)代替原生命令——Cache 封装会加 prefix,且过期逻辑不可控,无法满足原子加锁要求; - 锁 key 没带业务上下文(如用户 ID、商品 ID),导致全局互斥,吞吐量骤降;
- 没设
persistent => true,高并发下连接创建开销反成瓶颈,锁反而变慢。


















