不能直接用 set key value nx ex 批量加锁,因为三次串行 set 无法保证原子性,易导致部分加锁;必须用 lua 脚本在服务端一次性执行并实现失败回滚,才能满足全部成功或全部失败的要求。

为什么不能直接用 SET key value NX EX 批量加锁
Redis 单个 SET 命令确实能原子加锁,但一次只能操作一个 key。你想同时获取 lock:a、lock:b、lock:c 三个锁,如果用客户端串行发三次 SET ... NX EX,中间任意一步失败(比如第二个成功、第三个超时),就会出现部分加锁、状态不一致的问题——这不是“同时”,更谈不上原子性。
真正需要的是:要么全部成功,要么全部失败,且整个过程不可中断。这只能靠 Lua 脚本在服务端一次性执行来保证。
用 EVAL 执行 Lua 脚本实现批量原子加锁
核心思路:脚本遍历所有锁 key,逐个尝试 SET key value NX EX,只要有一个失败就立刻回滚已成功的(即删掉前面设好的),最后统一返回结果。Redis 保证整个 Lua 脚本的执行是原子的。
示例脚本(加锁超时 30 秒,value 用唯一随机字符串防误删):
redis.call('SET', KEYS[1], ARGV[1], 'NX', 'EX', ARGV[2])
redis.call('SET', KEYS[2], ARGV[1], 'NX', 'EX', ARGV[2])
redis.call('SET', KEYS[3], ARGV[1], 'NX', 'EX', ARGV[2])
return 1
但上面没做失败回滚,实际不可用。安全写法应带错误检查:
for i = 1, #KEYS do
local ok = redis.call('SET', KEYS[i], ARGV[1], 'NX', 'EX', ARGV[2])
if not ok then
-- 回滚前面已设的锁
for j = 1, i-1 do
redis.call('DEL', KEYS[j])
end
return 0
end
end
return 1
调用方式(以 redis-cli 为例):
redis-cli --eval lock-multi.lua lock:a lock:b lock:c , my_random_token 30
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
注意:--eval 后面的逗号是分隔 KEYS 和 ARGV 的固定语法;my_random_token 必须全局唯一,后续解锁要用它校验所有权。
解锁时必须校验 token,不能直接 DEL
批量加锁用了 token,解锁也得严格匹配——否则可能误删别人刚拿到的锁。Lua 解锁脚本必须先 GET 再比对,再 DEL,三步不能拆开。
错误做法:DEL lock:a lock:b lock:c —— 完全不校验,极度危险。
正确解锁脚本(单 key 示例,批量同理):
if redis.call('GET', KEYS[1]) == ARGV[1] then
return redis.call('DEL', KEYS[1])
else
return 0
end
批量解锁需循环处理每个 key,并统计成功数。若任意一个 key 的 token 不匹配,说明锁已被抢占或过期,整个解锁视为部分失效,调用方需自行判断是否重试或告警。
真实场景中容易被忽略的边界问题
Redis 集群环境下,多个 key 必须落在同一个 slot 才能用 Lua 批量操作,否则报 CROSSSLOT 错误。解决方法只有两个:
- 用
{...}包裹 key 名强制哈希到同一 slot,例如lock:{user123}:a、lock:{user123}:b - 改用 Redlock 算法(跨多个独立 Redis 实例加锁),但复杂度陡增,且官方已不推荐
另外,Lua 脚本执行时间不能过长(默认 5 秒超时),锁数量建议控制在 10 个以内;value 的 token 强烈建议用 UUIDv4 或加密安全随机字节,别用时间戳或自增 ID。










