Zookeeper基本知識點及使用java操作zk 且實現循環監聽節點

1. 概述

Zookeeper是一個開源的分佈式的,爲分佈式應用提供協調服務的Apache項目。

Zookeeper從設計模式角度來理解:是一個基於觀察者模式設計的分佈式服務管理框架,它負責存儲和管理大家都關心的數據,然後接受觀察者的註冊,一旦這些數據的狀態發生變化,Zookeeper就將負責通知已經在Zookeeper上註冊的那些觀察者做出相應的反應

 所以可以大概理解成:Zookeeper=文件系統+通知機制。

官網下載:https://zookeeper.apache.org/

2.  特點

  1. Zookeeper:一個領導者(Leader),多個跟隨者(Follower)組成的集羣
  2. 集羣中只要有半數以上節點存活,Zookeeper集羣就能正常服務。
  3. 全局數據一致:每個Server保存一份相同的數據副本,Client無論連接到哪個Server,數據都是一致的。
  4. 更新請求順序進行,來自同一個Client的更新請求按其發送順序依次執行。
  5. 數據更新原子性,一次數據更新要麼成功,要麼失敗。
  6. 實時性,在一定時間範圍內,Client能讀到最新數據。

3. 數據結構

ZooKeeper數據模型的結構與Unix文件系統很類似,整體上可以看作是一棵樹,每個節點稱做一個ZNode。每一個ZNode默認能夠存儲1MB的數據,每個ZNode都可以通過其路徑唯一標識。且節點的名稱都是唯一的,不允許重複。

 4. 應用場景

統一命名服務、統一配置管理、統一集羣管理、服務器節點動態上下線、軟負載均衡,分佈式鎖等。 

  • 統一命名:在分佈式系統中,經常需要對應用/服務進行統一命名,便於識別,和ip和域名類似,而zk的樹形結構有個特點就是節點名稱不允許重複,所以可以使用這個特性來實現命名服務。
  • 配置管理:集羣環境下,一般將應用的統一配置集中管理,即寫到zookeeper中的Znode上,然後客戶端通過監聽該節點數據,配置一變更,Zookeeper就立即通知監聽該節點的各個服務器
  • 統一集羣管理:集羣中的各個應用都將自身信息註冊到Zookeeper上,然後通過監聽節點獲取他們的實時狀態變化
  • 服務器節點動態上下線:集羣中,將服務器自身註冊爲Zookeeper上的一個節點,當服務器宕機,節點自動刪除,此時,客戶端就能即使響應,當下次請求時就能避免訪問宕機的服務器
  • 軟負載均衡:服務器作爲節點註冊到Zookeeper上,然後Zookeeper記錄每臺服務器被訪問的次數,讓訪問數最少的服務器區處理最新的客戶端請求
  • 分佈式鎖:可實現分佈式鎖。

5.Zookeeper內部原理

5.1 選舉機制(重要)

  • 半數機制:集羣中半數以上機器存活,集羣可用。所以Zookeeper適合安裝奇數臺服務器。
  • Zookeeper雖然在配置文件中並沒有指定Master和Slave。但是,Zookeeper工作時,是有一個節點爲Leader,其他則爲Follower,Leader是通過內部的選舉機制臨時產生的。

舉例說明:

假設有五臺服務器組成的Zookeeper集羣,它們的id從1-5(通過myid文件設置id),同時它們都是最新啓動的,也就是沒有歷史數據,在存放數據量這一點上,都是一樣的。假設這些服務器依序啓動,來看看會發生什麼?

注意:一開始都是會選舉自己的,但是如果選舉自己後沒當上Leader的話,下次其他服務器啓動,就會投票給其他的zk server

 

(1)服務器1啓動,此時只有它一臺服務器啓動了,它發出去的報文沒有任何響應,所以它的選舉狀態一直是LOOKING狀態。

(2)服務器2啓動,它與最開始啓動的服務器1進行通信,互相交換自己的選舉結果,由於兩者都沒有歷史數據,所以id值較大的服務器2勝出,但是由於沒有達到超過半數以上的服務器都同意選舉它(這個例子中的半數以上是3),所以服務器1、2還是繼續保持LOOKING狀態。

(3)服務器3啓動,根據前面的理論分析,服務器3成爲服務器1、2、3中的老大,而與上面不同的是,此時有三臺服務器選舉了它,所以它成爲了這次選舉的Leader。

(4)服務器4啓動,根據前面的分析,理論上服務器4的id應該是服務器1、2、3、4中最大的,但是由於前面已經有半數以上的服務器選舉了服務器3,所以它只能當Follow。

(5)服務器5啓動,同4一樣當小弟。

