一:为什么需要分布式锁
一般说到锁,我们会想到java的synchronized与Reentrantlock。Java 的 synchronized或者Reentrantlock 锁只能保证在同一个 JVM 进程内的多线程并发安全性。但是在多线程并发的情况下,并不能保证线程安全。因为现在的业务大多都是集群部署,也就是多个 JVM 实例,所以 Java 内置的锁无法保证集群环境的多线程安全性。所以如果业务是集群部署,那就需要分布式锁来保证整合集群的线程安全。
二? 分布式锁的特性
1:高性能
2:互斥性
3:可重入
4:防止死锁
三???分布式锁常用的几种实现方式
1:数据库(Mysql)
2:? redis
3:??ZooKeeper
四??Redisson-lock()与unLock()
首先Redis作为分布式锁有几个很重要的关键点,就是要满足原子性,正确的释放锁,过期时间以及锁续期。以下我们分析一下
1:引入依赖
<dependency>
? ? ? ? <groupId>org.redisson</groupId>
? ? ? ? <artifactId>redisson</artifactId>
? ? ? ? <version>3.11.1</version>
? ? </dependency>
2:查看一下lock方法
我们通过tryAcquire方法一层层进去看到最终是通过lua脚本来保证多个命令的原子性。
核心流程如下:
1: 请求进来判断当前执行的方法有没有上锁,如果没有上锁则进行上锁,上锁成功后返回nil。
2: 如果当前执行的方法已经有锁了,那就看持有锁的线程是不是自己,是自己就代表重入,直接给锁状态hincrby + 1,然后返回nil代表成功。
3: 如果当前执行的方法已经有锁但持有锁的线程不是当前请求的线程,那则代表竞争抢锁了,需要互斥,就pttl命令返回当前锁的剩余时间,然后外层进行等待重试。
Tips:ARGV[2]是一个由uuid:threadId组成的一个唯一的数值,在集群部署的情况下,threadId在不同机器上是可以重复的,所以用到uuid.
释放锁主要是unlock(),主要逻辑在unlockInnerAsync方法,主参数是根据线程id.因为有锁重入的情况,所以需要判断是否有锁重入,如果有锁重入那就先把锁重入次数减1,当没有锁重入的时候直接删除锁即可,核心源码如下:
如果当前线程没有持有锁,我们看源码可以看到返回 nil,因为无锁就不需要释放所以是直接返回。
但是如果当前线程持有锁,则执行hincrby, myLock, uuid:threadId, -1,这里为啥减1就是因为会有锁重入的情况。
减完1后需要判断值是否还大于0,如果大于0则代表是有锁重入的,还没释放完就不能删除掉,需要重新给锁续期。
如果减完1后值小于0,那就代表释放完了,没有锁重入的情况了,所以就可以直接删除掉,结果返回1就代表解锁成功。
五:锁续期
背景
如果有一个线程A持有锁,但是业务还没有执行结束,这个时候锁过期了,如果在这时其他线程可以抢锁,那么就会造成线程A与别的线程同时执行了上锁的业务逻辑,这样会造成了线程不安全。
解决办法:锁续期
锁续期就是专门用来解上述问题的,Redisson有个watchDog机制,我们现在看一下源码分析一下watchDog是如何解决的以及他心原理是什么。
watchDog是怎么生效的?
我们从加锁逻辑一层层找看哪里做了锁续期的操作,我们看到了tryLockInnerAsync方法里有个watchDog字段信息。
RFuture ttlRemainingFuture = tryLockInnerAsync(commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
我们可以发现每次加锁都会判断是否开启watchDog,只有leaseTime == -1的时候才会走开启看门狗的逻辑。什么时候等于-1呢,leaseTime是什么?
根据LongtryAcquire方法向上层代码逻辑看一下,发现leaseTime是lock()入口带来的。用户加锁是可以定义时间的,这个时间就是锁过期时间。也就是传入锁过期时间就会开启看门狗。
private LongtryAcquire(long leaseTime, TimeUnit unit, long threadId) {
return get(tryAcquireAsync(leaseTime, unit, threadId));
}
scheduleExpirationRenewal方法就是锁需求的实现方法
1:锁续期的核心方法
2:续期成功调用自己
3:续期的起始时间是超过过期时间的三分之一,比如你设置过期时间30秒,过了10秒你还没执行完就会续期。