ZooKeeper详解(二):Java客户端API操作ZooKeeper

三、Java客户端API操作ZooKeeper

引入依赖:

        <dependency>
            <groupId>org.apache.zookeeper</groupId>
            <artifactId>zookeeper</artifactId>
            <version>3.4.6</version>
        </dependency>
1)、创建会话

客户端可以通过创建一个ZooKeeper实例来连接ZooKeeper服务器。ZooKeeper的4中构造方法如下:

    public ZooKeeper(String connectString, int sessionTimeout, Watcher watcher)
        throws IOException
        
    public ZooKeeper(String connectString, int sessionTimeout, Watcher watcher,
            boolean canBeReadOnly)
        throws IOException
        
    public ZooKeeper(String connectString, int sessionTimeout, Watcher watcher,
            long sessionId, byte[] sessionPasswd)
        throws IOException
        
    public ZooKeeper(String connectString, int sessionTimeout, Watcher watcher,
            long sessionId, byte[] sessionPasswd, boolean canBeReadOnly)
        throws IOException        
参数名说明
connectString指ZooKeeper服务器列表,由,分开的host:port字符串组成。每一个都代表一台ZooKeeper机器,例如192.168.1.1:2181,192.168.1.2:2181,192.168.1.3:2181,这样就为客户端指定了三台服务器地址。另外,也可以在connectString中设置客户端连接上ZooKeeper后的根目录,方法是在host:port字符串之后添加上这个根目录,例如192.168.1.1:2181,192.168.1.2:2181,192.168.1.3:2181/zk01,这样就指定了该客户端连接上ZooKeeper服务器之后,所有对ZooKeeper的操作都会基于这个根目录
sessionTimeout指会话的超时时间,单位毫秒。在ZooKeeper中有会话的概念,在一个会话周期内,ZooKeeper客户端和服务器之间会通过心跳检测机制来维持会话的有效性,一旦在sessionTimeout时间内没有进行有效的心跳检测,会话就会失效
watcherZooKeepe允许客户端在构造方法中传入一个接口Watcher的实现类对象来作为默认的Watcher事件通知处理器
canBeReadOnlyboolean类型的参数,用于标识当前会话是否支持read-only(只读)模式。默认情况下,在ZooKeeper集群中,一个机器如果和集群中过半及以上机器失去了网络将连接,那么这个机器将不再处理客户端请求(包括读写请求),但是在某些使用场景下,当ZooKeeper服务器发生此类故障的时候,还是希望ZooKeeper服务器能够提供读服务——这就是ZooKeeper的read-only模式
sessionId和sessionPasswd分别代表会话ID和会话秘钥。这两个参数能够唯一确定一个会话,同时客户端使用这两个参数可以实现客户端会话复用,从而达到恢复会话的效果。具体使用方法是,第一次连接上ZooKeeper服务器时,通过调用ZooKeeper对象实例的以下两个接口,即可获得当前会话的ID和秘钥:long getSessionId();``byte[] getSessionPasswd();获得到这两个参数值之后,就可以在下次创建ZooKeeper对象实例的时候传入构造方法了

ZooKeeper客户端和服务端会话的建立是一个异步的过程,也就是说在程序中,构造方法会在处理客户端初始化工作后立即返回,在大多数情况下,此时并没有真正建立好一个可用的会话,在会话的生命周期中处于CONNECTING的状态

当该回话真正创建完毕后,ZooKeeper服务端会向会话对应的客户端发送一个事件通知,以告知客户端,客户端只有在获取这个通知之后,才算真正建立了会话

该构造方法内部实现了与ZooKeeper服务器之间的TCP连接创建,负责维护客户端会话的生命周期

//创建一个最基本的ZooKeeper会话实例
public class ZooKeeperTest implements Watcher {
    private static CountDownLatch countDownLatch = new CountDownLatch(1);

    public static void main(String[] args) throws IOException, InterruptedException {
        ZooKeeper zooKeeper = new ZooKeeper("ip:port", 5000, new ZooKeeperTest());
        System.out.println(zooKeeper.getState());
        countDownLatch.await();
        System.out.println("ZooKeeper session established");
    }

    @Override
    public void process(WatchedEvent event) {
        System.out.println("Receive watched event:" + event);
        if (Event.KeeperState.SyncConnected == event.getState()) {
            countDownLatch.countDown();
        }
    }
}

运行程序,输出结果如下:

CONNECTING
Receive watched event:WatchedEvent state:SyncConnected type:None path:null
ZooKeeper session established

