setnx加锁后锁会提前消失,是因为setnx与expire非原子操作,导致死锁或误删他人锁;PHP应使用SET命令NX+PX参数原子加锁,并用Lua脚本校验token后删除,集群下注意key同slot,续期需双重检查token和TTL。

setnx加锁后为什么锁会提前消失?
因为 setnx 本身不支持设置过期时间,单独用它加锁后必须再调用 expire,但这两步不是原子操作。如果进程在 setnx 成功后、expire 执行前崩溃或被杀,锁就永远不释放——这是死锁;反过来,如果锁快过期时业务还没执行完,另一个进程抢到锁并设了新过期时间,原进程结束后却误删了别人的锁——这才是“锁过期误删”的本质。
PHP里怎么用原子命令安全加锁?
别手写 setnx + expire,直接用 Redis 2.6.12+ 的 set 命令带参数:
SET lock:key "my_token" NX PX 30000
其中 NX 表示仅当 key 不存在时才设值,PX 30000 表示毫秒级过期。PHP 中这样调用:
$redis->set('lock:order:123', $token, ['NX', 'PX' => 30000]);
注意:$token 必须是当前进程唯一生成的随机字符串(比如 uniqid('', true)),不能写死,否则删锁时无法区分所有权。
立即学习“PHP免费学习笔记(深入)”;
解锁时为什么不能直接 del key?
因为 del 不检查 value,谁都能删。如果 A 进程的锁过期了,B 进程拿到锁并开始执行,A 进程此时执行完还去 del lock:order:123,就把 B 的锁干掉了。
正确做法是用 Lua 脚本保证“判断 value 再删除”原子性:
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
PHP 中这样执行:
$script = 'if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end';
$redis->eval($script, ['lock:order:123'], [$token]);
关键点:
-
$token必须和加锁时完全一致(建议用base64_encode(random_bytes(16))) - 不要用
get + del两步判断,网络延迟或并发下必然出错 - Redis 集群模式下,
eval脚本的 key 必须落在同一 slot,否则报CROSSSLOT错误
锁自动续期(renew)怎么做才不翻车?
长任务(比如导出大文件)容易超时,需要后台定时续期。但续期本身也有竞争风险:A 在续期时,锁可能已被 B 拿走,这时 A 把 B 的锁又延长了。
安全续期的前提是:只对“自己持有且尚未过期”的锁操作。实现要点:
- 加锁时记录本地过期时间戳(比如
$expireAt = time() + 30) - 续期前先用 Lua 脚本检查当前 value 是否仍为自己的
$token,且剩余 TTL > 0(可用pttl获取) - 续期命令仍是
set lock:key $token XX PX 30000(XX表示仅更新已存在 key) - 别依赖 PHP 的
sleep()做定时器,用独立 worker 或信号触发,避免阻塞主逻辑
真正难的是锁的生命周期管理——加锁、续期、解锁三个环节只要一个没对齐 token 或没做原子校验,分布式环境下的误删就几乎不可避免。



















