直接用del释放锁会误删key,因为未校验value归属;必须用lua脚本原子执行“get比对+del”,确保仅删除自己持有的锁。

为什么直接用 DEL 释放锁会误删 Key
分布式锁的常见实现是用 SET key value NX PX timeout 加锁,value 通常设为唯一请求标识(比如 UUID)。但释放锁时如果只做 DEL key,就完全不管当前 key 的 value 是否属于自己——只要锁过期或被其他客户端抢先续期,DEL 就会无差别删除,导致本不该释放的锁被干掉,业务逻辑错乱。
用 Lua 脚本保证「判断 + 删除」原子性
Redis 执行 Lua 脚本是原子的,脚本内所有命令按顺序执行、不会被其他客户端打断。所以释放锁必须把「检查 value 是否匹配」和「删除 key」塞进同一个 Lua 脚本里。
典型脚本长这样:
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
调用时传入:KEYS[1] 是锁 key,ARGV[1] 是加锁时写的唯一 value。返回 1 表示成功释放,0 表示 value 不匹配、未操作。
注意点:
-
redis.call("GET", ...)必须用GET,不能用redis.call("EXISTS", ...)或其他方式间接判断——因为要严格比对 value - 脚本里不能用
redis.pcall包裹DEL,除非你明确想吞掉错误;正常场景用redis.call更合适 - 如果锁 key 已过期被自动删除,
GET返回nil,跟ARGV[1]比较结果为false,脚本返回0,安全
Java/Jedis 中调用该 Lua 脚本的要点
Jedis 提供 eval 和 evalSha 两种方式。首次用 eval 提交脚本,Redis 会缓存并返回一个 SHA1 值;后续可用 evalSha 减少网络传输量。
关键实操建议:
- 把 Lua 脚本字符串定义为常量或从资源文件加载,避免拼接出错
- 务必校验
eval返回值是否为Long类型的1,而不是只看是否抛异常 - 不要在脚本里写超时重试逻辑——那是客户端的事;Lua 只负责“这次删得对不对”
- 若使用连接池(如 JedisPool),确保
eval调用在同一个 connection 上完成(Jedis 默认满足)
容易被忽略的边界:锁重入、可重入设计不等于 Lua 脚本能解决
上面的 Lua 脚本只解决「单次加锁、单次释放」的原子性。如果你的业务需要可重入锁(同一个 client 多次加锁、需对应多次释放),那 value 就不能只是 UUID,得是 “UUID:count”,且 Lua 脚本也要改成先 GET、解析 count、减一、为 0 再 DEL——这会让脚本变复杂,也增加出错概率。
更现实的做法是:业务层控制重入逻辑,Redis 层仍保持简单锁模型;或者直接换成熟方案如 Redisson 的 RLock,它内部已封装了带重入计数和自动续期的 Lua 脚本。
别为了“看起来通用”硬往 Lua 里塞状态管理——越简单的脚本,越不容易在线上翻车。











