Spring Cloud學習總結(一)

敲程式碼的ciery發表於2020-10-22

微服務學習

0.學習目標

  • 使用RestTemplate傳送請求
  • 明確SpringCloud的作用
  • 搭建Eureka註冊中心
  • 使用Robbin負載均衡
  • 使用Hystrix熔斷器

為什麼要學習Spring Cloud?

在專案開發中,隨著業務越來越多,導致功能間耦合性越來越高開發效率低系統執行緩慢難以維護及不穩定,微服務可以解決這些問題。而Spring Cloud是微服務的常用實現方式

言外之意,微服務可以降低專案功能模組間的耦合性,提高開發效率,利於專案維護

1.系統架構的演變

隨網際網路的發展,網站應用的規模不斷擴大,需求的激增帶來技術壓力。系統架構因此不斷進行相應的演進、升級、迭代。從單一應用,到垂直拆分、分散式服務、SOA、以及微服務、Google下的Service Mesh。

1.1 集中式架構

當網站流量很小的時候,只需要部署一個應用就可提供服務,這個時候可以將所有的服務部署在一起,以減少部署節點和成本
在這裡插入圖片描述

優點:

  • 系統開發速度快
  • 部署簡單,維護成本低
  • 適用於併發要求較低的系統

缺點:

  • 程式碼耦合度高(專案中不同模組間存在相互呼叫的時候就提升了耦合度),後期維護困難
  • 無法針對不同模組進行鍼對性優化(要改一個可能就得改全部模組)
  • 無法水平擴充套件(如果需要部署,要將專案中所有的模組進行部署)
  • 單點容錯率低(當某個模組因為某些原因導致當機,其他模組即使正常也無法使用),併發能力差(不能支援高併發的訪問量)

1.2 垂直拆分

當網站的訪問量逐漸增大,單一應用不再滿足需求。為應對更高的併發和業務需求,可根據業務功能對系統進行垂直拆分
在這裡插入圖片描述

優點:

  • 系統拆分實現流量分擔(從原來的由一個系統承擔所有的請求到現在根據不同的請求分配到不同的模組實現訪問分流),解決併發問題
  • 可針對不同模組進行優化(不同模組互相獨立)
  • 可水平擴充套件,負載均衡,容錯率高(模組是各自獨立的子系統)

缺點:

  • 系統間相互獨立,會有很多重複開發工作,影響開發效率(冗餘重複程式碼多

1.3 分散式服務

當垂直應用越來越多,應用間互動不可避免,可以將核心業務抽取出來作為獨立的服務,形成穩定的服務中心,使應用可更快速響應多變的市場需求

在這裡插入圖片描述

將基礎業務的重複功能抽取出來作為基礎服務,以降低程式碼的重複冗餘度。例如使用者中心和後臺管理系統都需要快取,那麼就抽取出一個快取服務作為專案的基礎服務,其他業務需要用到快取的時候直接呼叫即可

優點:

  • 將基礎服務進行抽取,系統間可相互呼叫,提高程式碼複用和開發效率

缺點:

  • 系統間耦合度變高,呼叫關係複雜,難以維護

1.4 面向服務架構(SOA)

Service Oriented Architecture:一種設計方法,包含多個服務,服務間通過相互依賴最後提供一系列的功能。

一個服務通常以獨立的形式存在於作業系統程式中,各個服務間通過網路進行呼叫

在這裡插入圖片描述

ESB(企業服務匯流排):一根用來連線各個服務節點的管道。為整合不同系統、不同協議的服務,ESB支援訊息的轉換解釋和路由工作,從而讓不同的服務間可互連互通

簡化了原本服務間互相呼叫中對於呼叫關係的配置和處理過程

優點:

  • 簡化不同服務間呼叫過程

缺點:(ESB的複雜性)

  • 每個供應商提供的ESB有偏差,自身實現較為複雜
  • 應用服務粒度較大,ESB整合整合所有服務和協議、資料轉換使得運維、部署、測試困難
  • 所有服務通過一個通路進行通訊,直接降低了通訊速度

1.5 微服務架構

SOA使用ESB元件的面向服務架構由於ESB本身實現複雜;應用粒度大,所有服務間通訊都通過ESB會減低通訊速度;部署測試比較麻煩

微服務架構是使用一套小服務來開發單一應用的方式或途徑,每個服務基於單一業務能力構建,執行在自己的程式中,並使用輕量級機制通訊,通常是HTTP API,並通過自動化部署機制獨立部署。這些服務可使用不同的程式語言實現,以及不同的資料儲存技術,並保持最低限度的集中式管理

在這裡插入圖片描述

API Gateway閘道器:一個伺服器,是系統的唯一入口,為使用者提供定製的API

其核心功能是所有的客戶端和消費端都通過唯一的閘道器接入微服務,在gateway層處理所有的非業務功能

也可進行身份驗證、監控、負載均衡、快取、請求分片與管理、靜態響應處理

gateway提供RESTful/HTTP的方式訪問服務(消費端無需記錄具體服務A/B/C的地址,直接記錄gateway的地址即可,gateway會根據各自的服務在此處的配置特點匹配不同的服務,而服務端通過服務註冊中心進行服務註冊和管理

微服務的特點:

  • 單一職責:微服務中每個服務都對應唯一的業務功能
  • 微:服務拆分粒度很小,例如可將使用者管理作為一個服務。服務雖小五臟俱全
  • 面向服務:每個服務都要對外暴露Rest風格的服務介面API,並不關心服務的技術實現,做到與平臺、語言無關,只需要保證提供的是Restful介面即可
  • 自治:服務間相互獨立
    • 團隊獨立:每個服務都是獨立的開發團隊
    • 技術獨立:面向服務,每個服務只要保證提供Restful介面即可,具體使用什麼技術並不關心
    • 前後端分離:後端不再為PC、移動端開發提供不同介面
    • 資料庫分離:每個服務可有自己的資料來源
    • 部署獨立:服務間可相互呼叫,但是可以做到一個服務重啟不影響其他服務,有利於持續整合和交付。每個服務都是獨立的元件,可複用、替換,降低耦合,易維護

微服務和SOA都是對系統進行拆分

微服務是基於SOA思想,可以將其看作是去除了ESB的的SOA

ESB是SOA的中心匯流排,設計圖是星型架構

微服務是去中心化的分散式軟體架構

功能SOA微服務
元件大小大塊業務邏輯單獨任務或小塊業務邏輯
耦合usually鬆耦合always鬆耦合
管理著重中央管理著重分散管理
目標確保應用能夠互動操作易維護、擴充套件、更輕量級的互動

總結

集中式架構
垂直拆分
分散式服務
SOA面向服務架構
微服務架構

集中式架構由於功能模組間耦合度高,單一應用不支援高併發的訪問量,根據不同的功能模組進行了垂直拆分

垂直拆分之後,可以通過請求分流解決併發問題,但是模組間相互獨立程式碼複用低,所以將共用的服務提取出來形成核心服務模組進行分散式服務

分散式服務將模組間通用模組抽取出來成為獨立服務為其他模組提供呼叫服務提高程式碼複用率,但是呼叫關係複雜難以維護

SOA面向服務架構通過ESB簡化呼叫,解決了分散式服務中不同服務間呼叫關係複雜的問題,但是ESB複雜實現複雜,測試部署運維成本高

所以引出微服務架構,服務間相互獨立,通過註冊到統一的註冊中心實現服務註冊,可通過註冊中心+訪問路徑對服務進行訪問

微服務架構:一套使用小服務/單一業務來開發單個應用的方式或途徑

特點:

  • 單一職責
  • 服務粒度小
  • 面向服務(暴露Restful介面)
  • 服務間相互獨立

與SOA的區別:微服務架構基於SOA,但是沒有使用ESB,有服務治理註冊中心,業務粒度小

2. 服務呼叫方式

2.1 RPC & HTTP

常見的遠端呼叫方式:

  • RPC:Remote Procedure Call遠端過程呼叫,基於Socket,工作在會話層可自定義資料格式,速度快、效率高。例如webservice、dubbo
  • HTTP:一種網路傳輸協議,基於TCP,工作在應用層規定了資料傳輸的格式。現在客戶端、瀏覽器與服務端通訊基本都使用HTTP協議,也可用於進行遠端服務呼叫。例如Restful風格介面、Spring Cloud
    • 缺點:訊息封裝臃腫
    • 優點:對服務的提供和呼叫方沒有任何技術、語言限定,自由靈活,更符合微服務理念
  • 區別:RPC的機制是根據語言的API來定義的,而非根據基於網路的應用來定義的

如果全部採用Java技術棧,使用Dubbo作為微服務架構

如果技術棧多元化,且並傾向於使用Spring家族,那麼使用Spring Cloud搭建微服務架構(使用HTTP協議實現服務間呼叫)

RPC:基於Socket,處於會話層,速度快,效率高

HTTP:基於TCP,處於應用層,訊息封裝臃腫但是對服務的提供和呼叫沒有技術、語言限制

ISO網路層次:應用層、表示層、會話層、傳輸層、網路層、資料鏈路層、物理層

3. Spring RestTemplate示例工程匯入

有三種http客戶端工具類包可方便進行http服務呼叫:

  • HttpClient
  • OkHttp
  • JDK原生的URLConnection

Spring提供了RestTemplate對上述三種http客戶端工具類包進行封裝,可以在Spring專案中使用RestTemplate進行遠端HTTP服務呼叫

RestTemplate支援的物件與JSON字串的序列化(物件到JSON)與反序列化(JSON到物件)

  1. 在啟動類位置註冊一個RestTemplate物件
@SpringBootApplication
public class HttpDemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(HttpDemoApplication.class, args);
    }

    // 也可直接在使用的時候RestTempalte restTempalte = new RestTemplate();
    // 但是使用new建立一個物件,會增加專案程式間的耦合度,並且這個物件建立之後就用這一次,用完不能立即清理造成記憶體浪費;且多處都可能要new一個restTemplate進行使用
    // 基於Spring框架,將RestTemplate物件註冊為一個bean物件加入到spring容器中進行管理,使用的時候通過@Autowired注入程式即可
    @Bean
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }
}
  1. 在測試類中使用RestTemplate進行網路通訊
