「如何設計」高可用的分散式鎖

陳於喆發表於2019-06-04

「如何設計」:

「如何設計」具備冪等性的服務
「如何設計」微服務中高可用的分散式鎖

分散式鎖定義

分散式環境下,鎖定全域性唯一公共資源 表現為:

  • 請求序列化
  • 互斥性

第一步是上鎖的資源目標,是鎖定全域性唯一公共資源,只有是全域性唯一的資源才存在多個執行緒或服務競爭的情況,互斥性表現為一個資源的隔離級別序列化,如果對照單機事務ACID的隔離型來說,互斥性的事務隔離級別是SERLALIZABLE,屬於最高的隔離級別。

(事務隔離級別:DEFAULT,READ_UNCOMMITTED,READ_COMMITED,REPEATABLE_READ,SERLALIZABLE)

分散式鎖目的

  • 解決業務層冪等性
  • 解決MQ消費端多次接受同一訊息
  • 確保序列 | 隔離級別
  • 多臺機器同時執行定時任務

尋找唯一資源進行上鎖

例子:

1. 防止使用者重複下單 共享資源進行上鎖的物件 : 【使用者id】

2. 訂單生成後傳送MQ給消費者進行積分的新增 尋找上鎖的物件 :【訂單id】

3. 使用者已經建立訂單,準備對訂單進行支付,同時商家在對這個訂單進行改價 尋找上鎖物件 : 【訂單id】
複製程式碼

基於redis分散式鎖

redis單執行緒序列處理天然就是解決序列化問題,用來解決分散式鎖是再適合不過。

實現方式:
setnx key value Expire_time
獲取到鎖 返回 1 , 獲取失敗 返回 0

存在問題:

鎖時間不可控

redis 只能在setnx指定一個鎖的超時時間,假設初始設定鎖的時間是10秒鐘,但是業務獲取到鎖跑了20秒鐘,在10秒鐘之後,如果又有一個業務可以獲取到相同的一把鎖,這個時候可能就存在兩個相同的業務都獲取得到鎖的問題,並且兩個業務處在並行階段。也就是第一個獲取鎖的業務無法對自身的鎖進行續租。

單點連線超時問題

redis 的client與server端並沒有維持心跳的機制,如果在連線出現問題,client會得到一個超時的回饋。

主從問題

redis的叢集實際上在CAP模式中是處在與AP的模型,保證可用性。在主從複製中“主”有資料,但可能“從”還沒有資料,這個時候,一旦主掛掉或者網路抖動等各種原因,可能會切換到“從”節點,這個時候有可能會導致兩個業務執行緒同時的獲取到兩把鎖。

image

  1. 業務執行緒-1 向主節點請求鎖
  2. 業務執行緒-1 獲取鎖
  3. 業務執行緒-1 獲取到鎖並開始執行業務
  4. 這個時候redis剛生成的鎖在主從之間還未進行同步
  5. redis這時候主節點掛掉了
  6. redis的從節點升級為主節點
  7. 業務執行緒-2 想新的主節點請求鎖
  8. 業務執行緒-2 獲取到新的主節點返回的鎖
  9. 業務執行緒-2 獲取到鎖開始執行業務
  10. 這個時候 業務執行緒-1 和 業務執行緒-2 同時在執行任務
redlock

上述的問題其實並不是redis的缺陷,只是redis採用了AP模型,它本身無法確保我們對一致性的要求。redis官方推薦redlock演算法來保證,問題是redlock至少需要三個redis主從例項來實現,維護成本比較高,相當於redlock使用三個redis叢集實現了自己的另一套一致性演算法,比較繁瑣,在業界也使用得比較少。

能不能使用redis作為分散式鎖

能不能使用redis作為分散式鎖,這個本身就不是redis的問題,還是取決於業務場景,我們先要自己確認我們的場景是適合 AP 還是 CP , 如果在社交發帖等場景下,我們並沒有非常強的事務一致性問題,redis提供給我們高效能的AP模型是非常適合的,但如果是交易型別,對資料一致性非常敏感的場景,我們可能要尋在一種更加適合的 CP 模型

redis可能作為高可用的分散式鎖並不合適,我們需要確立高可用分散式鎖的設計目標

高可用分散式鎖設計目標

  • 強一致性,是CP模型
  • 服務高可用,不存在單點問題
  • 鎖能夠續租和自動釋放
  • 業務接入簡單

三種分散式鎖方案對比

- redis zookeeper etcd
一致性演算法 zab raft
CAP AP CP CP
高可用 主從 N+1 N+1
實現 setnx create臨時有序節點 restful

基於zookeeper分散式鎖

剛剛也分析過,redis其實無法確保資料的一致性,先來看zookeeper是否合適作為我們需要的分散式鎖,首先zk的模式是CP模型,也就是說,當zk鎖提供給我們進行訪問的時候,在zk叢集中能確保這把鎖在zk的每一個節點都存在。

image

(這個實際上是zk的leader通過二階段提交寫請求來保證的,這個也是zk的叢集規模大了的一個瓶頸點)

zk 鎖實現的原理

