1、Redisson简介
Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务。其中包括(BitSet, Set, Multimap, SortedSet, Map, List, Queue, BlockingQueue, Deque, BlockingDeque, Semaphore, Lock, AtomicLong, CountDownLatch, Publish / Subscribe, Bloom filter, Remote service, Spring cache, Executor service, Live Object service, Scheduler service) Redisson提供了使用Redis的最简单和最便捷的方法。Redisson的宗旨是促进使用者对Redis的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。
关于Redisson项目的详细介绍可以在官方网站找到。
2、Redisson的RLock分布式锁
在《基于Redis实现的分布式锁》中,我们通过Redis的setNX+Lua方法实现了分布式锁,而Redisson是一个Redis客户端,是 Redis 官网推荐的 java 语言实现分布式锁的项目。
2.1、RLock层级结构
在Redisson中,Redisson 分布式锁的实现是基于 RLock 接口,而 RLock 锁接口实现源码主要是 RedissonLock 这个类,而源码中加锁、释放锁等操作都是使用 Lua 脚本来完成的,并且封装的非常完善,开箱即用。
- RedissonWriteLock 写锁
- RedissonReadLock 读锁
- RedissonTransactionalLock 事务锁
- RedissonFairLock 公平锁
- RedissonRedLock 红锁
2.2、RLock 常用接口
public interface RLock extends Lock, RLockAsync {
//获取名称
String getName();
/**
* 中断锁 表示该锁可以被中断 假如A和B同时调这个方法,A获取锁,B为获取锁,那么B线程可以通过
* Thread.currentThread().interrupt(); 方法真正中断该线程
*/
void lockInterruptibly(long leaseTime, TimeUnit unit) throws InterruptedException;
/**
* tryLock(long waitTime, long leaseTime, TimeUnit unit)方法和tryLock()方法(在Lock接口中定义)是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,且可以自己设置锁失效时间,后者默认30s。
* 在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。
*
* @param waitTime 获取锁的最大的等待时间
* @param leaseTime 锁失效时间
* @param unit 时间单位 小时、分、秒、毫秒等
*/
boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException;
/**
* 加锁 上面是默认30秒这里可以手动设置锁的有效时间
*
* @param leaseTime 锁有效时间
* @param unit 时间单位 小时、分、秒、毫秒等
*/
void lock(long leaseTime, TimeUnit unit);
//强制解锁
boolean forceUnlock();
/**
* 检验该锁是否被线程使用,如果被使用返回True
*/
boolean isLocked();
/**
* 检查指定线程是否获得此锁
*/
boolean isHeldByThread(long threadId);
/**
* 检查当前线程是否获得此锁
*/
boolean isHeldByCurrentThread();
//当前线程对该锁的持有数
int getHoldCount();
//锁的剩余有效时间
long remainTimeToLive();
}
3、基于Redisson分布式锁的实践
这里我们基于上一篇《基于Redis实现的分布式锁》中的项目进行改造。
3.1、修改pom文件
去掉原来的redis依赖(下面依赖已经包含了),直接引入如下依赖即可。
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.14.0</version>
</dependency>
3.2、修改测试类DemoController
&esmp;我们这里使用单Redis节点的模式,在原来的基础上只需要引入redisson依赖,然后修改DemoController即可,首先通过@Autowired注解注入RedissonClient对象,然后通过该对象获取锁,再使用即可。
@RestController
public class DemoController {
private Logger logger = LoggerFactory.getLogger(DemoController.class);
@Autowired
private RedissonClient redissonClient;
@RequestMapping("redissonLock")
public String testLock() {
logger.debug("进入testLock()方法;");
//“order"表示业务模块
RLock rLock = redissonClient.getLock("order");
try{
if(rLock.tryLock(1,25, TimeUnit.SECONDS)){
logger.debug("获取到锁了;");
Thread.sleep(20 * 1000);
}else{
logger.debug("获取锁失败了;");
}
}catch (Exception e){
e.printStackTrace();
}
logger.debug("方法执行完成;");
return "方法执行完成";
}
}
3.3、总结
该分布式锁的测试方法和前面类似,这里不再重复。通过前面的简单使用,我们可以知道,其实就是使用了Redisson框架的分布式锁,不需要我们再自己定义锁了,方便了很多,同时很多易错的细节已经被框架考虑了。
4、看门狗Watchdog
前面基于Redis的分布式锁实现和前面基于Redssion的简单使用,都存在一个问题:当锁的失效时间到了,但是业务逻辑还没有执行完成就会出现问题,即因为锁失效,其他线程就可以继续使用该分布式锁了。针对这类问题,在Redssion框架中,通过看门狗机制进行处理。
4.1、实现原理
为了避免锁超期失效这种情况的发生,Redisson内部提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期。默认情况下,看门狗的检查锁的超时时间是30秒钟,也可以通过修改Config.lockWatchdogTimeout来另行指定。
4.2、测试
使用不带参数的rLock.tryLock()方法时,就会自动的启用看门狗,如下所示,为了演示效果,我这里把休眠时间调成了60s(而锁的失效时间是30s)。
如果使用带参数的rLock.tryLock()方法,需要保证看门狗配置的参数lockWatchdogTimeout < leaseTime(锁失效时间),否则看门狗将失效。
@RequestMapping("redissonLock")
public String testLock() {
logger.debug("进入testLock()方法;");
//“order"表示业务模块
RLock rLock = redissonClient.getLock("order");
try{
if(rLock.tryLock()){
logger.debug("获取到锁了;");
Thread.sleep(60 * 1000);
}else{
logger.debug("获取锁失败了;");
}
}catch (Exception e){
e.printStackTrace();
}
logger.debug("方法执行完成;");
return "方法执行完成";
}
&esmp;重新启动项目后(8080、8081两个端口),我们先访问http://localhost:8080/redissonLock,这个时候该线程会获取到锁,等待35s后,访问http://localhost:8081/redissonLock接口,正常已经过了锁失效时间,这个时候线程二应该可以获取到锁的,但时间情况是获取失败。打印日志如下:
5、红锁(Redlock)
在上述单节点情况下,很难保证高可用性的。如果在主从模式、哨兵模式、集群模式模式下,又会出现异步数据丢失和脑裂等问题,那如何实现分布式锁的高可用性并且避免这些问题呢?
为了解决这些问题,在Redis官网提出了红锁(Redlock)的概念:假设有5个redis节点,这些节点之间既没有主从,也没有集群关系。客户端用相同的key和随机值在5个节点上请求锁,请求锁的超时时间应小于锁自动释放时间。当在3个(超过半数)redis上请求到锁的时候,才算是真正获取到了锁。如果没有获取到锁,则把部分已锁的redis释放掉。
在实际场景中,红锁是很少使用的。因为使用了红锁后会影响高并发环境下的性能,使得程序的体验更差。而且,在实际场景中,我们一般都是要保证Redis集群的可靠性。同时,使用红锁后,当加锁成功的RLock个数不超过总数的一半时,会返回加锁失败,即使在业务层面任务加锁成功了,但是红锁也会返回加锁失败的结果。另外,使用红锁时,需要提供多套Redis的主从部署架构,同时,这多套Redis主从架构中的Master节点必须都是独立的,相互之间没有任何数据交互。
使用Redisson框架来实现红锁的用法如下:
public void testRedLock(RedissonClient redisson1,RedissonClient redisson2, RedissonClient redisson3){
RLock lock1 = redisson1.getLock("lock1");
RLock lock2 = redisson2.getLock("lock2");
RLock lock3 = redisson3.getLock("lock3");
RedissonRedLock lock = new RedissonRedLock(lock1, lock2, lock3);
try {
// 同时加锁:lock1 lock2 lock3, 红锁在大部分节点上加锁成功就算成功。
lock.lock();
// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}