淺談分布式鎖
分布式一致性問題
首先我們先來看一個小例子:
假設某商城有一個商品庫存剩10個,用戶A想要買6個,用戶B想要買5個,在理想狀態下,用戶A先買走了6了,庫存減少6個還剩4個,此時用戶B應該無法購買5個,給出數量不足的提示;而在真實情況下,用戶A和B同時獲取到商品剩10個,A買走6個,在A更新庫存之前,B又買走了5個,此時B更新庫存,商品還剩5個,這就是典型的電商“秒殺”活動。
從上述例子不難看出,在高並發情況下,如果不做處理將會出現各種不可預知的後果。那麽在這種高並發多線程的情況下,解決問題最有效最普遍的方法就是給共享資源或對共享資源的操作加一把鎖,來保證對資源的訪問互斥。在Java JDK已經為我們提供了這樣的鎖,利用ReentrantLcok或者synchronized,即可達到資源互斥訪問的目的。但是在分布式系統中,由於分布式系統的分布性,即多線程和多進程並且分布在不同機器中,這兩種鎖將失去原有鎖的效果,需要我們自己實現分布式鎖——分布式鎖。
分布式鎖需要具備哪些條件
-
獲取鎖和釋放鎖的性能要好
-
判斷是否獲得鎖必須是原子性的,否則可能導致多個請求都獲取到鎖
-
網絡中斷或宕機無法釋放鎖時,鎖必須被清楚,不然會發生死鎖
-
可重入一個線程中可以多次獲取同一把鎖,比如一個線程在執行一個帶鎖的方法,該方法中又調用了另一個需要相同鎖的方法,則該線程可以直接執行調用的方法,而無需重新獲得鎖;
-
阻塞鎖和非阻塞鎖,阻塞鎖即沒有獲取到鎖,則繼續等待獲取鎖;非阻塞鎖即沒有獲取到鎖後,不繼續等待,直接返回鎖失敗。
分布式鎖實現方式
一、數據庫鎖
1. 基於MySQL鎖表
該實現方式完全依靠數據庫唯一索引來實現,當想要獲得鎖時,即向數據庫中插入一條記錄,釋放鎖時就刪除這條記錄。這種方式存在以下幾個問題:
(1) 鎖沒有失效時間,解鎖失敗會導致死鎖,其他線程無法再獲取到鎖,因為唯一索引insert都會返回失敗。
(2) 只能是非阻塞鎖,insert失敗直接就報錯了,無法進入隊列進行重試
(3) 不可重入,同一線程在沒有釋放鎖之前無法再獲取到鎖
2. 采用樂觀鎖增加版本號
根據版本號來判斷更新之前有沒有其他線程更新過,如果被更新過,則獲取鎖失敗。
二、緩存鎖
這裏我們主要介紹幾種基於Redis實現的分布式鎖:
1. 基於setnx、expire兩個命令來實現
基於setnx(set if not exist)的特點,當緩存裏key不存在時,才會去set,否則直接返回false。如果返回true則獲取到鎖,否則獲取鎖失敗,為了防止死鎖,我們再用expire命令對這個key設置一個超時時間來避免。但是這裏看似完美,實則有缺陷,當我們setnx成功後,線程發生異常中斷,expire還沒來的及設置,那麽就會產生死鎖。
解決上述問題有兩種方案:
第一種是采用Redis 2.6.12版本以後的set,它提供了一系列選項
- EX seconds – 設置鍵key的過期時間,單位時秒
- PX milliseconds – 設置鍵key的過期時間,單位時毫秒
- NX – 只有鍵key不存在的時候才會設置key的值
- XX – 只有鍵key存在的時候才會設置key的值
第二種采用setnx(),get(),getset()實現,大體的實現過程如下:
(1) 線程Asetnx,值為超時的時間戳(t1),如果返回true,獲得鎖。
(2) 線程B用get 命令獲取t1,與當前時間戳比較,判斷是否超時,沒超時false,如果已超時執行步驟3
(3) 計算新的超時時間t2,使用getset命令返回t3(這個值可能其他線程已經修改過),如果t1==t3,獲得鎖,如果t1!=t3說明鎖被其他線程獲取了
(4) 獲取鎖後,處理完業務邏輯,再去判斷鎖是否超時,如果沒超時刪除鎖,如果已超時,不用處理(防止刪除其他線程的鎖)
2. RedLock算法
RedLock算法是Redis作者推薦的一種分布式鎖實現方式,算法的內容如下:
(1) 獲取當前時間;
(2) 嘗試從5個相互獨立Redis客戶端獲取鎖;
(3) 計算獲取所有鎖消耗的時間,當且僅當客戶端從多數節點獲取鎖,並且獲取鎖的時間小於鎖的有效時間,認為獲得鎖;
(4) 重新計算有效期時間,原有效時間減去獲取鎖消耗的時間;
(5) 刪除所有實例的鎖
RedLock算法相對於單節點Redis鎖可靠性要更高,但是實現起來條件也較為苛刻。
(1) 必須部署5個節點才能讓RedLock的可靠性更強。
(2) 需要請求5個節點才能獲取到鎖,通過Future的方式,先並發向5個節點請求,再一起獲得響應結果,能縮短響應時間,不過還是比單節點Redis鎖要耗費更多時間。
然後由於必須獲取到5個節點中的3個以上,所以可能出現獲取鎖沖突,即大家都獲得了1-2把鎖,結果誰也不能獲取到鎖,這個問題,Redis作者借鑒了Raft算法的精髓,通過沖突後在隨機時間開始,可以大大降低沖突時間,但是這問題並不能很好的避免,特別是在第一次獲取鎖的時候,所以獲取鎖的時間成本增加了。
如果5個節點有2個宕機,此時鎖的可用性會極大降低,首先必須等待這兩個宕機節點的結果超時才能返回,另外只有3個節點,客戶端必須獲取到這全部3個節點的鎖才能擁有鎖,難度也加大了。
如果出現網絡分區,那麽可能出現客戶端永遠也無法獲取鎖的情況,介於這種情況,下面我們來看一種更可靠的分布式鎖ZooKeeper鎖。
ZooKeeper分布式鎖
首先我們來了解一下ZooKeeper的特性,看看它為什麽適合做分布式鎖,ZooKeeper是一個為分布式應用提供一致性服務的軟件,它內部是一個分層的文件系統目錄樹結構,規定統一個目錄下只能有一個唯一文件名。
數據模型:
- 永久節點:節點創建後,不會因為會話失效而消失
- 臨時節點:與永久節點相反,如果客戶端連接失效,則立即刪除節點
- 順序節點:與上述兩個節點特性類似,如果指定創建這類節點時,zk會自動在節點名後加一個數字後綴,並且是有序的。
監視器(watcher):
- 當創建一個節點時,可以註冊一個該節點的監視器,當節點狀態發生改變時,watch被觸發時,ZooKeeper將會向客戶端發送且僅發送一條通知,因為watch只能被觸發一次。
根據ZooKeeper的這些特性,我們來看看如何利用這些特性來實現分布式鎖:
-
創建一個鎖目錄lock
-
希望獲得鎖的線程A就在lock目錄下,創建臨時順序節點
-
獲取鎖目錄下所有的子節點,然後獲取比自己小的兄弟節點,如果不存在,則說明當前線程順序號最小,獲得鎖
-
線程B獲取所有節點,判斷自己不是最小節點,設置監聽(watcher)比自己次小的節點(只關註比自己次小的節點是為了防止發生“羊群效應”)
-
線程A處理完,刪除自己的節點,線程B監聽到變更事件,判斷自己是最小的節點,獲得鎖。
小結
在分布式系統中,共享資源互斥訪問問題非常普遍,而針對訪問共享資源的互斥問題,常用的解決方案就是使用分布式鎖,這裏只介紹了幾種常用的分布式鎖,分布式鎖的實現方式還有有很多種,根據業務選擇合適的分布式鎖,下面對上述幾種鎖進行一下比較:
數據庫鎖:
- 優點:直接使用數據庫,使用簡單。
- 缺點:分布式系統大多數瓶頸都在數據庫,使用數據庫鎖會增加數據庫負擔。
緩存鎖:
- 優點:性能高,實現起來較為方便,在允許偶發的鎖失效情況,不影響系統正常使用,建議采用緩存鎖。
- 缺點:通過鎖超時機制不是十分可靠,當線程獲得鎖後,處理時間過長導致鎖超時,就失效了鎖的作用。
zookeeper鎖:
- 優點:不依靠超時時間釋放鎖;可靠性高;系統要求高可靠性時,建議采用zookeeper鎖。
淺談分布式鎖