13.1 ZooKeeper伪集群安装和配置

安装目录:D:\develop\zookeeper-3.4.13

13.1.1 创建数据目录和日志目录

ZooKeeper节点数有以下要求:

  1. ZooKeeper集群节点数必须是基数
  2. ZooKeeper集群节点数必须是基数

image.png
image.png

13.1.2 创建myid文本文件

myid文件的特点如下:

  1. myid文件的唯一作用,是存放(伪)节点的编号;
  2. myid文件是一个文本文件,文件名称为myid;
  3. myid文件内容为一个数字,表示节点的编号;
  4. myid文件中,只能有一个数字,不能有其他的内容;
  5. myid文件的存放位置,默认处于数据目录下面

    13.1.3 创建和修改配置文件

    13.1.4 配置文件示例

    13.1.5 启动ZooKeeper伪集群

    image.png
    启动成功

    13.2 使用ZooKeeper进行分布式存储

    13.1.1 详解:Zookeeper存储模型

    ZooKeeper的存储模型是一棵以 “/“ 为根节点的树, 存储模型中的每一个节点,叫做ZNode(ZooKeeper node)节点。所有的ZNode节点,通过树的目录结构按照层次关系组织在一起,构成一棵ZNode树。

    ZooKeeper为了保证高吞吐和低延迟,整个树状的目录结构全部都放在内存中。与硬盘和其他的外存设备相比,机器的内存比较有限,使得ZooKeeper的目录结构,不能用于存放大量的数据。 ZooKeeper官方的要求是,每个节点存放的Payload负载数据的上限,仅仅为1M

    13.2.2 zkCli客户端指令清单

    image.png
    使用stat指令可以查看ZNode树的根节点“/”的状态信息
    image.png
    一个Znode的建立或者更新, 都会产生一个新的Zxid值,所以在节点信息中,保存了3个Zxid事务ID值,分别是:

  6. cZxid: Znode节点创建时的事务ID (Transaction id);

  7. mZxid: Znode节点修改时的事务ID,与子节点无关;
  8. pZxid: Znode节点的子节点的最后一次创建或者修改时间,与孙子节点无关

stat指令所返回的节点信息,包含的时间戳有两个:

  1. ctime: Znode节点创建时的时间戳;
  2. mtime: Znode节点最新一次更新发生时的时间戳

stat指令所返回的节点信息,包含的版本号有三个:

  1. dataversion: 数据版本号;
  2. cversion:子节点版本号;
  3. aclversion:节点的ACL权限修改版本号

13.3 实战: ZooKeeper应用开发

ZooKeeper应用开发,主要通过Java客户端API去连接和操作ZooKeeper集群。可以供选择的Java客户端API 有:

  1. ZooKeeper官方的Java客户端API
  2. 第三方的Java客户端API

    13.3.1 ZkClient开源客户端介绍

    ZkClient是一个开源客户端,在ZooKeeper原生API接口的基础上进行了包装,更便于开发人员使用。 ZkClient客户端,在一些著名的互联网开源项目中,到了应用,比如:阿里的分布式Dubbo框架,对它进行了集成使用

    ZkClient也有它自身的不少不足之处,具体如下:

  3. ZkClient社区不活跃,文档不够完善,几乎没有参考文档

  4. 异常处理简化(抛出RuntimeException);
  5. 重试机制比较难用
  6. 没有提供各种使用场景的参考实现

    13.3.2 Curator开源客户端介绍

    Curator是Netflix公司开源的一套ZooKeeper客户端框架,和ZkClient一样, Curator提供了非常底层的细节开发工作,包括Session会话超时重连、 掉线重连、 反复注册Watcher和NodeExistsException异常等

    13.3.3 Curator开发之环境准备

  7. curator-framework:对ZooKeeper的底层API的一些封装;

  8. curator-client:提供一些客户端的操作,例如重试策略等;
  9. curator-recipes:封装了一些高级特性,如: Cache事件监听、选举、分布式锁、分布式计数器、分布式Barrier等。 ```xml org.apache.curator curator-client 4.0.0 org.apache.ZooKeeper ZooKeeper
org.apache.curator curator-framework 4.0.0 org.apache.zookeeper zookeeper

org.apache.curator curator-recipes 4.0.0

  1. <a name="XIGIL"></a>
  2. ## 13.3.4 实战Curator:客户端实例创建
  3. 1. 使用工厂类`CuratorFrameworkFactory`的静态`newClient(…)`方法;
  4. 1. 使用工厂类`CuratorFrameworkFactory`的静态`builder `构造者方法。
  5. ```java
  6. public static CuratorFramework createSimple(String connectionString) {
  7. long stime = System.currentTimeMillis();
  8. // 重试策略:第一次重试等待1s,第二次重试等待2s,第三次重试等待4s
  9. // 第一个参数:等待时间的基础单位,单位为毫秒
  10. // 第二个参数:最大重试次数
  11. ExponentialBackoffRetry retryPolicy =
  12. new ExponentialBackoffRetry(1000, 3);
  13. // 获取 CuratorFramework 实例的最简单的方式
  14. // 第一个参数:zk的连接地址
  15. // 第二个参数:重试策略
  16. CuratorFramework client = CuratorFrameworkFactory.newClient(connectionString, retryPolicy);
  17. Logger.info("创建连接耗费时间ms:"+ (System.currentTimeMillis()-stime));
  18. return client;
  19. }
  20. /**方式二
  21. * @param connectionString zk的连接地址
  22. * @param retryPolicy 重试策略
  23. * @param connectionTimeoutMs 连接超时时间
  24. * @param sessionTimeoutMs 会话超时时间
  25. * @return CuratorFramework 实例
  26. */
  27. public static CuratorFramework createWithOptions(
  28. String connectionString, RetryPolicy retryPolicy,
  29. int connectionTimeoutMs, int sessionTimeoutMs) {
  30. // builder 模式创建 CuratorFramework 实例
  31. return CuratorFrameworkFactory.builder()
  32. .connectString(connectionString)
  33. .retryPolicy(retryPolicy)
  34. .connectionTimeoutMs(connectionTimeoutMs)
  35. .sessionTimeoutMs(sessionTimeoutMs)
  36. // 其他的创建选项
  37. .build();
  38. }