@RunWith(SpringRunner.class)
@SpringBootTest
public class RestTemplateTest {

    @Autowired
    private RestTemplate restTemplate;

    @Test
    public void test(){
        String url = "http://localhost/xxxx";
        // RestTemplate可對json格式字串進行反序列化
        // 使用restTemplate的getForObject(url,Object.class)方法,引數為呼叫的restful風格的url地址和呼叫返回值實體類的位元組碼
        // restTemplate可自動發起請求接收響應,並對響應結果進行反序列化
        Object object = restTemplate.getForObject(url, Object.class);
        System.out.println(user);
    }
}

4. Spring Cloud

微服務是一種架構方式,最終是需要技術進行實現的,目前最常用的就是Spring Cloud

  • 後臺硬:Spring家族一員
  • 技術強:Spring作為Java領域的前輩,功力深厚,有強力的技術團隊支撐
  • 使用基數大:很多公司使用Spring框架,Spring Cloud可與Spring框架無縫整合
  • 使用方便:Spring Cloud支援Spring Boot開發,用很少的配置可完成微服務框架的搭建

4.1 簡介

Spring Cloud同Spring(整合流程框架到自己的專案中)一樣,將現下流行的技術整合在一起,實現諸如配置管理、服務發現、智慧路由、負載均衡、熔斷器、控制匯流排、叢集狀態等功能;協調分散式環境中各系統,為各類服務提供模板性配置,涉及元件主要有:

  • Eureka:註冊中心
  • Zuul、Gateway:服務閘道器
  • Ribbon:負載均衡
  • Feign:服務呼叫
  • Hystrix、Resilience4j:熔斷器

在有需要的時候在專案新增元件對應的啟動依賴即可

在這裡插入圖片描述

請求通過Zuul/Gateway閘道器訪問服務,進入GateWay後通過Ribbon負載均衡演算法處理後分發到對應的服務模組中進行真正的請求訪問

功能模組通過在註冊中心Eureka進行註冊成為服務,服務間相互獨立,可通過Feign進行呼叫

如果需要更改服務的配置,可通過Config Server統一修改

4.2 版本

以英文單詞命名(倫敦地鐵站名)

常用Greenwich,較為穩定

5.微服務場景模擬

需求:查詢資料庫中的使用者資料並輸出到瀏覽器

目標:建立微服務——

  • 父工程demo-springcloud:新增spring boot父座標和管理其他元件的依賴
  • 使用者服務工程user-service:整合mybatis查詢資料庫中使用者資料;提供查詢使用者服務
  • 服務消費工程consumer-demo:利用查詢使用者服務獲取使用者資料並輸出到瀏覽器

1.建立一個父工程springcloud並匯入各元件版本

    <properties>
        <java.version>1.8</java.version>
        <!-- Spring Cloud使用Greenwich版本 -->
        <spring-cloud.version>Greenwich.SR1</spring-cloud.version>
        <mapper.starter.version>2.1.5</mapper.starter.version>
        <mysql.version>5.1.46</mysql.version>
    </properties>
            <!-- springCloud -->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

通過scopeimport可以整合spring-cloud-dependencies工程中的依賴

2.父工程中新建兩個子工程user-service和consumer-demo

    <!-- 子模組 -->
    <modules>
        <module>user-service</module>
        <module>consumer-demo</module>
    </modules>

5.1 搭建配置user-service工程

需求:可通過訪問http://localhost:8080/user/13輸出使用者資料(配置user-service工程並能夠根據使用者id查詢資料庫中使用者)

1.新增啟動器依賴(web、通用Mapper、mysql)

   <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- 通用Mapper啟動器 -->
        <dependency>
            <groupId>tk.mybatis</groupId>
            <artifactId>mapper-spring-boot-starter</artifactId>
            <!-- 因為父工程中已經指明依賴的版本,所以此處無需再寫 -->
        </dependency>
        <!-- mysql驅動 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
    </dependencies>

2.建立啟動引導類UserServiceApplication.java和配置檔案application.yml

@SpringBootApplication
// import tk.mybatis.spring.annotation.MapperScan; not import org.mybatis.spring.annotation.MapperScan;
@MapperScan("com.demo.mapper")
public class UserServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(UserServiceApplication.class, args);
    }
}
server:
  port: 8080