上面这段程序中,使用第一种构造方法ZooKeeper(String connectString, int sessionTimeout, Watcher watcher)来实例化了一个ZooKeeper对象,从而建立了会话

ZooKeeperConnect类实现了Watcher接口,重写了process方法,该方法负责处理来自ZooKeeper服务端的Watcher通知,在收到服务端发来的SyncConnected事件之后,解除主程序在CountDownLatch上的等待阻塞。至此,客户端会话创建完毕

//创建一个最基本的ZooKeeper会话实例,复用sessionId和sessionPassword来创建一个ZooKeeper对象实例
public class ZooKeeperTest2 implements Watcher {
    private static CountDownLatch countDownLatch = new CountDownLatch(1);

    public static void main(String[] args) throws IOException, InterruptedException {
        ZooKeeper zooKeeper = new ZooKeeper("ip:port", 5000, new ZooKeeperTest2());
        countDownLatch.await();
        long sessionId = zooKeeper.getSessionId();
        byte[] passwd = zooKeeper.getSessionPasswd();
        zooKeeper = new ZooKeeper("ip:port", 5000, new ZooKeeperTest2(), 1l, "test".getBytes());
        zooKeeper = new ZooKeeper("ip:port", 5000, new ZooKeeperTest2(), sessionId, passwd);
        Thread.sleep(Integer.MAX_VALUE);
    }

    @Override
    public void process(WatchedEvent event) {
        System.out.println("Receive watched event:" + event);
        if (Event.KeeperState.SyncConnected == event.getState()) {
            countDownLatch.countDown();
        }
    }
}

运行程序,输出结果如下:

Receive watched event:WatchedEvent state:SyncConnected type:None path:null
Receive watched event:WatchedEvent state:Expired type:None path:null
Receive watched event:WatchedEvent state:SyncConnected type:None path:null

上面这段程序中,第一次使用了错误的sessionId和sessionPasswd来创建ZooKeeper客户端的实例,结果客户端接收到了服务端的Expired事件通知;而第二次则使用正确的sessionId和sessionPasswd来创建客户端的实例,结果连接成功

2)、创建节点

客户端可以通过ZooKeeper的API来创建一个数据节点

    public String create(final String path, byte data[], List<ACL> acl,
            CreateMode createMode)
        
    public void create(final String path, byte data[], List<ACL> acl,
            CreateMode createMode,  StringCallback cb, Object ctx)        

这两个方法分别以同步和异步方式创建节点

参数名说明
path需要创建的数据节点的节点路径,例如,/zk01
data[]一个字节数组,是节点创建后的初始内容
acl节点的ACL策略
createMode节点类型,是一个枚举类型,通常有4种可选的节点类型:持久(PERSISTENT)、持久顺序(PERSISTENT_SEQUENTIAL)、临时(EPHEMERAL)、临时顺序(EPHEMERAL_SEQUENTIAL)
cb注册一个异步回调函数。需要实现StringCallback接口,主要是对void processResult(int rc, String path, Object ctx, String name)方法的重新,当服务端节点创建完毕后,ZooKeeper客户端就会自动调用这个方法,这样就可以处理相关的业务逻辑了
ctx用于传递一个对象,可以在回调方法执行的时候使用,通常是放一个上下文(Context)信息

无论是同步还是异步接口,ZooKeeper都不支持递归创建,即无法在父节点不存在的情况下创建一个子节点。另外,如果一个节点已经存在了,那么创建同名节点的时候,会抛出NodeExistsException异常

ZooKeeper的节点内容只支持字节数组类型

关于权限控制,如果应用场景没有太高的权限要求,可以不关注这个参数,只需要在acl参数中传入参数Ids.OPEN_ACL_UNSAFE,这就表明之后对这个节点的任何操作都不受权限控制

//ZooKeeper API创建节点,使用同步方法
public class ZooKeeperTest3 implements Watcher {
    private static CountDownLatch countDownLatch = new CountDownLatch(1);