13.3.5 实战Curator:节点创建

  1. @Test
  2. public void createNode() {
  3. //客户端实例
  4. CuratorFramework client = ClientFactory.createSimple("127.0.0.1");
  5. try {
  6. // 启动客户端实例,连接服务器
  7. client.start();
  8. // 创建一个ZNode结点
  9. // 节点数据为payload
  10. String data = "hello";
  11. byte[] payload = data.getBytes(StandardCharsets.UTF_8);
  12. String zkPath = "/test/CRUD/node-1";
  13. client.create()
  14. .creatingParentContainersIfNeeded()
  15. .withMode(CreateMode.PERSISTENT)
  16. .forPath(zkPath, payload);
  17. } catch (Exception e) {
  18. e.printStackTrace();
  19. }finally {
  20. CloseableUtils.closeQuietly(client);
  21. }
  22. }

ZooKeeper节点有四种类型 :

  1. PERSISTENT 持久化节点
  2. PERSISTENT_SEQUENTIAL 持久化顺序节点
  3. PHEMERAL 临时节
  4. EPHEMERAL_SEQUENTIAL 临时顺序节点

    13.3.6 实战Curator:读取节点

    在Curator 框架,与节点读取的有关的方法,主要有三个:
    (1)首先是判断节点是否存在,使用checkExists方法。
    (2)其次是获取节点的数据,使用getData方法。
    (3)最后是获取子节点列表,使用getChildren方法。

    1. /**
    2. * 读取节点
    3. */
    4. @Test
    5. public void readNode() {
    6. // 创建客户端
    7. CuratorFramework client = ClientFactory.createSimple("127.0.0.1:2181");
    8. try {
    9. client.start();
    10. String zkPath = "/test/CRUD/node-1";
    11. Stat stat = client.checkExists().forPath(zkPath);
    12. if (null != stat){
    13. byte[] payload = client.getData().forPath(zkPath);
    14. String data = new String(payload, "UTF-8");
    15. log.info("read data:{}", data);
    16. String parentPath = "/test";
    17. List<String> children = client.getChildren().forPath(parentPath);
    18. for (String child : children) {
    19. log.info("child:{}", child);
    20. }
    21. }
    22. } catch (Exception e) {
    23. e.printStackTrace();
    24. }finally {
    25. CloseableUtils.closeQuietly(client);
    26. }
    27. }

    13.3.7 实战Curator:更新节点

    使用setData() 方法,进行同步更新

    1. @Test
    2. public void updateNode() {
    3. CuratorFramework client = ClientFactory.createSimple("127.0.0.1:2181");
    4. try {
    5. client.start();
    6. String data = "hello world";
    7. byte[] payload = data.getBytes(StandardCharsets.UTF_8);
    8. String zkPath = "/test/CRUD/node-1";
    9. client.setData()
    10. .forPath(zkPath, payload);
    11. } catch (Exception e) {
    12. e.printStackTrace();
    13. }finally {
    14. CloseableUtils.closeQuietly(client);
    15. }
    16. }

    如果需要进行异步更新,如何处理呢?其实很简单: 通过SetDataBuilder 构造者实例的 inBackground(AsyncCallback callback)方法,设置一个AsyncCallback回调实例。

    1. @Test
    2. public void updateNodeAsync() {
    3. CuratorFramework client = ClientFactory.createSimple("127.0.0.1:2181");
    4. try {
    5. //更新完成监听器
    6. AsyncCallback.StringCallback callback = new AsyncCallback.StringCallback() {
    7. @Override
    8. public void processResult(int i, String s, Object o, String s1) {
    9. System.out.println(
    10. "i = " + i + " | " +
    11. "s = " + s + " | " +
    12. "o = " + o + " | " +
    13. "s1 = " + s1
    14. );
    15. }
    16. };
    17. client.start();
    18. String data = "hello, every body!";
    19. byte[] payload = data.getBytes(StandardCharsets.UTF_8);
    20. String zkPath = "/test/CRUD/node-1";
    21. client.setData()
    22. .inBackground(callback) // 设置回调实例
    23. .forPath(zkPath, payload);
    24. } catch (Exception e) {
    25. e.printStackTrace();
    26. }finally {
    27. CloseableUtils.closeQuietly(client);
    28. }
    29. }

    13.3.8 实战Curator:删除结点

    1. @Test
    2. public void deleteNode() {
    3. CuratorFramework client = ClientFactory.createSimple("127.0.0.1:2181");
    4. try {
    5. client.start();
    6. String zkPath = "/test/CRUD/node-1";
    7. client.delete().forPath(zkPath);
    8. // 删除后查看结果
    9. String parentPath = "/test";
    10. List<String> children = client.getChildren().forPath(parentPath);
    11. for (String child : children) {
    12. log.info("child:{}", child);
    13. }
    14. } catch (Exception e) {
    15. e.printStackTrace();
    16. }finally {
    17. CloseableUtils.closeQuietly(client);
    18. }
    19. }

    删除和更新操作一样,也可以异步进行

    13.4 实战:分布式命名服务

    命名服务,也就是提供系统中资源的标识能力。 ZooKeeper的命名服务,主要是利用ZooKeeper节点的树型分层结构和子节点的次序维护能力,为分布式系统中的资源命名。
    典型的分布式命名服务场景有

  5. 分布式API目录

  6. 分布式的ID生成器
  7. 分布式节点的命名

    13.4.1 ID生成器

    传统的数据库自增主键,或者单体Java应用的自增主键,已经不能满足分布式ID生成器的需求。在分布式系统环境中,迫切需要一种全新的唯一ID系统,这种系统需要满足以下需求:

  8. 全局唯一:不能出现重复ID

  9. 高可用: ID生成系统是非常基础系统,被许多关键系统调用,一旦宕机,会造成严重影响。


