redis分布式锁-可重入锁

上篇redis实现的分布式锁,有一个问题,它不可重入。

所谓不可重入锁,即若当前线程执行某个方法已经获取了该锁,那么在方法中尝试再次获取锁时,就会获取不到被阻塞。 同一个人拿一个锁 ,只能拿一次不能同时拿2次。

1、什么是可重入锁?它有什么作用?

可重入锁,也叫做递归锁,指的是在同一线程内,外层函数获得锁之后,内层递归函数仍然可以获取到该锁。 说白了就是同一个线程再次进入同样代码时,可以再次拿到该锁。 它的作用是:防止在同一线程中多次获取锁而导致死锁发生。

2、那么java中谁实现了可重入锁了?

在java的编程中synchronized 和 ReentrantLock都是可重入锁。我们可以参考ReentrantLock的代码

3、基于ReentrantLock的可重入锁

ReentrantLock,是一个可重入且独占式的锁,是一种递归无阻塞的同步锁。

3.1、看个ReentrantLock的例子
  1. import lombok.extern.slf4j.Slf4j;
  2. import java.util.concurrent.locks.ReentrantLock;
  3. @Slf4j
  4. public class ReentrantLockDemo {
  5. //锁
  6. private static ReentrantLock lock = new ReentrantLock();
  7. public void doSomething(int n){
  8. try{
  9. //进入递归第一件事:加锁
  10. lock.lock();
  11. log.info("--------lock()执行后,getState()的值:{} lock.isLocked():{}",lock.getHoldCount(),lock.isLocked());
  12. log.info("--------递归{}次--------",n);
  13. if(n<=2){
  14. this.doSomething(++n);
  15. }else{
  16. return;
  17. }
  18. }finally {
  19. lock.unlock();
  20. log.info("--------unlock()执行后,getState()的值:{} lock.isLocked():{}",lock.getHoldCount(),lock.isLocked());
  21. }
  22. }
  23. public static void main(String[] args) {
  24. ReentrantLockDemo reentrantLockDemo=new ReentrantLockDemo();
  25. reentrantLockDemo.doSomething(1);
  26. log.info("执行完doSomething方法 是否还持有锁:{}",lock.isLocked());
  27. }
  28. }
3.2、执行结果
  1. 16:35:58.051 [main] INFO com.test.ReentrantLockDemo - --------lock()执行后,getState()的值:1 lock.isLocked():true
  2. 16:35:58.055 [main] INFO com.test.ReentrantLockDemo - --------递归1次--------
  3. 16:35:58.055 [main] INFO com.test.ReentrantLockDemo - --------lock()执行后,getState()的值:2 lock.isLocked():true
  4. 16:35:58.055 [main] INFO com.test.ReentrantLockDemo - --------递归2次--------
  5. 16:35:58.055 [main] INFO com.test.ReentrantLockDemo - --------lock()执行后,getState()的值:3 lock.isLocked():true
  6. 16:35:58.055 [main] INFO com.test.ReentrantLockDemo - --------递归3次--------
  7. 16:35:58.055 [main] INFO com.test.ReentrantLockDemo - --------unlock()执行后,getState()的值:2 lock.isLocked():true
  8. 16:35:58.055 [main] INFO com.test.ReentrantLockDemo - --------unlock()执行后,getState()的值:1 lock.isLocked():true
  9. 16:35:58.055 [main] INFO com.test.ReentrantLockDemo - --------unlock()执行后,getState()的值:0 lock.isLocked():false
  10. 16:35:58.055 [main] INFO com.test.ReentrantLockDemo - 执行完doSomething方法 是否还持有锁:false
3.3、 从上面栗子可以看出ReentrantLock是可重入锁,那么他是如何实现的了,我们看下源码就知道了
  1. final boolean nonfairTryAcquire(int acquires) {
  2. final Thread current = Thread.currentThread();
  3. int c = getState();
  4. //先判断,c(state)是否等于0,如果等于0,说明没有线程持有锁
  5. if (c == 0) {
  6. //通过cas方法把state的值0替换成1,替换成功说明加锁成功
  7. if (compareAndSetState(0, acquires)) {
  8. //如果加锁成功,设置持有锁的线程是当前线程
  9. setExclusiveOwnerThread(current);
  10. return true;
  11. }
  12. }
  13. else if (current == getExclusiveOwnerThread()) {//判断当前持有锁的线程是否是当前线程
  14. //如果是当前线程,则state值加acquires,代表了当前线程加锁了多少次
  15. int nextc = c + acquires;
  16. if (nextc < 0) // overflow
  17. throw new Error("Maximum lock count exceeded");
  18. setState(nextc);
  19. return true;
  20. }
  21. return false;
  22. }

