Java中用Redis+Lua实现分布式锁的核心是:通过Lua脚本原子性完成加锁、设过期、校验唯一性,并在Lua中封装多key操作以模拟事务一致性。

Java 中用 Redis + Lua 实现分布式锁配合事务,核心思路是:**用 Lua 脚本原子性地完成“加锁 + 设置过期时间 + 校验唯一性”,再在业务逻辑中模拟事务语义(Redis 本身不支持跨命令的 ACID 事务,但可借助 Lua 或 watch/multi 实现有限一致性)**。下面分三块讲清楚怎么做、为什么这么设计、以及关键避坑点。
一、用 Lua 脚本实现安全的可重入分布式锁
Redis 单命令是原子的,但 SET + EXPIRE + 判断不是。所以必须把加锁逻辑写进 Lua 脚本里,由 Redis 一次性执行:
- 脚本检查 key 是否存在;不存在则 setnx 成功并设过期时间,返回 1;存在则比对 value(锁标识,如 UUID + 线程ID),匹配则续期(仅限同线程重入),返回 0 表示已持有;不匹配返回 -1 表示被别人持有
- 解锁也必须用 Lua:只允许 value 匹配时 del key,防止误删别人锁
- 推荐锁标识格式:
"{业务前缀}:{资源ID}:lock:{UUID}-{threadId}",避免不同 JVM 实例间冲突
二、在 Java 中调用 Lua 加锁/解锁(以 Jedis 为例)
先定义加锁脚本(Lua):
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
if redis.call("exists", KEYS[1]) == 0 then
redis.call("setex", KEYS[1], ARGV[1], ARGV[2])
return 1
else
local current = redis.call("get", KEYS[1])
if current == ARGV[2] then
redis.call("expire", KEYS[1], ARGV[1])
return 0
end
return -1
end
Java 调用示例:
立即学习“Java免费学习笔记(深入)”;
- 用
Jedis.eval()执行脚本,传入 key(如"order:123:lock")、过期秒数(如 30)、唯一值(如"a1b2c3-Thread-5") - 返回 1 → 加锁成功;0 → 已持有(可继续执行);-1 → 加锁失败,需重试或拒绝
- 解锁脚本更简单:
if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end
三、配合“类事务”操作:用 Lua 封装多 key 更新 + 锁校验
Redis 没有真正事务,但你可以把“读取旧值 → 计算新值 → 写入多个 key → 删除锁”整个流程塞进一个 Lua 脚本里,靠原子性保证一致性。例如扣减库存 + 记录日志:
- 脚本开头先 check lock key 是否存在且 value 匹配(即当前线程仍持锁)
- 再 get 库存、判断是否充足;充足则 decr stock,hset log,返回 success;否则返回 fail
- 这样即使业务代码崩溃,只要 Lua 没跑完,就不会出现中间态
- 注意:Lua 中不能 sleep、不能网络请求、不能超时太久(默认 5 秒超时),所以逻辑要轻量
四、实际使用中的关键细节
- 锁自动续期(Watchdog):如果业务耗时可能超锁过期时间(比如 30s),要用独立线程定期用相同 value 给 key 重新 expire,避免锁提前释放(Redisson 的 lockWatchdogTimeout 就是干这个的)
- 不要依赖 DEL 解锁:必须用 Lua 校验 value 后再删,否则 A 加锁、B 超时自动释放、A 还没执行完就 del,会误删 B 新加的锁
- RedLock 不必要:单 Redis 实例足够大多数场景;集群模式下优先用 Redisson 的 multiLock 或客户端分片路由保证 key 落同一 slot
- 和数据库事务分离:Redis 锁只保护缓存/中间状态;真正落库还是要靠 DB 的事务 + 唯一索引 + 乐观锁等兜底

















