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= 只有 key 不存在才设置(原子地「占住」);EX 30= 30 秒后自动过期(防持有者崩溃后锁永不释放);- 返回值:
OK表示抢到,nil表示已被别人占。
为什么 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 锁有三个必须知道的边界:
- 没有看门狗续期(watchdog):锁是固定 30 秒过期。如果你的业务确实需要跑两分钟,到期锁就自动释放,别人会进来——你得自己续期,或者把 EX 设得比业务上限长。
- 没有 fencing token:即使比对 UUID 删锁,持有者 A 在网络延迟后「晚醒」,仍可能和 B 短暂重叠。真正的强一致需要单调 fencing token(如 Redlock 的争议方案),本课不展开。
- 锁保的是「临界区互斥」,不保「业务幂等」:最好是锁保护的代码本身幂等(同一条任务重复执行结果一致),这样即使锁边界被突破,最坏也只是重复做一遍,不会出错。
一句话:Redis 锁是「大多数情况下的够用方案」,不是「教科书级严格互斥」。配合业务幂等,它足以撑起绝大多数生产场景。
↓ 下一步:05 章 · 业务锁封装 —— 怎么把这套基础设施翻译成业务语言。