ReentrantLock的加锁流程是:

1,先判断是否有线程持有锁,没有加锁进行加锁

2、如果加锁成功,则设置持有锁的线程是当前线程

3、如果有线程持有了锁,则再去判断,是否是当前线程持有了锁

4、如果是当前线程持有锁,则加锁数量(state)+1

  1. /**
  2. * 释放锁
  3. * @param releases
  4. * @return
  5. */
  6. protected final boolean tryRelease(int releases) {
  7. int c = getState() - releases;//state-1 减加锁次数
  8. //如果持有锁的线程,不是当前线程,抛出异常
  9. if (Thread.currentThread() != getExclusiveOwnerThread())
  10. throw new IllegalMonitorStateException();
  11. boolean free = false;
  12. if (c == 0) {//如果c==0了说明当前线程,已经要释放锁了
  13. free = true;
  14. setExclusiveOwnerThread(null);//设置当前持有锁的线程为null
  15. }
  16. setState(c);//设置c的值
  17. return free;
  18. }

看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、第二个问题:加锁次数(重入了多少次),怎么记录维护
  1. 他能记录下来加锁次数吗?

    如果valus值存的格式是:系进程id+线程id+加锁次数,那可以实现

  2. 存没问题了,但是重入次数要怎么维护了, 它肯定要保证原子性的,能解决吗?

    好像用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)等等。

Redisson github地址

7、Redisson的分布锁如何使用

引入依赖包
  1. <dependency>
  2. <groupId>org.redisson</groupId>
  3. <artifactId>redisson</artifactId>
  4. <version>3.15.5</version>
  5. </dependency>
代码
  1. import lombok.extern.slf4j.Slf4j;
  2. import org.redisson.Redisson;
  3. import org.redisson.api.RLock;
  4. import org.redisson.api.RedissonClient;
  5. import org.redisson.config.Config;
  6. import org.redisson.config.SingleServerConfig;
  7. @Slf4j
  8. public class ReentrantLockDemo1 {
  9. //锁
  10. public static RLock lock;
  11. static {
  12. //Redisson需要的配置
  13. Config config = new Config();
  14. String node = "127.0.0.1:6379";//redis地址
  15. node = node.startsWith("redis://") ? node : "redis://" + node;
  16. SingleServerConfig serverConfig = config.useSingleServer()
  17. .setAddress(node)
  18. .setTimeout(3000)//超时时间
  19. .setConnectionPoolSize(10)
  20. .setConnectionMinimumIdleSize(10);
  21. //serverConfig.setPassword("123456");//设置redis密码
  22. // 创建RedissonClient客户端实例
  23. RedissonClient redissonClient = Redisson.create(config);
  24. //创建redisson的分布式锁
  25. RLock rLock = redissonClient.getLock("666");
  26. lock = rLock;
  27. }
  28. public void doSomething(int n){
  29. try{
  30. //进入递归第一件事:加锁
  31. lock.lock();
  32. log.info("--------lock()执行后,getState()的值:{} lock.isLocked():{}",lock.getHoldCount(),lock.isLocked());
  33. log.info("--------递归{}次--------",n);
  34. if(n<=2){
  35. this.doSomething(++n);
  36. }else{
  37. return;
  38. }
  39. }finally {
  40. lock.unlock();
  41. log.info("--------unlock()执行后,getState()的值:{} lock.isLocked():{}",lock.getHoldCount(),lock.isLocked());
  42. }
  43. }
  44. public static void test(){
  45. log.info("--------------start---------------");
  46. ReentrantLockDemo1 reentrantLockDemo=new ReentrantLockDemo1();
  47. reentrantLockDemo.doSomething(1);
  48. log.info("执行完doSomething方法 是否还持有锁:{}",ReentrantLockDemo1.lock.isLocked());
  49. log.info("--------------end---------------");
  50. }
  51. public static void main(String[] args) {
  52. test();
  53. }
  54. }
