搜了一些Zookeeper的相關書籍和博客,但好多基本都是講Zookeeper的架構、用途,尤其分佈式中的應用講的真的是天花亂墜,但看完還是不會寫代碼,搞得自己理論豐富的一批,實踐完全懵逼。對於Zookeeper的Java客戶端API使用,基本沒有涉及或者講清楚,要麼就是講的很模糊。果然還是得自己來,通過Zookeeper的API來學習一下Zookeeper的功能,然後依據這些功能去思考怎麼用?爲什麼用?逐漸摸索Zookeeper的應用。
1. 環境準備
1. 安裝jdk,這就不細說了。
2.安裝Zookeeper集羣,網上很多詳細教程,但還是簡單貼出來自己的配置文件。可以直接複製。只是其中一個Zookeeper節點的配置文件,其餘兩個僅僅改動一下端口、dataDir以及dataLogDir的路徑即可。
# 服務器與客戶端之間交互的基本時間單元(ms)
tickTime=2000
#zookeeper集羣中的包含多臺server, 其中一臺爲leader, 集羣中其餘的server爲follower. initLimit參數配置初始化連接時, follower和leader之間的最長心跳時間. 此時該參數設置爲10, #說明時間限制爲10倍tickTime.
initLimit=10
# 該參數配置leader和follower之間發送消息, 請求和應答的最大時間長度. 此時該參數設置爲5, 說明時間限制爲5倍tickTime.
syncLimit=5
# zookeeper中使用的基本時間單位, 毫秒值.
tickTime=2000
#存儲內存中數據庫快照的位置
dataDir=/usr/local/zookeeper-cluster/zookeeper-2181/data
#存儲日誌的目錄,如果沒有設置該參數, 將使用和#dataDir相同的設置.
dataLogDir=/usr/local/zookeeper-cluster/zookeeper-2181/logs
# 用於監聽客戶端連接的端口,或者說客戶端請求連接的端口
clientPort=2181
maxClientCnxns=60
#這一段很重要,集羣配置的關鍵,server.後面跟的數字是標識某臺Zookeeper的關鍵,後續配置集羣會使用
server.1=127.0.0.1:2222:2225
server.2=127.0.0.1:3333:3335
server.3=127.0.0.1:4444:4445
#autopurge.snapRetainCount=3
#autopurge.purgeInterval=1
3. 引入jar包,這裏推薦使用Apache的java客戶端。
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.5.7</version>
</dependency>
2.API使用
1. 首先,創建Zookeeper客戶端對象。很簡單,直接new一個就行,但主要是注意構造方法的參數。Zookeeper構造方法重載版本比較多,必須瞭解其中的每個參數代表的含義。
public ZooKeeper(String connectString, int sessionTimeout, Watcher watcher, long sessionId, byte[] sessionPasswd, boolean canBeReadOnly, HostProvider aHostProvider, ZKClientConfig clientConfig) throws IOException
(1)String connectString :Zookeeper集羣的每個節點的IP地址和端口號,用逗號分隔。例如
String zkNodes = "127.0.0.1:2181,127.0.0.1:2182,127.0.0.1:2183";
(2)int sessionTimeout:會話超時時間,單位是毫秒,在sessionTimeout時間之內,客戶端會和服務端直接通過server發送PING請求來保持會話的有效性,俗稱“心跳檢測”,同時server重新激活client對應的會話。
Session是指當Client創建一個同Server的連接時產生的會話。連接Connected之後Session狀態就開啓,Zookeeper服務器和Client採用長連接方式(Client會不停地向Server發送心跳)保證session在不出現網絡問題、服務器宕機或Client宕機情況下可以一直存在。因此,在正常情況下,session會一直有效,並且ZK集羣上所有機器都會保存這個Session信息。
如果超出sessionTimeout的時間服務端仍未接收到客戶端的心跳檢測請求,那麼服務端就會將客戶端看做下線狀態,會將存儲的session給刪除。在ZK中,很多數據和狀態都是和會話綁定的,一旦會話失效,那麼ZK就開始清除和這個會話有關的信息,包括這個會話創建的臨時節點和註冊的所有Watcher。
但還有另一種情況就是,客戶端正常,但是當前會話所連接的Zookeeper集羣節點宕機或者其他原因心跳檢測失敗,也就是無法ping,ZK Client會馬上捕獲到這個異常,封裝爲一個ConnectionLoss的事件,然後啓動自動重連機制在地址列表中選擇新的地址進行重連。重連會有三種結果:
- 在session timeout時間內重連成功,client會重新收到一個syncconnected的event,並將連接重新持久化爲connected狀態
- 超過session timeout時間段後重連成功,client會收到一個expired的event,並將連接持久化爲closed狀態
- 一直重連不上,client將不會收到任何event
(3)Watcher watcher:這是Zookeeper中非常重要的一個特徵,由於客戶端與Zookeeper之間的連接是採用長連接,所以,可以通過客戶端在Zookeeper上註冊一個事件監聽器,也就是Watcher對象,當Zookeeper中發生某一個事件時就會回調該Watcher對象中的方法。這個Watcher對象的作用非常重要,這個事件監聽器將會一直存在,直到Zookeeper客戶端關閉連接。
(4)long sessionId:每一個會話session的建立,Zookeeper都會爲自動該會話分配一個全局唯一的會話id,所以一般該id不會由我們指定,一般都不會指定傳遞該參數。
(5)boolean canBeReadOnly:是否提供只讀服務(不提供寫服務)。
(6)HostProvider aHostProvider:隨機提供host進行連接。沒啥用,默認的即可。
(7)ZKClientConfig clientConfig:連接參數配置,
(8)byte[] sessionPasswd:提供連接zookeeper的sessionId和密碼,通過這兩個確定唯一一臺客戶端,目的是可以提供重複會話。
實際上,真正必須的參數也就是connectString、sessionTimeout和watcher,其餘的默認即可。所以Zookeeper也提供了這三個參數的重載版本,也是最常用的構造方法。
注意:zookeeper客戶端和服務器端會話的建立是一個異步的過程,也就是說在程序中,我們程序方法在處理完客戶端初始化後,立即返回(程序往下執行代碼,這樣,大多數情況下我們並沒有真正構建好一個可用會話,在會話的聲明週期處於"CONNECTING"時纔算真正建立完畢)
創建Zookeeper客戶端並建立連接的示例代碼如下:
public static void createZookeeperClient() throws IOException, InterruptedException {
/*
由於Zookeeper客戶端對象的建立和連接是異步執行,所以很有可能會因爲Zookeeper對象尚未建立連接,
主線程就繼續執行導致的執行報錯,這一點可以通過一些線程同步類輔助,保證Zookeeper連接完全建立成功後在進行後續操作
*/
final CountDownLatch countDownLatch = new CountDownLatch(1);
ZooKeeper zooKeeper = new ZooKeeper(zkNodes, 5000, new Watcher() {
@Override
public void process(WatchedEvent watchedEvent) {
//已建立連接
if (watchedEvent.getState().equals(Event.KeeperState.SyncConnected)) {
countDownLatch.countDown();
}
}
});
countDownLatch.await();
//do something...
}
2. Watcher對象的使用:Watcher可以實現對Zookeeper的監控,這個特性是來自於Zookeeper客戶端和服務端採用TCP長連接的方式進行通信。當Zookeeper發生某個事件時,會通過Watcher對象進行回調通知。Watcher是一個接口,所以必須要實現一個具體實現類,僅有一個接口方法process(),該方法的參數WatchedEvent就是Zookeeper服務端發生的事件,其中包含了該事件的所有信息。客戶端在向zk服務器註冊Watcher的同時,會將Watcher對象存儲在客戶端的WatchManager中。當zk服務端觸發Watcher事件後,會向客戶端發送通知,客戶端線程從WatcherManager中取出對應的Watcher對象。來執行回調邏輯。
public class WatcherTest1 implements Watcher {
@Override
public void process(WatchedEvent watchedEvent) {
}
}
對於WatchedEvent,其源碼比較簡單,其中只包含了三個主要的域變量,分別是EventType eventType, KeeperState keeperState, String path。eventType表示事件類型,keeperState表示連接狀態,path就表示事件的發生在哪個數據節點路徑上發生的。
public class WatchedEvent {
private final KeeperState keeperState;
private final EventType eventType;
private String path;
····
}
EventType和KeeperState都是枚舉類型
public static enum EventType {
None(-1),//無此節點
NodeCreated(1),//節點已創建成功
NodeDeleted(2),//節點已刪除成功
NodeDataChanged(3),//節點數據改變
NodeChildrenChanged(4),//子節點被創建、被刪除會發生事件觸發
DataWatchRemoved(5),//數據監視已被移除
ChildWatchRemoved(6);//子節點監視已被移除
private final int intValue;
....
}
public static enum KeeperState {
Unknown(-1),//從3.1.0版本開始被廢棄
Disconnected(0),//客戶端和服務器處於斷開連接狀態
NoSyncConnected(1),//從3.1.0版本開始被廢棄
SyncConnected(3),//客戶端和服務器處於連接狀態
AuthFailed(4),//權限驗證失敗狀態,通常同時也會收到AuthFailedException
ConnectedReadOnly(5),//只讀連接
SaslAuthenticated(6),//權限驗證通過
Expired(-112),//此時客戶端會話失效,通常同時也會收到SessionExpiredException
Closed(7);//連接資源關閉
....
}
Watcher特性
一次性
無論是服務端還是客戶端,一旦一個 Watcher 被觸發,ZooKeeper 都將其從相應的存儲中移除。因此,開發人員在 Watcher 的使用上要記住的一點是需要反覆註冊。例如,如果客戶端執行
getData("/znode1",true)
,後面對/znode1
的更改或刪除,客戶端都會獲得/znode1
的監控事件通知。如果/znode1
再次更改,如果客戶端沒有執行新一次設置新監視點的讀取,是不會發送監視事件通知的。但是,有一個監聽器對象是例外,在創建Zookeeper對象時,調用的構造方法中必須傳遞的Watcher對象就是和Zookeeper對象具有相同的生命週期,直到Zookeeper客戶端關閉連接,該Watcher纔會關閉對服務端的監聽。客戶端串行執行
客戶端Watcher回調的過程是一個串行同步的過程,這爲我們保證了順序,同時,需要開發人員注意的一點是,千萬不要因爲一個Watcher的處理邏輯影響了整個客戶端的Watcher回調。
輕量
WatchedEvent 是 ZooKeeper 整個 Watcher 通知機制的最小通知單元,這個數據結構中只包含三部分內容:通知狀態、事件類型和節點路徑。也就是說,Watcher 通知非常簡單,只會告訴客戶端發生了事件,而不會說明事件的具體內容。
另外,客戶端向服務端註冊 Watcher 的時候,並不會把客戶端真實的 Watcher 對象傳遞給服務端,僅僅只是在客戶端請求中使用 boolean 類型屬性進行了標記,同時服務端也僅僅只是保存了當前連接的 ServerCnxn 對象。如此輕量的Watcher機制設計,在網絡開銷和服務端內存開銷上都是非常廉價的。
3. 創建節點:Zookeeper的Java客戶端對於數據操作(增刪改查)都有同步和異步兩種實現方式。同步操作一般會有返回值,並且會拋出相應的異常。異步操作沒有返回值,也不會拋出異常。此外異步方法參數在同步方法參數的基礎上,會增加Callback和context兩個參數。
同步創建節點:全參數版本
public String create(String path, byte[] data, List<ACL> acl, CreateMode createMode, Stat stat, long ttl)
(1)path指的就是數據節點的路徑。
(2)data就是要存儲的字符串的字節數組形式。(不支持序列化方式,如果需要實現序列化,可使用java相關的序列化框架,如Hession)
(3)acl指節點權限,統一使用Ids.OPEN_ACL_UNSAFE權限即可(一般在權限沒有太高要求的場景下,沒必要關注)
(4)createMode節點類型,創建節點的類型:CreateMode.* 提供四種節點類型,分別是PERSISTENT (持久節點)、PERSISTENT_SEQUENTIAL(持久順序節點)、EPHEMRAL(臨時節點)、EPHEMRAL_SEQUENTIAL(臨時順序節點)
(5)stat節點狀態信息:在創建節點時可以手動指定節點的狀態信息,但一般無需傳入該參數。(非必須參數)
(6)ttl:過期時間,如果該節點在ttl時間之內未發生變動,就會被刪除。(非必須參數)
所以常用的同步創建節點方法的重載版本如下
public String create(String path, byte[] data, List<ACL> acl, CreateMode createMode)
示例代碼如下
public static void createNode() throws IOException, KeeperException, InterruptedException {
final CountDownLatch countDownLatch = new CountDownLatch(1);
ZooKeeper zooKeeper = new ZooKeeper(zkNodes, 5000, new Watcher() {
@Override
public void process(WatchedEvent watchedEvent) {
//已建立連接
if (watchedEvent.getState().equals(Event.KeeperState.SyncConnected)) {
countDownLatch.countDown();
}
}
});
countDownLatch.await();
//節點權限設置
ACL acl = new ACL(ZooDefs.Perms.ALL,ZooDefs.Ids.ANYONE_ID_UNSAFE);
List<ACL> acls = new ArrayList<ACL>();
acls.add(acl);
//創建節點
zooKeeper.create("/demo", "helloworld".getBytes(), acls, CreateMode.PERSISTENT);
System.out.println("over");
}
異步創建節點:
public void create(String path, byte[] data, List<ACL> acl, CreateMode createMode, StringCallback cb, Object ctx)
除了create同步方法中的四個必須參數以外,異步模式的create方法還增加了callback和context兩個參數。這個callback中的processResult方法會在節點創建好之後被調用,它有四個參數。第一個是int類型的resultCode,爲服務端響應碼, 0表示調用成功,-4表示端口連接,-110表示指定節點存在,-112表示會話已經過期。第二個參數是創建節點的路徑。第三個參數是context,當一個StringCallback類型對象作爲多個create方法的參數時,這個參數就很有用。第四個參數是創建節點的名字,其實與path參數相同。異步的方法不會拋出異常,而是會在回調StringCallback中處理所有的事件。
public interface StringCallback extends AsyncCallback {
void processResult(int var1, String var2, Object var3, String var4);
}
示例代碼
public static void createNode() throws IOException, KeeperException, InterruptedException {
//創建連接對象省略
final CountDownLatch countDownLatch = new CountDownLatch(1);
//節點權限設置
ACL acl = new ACL(ZooDefs.Perms.ALL,ZooDefs.Ids.ANYONE_ID_UNSAFE);
List<ACL> acls = new ArrayList<ACL>();
acls.add(acl);
zk.create("/demo", "helloworld".getBytes(), acls, CreateMode.PERSISTENT,new IStringCallBack(), countDownLatch);
countDownLatch.await();
System.out.println("over");
}
static class IStringCallBack implements AsyncCallback.StringCallback {
@Override
public void processResult(int rc, String path, Object ctx, String name) {
switch (KeeperException.Code.get(rc)) {
case CONNECTIONLOSS:
System.out.println("CONNECTIONLOSS");
break;
case OK:
System.out.println("OK - {" + path + ", " + name + ", " + ctx + "}");
CountDownLatch countDownLatch = (CountDownLatch) ctx;
countDownLatch.countDown();
break;
case NODEEXISTS:
System.out.println(path + "exists");
break;
default:
System.out.println("DEFAULT");
break;
}
}
}
4. 刪除節點:和創建節點類似,分同步和異步。
(1)同步
public void delete(String path, int version)
參數path,表示節點路徑
參數version,表示版本號,即表示本次刪除操作是針對該數據的某個版本進行操作。只有當version參數的值與節點狀態信息中的dataVersion值相等時,數據修改才能成功,否則會拋出BadVersion異常。這是爲了防止丟失數據的更新,在ZooKeeper提供的API中,所有的對已有節點的寫數據操作都有version參數。
(2)異步
public void delete(String path, int version, VoidCallback cb, Object ctx)
5. 修改節點數據:
(1)同步
public Stat setData(String path, byte[] data, int version)
(2)異步
public void setData(String path, byte[] data, int version, StatCallback cb, Object ctx)
參數path:表示數據節點路徑
參數data:表示要設置的數據
參數version:表示修改數據版本
6. 獲取節點的數據:
(1)同步:
public byte[] getData(String path, boolean watch, Stat stat)
zooKeeper.getData方法的返回值就是節點中存儲的數據值,它有三個參數,第一個參數是節點的路徑,用於表示要獲取哪個節點中的數據。第三個參數stat用於存儲節點的狀態信息,在調用getData方法前,會先構造一個空的Stat類型對象作爲參數傳給getData方法,當getData方法調用返回後,節點的狀態信息會被填充到stat對象中。
private void getDataSync() throws KeeperException, InterruptedException {
Stat stat = new Stat();
// getData的返回值是該節點的數據值,節點的狀態信息會賦值給stat對象
byte[] data = zooKeeper.getData("/node_1",true, stat);
System.out.println(new String(data));
System.out.println(stat);
}
第二個參數是一個bool類型的watch,這個參數比較重要。當watch爲true時,表示我們想要監控這個節點的數據變化,而使用的監聽器對象就是我們在創建Zookeeper客戶端時指定的那個監聽器對象,這個boolean參數時,false表示不對該節點監控。當節點的數據發生變化時,我們就可以拿到zk服務器推送給我們的通知。
第二個參數,我們還可以使用自定義的Watcher對象,但這種監控只能生效一次,這是getData方法的另一個重載版本
public byte[] getData(String path, Watcher watcher, Stat stat)
(2)異步
public void getData(String path, Watcher watcher, DataCallback cb, Object ctx)
public void getData(String path, boolean watch, DataCallback cb, Object ctx)
private void getDataAsync() {
zooKeeper.getData("/node", true, new AsyncCallback.DataCallback() {
public void processResult(int resultCode, String path, Object ctx, byte[] data, Stat stat) {
System.out.println(resultCode);
System.out.println(path);
System.out.println(ctx);
System.out.println(new String(data));//data就是獲取到的數據
System.out.println(stat);
}
}, "異步獲取節點的數據");
}