PHP中Redis SETNX加锁失效的根本原因是它仅保证“设值不覆盖”,未解决自动续期、异常释放、原子性删除等实际问题;必须配合EX过期时间、唯一锁值及Lua脚本校验释放,且避免主从脑裂风险。

PHP用SETNX加锁为什么经常失效
根本原因不是SETNX不好用,而是它只做“设值不覆盖”这一件事,没解决锁的自动续期、异常释放、原子性释放等现实问题。比如进程卡死没主动删锁,或网络超时导致DEL没执行,锁就永远挂在那里。
常见错误现象:SETNX返回1以为锁成功,但后续业务逻辑出错没释放;多个请求同时判断锁存在后都去GET再DEL,结果删了别人的锁;锁过期时间写死10秒,但任务偶尔跑12秒,中间出现并发。
- 永远别单独依赖
SETNX,必须配合过期时间(用SETEX或SET ... NX EX) - 锁值必须是唯一标识(如
uniqid().getmypid()),不能是固定字符串,否则无法安全校验和释放 - 释放锁必须用Lua脚本保证“判断+删除”原子性,例如:
if redis.call("GET", KEYS[1]) == ARGV[1] then return redis.call("DEL", KEYS[1]) else return 0 end
PHP中Redis::set的NX和EX参数怎么配才安全
PHP 7.0+ 的Redis::set支持原子性设置带过期的锁,比老式SETNX+EXPIRE组合更可靠——后者有竞态窗口:SETNX成功但EXPIRE失败,锁就永不过期。
正确写法是:$redis->set($key, $lockValue, ['NX', 'EX' => 30])。注意:NX必须大写,EX单位是秒(不是毫秒),且两个选项要一起传进数组,不能拆开。
立即学习“PHP免费学习笔记(深入)”;
- 不要用
setex或setnx单独调用,它们无法在一个命令里完成“不存在才设+带过期” - 过期时间建议按「P99业务耗时 × 2」预估,太短易误失锁,太长会阻塞恢复
- 如果Redis扩展版本低于5.0,
set不支持选项数组,得降级用EVAL执行Lua
锁被别人释放了怎么办?如何验证锁归属
分布式环境下,谁加的锁谁才能删,否则A刚处理到一半,B以为锁过期强行删掉,A接着删一次——结果B的后续操作就直接冲进临界区。关键在锁值唯一且可验证。
每次释放前,必须用Lua读取当前值并比对是否等于自己设的$lockValue。PHP里这样调用:
$script = "if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) else return 0 end";$redis->eval($script, [$key], 1, $lockValue);
-
$lockValue推荐用sprintf('%s:%d', uniqid('', true), getmypid()),兼顾全局唯一和进程可追溯 - 别用时间戳或随机数单独作value,碰撞概率虽低,但一旦撞上就是数据错乱
- 如果Lua执行返回0,说明锁已被他人修改或过期,此时不应重试加锁,而应检查业务是否已由其他节点完成
Redis主从切换时锁失效的真实风险
Redis哨兵或Cluster模式下,主节点宕机,从节点升主,但原主上未同步到从的锁可能还在。新主没有那个key,其他客户端一查就发现“锁没了”,立刻抢入——这就是经典的脑裂导致锁失效。
这不是PHP代码能绕过的,是Redis单机模型的固有限制。想缓解,只能降低主从复制延迟(调小repl-backlog-size、禁用slave-read-only以外的写)、或改用Redlock(但官方已不推荐)。
- 生产环境别指望靠“多实例+多数派”完全解决,Redlock在GC停顿、时钟跳跃场景下依然不可靠
- 真正稳的方案是:把锁粒度收窄(比如按用户ID分片加锁),或改用ZooKeeper/etcd这类强一致协调服务
- 如果坚持用Redis,至少确保所有锁操作走同一个Redis实例(别分散在不同db或cluster slot)
锁最难的从来不是怎么加,而是怎么确认它还有效、没被悄悄绕过、也没在故障时变成幻觉。越想用简单命令搞定,后面踩的坑越深。



