5.2 節點類型

  1. 持久型目錄節點:客戶端與Zookeeper斷開連接後,該節點依舊存在
  2. 持久性順序編號目錄節點:和持久型目錄節點不同的是,Zookeeper會給該節點名稱進行順序編號。
  3. 臨時目錄節點:客戶端與Zookeeper斷開連接後,該節點被刪除
  4. 臨時順序編號目錄節點:和臨時目錄節點不同的是,Zookeeper會給該節點名稱進行順序編號。

說明:創建znode時設置順序標識,znode名稱後會附加一個值,順序號是一個單調遞增的計數器,由父節點維護。如設置的節點名爲user,那麼添加順序標識後,生成的就編程user,user001,user002

5.3 Stat結構體

屬性有很多,需要記住的就是:

dataLength- znode的數據長度

numChildren - znode子節點數量

5.4 監聽器使用(重要)

常見的監聽器:

  • 監聽節點數據的變化:get path [watch]
  • 監聽子節點增減的變化:ls path [watch]

監聽注意點:

  監聽只能監聽一次節點變化,如果想再次監聽,那就必須再次發送監聽請求,即再次調用get path watch命令。

在代碼中實現循環監聽有兩種方式:

  1. 對某個路徑在本次監聽完後主動用簡單(exist()方法)的調用再次註冊監聽
  2. 使用addWatch方法設定AddWatchMode屬性

實現監聽:

  1. 首先要有一個main()線程
  2. 在main線程中創建Zookeeper客戶端,這時就會創建兩個線程,一個負責網絡連接通信(connet),一個負責監聽(listener)。
  3. 通過connect線程將註冊的監聽事件發送給Zookeeper。
  4. 在Zookeeper的註冊監聽器列表中將註冊的監聽事件添加到列表中。
  5. Zookeeper監聽到有數據或路徑變化,就會將這個消息發送給listener線程。
  6. listener線程內部調用了process()方法。

5.5 寫數據流程

6. 單節點集羣安裝

6.1 安裝前準備

(1)安裝Jdk

(2)拷貝Zookeeper安裝包到Linux系統下

(3)解壓到指定目錄

tar -zxvf zookeeper-3.4.10.tar.gz -C /opt/module/

6.2 修改配置

(1)將/opt/module/zookeeper-3.4.10/conf這個路徑下的zoo_sample.cfg修改爲zoo.cfg;

mv zoo_sample.cfg zoo.cfg

(2)打開zoo.cfg文件,修改dataDir路徑(保存數據到本地系統,便於數據管理):

vim zoo.cfg
dataDir=/opt/module/zookeeper-3.4.10/zkData

(3)在/opt/module/zookeeper-3.4.10/這個目錄上創建zkData文件夾

mkdir zkData

6.3 操作Zookeeper

(1)啓動Zookeeper服務器端

bin/zkServer.sh start

(2)查看進程是否啓動

jps

(3)查看狀態:

bin/zkServer.sh status

(4)啓動客戶端:

bin/zkCli.sh

(5)退出客戶端:

quit

(6)停止Zookeeper

bin/zkServer.sh stop

6.4 配置文件參數解讀

Zookeeper中的配置文件zoo.cfg中參數含義解讀如下

1.tickTime =2000:服務器之間或客戶端與服務器之間維持心跳的時間間隔,也就是每個tickTime時間就會發送一個心跳時間單位爲毫秒。

它用於心跳機制,並且設置最小的session超時時間爲兩倍心跳時間。(session的最小超時時間是2*tickTime)

2.initLimit =10:Follower和Leader初始通信時限

集羣中的Follower跟隨者服務器與Leader領導者服務器之間初始連接時能容忍的最多心跳數(tickTime的數量)。像這樣設置的話,啓動Zookeeper時Leader和Follow最大的連接時間爲2000*10=20s

3.syncLimit =5:Leader和Follower初始連接成功後,集羣中Leader與Follower之間的最大響應時間單位。假如響應超過syncLimit * tickTime,Leader認爲Follwer死掉,從服務器列表中刪除Follwer。這裏的話就是10秒

4.dataDir:數據文件目錄+數據持久化路徑,主要用於保存Zookeeper中的數據。

5.clientPort =2181:客戶端連接服務器端時的端口

7. 分佈式安裝部署

部署在3臺機器上

7.1 解壓安裝

解壓zookeeper的安裝包到3臺機器上

7.2 配置服務器編號

(1)在/opt/module/zookeeper-3.4.10/這個目錄下創建zkData

(2)在/opt/module/zookeeper-3.4.10/zkData目錄下創建一個myid的文件,並添加id

touch myid
vi myid
#在 myid文件中添加與server對應的編號2
2

添加myid文件,注意一定要在linux裏面創建,在notepad++裏面很可能亂碼

(3)在其他兩臺機器上都添加myid文件,並修改文件內容爲3、4