spring:
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/springcloud
    username: root
    password: root
  # 配置時間資料從資料庫讀出後轉為String輸出到前臺時鐘+8
  jackson:
    time-zone: GMT+8
    date-format: yyyy-MM-dd HH:mm:ss
mybatis:
  type-aliases-package: com.demo.domain

3.編寫測試程式碼(UserMapper、UserService、UserController)

  1. 1 實體類User.java
@Data
@Table(name = "tb_user")
public class User {
    @Id
    // 開啟主鍵自動回填(insert之後返回一個主鍵)
    @KeySql(useGeneratedKeys = true)
    private Long id;

    private String userName;

    private String password;

    private String name;

    private Integer sex;

    private Date birthday;

    private Date created;

    private Date updated;

    private String note;
}

3.2 對映介面UserMapper.java

import tk.mybatis.mapper.common.Mapper;

/**
 * @ClassName UserMapper
 * @Author wx
 * @Date 2020/10/19 0019 15:00
 **/
public interface UserMapper extends Mapper<User> {
}

3.3 業務實現類UserService.java

@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    /**
     * 根據id查詢使用者
     * @param id
     * @return
     */
    public User queryById(Long id) {
        return userMapper.selectByPrimaryKey(id);
    }
}

3.4 前後端互動類UserController.java

@RestController
@RequestMapping("/user")
public class UserController {
    @Autowired
    private UserService userService;

    @GetMapping("/{id}")
    public User queryById(@PathVariable("id") Long id) {
        return userService.queryById(id);
    }
}

4.測試

在這裡插入圖片描述

5.2 搭建配置consumer-demo工程

需求:訪問http://localhost:8083/consumer/13 使用RestTemplate獲取http://localhost:8080/user/13的資料(使用RestTemplate訪問user-service的路徑根據id查詢使用者)

1.新增啟動器依賴(web)

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

2.建立啟動引導類(註冊RestTemplate)和配置檔案(application.yml)

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

    // 構建一個RestTemplate物件加入到spring容器中進行統一管理
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}
server:
  port: 8083

spring:
  jackson:
  # 此處需要注意consumer配置jackson之後,user-service就不能配置了,否則會報錯
    time-zone: GMT+8
    date-format: yyyy-MM-dd HH:mm:ss

3.編寫測試程式碼(ConsumerController中使用RestTemplate訪問user-service服務獲取資料)

3.1 複製建立User.java實體類

// 此處無需和資料庫進行對映,所以直接就是普通的pojo類
@Data
public class User {
    private Long id;

    private String userName; // 使用者名稱

    private String password;

    private String name; // 姓名

    private Integer sex;

    private Date birthday;

    private Date created;

    private Date updated;

    private String note;
}

3.2 建立介面類ConsumerController.java

@RestController
@RequestMapping("/consumer")
public class ConsumerController {

    @Autowired
    RestTemplate restTemplate;

    @GetMapping("/{id}")
    public User queryById(@PathVariable("id") Long id) {
        String url = "http://localhost:8080/user/" + id;
        User user = restTemplate.getForObject(url, User.class);
        return user;
    }
}

4.測試

在這裡插入圖片描述

5.3 思考存在的問題

簡單回顧,剛才做的事情:

  • user-service:對外提供查詢使用者的介面
  • consumer-demo:通過RestTemplate訪問user-service提供的restful介面,查詢使用者資料

存在哪些問題?

  • 在consumer中,url地址屬於硬編碼,不方便後期維護
  • consumer需要記憶user-service的地址(url),如果變更的話可能得不到通知,地址url失效
  • consumer不清楚user-service的狀態,即使user-service當機也無法知道
  • user-service只有一臺服務,不具備高可用性(掛了就是掛了,沒有容災機制)
  • 即便user-service形成叢集,consumer還是得自己實現負載均衡(自己去考慮去呼叫哪個user-service服務)

總結一下,上面的問題(服務狀態不清楚;服務url硬編碼;服務沒有高可用)是分散式服務(user-service和consumer分割獨立)必然面臨的問題:

  • 服務管理
    • 如何自動註冊和發現服務
    • 如何實現服務狀態監管
    • 如何實現服務的動態路由(url可變)
  • 服務如何實現負載均衡
  • 服務如何解決容災問題
  • 服務如何實現統一配置

Spring Cloud可以通過整合各種元件解決上述問題。

6. Eureka註冊中心

6.1 認識Eureka

解決第一個問題:服務的管理

問題分析:

在5.1-5.2中,user-service對外提供服務,需要對外暴露自己的地址consumer-demo呼叫者需要記錄服務提供者user-service的地址。

未來地址變更的時候consumer這邊必須得及時更新,這一點在服務少的時候還沒有什麼問題,但是現在日益複雜的網際網路環境下,一個專案可能會拆分出十幾甚至幾十個微服務,這個時候再人為去管理地址,不僅開發困難,未來測試、釋出上線都很麻煩,和DevOps思想背道而馳

DevOps:系統可以通過一組過程、方法或系統,提高應用釋出和運維的效率,降低管理成本

以網約車為例進行說明。

網約車之前,人們出門只能叫計程車,那一些私家車想做出租沒有資格被稱為“黑車”;一些人又因為車太少了叫不到車,路上的車也沒辦法知道到底可否載人。一個想要,一個願意給,就是缺少引子缺少管理。

那這個時候滴滴這樣的網約車平臺出現了,所有想載客的私家車全部註冊,記錄車型、身份資訊等。叫車的人只要開啟APP,輸入目的地選擇車型,滴滴就會自動安排一個符合需求的車

Eureka做什麼?

Eureka就好比滴滴,負責管理、記錄服務提供者的資訊。服務呼叫者(使用者)無需自己尋找服務,只需要將自己的需求告訴Eureka,Eureka就會把符合使用者需求的服務告訴服務呼叫者。

同時Eureka與服務提供者之間通過“心跳”機制進行監控,當某個服務提供者出現問題,Eureka自然會將其從服務列表剔除。

由此,實現服務的自動註冊、發現和狀態監控

6.2 原理

在這裡插入圖片描述

  1. 服務提供者例項化服務
  2. 服務提供者將服務註冊到Eureka
  3. Eureka記錄著服務提供者註冊的服務資訊,例如服務地址等
  4. 服務消費者從Eureka定期獲取服務列表
  5. 基於負載均衡演算法服務消費者從可滿足需求的服務列表中選擇一個服務地址呼叫服務
  6. 服務提供者定期向Eureka傳送心跳
  7. Eureka會檢查那些沒有定期傳送心跳的服務提供者,並將其從服務列表中剔除
  • Eureka:服務註冊中心(可以是一個叢集),對外暴露自己的地址(為使用者提供服務列表)
  • ApplicationService:啟動後想Eureka註冊自己的資訊(地址、提供什麼服務)
  • ApplicationClient:向Eureka訂閱服務,Eureka就會將對應服務的所有提供者地址列表傳送給消費者,並且定時更新
  • renew心跳:提供者定時通過http方式向Eureka重新整理自己的狀態

Eureka的主要功能:

  • 進行服務管理(服務註冊)
  • 定期檢查服務狀態
  • 返回服務地址列表

6.3 入門案例

6.3.1 搭建eureka-server工程

需求:新增eureka對應依賴,編寫引導類搭建eureka服務並訪問eureka服務介面

