高階程序員之路-輕鬆學習分佈式鎖

前言

隨着互聯網技術的不斷髮展,數據量的不斷增長,業務邏輯日趨複雜,在這種背景下,傳統的集中式系統已經沒法知足咱們的業務需求,分佈式系統被應用在更多的場景,而在分佈式系統中訪問共享資源就須要一種互斥機制,來防止彼此之間的互相干擾,以保證一致性,在這種狀況下,咱們就須要用到分佈式鎖。redis

分佈式一致性問題

首先咱們先來看一個小例子:算法

假設某商城有一個商品庫存剩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. 獲取鎖和釋放鎖的性能要好
  2. 判斷是否得到鎖必須是原子性的,不然可能致使多個請求都獲取到鎖
  3. 網絡中斷或宕機沒法釋放鎖時,鎖必須被清楚,否則會發生死鎖
  4. 可重入一個線程中能夠屢次獲取同一把鎖,好比一個線程在執行一個帶鎖的方法,該方法中又調用了另外一個須要相同鎖的方法,則該線程能夠直接執行調用的方法,而無需從新得到鎖;

5.阻塞鎖和非阻塞鎖,阻塞鎖即沒有獲取到鎖,則繼續等待獲取鎖;非阻塞鎖即沒有獲取到鎖後,不繼續等待,直接返回鎖失敗。網絡

分佈式鎖實現方式

1、數據庫鎖

  1. 基於MySQL鎖表

該實現方式徹底依靠數據庫惟一索引來實現,當想要得到鎖時,即向數據庫中插入一條記錄,釋放鎖時就刪除這條記錄。這種方式存在如下幾個問題:多線程

(1) 鎖沒有失效時間,解鎖失敗會致使死鎖,其餘線程沒法再獲取到鎖,由於惟一索引insert都會返回失敗。併發

(2) 只能是非阻塞鎖,insert失敗直接就報錯了,沒法進入隊列進行重試分佈式

(3) 不可重入,同一線程在沒有釋放鎖以前沒法再獲取到鎖高併發

  1. 採用樂觀鎖增長版本號

根據版本號來判斷更新以前有沒有其餘線程更新過,若是被更新過,則獲取鎖失敗。性能

2、緩存鎖

這裏咱們主要介紹幾種基於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的這些特性,咱們來看看如何利用這些特性來實現分佈式鎖:

  1. 建立一個鎖目錄lock
  2. 但願得到鎖的線程A就在lock目錄下,建立臨時順序節點
  3. 獲取鎖目錄下全部的子節點,而後獲取比本身小的兄弟節點,若是不存在,則說明當前線程順序號最小,得到鎖
  4. 線程B獲取全部節點,判斷本身不是最小節點,設置監聽(watcher)比本身次小的節點(只關注比本身次小的節點是爲了防止發生「羊羣效應」)
  5. 線程A處理完,刪除本身的節點,線程B監聽到變動事件,判斷本身是最小的節點,得到鎖。

小結

在分佈式系統中,共享資源互斥訪問問題很是廣泛,而針對訪問共享資源的互斥問題,經常使用的解決方案就是使用分佈式鎖,這裏只介紹了幾種經常使用的分佈式鎖,分佈式鎖的實現方式還有有不少種,根據業務選擇合適的分佈式鎖,下面對上述幾種鎖進行一下比較:

數據庫鎖:

優勢:直接使用數據庫,使用簡單。

缺點:分佈式系統大多數瓶頸都在數據庫,使用數據庫鎖會增長數據庫負擔。

緩存鎖:

優勢:性能高,實現起來較爲方便,在容許偶發的鎖失效狀況,不影響系統正常使用,建議採用緩存鎖。

缺點:經過鎖超時機制不是十分可靠,當線程得到鎖後,處理時間過長致使鎖超時,就失效了鎖的做用。

zookeeper鎖:

優勢:不依靠超時時間釋放鎖;可靠性高;系統要求高可靠性時,建議採用zookeeper鎖。

缺點:性能比不上緩存鎖,由於要頻繁的建立節點刪除節點。

相關文章
相關標籤/搜索