    public static void main(String[] args) throws IOException, InterruptedException, KeeperException {
        ZooKeeper zooKeeper = new ZooKeeper("ip:port", 5000, new ZooKeeperTest3());
        countDownLatch.await();
        String path = zooKeeper.create("/zk-test-ephemeral-", "hello world".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
        System.out.println("Success create znode:" + path);
        String path2 = zooKeeper.create("/zk-test-ephemeral-", "hello world".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
        System.out.println("Success create znode:" + path2);
    }

    @Override
    public void process(WatchedEvent event) {
        System.out.println("Receive watched event:" + event);
        if (Event.KeeperState.SyncConnected == event.getState()) {
            countDownLatch.countDown();
        }
    }
}

运行程序,输出结果如下:

Receive watched event:WatchedEvent state:SyncConnected type:None path:null
Success create znode:/zk-test-ephemeral-
Success create znode:/zk-test-ephemeral-0000000004

上面这段程序中,使用了同步的节点创建接口:String create(final String path, byte data[], List<ACL> acl, CreateMode createMode)。在接口使用中,分别创建了两种类型的节点:临时节点和临时顺序节点。从返回的结果可以看出,如果创建了临时节点,那么API的返回值就是当时传入的path参数;如果创建了临时顺序节点,那么ZooKeeper会自动在节点后缀加上一个数字,并且在API接口的返回值中返回该数据及节点的一个完整的节点路径

//ZooKeeper API创建节点,使用异步方法
public class ZooKeeperTest4 implements Watcher {
    private static CountDownLatch countDownLatch = new CountDownLatch(1);

    public static void main(String[] args) throws IOException, InterruptedException {
        ZooKeeper zooKeeper = new ZooKeeper("ip:port", 5000, new ZooKeeperTest4());
        countDownLatch.await();
        zooKeeper.create("/zk-test-ephemeral-", "hello world".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL, new IStringCallback(), "I am context.");
        zooKeeper.create("/zk-test-ephemeral-", "hello world".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL, new IStringCallback(), "I am context.");
        zooKeeper.create("/zk-test-ephemeral-", "hello world".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL, new IStringCallback(), "I am context.");
        Thread.sleep(Integer.MAX_VALUE);
    }

    @Override
    public void process(WatchedEvent event) {
        if (Event.KeeperState.SyncConnected == event.getState()) {
            countDownLatch.countDown();
        }
    }

    static class IStringCallback implements AsyncCallback.StringCallback {
        @Override
        public void processResult(int rc, String path, Object ctx, String name) {
            System.out.println("Create path result: [" + rc + "," + path + "," + ctx + ", real path name:" + name + " ]");
        }
    }
}

运行程序,输出结果如下:

Create path result: [0,/zk-test-ephemeral-,I am context., real path name:/zk-test-ephemeral- ]
Create path result: [-110,/zk-test-ephemeral-,I am context., real path name:null ]
Create path result: [0,/zk-test-ephemeral-,I am context., real path name:/zk-test-ephemeral-0000000007 ]

AsyncCallback包含了StatCallback、DataCallback、ACLCallback、ChildrenCallback、Children2Callback、StringCallback、VoidCallback七种不同的回调接口

和同步方法最大的区别在于,节点的创建过程(包括网络通信和服务端的节点创建过程)是异步的。并且,在同步接口调用过程中,需要关注接口抛出异常的可能;但是在异步接口中,接口本身是不会抛出异常的,所有的异常都会在回调函数中通过响应码来体现

回调方法void processResult(int rc, String path, Object ctx, String name)的几个参数说明如下:

参数名说明
rcResult Code,服务端响应码。客户端可以从这个响应码中识别出API调用的结果。常见的响应码如:0(接口调用成功)、-4(客户端和服务器连接已断开)、-110(指定节点已存在)、-112(会话已过期)
path接口调用时传入API的数据节点的节点路径参数值
ctx接口调用时传入API的ctx参数值
name实际在服务端创建的节点名称
3)、删除节点

客户端可以通过ZooKeeper的API来删除一个节点,有如下两个方法:

    public void delete(final String path, int version)
        throws InterruptedException, KeeperException

    public void delete(final String path, int version, VoidCallback cb,
            Object ctx)

这两个方法分别以同步和异步方式删除节点

参数名说明
path指定数据节点的节点路径
version指定节点的数据版本
cb注册一个异步回调函数
ctx用于传递上下文信息的时候
//ZooKeeper API 删除节点,使用同步方法
public class ZooKeeperTest5 implements Watcher {
    private static CountDownLatch countDownLatch = new CountDownLatch(1);

    public static void main(String[] args) throws IOException, InterruptedException, KeeperException {
        String path = "/zk01";
        ZooKeeper zooKeeper = new ZooKeeper("ip:port", 5000, new ZooKeeperTest5());
        countDownLatch.await();
        zooKeeper.create(path, "hello world".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
        zooKeeper.delete(path, -1);
        Thread.sleep(Integer.MAX_VALUE);
    }

    @Override
    public void process(WatchedEvent event) {
        System.out.println("Receive watched event:" + event);
        if (Watcher.Event.KeeperState.SyncConnected == event.getState()) {
            countDownLatch.countDown();
        }
    }
}
4)、读取数据

1)getChildren

