目前主流的开源解决方案有jedis,redission,lettuce三种解决方案,其中jedis是同步的方案,如今包括spring-data也已经再也不内置使用了,替换成了lettuce。redission和lettuce都是基于netty的也就是说他俩都是异步非阻塞的,可是他们有什么区别呢?其实在使用语法上面有一些区别,redission对结果作了一层包装,经过包装类来进行一些额外的操做来达到异步操做,而且redission提供了额外的分部署锁功能。java

Jedis是Redis的Java实现的客户端,其API提供了比较全面的Redis命令的支持;Redisson实现了分布式和可扩展的Java数据结构,和Jedis相比, 功能较为简单,不支持字符串操做,不支持排序、事务、管道、分区等Redis特性。Redisson的宗旨是促进使用者对Redis的关注分离, 从而让使用者可以将精力更集中地放在处理业务逻辑上。git

Jedis使用阻塞的I/O,且其方法调用都是同步的,程序流须要等到sockets处理完I/O才能执行,不支持异步。Jedis客户端实例不是线程安全的,因此须要经过链接池来使用Jedis。github

Redisson使用非阻塞的I/O和基于Netty框架的事件驱动的通讯层,其方法调用是异步的。Redisson的API是线程安全的,因此能够操做单个Redisson链接来完成各类操做。redis

1、Redisson分布式锁的底层原理

熟悉Redis的同窗那么确定对setNx(set if not exist)方法不陌生,若是不存在则更新,其能够很好的用来实现咱们的分布式锁。对于某个资源加锁咱们只须要算法

setNx resourceName value

这里有个问题,加锁了以后若是机器宕机那么这个锁就不会获得释放因此会加入过时时间,加入过时时间须要和setNx同一个原子操做,在Redis2.8以前咱们须要使用Lua脚本达到咱们的目的,可是redis2.8以后redis支持nx和ex操做是同一原子操做。spring

set resourceName value ex 5 nx

Redission

Javaer都知道Jedis,Jedis是Redis的Java实现的客户端,其API提供了比较全面的Redis命令的支持。Redission也是Redis的客户端,相比于Jedis功能简单。Jedis简单使用阻塞的I/O和redis交互,Redission经过Netty支持非阻塞I/O。Jedis最新版本2.9.0是2016年的快3年了没有更新,而Redission最新版本是2018.10月更新。sql

Redission封装了锁的实现,其继承了java.util.concurrent.locks.Lock的接口,让咱们像操做咱们的本地Lock同样去操做Redission的Lock,下面介绍一下其如何实现分布式锁。编程

img

Redission不只提供了Java自带的一些方法(lock,tryLock),还提供了异步加锁,对于异步编程更加方便。 因为内部源码较多,就不贴源码了,这里用文字叙述来分析他是如何加锁的,这里分析一下tryLock方法:安全

  1. 尝试加锁:首先会尝试进行加锁,因为保证操做是原子性,那么就只能使用lua脚本,相关的lua脚本以下:数据结构

    img

    能够看见他并无使用咱们的sexNx来进行操做,而是使用的hash结构,咱们的每个须要锁定的资源均可以看作是一个HashMap,锁定资源的节点信息是Key,锁定次数是value。经过这种方式能够很好的实现可重入的效果,只须要对value进行加1操做,就能进行可重入锁。固然这里也能够用以前咱们说的本地计数进行优化。

  2. 若是尝试加锁失败,判断是否超时,若是超时则返回false。

  3. 若是加锁失败以后,没有超时,那么须要在名字为redisson_lock__channel+lockName的channel上进行订阅,用于订阅解锁消息,而后一直阻塞直到超时,或者有解锁消息。

  4. 重试步骤1,2,3,直到最后获取到锁,或者某一步获取锁超时。

对于咱们的unlock方法比较简单也是经过lua脚本进行解锁,若是是可重入锁,只是减1。若是是非加锁线程解锁,那么解锁失败。

img

