redisson.lock/lock带参数/trylock/trylock带参数 的区别
Q:redisson中trylock的使用,简单的例子
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
public class RedissonTryLockExample {
public static void main(String[] args) {
// 1. 配置Redisson客户端
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
// 2. 创建Redisson客户端实例
RedissonClient redisson = Redisson.create(config);
// 3. 获取锁对象
RLock lock = redisson.getLock("myLock");
try {
// 4. 尝试获取锁,最多等待100秒,上锁后30秒自动解锁
boolean isLocked = lock.tryLock(100, 30, TimeUnit.SECONDS);
if (isLocked) {
try {
// 5. 获取锁成功,执行业务逻辑
System.out.println("获取锁成功,执行关键代码...");
Thread.sleep(10000); // 模拟业务操作
} finally {
// 6. 释放锁
lock.unlock();
System.out.println("锁已释放");
}
} else {
System.out.println("获取锁失败,其他线程正在持有锁");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.println("线程被中断");
} finally {
// 7. 关闭Redisson客户端
redisson.shutdown();
}
}
}
方法说明
tryLock
方法有几种重载形式:
boolean tryLock()
- 立即尝试获取锁,成功返回true,失败返回falseboolean tryLock(long waitTime, long leaseTime, TimeUnit unit)
- 尝试获取锁,最多等待waitTime,锁的持有时间为leaseTime
注意事项
- 一定要在finally块中释放锁,避免死锁
- 建议设置合理的leaseTime(锁自动释放时间),防止业务逻辑执行时间过长导致锁无法释放
- 如果获取锁失败,应根据业务需求决定是重试还是放弃操作
👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉
Q:boolean isLocked = lock.tryLock(100, 30, TimeUnit.SECONDS); 我是否可以理解成:100秒之内,我要是还没有获取到锁,我就返回false,如果获取到锁了,我只能持有这个锁30s,30一到,不论我是否执行完业务,锁都会被释放掉
是的!lock.tryLock(100, 30, TimeUnit.SECONDS)
的含义如下:
-
100
(等待时间):- 最多等待 100秒 尝试获取锁。
- 如果在这 100 秒内锁被其他线程释放,并且当前线程成功获取到锁,返回
true
。 - 如果 100 秒后仍然没有获取到锁,返回
false
(不会无限等待)。
-
30
(租约时间 leaseTime):- 一旦成功获取锁,最多持有 30秒。
- 即使业务代码还没执行完,30秒后锁也会自动释放(Redisson 底层通过 Redis 的
pexpire
实现自动过期)。 - 如果业务在 30 秒内完成,可以手动
unlock()
提前释放锁。
⚠️ 注意事项:
-
锁自动释放可能导致问题:如果业务执行时间超过 30 秒,锁会被强制释放,而此时业务可能仍在执行,可能导致并发问题(其他线程拿到锁后操作共享数据)。
解决方案:合理估算业务时间,确保leaseTime
足够长,或使用lock.lock()
(默认不自动释放,需手动unlock()
)。 -
避免忘记释放锁:即使设置了自动释放,也建议在
finally
中手动unlock()
,防止异常情况下 Redisson 的自动续期(看门狗机制)未生效。
示例修正(推荐写法):
boolean isLocked = lock.tryLock(100, 30, TimeUnit.SECONDS);
if (isLocked) {
try {
// 业务逻辑(确保能在30秒内完成)
System.out.println("执行业务...");
} finally {
// 即使设置了自动释放,也主动解锁(避免异常时看门狗续期)
lock.unlock();
}
} else {
System.out.println("获取锁失败,请重试或放弃");
}
对比其他方法:
方法 | 行为 |
---|---|
lock.tryLock() |
立即尝试,成功返回 true ,失败返回 false ,不自动释放(需手动 unlock ) |
lock.tryLock(100, 30, SECONDS) |
等待最多100秒,成功后锁30秒自动释放 |
lock.lock() |
阻塞等待,默认30秒自动续期(需手动 unlock ) |
👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉
Q:我还有个疑问,既然不论我是否执行完逻辑了,30s锁都会自动释放,那么当我35秒的时候执行完逻辑了,由于finally代码块的存在,我一定会去执行 lock.unlock(); 这个时候会发生什么
场景还原
- 你使用
lock.tryLock(100, 30, TimeUnit.SECONDS)
获取锁 - 业务逻辑执行了 35 秒(超过了 leaseTime 30 秒)
- 在 finally 块中尝试执行
lock.unlock()
会发生什么?
此时调用 lock.unlock()
会抛出异常(IllegalMonitorStateException
),因为:
- 锁已经自动释放:30 秒到期时,Redis 已经自动删除了这个锁
- Redisson 会检查:在解锁时,Redisson 会先检查当前线程是否还持有这个锁(通过 Redis 的 key 是否存在)
- 发现锁不存在:由于锁已自动释放,Redisson 会认为你不是锁的持有者,于是抛出异常
如何处理这种情况?
你有几种选择:
方案1:捕获异常并处理(推荐)
boolean isLocked = lock.tryLock(100, 30, TimeUnit.SECONDS);
if (isLocked) {
try {
// 长时间业务逻辑...
TimeUnit.SECONDS.sleep(35); // 模拟超时
} finally {
try {
lock.unlock();
} catch (IllegalMonitorStateException e) {
// 锁已自动释放,无需处理
System.out.println("锁已自动释放,无需手动解锁");
}
}
}
方案2:确保业务在 leaseTime 内完成
// 设置足够长的 leaseTime
boolean isLocked = lock.tryLock(100, 60, TimeUnit.SECONDS);
if (isLocked) {
try {
// 确保业务在60秒内完成
} finally {
lock.unlock(); // 安全解锁
}
}
方案3:使用 Redisson 的看门狗机制(默认 lock() 方法)
lock.lock(); // 默认30秒看门狗自动续期
try {
// 长时间业务...
} finally {
lock.unlock(); // 安全解锁
}
关键点总结
- 超过 leaseTime 后解锁会抛异常,但不会影响业务逻辑执行
- Redisson 的设计哲学:宁可抛异常也不允许误删别人的锁(安全第一)
- 最佳实践:要么确保业务能在 leaseTime 内完成,要么使用看门狗自动续期
👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉
Q:好的,那么比如现在我有ABC三个线程,同时抢锁,此时A抢到了锁,BC会怎么做?是线程直接挂起等待A的唤醒?还是cpu一直空转?还是不停的查询A放锁了没有?
在 Redisson 的 tryLock
场景下,A 抢到锁后,B 和 C 的行为取决于你使用的具体方法。我们分情况讨论:
情况 1:使用 tryLock()
(无参数,非阻塞)
lock.tryLock(); // 立即尝试,不等待
- A:抢到锁,执行业务逻辑。
- B 和 C:
- 立即返回
false
,不会等待,也不会挂起或空转。 - 线程继续执行后续逻辑(比如返回错误或重试)。
- 不会主动轮询,除非你在代码里手动实现重试机制。
- 立即返回
情况 2:使用 tryLock(waitTime, leaseTime, unit)
(带超时等待)
lock.tryLock(100, 30, TimeUnit.SECONDS); // 最多等100秒
- A:抢到锁,持有 30 秒(或手动释放)。
- B 和 C:
- 不会挂起线程(不占用 CPU 空转)。
- 不会主动轮询(Redisson 底层通过 Redis 的发布订阅机制实现高效通知)。
- 行为如下:
- 先尝试一次抢锁(失败后订阅锁释放事件)。
- 通过 Redis 的
pub/sub
监听锁释放消息(非轮询,事件驱动)。 - 当 A 释放锁时,Redisson 会收到通知,唤醒 B 和 C 重新竞争锁。
- 如果在
waitTime
(100秒)内没等到锁,返回false
。
情况 3:使用 lock()
(阻塞式,无限等待)
lock.lock(); // 阻塞直到获取锁
- A:抢到锁,默认 30 秒自动续期(看门狗机制)。
- B 和 C:
- 线程挂起等待(通过 Java 的
LockSupport.park()
),不占用 CPU。 - 同样依赖 Redis 的
pub/sub
监听锁释放事件。 - 当 A 释放锁时,B 和 C 会被唤醒并竞争锁。
- 线程挂起等待(通过 Java 的
关键区别总结
行为 | tryLock() |
tryLock(waitTime, leaseTime) |
lock() |
---|---|---|---|
是否阻塞线程 | 不阻塞 | 阻塞(但有限超时) | 阻塞(无限等待) |
CPU 占用 | 无 | 无(事件驱动) | 无(线程挂起) |
底层机制 | 直接返回 | Redis 发布订阅 + 超时控制 | Redis 发布订阅 + 看门狗续期 |
补充说明:Redisson 的高效设计
- 无忙等待:Redisson 不会通过轮询(比如 while 循环)检查锁状态,而是用 Redis 的
pub/sub
监听锁释放事件,避免 CPU 空转。 - 公平性:多个等待线程(如 B 和 C)的唤醒顺序取决于 Redis 的事件通知顺序,非严格公平(若需要公平锁,可用
RFairLock
)。
最佳实践建议
- 如果希望线程不阻塞,用
tryLock()
。 - 如果需要有限时间等待,用
tryLock(waitTime, leaseTime)
。 - 如果业务必须拿到锁且不怕等待,用
lock()
(但要确保最终释放锁)。
👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉
Q:好的,那么redis 的 pub/sub 是如何唤醒 java 中的 BC线程的?
在 Redisson 中,当使用 tryLock(waitTime, leaseTime)
或 lock()
时,B 和 C 线程的唤醒机制依赖于 Redis 的 Pub/Sub(发布/订阅)和 Java 的线程同步机制。以下是详细流程:
1. Redis Pub/Sub 基础
Redis 的 Pub/Sub 是一种消息通知机制:
- 频道(Channel):Redisson 会为每个锁(如
myLock
)创建一个对应的 Redis 频道(如redisson_lock__channel:{myLock}
)。 - 订阅(Subscribe):B 和 C 线程抢锁失败后,会订阅这个频道。
- 发布(Publish):当 A 释放锁时,Redisson 会向该频道发送一条解锁消息。
2. B 和 C 线程的等待与唤醒流程
(1)抢锁失败后订阅频道
当 B 和 C 调用 lock.tryLock(100, 30, SECONDS)
时:
- 先尝试通过 Redis 的
SETNX
(或 Lua 脚本)抢锁。 - 如果抢锁失败,B 和 C 会通过 Redis 命令
SUBSCRIBE
订阅锁的频道:SUBSCRIBE redisson_lock__channel:{myLock}
(2)线程挂起(非忙等待)
- B 和 C 的线程会通过 Java 的
LockSupport.parkNanos()
挂起(进入 WAITING 状态),不占用 CPU。 - 挂起时会设置超时时间(剩余
waitTime
),避免无限等待。
(3)A 释放锁时触发通知
当 A 执行 lock.unlock()
时:
- Redisson 通过 Redis 命令发布一条解锁消息:
PUBLISH redisson_lock__channel:{myLock} "解锁消息"
- Redis 服务器会将消息推送给所有订阅该频道的客户端(B 和 C)。
(4)B 和 C 收到通知后唤醒
- Redisson 客户端(B 和 C 所在的服务)收到 Redis 的 Pub/Sub 消息。
- Redisson 内部通过
LockSupport.unpark()
唤醒挂起的线程(B 和 C)。 - 被唤醒的线程会重新尝试抢锁:
- 如果抢到锁,返回
true
。 - 如果锁已被其他线程抢走,再次挂起等待。
- 如果抢到锁,返回
(5)超时处理
如果在 waitTime
(如 100 秒)内未抢到锁:
- Redisson 会取消订阅 Redis 频道。
- 唤醒线程并返回
false
。
3. 关键设计亮点
-
无轮询:
依赖 Redis Pub/Sub 的事件驱动机制,避免线程轮询(减少 Redis 和 CPU 压力)。 -
线程挂起而非忙等待:
通过LockSupport.park()
挂起线程,不消耗 CPU,唤醒由事件触发。 -
避免无效竞争:
只有收到锁释放通知后,线程才会重新抢锁,减少无效的 Redis 请求。
4. 补充:Redisson 的看门狗机制(用于 lock()
)
如果使用 lock()
(无超时自动释放):
- Redisson 会启动一个看门狗线程,每 10 秒检查锁是否仍被持有。
- 如果锁仍被持有,自动续期(通过 Redis 的
pexpire
命令)。 - 确保业务执行时间较长时锁不会意外释放。
5. 代码层面的简化流程
// Redisson 底层伪代码(简化版)
public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) {
// 1. 尝试抢锁(Lua 脚本操作 Redis)
boolean acquired = tryAcquireLock();
if (acquired) return true;
// 2. 订阅锁释放频道
subscribeToLockChannel();
// 3. 挂起线程,等待通知或超时
long remainingTime = waitTime;
while (remainingTime > 0) {
LockSupport.parkNanos(remainingTime); // 线程挂起
if (isNotifiedByRedis()) { // 收到 Redis 的 Pub/Sub 通知
acquired = tryAcquireLock(); // 再次尝试抢锁
if (acquired) return true;
}
remainingTime = updateRemainingTime();
}
// 4. 超时后取消订阅并返回失败
unsubscribeFromLockChannel();
return false;
}
总结
- B 和 C 的唤醒:通过 Redis Pub/Sub 监听锁释放事件,事件到达后由 Redisson 主动唤醒线程。
- 零成本等待:线程挂起时不消耗 CPU,唤醒由事件驱动。
- 高性能设计:避免轮询 Redis,依赖原生 Pub/Sub 机制。
这种设计完美结合了 Redis 的高效通知和 Java 的线程同步能力!
👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉
Q:好的,比如说现在的参数是 lock.tryLock(100, 30, SECONDS) ,还是ABC这三个线程,A抢到锁之后,BC进入等待,并且超时时间是100s,A执行了20s后放锁,唤醒了BC,B抢到了锁,那么此时C再次进入等待,等待的时间被重置成100s么?还是80s?
在 lock.tryLock(100, 30, TimeUnit.SECONDS)
的场景下,C 线程的剩余等待时间不会被重置为 100 秒,而是继续沿用最初的剩余时间(80 秒)。以下是详细分析:
关键结论
- C 的等待时间是基于初始调用的总超时(100 秒)计算的,不会因中间被唤醒而重置。
- 当 B 抢到锁后,C 会继续等待,剩余时间 = 初始 100 秒 - 已等待时间(20 秒) = 80 秒。
- 如果 C 在接下来的 80 秒内仍未抢到锁,最终会超时返回
false
。
完整流程分析
时间线梳理:
-
T=0s:
- A、B、C 同时调用
lock.tryLock(100, 30, SECONDS)
。 - A 抢到锁,B 和 C 订阅 Redis 频道并挂起等待,开始 100 秒倒计时。
- A、B、C 同时调用
-
T=20s:
- A 释放锁,触发 Redis Pub/Sub 通知。
- B 和 C 被唤醒,立即尝试抢锁:
- B 抢到锁,持有 30 秒(新的租约)。
- C 抢锁失败,重新挂起等待,但剩余等待时间 =
100s - 20s = 80s
。
-
T=100s:
- 如果 B 在 30 秒内(即 T=50s 前)释放锁,C 可能有机会抢到锁。
- 如果 B 持有锁直到 T=50s(或 C 一直未抢到锁),C 会在 T=100s 时超时,返回
false
。
技术原理
-
Redisson 的等待时间计算:
- 在第一次调用
tryLock
时,Redisson 会记录调用的系统时间戳(如startTime=System.currentTimeMillis()
)。 - 每次唤醒后,剩余时间通过公式计算:
long remainingTime = startTime + waitTime - System.currentTimeMillis();
- 不会因中间唤醒而重置,始终基于初始调用的总超时。
- 在第一次调用
-
C 线程的行为:
- 每次被唤醒后,C 会检查剩余时间:
- 如果剩余时间
<= 0
,直接返回false
。 - 如果剩余时间
> 0
,再次挂起等待(剩余时间)。
- 如果剩余时间
- 每次被唤醒后,C 会检查剩余时间:
验证方法
可以通过日志打印时间验证:
long start = System.currentTimeMillis();
boolean locked = lock.tryLock(100, 30, TimeUnit.SECONDS);
if (locked) {
try {
System.out.println("抢到锁,耗时: " + (System.currentTimeMillis() - start) + "ms");
} finally {
lock.unlock();
}
} else {
System.out.println("未抢到锁,总耗时: " + (System.currentTimeMillis() - start) + "ms");
}
设计意义
- 避免无限等待:确保线程不会因多次唤醒而意外延长总等待时间。
- 公平性:所有线程遵守相同的初始超时约束,防止某些线程“插队”导致不公平。
总结
- C 的等待时间 = 初始 100 秒 - 已等待时间(如 20 秒)= 80 秒。
- Redisson 严格保证总超时不超过最初设定的
waitTime
,符合分布式锁的可靠性要求。
👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉
Q:好的,总结下来就是,在redisson中,如果是 lock.tryLock(100, 30, SECONDS) 这样加锁,底层使用的是 redis的时间发布和监听机制+setnx 的方法,如果使用的时候 lock.lock() 方法,那么使用的是看门狗机制,底层是 hash 的数据结构。那么是否有 lock.lock(100, 30, SECONDS) 这样的方法?底层用的是什么呢?
在 Redisson 中,确实存在 lock.lock(long leaseTime, TimeUnit unit)
方法(注意参数顺序与 tryLock
不同),但 没有 lock(100, 30, SECONDS)
这种三参数的方法。以下是详细解析:
1. Redisson 的 lock()
方法分类
(1)lock()
- 行为:阻塞等待,直到获取锁。
- 底层机制:看门狗自动续期(默认 30 秒续期一次,通过 Redis Hash 结构实现)。
- 数据结构:
KEY: "myLock" TYPE: "hash" FIELD: "<线程ID>" VALUE: "<重入次数>"
- 特点:锁不会自动释放,必须手动
unlock()
。
(2)lock(long leaseTime, TimeUnit unit)
- 示例:
lock(30, SECONDS)
- 行为:阻塞等待获取锁,但锁最多持有
leaseTime
时间后自动释放。 - 底层机制:
- 仍使用 Redis Hash 结构存储锁信息。
- 无看门狗续期,依赖 Redis 的 Key 过期自动释放(
pexpire
命令)。
- 特点:即使未手动
unlock()
,锁也会在leaseTime
后自动释放。
2. 为什么没有 lock(100, 30, SECONDS)
?
- 设计逻辑:
lock()
的语义是阻塞直到成功,不需要waitTime
参数(与tryLock
的“尝试”语义冲突)。- 如果需要超时等待,应使用
tryLock(waitTime, leaseTime, unit)
。
3. 关键方法对比
方法签名 | 阻塞行为 | 自动释放 | 底层机制 |
---|---|---|---|
lock() |
无限阻塞 | 看门狗续期(需手动解锁) | Redis Hash + 看门狗线程 |
lock(30, SECONDS) |
无限阻塞 | 30秒后自动释放 | Redis Hash + pexpire |
tryLock(100, 30, SECONDS) |
最多阻塞100秒 | 30秒后自动释放 | Redis Pub/Sub + SETNX |
tryLock() |
立即返回 | 需手动解锁 | Redis SETNX |
4. 底层实现差异
(1)lock(30, SECONDS)
的流程
- 通过 Lua 脚本在 Redis 中创建 Hash 键:
-- 参数:KEY, leaseTime, 线程ID redis.call('hset', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]);
- 如果锁被其他线程持有,订阅频道并挂起线程(类似
tryLock
的 Pub/Sub 机制)。 - 不启动看门狗续期,依赖
pexpire
的自动过期。
(2)与 tryLock(100, 30, SECONDS)
的区别
特性 | lock(30, SECONDS) |
tryLock(100, 30, SECONDS) |
---|---|---|
阻塞行为 | 无限阻塞直到成功 | 最多阻塞100秒 |
锁释放保证 | 业务可能未完成时自动释放 | 同左 |
适用场景 | 必须拿到锁且容忍自动释放 | 有限时间等待 + 自动释放 |
5. 如何选择?
- 需要严格确保业务完成:用
lock()
+ 手动unlock()
(看门狗续期)。 - 需要限制锁持有时间:用
lock(30, SECONDS)
或tryLock(100, 30, SECONDS)
。 - 需要非阻塞尝试:用
tryLock()
。
总结
lock(leaseTime, unit)
存在,但无waitTime
参数,因为它设计为无限等待。- 底层仍用 Redis Hash,但无看门狗,依赖 Redis 过期机制。
- 如果需要超时等待 + 自动释放,选择
tryLock(waitTime, leaseTime, unit)
。
Redisson 通过这种设计清晰区分了“阻塞无限等待”和“有限时间尝试”两种场景!
更多推荐
所有评论(0)