客户端可以通过ZooKeeper的API来获取一个节点的所有子节点,有如下8个方法可供使用:

    public List<String> getChildren(final String path, Watcher watcher)
        throws KeeperException, InterruptedException
        
    public List<String> getChildren(String path, boolean watch)
            throws KeeperException, InterruptedException 

    public void getChildren(final String path, Watcher watcher,
            ChildrenCallback cb, Object ctx)
        
    public void getChildren(String path, boolean watch, ChildrenCallback cb,
            Object ctx)
        
    public List<String> getChildren(final String path, Watcher watcher,
            Stat stat)
        
    public List<String> getChildren(String path, boolean watch, Stat stat)
            throws KeeperException, InterruptedException 
            
    public void getChildren(final String path, Watcher watcher,
            Children2Callback cb, Object ctx)
        
    public void getChildren(String path, boolean watch, Children2Callback cb,
            Object ctx)        
参数名说明
path指定数据节点的节点路径
watcher注册的Watcher。一旦在本次子节点获取之后,子节点列表发生变更的话,那么就会向客户端发送通知
watch表明是否需要注册一个Watcher
cb注册一个异步回调函数
ctx用于传递上下文信息的对象
stat指定数据节点的节点状态信息。调用方法时传入一个旧的stat变量,该stat变量会在方法执行过程中,被来自服务端响应的新stat对象替换

如果ZooKeeper客户端在获取到指定节点的子节点列表后,还需要订阅这个子节点列表的变化通知,那么就可以通过注册一个Watcher来实现。当有子节点被添加或是删除时,服务端就会向客户端发送一个NodeChildrenChanged(EventType.NodeChildrenChanged)类型的事件通知。在服务端发送给客户端的事件通知中,是不包含最新的节点列表的,客户端必须主动重新进行获取。通常客户端在收到这个事件通知后,就可以再次获取最新的子节点列表了

stat对象中记录了一个节点的基本属性信息,例如节点创建时的事务ID(cZxid)、最后一次修改的事务ID(mZxid)和节点数据内容的长度(dataLength)等。当需要获取这个节点最新的节点状态信息时,可以将一个旧的stat变量传入API方法,该stat变量会在方法执行过程中,被来自服务端响应的新stat对象替换

//ZooKeeper API获取子节点列表,使用同步方法
public class ZooKeeperTest6 implements Watcher {
    private static CountDownLatch countDownLatch = new CountDownLatch(1);
    private static ZooKeeper zooKeeper = null;

    public static void main(String[] args) throws IOException, InterruptedException, KeeperException {
        String path = "/zk01";
        zooKeeper = new ZooKeeper("ip:port", 5000, new ZooKeeperTest6());
        countDownLatch.await();
        //临时节点不能有子节点
        zooKeeper.create(path, "hello world".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
        zooKeeper.create(path + "/c1", "hello world".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
        List<String> childrenList = zooKeeper.getChildren(path, true);
        System.out.println(childrenList);
        zooKeeper.create(path + "/c2", "hello world".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
        Thread.sleep(Integer.MAX_VALUE);
    }

    @Override
    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("ReGet Child:" + zooKeeper.getChildren(event.getPath(), true));
                } catch (Exception e) {
                }
            }
        }
    }
}

运行程序,输出结果如下:

[c1]
ReGet Child:[c1, c2]

上面这段程序中,首先创建了一个父节点/zk01,以及一个子节点/zk01/c1。然后调用getChildren的同步接口来获取/zk01节点下的所有子节点,同时在接口调用的时候注册了一个Watcher。之后,继续向/zk01节点创建子节点/zk01/c2。由于之前对/zk01节点注册了一个Watcher,因此,一旦此时有子节点被创建,ZooKeeper服务端就会向客户端发出一个子节点变更的事件通知,于是,客户端在收到这个事件通知之后就可以再次调用getChildren方法来获取新的子节点列表

调用getChildren获取到的节点列表都是数据节点的相对节点路径

ZooKeeper服务端在向客户端发送Watcher NodeChildrenChanged事件通知的时候,仅仅只会发出一个通知,而不会把节点的变化情况发送给客户端,需要客户端自己重新获取。另外,由于Watcher通知是一次性的,即一旦触发一次通知后,该Watcher就失效了,因此客户端需要反复注册Watcher

//ZooKeeper API获取子节点列表,使用异步方法
public class ZooKeeperTest7 implements Watcher {
    private static CountDownLatch countDownLatch = new CountDownLatch(1);
    private static ZooKeeper zooKeeper = null;