Redission还有公平锁的实现,对于公平锁其利用了list结构和hashset结构分别用来保存咱们排队的节点,和咱们节点的过时时间,用这两个数据结构帮助咱们实现公平锁,这里就不展开介绍了,有兴趣能够参考源码。

RedLock

咱们想象一个这样的场景当机器A申请到一把锁以后,若是Redis主宕机了,这个时候从机并无同步到这一把锁,那么机器B再次申请的时候就会再次申请到这把锁,为了解决这个问题Redis做者提出了RedLock红锁的算法,在Redission中也对RedLock进行了实现。

img

经过上面的代码,咱们须要实现多个Redis集群,而后进行红锁的加锁,解锁。具体的步骤以下:

  1. 首先生成多个Redis集群的Rlock,并将其构形成RedLock。
  2. 依次循环对三个集群进行加锁,加锁的过程和上面的RedLock一致。
  3. 若是循环加锁的过程当中加锁失败,那么须要判断加锁失败的次数是否超出了最大值,这里的最大值是根据集群的个数,好比三个那么只容许失败一个,五个的话只容许失败两个,要保证多数成功。
  4. 加锁的过程当中须要判断是否加锁超时,有可能咱们设置加锁只能用3ms,第一个集群加锁已经消耗了3ms了。那么也算加锁失败。
  5. 3,4步里面加锁失败的话,那么就会进行解锁操做,解锁会对全部的集群在请求一次解锁。

能够看见RedLock基本原理是利用多个Redis集群,用多数的集群加锁成功,减小Redis某个集群出故障,形成分布式锁出现问题的几率。

Redis分布式锁的小结

  • 优势:对于Redis实现简单,性能对比ZK和Mysql较好。若是不须要特别复杂的要求,那么本身就能够利用setNx进行实现,若是本身须要复杂的需求的话那么能够利用或者借鉴Redission。对于一些要求比较严格的场景来讲的话可使用RedLock。
  • 缺点:须要维护Redis集群,若是要实现RedLock那么须要维护更多的集群。

原理图:

imgredisson实现Redis分布式锁的底层原理

1)加锁机制

我们来看上面那张图,如今某个客户端要加锁。若是该客户端面对的是一个redis cluster集群,他首先会根据hash节点选择一台机器。

这里注意,仅仅只是选择一台机器!这点很关键!

紧接着,就会发送一段lua脚本到redis上,那段lua脚本以下所示:

img

为啥要用lua脚本呢?

由于一大坨复杂的业务逻辑,能够经过封装在lua脚本中发送给redis,保证这段复杂业务逻辑执行的原子性

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

**KEYS[1]**表明的是你加锁的那个key,好比说:

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

这里你本身设置了加锁的那个锁key就是“myLock”。

**ARGV[1]**表明的就是锁key的默认生存时间,默认30秒。

**ARGV[2]**表明的是加锁的客户端的ID,相似于下面这样:

8743c9c0-0795-4907-87fd-6c719a6b4586:1

给你们解释一下,第一段if判断语句,就是用“exists myLock”命令判断一下,若是你要加锁的那个锁key不存在的话,你就进行加锁。

如何加锁呢?很简单,用下面的命令:

hset myLock 

   8743c9c0-0795-4907-87fd-6c719a6b4586:1 1

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

img

上述就表明“8743c9c0-0795-4907-87fd-6c719a6b4586:1”这个客户端对“myLock”这个锁key完成了加锁。

接着会执行“pexpire myLock 30000”命令,设置myLock这个锁key的生存时间是30秒。

好了,到此为止,ok,加锁完成了。