执行结果
  1. 2021-05-23 22:49:01.322 INFO 69041 --- [nio-9090-exec-1] org.redisson.Version : Redisson 3.15.5
  2. 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
  3. 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
  4. 2021-05-23 22:49:01.367 INFO 69041 --- [nio-9090-exec-1] com.test.ReentrantLockDemo1 : --------------start---------------
  5. 2021-05-23 22:49:01.435 INFO 69041 --- [nio-9090-exec-1] com.test.ReentrantLockDemo1 : --------lock()执行后,getState()的值:1 lock.isLocked():true
  6. 2021-05-23 22:49:01.436 INFO 69041 --- [nio-9090-exec-1] com.test.ReentrantLockDemo1 : --------递归1次--------
  7. 2021-05-23 22:49:01.442 INFO 69041 --- [nio-9090-exec-1] com.test.ReentrantLockDemo1 : --------lock()执行后,getState()的值:2 lock.isLocked():true
  8. 2021-05-23 22:49:01.442 INFO 69041 --- [nio-9090-exec-1] com.test.ReentrantLockDemo1 : --------递归2次--------
  9. 2021-05-23 22:49:01.448 INFO 69041 --- [nio-9090-exec-1] com.test.ReentrantLockDemo1 : --------lock()执行后,getState()的值:3 lock.isLocked():true
  10. 2021-05-23 22:49:01.448 INFO 69041 --- [nio-9090-exec-1] com.test.ReentrantLockDemo1 : --------递归3次--------
  11. 2021-05-23 22:49:01.456 INFO 69041 --- [nio-9090-exec-1] com.test.ReentrantLockDemo1 : --------unlock()执行后,getState()的值:2 lock.isLocked():true
  12. 2021-05-23 22:49:01.461 INFO 69041 --- [nio-9090-exec-1] com.test.ReentrantLockDemo1 : --------unlock()执行后,getState()的值:1 lock.isLocked():true
  13. 2021-05-23 22:49:01.465 INFO 69041 --- [nio-9090-exec-1] com.test.ReentrantLockDemo1 : --------unlock()执行后,getState()的值:0 lock.isLocked():false
  14. 2021-05-23 22:49:01.467 INFO 69041 --- [nio-9090-exec-1] com.test.ReentrantLockDemo1 : 执行完doSomething方法 是否还持有锁:false
  15. 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的方法,具体如下:

  1. <T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
  2. return evalWriteAsync(getRawName(), LongCodec.INSTANCE, command,
  3. "if (redis.call('exists', KEYS[1]) == 0) then " +
  4. "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
  5. "redis.call('pexpire', KEYS[1], ARGV[1]); " +
  6. "return nil; " +
  7. "end; " +
  8. "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
  9. "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
  10. "redis.call('pexpire', KEYS[1], ARGV[1]); " +
  11. "return nil; " +
  12. "end; " +
  13. "return redis.call('pttl', KEYS[1]);",
  14. Collections.singletonList(getRawName()), unit.toMillis(leaseTime), getLockName(threadId));
  15. }
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]
  1. protected String getLockName(long threadId) {
  2. return id + ":" + threadId;
  3. }
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,如下:
  1. return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
  2. "if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
  3. "return nil;" +
  4. "end; " +
  5. "local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
  6. "if (counter > 0) then " +
  7. "redis.call('pexpire', KEYS[1], ARGV[2]); " +
  8. "return 0; " +
  9. "else " +
  10. "redis.call('del', KEYS[1]); " +
  11. "redis.call('publish', KEYS[2], ARGV[1]); " +
  12. "return 1; " +
  13. "end; " +
  14. "return nil;",
  15. Arrays.asList(getRawName(), getChannelName()), LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId));
  16. }

看这个解锁的Lua脚本,流程跟Reentrantlock的解锁逻辑也基本相同没啥好说的了。


