1. 程式人生 > 實用技巧 >SpringCloud 學習筆記

SpringCloud 學習筆記

SpringCloud 學習筆記

目錄

1 簡介

1.1 什麼是 SpringCloud

SpringCloud 是一系列框架的有序集合,它利用 SpringBoot 的開發便利性巧妙地簡化了分散式系統基礎設施的開發,如服務發現註冊、配置中心、訊息匯流排、負載均衡、斷路器、資料監控等,都可以用 SpringBoot 的開發風格做到一鍵啟動和部署。Spring Cloud 並沒有重複製造輪子,它只是將各家公司開發的比較成熟、經得起考驗的服務框架組合起來,通過 SpringBoot 風格進行再封裝遮蔽掉了複雜的配置和實現原理

1.2 SpringCloud 能幹什麼

springCloud 可以實現服務發現註冊、配置中心、訊息匯流排、負載均衡、斷路器、資料監控。

  • 服務註冊發現(Eureka)

    相當於 Zookeeper,好處是服務呼叫方不直接依賴於服務提供方,保證服務高可用

  • 服務熔斷(Hystrix)

    可以保證某個服務提供方的異常不會影響整體系統的穩定

  • 資料監控(Zuul)

    Zuul 是在雲平臺上提供動態路由,監控,彈性,安全等邊緣服務的框架,相當於閘道器

  • 配置中心(Archaius)

    配置管理 API,包含一系列配置管理 API,提供動態型別化屬性、執行緒安全配置操作、輪詢框架、回撥機制等功能

  • 訊息匯流排(Bus)

    時間、訊息匯流排,用於在叢集(例如,配置變化事件)中傳播狀態變化

  • 日誌追蹤(Sleuth)

    日誌收集工具包,封裝了 Dapper 和 log-based 追蹤以及 Zipkin 和 HTrace 操作,為 SpringCloud 應用實現了一種分散式追蹤解決方案

  • 大資料處理(Data Flow)

    Data Flow 是一個用於開發和執行大範圍資料處理其模式包括 ETL,批量運算和持續運算的統一程式設計模型和託管服務。

2. 微服務

2.1 什麼是微服務

微服務(Microservice Architecture)是最近幾年流行的一種架構思想,關於它的概念很難一言以蔽之。

究竟什麼是微服務呢?我們在此引用 ThoughtWorks 公司的首席科學家 Martin Fowler 於2014年提出的一段話:

微服務原文:https://martinfowler.com/articles/microservices.html

  • 就目前而言,對於微服務,業界並沒有一個統一的,標準的定義
  • 但通常而言,微服務架構是一種架構模式,或者說是一種架構風格,它提倡將單一的應用程式劃分成一組小的服務,每個服務執行在其獨立的自己的程序內,服務之間互相協調,互相配置,為使用者提供最終價值。服務之間採用輕量級的通訊機制互相溝通,每個服務都圍繞著具體的業務進行構建,並且能夠被獨立的部署到生產環境中,另外,應儘量避免統一的,集中式的服務管理機制,對具體的一個服務而言,應根據業務上下文,選擇合適的語言,工具對其進行構建,可以有一個非常輕量級的集中式管理來協調這些服務,可以使用不同的語言來編寫服務,也可以使用不同的資料儲存。

可能有的人覺得官方的話太過生澀,我們從技術維度來理解下:

  • 微服務化的核心就是將傳統的一站式應用,根據業務拆分成一個一個的服務,徹底地請耦合,每一個微服務提供單個業務功能的服務,一個服務做一件事情,從技術角度看就是一種小而獨立的處理過程,類似程序的概念,能夠自行單獨啟動或銷燬,擁有自己獨立的資料庫。

2.2 微服務與微服務架構

微服務

強調的是服務的大小,他關注的是某一個點,是具體解決某一問題提供落地對應服務的一個服務應用,狹義地看,可以看做 IDEA 中一個個微服務工程,或者是Module。

IDEA 工具裡面使用Maven開發的一個個獨立的Module,它具體是使用SpringBoot開發的一個小模組,一個模組就做一件事

強調的是一個個的個體,每個個體完成一個具體的任務或者功能!

微服務架構

微服務架構是一種新的架構形式,它提倡將單一應用程式劃分成為一組小的服務,服務之間相互協調,互相配合,為使用者提供最終價值。每個服務執行在其獨立的程序中,服務與服務間採用輕量級的通訊機制互相協作,每個服務都圍繞著具體的業務進行構建,並且能夠獨立地部署到生產環境中,另外,應精良避免統一的,集中式地服務管理機制,對具體地一個服務而言,應根據業務上下文,選擇合適地語言,工具進行構建

2.3 微服務架構的四個核心問題

問題

  1. 服務很多,客戶端該如何訪問?
  2. 這麼多服務,服務之間如何通訊?
  3. 這麼多服務,如何治理?
  4. 服務掛了怎麼辦?

解決方案

  1. Spring Cloud NetFlix 一站式解決方案

    API閘道器: zuul 元件

    Feign:基於 HttpClient,使用 Http通訊方式,比如同步,阻塞

    Eureka:服務註冊發現

    熔斷機制:Hystrix

  2. Apache Dubbo Zookeeper 半自動,需要整合別人的

    API閘道器:沒有,找第三方元件,或者自己實現

    Dubbo:基於RPC高效能開源的Java框架

    Zookeeper:服務註冊發現

    熔斷機制:需要藉助 Hystrix

    Dubbo 這個方案並不完善,但它是RPC框架中最好的

  3. Spring Cloud Alibaba 一站式解決方案

    比 Spring Cloud NetFlix 更簡單

雖然主流是這三個方案,但萬變不離其宗

  1. API 閘道器:提供路由支援,客戶端通過 API 閘道器來訪問對應的微服務
  2. HTTP,RPC:解決微服務之間的通訊問題
  3. 服務註冊和發現:統一管理微服務,使得服務高可用
  4. 熔斷機制:解決服務掛了的問題,服務掛了的時候,服務降級,防止服務雪崩

綜合上面所說的,其實就是網路不可靠引起了這些問題

2.4 微服務優缺點

優點

  • 每個服務足夠內聚,足夠小,程式碼容易理解,這樣能聚集一個指定的業務功能或業務需求
  • 開發簡單,開發效率高,一個服務可能只是專一地幹一件事
  • 微服務能夠被小團隊單獨開發
  • 微服務是鬆耦合地,是有功能意義的服務,無論是在開發階段或部署階段都是獨立的
  • 微服務能使用不同的語言開發。
  • 易於和第三方整合,微服務允許容易且靈活的方式整合自動部署,通過持續整合工具,如 jenkins,Hudson,bamboo
  • 微服務易於被一個開發人員理解,修改和維護,這樣小團隊能夠更關注自己的工作成果。無需通過合作才能體現價值
  • 微服務允許你利用融合新技術
  • 微服務只是業務邏輯的程式碼,不會和 HTML、CS或其他介面混合
  • 每個微服務都有自己的儲存能力,可以有自己的資料庫,也可以統一資料庫

缺點

  • 開發人員要處理分散式的複雜性
  • 多服務運維難度會隨著服務的增加而增加,運維的壓力也在增大
  • 系統部署依賴
  • 服務間通訊成本
  • 資料一致性
  • 系統整合測試
  • 效能監控

2.5 微服務技術棧有哪些?

微服務條目 落地技術
服務開發 SpringBoot,Spring,SpringMVC
服務配置與管理 Netflix公司的Archaius、阿里的Diamond等
服務註冊與發現 Eureka、Consul、Zookeeper等
服務呼叫 Rest、RPC、gRPC
服務熔斷器 Hystrix、Envoy等
負載均衡 Ribbon、Nginx等
服務介面呼叫(客戶端呼叫服務的簡化工具) Feign等
訊息佇列 Kafka、RabbitMQ、ActiveMQ 等
服務配置中心管理 SpringCloudConfig、Chef等
服務路由(API閘道器) Zuul等
服務監控 Zabbix、Nagios、Metrics、Specatator等
全鏈路追蹤 Zipkin、Brave、Dapper等
服務部署 Docker、OpenStack、Kubernetes等
資料流操作開發包 SpringCloud Stream(封裝與 Redis,RabbitMQ,Kafka等傳送接收訊息)
事件訊息匯流排 SpringCloud Bus

2.6 為什麼選擇 SpringCloud 作為微服務架構

1. 選型依據

  • 整體解決方案和框架成熟度
  • 社群熱度
  • 可維護性
  • 學習區縣

2. 當前各大IT公司用得微服務架構有哪些?

  • 阿里:dubbo+HFS
  • 京東:JSF
  • 新浪:Motan
  • 噹噹網:DubboX
  • ......

3. 各微服務框架對比

功能點/服務框架 Netflix/SpringCloud Motan gRPC Thrift Dubbo/DubboX
功能定位 完整的微服務框架 RPC框架,但整合了ZK或Consul,實現了叢集環境的基本服務註冊/發現 RPC框架 RPC框架 服務框架
支援 Rest 是,Ribbon支援多種可插拔的序列化選擇
支援 RPC 是(Hession2)
支援多語言 是(Rest形式)?
負載均衡 是(服務端zuul+客戶端Ribbon),zuul服務,動態路由,雲端負載均衡 Eureka(針對中間層伺服器) 是(客戶端) 是(客戶端)
配置服務 Netfix Archius,Spring Cloud Config Server 幾種配置 是(zookeeper提供)
服務呼叫鏈監控 是(zuul),zuul提供邊緣服務,API閘道器
高可用/容錯 是(服務端Hystrix+客戶端Ribbon) 是(客戶端) 是(客戶端)
典型應用案例 Netfix Sina Google Facebook
社群活躍程度 一般 一般 2017年後重新開始維護,之前斷了5年
學習難度 中低
文件豐富度 一般 一般 一般
其他 SpringCloud Bus 為我們的應用程式帶來了更多管理斷點 支援降級 Netflix內部再開發整合gRPC IDL定義 實踐的公司比較多

