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(); }}