Python Web项目中怎么做分布式锁防并发冲突_使用redis-py实现SETNX与Lua原子脚本

Python Web项目中怎么做分布式锁防并发冲突_使用redis-py实现SETNX与Lua原子脚本

Python

直接用 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):

if redis.call("GET", KEYS[1]) == ARGV[1] then
    return redis.call("DEL", KEYS[1])
else
    return 0
end

Python 中调用方式:

Clippah

AI驱动的创意视频处理平台

  • 加锁:用 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),必须显式做超时控制,并在超时前主动续锁(GETSETPEXPIRE)——但续锁本身也有竞态,生产环境建议用 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 或没捕获所有异常类型(比如 KeyboardInterruptSystemExit),锁会残留

真正难的从来不是写几行 Lua,而是把锁的生命周期和业务流程对齐——超时、重试、清理、监控,缺一不可。

本文地址:https://www.sztg.com.cn/article/616688.html

如非特殊说明,本站内容均来自于网友自主分享,如有任何问题均请联系我们进行处理!

猜您喜欢