直接用 SETNX 不够安全,因其无法原子性完成设置值、过期时间和防误删三件事,易导致死锁或锁被误删;需结合 SET ... NX EX 和 Lua 脚本校验锁归属来保障安全性。

为什么直接用 SETNX 不够安全?
因为 SETNX 只能保证“设置键不存在时才写入”,但无法同时完成“设置值 + 设置过期时间 + 防误删”三件事。常见错误是先 SETNX 成功,再用 EXPIRE 设过期——如果进程在中间崩溃,锁就永远不释放,变成死锁。
更隐蔽的问题是:多个客户端都以为自己持锁成功,结果同时执行临界区逻辑。典型表现是数据库重复插入、库存扣减为负、任务被重复调度。
- Redis 2.6.12+ 支持
SET key value EX seconds NX命令,可原子完成“存在性判断 + 写入 + 过期”,但依然没解决“谁删谁的锁”问题 - 若客户端A持有锁,超时后自动释放,此时客户端B获取到锁;而A还在执行,结束后却执行了
DEL,把B的锁给删了 - 所以必须让“校验锁归属 + 删除”也原子化,只能靠 Lua 脚本
用 redis-py 实现带校验的 Lua 分布式锁
核心思路:加锁时写入唯一标识(如 UUID 或进程 ID),解锁时只允许该标识删除自己的锁。Lua 脚本在 Redis 端执行,天然原子。
示例脚本(保存为 unlock.lua):
立即学习“Python免费学习笔记(深入)”;
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
Python 中调用方式:
- 加锁:用
set(name, value, ex=30, nx=True),其中value是随机字符串(推荐uuid.uuid4().hex) - 解锁:预载 Lua 脚本,传入 key 和相同 value:
unlock_script(keys=[lock_key], args=[lock_value]) - 务必检查返回值:加锁返回
True/False,解锁返回1表示删除成功,0表示锁已被改或不存在
锁的超时时间怎么设才合理?
设太短:业务还没执行完锁就过期,其他客户端趁虚而入;设太长:出错后恢复慢,影响系统吞吐。这不是拍脑袋决定的,得结合实际。
- 先压测单次临界操作耗时,取 P99 值,再乘以 1.5~2 倍作为基础超时(比如 P99 是 800ms,初设为 2s)
- 如果业务中可能阻塞(如调第三方 API、大文件 IO),必须显式做超时控制,并在超时前主动续锁(
GETSET或PEXPIRE)——但续锁本身也有竞态,生产环境建议用 Redlock 或成熟库如redis-lock - Redis 主从异步复制下,主节点写入锁后宕机,从节点升主,原锁可能丢失。严格场景需考虑 Redlock,但多数 Web 场景用单实例 + 合理超时已足够
哪些地方最容易漏掉导致锁失效?
不是写了 SETNX 就算用了分布式锁,很多细节一漏,等于没锁。
- 没校验
set()返回值:redis.set(..., nx=True)失败返回None,不判断就直接进业务逻辑,相当于没锁 - 解锁不用 Lua,而是简单
DEL:这是最常见误用,会导致锁被误删 - 锁 key 没带业务上下文:比如全用
"order_lock",不同订单互相干扰。应拼接业务 ID,如f"order_lock:{order_id}" - 异常路径没解锁:try/except 里忘了
finally或没捕获所有异常类型(比如KeyboardInterrupt、SystemExit),锁会残留
真正难的从来不是写几行 Lua,而是把锁的生命周期和业务流程对齐——超时、重试、清理、监控,缺一不可。


















