二十三、Zookeeper(1)

u_hcy2000發表於2020-12-18

1 概述

1.1 分散式應用

分散式應用(distributed application)指的是應用程式分佈在不同計算機上,通過網路來共同完成一項任務的工作方式(協作)。以javaEE實現一個電商網站為例:

  • 單體應用:所有功能都寫在一個專案了;打包成一個可執行的war包;部署這個war包就可以完成整個網站所有功能。
  • 分散式應用:不同的功能寫在不同的專案裡;打包成多個可執行的war包;由多個執行的服務共同完成整個網站的完整功能。
    在這裡插入圖片描述

如上圖,這個電商網站包含了使用者管理、商品管理、訂單管理、支付管理4個模組(也稱為服務),在分散式應用裡面,許多功能都是多個服務共同協作完成的,服務間如何高效有序地協作就成為分散式開發中一個重要的問題。

  • 類比:
    • 單一應用 --> 所有工作都一個人完成
    • 分散式應用 --> 很多工作由多個人共同==協作==完成

1.2 什麼是zookeeper

ZooKeeper是一個分散式的,開放原始碼的分散式應用程式協調服務,是Google的Chubby一個開源的實現,是Hadoop和Hbase的重要元件。它主要是用來解決分散式應用中經常遇到的一些問題。

ZooKeeper從字面意思理解,【Zoo - 動物園,Keeper - 管理員】動物園中有很多種動物,這裡的動物就可以比作分散式環境下多種多樣的服務,而ZooKeeper做的就是管理這些服務。

1.3 架構

zookeeper的架構如下圖所示:
在這裡插入圖片描述

ZooKeeper叢集由一組Server節點組成,這一組Server節點中存在一個角色為Leader的節點,其他節點都為Follower。管與zookeeper架構有如下幾個要點:

  • 讀操作,直接讀取其中一個Follwer並直接返回,

  • 寫操作,zookeeper叢集會做如下兩步處理:

    • 這些請求會被髮送到Leader節點上
    • Leader節點上資料變更會同步到叢集中其他的Follower節點
  • Leader節點在接收到資料變更請求後,首先將變更寫入本地磁碟,以作恢復之用。當所有的寫請求持久化到磁碟以後,才會將變更應用到記憶體中。

  • 注意:持久化到硬碟的資料,只是用於服務重啟時資料恢復

  • 當Leader節點出現故障無法正常相應時,叢集會自動重新選舉一Follwer節點作為Leader。

1.4 儲存結構和分層名稱空間

ZooKeeper有一個分層的名稱空間,結構類似檔案系統的目錄結構,非常簡單而直觀。其中,ZNode是最重要的概念。
在這裡插入圖片描述

在ZooKeeper中每個名稱空間(Namespace)被稱為ZNode,你可以這樣理解,每個ZNode包含一個路徑和與之相關的屬性和資料,以及繼承自該節點的孩子列表。與傳統檔案系統不同的是,ZooKeeper中的資料儲存在記憶體中,實現了分散式同步服務的高吞吐和低延遲。

ZNode分類及特性:

1.4.1 永久節點和臨時節點(Ephemeral)

  • 永久節點(預設):一但建立成功就不會自動消失。
  • 臨時節點:建立成功後,一但客戶端與伺服器失去連線,就會自動刪除

1.4.2 有序節點(Sequence Nodes)

zookeeper支援建立有序節點,也就是在ZNode名稱後面自動拼接一個遞增的數字。

1.4.3 節點的更新與監聽(watches)

zookeeper客戶端可以監聽ZNode資料的變化,一但ZNode的資料發生變化,則自動通知到正在監聽的客戶端。

2 zookeeper安裝及常用指令

下載地址:http://archive.apache.org/dist/zookeeper/

本課程使用的Zookeeper版本為3.4.6,下載完成後可以獲得名稱為zookeeper-3.4.6.tar.gz的壓縮檔案。既可以在window環境執行,也可以在Linux環境執行。

zookeeper的部署分為單機模式、叢集模式和偽叢集模式。

一般來講,單機模式用於本地測試;叢集模式用於生產環境;偽叢集模式用於對zookeeper叢集的學習

