Spring Cloud 系列之 ZooKeeper 註冊中心
阿新 • • 發佈:2020-08-06
## 什麼是註冊中心
服務註冊中心是服務實現服務化管理的核心元件,類似於目錄服務的作用,主要用來儲存服務資訊,譬如提供者 url 串、路由資訊等。服務註冊中心是微服務架構中最基礎的設施之一。
註冊中心可以說是微服務架構中的“通訊錄”,它記錄了服務和服務地址的對映關係。在分散式架構中,服務會註冊到這裡,當服務需要呼叫其它服務時,就到這裡找到服務的地址,進行呼叫。
簡單理解就是:在沒有註冊中心時候,服務間呼叫需要知道被當服務調方的具體地址(寫死的 ip:port)。更換部署地址,就不得不修改呼叫當中指定的地址。而有了註冊中心之後,每個服務在呼叫別人的時候只需要知道服務名稱(軟編碼)就好,地址都會通過註冊中心根據服務名稱獲取到具體的服務地址進行呼叫。
![](https://mrhelloworld.com/resources/articles/spring/spring-cloud/zookeeper/timg.jpg " ")
舉個現實生活中的例子,比如說,我們手機中的通訊錄的兩個使用場景:
> 當我想給張三打電話時,那我需要在通訊錄中按照名字找到張三,然後就可以找到他的手機號撥打電話。—— 服務發現
>
> 李四辦了手機號並把手機號告訴了我,我把李四的號碼存進通訊錄,後續,我就可以從通訊錄找到他。—— 服務註冊
>
> 通訊錄 —— ?什麼角色(服務註冊中心)
總結:服務註冊中心的作用就是**服務的註冊**和**服務的發現**。
## 常見的註冊中心
- Netflix Eureka
- Alibaba Nacos
- HashiCorp Consul
- Apache ZooKeeper
- CoreOS Etcd
- CNCF CoreDNS
| 特性 | Eureka | Nacos | Consul | Zookeeper |
| :-------------- | :---------- | :------------------------- | :---------------- | :--------- |
| CAP | AP | CP + AP | CP | CP |
| 健康檢查 | Client Beat | TCP/HTTP/MYSQL/Client Beat | TCP/HTTP/gRPC/Cmd | Keep Alive |
| 雪崩保護 | 有 | 有 | 無 | 無 |
| 自動登出例項 | 支援 | 支援 | 不支援 | 支援 |
| 訪問協議 | HTTP | HTTP/DNS | HTTP/DNS | TCP |
| 監聽支援 | 支援 | 支援 | 支援 | 支援 |
| 多資料中心 | 支援 | 支援 | 支援 | 不支援 |
| 跨註冊中心同步 | 不支援 | 支援 | 支援 | 不支援 |
| SpringCloud整合 | 支援 | 支援 | 支援 | 支援 |
## CAP 原則與 BASE 理論
### CAP 原則
![](https://mrhelloworld.com/resources/articles/spring/spring-cloud/zookeeper/cap.jpg " ")
CAP 原則又稱 CAP 定理,指的是在一個分散式系統中, Consistency(一致性)、 Availability(可用性)、Partition tolerance(分割槽容錯性),三者不可得兼。
CAP 由 Eric Brewer 在 2000 年 PODC 會議上提出。該猜想在提出兩年後被證明成立,成為我們熟知的 CAP 定理。CAP 三者不可兼得。
| 特性 | 定理 |
| ------------------- | ------------------------------------------------------------ |
| Consistency | 也叫做資料原子性,系統在執行某項操作後仍然處於一致的狀態。在分散式系統中,更新操作執行成功後所有的使用者都應該讀到最新的值,這樣的系統被認為是具有強一致性的。等同於所有節點訪問同一份最新的資料副本。 |
| Availability | 每一個操作總是能夠在一定的時間內返回結果,這裡需要注意的是"一定時間內"和"返回結果"。一定時間內指的是,在可以容忍的範圍內返回結果,結果可以是成功或者是失敗。 |
| Partition tolerance | 在網路分割槽的情況下,被分隔的節點仍能正常對外提供服務(分散式叢集,資料被分佈儲存在不同的伺服器上,無論什麼情況,伺服器都能正常被訪問)。 |
#### 取捨策略
CAP 三個特性只能滿足其中兩個,那麼取捨的策略就共有三種:
- **CA without P**:如果不要求P(不允許分割槽),則C(強一致性)和A(可用性)是可以保證的。但放棄 P 的同時也就意味著放棄了系統的擴充套件性,也就是分散式節點受限,沒辦法部署子節點,這是違背分散式系統設計的初衷的。
- **CP without A**:如果不要求A(可用),相當於每個請求都需要在伺服器之間保持強一致,而P(分割槽)會導致同步時間無限延長(也就是等待資料同步完才能正常訪問服務),一旦發生網路故障或者訊息丟失等情況,就要犧牲使用者的體驗,等待所有資料全部一致了之後再讓使用者訪問系統。設計成 CP 的系統其實不少,最典型的就是分散式資料庫,如 Redis、HBase 等。對於這些分散式資料庫來說,資料的一致性是最基本的要求,因為如果連這個標準都達不到,那麼直接採用關係型資料庫就好,沒必要再浪費資源來部署分散式資料庫。
- **AP without C**:要高可用並允許分割槽,則需放棄一致性。一旦分割槽發生,節點之間可能會失去聯絡,為了高可用,每個節點只能用本地資料提供服務,而這樣會導致全域性資料的不一致性。典型的應用就如某米的搶購手機場景,可能前幾秒你瀏覽商品的時候頁面提示是有庫存的,當你選擇完商品準備下單的時候,系統提示你下單失敗,商品已售完。這其實就是先在 A(可用性)方面保證系統可以正常的服務,然後在資料的一致性方面做了些犧牲,雖然多少會影響一些使用者體驗,但也不至於造成使用者購物流程的嚴重阻塞。
#### 總結
現如今,對於多數大型網際網路應用的場景,主機眾多、部署分散,而且現在的叢集規模越來越大,節點只會越來越多,所以節點故障、網路故障是常態,因此分割槽容錯性也就成為了一個分散式系統必然要面對的問題。那麼就只能在 C 和 A 之間進行取捨。但對於傳統的專案就可能有所不同,拿銀行的轉賬系統來說,涉及到金錢的對於資料一致性不能做出一絲的讓步,C 必須保證,出現網路故障的話,寧可停止服務,可以在 A 和 P 之間做取捨。
總而言之,沒有最好的策略,好的系統應該是根據業務場景來進行架構設計的,只有適合的才是最好的。
### BASE 理論
CAP 理論已經提出好多年了,難道真的沒有辦法解決這個問題嗎?也許可以做些改變。比如 C 不必使用那麼強的一致性,可以先將資料存起來,稍後再更新,實現所謂的 “最終一致性”。
這個思路又是一個龐大的問題,同時也引出了第二個理論 BASE 理論。
> BASE:全稱 Basically Available(基本可用),Soft state(軟狀態),和 Eventually consistent(最終一致性)三個短語的縮寫,來自 ebay 的架構師提出。
BASE 理論是對 CAP 中一致性和可用性權衡的結果,其來源於對大型網際網路分散式實踐的總結,是基於 CAP 定理逐步演化而來的。其核心思想是:
> 既然無法做到強一致性(Strong consistency),但每個應用都可以根據自身的業務特點,採用適當的方式來使系統達到最終一致性(Eventual consistency)。
#### Basically Available(基本可用)
基本可用是指分散式系統在出現故障的時候,允許損失部分可用性(例如響應時間、功能上的可用性)。需要注意的是,基本可用絕不等價於系統不可用。
- 響應時間上的損失:正常情況下搜尋引擎需要在 0.5 秒之內返回給使用者相應的查詢結果,但由於出現故障(比如系統部分機房發生斷電或斷網故障),查詢結果的響應時間增加到了 1~2 秒。
- 功能上的損失:購物網站在購物高峰(如雙十一)時,為了保護系統的穩定性,部分消費者可能會被引導到一個降級頁面。
#### Soft state(軟狀態)
什麼是軟狀態呢?相對於原子性而言,要求多個節點的資料副本都是一致的,這是一種 “硬狀態”。
軟狀態是指允許系統存在中間狀態,而該中間狀態不會影響系統整體可用性。分散式儲存中一般一份資料會有多個副本,允許不同副本資料同步的延時就是軟狀態的體現。
#### Eventually consistent(最終一致性)
系統不可能一直是軟狀態,必須有個時間期限。在期限過後,應當保證所有副本保持資料一致性。從而達到資料的最終一致性。這個時間期限取決於網路延時,系統負載,資料複製方案設計等等因素。
實際上,不只是分散式系統使用最終一致性,關係型資料庫在某個功能上,也是使用最終一致性的,比如備份,資料庫的複製都是需要時間的,這個複製過程中,業務讀取到的值就是舊值。當然,最終還是達成了資料一致性。這也算是一個最終一致性的經典案例。
#### 總結
總的來說,BASE 理論面向的是大型高可用可擴充套件的分散式系統,和傳統事務的 ACID 是相反的,它完全不同於 ACID 的強一致性模型,而是通過犧牲強一致性來獲得可用性,並允許資料在一段時間是不一致的。
## 為什麼需要註冊中心
瞭解了什麼是註冊中心,那麼我們繼續談談,為什麼需要註冊中心。在分散式系統中,我們不僅僅是需要在註冊中心找到服務和服務地址的對映關係這麼簡單,我們還需要考慮更多更復雜的問題:
- 服務註冊後,如何被及時發現
- 服務宕機後,如何及時下線
- 服務如何有效的水平擴充套件
- 服務發現時,如何進行路由
- 服務異常時,如何進行降級
- 註冊中心如何實現自身的高可用
這些問題的解決都依賴於註冊中心。簡單看,註冊中心的功能有點類似於 DNS 伺服器或者負載均衡器,而實際上,註冊中心作為微服務的基礎元件,可能要更加複雜,也需要更多的靈活性和時效性。所以我們還需要學習更多 Spring Cloud 微服務元件協同完成應用開發。
註冊中心解決了以下問題:
- 服務管理
- 服務之間的自動發現
- 服務的依賴關係管理
## Zookeeper 介紹
![](https://mrhelloworld.com/resources/articles/spring/spring-cloud/zookeeper/image-20200726172810833.png " ")
Apache ZooKeeper 是一個開放原始碼的分散式應用程式協調元件,是 Hadoop 和 Hbase 的重要元件。它是一個為分散式應用提供一致性服務的軟體,提供的功能包括:配置維護、域名服務、分散式同步、組服務等。
在微服務專案開發中 ZooKeeper 主要的角色是當做服務註冊中心存在,我們將編寫好的服務註冊至 ZooKeeper 即可。
## ZooKeeper 安裝
### 環境準備
ZooKeeper 在 Java 中執行,版本 1.8 或更高(JDK 8 LTS,JDK 11 LTS,JDK 12 - Java 9 和 10 不支援)
### 下載
ZooKeeper 下載地址:
- https://zookeeper.apache.org/releases.html#download
- https://archive.apache.org/dist/zookeeper/
### 安裝
將檔案上傳至 Linux 伺服器。
#### 單機版
##### 建立目錄/解壓
建立 zookeeper 目錄。
```bash
mkdir -p /usr/local/zookeeper
```
將檔案解壓至該目錄。
```bash
tar -zxvf apache-zookeeper-3.6.1-bin.tar.gz -C /usr/local/zookeeper/
```
建立資料目錄、日誌目錄。
```bash
mkdir -p /usr/local/zookeeper/apache-zookeeper-3.6.1-bin/data
mkdir -p /usr/local/zookeeper/apache-zookeeper-3.6.1-bin/log
```
##### 修改配置檔案
```bash
# 進入配置檔案目錄
cd /usr/local/zookeeper/apache-zookeeper-3.6.1-bin/conf/
# ZooKeeper 啟動預設載入名為 zoo.cfg 的配置檔案,複製一份命名為 zoo.cfg
cp zoo_sample.cfg zoo.cfg
# 修改配置檔案
vi zoo.cfg
```
主要修改資料目錄`dataDir`、日誌目錄`dataLogDir`兩處即可,修改結果如下:
```properties
# The number of milliseconds of each tick
tickTime=2000
# The number of ticks that the initial
# synchronization phase can take
initLimit=10
# The number of ticks that can pass between
# sending a request and getting an acknowledgement
syncLimit=5
# the directory where the snapshot is stored.
# do not use /tmp for storage, /tmp here is just
# example sakes.
dataDir=/usr/local/zookeeper/apache-zookeeper-3.6.1-bin/data
dataLogDir=/usr/local/zookeeper/apache-zookeeper-3.6.1-bin/log
# the port at which the clients will connect
clientPort=2181
# the maximum number of client connections.
# increase this if you need to handle more clients
#maxClientCnxns=60
#
# Be sure to read the maintenance section of the
# administrator guide before turning on autopurge.
#
# http://zookeeper.apache.org/doc/current/zookeeperAdmin.html#sc_maintenance
#
# The number of snapshots to retain in dataDir
#autopurge.snapRetainCount=3
# Purge task interval in hours
# Set to "0" to disable auto purge feature
#autopurge.purgeInterval=1
## Metrics Providers
#
# https://prometheus.io Metrics Exporter
#metricsProvider.className=org.apache.zookeeper.metrics.prometheus.PrometheusMetricsProvider
#metricsProvider.httpPort=7000
#metricsProvider.exportJvmInfo=true
```
##### 啟動/關閉
啟動。
```bash
cd /usr/local/zookeeper/apache-zookeeper-3.6.1-bin/
bin/zkServer.sh start
---------------------------------------------------------------------------------
ZooKeeper JMX enabled by default
Using config: /usr/local/zookeeper/apache-zookeeper-3.6.1-bin/bin/../conf/zoo.cfg
Starting zookeeper ... STARTED
```
關閉。
```shell
cd /usr/local/zookeeper/apache-zookeeper-3.6.1-bin/
bin/zkServer.sh stop
---------------------------------------------------------------------------------
ZooKeeper JMX enabled by default
Using config: /usr/local/zookeeper/apache-zookeeper-3.6.1-bin/bin/../conf/zoo.cfg
Stopping zookeeper ... STOPPED
```
#### 叢集版
再準備兩臺機器,和剛才單機的機器加一起構成一個叢集環境(如果電腦跑不動就改為一臺機器跑三個程序的方式)。
##### 建立目錄/解壓
建立 zookeeper 目錄。
```bash
mkdir -p /usr/local/zookeeper
```
將檔案解壓至該目錄。
```bash
tar -zxvf apache-zookeeper-3.6.1-bin.tar.gz -C /usr/local/zookeeper/
```
建立資料目錄、日誌目錄。
```bash
mkdir -p /usr/local/zookeeper/apache-zookeeper-3.6.1-bin/data
mkdir -p /usr/local/zookeeper/apache-zookeeper-3.6.1-bin/log
```
##### myid 檔案
在 data 目錄下建立 myid 檔案,檔案中就只寫個 `1` 即可,其他兩個機器分別寫 `2` 和 `3`。
```bash
cd /usr/local/zookeeper/apache-zookeeper-3.6.1-bin/data/
vi myid
```
##### 修改配置檔案
```bash
# 進入配置檔案目錄
cd /usr/local/zookeeper/apache-zookeeper-3.6.1-bin/conf/
# zookeeper 啟動預設載入名為 zoo.cfg 的配置檔案,所以複製一份命名為 zoo.cfg
cp zoo_sample.cfg zoo.cfg
# 修改配置檔案
vi zoo.cfg
```
主要修改:
- 資料目錄`dataDir`
- 日誌目錄`dataLogDir`
- 埠`clientPort`(如果是一臺機器的偽叢集,需要修改 2181 埠,比如:2181、2182、2183)
- 叢集配置(如果是一臺機器的偽叢集,需要修改 2888 和 3888 的埠,比如:2888、2889、2890 和 3888、3889、3890)
```properties
# The number of milliseconds of each tick
tickTime=2000
# The number of ticks that the initial
# synchronization phase can take
initLimit=10
# The number of ticks that can pass between
# sending a request and getting an acknowledgement
syncLimit=5
# the directory where the snapshot is stored.
# do not use /tmp for storage, /tmp here is just
# example sakes.
dataDir=/usr/local/zookeeper/apache-zookeeper-3.6.1-bin/data
dataLogDir=/usr/local/zookeeper/apache-zookeeper-3.6.1-bin/log
# the port at which the clients will connect
clientPort=2181
# the maximum number of client connections.
# increase this if you need to handle more clients
#maxClientCnxns=60
#
# Be sure to read the maintenance section of the
# administrator guide before turning on autopurge.
#
# http://zookeeper.apache.org/doc/current/zookeeperAdmin.html#sc_maintenance
#
# The number of snapshots to retain in dataDir
#autopurge.snapRetainCount=3
# Purge task interval in hours
# Set to "0" to disable auto purge feature
#autopurge.purgeInterval=1
## Metrics Providers
#
# https://prometheus.io Metrics Exporter
#metricsProvider.className=org.apache.zookeeper.metrics.prometheus.PrometheusMetricsProvider
#metricsProvider.httpPort=7000
#metricsProvider.exportJvmInfo=true
# 叢集配置
# server.1 中的 1 是 myid 檔案中的內容,2888 用於叢集內部通訊,3888 用於選擇 leader
server.1=192.168.10.101:2888:3888
server.2=192.168.10.102:2888:3888
server.3=192.168.10.103:2888:3888
```
##### 啟動/關閉
啟動。
```bash
cd /usr/local/zookeeper/apache-zookeeper-3.6.1-bin/
bin/zkServer.sh start
#################################################################################
ZooKeeper JMX enabled by default
Using config: /usr/local/zookeeper/apache-zookeeper-3.6.1-bin/bin/../conf/zoo.cfg
Starting zookeeper ... STARTED
```
關閉。
```shell
cd /usr/local/zookeeper/apache-zookeeper-3.6.1-bin/
bin/zkServer.sh stop
#################################################################################
ZooKeeper JMX enabled by default
Using config: /usr/local/zookeeper/apache-zookeeper-3.6.1-bin/bin/../conf/zoo.cfg
Stopping zookeeper ... STOPPED
```
##### 叢集狀態檢視
```shell
cd /usr/local/zookeeper/apache-zookeeper-3.6.1-bin/
bin/zkServer.sh status
################################ 192.168.10.101 ################################
ZooKeeper JMX enabled by default
Using config: /usr/local/zookeeper/apache-zookeeper-3.6.1-bin/bin/../conf/zoo.cfg
Client port found: 2181. Client address: localhost.
Mode: follower
################################ 192.168.10.102 ################################
ZooKeeper JMX enabled by default
Using config: /usr/local/zookeeper/apache-zookeeper-3.6.1-bin/bin/../conf/zoo.cfg
Client port found: 2181. Client address: localhost.
Mode: leader
################################ 192.168.10.103 ################################
ZooKeeper JMX enabled by default
Using config: /usr/local/zookeeper/apache-zookeeper-3.6.1-bin/bin/../conf/zoo.cfg
Client port found: 2181. Client address: localhost.
Mode: follower
```
看到以上資訊說明 ZooKeeper 叢集環境已搭建成功,接下來就可以通過 RPC 框架對接 ZooKeeper,將 ZooKeeper 作為我們的註冊中心來使用。
## ZooKeeper 入門案例
`zookeeper-demo` 聚合工程。`SpringBoot 2.3.0.RELEASE`、`Spring Cloud Hoxton.SR5`。
### 建立專案
我們建立聚合專案來講解 ZooKeeper,首先建立一個 pom 父工程。
### 新增依賴
pom.xml
```xml
4.0.0
org.example
zookeeper-demo
1.0-SNAPSHOT
org.springframework.boot
spring-boot-starter-parent
2.3.0.RELEASE
Hoxton.SR5
org.springframework.cloud
spring-cloud-dependencies
${spring-cloud.version}
pom
import
```
### 商品服務 product-service
#### 建立專案
在剛才的父工程下建立一個 `product-service` 專案。
#### 新增依賴
主要新增 `spring-cloud-starter-zookeeper-discovery` 依賴。
```xml
org.springframework.cloud
spring-cloud-starter-zookeeper-discovery
```
完整依賴如下:
```xml
zookeeper-demo
org.example
1.0-SNAPSHOT
4.0.0
product-service
org.springframework.cloud
spring-cloud-starter-zookeeper-discovery
org.springframework.boot
spring-boot-starter-web
org.projectlombok
lombok
provided
org.springframework.boot
spring-boot-starter-test
test
org.junit.vintage
junit-vintage-engine
```
#### 配置檔案
application.yml
```yml
server:
port: 7070 # 埠
spring:
application:
name: product-service # 應用名稱
# 配置 ZooKeeper 註冊中心
cloud:
zookeeper:
discovery:
enabled: true # 如果不想使用 ZooKeeper 進行服務註冊和發現,設定為 false 即可
connect-string: 192.168.10.101:2181,192.168.10.102:2181,192.168.10.103:2181
```
#### 實體類
Product.java
```java
package org.example.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Product implements Serializable {
private Integer id;
private String productName;
private Integer productNum;
private Double productPrice;
}
```
#### 編寫服務
ProductService.java
```java
package org.example.service;
import org.example.pojo.Product;
import java.util.List;
/**
* 商品服務
*/
public interface ProductService {
/**
* 查詢商品列表
*
* @return
*/
List selectProductList();
}
```
ProductServiceImpl.java
```java
package org.example.service.impl;
import lombok.extern.slf4j.Slf4j;
import org.example.pojo.Product;
import org.example.service.ProductService;
import org.springframework.stereotype.Service;
import java.util.Arrays;
import java.util.List;
/**
* 商品服務
*/
@Slf4j
@Service
public class ProductServiceImpl implements ProductService {
/**
* 查詢商品列表
*
* @return
*/
@Override
public List selectProductList() {
log.info("商品服務查詢商品資訊...");
return Arrays.asList(
new Product(1, "華為手機", 1, 5800D),
new Product(2, "聯想筆記本", 1, 6888D),
new Product(3, "小米平板", 5, 2020D)
);
}
}
```
#### 控制層
ProductController.java
```java
package org.example.controller;
import org.example.pojo.Product;
import org.example.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/product")
public class ProductController {
@Autowired
private ProductService productService;
/**
* 查詢商品列表
*
* @return
*/
@GetMapping("/list")
public List selectProductList() {
return productService.selectProductList();
}
}
```
> 該專案我們可以通過單元測試進行測試,也可以直接通過 url 使用 postman 或者瀏覽器來進行測試。
#### 啟動類
通過 Spring Cloud 原生註解 `@EnableDiscoveryClient` 開啟服務註冊發現功能。
```java
package org.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
// 開啟 @EnableDiscoveryClient 註解,當前版本預設會開啟該註解
//@EnableDiscoveryClient
@SpringBootApplication
public class ProductServiceApplication {
public static void main(String[] args) {
SpringApplication.run(ProductServiceApplication.class, args);
}
}
```
### 訂單服務 order-service
#### 建立專案
在剛才的父工程下建立一個 `order-service` 專案。
#### 新增依賴
pom.xml
```xml
zookeeper-demo
org.example
1.0-SNAPSHOT
4.0.0
order-service
org.springframework.cloud
spring-cloud-starter-zookeeper-discovery
org.springframework.boot
spring-boot-starter-web
org.projectlombok
lombok
provided
org.springframework.boot
spring-boot-starter-test
test
org.junit.vintage
junit-vintage-engine
```
#### 配置檔案
application.yml
```yml
server:
port: 9090 # 埠
spring:
application:
name: order-service # 應用名稱
# 配置 ZooKeeper 註冊中心
cloud:
zookeeper:
discovery:
enabled: true # 如果不想使用 ZooKeeper 進行服務註冊和發現,設定為 false 即可
connect-string: 192.168.10.101:2181,192.168.10.102:2181,192.168.10.103:2181
```
#### 實體類
Product.java
```java
package org.example.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Product implements Serializable {
private Integer id;
private String productName;
private Integer productNum;
private Double productPrice;
}
```
Order.java
```java
package org.example.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.List;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Order implements Serializable {
private Integer id;
private String orderNo;
private String orderAddress;
private Double totalPrice;
private List productList;
}
```
#### 消費服務
OrderService.java
```java
package org.example.service;
import org.example.pojo.Order;
public interface OrderService {
/**
* 根據主鍵查詢訂單
*
* @param id
* @return
*/
Order selectOrderById(Integer id);
}
```
對於服務的消費我們這裡講三種實現方式:
- DiscoveryClient:通過元資料獲取服務資訊
- LoadBalancerClient:Ribbon 的負載均衡器
- @LoadBalanced:通過註解開啟 Ribbon 的負載均衡器
##### DiscoveryClient
Spring Boot 不提供任何自動配置的`RestTemplate` bean,所以需要在啟動類中注入 `RestTemplate`。
```java
package org.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
// 開啟 @EnableDiscoveryClient 註解,當前版本預設會開啟該註解
//@EnableDiscoveryClient
@SpringBootApplication
public class OrderServiceApplication {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
}
```
OrderServiceImpl.java
```java
package org.example.service.impl;
import lombok.extern.slf4j.Slf4j;
import org.example.pojo.Order;
import org.example.pojo.Product;
import org.example.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.web.client.RestTemplate;
import java.util.List;
@Slf4j
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private RestTemplate restTemplate;
@Autowired
private DiscoveryClient discoveryClient;
/**
* 根據主鍵查詢訂單
*
* @param id
* @return
*/
@Override
public Order selectOrderById(Integer id) {
log.info("訂單服務查詢訂單資訊...");
return new Order(id, "order-001", "中國", 22788D,
selectProductListByDiscoveryClient());
}
private List selectProductListByDiscoveryClient() {
StringBuffer sb = null;
// 獲取服務列表
List serviceIds = discoveryClient.getServices();
if (CollectionUtils.isEmpty(serviceIds))
return null;
// 根據服務名稱獲取服務
List serviceInstances = discoveryClient.getInstances("service-provider");
if (CollectionUtils.isEmpty(serviceInstances))
return null;
ServiceInstance si = serviceInstances.get(0);
sb = new StringBuffer();
sb.append("http://" + si.getHost() + ":" + si.getPort() + "/product/list");
log.info("訂單服務呼叫商品服務...");
log.info("從註冊中心獲取到的商品服務地址為:{}", sb.toString());
// ResponseEntity: 封裝了返回資料
ResponseEntity
- > response = restTemplate.exchange(
sb.toString(),
HttpMethod.GET,
null,
new ParameterizedTypeReference
- >() {});
log.info("商品資訊查詢結果為:{}", response.getBody());
return response.getBody();
}
}
```
##### LoadBalancerClient
OrderServiceImpl.java
```java
package org.example.service.impl;
import lombok.extern.slf4j.Slf4j;
import org.example.pojo.Order;
import org.example.pojo.Product;
import org.example.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.util.List;
@Slf4j
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private RestTemplate restTemplate;
@Autowired
private LoadBalancerClient loadBalancerClient; // Ribbon 負載均衡器
/**
* 根據主鍵查詢訂單
*
* @param id
* @return
*/
@Override
public Order selectOrderById(Integer id) {
log.info("訂單服務查詢訂單資訊...");
return new Order(id, "order-001", "中國", 22788D,
selectProductListByLoadBalancerClient());
}
private List
- > response = restTemplate.exchange(
sb.toString(),
HttpMethod.GET,
null,
new ParameterizedTypeReference
- >() {});
log.info("商品資訊查詢結果為:{}", response.getBody());
return response.getBody();
}
}
```
##### @LoadBalanced
啟動類注入 `RestTemplate` 時新增 `@LoadBalanced` 負載均衡註解,表示這個 `RestTemplate` 在請求時擁有客戶端負載均衡的能力。
```java
package com.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@EnableDiscoveryClient
@SpringBootApplication
public class OrderServiceApplication {
@Bean
@LoadBalanced // 負載均衡註解
public RestTemplate restTemplate() {
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
}
```
OrderServiceImpl.java
```java
package org.example.service.impl;
import lombok.extern.slf4j.Slf4j;
import org.example.pojo.Order;
import org.example.pojo.Product;
import org.example.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.util.List;
@Slf4j
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private RestTemplate restTemplate;
/**
* 根據主鍵查詢訂單
*
* @param id
* @return
*/
@Override
public Order selectOrderById(Integer id) {
log.info("訂單服務查詢訂單資訊...");
return new Order(id, "order-001", "中國", 22788D,
selectProductListByLoadBalancerAnnotation());
}
private List
- > response = restTemplate.exchange(
url,
HttpMethod.GET,
null,
new ParameterizedTypeReference
- >() {});
log.info("商品資訊查詢結果為:{}", response.getBody());
return response.getBody();
}
}
```
#### 控制層
OrderController.java
```java
package org.example.controller;
import org.example.pojo.Order;
import org.example.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/order")
public class OrderController {
@Autowired
private OrderService orderService;
/**
* 根據主鍵查詢訂單
*
* @param id
* @return
*/
@GetMapping("/{id}")
public Order selectOrderById(@PathVariable("id") Integer id) {
return orderService.selectOrderById(id);
}
}
```
#### 訪問
訪問:http://localhost:9090/order/1 結果如下:
![](https://mrhelloworld.com/resources/articles/spring/spring-cloud/zookeeper/image-20200429164343155.png " ")
使用 ZooKeeper 圖形化的客戶端工具 `ZooInspector` 連線 ZooKeeper 檢視服務資訊如下:
![](https://mrhelloworld.com/resources/articles/spring/spring-cloud/zookeeper/image-20200726221706629.png " ")
![](https://mrhelloworld.com/resources/articles/spring/spring-cloud/zookeeper/image-20200727003205891.png " ")
至此 ZooKeeper 註冊中心所有的知識點就講解結束了。
![](https://mrhelloworld.com/resources/articles/articles_bottom/end02.gif " ")
本文采用 `知識共享「署名-非商業性使用-禁止演繹 4.0 國際」許可協議`。
大家可以通過 `分類` 檢視更多關於 `Spring Cloud` 的文章。