最简可靠方案是用SET key value NX PX timeout_ms加锁、Lua脚本原子释放;禁用SETNX+EXPIRE(非原子致幽灵锁);解锁必须校验value唯一性,timeout需大于P99业务耗时并预留余量。

直接结论:用 SET key value NX PX timeout_ms 命令加锁,配合 Lua 脚本原子释放,是当前最简、最可靠、性能也足够高的方案。 不需要 Redlock、不依赖集群拓扑、不引入额外组件,只要 Redis 实例本身可用,就能满足绝大多数业务场景的互斥与防死锁需求。
为什么不能用 SETNX + EXPIRE 分两步?
这是新手最容易掉进的坑。表面上看逻辑清晰:先 SETNX 尝试占位,成功后再 EXPIRE 设过期时间。但这两步不是原子操作:
- 如果
SETNX成功,但在执行EXPIRE前客户端崩溃或网络中断,锁就永远卡在 Redis 里,变成“幽灵锁” - Redis 2.6.12 之后已完全支持
SET的复合参数,SETNX + EXPIRE属于过时写法,应彻底弃用 - Spring Data Redis 的
setIfAbsent(key, value, timeout, unit)底层就是封装的SET ... NX PX,可直接用,但要注意它不校验 value 再解锁
释放锁必须用 Lua 脚本,不能只靠 DEL
直接 DEL lock_key 是高危操作 —— 它不检查锁归属,任何客户端都能删掉别人正在用的锁。典型误删链路:
- 客户端 A 加锁成功,value 是
"a123" - A 执行业务慢,锁在 30s 后自动过期被 Redis 清理
- 客户端 B 立即获取到锁,value 是
"b456" - A 此时才执行完,调用
DEL lock_key,结果把 B 的锁给删了
正确做法是用如下 Lua 脚本(必须原子执行):
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
其中 KEYS[1] 是锁 key,ARGV[1] 是加锁时传入的唯一 value(如 UUID)。只有值匹配才删,否则返回 0 表示失败。
timeout_ms 设置多少才安全?
这个值不是越长越好,也不是越短越安全,它必须满足:timeout_ms > 预估最长业务耗时 + 网络抖动余量。常见误区:
- 设成 5s:库存扣减类操作可能因 DB 慢查询、GC 停顿卡住,导致锁提前释放,引发超卖
- 设成 300s(5 分钟):一旦客户端真挂了,其他服务要等整整 5 分钟才能抢到锁,业务不可接受
- 真实建议:从 30s 起步,上线后通过监控观察实际耗时 P99,再上浮 20%~30%;对长任务,考虑“看门狗”续期机制(需额外心跳线程)
注意:timeout_ms 是锁的绝对存活时间,不是“业务开始后多久过期”。Redis 不会因为你还在执行就延长它 —— 这点和本地锁完全不同。
真正容易被忽略的是:锁的 value 必须全局唯一且不可预测。用固定字符串(如 "1")或进程 PID 当 value,等于没做归属校验;用毫秒时间戳也不行,精度不够且易碰撞。务必用 UUID.randomUUID().toString() 或加密随机数生成器。



















