在Java中操作Zookeeper,使用Zookeeper實現分佈式鎖


 

在java中操作zk

依賴

<dependency>
    <groupId>org.apache.zookeeper</groupId>
    <artifactId>zookeeper</artifactId>
    <version>3.6.0</version>
</dependency>

 

連接到zk server

//連接字符串,zkServer的ip、port,如果是集羣逗號分隔
String connectStr = "192.168.1.1:2181";

//zookeeper對象就是一個zkCli
ZooKeeper zooKeeper = null;

try {
//初始次數爲1。後面要在內部類中使用,三種寫法:1、寫成外部類成員變量,不用加final;2、作爲函數局部變量,放在try外面,寫成final;3、寫在try中,不加final
    CountDownLatch countDownLatch = new CountDownLatch(1);
    //超時時間ms,監聽器
    zooKeeper = new ZooKeeper(connectStr, 5000, new Watcher() {
        public void process(WatchedEvent watchedEvent) {
            //如果狀態變成已連接
            if (watchedEvent.getState().equals(Event.KeeperState.SyncConnected)) {
                System.out.println("連接成功");
                //次數-1
                countDownLatch.countDown();
            }
        }
    });
    //等待,次數爲0時纔會繼續往下執行。等待監聽器監聽到連接成功,才能操作zk
    countDownLatch.await();
} catch (IOException | InterruptedException e) {
    e.printStackTrace();
}


//...操作zk。後面的demo都是寫在此處的


//關閉連接
try {
    zooKeeper.close();
} catch (InterruptedException e) {
    e.printStackTrace();
}

 

檢測節點是否存在

// 檢測節點是否存在

// 同步方式
Stat exists = null;
try {
    //如果存在,返回節點狀態Stat;如果不存在,返回null。第二個參數是watch
    exists = zooKeeper.exists("/mall",false);
} catch (KeeperException | InterruptedException e) {
    e.printStackTrace();
}
if (exists==null){
    System.out.println("節點不存在");
}
else {
    System.out.println("節點存在");
}


//異步回調
zooKeeper.exists("/mall",false, new AsyncCallback.StatCallback() {
    //第二個是path znode路徑,第三個是ctx 後面傳入實參,第四個是znode的狀態
    public void processResult(int i, String s, Object o, Stat stat) {
        //如果節點不存在,返回的stat是null
        if (stat==null){
            System.out.println("節點不存在");
        }
        else{
            System.out.println("節點存在");
        }
    }
// 傳入ctx,Object類型
},null);

操作後,服務端會返回處理結果,返回的void、null也算處理結果。

同步指的是當前線程阻塞,等待服務端返回數據,收到返回的數據才繼續往下執行;異步回調指的是,把對結果(返回的數據)的處理寫在回調函數中,當前線程繼續往下執行,收到返回的數據時自動調用回調函數來處理。

 

創建節點

//創建節點

