美文网首页javaWeb学习
Redisson分布式锁的实现原理

Redisson分布式锁的实现原理

作者: Djbfifjd | 来源:发表于2020-08-05 17:21 被阅读0次

一、Java原生锁的使用

在说分布式锁之前,先了解下 Java 的 synchronized,这种加锁方式可以针对某个方法或者某个代码块进行加锁,如:

public void doInfo (String uid) {
  synchronized(uid.intern()) {
  //TODO
  }
}

比如A用户进来执行了doInfo(),且还在 synchronized 代码块里处理相关业务,如果此时B用户也进来了,那么需要在 synchronized 的代码块外边等待直到A执行完代码块里的业务,这样就防止A和B在同一时间对同一资源进行操作可能造成数据错误的情况。

上面只是针对单机的操作,也就是说针对部署单个应用里有效,如果是微服务部署方式,同一个应用可能会部署到多台服务器,而访问的资源还是同一个,那么用户A在第一台服务上执行,B在第二台服务器上执行,那么 synchronized 此时是无效的。

二、分布式锁的概念

1️⃣线程锁:主要是用来给方法或者代码块加锁的。当某个方法或代码使用锁时,在同一时间只能有一个线程执行该方法或者该代码段。线程锁的实现其实是依靠线程之间共享的内存来实现的,因此线程锁只是针对同一个JVM中才有效。

2️⃣进程锁:主要是为了控制同一系统中某个资源同时被多个进程访问。而每个进程都是独立的,因此各个进程是无法互相访问彼此的资源的,也无法像线程那样通过 synchronized 来实现进程锁。

3️⃣分布式锁:控制分布式系统间同步请求共享资源的一种方式。在分布式系统中,如果不同的系统或是同一个系统的不同主机之间共享同一资源,那么访问这个资源的时候,需要互斥来防止彼此干扰来保证一致性,在这种情况下,便需要使用到分布式锁。

另外分布式锁应该具备以下特点:
①互斥性:在分布式高并发的条件下,同一时刻只能有一个线程获得锁。
②防止死锁:即在客户端使用锁过程中出现异常也不应该出现死锁,应该及时释放锁确保后续流程正常运行。分布式非常有必要设置锁的有效时间,确保系统出现故障后,在一定时间内能够主动去释放锁,避免造成死锁的情况。
③可靠性:只要大部分的中间件节点正常运行,客户端就可以加锁和解锁的操作。
④一致性:加锁和解锁应当是同一个客户端,自己加锁自己解锁,不能去操作其它锁。
⑤重入:ReentrantLock是可重入锁,它的特点:同一个线程可以重复拿到同一个资源的锁。重入锁非常有利于资源的高效利用。
⑥性能:对于访问量大的共享资源,需要考虑减少锁等待的时间,避免导致大量线程阻塞。所以在锁的设计时,需要考虑两点:

  1. 锁的颗粒度要尽量小。比如要通过锁来减库存,那这个锁的名称可以设置成是商品的ID,而不是任取名称。这样这个锁只对当前商品有效。
  2. 锁的范围尽量要小。比如只要锁2行代码就可以解决问题的,那就不要去锁10行代码。

针对以上Redisson都能很好的满足,下面就来分析下它。

三、Redisson

Redis 是最流行的 NoSQL 数据库解决方案之一,但 Redis 并没有对 Java 提供原生支持。相反,若想在 Java 程序中集成 Redis,必须使用 Redis 的第三方库。而 Redisson 就是用于在 Java 程序中操作 Redis 的库,开发者利用它可以在程序中轻松地使用 Redis。Redisson 在 java.util 中常用接口的基础上,提供了一系列具有分布式特性的工具类。Redis 分布式锁,一般就用 Redisson 框架,非常的简便易用。

核心代码如下:

RLock lock = redisson.getLock("myLock");
//加锁
lock.lock();
//释放锁
lock.unlock();

很清爽!此外,Redisson还支持redis单实例、redis哨兵、redis cluster、redis master-slave等各种部署架构,都可以完美实现。

1️⃣引入依赖

<dependency>
  <groupId>org.redisson</groupId>
  <artifactId>redisson</artifactId>
  <version>3.11.5</version>
</dependency>

2️⃣代码编写

@Autowired
private Redisson redisson;
private void testRedisson (String msg) {
   String lockKey = "test_lock_key";
   //获取锁
   RLock redLock = redisson.getLock(lockKey);
   try {
       //锁5秒钟
       redLock.lock(5, TimeUnit.SECONDS);
       //TODO 处理业务逻辑
    } catch (Exception e) {
       e.printStackTrace();
    } finally {
        //最后释放锁
       if (null != redLock && redLock.isLocked()) {
         redLock.unlock();
       }
    }
}

四、Redisson实现Redis分布式锁的底层原理


1️⃣加锁机制

见上图,现在某个客户端要加锁。如果该客户端面对的是一个redis cluster集群,它首先会根据hash节点选择一台机器。这里注意,仅仅只是选择一台机器!这点很关键!紧接着,就会发送一段lua脚本到redis上,那段lua脚本如下所示:

为啥要用lua脚本呢?因为一大坨复杂的业务逻辑,可以通过封装在lua脚本中发送给redis,保证这段复杂业务逻辑执行的原子性

那么,这段lua脚本是什么意思呢?

