Redis 分布式锁:三种实现和它们各自的坑

米阳 2022-6-11 4 6/11

单机用 threading.Lock 就够了,多实例部署就必须上分布式锁。Redis 是最常见的方案,但实现细节差一点,可靠性差很多。

第一版:SETNX + DEL

if redis.setnx(key, 1):
    try:
        do_something()
    finally:
        redis.delete(key)

两个致命问题:

  1. 进程崩了没执行到 delete,锁永久不释放
  2. 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 -

米阳

6月11日16:30

最后修改:2022年6月11日
0

非特殊说明,本博所有文章均为博主原创。

共有 0 条评论