ZooKeeper作为一个经典的CP模型分布式服务框架,在许多中间件或者集群中都需要使用到它。它主要是用来解决分布式应用中经常遇到的一些数据管理问题,主要应用场景包括:数据发布订阅、负载均衡、命名服务、Master选举、集群管理、配置管理、分布式队列、分布式锁等。
本文将介绍ZK的数据模型,以及三个Java客户端框架的使用。
ZK的特性
会话
客户端与服务端的一次会话连接,本质是TCP长连接,通过会话可以进行心跳检测和数据传输。会话(session)是zookepper非常重要的概念,客户端和服务端之间的任何交互操作都与会话有关。
客户端和服务端成功连接后,就创建了一次会话,ZK会话在整个运行期间的生命周期中,会在不同的会话状态之间切换,这些状态包括:CONNECTING、CONNECTED、RECONNECTING、RECONNECTED、CLOSE。
一旦客户端开始创建Zookeeper对象,那么客户端状态就会变成CONNECTING状态,同时客户端开始尝试连接服务端,连接成功后,客户端状态变为CONNECTED,通常情况下,由于断网或其他原因,客户端与服务端之间会出现断开情况,一旦碰到这种情况,Zookeeper客户端会自动进行重连服务,同时客户端状态再次变成CONNCTING,直到重新连上服务端后,状态又变为CONNECTED,在通常情况下,客户端的状态总是介于CONNECTING和CONNECTED之间。但是,如果出现诸如会话超时、权限检查或是客户端主动退出程序等情况,客户端的状态就会直接变更为CLOSE状态。
数据模型
ZooKeeper的视图结构和标准的Unix文件系统类似,其中每个节点称为“数据节点”或znode,每个znode可以存储数据,还可以挂载子节点,因此可以称之为“树”。一个znode都必须有值,如果没有值,节点是不能创建成功的。在Zookeeper中,znode是一个跟Unix文件系统路径相似的节点,可以往这个节点存储或获取数据。通过客户端可对znode进行增删改查的操作,还可以注册watcher监控znode的变化。
节点类型
1、Znode有两种类型:
短暂(ephemeral)(create -e /app1/test1 “test1” 客户端断开连接zk删除ephemeral类型节点)
持久(persistent) (create -s /app1/test2 “test2” 客户端断开连接zk不删除persistent类型节点)
2、Znode有四种形式的目录节点(默认是persistent )
PERSISTENT
PERSISTENT_SEQUENTIAL(持久序列/test0000000019 )
EPHEMERAL
EPHEMERAL_SEQUENTIAL
3、创建znode时设置顺序标识,znode名称后会附加一个值,顺序号是一个单调递增的计数器,由父节点维护
4、在分布式系统中,顺序号可以被用于为所有的事件进行全局排序,这样客户端可以通过顺序号推断事件的顺序
节点状态属性
使用get命令可以获取指定ZNode的数据内容和属性信息
状态属性 | 说明 |
cZxid | 数据节点创建时的事务ID |
ctime | 数据节点创建时的时间 |
mZxid | 数据节点最后一次更新时的事务ID |
mtime | 数据节点最后一次更新时的时间 |
pZxid | 数据节点的子节点列表最后一次被修改(是子节点列表变更,而不是子节点内容变更)时的事务ID |
cversion | 子节点的版本号 |
dataVersion | 数据节点的版本号 |
aclVersion | 数据节点的ACL版本号 |
ephemeralOwner | 如果节点是临时节点,则表示创建该节点的会话的SessionID;如果节点是持久节点,则该属性值为0 |
dataLength | 数据内容的长度 |
numChildren | 数据节点当前的子节点个数 |
ACL机制
ACL机制,表示为scheme:id:permissions,第一个字段表示采用哪一种机制,第二个id表示用户,permissions表示相关权限(如只读,读写,管理等)。
zookeeper提供了如下几种机制(scheme):
- world: 它下面只有一个id, 叫anyone, world:anyone代表任何人,zookeeper中对所有人有权限的结点就是属于world:anyone的
- auth: 它不需要id, 只要是通过authentication的user都有权限(zookeeper支持通过kerberos来进行authencation, 也支持username/password形式的authentication)
- digest: 它对应的id为username:BASE64(SHA1(password)),它需要先通过username:password形式的authentication
- ip: 它对应的id为客户机的IP地址,设置的时候可以设置一个ip段,比如ip:192.168.1.0/16, 表示匹配前16个bit的IP段
Java客户端框架实战
本文将介绍三个Java客户端:Zookeeper原生客户端、ZkClient、Curator,其中后两个在实际生产中使用的较多。
Zookeeper原生客户端
创建连接
private final static String CONNECTSTRING="127.0.0.1:2181";
private static CountDownLatch countDownLatch=new CountDownLatch(1);
public static void main(String[] args) throws IOException, InterruptedException {
ZooKeeper zooKeeper=new ZooKeeper(CONNECTSTRING, 5000, new Watcher() {
public void process(WatchedEvent watchedEvent) {
//如果当前的连接状态是连接成功的,那么通过计数器去控制
if(watchedEvent.getState()==Event.KeeperState.SyncConnected){
countDownLatch.countDown();
System.out.println(watchedEvent.getState());
}
}
});
countDownLatch.await();
System.out.println(zooKeeper.getState());
}
这里可以看出ZooKeeper创建连接是异步的,需要通过一个计数器控制。
增删改查节点
首先定义一个Watcher,实现各种场景的监听。
public class ApiOperatorDemo implements Watcher{
private final static String CONNECTSTRING="127.0.0.1:2181";
private static CountDownLatch countDownLatch=new CountDownLatch(1);
private static ZooKeeper zookeeper;
private static Stat stat=new Stat();
public void process(WatchedEvent watchedEvent) {
//如果当前的连接状态是连接成功的,那么通过计数器去控制
if(watchedEvent.getState()==Event.KeeperState.SyncConnected){
if(Event.EventType.None==watchedEvent.getType()&&null==watchedEvent.getPath()){
countDownLatch.countDown();
System.out.println(watchedEvent.getState()+"-->"+watchedEvent.getType());
}else if(watchedEvent.getType()== Event.EventType.NodeDataChanged){
try {
System.out.println("数据变更触发路径:"+watchedEvent.getPath()+"->改变后的值:"+
new String(zookeeper.getData(watchedEvent.getPath(),true,stat)));
} catch (KeeperException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else if(watchedEvent.getType()== Event.EventType.NodeChildrenChanged){//子节点的数据变化会触发
try {
System.out.println("子节点数据变更路径:"+watchedEvent.getPath()+"->节点的值:"+
zookeeper.getData(watchedEvent.getPath(),true,stat));
} catch (KeeperException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else if(watchedEvent.getType()== Event.EventType.NodeCreated){//创建子节点的时候会触发
try {
System.out.println("节点创建路径:"+watchedEvent.getPath()+"->节点的值:"+
zookeeper.getData(watchedEvent.getPath(),true,stat));
} catch (KeeperException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else if(watchedEvent.getType()== Event.EventType.NodeDeleted){//子节点删除会触发
System.out.println("节点删除路径:"+watchedEvent.getPath());
}
}
}
}
节点操作
public static void main(String[] args) throws Exception {
zookeeper=new ZooKeeper(CONNECTSTRING, 5000, new ApiOperatorDemo());
countDownLatch.await();
//创建节点
String result=zookeeper.create("/node1","123".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
zookeeper.getData("/node1",new ApiOperatorDemo(),stat); //增加一个
System.out.println("创建成功:"+result);
//修改数据
zookeeper.getData("/node2",new ApiOperatorDemo(),stat);
zookeeper.setData("/node2","666".getBytes(),-1);
Thread.sleep(2000);
//删除节点
zookeeper.getData("/node2",new ApiOperatorDemo(),stat);
zookeeper.delete("/node2",-1);
Thread.sleep(2000);
//创建节点和子节点
String path="/node123";
zookeeper.create(path,"123".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE,CreateMode.PERSISTENT);
Thread.sleep(2000);
Stat stat=zookeeper.exists(path+"/sub123",true);
if(stat==null){//表示节点不存在
zookeeper.create(path+"/sub123","123".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE,CreateMode.PERSISTENT);
TimeUnit.SECONDS.sleep(1);
}
//修改子路径
zookeeper.setData(path+"/sub123","666".getBytes(),-1);
Thread.sleep(2000);
// 获取指定节点下的子节点
List<String> childrens=zookeeper.getChildren(path,true);
System.out.println(childrens);
}
在getData()方法中可以传入一个Watcher,实现各种事件的监听。注意这里的监听是一次性的,只能触发一次。
原生的ZooKeeper存在一些不足,包括:Watcher只能监听一次,需要多次添加;不能直接创建树、删除树,只能单层操作等。因此我们一般优先其他两个。
ZkClient
ZkClient是一个开源客户端,在Zookeeper原生API接口的基础上进行了包装,更便于开发人员使用。内部实现了Session超时重连,Watcher反复注册等功能。像dubbo等框架对其也进行了集成使用。
创建连接
private final static String CONNECTSTRING="127.0.0.1:2181";
public static void main(String[] args) {
ZkClient zkClient=new ZkClient(CONNECTSTRING,5000);
System.out.println(zkClient + ": ok");
}
操作节点
ZkClient zkClient=new ZkClient(CONNECTSTRING,5000);
//zkclient 提供递归创建父节点的功能
zkClient.createPersistent("/zkclient/zkclient1/zkclient1-1/zkclient1-1-1",true);
System.out.println("success");
//删除节点
zkClient.deleteRecursive("/zkclient");
//获取子节点
List<String> list=zkClient.getChildren("/node123");
System.out.println(list);
// 监听节点值的变化
zkClient.subscribeDataChanges("/node123", new IZkDataListener() {
@Override
public void handleDataChange(String s, Object o) throws Exception {
System.out.println("节点名称:"+s+"->节点修改后的值"+o);
}
@Override
public void handleDataDeleted(String s) throws Exception {
}
});
// 写入节点值
zkClient.writeData("/node123","node123");
TimeUnit.SECONDS.sleep(2);
// 监听子节点的变化
zkClient.subscribeChildChanges("/node123", new IZkChildListener() {
@Override
public void handleChildChange(String s, List<String> list) throws Exception {
System.out.println("节点名称:"+s+"->"+"当前的节点列表:"+list);
}
});
zkClient.delete("/node123/node123");
TimeUnit.SECONDS.sleep(2);
这里的监听事件就不是一次性的,可以一直监听。
Curator
Curator是Netflix公司开源的一套Zookeeper客户端框架,和ZkClient一样,解决了非常底层的细节开发工作,包括连接重连、反复注册Watcher和NodeExistsException异常等。目前已经成为Apache的顶级项目。另外还提供了一套易用性和可读性更强的Fluent风格的客户端API框架。
创建连接
private final static String CONNECTSTRING="127.0.0.1:2181";
public static void main(String[] args) {
//创建会话的两种方式
// normal
CuratorFramework curatorFramework= CuratorFrameworkFactory.
newClient(CONNECTSTRING,5000,5000,
new ExponentialBackoffRetry(1000,3));
curatorFramework.start(); //start方法启动连接
//fluent风格
CuratorFramework curatorFramework1=CuratorFrameworkFactory.builder().connectString(CONNECTSTRING).sessionTimeoutMs(5000).
retryPolicy(new ExponentialBackoffRetry(1000,3)).
build();
curatorFramework1.start();
}
节点操作
try {
// 创建
String result=curatorFramework.create().creatingParentsIfNeeded().withMode(CreateMode.PERSISTENT).
forPath("/curator/curator1/curator11","123".getBytes());
System.out.println(result);
// 删除
// 默认情况下,version为-1
curatorFramework.delete().deletingChildrenIfNeeded().forPath("/curator");
// 查询
Stat stat=new Stat();
byte[] bytes=curatorFramework.getData().storingStatIn(stat).forPath("/curator");
System.out.println(new String(bytes)+"-->stat:"+stat);
// 更新
stat=curatorFramework.setData().forPath("/curator","123".getBytes());
System.out.println(stat);
} catch (Exception e) {
e.printStackTrace();
}
异步操作
ExecutorService service= Executors.newFixedThreadPool(1);
CountDownLatch countDownLatch=new CountDownLatch(1);
try {
curatorFramework.create().creatingParentsIfNeeded().withMode(CreateMode.EPHEMERAL).
inBackground(new BackgroundCallback() {
@Override
public void processResult(CuratorFramework curatorFramework, CuratorEvent curatorEvent) throws Exception {
System.out.println(Thread.currentThread().getName()+"->resultCode:"+curatorEvent.getResultCode()+"->"
+curatorEvent.getType());
countDownLatch.countDown();
}
},service).forPath("/enjoy","deer".getBytes());
} catch (Exception e) {
e.printStackTrace();
}
countDownLatch.await();
service.shutdown();
事务操作(curator独有的)
try {
Collection<CuratorTransactionResult> resultCollections=curatorFramework.inTransaction().create().forPath("/demo1","111".getBytes()).and().
setData().forPath("/demo1","333".getBytes()).and().commit();
for (CuratorTransactionResult result:resultCollections){
System.out.println(result.getForPath()+"->"+result.getType());
}
} catch (Exception e) {
e.printStackTrace();
}
监听器
Curator有三种watcher来做节点的监听
NodeCache:监视一个节点的创建、更新、删除
// NodeCache
NodeCache cache1=new NodeCache(curatorFramework,"/curator",false);
cache1.start(true);
cache1.getListenable().addListener(()-> System.out.println("节点数据发生变化,变化后的结果" +
":"+new String(cache1.getCurrentData().getData())));
curatorFramework.setData().forPath("/curator","666".getBytes());
PathChildrenCache:监视一个路径下子节点的创建、删除、节点数据更新
需要强调两点:
(1)只能监听子节点,监听不到当前节点
(2)不能递归监听,子节点下的子节点不能递归监控
// PathChildrenCache
PathChildrenCache cache2=new PathChildrenCache(curatorFramework,"/curator",true);
// Normal / BUILD_INITIAL_CACHE /POST_INITIALIZED_EVENT
cache2.start(PathChildrenCache.StartMode.POST_INITIALIZED_EVENT);
cache2.getListenable().addListener((curatorFramework1,pathChildrenCacheEvent)->{
switch (pathChildrenCacheEvent.getType()){
case CHILD_ADDED:
System.out.println("增加子节点");
break;
case CHILD_REMOVED:
System.out.println("删除子节点");
break;
case CHILD_UPDATED:
System.out.println("更新子节点");
break;
default:break;
}
});
curatorFramework.create().withMode(CreateMode.PERSISTENT).forPath("/curator","event".getBytes());
TimeUnit.SECONDS.sleep(1);
System.out.println("1");
curatorFramework.create().withMode(CreateMode.EPHEMERAL).forPath("/curator/event1","1".getBytes());
TimeUnit.SECONDS.sleep(1);
System.out.println("2");
curatorFramework.setData().forPath("/curator/event1","222".getBytes());
TimeUnit.SECONDS.sleep(1);
System.out.println("3");
curatorFramework.delete().forPath("/curator/event1");
System.out.println("4");
TreeCache:pathcaceh+nodecache 的合体(监视路径下的创建、更新、删除事件),缓存路径下的所有子节点的数据
TreeCache cache3 = new TreeCache(curatorFramework, "/curator");
cache3.start();
cache3.getListenable().addListener((curatorFramework1,treeCacheEvent)->{
ChildData data = treeCacheEvent.getData();
if(data==null)
{
System.out.println("数据为空");
return;
}
switch (treeCacheEvent.getType()){
case NODE_ADDED:
System.out.println("节点增加, path=" + data.getPath() + ", data=" + new String(data.getData(), "utf-8"));
break;
case NODE_UPDATED:
System.out.println("节点更新, path=" + data.getPath() + ", data=" + new String(data.getData(), "utf-8"));
break;
case NODE_REMOVED:
System.out.println("节点删除, path=" + data.getPath() + ", data=" + new String(data.getData(), "utf-8"));
break;
default:break;
}
});