本节里面,将会利用zookeeper官方的原生java api进行连接,然后演示一些创建、删除、修改、查询节点的操作。其实有两个比较好用的第三方客户端zkclient和curator,可以弥补原生api的很多不足,使用也比较简便,但是这两个东西也都是在原生api的基础上封装的,所以了解原生api的使用方法还是很有必要的,后面有时间的话会跟大家介绍一下另外两个客户端。
先给大家贴一张图,里面介绍了zookeeper原生java api的一些基础用法,可以先简单了解一下,下面会写一个demo程序对这几个方法进行逐个介绍。
创建一个maven工程zkTest,然后添加zookeeper、log4j的maven依赖。
org.apache.zookeeper
zookeeper
3.4.6
log4j
log4j
1.2.15
将下面的log4j.properties配置文件放到src/main/resources下面即可
log4j.rootLogger = info,stdout,D
log4j.appender.stdout = org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target = System.out
log4j.appender.stdout.layout = org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern = %d %t [%c{2}] %L [%-5p] %m%n
log4j.appender.D = org.apache.log4j.DailyRollingFileAppender
log4j.appender.D.File = ../logs/log.log
log4j.appender.D.Append = true
log4j.appender.D.Threshold = info
log4j.appender.D.DatePattern= '_'yyyy-MM-dd'.log'
log4j.appender.D.layout = org.apache.log4j.PatternLayout
log4j.appender.D.layout.ConversionPattern = %d %t [%c{2}] %L [%-5p] %m%n
package com.wkp.test.zookeeper.base;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.CountDownLatch;import org.apache.log4j.Logger;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.Watcher.Event.EventType;
import org.apache.zookeeper.Watcher.Event.KeeperState;
import org.apache.zookeeper.ZooDefs.Ids;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.Stat;public class ZookeeperBase {
private static Logger logger=Logger.getLogger(ZookeeperBase.class);private ZooKeeper zookeeper;
private String connectString;//连接zk服务端的ip:port,如果多个格式为 ip:port,ip:port......
private int sessionTimeout;//客户端连接超时时间 单位 ms
/** 信号量,阻塞程序执行,用于等待zookeeper连接成功,发送成功信号 */
private CountDownLatch countDown=new CountDownLatch(1);
public ZookeeperBase(String connectString,int sessionTimeout) throws IOException{
this.connectString=connectString;
this.sessionTimeout=sessionTimeout;
zookeeper=new ZooKeeper(this.connectString, this.sessionTimeout, new Watcher() {
public void process(WatchedEvent event) {
//影响的路径
String path = event.getPath();
//获取事件的状态
KeeperState state = event.getState();
//获取事件的类型
EventType type = event.getType();
if(KeeperState.SyncConnected.equals(state)){
if(EventType.None.equals(type)){
//连接建立成功,则释放信号量,让阻塞的程序继续向下执行
countDown.countDown();
logger.info("zk建立连接成功========");
}
}
}
});
}
public ZooKeeper getZkClient() throws InterruptedException{
//计数器到达0之前,一直阻塞,只有当信号量被释放,才会继续向下执行
countDown.await();
return zookeeper;
}
public void closeClient() throws InterruptedException{
if(zookeeper!=null){
zookeeper.close();
logger.info("zk关闭连接成功=======");
}
}
/**
* 创建节点
* @param path 节点path
* @param data 节点数据
* @return
* @throws InterruptedException
* @throws KeeperException
*/
public String createNode(String path,String data) throws InterruptedException, KeeperException{
ZooKeeper zk = this.getZkClient();
String str = zk.create(path, data.getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
return str;
}
/**
* 更新节点数据
* @param path 节点path
* @param data 节点数据
* @return
* @throws InterruptedException
* @throws KeeperException
*/
public Stat updateNode(String path,String data) throws InterruptedException, KeeperException{
ZooKeeper zk = this.getZkClient();
Stat stat = zk.setData(path, data.getBytes(),-1);
return stat;
}
/**
* 获得节点数据
* @param path 节点path
* @return
* @throws KeeperException
* @throws InterruptedException
*/
public String getData(String path) throws KeeperException, InterruptedException{
ZooKeeper zk = this.getZkClient();
byte[] data = zk.getData(path, false, null);
return new String(data);
}
/**
* 获取当前节点的子节点(不包含孙子节点)
* @param path 父节点path
* @return
* @throws KeeperException
* @throws InterruptedException
*/
public ListgetChildren(String path) throws KeeperException, InterruptedException{
ZooKeeper zk = this.getZkClient();
Listlist = zk.getChildren(path, false);
return list;
}
/**
* 判断节点是否存在
* @param path
* @return
* @throws KeeperException
* @throws InterruptedException
*/
public Stat exists(String path) throws KeeperException, InterruptedException{
ZooKeeper zk = this.getZkClient();
Stat stat = zk.exists("/", false);
return stat;
}
/**
* 删除节点
* @param path
* @throws InterruptedException
* @throws KeeperException
*/
public void deleteNode(String path) throws InterruptedException, KeeperException{
ZooKeeper zk = this.getZkClient();
zk.delete(path, -1);
logger.info("删除 "+path+" 节点成功");
}
public static void main(String[] args) throws IOException, InterruptedException, KeeperException {
ZookeeperBase base = new ZookeeperBase("192.168.74.4:2181,192.168.74.5:2181,192.168.74.6:2181", 2000);
ZooKeeper zkClient = base.getZkClient();
System.out.println("sessionId:"+zkClient.getSessionId()+",sessionTimeOut:"+zkClient.getSessionTimeout());
base.closeClient();
}
}
ZooKeeper(String connectString, int sessionTimeout, Watcher watcher)
我们看到Zookeeper的构造方法需要三个参数,第一个connectString是连接字符串,格式为"ip:port,ip:port......",例如上面代码中的"192.168.74.4:2181,192.168.74.5:2181,192.168.74.6:2181",sessionTimeout为连接超时时间,单位毫秒,watcher是一个监听对象,zookeeper中对数据的变更操作都会有对应的事件,如果对节点注册了监听,当节点状态发生变化时就会被注册的监听对象监控到(关于zookeeper的事件监听机制下一节会有专门的介绍)。
我们看到代码中用到了一个多线程辅助类CountDownLatch,CountDownLatch是一个同步辅助类,构造方法初始化计数器值为1,当CountDownLatch值为0之前,其await()方法会一直阻塞,countDown()方法会将计数器的值减1(关于CountDownLatch的详细使用方法可以自行查询一下)。我们看到获取zookeeper客户端对象的方法中调用了CountDownLatch的await方法,就会一直等待watcher对象中监听的连接事件被监听到了,并且调用countDown方法后await方法才会被放行,这时候才可以获取到zookeeper客户端对象。至于为什么要使用这个同步计数器呢,其实是因为java中new Zookeeper创建对象立马就会返回了,而客户端连接到服务端是耗时的,这个时候并没有真正的连接成功,如果这个时候拿zk客户端对象去做操作会报错,所以我们要等待连接建立成功的时候才能使用客户端对象。
通过zkClient.getSessionId()可以获取到当前连接对象的ID,运行main方法会看到控制台出现下面结果(去掉了环境变量的无用日志输出)
2018-09-16 16:06:33,215 main [zookeeper.ZooKeeper] 438 [INFO ] Initiating client connection, connectString=192.168.74.4:2181,192.168.74.5:2181,192.168.74.6:2181 sessionTimeout=2000 watcher=com.wkp.test.zookeeper.base.ZookeeperBase$1@15c8132a
2018-09-16 16:06:33,342 main-SendThread(192.168.74.6:2181) [zookeeper.ClientCnxn] 975 [INFO ] Opening socket connection to server 192.168.74.6/192.168.74.6:2181. Will not attempt to authenticate using SASL (unknown error)
2018-09-16 16:06:33,347 main-SendThread(192.168.74.6:2181) [zookeeper.ClientCnxn] 852 [INFO ] Socket connection established to 192.168.74.6/192.168.74.6:2181, initiating session
2018-09-16 16:06:33,431 main-SendThread(192.168.74.6:2181) [zookeeper.ClientCnxn] 1235 [INFO ] Session establishment complete on server 192.168.74.6/192.168.74.6:2181, sessionid = 0x265e106a0720002, negotiated timeout = 4000
2018-09-16 16:06:33,438 main-EventThread [base.ZookeeperBase] 45 [INFO ] zk建立连接成功========
sessionId:172791579301511170,sessionTimeOut:4000
2018-09-16 16:06:33,462 main [zookeeper.ZooKeeper] 684 [INFO ] Session: 0x265e106a0720002 closed
2018-09-16 16:06:33,463 main-EventThread [zookeeper.ClientCnxn] 512 [INFO ] EventThread shut down
2018-09-16 16:06:33,464 main [base.ZookeeperBase] 61 [INFO ] zk关闭连接成功=======
我们调用createNode方法创建节点,path为 "/testRoot",value为 "rootValue"
ZookeeperBase base = new ZookeeperBase("192.168.74.4:2181,192.168.74.5:2181,192.168.74.6:2181", 2000);
String result = base.createNode("/testRoot", "rootValue");
System.out.println(result);
base.closeClient();
然后会看到控制台输出了刚才创建的节点路径 "/testRoot",假如我们再运行一次上面的创建节点的程序的话,会报org.apache.zookeeper.KeeperException$NodeExistsException: KeeperErrorCode = NodeExists for /testRoot,由此可见不能重复创建节点,否则会报节点已经存在的异常。
创建节点的方法create(String path, byte[] data, List<ACL> acl, CreateMode createMode),需要接收四个参数,path是节点路径,data是节点值,ACL是zookeeper的权限相关的,我们查看上面createNode()方法中传的是Ids.OPEN_ACL_UNSAFE,表示开放所有权限(具体的下一节做详细介绍),createMode表示节点的类型,我们查看CreateMode的源码发现其是一个枚举类,里面定义了四种节点类型,从前到后分别是持久化节点、持久化有序节点、临时节点、临时有序节点。
接下来我们尝试创建一个a节点下的b节点即 "/a/b",此时a节点还没有创建
ZookeeperBase base = new ZookeeperBase("192.168.74.4:2181,192.168.74.5:2181,192.168.74.6:2181", 2000);
String result = base.createNode("/a/b", "test");
System.out.println(result);
base.closeClient();
此时控制台会报org.apache.zookeeper.KeeperException$NoNodeException: KeeperErrorCode = NoNode for /a/b,然后我们尝试给testRoot节点创建一个子节点 children 即 "/testRoot/children",
ZookeeperBase base = new ZookeeperBase("192.168.74.4:2181,192.168.74.5:2181,192.168.74.6:2181", 2000);
String result = base.createNode("/testRoot/children", "childrenValue");
System.out.println(result);
经运行发现children节点创建成功,由此可见:zookeeper的原生java api是不会自动递归创建节点的,即当父节点不存在事不可以创建子节点(下面的方法我没有写具体的调用,您可以自己参照上面代码调用验证哦)
更新节点数据调用的方法是 setData(String path, byte[] data, int version),前两个参数跟前面创建节点的一样这里就不解释了,最后一个version参数代表希望变更的节点的版本号(前面的章节有介绍过,随着变更节点版本号也会变更),如果版本号与节点实际版本号不对应会报异常(org.apache.zookeeper.KeeperException$BadVersionException: KeeperErrorCode = BadVersion for /testRoot/children),如果version传递值为 "-1",则表示忽略版本号限制。
判断节点是否存在调用 exists(String path, boolean watch) 方法,如果存在会返回对应节点,第二个参数是一个布尔类型,true表示需要对当前节点进行监听,false表示不需要监听,具体的会在下一节介绍。
获取节点数据方法getData(String path, boolean watch, Stat stat)
调用方法getChildren(String path, boolean watch)会返回所有子节点数据的集合,注意是返回所有子节点的数据,不会返回孙子节点的数据,如果想获取当前节点下的所有子节点、孙子节点、重孙子节点......需要自己写程序进行递归获取。
删除节点调用方法 delete(String path, int version) ,注意删除节点是不能递归删除节点,即如果当前节点有子节点,必须先把子节点删除,才可以删除父节点,否则会报 org.apache.zookeeper.KeeperException$NotEmptyException: KeeperErrorCode = Directory not empty for /testRoot
下一节会介绍一下zookeeper的监听机制即本节中的watcher,还会对zookeeper的ACL做个简单介绍。