「如何設計」:
分散式鎖定義
分散式環境下,鎖定全域性唯一公共資源 表現為:
- 請求序列化
- 互斥性
第一步是上鎖的資源目標,是鎖定全域性唯一公共資源,只有是全域性唯一的資源才存在多個執行緒或服務競爭的情況,互斥性表現為一個資源的隔離級別序列化,如果對照單機事務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的模型,保證可用性。在主從複製中“主”有資料,但可能“從”還沒有資料,這個時候,一旦主掛掉或者網路抖動等各種原因,可能會切換到“從”節點,這個時候有可能會導致兩個業務執行緒同時的獲取到兩把鎖。
- 業務執行緒-1 向主節點請求鎖
- 業務執行緒-1 獲取鎖
- 業務執行緒-1 獲取到鎖並開始執行業務
- 這個時候redis剛生成的鎖在主從之間還未進行同步
- redis這時候主節點掛掉了
- redis的從節點升級為主節點
- 業務執行緒-2 想新的主節點請求鎖
- 業務執行緒-2 獲取到新的主節點返回的鎖
- 業務執行緒-2 獲取到鎖開始執行業務
- 這個時候 業務執行緒-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的每一個節點都存在。
(這個實際上是zk的leader通過二階段提交寫請求來保證的,這個也是zk的叢集規模大了的一個瓶頸點)
zk 鎖實現的原理
說zk的鎖問題之前先看看zookeeper中幾個特性,這幾個特性構建了zk的一把分散式鎖 特性:
-
有序節點
當在一個父目錄下如 /lock 下建立 有序節點,節點會按照嚴格的先後順序建立出自節點 lock000001,lock000002,lock0000003,以此類推,有序節點能嚴格保證各個自節點按照排序命名生成。
-
臨時節點
客戶端建立了一個臨時節點,在客戶端的會話結束或會話超時,zookepper會自動刪除該解ID那。
-
事件監聽
在讀取資料時,我們可以對節點設定監聽,當節點的資料發生變化(1 節點建立 2 節點刪除 3 節點資料變成 4 自節點變成)時,zookeeper會通知客戶端。
結合這幾個特點,來看下zk是怎麼組合分散式鎖。
- 業務執行緒-1 業務執行緒-2 分別向zk的/lock目錄下,申請建立有序的臨時節點
- 業務執行緒-1 搶到/lock0001 的檔案,也就是在整個目錄下最小序的節點,也就是執行緒-1獲取到了鎖
- 業務執行緒-2 只能搶到/lock0002的檔案,並不是最小序的節點,執行緒2未能獲取鎖
- 業務執行緒-1 與 lock0001 建立了連線,並維持了心跳,維持的心跳也就是這把鎖的租期
- 當業務執行緒-1 完成了業務,將釋放掉與zk的連線,也就是釋放了這把鎖
zk分散式鎖的程式碼實現
zk官方提供的客戶端並不支援分散式鎖的直接實現,我們需要自己寫程式碼去利用zk的這幾個特性去進行實現。
zk分散式鎖客戶端假死的問題
客戶端建立了臨時有序節點並建立了事件監聽,就可以讓業務執行緒與zk維持心跳,這個心跳也就是這把鎖的租期。當客戶端的業務執行緒完成了執行就把節點進行刪除,也就釋放了這把鎖,不過中間也可能存在問題
-
客戶端掛掉
因為註冊的是臨時節點,客戶端掛掉,zk會進行感知,也就會把這個臨時節點刪除,鎖也就隨著釋放
-
業務執行緒假死
業務執行緒並沒有訊息,而是一個假死狀態,(例如死迴圈,死鎖,超長gc),這個時候鎖會被一直霸佔不能釋放,這個問題需要從兩個方面進行解決。
第一個是本身業務程式碼的問題,為何會出現死迴圈,死鎖等問題。
第二個是對鎖的異常監控問題,這個其實也是微服務治理的一個方面。
zk分散式鎖 的GC 問題
剛剛說了zk鎖的維持是靠zk和客戶端的心跳進行維持,如果客戶端出現了長時間的GC會出現什麼狀況
- 業務執行緒-1 獲取到鎖,但未開始執行業務
- 業務執行緒-2 發生長時間的GC
- 業務執行緒-1 和 zk 的心跳發生斷鏈
- lock0001 的臨時節點因為心跳斷鏈而被刪除
- 業務執行緒-2 獲取到鎖
- 業務執行緒-2 開始執行業務
- 業務執行緒-1 GC完畢,開始執行業務
- 業務執行緒-1 和 業務執行緒-2 同時執行業務
基於 etcd 分散式鎖
etcd分散式鎖的實現原理
etcd實現分散式鎖比zk要簡單很多,就是使用key value的方式進行寫入,在叢集中,如果存在key的話就不能寫入,也就意味著不能獲取到鎖,如果叢集中,可以寫入key,就意味著獲取得到鎖。
etc到使用了raft保證了叢集的一致性,也就是在外界看來,只要etcd叢集中某一臺機器存在了鎖,所有的機器也就存在了鎖,這個跟zk一樣屬於強一致性,並且資料是可以進行持久化,預設資料一更新就持久化。
鎖的租期續約問題
etcd 並不存在一個心跳的機制,所以跟redis一樣獲取鎖的時候就要對其進行expire的指定,這個時候就存在一個鎖的租期問題。
租期問題有幾種思路可以去解決,這裡討論其中一種:
在獲取到鎖的業務執行緒,可以開啟一個子執行緒去維護和輪訓這把鎖的有效時間,並定時的對這把鎖進行續租
假設業務執行緒獲取到一把鎖,鎖的expire時間為10s,業務執行緒會開啟一個子執行緒通過輪訓的方式每2秒鐘去把這把鎖進行續租,每次都將鎖的expire還原到10s,當業務執行緒執行完業務時,會把這把鎖進行刪除,事件完畢。
這種思路一樣會存在問題:
- 客戶端掛掉,業務執行緒和續租子執行緒都會掛掉,鎖最終會釋放
- 業務執行緒假死,這個跟zk的假死情況一樣,也是屬於業務程式碼應該解決的問題
- 客戶端超長GC問題,長GC導致續租子程式沒有進行及時續租,鎖被超時釋放。(GC的問題可能是個極端問題,一般GC超過幾秒就可能去檢視問題了)
總結
首先得了解清楚我們使用分散式鎖的場景,為何使用分散式鎖,用它來幫我們解決什麼問題,先聊場景後聊分散式鎖的技術選型。
無論是redis,zk,etcd其實在各個場景下或多或少都存在一些問題,例如redis的AP模型會限制很多使用場景,但它卻擁有了幾者中最高的效能,zookeeper的分散式鎖要比redis可靠很多,但他繁瑣的實現機制導致了它的效能不如redis,而且zk會隨著叢集的擴大而效能更加下降。etcd 看似是一種折中的方案,不過像鎖的租期續約都要自己去實現。
簡單來說,先了解業務場景,後進行技術選型。