分布式的ID生成器方案

  1. Java 的 UUID ;
  2. 分布式缓存Redis生成ID, 利用Redis的原子操作INCR和INCRBY,生成全局唯一的ID ;
  3. Twitter的Snowflake算法;
  4. ZooKeeper生成ID, 利用ZooKeeper 的顺序节点,生成全局唯一的ID;
  5. MongoDb的ObjectId, MongoDB是一个分布式的非结构化NoSql数据库,每插入一条记录,会自动生成的全局唯一的“_id” 字段值, 该值是一个12字节的字符串,可以作为分布式系统中全局唯一的ID。

UUID的优点:本地生成ID,不需要进行远程调用,时延低,性能高。
UUID的缺点: UUID过长, 16字节128位,通常以36长度的字符串表示,很多场景不适用,比如,由于UUID没有排序,无法保证趋势递增,用做数据库索引字段的效率就很低,新增记录存储入库时性能差。

13.4.2 实战:ZooKeeper分布式ID生成器

ZK的四种节点中,其中以下两种节点具备自动编号的能力:

  1. PERSISTENT_SEQUENTIAL 持久化顺序节点
  2. EPHEMERAL_SEQUENTIAL 临时顺序节点

通过创建ZK临时顺序节点的方法,生成全局唯一ID的演示代码,大致如下:

  1. public class IDMaker {
  2. private static final String ZK_ADDRESS = "127.0.0.1:2181";
  3. CuratorFramework client = null;
  4. private String createSeqNode(String pathPrefix) {
  5. try {
  6. // 创建一个 ZNode 顺序节点
  7. // 为了避免 zookeeper 的顺序节点暴增,建议创建后, 直接删除创建的节点
  8. String destPath = client.create()
  9. .creatingParentContainersIfNeeded()
  10. .withMode(CreateMode.EPHEMERAL_SEQUENTIAL)
  11. .forPath(pathPrefix);
  12. return destPath;
  13. } catch (Exception e) {
  14. e.printStackTrace();
  15. }
  16. return null;
  17. }
  18. //获取ID值
  19. public String makeId(String nodeName) {
  20. String str = createSeqNode(nodeName);
  21. if (null == str) {
  22. return null;
  23. }
  24. // 取得zk节点的末尾序号
  25. int index = str.lastIndexOf(nodeName);
  26. if (index >= 0) {
  27. index += nodeName.length();
  28. return index <= str.length() ? str.substring(index) : "";
  29. }
  30. return null;
  31. }
  32. }

测试用例:

  1. @Slf4j
  2. public class IDMakerTester {
  3. @Test
  4. public void testMakeId() {
  5. IDMaker idMaker = new IDMaker();
  6. idMaker.init();
  7. String nodeName = "/test/IDMaker/ID-";
  8. for (int i = 0; i < 10; i++) {
  9. String id = idMaker.makeId(nodeName);
  10. log.info("第"+ i + "个创建的 id 为:" + id);
  11. }
  12. idMaker.destroy();
  13. }
  14. }

image.png

13.4.3 实战:集群节点的命名服务

有以下两个方案,可供生成集群节点编号:

  1. 使用数据库的自增ID特性,用数据表,存储机器的MAC地址或者IP来维护
  2. 使用ZooKeeper持久顺序节点的次序特性,来维护节点的NodeId编号。

这里使用第二种:

  1. public class SnowflakeIdWorker {
  2. transient private CuratorFramework zkClient = null;
  3. //工作节点的路径
  4. private String pathPrefix = "/test/IDMaker/worker-";
  5. private String pathRegistered = null;
  6. public static SnowflakeIdWorker instance = new SnowflakeIdWorker();
  7. public SnowflakeIdWorker() {
  8. this.zkClient = ZKclient.instance.getClient();
  9. this.init();
  10. }
  11. // 在zookeeper中创建临时节点并写入信息
  12. public void init() {
  13. // 创建一个 ZNode 节点
  14. // 节点的 payload 为当前worker 实例
  15. try {
  16. byte[] payload = pathPrefix.getBytes(StandardCharsets.UTF_8);
  17. pathRegistered = zkClient.create()
  18. .creatingParentsIfNeeded()
  19. .withMode(CreateMode.EPHEMERAL_SEQUENTIAL)
  20. .forPath(pathPrefix, payload);
  21. } catch (Exception e) {
  22. e.printStackTrace();
  23. }
  24. }
  25. public long getId() {
  26. String sid = null;
  27. if (null == pathRegistered) {
  28. throw new RuntimeException("节点注册失败");
  29. }
  30. int index = pathRegistered.lastIndexOf(pathPrefix);
  31. if (index >= 0) {
  32. index += pathPrefix.length();
  33. sid = index <= pathRegistered.length() ? pathRegistered.substring(index) : null;
  34. }
  35. if (null == sid) {
  36. throw new RuntimeException("节点ID生成失败");
  37. }
  38. return Long.parseLong(sid);
  39. }
  40. }

13.4.4 结合ZK实现SnowFlake ID算法

  1. SnowFlake ID 的组成

