redis分佈式鎖和2個房客的故事你聽過嗎


爲什麼需要分佈式鎖這裏就不贅述了。常見的分佈式鎖實現方案有Redis、Zookeeper,數據庫。

設計一個分佈式鎖,至少應該保證以下3個方面:

  1. 安全: 獨享(相互排斥)。在任意一個時刻,只有一個客戶端持有鎖。
  2. 無死鎖:即便是天塌下來,也要鎖能釋放。
  3. 容錯。只要大部分Redis節點都活着,客戶端就可以獲取和釋放鎖。

redis部署方案一般有這3種。

  1. 單機模式

  2. master-slave + sentinel

  3. redis cluster模式

我們看這3種如何實現分佈式鎖。

  • 單機模式:正常情況下,單機模式沒什麼大問題,就怕萬一redis掛了就完蛋了。一般我們不會採用單機版,這裏之所以提到它,是因爲單機版的鎖是其他方案的基礎。redis鎖的命令是:
SET resource_name my_value NX PX ms 
//NX是指如果key不存在就就返回true,key存在返回false,PX可以指定過期時間

如果簡單的setnx,有些場景下會有問題。

《倆個房客的故事》 long long a ago,有A、B倆個房客前往同一家酒店,又都看上了同一間房,但是一間房同時只能容納一個人,經過一番舌槍脣戰,A搶到了第一次。A預計自己半個小時能完事,於是就開了半個小時的鐘點房。可是A這次發揮超常,30分鐘還沒完事,但是由於30分鐘時間已經到了,房間鎖自動打開(房間空置狀態),可以接下一位客人。這時B來了,進入房間,鎖上門。正準備幹事的時候,A幹完事出去的時候把門打開了(釋放鎖),房間變成空置狀態。A和B都很尷尬。

怎麼辦?爲了不讓B打擾,A想到了一個辦法,讓祕書A1每隔10分鐘就把鐘點房的時間重新設置成30分鐘。等A事幹完了再開門(釋放鎖)。

爲了避免A開了B上的鎖,酒店想出了一個辦法,指紋上鎖,開鎖的時候也必須用這個指紋。這樣A就開不了B上的鎖。

回到程序,比如A來setnx,默認過期時間30秒,獲取到鎖,但是A比較墨跡,鎖過期(自動釋放鎖)的時候還在執行,這時候B獲取鎖。等A執行完來釋放鎖的時候,其實釋放的是B的鎖。這個時候就需要多一層判斷。A和B,set的my_value必須不一樣(參考故事中的指紋)。當A釋放鎖的時候先判斷是否是自己的鎖,如果是自己的鎖再釋放。可以通過以下Lua腳本實現:

if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

也可以在自己的業務中實現,關鍵點就是my_value必須唯一,能區分開A和B。

至於A的問題,我們另起一個線程,來監控A的過期時間,每隔10秒鐘就把A的鎖過期時間設置成30秒,直到A釋放鎖。

  • 主從模式,單機版有單點故障,那master-slave應該沒問題了吧。master掛了slave頂上。但是請注意,master與slave之間數據同步是異步的。就是說master掛了的時候,可能有寫數據並沒有同步到slave。這時slave成爲master的時候還是丟了鎖。比如A獲取到某資源的鎖,這時master掛了,恰巧鎖還沒同步到slave。這時slave晉升爲master的時候並沒有A的鎖,這時B過來獲取資源鎖的時候就成功。安全性得不到保障。當然如果訪問量小這個模式完全夠了,哪有那麼巧的事。即便是正好趕上,由於訪問量小,彌補也比較容易。

  • 如果訪問量大,對安全要求高,就得另尋出路。redis官方給出了一個解決算法。就是Redlock算法。它要求有N組(臺)redis互相獨立的節點,它們互相獨立,沒有主從,也沒有集羣。客戶端要做的是(以5臺服務爲例):

1. 獲取當前Unix時間,以毫秒爲單位。
2. 依次嘗試從N個實例,使用相同的key和隨機值獲取鎖。當然向Redis設置鎖時,客戶端應該設置一個網絡連接和響應超時時間,這個超時時間應該小於鎖的失效時間。例如你的鎖自動失效時間爲10秒,則超時時間應該在5-50毫秒之間。這樣可以避免服務器端Redis已經掛掉的情況下,客戶端還在死死地等待響應結果。如果服務器端沒有在規定時間內響應,客戶端應該儘快嘗試另外一個Redis實例。
3. 客戶端使用當前時間減去開始獲取鎖時間(步驟1記錄的時間)就得到獲取鎖使用的時間。當且僅當從大多數(N/2+1 )的Redis節點都取到鎖,並且使用的時間小於鎖失效時間時,鎖纔算獲取成功。
4. 如果取到了鎖,key的真正有效時間等於有效時間減去獲取鎖所使用的時間(步驟3計算的結果)。
5. 如果因爲某些原因,獲取鎖失敗(沒有在至少N/2+1個Redis實例取到鎖或者取鎖時間已經超過了有效時間),客戶端應該在所有的Redis實例上進行解鎖。

上面這段來源redis中文網。我總結的大概流程是

  1. 依次嘗試從N個實例獲取鎖,注意的是獲取鎖的時間要遠小於鎖超時的時間。
  2. 當且僅當從大多數(N/2+1 )的Redis節點都取到鎖纔算成功,否則就是失敗。

只有當N/2+1個節點取到鎖纔是算成功。釋放鎖比較簡單,就是釋放每個節點的鎖。

當然Redlock解決了分佈式鎖的基本問題,別忘了它也有《倆個房客的故事》中的問題,解決方案和單機版的redis基本一樣。我們可以基於redis-client原生api來實現Redlock算法,也可以用一些框架,比如Redisson。

  • Redisson實現Redlock。語法就比較簡單
        RLock rLock1 = redissonRed1.getLock(lockKey);
        RLock rLock2 = redissonRed2.getLock(lockKey);
        RLock rLock3 = redissonRed2.getLock(lockKey);
        RedissonRedLock rLock = new RedissonRedLock(rLock1,rLock2,rLock3);
        rLock.lock();
        try {
      //搞事情....
        } finally {
         rLock.unlock();
        }

總結

我們介紹了redis鎖的一些基本要求,和常見問題,以及解決方案。當然還有其他的問題,望大家一起討論。至於Redlock的實現,建議用框架,可以少care一些細節。最後揭祕《倆個房客的故事》純屬本人虛構,如有雷同,天理不容。


本文分享自微信公衆號 - 程序員書單(CoderBooklist)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章