基於Redis的分散式鎖到底安全嗎(上)
Redlock演算法
基於單Redis節點的分散式鎖
SET resource_name my_random_value NX PX 30000
-
my_random_value
是由客戶端生成的一個隨機字串,它要保證在足夠長的一段時間內在所有客戶端的所有獲取鎖的請求中都是唯一的。 -
NX
表示只有當resource_name
對應的key值不存在的時候才能SET
成功。這保證了只有第一個請求的客戶端才能獲得鎖,而其它客戶端在鎖被釋放之前都無法獲得鎖。 -
PX 30000
表示這個鎖有一個30秒的自動過期時間。當然,這裡30秒只是一個例子,客戶端可以選擇合適的過期時間。
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
SETNX resource_name my_random_value
EXPIRE resource_name 30
-
客戶端1獲取鎖成功。
-
客戶端1在某個操作上阻塞了很長時間。
-
過期時間到了,鎖自動釋放了。
-
客戶端2獲取到了對應同一個資源的鎖。
-
客戶端1從阻塞中恢復過來,釋放掉了客戶端2持有的鎖。
-
客戶端1獲取鎖成功。
-
客戶端1訪問共享資源。
-
客戶端1為了釋放鎖,先執行'GET'操作獲取隨機字串的值。
-
客戶端1判斷隨機字串的值,與預期的值相等。
-
客戶端1由於某個原因阻塞住了很長時間。
-
過期時間到了,鎖自動釋放了。
-
客戶端2獲取到了對應同一個資源的鎖。
-
客戶端1從阻塞中恢復過來,執行
DEL
操縱,釋放掉了客戶端2持有的鎖。
-
客戶端1從Master獲取了鎖。
-
Master宕機了,儲存鎖的key還沒有來得及同步到Slave上。
-
Slave升級為Master。
-
客戶端2從新的Master獲取到了對應同一個資源的鎖。
分散式鎖Redlock
-
獲取當前時間(毫秒數)。
-
按順序依次向N個Redis節點執行獲取鎖的操作。這個獲取操作跟前面基於單Redis節點的獲取鎖的過程相同,包含隨機字串
my_random_value
,也包含過期時間(比如PX 30000
,即鎖的有效時間)。為了保證在某個Redis節點不可用的時候演算法能夠繼續執行,這個獲取鎖的操作還有一個超時時間(time out),它要遠小於鎖的有效時間(幾十毫秒量級)。客戶端在向某個Redis節點獲取鎖失敗以後,應該立即嘗試下一個Redis節點。這裡的失敗,應該包含任何型別的失敗,比如該Redis節點不可用,或者該Redis節點上的鎖已經被其它客戶端持有(注:Redlock原文中這裡只提到了Redis節點不可用的情況,但也應該包含其它的失敗情況)。 -
計算整個獲取鎖的過程總共消耗了多長時間,計算方法是用當前時間減去第1步記錄的時間。如果客戶端從大多數Redis節點(>= N/2+1)成功獲取到了鎖,並且獲取鎖總共消耗的時間沒有超過鎖的有效時間(lock validity time),那麼這時客戶端才認為最終獲取鎖成功;否則,認為最終獲取鎖失敗。
-
如果最終獲取鎖成功了,那麼這個鎖的有效時間應該重新計算,它等於最初的鎖的有效時間減去第3步計算出來的獲取鎖消耗的時間。
-
如果最終獲取鎖失敗了(可能由於獲取到鎖的Redis節點個數少於N/2+1,或者整個獲取鎖的過程消耗的時間超過了鎖的最初有效時間),那麼客戶端應該立即向所有Redis節點發起釋放鎖的操作(即前面介紹的Redis Lua指令碼)。
-
客戶端1成功鎖住了A, B, C,獲取鎖成功(但D和E沒有鎖住)。
-
節點C崩潰重啟了,但客戶端1在C上加的鎖沒有持久化下來,丟失了。
-
節點C重啟後,客戶端2鎖住了C, D, E,獲取鎖成功。
Martin的分析
-
前半部分,與Redlock無關。Martin指出,即使我們擁有一個完美實現的分散式鎖(帶自動過期功能),在沒有共享資源參與進來提供某種fencing機制的前提下,我們仍然不可能獲得足夠的安全性。
-
後半部分,是對Redlock本身的批評。Martin指出,由於Redlock本質上是建立在一個同步模型之上,對系統的記時假設(timing assumption)有很強的要求,因此本身的安全性是不夠的。
-
客戶端1從Redis節點A, B, C成功獲取了鎖(多數節點)。由於網路問題,與D和E通訊失敗。
-
節點C上的時鐘發生了向前跳躍,導致它上面維護的鎖快速過期。
-
客戶端2從Redis節點C, D, E成功獲取了同一個資源的鎖(多數節點)。
-
客戶端1和客戶端2現在都認為自己持有了鎖。
-
客戶端1向Redis節點A, B, C, D, E發起鎖請求。
-
各個Redis節點已經把請求結果返回給了客戶端1,但客戶端1在收到請求結果之前進入了長時間的GC pause。
-
在所有的Redis節點上,鎖過期了。
-
客戶端2在A, B, C, D, E上獲取到了鎖。
-
客戶端1從GC pause從恢復,收到了前面第2步來自各個Redis節點的請求結果。客戶端1認為自己成功獲取到了鎖。
-
客戶端1和客戶端2現在都認為自己持有了鎖。
-
為了效率(efficiency),協調各個客戶端避免做重複的工作。即使鎖偶爾失效了,只是可能把某些操作多做一遍而已,不會產生其它的不良後果。比如重複傳送了一封同樣的email。
-
為了正確性(correctness)。在任何情況下都不允許鎖失效的情況發生,因為一旦發生,就可能意味著資料不一致(inconsistency),資料丟失,檔案損壞,或者其它嚴重的問題。
-
如果是為了效率(efficiency)而使用分散式鎖,允許鎖的偶爾失效,那麼使用單Redis節點的鎖方案就足夠了,簡單而且效率高。Redlock則是個過重的實現(heavyweight)。
-
如果是為了正確性(correctness)在很嚴肅的場合使用分散式鎖,那麼不要使用Redlock。它不是建立在非同步模型上的一個足夠強的演算法,它對於系統模型的假設中包含很多危險的成分(對於timing)。而且,它沒有一個機制能夠提供fencing token。那應該使用什麼技術呢?Martin認為,應該考慮類似Zookeeper的方案,或者支援事務的資料庫。
-
Martin提出的fencing token的方案,需要對提供共享資源的服務進行修改,這在現實中可行嗎?
-
根據Martin的說法,看起來,如果資源伺服器實現了fencing token,它在分散式鎖失效的情況下也仍然能保持資源的互斥訪問。這是不是意味著分散式鎖根本沒有存在的意義了?
-
資源伺服器需要檢查fencing token的大小,如果提供資源訪問的服務也是包含多個節點的(分散式的),那麼這裡怎麼檢查才能保證fencing token在多個節點上是遞增的呢?
-
Martin對於fencing token的舉例中,兩個fencing token到達資源伺服器的順序顛倒了(小的fencing token後到了),這時資源伺服器檢查出了這一問題。如果客戶端1和客戶端2都發生了GC pause,兩個fencing token都延遲了,它們幾乎同時達到了資源伺服器,但保持了順序,那麼資源伺服器是不是就檢查不出問題了?這時對於資源的訪問是不是就發生衝突了?
-
分散式鎖+fencing的方案是絕對正確的嗎?能證明嗎?
由於這個故事實在太長了,所以先總結了前半部分推送出來,請大家閱讀評論。如果不出意外,兩三天之內我會推送下一篇,到時候我們再繼續分析antirez給出的反駁,Hacker News上出現的一些重要討論,以及與分散式鎖相關的一些問題。
(待續)