分佈式鎖

一、概念

什麼是分佈式?java

  • 分佈式的 CAP 理論告訴咱們:任何一個分佈式系統都沒法同時知足一 致性(Consistency)、可用性(Availability)和分區容錯性(Partition tolerance),最多隻能同時知足兩項。
  • 目前不少大型網站及應用都是分佈式部署的,分佈式場景中的數據一 致性問題一直是一個比較重要的話題。基於 CAP理理論,不少系統在設計之初就要對這三者作出取捨。在互聯⽹網領域的絕大多數的場景中,都須要犧牲強一致性來換取系統的高可用性,系統每每只須要保證最終一致 性,咱們爲了保證數據的最終一致性,須要不少的技術方案來支持,⽐如分佈式事務、分佈式鎖等。

什麼是鎖?mysql

  • 在單進程的系統中,當存在多個線程能夠同時改變某個變量(可變共享變量)時,就須要對變量或代碼塊作同步,使其在修改這種變量時可以線性執行消除併發修改變量。
  • 而同步的本質是經過鎖來實現的。爲了實現多個線程在一個時刻同一 個代碼塊只能有一個線程可執⾏,那麼須要在某個地⽅作個標記,這個標記必須每一個線程都能看到,當標記不存在時能夠設置該標記,其他後續線程發現已經有標記了則等待擁有標記的線程結束同步代碼塊取消標記後再去嘗試設置標記。這個標記能夠理理解爲鎖。
  • 不同地⽅實現鎖的⽅式也不同樣,只要能知足全部線程都能看獲得標記便可。如 Java 中 synchronize 是在對象頭設置標記,Lock 接口的實現類基本上都只是某一個 volitile 修飾的 int 型變量其保證每一個線程都能擁有對該 int 的可見性和原子修改,linux 內核中也是利⽤互斥量或信號量等內存數據作標記。
  • 除了利用內存數據作鎖其實任何互斥的都能作鎖(只考慮互斥狀況),如流水錶中流水號與時間結合作冪等校驗能夠看做是一個不會釋放的鎖,或者使用某個文件是否存在做爲鎖等。只須要滿⾜在對標記進⾏修改能保證原子性和內存可見性便可。

什麼是分佈式鎖?linux

  • Java線程中的鎖,基於Java的內存模型,每一個線程有自⼰的內存空間,多線程鎖是存在一個JVM之中的,若是操做的數據不在一個JVM 中,多線程中鎖就失效了,這種狀況下分佈式鎖就誕⽣了,即多個Java 實例、甚⾄不必定是Java程序、或多個系統須要操做同一個副本數據的時候,須要一個指揮交通的人指定操做的前後順序,這就是分佈式鎖的概念。
  • 在傳統的基於數據庫的架構中,對於數據的搶佔問題每每是經過數據庫事務(ACID)來保證的。在分佈式環境中,出於對性能以及一致性敏感度的要求,使得分佈式鎖成爲了一種比較常見而高效的解決方案。

分佈式鎖使用目的和場景redis

  • 效率:使用分佈式鎖能夠避免不同節點重複相同的工做,這些工做會浪費資源。好比用戶付了錢以後有可能不同節點會發出多封短信。
  • 正確性:加分佈式鎖一樣能夠避免破壞正確性的發⽣,若是兩個節點在同一條數據上面操做,好比多個節點機器器對同一個訂單操做不同的流程有可能會致使該筆訂單最後狀態出現錯誤,形成損失。

分佈式鎖特色算法

  • 互斥性:和咱們本地鎖同樣互斥性是最基本,可是分佈式鎖須要保證在不同節點的不同線程的互斥。
  • 可重入性:同一個節點上的同一個線程若是獲取了鎖以後那麼也能夠再次獲取這個鎖(避免死鎖)。
  • 鎖超時:和本地鎖同樣支持鎖超時,防止死鎖。
  • 高效,⾼可用:加鎖和解鎖須要高效,同時也須要保證高可⽤防⽌分佈式鎖失效,能夠增長降級。
  • 支持阻塞和非阻塞:和ReentrantLock同樣支持lock和trylock以及 tryLock(long timeOut)。
  • ⽀持公平鎖和⾮公平鎖(可選):公平鎖的意思是按照請求加鎖的順序得到鎖,非公平鎖就相反是無序的。這個通常來講實現的比較少。

二、常見的分佈式鎖

咱們了解了一些特色以後,咱們通常實現分佈式鎖有如下幾個方式:sql

  • MySql
  • zookeeper
  • Redis
  • 自研分佈式鎖:如谷歌的Chubby。

