Zookeeper-基礎

張鐵牛發表於2021-08-31

1. 簡介

zookeeper是一個開源的分散式協調服務, 提供分散式資料一致性解決方案,分散式應用程式可以實現資料統一配置管理、統一命名服務、分散式鎖、叢集管理等功能.

ZooKeeper主要服務於分散式系統,使用分散式系統就無法避免對節點管理的問題(需要實時感知節點的狀態、對節點進行統一管理等等),而由於這些問題處理起來可能相對麻煩和提高了系統的複雜性,ZooKeeper作為一個能夠通用解決這些問題的中介軟體就應運而生了。

2. 資料模型

2.1 模型結構

ZooKeeper 提供的名稱空間很像標準檔案系統的名稱空間。名稱是由斜槓 (/) 分隔的一系列路徑元素。ZooKeeper 名稱空間中的每個節點都由路徑標識。

與典型的為儲存而設計的檔案系統不同,ZooKeeper 資料儲存在記憶體中,這意味著 ZooKeeper 可以實現高吞吐量和低延遲。

2.2 模型的特點

  • 每個子目錄如/app1都被稱作一個znode(節點)。這個znode是被它所在的路徑唯一標識。
  • znode可以有子節點目錄,並且每個znode可以儲存資料。
  • znode是有版本的,每個znode中儲存的資料可以有多個版本,也就是一個訪問路徑中可以儲存多份資料。每次 znode 的資料更改時,版本號都會增加。例如,每當客戶端檢索資料時,它也會收到資料的版本。
  • 每個znode都有一個訪問控制列表 (ACL),它限制了誰可以做什麼。
  • znode可以被監控,包括這個目錄中儲存的資料的修改,子節點目錄變化等,一旦變化可以通知設定監控的客戶端。

⚠️注意:當使用zkCli.sh 建立會話時,節點的監聽事件只能被觸發一次。

2.3 節點分類

2.3.1 Persistent

持久節點:節點被建立後,就一直存在,除非客戶端主動刪除這個節點。

2.3.2 Persistent Sequential

持久順序節點:有序的持久節點。在zk中,每個父節點會為它的第一級子節點維護一份時序,會記錄每個子節點建立的先後順序。例如:

$ create -s /app1 # 建立 /app1 節點
Created /app10000000000 # 建立成功後的節點名稱為 /app10000000000 

2.3.3 Ephemeral

臨時節點:和持久節點不同的是,臨時節點的生命週期和客戶端會話繫結且臨時節點下面不能建立子節點。如果客戶端會話失效,那麼這個節點就會自動被清除掉。

注意:客戶端失效臨時節點會被清除,但如果是斷開連結,臨時節點並不會立馬被清除。

“立馬”:在會話超時持續時間內未從客戶端收到任何心跳訊號之後,zk伺服器將刪除該會話的臨時節點。但如果正常關閉會話,臨時節點會立馬被清除。

# 1. 建立會話
$ ./zkCli.sh
# 2. 建立臨時節點 /app3
$ create -e /app3
Created /app3
# 3. ctrl + c 關閉會話
# 4. 緊接著再次建立會話 
$ ./zkCli.sh
# 5. 檢視節點內容 發現臨時節點依舊存在
$ ls /
[app10000000000, app3, zookeeper]
# 6. 等待幾秒,再次檢視 發現臨時節點消失
$ ls /
[app10000000000, zookeeper]

# 7. 再次建立臨時節點 /app3
$ create -e /app3
Created /app3
# 8. 正常關閉會話
$ quit
# 9. 再次建立會話 臨時節點消失
$ ./zkCli.sh
$ ls /
[app10000000000, zookeeper]

2.3.4 Ephemeral Sequential

臨時順序節點:有序的臨時節點。建立es節點時,zk會維護一份時序,會記錄每個節點的順序。例如:

$ create -s -e /app2 # 建立臨時有序節點 /app2
Created /app20000000001 # 建立成功後的節點名稱為 app20000000001

3. 安裝

3.1 官方

官方地址:https://zookeeper.apache.org/releases.html

3.2 docker