    public static void main(String[] args) throws IOException, InterruptedException, KeeperException {
        String path = "/zk01";
        zooKeeper = new ZooKeeper("ip:port", 5000, new ZooKeeperTest7());
        countDownLatch.await();
        zooKeeper.create(path, "hello world".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
        zooKeeper.create(path + "/c1", "hello world".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
        zooKeeper.getChildren(path, true, new Children2CallbackImpl(), null);
        zooKeeper.create(path + "/c2", "hello world".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
        Thread.sleep(Integer.MAX_VALUE);
    }

    @Override
    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("ReGet Child:" + zooKeeper.getChildren(event.getPath(), true));
                } catch (Exception e) {
                }
            }
        }
    }

    static class Children2CallbackImpl implements AsyncCallback.Children2Callback {
        @Override
        public void processResult(int rc, String path, Object ctx, List<String> children, Stat stat) {
            System.out.println("Get Children znode result: [response code: " + rc +
                    ", param path: " + path + ", ctx:" + ctx + ", children list: " + children + ", stat:" + stat);
        }
    }
}

运行程序,输出结果如下:

Get Children znode result: [response code: 0, param path: /zk01, ctx:null, children list: [c1], stat:4294967400,4294967400,1564707077928,1564707077928,0,1,0,0,11,1,4294967401
ReGet Child:[c1, c2]

2)getData

客户端可以通过ZooKeeper的API来获取一个节点的数据内容

    public byte[] getData(final String path, Watcher watcher, Stat stat)
        throws KeeperException, InterruptedException
        
    public byte[] getData(String path, boolean watch, Stat stat)
            throws KeeperException, InterruptedException
            
    public void getData(final String path, Watcher watcher,
            DataCallback cb, Object ctx)
        
    public void getData(String path, boolean watch, DataCallback cb, Object ctx)        

客户端在获取一个节点的数据内容的时候,是可以进行Watcher注册的,一旦该节点的状态发生变更,那么ZooKeeper服务端就会向客户端发送一个NodeDataChanged(EventType.NodeDataChanged)的事件通知

//ZooKeeper API获取节点数据内容,使用同步方法
public class ZooKeeperTest8 implements Watcher {
    private static CountDownLatch countDownLatch = new CountDownLatch(1);
    private static ZooKeeper zooKeeper = null;
    private static Stat stat = new Stat();

    public static void main(String[] args) throws IOException, InterruptedException, KeeperException {
        String path = "/zk01";
        zooKeeper = new ZooKeeper("ip:port", 5000, new ZooKeeperTest8());
        countDownLatch.await();
        zooKeeper.create(path, "hello world".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
        System.out.println(new String(zooKeeper.getData(path, true, stat)));
        System.out.println(stat.getCzxid() + "," + stat.getMzxid() + "," + stat.getVersion());
        zooKeeper.setData(path, "hello world".getBytes(), -1);
        Thread.sleep(Integer.MAX_VALUE);
    }

    @Override
    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(new String(zooKeeper.getData(event.getPath(), true, stat)));
                    System.out.println(stat.getCzxid() + "," + stat.getMzxid() + "," + stat.getVersion());
                } catch (Exception e) {}
            }
        }
    }
}

运行程序,输出结果如下:

hello world
4294967412,4294967412,0
hello world
4294967412,4294967413,1

上面这段程序中,首先创建了一个节点/zk01,并初始化其数据内容为hello world。然后调用getData的同步方法来获取/zk01节点的数据内容,调用的同时注册了一个Watcher。之后,同样以hello world去更新该节点的数据内容,此时,由于我们之前在该节点上注册了一个Watcher,因此,一旦该节点的数据发生变化,ZooKeeper服务端就会向客户端发出一个数据变更的事件通知,于是,客户端可以在收到这个事件通知后,再次调用getData方法来获取新的数据内容

另外,在调用getData方法的同时,传入了一个stat变量,在ZooKeeper客户端的内部实现中,会从服务端的响应中获取到数据节点的最新节点状态信息,来替换这个客户端的旧状态

前后两次调用getData方法输出结果如下:

第一次:

hello world
4294967412,4294967412,0

第二次:

hello world
4294967412,4294967413,1

第一次时客户端主动调用getData方法来获取数据,第二次则是节点数据变更后,服务端发送Watcher事件通知给客户端后,客户端再次调用getData方法来获取数据。两次调用的输出结果中,节点数据内容的值并没有变化,但该节点在Zxid为4294967412时被创建,在Zxid为4294967413时被更新,于是节点的数据版本从0变化到1。这里要明确的一点是,节点的数据内容或是节点的数据版本变化,都被看作是ZooKeeper节点的变化,都会触发服务端的NodeDataChanged通知

