KEEL · 龙骨 · A CURRICULUM FOR THE AI ERA

04 · 分布式锁:SET NX EX + UUID + Lua — keel 龙骨

前面三章讲「队列」,是 Redis 当运行时的第一类问题。这一章讲第二类:多进程互斥。重点回答三个「为什么」——为什么 NX 和 EX 要一起用?为什么锁值要用 UUID?为什么释放要用 Lua 脚本?最后说清它到底保不保什么。

前面三章讲「队列」,是 Redis 当运行时的第一类问题。这一章讲第二类:多进程互斥。重点回答三个「为什么」——为什么 NX 和 EX 要一起用?为什么锁值要用 UUID?为什么释放要用 Lua 脚本?最后说清它到底保不保什么。


一、先立靶子:锁要防什么

两个 Worker 同时想去改「同一个资源」。没有锁:

Worker A:读 → 计算 → 写回
Worker B:读 → 计算 → 写回      ← A 还没写回,B 读到的就是旧值
结果:B 的写覆盖 A 的写,A 白干(或两者结果互相覆盖)

这类「读-改-写」竞态,在单机用线程锁,在分布式(多进程 / 多机器)就要用分布式锁。本质:把「检查有没有人占 + 占住」变成一个原子操作。Redis 单线程执行命令恰好提供原子性——所以分布式锁用 SET key value NX EX。

二、获取锁:SET NX EX

SET lock:report:42 <uuid> NX EX 30

为什么 NX 和 EX 必须同一条命令? 如果你分开写:SET key v NX 然后 EXPIRE key 30,中间进程崩溃,锁就没有过期时间,永远卡死。一条命令才能保证「占住」和「会过期」同时成立。

三、锁值为什么要用 UUID(防止误删别人的锁)

一个经典 bug:

Worker A 抢到锁,业务卡了 31 秒(超过 30 秒过期)
锁自动过期 → Worker B 抢到锁
Worker A 终于干完,执行 DEL lock   ← 删的是 B 的锁!
Worker C 又抢到锁 → B 和 C 同时进入临界区

解法:锁值用唯一 UUID,释放时先比对值,再删:

# 只有值相等才删(先 GET 比对,再 DEL)—— 但这两步不原子,见下节
GET lock:report:42        # 是自己的 uuid 吗
DEL lock:report:42

UUID 让每个持有者只删自己的锁,杜绝「误删别人的锁」。

四、释放为什么要用 Lua 脚本(原子地「比对+删」)

上节的「GET 比对 + DEL」有个竞态:A 刚 GET 完(确认是自己的),锁正好过期,B 抢到了,A 紧接着 DEL——又误删了 B 的。

解法:用 Lua 脚本把「比对 + 删除」打包成原子操作(Redis 执行 Lua 时单线程,中途不会被打断):

-- KEYS[1]=锁key, ARGV[1]=自己的uuid
if redis.call("GET", KEYS[1]) == ARGV[1] then
    return redis.call("DEL", KEYS[1])
else
    return 0
end

Python 里:

import redis, uuid

r = redis.Redis(host="localhost", port=6379, decode_responses=True)
LOCK_KEY = "lock:report:42"
token = str(uuid.uuid4())    # 每个持有者独一无二的锁值

# 获取
acquired = r.set(LOCK_KEY, token, nx=True, ex=30) is not None

# 释放(比对 + 删除 原子化)
if acquired:
    release = """
    if redis.call("GET", KEYS[1]) == ARGV[1] then
        return redis.call("DEL", KEYS[1])
    else
        return 0
    end
    """
    r.eval(release, 1, LOCK_KEY, token)

五、🚫 诚实边界:它不保什么

很多人以为「上了分布式锁就安全了」,其实 Redis 锁有三个必须知道的边界:

  1. 没有看门狗续期(watchdog):锁是固定 30 秒过期。如果你的业务确实需要跑两分钟,到期锁就自动释放,别人会进来——你得自己续期,或者把 EX 设得比业务上限长。
  2. 没有 fencing token:即使比对 UUID 删锁,持有者 A 在网络延迟后「晚醒」,仍可能和 B 短暂重叠。真正的强一致需要单调 fencing token(如 Redlock 的争议方案),本课不展开。
  3. 锁保的是「临界区互斥」,不保「业务幂等」:最好是锁保护的代码本身幂等(同一条任务重复执行结果一致),这样即使锁边界被突破,最坏也只是重复做一遍,不会出错。

一句话:Redis 锁是「大多数情况下的够用方案」,不是「教科书级严格互斥」。配合业务幂等,它足以撑起绝大多数生产场景。


↓ 下一步:05 章 · 业务锁封装 —— 怎么把这套基础设施翻译成业务语言。

进入 keel 阅读