小Cの已经记不起来的博客

用 Docker 搭建分布式锁的完整流程

最近有个跑在两台机器上的定时任务出事了:两边同一时刻各执行了一遍,数据库里同一批订单被写了两份,日志一对时间戳,分毫不差。以前单机跑的时候,靠数据库唯一索引就能糊弄过去,现在上到两台就不行了,得正经上分布式锁。方案里 Redis 算最省事的,但我不想为了一把锁在服务器上装个 Redis,就用 Docker 起容器。动手前一搜,全是讲 Redlock 理论的,为什么就没有一篇能直接照着敲的呢?可能是我瞎没找到吧... 折腾了一晚上总算跑通了,下面是实测可用的完整流程。

第一步:把 Redis 跑起来

首先拉镜像 docker pull docker.1ms.run/library/redis:7.2,我这里用了加速域名,不需要的或者拉的时候报错了,可以把 docker.1ms.run/library 删掉,直接 redis:7.2,没准儿你看到这篇文章的时候这个加速域名已经不可用了。

然后启动:

docker run -d --name redis-lock \
  -p 6379:6379 \
  -v ~/redis-lock-data:/data \
  redis:7.2 redis-server --requirepass "换成你的密码" --appendonly yes

-d 后台运行,-v 把数据目录映射到宿主机,容器删了数据还在。密码一定要设,Redis 裸奔在公网上被扫去挖矿的案例一搜一大把,别心存侥幸。

容器跑起来过后,验证一下活没活:

docker exec -it redis-lock redis-cli -a 换成你的密码 ping

返回 PONG 就成了,前面会提示一串命令行传密码不安全的警告,不用管,能 ping 通就行。

第二步:加锁和解锁

分布式锁听起来玄乎,单机版的核心其实就一条命令:

SET lock:order-task <uuid> NX PX 30000

NX 表示 key 不存在才能设置成功,谁先设置成功谁持有锁,后来的直接拿到 nil;PX 30000 是 30 秒过期时间,这个必须带,不然持有锁的进程崩了,锁就永远死在那了,谁都拿不到。value 里放 UUID,释放锁的时候要用它判断这把锁是不是自己的——这里有个大坑:如果你图省事直接 DEL,可能把自己锁过期后、被别人接手的锁给误删了。所以释放必须用 Lua 脚本保证原子性:

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

Redis 执行脚本是原子的,GET 和 DEL 中间不会插进别的命令,这样才能删得干净。

第三步:写段代码测一测

用 Python 举例,先把逻辑封一下:

import redis
import uuid

r = redis.Redis(host="127.0.0.1", port=6379,
                password="换成你的密码", decode_responses=True)

def acquire(key, expire_ms=30000):
    token = str(uuid.uuid4())
    if r.set(key, token, nx=True, px=expire_ms):
        return token
    return None

def release(key, token):
    script = """
    if redis.call("GET", KEYS[1]) == ARGV[1] then
        return redis.call("DEL", KEYS[1])
    else
        return 0
    end
    """
    return r.eval(script, 1, key, token) == 1

然后开两个终端,前后脚各执行一次下面的代码:

token = acquire("lock:order-task")
if token:
    print("拿到锁了,开始干活")
    time.sleep(10)   # <--- 模拟任务执行,先别释放
    release("lock:order-task", token)
else:
    print("拿锁失败,锁被别人占着")

不出问题的话,就是一个终端打印“拿到锁了”,另一个打印“拿锁失败”。想验证过期时间也好办,把 PX 改成 3000,让第一个进程 sleep 十秒再释放,三秒过后第二个进程就能拿到锁了。

不放心单机?加几台搞 Redlock

单点 Redis 总有挂的时候,主从切换的那几秒里锁可能就丢了。Redlock 的思路是往多个互相独立的 Redis 上同时加锁,拿到大多数才算成功,比如 3 台里至少 2 台。先起两个额外实例(加上前面那个一共 3 台):

docker run -d --name redis-lock-2 -p 6382:6379 \
  redis:7.2 redis-server --requirepass "换成你的密码"
docker run -d --name redis-lock-3 -p 6383:6379 \
  redis:7.2 redis-server --requirepass "换成你的密码"

说明一下,我这是在同一台机器上开 3 个容器图个演示,真上生产要分到不同机器,不然机器一挂全完,锁形同虚设。网上现成的 redlock 库好几年没更新了,可能是我瞎没找到维护得好的吧... 干脆手写一个,几十行的事:

clients = [redis.Redis(host="127.0.0.1", port=p,
           password="换成你的密码", decode_responses=True)
           for p in (6379, 6382, 6383)]

def redlock_acquire(key, expire_ms=30000):
    token = str(uuid.uuid4())
    ok = sum(1 for c in clients if c.set(key, token, nx=True, px=expire_ms))
    return token if ok >= 2 else None

def redlock_release(key, token):
    for c in clients:
        # 逐台执行前面那个 Lua 释放脚本即可
        ...

标准的 Redlock 还有获取锁耗时不能超过锁时长的校验、加锁失败要把已到手的锁全部回滚,我这里为了能一眼看懂做了简化,原理就这么回事,需要严格实现直接抄 Redis 官方文档里的伪代码就行。

几个容易忽略的点

这套东西现在跑在我一台 2 核 4G 的小机器上,三个容器加起来内存占用不到 100M,目前稳着呢,先这样。

评论

还没有评论。

发表评论

提交后评论将经过自动审核,审核通过后公开展示。

未在播放