Redis叢集環境下的-RedLock(真分散式鎖) 實踐

搜雲庫技術團隊發表於2018-11-11

在不同程式需要互斥地訪問共享資源時,分散式鎖是一種非常有用的技術手段。 有很多三方庫和文章描述如何用Redis實現一個分散式鎖管理器,但是這些庫實現的方式差別很大,而且很多簡單的實現其實只需採用稍微增加一點複雜的設計就可以獲得更好的可靠性。 這篇文章的目的就是嘗試提出一種官方權威的用Redis實現分散式鎖管理器的演算法,我們把這個演算法稱為RedLock。

Redlock是redis官方提出的實現分散式鎖管理器的演算法。這個演算法會比一般的普通方法更加安全可靠。關於這個演算法的討論可以看下官方文件

github.com/antirez/red…

安全和可靠性保證

在描述我們的設計之前,我們想先提出三個屬性,這三個屬性在我們看來,是實現高效分散式鎖的基礎。

1、一致性:互斥,不管任何時候,只有一個客戶端能持有同一個鎖。 2、分割槽可容忍性:不會死鎖,最終一定會得到鎖,就算一個持有鎖的客戶端宕掉或者發生網路分割槽。 3、可用性:只要大多數Redis節點正常工作,客戶端應該都能獲取和釋放鎖。

為什麼基於故障切換的方案不夠好

為了理解我們想要提高的到底是什麼,我們先看下當前大多數基於Redis的分散式鎖三方庫的現狀。 用Redis來實現分散式鎖最簡單的方式就是在例項裡建立一個鍵值,建立出來的鍵值一般都是有一個超時時間的(這個是Redis自帶的超時特性),所以每個鎖最終都會釋放。

而當一個客戶端想要釋放鎖時,它只需要刪除這個鍵值即可。 表面來看,這個方法似乎很管用,但是這裡存在一個問題:在我們的系統架構裡存在一個單點故障,如果Redis的master節點當機了怎麼辦呢?有人可能會說:加一個slave節點!在master當機時用slave就行了!但是其實這個方案明顯是不可行的,因為這種方案無法保證第1個安全互斥屬性,因為Redis的複製是非同步的。 總的來說,這個方案裡有一個明顯的競爭條件(race condition),舉例來說:

1、客戶端A在master節點拿到了鎖。 2、master節點在把A建立的key寫入slave之前當機了。 3、slave變成了master節點 4、B也得到了和A還持有的相同的鎖(因為原來的slave裡還沒有A持有鎖的資訊)

當然,在某些特殊場景下,前面提到的這個方案則完全沒有問題,比如在當機期間,多個客戶端允許同時都持有鎖,如果你可以容忍這個問題的話,那用這個基於複製的方案就完全沒有問題,否則的話我們還是建議你採用這篇文章裡接下來要描述的方案。

Redlock 簡介

在不同程式需要互斥地訪問共享資源時,分散式鎖是一種非常有用的技術手段。實現高效的分散式鎖有三個屬性需要考慮:

1、安全屬性:互斥,不管什麼時候,只有一個客戶端持有鎖 2、效率屬性A:不會死鎖 3、效率屬性B:容錯,只要大多數redis節點能夠正常工作,客戶端端都能獲取和釋放鎖。

Redlock 演算法

在分散式版本的演算法裡我們假設我們有N個Redis master節點,這些節點都是完全獨立的,我們不用任何複製或者其他隱含的分散式協調演算法。我們已經描述瞭如何在單節點環境下安全地獲取和釋放鎖。因此我們理所當然地應當用這個方法在每個單節點裡來獲取和釋放鎖。在我們的例子裡面我們把N設成5,這個數字是一個相對比較合理的數值,因此我們需要在不同的計算機或者虛擬機器上執行5個master節點來保證他們大多數情況下都不會同時當機。一個客戶端需要做如下操作來獲取鎖:

1、獲取當前時間(單位是毫秒)。