KEYS[1]代表加锁的那个key,比如说:

RLock lock = redisson.getLock("myLock");

这里设置了加锁的那个锁key就是“myLock”。
ARGV[1]代表的就是锁key的默认生存时间,默认30秒。
ARGV[2]代表的是加锁的客户端的ID,类似于下面这样:6094a8d0-0125-4237-67cd-6c419a3b8975:1

第一段if判断语句,就是用“exists myLock”命令判断一下,如果要加锁的那个锁key不存在的话,就进行加锁。如何加锁呢?很简单,用下面的命令:
hset myLock 6094a8d0-0125-4237-67cd-6c419a3b8975:1 1

通过这个命令设置一个hash数据结构,这行命令执行后,会出现一个类似下面的数据结构:

mylock:
{
     “6094a8d0-0125-4237-67cd-6c419a3b8975:1”:  1
}

上述就代表“6094a8d0-0125-4237-67cd-6c419a3b8975:1”这个客户端对“myLock”这个锁key完成了加锁。接着会执行“pexpire myLock 30000”命令,设置myLock这个锁key的生存时间是30秒。到此为止,加锁完成了。

小结:
①线程去获取锁,获取成功:执行lua脚本,保存数据到redis数据库。
②线程去获取锁,获取失败:一直通过while循环尝试获取锁,获取成功后,执行lua脚本,保存数据到redis数据库。

2️⃣锁互斥机制

在分布式高并发的条件下,同一时刻只能有一个线程获得锁,这是最基本的一点。
此时,如果客户端2来尝试加锁,执行了同样的一段lua脚本,会怎样?很简单,第一个if判断会执行“exists myLock”,发现myLock这个锁key已经存在了。接着第二个if判断,判断一下,myLock锁key的hash数据结构中,是否包含客户端2的ID,但是明显不是的,因为那里包含的是客户端1的ID。所以,客户端2会获取到pttl myLock返回的一个数字,这个数字代表了myLock这个锁key的剩余生存时间。比如还剩15000毫秒的生存时间。此时客户端2会进入一个while循环,不停的尝试加锁。

3️⃣watch dog自动延期机制

客户端1加锁的锁key默认生存时间才30秒,如果超过了30秒,客户端1还想一直持有这把锁,怎么办呢?其实只要客户端1加锁成功,就会启动一个watch dog看门狗,它是一个后台线程,会每隔10秒检查一下,如果客户端1还持有锁key,那么就会不断的延长锁key的生存时间。

4️⃣可重入加锁机制

那如果客户端1都已经持有了这把锁了,结果可重入的加锁会怎么样呢?比如下面这种代码:

这时来分析一下上面那段lua脚本。

第一个if判断肯定不成立,“exists myLock”会显示锁key已经存在了。
第二个if判断会成立,因为myLock的hash数据结构中包含的那个ID,就是客户端1的那个ID,也就是“6094a8d0-0125-4237-67cd-6c419a3b8975:1”。此时就会执行可重入加锁的逻辑,它会用:incrby myLock 6094a8d0-0125-4237-67cd-6c419a3b8975:1 1。通过这个命令,对客户端1的加锁次数,累加1。此时myLock数据结构变为下面这样:

mylock:
{
     “6094a8d0-0125-4237-67cd-6c419a3b8975:1”:  2
}

那个myLock的hash数据结构中的那个客户端ID,就对应着加锁的次数。

图解可重入加锁:

Redisson可以实现可重入加锁机制的原因:

1、Redis存储锁的数据类型是 Hash类型
2、Hash数据类型的key值包含了当前线程信息。
下面是redis存储的数据:

这里表面数据类型是Hash类型,Hash类型相当于Java的 <key,<key1,value>> 类型,这里key是指 'redisson'。它的有效期还有9秒,再来看里面的key1值为078e44a3-5f95-4e24-b6aa-80684655a15a:45它的组成是:guid + 当前线程的ID。后面的value是就和可重入加锁有关,举图说明:

上图的意思就是可重入锁的机制,它最大的优点就是相同线程不需要在等待锁,而是可以直接进行相应操作。

5️⃣释放锁机制

执行lock.unlock(),就可以释放分布式锁。其实说白了,就是每次都对myLock数据结构中的那个加锁次数减1。如果发现加锁次数是0了,说明这个客户端已经不再持有锁了,此时就会用del myLock命令,从redis里删除这个key。然后,另外的客户端2就可以尝试完成加锁了。

这就是所谓的分布式锁的开源Redisson框架的实现机制。一般在生产系统中,可以用Redisson框架提供的这个类库来基于redis进行分布式锁的加锁与释放锁。

6️⃣上述Redis分布式锁的缺点

客户端1 对某个master节点写入了redisson锁,此时会异步复制给对应的 slave节点。但是这个过程中一旦发生 master节点宕机复制失败,主备切换,slave节点从变为了 master节点。这时客户端2 来尝试加锁的时候,在新的master节点上也能加锁,此时就会导致多个客户端对同一个分布式锁完成了加锁。这时系统在业务语义上一定会出现问题,导致各种脏数据的产生。缺陷在哨兵模式或者主从模式下,如果 master 实例宕机的时候,可能导致多个客户端同时完成加锁。

相关文章

网友评论

    本文标题:Redisson分布式锁的实现原理

    本文链接:https://www.haomeiwen.com/subject/jrrorktx.html