2.1 單機模式

單機模式就是隻在一臺機器上啟動一個zookeeper服務,這種模式配置簡單,但是有單點故障問題,只適合在本地除錯使用。

部署步驟

  1. 把 zookeeper 的壓縮包(zookeeper-3.4.6.tar.gz),解壓縮壓縮包。
    在這裡插入圖片描述

  2. 進入zookeeper-3.4.6目錄,建立data目錄

在這裡插入圖片描述

  1. 進入conf目錄 ,把zoo_sample.cfg 複製得到zoo.cfg檔案。
    在這裡插入圖片描述

  2. 開啟zoo.cfg檔案, 修改data屬性:dataDir=…/data
    在這裡插入圖片描述

  3. 進入Zookeeper的bin目錄,雙擊zkServer.cmd,啟動Zookeeper服務
    在這裡插入圖片描述

2.2 叢集模式(瞭解即可,無需操作)

為了獲得可靠的 ZooKeeper 服務,使用者應該在一個叢集上部署 ZooKeeper 。只要叢集上大多數的ZooKeeper 服務啟動了,那麼總的 ZooKeeper 服務將是可用的。另外,最好使用奇數臺機器。 如果 zookeeper擁有 5 臺機器,那麼它就能處理 2 臺機器的故障了。
在這裡插入圖片描述

  • 埠說明

    • 2181 Client連線的埠,和單機模式相同
    • 2888 follower 連線到 leader的埠
    • 3888 leader 選舉的埠
  • myid

    • 用來標記叢集中當前服務的身份(1~255)

部署步驟

叢集模式的部署與單機模式模式類似,只是需要部署到多臺機器上 ,再對配置做一些調整。這裡以3臺機器為例,具體部署步驟如下:

  1. 參照單機模式的部署步驟,在3臺機器上進行部署

  2. 在每個機器的dataDir(zoo.cfg裡定義的屬性)目錄下建立名為myid的檔案,併為三臺機器上的myid寫入不同的數字(1~255),例如這裡三臺機器。

    第1臺機器myid內容(data/myid):

    1
    

    第2臺機器myid內容(data/myid):

    2
    

    第3臺機器myid內容(data/myid):

    3
    

    注意:myid裡的內容是和zoo.cfg裡的server.xx相對應的

  3. 修改每一臺機器的配置檔案(zoo.cfg),完整的配置如下:

    #基本事件單元,以毫秒為單位,它用來指示心跳
    tickTime=2000
    #叢集中的follower與leader之間 初始連線 時能容忍的最多心跳數(tickTime的數量)
    initLimit=10
    #叢集中的follower與leader之間 請求和應答 之間能容忍的最多心跳數(tickTime的數量)
    syncLimit=5
    
    #儲存記憶體中資料庫快照的位置
    dataDir=../data
    #監聽客戶端連線的埠
    clientPort=2181
    
    #格式 server.id=host:port1:port2
    #用來指出叢集中的所有機器;有多少臺機器,這裡就要有多少條配置
    #id就是機器的myid
    #host就是機器的IP地址
    #第一個埠是從 follower 連線到 leader的埠,
    #第二個埠是用來進行 leader 選舉的埠
    server.1=host1:2888:3888
    server.2=host2:2888:3888
    server.3=host3:2888:3888
    
  4. 分別啟動三臺機器上的zookeeper

2.3 偽叢集模式