$ docker run --name zookeeper --restart always -d zookeeper

3.2 docker-compose

⚠️注意:執行指令碼前,需先將配置檔案掛載到宿主機上。

version: '3.1'
services:
  zk:
    image: zookeeper
    restart: always
    container_name: zookeeper
    ports:
      - 2181:2181
    volumes:
      - ./data:/data
      - ./logs:/datalog
      - ./conf/zoo.cfg:/conf/zoo.cfg

3.3 配置資訊

# 資料儲存目錄
dataDir=/data
# 日誌儲存目錄
dataLogDir=/datalog
# 叢集模式下 節點之間的心跳時間2s(每2s進行一次心跳檢測)
tickTime=2000
# 叢集初始化時 節點之間同步次數。(5 * 2s = 10s 10s內未初始化成功則初始化失敗)
initLimit=5
# 叢集模式下 同步資料次數。 (2 * 2s = 4s 4s內未同步則超時失敗)
syncLimit=2
# 資料快照保留個數
autopurge.snapRetainCount=3
# 資料快照清除時間間隔(單位為小時) 0:不清除 ,1:1小時 
autopurge.purgeInterval=0
# 最大的客戶端連結數
maxClientCnxns=60

4. 基礎命令

不同版本之前的命令會有所差異,本章是基於zk:3.7版本。官方地址

4.1 建立會話

首先執行命令,建立新的會話,進入終端。

# 進入到zk的安裝目錄的bin目錄下執行
$ ./zkCli.sh
# 如果zk不是在本機 則可以使用server引數指定連結地址
$ ./zkCli.sh -server 127.0.0.1:2181

4.2 ls

ls [-s] [-w] [-R] path:ls 命令用於檢視某個路徑下目錄列表。

  • -s:檢視此znode狀態資訊。
  • -w:監聽此znode目錄變化。
  • -R:遞迴檢視。
$ ls / # 檢視根目錄
[app, app10000000000, zookeeper]
$ ls /app
[]
$ ls -s /app
[app01, app02]
cZxid = 0x1b
ctime = Sun Aug 29 13:07:24 UTC 2021
mZxid = 0x1b
mtime = Sun Aug 29 13:07:24 UTC 2021
pZxid = 0x2e
cversion = 2
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 0
numChildren = 2

監聽znode目錄變化:

4.3 create

create [-s] [-e] [-c] [-t ttl] path [data] [acl]:create 用於建立znode。

  • -s:建立順序節點。

  • -e:建立臨時節點。

  • -c:建立一個容器節點,當容器中的最後一個子節點被刪除時,容器也隨之消失。

  • -t:設定節點存活時間(毫秒)。

    ⚠️注意:如果要設定超時時間需在配置檔案中啟用配置:zookeeper.extendedTypesEnabled=true

  • data:設定的資料。

  • acl:訪問控制配置。

$ ls /
[app, app10000000000, zookeeper]
# 建立持久節點 /app3
$ create /app3 zhangtieniu
Created /app3
# 建立有序的持久節點 /app4
$ create -s /app4 zhangsan
Created /app40000000011
# 建立臨時節點 /app5
$ create -e /app5 linshi
Created /app5
# 建立臨時有序節點 /app6
$ create -s -e /app6 linshiyouxu
Created /app60000000013
$ ls /
[app, app10000000000, app3, app40000000011, app5, app60000000013, zookeeper]

4.4 get

get [-s] [-w] path: 命令用於獲取節點資料和狀態資訊。

  • -s:檢視此znode狀態資訊。
  • -w:監聽此znode值變化。
$ get /app3 # 檢視/app3 znode內容
zhangtieniu

監聽znod值變化:

4.5 stat

stat [-w] path:命令用於檢視節點狀態資訊。

  • -w:監聽節點狀態變化。
