前言
隨著網際網路技術的不斷髮展,資料量的不斷增加,業務邏輯日趨複雜,在這種背景下,傳統的集中式系統已經無法滿足我們的業務需求,分散式系統被應用在更多的場景,而在分散式系統中訪問共享資源就需要一種互斥機制,來防止彼此之間的互相干擾,以保證一致性,在這種情況下,我們就需要用到分散式鎖。
公眾號
-
全網唯一一個從0開始幫助Java開發者轉做大資料領域的公眾號~
-
公眾號大資料技術與架構或者搜尋import_bigdata關注,大資料學習路線最新更新,已經有很多小夥伴加入了~
分散式一致性問題
首先我們先來看一個小例子:
假設某商城有一個商品庫存剩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,即可達到資源互斥訪問的目的。但是在分散式系統中,由於分散式系統的分佈性,即多執行緒和多程式並且分佈在不同機器中,這兩種鎖將失去原有鎖的效果,需要我們自己實現分散式鎖——分散式鎖。
分散式鎖需要具備哪些條件
-
獲取鎖和釋放鎖的效能要好
-
判斷是否獲得鎖必須是原子性的,否則可能導致多個請求都獲取到鎖
-
網路中斷或當機無法釋放鎖時,鎖必須被清楚,不然會發生死鎖
-
可重入一個執行緒中可以多次獲取同一把鎖,比如一個執行緒在執行一個帶鎖的方法,該方法中又呼叫了另一個需要相同鎖的方法,則該執行緒可以直接執行呼叫的方法,而無需重新獲得鎖;
5.阻塞鎖和非阻塞鎖,阻塞鎖即沒有獲取到鎖,則繼續等待獲取鎖;非阻塞鎖即沒有獲取到鎖後,不繼續等待,直接返回鎖失敗。
分散式鎖實現方式
一、資料庫鎖
一般很少使用資料庫鎖,效能不好並且容易產生死鎖。
1. 基於MySQL鎖表
該實現方式完全依靠資料庫唯一索引來實現,當想要獲得鎖時,即向資料庫中插入一條記錄,釋放鎖時就刪除這條記錄。這種方式存在以下幾個問題:
(1) 鎖沒有失效時間,解鎖失敗會導致死鎖,其他執行緒無法再獲取到鎖,因為唯一索引insert都會返回失敗。
(2) 只能是非阻塞鎖,insert失敗直接就報錯了,無法進入佇列進行重試
(3) 不可重入,同一執行緒在沒有釋放鎖之前無法再獲取到鎖
2. 採用樂觀鎖增加版本號
根據版本號來判斷更新之前有沒有其他執行緒更新過,如果被更新過,則獲取鎖失敗。
二、快取鎖
具體例項可以參考我講述Redis的系列文章,裡面有完整的Redis分散式鎖實現方案
這裡我們主要介紹幾種基於redis實現的分散式鎖:
1. 基於setnx、expire兩個命令來實現
基於setnx(set if not exist)的特點,當快取裡key不存在時,才會去set,否則直接返回false。如果返回true則獲取到鎖,否則獲取鎖失敗,為了防止死鎖,我們再用expire命令對這個key設定一個超時時間來避免。但是這裡看似完美,實則有缺陷,當我們setnx成功後,執行緒發生異常中斷,expire還沒來的及設定,那麼就會產生死鎖。
解決上述問題有兩種方案
第一種是採用redis2.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鎖。
缺點:效能比不上快取鎖,因為要頻繁的建立節點刪除節點。
事實上,大家只要參考引入下面的程式碼: github.com/yujiasun/Di… 這個過程有基於redis和zookeeper分散式工具集-包括:分散式鎖實現,分散式速率限制器,分散式ID生成器等。
重要的事情說三遍:
不要去用一個沒有經過嚴酷環境考驗的自己寫的分散式鎖
不要去用一個沒有經過嚴酷環境考驗的自己寫的分散式鎖
不要去用一個沒有經過嚴酷環境考驗的自己寫的分散式鎖
公眾號
-
全網唯一一個從0開始幫助Java開發者轉做大資料領域的公眾號~
-
公眾號大資料技術與架構或者搜尋import_bigdata關注,大資料學習路線最新更新,已經有很多小夥伴加入了~