redis setnx不能直接作分布式锁,因其无法原子性设置过期时间,易致死锁;须用set key val ex s nx配合唯一value及lua脚本安全解锁。

Redis SETNX 命令为什么不能直接当分布式锁用
因为 SETNX 只能保证“设置不存在的 key”,但没法同时设置过期时间,导致锁可能永远不释放。一旦进程崩溃或网络中断,lock_key 就卡在 Redis 里,后续所有请求全被堵死。
真实场景下,你得原子性地完成「加锁 + 设过期」两件事。PHP 里最稳妥的方式是用 SET 命令的扩展参数:
-
SET lock_key unique_value EX 30 NX:其中unique_value是客户端生成的随机字符串(比如uniqid('', true)),用于后续校验是否自己加的锁 -
EX 30表示 30 秒自动过期,避免死锁;NX确保只在 key 不存在时才设置 - 返回
"OK"表示抢锁成功,nil表示失败,不能靠返回值真假判断,得严格比对字符串
解锁必须用 Lua 脚本,不能先 GET 再 DEL
如果用 PHP 先 GET lock_key 判断值是不是自己的,再调 DEL lock_key,中间存在竞态:另一个客户端可能在 GET 之后、DEL 之前完成加锁,结果你把别人的锁删了。
唯一安全的解锁方式是用 Lua 脚本一次性执行「判断 value 是否匹配 + 删除」:
立即学习“PHP免费学习笔记(深入)”;
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
调用时传入:eval($script, 1, 'lock_key', $unique_value)。注意 KEYS[1] 和 ARGV[1] 必须严格对应,否则脚本返回 0 却误判为删除成功。
PHP 实现里容易漏掉的三个关键点
很多人抄个 demo 就上线,结果压测一跑就出问题。这三个地方几乎必踩坑:
- 没校验
unique_value类型:必须是字符串,且长度足够防碰撞;用mt_rand()拼接不如直接用random_bytes(16)+bin2hex() - 锁超时时间写死:30 秒不是万能值。业务逻辑耗时波动大时,得配合看门狗机制(比如另起一个协程/进程定期续期)
- Redis 连接异常没兜底:
set或eval报RedisException时,不能静默吞掉——要明确抛出或降级为本地锁(仅限单机部署场景)
Redlock 算法在 PHP 项目里通常没必要上
Antirez 提出的 Redlock 是为多 Redis 节点设计的强一致性方案,但实际 PHP 应用里,95% 的场景用单节点 Redis + 正确的 SET + Lua 解锁已足够。引入 Redlock 会显著增加延迟,且 PHP 官方 redis 扩展不原生支持 multi-exec 流水线协调多个实例。
真需要高可用,优先考虑 Redis 哨兵或 Cluster 模式下的主从自动切换,而不是在应用层硬套 Redlock。除非你的业务要求「跨机房容灾级锁一致性」,否则就是过度设计。
最常被忽略的是锁粒度——别拿一个 user:123 当全局锁去控库存,应该按业务实体拆,比如 stock:sku_456。锁太粗,性能瓶颈立马出现;锁太细,维护成本飙升。这事没法靠框架解决,得看具体怎么划分资源边界。











