TP5.1用SETNX防重复不加原子性保障和唯一标识校验等于没锁;必须用set命令原子设置+过期+唯一值,并用Lua脚本校验后删除,否则会死锁或误删他人锁。

直接说结论:TP5.1 用 SETNX 做定时任务防重复,**不加原子性保障和唯一标识校验,等于没锁**。常见表现是“看起来只执行一次”,但高峰或异常时仍会重复写库、发消息、扣库存。
为什么 TP5.1 的 setNx() 调用本身不安全
ThinkPHP 5.1 的 Redis::setNx() 方法只做键存在性判断,不带过期时间设置——必须额外调用 expire()。这两步非原子执行,一旦进程在 setNx() 成功后、expire() 前崩溃,锁就永远卡住。
- TP5.1 默认 Redis 驱动不提供原子
SET key value NX EX seconds封装,得自己拼命令或改底层 - 很多示例代码写成:
$redis->setNx('task:sync', 1); $redis->expire('task:sync', 30);—— 这就是经典死锁温床 - 即使加了
expire,解锁时若用$redis->delete('task:sync'),仍可能删掉别人刚续上的锁
TP5.1 中真正可用的加锁写法(带唯一值 + 过期)
必须绕过框架封装,直调 eval 或用 set 命令的原子参数。推荐用 set 命令一步到位:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
php
// 正确:原子设置 + 过期 + 唯一值(防止误删)
$lockKey = 'task:order_cleanup';
$lockValue = uniqid('', true); // 必须每个客户端不同,不能硬编码
$result = $redis->set($lockKey, $lockValue, ['nx', 'ex' => 60]);
// 返回 false 表示抢锁失败,直接 return
if (!$result) {
return;
}
-
['nx', 'ex' => 60]对应 Redis 原生命令SET task:order_cleanup "xxx" NX EX 60 -
uniqid('', true)比时间戳更可靠,避免多进程同一毫秒生成相同值 - TP5.1 的
Redis::set()支持该数组参数,无需手写eval脚本
解锁必须用 Lua 脚本,不能直接 delete
直接 $redis->delete($lockKey) 是最大雷区:A 进程锁过期,B 抢到锁;A 业务结束仍执行 delete,把 B 的锁干掉了。
php
// 正确解锁 Lua 脚本(原子判断+删除)
$lua = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
$redis->eval($lua, [$lockKey], [$lockValue]);
- 脚本里用
ARGV[1]校验是否自己设的值,不是就拒绝删除 - TP5.1 的
Redis::eval()可直接传入,别图省事用delete - 如果业务逻辑耗时可能超锁过期时间(比如 DB 慢查询),需额外加自动续期逻辑,否则锁提前释放
最易被忽略的点:锁有效期不是拍脑袋定的。它必须大于「最长可能业务耗时」,但又不能太长——否则故障时恢复窗口过大。建议先压测出 P99 耗时,再加 20% 缓冲,而不是统一写 30 秒或 300 秒。

















