转 https://blog.youkuaiyun.com/column/details/14599.html
简介
如果你还处于单机时代,那么你将很少用到Zookeeper,很多更好的方案可以帮助你解决问题。一旦涉及到分布式应用,或许在每做一个决定的时候都要想一想,是否可以使用Zookeeper来实现。
Zookeeper是Apache Hadoop的一个子项目,主要是用来解决分布式应用中经常遇到的一些数据管理问题。下图列举了一些可能会遇到的场景。
Zookeeper使得分布式程序能够通过一个共享的、树形结构的名字空间来进行相互协调。组成这个树形结构的数据节点被称作ZNode,它们之间的层级关系就像文件系统的目录结构一样。
zookeeper主要有以下角色:
角色 | 说明 |
---|---|
Leader(领导者) | 为客户端提供读和写的服务,负责投票的发起和决议,更新系统状态。 |
Follower(跟随者) | 为客户端提供读服务,如果是写服务则转发给Leader。在选举过程中参与投票。 |
Observe(观察者) | 为客户端提供读服务器,如果是写服务则转发给Leader。不参与选举过程中的投票,也不参与“过半写成功”策略。在不影响写性能的情况下提升集群的读性能。此角色于zookeeper3.3系列新增的角色。 |
client(客户端) | 连接zookeeper服务器的使用着,请求的发起者。独立于zookeeper服务器集群之外的角色。 |
Zookeeper的客户脚本及命令
1> 启动Zookeeper :
zkServer.sh start
2> zkCli.sh 为连接zookeeper服务器的客户端脚本(默认连接本地的服务器):
sh zkCli.sh
3> zkCli.sh 为连接zookeeper服务器的客户端脚本(指定服务器):
sh zkCli.sh -server ip:port
4> 创建操作:
create [-s] [-e] path data acl
其中,-s或-e分别指定节点为顺序或临时节点,默认情况下创建的为持久节点。
执行如下命令:
create /zk-create-demo 'hello world'
通过上面的命令创建了一个名字叫zk-create-demo的节点,其中节点的内容为‘hello world’。其中acl命令是用来做权限控制的,此例中没有传递此参数,默认不作任何权限控制。
5>查看ls 删除 delete 更新 set 获取值 get
Zookeeper客户端API
1. 创建会话
pom 文件:
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>版本</version>
</dependency>
public ZooKeeper(String connectString, int sessionTimeout, Watcher watcher)
其中,connectString表示要连接的zookeeper服务器地址列表,格式为:192.168.0.1:2181。支持多个地址拼接,中间用逗号分隔。其中地址后面还可以拼接上zookeeper的操作路径,比如:192.168.0.1:2181/zk/test。
sessionTimeout:会话超时时间,单位“毫秒”。通过心跳来监测会话的有效性。
watcher:监听节点的状态变化,如果发生变化则通知此watcher,做出相应处理。如果不需要监听,则可设置为null。
public class TestSession implements Watcher{
private static CountDownLatch countDownLatch = new CountDownLatch(1);
public static void main(String[] args) throws IOException {
Long startTime = new Date().getTime();
ZooKeeper zooKeeper = new ZooKeeper("192.168.0.1:2181",5000,new TestSession());
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("创建连接花费时间:" + (new Date().getTime() - startTime) + "ms");
System.out.println("连接状态:" + zooKeeper.getState());
}
public void process(WatchedEvent event) {
System.out.println("Receive watcher event:" + event);
if(Event.KeeperState.SyncConnected == event.getState()){
countDownLatch.countDown();
}
}
}
由于Zookeeper客户端和服务器创建会话是异步过程,因此使用CountDownLatch来阻塞线程,等待服务器创建完成,并发送事件通知。
2.创建节点
Zookeeper提供了两个创建数据节点的方法。
同步创建数据节点方法:
public String create(final String path, byte data[], List<ACL> acl,
CreateMode createMode)
throws KeeperException, InterruptedException
异步创建数据节点方法:
public void create(final String path, byte data[], List<ACL> acl,
CreateMode createMode, StringCallback cb, Object ctx)
参数说明
参数名称 | 说明 |
---|---|
path | 创建节点的路径,比如:/zk-test-create。 |
data[] | 字节数组,创建节点初始化内容。使用者需自己进行序列化和反序列化。复杂对象可使用 Hessian或Kryo进行进行序列化和反序列化。 |
acl | 节点的acl策略 |
createMode | 节点类型,类型定义在枚举CreateMode中:(1)PERSISTENT:持久;(2)PERSISTENT_SEQUENTIAL:持久顺序;(3)EPHEMERAL:临时;(4)EPHEMERAL_SEQUENTIAL:临时顺序。 |
cb | 异步创建方法参数。注册的回调函数,需实现StringCallback接口。主要针对public void processResult(int rc, String path, Object ctx, String name);接口进行重写。数据节点创建完成之后,会调用此方法进行业务逻辑处理。 |
ctx | 异步创建方法参数。用户传递一个对象,可以在回调方法执行时使用,通常是放一个上下文(Context)信息 |
创建节点demo
下面以具体代码来说明不同方法的使用,针对不同的方法有相应的注释说明:
package com.secbro.learn;
import org.apache.zookeeper.*;
import java.util.concurrent.CountDownLatch;
/**
* Created by zhuzs on 2017/3/23.
*/
public class TestCreateNode implements Watcher {
private static CountDownLatch countDownLatch = new CountDownLatch(1);
public static void main(String[] args) throws Exception {
ZooKeeper zooKeeper = new ZooKeeper("127.0.0.1:2181", 5000, new TestCreateNode());
countDownLatch.await();
// 同步创建临时节点
String ephemeralPath = zooKeeper.create("/zk-test-create-ephemeral-", "123".getBytes(),
ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
System.out.println("同步创临时建节点成功:" + ephemeralPath);
// 同步创建临时顺序节点
String sequentialPath = zooKeeper.create("/zk-test-create-sequential-", "456".getBytes(),
ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
System.out.println("同步创建临时顺序节点成功:" + sequentialPath);
// 异步创建临时节点
zooKeeper.create("/zk-test-create-async-ephemeral-", "abc".getBytes(),
ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL, new MyStringCallBack(), "我是传递内容");
// 异步创建临时顺序节点
zooKeeper.create("/zk-test-create-async-sequential-","def".getBytes(),
ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL,new MyStringCallBack(),"我是传递内容");
Thread.sleep(10000); // 验证等待回调结果使用,可根据实际情况自行调整
}
public void process(WatchedEvent event) {
if (Event.KeeperState.SyncConnected == event.getState()) {
countDownLatch.countDown();
}
}
}
class MyStringCallBack implements AsyncCallback.StringCallback {
public void processResult(int rc, String path, Object ctx, String name) {
System.out.println("异步创建回调结果:状态:" + rc +";创建路径:" +
path + ";传递信息:" + ctx + ";实际节点名称:" + name);
}
}
输出结果为
同步创临时建节点成功:/zk-test-create-ephemeral-
同步创建临时顺序节点成功:/zk-test-create-sequential-0000000023
异步创建回调结果:状态:0;创建路径:/zk-test-create-async-ephemeral-;传递信息:我是传递内容;实际节点名称:/zk-test-create-async-ephemeral-
异步创建回调结果:状态:0;创建路径:/zk-test-create-async-sequential-;传递信息:我是传递内容;实际节点名称:/zk-test-create-async-sequential-0000000025
根据上面的代码和结果,很容易得知不同方法的使用方式。
注意事项
(1)Zookeeper不支持递归创建数据节点,无法在父节点不存在的情况下创建子节点。否则会抛出类似以下异常:
(2)如果节点已经存在,再创建同名节点,会抛出NodeExistsException。
(3)关于权限控制,如果没有特殊要求,可按照上面例子中直接设置为ZooDefs.Ids.OPEN_ACL_UNSAFE,表明之后对节点的任何操作都不受权限控制。
StringCallback接口相关
StringCallback接口集成了AsyncCallback接口,来实现回调时的业务处理。其中AsyncCallback接口还包8个回调接口:StatCallback、DataCallback、ACLCallback、ChildrenCallback、Children2Callback、VoidCallback、MultiCallback、StringCallback。可以在不同的异步接口中实现不同的回调接口。
StringCallback接口的public void processResult(int rc, String path, Object ctx, String name)方法。
从上面的实例中已经可以看出。
int rc为服务器的响应码,0表示调用成功,-4表示连接已断开,-110表示指定节点已存在,-112表示会话已过期。
String path调用create方法时传入的path。
Object ctx调用create方法时传入的ctx。
String name创建成功的节点名称。
3. 获取子节点列表方法
Zookeeper原生客户端API提供了以下8中获取子节点列表的方法,每个方法的使用说明参考注释内容:
/**
* 返回指定路径下面的子节点列表。
* 如果watcher不为null,并且调用成功(没有异常),会将watcher注册在指定的path
* 上。当path(父节点)被删除或者path下面创建/删除子节点,将触发通知watcher。
*
* 返回结果列表不保证有序性。
*/
public List<String> getChildren(final String path, Watcher watcher)
/**
* 使用说明同上一个方法。
*/
public List<String> getChildren(String path, boolean watch)
/**
* getChildren(String, Watcher)的异步版本
*/
public void getChildren(final String path, Watcher watcher,
ChildrenCallback cb, Object ctx)
/**
* getChildren(String, boolean)的异步版本
*/
public void getChildren(String path, boolean watch, ChildrenCallback cb,
Object ctx)
/**
* 为指定的路径返回stat和子节点列表。
*
*/
public List<String> getChildren(final String path, Watcher watcher,
Stat stat)
/**
* 使用说明同上一个方法。
*/
public List<String> getChildren(String path, boolean watch, Stat stat)
/**
* getChildren(String, Watcher, Stat)的异步版本
*/
public void getChildren(final String path, Watcher watcher,
Children2Callback cb, Object ctx)
/**
* getChildren(String, boolean, Stat)的异步版本
*/
public void getChildren(String path, boolean watch, Children2Callback cb,
Object ctx)
参数说明
参数名 | 说明 |
---|---|
path | 指定数据节点的路径,获取该节点下面的子节点 |
watcher | 注册在path上的Watcher。path被删除或者path下面的创建/删除节点,向客户端发送通知。可为null |
watch | 表示是否需要注册一个watcher。true:注册默认watcher,false:不需要注册watcher |
cb | 注册一个异步的回调函数 |
ctx | 用户传递信息 |
stat | 指定数据节点状态信息。传入旧stat,方法执行过程中会将其替换为新stat对象。 |
具体案例
package com.secbro.learn;
import org.apache.zookeeper.*;
import java.util.List;
import java.util.concurrent.CountDownLatch;
/**
* 获得子节点列表的方法
* Created by zhuzs on 2017/3/26.
*/
public class TestGetChildrenMethod implements Watcher {
private static CountDownLatch countDownLatch = new CountDownLatch(1);
private static ZooKeeper zooKeeper;
public static void main(String[] args) throws Exception {
zooKeeper = new ZooKeeper("127.0.0.1:2181", 5000, new TestGetChildrenMethod());
countDownLatch.await();
// 创建父节点/test
zooKeeper.create("/test", "123".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
// 在父节点/test下面创建a1节点
zooKeeper.create("/test/a1", "456".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
// 同步获得结果
List<String> childrenList = zooKeeper.getChildren("/test", true);
System.out.println("同步getChildren获得数据结果:" + childrenList);
// 异步获得结果
zooKeeper.getChildren("/test",true,new MyChildren2Callback(),null);
// 在父节点/test下面创建a2节点
zooKeeper.create("/test/a2", "456".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
Thread.sleep(10000);
}
public void process(WatchedEvent event) {
if (Event.KeeperState.SyncConnected == event.getState()) {
if(Event.EventType.None == event.getType() && null == event.getPath()){ // 连接时的监听事件
countDownLatch.countDown();
} else if (event.getType() == Event.EventType.NodeChildrenChanged){ // 子节点变更时的监听
try {
System.out.println("重新获得Children,并注册监听:" + zooKeeper.getChildren(event.getPath(),true));
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
class MyChildren2Callback implements AsyncCallback.Children2Callback{
public void processResult(int rc, String path, Object ctx, List<String> children, Stat stat) {
System.out.println("异步获得getChildren结果,rc=" + rc
+ ";path=" + path + ";ctx=" + ctx + ";children=" + children +";stat=" + stat);
}
}
输出结果:
同步getChildren获得数据结果:[a1]
异步获得getChildren结果,rc=0;path=/test;ctx=null;children=[a1];stat=14277,14277,1490498559704,1490498559704,0,1,0,0,3,1,14278
重新获得Children,并注册监听:[a1, a2]
上面的例子,先创建了一个父节点test,然后在其下面创建了a1子节点,并注册监听。当父节点test下面创建了新的a2子节点之后,监听事件触发。但此时需要重新主动获取该父节点下面的子节点,并继续注册监听事件。
上面只提供了新增子节点的案例,其实删除子节点或删除父节点同样会触发监听事件。
4. 读取节点内容方法介绍
Zookeeper提供了两个方法来获取节点内容,同步获取和异步获取:
public byte[] getData(String path, boolean watch, Stat stat)
public void getData(final String path, Watcher watcher,
DataCallback cb, Object ctx)
参数说明
参数 | 说明 |
---|---|
path | 指定数据节点的路径,获取该节点下面的子节点 |
watcher | 注册在path上的Watcher。节点变更会通知会向客户端发起通知。 |
stat | 指定数据节点状态信息。传入旧stat,方法执行过程中会将其替换为新stat对象。 |
watch | 表示是否需要注册一个watcher。true:注册默认watcher,false:不需要注册watcher |
cb | 注册一个异步回调函数 |
ctx | 传递上下文信息 |
具体案例
同步方法
package com.secbro.learn;
import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;
import java.util.concurrent.CountDownLatch;
/**
* 读取节点数据
* Created by zhuzs on 2017/3/27.
*/
public class TestGetData implements Watcher{
private static CountDownLatch countDownLatch = new CountDownLatch(1);
private static ZooKeeper zooKeeper;
private static Stat stat = new Stat();
public static void main(String[] args) throws Exception{
zooKeeper = new ZooKeeper("127.0.0.1:2181", 5000, new TestGetData());
countDownLatch.await();
String path = "/test-get-data";
zooKeeper.create(path,"123".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
System.out.println("同步读取节点内容:" + new String(zooKeeper.getData(path,true,stat)));
System.out.println("同步读取Stat:czxid=" + stat.getCzxid()
+ ";mzxid=" + stat.getMzxid() + ";version=" + stat.getVersion());
zooKeeper.setData(path,"123".getBytes(),-1);
Thread.sleep(10000);
}
public void process(WatchedEvent event) {
if (Event.KeeperState.SyncConnected == event.getState()) {
if(Event.EventType.None == event.getType() && null == event.getPath()){ // 连接时的监听事件
countDownLatch.countDown();
} else if (event.getType() == Event.EventType.NodeDataChanged){ // 子节点内容变更时的监听
try {
System.out.println("监听获得通知内容:data="
+ new String(zooKeeper.getData(event.getPath(),true,stat)));
System.out.println("监听获得通知Stat:czxid=" + stat.getCzxid()
+ ";mzxid=" + stat.getMzxid() + ";version=" + stat.getVersion());
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
运行结果:
同步读取节点内容:123
同步读取Stat:czxid=14700;mzxid=14700;version=0
监听获得通知内容:data=123
监听获得通知Stat:czxid=14700;mzxid=14701;version=1
代码的基本逻辑为创建一个临时节点,然后读取临时节点内容,并注册监听,当节点变化(内容变化或版本信息变化),触发监听事件,获取最新的节点信息。
异步方法
package com.secbro.learn;
import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;
import java.util.concurrent.CountDownLatch;
/**
* 读取节点数据
* Created by zhuzs on 2017/3/27.
*/
public class TestGetData implements Watcher{
private static CountDownLatch countDownLatch = new CountDownLatch(1);
private static ZooKeeper zooKeeper;
private static Stat stat = new Stat();
public static void main(String[] args) throws Exception{
zooKeeper = new ZooKeeper("127.0.0.1:2181", 5000, new TestGetData());
countDownLatch.await();
String path = "/test-get-data";
zooKeeper.create(path,"123".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
// 异步读取节点内容
zooKeeper.getData(path,true,new MyDataCallback(),null);
zooKeeper.setData(path,"123".getBytes(),-1);
Thread.sleep(10000);
}
public void process(WatchedEvent event) {
if (Event.KeeperState.SyncConnected == event.getState()) {
if(Event.EventType.None == event.getType() && null == event.getPath()){ // 连接时的监听事件
countDownLatch.countDown();
} else if (event.getType() == Event.EventType.NodeDataChanged){ // 子节点内容变更时的监听
try {
System.out.println("监听获得通知内容:data="
+ new String(zooKeeper.getData(event.getPath(),true,stat)));
System.out.println("监听获得通知Stat:czxid=" + stat.getCzxid()
+ ";mzxid=" + stat.getMzxid() + ";version=" + stat.getVersion());
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
class MyDataCallback implements AsyncCallback.DataCallback{
public void processResult(int rc, String path, Object ctx, byte[] data, Stat stat) {
System.out.println("异步返回结果:rc=" + rc + ";path=" + path + ";data=" + new String(data));
System.out.println("异步读取Stat:czxid=" + stat.getCzxid()
+ ";mzxid=" + stat.getMzxid() + ";version=" + stat.getVersion());
}
}
运行结果:
异步返回结果:rc=0;path=/test-get-data;data=123
异步读取Stat:czxid=14704;mzxid=14704;version=0
监听获得通知内容:data=123
监听获得通知Stat:czxid=14704;mzxid=14705;version=1
异步方法与同步方法业务逻辑基本相同,区别点在于将同步获取改为异步获取。
更新操作中的版本参数如果为-1,则表示更新操作针对任何版本均可。当更新版本不为-1,且不等于节点的目前版本,则更新失败
5. 删除节点
同步删除:
public void delete(final String path, int version)
异步删除:
public void delete(final String path, int version, VoidCallback cb,
Object ctx)
参数说明
参数 | 说明 |
---|---|
path | 操作节点路径 |
version | 指定更新节点的数据版本。当为-1时表示任何版本 |
cb | 注册一个回调函数 |
ctx | 传递上下文信息 |
其他说明
1、版本操作同修改节点使用方法。
2、如果一个节点下面有子节点,需先删除子节点,然后才能删除父节点。
zookeeper递归删除所有节点
public void deleteNode(String path) throws KeeperException, InterruptedException {
String pathFull = path;
if(path.equalsIgnoreCase("/zookeeper")){
return;
}
List<String> childNodeList = zooKeeper.getChildren(path,false);
if(childNodeList.size()>0){
for(String str :childNodeList){
if(pathFull.equals("/")){
deleteNode(pathFull+str);
}else {
deleteNode(pathFull+"/"+str);
}
}
}else{
zooKeeper.delete(path,-1);
}
}