3. SpringCloud 入門概述

3.1 什麼是 SpringCloud

SpringCloud 官方學習文件:https://spring.io/projects/spring-cloud#learn

SpringCloud,基於 SpringBoot 提供了一套微服務解決方案,包括服務註冊於發現,配置中心,全鏈路監控,服務閘道器,負載均衡,熔斷器等元件,除了基於 NetFlix 的開源元件做高度抽象封裝之外,還有一些選型中立的開源元件。

SpringCloud 了 SpringBoot 的開發便利,巧妙地簡化了分散式系統基礎設施的開發,SpringCloud為開發人員提供了快速構建分散式系統的一些工具,包括配置管理,服務發現,斷路器,路由,微代理,事件匯流排,全域性鎖,決策競選,分散式會話等等,他們都可以用 SpringBoot 的開發風格做到一鍵啟動和部署。

SpringBoot 並沒有重複造輪子,它只是將目前各家公司開發的比較成熟,經得起考研的服務框架組合起來,通過 SpringBoot 風格進行再封裝,遮蔽掉了複雜的配置和實現原理,最終給開發者留出了一套簡單易懂,易部署和易維護的分散式系統開發工具包

SpringCloud 是分散式微服務架構下的一站式解決方案,是各個微服務架構落地技術的集合體,俗稱微服務全家桶。

3.2 SpringCloud 和 SpringBoot 關係

  • SpringBoot 專注於快速方便地開發單個個體微服務。-jar
  • SpringCloud 是關注全域性地微服務協調整理治理框架,它將 SpringBoot 開發地一個個體單體微服務整合並管理起來,為各個微服務之間提供:配置管理,服務發現,斷路器,路由,微代理,事件中線,全域性鎖,決策競選,分散式會話等等整合服務。
  • SpringBoot 可以離開 SpringCloud 獨立使用,開發專案,但是 SpringCloud 離不開 SpringBoot,屬於依賴關係
  • SpringBoot 專注於快速、方便地開發單個個體微服務,SpringCloud關注全域性地服務治理框架

3.3 Dubbo 和 SpringCloud 對比

Dubbo SpringCloud
服務註冊中心 Zookeeper SpringCloud NetFlix Eureka
服務呼叫方式 RPC REST API
服務監控 Dubbo-monitor Spring Boot Admin
斷路器 不完善 Spring Cloud Netflix Hystrix
服務閘道器 Spring Cloud Netflix Zuul
分散式配置 Spring Cloud Config
服務跟蹤 Spring Cloud Sleuth
訊息匯流排 Spring Cloud Bus
資料流 Spring Cloud Stream
批量任務 Spring Cloud Task

最大區別:SpringCloud 拋棄了 Dubbo 的 RPC 通訊,採用的是基於HTTP的REST方式

嚴格來說,這兩種方式各有優劣,雖然從一定程度上來說,後者犧牲了服務呼叫的效能,但也避免了上面提到的原生 RPC 帶來的問題。而且 REST 相比於 RPC 更靈活,服務提供方和呼叫方的依賴只依靠一紙契約,不存在程式碼級別的強依賴,這再強調快速研發的微服務環境下,顯得更加合適。

品牌機和組裝機的區別

很明顯,SpringCloud 的功能比 Dubbo 更加強大,涵蓋面更廣,而且作為 Spring 的拳頭專案,它也能夠於 Spring Framework、SpringBoot、Spring Data、Spring Batch等其他 Spring 專案完美融合,這些對於微服務而言是至關重要的。使用 Dubbo 構建的微服務架構就像組裝電腦,各環節我們的選擇自由度很高,但是最終結果很有可能因為一條記憶體條質量不行就點不亮了,總是讓人不怎麼放心,但是如果你是一名高手,那這些都不是問題。而 SpringCloud 就像是品牌機,再 Spring Source 的整合下,做了大量的相容性測試,保證了機器擁有更高的穩定性,但是如果要在使用非原裝元件外的東西,就需要對其基礎有足夠了解。

社群支援度和更新力度

最為重要的是,Dubbo 停止了5年左右的更新,雖然 2017.7 重啟了,對於技術發展的新需求,需要由開發者自行拓展升級(比如噹噹網弄出了 DubboX),這對於很多想要採用微服務架構的中小軟體組織,顯然是不太合適的,中小公司沒有這麼強大的技術能力去修改 Dubbo 原始碼+周邊的一整套解決方案,並不是每一個公司都有阿里的大牛+真實的線上生產環境測試過。

總結

曾風靡國內的開源 RPC 服務框架 Dubbo 在重啟維護後,令許多使用者為之雀躍,但同時,也迎來了一些之一的聲音,網際網路技術發展迅速,Dubbo 是否還能跟上時代?Dubbo 與 SpringCloud 相比又有何優勢何差異?是否會由相關舉措保證 Dubbo 的後續更新頻率?

解決的問題域不一樣:Dubbo的定位是一款RPC框架,SpringCloud的目標是微服務架構下的一站式解決方案

3.4 SpringCloud 版本號

SpringCloud 是一個由眾多獨立專案組成的大型綜合專案,每個子專案有不同的發行節奏,都維護者自己的釋出版本號,SpringCloud 通過一個資源清單BOM(Bill of Materals)來管理每個版本的專案清單。為了避免與子專案的釋出號混淆,所以沒有采用版本號的方式,而是通過命名的方式。

這些版本名稱的命名方式採用了倫敦地鐵站的名稱,同時根據字母表的順序來對應版本事件順序。比如:最早的 Release 版本:Angel,第二個 Release 版本:Brixton,然後是 Camden、Dalston、Edgware,目前最新版本是 Hoxton

大版本說明

SpringBoot SpringCloud 關係
1.2.x Angel版本(天使) 相容 SpringBoot 1.2.x
1.3.x Brixton版本(布里克斯頓) 相容 SpringBoot 1.3.x,也相容 SpringBoot 1.4.x
1.4.x Camden版本(卡姆登) 相容 SpringBoot 1.4.x,也相容 SpringBoot 1.5.x
1.5.x Dalston版本(多爾斯頓) 相容 SpringBoot 1.5.x,不相容 SpringBoot 2.0.x
1.5.x Edgware版本(埃奇韋爾) 相容 SpringBoot 1.5.x, 不相容 SpringBoot 2.0.x
2.0.x Finchley版本(芬奇利) 相容 SpringBoot 2.0.x,不相容 SpringBoot 1.5.x
2.1.x Greenwich版本(格林威治) 相容 SpringBoot 2.1.x
2.2.x Hoxton版本(霍克斯頓) 相容 SpringBoot 2.2.x

實際開發版本關係

spring-boot-starter-parent spring-cloud-dependencies
1.5.2.RELEASE Daiston.RC1
1.5.9.RELEASE Edgware.RELEASE
1.5.16.RELEASE Edgware.SR5
1.5.20.RELEASE Edgware.SR5
2.0.2.RELEASE Finchiey.BUILD-SNAPSHOT
2.0.6.RELEASE Finchiey.SR2
2.1.4.RELEASE Greenwich.SR1
2.1.0.RELEASE-2.1.14.RELEASE Greenwich.SR5
2.2.0.M4 Hoxton.SR4

4. 第一個 SpringCloud 案例

我們模擬一下生產者和消費者的案例

建立一個Maven父工程 SpringCloud-Study ,並規範好子工程依賴版本

父工程的 pom.xml 如下

:SpringBoot 和 SpringCloud 的版本要匹配(可以參考上面開發版本號,或者查詢官網),不然可能會出現很多意想不到的錯誤

<properties>
    <mysql.version>5.1.46</mysql.version>
    <lombok.version>1.18.12</lombok.version>
    <log4j.version>1.2.17</log4j.version>
    <druid.version>1.1.23</druid.version>
    <mybatisplus.version>3.3.2</mybatisplus.version>
</properties>

<dependencyManagement>
    <dependencies>
        <!-- SpringBoot 版本依賴 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>2.1.14.RELEASE</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <!-- SpringCloud 版本依賴 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>Greenwich.SR3</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <!-- mysql -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>${mysql.version}</version>
        </dependency>
        <!-- lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>${lombok.version}</version>
        </dependency>
        <!-- log4j -->
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>${log4j.version}</version>
        </dependency>
        <!-- log4j 日誌門面 -->
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-core</artifactId>
            <version>1.2.3</version>
        </dependency>
        <!-- druid -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>${druid.version}</version>
        </dependency>
        <!-- MyBatisPlus -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>${mybatisplus.version}</version>
        </dependency>
    </dependencies>
</dependencyManagement>

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

建立資料庫和表

建立一個名為 “db01” 的的資料庫,並建立 dept 表

dept 表的建表 SQL 語句如下

create table dept
(
    deptno      bigint auto_increment primary key,
    dname       varchar(255) null,
    db_resource varchar(255) null
);

往表中插入資料

-- 插入資料 DATABASE() 這個函式是顯示具體是當前資料庫的名稱
insert into dept (dname, db_resource) VALUES ('開發部',DATABASE());
insert into dept (dname, db_resource) VALUES ('人事部',DATABASE());
insert into dept (dname, db_resource) VALUES ('財務部',DATABASE());
insert into dept (dname, db_resource) VALUES ('市場部',DATABASE());
insert into dept (dname, db_resource) VALUES ('運維部',DATABASE());
-- 查詢插入後的所有資料
select * from dept;

建立API工程

建立一個子工程,工程名為 SpringCloud-API,並建立表對應的實體類

該子工程的 pom.xml 中依賴配置如下

<dependencies>
    <!-- lombok -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>
    <!-- MybatisPlus -->
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus</artifactId>
        <version>3.3.2</version>
    </dependency>
</dependencies>

實體類 Dept.java

@Data
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = true)
public class Dept implements Serializable {

    /**
     * 逐漸
     */
    @TableId(value = "deptno" ,type = IdType.AUTO)
    private Long deptNo;
    /**
     * 部門名
     */
    @TableField("dname")
    private String dname;
    /**
     * 這個資料存在哪個資料庫,一個服務對應一個數據庫,同一個資訊可能存在不同的資料庫
     */
    @TableField("db_resource")
    private String db_resource;

}