$ stat /app3
cZxid = 0x34 # 建立節點時的事務id
ctime = Sun Aug 29 14:31:52 UTC 2021 # 建立時間
mZxid = 0x34 # 建立的版本號
mtime = Sun Aug 29 14:31:52 UTC 2021 # 修改時間
pZxid = 0x34 # 子節點列表最後一次被修改的事務id
cversion = 0 # 節點版本號
dataVersion = 0 # 資料版本號
aclVersion = 0 # 許可權版本號 
ephemeralOwner = 0x0 # 臨時擁有者
dataLength = 11 # 節點值資料長度
numChildren = 0 # 子節點數量

4.6 set

set [-s] [-v version] path data: 命令用於修改節點儲存的資料。

  • -s:檢視設定成功後的狀態資訊。
  • -v:指定設定值的版本號,該值只能為該節點的最新版本。可以使用其實現樂觀鎖。
$ set /app wangmazi
$ set -s /app lisi
cZxid = 0x1b
ctime = Sun Aug 29 13:07:24 UTC 2021
mZxid = 0x43
mtime = Sun Aug 29 14:50:52 UTC 2021
pZxid = 0x3f
cversion = 4
dataVersion = 6
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 4
numChildren = 4

4.7 delete

delete [-v version] path:刪除指定節點。

⚠️注意:當刪除的節點有子節點時,不能使用該命令。需使用deleteall命令刪除。

  • -v:刪除指定版本。與set同理
$ delete /app3

4.8 quit

關閉會話。

5. 節點的監聽機制

zk的監聽機制,可以使客戶端可以監聽znode節點的變化,znode節點的變化出發相應的事件,然後清除對該節點的檢測。

⚠️注意:在這裡再強調一次,當使用zkCli.sh 建立會話時,對znode的監聽只能觸發一次。但在使用java客戶端連結時,可以一直觸發。

$ ls -w /path # 監聽節點目錄的變化
$ get -w /path # 監聽節點資料的變化

6. quick start

6.1 專案結構

.
├── pom.xml
└── src
    └── main
        ├── java
        │   └── com
        │       └── ldx
        │           └── zookeeper
        │               ├── ZookeeperApplication.java # 啟動類
        │               ├── config
        │               │   ├── CuratorClientConfig.java # zk配置類
        │               │   └── CuratorClientProperties.java # zk 配置屬性檔案
        │               ├── controller
        │               │   └── AppController.java # zk 測試檔案
        │               └── util
        │                   └── CuratorClient.java # zk工具類
        └── resources
            └── application.yaml # 服務配置檔案

6.2 引入依賴

Curator 是 Netflix 公司開源的一套 zookeeper 客戶端框架,解決了很多 Zookeeper 客戶端非常底層的細節開發工作,包括連線重連、反覆註冊 Watcher 和 NodeExistsException 異常等。

curator-recipes:封裝了一些高階特性,如:Cache 事件監聽、選舉、分散式鎖、分散式計數器、分散式 Barrier 等。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.4</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.ldx</groupId>
    <artifactId>zookeeper</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>zookeeper</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
       <!-- client 操作工具包 -->
        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-recipes</artifactId>
            <version>5.2.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

6.3 application.yaml

server:
  port: 8080
# curator配置
curator-client:
  # 連線字串
  connection-string: localhost:2181
  # 根節點
  namespace: ldx
  # 節點資料編碼
  charset: utf8
  # session超時時間
  session-timeout-ms: 60000
  # 連線超時時間
  connection-timeout-ms: 15000
  # 關閉連線超時時間
  max-close-wait-ms: 1000
  # 預設資料
  default-data: ""
  # 當半數以上zookeeper服務出現故障仍然提供讀服務
  can-be-read-only: false
  # 自動建立父節點
  use-container-parents-if-available: true
  # 重試策略,預設使用BoundedExponentialBackoffRetry
  retry:
    max-sleep-time-ms: 10000
    base-sleep-time-ms: 1000
    max-retries: 3
  # 認證資訊
  #auth:
    #scheme: digest
    # auth: username:password

6.4 CuratorClientProperties

package com.ldx.zookeeper.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;

/**
 * zk 屬性配置類
 *
 * @author ludangxin
 * @date 2021/8/31
 */
