Curator框架实现Zookeeper基本操作

Zookeeper是一个Apache开源的分布式的应用,为系统架构提供协调服务。从设计模式角度来审视:该组件是一个基于观察者模式设计的框架,负责存储和管理数据,接受观察者的注册,一旦数据的状态发生变化,Zookeeper就将负责通知已经在Zookeeper上注册的观察者做出相应的反应,从而实现集群中类似Master/Slave管理模式。ZooKeeper的目标就是封装好复杂易出错的关键服务,将简单易用的接口和性能高效、功能稳定的系统提供给用户。

curator是对zookeeper原生api的封装。原生的api特别难用

curator提供了流式编程风格,做的非常不错,是目前使用率最高的一种zookeeper框架。

curator它主要包含三个依赖(curator的依赖都已经放到maven仓库,你直接使用maven来构建它。对于大多数人来说,我们可能最常需要引入的是curator-recipes):

官方文档说明

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();
}

 

posted @ 2022-04-24 18:29  残城碎梦  阅读(1217)  评论(0编辑  收藏  举报