建立生產者

建立一個子工程,工程名為 SpringCloud-Provider-8001

該子工程的 pom.xml 如下

<dependencies>
    <!-- 我們需要拿到實體類,所以需要配置 api module -->
    <dependency>
        <groupId>com.xp</groupId>
        <artifactId>SpringCloud-API</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <dependency>
        <groupId>log4j</groupId>
        <artifactId>log4j</artifactId>
    </dependency>
    <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-core</artifactId>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
    </dependency>
    <!-- MybatisPlus -->
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.3.2</version>
    </dependency>
    <!-- test -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-test</artifactId>
    </dependency>
    <!-- web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <!-- 排除tomcat依賴,使用jetty伺服器 -->
        <exclusions>
            <exclusion>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-tomcat</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <!-- jetty -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jetty</artifactId>
    </dependency>
</dependencies>

然後配置 SpringBoot 的配置檔案 applicaton.yml

# 配置埠號
server:
  port: 8001
# 配置 mybatis
mybatis-plus:
  type-aliases-package: com.xp.model.entity
  configuration:
    map-underscore-to-camel-case: false
    cache-enabled: true
  mapper-locations: classpath:mapper/*Mapper.xml
# 配置資料來源
spring:
  application:
    name: springcloud-study-provider
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: org.gjt.mm.mysql.Driver
    url: jdbc:mysql://localhost:3306/db01?useUnicode=true&characterEncoding=utf-8&useSSL=false
    username: root
    password: root

搭建 mvc 三層

建立 mapper 包並建立 DeptMapper 介面

@Mapper
@Repository
public interface DeptMapper extends BaseMapper<Dept> {

    boolean addDept(Dept dept);

}

在 resource 目錄下建立mapper包並在mapper包內建立 DeptMapper.xml 實現 DeptMapper 介面

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xp.mapper.DeptMapper">
    <insert id="addDept">
        insert into dept (dname, db_resource)
        values (#{dname},DATABASE());
    </insert>
</mapper>

在 SpringbBoot 程式主啟動類上加上 mapper 掃描的註解

@SpringBootApplication
@MapperScan("com.xp.mapper") // 掃描mapper介面所在的包
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class,args);
    }

}

建立 service 包並建立 DeptService 介面及其實現類

DeptService 介面

public interface DeptService extends IService<Dept> {

    boolean addDept(Dept dept);

}

DeptService 介面實現類

@Service
public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements DeptService {

    @Override
    public boolean addDept(Dept dept) {
        return this.baseMapper.addDept(dept);
    }
}

建立 controller 包,並建立 DeptController 對外提供服務

@RestController
public class DeptController {

    @Autowired
    DeptService deptService;

    @PostMapping("/dept/add")
    public boolean addDept(Dept dept){
        return deptService.addDept(dept);
    }

    @GetMapping("/dept/get/{id}")
    public Dept get(@PathVariable("id")Long id){
        return deptService.getById(id);
    }

    @GetMapping("/dept/get")
    public List<Dept> getList(){
        return deptService.list();
    }

}

到這裡,一個簡單的服務生產(提供)者就完成了

我們可以測試一下是否能夠成功使用這個服務

啟動專案,然後訪問 8001 埠的具體介面,比如 http://localhost:8001/dept/get

建立消費者

建立一個子工程,工程名為 SpringCloud-Consumer-80

該工程的 pom.xml 配置檔案如下

<dependencies>
    <!-- 我們需要使用到實體類,所以需要引入 api module -->
    <dependency>
        <groupId>com.xp</groupId>
        <artifactId>SpringCloud-API</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
    <!-- SpringBoot -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <exclusions>
            <exclusion>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-tomcat</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <!-- jetty -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jetty</artifactId>
    </dependency>
</dependencies>

application.yml 配置檔案如下:

只修改了埠號為 80,因為我們網站預設是80埠訪問,所以如果我們設定伺服器啟動的埠號為80埠,我們可以直接使用 http://localhost/XXXX 來訪問具體的專案介面,就跟我們平時上網直接訪問網站一樣(比如百度:http://www.baidu.com,不是http://www.baidu.com:8080),不需要在網址後面加上埠號。

server:
  port: 80

我們學過 SpringBoot 就會知道,SpringBoot 中會有很多 XXXTemplate(比如 JdbcTemplate)。我們也知道 SpringCloud 搭建的微服務是通過 HTTP 的 REST 來進行通訊的,所以消費者要呼叫生產者的服務,就得通過 HTTP 得 REST 來進行呼叫,SpringCloud 中有提供 RestTemplate 這個類來簡化我們的呼叫過程。

那麼如何使用 RestTemplate 這個類來進行服務的呼叫呢

我們只需要將 RestTemplate 這個類注入到 Spring 容器中,然後在需要呼叫遠端服務的 Controller 中自動注入使用即可

建立一個配置類ConfigBean,並將 RestTemplate 注入到 Spring 容器中

@Configuration
public class ConfigBean {

    // 將 RestTemplate 注入到 Spring 容器中
    @Bean
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }

}

建立 DeptConsumerController 類,並使用 RestTemplate 呼叫遠端的服務

@RestController
public class DeptConsumerController {

    /**
     * 消費者不應該有 Service 層
     * 所以使用 RestTemplate 來通過 Restful風格請求來呼叫具體的 service
     * 首先需要將 RestTemplate 注入到 Spring 容器中
     * RestTemplate 提供多種編寫訪問遠端 http 服務的方法,簡單的 RESTFUL 服務模板
     * RestTemplate 中 getForObject 是使用get方式來獲取服務提供者的服務
     * RestTemplate 中 postForObject 是使用post方式來獲取服務提供者的服務
     */
    @Autowired
    private RestTemplate restTemplate;

    // 由於遠端呼叫服務者的地址字首是一樣的,所以直接使用常量來防止寫錯
    private static final String REST_URL_PREFIX = "http://localhost:8001";

    @RequestMapping("/consumer/dept/get/{id}")
    public Dept get(@PathVariable("id") Long id){
        return restTemplate.getForObject(REST_URL_PREFIX+"/dept/get/"+id,Dept.class);
    }

    @RequestMapping("/consumer/dept/add")
    public boolean add(Dept dept){
        return restTemplate.postForObject(REST_URL_PREFIX+"/dept/add",dept,Boolean.class);
    }

    @RequestMapping("/consumer/dept/get")
    public List<Dept> getList(){
        return restTemplate.getForObject(REST_URL_PREFIX+"/dept/get",List.class);
    }

}

可能會有小夥伴會問,這個專案怎麼沒有 Service 層,這是因為消費者不應該有 Servcie 層,而是應該通過遠端呼叫 Service 層來完成業務需求。微服務不正是將以前單體應用(all in one)中的服務拆分出來,然後根據具體的專案(消費者)需求呼叫不同的服務。

接下來,我們測試下消費者是否可以呼叫生產者提供的服務

先啟動生產者,然後啟動消費者訪問消費者的具體介面

到這裡,第一個簡單的 SpringCloud 生產者消費者案例就完成了

5. Eureka 服務註冊與發現

5.1 什麼是 Eureka

  • 怎麼讀? juˈriːkə
  • Netflix 在設計 Eureka 時,遵循的就是 AP 原則
  • Eureka 是 Netflix 的一個子模組,也是核心 模組之一,Eureka 是一個基於 REST 的服務,用於定位服務,以之實現雲端中介軟體層服務發現和故障轉移,服務註冊與發現對於微服務來說是非常重要的,有了服務註冊於發現,只需要使用服務的識別符號,就可以訪問到服務,而不需要修改服務呼叫的配置檔案了,功能類似於 Dubbo 的註冊中心,比如 Zookeeper

5.2 原理講解

  • Eureka 的基本架構

    • SpringCloud 封裝了 Netflix 公司開發的 Eureka 模組來實現服務註冊和發現(對比 Zookeeper)

    • Eureka 採用了 C-S 架構設計,Eureka Server 作為服務註冊功能的伺服器,他是服務註冊中心

    • 而系統中的其他微服務,使用 Eureka 的客戶端連線到 Eureka Server 並維持心跳連結。這樣系統的維護人員就可以通過 Eureka Server 來監控系統中各個微服務是否正常執行,SpringCloud 的一些其他模組(比如 Zuul)就可以通過 Eureka Server 來發現系統中的其他微服務,並執行相關的邏輯

    • 和 Dubbo 架構的對比

    • Eureka 包含兩個元件: Eureka Server 何 Eureka Client

    • Eureka Server 提供服務註冊服務,各個節點啟動後,會在 Eureka Server 中進行註冊,這樣 Eureka Server 中的服務登錄檔中儲存所有伺服器節點的資訊,伺服器節點的資訊可以在介面中直觀的看到

    • Eureka Client 是一個 Java 客戶端,用於簡化 Eureka Server 的互動,客戶端同時也具備一個內建的,使用輪詢負載演算法的負載均衡器。在啟動後,將會向 Eureka Server 傳送心跳(預設週期為30秒)。如果 Eureka Server 在多個心跳週期內沒有接收到某個節點的心跳,Eureka Server 將會從服務登錄檔中把這個服務節點移除掉(預設週期為90秒)

  • 三大角色

    • Eureka Server:提供服務的註冊與發現

    • Service Provider:將自身服務註冊到 Eureka 中,從而使消費者方能夠找到

    • Service Consumer:服務消費方從 Eureka 中獲取註冊服務列表,從而找到消費服務

5.3 構建步驟