SnowFlake算法所生成的ID是一个64bit的长整形数字
image.png

  1. SnowFlake ID 的实现

    1. public class SnowflakeIdGenerator {
    2. public static SnowflakeIdGenerator instance = new SnowflakeIdGenerator();
    3. public synchronized void init(long workerId) {
    4. if (workerId > MAX_WORKER_ID) {
    5. // zk分配的workerId过大
    6. throw new IllegalArgumentException("woker Id wrong: " + workerId);
    7. }
    8. instance.workerId = workerId;
    9. }
    10. private SnowflakeIdGenerator() {
    11. }
    12. /**
    13. * 开始使用该算法的时间为: 2017-01-01 00:00:00
    14. */
    15. private static final long START_TIME = 1483200000000L;
    16. /**
    17. * worker id 的bit数,最多支持8192个节点
    18. */
    19. private static final int WORKER_ID_BITS = 13;
    20. /**
    21. * 序列号,支持单节点最高每毫秒的最大ID数1024
    22. */
    23. private final static int SEQUENCE_BITS = 10;
    24. /**
    25. * 最大的 worker id ,8091
    26. * -1 的补码(二进制全1)右移13位, 然后取反
    27. */
    28. private final static long MAX_WORKER_ID = ~(-1L << WORKER_ID_BITS);
    29. /**
    30. * 最大的序列号,1023
    31. * -1 的补码(二进制全1)右移10位, 然后取反
    32. */
    33. private final static long MAX_SEQUENCE = ~(-1L << SEQUENCE_BITS);
    34. /**
    35. * worker 节点编号的移位
    36. */
    37. private final static long WORKER_ID_SHIFT = SEQUENCE_BITS;
    38. /**
    39. * 时间戳的移位
    40. */
    41. private final static long TIMESTAMP_LEFT_SHIFT = WORKER_ID_BITS + SEQUENCE_BITS;
    42. /**
    43. * 该项目的worker 节点 id
    44. */
    45. private long workerId;
    46. /**
    47. * 上次生成ID的时间戳
    48. */
    49. private long lastTimestamp = -1L;
    50. /**
    51. * 当前毫秒生成的序列
    52. */
    53. private long sequence = 0L;
    54. /**
    55. * Next id long.
    56. *
    57. * @return the nextId
    58. */
    59. public Long nextId() {
    60. return generateId();
    61. }
    62. private synchronized long generateId() {
    63. long current = System.currentTimeMillis();
    64. if (current < lastTimestamp) {
    65. // 如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过,出现问题返回-1
    66. return -1;
    67. }
    68. if (current == lastTimestamp) {
    69. // 如果当前生成id的时间还是上次的时间,那么对sequence序列号进行+1
    70. sequence = (sequence + 1) & MAX_SEQUENCE;
    71. if (sequence == MAX_SEQUENCE) {
    72. // 当前毫秒生成的序列数已经大于最大值,那么阻塞到下一个毫秒再获取新的时间戳
    73. current = this.nextMs(lastTimestamp);
    74. }
    75. }else {
    76. // 当前的时间戳已经是下一个毫秒
    77. sequence = 0L;
    78. }
    79. // 更新上次生成id的时间戳
    80. lastTimestamp = current;
    81. // 进行移位操作生成int64的唯一ID
    82. //时间戳右移动23位
    83. long time = (current - START_TIME) << TIMESTAMP_LEFT_SHIFT;
    84. //workerId 右移动10位
    85. long workerId = this.workerId << WORKER_ID_SHIFT;
    86. return time | workerId | sequence;
    87. }
    88. private long nextMs(long timeStamp) {
    89. long current = System.currentTimeMillis();
    90. while (current <= timeStamp) {
    91. current = System.currentTimeMillis();
    92. }
    93. return current;
    94. }
    95. }

    测试: ```java @Slf4j public class SnowflakeIdTest { @Test public void snowflakeIdTest() {

    1. long workId = SnowflakeIdWorker.instance.getId();
    2. SnowflakeIdGenerator.instance.init(workId);
    3. ExecutorService es = Executors.newFixedThreadPool(10);
    4. final HashSet idSet = new HashSet();
    5. Collections.synchronizedCollection(idSet);
    6. long start = System.currentTimeMillis();
    7. log.info("开始生产 *");
    8. for (int i = 0; i < 10; i++) {
    9. es.execute(() -> {
    10. for (int j = 0; j < 5000; j++) {
    11. long id = SnowflakeIdGenerator.instance.nextId();
    12. synchronized (idSet){
    13. idSet.add(id);
    14. }
    15. }
    16. });
    17. }
    18. es.shutdown();
    19. try {
    20. es.awaitTermination(10, TimeUnit.SECONDS);
    21. } catch (InterruptedException e) {
    22. e.printStackTrace();
    23. }
    24. long end = System.currentTimeMillis();
    25. log.info("生产id结束");
    26. log.info("* 耗费: " + (end - start) + " ms!");

    } }

  1. ![image.png](https://cdn.nlark.com/yuque/0/2022/png/27178439/1656899734852-a43910ed-1b24-4ef0-855a-338c0a515a14.png#clientId=uabe10458-5f87-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=68&id=u6221d641&margin=%5Bobject%20Object%5D&name=image.png&originHeight=68&originWidth=694&originalType=binary&ratio=1&rotation=0&showTitle=false&size=18183&status=done&style=none&taskId=u6dc3b5d2-b082-4f2d-8d02-144f95f9b18&title=&width=694)<br /> SnowFlake算法的优点:<br />(1)生成ID时不依赖于数据库,完全在内存生成,高性能高可用。<br />(2)容量大,每秒可生成几百万ID。<br />(3) ID呈趋势递增,后续插入数据库的索引树的时候,性能较高。 <br /> SnowFlake算法的缺点:<br />(1)依赖于系统时钟的一致性,如果某台机器的系统时钟回拨,有可能造成ID冲突,或者ID乱序;<br />(2)在启动之前,如果这台机器的系统时间回拨过,那么有可能出现ID重复的危险
  2. <a name="k4Idn"></a>
  3. # 13.5 重点:分布式事件监听
  4. 事件监听有两种模式:<br />(1)一种是标准的观察者模式;<br />(2)一种是缓存监听模式。 <br /> 第一种标准的观察者模式,是通过Watcher监听器去实现;第二种缓存监听模式,通过引入了一种本地缓存视图Cache机制去实现。 第二种Cache事件监听机制,可以理解为一个本地缓存视图与远程ZooKeeper视图的对比过程,简单来说, Cache在客户端缓存了Znode的各种状态,当感知到ZooKeeper集群Znode状态变化,会触发event事件, 注册在这些事件上的监听器会处理这些事件。
  5. <a name="nj74O"></a>
  6. ## 13.5.1 Watcher标准的事件处理器
  7. ZooKeeper中,接口类型Watcher用于表示一个标准的事件处理器,用来定义收到事件通知后相关的回调处理逻辑。 接口类型Watcher包含KeeperStateEventType两个内部枚举类,分别代表了通知状态和事件类型。 <br /> 定义回调处理逻辑,需要使用Watcher接口的事件回调方法:<br />`process(WatchedEvent event)`
  8. 一个Watcher监听器在向服务端完成注册后,当服务端的一些事件触发了这个Watcher,那么就会向注册过的客户端会话发送一个事件通知,来实现分布式的通知功能。在Curator客户端收到服务器的通知后,会封装一个WatchedEvent 事件实例,传递给监听器的processWatchedEvent)回调方法 <br /> <br /> WatchedEvent包含了三个基本属性:<br />(1)通知状态(keeperState)<br />(2)事件类型(EventType)<br />(3)节点路径(path
  9. 1. Watcher 接口定义的通知状态和事件类型
  10. 1. Watcher 使用实战
  11. ```java
  12. @Slf4j
  13. public class ZKWatcherDemo {
  14. private String workerPath = "/test/listener/node";
  15. private String subWorkerPath = "/test/listener/node/id-";
  16. //利用watcher来对节点进行监听操作
  17. @Test
  18. public void testWatcher() {
  19. CuratorFramework client = ZKclient.instance.getClient();
  20. // 检查节点是否存在,没有则创建
  21. boolean isExist = ZKclient.instance.isNodeExist(workerPath);
  22. if (!isExist) {
  23. ZKclient.instance.createNode(workerPath, null);
  24. }
  25. try {
  26. Watcher w = new Watcher() {
  27. @Override
  28. public void process(WatchedEvent watchedEvent) {
  29. System.out.println("监听到的变化 watchedEvent = " + watchedEvent);
  30. }
  31. };
  32. byte[] content = client.getData().usingWatcher(w).forPath(workerPath);
  33. log.info("监听节点内容: " + new String(content));
  34. // 第一次变更节点数据
  35. client.setData().forPath(workerPath, "第 1 次更改内容".getBytes());
  36. // 第二次变更节点数据
  37. client.setData().forPath(workerPath, "第 2 次更改内容".getBytes());
  38. Thread.sleep(Integer.MAX_VALUE);
  39. } catch (Exception e) {
  40. e.printStackTrace();
  41. }
  42. }
  43. }


