操作

create:创建一个znode(必须要有父节点)
delete:删除一个znode(该znode不能有任何子节点)
exists:测试一个znode是否存在并且查询它的元数据
getACL,setACL:获取/设置一个znode的ACL
getChildren:获取一个znode的子节点列表
getData,setData:获取/设置一个znode所保存的数据
sync:将客户端的znode视图与ZooKeeper同步

ZooKeeper中的更新操作是有条件的。在使用delete或setData操作时必须提供被更新znode的版本号(可以通过exists操作获得)。如果版本号不匹配,则更新操作会失败。更新操作是非阻塞操作,因此一个更新失败的客户端(由于其他进程同时在更新同一个znode)可以决定是否重试,或执行其他操作,并不会因此而阻塞其他进程的执行。
虽然ZooKeeper可以被看作是一个文件系统,但出于简单性的需要,有一些文件系统的基本操作被它摒弃了。由于ZooKeeper中的文件较小并且总是被整体读写,因此没有必要提供打开、关闭或查找操作。
Sync操作与POSIX文件系统中的fsync()操作是不同的。如前所述,ZooKeeper中的写操作具有原子性,一个成功的写操作会保证将数据写到ZooKeeper服务器的持久存储介质中。然而,ZooKeeper允许客户端读到的数据滞后于ZooKeeper服务的最新状态,因此客户端可以使用sync操作来获取数据的最新状态。

1. 集合更新(Multiupdate)

ZooKeeper中有一个被称为multi的操作,用于将多个基本操作集合成一个操作单元,并确保这些基本操作同时被成功执行,或者同时失败,不会发生其中部分基本操作被成功执行而其他基本操作失败的情况。
集合更新可以被用于在ZooKeeper中构建需要保持全局一致性的数据结构,例如构建一个无向图。在ZooKeeper中用一个znode来表示无向图中的一个顶点,为了在两个顶点之间添加或删除一条边,我们需要同时更新两个顶点所分别对应的两个znode,因为每个znode中都有指向对方的引用。如果我们只用ZooKeeper的基本操作来实现边的更新,可能会让其他客户端发现无向图处于不一致的状态,即一个顶点具有指向另一个顶点的引用而对方却没有对应的应用。将针对两个znode的更新操作集合到一个multi操作中可以保证这组更新操作的原子性,也就保证了一对顶点之间不会出现不完整的连接。

2. 关于API

对于ZooKeeper客户端来说,主要有两种语言绑定(binding)可以使用:Java和C;当然也可以使用Perl、Python和REST的contrib绑定。对于每一种绑定语言来说,在执行操作时都可以选择同步执行或异步执行。看exists的签名
public Stat exists(final String path, Watcher watcher) throws KeeperException, InterruptedException
它返回一个封装有znode元数据的Stat对象(如果znode不存在,则返回null)
异步执行的签名如下

public void exists(final String path, Watcher watcher, StatCallback cb, Object ctx)

因为所有操作的结果都是通过回调来传送的,因此在Java API中异步方法的放回类型都是void。调用者传递一个回调的实现,当ZooKeeper相应时,该回调方法被调用。这种情况下,回调采用StatCallback接口,它有以下方法:

/**
* 回调
* @param rc 返回代码,对应于KeeperException的代码。每个非零代码都代表一个异常
* @param path 对应于客户端传递给exists方法的参数,用于识别这个回调所相应的请求。当path参数不能提供足够的信息时,客户端可以通过ctx参数来区分不同请求。如果path参数提供了足够的信息,ctx可以设为null
* @param ctx 对应于客户端传递给exists方法的参数,用于识别这个回调所相应的请求。可以是任意对象
* @param stat 这种情况下,stat参数是null
*/
public void processResult(int rc, String path, Object ctx, Stat stat) {
System.out.println("rc:" + rc);
System.out.println("path:" + path);
System.out.println("ctx:" + ctx);
}

异步API允许以流水线方式处理请求,这在某些情况下可以提供更好的吞吐量。

3. 观察触发器

在exists、getChildren和getData这些读操作上可以设置观察,这些观察可以被写操作create、delete和setData触发。ACL相关的操作不参与触发任何观察。当一个观察被触发时会产生一个观察事件,这个观察和触发它的操作共同决定着观察事件的类型。
当做观察的znode被创建、删除或其数据被更新时,设置在exists操作上的观察将被触发。
当所观察的znode被删除或其数据被更新时,设置在getData操作上的观察将被触发。创建znode不会触发getData操作上的观察,因为getData操作成功执行的前提是znode必须已经存在。
当所观察的znode的一个子节点被创建或删除时,或所观察的znode自己被删除时,设置在getChildren操作上的观察将会被触发。可以通过观察时间的类型来判断被删除的是znode还是其子节点:NodeDelete类型代表znode被删除;NodeChildrenChanged类型代表一个子节点被删除。