Mysql分佈式鎖數據庫

  • 建立一張 distribution_lock表
  • mysql悲觀鎖的實現方式: 事務+ for update的⽅式
  • mysql樂觀鎖實現⽅式:咱們能夠對咱們的表加⼀個版本號字段,那麼咱們查詢出來一個版本號以後,update或者delete的時候須要依賴咱們查詢出來的版本號,判斷當前數據庫和查詢出來的版本號是否相等,若是相等那麼就能夠執行,若是不等那麼就不能執行。這樣的一個策略略很像咱們的CAS(Compare And Swap),⽐較並交換是一個原子操做。這樣咱們就能避免加select * for update行鎖的開銷。

Mysql釋放鎖超時編程

咱們有可能會遇到咱們的機器節點掛了,那麼這個鎖就不會獲得釋放,咱們能夠啓動一個定時任務,經過計算通常咱們處理任務的⼀般的時間,好比是10ms,那麼咱們能夠稍微擴⼤一點,當這個鎖超過100ms沒有被釋放咱們就能夠認定是節點掛了而後將其直接釋放。緩存

Mysql小結安全

優勢:理解起來簡單,不須要維護額外的第三⽅中間件(好比 Redis,Zk)。

缺點:雖然容易理解可是實現起來較爲繁瑣,須要本身考慮鎖超時,加事務等等。性能侷限於數據庫,通常對比緩存來講性能較低。對於高併發的場景並不是很適合。

ZooKeeper分佈式鎖

ZooKeeper也是咱們常見的實現分佈式鎖方法,ZooKeeper是以Paxos算法爲基礎分佈式應⽤程序協調服務。Zk的數據節點和文件目錄相似,因此咱們能夠用此特性實現分佈式鎖。咱們以某個資源爲目錄,而後這個目錄下面的節點就是咱們須要獲取鎖的客戶端,未獲取到鎖的客戶端註冊須要註冊Watcher到上一個客戶端,能夠用下圖表示。

/lock是咱們⽤用於加鎖的⽬目錄,/resource_name是咱們鎖定的資源,其下⾯的節點按照咱們加鎖的順序排列。

鎖超時失效時間

Zookeeper不須要配置鎖超時失效時間,因爲咱們設置節點是臨時節點,咱們的每一個機器維護着一個zookeeper的session,經過這個session,ZK能夠判斷機器器是否宕機。若是咱們的機器掛掉的話,那麼這個臨時節點對應的就會被刪除,因此咱們不須要關心鎖超時失效時間。

zookeeper小結

優勢:ZK能夠不須要關心鎖超時失效時間,實現起來有現成的第三方包,⽐較⽅便,而且支持讀寫鎖,ZK獲取鎖會按照加鎖的順序,因此其是公平鎖。對於⾼可用利用ZK集羣進⾏保證。

缺點:ZK須要額外維護,增長維護成本,性能和Mysql相差不大,依然⽐較差。而且須要開發⼈員了解ZK是什麼。

Redis分佈鎖

你們在網上搜索分佈式鎖,恐怕最多的實現就是Redis了了,Redis由於其性能好,實現起來簡單因此讓不少人都對其十分青睞。

Redis分佈式鎖簡單實現

熟悉Redis的同窗那麼確定對setNx(set if not exist)方法不陌⽣,若是不存在則更新,其能夠很好的用來實現咱們的分佈式鎖。對於某個資源加鎖咱們只須要 setNx resourceName value這裏有個問題,加鎖了以後若是機器宕機那麼這個鎖就不會獲得釋放因此會加入過時時間,加⼊過時時間須要和setNx同一個原子操做,在Redis2.8以前咱們須要使用Lua腳本達到咱們的目的,可是redis2.8以後redis⽀持nx和ex操做是同 一原子操做。set resourceName value ex 5 nx

Redis小結

優勢:對於Redis實現簡單,性能對比ZK和Mysql較好。若是不須要特別複雜的要求,那麼自⼰就能夠利⽤setNx進⾏實現,若是自⼰須要複雜的需求的話那麼能夠利用或者借鑑Redission。對於一些要求比較嚴格的場景來講的話可使用RedLock。

缺點:須要維護Redis集羣,若是要實現RedLock那麼須要維護更多的集羣。

三、常見的分佈式鎖框架

curator框架實現了zookeeper版的分佈式鎖:Curator實現了可重入鎖 (InterProcessMutex),也實現了不可重入鎖(InterProcessSemaphoreMutex)。在可重入鎖中還實現了讀寫鎖。

  • 加鎖的流程具體以下:

1. 首先進⾏可重入的斷定:這里的可重入鎖記錄在ConcurrentMap<Thread, LockData> threadData這個Map里面,若是threadData.get(currentThread) 是有值的那麼就證實是可重入鎖,而後記錄就會加1。咱們以前的Mysql其實也能夠經過這種⽅法去優化,能夠不須要count字段的值,將這個維護在本地能夠提升性能。

2. 而後在咱們的資源目錄下建立一個節點:⽐如這裏建立一個/0000000002這個節點,這個節點須要設置爲EPHEMERAL_SEQUENTIAL也就是臨時節點而且有序。

3. 獲取當前目錄下全部子節點,判斷⾃己的節點是否位於子節點第一個。

4. 若是是第一個,則獲取到鎖,那麼能夠返回。

5. 若是不是第一個,則證實前面已經有人獲取到鎖了,那麼須要獲取⾃⼰節點的前一個節點。/0000000002的前一個節點是/0000000001,咱們獲取到這個節點以後,再上面註冊Watcher(這里的watcher其實調用的是 object.notifyAll(),⽤來解除阻塞)。

6. object.wait(timeout)或object.wait():進行阻塞等待這⾥和咱們第5步的 watcher相對應。

  • 解鎖的具體流程:

1. ⾸先進⾏可重⼊鎖的斷定:若是有可重入鎖只須要次數減1便可,減1以後加鎖次數爲0的話繼續下面步驟,不爲0直接返回。

2. 刪除當前節點。

3. 刪除threadDataMap⾥面的可重入鎖的數據。

Redission框架實現了redis版的分佈式鎖:可重入、阻塞、讀寫、紅鎖、連鎖等。 Redission封裝了鎖的實現,其繼承了了java.util.concurrent.locks.Lock的接⼝,Redission不僅提供了Java自帶的一些方法(lock,tryLock),還提供了異步加鎖,對於異步編程更加方便。

1. 嘗試加鎖:⾸先會嘗試進行加鎖,因爲保證操做是原⼦性,那麼就只能使用lua腳本。能夠看見他並無使用咱們的sexNx來進⾏操做,而是使用的hash結構,咱們的每個須要鎖定的資源均可以看作是一個HashMap,鎖定資源的節點信息是Key,鎖定次數是value。經過這種方式能夠很好的實現可重入的效果,只須要對value進行加1操做,就能進⾏可重 鎖。固然這⾥也能夠⽤以前咱們說的本地計數進⾏優化。

2. 若是嘗試加鎖失敗,判斷是否超時,若是超時則返回false。

3. 若是加鎖失敗以後,沒有超時,那麼須要在名字爲 redisson_lock__channel+lockName的channel上進行訂閱,用於訂閱解鎖消息,而後一直阻塞直到超時,或者有解鎖消息。

4. 重試步驟1,2,3,直到最後獲取到鎖,或者某一步獲取鎖超時。

5. redission的unlock⽅方法比較簡單也是經過lua腳本進⾏解鎖,若是是可重⼊入鎖,只是減1。若是是非加鎖線程解鎖,那麼解鎖失敗。

四、分佈式鎖的安全問題

1. 咱們想象⼀個這樣的場景當機器A申請到⼀把鎖以後,若是Redis主宕機了了,這個時候從機並無同步到這一把鎖,那麼機器B再次申請的時候就會再次申請到這把鎖,

2. 長時間的GC pause,在GC的時候會發生STW(stop-the-world),例如 CMS垃圾回收器,他會有兩個階段進行STW防止引用繼續進⾏變化。 (redis、zk、mysql)

3. 時鐘發生跳躍:對於Redis服務器若是其時間發⽣了向跳躍,那麼確定會影響咱們鎖的過時時間,那麼咱們的鎖過時時間就不是咱們預期的了,也會出現client1和client2獲取到同一把鎖,那麼也會出現不安全,這個對於Mysql也會出現。可是ZK因爲沒有設置過時時間,那麼發生跳躍也不會受影響。

4. ⻓時間的網絡I/O:這個問題和咱們的GC的STW很像,也就是咱們這個獲取了鎖以後咱們進⾏網絡調用,其調用時間由可能比咱們鎖的過時時間都還長,那麼也會出現不安全的問題,這個Mysql也會有,ZK也不會出現這個問題。

五、⼩結

咱們主要講了多種分佈式鎖的實現⽅法,以及他們的一些優缺點。最後也說了一下有關於分佈式鎖的安全的問題,對於不同的業務須要的安全程度徹底不同,咱們須要根據⾃己的業務場景,經過不同的維度分析,選取最適合本身的方案。

相關文章
相關標籤/搜索
本站公眾號
   歡迎關注本站公眾號,獲取更多信息