@Data
@ConfigurationProperties(prefix = "curator-client")
public class CuratorClientProperties {
    /**
     * 連線地址
     */
    private String connectionString;
    /**
     * 名稱空間
     */
    private String namespace;
    /**
     * 字符集
     */
    private String charset = "utf8";
    /**
     * 會話超時時間 毫秒
     */
    private int sessionTimeoutMs = 60000;
    /**
     * 連線超時時間 毫秒
     */
    private int connectionTimeoutMs = 15000;
    /**
     * 最大關閉等待時間 毫秒
     */
    private int maxCloseWaitMs = 1000;
    /**
     * 預設資料
     */
    private String defaultData = "";
    /**
     * 當半數以上zookeeper服務出現故障仍然提供讀服務
     */
    private boolean canBeReadOnly = false;
    /**
     * 自動建立父節點
     */
    private boolean useContainerParentsIfAvailable = true;
    /**
     * 執行緒池名稱
     */
    private String threadFactoryClassName;
    private Retry retry = new Retry();
    private Auth auth = new Auth();

    @Data
    public static class Retry {
        private int maxSleepTimeMs = 10000;
        private int baseSleepTimeMs = 1000;
        private int maxRetries = 3;
    }

    @Data
    public static class Auth {
        private String scheme = "digest";
        private String auth;
    }

}

6.5 CuratorClientConfig

package com.ldx.zookeeper.config;

import com.ldx.zookeeper.util.CuratorClient;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.apache.curator.RetryPolicy;
import org.apache.curator.ensemble.EnsembleProvider;
import org.apache.curator.ensemble.fixed.FixedEnsembleProvider;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.api.ACLProvider;
import org.apache.curator.framework.api.CompressionProvider;
import org.apache.curator.framework.imps.GzipCompressionProvider;
import org.apache.curator.retry.BoundedExponentialBackoffRetry;
import org.apache.curator.utils.DefaultZookeeperFactory;
import org.apache.curator.utils.ZookeeperFactory;
import org.apache.zookeeper.ZooDefs;
import org.apache.zookeeper.data.ACL;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.nio.charset.Charset;
import java.util.List;
import java.util.concurrent.ThreadFactory;

/**
 * zk 配置類
 *
 * @author ludangxin
 * @date 2021/8/31
 */
@Slf4j
@Configuration
@EnableConfigurationProperties(CuratorClientProperties.class)
public class CuratorClientConfig {
    @Bean
    public EnsembleProvider ensembleProvider(CuratorClientProperties curatorClientProperties) {
        return new FixedEnsembleProvider(curatorClientProperties.getConnectionString());
    }

    @Bean
    public RetryPolicy retryPolicy(CuratorClientProperties curatorClientProperties) {
        CuratorClientProperties.Retry retry = curatorClientProperties.getRetry();
        return new BoundedExponentialBackoffRetry(retry.getBaseSleepTimeMs(), retry.getMaxSleepTimeMs(), retry.getMaxRetries());
    }

    @Bean
    public CompressionProvider compressionProvider() {
        return new GzipCompressionProvider();
    }

    @Bean
    public ZookeeperFactory zookeeperFactory() {
        return new DefaultZookeeperFactory();
    }

    @Bean
    public ACLProvider aclProvider() {
        return new ACLProvider() {
            @Override
            public List<ACL> getDefaultAcl() {
                return ZooDefs.Ids.CREATOR_ALL_ACL;
            }
            @Override
            public List<ACL> getAclForPath(String path) {
                return ZooDefs.Ids.CREATOR_ALL_ACL;
            }
        };
    }

