Redis進階-Redisson分佈式鎖實現原理及源碼解析

 

watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=

前言

1、分佈式鎖的概念和使用場景

整理了一張redis知識圖譜分享給你們:redis

watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=

分佈式鎖是控制分佈式系統之間同步訪問共享資源的一種方式。算法

在分佈式系統中,經常須要協調他們的動做。若是不一樣的系統或是同一個系統的不一樣主機之間共享了一個或一組資源,那麼訪問這些資源的時候,每每須要互斥來防止彼此干擾來保證,這個時候,便須要使用到分佈式鎖。服務器

2、將redis官網對於分佈式鎖(紅鎖)的定義和Redisson實現作歸納性總結

該部分能夠先粗略的瀏覽一下,領略其官方的理論定義,讀完後續內容會對該環節有更清晰的理解。網絡

對於Redis分佈式鎖(紅鎖)官網定義:分佈式

watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=

中文對如上5點作出解釋:ide

redis紅鎖算法:測試

在Redis的分佈式環境中,咱們假設有N個Redis master。這些節點徹底互相獨立,不存在主從複製或者其餘集羣協調機制。咱們確保將在N個實例上使用與在Redis單實例下相同方法獲取和釋放鎖。如今咱們假設有5個Redis master節點,同時咱們須要在5臺服務器上面運行這些Redis實例,這樣保證他們不會同時都宕掉。編碼

爲了取到鎖,客戶端應該執行如下操做:lua

  • 獲取當前時間,以毫秒爲單位。spa

  • 依次嘗試從5個實例,使用相同的key和隨機值(Redisson中給出的是UUID + ThreadId)獲取鎖。當向Redis請求獲取鎖時,客戶端應該設置一個網絡鏈接和響應超時時間(咱們接下來會在加鎖的環節屢次提到這個時間),這個超時時間應該小於鎖的失效時間。例如你的鎖自動失效時間爲10秒,則超時時間應該在5-50毫秒之間。這樣能夠避免服務器端Redis已經掛掉的狀況下,客戶端還在一直等待響應結果。若是服務器端沒有在規定時間內響應,客戶端應該儘快嘗試去另一個Redis實例請求獲取鎖。

  • 客戶端使用當前時間減去開始獲取鎖時間(步驟1記錄的時間)就獲得獲取鎖使用的時間。當且僅當從大多數(N/2+1,這裏是3個節點)的Redis節點都取到鎖,而且使用的時間小於鎖失效時間時,鎖纔算獲取成功。

  • 若是取到了鎖,key的真正有效時間等於有效時間減去獲取鎖所使用的時間(步驟3計算的結果)。

  • 若是由於某些緣由,獲取鎖失敗(沒有在至少N/2+1個Redis實例取到鎖或者取鎖時間已經超過了有效時間),客戶端應該在全部的Redis實例上進行解鎖(即使某些Redis實例根本就沒有加鎖成功,防止某些節點獲取到鎖可是客戶端沒有獲得響應而致使接下來的一段時間不能被從新獲取鎖)。

針對如上幾點,redisson的實現:

watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=

watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=  

3、基於Redisson的分佈式實現方案

在分析Redisson的源碼前,先重申一下咱們本文的重點放在分佈式鎖的加鎖、鎖重入、未獲取到鎖的線程繼續獲取鎖、釋放鎖四個過程!但願能夠對你們有所幫助。

鎖重入:咱們假設,一次加鎖時間爲30秒,固然Redisson默認的也是30秒,可是業務執行時間大於30秒,若是沒有鎖重入的實現,那麼30秒後鎖失效,業務邏輯就會陷入沒法保證正確性的嚴重後果中。

第一步:添加依賴 

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

在正式編碼前,咱們先看下有關Redisson實現分佈式鎖的核心類之間的關係,以下圖:

watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=

第二步:正式編碼測試代碼

@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest(classes = BootIntegrationComponentApplication.class)
public class ReidsRedLockTest {

    private ExecutorService executorService = Executors.newCachedThreadPool();

    public RedissonRedLock getRedLock(){
        Config config1 = new Config();
        config1.useClusterServers()
                .addNodeAddress("redis://127.0.0.1:9001","redis://127.0.0.1:9002","redis://127.0.0.1:9003"
                        ,"redis://127.0.0.1:9004","redis://127.0.0.1:9005","redis://127.0.0.1:9006")
                .setPassword("123");
        RedissonClient redissonClient1 = Redisson.create(config1);//建立redissonClient對象,設置一系列的redis參數
        RLock rLock1 = redissonClient1.getLock("red_lock");
        //若是有多個redis cluster集羣,則參考如上的寫法建立對應的RLock對象,並傳入下面的RedissonRedLock構造方法中。
        return new RedissonRedLock(rLock1);//獲取redisson紅鎖
    }