(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都已经持有了这把锁了,结果可重入的加锁会怎么样呢?

好比下面这种代码:

img

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

第一个if判断确定不成立,“exists myLock”会显示锁key已经存在了。

第二个if判断会成立,由于myLock的hash数据结构中包含的那个ID,就是客户端1的那个ID,也就是“8743c9c0-0795-4907-87fd-6c719a6b4586:1”

此时就会执行可重入加锁的逻辑,他会用:

incrby myLock

8743c9c0-0795-4907-87fd-6c71a6b4586:1 1

经过这个命令,对客户端1的加锁次数,累加1。

此时myLock数据结构变为下面这样:

img

你们看到了吧,那个myLock的hash数据结构中的那个客户端ID,就对应着加锁的次数

(5)释放锁机制

若是执行lock.unlock(),就能够释放分布式锁,此时的业务逻辑也是很是简单的。

其实说白了,就是每次都对myLock数据结构中的那个加锁次数减1

若是发现加锁次数是0了,说明这个客户端已经再也不持有锁了,此时就会用:

“del myLock”命令,从redis里删除这个key。

而后呢,另外的客户端2就能够尝试完成加锁了。

这就是所谓的分布式锁的开源Redisson框架的实现机制。

通常咱们在生产系统中,能够用Redisson框架提供的这个类库来基于redis进行分布式锁的加锁与释放锁。

2、使用redisson实现分布式锁

1. 可重入锁(Reentrant Lock)

Redisson的分布式可重入锁RLock Java对象实现了java.util.concurrent.locks.Lock接口,同时还支持自动过时解锁。

public void testReentrantLock(RedissonClient redisson){

        RLock lock = redisson.getLock("anyLock");
        try{
            // 1. 最多见的使用方法
            //lock.lock();

            // 2. 支持过时解锁功能,10秒钟之后自动解锁, 无需调用unlock方法手动解锁
            //lock.lock(10, TimeUnit.SECONDS);

            // 3. 尝试加锁,最多等待3秒,上锁之后10秒自动解锁
            boolean res = lock.tryLock(3, 10, TimeUnit.SECONDS);
            if(res){    //成功
                // do your business

            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

    }

Redisson同时还为分布式锁提供了异步执行的相关方法:

public void testAsyncReentrantLock(RedissonClient redisson){
        RLock lock = redisson.getLock("anyLock");
        try{
            lock.lockAsync();
            lock.lockAsync(10, TimeUnit.SECONDS);
            Future<boolean> res = lock.tryLockAsync(3, 10, TimeUnit.SECONDS);

            if(res.get()){
                // do your business

            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

    }

2. 公平锁(Fair Lock)

Redisson分布式可重入公平锁也是实现了java.util.concurrent.locks.Lock接口的一种RLock对象。在提供了自动过时解锁功能的同时,保证了当多个Redisson客户端线程同时请求加锁时,优先分配给先发出请求的线程。

public void testFairLock(RedissonClient redisson){

        RLock fairLock = redisson.getFairLock("anyLock");
        try{
            // 最多见的使用方法
            fairLock.lock();

            // 支持过时解锁功能, 10秒钟之后自动解锁,无需调用unlock方法手动解锁
            fairLock.lock(10, TimeUnit.SECONDS);

            // 尝试加锁,最多等待100秒,上锁之后10秒自动解锁
            boolean res = fairLock.tryLock(100, 10, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            fairLock.unlock();
        }

    }

Redisson同时还为分布式可重入公平锁提供了异步执行的相关方法:

RLock fairLock = redisson.getFairLock("anyLock");
fairLock.lockAsync();
fairLock.lockAsync(10, TimeUnit.SECONDS);
Future<boolean> res = fairLock.tryLockAsync(100, 10, TimeUnit.SECONDS);

3. 联锁(MultiLock)

Redisson的RedissonMultiLock对象能够将多个RLock对象关联为一个联锁,每一个RLock对象实例能够来自于不一样的Redisson实例。

public void testMultiLock(RedissonClient redisson1,
                              RedissonClient redisson2, RedissonClient redisson3){

        RLock lock1 = redisson1.getLock("lock1");
        RLock lock2 = redisson2.getLock("lock2");
        RLock lock3 = redisson3.getLock("lock3");

        RedissonMultiLock lock = new RedissonMultiLock(lock1, lock2, lock3);

        try {
            // 同时加锁:lock1 lock2 lock3, 全部的锁都上锁成功才算成功。
            lock.lock();

            // 尝试加锁,最多等待100秒,上锁之后10秒自动解锁
            boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);

        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

    }

4. 红锁(RedLock)

Redisson的RedissonRedLock对象实现了Redlock介绍的加锁算法。该对象也能够用来将多个RLock 对象关联为一个红锁,每一个RLock对象实例能够来自于不一样的Redisson实例。

public void testRedLock(RedissonClient redisson1,
                              RedissonClient redisson2, RedissonClient redisson3){

        RLock lock1 = redisson1.getLock("lock1");
        RLock lock2 = redisson2.getLock("lock2");
        RLock lock3 = redisson3.getLock("lock3");

        RedissonRedLock lock = new RedissonRedLock(lock1, lock2, lock3);
      try {
            // 同时加锁:lock1 lock2 lock3, 红锁在大部分节点上加锁成功就算成功。
            lock.lock();

            // 尝试加锁,最多等待100秒,上锁之后10秒自动解锁
            boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);

        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

    }

5. 读写锁(ReadWriteLock)

Redisson的分布式可重入读写锁RReadWriteLock Java对象实现了java.util.concurrent.locks.ReadWriteLock接口。同时还支持自动过时解锁。该对象容许同时有多个读取锁,可是最多只能有一个写入锁。

RReadWriteLock rwlock = redisson.getLock("anyRWLock");
// 最多见的使用方法
rwlock.readLock().lock();
// 或
rwlock.writeLock().lock();

// 支持过时解锁功能
// 10秒钟之后自动解锁
// 无需调用unlock方法手动解锁
rwlock.readLock().lock(10, TimeUnit.SECONDS);
// 或
rwlock.writeLock().lock(10, TimeUnit.SECONDS);

// 尝试加锁,最多等待100秒,上锁之后10秒自动解锁
boolean res = rwlock.readLock().tryLock(100, 10, TimeUnit.SECONDS);
// 或
boolean res = rwlock.writeLock().tryLock(100, 10, TimeUnit.SECONDS);
...
lock.unlock();

6. 信号量(Semaphore)

Redisson的分布式信号量(Semaphore)Java对象RSemaphore采用了与java.util.concurrent.Semaphore类似的接口和用法。

RSemaphore semaphore = redisson.getSemaphore("semaphore");
semaphore.acquire();
//或
semaphore.acquireAsync();
semaphore.acquire(23);
semaphore.tryAcquire();
//或
semaphore.tryAcquireAsync();
semaphore.tryAcquire(23, TimeUnit.SECONDS);
//或
semaphore.tryAcquireAsync(23, TimeUnit.SECONDS);
semaphore.release(10);
semaphore.release();
//或
semaphore.releaseAsync();

7. 可过时性信号量(PermitExpirableSemaphore)

Redisson的可过时性信号量(PermitExpirableSemaphore)实在RSemaphore对象的基础上,为每一个信号增长了一个过时时间。每一个信号能够经过独立的ID来辨识,释放时只能经过提交这个ID才能释放。

RPermitExpirableSemaphore semaphore = redisson.getPermitExpirableSemaphore("mySemaphore");
String permitId = semaphore.acquire();
// 获取一个信号,有效期只有2秒钟。
String permitId = semaphore.acquire(2, TimeUnit.SECONDS);
// ...
semaphore.release(permitId);

8. 闭锁(CountDownLatch)

Redisson的分布式闭锁(CountDownLatch)Java对象RCountDownLatch采用了与java.util.concurrent.CountDownLatch类似的接口和用法。

RCountDownLatch latch = redisson.getCountDownLatch("anyCountDownLatch");
latch.trySetCount(1);
latch.await();

// 在其余线程或其余JVM里
RCountDownLatch latch = redisson.getCountDownLatch("anyCountDownLatch");
latch.countDown();

参考资料

Redission</boolean></boolean>