1. eureka-server:

  1. 建立一個SpringBoot專案 SpringCloud-Eureka-7001

  2. 匯入依賴

    pom.xml的依賴如下:

    <dependencies>
        <!-- Eureka -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-eureka-server</artifactId>
            <version>1.4.7.RELEASE</version>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-tomcat</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <!-- jetty -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jetty</artifactId>
        </dependency>
    </dependencies>
    
  3. 配置 eureka-server

    在 resources 目錄下建立 application.yml,然後配置 eureka-server

    server:
      port: 7001 # 埠號
    
    # eureka 配置
    eureka:
      instance:
        hostname: localhost # 服務端的例項名稱
      client:
        register-with-eureka: false # 表示是否向 eureka 註冊中心註冊自己
        fetch-registry: false # fetch-registry 如果為 false,則表示自己為註冊中心,,不去檢索服務
        service-url: # 監控頁面
          defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
    
  4. 開啟 eureka-server

    在主啟動類上加上 @EnableEurekaServer 註解

    // 啟動之後訪問 http://localhost:7001
    @SpringBootApplication
    @EnableEurekaServer // 表示是服務端的啟動類,可以接受別人註冊進來
    public class EurekaApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(EurekaApplication.class,args);
        }
    
    }
    
  5. 啟動專案,測試訪問 Eureka 監控中心

    開啟瀏覽器,輸入 http://localhost:7001 ,測試訪問 Eureka 監控中心

    如果可以訪問,代表 eureka-server 啟動成功

2. Service Provider

將我們之前寫的8001服務提供者註冊到7001的eureka中

  1. 修改8001服務的pom檔案,增加eureka支援

    <!-- Eureka -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-eureka</artifactId>
        <version>1.4.7.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-config</artifactId>
        <version>2.1.4.RELEASE</version>
    </dependency>
    
  2. 修改 yml

    增加eureka的配置

    # eureka 配置
    eureka:
      client:
        service-url:
          defaultZone: http://localhost:7001/eureka/
      instance:
        instance-id: springcloud-provider-8001 # 修改 Eureka 中預設描述資訊
    
  3. 開啟註解支援,讓服務自動註冊到 Eureka 中

    @SpringBootApplication
    @EnableEurekaClient // 在服務啟動後自動註冊到 Eureka 中
    @MapperScan("com.xp.mapper")
    public class Application {
    
        public static void main(String[] args) {
            SpringApplication.run(Application.class,args);
        }
    
    }
    
  4. 啟動,測試是否成功註冊

    訪問 http://localhost:7001 Eureka 監控中心

    滑鼠懸停在如圖所在位置,可以看到該服務的IP資訊

3. actuator與註冊微服務資訊完善

在yml配置檔案中加多一個配置

# eureka 配置
eureka:
  client:
    service-url:
      defaultZone: http://localhost:7001/eureka/
  instance:
    instance-id: springcloud-provider-8001 # 修改 Eureka 中預設描述資訊
    prefer-ip-address: true # 訪問路徑可以顯示ip地址

然後重啟重新整理

會發現原本的專案訪問地址顯示出了ip地址

如果我們點選這個連結,會顯示空白錯誤頁,在pom檔案中增加如下依賴

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

重啟重新整理這時會顯示空內容

增加yml配置

# info 配置
info:
  app.name: xp-springcloud
  company.name: xp

然後重啟重新整理

就會顯示我們自定義的內容了

4. Eureka 的自我保護機制

在之前的重啟中,應該會出現下面這段紅字,這段紅字的意思是我們Eureka註冊中心註冊的某個服務可能掛了

在 Dubbo+Zookeeper 中,如果註冊的服務檢測到未能連線,則會整個註冊中的服務都不能訪問。重新整理頁面,我們仍能在Eureka監控中心中看到這個服務,並且點進取還能檢視到這個服務的資訊,這是為什麼呢?

這是因為Eureka中有自我保護機制

自我保護機制:好似不如賴活著

一句話總結:某時刻某一個微服務不可以用了,eureka不會立刻清理,依舊會對該微服務的資訊進行儲存!

  • 預設情況下:如果EurekaServer 在一定時間內沒有接收到某個微服務例項的心跳,EurekaServer將會登出該例項(預設90秒)。但是當時網路分割槽故障發生時,微服務與Eureka之間無法正常通訊,以上行為可能變得非常危險了——因為微服務本身其實是健康的,此時本部應該登出這個服務,Eureka通過自我保護機制來解決這個問題——當EurekaServer節點在短時間內丟失過多客戶端時(可能發生了網路分割槽故障),那麼這個節點就會進入自我保護模式。一旦進入該模式,EurekaServer就會保護服務登錄檔中的資訊,不再刪除服務登錄檔中的資料(也就是不會登出任何微服務)。當網路故障恢復猴,該EurekaServer節點會自動推出自我保護模式。
  • 在自我保護模式中,EurekaServer會保護服務登錄檔中的資訊,不再登出任何服務例項。當它收到的心跳數恢復到閾值以上時,該EurekaServer節點就會自動推出自我保護模式。它的設計哲學就是寧可保留錯誤的服務註冊資訊,也部不盲目登出任何可能健康的服務例項。一句話:好死不如賴活著
  • 綜上,自我保護模式是一種應對網路異常的安全保護措施。它的架構哲學是寧可同時保留所有微服務(健康的微服務和不健康的微服務都會保留),也不盲目登出任何健康的微服務,使用自我保護模式,可以讓Eureka叢集更加的健壯和穩定
  • 在SpringCloud中,可以使用eureka.server.enable-self-preservation = false 禁用自我保護模式【不推薦關閉自我保護機制】

5. 8001服務發現Discovery

  • 對於註冊進 Eureka 裡面的微服務,可以通過服務發現來獲得該服務的資訊。【對外暴露服務】

  • 修改 DeptController

    先自動注入 DiscoveryClient

    @Autowired
    private DiscoveryClient client;
    

    編寫一個介面,用於我們獲得該服務的資訊

    // 註冊進來的微服務,獲取一些訊息
    @GetMapping("/dept/discovery")
    public Object discovery(){
        // 獲得微服務列表的清單
        List<String> services = client.getServices();
        List<ServiceInstance> instances = client.getInstances("SPRINGCLOUD-PROVIDER");
        for (ServiceInstance instance : instances) {
            System.out.println(instance.getHost()+"\t"+instance.getPort()+"\t"+instance.getUri()+"\t"+instance.getServiceId());
        }
        return this.client;
    }
    

    點進 ServiceInstance 這個類,發現這個類提供了獲取註冊服務資訊的方法

    public interface ServiceInstance {
        default String getInstanceId() {
            return null;
        }
    
        String getServiceId();
    
        String getHost();
    
        int getPort();
    
        boolean isSecure();
    
        URI getUri();
    
        Map<String, String> getMetadata();
    
        default String getScheme() {
            return null;
        }
    }
    

    我們使用的這些方法在EurekaServiceInstance中實現

    然後在消費者的 DeptConsumerController 中遠端呼叫這個服務

    @RequestMapping("/consumer/dept/discovery")
    public Object discovery(){
        return restTemplate.getForObject(REST_URL_PREFIX+"/dept/discovery",Object.class);
    }
    

    依次啟動註冊中心,服務提供者,服務消費者,使用瀏覽器訪問測試

    控制檯輸出如下:

5.4 叢集配置

  • 新建 SpringCloud-Eureka-7002 和 SpringCloud-Eureka-7003 兩個工程專案

  • 將 7001 的內容複製到 7002 和 7003 中

  • 修改對映配置

    windows域名對映

    使用資源管理器進入 C:\Windows\System32\drivers\etc 目錄,修改 host 檔案

    或者使用火絨安全軟體自帶的功能修改

    向 host 檔案中增加域名對映

    127.0.0.1 eureka7001.com
    127.0.0.1 eureka7002.com
    127.0.0.1 eureka7003.com

    若無法修改,則可能是因為host檔案被設定成只讀檔案

    右鍵-->屬性-->將只讀勾選去掉

  • 修改 yml 配置檔案

    修改註冊中心啟動的埠號以及例項的主機名,並關聯叢集

    register-with-eureka設定為true或者不設定

    關聯叢集時,將其他註冊中心的訪問地址填寫在 eureka.client.service-url.defaultZone 中,使用逗號隔開

    server:
      port: 7003
    
      # Eureka
    eureka:
      instance:
        hostname: eureka7003.com
      client:
        register-with-eureka: true # 向Eureka註冊中心註冊自己
        fetch-registry: false # 表示自己是註冊中心,不去檢索服務
        service-url:
          # 單機:http://${eureka.instance.hostname}:${server.port}/eureka/
          # 叢集(關聯):使用逗號隔開,關聯其他的註冊中心
          defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/
    
  • 啟動 Eureka 叢集

    啟動 7001,7002和7003註冊中心,訪問任意一個服務註冊中心監控頁面

    DS Replicas 中有其他註冊中心且unavailable-replicas為空,available-replicas中有其他註冊中心,則表示Eureka叢集搭建成功

  • 將服務註冊到叢集中

    修改8001服務提供者的 yml 配置檔案,defaultZone中增加Eureka註冊中心,並使用逗號隔開

    # eureka 配置
    eureka:
      client:
        service-url:
          defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/
      instance:
        instance-id: springcloud-provider-8001 # 修改 Eureka 中預設描述資訊
        prefer-ip-address: true # 訪問路徑可以顯示ip地址
    

    開啟8001服務,然後分別訪問每個註冊中心的監控頁面,會發現每個註冊中心中都註冊了這個服務

5.5 對比 Zookeeper

回顧CAP原則

RDBMS(MySQL、Oracle、sqlServer) ---> ACID

NoSQL(redis、mongodb) ---> CAP11

ACID是什麼?

  • A(Atomicity)原子性
  • C(Consistency)一致性
  • I(Isolation)隔離性
  • D(Durability)永續性

CAP是什麼?

  • C(Consistency)強一致性
  • A(Availability)可用性
  • P(Partition tolerance)分割槽容錯性

CAP的三進二:CA、AP、CP

作為服務註冊中心,Eureka比Zookeeper好在哪裡?

著名的CAP理論指出,一個分散式系統不可能同時C(一致性)、A(可用性)、P(容錯性),由於分割槽容錯性P在分散式系統中是必須要保證的,因此我們只能在A和C之間進行權衡

  • Zookeeper 保證的是CP
  • Eureka 保證的是AP