    @Test
    public void redisRedLock() throws Exception {
        RedissonRedLock redLock = getRedLock();

        int[] count = {0};
        for (int i = 0; i < 1000; i++) {
            executorService.submit(() -> {
                try {
                    redLock.tryLock(10, TimeUnit.SECONDS);//加鎖
                    count[0]++;
                    Thread.sleep(50000L);
                } catch (Exception e) {
                    log.error("添加分佈式鎖異常:",e);
                } finally {
                    try {
                        redLock.unlock();//釋放鎖
                    } catch (Exception e) {
                        log.error("解除分佈式鎖異常:",e);
                    }
                }
            });
        }
        executorService.shutdown();
        executorService.awaitTermination(1, TimeUnit.HOURS);
        log.info("計算後的結果:{}",count[0]);
    }
}

4、加鎖過程分析

首先咱們將加鎖過程的方法調用棧列出,按照調用步驟分析加鎖的源碼實現:

watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=

由上述調用棧能夠看到,實現加鎖的核心方法是:

watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=

這是一個調用lua腳本的執行過程,接下來對該方法作詳細解釋:

watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=

針對lua腳本中參數佔位符的問題:

  •  KEYS[1] = getName(),
  • ARGV[1] = internalLockLeaseTime 
  • ARGV[2] = getLockName(threadId)

針對getLockName(threadId)方法,在建立redis鏈接管理器時,設置了id = UUID,具體以下

watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=

  咱們假設線程A,執行完上面的lua腳本,而且持有了該分佈式鎖,接下來針對線程A來講,直到業務邏輯結束,釋放鎖以前,該線程A,都將進入鎖重入的環節,一直持續到業務邏輯執行完成,線程主動釋放鎖。而沒有持有鎖的線程,則進入爭搶鎖的過程,一直到持有鎖(至因而公平競爭仍是非公平競爭,咱們先留一個懸念,歡迎各位看官老爺在評論區留言討論)。

5、鎖重入過程分析

再讓咱們回到加鎖過程當中方法調用棧的圖片上,咱們能夠看到方法:

watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=

上圖中的紅框便是鎖重入的實現方法,詳細解釋以下:

watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=

一樣是利用lua腳本實現,

watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=

具體邏輯爲:

  • 咱們假設線程A持有了該鎖,則後臺線程會在該鎖持續了初始失效時間除3取整數的時間節點,作鎖重入的操做。
  • if判斷指定的key是否存在,且是否爲當前線程所持有
  • 若是被當前線程持有,則將失效時間重置爲初始失效時間,redisson默認爲30秒。
  • 若是上面兩步操做成功,則返回1,也便是true;不然返回false。

6、未獲取到鎖的線程繼續獲取鎖

讓咱們將思路繼續回到線程A獲取鎖的邏輯中,咱們經過加鎖方法調用棧能夠看到方法:

public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException 
複製代碼

該方法實屬有些長,咱們就分段截取分析。

watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk= watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=

經過上圖的分析,咱們知道,若是一個線程初次沒有獲取到鎖,則會一直嘗試獲取鎖,直到咱們設置的針對獲取該redis實例鎖的超時時間耗盡才罷休,在這個過程當中沒有獲取到鎖,則認爲在該redis實例獲取鎖失敗。

7、鎖釋放過程分析

咱們仍是先將鎖釋放過程方法調用棧列出:

watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk= watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=

經過上圖能夠看到,在鎖釋放的過程當中,最核心的方法就是:

watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=

分析其lua腳本實現邏輯:

watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=

分析可知,在刪除對應的key以後,會發布一條消息以供其餘未獲取到鎖的線程訂閱,此邏輯和加鎖過程遙相呼應,而且在刪除key以後作了移除鎖重入資格的操做,以保證當前線程完全釋放鎖。

8、易混淆概念

咱們所說的一個redis實例,並非一個Redis集羣中的某一個master節點或者Slave節點,針對redis集羣,一個集羣在redLock算法中只是一個實例節點,至於咱們的key值放在了哪一個slot,是由Redis集羣的一致性算法決定的。一樣對於哨兵模式也是這樣。因此針對RedLock算法來講,若是有N個實例,則是指N個cluster集羣、N個sentinel集羣、N個redis單實例節點。而不是一個集羣中的N個實例。


 

相關文章
相關標籤/搜索