    @Bean
    @SneakyThrows
    public CuratorFrameworkFactory.Builder builder(EnsembleProvider ensembleProvider,
                                                   RetryPolicy retryPolicy,
                                                   CompressionProvider compressionProvider,
                                                   ZookeeperFactory zookeeperFactory,
                                                   ACLProvider aclProvider,
                                                   CuratorClientProperties curatorClientProperties) {
        String charset = curatorClientProperties.getCharset();
        CuratorFrameworkFactory.Builder builder = CuratorFrameworkFactory.builder()
                .ensembleProvider(ensembleProvider)
                .retryPolicy(retryPolicy)
                .compressionProvider(compressionProvider)
                .zookeeperFactory(zookeeperFactory)
                .namespace(curatorClientProperties.getNamespace())
                .sessionTimeoutMs(curatorClientProperties.getSessionTimeoutMs())
                .connectionTimeoutMs(curatorClientProperties.getConnectionTimeoutMs())
                .maxCloseWaitMs(curatorClientProperties.getMaxCloseWaitMs())
                .defaultData(curatorClientProperties.getDefaultData().getBytes(Charset.forName(charset)))
                .canBeReadOnly(curatorClientProperties.isCanBeReadOnly());
        if (!curatorClientProperties.isUseContainerParentsIfAvailable()) {
            builder.dontUseContainerParents();
        }
        CuratorClientProperties.Auth auth = curatorClientProperties.getAuth();
        if (StringUtils.isNotBlank(auth.getAuth())) {
            builder.authorization(auth.getScheme(), auth.getAuth().getBytes(Charset.forName(charset)));
            builder.aclProvider(aclProvider);
        }
        String threadFactoryClassName = curatorClientProperties.getThreadFactoryClassName();
        if (StringUtils.isNotBlank(threadFactoryClassName)) {
            try {
                Class<?> cls = Class.forName(threadFactoryClassName);
                ThreadFactory threadFactory = (ThreadFactory) cls.newInstance();
                builder.threadFactory(threadFactory);
            } catch (Exception e) {
                log.error("init CuratorClient error", e);
            }
        }
        return builder;
    }

    @Bean(initMethod = "init", destroyMethod = "stop")
    public CuratorClient curatorClient(CuratorFrameworkFactory.Builder builder) {
        return new CuratorClient(builder);
    }

}

6.6 CuratorClient

package com.ldx.zookeeper.util;

import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.cache.CuratorCache;
import org.apache.curator.framework.recipes.cache.CuratorCacheListener;
import org.apache.curator.framework.recipes.locks.*;
import org.apache.curator.framework.state.ConnectionState;
import org.apache.zookeeper.CreateMode;
import java.nio.charset.Charset;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;

/**
 * zookeeper工具類
 *
 * @author ludangxin
 * @date 2021/8/30
 */
@Slf4j
public class CuratorClient {
    /**
     * 預設的字元編碼集
     */
    private static final String DEFAULT_CHARSET = "utf8";
    /**
     * 客戶端
     */
    private final CuratorFramework client;
    /**
     * 字符集
     */
    private String charset = DEFAULT_CHARSET;

    @SneakyThrows
    public CuratorClient(CuratorFrameworkFactory.Builder builder) {
        client = builder.build();
    }

    @SneakyThrows
    public CuratorClient(CuratorFrameworkFactory.Builder builder, String charset) {
        client = builder.build();
        this.charset = charset;
    }

    public void init() {
        client.start();
        client.getConnectionStateListenable().addListener((client, state) -> {
            if (state==ConnectionState.LOST) {
                // 連線丟失
                log.info("lost session with zookeeper");
            } else if (state==ConnectionState.CONNECTED) {
                // 連線新建
                log.info("connected with zookeeper");
            } else if (state==ConnectionState.RECONNECTED) {
                // 重新連線
                log.info("reconnected with zookeeper");
            }
        });
    }

    /**
     * 關閉會話
     */
    public void stop() {
        log.info("zookeeper session close");
        client.close();
    }

    /**
     * 建立節點
     *
     * @param mode     節點型別
     *   1、PERSISTENT 持久化目錄節點,儲存的資料不會丟失。
     *   2、PERSISTENT_SEQUENTIAL順序自動編號的持久化目錄節點,儲存的資料不會丟失
     *   3、EPHEMERAL臨時目錄節點,一旦建立這個節點的客戶端與伺服器埠也就是session 超時,這種節點會被自動刪除
     *   4、EPHEMERAL_SEQUENTIAL臨時自動編號節點,一旦建立這個節點的客戶端與伺服器埠也就是session 超時,這種節點會被自動刪除,並且根據當前已經存在的節點數自動加 1,然後返回給客戶端已經成功建立的目錄節點名。
     * @param path     節點名稱
     * @param nodeData 節點資料
     */
    @SneakyThrows
    public void createNode(CreateMode mode, String path, String nodeData) {
       // 使用creatingParentContainersIfNeeded()之後Curator能夠自動遞迴建立所有所需的父節點
       client.create().creatingParentsIfNeeded().withMode(mode).forPath(path, nodeData.getBytes(Charset.forName(charset)));
    }