2、輪流用相同的key和隨機值在N個節點上請求鎖,在這一步裡,客戶端在每個master上請求鎖時,會有一個和總的鎖釋放時間相比小的多的超時時間。比如如果鎖自動釋放時間是10秒鐘,那每個節點鎖請求的超時時間可能是5-50毫秒的範圍,這個可以防止一個客戶端在某個宕掉的master節點上阻塞過長時間,如果一個master節點不可用了,我們應該儘快嘗試下一個master節點。

3、客戶端計算第二步中獲取鎖所花的時間,只有當客戶端在大多數master節點上成功獲取了鎖(在這裡是3個),而且總共消耗的時間不超過鎖釋放時間,這個鎖就認為是獲取成功了。

4、如果鎖獲取成功了,那現在鎖自動釋放時間就是最初的鎖釋放時間減去之前獲取鎖所消耗的時間。

5、如果鎖獲取失敗了,不管是因為獲取成功的鎖不超過一半(N/2+1)還是因為總消耗時間超過了鎖釋放時間,客戶端都會到每個master節點上釋放鎖,即便是那些他認為沒有獲取成功的鎖。

Redisson 實現方式(紅鎖 RedLock)

github Redisson github.com/redisson/re…

Maven

<!-- JDK 1.8+ compatible -->
<dependency>
   <groupId>org.redisson</groupId>
   <artifactId>redisson</artifactId>
   <version>3.9.0</version>
</dependency>  

<!-- JDK 1.6+ compatible -->
<dependency>
   <groupId>org.redisson</groupId>
   <artifactId>redisson</artifactId>
   <version>2.14.0</version>
</dependency>
複製程式碼

叢集模式配置

叢集模式除了適用於Redis叢集環境,也適用於任何雲端計算服務商提供的叢集模式,例如AWS ElastiCache叢集版Azure Redis Cache阿里雲(Aliyun)的雲資料庫Redis版

程式化配置叢集的用法:

@Bean
public RedissonClient redissonClient() {
    Config config = new Config();
    config.useClusterServers()
            .setScanInterval(2000) // 叢集狀態掃描間隔時間,單位是毫秒
            //可以用"rediss://"來啟用SSL連線
            .addNodeAddress("redis://127.0.0.1:7000", "redis://127.0.0.1:7001")
            .addNodeAddress("redis://127.0.0.1:7002");
    return Redisson.create(config);
}
複製程式碼

基於Redis的Redisson紅鎖RedissonRedLock物件實現了Redlock介紹的加鎖演算法。該物件也可以用來將多個RLock物件關聯為一個紅鎖,每個RLock物件例項可以來自於不同的Redisson例項。

RLock lock1 = redissonClient1.getLock("lock1");
RLock lock2 = redissonClient2.getLock("lock2");
RLock lock3 = redissonClient3.getLock("lock3");

RedissonRedLock lock = new RedissonRedLock(lock1, lock2, lock3);
// 同時加鎖:lock1 lock2 lock3
// 紅鎖在大部分節點上加鎖成功就算成功。
lock.lock();
...
lock.unlock();
複製程式碼

Redisson 監控鎖

大家都知道,如果負責儲存某些分散式鎖的某些Redis節點當機以後,而且這些鎖正好處於鎖住的狀態時,這些鎖會出現鎖死的狀態。為了避免這種情況的發生,Redisson內部提供了一個監控鎖的看門狗,它的作用是在Redisson例項被關閉前,不斷的延長鎖的有效期。預設情況下,看門狗的檢查鎖的超時時間是30秒鐘,也可以通過修改Config.lockWatchdogTimeout來另行指定。

另外Redisson還通過加鎖的方法提供了leaseTime的引數來指定加鎖的時間。超過這個時間後鎖便自動解開了。

RedissonRedLock lock = new RedissonRedLock(lock1, lock2, lock3);
// 給lock1,lock2,lock3加鎖,如果沒有手動解開的話,10秒鐘後將會自動解開
lock.lock(10, TimeUnit.SECONDS);

// 為加鎖等待100秒時間,並在加鎖成功10秒鐘後自動解開
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
...
lock.unlock();
複製程式碼

往期精彩文章

Redis叢集環境下的-RedLock(真分散式鎖) 實踐

相關文章