//ZooKeeper API获取节点数据内容,使用异步方法
public class ZooKeeperTest9 implements Watcher {
    private static CountDownLatch countDownLatch = new CountDownLatch(1);
    private static ZooKeeper zooKeeper = null;

    public static void main(String[] args) throws IOException, InterruptedException, KeeperException {
        String path = "/zk01";
        zooKeeper = new ZooKeeper("ip:port", 5000, new ZooKeeperTest9());
        countDownLatch.await();
        zooKeeper.create(path, "hello world".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
        zooKeeper.getData(path, true, new DataCallbackImpl(), null);
        zooKeeper.setData(path, "hello world".getBytes(), -1);
        Thread.sleep(Integer.MAX_VALUE);
    }

    @Override
    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 {
                    zooKeeper.getData(event.getPath(), true, new DataCallbackImpl(), null);
                } catch (Exception e) {
                }
            }
        }
    }

    static class DataCallbackImpl implements AsyncCallback.DataCallback {
        @Override
        public void processResult(int rc, String path, Object ctx, byte[] data, Stat stat) {
            System.out.println(rc + "," + path + "," + new String(data));
            System.out.println(stat.getCzxid() + "," + stat.getMzxid() + "," + stat.getVersion());
        }
    }
}

运行程序,输出结果如下:

0,/zk01,hello world
4294967420,4294967420,0
0,/zk01,hello world
4294967420,4294967421,1
5)、更新数据

客户端可以通过ZooKeeper的API来更新一个节点的数据内容,有如下两个方法:

    public Stat setData(final String path, byte data[], int version)
        throws KeeperException, InterruptedException
        
    public void setData(final String path, byte data[], int version,
            StatCallback cb, Object ctx)        

上面两个API分别是同步和异步的更新方法

参数名说明
path指定数据节点的节点路径
data[]一个字节数组,需要使用该数据内容来覆盖节点现在的数据内容
version指定节点的数据版本
cb注册一个异步回调函数
ctx用于传递上下文信息的对象
//ZooKeeper API更新节点数据内容,使用同步方法
public class ZooKeeperTest10 implements Watcher {
    private static CountDownLatch countDownLatch = new CountDownLatch(1);
    private static ZooKeeper zooKeeper = null;

    public static void main(String[] args) throws IOException, InterruptedException, KeeperException {
        String path = "/zk01";
        zooKeeper = new ZooKeeper("ip:port", 5000, new ZooKeeperTest10());
        countDownLatch.await();
        zooKeeper.create(path, "123".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
        Stat stat = zooKeeper.setData(path, "456".getBytes(), -1);
        System.out.println(stat.getCzxid() + "," + stat.getMzxid() + "," + stat.getVersion());
        Stat stat2 = zooKeeper.setData(path, "456".getBytes(), -1);
        System.out.println(stat2.getCzxid() + "," + stat2.getMzxid() + "," + stat2.getVersion());
        try {
            zooKeeper.setData(path, "456".getBytes(), stat.getVersion());
        } catch (KeeperException e) {
            System.out.println("Error:" + e.code() + "," + e.getMessage());
        }
        Thread.sleep(Integer.MAX_VALUE);
    }

    @Override
    public void process(WatchedEvent event) {
        if (Event.KeeperState.SyncConnected == event.getState()) {
            if (Event.EventType.None == event.getType() && null == event.getPath()) {
                countDownLatch.countDown();
            }
        }
    }
}

运行程序,输出结果如下:

4294967430,4294967431,1
4294967430,4294967432,2
Error:BADVERSION,KeeperErrorCode = BadVersion for /zk01

上面这段程序中,前后进行了三次更新操作,分别使用了不同的version

在第一次更新操作中,使用的版本是-1,并且更新成功。在ZooKeeper中,数据版本都是从0开始计数的,-1仅仅是一个标识符,如果客户端传入的版本参数是-1,就是告诉ZooKeeper服务器,客户端需要基于数据的最新版本进行更新操作

第一次更新操作成功执行后,ZooKeeper服务端会返回给客户端一个数据节点的节点状态信息对象:stat,可以从中获取服务器上该节点的最新数据版本。从程序的运行情况可以看出,第一次更新操作完成后,节点的数据版本变更为1。于是在第二次更新操作中,调用方法的时候传入了这个版本号,也执行成功,同时看到了这时的数据版本已经变更为2了

在进行第三次操作的时候,程序依然使用了之前的数据版本1来进行更新操作,于是更新失败了

//ZooKeeper API更新节点数据内容,使用异步方法
public class ZooKeeperTest11 implements Watcher {
    private static CountDownLatch countDownLatch = new CountDownLatch(1);
    private static ZooKeeper zooKeeper = null;

