单机用 threading.Lock 就够了,多实例部署就必须上分布式锁。Redis 是最常见的方案,但实现细节差一点,可靠性差很多。
第一版:SETNX + DEL
if redis.setnx(key, 1):
try:
do_something()
finally:
redis.delete(key)
两个致命问题:
- 进程崩了没执行到 delete,锁永久不释放
- A 的锁超时自动释放了,B 拿到锁,A 执行完把 B 的锁删了
第二版:加过期时间 + 校验持有者
import uuid
token = str(uuid.uuid4())
ok = redis.set(key, token, nx=True, ex=10)
if ok:
try:
do_something()
finally:
# 用 Lua 保证“判断 + 删除”原子
redis.eval("""
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
""", 1, key, token)
SET key val NX EX 10 一条命令原子完成“不存在才设置”和“设过期”,比 setnx + expire 两条命令安全——后者中间崩溃就没过期时间了。
删除时用 Lua 脚本,因为 get 再 del 是两步,中间可能锁刚好过期被别人拿走。
第三版:续期(看门狗)
业务执行时间不确定,过期时间设短了锁提前释放,设长了宕机后要等很久。折中方案是开一个后台线程定期续期:
def renew():
while not stop:
time.sleep(3)
if redis.eval("if redis.call('get',KEYS[1])==ARGV[1] then return redis.call('expire',KEYS[1],10) else return 0 end", 1, key, token):
continue
break # 锁已不属于自己,停止续期
Redisson 的 RLock 就是这个机制,Java 项目直接用它,别自己写。
还有一个绕不过去的问题
主从架构下,A 在 master 拿到锁,master 还没同步到 slave 就宕机了,slave 升主,B 又拿到同一把锁。这是异步复制带来的,纯 Redis 方案解决不了。
要求强一致(比如扣款)就别用 Redis 锁:
- 用数据库行锁
SELECT ... FOR UPDATE,或者唯一索引兜底 - 用 ZooKeeper/etcd 这类基于共识的实现
- 业务上做幂等,重复执行也无害
实际项目里的选择
我的做法是:普通任务用 Redis 锁(SET NX EX + Lua 释放),关键资金操作不依赖锁,靠数据库唯一约束和状态机保证。多一层校验,比追求“完美的分布式锁”实在。
- THE END -
最后修改:2022年6月11日
非特殊说明,本博所有文章均为博主原创。
如若转载,请注明出处:https://mi-blog.cn/archives/659
共有 0 条评论