7.3 配置zoo.cfg文件

(1) 重命名/opt/module/zookeeper-3.4.10/conf這個目錄下的zoo_sample.cfg爲zoo.cfg

(2) 打開zoo.cfg文件,修改數據存儲地址

dataDir=/opt/module/zookeeper-3.4.10/zkData

再添加如下配置

增加如下配置
#######################cluster##########################
server.2=192.168.24.200:2888:3888
server.3=192.168.24.201:2888:3888
server.4=192.168.24.202:2888:3888

(3)配置參數解讀

server.A=B:C:D

A是一個數字,表示這個是第幾號服務器,即設置在myid中的值

集羣模式下配置一個文件myid,這個文件在dataDir目錄下,這個文件裏面有一個數據就是A的值,Zookeeper啓動時讀取此文件,拿到裏面數據與zoo.cfg裏面的配置信息比較從而判斷到底是哪個server。

B是這個服務器的ip地址;

C是這個服務器與集羣中的Leader服務器交換數據的端口;

D是萬一集羣中的Leader服務器掛了,需要一個端口來重新進行選舉,選出一個新的Leader,而這個端口就是用來執行選舉時服務器相互通信的端口。

7.4 啓動服務器

分別啓動3臺機器的zk server

bin/zkServer.sh start

8. 客戶端命令行操作

  • ls path [watch]:使用 ls 命令來查看當前znode中所包含的內容
  • ls2 path [watch]:看當前節點數據並能看到更新次數等數據
  • create:普通創建 (-s  含有序列,-e  臨時節點)
  • get path [watch]:獲得節點的值
  • set:設置節點的具體值
  • stat:查看節點狀態
  • delete:刪除節點
  • rmr:遞歸刪除節點

1.啓動客戶端

bin/zkCli.sh

2.創建2個普通節點

create /sanguo "jinlian"
create /sanguo/shuguo "liubei"

3.創建短暫節點

create -e /sanguo/wuguo "zhouyu"

4.創建帶序號的節點

create -s /sanguo/weiguo/xiaoqiao "jinlian"
create -s /sanguo/weiguo/daqiao "jinlian"
create -s /sanguo/weiguo/daqiao "jinlian"

結果:

Created /sanguo/weiguo/xiaoqiao0000000000

Created /sanguo/weiguo/daqiao0000000001

Created /sanguo/weiguo/diaocan0000000002

如果原來沒有序號節點,序號從0開始依次遞增。如果原節點下已有2個節點,則再排序時從2開始,以此類推。

5.獲得節點的值

get /sanguo

 6.節點的值變化監聽,在命令行可添加watch(上面的命令列表查看)的條件下,給命令最後添加watch,可以實現監聽節點值的變化

get /sanguo watch

7. 監聽某節點的子節點路徑變化

ls /sanguo watch

9. JAVA 操作zookeeper

9.1 引入依賴包

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

9.2  代碼展示

public class MyZkClient {
    // 集羣連接地址,多個以逗號分隔
    private static final String CONNECT_ADDRES = "127.0.0.1:2181,127.0.0.1:2182";
    // 會話超時時間
    private static final int SESSIONTIME = 2000;
    // 信號量,讓zk在連接之前等待,連接成功後才能往下走.
    private static final CountDownLatch countDownLatch = new CountDownLatch(1);
    private static String LOG_MAIN = "【main】 ";
    private static ZooKeeper zk;

