Redis SETNX锁失效的根本原因是未保障原子性及过期机制,正确做法是用SET key value NX EX seconds一条命令设锁,value用唯一标识,解锁用Lua脚本校验后删除,并配合指数退避重试。
Redis SETNX 实现锁时为什么总失效
根本原因不是命令用错,而是没处理好原子性边界和过期时间。比如只用 SETNX 设键,却忘了设过期,一旦进程崩溃或网络中断,锁就永远卡住;或者先 SETNX 再 EXPIRE,中间若服务挂了,锁就无过期机制。
正确做法是用 SET key value NX EX seconds 一条命令完成「不存在才设值 + 自动过期」,PHPRedis 中对应 set($key, $value, ['nx', 'ex' => $ttl])。其中 $value 必须是唯一标识(如 uniqid('', true)),后续解锁时靠它校验所有权,避免误删别人持有的锁。
- 不要用
get+set或setnx+expire两步操作 -
$ttl建议设为业务执行最大耗时的 2–3 倍,太短易误释放,太长会阻塞后续请求 - 锁 key 最好带业务上下文,例如
"lock:order:create:{$userId}",避免全局冲突
解锁必须用 Lua 脚本保证原子性
单纯 GET 判断再 DEL 是典型竞态:A 进程读到自己的 value,正要删,B 进程已超时释放并重新持锁,此时 A 的 DEL 就把 B 的锁干掉了。
唯一安全解法是用 Lua 脚本在 Redis 端完成「判断 value 相等再删」,PHP 中这样写:
立即学习“PHP免费学习笔记(深入)”;
public function unlock($key, $value) {
$script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
return $this->redis->eval($script, [$key], [$value]);
}
注意:eval() 返回 1 表示删除成功,0 表示未操作(可能锁已过期或被覆盖),不能忽略返回值直接认为解锁成功。