    public static void main(String[] args) throws IOException, InterruptedException, KeeperException {
        String path = "/zk01";
        zooKeeper = new ZooKeeper("ip:port", 5000, new ZooKeeperTest11());
        countDownLatch.await();
        zooKeeper.create(path, "123".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
        zooKeeper.setData(path, "456".getBytes(), -1, new StatCallbackImpl(), null);
        Thread.sleep(Integer.MAX_VALUE);
    }

    @Override
    public void process(WatchedEvent event) {
        if (Event.KeeperState.SyncConnected == event.getState()) {
            if (Event.EventType.None == event.getType() && null == event.getPath()) {
                countDownLatch.countDown();
            }
        }
    }

    static class StatCallbackImpl implements AsyncCallback.StatCallback {
        @Override
        public void processResult(int rc, String path, Object ctx, Stat stat) {
            if (rc == 0) {
                System.out.println("SUCCESS");
            }
        }
    }
}
6)、检测节点是否存在

客户端可以通过ZooKeeper的API来检测节点是否存在

    public Stat exists(final String path, Watcher watcher)
        throws KeeperException, InterruptedException
        
    public Stat exists(String path, boolean watch) throws KeeperException,
        InterruptedException
        
    public void exists(final String path, Watcher watcher,
            StatCallback cb, Object ctx)
            
    public void exists(String path, boolean watch, StatCallback cb, Object ctx)           
参数名说明
path指定数据节点的节点路径
watcher注册的Watcher,用于监听三类事件:节点被创建、节点被删除、节点被更新
watch指定是否复用ZooKeeper中默认的Watcher
cb注册一个异步回调函数
ctx用于传递上下文信息的对象

如果在调用方法时注册Watcher的话,一旦节点被创建、删除或是数据被更新都会通知客户端

//ZooKeeper API检测节点是否存在,使用同步方法
public class ZooKeeperTest12 implements Watcher {
    private static CountDownLatch countDownLatch = new CountDownLatch(1);
    private static ZooKeeper zooKeeper = null;

    public static void main(String[] args) throws IOException, InterruptedException, KeeperException {
        String path = "/zk01";
        zooKeeper = new ZooKeeper("ip:port", 5000, new ZooKeeperTest12());
        countDownLatch.await();
        zooKeeper.exists(path, true);
        zooKeeper.create(path, "".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
        zooKeeper.setData(path, "123".getBytes(), -1);
        zooKeeper.create(path + "/c1", "".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
        zooKeeper.delete(path + "/c1", -1);
        zooKeeper.delete(path, -1);
        Thread.sleep(Integer.MAX_VALUE);
    }

    @Override
    public void process(WatchedEvent event) {
        try {
            if (Event.KeeperState.SyncConnected == event.getState()) {
                if (Event.EventType.None == event.getType() && null == event.getPath()) {
                    countDownLatch.countDown();
                } else if (Event.EventType.NodeCreated == event.getType()) {
                    System.out.println("Node(" + event.getPath() + ")Created");
                    zooKeeper.exists(event.getPath(), true);
                } else if (Event.EventType.NodeDeleted == event.getType()) {
                    System.out.println("Node(" + event.getPath() + ")Deleted");
                    zooKeeper.exists(event.getPath(), true);
                } else if (Event.EventType.NodeDataChanged == event.getType()) {
                    System.out.println("Node(" + event.getPath() + ")DataChanged");
                    zooKeeper.exists(event.getPath(), true);
                }
            }
        } catch (Exception e) {
        }
    }
}

运行程序,输出结果如下:

Node(/zk01)Created
Node(/zk01)DataChanged
Node(/zk01)Deleted

上面这段程序中,针对节点/zk01先后进行了如下操作:

1)通过exists方法来检测是否存在指定节点,同时注册了一个Watcher

2)创建节点/zk01,此时服务端马上会向客户端发送一个事件通知:NodeCreated。客户端在收到该事件通知后,再次调用exists方法,同时注册Watcher

3)更新该节点的数据,这个时候,服务端又会向客户端发送一个事件通知:NodeDataChanged。客户端在收到该事件通知后,继续调用exists方法,同时注册Watcher

4)创建子节点/zk01/c1

5)删除子节点/zk01/c1

6)删除节点/zk01。此时客户端会收到服务端的事件通知:NodeDeleted