    public void createConnection(String connectAddres, int sessionTimeOut) {
        try {
            zk = new ZooKeeper(connectAddres, sessionTimeOut, new Watcher() {
                public void process(WatchedEvent watchedEvent) {
                    System.out.println("事件來了");
                    // 獲取事件狀態
                    Watcher.Event.KeeperState keeperState = watchedEvent.getState();
                    // 獲取事件類型
                    Watcher.Event.EventType eventType = watchedEvent.getType();
                    // zk 路徑
                    String path = watchedEvent.getPath();
                    System.out.println("進入到 process() keeperState:" + keeperState + ", eventType:" + eventType + ", path:" + path);
                    // 判斷是否建立連接
                    if (Watcher.Event.KeeperState.SyncConnected == keeperState) {
                        if (Watcher.Event.EventType.None == eventType) {
                            // 如果建立建立成功,讓後程序往下走
                            System.out.println(LOG_MAIN + "zk 建立連接成功!");
                            countDownLatch.countDown();
                        } else if (Watcher.Event.EventType.NodeCreated == eventType) {
                            System.out.println(LOG_MAIN + "事件通知,新增node節點" + path);
                        } else if (Watcher.Event.EventType.NodeDataChanged == eventType) {
                            System.out.println(LOG_MAIN + "事件通知,當前node節點" + path + "被修改....");
                        }
                        else if (Watcher.Event.EventType.NodeDeleted == eventType) {
                            System.out.println(LOG_MAIN + "事件通知,當前node節點" + path + "被刪除....");
                        }

                    }
                    System.out.println("--------------------------------------------------------");
                }
            });
            System.out.println(LOG_MAIN + "zk 開始啓動連接服務器....");
            countDownLatch.await();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public boolean createPath(String path, String data) {
        try {
            this.exists(path, true);
            this.zk.create(path, data.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
            System.out.println(LOG_MAIN + "節點創建成功, Path:" + path + ",data:" + data);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }

    /**
     * 判斷指定節點是否存在
     *
     * @param path
     *            節點路徑
     */
    public Stat exists(String path, boolean needWatch) {
        try {
            return this.zk.exists(path, needWatch);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    public boolean updateNode(String path,String data) throws KeeperException, InterruptedException {
        exists(path, true);
        this.zk.setData(path, data.getBytes(), -1);
        return false;
    }


    public static void main(String[] args) throws KeeperException, InterruptedException {
        MyZkClient zkClientWatcher = new MyZkClient();
        zkClientWatcher.createConnection(CONNECT_ADDRES, SESSIONTIME);
        boolean createResult = zkClientWatcher.createPath("/p15", "pa-644064");
        zkClientWatcher.updateNode("/p15","7894561");
        
        // 一次性監聽器
        zk.getData("/p15", new Watcher() {
            public void process(WatchedEvent watchedEvent) {
                System.out.println("haha");
            }
        }, null);

        //添加自定義監聽器
        //AddWatchMode.PERSISTENT表示循環監聽指定節點,能監聽到子節點的新增和刪除,監聽不了子節點的更新。監聽不了孫節點的任何操作
        //AddWatchMode.PERSISTENT_RECURSIVE 表示循環監聽指定節點及其子孫節點的增刪改操作
        zk.addWatch("/p15", new Watcher() {
            public void process(WatchedEvent watchedEvent) {
                System.out.println("xixi");
            }
        }, AddWatchMode.PERSISTENT);
        Thread.sleep(Long.MAX_VALUE);
    }
}

代碼說明:

1. 使用CountDownLatch的作用是讓zk client連接服務器後再去做新增,修改那些操作,因爲可以看到,我們在createConnection後立即就調了createPath創建節點了,如果zk client都還沒連上zk server,就去做新增,修改,那肯定是會出問題的

2.在createConnection方法中可看到我們在new ZooKeeper時傳入了一個Watcher的一個匿名內部類,重寫了process方法,這裏其實就是添加了一個默認的事件監聽器。該默認的事件監聽器默認只會監聽一次,且監聽的觸發是在zk client連接上zk server。

3.創建節點時調用ZooKeeper類的create方法,且通過CreateMode參數來改變節點的類型(暫時性和持久性)

4.在updateNode()和createPath()方法中可以看到調用了this.exists(path, true)方法,true就表示給path節點再次添加事件監聽。通過次方法來實現簡單的循環監聽。

因爲上面說過了,在zk client連接上zk server後,事件就已經監聽過一次了,而zk 的事件監聽也只能是一次,如果想再次監聽,必須再次發送監聽請求。而this.exists(path, true)方法不僅判斷了節點是否存在,還會爲節點再次添加事件監聽,當監聽到事件後,會調用new ZooKeeper()中實現的Watch監聽器。

5.如果不想使用new ZooKeeper()中實現的Watch監聽器,就自己再寫個監聽器,然後調用exists的重載方法 exists(String path, Watcher watcher),這樣監聽到後就會執行新的監聽器代碼

6.可以看到我的main方法中有端代碼:zk.getData。。。,這個事件就是自己重新定義了一個監聽器,沒有再使用那個默認的監聽器。

9.3 自定義註冊事件及循環監聽

(1)自定義監聽器:實現Watcher接口,實現process方法

(2)註冊事件:zk.addWatch(),其中有個參數AddWatchMode。它有兩個值:

AddWatchMode.PERSISTENT:循環監聽指定節點,能監聽到子節點的新增和刪除,監聽不了子節點的更新。監聽不了孫節點的任何操作

AddWatchMode.PERSISTENT_RECURSIVE:循環監聽指定節點及其子孫節點的增刪改操作

使用addWatch()監聽的節點即使被刪除,也會持續監聽。將節點刪除後再次添加,會發現依然是可以監聽到節點的新增的

 

9.4 WatchedEvent

WatchedEvent表示監聽器中process方法中的回調參數:WatchedEvent watchedEvent

該參數定義了事件通知相關的邏輯,包含KeeperState和EventType兩個枚舉類,分別代表了通知狀態和事件類型

 

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