ZooKeeper典型應用場景:分佈式鎖

  分佈式鎖是控制分佈式系統之間同步訪問共享資源的一種方式。若是不一樣的系統或是同一個系統的不一樣主機之間共享了一個或一組資源,那麼訪問這些資源的時候,每每須要經過一些互斥手段來防止彼此之間的干擾,以保證一致性,在這種狀況下,就須要使用分佈式鎖了。數據庫

  在平時的實際項目開發中,咱們每每不多會去在乎分佈式鎖,而是依賴於關係型數據庫固有的排他性來實現不一樣進程之間的互斥,但大型分佈式系統的性能瓶頸每每集中在數據庫操做上。本文咱們來看看ZooKeeper如何實現分佈式鎖,主要講解排他鎖和共享鎖兩類分佈式鎖。編程

排他鎖

Exclusive Locks,簡稱X鎖,又稱爲寫鎖或獨佔鎖,是一種基本的鎖類型。若是事務T1對數據對象O1加上了排他鎖,那麼在整個加鎖期間,只容許事務T1對O1進行讀取和更新操做,其它任何事務都不能再對這個數據對象進行任何類型的操做——直到T1釋放了排他鎖。服務器

從上面講解的排它鎖的基本概念中,咱們能夠看到,排他鎖的核心是如何保證當前有且僅有一個事務得到鎖,而且鎖被釋放後,全部正在等待獲取鎖的事務都可以被通知到,下面咱們就來看看如何藉助Zookeeper實現排他鎖。網絡

定義鎖

一般的Java開發編程中,有兩種常見的方式能夠用來定義鎖,分別是synchronized機制和JDK5提供的ReentrantLock。在ZooKeeper中,是經過ZooKeeper上的數據節點來表示一個鎖,如/exclusive_lock/lock節點就能夠被定義爲一個鎖。以下圖所示:分佈式

獲取鎖

在須要獲取排他鎖時,全部的客戶端都會試圖經過調用create()接口,在/exclusive_lock節點下建立臨時子節點/exclusive_lock/lock。ZooKeeper會保證在全部的客戶端中,最絡只有一個客戶端可以建立成功,那麼就能夠認爲該客戶端獲取了鎖。同時,全部沒有獲取到鎖的客戶端就須要到/exclusive_lock節點上註冊一個子節點變動的Watcher監聽 , 以便實時監聽到lock節點的變動狀況。性能

釋放鎖

在「定義鎖」的部分,咱們已經提到,/exclusive_lock是一個臨時節點,所以在如下兩種狀況下,可能釋放鎖:3d

  • 當前獲取鎖的客戶端機器宕機,那麼ZooKeeper上的這個臨時節點就會被移除;
  • 正常執行完業務邏輯後,客戶端就會主動將本身建立的臨時節點刪除。

不管什麼狀況下移除了lock節點,ZooKeeper都會通知全部在/exclusive_lock節點上註冊了子節點變動Watcher監聽的客戶端。這些客戶端在接收到通知後,再次從新發起分佈式鎖獲取,即重複「獲取鎖」過程,對象

整個排他鎖的獲取和釋放流程以下圖所示:blog

共享鎖

Shared Locks,簡單S鎖,一樣是一種基本的鎖類型。若是事務T1對數據對象O1加上了共享鎖,那麼當前事務只能對O1進行讀取操做,其它事務也只能對這個數據對象加共享鎖——直到該數據對象上的全部共享鎖都被釋放。共享鎖和排他鎖最根本的區別在於,加上了排他鎖後,數據對象只對一個事務可見,而加上了共享鎖後,數據對全部事務均可見。下面看如何藉助ZooKeeper來實現共享鎖。接口

定義鎖

和排他鎖同樣,一樣是經過ZooKeeper上的數據節點來表示一個鎖,是一個相似於/shared_lock/[Hostname]-請求類型-序號的臨時順序節點,如:

/shared_lock/192.168.0.1-R-00000001,那麼 這個節點就表明了一個共享鎖。

獲取鎖

在須要獲取共享鎖時,全部客戶端都會到/shared_lock這個節點下面建立一個臨時順序節點,若是當前是讀請求,那麼就建立例如 /shared_lock/192.168.0.1-R-00000001的節點。若是是寫請求,那麼就建立例如 /shared_lock/192.168.0.1-W-00000001的節點,以下圖所示:

判斷讀寫順序

根據共享鎖的定義,不一樣的事務均可以同時對同一個數據對象進行讀操做,而更新操做必須在當前沒有任何事務讀寫操做的狀況下進行。基於這個原則來經過ZooKeeper的節點來肯定分佈式讀寫順序,大概能夠分爲以下4個步驟:

  1. 建立完節點後,獲取/shared_lock節點下的全部子節點,並對該節點註冊了子節點變動的Wathcher監聽
  2. 肯定本身的節點序號在全部子節點中的順序
  3. 對於讀請求:

若是沒有比本身序號小的子節點,或是全部比本身序號小的子節點都是讀請求,那麼代表 本身已經成功獲取到了共享鎖,同時開始執行讀取邏輯。

若是比本身序號小的子節點中有寫請求,那麼就須要進入等待;

對於寫請求:

若是本身不是序號最小的子節點,那麼就須要進入等待。

4.接收到Watcher的通知後,重複步驟1。

釋放鎖

釋放鎖的邏輯和排他鎖是一致的,這裏再也不贅述。整個共享鎖的獲取能夠用下圖表示。

羊羣效應

上面講解的這個共享鎖實現,大致上能知足通常分佈式集羣競爭鎖的需求,而且性能都還能夠先——這裏說的通常場景是隻集羣規模不是特別大,通常10臺機器之內。可是機器規模擴大後,會有什麼問題呢?咱們着重來看上面「判斷讀寫順序「過程的步驟3,結合下圖實例,看看實際運行的狀況。

針對如上圖所示的狀況進行分析

1. 192.168.0.1首先進行讀操做,完成後將節點/shared_lock/192.168.0.1-R-00000001刪除。

2. 餘下4臺機器均收到這個節點移除的通知,而後從新從/shared_lock節點上獲取一份新的子節點列表。

3. 每臺機器判斷本身的讀寫順序,其中192.168.0.2檢測到本身序號最小,因而進行寫操做,餘下的機器則繼續等待。

4. 繼續…

能夠看到,192.168.0.1客戶端在移除本身的共享鎖後,Zookeeper發送了子節點更變Watcher通知給全部機器,然而除了給192.168.0.2產生影響外,對其餘機器沒有任何做用。大量的Watcher通知和子節點列表獲取兩個操做會重複運行,這樣會形成系能鞥影響和網絡開銷,更爲嚴重的是,若是同一時間有多個節點對應的客戶端完成事務或事務中斷引發節點小時,Zookeeper服務器就會在短期內向其餘全部客戶端發送大量的事件通知,這就是所謂的羊羣效應。

能夠有以下改動來避免羊羣效應。

1. 客戶端調用create接口常見相似於/shared_lock/[Hostname]-請求類型-序號的臨時順序節點。

2. 客戶端調用getChildren接口獲取全部已經建立的子節點列表(不註冊任何Watcher)。

3. 若是沒法獲取共享鎖,就調用exist接口來對比本身小的節點註冊Watcher。對於讀請求:向比本身序號小的最後一個寫請求節點註冊Watcher監聽。對於寫請求:向比本身序號小的最後一個節點註冊Watcher監聽。

4. 等待Watcher通知,繼續進入步驟2。

此方案改動主要在於:每一個鎖競爭者,只須要關注/shared_lock節點下序號比本身小的那個節點是否存在便可。

相關文章
相關標籤/搜索