从上面6个操作以及服务端对应的通知发送中,可以得到如下结论:

  • 无论指定节点是否存在,通过调用exists方法都可以注册Watcher
  • exists方法中注册的Watcher能够对节点创建、节点删除和节点数据更新事件进行监听
  • 对于指定节点的子节点的各种变化,都不会通知客户端
7)、权限控制

通过设置ZooKeeper服务端上数据节点的ACL来控制客户端对该数据节点的访问权限:如果一个客户端符合该ACL控制,那么就可以对其进行访问,否则将无法操作。针对这样的控制机制,ZooKeeper提供了多种权限控制模式,分别是world、auth、digest、ip和super

ZooKeeper客户端提供了响应的API方法来进行权限信息的设置,如下:

    public void addAuthInfo(String scheme, byte auth[])
参数名说明
scheme权限控制模式,分为world、auth、digest、ip和super
auth具体的权限信息

该方法主要用于为当前ZooKeeper会话添加权限信息,之后凡是通过该会话对ZooKeeper服务端进行的任何操作都会带上该权限信息

//使用无权限信息的ZooKeeper会话访问含权限信息的数据节点
public class ZooKeeperAuthTest {
    final static String PATH = "/zk01-auth";

    public static void main(String[] args) throws IOException, KeeperException, InterruptedException {
        ZooKeeper zooKeeper = new ZooKeeper("ip:port", 5000, null);
        zooKeeper.addAuthInfo("digest", "hxt:123456".getBytes());
        zooKeeper.create(PATH, "init".getBytes(), ZooDefs.Ids.CREATOR_ALL_ACL, CreateMode.EPHEMERAL);
        ZooKeeper zooKeeper2 = new ZooKeeper("ip:port", 5000, null);
        zooKeeper2.getData(PATH, false, null);
        Thread.sleep(Integer.MAX_VALUE);
    }
}

运行程序,输出结果如下:

org.apache.zookeeper.KeeperException$NoAuthException: KeeperErrorCode = NoAuth for /zk01-auth

上面这段程序中,首先通过一个包含权限信息的客户端会话创建了一个数据节点,采用了digest模式,这非常类似于username:password的格式,然后使用另一个不包含权限信息的客户端会话对其进行访问,运行后输出了异常信息:NoAuth for /zk01-auth

一旦对一个数据节点设置了权限信息,那么其他没有权限设置或者使用错误权限信息的客户端会话将无法访问该数据节点,ZooKeeper服务端能够为我们实现权限控制

//删除节点接口的权限控制
public class ZooKeeperAuthTest2 {
    final static String PATH = "/zk01-auth";
    final static String PATH2 = "/zk01-auth/child";

    public static void main(String[] args) throws IOException, KeeperException, InterruptedException {
        ZooKeeper zooKeeper = new ZooKeeper("ip:port", 5000, null);
        zooKeeper.addAuthInfo("digest", "hxt:123456".getBytes());
        zooKeeper.create(PATH, "init".getBytes(), ZooDefs.Ids.CREATOR_ALL_ACL, CreateMode.EPHEMERAL);
        zooKeeper.create(PATH2, "init".getBytes(), ZooDefs.Ids.CREATOR_ALL_ACL, CreateMode.EPHEMERAL);
        try {
            ZooKeeper zooKeeper2 = new ZooKeeper("ip:port", 5000, null);
            zooKeeper2.delete(PATH2, -1);
        } catch (Exception e) {
            System.out.println("删除节点失败: " + e.getMessage());
        }
        ZooKeeper zooKeeper3 = new ZooKeeper("ip:port", 5000, null);
        zooKeeper3.addAuthInfo("digest", "hxt:123456".getBytes());
        zooKeeper3.delete(PATH2, -1);
        System.out.println("成功删除节点: " + PATH2);
        ZooKeeper zooKeeper4 = new ZooKeeper("ip:port", 5000, null);
        zooKeeper4.delete(PATH, -1);
        System.out.println("成功删除节点: " + PATH);
        Thread.sleep(Integer.MAX_VALUE);
    }
}

运行程序,输出结果如下:

删除节点失败: KeeperErrorCode = NoAuth for /zk01-auth/child
成功删除节点: /zk01-auth/child
成功删除节点: /zk01-auth

上面这段程序中,第三次删除操作使用的是没有包含权限信息的客户端会话,但最终却成功删除了数据节点。当客户端对一个数据节点添加了权限信息后,对于删除操作而言,其作用范围是其子节点。也就是说,当我们对一个数据节点添加权限信息后,依然可以自由地删除这个节点,但是对于这个节点的子节点,就必须使用相应的权限信息才能够删除它

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

邋遢的流浪剑客

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值