既然Watcher监听器是一次性的,如果要反复使用,怎么办呢? 需要反复的通过构造者的usingWatcher方法,去提前进行注册。所以, Watcher监听器不适用于节点的数据频繁变动或者节点频繁变动这样的业务场景,而是适用于一些特殊的、变动不频繁的场景,比 如会话超时、授权失败等这样的特殊场景。既然Watcher需要反复注册,比较繁琐,所以, Curator引入了Cache来监听ZooKeeper服务端的事件。 Cache对ZooKeeper事件监听进行了封装,能够自动处理反复注册监听。

13.5.2 NodeCache 节点缓存的监听

Curator引入的Cache缓存实现, Cache缓存拥有一个系列的类型,包括了Node Cache 、Path Cache、 Tree Cache三组类。
(1)Node Cache节点缓存可以用于ZNode节点的监听;
(2)Path Cache子节点缓存用于ZNode的子节点的监听;
(3) Tree Cache树缓存是Path Cache的增强, 不仅仅能监听子节点,也能监听ZNode节点自身

Node Cache 事件监听的实战案例

  1. @Test
  2. public void testNodeCache() {
  3. boolean isExist = ZKclient.instance.isNodeExist(workerPath);
  4. if (!isExist) {
  5. ZKclient.instance.createNode(workerPath, null);
  6. }
  7. CuratorFramework client = ZKclient.instance.getClient();
  8. try {
  9. NodeCache nodeCache = new NodeCache(client, workerPath, false);
  10. NodeCacheListener listener = new NodeCacheListener() {
  11. @Override
  12. public void nodeChanged() throws Exception {
  13. ChildData childData = nodeCache.getCurrentData();
  14. log.info("ZNode节点状态改变, path={}", childData.getPath());
  15. log.info("ZNode节点状态改变, data={}", new String(childData.getData(), "UTF-8"));
  16. log.info("ZNode节点状态改变, stat={}", childData.getStat());
  17. }
  18. };
  19. //启动节点事件监听
  20. nodeCache.getListenable().addListener(listener);
  21. nodeCache.start();
  22. //第一次变更节点数据
  23. client.setData().forPath(workerPath, "第一次更改内容".getBytes(StandardCharsets.UTF_8));
  24. Thread.sleep(1000);
  25. //第二次变更节点数据
  26. client.setData().forPath(workerPath, "第二次更改内容".getBytes(StandardCharsets.UTF_8));
  27. Thread.sleep(1000);
  28. // 第 3 次变更节点数据
  29. client.setData().forPath(workerPath, "第三次更改内容".getBytes(StandardCharsets.UTF_8));
  30. Thread.sleep(1000);
  31. } catch (Exception e) {
  32. // e.printStackTrace();
  33. log.error("创建 NodeCache 监听失败, path={}", workerPath);
  34. }
  35. }

