文章目录
前言
距离上次的分享已经有近一个月,期间不是没有遇到问题,本来想等项目学习完之后再统一的去回顾之前出现的种种问题并分享给大家,避免踩坑,但是当我突然发现我在学习的过程中让自己对锁有了一个比之前更清晰的认知,所以我将分别介绍一下synchronized和redisson的使用及一些常见的概念进行一个我自己的理解,如果文章中出现错误或者有歧义的地方希望大家多多指正,不胜感激!!!废话不多说我们直接进入主题......
一、使用synchronized加锁
1.首先展示一下没有加锁的业务
1).编写一段伪代码(未加锁):
代码如下(示例):
@ResponseBody
@GetMapping("/testLocalLock")
public String testLocalLock() throws InterruptedException {
//synchronized (this){
System.out.println("进入方法"+Thread.currentThread().getId());
//模拟业务处理
Thread.sleep(1000);
System.out.println("---------------执行中....-----------------");
System.out.println("执行完毕"+Thread.currentThread().getId());
//}
return "";
}
2).我们用JMeter模拟十个线程同时请求这个接口看下效果
进入方法93
进入方法92
进入方法94
进入方法95
进入方法96
进入方法97
进入方法98
进入方法99
进入方法100
进入方法101
---------------执行中....-----------------
---------------执行中....-----------------
执行完毕93
执行完毕92
---------------执行中....-----------------
执行完毕94
---------------执行中....-----------------
执行完毕95
---------------执行中....-----------------
执行完毕96
---------------执行中....-----------------
执行完毕97
---------------执行中....-----------------
执行完毕98
---------------执行中....-----------------
执行完毕99
---------------执行中....-----------------
执行完毕100
---------------执行中....-----------------
执行完毕101
3).结果说明:
效果很明显,线程会同时进入到方法中去进行业务逻辑,想象一下如果这个的业务逻辑正好是需要读写数据库或者公共的数据的,那肯定是有数据安全性的危险的,所以这个时候我们就需要引synchronized来对不希望线程同时去操作的业务进行加锁
2.然后展示一下加锁之后的业务处理
1).编写一段伪代码(加锁)
代码如下(示例):
@ResponseBody
@GetMapping("/testLocalLock")
public String testLocalLock() throws InterruptedException {
synchronized (this){
System.out.println("进入方法"+Thread.currentThread().getId());
//模拟业务处理
Thread.sleep(1000);
System.out.println("---------------执行中....-----------------");
System.out.println("执行完毕"+Thread.currentThread().getId());
}
return "";
}
2).我们用JMeter模拟十个线程同时请求这个接口看下效果
进入方法92
---------------执行中....-----------------
执行完毕92
进入方法101
---------------执行中....-----------------
执行完毕101
进入方法100
---------------执行中....-----------------
执行完毕100
进入方法99
---------------执行中....-----------------
执行完毕99
进入方法98
---------------执行中....-----------------
执行完毕98
进入方法97
---------------执行中....-----------------
执行完毕97
进入方法96
---------------执行中....-----------------
执行完毕96
进入方法95
---------------执行中....-----------------
执行完毕95
进入方法94
---------------执行中....-----------------
执行完毕94
进入方法93
---------------执行中....-----------------
执行完毕93
3).结果说明
我们看到加完锁之后,每次只允许一个线程去执行业务逻辑,其他的线程需要等这个线程执行完之后再去执行,这样的话我们就解决了在单体服务下的并发可能带来的问题。
但是我们还要思考一个问题:synchronized (this)中的这个this是什么意思?它指的是当前服务中的当前代码段的这个线程。
那我们服务分布式部署在多台机器上的时候,我们这个this还能所住其他服务器的线程吗?答案是肯定不会呀!
所以分布式部署的情况下我们就需要用到分布式锁——redisson!!!
看会儿蓝天白云放松一下~~
二、分布式锁——redisson
1.简单介绍一下redisson的使用和原理
1)引入依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
version>3.12.0</version>
</dependency>
2)将RedissonClient注入,并编写上锁代码
代码如下(示例):
@Autowired
private RedissonClient redissonClient;
@ResponseBody
@GetMapping("/hello")
public String hello(){
//1.获取一把锁,只要锁的名字一样,就是同一把锁
RLock lock = redissonClient.getLock("my-lock");
//2.加锁
lock.lock();//阻塞式等待。默认加的锁都是30s
//lock.lock(10, TimeUnit.SECONDS);省掉了整个续期操作,手动解锁
try {
System.out.println("加锁成功,执行业务"+ Thread.currentThread().getId());
Thread.sleep(30000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("释放锁..."+ Thread.currentThread().getId());
lock.unlock();
}
return "hello";
}
3)测试结果
测试思路:本人使用的网关,将JMeter发送的请求发到网关上,然后将服务在不端口下启动,模拟部署在多台服务器上,当有一个正在执行的时候,其他服务只能等待该服务释放锁之后才可以执行,锁的相关数据可以到redis中去查看。
4)redisson的相关原理介绍
1.获取一把锁,只要锁的名字一样,就是同一把锁;
2.当我们加锁的时候未指定锁的过期时间,但是如果我们释放锁失败会导致死锁问题吗?答案是不会
首先我们看一下lock.lock();这个方法
我们看到redisson的lock方法是实现了JUC包下Lock接口,进入到实现类;
我们看到当没有指定过期时间的时候,系统默认的将-1做为参数传到了this.lock();这个方法中
this.lock()又去调用了tryAcquire();方法,并把-1作为过期时间传到这个方法中;
tryAcquire();又继续调用了tryAcquireAsync();方法,并把-1作为过期时间传到这个方法中;
在这个tryAcquireAsync();中对这个过期时间进行了判断,如果传值了就用我们传进来的,如果没传的话,就将this.commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout()这个作为过期时间,这个是什么呢?让我们继续往下看;
这就是我们经常提到的redisson的看门狗,我们发现在他的配置中看门狗的默认值是30S,也就是说我们如果新建锁的时候没有指定锁的过期时间,默认的过期时间就是看门狗的过期时间,也就是30S!
那么问题多的小明又要问了,如果说这个锁中的业务处理超过30S了,那么不就有问题了吗?锁不久失效了吗?(如果业务处理超过30S的话可能要考虑的就不是锁的问题了。。。)这个我们还要看一下看门狗的机制:
我们看再通过看门狗获取完ttlRemainingFuture之后,去判断是否有异常,如果没有的话我们会执行scheduleExpirationRenewal();这个方法;
在这个定时方法中我们去判断所是否还在继续使用;如果还在使用的话我们调用renewExpiration();
在renewExpiration();这个方法中我们通过newTimeout();这个定时任务去重新规定了锁的过期时间;
这个定时任务的周期就是this.internalLockLeaseTime / 3L,那么这个internalLockLeaseTime 是多少呢?
最后我们发现定时的时间就是三分之一的看门狗的时间!也就是说,当我们创建锁的时候未指定锁的过期时间,那我们会默认的给这个锁一个30S的过期时间,当这个锁在占用且没有发生异常的时候,我们会每过10S中将锁的过期时间重新规定回30S。
2.简单介绍一下redisson不同类型锁的使用
1)读写锁
代码如下(示例):
@ResponseBody
@GetMapping("/write")
public String writeValue(){
RReadWriteLock lock = redissonClient.getReadWriteLock("rw-lock");
String s = null;
RLock rLock = lock.writeLock();
try {
//1.改数据加写锁
rLock.lock();
System.out.println("写锁枷锁成功。。。"+Thread.currentThread().getId());
s = UUID.randomUUID().toString();
Thread.sleep(30000);
redisTemplate.opsForValue().set("writeValue",s);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
rLock.unlock();
}
return s;
}
@ResponseBody
@GetMapping("/read")
public String readValue(){
RReadWriteLock lock = redissonClient.getReadWriteLock("rw-lock");
String s = null;
//加读锁
RLock rLock = lock.readLock();
rLock.lock();
System.out.println("读锁枷锁成功。。。"+Thread.currentThread().getId());
try {
s = redisTemplate.opsForValue().get("writeValue");
Thread.sleep(30000);
} catch (Exception e) {
e.printStackTrace();
}finally {
rLock.unlock();
}
return s;
}
读写锁的特点:
· 保证一定能读到最新数据,修改期间,写锁是一个排他锁(互斥锁),读锁是一个共享锁
· 写锁没释放,读就必须等待
· 读 + 读 相当于无锁,并发读只会在redis中记录好,所有当前的读锁,他们都会同时加锁成功
· 写 + 读 等待写锁释放
· 写 + 写 阻塞模式
· 读 + 写 有读锁,写也需要等待
· 只要有写的存在,都必须等待
2)信号量
代码如下(示例):
@GetMapping("/park")
@ResponseBody
public String park() throws InterruptedException {
RSemaphore park = redissonClient.getSemaphore("park");
park.acquire();//获取一个信号,获取一个值,占一个位置
boolean b = park.tryAcquire();
if (b){
//执行业务
}else {
return "error";
}
return "ok";
}
@GetMapping("/go")
@ResponseBody
public String go() throws InterruptedException {
RSemaphore park = redissonClient.getSemaphore("park");
park.release();//释放一个信号,释放一个位置
return "ok";
}
当我们创建一个信号量的时候,当我们执行park的时候,信号量会加1,当执行go的时候,信号量会减一,当信号量为0的时候,我们在执行go的时候就需要去等待park方法执行信号量加1才能继续执行。
基于信号量的这个特点我们可以用作分布式限流。
3)闭锁
代码如下(示例):
@ResponseBody
@GetMapping("/lockDoor")
public String lockDoor() throws InterruptedException {
RCountDownLatch door = redissonClient.getCountDownLatch("door");
door.trySetCount(5);
door.await();//等待闭锁都完成
return "放假了";
}
@ResponseBody
@GetMapping("/gogogo/{id}")
public String gogogo(@PathVariable Long id){
RCountDownLatch door = redissonClient.getCountDownLatch("door");
door.countDown();//计数减一
return id+"班的人都走了...";
}
闭锁为了方便理解我们可以举个例子,小明的学校要放假锁门了,学校一共有三个班级,只有当这三个班级都走了,才能锁门,所以执行lockDoor的时候,发现闭锁中还有三个班没有走完,每当gogogo执行一次,代表有一个班级走了,当三个班级都走完的时候我们的闭锁中值为0,lockDoor开始执行。
总结
以上就是我对synchronized和redisson的一些理解和使用,感兴趣的朋友可以自己动手利用代码实现以下,自己看看效果对于加深理解和记忆更有帮助哦,总结不易,如果感觉对你有帮助的话,请给我一个一键三连,我还会继续分享我在工作学习过程中的有意思的、有意义的相关知识,下期再见!