Zookeeper保證的是CP

當向服務註冊中心查詢服務列表時,我們可以容註冊中心返回的是幾分鐘以前的註冊資訊,但不能接受服務直接down掉不可用,也就是說,服務註冊功能對可用性的要求要高於一致性。但是zk會出現這樣一種情況,當master節點因為網路故障與其他節點失去聯絡時,剩餘節點會重新進行leader選舉。問題在於,選舉leader的時間太長,30-120s,且選舉期間整個zk叢集都是不可用的,這就導致在選舉期間註冊服務癱瘓,在雲部署的環境下,因為網路問題使得zk叢集失去master節點是較大概率會發生的事件,雖然服務最終能夠恢復,但是漫長的選舉事件導致的註冊長期不可用是不能容忍的。

Eureka保證的是AP

Eureka看明白了這一點,因此在設計時就優先保證可用性,Eureka各個節點都是平等的,幾個節點掛掉不會影響正常節點的工作,剩餘的節點依然可以提供註冊和查詢服務。而Eureka的客戶端在向某個Eureka註冊時,如果發現連線失敗,則會自動切換至其他節點,只要有一臺Eureka還在,就能保證註冊服務的可用性,只不過查到的資訊可能不是最新的,除此之外,Eureka還有一種自我保護機制,日過在15分鐘內超過85%的節點都沒有正常的心跳,那麼Eureka預設為客戶端與註冊中心出現了網路故障,此時會出現一下幾種情況:

  1. Eureka不再從註冊列表中移除因為長時間沒收到心跳而應該過期的服務
  2. Eureka任然能夠接受新服務的註冊和查詢請求,但是不會被同步到其他節點(即保證當前節點依然可用)
  3. 當網路穩定時,當前例項新的註冊資訊會被同步到其他節點中

因此,Eureka可以很好地應對因網路故障導致部分節點失去聯絡地情況,而不會像Zookeeper那樣使整個註冊服務癱瘓

6. Spring Cloud Ribbon

6.1 Ribbon 是什麼?

  • Spring Cloud Ribbon 是基於 Netflix Ribbon 實現的一套客戶端負載均衡的工具

  • 簡單的說,Ribbon 是 Netflix釋出的開源專案,主要功能是提供客戶端的軟體負載均衡演算法,將 NetFlix 的中間層服務連線在一起。Ribbon 的客戶端元件提供一系列完整的配置項如:連線超時,重試等等。簡單的說,就是在配置檔案中列出 LoadBalancer(簡稱LB:負載均衡)後面所有的機器,Ribbon 會自動地幫助你基於某種規則(如簡單輪詢,隨機連線等等)去連線這些機器,我們也很容易使用 Ribbon 實現自定義地負載均衡演算法!

6.2 Ribbon 能幹嘛?

  • LB,即負載均衡(Load Balance),在微服務或分散式叢集中經常用地一種應用。

  • 負載均衡簡單地說就是將使用者地請求平攤地分配到多個服務上,從而達到系統的HA(高可用)。

  • 常見的負載均衡軟體有 Nginx,Lvs 等等

  • dubbo、SpringCloud 中均給我們提供了負載均衡,SpringCloud的負載均衡演算法可以自定義

  • 負載均衡簡單分類:

    • 集中式LB

      • 即在服務的消費方和提供方之間使用獨立的LB設施,如Nginx,由該實施負責把訪問請求通過某種策略轉發至服務的提供方!
    • 程序式LB

      • 將LB邏輯整合到消費方,消費方從服務註冊中心獲知有哪些地址可用,然後自己再從這些地址中選出一個合適的伺服器。
      • Ribbon就是屬於程序式LB,它只是一個類庫,集成於消費方程序,消費方通過它來獲取提供方的地址!

6.3 使用Ribbon實現負載均衡

建立2個數據庫,分別為db02和db03,出了db_resourece 欄位的內容不相同,其他的資料內容完全相同

# 建立資料庫
create database db03;
# 使用剛剛建立的資料庫
use db03;
# 建立 dept 表
create table dept
(
    deptno      bigint auto_increment
        primary key,
    dname       varchar(255) null,
    db_resource varchar(255) null
);
# 插入資料
insert into dept (dname, db_resource) VALUES ('開發部',DATABASE());
insert into dept (dname, db_resource) VALUES ('人事部',DATABASE());
insert into dept (dname, db_resource) VALUES ('財務部',DATABASE());
insert into dept (dname, db_resource) VALUES ('市場部',DATABASE());
insert into dept (dname, db_resource) VALUES ('運維部',DATABASE());

新建2個工程專案SpringCloud-Provider-8002、SpringCloud-Provider-8003,複製8001中的程式碼以及配置檔案到這兩個專案中(相當於將8001專案複製兩份)

修改8002和8003中yml配置檔案中的服務埠號,連線的資料庫和eureka例項描述

注:yml配置中 spring.application.name 不能修改,因為負載均衡的前提是服務名一致

# 修改埠號為800x
server:
  port: 8002
# 修改資料庫為800x
spring:
  application:
  	name: springcloud-provider # 這個不能改,服務名稱一致是負載均衡的前提
  datasource:
      type: com.alibaba.druid.pool.DruidDataSource
      driver-class-name: org.gjt.mm.mysql.Driver
      url: jdbc:mysql://localhost:3306/db02?useUnicode=true&characterEncoding=utf-8&useSSL=false
      username: root
      password: root
# eureka 配置,修改例項描述為800x
eureka:
  client:
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/
  instance:
    instance-id: springcloud-provider-8002 # 修改 Eureka 中預設描述資訊
    prefer-ip-address: true # 訪問路徑可以顯示ip地址

修改消費者80的pom檔案,新增eureka和ribbon依賴

<!-- ribbon -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-ribbon</artifactId>
    <version>1.4.7.RELEASE</version>
</dependency>
<!-- eureka -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-eureka</artifactId>
    <version>1.4.7.RELEASE</version>
</dependency>

修改消費者80的yml,新增eureka配置

server:
  port: 80

# eureka
eureka:
  client:
    register-with-eureka: false # 不向Eureka註冊中心註冊自己
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/

開啟負載均衡

在 ConfigBean 類中將 RestTemplate 新增 @LoadBalanced 註解開啟負載均衡

@Configuration
public class ConfigBean {

    // 配置負載均衡實現 RestTemplate
    @Bean
    @LoadBalanced // ribbon
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }

}

修改消費者80的Controller,因為我們使用負載均衡,所以我們應該訪問服務名

// 沒有負債均衡前 private static final String REST_URL_PREFIX = "http://localhost:8001";
// 使用負載均衡,這裡應該是一個變數,我們使用服務名來訪問
private static final String REST_URL_PREFIX = "http://SPRINGCLOUD-PROVIDER";

消費者80的啟動類,新增 @RibbonClient 註解

@SpringBootApplication
@EnableEurekaClient
@RibbonClient
public class ConsumerApplication {

    public static void main(String[] args) {
        SpringApplication.run(ConsumerApplication.class,args);
    }

}

分別依次啟動7001、7002、7003 eureka註冊中心,8001、8002、8003 服務提供者,80 服務消費者

先訪問 eureka 監控頁面,檢視服務是否註冊成功

然後通過消費者訪問介面遠端呼叫服務: http://localhost/consumer/dept/get/1

檢視db_resource的變化,我們會發現它會迴圈出現db01,db02,db03,至此我們成功使用Ribbon實現負載均衡

6.4 Ribbon 負載均衡演算法

6.4.1 IRule

Ribbon 的負載均衡演算法都是實現了 IRule 介面

點進 IRule 介面,檢視其實現類以及繼承關係

實現類

繼承關係

演算法說明:

RoundRobinRule:預設輪詢方式

RandomRule:隨機方式

AvailabilityFilteringRule:會先過濾掉跳閘,訪問故障的服務,對剩下的服務進行輪詢方式

RetryRule:會先按照輪詢獲取服務,如果服務獲取失敗,會在指定的時間內進行重試,若再獲取不到則會放棄

BestAvaliableRule:會先過濾掉由於多次訪問故障而處於斷路器跳閘狀態的服務,然後選擇一個併發量最小的服務

ZoneAvoidanceRule:根據效能和可用性來選擇

6.4.2 分析RoundRobinRule演算法原始碼

RoundRobinRule 類中核心的演算法就在 choose(ILoadBalancer lb, Object key) 這個方法中

public Server choose(ILoadBalancer lb, Object key) {
    // 判斷是否進行負載均衡
    if (lb == null) {
        log.warn("no load balancer");
        return null;
    }

    Server server = null;
    // 嘗試獲取下一個服務的次數,用於計數
    int count = 0;
    while (server == null && count++ < 10) {
        // 獲取存活的服務
        List<Server> reachableServers = lb.getReachableServers();
        // 獲取所有服務
        List<Server> allServers = lb.getAllServers();
        // 存活服務的數量
        int upCount = reachableServers.size();
        // 所有服務的數量
        int serverCount = allServers.size();

        // 如果存活服務的數量為0或者總服務數量為0,則不會進行負載均衡
        if ((upCount == 0) || (serverCount == 0)) {
            log.warn("No up servers available from load balancer: " + lb);
            return null;
        }

        // 獲取下一個服務的索引值
        int nextServerIndex = incrementAndGetModulo(serverCount);
        // 通過索引值獲取下一服務
        server = allServers.get(nextServerIndex);

        // 如果下一個服務為 null(空),執行緒禮讓,稍等片刻後進行下一次嘗試獲取下一個服務
        if (server == null) {
            Thread.yield();
            continue;
        }

        // 如果下一個服務存在,則返回下一個服務
        if (server.isAlive() && (server.isReadyToServe())) {
            return (server);
        }

        // 上面的條件都不成立,則認為服務不存在,為 null,進行下一次負載均衡嘗試
        server = null;
    }

    // 如果嘗試獲取下一個服務的次數達到10次或以上,則提示嘗試10次後,沒有可用的服務
    if (count >= 10) {
        log.warn("No available alive servers after 10 tries from load balancer: "
                + lb);
    }
    // 嘗試10次或以上後沒有獲取到服務,返回null
    return server;
}

