單機的分佈式鎖因爲只作用在一個Redis節點上,所以可用性上是存在問題的,即使Redis通過sentinel保證高可用,如果這個master節點由於某些原因發生了主從切換,那麼就會出現鎖丟失的情況:
- 1.在Redis的master節點上拿到了鎖;
- 2.但是這個加鎖的key還沒同步到slave節點;
- 3.master故障,發生故障轉移,slave節點升級爲master節點;
- 4.導致鎖丟失。
在Redis的分佈式環境中,我們假設有N個Redis Master。這些節點完全互相獨立,不存在主從複製或者其他集羣協調機制。我們確保將在N個實例上使用與在Redis單實例下相同方法獲取和釋放鎖。現在我們假設有5個Redis master節點,同時我們需要在5臺服務器上面運行這些Redis實例,這樣保證他們不會同時都宕機。
爲了取到鎖,客戶端應該執行以下操作:
- 獲取當前Unix時間,以毫秒爲單位。
- 依次嘗試從5個實例,使用相同的key和具有唯一性的value(例如UUID)獲取鎖。當向Redis請求獲取鎖時,客戶端應該設置一個網絡連接和響應超時時間,這個超時時間應該小於鎖的失效時間。例如你的鎖自動失效時間爲10秒,則超時時間應該在5-50毫秒之間。這樣可以避免服務器端Redis已經掛掉的情況下,客戶端還在死死地等待響應結果。如果服務器端沒有在規定時間內響應,客戶端應該儘快嘗試去另外一個Redis實例請求獲取鎖。
- 客戶端使用當前時間減去開始獲取鎖時間(步驟1記錄的時間)就得到獲取鎖使用的時間。當且僅當大多數(N/2+1,這裏是三個節點)的Redis節點都取到鎖,並且使用的時間小於鎖失效時間時,鎖纔算獲取成功。
- 如果取到了鎖,key的真正有效時間等於有效時間減去獲取鎖所使用的時間(步驟3計算的結果)。
- 如果因爲某些原因,獲取鎖失敗(沒有在至少N/2+1個Redis實例取到獲取鎖或者取鎖時間已經超過了有效時間),客戶端應該在所有的Redis實例上進行解鎖(即使某些Redis實例根本就沒有加鎖成功,防止某些節點獲取到鎖但是客戶端沒有得到響應而導致接下來的一段時間不能被重新獲取鎖)。
使用
redisson已經有對redlock算法封裝
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.3.2</version>
</dependency>
Config config = new Config();
config.useSentinelServers()
.addSentinelAddress("127.0.0.1:6369","127.0.0.1:6379", "127.0.0.1:6389")
.setMasterName("masterName")
.setPassword("password")
.setDatabase(0);
RedissonClient redissonClient = Redisson.create(config);
// 還可以getFairLock(), getReadWriteLock()
RLock redLock = redissonClient.getLock("REDLOCK_KEY");
boolean isLock;
try {
isLock = redLock.tryLock();
// 500ms拿不到鎖, 就認爲獲取鎖失敗。10000ms即10s是鎖失效時間。
isLock = redLock.tryLock(500, 10000, TimeUnit.MILLISECONDS);
if (isLock) {
//TODO if get lock success, do something;
}
} catch (Exception e) {
} finally {
// 無論如何, 最後都要解鎖
redLock.unlock();
}