基於Redisson實現分散式鎖原始碼解讀

編碼技術團隊發表於2021-07-30

文章目錄

  • 一、分散式鎖的概念 使用場景
  • 二、將redis官網對於分散式鎖(紅鎖)的定義和Redisson實現做概括性總結
  • 三、基於Redisson的分散式實現方案
  • 四、加鎖過程分析
  • 五、鎖重入過程分析
  • 六、未獲取到鎖的執行緒繼續獲取鎖
  • 七、鎖釋放過程分析
  • 八、易混淆概念

 

一、分散式鎖的概念 使用場景

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

  在分散式系統中,常常需要協調他們的動作。如果不同的系統或是同一個系統的不同主機之間共享了一個或一組資源,那麼訪問這些資源的時候,往往需要互斥來防止彼此干擾來保證一致性,這個時候,便需要使用到分散式鎖。

 

二、將redis官網對於分散式鎖(紅鎖)的定義和Redisson實現做概括性總結

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

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

       中文對如上5點做出解釋:

redis紅鎖演算法:

  在Redis的分散式環境中,我們假設有N個Redis master。這些節點完全互相獨立,不存在主從複製或者其他叢集協調機制。我們確保將在N個例項上使用與在Redis單例項下相同方法獲取和釋放鎖。現在我們假設有5個Redis master節點,同時我們需要在5臺伺服器上面執行這些Redis例項,這樣保證他們不會同時都宕掉。

  為了取到鎖,客戶端應該執行以下操作:

  • 1、獲取當前時間,以毫秒為單位。
  • 2、依次嘗試從5個例項,使用相同的key和隨機值(Redisson中給出的是UUID + ThreadId)獲取鎖。當向Redis請求獲取鎖時,客戶端應該設定一個網路連線和響應超時時間(我們接下來會在加鎖的環節多次提到這個時間),這個超時時間應該小於鎖的失效時間。例如你的鎖自動失效時間為10秒,則超時時間應該在5-50毫秒之間。這樣可以避免伺服器端Redis已經掛掉的情況下,客戶端還在一直等待響應結果。如果伺服器端沒有在規定時間內響應,客戶端應該儘快嘗試去另外一個Redis例項請求獲取鎖。
  • 3、客戶端使用當前時間減去開始獲取鎖時間(步驟1記錄的時間)就得到獲取鎖使用的時間。當且僅當從大多數(N/2+1,這裡是3個節點)的Redis節點都取到鎖,並且使用的時間小於鎖失效時間時,鎖才算獲取成功。
  • 4、如果取到了鎖,key的真正有效時間等於有效時間減去獲取鎖所使用的時間(步驟3計算的結果)。
  • 5、如果因為某些原因,獲取鎖失敗(沒有在至少N/2+1個Redis例項取到鎖或者取鎖時間已經超過了有效時間),客戶端應該在所有的Redis例項上進行解鎖(即便某些Redis例項根本就沒有加鎖成功,防止某些節點獲取到鎖但是客戶端沒有得到響應而導致接下來的一段時間不能被重新獲取鎖)。

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

 

 

 

 

 

三、基於Redisson的分散式實現方案

  在分析Redisson的原始碼前,先重申一下我們本文的重點放在分散式鎖的加鎖、鎖重入、未獲取到鎖的執行緒繼續獲取鎖、釋放鎖四個過程!希望可以對大家有所幫助。

  鎖重入:我們假設,一次加鎖時間為30秒,當然Redisson預設的也是30秒,但是業務執行時間大於30秒,如果沒有鎖重入的實現,那麼30秒後鎖失效,業務邏輯就會陷入無法保證正確性的嚴重後果中。

第一步:新增依賴 

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

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

 

 

 

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

@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]);
    }
}

 

四、加鎖過程分析

首先我們將加鎖過程的方法呼叫棧列出,按照呼叫步驟分析加鎖的原始碼實現:

由上述呼叫棧可以看到,實現加鎖的核心方法是:

這是一個呼叫lua指令碼的執行過程,接下來對該方法做詳細解釋:

針對lua指令碼中引數佔位符的問題:

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

針對getLockName(threadId)方法,在建立redis連線管理器時,設定了id = UUID,具體如下

 

 

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

 

五、鎖重入過程分析

再讓我們回到加鎖過程中方法呼叫棧的圖片上,我們可以看到方法:

 

 

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

同樣是利用lua指令碼實現,

具體邏輯為:

  • 0、我們假設執行緒A持有了該鎖,則後臺執行緒會在該鎖持續了初始失效時間除3取整數的時間節點,做鎖重入的操作。
  • 1、if判斷指定的key是否存在,且是否為當前執行緒所持有
  • 2、如果被當前執行緒持有,則將失效時間重置為初始失效時間,redisson預設為30秒。
  • 3、如果上面兩步操作成功,則返回1,也即是true;否則返回false。

 

六、未獲取到鎖的執行緒繼續獲取鎖

讓我們將思路繼續回到執行緒A獲取鎖的邏輯中,我們通過加鎖方法呼叫棧可以看到方法:

public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException 

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

 

  通過上圖的分析,我們知道,如果一個執行緒初次沒有獲取到鎖,則會一直嘗試獲取鎖,直到我們設定的針對獲取該redis例項鎖的超時時間耗盡才罷休,在這個過程中沒有獲取到鎖,則認為在該redis例項獲取鎖失敗。

 

七、鎖釋放過程分析

我們還是先將鎖釋放過程方法呼叫棧列出:

 

 

 

 

通過上圖可以看到,在鎖釋放的過程中,最核心的方法就是:

分析其lua指令碼實現邏輯:

 

 

  分析可知,在刪除對應的key之後,會發布一條訊息以供其他未獲取到鎖的執行緒訂閱,此邏輯和加鎖過程遙相呼應,並且在刪除key之後做了移除鎖重入資格的操作,以保證當前執行緒徹底釋放鎖。

 

八、易混淆概念

        我們所說的一個redis例項,並不是一個Redis叢集中的某一個master節點或者Slave節點,針對redis叢集,一個叢集在redLock演算法中只是一個例項節點,至於我們的key值放在了哪個slot,是由Redis叢集的一致性演算法決定的。同樣對於哨兵模式也是這樣。所以針對RedLock演算法來說,如果有N個例項,則是指N個cluster叢集、N個sentinel叢集、N個redis單例項節點。而不是一個叢集中的N個例項。

 

系列高質量文章,請參閱公眾號:編碼技術團隊 ,承諾 :無廣告!持續推送優質系列原創博文

 

相關文章