基於Zookeeper實現多進程分佈式鎖

1、zookeeper簡介及基本操做java

Zookeeper 並非用來專門存儲數據的,它的做用主要是用來維護和監控你存儲的數據的狀態變化。當對目錄節點監控狀態打開時,一旦目錄節點的狀態發生變化,Watcher 對象的 process 方法就會被調用。node

建立Zookeeper實例時便可綁定一個Watcher對象,如 ZooKeeper zk = new ZooKeeper(zookeeperQuorum, sessionTimeout, Watcher; 任何實現org.apache.zookeeper.Watcher接口的類均可做爲一個Watcher對象。
zookeeperQuorum=IP+端口(xxx.xxx.xxx.xxx:2181,xxx.xxx.xxx.xxx:2181,xxx.xxx.xxx.xxx:2181)多個逗號隔開
能夠設置觀察的操做:exists,getChildren,getData
能夠觸發觀察的操做:create,delete,setData

apache

2、基於zookeeper的分佈式鎖原理session

讓咱們來回顧一下Zookeeper節點的概念:分佈式

 

Zookeeper的數據存儲結構就像一棵樹,這棵樹由節點組成,這種節點叫作Znode。ide

Znode分爲四種類型:測試

1.持久節點 (PERSISTENT)this

默認的節點類型。建立節點的客戶端與zookeeper斷開鏈接後,該節點依舊存在 。spa

2.持久節點順序節點(PERSISTENT_SEQUENTIAL)orm

所謂順序節點,就是在建立節點時,Zookeeper根據建立的時間順序給該節點名稱進行編號:

 


3.臨時節點(EPHEMERAL)

和持久節點相反,當建立節點的客戶端與zookeeper斷開鏈接後,臨時節點會被刪除:

 

4.臨時順序節點(EPHEMERAL_SEQUENTIAL)

顧名思義,臨時順序節點結合和臨時節點和順序節點的特色:在建立節點時,Zookeeper根據建立的時間順序給該節點名稱進行編號;當建立節點的客戶端與zookeeper斷開鏈接後,臨時節點會被刪除。

 

Zookeeper分佈式鎖的原理

Zookeeper分佈式鎖偏偏應用了臨時順序節點。具體如何實現呢?讓咱們來看一看詳細步驟:

獲取鎖

首先,在Zookeeper當中建立一個持久節點ParentLock。當第一個客戶端想要得到鎖時,須要在ParentLock這個節點下面建立一個臨時順序節點 Lock1。

 


以後,Client1查找ParentLock下面全部的臨時順序節點並排序,判斷本身所建立的節點Lock1是否是順序最靠前的一個。若是是第一個節點,則成功得到鎖。

 


這時候,若是再有一個客戶端 Client2 前來獲取鎖,則在ParentLock下載再建立一個臨時順序節點Lock2。


Client2查找ParentLock下面全部的臨時順序節點並排序,判斷本身所建立的節點Lock2是否是順序最靠前的一個,結果發現節點Lock2並非最小的。

因而,Client2向排序僅比它靠前的節點Lock1註冊Watcher,用於監聽Lock1節點是否存在。這意味着Client2搶鎖失敗,進入了等待狀態。

 


這時候,若是又有一個客戶端Client3前來獲取鎖,則在ParentLock下載再建立一個臨時順序節點Lock3。

 


Client3查找ParentLock下面全部的臨時順序節點並排序,判斷本身所建立的節點Lock3是否是順序最靠前的一個,結果一樣發現節點Lock3並非最小的。

因而,Client3向排序僅比它靠前的節點Lock2註冊Watcher,用於監聽Lock2節點是否存在。這意味着Client3一樣搶鎖失敗,進入了等待狀態。

 


這樣一來,Client1獲得了鎖,Client2監聽了Lock1,Client3監聽了Lock2。這偏偏造成了一個等待隊列,很像是Java當中ReentrantLock所依賴的

釋放鎖

釋放鎖分爲兩種狀況:

1.任務完成,客戶端顯示釋放

當任務完成時,Client1會顯示調用刪除節點Lock1的指令。

 


2.任務執行過程當中,客戶端崩潰

得到鎖的Client1在任務執行過程當中,若是Duang的一聲崩潰,則會斷開與Zookeeper服務端的連接。根據臨時節點的特性,相關聯的節點Lock1會隨之自動刪除。

 


因爲Client2一直監聽着Lock1的存在狀態,當Lock1節點被刪除,Client2會馬上收到通知。這時候Client2會再次查詢ParentLock下面的全部節點,確認本身建立的節點Lock2是否是目前最小的節點。若是是最小,則Client2瓜熟蒂落得到了鎖。

 


同理,若是Client2也由於任務完成或者節點崩潰而刪除了節點Lock2,那麼Client3就會接到通知。

 


最終,Client3成功獲得了鎖。

 

3、基於zookeeper的分佈式鎖代碼實現

import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;

import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooDefs;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.Stat;

public class DistributeLock implements Watcher{
  private ZooKeeper zk;
  //當前鎖
  private String current_lock;
  //競爭的資源
  private String lockName;
  //根節點
  private String ROOT_LOCK = "/dlock";
  //因爲zookeeper監聽節點狀態會當即返回,因此須要使用CountDownLatch(也可以使用信號量等其餘機制)
  private CountDownLatch latch;

  public DistributeLock(String zkAddress, String lockName) {
    this.lockName = lockName;
    try {
      zk = new ZooKeeper(zkAddress, 30000, this);
      //獲取根節點狀態
      Stat stat = zk.exists(ROOT_LOCK, false);
      //若是根節點不存在,則建立根節點,根節點類型爲永久節點
      if(stat == null) {
        System.out.println("根節點不存在");
        zk.create(ROOT_LOCK, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE,  CreateMode.PERSISTENT);
      }
    } catch (Exception e) {
      e.printStackTrace();
    }
  }

  //獲取鎖
  public void lock() {
    try {
      //在根節點下建立臨時順序節點,返回值爲建立的節點路徑
      current_lock = zk.create(ROOT_LOCK + "/" + lockName, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE,  CreateMode.EPHEMERAL_SEQUENTIAL);
      //獲取根節點下的全部臨時順序節點,不設置監視器
      List<String> children = zk.getChildren(ROOT_LOCK, false);
      //對根節點下的全部臨時順序節點進行從小到大排序
      Collections.sort(children);
      //判斷當前節點是否爲最小節點,若是是則獲取鎖,若不是,則找到本身的前一個節點,監聽其存在狀態
      int curIndex = Collections.binarySearch(children, current_lock.substring(current_lock.lastIndexOf("/") + 1));
      // if(current_lock.equals(ROOT_LOCK + "/" + children.get(0))) {
      if(curIndex == 0) {
        System.out.println("獲取鎖成功");
        return;
      }else {
        //獲取當前節點前一個節點的路徑
        // String prev = children.get(Collections.binarySearch(children, current_lock) - 1);
        String prev = children.get(curIndex - 1);
        //監聽當前節點的前一個節點的狀態
        Stat stat = zk.exists(ROOT_LOCK + "/" + prev, true);
        //此處再次判斷該節點是否存在,該步驟也可省略
        if(stat == null) {
          System.out.println("獲取鎖成功");
          return;
        }else {
          System.out.println("等待鎖......");
          latch = new CountDownLatch(1);
          //進入等待鎖狀態
          latch.await();
          System.out.println("獲取鎖成功");
          latch = null;
        }
      }
    } catch (KeeperException e) {
      e.printStackTrace();
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }
  //釋放鎖
  public void unlock() {
    try {
      //刪除建立的節點
      zk.delete(current_lock, -1);
      current_lock = null;
      //關閉zookeeper鏈接
      zk.close();
    } catch (InterruptedException e) {
      e.printStackTrace();
    } catch (KeeperException e) {
      e.printStackTrace();
    }
  }

  @Override
  public void process(WatchedEvent event) {
    if(this.latch != null) {
      latch.countDown();
    }
  }
}

 

啓動多個進程進行測試,將如下代碼複製多份,啓動多個進程,觀察輸出結果,能夠看出已成功實現多進程分佈式鎖

import java.text.SimpleDateFormat;
import java.util.Date;

public class Test1{   public static void main(String[] args) throws Exception {     DistributeLock lock = new DistributeLock("127.0.0.1:2181", "lock");     SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");     lock.lock();     System.out.println(sdf.format(new Date()) + "開始執行業務......");     Thread.sleep(30000);     System.out.println(sdf.format(new Date()) + "業務處理結束......");     lock.unlock();   }}

相關文章
相關標籤/搜索