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 方法有几种重载形式:

  1. boolean tryLock() - 立即尝试获取锁,成功返回true,失败返回false
  2. boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) - 尝试获取锁,最多等待waitTime,锁的持有时间为leaseTime

注意事项

  1. 一定要在finally块中释放锁,避免死锁
  2. 建议设置合理的leaseTime(锁自动释放时间),防止业务逻辑执行时间过长导致锁无法释放
  3. 如果获取锁失败,应根据业务需求决定是重试还是放弃操作

👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉

Q:boolean isLocked = lock.tryLock(100, 30, TimeUnit.SECONDS); 我是否可以理解成:100秒之内,我要是还没有获取到锁,我就返回false,如果获取到锁了,我只能持有这个锁30s,30一到,不论我是否执行完业务,锁都会被释放掉

是的lock.tryLock(100, 30, TimeUnit.SECONDS) 的含义如下:

  1. 100(等待时间)

    • 最多等待 100秒 尝试获取锁。
    • 如果在这 100 秒内锁被其他线程释放,并且当前线程成功获取到锁,返回 true
    • 如果 100 秒后仍然没有获取到锁,返回 false(不会无限等待)。
  2. 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(); 这个时候会发生什么

场景还原

  1. 你使用 lock.tryLock(100, 30, TimeUnit.SECONDS) 获取锁
  2. 业务逻辑执行了 35 秒(超过了 leaseTime 30 秒)
  3. 在 finally 块中尝试执行 lock.unlock()

会发生什么?

此时调用 lock.unlock() 会抛出异常IllegalMonitorStateException),因为:

  1. 锁已经自动释放:30 秒到期时,Redis 已经自动删除了这个锁
  2. Redisson 会检查:在解锁时,Redisson 会先检查当前线程是否还持有这个锁(通过 Redis 的 key 是否存在)
  3. 发现锁不存在:由于锁已自动释放,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(); // 安全解锁
}

关键点总结

  1. 超过 leaseTime 后解锁会抛异常,但不会影响业务逻辑执行
  2. Redisson 的设计哲学:宁可抛异常也不允许误删别人的锁(安全第一)
  3. 最佳实践:要么确保业务能在 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 的发布订阅机制实现高效通知)。
    • 行为如下
      1. 先尝试一次抢锁(失败后订阅锁释放事件)。
      2. 通过 Redis 的 pub/sub 监听锁释放消息(非轮询,事件驱动)。
      3. 当 A 释放锁时,Redisson 会收到通知,唤醒 B 和 C 重新竞争锁。
      4. 如果在 waitTime(100秒)内没等到锁,返回 false

情况 3:使用 lock()(阻塞式,无限等待)

lock.lock(); // 阻塞直到获取锁
  • A:抢到锁,默认 30 秒自动续期(看门狗机制)。
  • B 和 C
    • 线程挂起等待(通过 Java 的 LockSupport.park()),不占用 CPU
    • 同样依赖 Redis 的 pub/sub 监听锁释放事件。
    • 当 A 释放锁时,B 和 C 会被唤醒并竞争锁。

关键区别总结

行为 tryLock() tryLock(waitTime, leaseTime) lock()
是否阻塞线程 不阻塞 阻塞(但有限超时) 阻塞(无限等待)
CPU 占用 无(事件驱动) 无(线程挂起)
底层机制 直接返回 Redis 发布订阅 + 超时控制 Redis 发布订阅 + 看门狗续期

补充说明:Redisson 的高效设计

  1. 无忙等待:Redisson 不会通过轮询(比如 while 循环)检查锁状态,而是用 Redis 的 pub/sub 监听锁释放事件,避免 CPU 空转。
  2. 公平性:多个等待线程(如 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) 时:

  1. 先尝试通过 Redis 的 SETNX(或 Lua 脚本)抢锁。
  2. 如果抢锁失败,B 和 C 会通过 Redis 命令 SUBSCRIBE 订阅锁的频道:
    SUBSCRIBE redisson_lock__channel:{myLock}
    
