Redissson分布式锁
1、引入依赖
<dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>3.13.6</version>
</dependency>
2、配置Redisson客户端
/*** Redisson配置类,用于配置Redisson客户端。*/
@Configuration
public class RedissonConfig {/*** 创建并配置RedissonClient Bean。* * @return 配置好的RedissonClient实例*/@Beanpublic RedissonClient redissonClient() {// 创建Redisson配置对象Config config = new Config();// 配置单节点模式config.useSingleServer()// 设置Redis服务器地址.setAddress("redis://192.168.100.128:6379")// 设置Redis服务器密码.setPassword("1234")// 设置连接池大小.setConnectionPoolSize(64)// 设置最小空闲连接数.setConnectionMinimumIdleSize(24)// 设置空闲连接超时时间(毫秒).setIdleConnectionTimeout(10000)// 设置连接超时时间(毫秒).setConnectTimeout(10000)// 设置命令等待超时时间(毫秒).setTimeout(3000)// 设置命令重试次数.setRetryAttempts(3)// 设置命令重试间隔时间(毫秒).setRetryInterval(1500);// 创建并返回RedissonClient实例return Redisson.create(config);}
}
3、注入并使用
(1)、可重入锁
概念:可重入锁(Reentrant Lock)是一种允许同一个线程多次获取同一把锁的锁机制。也就是说,当一个线程已经持有某个锁时,它可以再次获取该锁而不会被阻塞。这种锁机制能够避免死锁问题,并简化锁的使用。
可重入锁的主要特点是:
同一线程可多次获取:同一个线程可以多次获取同一把锁,而不会被阻塞。
计数器维护:可重入锁内部维护一个计数器,每次获取锁时计数器加1,每次释放锁时计数器减1,当计数器为0时,锁才真正被释放。
原理:可重入锁的实现通常依赖于一个计数器和一个持有锁的线程标识。当一个线程第一次获取锁时,计数器加1,并记录持有锁的线程标识。当同一个线程再次获取锁时,只需将计数器加1,而不会阻塞线程。当线程释放锁时,计数器减1,当计数器为0时,锁才真正被释放,并允许其他线程获取锁。
在Redisson中,可重入锁的实现基于Redis的原子操作和Lua脚本。Redisson通过维护一个计数器和持有锁的线程标识,实现了可重入锁的功能。
简单使用:
@Autowiredprivate RedissonClient redissonClient;@Testpublic void performTaskWithLock() {RLock lock = redissonClient.getLock("myLock");try {boolean isLock = lock.tryLock(1, 10, TimeUnit.SECONDS);if (isLock) {try {System.out.println("外层获取锁,开始执行");// 同一线程再次获取锁(重入)innerMethod(lock);} finally {lock.unlock(); // 释放外层锁}}} catch (InterruptedException e) {throw new RuntimeException(e);}}// 同一线程内的方法private void innerMethod(RLock lock) throws InterruptedException {// 同一线程再次获取同一把锁boolean isInnerLock = lock.tryLock(1, 10, TimeUnit.SECONDS);if (isInnerLock) {try {System.out.println("内层成功获取同一把锁,可重入性验证通过");} finally {lock.unlock(); // 释放内层锁}}}
在上述代码中,我们使用redissonClient.getLock("myLock")获取一个分布式锁对象,然后使用lock.tryLock()方法尝试获取锁,并在任务完成后释放锁。
锁的获取和释放
获取锁:使用RLock对象的tryLock()或lock()方法来获取锁。tryLock()方法允许设置等待时间和锁的自动释放时间。
释放锁:使用RLock对象的unlock()方法来释放锁。确保在finally块中释放锁,以避免死锁。
(2)、公平锁
概念:公平锁(Fair Lock)是一种确保锁的获取顺序与请求顺序相同的锁机制。即先请求锁的线程优先获取锁,后请求的线程只能在前面的线程释放锁后才能获取锁。这种机制可以避免“饥饿”现象,确保每个线程都能公平地获取锁。
公平锁的实现通常依赖于一个队列来记录请求锁的顺序。每次有线程请求锁时,会将其添加到队列中,当锁被释放时,从队列中按照请求顺序依次唤醒等待的线程。
原理:在Redisson中,公平锁的实现基于Redis的有序集合(Sorted Set)和Lua脚本。每次请求锁时,线程会被添加到一个有序集合中,并按照时间戳排序。当锁被释放时,按照有序集合中的顺序依次唤醒等待的线程。
简单使用:
public void performTaskWithFairLock() {// 1. 获取公平锁对象RLock fairLock = redissonClient.getFairLock("myFairLock");try {// 2. 尝试获取锁boolean isLock = fairLock.tryLock(1, 10, TimeUnit.SECONDS); // 3. 判断是否获取到锁if (isLock) { try {System.out.println("获得公平锁,正在执行任务...");// 执行任务} finally {// 4. 释放锁fairLock.unlock(); System.out.println("释放公平锁。");}} else {System.out.println("无法获取公平锁。");}} catch (InterruptedException e) {e.printStackTrace();}
}
(3)、读写锁
概念:读写锁(Read-Write Lock)是一种允许多个读操作同时进行,但写操作必须独占的锁机制。读写锁分为两种锁:读锁和写锁。
读锁:允许多个线程同时获取读锁,只要没有线程持有写锁。读锁之间是共享的。
写锁:只允许一个线程获取写锁,并且在写锁持有期间,其他线程不能获取读锁或写锁。写锁是独占的。
读写锁的主要目的是提高并发性和性能。在读多写少的场景下,读写锁可以显著提高系统的并发处理能力。
原理:读写锁的实现通常依赖于两个锁:一个读锁和一个写锁。读锁允许多个线程同时获取,而写锁只允许一个线程获取。在获取写锁时,需要确保没有线程持有读锁或写锁。
在Redisson中,读写锁的实现基于Redis的原子操作和Lua脚本。Redisson通过两个键来分别控制读锁和写锁,并使用Lua脚本确保锁操作的原子性。
简单实用:
public void performTaskWithReadWriteLock() {// 1. 获取读写锁对象RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("myReadWriteLock");// 2. 从读写锁对象中分别获取读锁和写锁RLock readLock = readWriteLock.readLock();RLock writeLock = readWriteLock.writeLock();try {// 3. 尝试获取读锁if (readLock.tryLock(10, 60, TimeUnit.SECONDS)) { try {System.out.println("获取读锁,正在执行读任务...");// 执行读任务} finally {// 4. 释放读锁readLock.unlock(); System.out.println("释放读锁。");}}// 5. 尝试获取写锁if (writeLock.tryLock(10, 60, TimeUnit.SECONDS)) { try {System.out.println("获取写锁,正在执行写任务...");// 执行写任务} finally {// 6. 释放写锁writeLock.unlock();System.out.println("释放写锁。");}}} catch (InterruptedException e) {e.printStackTrace();}
}
(4)、联锁
概念:联锁(MultiLock)是一种允许将多个锁关联在一起,实现“全部获取”或“全部释放”的锁机制。
全部获取: 只有当所有参与联锁的锁都被成功获取后,才算成功获取联锁。
全部释放: 释放联锁时,会自动释放所有参与联锁的锁。
联锁适用于需要同时获取多个资源的场景,例如分布式事务中需要锁定多个数据表。
原理:Redisson 的联锁基于 RedissonMultiLock 对象实现。RedissonMultiLock 对象可以将多个 RLock 对象关联在一起,并提供 tryLock() 和 unlock() 方法来统一管理这些锁。
在调用 tryLock() 方法时,RedissonMultiLock 会尝试依次获取所有参与联锁的锁。如果所有锁都获取成功,则返回 true,否则释放已经获取到的锁,并返回 false。
在调用 unlock() 方法时,RedissonMultiLock 会自动释放所有参与联锁的锁,无论这些锁是否被当前线程持有。
简单食用:
public void performTaskWithMultiLock() {// 获取多个锁对象RLock lock1 = redissonClient.getLock("lock1");RLock lock2 = redissonClient.getLock("lock2");RLock lock3 = redissonClient.getLock("lock3");// 创建联锁对象RLock multiLock = redissonClient.getMultiLock(lock1, lock2, lock3);try {// 尝试获取联锁,等待 10 秒if (multiLock.tryLock(10, TimeUnit.SECONDS)) {try {System.out.println("获取联锁成功,正在执行任务...");// 执行需要所有锁的任务} finally {// 释放联锁multiLock.unlock();System.out.println("释放联锁。");}} else {System.out.println("获取联锁失败。");}} catch (InterruptedException e) {e.printStackTrace();}
}
代码分析:
获取多个锁对象: 首先,获取需要参与联锁的多个 RLock 对象。
创建联锁对象: 使用 redissonClient.getMultiLock(lock1, lock2, lock3) 创建一个 RLock 对象,并将之前获取的多个锁对象作为参数传入。
尝试获取联锁: 调用 multiLock.tryLock(10, TimeUnit.SECONDS) 尝试获取联锁,最多等待 10 秒。
执行任务: 如果成功获取联锁,则执行需要所有锁保护的任务。
释放联锁: 最后,在 finally 块中调用 multiLock.unlock() 释放联锁,这会自动释放所有参与联锁的锁。
4、WatchDog机制
作用:Redisson 的看门狗机制就是为了解决上述“锁提前过期,业务还未执行完”的问题,它能自动延长锁的过期时间,保证只要持有锁的客户端业务还在执行(未主动释放锁 ),锁就不会因为过期而被释放,从而维持锁的有效性和互斥性。
原理与流程:获取锁时的初始化
当使用 Redisson 的 RLock(如 getLock 方法 )获取分布式锁时,如果未手动指定锁的过期时间(即使用默认的 -1 ,表示锁的过期时间由看门狗机制管理 ),Redisson 会启动看门狗逻辑。
它会先尝试用 setIfAbsent 命令向 Redis 设置锁(带一个初始的过期时间,默认是 30 秒 ,可通过 config.setLockWatchdogTimeout(...) 调整 ),如果设置成功,说明获取锁成功。
后台定时任务续命
获取锁成功后,Redisson 会在客户端后台启动一个定时任务(可以理解为一个守护线程 ),这个定时任务会周期性地(默认每隔 10 秒 ,是初始过期时间的 1/3 ,即 30/3 = 10 秒 )检查当前客户端是否还持有锁(通过判断 Redis 中锁的 key 是否存在,且归属为当前客户端 )。
如果持有锁,就会向 Redis 发送一个 PEXPIRE 命令,将锁的过期时间重新设置为初始的过期时间(如 30 秒 ),实现“续命”。只要业务逻辑还在执行,这个定时任务就会不断给锁延长过期时间,保证锁不会因为超时被释放。
锁释放时的清理
当客户端主动调用 unlock 方法释放锁时,Redisson 会先校验锁的归属(确保是当前客户端持有的锁 ),然后删除 Redis 中的锁 key ,同时会停止后台的看门狗定时任务,避免不必要的续命操作。
如果客户端因故障崩溃,无法主动释放锁,那么看门狗的定时任务也会随着客户端进程的结束而终止,锁在初始过期时间(或最后一次续命后的过期时间 )到达后,会自动在 Redis 中过期释放,不会一直占用锁。
配置:
Config config = new Config();
config.setLockWatchdogTimeout(60000); // 设置初始过期时间为 60 秒
RedissonClient redisson = Redisson.create(config);
注意:如果手动指定了锁的过期时间(即调用 lock 方法时传入了过期时间参数,如 lock(10, TimeUnit.SECONDS) ),那么看门狗机制不会生效,锁会在指定的过期时间后自动释放,不会自动续命。这种场景下要确保业务逻辑执行时间不会超过指定的过期时间,否则仍会出现锁提前释放问题。
public void test() throws Exception {RLock lock = redissonClient.getLock("myLock");// 方式一: 不停重试,直到获取锁成功,具有 Watch Dog 自动延期机制,默认续约时间为 30 秒lock.lock(); // 方式二: 尝试获取锁 10 秒,获取成功返回 true,否则返回 false,具有 Watch Dog 自动延期机制,默认续约时间为 30 秒boolean res1 = lock.tryLock(10, TimeUnit.SECONDS); // 方式三: 尝试获取锁 10 秒,如果获取成功,则持有锁,否则抛出异常,leaseTime 为 10 秒,不会自动续约try {lock.lock(10, TimeUnit.SECONDS); } catch (InterruptedException e) {// 处理异常}// 方式四: 尝试获取锁 100 秒,如果获取成功,则持有锁 10 秒,leaseTime 为 10 秒,不会自动续约boolean res2 = lock.tryLock(100, 10, TimeUnit.SECONDS); Thread.sleep(40000L);lock.unlock();
}
总结:有些分布式锁实现可能需要开发者自己在业务代码中定时去延长锁的过期时间(比如启动一个单独的线程,定时检查业务是否还在执行,然后调用 Redis 命令延长锁 ),而 Redisson 的看门狗机制将这个过程封装在了客户端 SDK 内部,对开发者透明,使用起来更简单、便捷,减少了手动实现的复杂度和出错概率,是 Redisson 分布式锁的一个很实用的特性。
总之,Redisson 看门狗机制通过自动延长锁的过期时间,很好地解决了分布式锁因业务执行时长不确定而可能提前过期的问题,提升了分布式锁在实际应用中的可靠性和易用性 。