ZooKeeper实战(一)--ZK特性与三个Java客户端(ZooKeeper、ZkClient、Curator)

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;
            }
        });

 

 

 

 

 

 

 

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值