//同步方式
try {
    //數據要寫成byte[],不攜帶數據寫成null;默認acl權限使用ZooDefs.Ids.OPEN_ACL_UNSAFE;最後一個是節點類型,P是永久,E是臨時,S是有序
    zooKeeper.create("/mall", "abcd".getBytes(),  ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
    System.out.println("已創建節點/mall");
    //如果節點已存在,會拋出異常
} catch (KeeperException | InterruptedException e) {
     System.out.println("創建節點/mall失敗,請檢查節點是否已存在");
    e.printStackTrace();
}


//異步回調
zooKeeper.create("/mall", "abcd".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT, new AsyncCallback.Create2Callback(){
    //第二個path,第三個ctx,第四個節點狀態
    public void processResult(int i, String s, Object o, String s1, Stat stat) {
        //回調方式不拋出異常,返回的stat是創建節點的狀態,如果節點已存在,返回的stat是null
        if (stat==null){
            System.out.println("創建節點/mall失敗,請檢查節點是否已存在");
        }
        else {
            System.out.println("節點創建成功");
        }
    }
    //ctx實參
},null);

 

刪除節點

 //刪除節點

//同步方式
try {
    //第二個參數是版本號,-1表示可以是任何版本
    zooKeeper.delete("/mall1",-1);
    System.out.println("成功刪除節點/mall");
} catch (InterruptedException | KeeperException e) {
    System.out.println("刪除節點/mall失敗");
    e.printStackTrace();
}


//異步回調
zooKeeper.delete("/mall2", -1, new AsyncCallback.VoidCallback() {
    //第二個是path,第三個是ctx
    public void processResult(int i, String s, Object o) {
        
    }
//
//ctx實參
},null);

delete()只能刪除沒有子節點的znode,如果該znode有子節點會拋出異常。

沒有提供遞歸刪除子節點的方法,如果要刪除帶有子節點的znode,需要自己實現遞歸刪除。可以先getChildren()獲取子節點列表,遍歷列表依次刪除子節點,再刪除父節點。

 

獲取子節點列表

//獲取子節點列表,List<String>,比如/mall/user,/mall/order,返回的是["user"、"order"]

//同步方式
List<String> children = null;
try {
    //第二個參數是watch
    children = zooKeeper.getChildren("/mall", false);
} catch (KeeperException | InterruptedException e) {
    e.printStackTrace();
}
System.out.println("子節點列表:" + children);


//異步
zooKeeper.getChildren("/mall", false, new AsyncCallback.ChildrenCallback() {
    //第二個起依次是:path、ctx、返回的子節點列表
    public void processResult(int i, String s, Object o, List<String> list) {
        System.out.println("子節點列表:" + list);
    }
//ctx實參
}, null);

只獲取子節點,不獲取孫節點。

watch可以寫布爾值,要添加監聽就寫true,不監聽寫false;也可以寫成Watcher對象,new一個Watcher對象表示要監聽,null表示不監聽。

 

獲取節點數據

//獲取節點數據,返回byte[]

//同步方式
byte[] data = null;
try {
    //第二個參數是watch,第三個是stat
    data = zooKeeper.getData("/mall", false, null);
} catch (KeeperException | InterruptedException e) {
    e.printStackTrace();
}
//調用new String()時要判斷data是否爲null,如果是null會拋NPE
if (data==null){
    System.out.println("該節點沒有數據");
}
else{
    System.out.println("節點數據:"+new String(data));
}


//異步回調
zooKeeper.getData("/mall", false, new AsyncCallback.DataCallback() {
    //第二個起依次是:path、ctx、返回的節點數據、節點狀態
    public void processResult(int i, String s, Object o, byte[] bytes, Stat stat) {
        //不必判斷bytes是否是null,如果節點沒有數據,不會調用回調函數;執行到此,說明bytes不是null
        System.out.println("節點數據:" + new String(bytes) );
    }
    //ctx實參
}, null);

 

設置、更新節點數據

//設置|更新節點據

//同步方式
try {
    //最後一個參數是版本號
    zooKeeper.setData("/mall", "1234".getBytes(), -1);
    System.out.println("設置節點數據成功");
} catch (KeeperException | InterruptedException e) {
    System.out.println("設置節點數據失敗");
    e.printStackTrace();
}


//異步回調
zooKeeper.setData("/mall", "1234".getBytes(), -1, new AsyncCallback.StatCallback() {
    //第二個是path,第三個是ctx
    public void processResult(int i, String s, Object o, Stat stat) {

    }
// ctx
},null);

 

設置acl權限

//設置acl權限
    
//第一個參數指定權限,第二個是Id對象
ACL acl = new ACL(ZooDefs.Perms.ALL, new Id("auth", "chy:abcd"));

List<ACL> aclList = new ArrayList<>();
aclList.add(acl);

//如果List中只有一個ACL對象,也可以這樣寫
//List<ACL> aclList = Collections.singletonList(auth);
    
//驗證權限,需寫在設置權限之前。如果之前沒有設置權限,也需要先驗證本次即將設置的用戶
zooKeeper.addAuthInfo("digest","chy:abcd".getBytes());


//方式一  setAcl
try {
    //第二個參數是List<ACL>,第三個參數是版本號
    zooKeeper.setACL("/mall", aclList, -1);
    System.out.println("設置權限成功");
} catch (KeeperException | InterruptedException e) {
    System.out.println("設置權限失敗");
    e.printStackTrace();
}


//方式二 在創建節點時設置權限
try {
    zooKeeper.create("/mall","abcd".getBytes(),aclList,CreateMode.PERSISTENT);
    System.out.println("已創建節點並設置權限");
} catch (KeeperException | InterruptedException e) {
    e.printStackTrace();
}

設置權限之後,連接zk server進行操作時,需要先驗證用戶。
此處未寫對應的異步回調。

 

查看acl權限

 //查看acl權限
    
//設置權限之後,以後操作時需要先驗證用戶,一次session中驗證一次即可
zooKeeper.addAuthInfo("digest","chy:abcd".getBytes());


//同步方式
try {
    List<ACL> aclList = zooKeeper.getACL("/mall", null);
    System.out.println("acl權限:"+aclList);
} catch (KeeperException | InterruptedException e) {
    System.out.println("獲取acl權限失敗");
    e.printStackTrace();
}


//異步回調
zooKeeper.getACL("/mall3", null, new AsyncCallback.ACLCallback() {
    //第二個起:path、ctx、獲取到的List<ACL>、節點狀態
    public void processResult(int i, String s, Object o, List<ACL> list, Stat stat) {
        //就算沒有手動設置acl權限,默認也是有值的
        System.out.println("acl權限:"+list);
    }
//ctx實參
},null);

 

添加監聽

//添加監聽  方式一
try {
    CountDownLatch countDownLatch = new CountDownLatch(1);

    zooKeeper.getData("/mall", new Watcher() {
        public void process(WatchedEvent watchedEvent) {
            //watcher會監聽該節點所有的事件,不管發生什麼事件都會調用process()來處理,需要先判斷一下事件類型
            if (watchedEvent.getType().equals(Event.EventType.NodeDataChanged)){
                System.out.println("節點數據改變了");
                //會一直監聽,如果只監聽一次數據改變,將下面這句代碼取消註釋即可
                //countDownLatch.countDown();
            }
        }
    }, null);
    //默認watcher是一次性的,如果要一直監聽,需要藉助CountDownLatch
    countDownLatch.await();
} catch (KeeperException | InterruptedException e) {
    e.printStackTrace();
}

exists()、getData()、getChildren()都具有添加監聽的功能,用法類似。

會遞歸監聽子孫節點,子孫節點的數據改變也算NodeDataChanged,子孫節點的創建|刪除也算NodeCreated|NodeDeleted。

 

//添加監聽  方式二   可指定是否遞歸監聽子孫節點   
try {
    CountDownLatch countDownLatch1 = new CountDownLatch(1);
    zooKeeper.addWatch("/mall", new Watcher() {
        @Override
        public void process(WatchedEvent watchedEvent) {
            if (watchedEvent.getType().equals(Event.EventType.NodeDataChanged)){
                System.out.println("節點數據改變了");
                //如果只監聽一次數據改變,將下面這句代碼註釋掉
                //countDownLatch1.countDown();
            }
        }
    //監聽模式,PERSISTENT是不監聽子孫節點,PERSISTENT_RECURSIVE是遞歸監聽子孫節點
    }, AddWatchMode.PERSISTENT_RECURSIVE);
    countDownLatch1.await();
} catch (KeeperException | InterruptedException e) {
    e.printStackTrace();
}

countDownLatch1.await(); 會阻塞線程,最好啓動一條新線程來監聽。

 

移除監聽

//移除監聽 方式一
try {
    zooKeeper.addWatch("/mall",null,AddWatchMode.PERSISTENT);
    System.out.println("已移除監聽");
} catch (KeeperException | InterruptedException e) {
    e.printStackTrace();
}

就是上面添加監聽的那些方法,watch|watcher參數,如果是boolean類型,設置爲false即可關閉監聽;如果是Watcher類型,可設置爲null覆蓋之前設置的監聽。

會移除整個監聽,不需要傳入監聽對象watcher。

 

//移除監聽 方式二
try {
    //第二個參數是Watcher,原來添加的那個Watcher監聽對象,不能是null
    //第三個參數指定要移除監聽的哪部分,Any是移除整個監聽,Data是移除對數據的監聽,Children是移除對子節點的遞歸監聽
    //最後一個參數指定未連接到zkServe時,是否移除本地監聽部分
    zooKeeper.removeWatches("/mall",watcher, Watcher.WatcherType.Children,true);
} catch (InterruptedException | KeeperException e) {
    e.printStackTrace();
}

監聽由2部分組成,一部分在zkServer上,事件發生時通知對應的zkCli;一部分在zkCli(本地),收到zkServer的通知時做出一些處理。最後一個參數指定未連接到zkServer時,是否移除本地(zkCli)監聽部分。如果移除了本地監聽,就算zkServer進行通知,zkCli也不會做出反應。

這種方式功能更全,可以指定移除監聽的哪個部分,但需要傳入watcher對象,添加監聽時要用一個變量來保存watcher對象。

 

使用zk實現分佈式鎖的3種方式

第1種
創建一個臨時znode,如果創建成功,則獲取到鎖,操作完刪除znode釋放鎖;
如果創建失敗(拋出異常),說明鎖被其它線程持有,當前線程休眠一小會兒,之後重試,設置一個計數器,重試指定次數後還沒有獲取到鎖就放棄。

缺點:未獲取到鎖時會重試多次,浪費資源

 

第2種
在第一種的基礎修改,如果創建失敗,給znode加一個watcher,監聽節點刪除事件(釋放鎖),當前線程休眠,節點刪除事件發生時喚醒線程。

缺點:會發生驚羣現象,如果多個線程同時等待鎖,釋放鎖後等待鎖的線程都會被喚醒,但只有一個線程可以獲取到鎖,其它線程剛醒來又要沉睡。

 

第3種
在第二種的基礎上改,要獲取鎖時先在指定節點下創建有序的臨時znode作爲子節點,
獲取指定節點的子節點數量,如果只有1個子節點,則當前線程獲取到鎖,操作完刪除自己創建的節點或者關閉連接自動刪除;如果子節點元素數量>1,獲取倒數第二個元素,監聽它的節點刪除事件,在回調函數中寫操作。

這種方式,獲取鎖的順序和創建節點的順序一致。

 

無論使用哪一種,都是創建臨時節點,避免獲取到鎖後機器故障,不能及時釋放鎖。

自己寫代碼實現zk的分佈式鎖有點麻煩,可以使用現成的輪子Curator。

 

如果方便使用redis,儘量使用redis來實現分佈式鎖,redis與springboot的整合更好,實現分佈式鎖也更簡單。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章