一个观察事件中包含涉及该事件的znode的路径,因此对于NodeCreated和NodeDeleted事件来说,可通过路径来判断哪一个节点被创建或删除。为了能够在NodeChildrenChanged事件发生之后判断是哪些子节点被修改,需要重新调用getChildren来获取新的子节点列表。与之类似,为了能够在NodeDataChanged事件之后获取新的数据,需要调用getData。

测试

package com.zhen.zookeeper.existsAndWatcher;

import java.io.IOException;
import java.util.List;
import java.util.concurrent.CountDownLatch;

import org.apache.zookeeper.AsyncCallback.StatCallback;
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.KeeperState;
import org.apache.zookeeper.ZooDefs.Ids;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.Stat;

/**
* @author FengZhen
* @date 2018年10月13日
* exists与观察者
* state=-112 会话超时状态
state= -113 认证失败状态
state= 1 连接建立中
state= 2 (暂时不清楚如何理解这个状态,ZOO_ASSOCIATING_STATE)
state=3 连接已建立状态
state= 999 无连接状态


type=1 创建节点事件
type=2 删除节点事件
type=3 更改节点事件
type=4 子节点列表变化事件
type= -1 会话session事件
type=-2 监控被移除事件

*/
public class ExistsAndWatcher implements Watcher{

private static final int SESSION_TIMEOUT = 5000;

private ZooKeeper zk;
private CountDownLatch connectedSignal = new CountDownLatch(1);

public void connect(String hosts) throws IOException, InterruptedException {
/**
* hosts:ZooKeeper服务的主机地址(可指定端口,默认是2181)
* SESSION_TIMEOUT:以毫秒为单位的会话超时参数(此处为5秒)
* this:Watcher对象的实例。Watcher对象接收来自于ZooKeeper的回调,以获得各种事件的通知。
*/
zk = new ZooKeeper(hosts, SESSION_TIMEOUT, this);
connectedSignal.await();
}

/**
* 当客户端已经与ZK建立连接后,Watcher的process方法会被调用
* 参数是一个用于表示该连接的事件。
*/
public void process(WatchedEvent event) {
int type = event.getType().getIntValue();
System.out.println("watchedEven--" + event.getState().getIntValue() + " : " + type);
//连接事件
if (event.getState() == KeeperState.SyncConnected) {
/**
* 通过调用CountDownLatch的countDown方法来递减它的计数器。
* 锁存器(latch)被创建时带有一个值为1的计数器,用于表示在它释放所有等待线程之前需要发生的事件数。
* 在调用一次countDown方法之后,计数器的值变为0,则await方法返回。
*/
connectedSignal.countDown();
}
//如果为创建或者删除znode的话,需要再添加一个观察者,观察后续操作
if (type == 1 || type == 2) {
existsAndWatcher("/zoo");
}
}

public void create(String groupName) throws KeeperException, InterruptedException {
String path = "/" + groupName;
/**
* 用ZK的create方法创建一个新的ZK的znode
* path:路径(用字符串表示)
* null:znode的内容(字节数组,此处为空值)
* Ids.OPEN_ACL_UNSAFE:访问控制列表(简称ACL,此处为完全开放的ACL,允许任何客户端对znode进行读写)
* CreateMode.PERSISTENT:znode类型
* znode类型可以分为两种:1.短暂的(ephemeral) 2.持久的(persistent)
* 创建znode的客户端断开连接时,无论客户端是明确断开还是因为任何原因而终止,短暂znode都会被ZK服务删除。持久znode不会被删除。
* create方法的返回值是ZK所创建的节点路径
*/
String createdPath = zk.create(path, null, Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
System.out.println("Created " + createdPath);
}

public void close() throws InterruptedException {
zk.close();
}

public boolean exists(String path) throws KeeperException, InterruptedException {
Stat exists = zk.exists(path, true);
return null != exists;
}

public void existsAndWatcher(String path) {
zk.exists(path, this, new StatCallback() {
/**
* 回调
* @param rc 返回代码,对应于KeeperException的代码。每个非零代码都代表一个异常
* @param path 对应于客户端传递给exists方法的参数,用于识别这个回调所相应的请求。当path参数不能提供足够的信息时,客户端可以通过ctx参数来区分不同请求。如果path参数提供了足够的信息,ctx可以设为null
* @param ctx 对应于客户端传递给exists方法的参数,用于识别这个回调所相应的请求。可以是任意对象
* @param stat 这种情况下,stat参数是null
*/
public void processResult(int rc, String path, Object ctx, Stat stat) {
System.out.println("rc:" + rc);
System.out.println("path:" + path);
System.out.println("ctx:" + ctx);
}
}, "标记回调所相应的请求");
}

public void delete(String groupName) {
String path = "/" + groupName;
try {
List<String> children = zk.getChildren(path, false);
for (String child : children) {
zk.delete(path + "/" + child, -1);
}
/**
* delete方法有两个参数
* path:节点路径
* -1:版本号
* 如果所提供的版本号与znode的版本号一致,ZK会删除这个znode。
* 这是一种乐观的加锁机制,使客户端能够检测出对znode的修改冲突。
* 通过将版本号设置为-1,可以绕过这个版本检测机制,不管znode的版本号是什么而直接将其删除。
* ZK不支持递归删除,因此在删除父节点之前必须先删除子节点
*/
zk.delete(path, -1);
} catch (KeeperException e) {
System.out.printf("Group %s does not exist\n", groupName);
System.exit(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}

public static void main(String[] args) throws IOException, InterruptedException, KeeperException {
String hosts = "localhost:2181";
String groupName = "zoo";
ExistsAndWatcher existsAndWatcher = new ExistsAndWatcher();
existsAndWatcher.connect(hosts);
//同步
boolean exists = existsAndWatcher.exists("/zoo");
System.out.println("exists zoo:" + exists);
//异步
existsAndWatcher.existsAndWatcher("/zoo");
existsAndWatcher.create(groupName);
existsAndWatcher.delete(groupName);

existsAndWatcher.close();
}

}

  

4. ACL列表

每个znode被创建时都会带有一个ACL列表,用于决定谁可以对它执行何种操作。
ACL依赖于ZooKeeper的客户端身份验证机制。ZooKeeper提供了以下几种身份验证方式
 Digest:通过用户名和密码来识别客户端
 Sasl:通过Kerberos来识别客户端
 Ip:通过客户端的IP地址来识别客户端

在建立一个ZooKeeper会话之后,客户端可以对自己进行身份验证。虽然znode的ACL列表会要求所有的客户端是经过验证的,但ZooKeeper的身份验证过程却是可选的,客户端必须自己进行身份验证来支持对znode的访问。
使用digest方式进行身份验证的例子
zk.addAuthInfo("digest", "fz:secret".getBytes());
每个ACL都是身份验证方式、符合该方式的一个身份和一组权限的组合。例如,如果打算给IP地址为10.0.0.1的客户端对某个znode的读权限,可以使用IP验证方式、10.0.0.1和READ权限在该znode上设置一个ACL。

测试

package com.zhen.zookeeper.ACL;

import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;

import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.ACL;
import org.apache.zookeeper.data.Id;
import org.apache.zookeeper.server.auth.DigestAuthenticationProvider;
import org.apache.zookeeper.Watcher.Event.KeeperState;
import org.apache.zookeeper.ZooDefs.Perms;


/**
* @author FengZhen
* @date 2018年11月25日
* ACL
*/
public class ACLTest implements Watcher{

private static final int SESSION_TIMEOUT = 5000;

private ZooKeeper zk;
private CountDownLatch connectedSignal = new CountDownLatch(1);

public void connect(String hosts) throws IOException, InterruptedException {
/**
* hosts:ZooKeeper服务的主机地址(可指定端口,默认是2181)
* SESSION_TIMEOUT:以毫秒为单位的会话超时参数(此处为5秒)
* this:Watcher对象的实例。Watcher对象接收来自于ZooKeeper的回调,以获得各种事件的通知。
*/
zk = new ZooKeeper(hosts, SESSION_TIMEOUT, this);
//根据IP
zk.addAuthInfo("ip", "192.168.1.103".getBytes());
//根据用户密码
zk.addAuthInfo("digest", "fz:123456".getBytes());
connectedSignal.await();
}

/**
* 当客户端已经与ZK建立连接后,Watcher的process方法会被调用
* 参数是一个用于表示该连接的事件。
*/
public void process(WatchedEvent event) {
//连接事件
if (event.getState() == KeeperState.SyncConnected) {
/**
* 通过调用CountDownLatch的countDown方法来递减它的计数器。
* 锁存器(latch)被创建时带有一个值为1的计数器,用于表示在它释放所有等待线程之前需要发生的事件数。
* 在调用一次countDown方法之后,计数器的值变为0,则await方法返回。
*/
connectedSignal.countDown();
}
}

public void createACLIP(String groupName) throws KeeperException, InterruptedException {
String path = "/" + groupName;
/**
* 用ZK的create方法创建一个新的ZK的znode
* path:路径(用字符串表示)
* null:znode的内容(字节数组,此处为空值)
* Ids.OPEN_ACL_UNSAFE:访问控制列表(简称ACL,此处为完全开放的ACL,允许任何客户端对znode进行读写)
* CreateMode.PERSISTENT:znode类型
* znode类型可以分为两种:1.短暂的(ephemeral) 2.持久的(persistent)
* 创建znode的客户端断开连接时,无论客户端是明确断开还是因为任何原因而终止,短暂znode都会被ZK服务删除。持久znode不会被删除。
* create方法的返回值是ZK所创建的节点路径
*/
//添加权限,设置IP
ACL aclIP = new ACL(Perms.ALL, new Id("ip", "192.168.1.103"));
System.out.println(aclIP);
List<ACL> acls = new ArrayList<ACL>();
acls.add(aclIP);
String createdPath = zk.create(path, null, acls, CreateMode.PERSISTENT);
System.out.println("Created " + createdPath);
}

public void createACLDigest(String groupName) throws KeeperException, InterruptedException, NoSuchAlgorithmException {
String path = "/" + groupName;
/**
* 用ZK的create方法创建一个新的ZK的znode
* path:路径(用字符串表示)
* null:znode的内容(字节数组,此处为空值)
* Ids.OPEN_ACL_UNSAFE:访问控制列表(简称ACL,此处为完全开放的ACL,允许任何客户端对znode进行读写)
* CreateMode.PERSISTENT:znode类型
* znode类型可以分为两种:1.短暂的(ephemeral) 2.持久的(persistent)
* 创建znode的客户端断开连接时,无论客户端是明确断开还是因为任何原因而终止,短暂znode都会被ZK服务删除。持久znode不会被删除。
* create方法的返回值是ZK所创建的节点路径
*/
//添加权限,设置IP
ACL aclIP = new ACL(Perms.ALL, new Id("digest", DigestAuthenticationProvider.generateDigest("fz:123456")));
System.out.println(aclIP);
List<ACL> acls = new ArrayList<ACL>();
acls.add(aclIP);
String createdPath = zk.create(path, null, acls, CreateMode.PERSISTENT);
System.out.println("Created " + createdPath);
}

public void close() throws InterruptedException {
zk.close();
}

public void writeZnodeACLDigest(String groupName) throws KeeperException, InterruptedException {
String path = "/" + groupName;
zk.setData(path, "test_digest_data".getBytes(), 0);
}

public void writeZnodeACLIP(String groupName) throws KeeperException, InterruptedException {
String path = "/" + groupName;
zk.setData(path, "test_ip_data".getBytes(), 0);
}

public void readZnode(String groupName) throws KeeperException, InterruptedException {
String path = "/" + groupName;
String data = zk.getData(path, false, null).toString();
System.out.println("data = " + data);
}

public static void main(String[] args) throws IOException, InterruptedException, KeeperException, NoSuchAlgorithmException {
String hosts = "192.168.1.103:2181";
String groupNameIP = "znode_acl_test_ip";
String groupNameDigest = "znode_acl_test_digest";
ACLTest aclTest = new ACLTest();
aclTest.connect(hosts);
//digest
// aclTest.createACLDigest(groupNameDigest);
// aclTest.writeZnodeACLDigest(groupNameDigest);
aclTest.readZnode(groupNameDigest);

//IP
// aclTest.createACLIP(groupNameIP);
// aclTest.writeZnodeACLIP(groupNameIP);
// aclTest.readZnode(groupNameIP);

aclTest.close();

//创建完带有ACL的znode之后,查看ACL
//[zk: localhost:2181(CONNECTED) 7] getAcl /znode_acl_test_ip
//'ip,'192.168.1.103
//: cdrwa

//不设置IP直接读取该znode内容,报错如下
//Exception in thread "main" org.apache.zookeeper.KeeperException$NoAuthException: KeeperErrorCode = NoAuth for /znode_acl_test_ip

}


}