簡單來說,叢集偽分佈模式就是在單機下模擬叢集的ZooKeeper服務。因此需要修改埠來解決埠衝突的問題,如下圖所示:
在這裡插入圖片描述
部署步驟:

  1. 把 zookeeper 的壓縮包(zookeeper-3.4.6.tar.gz),解壓縮壓縮包。並通過複製得到三個資料夾,為了方便演示,這裡命名為zookeeper01、zookeeper02、zookeeper03
    在這裡插入圖片描述

  2. 分別進入zookeeper安裝目錄(zookeeper01、zookeeper02、zookeeper03),建立data目錄。以zookeeper01為例,如下圖所示:
    在這裡插入圖片描述

  3. 分別進入三個zookeeper的conf目錄 ,把zoo_sample.cfg 複製得到zoo.cfg檔案。以zookeeper01為例,如下圖所示:
    在這裡插入圖片描述

  4. 分別修改三個配置檔案,三臺機器的配置檔案唯一不同的屬性就是clientPort,其他配置檔案都一樣。內容如下:

    zookeeper01/config/zoo.cfg

    #基本事件單元,以毫秒為單位,它用來指示心跳
    tickTime=2000
    #叢集中的follower與leader之間 初始連線 時能容忍的最多心跳數(tickTime的數量)
    initLimit=10
    #叢集中的follower與leader之間 請求和應答 之間能容忍的最多心跳數(tickTime的數量)
    syncLimit=5
    
    #儲存記憶體中資料庫快照的位置
    dataDir=../data
    #監聽客戶端連線的埠
    clientPort=12181
    
    #格式 server.id=host:port1:port2
    #用來指出叢集中的所有機器;有多少臺機器,這裡就要有多少條配置
    #id就是機器的myid
    #host就是機器的IP地址
    #第一個埠是從 follower 連線到 leader的埠,
    #第二個埠是用來進行 leader 選舉的埠
    server.1=127.0.0.1:12888:13888
    server.2=127.0.0.1:22888:23888
    server.3=127.0.0.1:32888:33888
    

    zookeeper02/config/zoo.cfg

    #基本事件單元,以毫秒為單位,它用來指示心跳
    tickTime=2000
    #叢集中的follower與leader之間 初始連線 時能容忍的最多心跳數(tickTime的數量)
    initLimit=10
    #叢集中的follower與leader之間 請求和應答 之間能容忍的最多心跳數(tickTime的數量)
    syncLimit=5
    
    #儲存記憶體中資料庫快照的位置
    dataDir=../data
    #監聽客戶端連線的埠
    clientPort=22181
    
    #格式 server.id=host:port1:port2
    #用來指出叢集中的所有機器;有多少臺機器,這裡就要有多少條配置
    #id就是機器的myid
    #host就是機器的IP地址
    #第一個埠是從 follower 連線到 leader的埠,
    #第二個埠是用來進行 leader 選舉的埠
    server.1=127.0.0.1:12888:13888
    server.2=127.0.0.1:22888:23888
    server.3=127.0.0.1:32888:33888
    

    zookeeper03/config/zoo.cfg

    #基本事件單元,以毫秒為單位,它用來指示心跳
    tickTime=2000
    #叢集中的follower與leader之間 初始連線 時能容忍的最多心跳數(tickTime的數量)
    initLimit=10
    #叢集中的follower與leader之間 請求和應答 之間能容忍的最多心跳數(tickTime的數量)
    syncLimit=5
    
    #儲存記憶體中資料庫快照的位置
    dataDir=../data
    #監聽客戶端連線的埠
    clientPort=32181
    
    #格式 server.id=host:port1:port2
    #用來指出叢集中的所有機器;有多少臺機器,這裡就要有多少條配置
    #id就是機器的myid
    #host就是機器的IP地址
    #第一個埠是從 follower 連線到 leader的埠,
    #第二個埠是用來進行 leader 選舉的埠
    server.1=127.0.0.1:12888:13888
    server.2=127.0.0.1:22888:23888
    server.3=127.0.0.1:32888:33888
    
  5. 分別進入三個zookeeper的data目錄,建立名為myid的檔案,裡內容是三個不同的數字(1~255),要和zook.cfg裡的server.x屬性相一一對應,用來標記不同的zookeepr節點。

在這裡插入圖片描述
6. 分別啟動三個zookeeper服務。

2.4 zookeeper指令

命名學習階段,我們使用單機模式即可,在學習zookeeper命令前先啟動zookeeper(詳見2.1節)。

啟動客戶端,在安裝目錄下,直接雙擊zkCli.cmd檔案,如下圖所示。
在這裡插入圖片描述

2.4.1 查詢所有命令(help)

在這裡插入圖片描述

2.4.2 查詢節點(ls)

在這裡插入圖片描述

2.4.3 建立節點(create)

在這裡插入圖片描述