    /**
     * 建立節點
     *
     * @param mode 節點型別
     *   1、PERSISTENT 持久化目錄節點,儲存的資料不會丟失。
     *   2、PERSISTENT_SEQUENTIAL順序自動編號的持久化目錄節點,儲存的資料不會丟失
     *   3、EPHEMERAL臨時目錄節點,一旦建立這個節點的客戶端與伺服器埠也就是session 超時,這種節點會被自動刪除
     *   4、EPHEMERAL_SEQUENTIAL臨時自動編號節點,一旦建立這個節點的客戶端與伺服器埠也就是session 超時,這種節點會被自動刪除,並且根據當前已經存在的節點數自動加 1,然後返回給客戶端已經成功建立的目錄節點名。
     * @param path 節點名稱
     */
    @SneakyThrows
    public void createNode(CreateMode mode, String path) {
       // 使用creatingParentContainersIfNeeded()之後Curator能夠自動遞迴建立所有所需的父節點
       client.create().creatingParentsIfNeeded().withMode(mode).forPath(path);
    }

    /**
     * 刪除節點資料
     *
     * @param path 節點名稱
     */
    @SneakyThrows
    public void deleteNode(final String path) {
       deleteNode(path, true);
    }

    /**
     * 刪除節點資料
     *
     * @param path 節點名稱
     * @param deleteChildre 是否刪除子節點
     */
    @SneakyThrows
    public void deleteNode(final String path, Boolean deleteChildre) {
       if (deleteChildre) {
           // guaranteed()刪除一個節點,強制保證刪除,
           // 只要客戶端會話有效,那麼Curator會在後臺持續進行刪除操作,直到刪除節點成功
           client.delete().guaranteed().deletingChildrenIfNeeded().forPath(path);
       } else {
           client.delete().guaranteed().forPath(path);
       }
    }

    /**
     * 設定指定節點的資料
     *
     * @param path 節點名稱
     * @param data 節點資料
     */
    @SneakyThrows
    public void setNodeData(String path, String data) {
       client.setData().forPath(path, data.getBytes(Charset.forName(charset)));
    }

    /**
     * 獲取指定節點的資料
     *
     * @param path 節點名稱
     * @return 節點資料
     */
    @SneakyThrows
    public String getNodeData(String path) {
       return new String(client.getData().forPath(path), Charset.forName(charset));
    }

    /**
     * 獲取資料時先同步
     *
     * @param path 節點名稱
     * @return 節點資料
     */
    public String synNodeData(String path) {
        client.sync();
        return getNodeData(path);
    }

    /**
     * 判斷節點是否存在
     *
     * @param path 節點名稱
     * @return true 節點存在,false 節點不存在
     */
    @SneakyThrows
    public boolean isExistNode(final String path) {
        client.sync();
        return Objects.nonNull(client.checkExists().forPath(path));
    }

    /**
     * 獲取節點的子節點
     *
     * @param path 節點名稱
     * @return 子節點集合
     */
    @SneakyThrows
    public List<String> getChildren(String path) {
        return client.getChildren().forPath(path);
    }

    /**
     * 建立排他鎖
     *
     * @param path 節點名稱
     * @return 排他鎖
     */
    public InterProcessSemaphoreMutex getSemaphoreMutexLock(String path) {
       return new InterProcessSemaphoreMutex(client, path);
    }

    /**
     * 建立可重入排他鎖
     *
     * @param path 節點名稱
     * @return 可重入排他鎖
     */
    public InterProcessMutex getMutexLock(String path) {
       return new InterProcessMutex(client, path);
    }

