

🔥个人主页:北极的代码(欢迎来访)
🎬作者简介:java后端学习者
✨命运的结局尽可永在,不屈的挑战却不可须臾或缺!
前言:
前面我们手动的设置了Redis的分布式锁,并且经过一系列的优化(Lua 脚本 + UUID 解决了原子性 和释放别人锁 的问题),,虽然已经解决了"误删和原子性"这两个最基础的问题,但在"锁生命周期跟随业务、可重入、高效阻塞等待、公平性、多节点可靠性"这些高级特性上,还是有一定的缺陷的。
摘要:
本文深入分析了Redis分布式锁的手动实现与Redisson解决方案的差异。手动实现存在五大问题:固定锁过期时间导致业务超时风险、不可重入性、低效的自旋等待、缺乏公平性保证、单点故障风险。Redisson通过自动续期机制、可重入锁设计、高效阻塞等待、公平锁实现和RedLock算法,全面解决了这些问题。文章详细介绍了Redisson的配置方法、核心原理(如Hash结构存储锁信息、WatchDog自动续期机制)以及MultiLock联锁机制,为高并发场景下的分布式锁应用提供了专业级解决方案。
存在的问题:
1.锁过期时间固定 → 无法处理业务执行时间不确定
手动实现: 加锁时设置一个固定过期时间(比如 10 秒)。
问题:
如果业务偶尔抖动(GC、慢查询、网络延迟),执行了 15 秒 → 锁在第 10 秒就没了,其他线程进来,在第 15 秒虽然不会删别人的锁(因为 UUID 不匹配),但后半段的执行已经失去了锁的保护,相当于无锁状态。
如果为了保险设置很长的过期时间(比如 60 秒),那万一业务真卡死,其他线程要等 60 秒才能拿到锁,妥妥的故障。
Redisson 的做法:
自动续期(Watchdog)。只要业务还在跑,锁就会一直被续命,既不会提前释放,也不会卡死别人太久。
简单的说,我们自己实现的只能赌一个合适的过期时间,而 Redisson 能做到锁跟着业务走。
2. 不可重入(non-reentrant)
手动实现: 同一个线程第二次尝试获取同一把锁时,会被自己阻塞。
场景:
javajava public void transfer() { lock("account:123"); innerCheck(); // 里面又调用了 lock("account:123") unlock(...); } public void innerCheck() { lock("account:123"); // 死等自己 // ... }问题: 同一个线程,在未释放锁的情况下再次加锁 → 死等自己,死锁。
Redisson 的做法:
可重入锁,内部通过计数器和线程标识判断,同一线程重复加锁只增加计数,不会阻塞。
3. 无法阻塞等待锁(只能 try 或自旋)
手动实现:
要么
setIfAbsent立即返回成功/失败(非阻塞)要么自己写
while循环 +sleep自旋等待问题:
非阻塞:拿不到就放弃,很多场景你需要等一会儿再试
自旋:要么浪费 CPU(无 sleep),要么响应延迟 + 浪费资源(固定 sleep)
Redisson 的做法:
tryLock(10, TimeUnit.SECONDS)真正的阻塞等待,内部基于订阅 + 信号量实现,锁释放后立即唤醒,不浪费 CPU,响应快。
4. 没有公平性保证
手动实现: 谁抢得快谁得锁。
问题: 在高并发下,某些线程可能一直抢不到锁(饥饿),不是真正的"先来先得"。
Redisson 的做法:
提供公平锁实现,基于 Redis 队列保证请求顺序。
5. 单点/主从切换时的可靠性风险
手动实现: 假设 Redis 单机或主从。
问题:
主节点宕机,从节点还没同步锁数据 → 锁丢失,多个客户端同时拿到同一把锁
你无法用 RedLock 算法解决这个问题
Redisson 的做法:
内置 RedLock(红锁)实现,需要向多个独立 Redis 节点申请锁,半数以上成功才算成功,能容忍部分节点故障。
总结对比表
| 问题 | 手动实现 | Redisson |
|---|---|---|
| 原子性 + 防误删 | ✅ 已解决(Lua + UUID) | ✅ 有 |
| 业务执行时间不确定(锁提前释放) | ❌ 固定过期时间,风险存在 | ✅ 自动续期 |
| 可重入 | ❌ 同一线程自己锁自己 | ✅ 支持 |
| 阻塞等待锁 | ❌ 无 / 自旋低效 | ✅ 高效阻塞 + 唤醒 |
| 公平锁 | ❌ 无 | ✅ 有 |
| 主从切换 / 多节点一致性 | ❌ 单点 / 异步复制风险 | ✅ RedLock |
什么时候必须上 Redisson
| 场景 | 建议 |
|---|---|
| 低并发、业务简单、锁持有时间确定且很短 | SET NX EX + Lua 解锁 够用 |
| 高并发、业务执行时间不确定 | 必须上 Redisson(需要续期) |
| 需要可重入锁(一个方法调另一个方法) | 必须上 Redisson(自己实现很麻烦) |
| 需要阻塞等待锁,且对性能有要求 | 必须上 Redisson(自旋 + 休眠效率低) |
| 需要公平锁、读写锁、红锁等高级特性 | 必须上 Redisson |
代码实现Redisson:
1.引入依赖:
XML
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.6</version>
</dependency>
这里需要注意的是,如果是我们引入的依赖爆红的话,可能是maven没刷新加载,刷新完我们可以看到侧面的相应依赖项:

2.在配置类中配置
告诉 Spring 如何创建 RedissonClient 这个对象,并把它放进 Spring 容器管理。
更直白地说:配置类就是教 Spring 怎么 new 这个对象。
javapackage com.hmdp.config; import org.redisson.Redisson; import org.redisson.api.RedissonClient; import org.redisson.config.Config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class RedissonConfig { @Bean public RedissonClient redissonClient(){ //创建配置 Config config=new Config(); config.useSingleServer().setAddress("redis://127.0.0.1:6379"); return Redisson.create(config); } }这里标上配置类的注解,@Configuration /标记这是一个配置类
然后加上**@Bean交给Spring管理**,这个方法返回的对象要放进容器
这里的Redis连接的是我们自己的主机地址,记得别写错,设置密码的自己也可以加上。
这里需要注意的是
如果我们已经导入了依赖并且maven也已经刷新了,RedisClient仍旧报错,这时我们只需要**清理一下Idea的缓存,**有可能是
Maven 刷新信号没传给索引 :我们点了刷新,依赖下载好了,但 IDEA 的索引更新任务可能因为某些原因没触发或没完成
并发写冲突 :Maven 在后台下载依赖(往磁盘写),IDEA 同时在读依赖(建索引),可能锁文件或状态不一致
因此我们清楚缓存:重启后,IDEA 发现缓存是空的,就会重新扫描整个项目的 classpath(包括所有的 Maven 依赖),重新生成索引。
总之:
写配置类是为了让 Spring 认识并管理那些不是我们写的类(比如 RedissonClient)。
用
@ServiceSpring 能自动发现,用RedissonClient必须通过@Bean告诉 Spring 怎么创建它。
3.实现类实现:
配置类写好之后,下一步就是在实现类(比如 Service)里使用 RedissonClient 来加锁和解锁。
注入:(Spring 会自动把配置类里创建的那个对象拿过来)
java@Resource private RedissonClient redissonClient;
java@Service public class OrderService { // 注入 RedissonClient(Spring 会自动把配置类里创建的那个对象拿过来) @Autowired private RedissonClient redissonClient; public void createOrder(String orderId) { // 1. 获取锁(根据业务 key 命名) RLock lock = redissonClient.getLock("lock:order:" + orderId); // 2. 加锁(阻塞等待,带自动续期) lock.lock(); try { // 3. 执行业务逻辑 // 例如:查询库存、扣减库存、创建订单等 System.out.println("执行业务,orderId: " + orderId); Thread.sleep(5000); // 模拟业务执行 } finally { // 4. 释放锁(一定要在 finally 里释放) lock.unlock(); } } }获取锁
boolean isLock = lock.tryLock();这里参数是干嘛的
方法 行为 适用场景 tryLock()只试一次,被占用立即返回 false 能接受失败、需要快速响应的场景(如秒杀提示"已售罄") tryLock(10, TimeUnit.SECONDS)最多等 10 秒,期间会不断重试 可以等待、希望最终能执行成功的场景 lock()无限等待,直到拿到锁 必须执行成功、不介意阻塞的场景
源码解析:
Redisson可重入锁的原理:
Redisson 的实现原理可以概括为:用 Redis 的 Hash 结构存储锁的持有者和重入次数。
普通的分布式锁,用
SET NX实现的那个,在 Redis 里存的是一个字符串,比如"锁的持有者:线程A"。如果同一个线程再次去拿这把锁:
线程A 已经持有锁,Redis 里已经有这个 key 了
线程A 再次执行 SET NX,发现 key 已存在
线程A 认为自己没拿到锁,就会阻塞等待
但持有锁的就是它自己,于是它永远等不到锁释放 → 死锁
这就是不可重入的问题。
Redisson 的解决方案
Redisson 不存简单的字符串,而是用 Hash(哈希表) 来存锁的信息。
Hash 的结构:
大 Key:锁的名字,比如
"myLock"里面的小 Field:持有锁的线程的唯一标识
里面的 Value:这个线程加锁的次数(计数器)
举个例子:
text
Hash "myLock" 的内容: - field = "uuid-123:线程ID-45" - value = 3意思是:
uuid-123:线程ID-45这个线程,已经对myLock这把锁加了 3 次锁。加锁的流程
第一次加锁:
检查 Hash
myLock是否存在 → 不存在创建 Hash,设置 field 为当前线程标识,value 设为 1
设置过期时间(比如 30 秒)
同一个线程第二次加锁:
检查 Hash
myLock是否存在 → 存在检查 Hash 里有没有当前线程的 field→ 有
不是创建新锁,而是把 value 加 1(从 1 变成 2)
刷新过期时间(重新计时 30 秒)
第三次加锁:
- 同样,value 从 2 变成 3,再刷新过期时间
关键点: 后面的加锁不会创建新的锁条目,只是把计数器往上加,同时把锁的存活时间重新计时。
解锁的流程
第一次解锁:
找到 Hash
myLock找到当前线程对应的 field
把 value 减 1(从 3 变成 2)
因为 value 还大于 0,说明还有重入,所以不删锁,只刷新过期时间
第二次解锁:
- value 从 2 变成 1,还是不删锁
第三次解锁:
value 从 1 变成 0
这时才真正删除 Redis 里的 Hash,锁被释放
关键点: 解锁多少次,计数器就减多少次。只有减到 0 时,锁才会真正消失。
类比:
普通锁像一扇门:
第一次进去,锁上门
第二次想进去,发现门锁着,以为别人在里面,就在门口等
但其实是自己锁的,永远等不到
Redisson 的可重入锁像一个计数器:
第一次进去,计数器显示 1
第二次进去,计数器变成 2(门不关)
第三次进去,计数器变成 3
出来一次,计数器减 1
直到计数器变回 0,门才真正锁上
用 Hash 存储:大 Key 是锁名,小 Field 是线程标识,Value 是重入次数
同线程重入:不阻塞,只是把次数加 1,同时刷新过期时间
真正释放:每次解锁次数减 1,只有减到 0 才删除 Redis 中的锁
这样既解决了同一线程重复加锁的死锁问题,又保证了锁的寿命跟业务执行时间走,不会提前过期。
Redisson的锁重试和WatchDog机制原理:
这两个机制解决了分布式锁中的两个痛点:
-
锁重试:拿不到锁时怎么办(阻塞等待 vs 立即返回)
-
WatchDog:业务没执行完,锁过期了怎么办自动续期)
一、锁重试机制
1. 三种获取锁的方式
方法 行为 适用场景 lock()无限等待,直到拿到锁 必须执行成功的任务 tryLock()只试一次,拿不到立即返回 false 可接受失败的场景 tryLock(waitTime, leaseTime, unit)最多等待 waitTime,期间不断重试 希望等待但不想无限等 2.
tryLock(waitTime, leaseTime, unit)的重试原理场景: 线程 A 持有锁,线程 B 来拿锁
线程 B 执行:
java
boolean locked = lock.tryLock(10, 30, TimeUnit.SECONDS); // 最多等 10 秒,拿到锁后锁的有效期是 30 秒内部流程:
text
线程 B 尝试加锁 ↓ Redis:锁被线程 A 占着 ↓ 线程 B 不是立即返回失败,而是订阅 Redis 的频道(发布订阅) ↓ 线程 B 阻塞等待(不浪费 CPU) ↓ 线程 A 释放锁时,发布一条消息到该频道 ↓ 线程 B 收到消息,立即重新尝试加锁 ↓ 如果成功,返回 true;如果失败,继续等待 ↓ 超过 10 秒还没成功 → 返回 false关键点:
不是轮询:用 Redis 的发布订阅,锁释放时主动通知,响应快
不占 CPU:等待期间线程是阻塞的,不会空转
有超时:不会无限等下去
3. 重试的间隔
Redisson 的重试不是固定间隔,而是:
第一次失败后,订阅频道,进入阻塞等待
收到释放通知后,立即重新尝试
如果没有收到通知(网络问题等),也会有一个小的退避策略
二、WatchDog 机制(自动续期)
1. 解决什么问题
遇到的问题:
设置锁过期时间 10 秒
业务执行了 15 秒
第 10 秒时锁自动释放,其他线程进来,数据错乱
WatchDog 就是解决这个问题的:业务没执行完,锁就不会过期。
2. WatchDog 的工作原理
默认行为:
调用
lock.lock()或tryLock()(不传 leaseTime)时Redisson 会给锁设置一个默认过期时间(30 秒)
同时启动一个后台线程(WatchDog)
WatchDog 的工作:
text
锁被成功获取(过期时间 30 秒) ↓ WatchDog 线程启动 ↓ 每 10 秒检查一次(30 秒的 1/3) ↓ 检查:当前线程是否还持有这把锁? ↓ 如果是 → 把锁的过期时间重置为 30 秒(续期) ↓ 如果否 → 停止续期时间线示例:
text
0s 10s 20s 30s 40s 50s 60s |-----|-----|-----|-----|-----|-----| 锁获取 ↑续期 ↑续期 ↑续期 ↑续期 ↑续期 (每10秒续期到30秒) ↑ 业务结束,释放锁只要业务还在跑,锁的过期时间永远被推到"当前时间 + 30 秒"。
3. WatchDog 的开启条件
加锁方式 WatchDog 是否启动 lock()✅ 启动 tryLock()✅ 启动 tryLock(waitTime, TimeUnit.SECONDS)(不传 leaseTime)✅ 启动 tryLock(waitTime, leaseTime, unit)(传了 leaseTime)❌ 不启动 lock(leaseTime, unit)(传了 leaseTime)❌ 不启动 规则: 如果你手动指定了锁的持有时间(leaseTime),Redisson 就认为你知道自己在做什么,不会帮你续期。时间到了锁就释放,不管业务是否完成。
4. WatchDog 的停止条件
WatchDog 会在以下情况停止:
业务执行完,调用
unlock()释放锁当前线程不再持有锁(被其他情况释放了)
Redisson 客户端关闭
java
RLock lock = redissonClient.getLock("myLock"); // 尝试加锁,最多等 5 秒(重试机制),拿到后锁有效期 30 秒(WatchDog 会续期) boolean locked = lock.tryLock(5, TimeUnit.SECONDS); if (locked) { try { // 业务执行中... // WatchDog 每 10 秒检查并续期 // 业务执行 10 分钟也不会锁过期 } finally { lock.unlock(); // 释放锁,WatchDog 停止 } }流程图:
text
线程尝试获取锁 ↓ 锁被占用? ├─ 否 → 拿到锁,启动 WatchDog │ ↓ │ 执行业务 │ ↓ │ WatchDog 每 10 秒续期 │ ↓ │ 业务完成,unlock,停止 WatchDog │ └─ 是 → 订阅频道,阻塞等待 ↓ 收到释放通知 ↓ 重新尝试加锁 ↓ 成功 → 拿到锁,启动 WatchDog 超时 → 返回 false
| 功能 | 我们自己实现的锁 | Redisson |
|---|---|---|
| 拿不到锁怎么办 | 要么立即失败,要么自己写 while 循环 | 内置发布订阅,阻塞等待,不占 CPU |
| 重试间隔 | 自己写 Thread.sleep(),固定间隔 | 收到通知立即重试,响应快 |
| 锁过期问题 | 设置固定时间,业务超时锁就没了 | WatchDog 自动续期,锁跟着业务走 |
| 续期实现 | 需要自己写定时任务 | 内置后台线程,自动续期 |
需要注意的坑
1. 手动指定 leaseTime 会关闭 WatchDog
java
// ❌ WatchDog 不启动,30 秒后锁强制释放 lock.lock(30, TimeUnit.SECONDS); // ✅ WatchDog 启动,自动续期 lock.lock(); // ✅ 也是不启动(传了 leaseTime) lock.tryLock(10, 30, TimeUnit.SECONDS);2. 业务时间超过 WatchDog 续期间隔
WatchDog 每 10 秒续一次,如果业务在 10 秒内完成,续期不会发生。如果业务超过 10 秒,续期会在第 10 秒时执行,把锁延长到 30 秒。
极端情况: 业务在第 10 秒刚好完成,续期还没来得及执行,锁会怎样?------ 业务完成了,锁也会正常释放,没问题。
3. 网络断开时 WatchDog 的行为
如果 Redisson 和 Redis 断开连接:
WatchDog 无法续期
锁会在 30 秒后自动释放
这是合理的设计:宁可让锁提前释放,也不能让锁永远锁住
总结
| 机制 | 解决的问题 | 实现方式 |
|---|---|---|
| 锁重试 | 拿不到锁时如何等待 | Redis 发布订阅 + 阻塞等待,有超时控制 |
| WatchDog | 业务没执行完锁就过期 | 后台线程定期检查并续期,默认每 10 秒续到 30 秒 |
-
锁重试让我们不用自己写轮询,等待锁释放时能被立即唤醒
-
WatchDog让我们不用担心业务执行时间超锁,锁会自动跟着业务走
这两个机制加在一起,就是 Redisson 能成为生产级分布式锁方案的核心原因。
Redisson MultiLock 的原理
MultiLock 是 Redisson 提供的联锁 机制,它的作用是:同时锁定多个资源,要么全部锁住,要么一个都不锁。
1、什么时候需要 MultiLock
场景: 一个操作需要同时操作多个独立的资源,必须保证所有资源都被锁定,才能开始执行。
例子: 跨账户转账
从账户 A 扣钱
往账户 B 加钱
为了保证数据一致,必须同时锁定账户 A 和账户 B。如果只锁 A,不锁 B,可能出现:A 扣了钱,但 B 没加上(因为 B 被别的操作锁着)。
MultiLock 的作用: 把对多个锁的加锁操作变成一个原子操作。
2、MultiLock 的核心原则
MultiLock 实现的是**"多锁合一"**的逻辑:
原则 说明 全部成功才算成功 必须所有锁都获取成功,整个加锁才算成功 部分失败则全部回滚 只要有一个锁没拿到,已经拿到的锁全部释放 统一过期时间 所有锁使用相同的过期时间,同时释放 3、MultiLock 的使用方式
java
// 1. 创建多个独立的锁 RLock lock1 = redissonClient.getLock("lock:account:A"); RLock lock2 = redissonClient.getLock("lock:account:B"); RLock lock3 = redissonClient.getLock("lock:account:C"); // 2. 创建 MultiLock,把多个锁组合在一起 RLock multiLock = redissonClient.getMultiLock(lock1, lock2, lock3); // 3. 像使用普通锁一样使用 MultiLock multiLock.lock(); // 同时锁住 A、B、C try { // 执行跨账户操作 transferMoney(); } finally { multiLock.unlock(); // 同时释放 A、B、C }注意: 调用
multiLock.lock()时,Redisson 会逐个去获取lock1、lock2、lock3,而不是一次性的原子命令(因为 Redis 不支持跨 key 的原子操作)。4、MultiLock 的加锁流程
text
调用 multiLock.lock() ↓ 创建一个列表,记录成功获取的锁 ↓ 遍历所有锁(lock1, lock2, lock3...) ↓ 对当前锁尝试加锁 ├─ 加锁成功 → 把锁加入"成功列表",继续下一个 └─ 加锁失败 → 进入重试等待(有超时时间) ↓ 如果最终所有锁都成功 → 返回 true ↓ 【如果任何一个锁加锁失败(超时或异常)】 ↓ 遍历"成功列表",对所有已成功获取的锁执行 unlock ↓ 抛出异常或返回 false关键点: MultiLock 不是原子的,但通过"全部成功才保留,部分失败就回滚"的机制,实现了逻辑上的原子性。
5、锁重试和 WatchDog 在 MultiLock 中的行为
1. 锁重试
java
// 最多等待 10 秒,拿到所有锁后锁的有效期是 30 秒 boolean locked = multiLock.tryLock(10, 30, TimeUnit.SECONDS);重试逻辑:
遍历每个锁时,对单个锁应用等待时间
如果一个锁拿不到,MultiLock 会在剩余时间内不断重试这个锁
如果某个锁一直拿不到,最终超时,则释放所有已获取的锁
2. WatchDog 的行为
java
multiLock.lock(); // 不传 leaseTime,启动 WatchDogWatchDog 在 MultiLock 中的特殊之处:
每个子锁独立续期 :WatchDog 会对 MultiLock 中的每一个锁分别进行续期
正确逻辑 :
multiLock.lock()内部调用每个子锁的lock(),每个子锁都有自己的 WatchDog。只要 MultiLock 还没被 unlock,每个子锁的 WatchDog 都会独立续期。潜在问题: 如果所有子锁都不传 leaseTime,那么会有 N 个 WatchDog 线程在运行(每个子锁一个),资源消耗会翻倍。
6、MultiLock 的解锁流程
text
调用 multiLock.unlock() ↓ 遍历所有子锁 ↓ 对每个子锁执行 unlock ↓ 如果有锁在 unlock 时抛出异常 ↓ 继续尝试解锁其他锁(不能因为一个失败就停止) ↓ 最终汇总异常情况关键点: 解锁不会因为某个锁失败就停止,会尽可能释放所有锁。
7、MultiLock 的局限性
局限性 说明 不是分布式事务 MultiLock 只能保证"锁"的一致性,不能回滚业务数据。比如 A 扣钱成功了,B 加钱失败了,MultiLock 不会帮你自动回滚 A 的扣钱操作。 性能开销 加锁时间 = N × 单锁加锁时间,锁越多越慢 可靠性降低 N 个锁中任意一个 Redis 节点故障,整个 MultiLock 就无法工作 WatchDog 膨胀 每个子锁都有自己的 WatchDog 线程,锁多了线程数会翻倍
8、和 RedLock 的区别
很多人会把 MultiLock 和 RedLock 搞混,它们完全不同:
对比 MultiLock RedLock 目的 同时锁定多个不同的资源 对同一个资源在多个 Redis 节点上锁定 锁的对象 lock1, lock2, lock3(不同业务资源) 同一个锁名,多个 Redis 实例 使用场景 跨账户转账、批量操作 高可靠性分布式锁(防止主从切换丢锁) MultiLock 示例: 锁住账户 A、账户 B、账户 C(三个不同的业务资源)
RedLock 示例: 在 Redis-1、Redis-2、Redis-3 三个节点上,锁住同一个"订单 123"(同一个资源,多个节点备份)
结语:如果对你有帮助,请点赞,关注,收藏,你的支持就是我最大的鼓励!

