Curator框架实现Zookeeper基本操作
Zookeeper是一个Apache开源的分布式的应用,为系统架构提供协调服务。从设计模式角度来审视:该组件是一个基于观察者模式设计的框架,负责存储和管理数据,接受观察者的注册,一旦数据的状态发生变化,Zookeeper就将负责通知已经在Zookeeper上注册的观察者做出相应的反应,从而实现集群中类似Master/Slave管理模式。ZooKeeper的目标就是封装好复杂易出错的关键服务,将简单易用的接口和性能高效、功能稳定的系统提供给用户。
curator是对zookeeper原生api的封装。原生的api特别难用。
curator提供了流式编程风格,做的非常不错,是目前使用率最高的一种zookeeper框架。
curator它主要包含三个依赖(curator的依赖都已经放到maven仓库,你直接使用maven来构建它。对于大多数人来说,我们可能最常需要引入的是curator-recipes):
- curator-recipes:包含了curator实现的各项功能,如读写锁、互斥锁、队列等,依赖于framework和Client:http://curator.apache.org/curator-recipes/index.html
- curator-framework:包含了高层级的流式API,构建在Client之上如对节点的增删改查等:http://curator.apache.org/curator-recipes/index.html
- curator-client:Zookeeper的基础客户端实现,如连接、重试、超时处理等:http://curator.apache.org/curator-client/index.html
官方文档说明
Curator 2.x.x-兼容两个zk 3.4.x 和zk 3.5.x,Curator 3.x.x-兼容zk 3.5。
因此为了不必要的麻烦,我们推荐使用2.x.x。
如果你想在 Zookeeper 3.4.x 中使用Curator ,可以选择 4.2.x 版本的 curator。
curator 4.2.x 版本和 zookeeper 3.4.x 版本会在兼容模式下运行。
为了使用这种模式,你必须在版本管理工具中移除对 Zookeeper 的依赖,并且重新添加对 Zookeeper 的依赖。
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>4.2.0</version>
<exclusions>
<exclusion>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.14</version>
</dependency>
一般我们使用引入以下依赖即可:
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>2.13.0</version>
</dependency>
Curator使用演示
创建客户端会话
//并没有创建会话连接
CuratorFramework client = CuratorFrameworkFactory.builder()
.connectString(addr)
.connectionTimeoutMs(timeout)
.sessionTimeoutMs(timeout)
.retryPolicy(new ExponentialBackoffRetry(1000, 3))//初始时间1s,重试连接3次
.namespace("test")
.build();
//创建会话连接,是个阻塞方法
client.start();
说一下retryPolicy,重连策略,建议用其中两种:
//重连3次,每次休息3秒
new RetryNTimes(3,3000);
//重连3次,每次休息大约是1秒
new ExponentialBackoffRetry(1000,3);
//初始化一个大概的等待时间1秒,然后开始重连,最多重连3次,每次最多休息2秒
new ExponentialBackoffRetry(1000,3,2000);
//计算通过这个初始化的大约时间,计算实际需要睡眠多久
long sleepMs = baseSleepTimeMs * Math.max(1, random.nextInt(1 << (retryCount + 1)));
namespace代表命名空间,注意的是,curator会自动创建。
创建节点
创建普通节点
client.create().forPath("/comm_msg_nd","ctx".getBytes());
client.create().forPath("/comm_no_msg_nd");
创建临时节点
client.create().withMode(CreateMode.EPHEMERAL).forPath("/ephe_nd","ff".getBytes());
//creatingParentsIfNeeded会自动创建父节点
client.create().creatingParentsIfNeeded().withMode(CreateMode.EPHEMERAL).forPath("/ephe_nd","ff".getBytes());
需要注意,只有叶节点可以做临时节点,所以叶节点的父节点必须是永久节点,也就是creatingParentsIfNeeded这个方法创建的父节点必须是永久节点。
节点的类型有:
- 持久节点
- 持久顺序节点
- 临时节点
- 临时顺序节点
public enum CreateMode {
PERSISTENT(0, false, false),
PERSISTENT_SEQUENTIAL(2, false, true),
EPHEMERAL(1, true, false),
EPHEMERAL_SEQUENTIAL(3, true, true);
}
删除节点
//删除单独一个节点
client.delete().forPath("/comm_msg_nd");
//删除多级节点
client.create().creatingParentsIfNeeded().forPath("/p1/p2/p3/multi_nd");
client.delete().deletingChildrenIfNeeded().forPath("/p1");
//删除指定版本的节点
client.delete().withVersion(0).forPath("/comm_msg_nd");
//保证删除,失败后继续执行
client.delete().guaranteed().forPath("/comm_msg_nd");
查询节点
Stat stat = new Stat();
byte[] ctx = cc.getData().storingStatIn(stat).forPath("/comm_msg_nd");
//获取节点内容为ctx
System.out.println(new String(ctx));
//获取该节点stat
//78,78,1573789366124,1573789366124,0,0,0,0,3,0,78
System.out.println(stat);
修改数据
Stat stat = new Stat();
stat = cc.setData().forPath("/comm_msg_nd","new ctx".getBytes());
System.out.println(stat);
以上的操作都是同步的。
异步调用
ExecutorService es = Executors.newFixedThreadPool(2);
//带线程池的异步接口
client.create().inBackground((client,event)->{
//pool-3-thread-1,CuratorEventImpl{type=CREATE, resultCode=0, path='/abc', name='/abc', children=null, context=null,stat=114,114,1573797468244,1573797468244,0,0,0,0,13,0,114, data=null, watchedEvent=null, aclList=null, opResults=null}
System.out.println(Thread.currentThread().getName()+","+event);
},es).forPath("/abc");
//不带线程池的异步接口
client.delete().inBackground((client,event)->{
//main-EventThread,CuratorEventImpl{type=DELETE, resultCode=0, path='/abc', name='null', children=null, context=null, stat=null, data=null, watchedEvent=null, aclList=null, opResults=null}
System.out.println(Thread.currentThread().getName()+","+event);
}).forPath("/abc");
Thread.sleep(Integer.MAX_VALUE);
1)inBackground() 该方法就是添加一个异步的回调方法,参数是BackgroundCallback接口,是一个函数式接口。
2)BackgroundCallback的接口参数为client(当前客户端实例)及event(服务端事件)
3)事件类型,CuratorEventType,包含如下信息 {type=DELETE, resultCode=0, path='/abc', name='null', children=null, context=null, stat=null, data=null, watchedEvent=null, aclList=null, opResults=null}。type对应的就是操作类型,比如delete对应的delete(),create对应create()等,resultCode是响应码,0代表成功
4)线程池es的作用,通过名称可以看到,默认情况下都是使用main-EventThread线程来串行执行,如果耗时较长会有影响,可以通过定制线程池来缓解这种情况。
事件监听
zookeeper原生支持通过注册Watcher来进行事件监听,但是其使用并不是特别方便,需要开发人员自己反复注册Watcher,比较繁琐。curator引入了cache来实现zookeeper服务端事件的监听。Cache是Curator中对时间监听的包装,其对事件的监听其实可以近似看作是一个本地缓存视图和远程zookeeper视图的对比过程。
cache分为两类监听类型,节点监听和子节点监听。
- NodeCache(监听和缓存根节点变化) 只监听单一个节点(变化 添加,修改,删除)。
- PathChildrenCache(监听和缓存子节点变化) 监听这个节点下的所有子节点(变化 添加,修改,删除)。
- TreeCache(监听和缓存根节点变化和子节点变化) NodeCache+ PathChildrenCache 监听当前节点及其下的所有子节点的变化。
NodeCache
//创建节点数据监听对象
final NodeCache nodeCache = new NodeCache(client, "/hello");
//开始缓存
/**
* start参数为true:可以直接获取监听的节点,System.out.println(nodeCache.getCurrentData());为ChildData{path='/aa', stat=607,765,1580205779732,1580973376268,2,1,0,0,5,1,608
, data=[97, 98, 99, 100, 101]}
* 参数为false:不可以获取监听的节点,System.out.println(nodeCache.getCurrentData());为null
*/
nodeCache.start(true);
System.out.println(nodeCache.getCurrentData());
//添加监听对象
nodeCache.getListenable().addListener(new NodeCacheListener() {
//如果节点数据有变化,会回调该方法
public void nodeChanged() throws Exception {
String data = new String(nodeCache.getCurrentData().getData());
System.out.println("数据Watcher:路径=" + nodeCache.getCurrentData().getPath()
+ ":data=" + data);
}
});
PathChildrenCache
//监听指定节点的子节点变化情况包括新增子节点 子节点数据变更 和子节点删除
//true表示用于配置是否把节点内容缓存起来,如果配置为true,客户端在接收到节点列表变更的同时,也能够获取到节点的数据内容(即:event.getData().getData())ͺ如果为false 则无法取到数据内容(即:event.getData().getData())
PathChildrenCache childrenCache = new PathChildrenCache(client,"/hello",true);
/**
* NORMAL: 普通启动方式, 在启动时缓存子节点数据
* POST_INITIALIZED_EVENT:在启动时缓存子节点数据,提示初始化
* BUILD_INITIAL_CACHE: 在启动时什么都不会输出
* 在官方解释中说是因为这种模式会在start执行执行之前先执行rebuild的方法,而rebuild的方法不会发出任何事件通知。
*/
childrenCache.start(PathChildrenCache.StartMode.POST_INITIALIZED_EVENT);
System.out.println(childrenCache.getCurrentData());
//添加监听
childrenCache.getListenable().addListener(new PathChildrenCacheListener() {
@Override
public void childEvent(CuratorFramework client, PathChildrenCacheEvent event) throws Exception {
if(event.getType() == PathChildrenCacheEvent.Type.CHILD_UPDATED){
System.out.println("子节点更新");
System.out.println("节点:"+event.getData().getPath());
System.out.println("数据" + new String(event.getData().getData()));
}else if(event.getType() == PathChildrenCacheEvent.Type.INITIALIZED ){
System.out.println("初始化操作");
}else if(event.getType() == PathChildrenCacheEvent.Type.CHILD_REMOVED ){
System.out.println("删除子节点");
System.out.println("节点:"+event.getData().getPath());
System.out.println("数据" + new String(event.getData().getData()));
}else if(event.getType() == PathChildrenCacheEvent.Type.CHILD_ADDED ){
System.out.println("添加子节点");
System.out.println("节点:"+event.getData().getPath());
System.out.println("数据" + new String(event.getData().getData()));
}else if(event.getType() == PathChildrenCacheEvent.Type.CONNECTION_SUSPENDED ){
System.out.println("连接失效");
}else if(event.getType() == PathChildrenCacheEvent.Type.CONNECTION_RECONNECTED ){
System.out.println("重新连接");
}else if(event.getType() == PathChildrenCacheEvent.Type.CONNECTION_LOST ){
System.out.println("连接失效后稍等一会儿执行");
}
}
});
TreeCache
TreeCache treeCache = new TreeCache(client,"/hello");
treeCache.start();
System.out.println(treeCache.getCurrentData("/hello"));
treeCache.getListenable().addListener(new TreeCacheListener() {
@Override
public void childEvent(CuratorFramework client, TreeCacheEvent event) throws Exception {
if(event.getType() == TreeCacheEvent.Type.NODE_ADDED){
System.out.println(event.getData().getPath() + "节点添加");
}else if (event.getType() == TreeCacheEvent.Type.NODE_REMOVED){
System.out.println(event.getData().getPath() + "节点移除");
}else if(event.getType() == TreeCacheEvent.Type.NODE_UPDATED){
System.out.println(event.getData().getPath() + "节点修改");
}else if(event.getType() == TreeCacheEvent.Type.INITIALIZED){
System.out.println("初始化完成");
}else if(event.getType() ==TreeCacheEvent.Type.CONNECTION_SUSPENDED){
System.out.println("连接过时");
}else if(event.getType() ==TreeCacheEvent.Type.CONNECTION_RECONNECTED){
System.out.println("重新连接");
}else if(event.getType() ==TreeCacheEvent.Type.CONNECTION_LOST){
System.out.println("连接过时一段时间");
}
}
});
注:在一些Curator版本中以上几个类被标识过期,被CuratorCache取代。使用方法也会有所不同。
// 创建CuratorCache实例,基于路径/father/son/grandson1(这里说的路径都是基于命名空间下的路径)
// 缓存构建选项是SINGLE_NODE_CACHE
CuratorCache cache = CuratorCache.build(curator, "/father/son/grandson1",
CuratorCache.Options.SINGLE_NODE_CACHE);
// 创建一系列CuratorCache监听器,都是通过lambda表达式指定
CuratorCacheListener listener = CuratorCacheListener.builder()
// 初始化完成时调用
.forInitialized(() -> System.out.println("[forInitialized] : Cache initialized"))
// 添加或更改缓存中的数据时调用
.forCreatesAndChanges(
(oldNode, node) -> System.out.printf("[forCreatesAndChanges] : Node changed: Old: [%s] New: [%s]\n",
oldNode, node)
)
// 添加缓存中的数据时调用
.forCreates(childData -> System.out.printf("[forCreates] : Node created: [%s]\n", childData))
// 更改缓存中的数据时调用
.forChanges(
(oldNode, node) -> System.out.printf("[forChanges] : Node changed: Old: [%s] New: [%s]\n",
oldNode, node)
)
// 删除缓存中的数据时调用
.forDeletes(childData -> System.out.printf("[forDeletes] : Node deleted: data: [%s]\n", childData))
// 添加、更改或删除缓存中的数据时调用
.forAll((type, oldData, data) -> System.out.printf("[forAll] : type: [%s] [%s] [%s]\n", type, oldData, data))
.build();
// 给CuratorCache实例添加监听器
cache.listenable().addListener(listener);
// 启动CuratorCache
cache.start();
Master选举
分布式执行一些不需要同时执行的复杂任务,curator利用zk的特质,实现了这个选举过程。其实就是利用了多个zk客户端在同一个位置建节点,只会有一个客户端建立成功这个特性。来实现同一时间,只会选择一个客户端执行任务。
LeaderSelector selector = new LeaderSelector(cc, "/tmp/leader/master", new LeaderSelectorListener() {
@Override
public void takeLeadership(CuratorFramework client) throws Exception {
//成为leader了
System.out.println("do leader work");
Thread.sleep(5000);
System.out.println("end work");
}
@Override
public void stateChanged(CuratorFramework client, ConnectionState newState) {
System.out.println("stateChanged:"+newState);
}
});
selector.autoRequeue();
selector.start();
Thread.sleep(Integer.MAX_VALUE);
分布式锁
String lockPath = "/123/111";
InterProcessMutex lock = new InterProcessMutex(client,lockPath);
lock.acquire();
//do something
lock.release();
curator直接给出了分布式锁的实现。原理是客户端创建锁节点,执行完毕后再删除锁节点。一个客户端先检查是否有锁节点,如果没有,说明可以执行,则创建锁节点去执行。如果有锁节点,则说明现在锁在别的客户端那里,自己则需要等待。
分布式计数器
分布式计数器的一个典型应用场景是统计在线人数。
指定一个zookeeper节点作为计数器,多个应用实例在分布式锁的控制下,通过更新该数据节点的内容来实现技术功能。
通过类DistributedAtomicInteger来实现。
//分布式计数器
DistributedAtomicInteger atomicInteger = new DistributedAtomicInteger(client,"/distributed_atomic_counter",new ExponentialBackoffRetry(1000,3));
AtomicValue av = atomicInteger.add(10);
System.out.println(av.succeeded());//true
System.out.println(av.preValue());//0
System.out.println(av.postValue());//10
每个DistributedAtomicXXX里面都有一个AtomicValue,这个是分布式的核心实现类。
AtomicValue<byte[]> trySet(MakeValue makeValue) throws Exception
{
MutableAtomicValue<byte[]> result = new MutableAtomicValue<byte[]>(null, null, false);
//尝试下乐观锁
tryOptimistic(result, makeValue);
if ( !result.succeeded() && (mutex != null) )
{
//失败的话再使用排他锁
tryWithMutex(result, makeValue);
}
return result;
}
分布式Barrier
Barrier是一种用来控制多线程之间同步的经典方式,在JDK中也自带了CyclicBarrier实现。
curator用DistributeBarrier类来实现。
1)分布式的Barrier(主线程触发)
for (int i = 0; i < 5; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+" is ready ");
CuratorFramework client = CuratorFrameworkFactory.builder()
.connectString("ip:port")
.sessionTimeoutMs(2000)
.connectionTimeoutMs(5000)
.retryPolicy(new ExponentialBackoffRetry(1000,3))
.namespace("test")
.build();
client.start();
DistributedBarrier barrier = new DistributedBarrier(client,"/distributed_barrier");
try {
barrier.setBarrier();
barrier.waitOnBarrier();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" run ");
}).start();
}
Thread.sleep(5000);
DistributedBarrier barrier = new DistributedBarrier(cc,"/distributed_barrier");
barrier.removeBarrier();
2)分布式的Barrier(根据等待线程数量触发,同时进入 and 同时退出)
for (int i = 0; i < 5; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+" is ready ");
CuratorFramework client = CuratorFrameworkFactory.builder()
.connectString("ip:port")
.sessionTimeoutMs(2000)
.connectionTimeoutMs(5000)
.retryPolicy(new ExponentialBackoffRetry(1000,3))
.namespace("test")
.build();
client.start();
DistributedDoubleBarrier barrier = new DistributedDoubleBarrier(client,"/distributed_barrier",5);
try {
//进入时会等待,5个才会同时进入
barrier.enter();
Thread.sleep(3000);
//退出时依然要等待,5个才会同时退出
barrier.leave();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" run ");
}).start();
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了