2.4.4 查詢節點資料(get)

在這裡插入圖片描述

# ­­­­­­­­­­­節點的狀態資訊,也稱為stat結構體­­­­­­­­­­­­­­­­­­­
# 建立該znode的事務的zxid(ZooKeeper Transaction ID)
# 事務ID是ZooKeeper為每次更新操作/事務操作分配一個全域性唯一的id,表示zxid,值越小,表示越先執行
cZxid = 0x4454 # 0x0表示十六進位制數0
ctime = Thu Jan 01 08:00:00 CST 1970  # 建立時間
mZxid = 0x4454 						  # 最後一次更新的zxid
mtime = Thu Jan 01 08:00:00 CST 1970  # 最後一次更新的時間
pZxid = 0x4454 						  # 最後更新的子節點的zxid
cversion = 5 						  # 子節點的變化號,表示子節點被修改的次數
dataVersion = 0 					  # 當前節點的變化號,0表示從未被修改過
aclVersion = 0  					  # 訪問控制列表的變化號 access control list
# 如果臨時節點,表示當前節點的擁有者的sessionId
ephemeralOwner = 0x0				  # 如果不是臨時節點,則值為0
dataLength = 13 					  # 資料長度
numChildren = 1 					  # 子節點資料

2.4.5 建立有序點(create -s)

在這裡插入圖片描述

2.4.6 建立臨時節點(create -e)

在這裡插入圖片描述

關閉客戶端,再次開啟檢視 app3節點消失(不是立即消失,需要一點時間)

2.4.7 建立有序臨時節點(create -e -s)

在這裡插入圖片描述

2.4.8 修改節點資料(set)

在這裡插入圖片描述

2.4.9 刪除節點(delete)

在這裡插入圖片描述

2.4.10 遞迴刪除節點(rmr)

在這裡插入圖片描述

3 zookeeper的java客戶端

  • 原生Java API(不推薦使用)

​ ZooKeeper 原生Java API位於org.apache.ZooKeeper包中

​ ZooKeeper-3.x.x. Jar (這裡有多個版本)為官方提供的 java API

  • Apache Curator(推薦使用)

​ Apache Curator是 Apache ZooKeeper的Java客戶端庫。

​ Curator.專案的目標是簡化ZooKeeper客戶端的使用。

​ 例如,在以前的程式碼展示中,我們都要自己處理ConnectionLossException.

​ 另外 Curator為常見的分散式協同 服務提供了高質量的實現。
​ Apache Curator最初是Netfix研發的,後來捐獻了 Apache基金會,目前是 Apache的頂級專案

  • ZkClient(不推薦使用)

​ Github上一個開源的ZooKeeper客戶端,由datameer的工程師Stefan Groschupf和Peter Voss一起開發。 zkclient-x.x.Jar也是在源生 api 基礎之上進行擴充套件的開源 JAVA 客戶端。

3.1 初始化專案

  1. 使用Idea生成maven專案

    groupId: com.itheima.study.zk

    artifactId:statudy-zk

    version: 1.0-SNAPSHOT
    在這裡插入圖片描述
    在這裡插入圖片描述
    在這裡插入圖片描述

  2. 在pom檔案中加入Maven依賴,完整 的pom.xml檔案內容如下:

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.itheima.study.zk</groupId>
    <artifactId>statudy-zk</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <!--zookeeper的依賴-->
        <dependency>
            <groupId>org.apache.zookeeper</groupId>
            <artifactId>zookeeper</artifactId>
            <version>3.4.6</version>
        </dependency>
        <!--	zookeeper CuratorFramework 是Netflix公司開發一款連線zookeeper服務的框架,通過封裝的一套高階API 簡化了ZooKeeper的操作,提供了比較全面的功能,除了基礎的節點的操作,節點的監聽,還有叢集的連線以及重試。-->
        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-framework</artifactId>
            <version>4.0.1</version>
        </dependency>
        <!--    curator-recipes實現了zookeeper所具備的技巧(recipes)。例如:分散式鎖、佇列-->
        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-recipes</artifactId>
            <version>4.0.1</version>
        </dependency>
    </dependencies>

</project>

