redis分布式锁之可重入锁的实现代码
上篇,有一个问题,它不可重入。
所谓不可重入锁,即若当前线程执行某个方法已经获取了该锁,那么在方法中尝试再次获取锁时,就会获取不到被阻塞。 同一个人拿一个锁 ,只能拿一次不能同时拿2次。
1、什么是可重入锁?它有什么作用?
可重入锁,也叫做递归锁,指的是在同一线程内,外层函数获得锁之后,内层递归函数仍然可以获取到该锁。 说白了就是同一个线程再次进入同样代码时,可以再次拿到该锁。 它的作用是:防止在同一线程中多次获取锁而导致死锁发生。
2、那么java中谁实现了可重入锁了?
在java的编程中synchronized 和 reentrantlock都是可重入锁。我们可以参考reentrantlock的代码
3、基于reentrantlock的可重入锁
reentrantlock,是一个可重入且独占式的锁,是一种递归无阻塞的同步锁。
3.1、看个reentrantlock的例子
3.2、执行结果
16:35:58.051 [main] info com.test.reentrantlockdemo - --------lock()执行后,getstate()的值:1 lock.islocked():true
16:35:58.055 [main] info com.test.reentrantlockdemo - --------递归1次--------
16:35:58.055 [main] info com.test.reentrantlockdemo - --------lock()执行后,getstate()的值:2 lock.islocked():true
16:35:58.055 [main] info com.test.reentrantlockdemo - --------递归2次--------
16:35:58.055 [main] info com.test.reentrantlockdemo - --------lock()执行后,getstate()的值:3 lock.islocked():true
16:35:58.055 [main] info com.test.reentrantlockdemo - --------递归3次--------
16:35:58.055 [main] info com.test.reentrantlockdemo - --------unlock()执行后,getstate()的值:2 lock.islocked():true
16:35:58.055 [main] info com.test.reentrantlockdemo - --------unlock()执行后,getstate()的值:1 lock.islocked():true
16:35:58.055 [main] info com.test.reentrantlockdemo - --------unlock()执行后,getstate()的值:0 lock.islocked():false
16:35:58.055 [main] info com.test.reentrantlockdemo - 执行完dosomething方法 是否还持有锁:false
3.3、 从上面栗子可以看出reentrantlock是可重入锁,那么他是如何实现的了,我们看下源码就知道了
reentrantlock的加锁流程是:
1,先判断是否有线程持有锁,没有加锁进行加锁
2、如果加锁成功,则设置持有锁的线程是当前线程
3、如果有线程持有了锁,则再去判断,是否是当前线程持有了锁
4、如果是当前线程持有锁,则加锁数量(state)+1
看reentrantlock的解锁代码我们知道,每次释放锁的时候都对state减1,
当c值等于0的时候,说明锁重入次数也为0了,
最终设置当前持有锁的线程为null,state也设置为0,锁就释放了。
4、那么redis要怎么实现可重入的操作了?
看reentrantlock的源码我们知道,它是加锁成功了,记录了当前持有锁的线程,并通过一个int类型的数字,来记录了加锁次数。
我们知道reentrantlock的实现原理了,那么redis只要下面两个问题解决,就能实现重入锁了:
1、怎么保存当前持有的线程
2、加锁次数(重入了多少次),怎么记录维护
4.1、第一个问题:怎么保存当前持有的线程
1.上一篇文章我们用的是redis 的set命令存的是string类型,他能保存当前持有的线程吗?
valus值我们可以保存当前线程的id来解决。
2. 但是集群环境下我们线程id可能是重复了那怎么解决?
项目在启动的生成一个全局进程id,使用进程id+线程id 那就是唯一的了
4.2、第二个问题:加锁次数(重入了多少次),怎么记录维护
他能记录下来加锁次数吗?
如果valus值存的格式是:系进程id+线程id+加锁次数,那可以实现
存没问题了,但是重入次数要怎么维护了, 它肯定要保证原子性的,能解决吗?
好像用java代码或者lua脚本都没法解决,因为都是实现都需要两步来维护这个重入次数的
- 第一步:先获取到valus值,把取到加锁次数+1
- 第二部:把新的值再设置进去
- 在执行第二步操作之前,如果这个key失效了(设置持有锁超时了),如果还能再设置进去,就会有并发问题了
5、我们已经知道set是不支持重入锁的,但我们需要重入锁,怎么办呢?
目前对于redis的重入锁业界还是有很多解决方案的,最流行的就是采用redisson。
6、什么是 redisson?
redisson是redis官方推荐的java版的redis客户端。 它基于java实用工具包中常用接口,为使用者提供了一系列具有分布式特性的常用工具类。 它在网络通信上是基于nio的netty框架,保证网络通信的高性能。 在分布式锁的功能上,它提供了一系列的分布式锁;如:可重入锁(reentrant lock)、公平锁(fair lock、联锁(multilock)、 红锁(redlock)、 读写锁(readwritelock)等等。
7、redisson的分布锁如何使用
引入依赖包
代码
执行结果
2021-05-23 22:49:01.322 info 69041 --- [nio-9090-exec-1] org.redisson.version : redisson 3.15.5
2021-05-23 22:49:01.363 info 69041 --- [sson-netty-5-22] o.r.c.pool.masterconnectionpool : 10 connections initialized for /127.0.0.1:6379
2021-05-23 22:49:01.363 info 69041 --- [sson-netty-5-23] o.r.c.pool.masterpubsubconnectionpool : 1 connections initialized for /127.0.0.1:6379
2021-05-23 22:49:01.367 info 69041 --- [nio-9090-exec-1] com.test.reentrantlockdemo1 : --------------start---------------
2021-05-23 22:49:01.435 info 69041 --- [nio-9090-exec-1] com.test.reentrantlockdemo1 : --------lock()执行后,getstate()的值:1 lock.islocked():true
2021-05-23 22:49:01.436 info 69041 --- [nio-9090-exec-1] com.test.reentrantlockdemo1 : --------递归1次--------
2021-05-23 22:49:01.442 info 69041 --- [nio-9090-exec-1] com.test.reentrantlockdemo1 : --------lock()执行后,getstate()的值:2 lock.islocked():true
2021-05-23 22:49:01.442 info 69041 --- [nio-9090-exec-1] com.test.reentrantlockdemo1 : --------递归2次--------
2021-05-23 22:49:01.448 info 69041 --- [nio-9090-exec-1] com.test.reentrantlockdemo1 : --------lock()执行后,getstate()的值:3 lock.islocked():true
2021-05-23 22:49:01.448 info 69041 --- [nio-9090-exec-1] com.test.reentrantlockdemo1 : --------递归3次--------
2021-05-23 22:49:01.456 info 69041 --- [nio-9090-exec-1] com.test.reentrantlockdemo1 : --------unlock()执行后,getstate()的值:2 lock.islocked():true
2021-05-23 22:49:01.461 info 69041 --- [nio-9090-exec-1] com.test.reentrantlockdemo1 : --------unlock()执行后,getstate()的值:1 lock.islocked():true
2021-05-23 22:49:01.465 info 69041 --- [nio-9090-exec-1] com.test.reentrantlockdemo1 : --------unlock()执行后,getstate()的值:0 lock.islocked():false
2021-05-23 22:49:01.467 info 69041 --- [nio-9090-exec-1] com.test.reentrantlockdemo1 : 执行完dosomething方法 是否还持有锁:false
2021-05-23 22:49:01.467 info 69041 --- [nio-9090-exec-1] com.test.reentrantlockdemo1 : --------------end---------------
看控制台打印能清楚知道redisson是支持可重入锁了。
8、那么redisson是如何实现的了?
我们跟一下lock.lock()的代码,发现它最终调用的是org.redisson.redissonlock#trylockinnerasync的方法,具体如下:
8.1、上面的代码,用到的redis命令先梳理一下
exists 查询一个key是否存在
exists key [key ...]
返回值
如下的整数结果
1 如果key存在
0 如果key不存在
hincrby :将hash中指定域的值增加给定的数字
pexpire:设置key的有效时间以毫秒为单位
hexists:判断field是否存在于hash中
pttl:获取key的有效毫秒数
8.2、看lua脚本传入的参数我们知道:
- keys[1] = key的值
- argv[1]) = 持有锁的时间
- argv[2] = getlockname(threadid) 下面id就算系统在启动的时候会全局生成的uuid 来作为当前进程的id,加上线程id就是getlockname(threadid)了,可以理解为:进程id+系统id = argv[2]
8.3、代码截图
从截图上可以看到,它是使用lua脚本来保证多个命令执行的原子性,使用了hash来实现了分布式锁
现在我们来看下lua脚本的加锁流程
8.4、第一个if判断
- 204行:它是先判断了当前key是否存在,从exists命令我们知道返回值是0说明key不存在,说明没有加锁
- 205行:hincrby命令是对 argv[2] = 进程id+系统id 进行原子自增加1
- 206行:是对整个hash设置过期期间
8.5、下面来看第二个if判断
- 209行:判断field是否存在于hash中,如果存在返回1,返回1说明是当前进程+当前线程id 之前已经获得到锁了
- 210行:hincrby命令是对 argv[2] = 进程id+系统id 进行原子自增加1,说明重入次数加1了
- 211行:再对整个hash设置过期期间
8.6、下图是redis可视化工具看到是如何在hash存储的结构
redisson的整个加锁流程跟reentrantlock的加锁逻辑基本相同
8.7、解锁代码位于 org.redisson.redissonlock#unlockinnerasync,如下:
看这个解锁的lua脚本,流程跟reentrantlock的解锁逻辑也基本相同没啥好说的了。
以上就是redis分布式锁-可重入锁的详细内容,更多关于redis分布式锁的资料请关注其它相关文章!