(2)线程挂起(非忙等待)
  • B 和 C 的线程会通过 Java 的 LockSupport.parkNanos() 挂起(进入 WAITING 状态),不占用 CPU
  • 挂起时会设置超时时间(剩余 waitTime),避免无限等待。
(3)A 释放锁时触发通知

当 A 执行 lock.unlock() 时:

  1. Redisson 通过 Redis 命令发布一条解锁消息:
    PUBLISH redisson_lock__channel:{myLock} "解锁消息"
    
  2. Redis 服务器会将消息推送给所有订阅该频道的客户端(B 和 C)。
(4)B 和 C 收到通知后唤醒
  1. Redisson 客户端(B 和 C 所在的服务)收到 Redis 的 Pub/Sub 消息。
  2. Redisson 内部通过 LockSupport.unpark() 唤醒挂起的线程(B 和 C)。
  3. 被唤醒的线程会重新尝试抢锁
    • 如果抢到锁,返回 true
    • 如果锁已被其他线程抢走,再次挂起等待。
(5)超时处理

如果在 waitTime(如 100 秒)内未抢到锁:

  1. Redisson 会取消订阅 Redis 频道。
  2. 唤醒线程并返回 false

3. 关键设计亮点

  1. 无轮询
    依赖 Redis Pub/Sub 的事件驱动机制,避免线程轮询(减少 Redis 和 CPU 压力)。

  2. 线程挂起而非忙等待
    通过 LockSupport.park() 挂起线程,不消耗 CPU,唤醒由事件触发。

  3. 避免无效竞争
    只有收到锁释放通知后,线程才会重新抢锁,减少无效的 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 秒)。以下是详细分析:


关键结论

  1. C 的等待时间是基于初始调用的总超时(100 秒)计算的,不会因中间被唤醒而重置。
  2. 当 B 抢到锁后,C 会继续等待,剩余时间 = 初始 100 秒 - 已等待时间(20 秒) = 80 秒
  3. 如果 C 在接下来的 80 秒内仍未抢到锁,最终会超时返回 false

完整流程分析

时间线梳理:
  1. T=0s

    • A、B、C 同时调用 lock.tryLock(100, 30, SECONDS)
    • A 抢到锁,B 和 C 订阅 Redis 频道并挂起等待,开始 100 秒倒计时
  2. T=20s

    • A 释放锁,触发 Redis Pub/Sub 通知。
    • B 和 C 被唤醒,立即尝试抢锁
      • B 抢到锁,持有 30 秒(新的租约)。
      • C 抢锁失败,重新挂起等待,但剩余等待时间 = 100s - 20s = 80s
  3. T=100s

    • 如果 B 在 30 秒内(即 T=50s 前)释放锁,C 可能有机会抢到锁。
    • 如果 B 持有锁直到 T=50s(或 C 一直未抢到锁),C 会在 T=100s 时超时,返回 false

技术原理

  1. Redisson 的等待时间计算

    • 在第一次调用 tryLock 时,Redisson 会记录调用的系统时间戳(如 startTime=System.currentTimeMillis())。
    • 每次唤醒后,剩余时间通过公式计算:
      long remainingTime = startTime + waitTime - System.currentTimeMillis();
      
    • 不会因中间唤醒而重置,始终基于初始调用的总超时。
  2. C 线程的行为

    • 每次被唤醒后,C 会检查剩余时间:
      • 如果剩余时间 <= 0,直接返回 false
      • 如果剩余时间 > 0,再次挂起等待(剩余时间)。

验证方法

可以通过日志打印时间验证:

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) 的流程
  1. 通过 Lua 脚本在 Redis 中创建 Hash 键:
    -- 参数:KEY, leaseTime, 线程ID
    redis.call('hset', KEYS[1], ARGV[2], 1);
    redis.call('pexpire', KEYS[1], ARGV[1]);
    
  2. 如果锁被其他线程持有,订阅频道并挂起线程(类似 tryLock 的 Pub/Sub 机制)。
  3. 不启动看门狗续期,依赖 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 通过这种设计清晰区分了“阻塞无限等待”和“有限时间尝试”两种场景!

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