分析:

  • Eureka是服務註冊中心,只能做服務註冊自身並不提供服務也不消費服務
  • 可以搭建web工程使用eureka(可通過Spring Boot方式搭建)

1.建立工程

在5.1中建立的springcloud工程中新增一個module,命名為eureka-server

2.新增啟動器依賴

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
            <!-- 此處由於父模組中已經通過spring-cloud-dependencies對版本進行鎖定,所以無需再設定 -->
        </dependency>
    </dependencies>

3.編寫啟動引導類(新增Eureka的服務註解@EnableEurekaServer)和配置檔案application.yml

// 宣告當前應用是eureka服務
@EnableEurekaServer
@SpringBootApplication
public class EurekaServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(EurekaServerApplication.class, args);
    }
}

4.修改配置檔案(例如埠、應用名稱等)

server:
  port: 10086

spring:
  application:
  # 設定註冊器應用名稱,會在Eureka中作為服務的id標識(serviceId)
    name: eureka-server
eureka:
  client:
    service-url:
      # eureka服務地址,如果是叢集則需要指定其他叢集的eureka地址
      defaultZone: http://127.0.0.1:10086/eureka
    # 不註冊自己(如果是叢集需要設定為true,使得叢集中其他eureka節點可以發現本eureka
    register-with-eureka: false
    # 不拉取自己的服務(同register-with-eureka,如果是叢集則設定true)
    fetch-registry: false

5.測試

在這裡插入圖片描述

如果application.yml中設定register-with-eureka為true,即註冊Eureka本身到服務註冊中心,則eureka的服務介面上可以看到自己服務
在這裡插入圖片描述

6.3.2 服務註冊

需求:將user-service服務註冊到Eureka

分析:

  • 服務註冊:在服務提供者user-service上新增Eureka客戶端依賴,自動將這個服務註冊到EurekaServer服務地址列表
    • 新增依賴
    • 改造啟動引導類,新增開啟Eureka客戶端發現註解
    • 修改配置檔案,設定Eureka服務註冊地址

1.新增依賴

​ user-service的依賴檔案pom.xml中新增Eureka客戶端依賴

        <!-- Eureka客戶端依賴 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>

2.改造啟動引導類

​ 啟動類中新增@@EnableDiscoveryClient開啟Eureka客戶端發現功能

// 開啟Eureka客戶端發現
@EnableDiscoveryClient
@SpringBootApplication
@MapperScan("com.demo.mapper")
public class UserServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(UserServiceApplication.class, args);
    }
}

3.修改配置檔案

​ 在配置檔案application.yml中新增服務名稱和Eureka服務註冊中心地址

server:
  port: 8080

spring:
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/springcloud
    username: root
    password: root
  application:
    # 應用名(將來會作為服務的id使用)
    name: user-service

mybatis:
  type-aliases-package: com.demo.domain
  
# 新增Eureka服務註冊中心地址,url要和Eureka工程的url保持一致
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:10086/eureka

4.測試

​ 啟動Eureka服務和user-service服務之後,可以看到Eureka的服務介面上有了user-service服務

在這裡插入圖片描述
在這裡插入圖片描述
在這裡插入圖片描述

6.3.3 服務發現

在consumer-demo中可以根據服務名稱進行呼叫

分析:

  • 服務發現:在服務消費者consumer-demo上新增Eureka客戶端依賴,可使用工具類DiscoveryClient根據服務名稱獲取對應的服務地址列表
    • 新增依賴
    • 改造啟動引導類,新增開啟Eureka客戶端發現註解
    • 修改配置檔案,設定Eureka服務地址
    • 改造處理器ConsumerController,使用DiscoveryClient根據服務名稱獲取對應的服務地址,並通過RestTemplate呼叫

1.新增依賴

​ consumer-demo的依賴檔案pom.xml中新增Eureka客戶端依賴

        <!-- Eureka客戶端依賴 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>

2.修改啟動引導類

​ 啟動類中新增@@EnableDiscoveryClient開啟Eureka客戶端發現功能

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

    // 構建一個RestTemplate物件加入到spring容器中進行統一管理
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

3.修改配置檔案

​ 在配置檔案application.yml中新增服務名稱和Eureka服務註冊中心地址

server:
  port: 8083

spring:
  jackson:
    time-zone: GMT+8
    date-format: yyyy-MM-dd HH:mm:ss
  application:
    name: consumer-demo

# Eureka地址
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:10086/eureka

4.改造處理器類

​ 使用工具類DiscoveryClient的getInstance方法(引數為user-service服務提供者在其自身applicaiton.yml配置的服務名)發現user-service服務(getInstance方法獲取的是服務列表,實際中此時列表中只有一個服務,所以通過get(0)獲取user-service服務),然後通過服務例項拼接url,最後通過restTemplate訪問user-service提供的服務

@RestController
@RequestMapping("/consumer")
public class ConsumerController {

    @Autowired
    RestTemplate restTemplate;

    @Autowired
    DiscoveryClient discoveryClient;

    @GetMapping("/{id}")
    public User queryById(@PathVariable("id") Long id) {
       // String url = "http://localhost:8080/user/" + id;

        // 和獲取eureka中註冊的user-service例項
        List<ServiceInstance> serviceInstances = discoveryClient.getInstances("user-service");
        ServiceInstance serviceInstance = serviceInstances.get(0);
        String url = "http://" + serviceInstance.getHost() + ":" + serviceInstance.getPort() + "/user/" + id;
        User user = restTemplate.getForObject(url, User.class);
        return user;
    }
}

5.debug檢視url拼接成功

在這裡插入圖片描述

6.測試

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-IGnlrcaV-1603379783732)(D:\面試\Spring Cloud\images\服務發現)]

總結:

Eureka註冊中心需要:

  • 新增Eureka Server依賴
  • 啟動類需要新增@EnableEurekaServer註解表明eureka服務開啟
  • 配置檔案需要新增服務名稱和Eureka服務地址

服務註冊和服務發現都需要:

  • 新增Eureka Client依賴
  • 啟動類需要新增@EnableDiscoveryClient註解表明eureka客戶端發現功能開啟
  • 配置檔案需要新增服務名稱spring.application.name和Eureka服務地址eureka.client.service-url.defaultZone

6.4 Eureka詳解

6.4.1 基礎架構

Eureka架構有三個核心角色,分別是服務註冊中心、服務提供者和服務消費者。

  • 服務註冊中心
    • Eureka的服務端應用,提供服務註冊和發現功能,例剛建立的eureka-server
  • 服務提供者
    • 提供服務的應用,可以是SpringBoot應用,也可通過其他技術實現。只需要保證可以對外暴露自己的Restful風格介面即可,例user-service
  • 服務消費者
    • 消費應用從註冊中心獲取服務列表,從中得知每個服務提供者的資訊,知道去哪裡呼叫服務。例consumer-demo
6.4.2 高可用的Eureka Server

需求:啟動兩臺eureka-server,在eureka服務介面可看到兩個eureka例項

分析:Eureka Server是個web應用,可啟動多個例項(配置不同埠)保證Eureka Server的高可用

所謂的高可用註冊中心,其實就是把Eureka Server自己也作為一個服務,註冊到其他的Eureka Server上,這樣多個Eureka Server可以互相發現對方從而形成叢集

