Curator是一套开源的ZooKeeper客户端框架,本文会学习并记录一些API。
Curator使用
创建Curator客户端的Maven依赖,不要使用高版本的
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>2.10.0</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>2.10.0</version>
</dependency>
创建会话
//使用CuratorFrameworkFactory这个工厂类的静态方法来创建客户端
static CuratorFramework newClient(
String connectString, //ZooKeeper服务器列表,可以同时连接多台机器
//使用','隔开
int sessionTimeoutMs, //会话超时时间, 单位毫秒
int connectTimeoutMs, //连接创建超时时间,单位毫秒
RetryPolicy retryPolicy //重试策略的接口
);
// RetryPolicy接口的一个实现类
ExponentialBackoffRetry(
int baseSleepTimeMs, //初始Sleep时间
int maxRetries, //最大重试次数
int maxSleepMs //最大sleep时间
);
创建节点
CuratorFramework client = CuratorFrameworkFactory.newClient(...);
client.start();
String path = "/zk-book"
//创建一个节点,初始内容为空
//如果没有设置节点属性,Curator默认创建的是持久节点
client.create().forPath(path)
//创建一个节点,附带初始内容
client.crete().forPath(path, "hello".getBytes());
//创建一个临时节点,初始内容为空
client.create().withNode(CreateMode.EPHEMERAL).forPath(path);
//创建一个临时节点,并自动递归创建父节点
client.create().createParentsIfNeeded()
.withMode(CreateMode.EPHEMERAL)
.forPath(path);
在使用ZooKeeper的时候,会碰到NoNodeException异常,其中一个可能的原因就是视图对一个不存在的父节点创建字节点,调用createParentsIfNeed()可以自动递归创建所有需要的父节点。
ZooKeeper中规定所有非叶节点必须为持久节点,调用createParentsIfNeeded方法之后,只有path参数对应的数据节点是临时节点,其余父节点均为持久节点。
删除节点
//删除一个节点
client.delete().forPath();
//删除一个节点,并递归删除其所有子节点
client.delete().deletingChildrenIfNeeded().forPath(path);
//删除一个节点,强制指定版本进行删除
client.delete().withVersion(version).forPath(path);
//删除一个节点,保证可以被强制删除
client.delete().guaranteed().forPath(path);
guaranteed()这个API在客户端会话有效的阶段,会持续进行节点删除操作,直至删除成功。
在ZooKeeper客户端使用过程中,可能会碰到这样的问题:客户端执行一个删除节点操作,但由于一些网络原因,导致删除节点失败。Curator引入一个重试机制:guaranteed方法,当客户端碰到网络异常的时候,会记录下这次失败的操作,只要客户端会话有效,那么会保证节点被删除。
读取数据
//读取一个节点的数据内容, 返回的结果是byte[]
client.getData().forPath(path);
//读取一个节点的数据内容,同时获取到该节点的stat
client.getData().storingStatIn(stat).forPath(path);
更新数据
//更新一个节点的数据内容,返回一个stat对象
client.setData().forPath(path);
//更新一个节点的数据内容,强制指定版本进行更新
//withVersion的API就是用来实现CASd的,version通常是从一个旧的stat对象中获取
client.setData().withVersion(version).forPath(path);
事件监听
- NodeCache
用于监听指定ZooKeeper数据节点本身的变化。
//Cache实现对服务端事件的监听
public NodeCache(
CuratorFramework client, //Curator客户端实例
String path, //数据节点的节点路径
boolean dataIsCompressed //是否进行数据压缩
);
//回调voidnodeChanged方法一旦注册的事件发生
puiblic interface NodeCacheListener{
public void voidnodeChanged() throws Exception;
}
- PahtChildrenCache
用于监听指定ZooKeeper数据节点的子节点变化情况,节点本身的数据变化并不会触发异步调用。
子节点的变化包括新增字节点(CHILD_ADDED)、字节点数据变更(CHILD_UPDATED)和字节点删除(CHILD_REMOVED)三类。
public PathChildrenCache(
client, //Curator客户端实例
path, //数据节点的节点路径
dataIsCompressed, //是否进行数据压缩
cacheData, //true,用于缓存节点的内容
executorService //专门的线程池处理事件通知
);
public interface PathChildrenCacheListenner{
public void childEvent(CuratorFramework client, PathChidrenCacheEvent event) throws Execption;
}
Master选举
适用场景:对于一个复杂的认为,仅需要从集群中选举一台进行处理即可。
思路:选择一个根节点,例如/my_root,多台机器同时向该节点创建一个子节点/my_root/me_lock,利用ZooKeeper的特性,最终只用一台及其能够创建成功,成功的那台机器就作为Master。
public Class LeaderSelector(
CuratorFramework client,
String path,
LeaderSelectorListener listener //监听器
);
//选举成功之后的回调函数
public interface LeaderSelectorListener extends ConnectonStateListener{
public void takLeadership(CuratorFramework client) thorws Exception;
}
//自定义实现适配器
public abstract LeaderSelectorListenerAdapter implements LeaderSelectorListener{
@Override
public void statChange(CuratorFramework client, ConnectionState newState){
//TODO
}
}
分布式锁
在分布式环境,为了保证数据的一致性,经常在某个程序的某个运行点(例如,减库存操作或生成流水线号)需要进行同步控制。
public InterProcessMutex(
CrutorFramework client,
String lock_path
);
//两个核心API,锁的获取与释放
public void acquire() throws Exception;
public void release() throws Exception;
public static void testDistributeLockSample(){
String path = "/curator_recipes_lock_path";
CuratorFramework client = CuratorFrameworkFactory.builder()
.connectString("127.0.0.1")
.retryPolicy(new ExponentialBackoffRetry(1000, 3))
.build();
client.start();
InterProcessMutex lock = new InterProcessMutex(client, path);
CountDownLatch countDownLatch = new CountDownLatch(1);
for(int i = 0; i < 30; ++i){
new Thread(new Runnable() {
@Override
public void run() {
try{
countDownLatch.await(); //主线程等待所有子线程创建完成
}catch (Exception e) { e.printStackTrace(); }
try{
lock.acquire(); //加锁
}catch (Exception e){ e.printStackTrace(); }
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss|SSS");
String orderNo = sdf.format(new Date());
System.out.println("生成的订单号: " + orderNo);
try{
lock.release(); //释放锁
}catch (Exception e) { e.printStackTrace();}
}
}).start();
}
countDownLatch.countDown();
}
分布式计数器
分布式计数器的一个典型场景是统计系统的在线人数。基于ZooKeeper的分布式计数器的实现思路:指定一个ZooKeeper数据节点作为计数器,多个应用实例在分布式锁的控制下,通过更新该数据节点的内容来实现计数功能。
- 分布式AtomicInteger
public DistributedAtomicInteger(
CuratoFramework client,
String path,
RetryPolicy policy
);
try {
DistributedAtomicInteger atomicInteger = new DistributedAtomicInteger(
client, path, new ExponentialBackoffRetry(3, 1000));
AtomicValue<Integer> rc = atomicInteger.add(8);
System.out.println("Result: " + rc.succeeded() + ", value: " + rc.postValue());
}catch (Exception e){
e.printStackTrace();
}
- 分布式Barrier
public DistributedDoubleBarrier(
CuratorFramework client,
String path,
int nums
);
//程序将阻塞于以下两个方法
//直到达到nums的数量
public void enter();
public void leave();
public static void testDistributedDoubleBarrierSample(){
String barrier_path = "/curator_recipes_barrier_path";
try{
for(int i = 0; i < 5; ++i){
new Thread(new Runnable() {
@Override
public void run() {
try{
CuratorFramework client = CuratorFrameworkFactory.builder()
.connectString("127.0.0.1:2181")
.retryPolicy(new ExponentialBackoffRetry(1000, 3))
.build();
client.start();
DistributedDoubleBarrier barrier = new DistributedDoubleBarrier(client, barrier_path, 5);
Thread.sleep(Math.round(Math.random() * 3000));
System.out.println(Thread.currentThread().getName() + "进入Barrier");
barrier.enter();
System.out.println("启动:...");
Thread.sleep(Math.round(Math.random() * 3000));
barrier.leave();
System.out.println("退出");
}catch (Exception e){
e.printStackTrace();
}
}
}).start();
}
}catch (Exception e){
e.printStackTrace();
}
}
输出结果
Thread-3进入Barrier
Thread-4进入Barrier
Thread-2进入Barrier
Thread-0进入Barrier
Thread-1进入Barrier
启动:...
启动:...
启动:...
启动:...
启动:...
退出
退出
退出
退出
退出
上面展示的DistributedDoubleBarrier与JDK中的CyclicBarrier有着非常类似的实现,它们都指定了进入Barrier的成员数阈值,例如上面程序的”5”。每个线程都会阻塞于enter和leave方法,一旦准备进入Barrier的成员达到5个之后,所有的成员会宏观上会变为非阻塞。
工具类
- ZKPaths:构建ZNode路径、递归创建和删除节点;
- EnsurePath:提供了一种能够确保数据节点存在的机制,节点若不存在,创建数据节点,反之,什么也不做。
public static void testUtilsSample(){
try{
String path = "/curator_zkpath_sample";
CuratorFramework client = CuratorFrameworkFactory.newClient(
"127.0.0.1:2181",
5000,
3000,
new ExponentialBackoffRetry(1000, 3)
);
client.start();
ZooKeeper zooKeeper = client.getZookeeperClient().getZooKeeper();
System.out.println(ZKPaths.fixForNamespace(path, "/sub")); // 输出/curator_zkpath_sample/sub
System.out.println(ZKPaths.makePath(path, "/sub")); //输出/curator_zkpath_sample/sub
System.out.println(ZKPaths.getNodeFromPath("/curator_zkpath_sample/sub2")); //输出sub2
ZKPaths.PathAndNode pn = ZKPaths.getPathAndNode(path + "/sub");
System.out.println(pn.getPath()); // 输出/curator_zkpath_sample
System.out.println(pn.getNode()); // 输出sub
String dir1 = path + "/child1";
String dir2 = path + "/child2";
ZKPaths.mkdirs(zooKeeper, dir1);
ZKPaths.mkdirs(zooKeeper, dir2);
System.out.println(ZKPaths.getSortedChildren(zooKeeper, path)); // 输出[child1, child2]
client.usingNamespace("zk-book");
// EnsurePath ..被弃用
}catch (Exception e){
e.printStackTrace();
}
}
输出结果
生成的订单号: 19:43:04|781
生成的订单号: 19:43:05|312
生成的订单号: 19:43:05|689
生成的订单号: 19:43:05|723
生成的订单号: 19:43:05|976
生成的订单号: 19:43:06|193
生成的订单号: 19:43:06|470
生成的订单号: 19:43:06|637
生成的订单号: 19:43:06|776
生成的订单号: 19:43:06|889
生成的订单号: 19:43:07|042
生成的订单号: 19:43:07|180
生成的订单号: 19:43:07|359
生成的订单号: 19:43:07|434
生成的订单号: 19:43:07|509
生成的订单号: 19:43:07|582
生成的订单号: 19:43:07|653
生成的订单号: 19:43:07|697
生成的订单号: 19:43:07|707
生成的订单号: 19:43:07|756
生成的订单号: 19:43:07|836
生成的订单号: 19:43:07|926
生成的订单号: 19:43:08|011
生成的订单号: 19:43:08|087
生成的订单号: 19:43:08|170
生成的订单号: 19:43:08|191
生成的订单号: 19:43:08|213
生成的订单号: 19:43:08|229
生成的订单号: 19:43:08|237
生成的订单号: 19:43:08|243