說zk的鎖問題之前先看看zookeeper中幾個特性,這幾個特性構建了zk的一把分散式鎖 特性:

  • 有序節點

    當在一個父目錄下如 /lock 下建立 有序節點,節點會按照嚴格的先後順序建立出自節點 lock000001,lock000002,lock0000003,以此類推,有序節點能嚴格保證各個自節點按照排序命名生成。

  • 臨時節點

    客戶端建立了一個臨時節點,在客戶端的會話結束或會話超時,zookepper會自動刪除該解ID那。

  • 事件監聽

    在讀取資料時,我們可以對節點設定監聽,當節點的資料發生變化(1 節點建立 2 節點刪除 3 節點資料變成 4 自節點變成)時,zookeeper會通知客戶端。

結合這幾個特點,來看下zk是怎麼組合分散式鎖。

image

  1. 業務執行緒-1 業務執行緒-2 分別向zk的/lock目錄下,申請建立有序的臨時節點
  2. 業務執行緒-1 搶到/lock0001 的檔案,也就是在整個目錄下最小序的節點,也就是執行緒-1獲取到了鎖
  3. 業務執行緒-2 只能搶到/lock0002的檔案,並不是最小序的節點,執行緒2未能獲取鎖
  4. 業務執行緒-1 與 lock0001 建立了連線,並維持了心跳,維持的心跳也就是這把鎖的租期
  5. 當業務執行緒-1 完成了業務,將釋放掉與zk的連線,也就是釋放了這把鎖
zk分散式鎖的程式碼實現

zk官方提供的客戶端並不支援分散式鎖的直接實現,我們需要自己寫程式碼去利用zk的這幾個特性去進行實現。

image

zk分散式鎖客戶端假死的問題

客戶端建立了臨時有序節點並建立了事件監聽,就可以讓業務執行緒與zk維持心跳,這個心跳也就是這把鎖的租期。當客戶端的業務執行緒完成了執行就把節點進行刪除,也就釋放了這把鎖,不過中間也可能存在問題

  • 客戶端掛掉

    因為註冊的是臨時節點,客戶端掛掉,zk會進行感知,也就會把這個臨時節點刪除,鎖也就隨著釋放

  • 業務執行緒假死

    業務執行緒並沒有訊息,而是一個假死狀態,(例如死迴圈,死鎖,超長gc),這個時候鎖會被一直霸佔不能釋放,這個問題需要從兩個方面進行解決。

    第一個是本身業務程式碼的問題,為何會出現死迴圈,死鎖等問題。

    第二個是對鎖的異常監控問題,這個其實也是微服務治理的一個方面。

zk分散式鎖 的GC 問題

剛剛說了zk鎖的維持是靠zk和客戶端的心跳進行維持,如果客戶端出現了長時間的GC會出現什麼狀況

image

  1. 業務執行緒-1 獲取到鎖,但未開始執行業務
  2. 業務執行緒-2 發生長時間的GC
  3. 業務執行緒-1 和 zk 的心跳發生斷鏈
  4. lock0001 的臨時節點因為心跳斷鏈而被刪除
  5. 業務執行緒-2 獲取到鎖
  6. 業務執行緒-2 開始執行業務
  7. 業務執行緒-1 GC完畢,開始執行業務
  8. 業務執行緒-1 和 業務執行緒-2 同時執行業務

基於 etcd 分散式鎖

etcd分散式鎖的實現原理

etcd實現分散式鎖比zk要簡單很多,就是使用key value的方式進行寫入,在叢集中,如果存在key的話就不能寫入,也就意味著不能獲取到鎖,如果叢集中,可以寫入key,就意味著獲取得到鎖。

etc到使用了raft保證了叢集的一致性,也就是在外界看來,只要etcd叢集中某一臺機器存在了鎖,所有的機器也就存在了鎖,這個跟zk一樣屬於強一致性,並且資料是可以進行持久化,預設資料一更新就持久化。

image

鎖的租期續約問題

etcd 並不存在一個心跳的機制,所以跟redis一樣獲取鎖的時候就要對其進行expire的指定,這個時候就存在一個鎖的租期問題。

租期問題有幾種思路可以去解決,這裡討論其中一種:

在獲取到鎖的業務執行緒,可以開啟一個子執行緒去維護和輪訓這把鎖的有效時間,並定時的對這把鎖進行續租

image

假設業務執行緒獲取到一把鎖,鎖的expire時間為10s,業務執行緒會開啟一個子執行緒通過輪訓的方式每2秒鐘去把這把鎖進行續租,每次都將鎖的expire還原到10s,當業務執行緒執行完業務時,會把這把鎖進行刪除,事件完畢。

這種思路一樣會存在問題:

  1. 客戶端掛掉,業務執行緒和續租子執行緒都會掛掉,鎖最終會釋放
  2. 業務執行緒假死,這個跟zk的假死情況一樣,也是屬於業務程式碼應該解決的問題
  3. 客戶端超長GC問題,長GC導致續租子程式沒有進行及時續租,鎖被超時釋放。(GC的問題可能是個極端問題,一般GC超過幾秒就可能去檢視問題了)

總結

首先得了解清楚我們使用分散式鎖的場景,為何使用分散式鎖,用它來幫我們解決什麼問題,先聊場景後聊分散式鎖的技術選型。

無論是redis,zk,etcd其實在各個場景下或多或少都存在一些問題,例如redis的AP模型會限制很多使用場景,但它卻擁有了幾者中最高的效能,zookeeper的分散式鎖要比redis可靠很多,但他繁瑣的實現機制導致了它的效能不如redis,而且zk會隨著叢集的擴大而效能更加下降。etcd 看似是一種折中的方案,不過像鎖的租期續約都要自己去實現。

簡單來說,先了解業務場景,後進行技術選型。

相關文章