Redisson實現分佈式鎖及其底層原理解析

目前主流的開源解決方案有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分佈式鎖的底層原理

https://mp.weixin.qq.com/s/y_Uw3P2Ll7wvk_j5Fdlusw

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();

參考資料

Redisson 分佈式鎖和同步器

Redission</boolean></boolean>

相關文章
相關標籤/搜索