1. Curator 介绍

Apache Curator 是一个用于简化 Apache ZooKeeper 使用的 Java 库。它提供了一组高层次的 API 和实用工具,帮助开发者更轻松地与 ZooKeeper 集成并使用其功能。ZooKeeper 本身是一个强大的分布式协调服务,但其原生 API 相对底层,并且在处理连接管理、重试机制和会话恢复等方面需要较多的手动处理。Curator 旨在解决这些问题,通过更易用的接口和高级抽象来提高开发效率和代码的可维护性。

1.1. 类似 Redisson

Curator 之于 ZooKeeper,就像 Redisson 之于 Redis。 这里使用 Redisson 而不是 Jedis,是因为 Jedis 提供的是比较原生的API,而 Redisson 封装了更丰富的功能(如直接提供分布式锁、限流器等类)。

同样 ZooKeeper 也有类似于 Jedis 的工具 - Apache ZooKeeper Java Client 。同样也是 ZooKeeper 官方提供的 Java 客户端,提供基础的 ZooKeeper 服务访问和操作功能,相对而言比较原生的API。

1.2. 主要特性

1、连接管理

Curator 提供了灵活的连接管理,包括自动重试和复杂的重试策略,如指数退避(重试次数增加后,重试间隔时间指数级增加)。

它可以自动处理会话过期和重连,从而减少开发者的工作量。其内建的重试机制和会话恢复功能,提高了应用程序的可靠性。

2.节点及数据管理

Curator 提供 ZooKeeper 中原生的节点及数据管理,如:创建、删除节点,更新数据等,但 watch 监听进一步做了包装。

3.Recipe 实现

Curator 实现了一些常见的分布式系统模式(称为 Recipes),这包括:领导选举、分布式锁、共享计数器、分布式队列、分布式屏障等。
这些模式经过精心设计和测试,可以直接用于生产环境中。

2. 连接管理

使用 Curator 的第一步就是要创建 ZooKeeper 客户端的连接,用法上都是先创建 CuratorFramework 对象,然后执行 start() 方法启动客户端,初始化连接。

2.1. 基础创建

示例代码:

CuratorFramework client = CuratorFrameworkFactory.newClient(
    "localhost:2181", 
    new RetryOneTime(1000)
);
client.start();

CuratorFrameworkFactory.newClient 是创建 Curator 客户端最简单的方法,就两个参数:

  • 服务器连接:ZooKeeper 服务器连接字符串。
  • 重试策略:这里使用的是 RetryOneTime(1000),表示如果连接失败,则在 1000 毫秒后重试一次。

2.2. 高阶创建

高阶地创建CuratorFramework客户端,就需要用到 CuratorFrameworkFactory.Builder 了。可以实现更多的定制化选项,如设置会话超时、连接超时、重试策略以及命名空间。

示例代码:

CuratorFramework client = CuratorFrameworkFactory.builder()
    .connectString("localhost:2181")
    .sessionTimeoutMs(5000)
    .connectionTimeoutMs(5000)
    .retryPolicy(new ExponentialBackoffRetry(100, 3 , 1000))
    .namespace("myNamespace")
    .authorization("digest", "user:password".getBytes())
    .build();
client.start();

CuratorFrameworkFactory.builder() 创建了一个可定制的构建器。

  • .connectString("localhost:2181"): 设置了 ZooKeeper 服务器的连接字符串。
  • .sessionTimeoutMs(5000): 设置了会话超时时间为 5000 毫秒。这意味着如果客户端超过 5 秒没有与 ZooKeeper 服务器交互,那么会话将过期。
  • .connectionTimeoutMs(5000): 设置了连接超时时间为 5000 毫秒。如果客户端在 5 秒内无法成功连接到 ZooKeeper 服务器,则会抛出异常。
  • .retryPolicy(new ExponentialBackoffRetry(100, 3 , 1000)): 设置了重试策略为指数退避策略。在这种情况下,首次重试间隔为 100 毫秒,最多重试 3 次,最大重试间隔不超过 1000 毫秒。
  • .namespace("myNamespace"): 设置了命名空间为 myNamespace。这有助于避免不同应用之间的节点命名冲突。
  • .authorization("digest", "user:password".getBytes()): 设置了客户端的安全认证信息。在这个例子中,使用了 Digest 认证方式,并指定了用户名和密码。

2.3. 重连策略

高阶使用的例子中有提到自动重试的策略 .retryPolicy(new ExponentialBackoffRetry(1000, 3)),且提到为指数退避策略。

自动重试很好理解,当因为网络等原因导致连接中断时,客户端会尝试重新连接。那什么是指数退避策略呢。

指数退避(Exponential Backoff)