3.1 實戰1:ZK節點CRUD操作

  • CuratorFramework類是操作zk節點的入口,有以下幾個要點:

    • CuratorFramework使用fluent風格介面。
    • CuratorFramework使用CuratorFrameworkFactory進行分配。 它提供了工廠方法以及構造器建立例項。
    • CuratorFramework例項完全是執行緒安全的。
  • 下面是對zk節點增刪改查的操作:

com.itheima.study.zk.crud.CRUDDemo


/**
 * 對zk節點增刪改查操作
 */
public class CRUDDemo {
    public static void main(String[] args) throws Exception {

       //超時重連策略,每隔1s重試一次,共重試10次
        RetryPolicy retryPolicy = new RetryNTimes(10,1000);
        //構造zk客戶端
        CuratorFramework client = CuratorFrameworkFactory.newClient("127.0.0.1:2181",retryPolicy);
        //啟動client
        client.start();

        /查詢子節點///
        List<String> rootNodes = client.getChildren().forPath("/");
        System.out.println("[查詢子節點]根節點:"+rootNodes);
        /建立/
        //1-建立節點(父節點要先存在)
        client.create().forPath("/a");
        //2-建立節點且指定資料(父節點要先存在)
        client.create().forPath("/b","這是b節點的內容".getBytes());
        //3-迴圈建立,如果父節點不存在則自動建立
        client.create().creatingParentsIfNeeded().forPath("/c/d");
        //4-指定模式
        //CreateMode.PERSISTENT 永久節點(預設)
        client.create().creatingParentsIfNeeded().withMode(CreateMode.PERSISTENT).forPath("/e");
        //CreateMode.PERSISTENT_SEQUENTIAL 永久有序節點
        client.create().creatingParentsIfNeeded().withMode(CreateMode.PERSISTENT_SEQUENTIAL).forPath("/f");
        //CreateMode.EPHEMERAL  臨時節點
        client.create().creatingParentsIfNeeded().withMode(CreateMode.EPHEMERAL).forPath("/g");
        //CreateMode.EPHEMERAL_SEQUENTIAL 臨時有序節點
        client.create().creatingParentsIfNeeded().withMode(CreateMode.EPHEMERAL_SEQUENTIAL).forPath("/h");

        System.out.println("[建立]完成,根節點:"+client.getChildren().forPath("/"));
        System.out.println("[建立]完成,輸入任何字元繼續……");
        System.in.read();

        /查詢//
        byte[] values = client.getData().forPath("/b");
        System.out.println("[查詢]path:/b,value:"+new String(values));

        修改
        client.setData().forPath("/b","這是修改後的b節點內容".getBytes());
        System.out.println("[修改]完成,path:/b,value:"+new String(client.getData().forPath("/b")));

        ///刪除
        //deletingChildrenIfNeeded - 如果有未刪除的子節點則先刪除子節點
        client.delete().deletingChildrenIfNeeded().forPath("/a");
        client.delete().deletingChildrenIfNeeded().forPath("/b");
        client.delete().deletingChildrenIfNeeded().forPath("/c");
        client.delete().deletingChildrenIfNeeded().forPath("/e");
        System.out.println("[刪除]完成,根節點:"+client.getChildren().forPath("/"));

        //關閉客戶端
        client.close();
    }

}

  • 執行結果如下圖所示
    在這裡插入圖片描述

3.2 實戰2:zNode監聽(watches)

Curator引入了Cache的概念用來實現對ZooKeeper伺服器端進行事件監聽。共有三種Cache:

  • NodeCache 可以監聽節點資料變化
  • PathChildrenCache 可以監聽子節點變化
  • TreeCache 可以監聽節點及子節點變化

這裡以NodeCache為例,寫一個監聽節點資料變化的例子。程式碼如下:

com.itheima.study.zk.cache.NodeCacheDemo

/**
 * 監聽節點變化例項
 */
