Curator

Curator是Netflix公司开源的一套ZooKeeper客户端框架,作者是Jordan Zimmerman。
和ZkClient一样,Curator解决了很多ZooKeeper客户端非常底层的细节开发工作,包括连接重连、反复注册Watcher和NodeExistsException异常等,目前已经成为了Apache的顶级项目。

除了封闭一些开发人员不需要特别关注的底层细节,Curator还在ZooKeeper原生API的基础上进行了包装,提供了一套易用性和可读性更强的Fluent风格的客户端API框架。

除此之外,Curator中还提供了ZooKeeper各种应用场景(Recipe,如共享锁服务、Master选举机制和分布式计数器等)的抽象封装。

官网文档:
​​​http://curator.apache.org/getting-started.html​

maven:

<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.9</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>2.4.2</version>
</dependency>

创建会话

package zookeeper;

import java.io.IOException;

import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.retry.ExponentialBackoffRetry;


public class Create_Session_Sample {
public static void main(String[] args)throws IOException,InterruptedException{
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000,3);
CuratorFramework client = CuratorFrameworkFactory.newClient("127.0.0.1:2181", 5000,3000,retryPolicy);
client.start();
Thread.sleep(Integer.MAX_VALUE);
}
}

该例中,首先创建的一个名为ExponentialBackoffRetry的重试策略。
CuratorFrameworkFactory工厂在创建出一个客户端CuratorFramework实例之后,实质上并没有完成会话的创建,而是需要调用CuratorFramework的start()方法来完成会话的创建。

使用Fluent风格的API接口来创建会话
Curator提供的API接口在设计上最大的亮点在于其遵循了Fluent设计风格。

package zookeeper;

import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.retry.ExponentialBackoffRetry;

public class Create_Session_Sample_fluent {
public static void main(String[] args) throws Exception{
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000,3);
CuratorFramework client =
CuratorFrameworkFactory.builder()
.connectString("127.0.0.1:2181")
.sessionTimeoutMs(5000)
.retryPolicy(retryPolicy)
.build();
client.start();
Thread.sleep(Integer.MAX_VALUE);
}
}

使用Curator创建含隔离命名空间的会话
为了实现不同的ZooKeeper业务之间的隔离,往往会为每个业务分配一个独立的命名空间,即指定一个ZooKeeper根路径。

CuratorFrameworkFactory.builder()
.connectString("127.0.0.1:2181")
.sessionTimeoutMs(5000)
.retryPolicy(retryPolicy)
.namespace("base")
.build();

创建节点

package zookeeper;

import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.apache.zookeeper.CreateMode;


public class Create_Node_Sample {
static String path = "/zk-book/c1";
static CuratorFramework client = CuratorFrameworkFactory.builder()
.connectString("127.0.0.1:2181")
.sessionTimeoutMs(5000)
.retryPolicy(new ExponentialBackoffRetry(1000,3))
.build();
public static void main(String[] args) throws Exception{
client.start();
client.create()
.creatingParentsIfNeeded()
.withMode(CreateMode.EPHEMERAL)
.forPath(path,"init".getBytes());
}
}

删除节点

package zookeeper;

import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.data.Stat;


public class Del_Data_Sample {
static String path = "/zk-book/c1";
static CuratorFramework client = CuratorFrameworkFactory.builder()
.connectString("127.0.0.1:2181")
.sessionTimeoutMs(5000)
.retryPolicy(new ExponentialBackoffRetry(1000,3))
.build();
public static void main(String[] args) throws Exception{
client.start();
client.create()
.creatingParentsIfNeeded()
.withMode(CreateMode.EPHEMERAL)
.forPath(path,"init".getBytes());
Thread.sleep(2000);
Stat stat = new Stat();
client.getData().storingStatIn(stat).forPath(path);
client.delete().deletingChildrenIfNeeded()
.withVersion(stat.getVersion()).forPath(path);
Thread.sleep(2000);
}
}

这里guaranteed()方法,会在失败时重新尝试。

读取数据

package zookeeper;

import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.data.Stat;


public class Get_Data_Sample {
static String path = "/zk-book/c1";
static CuratorFramework client = CuratorFrameworkFactory.builder()
.connectString("127.0.0.1:2181")
.sessionTimeoutMs(5000)
.retryPolicy(new ExponentialBackoffRetry(1000,3))
.build();
public static void main(String[] args) throws Exception{
client.start();
client.create()
.creatingParentsIfNeeded()
.withMode(CreateMode.EPHEMERAL)
.forPath(path,"init".getBytes());
Thread.sleep(2000);
Stat stat = new Stat();
System.out.println(new String(client.getData().storingStatIn(stat).forPath(path)));
Thread.sleep(2000);
}
}

更新数据

package zookeeper;

