Java架構直通車——RedLock是否可以做分佈式鎖

引入:Redis SetNX侷限性

我在Java架構直通車——基於Redis的Set NX實現分佈式鎖一文中寫了,採用SetNX會出現兩個問題,一個是超時問題,可以採用隨機值解決。另一個是主從切換或者宕機恢復後的鎖的問題,這個問題是否能用RedLock解決呢?

所以首先來看下RedLock的原理。

RedLock原理

Redis 官方站這篇文章提出了一種權威的基於 Redis 實現分佈式鎖的方式名叫 Redlock,此種方式比原先的單節點的方法更安全。它可以保證以下特性:

  • 安全特性:互斥訪問,即永遠只有一個 client 能拿到鎖
  • 避免死鎖:最終 client 都可能拿到鎖,不會出現死鎖的情況,即使原本鎖住某資源的 client crash 了或者出現了網絡分區
  • 容錯性:只要大部分 Redis 節點存活就可以正常提供服務

接下來介紹算法的原理。
算法很易懂,起 5 個 master 節點,分佈在不同的機房儘量保證可用性。爲了獲得鎖,client 會進行如下操作:

  1. 得到當前的時間,微秒單位
  2. 嘗試順序地在 5 個實例上申請鎖,當然需要使用相同的 key 和 random value,這裏一個 client 需要合理設置與 master 節點溝通的 timeout 大小,避免長時間和一個 fail 了的節點浪費時間
  3. 當 client 在大於等於 3 個 master 上成功申請到鎖的時候,且它會計算申請鎖消耗了多少時間,這部分消耗的時間採用獲得鎖的當下時間減去第一步獲得的時間戳得到,如果鎖的持續時長(lock validity time)比流逝的時間多的話,那麼鎖就真正獲取到了。
  4. 如果鎖申請到了,那麼鎖真正的 lock validity time 應該是 origin(lock validity time) - 申請鎖期間流逝的時間
  5. 如果 client 申請鎖失敗了,那麼它就會在少部分申請成功鎖的 master 節點上執行釋放鎖的操作,重置狀態

也就是滿足多數決,以及滿足剩餘有效時間大於0,那麼就可以獲取到鎖。

解決宕機恢復後鎖的問題

那麼我們回過頭來看之前的問題,如果有節點宕機了怎麼辦?

如果我們的節點沒有持久化機制,client 從 5 個 master 中的 3 個處獲得了鎖,然後其中一個重啓了,這是注意 整個環境中又出現了 3 個 master 可供另一個 client 申請同一把鎖! 違反了互斥性。

如果我們開啓了 AOF 持久化那麼情況會稍微好轉一些,因爲 Redis 的過期機制是語義層面實現的,所以在 server 掛了的時候時間依舊在流逝,重啓之後鎖狀態不會受到污染。

但是考慮斷電之後呢,AOF部分命令沒來得及刷回磁盤直接丟失了,除非我們配置刷回策略爲 fsnyc = always,但這會損傷性能。

解決這個問題的方法是,當一個節點重啓之後,我們規定 在 max TTL 期間它是不可用的 ,這樣它就不會干擾原本已經申請到的鎖,等到它 crash 前的那部分鎖都過期了,環境不存在歷史鎖了,那麼再把這個節點加進來正常工作。

單機鎖和多機鎖的比較

考慮選用單機的鎖還是多機的鎖,就要明確你爲什麼使用分佈式鎖,爲了性能還是正確性?爲了幫你區分這二者,在這把鎖 fail 了的時候你可以詢問自己以下問題:

  • 性能的: 擁有這把鎖使得你不會重複勞動(例如一個 job 做了兩次),如果這把鎖 fail 了,兩個節點同時做了這個 Job,那麼這個 Job 增加了你的成本。
  • 正確性的: 擁有鎖可以防止併發操作污染你的系統或者數據,如果這把鎖 fail 了兩個節點同時操作了一份數據,結果可能是數據不一致、數據丟失、file 衝突等,會導致嚴重的後果。

如果你只是爲了性能,那沒必要用 Redlock,它成本高且複雜,你只用一個 Redis 實例也夠了,最多加個從防止主掛了。當然,你使用單節點的 Redis 那麼斷電或者一些情況下,你會丟失鎖,但是你的目的只是加速性能且斷電這種事情不會經常發生,這並不是什麼大問題。並且如果你使用了單節點 Redis,那麼很顯然你這個應用需要的鎖粒度是很模糊粗糙的,也不會是什麼重要的服務。

那麼是否 Redlock 對於要求正確性的場景就合適呢?Martin 列舉了若干場景證明 Redlock 這種算法是不可靠的。

爲什麼RedLock是不可靠的!

得到鎖的 client1 在持有鎖的期間 pause 了一段時間,例如 GC 停頓。鎖有過期時間(一般叫租約,爲了防止某個 client 崩潰之後一直佔有鎖),但是如果 GC 停頓太長超過了鎖租約時間,此時鎖已經被另一個 client2 所得到,原先的 client1 還沒有感知到鎖過期,那麼奇怪的結果就會發生。

也就是已經持有鎖的線程無法感知到自己已經失去了鎖。

修復問題的方法也很簡單:你需要在每次寫操作時加入一個 fencing token(柵欄)。這個場景下,fencing token 可以是一個遞增的數字(lock service 可以做到),每次有 client 申請鎖就遞增一次。

client1 申請鎖同時拿到 token33,然後它進入長時間的停頓鎖也過期了。client2 得到鎖和 token34 寫入數據,緊接着 client1 活過來之後嘗試寫入數據,自身 token33 比 34 小因此寫入操作被拒絕。注意這需要存儲層來檢查 token,但這並不難實現

  • 如果你使用 Zookeeper 作爲 lock service 的話那麼你可以使用 zxid 作爲遞增數字。
  • 而你使用Redis,是不是可以使用時間戳呢?

不過獲取到時間戳也是不可靠的。學術界有個說法,算法對時間不做假設:因爲進程可能pause一段時間、數據包可能因爲網絡延遲延後到達、時鐘可能根本就是錯的。而可靠的算法依舊要在上述假設下做正確的事情。

Martin 認爲 Redlock 實在不是一個好的選擇,對於需求性能的分佈式鎖應用它太重了且成本高;對於需求正確性的應用來說它不夠安全。因爲它對高危的時鐘或者說其他上述列舉的情況進行了不可靠的假設,如果你的應用只需要高性能的分佈式鎖不要求多高的正確性,那麼單節點 Redis 夠了如果你的應用想要保住正確性,那麼不建議 Redlock,建議使用一個合適的一致性協調系統,例如 Zookeeper,且保證存在 fencing token

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