public class NodeCacheDemo {
    public static void main(String[] args) throws Exception {
        //超時重連策略,每1000毫秒重試一次,重試10次
        RetryPolicy retryPolicy = new RetryNTimes(10, 1000);
        //建立客戶端
        CuratorFramework client = CuratorFrameworkFactory.newClient("127.0.0.1:2181", retryPolicy);
        //開啟客戶端
        client.start();

        //構造nodeCache
        NodeCache nodeCache = new NodeCache(client, "/hello/a");
        //開始監聽
        nodeCache.start(true);
        //設定監聽回撥物件
        nodeCache.getListenable().addListener(new NodeCacheListener() {
            @Override
            public void nodeChanged() throws Exception {
                ChildData currentData = nodeCache.getCurrentData();
                if (null == currentData) {
                    System.out.println("[資料監聽]節點刪除:" + nodeCache.getPath());
                } else {
                    System.out.println("[資料監聽]" + nodeCache.getPath() + ":" + new String(currentData.getData()));
                }
            }
        });
        System.in.read();
      
        //關閉client
        client.close();
    }
}

執行時開啟zkCli.cmd修改/hello/a的data,再刪除節點,更新執行結果如下:

在這裡插入圖片描述

4 zookeeper使用場景

4.1 分散式鎖

4.1.1 為什麼使用分散式鎖

一個方法在高併發情況下的同一時間只能被同一個執行緒執行,在傳統單體應用單機部署的情況下,可以使用Java併發處理相關的API(如ReentrantLcok或synchronized)進行互斥控制。但是,隨著業務發展的需要,原單體單機部署的系統被演化成分散式系統後,由於分散式系統多執行緒、多程式並且分佈在不同機器上,這將使原單機部署情況下的併發控制鎖策略失效,為了解決這個問題就需要一種跨JVM的互斥機制來控制共享資源的訪問,這就是分散式鎖要解決的問題.
在這裡插入圖片描述

4.1.2 基於zookeeper的分散式鎖原理

一個分散式鎖對應ZooKeeper的一個節點,每個需要獲取這個分散式鎖的客戶端執行緒在這個節點下建立一個臨時有序節點,此時有兩種情況:

  1. 建立的臨時順序節點是資料夾下的第一個節點,則認為是獲取分散式鎖成功。

  2. 建立的臨時順序節點不是資料夾下的第一個節點,則認為當前鎖已經被另一個客戶端執行緒獲取,此時需要進入阻塞狀態,等待節點順序中的前一個節點釋放鎖的時候喚醒當前執行緒

在這裡插入圖片描述

4.2 配置中心

對於專案中用到的配置資訊(例如資料庫地址、使用者名稱、密碼……)一般有兩種處理方式:

  • 直接放到專案配置檔案裡,會有如下缺點:
    • 容易導致敏感資訊洩露(例如線上的使用者名稱、密碼……)
    • 一但配置資訊變化(例如資料庫密碼變化),需要重新打包上線
  • 配置中心,將配置資訊維護到配置中心裡
    • 流程:
      • 服務啟動的時候直接來配置中心獲取配置資訊
      • 配置資訊變化時直接修改配置中心裡的資料,配置中心將新的配置資訊推送給服務(tomcat),無需重啟服務。
        在這裡插入圖片描述

4.3 註冊中心

在生產環境中需要在一個業務伺服器(例如tomcat)中呼叫另一個業務伺服器的介面(例如HTTP介面),那麼就需要知道被呼叫方的IP地址和埠我們有三種方案:

  • IP&埠寫死在專案裡(不推薦)

    • 優點:簡單直接。
    • 缺點:如果被呼叫方IP地址和埠發生變化或者部署節點的數量發生變化,就需要修改呼叫方程式碼重新上線。

在這裡插入圖片描述

  • 類比:

    • app1–>同學A
    • app2–>同學B
    • IP+埠–>電話號碼
    • 呼叫–>同學A撥打同學B的電話
  • 反向代理伺服器,例如nginx

    • 優點:呼叫方只需要配置Nginx地址,無需關心被呼叫用者真實的IP地址和埠。

