Redlock不能用单个EVAL脚本实现,因Lua仅限单实例执行、禁止网络调用,无法跨节点协调;其核心是客户端在多数独立Redis实例上原子加锁(SET NX PX)并验证成功数≥N/2+1且总耗时小于TTL。

Redlock 为什么不能直接用单个 EVAL 脚本实现?
Redlock 的核心不是“加一把锁”,而是“在多数 Redis 实例上成功获取带超时的锁”。Lua 脚本本身只能运行在单个 Redis 实例中,EVAL 无法跨实例执行或协调多个节点的状态。试图写一个 Lua 脚本去“自动连其他 Redis 实例”完全不可行——Redis 的 Lua 环境禁止网络调用、无 DNS 解析、无 socket 支持。
所以 Redlock 的协调逻辑必须由客户端完成,Lua 只负责原子地完成「在当前实例尝试加锁」这一步。
每个 Redis 实例上该用什么 Lua 脚本加锁?
标准做法是用 SET key random_value NX PX timeout_ms 的原子语义,但为避免误删他人锁(比如释放阶段被其他客户端覆盖),value 必须唯一且可验证。Lua 脚本用来保证「校验 value 再删除」的原子性,但加锁本身其实不需要 Lua —— SET ... NX PX 已足够原子。不过若需更精细控制(如记录时间戳、拒绝重入),才引入 Lua。
最简安全加锁脚本(推荐):
return redis.call("SET", KEYS[1], ARGV[1], "NX", "PX", ARGV[2]) == "OK" and 1 or 0
说明:
-
KEYS[1]是锁 key(如"lock:order:123") -
ARGV[1]是客户端唯一标识(如 UUID,用于后续解锁校验) -
ARGV[2]是锁过期毫秒数(如30000),不是绝对时间 - 返回
1表示加锁成功,0表示失败(key 已存在或命令被拒绝)
解锁时为什么必须用 Lua 而非 DEL?
直接 DEL lock:key 会删掉别人持有的锁——只要锁没过期,谁都可删。必须先检查 value 是否匹配,再删除,且这两步必须原子。这就是 Lua 的刚性用途。
标准解锁脚本:
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
注意点:
- 不要用
GET + DEL两步命令,中间可能被其他客户端修改 - 不要用
redis.call("DEL", ...)无条件执行,必须先GET校验 - 返回
1表示成功删除(即自己持有的锁),0表示锁不存在或不属于当前客户端 - 即使锁已过期,
GET返回nil,脚本仍安全返回0,不会误删
客户端如何组合多个实例调用才算 Redlock 成功?
这才是 Redlock 的关键逻辑层,完全在客户端代码里,和 Lua 无关。以 5 个 Redis 实例为例:
- 记录开始时间
start_time = time.Now() - 对每个实例,并发(或串行)执行加锁 Lua 脚本,设置合理超时(如
200ms) - 统计成功加锁的实例数;若 ≥ 3(即 > N/2),继续下一步
- 计算已耗时:
elapsed = time.Since(start_time);剩余有效锁时间 =original_ttl - elapsed - 若剩余时间 ≤ 0,说明加锁过程太慢,整体失败,需立即对已成功实例执行解锁 Lua
- 否则,认为 Redlock 获取成功,持有锁时间为「剩余有效锁时间」,不是原始 TTL
容易踩的坑:
- 忽略网络延迟导致的「实际锁有效期缩水」,硬把锁当 full TTL 用
- 解锁时只清部分实例,留下残留锁(必须遍历所有实例,对每个都执行解锁 Lua)
- 用系统时钟同步假设各 Redis 实例时间一致 —— Redlock 不依赖时钟同步,但要求各实例自身时钟漂移不大(官方建议
真正难的是客户端对超时、重试、时钟偏差、网络分区的综合处理,Lua 只是其中两个原子操作的载体。别指望靠一段脚本“实现 Redlock”,它本质是个分布式协议,Lua 只是螺丝钉。