是一种在网络编程和分布式系统中常用的重试策略,用于处理失败的请求或连接。它的核心思想是在每次重试之间等待一个逐渐增加的时间间隔,以减少连续重试对系统造成的压力。

工作原理:

  1. 初始重试间隔:指数退避策略通常从一个较小的初始时间间隔开始,例如 100 毫秒。
  2. 逐步增加重试间隔:每次重试失败后,下一个重试的时间间隔会按照指数级增长。例如,第一次重试间隔为 100 毫秒,第二次为 200 毫秒,第三次为 400 毫秒,依此类推。
  3. 最大重试间隔:为了避免无限增长的时间间隔导致长时间等待,通常会设定一个最大重试间隔。例如,最大重试间隔可能设置为 1 分钟。
  4. 随机化:为了进一步分散请求,可以在每个重试间隔的基础上添加一定的随机偏移量,以避免多个客户端同时重试导致的“重试风暴”。
解释 ExponentialBackoffRetry(100, 3, 1000)

ExponentialBackoffRetry(100, 3, 1000 创建一个指数退避策略实例,其中:

  • 100: 初始重试间隔为 100 毫秒。
  • 3: 最多重试 3 次。
  • 1000: 最大重试间隔为 1000 毫秒(1 秒)。

指数退避的重试间隔计算方法通常是根据指数级增长的方式来增加时间间隔,每次重试间隔是按照前一次的基础上乘以 2 来计算的,但它也会被限制在最大重试间隔之内。对于这个具体例子,重试间隔时间会是:

  1. 第一次重试:间隔是初始值 100 毫秒。
  2. 第二次重试:间隔是 100 * 2 = 200 毫秒。
  3. 第三次重试:间隔是 100 * 4 = 400 毫秒。

在这个例子中,虽然我们设定了最大间隔为 1000 毫秒,但由于最多只重试 3 次,所以实际的间隔不会达到最大设定值 1000 毫秒。如果重试次数增加,那么一旦增长的间隔时间超过 1000 毫秒,它将不会继续增长,因为 1000 是设置的最大间隔限制。

2.4. 命名空间

命名空间是 Curator 客户端在逻辑上包装的概念,ZooKeeper 并未提供。
当你在 Curator 中定义了一个命名空间后,Curator 会自动在该命名空间下对所有的 ZooKeeper 操作进行相对路径处理。也就是说,你在 Curator 客户端中定义路径时,无需考虑命名空间的前缀,Curator 会自动帮你在实际操作时加上。

例如上述 .namespace("myNamespace"),定义了 myNamespace 的命名空间,当我们基于该客户端连接创建一个 /node1 节点。

client.create().forPath("/node1", "data".getBytes());

这行代码将在实际的 ZooKeeper 路径 /myNamespace/node1 上创建一个节点,而不是 /node1

应用场景:

  • 隔离性:通过命名空间,能够将不同的应用或模块在 ZooKeeper 上的操作隔离开来,避免节点名称冲突。
  • 简化管理:命名空间使得节点路径管理更加简单和直观。在创建和管理节点时,开发者无需手动添加命名空间前缀,减少了出错的可能性。
  • 迁移和版本控制:在应用升级或迁移过程中,可以为新版本或测试环境创建单独的命名空间,方便对比和验证。通过命名空间,也可以轻松实现同一应用的不同版本共存。

3. 节点及数据管理

下面例子中假设已经定义好了 CuratorFramework 变量 client

3.1. 基本操作

3.1.1. 创建

示例:

        String path = client.create()
                .creatingParentsIfNeeded()
                .withMode(CreateMode.PERSISTENT_SEQUENTIAL)
                .forPath("/example/path", "initial_data".getBytes());

解释:

  • create(): 创建一个节点。如果节点已经存在则抛出异常。
  • creatingParentsIfNeeded(): 如果父节点不存在,则会自动创建父节点。
  • forPath:第一个参数为路径,第一个参数为节点值。
  • withMode(): 设置节点类型(持久节点、临时节点)。

    • 持久节点 (CreateMode.PERSISTENT)
    • 临时节点 (CreateMode.EPHEMERAL)
    • 持久有序节点 (CreateMode.PERSISTENT_SEQUENTIAL)
    • 临时有序节点 (CreateMode.EPHEMERAL_SEQUENTIAL)

3.1.2. 删除

示例:

        client.delete()
              .guaranteed()
              .deletingChildrenIfNeeded()
              .forPath("/example/path");

解释:

  • delete(): 删除一个节点。
  • deletingChildrenIfNeeded(): 删除节点以及其所有子节点。
  • guaranteed(): 保证节点删除,即使连接中断后,也会在重新连接后继续尝试删除节点。

3.1.3. 更新

示例:

        client.setData()
              .forPath("/example/path", "updated_data".getBytes());

解释:

  • setData(): 设置节点的数据。如果节点不存在将抛出异常。

3.1.4. 获取节点值

示例:

        byte[] data = client.getData().forPath("/example/path");
        System.out.println("Node data: " + new String(data));

解释:

  • getData(): 获取节点的数据。

3.1.5. 检查节点是否存在

示例:

        Stat stat = client.checkExists().forPath("/example/path");
        if (stat != null) {
            System.out.println("Node exists at: /example/path");
        } else {
            System.out.println("Node does not exist at: /example/path");
        }

解释:

  • checkExists(): 检查节点是否存在。

3.1.6. 获取所有子节点

示例:

List<String> children = client.getChildren().forPath(parentPath);

解释:

  • getChildren().forPath(parentPath): 调用这个方法来获取指定路径下的子节点。返回的 List<String> 包含所有子节点的名称,但不包含路径。
注意

判断一个节点是否有子节点,也是通过这个方法,然后通过 children 集合是否为空来判断。

3.2. 版本管理

在 ZooKeeper 中,每个节点都有一个版本编号,用于记录对该节点数据的修改次数。每次更新节点数据时,版本号会递增。

在更新数据、删除节点等操作上,使用版本号可以实现乐观锁,确保数据的一致性和正确性。

1、版本号的作用
  • 数据一致性:确保只有在你知道节点当前版本的情况下才能更新数据。这防止了旧数据覆盖新数据的情况。
  • 乐观锁:通过比较和设置版本号来实现乐观锁控制。
2、使用版本号的操作
  • 获取节点数据和版本号:使用 client.getData().storingStatIn(stat).forPath(path); 可以同时获取节点的数据和版本信息。
  • 更新节点数据时指定版本号:使用 client.setData().withVersion(stat.getVersion()).forPath(path, "new_data".getBytes()); 指定版本号进行更新。
  • 错误版本号的更新尝试:如果指定的版本号不匹配,则抛出异常。这是检测并避免数据冲突的一种方式。
3、示例代码

以下是如何在 Curator 中使用 ZooKeeper 节点的版本号来确保数据更新一致性的示例。

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

public class CuratorVersioningExample {
    public static void main(String[] args) throws Exception {
        CuratorFramework client = CuratorFrameworkFactory.newClient(
                "localhost:2181", new ExponentialBackoffRetry(1000, 3));
        client.start();

        String path = "/example/versioned_node";

        // 创建一个持久节点
        if (client.checkExists().forPath(path) == null) {
            client.create().creatingParentsIfNeeded().forPath(path, "initial_data".getBytes());
        }

        // 获取节点的数据和版本号
        Stat stat = new Stat();
        byte[] data = client.getData().storingStatIn(stat).forPath(path);
        System.out.println("Current data: " + new String(data));
        System.out.println("Current version: " + stat.getVersion());

        // 尝试更新节点数据(使用正确的版本号进行更新)
        try {
            client.setData().withVersion(stat.getVersion()).forPath(path, "new_data".getBytes());
            System.out.println("Data updated successfully with correct version.");
        } catch (Exception e) {
            System.out.println("Failed to update data: " + e.getMessage());
        }

        // 获取更新后的数据和版本号
        data = client.getData().storingStatIn(stat).forPath(path);
        System.out.println("Updated data: " + new String(data));
        System.out.println("Updated version: " + stat.getVersion());

        // 尝试使用错误的版本号更新节点数据
        try {
            client.setData().withVersion(stat.getVersion() - 1).forPath(path, "incorrect_version_data".getBytes());
            System.out.println("Data updated with incorrect version (unexpected).");
        } catch (Exception e) {
            System.out.println("Failed to update data with incorrect version: " + e.getMessage());
        }

        client.close();
    }
}
4、withVersion

.withVersion() 方法在 Apache Curator 中主要用于两类操作:节点数据的更新和节点的删除。这两个操作都是与 ZooKeeper 数据节点的版本号直接相关的操作,利用版本号可以实现乐观锁机制,确保操作的原子性和数据的一致性。

节点数据更新 (setData):

client.setData().withVersion(version).forPath(path, newData);

节点删除 (delete):

client.delete().withVersion(version).forPath(path);

4. Recipe 实现

Apache Curator 是一个用于简化 Apache ZooKeeper 使用的高层次客户端库。它不仅提供了 ZooKeeper 原生 API 的封装,还提供了一系列称为 "Recipes" 的高级工具,这些工具实现了常见的分布式系统的设计模式。这些 Recipes 大大简化了使用 ZooKeeper 进行分布式协调的复杂性。

一些常用的 Curator Recipes 如:Leader Election(领导选举)、Shared Reentrant Lock(可重入锁)、Barrier(屏障)、Cache(缓存)、DistributedQueue(分布式队列)等。

Cache 即 NodeCache等缓存,单起一章讲。

4.1. Leader Election(领导选举)

领导者选举在分布式系统中非常重要,因为它允许多个进程协调并选择一个进程作为“主”进程来执行特定任务。

1、示例

代码示例:

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 CuratorLeaderElectionExample {

    public static void main(String[] args) throws Exception {
        String zkConnectionString = "localhost:2181";
        String leaderPath = "/example/leader";

        // 创建并启动 Curator 客户端
        CuratorFramework client = CuratorFrameworkFactory.newClient(
                zkConnectionString,
                new ExponentialBackoffRetry(1000, 3)
        );
        client.start();

        // 创建 LeaderSelector 实例
        LeaderSelector leaderSelector = new LeaderSelector(client, leaderPath, new LeaderSelectorListenerAdapter() {
            @Override
            public void takeLeadership(CuratorFramework client) throws Exception {
                System.out.println("I am the leader now!");
                // 在这里执行主节点相关的任务
                Thread.sleep(3000); // 模拟领导者任务执行
                System.out.println("Releasing leadership");
            }
        });

        // 自动重新排队,确保在领导权释放后能够重新竞选
        leaderSelector.autoRequeue();
        leaderSelector.start();

        // 让主线程保持运行状态以观察选举过程
        Thread.sleep(Long.MAX_VALUE);
        // 省略 close 代码
    }
}

代码解释:

  • LeaderSelector: 用于执行领导者选举的核心类。LeaderSelectorListenerAdapter 提供了 takeLeadership 方法,当节点成为领导者时调用。
  • autoRequeue: 确保当当前领导者释放领导权后,该实例可以重新参与选举。
  • takeLeadership: 这是当节点成为领导者时执行的逻辑。在这里,我们模拟执行了一些任务,然后释放领导权。
2、实现原理

ZooKeeper 的领导者选举通常是基于临时顺序节点(ephemeral sequential nodes)实现的。以下是基本的流程和原理:

  • 创建临时顺序节点:每个参与选举的客户端都会在一个特定的“选举”路径下创建一个临时顺序节点。
  • 获取子节点列表并排序:每个客户端获取这个路径下的所有子节点,并按顺序排列。
  • 判断自己是否为序号最小的节点:如果某个客户端创建的节点是序号最小的节点,则它成为领导者。
  • 监听前一个节点:如果不是最小的节点,则监听比自己小的那个节点(前一个节点)。如果前一个节点消失(意味着持有该节点的客户端故障或断开连接),那么当前节点检查自己是否成为了新的最小节点。
  • 重复过程:由于创建的是临时节点,客户端断开连接时节点会自动删除,所以当领导者断开连接时,会触发新的选举。

这种机制可以保证在分布式系统中选出一个唯一的领导者,并且在领导者失效时能够迅速选出新的领导者。Curator 对这一原理进行了封装,使得实现领导者选举更加简洁和易于使用。

4.2. Shared Reentrant Lock(可重入锁)

使用 ZooKeeper 实现分布式锁是一个常见的用例,而 Apache Curator 提供了 InterProcessMutex 来实现共享的可重入锁。这种锁机制确保了在分布式系统中,多个客户端可以安全地访问共享资源。

1、示例

代码示例:

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 CuratorSharedReentrantLockExample {

    public static void main(String[] args) throws Exception {
        String zkConnectionString = "localhost:2181";
        String lockPath = "/example/lock";

        // 创建并启动 Curator 客户端
        CuratorFramework client = CuratorFrameworkFactory.newClient(
                zkConnectionString,
                new ExponentialBackoffRetry(1000, 3)
        );
        client.start();

        // 创建一个 InterProcessMutex 实例
        InterProcessMutex lock = new InterProcessMutex(client, lockPath);

        try {
            // 尝试获取锁
            if (lock.acquire(10, java.util.concurrent.TimeUnit.SECONDS)) {
                try {
                    System.out.println("Lock acquired, performing some work...");

                    // 在这里进行锁定区间内的工作
                    Thread.sleep(5000);

                } finally {
                    // 确保释放锁
                    lock.release();
                    System.out.println("Lock released");
                }
            } else {
                System.out.println("Failed to acquire lock");
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            client.close();
        }
    }
}

代码解释:

  • InterProcessMutex: 这是 Curator 提供的可重入锁实现。它确保在分布式系统中多个客户端能够安全地获取和释放锁。
  • acquire: 尝试获取锁。在这个示例中,我们设置了一个超时时间(10秒)。
  • release: 确保在完成工作后释放锁。
2、实现原理

ZooKeeper 的分布式锁通常基于临时顺序节点实现,其原理如下:

  • 创建临时顺序节点: 每个想要获取锁的客户端在一个特定的锁路径下创建一个临时顺序节点。
  • 获取子节点列表并排序: 每个客户端获取这个路径下的所有子节点,并按顺序排列。
  • 判断自己是否为序号最小的节点: 如果某个客户端创建的节点是最小的节点,则它获得锁。
  • 监听前一个节点: 如果不是最小的节点,则监听比自己小的那个节点(前一个节点)。如果前一个节点消失(意味着持有该节点的客户端释放了锁),那么当前节点检查自己是否成为了新的最小节点,从而获得锁。
  • 重复过程: 客户端断开连接时,临时节点会自动删除,从而触发锁的重新分配。

Curator 的 InterProcessMutex 对这一过程进行了封装,自动处理节点的创建、排序、监听和删除,使得开发者不必手动实现这些细节。这种机制确保了在分布式系统中,锁可以安全、可靠地被多个进程共享和控制。

4.3. Barrier(屏障)

在分布式计算中,Barrier 是一种同步原语,用于确保一组参与进程在执行某个操作之前必须等待所有其他进程都到达 Barrier。这类似于一个起跑线上的赛跑者,必须等到所有人都准备好之后才能一起出发。

1、示例

代码示例:

import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.barriers.DistributedBarrier;
import org.apache.curator.retry.ExponentialBackoffRetry;

public class CuratorDistributedBarrierExample {

    public static void main(String[] args) throws Exception {
        String zkConnectionString = "localhost:2181";
        String barrierPath = "/example/barrier";

        // 创建并启动 Curator 客户端
        CuratorFramework client = CuratorFrameworkFactory.newClient(
                zkConnectionString,
                new ExponentialBackoffRetry(1000, 3)
        );
        client.start();

        // 创建一个 DistributedBarrier 实例
        DistributedBarrier barrier = new DistributedBarrier(client, barrierPath);

        try {
            System.out.println("Waiting at the barrier...");
            
            // 等待所有参与者到达 Barrier
            barrier.waitAtBarrier(10, java.util.concurrent.TimeUnit.SECONDS);

            System.out.println("All participants have reached the barrier. Proceeding...");
            
            // 在这里进行需要同步的操作
            Thread.sleep(5000);
            
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            client.close();
        }
    }
}

代码解释:

  • CuratorFramework: 创建并启动一个 Curator 客户端以连接到 ZooKeeper。
  • DistributedBarrier: 这是 Curator 提供的 Barrier 实现。它确保在分布式系统中多个客户端可以安全地等待所有参与者到达 Barrier。
  • waitAtBarrier: 尝试等待所有参与者到达 Barrier。在这个示例中,我们设置了一个超时时间(10秒)。
  • finally: 确保在完成操作后关闭 Curator 客户端。
2、实现原理

假设我们有两个参与者 A 和 B,它们需要同时执行某个操作:

  • 参与者 A 到达:A 在 /example/barrier 下创建一个临时节点。
  • 参与者 B 到达:B 在 /example/barrier 下也创建一个临时节点。
  • 两个参与者都到达:此时,屏障节点 /example/barrier 已经有两个子节点,代表两个参与者都已到达。
  • 删除屏障节点:最后一个到达的参与者(假设是 B)删除屏障节点 /example/barrier。
  • 触发监听器:屏障节点的删除触发所有参与者的监听器,告知它们可以继续执行下一步。

通过这种方式,ZooKeeper 的 Barrier 实现确保了所有参与者都可以同步地执行下一步操作,这对于分布式系统中的同步操作非常有用。

4.4. DistributedQueue(分布式队列)

1、示例

Curator 提供了一些高级原语来简化在 ZooKeeper 上实现分布式队列的过程,例如 DistributedQueue。

代码示例:

import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.queue.DistributedQueue;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.apache.curator.framework.recipes.queue.QueueBuilder;

public class CuratorDistributedQueueExample {

    public static void main(String[] args) throws Exception {
        String zkConnectionString = "localhost:2181";
        String queuePath = "/example/queue";

        // 创建并启动 Curator 客户端
        CuratorFramework client = CuratorFrameworkFactory.newClient(
                zkConnectionString,
                new ExponentialBackoffRetry(1000, 3)
        );
        client.start();

        // 创建一个 DistributedQueue 实例
        DistributedQueue<String> queue = QueueBuilder.builder(
                client,
                null,  // 这里可以设置一个消费者
                (message) -> { System.out.println("Consumed: " + message); }, // 消费者逻辑
                queuePath
        ).buildQueue();

        queue.start();

        // 生产者 - 向队列中添加消息
        queue.put("Message 1");
        queue.put("Message 2");

        // 这里可以模拟消费者进行消费

        // 优雅关闭队列和客户端
        Thread.sleep(10000);
        queue.close();
        client.close();
    }
}

代码解释:

  • DistributedQueue: 使用 QueueBuilder 创建一个 DistributedQueue 实例。可以指定一个消费者逻辑来处理队列中的消息。
  • put: 向队列中添加消息,模拟生产者的行为。
  • 消费者逻辑: 在 QueueBuilder 中设置消费者逻辑,处理从队列中取出的消息。
  • queue.close() 和 client.close(): 确保在操作完成后关闭队列和 Curator 客户端。
2、实现原理

在 ZooKeeper 中,实现一个分布式队列的原理通常基于有序节点和监听机制:

  • 有序节点: 利用 ZooKeeper 的有序节点特性,每个加入队列的元素在一个指定路径下创建一个有序节点,这样可以自动确保元素的顺序。
  • 生产者: 通过在指定路径下创建有序子节点来添加元素。例如,生产者在 /queue 路径下依次创建 /queue/element-0000000001、/queue/element-0000000002 等节点。
  • 消费者: 监听 /queue 路径的子节点变化,获取所有子节点列表并排序,取出序号最小的节点进行消费。
  • 消费后删除节点: 消费者在处理完节点中的数据后,删除该节点以从队列中移除该任务。

5. 节点监听

Curator 提供了多个工具用于 ZooKeeper 节点监听(watch),常见的有:NodeCache、PathChildrenCache 和 TreeCache,这里主要讲解 NodeCache。

NodeCache 是 Apache Curator 提供的一种实用工具,用于监听 ZooKeeper 中某个特定节点的数据变化。它在客户端中维护了该节点的一个本地缓存,当节点的数据发生变化时,NodeCache 会自动更新缓存,并通知注册的监听器。这种机制非常适合需要实时监控节点数据变化的场景。

5.1. NodeCache 使用示例

NodeCache 的使用步骤

  1. 创建 CuratorFramework 实例并启动。
  2. 创建 NodeCache 实例。
  3. 注册监听器以响应节点数据变化。
  4. 启动 NodeCache。
  5. 处理完后关闭 NodeCache 和 CuratorFramework。

代码示例:

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;

public class NodeCacheExample {

    private static final String ZK_ADDRESS = "localhost:2181"; // ZooKeeper 连接地址
    private static final String ZNODE_PATH = "/example/nodeCache"; // 监听的节点路径

    public static void main(String[] args) throws Exception {
        // 1. 创建 CuratorFramework 实例
        CuratorFramework client = CuratorFrameworkFactory.newClient(
                ZK_ADDRESS,
                new ExponentialBackoffRetry(1000, 3)
        );

        // 2. 启动 CuratorFramework 客户端
        client.start();

        // 3. 创建 NodeCache 实例
        NodeCache nodeCache = new NodeCache(client, ZNODE_PATH);

        // 4. 注册监听器
        NodeCacheListener listener = () -> {
            byte[] newData = nodeCache.getCurrentData().getData();
            System.out.println("Node data updated, new data: " + new String(newData));
        };
        nodeCache.getListenable().addListener(listener);

        // 5. 启动 NodeCache
        nodeCache.start(true);

        // 模拟等待
        System.out.println("Listening for data changes on node: " + ZNODE_PATH);
        Thread.sleep(100000);

        // 6. 关闭 NodeCache 和 CuratorFramework 客户端
        nodeCache.close();
        client.close();
    }
}

当 NodeCache 启动后,本地 NodeCache 中数据会自动更新变化。当如果我们需要自定义逻辑,当数据变化时处理对应逻辑,则需要注册NodeCacheListener。其中 nodeCache.getCurrentData().getData() 为更新后的数据,如果值为 null,可能意味着当前节点已被删除。

5.2. start、close

1、start(boolean buildInitialCache)
  • start(true):当参数为 true 时,NodeCache 会在启动时立即尝试从 ZooKeeper 获取指定节点的初始数据,并将其存储在本地缓存中。这意味着一旦 NodeCache 启动,监听器将立即收到节点的初始数据更新事件。
  • start(false):当参数为 false 时,NodeCache 不会在启动时立即尝试获取节点的初始数据。它将仅在节点数据实际发生变化时才更新本地缓存,并触发监听器。这意味着如果在 NodeCache 启动后节点数据尚未变化,则监听器不会接收到任何更新事件,直到节点数据真正发生变化。
2、start 方法步骤

start() 方法执行了几个重要操作,以便开始监听指定的节点路径,并维护其数据的本地缓存:

  • 初始化数据:在启动时,NodeCache 会尝试从 ZooKeeper 中获取指定节点的初始数据。如果节点存在且可访问,它将缓存该数据。
  • 注册监听器:NodeCache 会在 ZooKeeper 上注册一个监听器,用于监听该节点的变化事件,包括节点数据的更新、节点的创建和删除。
  • 启动背景线程:NodeCache 会启动必要的后台线程,以处理来自 ZooKeeper 的事件并更新缓存。
3、close 方法步骤

close() 方法用于释放 NodeCache 实例所占用的资源,并停止其所有后台活动和监听器。这是一个关键的方法,确保在不再需要使用 NodeCache 时,正确地关闭并清理资源。具体来说,close() 方法的作用包括:

  • 停止监听器:close() 方法会停止 NodeCache 中注册的所有监听器,不再接收来自 ZooKeeper 的事件更新。这意味着任何节点数据的变化都将不再通知给应用程序。
  • 释放资源:NodeCache 使用了一些资源,包括线程、网络连接和内存等。close() 方法确保这些资源被释放,以避免资源泄漏。
  • 确保线程安全:关闭 NodeCache 可以确保在应用程序结束或不再需要节点监控时,系统资源被适当地释放,防止后台线程继续运行或进入不确定的状态。
4、不能只关闭 CuratorFramework 而不关闭 NodeCache

在结束对 NodeCache 的使用或 curatorFramework.close() 之前,确保调用 nodeCache.close() 来释放资源和停止后台线程。

否则会导致的问题:

  • 资源泄漏:如果你不手动关闭 NodeCache 而仅仅关闭 CuratorFramework 客户端,可能会造成资源泄漏。虽然 CuratorFramework 的关闭会终止与 ZooKeeper 的连接,但 NodeCache 本身的资源(如线程或其他结构)可能没有被适当释放。
  • 异常行为:由于 NodeCache 依赖于 CuratorFramework 的连接来接收事件和数据更新,关闭 CuratorFramework 后,NodeCache 可能会尝试继续操作而没有有效的连接,这可能导致异常或者不可预测的行为。
  • 不必要的线程活动:如果 NodeCache 的内部线程未关闭,它们可能会继续运行,浪费系统资源。

5.3. 实现原理

ZooKeeper 的原生 watch 机制是一次性的。这意味着当一个事件触发 watch 后,watch 就会被移除,必须重新注册以继续监听后续事件。

但 Curator NodeCache 框架通过封装和管理这些细节,实现了持久的节点监听。以下是 NodeCache 持久监听的核心原理:

  • 自动重置 watch:当 NodeCache 接收到一个节点事件时,它会自动重新注册 watch,以确保持续监听节点的后续变化。这是通过在事件回调中重新设置 watch 实现的。
  • 事件处理:NodeCache 使用 Curator 提供的方法来注册对节点的监听。Curator 内部会通过一个事件循环来处理来自 ZooKeeper 的事件。当节点的数据发生变化时,Curator 会接收到通知,并将事件分派给 NodeCache 的监听器。
  • 缓存管理:NodeCache 维护一个缓存来存储节点的当前数据。当节点数据发生变化时,缓存会更新,并且任何注册的监听器都会被触发来处理这个变化。
  • 后台线程:NodeCache 使用一个后台线程来处理这些事件和缓存更新,因此应用程序的主线程不会被阻塞。
  • 错误处理和重试机制:Curator 本身提供了重试机制来处理临时的连接问题(例如网络故障)。NodeCache 利用这种机制在遇到临时问题时自动重试,以确保持续的连接和事件监听。

5.4. 网络中断导致的问题

1、watch中断(解决)

因为 ZooKeeper 的 watch 是一次性的,需要在每次事件触发后重新设置。在使用 ZooKeeper 的原生 watch 机制时,如果在处理事件后还未重新注册 watch 就发生网络故障,可能会导致后续的事件无法监听到。

然而,Curator 框架通过其高级封装和管理,极大地缓解了这一问题:

  • 自动重连:Curator 内部管理 ZooKeeper 客户端的会话和连接状态。如果检测到网络故障或连接中断,它会自动进行重连。
  • 重新注册:在重连期间,Curator 会尝试重新建立所有必要的 watch。这包括在 NodeCache 中用于监听节点变化的 watch。
2、数据一致性(解决)

如果客户端在网络中断期间, ZooKeeper 服务端数据发生变更,客户端是没有监听到变更消息的。在客户端重连后,是否会导致数据不一致?

不会的,如上述 Curator 在重连后会重建所有必要的 watch。我们可以在 CuratorFramework 注册的重连监听器中调用 NodeCache.getCurrentData() 主动更新值。或者最初 NodeCache.start(true) 参数设置为 true

3、事件丢失(未解决)

上述在网络中断重连后,可以保障数据最终一致性。但在中断期间watch 的数据变更事件是否会重新推送呢?不会,ZooKeeper 不像 Etcd,没有事件回放功能。

如果业务上需要基于 NodeCacheListener 变更事件做处理,可能会有依赖。所以业务上需要合理设计,规避这种影响,或者是否可以在重连事件中增加处理。

5.5. PathChildrenCache

NodeCache 只用于监听指定节点自身的变化,而 PathChildrenCache 是专门用于监听指定节点的子节点变化,包括子节点的添加、删除和数据更新。

示例代码:

import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.apache.curator.framework.recipes.cache.PathChildrenCache;
import org.apache.curator.framework.recipes.cache.PathChildrenCacheEvent;
import org.apache.curator.framework.recipes.cache.PathChildrenCacheListener;

public class PathChildrenCacheExample {

    public static void main(String[] args) {
        String zookeeperConnectionString = "localhost:2181";
        CuratorFramework client = CuratorFrameworkFactory.builder()
                .connectString(zookeeperConnectionString)
                .retryPolicy(new ExponentialBackoffRetry(1000, 3))
                .build();
        
        client.start();
        
        PathChildrenCache childrenCache = new PathChildrenCache(client, "/example/path", true);
        
        PathChildrenCacheListener listener = new PathChildrenCacheListener() {
            @Override
            public void childEvent(CuratorFramework client, PathChildrenCacheEvent event) {
                switch (event.getType()) {
                    case CHILD_ADDED:
                        System.out.println("Child added: " + event.getData().getPath());
                        break;
                    case CHILD_UPDATED:
                        System.out.println("Child updated: " + event.getData().getPath());
                        break;
                    case CHILD_REMOVED:
                        System.out.println("Child removed: " + event.getData().getPath());
                        break;
                    default:
                        break;
                }
            }
        };
        
        childrenCache.getListenable().addListener(listener);
        
        try {
            childrenCache.start(PathChildrenCache.StartMode.NORMAL);
            // 运行一段时间,等待事件发生
            Thread.sleep(10000);  // 示例中运行10秒
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 关闭资源
            try {
                childrenCache.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
            client.close();
        }
    }
}

5.6. TreeCache

TreeCache 是一个更高级的工具,可以监听一个节点及其所有子节点(递归)的变化。它结合了 NodeCache 和 PathChildrenCache 的功能,是一个强大的工具用于监控整个子树的变化。

在 TreeCache 的例子中,监听事件包括了节点的添加、更新和删除,这些事件既可能来自于子节点,也可能来自于自身节点。事实上,TreeCache 是用来监听整个树结构的变化,包括目标节点及其所有子节点的变化。

设是对路径 /example/path 进行监听,示例中可以区分是自身节点还是子节点:

import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.apache.curator.framework.recipes.cache.TreeCache;
import org.apache.curator.framework.recipes.cache.TreeCacheEvent;
import org.apache.curator.framework.recipes.cache.TreeCacheListener;

public class TreeCacheExample {

    public static void main(String[] args) {
        String zookeeperConnectionString = "localhost:2181";
        CuratorFramework client = CuratorFrameworkFactory.builder()
                .connectString(zookeeperConnectionString)
                .retryPolicy(new ExponentialBackoffRetry(1000, 3))
                .build();
        
        client.start();
        
        TreeCache treeCache = new TreeCache(client, "/example/path");
        
        TreeCacheListener listener = new TreeCacheListener() {
            @Override
            public void childEvent(CuratorFramework client, TreeCacheEvent event) {
                String path = event.getData().getPath();
                if ("/example/path".equals(path)) {
                    System.out.println("Event for the root node:");
                } else {
                    System.out.println("Event for a child node:");
                }
                switch (event.getType()) {
                    case NODE_ADDED:
                        System.out.println("Node added: " + path);
                        break;
                    case NODE_UPDATED:
                        System.out.println("Node updated: " + path);
                        break;
                    case NODE_REMOVED:
                        System.out.println("Node removed: " + path);
                        break;
                    default:
                        break;
                }
            }
        };
        
        treeCache.getListenable().addListener(listener);
        
        try {
            treeCache.start();
            // 运行一段时间,等待事件发生
            Thread.sleep(10000);  // 示例中运行10秒
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 关闭资源
            try {
                treeCache.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
            client.close();
        }
    }
}

KerryWu
641 声望159 粉丝

保持饥饿


引用和评论

0 条评论