Zookeeper(八)-zookeeper客户端Curator

zookeeper的原生api相对来说比较繁琐,比如:对节点添加监听事件,当监听触发后,我们需要再次手动添加监听,否则监听只生效一次;再比如,断线重连也需要我们手动代码来判断处理等等。对于curator的介绍,从网上百度了一段:Curator是Netflix开源的一套zookeeper客户端框架,用它来操作zookeeper更加方便,按Curator官方所比喻的,guava to JAVA,curator to zookeeper,Curator采用了fluent风格的代码,非常简洁

和Curator相比, 另一个ZooKeeper客户端——zkClient(https://github.com/sgroschupf/zkclient)的不足之处: 
文档几乎没有 
异常处理弱爆了(简单的抛出RuntimeException) 
重试处理太难用了 
没有提供各种使用场景的实现 

对ZooKeeper自带客户端(ZooKeeper类)的"抱怨": 
只是一个底层实现 
要用需要自己写大量的代码 
很容易误用 
需要自己处理连接丢失, 重试等

----------------------------------------------------------------------------------------------------------------------------------

  Curator包含6部分,均可提供单独jar包,每个包简单介绍如下:

  client:zk-client的替代品,提供一些底层处理跟工具类;

  framework:高级封装,大大简化了zk的客户端编程,包含对zk的连接管理,重试机制等;

  repices:提供了一些常用的操作,比如持续监听,锁,选举等;

  utilities:各种工具类;

  errors:curator对异常跟错误的处理;

  extendsion:扩展包;

maven依赖如下

Xml代码   收藏代码
  1. <dependency>  
  2.     <groupId>org.apache.curatorgroupId>  
  3.     <artifactId>curator-recipesartifactId>  
  4.     <version>2.5.0version>  
  5. dependency>  

 

 

按照官方给出的文档和包结构,可以轻松的看出Curator功能分两大类,一是对zookeeper的一些基本命令的封装,比如增删改查。是他的framework模块,一个是他的高级特性,即recipes模块。

 

一、framework模块

Curator提供了一套Fluent风格的操作API。这在很多脚本类语言里比较流行。

比如他创建client的代码是这样

Java代码   收藏代码
  1. CuratorFramework client = builder.connectString("192.168.11.56:2180")  
  2.         .sessionTimeoutMs(30000)  
  3.         .connectionTimeoutMs(30000)  
  4.         .canBeReadOnly(false)  
  5.         .retryPolicy(new ExponentialBackoffRetry(1000, Integer.MAX_VALUE))  
  6.         .namespace(namespace)  
  7.         .defaultData(null)  
  8.         .build();  
  9. client.start();  

 一路点到底,这就是所谓的Fluent风格。 

 

我们再看增删改查的

Java代码   收藏代码
  1. public class CrudExamples {  
  2.     private static CuratorFramework client = ClientFactory.newClient();  
  3.     private static final String PATH = "/crud";  
  4.   
  5.     public static void main(String[] args) {  
  6.         try {  
  7.             client.start();  
  8.   
  9.             client.create().forPath(PATH, "I love messi".getBytes());  
  10.   
  11.             byte[] bs = client.getData().forPath(PATH);  
  12.             System.out.println("新建的节点,data为:" + new String(bs));  
  13.   
  14.             client.setData().forPath(PATH, "I love football".getBytes());  
  15.   
  16.             // 由于是在background模式下获取的data,此时的bs可能为null  
  17.             byte[] bs2 = client.getData().watched().inBackground().forPath(PATH);  
  18.             System.out.println("修改后的data为" + new String(bs2 != null ? bs2 : new byte[0]));  
  19.   
  20.             client.delete().forPath(PATH);  
  21.             Stat stat = client.checkExists().forPath(PATH);  
  22.   
  23.             // Stat就是对zonde所有属性的一个映射, stat=null表示节点不存在!  
  24.             System.out.println(stat);  
  25.         } catch (Exception e) {  
  26.             e.printStackTrace();  
  27.         } finally {  
  28.             CloseableUtils.closeQuietly(client);  
  29.         }  
  30.     }  
  31. }  

 常用接口有

create()增

delete(): 删

checkExists(): 判断是否存在

setData():  改

getData(): 查

所有这些方法都以forpath()结尾,辅以watch(监听),withMode(指定模式),和inBackground(后台运行)等方法来使用。

(补充) 1、建立连接

  建立连接需要指定zk地址以及重试策略等,先上代码再解释:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
RetryPolicy retry = new ExponentialBackoffRetry( 1000 , 5 ); //重试5次,每次间隔时间指数增长(有具体增长公式)
RetryPolicy retry1 = new RetryNTimes( 5 , 5000 ); //重试5次,每次间隔5秒
RetryPolicy retry2 = new RetryUntilElapsed( 60000 * 2 , 5000 ); //重试2分钟,每次间隔5秒
//普通创建
CuratorFramework client = CuratorFrameworkFactory.newClient( "localhost:2181" , 5000 , 5000 , retry);
//fluent风格创建
CuratorFramework client1 = CuratorFrameworkFactory.builder()
          .connectString( "localhost:2181" )
          .connectionTimeoutMs( 5000 )   //连接超时时间
          .sessionTimeoutMs( 3000 )      //会话超时时间
          .retryPolicy(retry)
          .build();
 
  //建立连接
  client.start();

  如注释,创建客户端连接我们通常需要指定重试策略,curator提供了3种重试机制,分别如上;对于fluent风格,就是每个操作都返回了一个对象,我们可以一直通过[.方法名]的方式书写代码;client创建了之后,需要调用start方法才能真正去建立连接。会话超时时间是指当连接发生故障时,由于zk的心跳机制检测,服务端认为会话超时的时间,会清除session;

  2、创建、删除、更新节点

  连接建立之后,我们可以在服务器上进行创建节点的操作,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//创建节点
String path = client.create()
          .creatingParentsIfNeeded()        //对节点路径上没有的节点进行创建
          .withMode(CreateMode.EPHEMERAL)   //临时节点
          .forPath( "/curator/test" , "123" .getBytes());  //节点路径,节点的值
 
//删除节点
client.delete()
        .guaranteed()      //删除失败,则客户端持续删除,直到节点删除为止
        .deletingChildrenIfNeeded()   //删除相关子节点
        .withVersion(- 1 )    //无视版本,直接删除
        .forPath( "/curator/mytest" );
//更新节点信息
Stat stat2 = new Stat();
byte [] theValue2 = client.getData().storingStatIn(stat).forPath( "/curator/test" );
client.setData()
        .withVersion(stat2.getVersion())  //版本校验,与当前版本不一致则更新失败,-1则无视版本信息进行更新
        .forPath( "/curator/test" , "456" .getBytes());
//判断节点是否存在(存在返回节点信息,不存在则返回null)
Stat s = client.checkExists().forPath( "/curator/test" );

  持久节点是persistent,我们创建的节点可能有好几层,如果服务器不存在父节点则会报错并创建失败,createingParentsIfNodeed()的作用是在父节点不存在的时候进行创建。删除操作可能由于网络抖动等情况导致删除失败,由于节点数据操作一般对业务影响较大,故多数都会带持续删除的动作来确保正确删除;节点更新删除等操作若考虑版本校验,则采用代码所示方式,在获取节点数据的时候对节点状态进行赋值,然后通过节点状态可以获得版本信息。判断节点是否存在,一般通过节点信息判断,若不存在,则节点信息为null。

  3、获取字节点列表

  只有一行代码,返回string类型的list

1
2
//获取子节点列表
List paths = client.getChildren().forPath( "/curator" );

  4、异步操作

  异步操作不会阻塞代码执行,对于操作完成后的业务处理,需要设定回调函数来完成。以判断节点是否存在为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ExecutorService es = Executors.newFixedThreadPool( 5 ); //异步操作线程池,
//异步判断操作
Stat s1 = client.checkExists().inBackground().forPath( "/curator/test" ); //无回调
client.checkExists().inBackground( new BackgroundCallback() {  //有回调
     @Override
     public void processResult(CuratorFramework curatorFramework, CuratorEvent curatorEvent) throws Exception {
         CuratorEventType c = curatorEvent.getType(); //事件类型,可在CuratorEventType看到具体种类
         int r = curatorEvent.getResultCode(); //0,执行成功,其它,执行失败
         Object o = curatorEvent.getContext(); //事件上下文,一般是由调用方法传入,供回调函数使用的参数
         String p = curatorEvent.getPath(); //节点路径
         List li = curatorEvent.getChildren(); //子节点列表
         byte [] datas = curatorEvent.getData(); //节点数据
         //一些其它操作
     }
},es).forPath( "/curator/test" );

  异步操作实际是在后台另起一个线程来完成该操作,若线程较多势必会影响服务器性能,所以要用线程池来尽量降低对服务器的消耗。需要考虑线程池的关闭操作,较繁琐,不作赘述。

  5、节点、子节点监听

  节点监听需要用repices包中的NodeCache来完成,代码如下:

1
2
3
4
5
6
7
8
9
10
//节点监听
final NodeCache cache = new NodeCache(client, "/curator/test" );
cache.start();
cache.getListenable().addListener( new NodeCacheListener() { //监听对象
     @Override
     public void nodeChanged() throws Exception { //重写监听方法
         byte [] ret = cache.getCurrentData().getData();
         System.out.println( "当前节点内容是:" + new String(ret));
     }
});

  子节点的监听需要用PathChildrenCache来完成,跟节点本身不一样,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//子节点监听
final PathChildrenCache pccache = new PathChildrenCache(client, "/curator" , true ); //true指当子节点变化时,获取子节点内容
pccache.start();
pccache.getListenable().addListener( new PathChildrenCacheListener() {
     @Override
     public void childEvent(CuratorFramework curatorFramework, PathChildrenCacheEvent pathChildrenCacheEvent) throws Exception { //重写监听方法
         switch (pathChildrenCacheEvent.getType()){ //子节点的事件类型
             case CHILD_ADDED:
                 System.out.println(pathChildrenCacheEvent.getData()); //通过pathChildrenCacheEvent,可以获取到节点相关的数据
                 break ;
             case CHILD_REMOVED:
                 System.out.println(pathChildrenCacheEvent.getData().getPath());
                 break ;
             case CHILD_UPDATED:
                 break ;
             default :
                 break ;
         }
     }
});

  6、权限控制部分,略。

 此外,Curator还支持事务,一组crud操作同生同灭。代码如下

Java代码   收藏代码
  1. /** 
  2.  * 事务操作 
  3.  *  
  4.  * @author shencl 
  5.  */  
  6. public class TransactionExamples {  
  7.     private static CuratorFramework client = ClientFactory.newClient();  
  8.   
  9.     public static void main(String[] args) {  
  10.         try {  
  11.             client.start();  
  12.             // 开启事务  
  13.             CuratorTransaction transaction = client.inTransaction();  
  14.   
  15.             Collection results = transaction.create()  
  16.                     .forPath("/a/path""some data".getBytes()).and().setData()  
  17.                     .forPath("/another/path""other data".getBytes()).and().delete().forPath("/yet/another/path")  
  18.                     .and().commit();  
  19.   
  20.             for (CuratorTransactionResult result : results) {  
  21.                 System.out.println(result.getForPath() + " - " + result.getType());  
  22.             }  
  23.         } catch (Exception e) {  
  24.             e.printStackTrace();  
  25.         } finally {  
  26.             // 释放客户端连接  
  27.             CloseableUtils.closeQuietly(client);  
  28.         }  
  29.   
  30.     }  
  31. }  

 这段的代码的运行结果,由于最后一步delete的节点不存在,所以整个事务commit失败。失败的原因会放在Collection中,非常友好。

 

好了framework部分的内容就这么多,是不是特别简单呢。下面就来看看recipes包的内容吧。。

 

Recipes部分提供的功能官网列的很详细,点击这里。注意文章第一段:Curator宣称,Recipes模块实现了除二阶段提交之外的所有zookeeper特性。

 

二、Recipes模块

 

主要有

Elections(选举),Locks(锁),Barriers(关卡),Atomic(原子量),Caches,Queues等

 

1、 Elections

选举主要依赖于LeaderSelector和LeaderLatch2个类。前者是所有存活的客户端不间断的轮流做Leader,大同社会。后者是一旦选举出Leader,除非有客户端挂掉重新触发选举,否则不会交出领导权。某党?

 

这两者在实现上是可以切换的,直接上代码,怎么切换注释里有。由于篇幅所限,这里仅贴出基于LeaderSelector的选举,更多代码见附件

Java代码   收藏代码
  1. /** 
  2.  * 本类基于leaderSelector实现,所有存活的client会公平的轮流做leader 
  3.  * 如果不想频繁的变化Leader,需要在takeLeadership方法里阻塞leader的变更! 或者使用 {@link} 
  4.  * LeaderLatchClient 
  5.  */  
  6. public class LeaderSelectorClient extends LeaderSelectorListenerAdapter implements Closeable {  
  7.     private final String name;  
  8.     private final LeaderSelector leaderSelector;  
  9.     private final String PATH = "/leaderselector";  
  10.   
  11.     public LeaderSelectorClient(CuratorFramework client, String name) {  
  12.         this.name = name;  
  13.         leaderSelector = new LeaderSelector(client, PATH, this);  
  14.         leaderSelector.autoRequeue();  
  15.     }  
  16.   
  17.     public void start() throws IOException {  
  18.         leaderSelector.start();  
  19.     }  
  20.   
  21.     @Override  
  22.     public void close() throws IOException {  
  23.         leaderSelector.close();  
  24.     }  
  25.   
  26.     /** 
  27.      * client成为leader后,会调用此方法 
  28.      */  
  29.     @Override  
  30.     public void takeLeadership(CuratorFramework client) throws Exception {  
  31.         int waitSeconds = (int) (5 * Math.random()) + 1;  
  32.         System.out.println(name + "是当前的leader");  
  33.         try {  
  34.             Thread.sleep(TimeUnit.SECONDS.toMillis(waitSeconds));  
  35.         } catch (InterruptedException e) {  
  36.             Thread.currentThread().interrupt();  
  37.         } finally {  
  38.             System.out.println(name + " 让出领导权\n");  
  39.         }  
  40.     }  

 

Java代码   收藏代码
  1. /** 
  2.  * leader选举 
  3.  *  
  4.  * @author shencl 
  5.  */  
  6. public class LeaderSelectorExample {  
  7.   
  8.     public static void main(String[] args) {  
  9.   
  10.         List clients = Lists.newArrayList();  
  11.         List examples = Lists.newArrayList();  
  12.         try {  
  13.             for (int i = 0; i < 10; i++) {  
  14.                 CuratorFramework client = ClientFactory.newClient();  
  15.                 LeaderSelectorClient example = new LeaderSelectorClient(client, "Client #" + i);  
  16.                 clients.add(client);  
  17.                 examples.add(example);  
  18.   
  19.                 client.start();  
  20.                 example.start();  
  21.             }  
  22.   
  23.             System.out.println("----------先观察一会选举的结果-----------");  
  24.             Thread.sleep(10000);  
  25.   
  26.             System.out.println("----------关闭前5个客户端,再观察选举的结果-----------");  
  27.             for (int i = 0; i < 5; i++) {  
  28.                 clients.get(i).close();  
  29.             }  
  30.   
  31.             // 这里有个小技巧,让main程序一直监听控制台输入,异步的代码就可以一直在执行。不同于while(ture)的是,按回车或esc可退出  
  32.             new BufferedReader(new InputStreamReader(System.in)).readLine();  
  33.   
  34.         } catch (Exception e) {  
  35.             e.printStackTrace();  
  36.         } finally {  
  37.             for (LeaderSelectorClient exampleClient : examples) {  
  38.                 CloseableUtils.closeQuietly(exampleClient);  
  39.             }  
  40.             for (CuratorFramework client : clients) {  
  41.                 CloseableUtils.closeQuietly(client);  
  42.             }  
  43.         }  
  44.     }  
  45. }  

 

2、locks

curator lock相关的实现在recipes.locks包里。顶级接口都是InterProcessLock。我们直接看最有代表性的InterProcessReadWriteLock 进程内部读写锁(可重入读写锁)。什么叫可重入,什么叫读写锁。不清楚的先查好资料吧。总之读写锁一定是成对出现的。    简易传送门

 

我们先定义两个任务,可并行的执行的,和互斥执行的。

Java代码   收藏代码
  1. /** 
  2.  * 并行任务 
  3.  *  
  4.  * @author shencl 
  5.  */  
  6. public class ParallelJob implements Runnable {  
  7.   
  8.     private final String name;  
  9.   
  10.     private final InterProcessLock lock;  
  11.   
  12.     // 锁等待时间  
  13.     private final int wait_time = 5;  
  14.   
  15.     ParallelJob(String name, InterProcessLock lock) {  
  16.         this.name = name;  
  17.         this.lock = lock;  
  18.     }  
  19.   
  20.     @Override  
  21.     public void run() {  
  22.         try {  
  23.             doWork();  
  24.         } catch (Exception e) {  
  25.             // ingore;  
  26.         }  
  27.     }  
  28.   
  29.     public void doWork() throws Exception {  
  30.         try {  
  31.             if (!lock.acquire(wait_time, TimeUnit.SECONDS)) {  
  32.                 System.err.println(name + "等待" + wait_time + "秒,仍未能获取到lock,准备放弃。");  
  33.             }  
  34.             // 模拟job执行时间0-4000毫秒  
  35.             int exeTime = new Random().nextInt(4000);  
  36.             System.out.println(name + "开始执行,预计执行时间= " + exeTime + "毫秒----------");  
  37.             Thread.sleep(exeTime);  
  38.         } catch (Exception e) {  
  39.             e.printStackTrace();  
  40.         } finally {  
  41.             lock.release();  
  42.         }  
  43.     }  
  44. }  

 

Java代码   收藏代码
  1. /** 
  2.  * 互斥任务 
  3.  *  
  4.  * @author shencl 
  5.  */  
  6. public class MutexJob implements Runnable {  
  7.   
  8.     private final String name;  
  9.   
  10.     private final InterProcessLock lock;  
  11.   
  12.     // 锁等待时间  
  13.     private final int wait_time = 10;  
  14.   
  15.     MutexJob(String name, InterProcessLock lock) {  
  16.         this.name = name;  
  17.         this.lock = lock;  
  18.     }  
  19.   
  20.     @Override  
  21.     public void run() {  
  22.         try {  
  23.             doWork();  
  24.         } catch (Exception e) {  
  25.             // ingore;  
  26.         }  
  27.     }  
  28.   
  29.     public void doWork() throws Exception {  
  30.         try {  
  31.             if (!lock.acquire(wait_time, TimeUnit.SECONDS)) {  
  32.                 System.err.println(name + "等待" + wait_time + "秒,仍未能获取到lock,准备放弃。");  
  33.             }  
  34.             // 模拟job执行时间0-2000毫秒  
  35.             int exeTime = new Random().nextInt(2000);  
  36.             System.out.println(name + "开始执行,预计执行时间= " + exeTime + "毫秒----------");  
  37.             Thread.sleep(exeTime);  
  38.         } catch (Exception e) {  
  39.             e.printStackTrace();  
  40.         } finally {  
  41.             lock.release();  
  42.         }  
  43.     }  
  44. }  

 

锁测试代码

 

Java代码   收藏代码
  1. /** 
  2.  * 分布式锁实例 
  3.  *  
  4.  * @author shencl 
  5.  */  
  6. public class DistributedLockExample {  
  7.     private static CuratorFramework client = ClientFactory.newClient();  
  8.     private static final String PATH = "/locks";  
  9.   
  10.     // 进程内部(可重入)读写锁  
  11.     private static final InterProcessReadWriteLock lock;  
  12.     // 读锁  
  13.     private static final InterProcessLock readLock;  
  14.     // 写锁  
  15.     private static final InterProcessLock writeLock;  
  16.   
  17.     static {  
  18.         client.start();  
  19.         lock = new InterProcessReadWriteLock(client, PATH);  
  20.         readLock = lock.readLock();  
  21.         writeLock = lock.writeLock();  
  22.     }  
  23.   
  24.     public static void main(String[] args) {  
  25.         try {  
  26.             List jobs = Lists.newArrayList();  
  27.             for (int i = 0; i < 10; i++) {  
  28.                 Thread t = new Thread(new ParallelJob("Parallel任务" + i, readLock));  
  29.                 jobs.add(t);  
  30.             }  
  31.   
  32.             for (int i = 0; i < 10; i++) {  
  33.                 Thread t = new Thread(new MutexJob("Mutex任务" + i, writeLock));  
  34.                 jobs.add(t);  
  35.             }  
  36.   
  37.             for (Thread t : jobs) {  
  38.                 t.start();  
  39.             }  
  40.         } catch (Exception e) {  
  41.             e.printStackTrace();  
  42.         } finally {  
  43.             CloseableUtils.closeQuietly(client);  
  44.         }  
  45.     }  
  46. }  

 

看到没,用法和java concurrent包里的ReentrantReadWriteLock 是一模一样的。

事实上,整个recipes包的目录结构、实现原理同java concurrent包的设置是很一致的。比如有queue,Semaphore,Barrier等类,。他整个就是模仿jdk的实现,只不过是基于分布式的!

 

后边的几项,Barriers(关卡),Atomic(原子量),Caches,Queues和java concurrent包里的类的用法是一样的,就不继续贴了,有些附件里有。

要说明的是:有的功能性能不是特别理想,网上也没见有大的项目的使用案例。比如基于CAS机制的atomic,在某些情况重试的效率还不如硬同步,要是zookeeper节点再一多,各个节点之间通过event触发的数据同步极其频繁。那性能可以想象。

 

三、测试方法

 curator提供了很好的测试工具,你甚至是可以在完全没有搭建zookeeper server端的情况下,完成测试。

有2个重要的类

TestingServer 模拟单点, TestingCluster模拟集群。

需要使用的话,得依赖

Xml代码   收藏代码
  1. <dependency>  
  2.     <groupId>org.apache.curatorgroupId>  
  3.     <artifactId>curator-testartifactId>  
  4.     <version>2.5.0version>  
  5. dependency>  

 

 

 

全文完。

 

本文参考:

http://curator.apache.org/

http://www.cnblogs.com/hzhuxin/archive/2012/11/01/2749341.html

http://www.chengxuyuans.com/Java+/72042.html

http://macrochen.iteye.com/blog/1366136

你可能感兴趣的:(ZooKeeper)