redis分布式锁-可重入锁的更多相关文章

  1. 二、多线程基础-乐观锁_悲观锁_重入锁_读写锁_CAS无锁机制_自旋锁

    1.10乐观锁_悲观锁_重入锁_读写锁_CAS无锁机制_自旋锁1)乐观锁:就像它的名字一样,对于并发间操作产生的线程安全问题持乐观状态,乐观锁认为竞争不总是会发生,因此它不需要持有锁,将 比较-设置 ...

  2. Java 显示锁 之 重入锁 ReentrantLock(七)

    ReentrantLock 重入锁简介 重入锁 ReentrantLock,顾名思义,就是支持同一个线程对资源的重复加锁.另外,该锁还支持获取锁时的公平与非公平性的选择. 重入锁 ReentrantL ...

  3. java面试-公平锁/非公平锁/可重入锁/递归锁/自旋锁谈谈你的理解

    一.公平锁/非公平锁/可重入锁/递归锁/自旋锁谈谈你的理解 公平锁:多个线程按照申请的顺序来获取锁. 非公平锁:多个线程获取锁的先后顺序与申请锁的顺序无关.[ReentrantLock 默认非公平.s ...

  4. 浅谈Java中的公平锁和非公平锁,可重入锁,自旋锁

    公平锁和非公平锁 这里主要体现在ReentrantLock这个类里面了 公平锁.非公平锁的创建方式: //创建一个非公平锁,默认是非公平锁 Lock lock = new ReentrantLock( ...

  5. Java锁机制-重入锁

    锁的种类: 读写锁   悲观锁  乐观锁 CSA无锁  自旋锁  AQS 非公平锁 公平锁 互斥锁 排它锁  分布式锁(redis实现 和 zk实现) 轻量级锁(lock),重量级锁(synchron ...

  6. 可重入锁 & 不可重入锁

    可重入锁指同一个线程可以再次获得之前已经获得的锁,避免产生死锁. Java中的可重入锁:synchronized 和 java.util.concurrent.locks.ReentrantLock. ...

  7. Java并发编程原理与实战十七:AQS实现重入锁

    一.什么是重入锁 可重入锁就是当前持有锁的线程能够多次获取该锁,无需等待 二.什么是AQS AQS是JDK1.5提供的一个基于FIFO等待队列实现的一个用于实现同步器的基础框架,这个基础框架的重要性可 ...

  8. 浅谈Java中的锁:Synchronized、重入锁、读写锁

    Java开发必须要掌握的知识点就包括如何使用锁在多线程的环境下控制对资源的访问限制 ◆ Synchronized ◆ 首先我们来看一段简单的代码: 12345678910111213141516171 ...

  9. synchronized 是可重入锁吗?为什么?

    什么是可重入锁? 关于什么是可重入锁,我们先来看一段维基百科的定义. 若一个程序或子程序可以“在任意时刻被中断然后操作系统调度执行另外一段代码,这段代码又调用了该子程序不会出错”,则称其为可重入(re ...

随机推荐

  1. Git 上传项目到 Github

    Git 上传项目到 Github 该文章主要讲解Git 上传项目到 Github,Gitee同理 配置Git 下载.安装Git 下载后一路(傻瓜式安装)直接安装即可 如果第一次使用git的话,需要设置 ...

  2. day-05-字典

    字典的初识 why: 列表可以存储大量的数据,但数据之间的关联性不强 列表的查询速度比较慢.数量越大查询越慢 what:容器型数据类型:dict how: 数据类型的分类(可变与不可变) 可变(不可哈 ...

  3. codeup 1918 简单计算器

    题目描述:读入一个只包含 + ,-,×, / 的非负整数计算表达式,计算该表达式的值.输入格式:测试输入包含若干测试用例,每个测试用例占一行,每行不超过200个字符,整数和运草符之间用一个空格分隔.没 ...

  4. 利用查询条件对象,在Asp.net Web API中实现对业务数据的分页查询处理

    在Asp.net Web API中,对业务数据的分页查询处理是一个非常常见的接口,我们需要在查询条件对象中,定义好相应业务的查询参数,排序信息,请求记录数和每页大小信息等内容,根据这些查询信息,我们在 ...

  5. 06_pytorch的autograd操作

    06_pytorch的autograd操作 目录 一.引言 二.Variable 2.1 Variable 的数据结构 2.2 反向传播 2.3 autograd 求导数和手动求导数 三.计算图 3. ...

  6. win10美化,让你的win10独一无二,与众不同!

    2020.06.23 更新 1 原则 美化之前,得先有一个目标对不对,笔者是一个喜欢简单的人,因此美化本着三大原则:简单,干净,整洁. 呃....好像很抽象的样子,上图吧.反正没图没真相. 怎么样,还 ...

  7. NumPy之:标量scalars

    目录 简介 scalar类型的层次结构 内置Scalar类型 boolean Integers Unsigned integers Floating-point numbers Complex flo ...

  8. 浅析 Find My 原理

    什么是 Find My 网络? 2021年4月21日的苹果发布会发布了一款新的产品:AirTag.使用的是 BLE + UWB 的技术.BLE 和 Find My 网络为 AirTag 提供了 GPS ...

  9. Jenkins 项目类型及配置项

    0. 简介 1. Freestyle project 1)General 2)源码管理 3)构建触发器 4)构建环境 5)构建 6)构建后操作 2. Maven 项目 1)所有配置项 2)Build ...

  10. 4.Linux命令基本格式与简单使用

    Linux命令名组成: 在Linux/Unix系统下输入命令,就会进行相应的操作,那么这个命令有如下组成: 命令名 [选项] [参数] 注:[]的内容代表可选 命令实例: ls #显示当前文件夹下的所 ...