import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.data.Stat;


public class Set_Data_Sample {
static String path = "/zk-book/c1";
static CuratorFramework client = CuratorFrameworkFactory.builder()
.connectString("127.0.0.1:2181")
.sessionTimeoutMs(5000)
.retryPolicy(new ExponentialBackoffRetry(1000,3))
.build();
public static void main(String[] args) throws Exception{
client.start();
client.create()
.creatingParentsIfNeeded()
.withMode(CreateMode.EPHEMERAL)
.forPath(path,"init".getBytes());
Thread.sleep(2000);
Stat stat = new Stat();
System.out.println(new String(client.getData().storingStatIn(stat).forPath(path)));
Thread.sleep(2000);

try{
client.setData().withVersion(stat.getVersion()).forPath(path);
}catch(Exception e){
System.out.println("Fail set node due to " + e.getMessage());
}
}
}

异步接口

Curator中引入了BackgroundCallback接口,用来处理异步接口调用之后服务端返回的结果信息,其接口定义如下:

public interface BackgroundCallback{
/**
* Called when the async background operation completes
* @param client the client
* @param event operation result details
*/ @throws Exception errors
public void processResult(CuratorFramework client, CuratorEvent event) throws Exception;
}

代码示例:

package zookeeper;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.api.BackgroundCallback;
import org.apache.curator.framework.api.CuratorEvent;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.apache.zookeeper.CreateMode;


public class Create_Node_Background_Sample {
static String path = "/zk-book";
static CuratorFramework client = CuratorFrameworkFactory.builder()
.connectString("127.0.0.1:2181")
.sessionTimeoutMs(5000)
.retryPolicy(new ExponentialBackoffRetry(1000,3))
.build();

static CountDownLatch semaphore = new CountDownLatch(2);
static ExecutorService tp = Executors.newFixedThreadPool(2);

public static void main(String[] args) throws Exception{
client.start();

System.out.println("Main thread:" + Thread.currentThread().getName());
client.create().creatingParentsIfNeeded().withMode(CreateMode.EPHEMERAL).inBackground(new BackgroundCallback(){

@Override
public void processResult(CuratorFramework client, CuratorEvent event) throws Exception {
System.out.println("event[code:" + event.getResultCode() + ",type:" + event.getType() + "]");
System.out.println("Thread of processResult:" + Thread.currentThread().getName());
semaphore.countDown();
}

}).forPath(path,"init".getBytes());

client.create().creatingParentsIfNeeded().withMode(CreateMode.EPHEMERAL).inBackground(new BackgroundCallback(){

@Override
public void processResult(CuratorFramework client, CuratorEvent event) throws Exception {
System.out.println("event[code:"+event.getResultCode() + ",type:" + event.getType() + "]");
System.out.println("Thread of processResult:" + Thread.currentThread().getName());
semaphore.countDown();
}

}).forPath(path,"init".getBytes());
semaphore.await();
tp.shutdown();
}
}

事件监听

Curator引入了Cache来实现对ZooKeeper央事件的监听 。同时Curator自动重复注册监听,从而大大简化了原生 API 开发的繁琐过程。
以下代码示例需要引入maven

<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>2.4.2</version>
</dependency>

NodeCache

NodeCache用于监听指定ZooKeeper数据节点本身的变化。

package zookeeper;


import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.cache.NodeCache;
import org.apache.curator.framework.recipes.cache.NodeCacheListener;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.apache.zookeeper.CreateMode;


public class NodeCache_Sample {
static String path = "/zk-book/nodecache";
static CuratorFramework client = CuratorFrameworkFactory.builder()
.connectString("127.0.0.1:2181")
.sessionTimeoutMs(5000)
.retryPolicy(new ExponentialBackoffRetry(1000,3))
.build();


public static void main(String[] args) throws Exception{
client.start();

System.out.println("Main thread:" + Thread.currentThread().getName());
client.create().creatingParentsIfNeeded().withMode(CreateMode.EPHEMERAL).forPath(path,"init".getBytes());

final NodeCache cache = new NodeCache(client,path,false);
cache.start(true);
cache.getListenable().addListener(new NodeCacheListener(){

@Override
public void nodeChanged() throws Exception {
System.out.println("Node data update,new data:"+new String(cache.getCurrentData().getData()));
}

});
client.setData().forPath(path,"u".getBytes());
Thread.sleep(1000);
client.delete().deletingChildrenIfNeeded().forPath(path);
Thread.sleep(Integer.MAX_VALUE);
cache.close();
}
}

PathChildrenCache

用于监听指定ZooKeeper数据节点的子节点变化情况。

Master选举