image.png

13.5.3 Path Cache 子节点监听

(1)只能监听子节点,监听不到当前节点
(2)不能递归监听,子节点下的子节点不能递归监控

启动节点的事件监听start方法,可以传入启动模式作为参数, 启动模式定义在StartMode枚举中,具体如下
( 1) BUILD_INITIAL_CACHE模式:启动时同步初始化Cache,表示创建Cache后,就从服务器拉取对应的数据;
( 2) POST_INITIALIZED_EVENT模式:启动时异步初始化Cache,表示创建Cache后,从服务器拉取对应的数据,完成后 PathChildrenCacheEvent.Type#INITIALIZED事件, Cache中Listener会收到该事件的通知;
( 3) NORMAL模式:启动时,异步初始化cache,完成后不会发出通知。

  1. @Test
  2. public void testPathChildrenCache() {
  3. //检查节点是否存在,没有则创建
  4. boolean isExist = ZKclient.instance.isNodeExist(workerPath);
  5. if (!isExist) {
  6. ZKclient.instance.createNode(workerPath, null);
  7. }
  8. CuratorFramework client = ZKclient.instance.getClient();
  9. try {
  10. PathChildrenCache cache = new PathChildrenCache(client, workerPath, true);
  11. PathChildrenCacheListener listener = new PathChildrenCacheListener() {
  12. @Override
  13. public void childEvent(CuratorFramework client, PathChildrenCacheEvent event) throws Exception {
  14. try {
  15. ChildData data = event.getData();
  16. switch (event.getType()) {
  17. case CHILD_ADDED:
  18. log.info("子节点增加, path={}, data={}", data.getPath(), new String(data.getData(), "UTF-8"));
  19. break;
  20. case CHILD_UPDATED:
  21. log.info("子节点更新, path={}, data={}", data.getPath(), new String(data.getData(), "UTF-8"));
  22. break;
  23. case CHILD_REMOVED:
  24. log.info("子节点删除, path={}, data={}", data.getPath(), new String(data.getData(), "UTF-8"));
  25. break;
  26. default:
  27. break;
  28. }
  29. }catch (Exception e) {
  30. e.printStackTrace();
  31. }
  32. }
  33. };
  34. cache.getListenable().addListener(listener);
  35. cache.start(PathChildrenCache.StartMode.BUILD_INITIAL_CACHE);
  36. Thread.sleep(1000);
  37. for (int i = 0; i < 3; i++) {
  38. ZKclient.instance.createNode(subWorkerPath + i, null);
  39. }
  40. Thread.sleep(1000);
  41. for (int i = 0; i < 3; i++) {
  42. ZKclient.instance.deleteNode(subWorkerPath + i);
  43. }
  44. } catch (Exception e) {
  45. log.error("PathCache监听失败, path=", workerPath);
  46. }
  47. }

13.5.4 Tree Cache 节点树缓存

Tree Cache可以看做是Node Cache、 Path Cache的合体; Tree Cache不光能监听子节点,也能监听节点自身。

  1. @Test
  2. public void testTreeCache() {
  3. //检查节点是否存在,没有则创建
  4. boolean isExist = ZKclient.instance.isNodeExist(workerPath);
  5. if (!isExist) {
  6. ZKclient.instance.createNode(workerPath, null);
  7. }
  8. CuratorFramework client = ZKclient.instance.getClient();
  9. try {
  10. TreeCache treeCache = new TreeCache(client, workerPath);
  11. TreeCacheListener listener = new TreeCacheListener() {
  12. @Override
  13. public void childEvent(CuratorFramework client, TreeCacheEvent event) {
  14. try {
  15. ChildData data = event.getData();
  16. if (data == null) {
  17. log.info("数据为空");
  18. return;
  19. }
  20. switch (event.getType()) {
  21. case NODE_ADDED:
  22. log.info("[TreeCache]节点增加, path={}, data={}",
  23. data.getPath(), new String(data.getData(), "UTF-8"));
  24. break;
  25. case NODE_UPDATED:
  26. log.info("[TreeCache]节点更新, path={}, data={}",
  27. data.getPath(), new String(data.getData(), "UTF-8"));
  28. break;
  29. case NODE_REMOVED:
  30. log.info("[TreeCache]节点删除, path={}, data={}",
  31. data.getPath(), new String(data.getData(), "UTF-8"));
  32. break;
  33. default:
  34. break;
  35. }
  36. } catch (Exception e) {
  37. e.printStackTrace();
  38. }
  39. }
  40. };
  41. //设置监听器
  42. treeCache.getListenable().addListener(listener);
  43. //启动缓存视图
  44. treeCache.start();
  45. Thread.sleep(1000);
  46. //创建 3 个子节点
  47. for (int i = 0; i < 3; i++) {
  48. ZKclient.instance.createNode(subWorkerPath + i, null);
  49. }
  50. Thread.sleep(1000);
  51. //删除 3 个子节点
  52. for (int i = 0; i < 3; i++) {
  53. ZKclient.instance.deleteNode(subWorkerPath + i);
  54. }
  55. Thread.sleep(1000);
  56. //删除当前节点
  57. ZKclient.instance.deleteNode(workerPath);
  58. Thread.sleep(Integer.MAX_VALUE);
  59. } catch (Exception e) {
  60. log.error("PathCache 监听失败, path=", workerPath);
  61. }
  62. }