在這裡插入圖片描述

  • 類比:

    • nginx --> 班長
    • 反向代理伺服器轉發–>同學A讓班長捎話給同學B
  • 註冊中心,例如zookeeper

    • 優點:呼叫方無需配置被呼叫方的IP地址,當被呼叫方的資訊變化可以自動更新。
    • 流程:
      • 註冊,app2啟動時將IP、埠等資訊傳送到註冊中心
      • app1啟動時去註冊中心拉取app2的資訊(IP、埠等資訊)
      • 發起呼叫(例如HTTP請求)
    • 分散式框架dubbo推薦使用zookeeepr作為註冊中心。

在這裡插入圖片描述

  • 類比:
    • 註冊中心 --> 班主任
    • 註冊 -->所有同學在入學的時候都將手機號告訴班主任
    • 拉取目標伺服器資訊再發起呼叫–>同學A找班主任要同學B的電話號碼再撥打電話

4.4 其他

除了上面列舉的三種常見使用場景,zookeeper還可以實現名稱伺服器、佇列、barrier(柵欄)、原子型別、選舉等功能,在此不再贅述。

5 應用場景實戰:基於ZK的分散式鎖

5.1 核心類

在這裡插入圖片描述

5.2 實戰場景

這裡我們模擬修改使用者積分(score)的操作,如果多個執行緒同時修改一個使用者的積分就會有併發問題,因此需要加入鎖來進行控制。

【詳見4.1.1】

5.3 程式碼實現

  1. 建立package: com.itheima.study.zk.lock
  2. 建立UserDao,提供getScoreFromDb和updateScore兩個方法
  3. 建立DistributedLockDemo類以及main方法
  4. 完善main方法
    1. 例項化zookeeper客戶端CuratorFramework
    2. 例項化分散式鎖InterProcessLock
    3. 例項化UserDao
    4. 併發執行100遍修改使用者積分(score)操作
    5. 輸出使用者最終的積分(score)
  5. 執行檢視結果
  6. 去除程式碼中鎖的邏輯再次執行並檢視結果
  • com.itheima.study.zk.lock.UserService
/**
 * 使用者DAO層,模擬讀寫資料庫
 */
public class UserDao {
	
    private int score = 0;

    /**
     * 模擬從資料庫獲取使用者積分
     * @return
     */
    public int getScoreFromDb() {
        //模擬網路請求耗時1毫秒
        try {
            Thread.sleep(1L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //查徐資料
        return score;
    }

    /**
     * 模擬更新資料庫裡的使用者積分
     * @param score
     */
    public void updateScore(int score) {
        //模擬網路請求耗時1毫秒
        try {
            Thread.sleep(1L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.score = score;
    }
}

  • com.itheima.study.zk.lock.DistributedLockDemo
 /**
 * curator實現的分散式鎖使用例項
 */
public class DistributedLockDemo {

    public static void main(String[] args) throws InterruptedException {
        //構造超時重試策略,每1s重試一次,重試10次
        RetryPolicy retryPolicy = new RetryNTimes(10,1000);
        //構造客戶端
        CuratorFramework client = CuratorFrameworkFactory.newClient("localhost:2181",retryPolicy);
        // 啟動客戶端
        client.start();

        //構造鎖
        InterProcessLock lock = new InterProcessMutex(client,"/user/1/update");

        //構造userDao
        UserDao userDao = new UserDao();

        //併發修改100遍
        for (int i = 0; i < 100; i++) {
            new Thread((new Runnable() {
                @Override
                public void run() {
                    try {
                        lock.acquire();
                        // 查詢當前積分
                        int score = userDao.getScoreFromDb();
                        // 積分加1
                        score++;
                        //更新資料庫
                        userDao.updateScore(score);
                    } catch (Exception e) {
                        e.printStackTrace();
                    } finally {
                        try {
                            lock.release();
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                }
            })).start();
        }
        //隨眠5s等待任務執行完成
        Thread.sleep(5000L);
        //輸出最終的結果
        System.out.println("完成,結果:"+userDao.getScoreFromDb());
           
        //關閉client
        client.close();
    }
}
  • 輸出結果

在這裡插入圖片描述

  • 註釋掉程式碼中獲取鎖和釋放鎖的邏輯
    在這裡插入圖片描述

  • 再次執行,執行的結果就不是100了,而且每次都不同,這就是併發問題導致的。
    在這裡插入圖片描述

相關文章