最直接的體現就是Eureka服務配置類中將register-with-eureka和fetch-registry設定註釋掉/設定為true,表名本Eureka可以被註冊到服務上,也可以拉取其他Eureka服務

服務同步:

多個Eureka Server之間可互相註冊為服務,當服務提供者註冊到Eureka Server叢集中的某個節點時,該節點會把服務的資訊同步給叢集中的每個節點,從而實現資料同步。

所以無論客戶端訪問到Eureka Server叢集中的任意一個節點,都可獲取完整的服務列表資訊

作為客戶端(服務提供者和服務消費者),需要將資訊註冊到每個Eureka中

在這裡插入圖片描述

1.修改Eureka-server的配置檔案

server:
  # 如果port有值使用port值;如果沒有使用10086
  port: ${port:10086}
#  port: 10086

spring:
  application:
    # 設定註冊器應用名稱,會在Eureka中作為服務的id標識(serviceId)
    name: eureka-server
eureka:
  client:
    service-url:
      # eureka服務地址,如果是叢集則需要指定其他叢集的eureka地址
#      defaultZone: http://127.0.0.1:10086/eureka
      defaultZone: ${defaultZone:http://127.0.0.1:10086/eureka}
    # 不註冊自己(如果是叢集需要設定為true,使得叢集中其他eureka節點可以發現本eureka)預設為true
#    register-with-eureka: false
    # 不拉取自己的服務(同register-with-eureka,如果是叢集則設定true)預設為true
#    fetch-registry: false

2.修改服務啟動配置,10086的啟動的eureka地址為10087;10087的啟動eureka地址為10086

在這裡插入圖片描述
在這裡插入圖片描述

3.分別啟動兩個eureka註冊器,可以看到10086和10087下的兩個eureka應用
在這裡插入圖片描述
在這裡插入圖片描述

4.兩個註冊服務頁面都可以看到user-service服務和consumer-demo服務

​ Eureka客戶端需要在配置檔案中新增另一個Eureka註冊中心的地址,以防當第一個Eureka失效的時候,可以通過找第二個Eureka完成正常業務(實際上只有寫一個Eureka服務地址即可,Eureka叢集間可以進行服務轉發共享;但是以防寫的那個Euerka服務是壞的,導致整個服務無法註冊上去,繼而導致叢集中無法進行共享【都因為註冊的eureka無效導致註冊不到Eureka註冊中心,何來eureka叢集中共享這個服務一說】)

eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:10086/eureka,http://127.0.0.1:10087/eureka

在這裡插入圖片描述

總結

配置高可用的Eureka Server,可同時開啟幾個Eureka Server服務,只要區分其埠不同即可。

若有三個Eureka,則每個Eureka Server都需要註冊到其他幾個Eureka服務中,假設三個Eureka服務的埠分別為10086/10087/10088,則:

  • 10086要註冊到10087和10088上
  • 10087要註冊到10086和10088上
  • 10088要註冊到10086和10087上

例如在啟動10086埠的Eurea服務的時候配置環境引數-DdefaultZone為http://127.0.0.1:10087/eureka,http://127.0.0.1:10088/eureka

eureka:
client:
 service-url:
   defaultZone: http://127.0.0.1:10086/eureka,http://127.0.0.1:10087/eureka
6.4.3 Eurka客戶端

Eureka服務提供者向Eureka註冊服務之後,可以完成服務續約和服務失效間隔配置

Eureka服務消費者可以完成從Eureka拉取服務列表的時間間隔

6.4.3.1 服務註冊

服務提供者在啟動時,會檢測配置屬性中的eureka.client.register-with-erueka=true 引數是否正確,事實上預設就是true。如果值確實為true,則會向EurekaServer發起一個Rest請求,並攜帶自己的後設資料資訊Eureka Server會把這些資訊儲存到一個雙層Map結構中

  • 第一層Map的key是服務id,一般是配置中的spring.application.name屬性,例如user-service
  • 第二層Map的key是服務例項id,一般是host+serverId+port,例如localhost:user-service:8080,值則是服務的例項物件,即一個服務id可以同時啟動不同服務例項id構成叢集

預設註冊時使用的是主機名或者localhost,如果想用ip進行註冊,可以在user-service中新增配置如下

eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:10086/eureka
  instance:
    # 設定服務例項更傾向於使用ip定址而非host名,
    prefer-ip-address: true
    # 配置服務例項具體的ip地址
    ip-address: 127.0.0.1

修改完後先後重啟user-serviceconsumer-demo;在呼叫服務的時候就已經變成ip地址;

需要注意的是:不是在eureka中的控制檯服務例項狀態顯示,那塊照例顯示主機名稱

user-server配置ip之後consumer-demo服務消費者debug可以看到url的host從主機名稱修改為ip

在這裡插入圖片描述

6.4.3.2 服務續約

在註冊服務完成以後,服務提供者會維持一個心跳(定時向EurekaServer發起Rest請求),告訴EurekaServer:“我 還活著”。這個我們稱為服務的續約(renew)

可以在user-service中新增如下配置項

eureka:
  instance:
    # 續約間隔,預設30s(半分鐘)
    lease-renewal-interval-in-seconds: 5
    # 服務失效間隔,預設90s(一分鐘半)預設服務中斷90s算徹底失效
    lease-expiration-duration-in-seconds: 5
  • lease-renewal-interval-in-seconds:服務續約(renew)的間隔,預設為30秒
  • lease-expiration-duration-in-seconds:服務失效時間,預設值90秒

預設情況下每隔30秒服務提供者會向註冊中心傳送一次心跳,證明自己還活著。

如果超過90秒沒有傳送心跳, Eureka Server就會認為該服務當機,會定時(eureka.server.eviction-interval-timer-in-ms設定的時間)從服務列表 中移除,這兩個值在生產環境不要修改,預設即可。

6.4.3.3 服務消費者獲取服務列表

當服務消費者啟動時,會檢測eureka.client.fetch-registry=true引數的值,如果為true,則會從Eureka Server服務的列表拉取只讀備份,然後快取在本地。並且每隔30秒會重新拉取並更新資料。可以在 consumer-demo專案中通過下面的引數來修改

# Eureka地址
eureka:
client:
 service-url:
   defaultZone: http://127.0.0.1:10086/eureka
 # 獲取服務地址列表間隔時間,預設30s
 registry-fetch-interval-seconds: 10
6.4.4 失效剔除和自我保護
6.4.4.1 服務下線

當服務提供者進行正常關閉操作時,它會觸發一個服務下線的REST請求給Eureka Server,告訴服務註冊中心:“我要下線 了”。服務中心接受到請求之後,將該服務置為下線狀態。

6.4.4.2 失效剔除

有時我們的服務可能由於記憶體溢位或網路故障等原因使得服務不能正常的工作,而服務註冊中心並未收到“服務下線”的請求。相對於服務提供者的“服務續約”操作,服務註冊中心在啟動時會建立一個定時任務,預設每隔一段時間 (預設為60秒)將當前清單中超時(預設為90秒)沒有續約的服務剔除,這個操作被稱為失效剔除。 可以通過eureka.server.eviction-interval-timer-in-ms引數對其進行修改,單位是毫秒

eureka:
  client:
    service-url:
      # eureka服務地址,如果是叢集則需要指定其他叢集的eureka地址
#      defaultZone: http://127.0.0.1:10086/eureka
      defaultZone: ${defaultZone:http://127.0.0.1:10086/eureka}
    # 不註冊自己(如果是叢集需要設定為true,使得叢集中其他eureka節點可以發現本eureka)預設為true
    register-with-eureka: false
    # 不拉取自己的服務(同register-with-eureka,如果是叢集則設定true)預設為true
    fetch-registry: false
  server:
    # 服務失效剔除時間間隔,預設60s
    eviction-interval-timer-in-ms: 60000
6.4.4.3 自我保護

我們關停一個服務,很可能會在Eureka皮膚看到一條警告

在這裡插入圖片描述

這是觸發了Eureka的自我保護機制。

當服務未按時進行心跳續約時,Eureka會統計服務例項最近15分鐘心跳續約的比例是否低於了85%。在生產環境下,因為網路延遲等原因,心跳失敗例項的比例很有可能超標,但是此時就把服務剔除列表並不妥當,因為服務可能沒有當機。Eureka在這段時間內不會剔除任何服務例項,直到網路恢復正常。

生產環境下這很有效,保證了大多數服務依然可用,不過也有可能獲取到失敗的服務例項,因此服務呼叫者必須做好服務的失敗容錯。

可通過配置eureka.server.enable-self-preservation=false關閉自我保護模式

eureka:
  client:
    service-url:
      # eureka服務地址,如果是叢集則需要指定其他叢集的eureka地址
#      defaultZone: http://127.0.0.1:10086/eureka
      defaultZone: ${defaultZone:http://127.0.0.1:10086/eureka}
    # 不註冊自己(如果是叢集需要設定為true,使得叢集中其他eureka節點可以發現本eureka)預設為true
    register-with-eureka: false
    # 不拉取自己的服務(同register-with-eureka,如果是叢集則設定true)預設為true
    fetch-registry: false
  server:
    # 服務失效剔除時間間隔,預設60s
    eviction-interval-timer-in-ms: 60000
    # 關閉自我保護模式,預設為true開啟模式
    enable-self-preservation: false

7. Ribbon負載均衡

7.1 Ribbon概念

學習目標:負載均衡是什麼?Ribbon的作用是什麼?

負載均衡:是一個演算法,可以通過該演算法從地址列表中獲取一個地址進行服務呼叫

在6中的案例啟動了一個user-service,然後通過DiscoveryClient去獲取服務例項資訊,最後獲取ip和埠來訪問這個user-service服務

實際環境中可能存在多個user-service,那麼此時通過DiscoveryClient獲取到的就是一個服務例項列表,服務消費者應該訪問哪一個呢?

這個時候就需要編寫負載均衡演算法,在例項列表中進行選擇最合適的服務例項以供服務消費者呼叫

在Spring Cloud中提供了負載均衡元件:Ribbon,簡單修改程式碼就可使用

什麼是Ribbon?

Ribbon是Netflix釋出的負載均衡器,有助於控制HTTP和TCP客戶端的行為。為Ribbon配置服務提供者地址列表後,Ribbon就可基於某種負載均衡演算法,自動幫助服務消費者去請求

Ribbon預設提供很多負載均衡演算法,例如輪詢、隨機,預設輪詢。也可為其實現自定義的負載均衡演算法。

輪詢:例如一共有三個服務,那麼第一次呼叫第一個服務,第二次呼叫第二個服務,第三次呼叫第三個服務,第四次又回來呼叫第一個服務

隨機:一共有三個服務,每次呼叫都隨機

Ribbon提供了輪詢、隨機兩種負載均衡演算法(預設是輪詢),可以實現從地址列表中使用負載均衡演算法獲取地址進行服務呼叫

7.2 Ribbon負載均衡應用

需求:配置啟動兩個使用者服務,在consumer-demo中使用RestTemplate直接訪問服務名稱http://user-service/user/13實現根據使用者id獲取使用者

分析:使用Ribbon負載均衡元件,在執行RestTemplate傳送服務地址請求的時候,使用負載均衡攔截器進行攔截,根據服務名獲取服務地址列表,使用Ribbon負載均衡演算法從服務地址列表中選擇一個服務地址,訪問該地址獲取服務資料

1.啟動多個user-service例項(開啟兩個埠8080、8081)

server:
  port: ${port:8080}

2.在consumer-demo中修改RestTemplate例項化方法,新增負載均衡註解@LoadBalance

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

    // 構建一個RestTemplate物件加入到spring容器中進行統一管理
    @Bean
    // 開啟負載均衡
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

3.修改ConsumerController

​ 使用服務名user-service拼接url

    @GetMapping("/{id}")
    public User queryById(@PathVariable("id") Long id) {
        String url = "http://user-service/user/" + id;
        User user = restTemplate.getForObject(url, User.class);
        return user;
    }

4.測試

在這裡插入圖片描述
在這裡插入圖片描述

Ribbon預設的負載均衡策略是輪詢。Spring Boot提供修改負載均衡規則的配置入口,例如在consumer-demo的配置檔案applicaiton.yml中新增如下配置即負載均衡演算法變為隨機

user-service:
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule

格式:{服務名稱}.ribbon.NFLoadBalancerClassName

7.3 原始碼分析

為什麼只輸入了service名稱就可以訪問了呢?明明之前6示例中還要獲取ip和埠。

顯然是有元件根據service名稱,獲取到了服務例項的ip和埠。

因為consumer-demo使用的是RestTemplate,spring的負載均衡自動配置類LoadBalancerAutoConfiguration.LoadBalancerInerceptorConfig方法會自動配置負載均衡攔截器(在spring-cloud-commons-**.jar包中的spring-factories中定義的自動配置類),就是LoadBalancerInterceptor,這個類會對RestTemplate的請求進行攔截,然後從Eureka根據服務id(user-service)獲取服務列表,隨後利用負載均衡演算法得到真實的服務地址資訊(ip+埠),替換服務id

// LoadBalancerInterceptor.java
	@Override
	public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
			final ClientHttpRequestExecution execution) throws IOException {
		final URI originalUri = request.getURI();
        // 獲取serviceName服務名即服務id
		String serviceName = originalUri.getHost();
		Assert.state(serviceName != null,
				"Request URI does not contain a valid hostname: " + originalUri);
		return this.loadBalancer.execute(serviceName,
				this.requestFactory.createRequest(request, body, execution));
	}
// RibbonLoadBalancerClient.java
	public <T> T execute(String serviceId, LoadBalancerRequest<T> request, Object hint)
			throws IOException {
        // 根據服務id獲取負載均衡器
		ILoadBalancer loadBalancer = getLoadBalancer(serviceId);
		Server server = getServer(loadBalancer, hint);
        // 使用負載均衡器輪詢服務地址列表
		if (server == null) {
			throw new IllegalStateException("No instances available for " + serviceId);
		}
		RibbonServer ribbonServer = new RibbonServer(serviceId, server,
				isSecure(server, serviceId),
				serverIntrospector(serviceId).getMetadata(server));

		return execute(serviceId, ribbonServer, request);
	}

多次訪問consumer-demo的請求地址,可以發現底層實現了負載均衡(在8080和8081中互相切換)

總結:

在例項化RestTemplate的時候使用@LoadBalanced,對服務消費者傳送的請求進行攔截,攔截服務名之後根據服務名找尋服務地址列表,然後利用負載均衡演算法(預設輪詢)去選擇一個服務地址返回給服務消費者,以供其進行呼叫,即服務地址直接可以使用服務名來訪問服務(攔截請求根據服務名在服務列表中根據負載均衡演算法選擇一個服務地址返回)

8. Hystrix熔斷器

8.1 簡介

Hystrix在微服務系統中是一款提供保護機制的元件,同Eureka一樣由Netflix公司開發。

Hystrix是一個開源的延遲和容錯庫,用於隔離訪問遠端服務、第三方庫,防止出現級聯失敗。

8.2 雪崩問題

微服務中,服務間呼叫關係複雜。一個請求可能需要呼叫多個微服務介面才能實現,形成非常複雜的呼叫鏈路。

在這裡插入圖片描述

如上圖,一個請求需要呼叫A P H I四個服務,這四個服務又可能呼叫其他服務。

如果此時,有一個服務出現異常,例如I

在這裡插入圖片描述

微服務I出現異常,請求阻塞,使用者請求就不會得到響應,這個執行緒也不會釋放。越來越多的使用者請求到來,越來越多的執行緒會被阻塞(服務I資源一直被第一個阻塞的執行緒佔用著,其他訪問服務I的請求無法正常訪問被阻塞)

在這裡插入圖片描述

伺服器支援的執行緒併發數有限,如果請求一直被阻塞,會導致伺服器資源耗盡,從而導致所有其他服務都不可用,形成雪崩效應。

好比一個汽車生產線,生產不同的汽車,需要使用不同的零件。如果某個零件因為種種原因無法使用,那麼就會導致整臺車無法裝配,陷入等待零件的狀態,直到零件到位才能繼續組裝。

此時如果多個車型都需要裝載這個零件,那麼整個工廠都將陷入等待狀態,導致所有生產都陷入癱瘓,這個無法使用的零件負面波及範圍不斷擴大

Hystrix解決雪崩問題的手段主要是服務降級,包括:

  • 執行緒隔離:使用者請求不直接訪問服務,而是使用執行緒池中空閒的執行緒訪問服務,加速失敗判斷時間
  • 服務熔斷

8.3 執行緒隔離

8.3.1 原理

在這裡插入圖片描述

  • Hystrix為每個依賴服務呼叫分配一個小的執行緒池,如果執行緒池已滿,呼叫會被立即拒絕,預設不採用排隊加速失敗判定時間
  • 使用者的請求將不再直接訪問服務,而是通過執行緒池中的空閒執行緒來訪問服務。如果執行緒池已滿,或者請求超時,就會進行服務降級處理。

服務降級:優先保證核心服務,非核心服務不可用或者若可用

使用者的請求故障時,不會被阻塞,也不會無休止等待或看到系統崩潰,至少可以看到一個執行結果(例如返回友好的提示資訊)

服務降級雖然會導致請求失效,但不會導致阻塞,最多會影響這個依賴服務對應的執行緒池中的資源,對其他服務沒有響應。

觸發Hystrix服務降級的情況:

  • 執行緒池已滿
  • 請求超時

總結:當使用者請求的服務所能容納的執行緒滿了,或者請求沒有響應的時候就會進行服務降級操作

8.3.2 實踐

1.匯入依賴

consumer-demo中的pom.xml匯入Hystrix依賴

        <!-- Hystrix熔斷器依賴 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
        </dependency>

2.開啟熔斷

consumer-demo的啟動引導類中新增@@EnableCircuitBreaker註解表明開啟Hystrix熔斷器

@EnableDiscoveryClient
@SpringBootApplication
// 開啟熔斷器
@EnableCircuitBreaker
public class ConsumerApplication {
    public static void main(String[] args) {
        SpringApplication.run(ConsumerApplication.class, args);
    }

    // 構建一個RestTemplate物件加入到spring容器中進行統一管理
    @Bean
    // 開啟負載均衡
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

微服務中經常會引入@EnableDiscoveryClient @SpringBootApplicaiton @EnableCircuitBreaker註解,Spring提供一個組合註解@SpringCloudApplication

// @SpringCloudApplicaiton
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootApplication
@EnableDiscoveryClient
@EnableCircuitBreaker
public @interface SpringCloudApplication {

}

所以可以將三個註解替換成一個@SpringCloudApplication

@SpringCloudApplication 
//整合@SpringBootApplication @EnableDiscoveryClient @EnableCircuitBreaker
public class ConsumerApplication {
    public static void main(String[] args) {
        SpringApplication.run(ConsumerApplication.class, args);
    }

    // 構建一個RestTemplate物件加入到spring容器中進行統一管理
    @Bean
    // 開啟負載均衡
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

3.編寫降級邏輯

當目標服務的呼叫出現故障,我們希望快速失敗,給使用者一個友好提示。因此需要提前編寫好失敗時的降級處理邏輯,要使用HystrixCommand來完成。

因為熔斷的降級邏輯方法必須跟正常邏輯方法保證:相同的引數列表和返回值宣告

失敗邏輯中返回User物件沒有太大意義,一般會返回友好提示。所以把queryById的方法改造為返回String,

反正也是Json資料。這樣失敗邏輯中返回一個錯誤說明,會比較方便。

@RestController
@RequestMapping("/consumer")
@Slf4j
public class ConsumerController {

    @Autowired
    RestTemplate restTemplate;

    @Autowired
    DiscoveryClient discoveryClient;

    @GetMapping("/{id}")
    // 編寫降級邏輯
    @HystrixCommand(fallbackMethod = "queryByIdFallback")
    public String queryById(@PathVariable("id") Long id) {
        String url = "http://user-service/user/" + id;
        return restTemplate.getForObject(url, String.class);
    }

    // 要和正確的邏輯queryById保持相同的引數列表和返回值宣告(由於錯誤宣告返回User物件沒啥太大意義,所以乾脆修改正確邏輯返回型別為String)
    public String queryByIdFallback(Long id) {
        log.error("查詢使用者資訊失敗,id : {}", id);
        return "對不起,網路太擁擠了";
    }
}

@HystrixCommand(fallbackMethod = “queryByIdFallback”):用來宣告一個降級邏輯的方法

4.測試

user-service正常提供服務的時候,訪問同6一樣,但是當停掉user-service服務之後,再次訪問頁面返回降級處理資訊

在這裡插入圖片描述

分析:Hystrix發現請求無響應,就進入服務降級邏輯

5.編寫預設的Fallback

把fallback寫到某個業務方法上,如果這樣的需要進行降級的邏輯很多,豈不是得寫很多?

可以把Fallback配置載入類上,實現預設fallback

@RestController
@RequestMapping("/consumer")
@Slf4j
@DefaultProperties(defaultFallback = "defaultFallback") // 指路統一的降級方法,所有方法返回型別要和這個降級處理方法相同
public class ConsumerController {

    @Autowired
    RestTemplate restTemplate;

    @Autowired
    DiscoveryClient discoveryClient;

    @GetMapping("/{id}")
    // 編寫降級邏輯
//    @HystrixCommand(fallbackMethod = "queryByIdFallback")
    @HystrixCommand
    public String queryById(@PathVariable("id") Long id) {
        String url = "http://user-service/user/" + id;
        // 消費者有一個預設的拉取服務時間,在這個拉取之前請求會因為找不到user-service而進入服務降級
        return restTemplate.getForObject(url, String.class);
    }

    // 預設所有的服務降級走這個方法
    public String defaultFallback() {
        return "預設提示:對不起,網路太擁擠了";
    }
}

在這裡插入圖片描述

@DefaultProperties(defaultFallback = “defaultFallBack”):在類上指明統一的失敗降級方法;該類中所有方法返回型別要與處理失敗的方法的返回型別一致

注意到一個現象:剛開始訪問的時候還是請求不到進入服務降級邏輯,分析後得出因為消費者有一個預設的拉取服務時間,在這個拉取之前請求會因為找不到user-service而進入服務降級

6.超時設定

前面的案例中請求在超時1s就會進行服務降級,返回錯誤提示資訊,這個是因為Hystrix的預設超時時長為1s。可以修改consumer-demo中的配置檔案application.yml

hytrix:
  command:
    default:
      execution:
        isolation:
          thread:
          # Millisecond毫秒
            timeoutInMilliseconds: 2000

這個配置會作用於全域性所有方法。

8.4 服務熔斷

8.4.1 原理

服務熔斷中使用的熔斷器,也稱斷路器,英文單詞為Circuit Breaker

熔斷機制與家中使用電路熔斷原理類似:如果電路發生短路的時候能立刻熔斷電路,避免發生災難。

在分散式系統中應用服務熔斷後,服務呼叫方可以自己判斷哪些服務反應慢或者存在大量超時,可針對這些服務進行主動熔斷,防止整個系統被拖垮

Hystrix的服務熔斷機制,可以實現彈性容錯;當服務請求情況好轉之後,可自動重連。通過斷路的方式,將後續請求直接拒絕,一段時間(預設5s)之後允許部分請求通過,如果呼叫成功則回到斷路器關閉狀態,否則繼續開啟拒絕請求的服務

總結:判斷哪些服務斷了或者請求超時,進行斷路處理;後續過預設5s的時間處於半開狀態允許部分請求通過,如果呼叫成功則乾脆恢復全部服務,否則繼續拒絕請求服務

在這裡插入圖片描述

Hysrtix的熔斷狀態機有三個狀態:

  • Closed:關閉狀態(斷路器關閉),所有請求都正常訪問
  • Open:開啟狀態(斷路器開啟),所有請求都被降級。Hystrix會對請求情況計數,當一定時間內失敗請求百分比達到閾值,則觸發熔斷,斷路器會完全開啟。預設失敗比例的閾值為50%,請求次數最少不低於20次(例如請求20次的時候有10次請求失敗,那麼就會觸發熔斷機制,開啟斷路器使得所有請求被降級)
  • Half Open:半開狀態,不是永久的,斷路器開啟後會進入休眠狀態即Open狀態不進行任何服務處理(預設5s)。隨後斷路器會自動進入半開狀態,此時會釋放部分請求通過,若請求正常則關閉斷路器;否則繼續保持開啟再次進行休眠計時

總結:超過20次請求中有50%請求超時的情況下觸發服務熔斷機制,斷路器開啟;斷路器開啟5s內處於休眠狀態,所有請求拒絕;5s後處於半開狀態,允許一部分請求通過,如果這些請求正常訪問,那麼斷路器關閉,所有請求正常放行;如果有不正常的則斷路器重新返回斷路器開啟狀態(等待5s休眠後再次重複上述過程)

8.4.2 實踐

1.修改consumer-demo的ConsumerController方法,對id=1的時候丟擲異常,表示請求失敗(手動建立熔斷啟用條件)

如果引數是id為1,一定失敗,其它情況都成功(得清空user-service的休眠邏輯)

@RestController
@RequestMapping("/consumer")
@Slf4j
@DefaultProperties(defaultFallback = "defaultFallback") // 指路統一的降級方法,所有方法返回型別要和這個降級處理方法相同
public class ConsumerController {

    @Autowired
    RestTemplate restTemplate;

    @Autowired
    DiscoveryClient discoveryClient;

    @GetMapping("/{id}")
    // 編寫降級邏輯
    @HystrixCommand
    public String queryById(@PathVariable("id") Long id) {
        if (id == 1) {
            throw new RuntimeException("太忙了");
        }
        String url = "http://user-service/user/" + id;
        // 消費者有一個預設的拉取服務時間,在這個拉取之前請求會因為找不到user-service而進入服務降級
        return restTemplate.getForObject(url, String.class);
    }

    // 預設所有的服務降級走這個方法
    public String defaultFallback() {
        return "預設提示:對不起,網路太擁擠了";
    }
}

2.測試

瘋狂訪問id=1的請求(超過20次),就會觸發熔斷。斷路器的開啟,一切請求都會被降級處理,所以當id=13的時候依舊會繼續呼叫服務降級處理邏輯

等到大概5s之後,繼續訪問id=13恢復(這個時候斷路器處於半開狀態,允許id=13的請求通過,因為訪問成功所以斷路器關閉

在這裡插入圖片描述

可以通過修改consumer-demo中的配置檔案applicaiton.yml修改服務熔斷機制的相關配置

  • 最少請求次數:requestVolumeThreshold 預設20次
  • 請求錯誤佔比:errorThresholdPercentage 預設50%
  • 熔斷休眠時間:sleepWindowInMilliseconds 預設5s

具體的配置可參考HystrixCommandProperties

# 熔斷器配置
hystrix:
  command:
    default:
      circuitBreaker:
        errorThresholdPercentage: 50 # 觸發熔斷錯誤比例閾值,預設值50%
        sleepWindowInMilliseconds: 10000 # 熔斷後休眠時長,預設值5秒
        requestVolumeThreshold: 10 # 熔斷觸發最小請求次數,預設值是20
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 2000  # 熔斷超時設定,預設為1秒

需要注意熔斷器配置得加熔斷超時時間:(請求超時)hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=2000

否則熔斷不生效

簡單總結

  1. 系統架構演變
    1. 集中式架構
    2. 垂直拆分服務
    3. 分散式服務架構
    4. SOA面向服務架構
    5. 微服務架構
  2. 服務呼叫方式
    1. RPC
    2. HTTP
      1. HttpClient
      2. OkHttp
      3. JDK原生的URLConnection
      4. Spring提供工具類RestTemplate對上述三種工具進行封裝(例項化restTemplate物件後即可使用)
  3. Spring Cloud
    1. 是微服務架構的一種實現框架
    2. 版本:以單詞命名
  4. 搭建工程
    1. 服務提供工程user-service
    2. 服務消費工程consumer-demo
    3. 服務註冊中心eureka-server
      1. 提供服務註冊和發現服務
      2. 高可用Eureka
  5. Ribbon負載均衡
    1. 根據服務名到Eureka服務註冊中心獲取服務地址列表,再根據Ribbon負載均衡演算法從地址列表中獲取一個服務地址並訪問
    2. 負載均衡演算法
      1. 輪詢(預設)
      2. 隨機
  6. Hystrix熔斷器
    1. 可以在服務呼叫的時候,服務出現異常進行服務降級(及時返回服務失敗結果),由此避免一直長時間等待服務返回結果而出現雪崩效應
    2. 執行緒隔離:加速失敗判斷
    3. 服務熔斷:中斷所有請求等待斷路器休眠之後半開放請求通過

相關文章