Curator不仅为开发者提供了更为便利的API接口,而且还提供了一些典型场景的使用参考。这些使用参考都在recipes包中,读者需要单独依赖以下Maven依赖来获取:
<!-- Curator工具包 -->
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>2.4.2</version>
</dependency>
事件监听
ZooKeeper原生支持通过注册Watcher来进行事件监听,但是其使用并不是特别方便,需要开发人员自己反复注册Watcher, 比较繁琐。Curator 引入了Cache 来实现对ZooKeeper服务端事件的监听。Cache是Curator中对事件监听的包装,其对事件的监听其实可以近似看作是一个本地缓存视图和远程ZooKeeper视图的对比过程。同时Curator能够自动为开发人员处理反复注册监听,从而大大简化了原生API开发的繁琐过程。
Cache分为两类监听类型:节点监听和子节点监听
。
NodeCache:
NodeCache用于监听指定Zookeeper数据结点本身的变化,其构造方法有如下两个:
public NodeCache(CuratorFramework client, String path)
public NodeCache(CuratorFramework client, String path, boolean dataIsCompressed)
同时,NodeCache定义了事件处理的回调接口NodeCacheListener:
public interface NodeCacheListener {
void nodeChanged() throws Exception;
}
当数据节点的内容发生变化的时候,就会回调该方法。
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();
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);
}
}
在上面的示例程序中,首先构造了一个NodeCache实例,然后调用start方法,该方法有个boolean类型的参数,默认是false,如果设置为true,那么NodeCache在第一次启动的时候就会立刻从ZooKeeper上读取对应节点的数据内容,并保存在Cache中。
NodeCache不仅可以用于监听数据节点的内容变更,也能监听指定节点是否存在。如果原本节点不存在,那么Cache就会在节点被创建后触发NodeCacheListener。但是,如果该数据节点被删除,那么Curator 就无法触发NodeCacheListener了。
PathChildrenCache:
PathChildrenCache用于监听指定Zookeeper数据结点的子节点变化情况。构造方法如下:
public PathChildrenCache(CuratorFramework client, String path, boolean cacheData);
public PathChildrenCache(CuratorFramework client, String path, boolean cacheData, ThreadFactory threadFactory);
public PathChildrenCache(CuratorFramework client, String path, boolean cacheData, boolean datalsCompressed, ThreadFactroy threadFactory);
public PathChildrenCache(CuratorFramework client, String path, boolean cacheData, boolean datalsCompressed, final ExecutorService executorService);
public PathChildrenCache(CuratorFramework client, String path, boolean cacheData, boolean datalsCompressed, final CloseableExecutorService executorService);
PathChildrenCache定义了事件处理的回调接口PathChildrenCacheListener:
public interface PathChildrenCacheListener {
void childEvent(CuratorFramework var1, PathChildrenCacheEvent var2) throws Exception;
}
当指定节点的子节点发生变化时,就会回调该方法。PathChildrenCacheEvent类中定义了所有的事件类型,主要包括
- 新增子节点(CHILD_ ADDED).
- 子节点数据变更(CHILD_ UPDATED)
- 子节点删除(CHILD_ REMOVED)
public class PathChildrenCache_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();
public static void main(String[] args) throws Exception {
client.start();
PathChildrenCache cache = new PathChildrenCache(client, path,true);
cache.start(PathChildrenCache.StartMode.POST_INITIALIZED_EVENT);
cache.getListenable().addListener(new PathChildrenCacheListener() {
@Override
public void childEvent(CuratorFramework curatorFramework, PathChildrenCacheEvent pathChildrenCacheEvent) throws Exception {
switch (pathChildrenCacheEvent.getType()) {
case CHILD_ADDED:
System.out.println("CHILD_ADDED," + pathChildrenCacheEvent.getData().getPath());
break;
case CHILD_UPDATED:
System.out.println("CHILD_UPDATED," + pathChildrenCacheEvent.getData().getPath());
break;
case CHILD_REMOVED:
System.out.println("CHILD_REMOVED," + pathChildrenCacheEvent.getData().getPath());
break;
default:
break;
}
}
});
client.create().withMode(CreateMode.PERSISTENT).forPath(path);
Thread.sleep(1000);
client.create().withMode(CreateMode.PERSISTENT).forPath(path + "/c1");
Thread.sleep(1000);
client.delete().forPath(path + "/c1");
Thread.sleep(1000);
client.delete().forPath(path);
Thread.sleep(Integer.MAX_VALUE);
}
}
CHILD_ADDED,/zk-book/c1
CHILD_REMOVED,/zk-book/c1
在上面这个示例程序中,对/zk-book 节点进行了子节点变更事件的监听,一旦该节点新增/删除子节点,或者子节点数据发生变更,就会回调PathChildren CacheListener,并根据对应的事件类型进行相关的处理。同时,我们也看到,对于节点/zk-book本身的变更,并没有通知到客户端。
另外,和其他ZooKeeper客户端产品一样,Curator 也无法对二级子节点进行事件监听。也就是说,如果使用PathChildrenCache 对/zk-book 进行监听,那么当/zk-book/c1/c2节点被创建或删除的时候,是无法触发子节点变更事件的。
Master选举
在Zookeeper中可以比较方便地实现Master选举的功能,其大体思路非常简单:
选择一个根节点,例如/master_select, 多台机器同时向该节点创建一个子节点/master_ selectlock,利用ZooKeeper的特性,最终只有一台机器能够创建成功,成功的那台机器就作为Master。
Curator也是基于这个思路,但是它将节点创建、事件监听和自动选举过程进行了封装,开发人员只需要调用简单的API即可实现Master选举。
//使用Curator实现分布式Master选举
public class Recipes_MasterSelect {
static String master_path = "/curator_recipes_master_path";
static CuratorFramework client = CuratorFrameworkFactory.builder()
.connectString("127.0.0.1:2181")
.retryPolicy(new ExponentialBackoffRetry(1000, 3))
.build();
public static void main(String[] args) throws InterruptedException {
client.start();
LeaderSelector selector = new LeaderSelector(client,
master_path,
new LeaderSelectorListenerAdapter() {
@Override
public void takeLeadership(CuratorFramework curatorFramework) throws Exception {
System.out.println("称为Master角色");
Thread.sleep(3000);
System.out.println("完成Master操作,释放Master权利");
}
});
selector.autoRequeue();
selector.start();
Thread.sleep(Integer.MAX_VALUE);
}
}
在上面这个示例程序中,可以看到主要是创建了一个LeaderSelector实例,该实例,负责封装所有和Master选举相关的逻辑,包括所有和ZooKeeper服务器的交互过程。其.中master_path代表了一个Master选举的根节点,表明本次Master选举都是在该节点下进行的。
在创建LeaderSelector 实例的时候,还会传入一个监听器: Leade rSelectorListenerAdapter。这需要开发人员自行实现,Curator 会在成功获取Master权利的时候回调该监听器,其定义如下。
public interface LeaderSelectorListener extends ConnectionStateListener {
void takeLeadership(CuratorFramework var1) throws Exception;
}
//实现类
public abstract class LeaderSelectorListenerAdapter implements LeaderSelectorListener {
public LeaderSelectorListenerAdapter() {
}
public void stateChanged(CuratorFramework client, ConnectionState newState) {
if (newState == ConnectionState.SUSPENDED || newState == ConnectionState.LOST) {
throw new CancelLeadershipException();
}
}
}
LeaderSelectorListener接口中最重要的方法就是takeLeadership方法,Curator会在竞争到Master后自动调用该方法,开发者可以在这个方法中实现自己的业务逻辑。需要注意的一点是,一旦执行完takeLeadership方法,Curator就会立即释放Master权利,然后重新开始新一轮的Master选举。
在示例程序中,通过sleep来简单地模拟业务逻辑的执行,同时运行两个应用程序后,仔细观察控制台输出,可以发现,当一个应用程序完成Master逻辑后,另一个应用程序的takeLeadership方法才会被调用。这也就说明,当一个应用实例成为Master后,其他应用实例会进入等待,直到当前Master挂了或退出后才会开始选举新的Master。
同时,可以在Zookeeper上的/curator_recipes_master_path
节点下,会不断有子节点被创建出来:
分布式锁
在分布式环境中,为了保证数据的一致性,经常在程序的某个运行点(例如,减库存操作或流水号生成等)需要进行同步控制。以一个“流水号生成”的场景为例,普通的后台应用通常都是使用时间戳方式来生成流水号,但是在用户量非常大的情况下,可能会出现并发问题。
示例:典型的并发问题
public class Recipes_NoLock {
public static void main(String[] args) {
final CountDownLatch down = new CountDownLatch(1);
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
down.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss|SSS");
String orderNo = sdf.format(new Date());
System.out.println("生成的订单号是: " + orderNo);
}).start();
}
down.countDown();
}
}
生成的订单号是: 17:24:09|104
生成的订单号是: 17:24:09|104
生成的订单号是: 17:24:09|104
生成的订单号是: 17:24:09|104
生成的订单号是: 17:24:09|104
生成的订单号是: 17:24:09|104
生成的订单号是: 17:24:09|104
生成的订单号是: 17:24:09|104
生成的订单号是: 17:24:09|104
生成的订单号是: 17:24:09|104
使用Curator实现分布式锁功能:
public class Recipes_Lock {
static String lock_path = "/curator_recipes_lock_path";
static CuratorFramework client = CuratorFrameworkFactory.builder()
.connectString("127.0.0.1:2181")
.retryPolicy(new ExponentialBackoffRetry(1000, 3))
.build();
public static void main(String[] args) {
client.start();
final InterProcessMutex lock = new InterProcessMutex(client, lock_path);
final CountDownLatch downLatch = new CountDownLatch(1);
for (int i = 0; i < 30; i++) {
new Thread(() -> {
try {
downLatch.await();
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();
downLatch.countDown();
}
}
}
生成的订单号是:20:06:27|915
生成的订单号是:20:06:27|948
生成的订单号是:20:06:27|971
生成的订单号是:20:06:27|990
生成的订单号是:20:06:28|001
生成的订单号是:20:06:28|008
生成的订单号是:20:06:28|018
生成的订单号是:20:06:28|030
.上面这个示例程序就借助Curator来实现了-一个简单的分布式锁。其核心接口如下:
public interface InterProcessLock
- public void acquire() throws Exception;
- public void release() throws Exception;
这两个接口分别用来实现分布式锁的获取与释放过程。
分布式计数器
基于Zookeeper的分布式计数器的实现思路也非常简单:
指定一个Zookeeper数据结点作为计数器,多个应用示例在分布式锁的控制下,通过更新该数据结点的内容来实现计数功能。
Curator同样将这一系列逻辑封装在了DistributedAtomicInteger类中,从其类名我们可以看出这是一个可以在分布式环境中使用的原子整型,其具体使用方式如下:
//使用Curator实现分布式计数器
public class Recipes_DistAtomicInt {
static String distatomicint_path = "/curator_recipes_distatomicint_path";
static CuratorFramework client = CuratorFrameworkFactory.builder()
.connectString("127.0.0.1:2181")
.retryPolicy(new ExponentialBackoffRetry(1000, 3))
.build();
public static void main(String[] args) throws Exception {
client.start();
DistributedAtomicInteger atomicInteger = new DistributedAtomicInteger(client,
distatomicint_path, new RetryNTimes(3, 1000));
AtomicValue<Integer> rc = atomicInteger.add(8);
System.out.println("Result: " + rc.succeeded());
}
}
分布式Barrier
Barrier是- -种用来控制多线程之间同步的经典方式,在JDK中也自带了CyclicBarrier实现。
使用CyclicBarrier模拟一个赛跑比赛:
public class Recipes_CyclicBarrier {
public static CyclicBarrier barrier = new CyclicBarrier(3);
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(3);
executor.submit(new Thread(new Runner("1号选手")));
executor.submit(new Thread(new Runner("2号选手")));
executor.submit(new Thread(new Runner("3号选手")));
executor.shutdown();
}
}
class Runner implements Runnable {
private String name;
public Runner(String name) {
this.name = name;
}
@Override
public void run() {
System.out.println(name + "准备好了.");
try {
Recipes_CyclicBarrier.barrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
System.out.println(name + "起跑!");
}
}
1号选手准备好了.
3号选手准备好了.
2号选手准备好了.
3号选手起跑!
2号选手起跑!
1号选手起跑!
上面就是一个使用JDK自带的CyclicBarrier实现的赛跑比赛程序,可以看到多线程在并发情况下,都会准确地等待所有线程都处于就绪状态后才开始同时执行其他业务逻辑。如果是在同一个JVM中的话,使用CyclicBarrier完全可以解决诸如此类的多线程同步问题。但是,如果是在分布式环境中又该如何解决呢?Curator中提供的DistributedBarrier就是用来实现分布式Barrier的。
//使用Curator实现分布式Barrier
public class Recipes_Barrier {
static String barrier_path = "/curator_recipes_barrier_path";
static DistributedBarrier barrier;
public static void main(String[] args) throws Exception {
for (int i = 0; i < 5; i++) {
new Thread(() -> {
try {
CuratorFramework client = CuratorFrameworkFactory
.builder()
.connectString("127.0.0.1:2181")
.retryPolicy(new ExponentialBackoffRetry(1000, 3))
.build();
client.start();
barrier = new DistributedBarrier(client, barrier_path);
System.out.println(Thread.currentThread().getName() + "号barrier设置");
barrier.setBarrier();
barrier.waitOnBarrier();
System.err.println("启动...");
} catch (Exception e) {
e.printStackTrace();
}
}).start();
Thread.sleep(10000);
barrier.removeBarrier();
}
}
}
在上面这个实例程序中,我们模拟了5个线程,通过调用DistributedBarrier .setBarrier()方法来完成Barrier 的设置,并通过调用Dist ributedBarrier.waitOnBarrier()方法来等待Barrier 的释放。然后在主线程中,通过调用DistributedBarrier.removeBarrier()方法来释放Barrier, 同时触发所有等待该Barrier的5个线程同时进行各自的业务逻辑。
和上面这种由主线程来触发Barrier释放不同的是,Curator还提供了另一种线程自发触发Barrier释放的模式,使用方式如下:
public class Recipis_Barrier2 {
static String barrier_path = "/curator_recipes_barrier_path";
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(() -> {
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();
}
}
}
Thread-3号进入barrier
Thread-0号进入barrier
Thread-2号进入barrier
Thread-1号进入barrier
Thread-4号进入barrier
启动...
启动...
启动...
启动...
启动...
退出...
退出...
退出...
退出...
退出...
上面这个示例程序就是一个和JDK自带的cyclicBarrier非常类似的实现了,它们都指定了进入Barrier的成员数阈值,例如上面示例程序中的“5”。 每个Barrier 的参与者都会在调用DistributedDoubleBarrier .enter()方法之后进行等待,此时处于准备进入状态。一旦准备进入Barrier的成员数达到5个后,所有的成员会被同时触发进入。
之后调用DistributedDoubleBarrier .leave()方法则会再次等待,此时处于准备退出状态。一旦准备退出Barrier的成员数达到5个后,所有的成员同样会被同时触发退出。因此,使用Curator的DistributedDoubleBarrier能够很好地实现一个分布式Barrier,并控制其同时进入和退出。