分布式系统中,经常遇到一个场景,一个复杂的任务,只需要从集群中选举出一台进行处理即可。其思路:
选择一个根节点,如/master_select,多台机器同时向该节点创建一个子节点/master_select/lock,利用ZooKeeper的特性,最终只有一台机器能够创建成功,成功的那台机器就作为Master。

package zookeeper;


import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.leader.LeaderSelector;
import org.apache.curator.framework.recipes.leader.LeaderSelectorListenerAdapter;
import org.apache.curator.retry.ExponentialBackoffRetry;


public class Recipes_MasterSelect {
static String master_path = "/curator_recipes_master_path";
static CuratorFramework client = CuratorFrameworkFactory.builder()
.connectString("127.0.0.1:2181")
.sessionTimeoutMs(5000)
.retryPolicy(new ExponentialBackoffRetry(1000,3))
.build();


public static void main(String[] args) throws Exception{
client.start();

LeaderSelector selector = new LeaderSelector(client,master_path,new LeaderSelectorListenerAdapter(){

@Override
public void takeLeadership(CuratorFramework client) throws Exception {
System.out.println("成为Master角色");
Thread.sleep(3000);
System.out.println("完成Master操作,释放Master权利");
}
});
selector.autoRequeue();
selector.start();
Thread.sleep(Integer.MAX_VALUE);
selector.close();
}
}

该程序主要是创建了一个LeaderSelector实例,该实例负责封装所有和Master选举相关的逻辑,包括所有和ZooKeeper服务器的交互过程。其中master_path代表了一个Master选举的根节点,表明本次Master选举都是在该节点下进行的。
在创建 LeaderSelector实例的时候,还会创建一个监听器LeaderSelectorListenerAdapter,这需要开发人员自行实现,Curator会在成功获取Master权利的时候回调该监听器。
Curator会在竞争到Master后自动调用该方法。一旦执行完takeLeadership方法,Curator就会立即释放Master权利,然后重新开始新一轮的Master选举。

分布式锁

在分布式环境中,为了保证数据的一致性,经常在程序的某个运行点需要进行同步控制。例如“流水号生成”场景,普通的后台应用通常使用时间戳生成流水号,但是在用户量非常大的情况下,可能出现并发问题。

代码示例:

package zookeeper;


import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.CountDownLatch;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.apache.curator.retry.ExponentialBackoffRetry;


public class Recipes_Lock {
static String lock_path = "/curator_recipes_lock_path";
static CuratorFramework client = CuratorFrameworkFactory.builder()
.connectString("127.0.0.1:2181")
.sessionTimeoutMs(5000)
.retryPolicy(new ExponentialBackoffRetry(1000,3))
.build();


public static void main(String[] args) throws Exception{
client.start();

final InterProcessMutex lock = new InterProcessMutex(client,lock_path);
final CountDownLatch down=new CountDownLatch(1);
for(int i=0;i<30;i++){
new Thread(new Runnable(){

@Override
public void run() {
try{
down.await();
lock.acquire();
}catch(Exception e){

}
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss|SSS");
String orderNo=sdf.format(new Date());
System.out.println("生成的订单号是:"+orderNo);
try{
lock.release();
}catch(Exception e1){

}

}

}).start();
}
down.countDown();
}
}

输出:

生成的订单号是:16:39:00|076
生成的订单号是:16:39:00|110
生成的订单号是:16:39:00|132
生成的订单号是:16:39:00|184
生成的订单号是:16:39:00|249
生成的订单号是:16:39:00|285
生成的订单号是:16:39:00|316
生成的订单号是:16:39:00|349
生成的订单号是:16:39:00|390
生成的订单号是:16:39:00|406
生成的订单号是:16:39:00|440
生成的订单号是:16:39:00|471
生成的订单号是:16:39:00|494
生成的订单号是:16:39:00|517
生成的订单号是:16:39:00|538
生成的订单号是:16:39:00|556
生成的订单号是:16:39:00|588
生成的订单号是:16:39:00|607
生成的订单号是:16:39:00|623
生成的订单号是:16:39:00|643
生成的订单号是:16:39:00|663
生成的订单号是:16:39:00|693
生成的订单号是:16:39:00|706
生成的订单号是:16:39:00|735
生成的订单号是:16:39:00|749
生成的订单号是:16:39:00|762
生成的订单号是:16:39:00|786
生成的订单号是:16:39:00|803
生成的订单号是:16:39:00|837
生成的订单号是:16:39:00|864

其它一些应用场景

  • 分布式计数器
  • 分布式Barrier

工具类

ZKPaths

提供了一些简单的API来构建ZNode路径、递归创建和删除节点等。

EnsurePath

提供了一种能够确保数据节点存在的机制。

TestingServer

Curator提供的一种简易的ZooKeeper服务端

TestingCluster

模拟ZooKeeper集群环境的Curator工具类。