    /**
     * 建立一組可重入排他鎖
     *
     * @param paths 節點名稱集合
     * @return 鎖容器
     */
    public InterProcessMultiLock getMultiMutexLock(List<String> paths) {
        return new InterProcessMultiLock(client, paths);
    }

    /**
     * 建立一組任意型別的鎖
     *
     * @param locks 鎖集合
     * @return 鎖容器
     */
    public InterProcessMultiLock getMultiLock(List<InterProcessLock> locks) {
        return new InterProcessMultiLock(locks);
    }

    /**
     * 加鎖
     *
     * @param lock 分散式鎖物件
     */
    @SneakyThrows
    public void acquire(InterProcessLock lock) {
       lock.acquire();
    }

    /**
     * 加鎖
     *
     * @param lock 分散式鎖物件
     * @param time 等待時間
     * @param unit 時間單位
     */
    @SneakyThrows
    public void acquire(InterProcessLock lock, long time, TimeUnit unit) {
       lock.acquire(time, unit);
    }

    /**
     * 釋放鎖
     *
     * @param lock 分散式鎖物件
     */
    @SneakyThrows
    public void release(InterProcessLock lock) {
       lock.release();
    }

    /**
     * 檢查是否當前jvm的執行緒獲取了鎖
     *
     * @param lock 分散式鎖物件
     * @return true/false
     */
    public boolean isAcquiredInThisProcess(InterProcessLock lock) {
        return lock.isAcquiredInThisProcess();
    }

    /**
     * 獲取讀寫鎖
     *
     * @param path 節點名稱
     * @return 讀寫鎖
     */
    public InterProcessReadWriteLock getReadWriteLock(String path) {
        return new InterProcessReadWriteLock(client, path);
    }

    /**
     * 監聽資料節點的變化情況
     *
     * @param path 節點名稱
     * @param listener 監聽器
     * @return 監聽節點的TreeCache例項
     */
    @SneakyThrows
    public CuratorCache watch(String path, CuratorCacheListener listener) {
        CuratorCache curatorCache = CuratorCache.builder(client, path).build();
        curatorCache.listenable().addListener(listener);
        curatorCache.start();
        return curatorCache;
    }

    /**
     * 監聽資料節點的變化情況
     *
     * @param path 節點名稱
     * @param listener 監聽器
     * @return 監聽節點的TreeCache例項
     */
    public CuratorCache watch(String path, CuratorCacheListener listener, Executor executor) {
        CuratorCache curatorCache = CuratorCache.builder(client, path).build();
        curatorCache.listenable().addListener(listener, executor);
        curatorCache.start();
        return curatorCache;
    }

    /**
     * 取消監聽節點
     *
     * @param path 節點名稱
     * @param listener 監聽器
     */
    public void unwatch(String path, CuratorCacheListener listener) {
        CuratorCache curatorCache = CuratorCache.builder(client, path).build();
        curatorCache.listenable().removeListener(listener);
    }

}

6.7 AppController

package com.ldx.zookeeper.controller;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ldx.zookeeper.util.CuratorClient;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.curator.framework.recipes.cache.CuratorCacheListener;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.data.Stat;
import org.springframework.web.bind.annotation.*;

/**
 * demo
 *
 * @author ludangxin
 * @date 2021/8/30
 */
@Slf4j
@RestController
@RequestMapping("app")
@RequiredArgsConstructor
public class AppController {

   private final CuratorClient curatorClient;

   @GetMapping("{appName}")
   public String getData(@PathVariable String appName) {
      return curatorClient.getNodeData(setPrefix(appName));
   }

   @PostMapping("{appName}")
   public String addApp(@PathVariable String appName, @RequestParam String data) {
      curatorClient.createNode(CreateMode.PERSISTENT, setPrefix(appName), data);
      return "ok";
   }

   @PostMapping("{appName}/{childName}")
   public String addAppChild(@PathVariable String appName, @PathVariable String childName, @RequestParam String data) {
      curatorClient.createNode(CreateMode.PERSISTENT, setPrefix(appName).concat(setPrefix(childName)), data);
      return "ok";
   }