TreeCacheEvent的事件类型,具体为:
(1) NODE_ADDED 对应于节点的增加;
(2) NODE_UPDATED 对应于节点的修改;
(3) NODE_REMOVED 对应于节点的删除。

Curator 事件监听的原理
Curator事件监听的原理:无论是PathChildrenCache,还是TreeCache,所谓的监听,都是进行Curator本地缓存视图和ZooKeeper服务器远程的数据节点的对比,并且进行数据同步时,会触发相应的事件。 以NODE_ADDED(节点新增事件)的触发为例, 进行简单说明。在本地缓存视图开始的创建的时候,本地视图为空,从服务器进行数据同步的时,本地的监听器就能监听到NODE_ADDED事件。为什么呢? 刚开始本地缓存并没有内容,然后本地缓存和服务器缓存进行对比,发现ZooKeeper服务器是有节点数据的,这才将服务器的节点缓存到本地,也会触发本地缓存的NODE_ADDED事件。

13.6 分布式锁原理与实战

13.6.1 公平锁和可重入锁的原理

13.6.2 ZooKeeper分布式锁的原理

13.6.3 分布式锁的基本流程

  1. public interface Lock {
  2. boolean lock();
  3. boolean unlock();
  4. }

使用ZooKeeper实现分布式锁的算法, 有以下几个要点:
(1)一把分布式锁通常使用一个Znode节点表示;如果锁对应的Znode节点不存在,首先创建Znode节点。这里假设为“/test/lock”,代表了一把需要创建的分布式锁。
(2)抢占锁的所有客户端,使用锁的Znode节点的子节点列表来表示;如果某个客户端需要占用锁,则在“/test/lock”下创建一个临时有序的子节点。
(3)如果判定客户端是否占有锁呢? 很简单,客户端创建子节点后,需要进行判断:自己创建的子节点,是否为当前子节点列表中序号最小的子节点。如果是,则认为加锁成功;如果不是,则监听前一个Znode子节点变更消息,等待前一个节点释放锁。
(4)一旦队列中的后面的节点,获得前一个子节点变更通知,则开始进行判断,判断自己是否为当前子节点列表中序号最小的子节点,如果是,则认为加锁成功;如果不是,则持续监听,一直到获得锁。
(5)获取锁后,开始处理业务流程。完成业务流程后,删除自己的对应的子节点,完成释放锁的工作,以方面后继节点能捕获到节点变更通知,获得分布式锁。

13.6.4 实战:加锁的实现

lock()方法的大致流程是:首先尝试着去加锁,如果加锁失败就去等待,然后再重复

  1. lock() 方法的实现代码 ```java @Override public boolean lock() {

    1. //可重入,确保同一线程,可以重复加锁
    2. synchronized (this) {
    3. if (lockCount.get() == 0) {
    4. thread = Thread.currentThread();
    5. lockCount.incrementAndGet();
    6. }else {
    7. if (!thread.equals(Thread.currentThread())){
    8. return false;
    9. }
    10. lockCount.incrementAndGet();
    11. return true;
    12. }
    13. }
    14. try {
    15. boolean locked = false;
    16. //首先尝试着 去加锁
    17. locked = tryLock();
    18. if (locked) {
    19. return true;
    20. }
    21. // 如果加锁失败就去等待
    22. while (!locked) {
    23. await();
    24. //获取等待的子节点列表
    25. List<String> waiters = getWaiters();
    26. // 判断是否加锁成功
    27. if (checkLocked(waiters)) {
    28. locked = true;
    29. }
    30. }
    31. return true;
    32. }catch (Exception e) {
    33. e.printStackTrace();
    34. unlock();
    35. }
    36. return false;

    }

  1. 2. tryLock()尝试加锁
  2. - 创建临时顺序节点,并且保存自己的节点路径
  3. - 判断是否是第一个,如果是第一个,则加锁成功。如果不是,就找到前一个Znode节点,并且保存其路径到prior_path
  4. ```java
  5. private boolean tryLock() throws Exception {
  6. //创建临时Znode
  7. locked_path = ZKclient.instance.createEphemeralSeqNode(LOCK_PREFIX);
  8. if (null == locked_path) {
  9. throw new Exception("zk error");
  10. }
  11. //取得加锁的排队编号
  12. locked_short_path = getShortPath(locked_path);
  13. //获取加锁的对列
  14. List<String> waiters = getWaiters();
  15. //获取等待的子节点列表,判断自己是否第一个
  16. if (checkLocked(waiters)) {
  17. return true;
  18. }
  19. // 判断自己排第几个
  20. int index = Collections.binarySearch(waiters, locked_short_path);
  21. if (index < 0) {
  22. // 网络抖动,获取到的子节点列表里可能已经没有自己了
  23. throw new Exception("节点没有找到: " + locked_short_path);
  24. }
  25. //如果自己没有获得锁
  26. // 保存前一个节点,稍候会监听前一个节点
  27. prior_path = ZK_PATH + "/" + waiters.get(index - 1);
  28. return false;
  29. }
  1. checkLocked()检查是否持有锁
    在checkLocked()方法中,判断是否可以持有锁。判断规则很简单:当前创建的节点,是否在上一步获取到的子节点列表的第一个位置:

(1)如果是,说明可以持有锁,返回true,表示加锁成功;
(2)如果不是,说明有其他线程早已先持有了锁,返回false。

  1. private boolean checkLocked(List<String> waiters) {
  2. //节点按照编号,升序排列
  3. Collections.sort(waiters);
  4. // 如果是第一个,代表自己已经获得了锁
  5. if (locked_short_path.equals(waiters.get(0))) {
  6. log.info("成功的获取分布式锁,节点为{}", locked_short_path);
  7. return true;
  8. }
  9. return false;
  10. }
  1. await() 监听前一个节点释放锁