//下一次獲取的位置
private AtomicInteger nextServerCyclicCounter;
private int incrementAndGetModulo(int modulo) {
    for (;;) {
        int current = nextServerCyclicCounter.get();
        int next = (current + 1) % modulo;
        // 由於可能是多執行緒同時訪問,所有使用 CAS 來確保執行緒安全
        if (nextServerCyclicCounter.compareAndSet(current, next))
            return next;
    }
}

6.4.3 自定義負載均衡演算法

在消費者80啟動類的上一級目錄建立一個包rule,然後建立兩個類 MyRule 和 RuleBeanConfig,具體結構如下:

為什麼不能放在啟動類的上下文中呢?
在 Ribbon 的官方文件中6.2 Customizing the Ribbon Client(定製 Ribbon 客戶端)有明確的提示 (官方文件:https://cloud.spring.io/spring-cloud-static/spring-cloud-netflix/2.1.0.RC2/single/spring-cloud-netflix.html#spring-cloud-ribbon)

自定義配置類必須是有@Configuration註解的類s,不過需要注意的是它不能在主應用程式上下文的@ComponentScan中。否則,它會被所有的@RibbonClients共享。如果你使用@ComponentScan(或者@SpringBootApplication),你應該採取措施避免包含它(例如:可以將其放在不重疊的包中或在@ComponentScan指定顯式掃描包)

The CustomConfiguration clas must be a @Configuration class, but take care that it is not in a @ComponentScan for the main application context. Otherwise, it is shared by all the @RibbonClients. If you use @ComponentScan (or @SpringBootApplication), you need to take steps to avoid it being included (for instance, you can put it in a separate, non-overlapping package or specify the packages to scan explicitly in the @ComponentScan).

編寫 MyRule ,自定義負載均衡演算法

先隨便將 IRule 的一個演算法子類複製貼上到 MyRule 中(這裡是將 RandomRule 內的程式碼拷貝修改的)作為演算法框架,然後根據自己的演算法修改其中的內容

這裡的演算法是每個服務迴圈5次後切換到其他服務

public class MyRule extends AbstractLoadBalancerRule {

	// 用於記錄迴圈次數
    private int total = 0;
    // 當前服務索引值
    private int currentIndex = 0;

    /**
     * 自定義負載均衡演算法
     * 每個服務迴圈5次後切換到其他服務
     */
    public Server choose(ILoadBalancer lb, Object key) {
        if (lb == null) {
            return null;
        }
        Server server = null;

        while (server == null) {
            if (Thread.interrupted()) {
                return null;
            }
            List<Server> upList = lb.getReachableServers();
            List<Server> allList = lb.getAllServers();

            int serverCount = allList.size();
            if (serverCount == 0) {
                return null;
            }

            // --------------自定義的 ribbon 負載均衡演算法
            // 當訪問了5次時
            if(total>4){
                // 重置計數
                total = 0;
                // 當前服務索引值+1 = 下一個服務的索引值
                currentIndex ++;
                // 如果下一個服務索引值 >= 存活的服務數
                if (currentIndex >= upList.size()){
                    // 重置服務索引值,重新開始輪詢
                    currentIndex = 0;
                }
            }
            // 獲取下一個服務
            server = upList.get(currentIndex);
            // 計數+1
            total ++;
            // --------------

            if (server == null) {
                Thread.yield();
                continue;
            }

            if (server.isAlive()) {
                return (server);
            }

            server = null;
            Thread.yield();
        }

        return server;

    }

    @Override
    public Server choose(Object key) {
        return choose(getLoadBalancer(), key);
    }

    @Override
    public void initWithNiwsConfig(IClientConfig clientConfig) {
        // TODO Auto-generated method stub
    }
}

將 MyRule 註冊到 Spring 容器中

@Configuration
public class RuleBeanConfig {

    @Bean
    public MyRule myRule(){
        return new MyRule();
    }

}

在主啟動類中關聯 Rule 配置

@SpringBootApplication
@EnableEurekaClient
// name 為服務名,configuration為自定義演算法規則的 Spring 配置類
@RibbonClient(name = "SPRINGCLOUD-PROVIDER",configuration = RuleBeanConfig.class)
public class ConsumerApplication {

    public static void main(String[] args) {
        SpringApplication.run(ConsumerApplication.class,args);
    }

}

依次啟動註冊中心,服務提供者,服務消費者,訪問 http://localhost/consumer/dept/get/1 ,檢視演算法是否成功生效

7. Feign 負載均衡

7.1 簡介

feign 是宣告式的 web service 客戶端,它讓微服務之間的呼叫變得更簡單了,類似 Controller 呼叫 Service,Spring Cloud 集成了 Ribbon 和 Eureka,可在使用 Feign 時提供負載均衡的 http 客戶端。

只需要建立一個介面,然後天界註解即可!

Feign,主要是社群,大家都習慣面向介面程式設計,這個是很多開發人員的規範,呼叫微服務訪問的兩種方法:

  1. 微服務名字【Ribbon】
  2. 介面和註解【Feign】

Feign 能幹什麼

  • Feign 旨在使編寫 Java http 客戶端變得更容易
  • 前面在使用 Ribbon + RestTemplate 時,利用 RestTemplate 對 Http 請求的封裝處理,形成了一套模板化的呼叫方法,但是在實際開發中,由於對服務依賴的呼叫可能不止一處,往往一個介面會被多處呼叫,所以通常都會針對每個微服務自行封裝一些客戶端類來包裝這些依賴服務的呼叫,所以,Feign 在此基礎上做了進一步封裝,由他來幫助我們定義和實現依賴服務介面的定義,在Feign的實現下,我們只需要建立一個介面並使用註解的方式來配置它(類似於以前Dao介面上標註 Mapper 註解,現在是一個微服務介面上面標註一個 Feign 註解即可。)即可完成對服務提供方的介面繫結,簡化了使用 Spring Cloud Ribbon時,自動封裝服務呼叫客戶端的開發量。

Feign集成了Ribbon

  • 利用 Ribbon 維護了微服務的服務列表資訊,並且通過輪詢實現了客戶端的負載均衡,而與 Ribbon 不同的是,通過 Feign 只需要定義服務繫結介面且以宣告式的方法,優雅而簡單的實現了服務呼叫。

7.2 Feign 使用步驟

建立一個新的工程 SpringCloud-Consumer-Feign-80,將原來的消費者80程式碼以及配置複製到這個工程專案中

修改pom檔案,新增 Feign 依賴

<!-- Feign -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-feign</artifactId>
    <version>1.4.7.RELEASE</version>
</dependency>

編寫介面 DeptService

:@FeignClient 註解中 value/name 的值為要繫結的微服務的名稱

這裡的 @GetMapping

@FeignClient(value = "SPRINGCLOUD-PROVIDER") // value的值為繫結的微服務名稱
public interface DeptClientService {

    // 不使用 Feign 之前,遠端呼叫服務是使用 RestTemplate 的 getForObject 和 @RequestMapping 獲取的
    // @Autowired
    // private RestTemplate restTemplate;
    // @RequestMapping("/consumer/dept/get/{id}")
    // public Dept get(@PathVariable("id") Long id){
    //     return restTemplate.getForObject(REST_URL_PREFIX+"/dept/get/"+id,Dept.class);
    // }
    
    @GetMapping("/dept/get/{id}")
    Dept queryById(@PathVariable("id") Long id);

    @GetMapping("/dept/get")
    List<Dept> queryAll();

    @PostMapping("/dept/add")
    boolean addDept(Dept dept);

}

修改Controlle

因為 Feign 使用的是介面和註解,所以不需要使用 REST 的 RestTemplate

@RestController
public class DeptConsumerController {

    @Autowired
    private DeptClientService service;

    @RequestMapping("/consumer/dept/get/{id}")
    public Dept get(@PathVariable("id") Long id){
        return this.service.queryById(id);
    }

    @RequestMapping("/consumer/dept/add")
    public boolean add(Dept dept){
        return this.service.addDept(dept);
    }

    @RequestMapping("/consumer/dept/get")
    public List<Dept> getList(){
        return this.service.queryAll();
    }

}

依次啟動註冊中心,服務提供者,SpringCloud-Consumer-Feign-80 ,測試是否能夠成功獲取服務

8. Hystrix

8.1 簡介

分散式系統面臨的問題

複雜分散式體系結構中的應用程式有數十個依賴關係,每個依賴關係在某些 時候將不再避免的失敗!

服務雪崩

多個微服務之間呼叫的時候,假設服務A呼叫微服務B 和微服務C,微服務B 和微服務C 又呼叫其他的微服務,這就是所謂的“扇出”,如果扇出的鏈路上某個微服務的呼叫響應時間過長或者不可用,對微服務A 的呼叫就會佔用越來越多的系統資源,進而引起系統崩潰,所謂的“雪崩效應”。

對於高流量的應用來說,單一的後端依賴可能會導致所有伺服器上的所有資源都在幾秒中飽和。比失敗更糟糕的是,這些應用程式還可能導致服務之間的延遲增加,備份佇列,執行緒和其他系統資源緊張,導致整個系統發生更多的級聯故障,這些都表示需要對故障和延遲進行隔離和管理,以便單個依賴關係的失敗,不能取消整個應用程式或系統。

我們需要棄車保帥

什麼是Hystrix

Hystrix 是一個用於處理分散式系統的延遲和容錯的開源庫,在分散式系統裡,許多依賴不可避免地都會呼叫失敗,比如超時、異常等,Hystrix 能夠保證在一個依賴出問題的情況下,不會導致整體服務失敗,避免級聯故障,以便提高分散式系統的彈性。
“斷路器”本省是一種開關裝置,當某個服務但願發生故障後,通過斷路器的故障監控(類似熔斷保險絲),向呼叫方返回一個服務預期的,可處理的備選響應(FailBack),而不是長時間地等待或者丟擲呼叫方式無法處理的異常,這樣就可以保證服務呼叫方的執行緒不會被長時間,不必要的佔用,從而避免了故障在分散式系統中的蔓延,乃至雪崩

能幹嘛

  • 服務降級
  • 服務熔斷
  • 服務限流
  • 接近實時的監控
  • ......

8.2 服務熔斷

是什麼

熔斷機制是對應雪崩效應的一種微服務鏈路保護機制。
當扇出鏈路的某個微服務不可用或者響應時間太長時,會進行服務的降級,進而熔斷該節點微服務的呼叫,快速返回錯誤的響應資訊。當檢測到該節點微服務呼叫響應正常後恢復呼叫鏈路。在 SpringCloud框架裡熔斷機制通過 Hystrix 實現。Hystrix 會監控微服務間呼叫的狀況,當失敗的呼叫到一定閾值,預設是5秒內20此呼叫失敗就會啟動熔斷機制,熔斷機制的註解是 @HystrixCommand

如何使用

建立一個新的工程專案 SpringCloud-Hystrix-8001,將原來的 8001 服務提供者的程式碼和配置複製一份過來

在pom檔案中新增 Hystrix 依賴

<!-- Hystrix -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-hystrix</artifactId>
    <version>1.4.7.RELEASE</version>
</dependency>

修改 DeptController

@RestController
public class DeptController {

    @Autowired
    DeptService deptService;

    // 熔斷後呼叫 hystrixGet 備選方法
    @HystrixCommand(fallbackMethod = "hystrixGet")
    @GetMapping("/dept/get/{id}")
    public Dept get(@PathVariable("id")Long id){
        Dept dept = deptService.getById(id);
        // id 傳的是空或者是無法查詢到id對應的部門資訊時
        if (dept==null){
            throw new RuntimeException("id-->"+id+", is not exist or can not find it");
        }
        return dept;
    }

    // /dept/get/{id} 熔斷時呼叫的方法
    public Dept hystrixGet(@PathVariable("id") Long id){
        return new Dept()
                .setDeptNo(id)
                .setDname("id-->"+id+"沒有對應的資訊,null-->Hystrix")
                .setDb_resource("no this database in MySQL");
    }

}

當發生熔斷時,我們需要快速返回錯誤資訊,上面的程式碼中,如果 get 方法無法使用或太長無法響應,則會因為使用了熔斷的註解 @HystrixCommand(fallbackMethod = "hystrixGet") 在異常時不會進行丟擲,而是使用 hystrixGet 方法代替 get 方法來返回錯誤資訊

新增熔斷支援 @EnableCircuitBreaker

@SpringBootApplication
@EnableEurekaClient // 在服務啟動後自動註冊到 Eureka 中
@EnableDiscoveryClient // 服務發現
@MapperScan("com.xp.mapper")
@EnableCircuitBreaker // 新增對熔斷的支援  CircuitBreaker:斷路器
public class ApplicationHystrix8001 {

    public static void main(String[] args) {
        SpringApplication.run(ApplicationHystrix8001.class,args);
    }

}

依次啟動註冊中心,SpringCloud-Hystrix-8001,服務消費者,訪問 http://localhost/consumer/dept/get/0 測試是否能夠觸發熔斷

然後我們停止 SpringCloud-Hystrix-8001

8.3 服務降級

是什麼

當伺服器壓力劇增的情況下,根據當前業務情況及流量對一些服務和頁面有策略地降級,以此釋放資源以保證核心任務的正常執行。也就是說,當整個微服務整體的負載超出了預設的上限閾值或即將到來的流量預計將會超過預設的閾值時,為了保證重要或基本的服務能正常執行,我們可以將一些不重要不緊急的服務或任務進行服務的延遲使用暫停使用

舉個例子,在銀行中,有很多個服務視窗可以辦理業務,其中也有特殊視窗(比如軍人優先,殘障人士優先的視窗),當銀行來了很多人進行辦理業務(伺服器壓力劇增,伺服器負載超過了上限閾值),普通視窗已經排了很多人了。這時候,銀行會暫時地關閉或延遲特殊視窗的服務,將特殊視窗變成普通視窗來辦理所有人的業務,待到銀行中辦理業務的人不那麼多時(下降到上限閾值以下)(將一些不重要或不緊急的服務或任務進行延遲使用或暫停使用),這就是服務降級。

如何使用

使用 Feign 實現服務降級

服務降級是客戶端來實現的,所以我們需要修改 SpringCloud-Consumer-Feign-80 工程專案

建立一個工廠類 DeptClientServiceFallbackFactory 用來統一對 DeptClentService 類裡的服務進行降級

@Component // 將這個類註冊到 Spring 容器中
public class DeptClientServiceFallbackFactory implements FallbackFactory {
    @Override
    public Object create(Throwable throwable) {
        // 服務降級,當客戶端服務關閉或延遲使用時,顯示對應的資訊告知使用者
        return new DeptClientService() {
            @Override
            public Dept queryById(Long id) {
                return new Dept().setDeptNo(id).setDname("沒有對應的資訊,客戶端提供了降級的資訊,這個服務現在已經被關閉").setDb_resource("沒有資料");
            }

            @Override
            public List<Dept> queryAll() {
                return null;
            }

            @Override
            public boolean addDept(Dept dept) {
                return false;
            }
        };
    }
}

修改 DeptClientService

// fallbackFactory 的值為服務降級的工廠類
@FeignClient(value = "SPRINGCLOUD-PROVIDER",fallbackFactory = DeptClientServiceFallbackFactory.class)
public interface DeptClientService {

    @GetMapping("/dept/get/{id}")
    Dept queryById(@PathVariable("id") Long id);

    @GetMapping("/dept/get")
    List<Dept> queryAll();

    @PostMapping("/dept/add")
    boolean addDept(Dept dept);

}

測試

依次開啟註冊中心,服務提供者,SpringCloud-Consumer-Feign-80,先測試服務是否能成功使用,然後再關閉服務提供者,重新整理檢視顯示結果

8.4 監控中心

建立一個新的工程專案 SpringCloud-Hystrix-Dashboard-9001

新增依賴

<dependencies>
    <!-- 我們需要使用到實體類,所以需要引入 api module -->
    <dependency>
        <groupId>com.xp</groupId>
        <artifactId>SpringCloud-Study-API</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
    <!-- SpringBoot -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <exclusions>
            <exclusion>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-tomcat</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <!-- jetty -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jetty</artifactId>
    </dependency>
    <!-- ribbon -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-ribbon</artifactId>
        <version>1.4.7.RELEASE</version>
    </dependency>
    <!-- Feign -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-feign</artifactId>
        <version>1.4.7.RELEASE</version>
    </dependency>
    <!-- eureka -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-eureka</artifactId>
        <version>1.4.7.RELEASE</version>
    </dependency>
    <!-- hystrix -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-hystrix</artifactId>
        <version>1.4.7.RELEASE</version>
    </dependency>
    <!-- hystrixDashboard -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-hystrix-dashboard</artifactId>
        <version>1.4.7.RELEASE</version>
    </dependency>
</dependencies>

配置 aplication.yml 檔案

配置服務啟動的埠號

server:
  port: 9001

開啟監控頁面

在 SpringCloud-Hystrix-Dashboard-9001 主啟動類上加上 @EnableHystrixDashboard 註解

@SpringBootApplication
@EnableHystrixDashboard // 開啟 hystrix 監控頁面
public class ApplicationDashBoard9001 {

    public static void main(String[] args) {
        SpringApplication.run(ApplicationDashBoard9001.class,args);
    }

}

啟動 SpringCloud-Hystrix-Dashboard-9001 ,訪問 http://localhost:9001/hystrix 檢視是否能成功啟動

這個監控頁面首頁可以看出,如果某個服務提供者需要被監控,則需要註冊到 /actuator/hystrix.stream

監控服務提供者 SpringCloud-Hystrix-8001

在 SpringCloud-Hystrix-8001 主啟動類中註冊監控

@SpringBootApplication
@EnableEurekaClient // 在服務啟動後自動註冊到 Eureka 中
@EnableDiscoveryClient // 服務發現
@MapperScan("com.xp.mapper")
@EnableCircuitBreaker // 新增對熔斷的支援  CircuitBreaker:斷路器
public class ApplicationHystrix8001 {

    public static void main(String[] args) {
        SpringApplication.run(ApplicationHystrix8001.class,args);
    }

    @Bean
    public ServletRegistrationBean hystrixMetricsStreamServlet(){
        ServletRegistrationBean registrationBean = new ServletRegistrationBean(new HystrixMetricsStreamServlet());
        registrationBean.addUrlMappings("/actuator/hystrix.stream");
        return registrationBean;
    }

}

http://localhost:9001/hystrix 頁面輸入監控地址,點選Monitor Stream 按鈕進行監控

監控分析:

  • 七色:進入監控介面後會有其中顏色的數字,其含義可以對用右上角相同顏色的單詞表示的狀態,其值代表該狀態下觸發的次數
  • 一圈:圈的大小代表該伺服器的流量,圖越大流量越大
  • 一線:代表監控間隔中,服務被訪問的頻率的折線圖
  • 通過觀察這些就可以在大量的例項中找出故障例項和高壓例項進行修復和維護

Dashboard監控說明圖:

(圖引自 https://blog.csdn.net/qq_33404395/article/details/80917484)

9. Zuul 路由閘道器

什麼是Zuul

在 SpringCloud 官網中有這麼一個圖

其中的 API Gateway,就是路由閘道器,Zuul 就是用來充當這個架構圖中的 API Gateway

Zuul 包含了對請求的路由和過濾兩個主要的功能

其中路由功能負責將外部請求轉發到具體的微服務例項上,是實現外部訪問統一入口的基礎,而過濾器功能則是負責對請求的處理過程進行干預,是實現請求校驗,服務聚合等功能的基礎。Zuul和Eureka進行整合,將 Zuul 自身註冊為 Eureka 服務治理下的應用,同時從 Eureka 中獲得其他微服務的資訊,也即以後的訪問微服務都是通過 Zuul 跳轉後獲得

注意:Zuul 服務最終還是會註冊 Eureka

提供:代理+路由+過濾器 三大功能!

如何使用

建立一個新的工程專案 SpringCloud-Zuul-9527

新增依賴

<dependencies>
    <!-- SpringBoot -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <exclusions>
            <exclusion>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-tomcat</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <!-- jetty -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jetty</artifactId>
    </dependency>
    <!-- eureka -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-eureka</artifactId>
        <version>1.4.7.RELEASE</version>
    </dependency>
    <!-- zuul -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-zuul</artifactId>
        <version>1.4.7.RELEASE</version>
    </dependency>
</dependencies>

編寫 application.yml 配置檔案

配置 zuul 路由

server:
  port: 9527

spring:
  application:
    name: springcloud-zuul

# euerka
eureka:
  client:
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/
  instance:
    instance-id: zuul9527.com

# zuul
zuul:
  routes:
    dept.serviceId: springcloud-provider # 服務名
    dept.path: /mydept/** # 訪問該服務的路徑
  ignored-services: "*" # 禁止直接通過服務名訪問服務,需要加上雙引號
  prefix: /xp # 字首,如果不加上字首則無法訪問

info:
  app.name: xp-springcloud-zuul
  company.name: xp

開啟註解

在主啟動類中增加 @EnableZuulProxy 註解

@SpringBootApplication
@EnableZuulProxy // 開啟 zuul 代理
public class ApplicationZuul9527 {

    public static void main(String[] args) {
        SpringApplication.run(ApplicationZuul9527.class,args);
    }

}

域名對映

為了模仿我們真實開發中使用網址進行訪問,所以進行域名對映

127.0.0.1 www.xp.com

測試

依次啟動註冊中心,服務提供者,SpringCloud-Zuul-9527

訪問 http://www.xp.com:9527/xp/mydept/dept/get/1 測試

如果埠號使用 80 埠,則與真實的網站訪問無異

10. SpringCloud Config 分散式配置

10.1 概述

分散式系統面臨的配置檔案問題

微服務意味著要將單體應用中的業務拆分成一個個子服務,每個自服務的粒度相對較小,因此係統中會出現大量的服務,由於每個服務都需要必要的配置資訊才能執行,所以一套集中式的,動態的配置管理設施是必不可少的。SpringCloud 提供了 ConfigServer 來解決這個問題,我們每一個微服務自己帶著一個 application.yml,那上百個的配置檔案要修改起來,豈不是要發瘋。

什麼是 SpringCloud Config 分散式配置中心

Spring Cloud Config 為微服務架構中心的微服務提供集中化的外部配置支援,配置伺服器為各個不同微服務應用的所有環節提供了一個中心化的外部配置

Spring Cloud Config 分為服務端客戶端兩部分

服務端也成為分散式配置中心,它是一個獨立的微服務應用,用來連線配置伺服器併為客戶端提供獲取配置資訊,加密,解密資訊等訪問介面。

客戶端則是通過指定的配置中心來管理應用資源,以及與業務相關的配置內容,並在啟動的時候從配置中心獲取和載入配置資訊。配置伺服器預設採用git來儲存配置資訊,這樣有助於對環境配置進行版本管理。並且可以通過git客戶端工具來方便的管理和訪問配置內容。

Spring Cloud Config 分散式配置中心能幹嘛

  • 集中管理配置檔案
  • 不同環境,不同配置,動態化的配置更新,分環境部署,比如 /dev,/test,/prod,/beta,/release 等等
  • 執行期間動態調整配置,不再需要在每個服務部署的機器上編寫配置檔案,服務會向配置中心統一拉去配置自己的資訊
  • 當配置發生變動時,服務不需要重啟,即可感知配置的變化,並應用新的配置
  • 將配置資訊以REST介面的形式暴露

10.2 Spring Cloud Config 分散式配置中心與 git 整合

由於 Spring Cloud Config 預設使用 git 來儲存配置檔案(也有其他方式,比如支援 SVN 和本地檔案 ),但是最推薦的還是 git,而且使用的是 http/https 訪問的形式

由於國內碼雲gitee比github速度要快,所以這裡使用gitee來當作git的遠端倉庫

服務端

在碼雲上建立一個新的遠端倉庫,然後使用ssh克隆到本地

在本地克隆遠端倉庫

新建一個檔案 application.yml ,使用記事本開啟,然後編寫配置提交到遠端倉庫

這裡所說的提交到遠端倉庫,都是使用如下的git命令操作

git add .
git commit -m 提交的資訊
git push 

application.yml 的配置內容如下

spring:
  profiles:
  active: dev

---
spring:
  profiles: dev
  application:
    name: springcloud-config-dev

---
spring:
  profiles: test
  application:
    name: springcloud-config-test

建立一個新的工程專案 SpringCloud-Config-3344,作為 Spring Cloud Config 的服務端

匯入依賴

<dependencies>
    <!-- SpringCloudConfig-server 服務端 -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-config-server</artifactId>
    </dependency>
    <!-- springboot -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <exclusions>
            <exclusion>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-tomcat</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <!-- jetty -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jetty</artifactId>
    </dependency>
</dependencies>

編寫 application.yml 配置檔案

server:
  port: 3344
spring:
  application:
    name: SpringCloud-Config-server
  cloud:
    config:
      server:
        git:
          uri: https://gitee.com/windows_xp_xp/spring-cloud-config-test.git # https 連結,不是 ssh 連結

在啟動類上加上 @EnableConfigServer 註解

@SpringBootApplication
@EnableConfigServer // 開啟 SpringCloudConfig 服務端
public class ApplicationConfig3344 {

    public static void main(String[] args) {
        SpringApplication.run(ApplicationConfig3344.class,args);
    }

}

啟動該專案並訪問 http://localhost:3344/application-dev.yml 檢視是否配置成功

觀察 http://localhost:3344/application-dev.ymlhttp://localhost:3344/application-test.yml 的不同

在官網中有寫到具體怎麼訪問 Config 服務端中的配置檔案

官網地址:https://docs.spring.io/spring-cloud-config/docs/2.2.5.RELEASE/reference/html/#_spring_cloud_config_client

/{application}/{profile}[/{label}]
/{application}-{profile}.yml
/{label}/{application}-{profile}.yml
/{application}-{profile}.properties
/{label}/{application}-{profile}.properties

其中 application 表示配置檔案的名字,profile 表示配置的環境(比如:dev,test),label 表示遠端倉庫的分支

:有可能會出現訪問失敗的可能,報錯資訊為 Authentication is required but no CredentialsProvider has been registered,此時,需要將倉庫許可權設定為公開或在服務端配置檔案中新增碼雲的賬號和密碼

server:
  port: 3344
spring:
  application:
    name: SpringCloud-Config-server
  cloud:
    config:
      server:
        git:
          uri: https://gitee.com/windows_xp_xp/spring-cloud-config-test.git # https 連結,不是 ssh 連結
          # 配置賬號和密碼,一般來說,為了安全起見,建議使用賬號密碼的方式來獲取遠端倉庫的檔案配置
          username: xxxx # 碼雲賬號
          password: xxxx # 碼雲密碼

客戶端

在我們本地的git倉庫中建立配置檔案 application-client.yml

spring:
  profiles:
  active: dev

---
server:
  port: 8021
spring:
  application:
    name: springcloud-config-dev
  profiles: dev
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: org.gjt.mm.mysql.Driver
    url: jdbc:mysql://localhost:3306/db01?useUnicode=true&characterEncoding=utf-8&useSSL=false
    username: root
    password: root
# eureka 配置
eureka:
  client:
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/
  instance:
    instance-id: springcloud-config # 修改 Eureka 中預設描述資訊
    prefer-ip-address: true # 訪問路徑可以顯示ip地址

---
server:
  port: 8022
spring:
  application:
    name: springcloud-config-test
  profiles: test
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: org.gjt.mm.mysql.Driver
    url: jdbc:mysql://localhost:3306/db01?useUnicode=true&characterEncoding=utf-8&useSSL=false
    username: root
    password: root
# eureka 配置
eureka:
  client:
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/
  instance:
    instance-id: springcloud-config # 修改 Eureka 中預設描述資訊
    prefer-ip-address: true # 訪問路徑可以顯示ip地址

將 application-client.yml 提交到碼雲遠端倉庫中

建立一個新的工程專案 SpringCloud-Config-Client ,模擬獲取 Config 配置的客戶端

編寫配置檔案 application.yml,bootstrap.yml

application.yml 使用者級別的配置

# 使用者級別的配置
spring:
  application:
    name: SpringCloud-Config-Client-3355

bootstrap.yml 系統級別的配置

# 系統級別的配置
spring:
  cloud:
    config:
      uri: http://localhost:3344 # 提供遠端使用配置檔案的微服務uri
      profile: test # 環境選擇
      label: master # 分支
      name: application-client # 需要從git讀取的資源名稱,不需要字尾,這裡獲取的就是 applicaiont-client.yml

編寫 Controller 訪問從遠端配置檔案配置的變數引數

@RestController
public class ConfigClientController {

    @Value("${spring.application.name}")
    private String applicationName;

    @Value("${eureka.client.service-url.defaultZone}")
    private String defaultZone;

    @Value("${server.port}")
    private String serverPort;

    @RequestMapping("/config")
    public HashMap<String, String>  config(){
        HashMap<String, String> configMap = new HashMap<>();
        configMap.put("applicationName:",applicationName);
        configMap.put("defaultZone:",defaultZone);
        configMap.put("serverPort:",serverPort);
        return configMap;
    }

}

啟動 SpringCloud-Config-Client ,訪問 http://localhost:8021/config

我們需要修改配置的時候,在 bootstarp.yml 中修改 spring.cloud.config.profile 即可