Redis Lua脚本通过单线程原子执行实现多key安全加锁:对每个KEYS[i]调用redis.call("SET", key, ARGV[1], "NX", "PX", ARGV[2]),任一失败即DEL已设key并返回0;锁值用"{client_id}:{uuid}"确保唯一可验,超时单位为毫秒。

Redis Lua脚本里怎么安全实现多个key的原子性加锁?
不能靠客户端拼多个 SET 命令,也不该用 WATCH + MULTI —— 它们在分布式多key场景下无法保证原子性。真正可行的是把整个加锁逻辑塞进一个 Lua 脚本里执行,让 Redis 单线程串行处理。
核心思路:对每个 key 尝试 SET key value NX PX timeout,只要有一个失败(比如已存在或超时),就立刻回滚之前成功设置的所有 key(用 DEL)并返回失败。Lua 里没有异常机制,得靠显式判断和提前 return。
- 必须用
redis.call("SET", key, val, "NX", "PX", timeout),不能用redis.pcall(它会吞掉错误,导致误判) - 所有 key 和参数必须通过
KEYS/ARGV传入,不能硬编码;KEYS放 key 列表(KEYS[1],KEYS[2]...),ARGV[1]放唯一锁值(如 client_id + random),ARGV[2]放毫秒级超时 - 脚本返回值建议统一:成功返回
1,失败返回0,方便客户端解析
为什么不能直接用 redis.set() 多次调用实现 MultiLock?
因为网络延迟、客户端崩溃、Redis 主从异步复制等,都会让“部分加锁成功”成为常态。例如你顺序对 keyA、keyB、keyC 加锁,刚设完 keyA,客户端断连了,keyB/keyC 没设上 —— 此时 keyA 的锁就成了孤儿,其他客户端永远拿不到完整锁。
Lua 脚本天然具备原子性:要么全部执行完,要么完全不执行(脚本中途报错也不会留下半截状态)。但要注意:EVAL 执行期间会阻塞其他命令,所以脚本必须极简,不能有循环等待、HTTP 请求或耗时计算。
- 别在 Lua 里做 sleep 或 while 等待重试 —— Redis 不允许长时间运行脚本,超时会直接中断并报
OOM command not allowed when used memory > 'maxmemory'或BUSY Redis is busy running a script - 锁值(value)必须全局唯一且可验证,推荐用
"{client_id}:{uuid}"格式,后续解锁时才能校验所有权 - 超时时间(
ARGV[2])单位是毫秒,别传秒——传30是 30 毫秒,不是 30 秒
解锁时怎么避免误删别人持有的锁?
解锁不能简单 DEL key1 key2 key3 —— 这会破坏锁的持有权语义。必须先 GET 锁值,比对是否等于当前 client 的锁标识,相等才 DEL。这个“判断+删除”操作也必须原子化,所以还得写一个 Lua 解锁脚本。
常见错误是用两个命令:先 GET 再 DEL,中间可能被别的客户端修改。正确做法是把比对和删除写在一个脚本里,用 redis.call("GET", KEYS[1]) == ARGV[1] 判断,成立才 redis.call("DEL", KEYS[1])。
- 解锁脚本要支持批量 key,遍历
KEYS数组逐个校验删除,任一 key 校验失败不影响其余 key 处理 - 返回值建议统计成功解锁的 key 数量(比如
local count = 0; if ... then count = count + 1 end; return count),方便客户端确认是否全部释放 - 不要在解锁脚本里尝试续期或刷新 TTL —— 那属于另一个语义,应单独设计,混在一起会让逻辑爆炸
实际部署时最容易忽略的三个细节
很多人写了脚本本地测试通过,上线后却频繁失锁或死锁,问题往往出在这些地方:
- 客户端没设置正确的
script load缓存或EVALSHA回退逻辑,导致高频重复传输大脚本,触发 Redis 的maxmemory限制或网络开销飙升 - 锁超时时间设得太短(比如 100ms),而业务操作偶尔毛刺超时,导致锁自动过期后被别的客户端抢占,原线程继续执行造成数据错乱
- 没处理主从切换场景:Redis 主节点挂了,从节点升主,但旧主上未同步的锁状态丢失,新主上无该锁记录,导致多个客户端同时拿到“同一把锁”——这不是 Lua 能解决的,得靠 Redlock 或租约机制兜底
MultiLock 的本质是提升资源协调粒度,不是银弹。真要强一致性,得接受性能折损或引入 ZooKeeper 等 CP 系统。


