监听前一个ZNode节点(prior_path成员) 的删除事件

  1. private void await() throws Exception {
  2. if (null == prior_path) {
  3. throw new Exception("prior_path error");
  4. }
  5. final CountDownLatch latch = new CountDownLatch(1);
  6. //监听方式一: Watcher 一次性订阅
  7. //订阅比自己次小顺序节点的删除事件
  8. Watcher w = new Watcher() {
  9. @Override
  10. public void process(WatchedEvent watchedEvent) {
  11. System.out.println("监听到的变化 watchedEvent = " + watchedEvent);
  12. log.info("[WatchedEvent]节点删除");
  13. latch.countDown();
  14. }
  15. };
  16. //开始监听
  17. client.getData().usingWatcher(w).forPath(prior_path);
  18. //限时等待,最长加锁时间为 3s
  19. latch.await(WAIT_TIME, TimeUnit.SECONDS);
  20. }

13.6.5 实战:释放锁的实现

(1)减少重入锁的计数,如果最终的值不是0,直接返回,表示成功的释放了一次;
(2)如果计数器为0,移除Watchers监听器,并且删除创建的Znode临时节点。

  1. @Override
  2. public boolean unlock() {
  3. //只有加锁的线程,能够解锁
  4. if (!thread.equals(Thread.currentThread())) {
  5. return false;
  6. }
  7. //减少可重入的计数
  8. int newLockCount = lockCount.decrementAndGet();
  9. //计数不能小于 0
  10. if (newLockCount < 0) {
  11. throw new IllegalMonitorStateException("计数不对: " + locked_path);
  12. }
  13. //如果计数不为 0,直接返回
  14. if (newLockCount != 0) {
  15. return true;
  16. }
  17. try {
  18. //删除结点
  19. if (ZKclient.instance.isNodeExist(locked_path)) {
  20. client.delete().forPath(locked_path);
  21. }
  22. }catch (Exception e) {
  23. e.printStackTrace();
  24. return false;
  25. }
  26. return true;
  27. }

13.6.6 实战:分布式锁的使用

  1. @Slf4j
  2. public class ZkLockTester {
  3. //需要锁来保护的公共资源
  4. //变量
  5. int count = 0;
  6. @Test
  7. public void testLock() throws InterruptedException {
  8. //10 个并发任务
  9. for (int i = 0; i < 10; i++) {
  10. FutureTaskScheduler.add(() -> {
  11. //创建锁
  12. ZkLock lock = new ZkLock();
  13. lock.lock();
  14. //每条线程,执行10次累加
  15. for (int j = 0; j < 10; j++) {
  16. //公共的资源变量累加
  17. count++;
  18. }
  19. try {
  20. Thread.sleep(1000);
  21. } catch (InterruptedException e) {
  22. e.printStackTrace();
  23. }
  24. log.info("count = " + count);
  25. //释放锁
  26. lock.unlock();
  27. });
  28. }
  29. Thread.sleep(Integer.MAX_VALUE);
  30. }
  31. }

13.6.7 实战:curator的InterProcessMutex可重入锁

分布式锁Zlock自主实现主要的价值: 学习一下分布式锁的原理和基础开发, 仅此而已。实际的开发中,如果需要使用到分布式锁,不建议去自己造轮子,建议直接使用Curator客户端中的各种官方实现的分布式锁,比如其中的InterProcessMutex 可重入锁。

  1. @Test
  2. public void testZkMutex() throws InterruptedException {
  3. CuratorFramework client = ZKclient.instance.getClient();
  4. //创建互斥锁
  5. final InterProcessMutex zkMutex = new InterProcessMutex(client, "/mutex");
  6. //每条线程,执行10次累加
  7. for (int i = 0; i < 10; i++) {
  8. FutureTaskScheduler.add(() -> {
  9. try {
  10. //获取互斥锁
  11. zkMutex.acquire();
  12. for (int j = 0; j < 10; j++) {
  13. //公共的资源变量累加
  14. count++;
  15. }
  16. try {
  17. Thread.sleep(1000);
  18. }catch (Exception e) {
  19. e.printStackTrace();
  20. }
  21. log.info("count = " + count);
  22. //释放互斥锁
  23. zkMutex.release();
  24. } catch (Exception e) {
  25. e.printStackTrace();
  26. }
  27. });
  28. }
  29. Thread.sleep(Integer.MAX_VALUE);
  30. }

13.6.8 ZooKeeper分布式锁的优点和缺点

(1)优点: ZooKeeper分布式锁(如InterProcessMutex),能有效的解决分布式问题,不可重入问题,使用起来也较为简单。
(2)缺点: ZooKeeper实现的分布式锁,性能并不太高。为啥呢? 因为每次在创建锁和释放锁的过程中,都要动态创建、销毁瞬时节点来实现锁功能。大家知道, ZK中创建和删除节点只能通过Leader服务器来执行,然后Leader服务器还需要将数据同不到所有的Follower机器上,这样频繁的网络通信,性能的短板是非常突出的。

在目前分布式锁实现方案中,比较成熟、主流的方案有两种:
(1)基于Redis的分布式锁
(2)基于ZooKeeper的分布式锁

两种锁,分别适用的场景为:
( 1)基于ZooKeeper的分布式锁,适用于高可靠(高可用)而并发量不是太大的场景;
( 2)基于Redis的分布式锁,适用于并发量很大、性能要求很高的、而可靠性问题可以通过其他方案去弥补的场景。