   @PutMapping("{appName}")
   public String setData(@PathVariable String appName, String data) {
      curatorClient.setNodeData(setPrefix(appName), data);
      return "ok";
   }

   @PutMapping("{appName}/{childName}")
   public String setData(@PathVariable String appName, @PathVariable String childName, String data) {
      curatorClient.setNodeData(setPrefix(appName).concat(setPrefix(childName)), data);
      return "ok";
   }

   @DeleteMapping("{appName}")
   public String delApp(@PathVariable String appName) {
      curatorClient.deleteNode(setPrefix(appName));
      return "ok";
   }

   @PostMapping("{appName}/watch/dir")
   public String watchAppDir(@PathVariable String appName) {
      CuratorCacheListener listener = CuratorCacheListener.builder().forDeletes(obj -> {
         String path = obj.getPath();
         String data = new String(obj.getData());
         Stat stat = obj.getStat();
         log.info("節點:{} 被刪除,節點資料:{},節點狀態:\\{version:{},createTime:{}\\}", path, data,
            stat.getVersion(), stat.getCtime());
      }).build();
      curatorClient.watch(setPrefix(appName), listener);
      return "ok";
   }

   @PostMapping("{appName}/watch/data")
   public String watchAppData(@PathVariable String appName) {
      ObjectMapper mapper = new ObjectMapper();
      CuratorCacheListener listener = CuratorCacheListener.builder().forChanges((oldNode, newNode) -> {
         try {
            String path = oldNode.getPath();
            log.info("節點:{} 被修改,修改前:{} ", path, mapper.writeValueAsString(oldNode));
            log.info("節點:{} 被修改,修改後:{} ", path, mapper.writeValueAsString(newNode));
         } catch(JsonProcessingException e) {
            e.printStackTrace();
         }
      }).build();
      curatorClient.watch(setPrefix(appName), listener);
      return "ok";
   }

   private String setPrefix(String appName) {
      String prefix = "/";
      if(!appName.startsWith(prefix)) {
         appName = prefix.concat(appName);
      }
      return appName;
   }
}

7. ZAB 協議介紹

ZAB(ZooKeeper Atomic Broadcast 原子廣播) 協議是為分散式協調服務 ZooKeeper 專門設計的一種支援崩潰恢復的原子廣播協議。 在 ZooKeeper 中,主要依賴 ZAB 協議來實現分散式資料一致性,基於該協議,ZooKeeper 實現了一種主備模式的系統架構來保持叢集中各個副本之間的資料一致性。

ZAB 協議兩種基本的模式:崩潰恢復和訊息廣播

ZAB協議包括兩種基本的模式,分別是 崩潰恢復和訊息廣播。當整個服務框架在啟動過程中,或是當 Leader 伺服器出現網路中斷、崩潰退出與重啟等異常情況時,ZAB 協議就會進人恢復模式並選舉產生新的Leader伺服器。當選舉產生了新的 Leader 伺服器,同時叢集中已經有過半的機器與該Leader伺服器完成了狀態同步之後,ZAB協議就會退出恢復模式。其中,所謂的狀態同步是指資料同步,用來保證叢集中存在過半的機器能夠和Leader伺服器的資料狀態保持一致

當叢集中已經有過半的Follower伺服器完成了和Leader伺服器的狀態同步,那麼整個服務框架就可以進人訊息廣播模式了。 當一臺同樣遵守ZAB協議的伺服器啟動後加人到叢集中時,如果此時叢集中已經存在一個Leader伺服器在負責進行訊息廣播,那麼新加人的伺服器就會自覺地進人資料恢復模式:找到Leader所在的伺服器,並與其進行資料同步,然後一起參與到訊息廣播流程中去。正如上文介紹中所說的,ZooKeeper設計成只允許唯一的一個Leader伺服器來進行事務請求的處理。Leader伺服器在接收到客戶端的事務請求後,會生成對應的事務提案併發起一輪廣播協議;而如果叢集中的其他機器接收到客戶端的事務請求,那麼這些非Leader伺服器會首先將這個事務請求轉發給Leader伺服器。

相關文章