直接用RedisTemplate.execute()调用Lua锁脚本会失败,因未严格分离KEYS与ARGV:若未使用带Collection<String> keys参数的重载方法,key会被误作ARGV传入,导致Lua中KEYS[1]为空而报错ERR wrong number of arguments。

为什么直接用 RedisTemplate.execute() 调用 Lua 锁脚本会失败?
因为 RedisTemplate.execute() 默认不支持传入 key 和 arg 的分离传递,而 Redis 的 EVAL 命令要求 Lua 脚本中所有被操作的 key 必须显式声明在 KEYS 数组里,否则会报错 ERR Bad number of args 或 ERR wrong number of arguments for 'eval' command。Spring Boot 的 RedisTemplate 封装了这一逻辑,但必须用带 Collection<String> keys 参数的重载方法,否则 key 会被当成 arg 传入,Lua 脚本里读不到 KEYS[1]。
如何正确调用带 key/arg 分离的 Lua 锁脚本?
必须使用 RedisTemplate.execute(RedisScript, Collection<String>, Object...) 这个签名,其中:
-
RedisScript需提前定义好返回类型(如Long.class)、脚本内容和 SHA1(可选) -
Collection<String>是实际参与原子操作的 key 列表,对应 Lua 中的KEYS -
Object...是纯参数(ARGV),比如锁过期时间、唯一 client ID
示例脚本(尝试加锁):
local key = KEYS[1]
local uuid = ARGV[1]
local expire = tonumber(ARGV[2])
if redis.call("exists", key) == 0 then
return redis.call("setex", key, expire, uuid)
end
return 0
Java 调用:
String script = "..."; // 上面那段 Lua
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
Long result = redisTemplate.execute(
redisScript,
Collections.singletonList("lock:order:123"), // KEYS
"client-abc-456", "30" // ARGV
);
加锁成功后,为什么用 redis.call("get", KEYS[1]) 拿不到值?
不是 Lua 写错了,而是你没意识到:Redis 在 EVAL 执行期间对涉及的 key 会自动加锁(基于 key 的单线程串行执行),但脚本内不能用 GET 读自己刚 SETEX 的值——因为 SETEX 返回的是 OK,不是值本身;而后续 GET 要等整个 EVAL 结束才生效。更关键的是:如果你在脚本里写 return redis.call("get", KEYS[1]),它确实能返回值,但这个值是加锁后的结果,不是“是否加锁成功”的布尔语义。
真正该返回的是加锁动作的结果:
- 返回
1表示加锁成功(key 不存在,已设置) - 返回
0表示加锁失败(key 已存在) - 不要在 Lua 里做额外判断或返回复杂结构,避免网络往返和序列化开销
释放锁时,为什么用 del 直接删 key 有风险?
因为锁可能已被别人续期或误删。安全释放必须校验 owner(比如存的是 client ID),只能由加锁者删除。所以释放脚本必须先 GET 再 DEL,且整个过程要原子:
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
注意点:
- 必须用
execute()同样方式调用,KEYS传锁 key,ARGV传当前 client ID - 返回
1表示释放成功,0表示锁不属于当前 client - 别用
RedisTemplate.delete()直接删——它不校验 owner,会破坏锁安全性
分布式锁的可靠性不取决于脚本多酷,而在于 key/arg 是否严格分离、owner 校验是否在 Lua 内完成、以及调用方是否处理了 null 或非预期返回值。漏掉任意一环,都可能让锁形同虚设。


















