Redis-实现分布式锁
目录
1.JVM锁在集群下的漏洞
2.分布式锁实现
3.基于Redis实现(简易)
3.1线程安全问题
3.2解决分布式锁误删
代码改动
3.3实现分布式锁原子性
漏洞场景
Lua脚本实现分布式锁原子性
4.基于Redis的分布式锁优化
4.1Redissoon
4.2可重入锁
1. 存储结构:Redis Hash
2. 加锁/重入:一段 Lua 脚本完成
3. 看门狗(续期机制)
4. 解锁:计数减到 0 才真正删 Key
5. 公平/非公平可选
6. 一句话总结
4.3不可重试与超时释放
1. 入口:tryLock 带等待时间
2. 重试总流程(简化源码)
3. 等待-唤醒机制(PubSub + 信号量)
4. 退出条件
5. 与“看门狗”的关系
6. 一句话总结
4.4联锁
1. 结构:只是一个 List 封装器
2. 加锁流程:顺序遍历 + 失败即回滚
3. 解锁流程:全部释放,异常也不停
4. 一致性保证(可重入、超时、回滚)
5. 典型应用场景
6. 与 RedLock 的区别(易混淆)
7. 一句话总结
5.总结
1.JVM锁在集群下的漏洞
在集群模式下,JVM 锁(如
synchronized
或ReentrantLock
)存在根本性漏洞:
它只能保证「同一个 JVM 进程」内的互斥,无法跨节点生效。因此,当业务部署了多台实例时,JVM 锁会完全失效,导致并发安全问题。具体表现与成因如下:
🔍 核心漏洞:锁的「作用域」仅限当前进程
场景 单机 集群(多节点) 锁实现 synchronized
/ReentrantLock
同上 锁对象 当前堆里的 Object
每个节点各自独立的对象 结果 互斥成功 多节点同时拿到锁,互斥失败
2.分布式锁实现
3.基于Redis实现(简易)
分布式锁类-获得锁,释放锁方法
主类
3.1线程安全问题
线程1业务堵塞超时后被强制释放锁,随即线程2申请到锁开始执行业务。
此时线程1恢复业务执行完后释放锁——此时将线程2申请的锁释放了。
随即线程3申请到锁开始执行业务。
此时线程2与线程3都在执行业务,存在线程安全问题。
3.2解决分布式锁误删
在获取锁的时候存入线程标示。
释放锁的时候对比该锁的线程标示是否和自己相同,只有相同的情况下才能释放。
代码改动
由于线程自身id都是自增,若在集群环境下,系统生成的线程id会出现重复。
所以通过UUID为线程id实现唯一。
3.3实现分布式锁原子性
漏洞场景
虽然可以通过线程标识判断当前锁是否可释放。
但是若判断锁可释放后,在释放锁时发生堵塞,由于超时自动释放锁。
此时线程2获取锁后开始执行业务。
而线程1此时恢复由于之前已经判断过锁可释放,此时直接释放锁——将线程2锁释放了。
此时线程3又获取锁,线程2与线程3并发执行,存在线程安全。
Lua脚本实现分布式锁原子性
总结三句话
Redis 单线程 ⇒ 脚本一旦开始就不会被别的命令打断;
Lua 把“读-判断-写”打包 ⇒ 外界看就像一条命令;
所以用 Lua 就能在不加锁、不引入复杂协议的前提下,白嫖到原子性。
4.基于Redis的分布式锁优化
4.1Redissoon
4.2可重入锁
Redisson 可重入锁(
RLock
)在单台 Redis 上就能保证线程级可重入 + 分布式互斥,核心思路是:“同一线程可以多次加锁,每加一次计数 +1;释放时计数 -1;计数归零才真正删 key。”
下面按存储结构 → 加锁/重入 → 看门狗 → 解锁四步拆解。
1. 存储结构:Redis Hash
key = 锁名(如
myLock
)field =
UUID:threadId
(客户端实例唯一标识 + 线程 ID)value = 重入次数(初始 1,每次重入 +1)
复制
myLock: {"7f3e7498-36a7-4c65-996b-3a2b3c4d5e6f:123": 3 }
2. 加锁/重入:一段 Lua 脚本完成
逻辑
锁不存在 →
hincrby
创建计数并设置过期时间(默认 30 s)。锁已存在且 field 存在 → 同一线程重入,计数 +1,重新刷过期时间。
锁已存在但 field 不存在 → 获取失败,返回剩余 TTL,供外层阻塞或重试。
Lua 骨架(简化):
if (redis.call('exists', KEYS[1]) == 0) thenredis.call('hincrby', KEYS[1], ARGV[2], 1);redis.call('pexpire', KEYS[1], ARGV[1]);return nil; end; if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) thenredis.call('hincrby', KEYS[1], ARGV[2], 1);redis.call('pexpire', KEYS[1], ARGV[1]);return nil; end; return redis.call('pttl', KEYS[1]);
KEYS[1] = 锁名
ARGV[1] = 过期毫秒数
ARGV[2] =
UUID:threadId
3. 看门狗(续期机制)
如果调用
lock()
时 不指定 leaseTime,Redisson 会启动一个 后台定时任务(10 s 检查一次),只要当前线程还持有锁,就把过期时间重新刷成 30 s,防止业务没完锁就被 Redis 删掉。一旦线程调用
unlock()
把计数减到 0,定时任务随之取消。
4. 解锁:计数减到 0 才真正删 Key
同样用 Lua 保证原子性:
if (redis.call('hexists', KEYS[1], ARGV[2]) == 0) thenreturn nil; -- 不是当前线程,直接返回 end; local counter = redis.call('hincrby', KEYS[1], ARGV[2], -1); if (counter > 0) thenredis.call('pexpire', KEYS[1], ARGV[1]);return 0; elseredis.call('del', KEYS[1]);return 1; end;
计数 > 0 → 只减计数,不删 Key(重入释放)。
计数 = 0 → 删除整个 Key(真正释放)。
5. 公平/非公平可选
非公平锁(默认
RLock
)——抢锁线程无序,谁先到谁得。公平锁(
RFairLock
)——用 Redis List + ZSet 维护FIFO 排队,先请求的线程先拿到锁。
6. 一句话总结
Redisson 可重入锁把 “线程身份” 保存在 Redis Hash 的 field 中,用计数器实现重入,用Lua 脚本保证原子,用看门狗自动续期,计数归零才真正释放,从而单 Redis 节点即可支持分布式 + 可重入 + 不死锁。
4.3不可重试与超时释放
Redisson 的“可重试锁”并不是一种新的锁类型,而是
RLock.tryLock(long waitTime, long leaseTime, TimeUnit)
在 waitTime > 0 时自动表现出来的 获取失败后再重试 机制。
核心目标:在指定等待时间内,尽可能让线程最终拿到锁,而不是失败就立即返回 false。下面按 调用入口 → 重试流程 → 等待-唤醒机制 → 超时退出 四段说明。
1. 入口:tryLock 带等待时间
// 最多等 3s,锁 10s 后自动过期 boolean isLock = redisson.getLock("orderLock").tryLock(3, 10, TimeUnit.SECONDS);
leaseTime=10s 只是“万一我没解锁”的兜底 TTL,不影响重试。
waitTime=3s 才是“可重试”的总时限;内部会循环+阻塞直到超时。
2. 重试总流程(简化源码)
long remain = unit.toMillis(waitTime); while (remain > 0) {// 2.1 一次原子化抢锁(Lua 脚本)Long ttl = tryAcquireOnceAsync(remain, leaseTime, threadId);if (ttl == null) { // 返回 null 表示抢到了return true;}// 2.2 没抢到,决定等多久long await = Math.min(remain, ttl); // 等“剩余等待时间”与“锁过期时间”较小值boolean gotSignal = subscribeAndAwaitLockReleased(await); // 订阅解锁事件if (!gotSignal) { // 等待超时仍没通知return false;}remain -= (System.currentTimeMillis() - start); // 扣掉已用时间 } return false;
remain获取等待时间(毫秒)
当等待时间>0时,进入抢锁主循环。
RedissonLock.tryLockInnerAsync方法中拼接了Lua脚本保持抢锁的原子性。
3. 等待-唤醒机制(PubSub + 信号量)
抢锁失败后线程执行
SUBSCRIBE redisson_lock__channel:{orderLock}
监听锁释放事件。持有锁的线程释放锁时,Lua 脚本里会
PUBLISH redisson_lock__channel:{orderLock} 1
把解锁消息推送给正在等待的客户端。等待线程收到消息后立即被唤醒,再次循环抢锁;
若规定时间内没收到消息,则放弃订阅,返回 false。这种“先订阅再阻塞,解锁事件唤醒”的方案避免了无脑自旋,对 CPU 非常友好 。
4. 退出条件
条件 行为 抢到锁(Lua 返回 null) 立即返回 true 剩余等待时间耗尽 返回 false 解锁消息及时到达 被唤醒后继续下一轮抢锁
5. 与“看门狗”的关系
可重试解决“获取”阶段的失败重试。
看门狗解决“拿到锁后”业务还没执行完导致 TTL 过期的续租问题。
两者独立但可共存:先重试拿到锁,再看门狗定时续期。
6. 一句话总结
Redisson 的“可重试锁”利用 Lua 原子抢锁 + 解锁 PubSub 通知 + 限时循环等待 三板斧,在 waitTime 内阻塞-唤醒-再抢,既保证最终成功率,又避免忙等耗 CPU 。
通过 “leaseTime 强制过期” 和 “看门狗自动续期” 两种互补策略,既保证锁最终一定会被释放(无死锁),又允许业务执行时间不确定(无误释放);开发者只需按任务长短选择是否指定 leaseTime,即可优雅解决“超时释放”问题
4.4联锁
Redisson 的
MultiLock
(联锁)并不是一种“新锁”,而是把多个独立的RLock
打包成一把逻辑锁——
要么全部加锁成功,要么全部回滚释放,对外暴露的 API 和单机锁一模一样,内部却用**“遍历 + 回滚”策略实现分布式场景下的原子性**。下面按 结构 → 加锁流程 → 解锁流程 → 一致性保证 → 应用场景 五步彻底剖开。
1. 结构:只是一个
List<RLock>
封装器public class RedissonMultiLock implements Lock {private final List<RLock> locks; // 内部维护的普通锁列表public RedissonMultiLock(RLock... locks) {this.locks = Arrays.asList(locks);} }
可以来自同一 Redis 实例的不同 key,也可以跨实例 / 跨机房;
对外暴露的
lock()
/tryLock()
/unlock()
与单机锁无差别。
2. 加锁流程:顺序遍历 + 失败即回滚
核心代码(精简):
long remain = unit.toMillis(waitTime); List<RLock> acquired = new ArrayList<>(); for (RLock lock : locks) {long elapsed = System.currentTimeMillis() - start;remain = time - elapsed;if (remain <= 0 && waitTime != -1) {unlockInner(acquired); // ① 超时了,回滚已拿到的锁return false;}boolean ok = lock.tryLock(remain, unit);if (!ok) {unlockInner(acquired); // ② 当前锁获取失败,回滚return false;}acquired.add(lock); // ③ 记录成功 } return true; // 全部成功
顺序尝试,实时扣减剩余等待时间,防止因某个节点慢导致整体卡死;
任何一步失败立即 逆序释放已拿到的锁,保证“整锁整放”。
3. 解锁流程:全部释放,异常也不停
public void unlock() {for (RLock lock : locks) {try {lock.unlock();} catch (Exception ignore) {// 个别锁解锁失败也不中断,继续释放其余}} }
不判断持有者,不中断循环,最大限度避免“残留锁”。
4. 一致性保证(可重入、超时、回滚)
问题 MultiLock 解法 部分成功 失败即 unlockInner
逆序释放,原子性靠回滚实现可重入 不自己维护计数,依赖单个 RLock
的可重入语义;同线程重复调用,内部tryLock
会返回 true超时控制 遍历里实时 remain = time - elapsed
,防止单节点慢拖死整体节点宕机 若某 RLock
一直tryLock
失败,整体拿不到锁,不会形成“半锁”状态
5. 典型应用场景
转账 / 聚合订单
需要同时锁定多个账户或订单,“只锁一半”会出资金风险RLock l1 = redisson1.getLock("acc:1001"); RLock l2 = redisson2.getLock("acc:1002"); RedissonMultiLock ml = new RedissonMultiLock(l1, l2); ml.lock(); try { /* 执行转账 */ } finally { ml.unlock(); }
跨机房调度
不同机房各有一组 Redis,MultiLock 把多机房锁打包,避免主从同步延迟带来的“锁丢失”。批量库存扣减
多个商品库存分布在 不同分片 Redis,一次性全锁住再批量扣,防止超卖。
6. 与 RedLock 的区别(易混淆)
特性 MultiLock RedLock 节点角色 任意(可主从、可单实例不同 key) 必须 N 个独立 Master 成功条件 全部成功 ≥ N/2+1 成功 实现层面 Java 端遍历+回滚 Lua 脚本 + 时钟漂移校正 一致性强度 全有或全无(业务原子) 多数派(容忍少数节点故障)
7. 一句话总结
Redisson MultiLock 通过Java 端顺序遍历加锁、失败逆序回滚的简单策略,把多把分布式锁组合成一把“整体原子”锁,既解决了多资源同时互斥的需求,又天然规避了主从复制延迟带来的锁丢失问题,是“全有或全无”高一致性场景的首选利器