1. 程式人生 > 其它 >Mybatis-終極版-01

Mybatis-終極版-01

http://localhost:8001/customer/30[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-zpn6SWDb-1607759535247)(Pictures/1587880250279.png)]

Author:Eric

Version:9.0.0

文章目錄

一、SpringCloud介紹


1.1 微服務架構

微服務架構的提出者:馬丁福勒

https://martinfowler.com/articles/microservices.html

簡而言之,微服務架構樣式[1]是一種將單個應用程式開發為一組小服務的方法,每個小服務都在自己的程序中執行並與輕量級機制(通常是HTTP資源API)進行通訊。這些服務圍繞業務功能構建,並且可以由全自動部署機制獨立部署。這些服務的集中管理幾乎沒有,它可以用不同的程式語言編寫並使用不同的資料儲存技術。

1、 微服務架構只是一個樣式,一個風格。

2、 將一個完成的專案,拆分成多個模組去分別開發。

3、 每一個模組都是單獨的執行在自己的容器中。

4、 每一個模組都是需要相互通訊的。 Http,RPC,MQ。

5、 每一個模組之間是沒有依賴關係的,單獨的部署。

6、 可以使用多種語言去開發不同的模組。

7、 使用MySQL資料庫,Redis,ES去儲存資料,也可以使用多個MySQL資料庫。

總結:將複雜臃腫的單體應用進行細粒度的劃分,每個拆分出來的服務各自打包部署。

1.2 SpringCloud介紹

  • SpringCloud是微服務架構落地的一套技術棧。

  • SpringCloud中的大多數技術都是基於Netflix公司的技術進行二次研發。

  • SpringCloud的中文社群網站:http://springcloud.cn/

  • SpringCloud的中文網:http://springcloud.cc/

  • 八個技術點:

    • Eureka - 服務的註冊與發現
    • Robbin - 服務之間的負載均衡
    • Feign - 服務之間的通訊
    • Hystrix - 服務的執行緒隔離以及斷路器
    • Zuul - 服務閘道器
    • Stream - 實現MQ的使用
    • Config - 動態配置
    • Sleuth - 服務追蹤

二、服務的註冊與發現-Eureka【重點


2.1 引言

Eureka就是幫助我們維護所有服務的資訊,以便服務之間的相互呼叫

Eureka
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-T4B8Cb5e-1607759535255)(Pictures/1587887319057.png)]

SpringBoot 2.2.9

SpringCloud Hoxton.SR4

2.2 Eureka的快速入門

2.2.1 建立一個父工程

建立一個父工程【springcloud-ghy】,並且在父工程中指定SpringCloud的版本,並且將packing修改為pom

除了pom.xml【idea配置,.gitignore】,其他都可以刪掉

<packaging>pom</packaging>

<properties>
    <java.version>1.8</java.version>
</properties>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>Hoxton.SR4</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>
2.2.2 建立EurekaServer【01-eureka-server】

建立eureka的server【01-eureka-server】

建立Maven工程,改成SpringBoot工程

  1. 匯入依賴
    <!-- 1. 匯入eureka的服務 -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
    </dependency>
  2. 使用JBL SpringBootAppGen外掛生成引導類和配置檔案

啟動引導類添加註解 @EnableEurekaServer

@SpringBootApplication
@EnableEurekaServer  //代表當前應用是一個Eureak服務
public class EurekaApplication {
    public static void main(String[] args) {
        SpringApplication.run(EurekaApplication.class, args);
    }
}

編寫yml配置檔案

server:
  port: 10086

spring:
  application:
    name: eureka-server

eureka:
  instance:
    hostname: localhost
  client:
    #當前服務要不要註冊到eureka
    registerWithEureka: false
    #拉取資訊
    fetchRegistry: false
    serviceUrl:
      #eureka的服務地址
      # http://localhost:10086/eureka/
      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/

測試

  1. 啟動引導類
  2. 瀏覽器訪問 http://localhost:10086,能夠看到一個美麗的介面【Eureka】就成功了
2.2.3 建立EurekaClient【02-search-service】

建立Maven工程【02-search-service】,修改為SpringBoot

匯入依賴

<!-- 1. 匯入eureka的客戶端起步依賴-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

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

在啟動引導類上添加註解 @EnableEurekaClient

@SpringBootApplication
@EnableEurekaClient  //標記當前服務是Eureka的客戶端
public class SearchApplication {
    public static void main(String[] args) {
        SpringApplication.run(SearchApplication.class, args);
    }
}

編寫配置檔案

server:
  port: 9001

spring:
  application:
    name: search-service

# 指定eurkea服務地址
eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:10086/eureka/

編寫SearchController

package com.qf.controller;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author ghy
 * @version 1.0
 * @date 2020-09-09
 **/
@RestController
@RequestMapping("/search")
public class SearchController {

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

    @RequestMapping("/test")
    public String test() {
        return "Hello Test " + port;
    }

}

測試

  1. 先啟動 00-eureka-server
  2. 啟動引導類
  3. 瀏覽器重新整理 http://localhost:10086,能夠看到 search-service
  4. 瀏覽器訪問 http://localhost:9001/search/test,能夠看到效果,就代表成功
2.2.4 建立EurekaClient【03-customer-service】

匯入依賴

<!-- 1. 匯入eureka的客戶端起步依賴-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

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

在啟動引導類上添加註解 @EnableEurekaClient

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

    @Bean
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }

}

編寫配置檔案

server:
  port: 8001

spring:
  application:
    name: customer-service

eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:10086/eureka/

編寫SearchController

package com.ghy.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

/**
 * @author ghy
 * @version 1.0
 * @date 2020-09-09
 **/
@RestController
@RequestMapping("/customer")
public class CustomerController {

    @Autowired
    RestTemplate restTemplate;

    @GetMapping("/test")
    public String test(){
        String result = restTemplate.getForObject("http://localhost:9001/search/test", String.class);
        return result;
    }

}

測試

  1. 先啟動 01-eureka-server
  2. 啟動引導類
  3. 瀏覽器重新整理 http://localhost:10086,能夠看到 customer-service
  4. 瀏覽器訪問 http://localhost:9001/customer/test,能夠看到效果,就代表成功
2.2.5 使用到EurekaClient的物件去獲取服務資訊

使用到EurekaClient的物件去獲取服務資訊

在 03-customer-service 工程的 CustomerController中注入

@Autowired
private EurekaClient eurekaClient;

正常RestTemplate呼叫即可

@GetMapping("/test")
public String test(){

    InstanceInfo instanceInfo = eurekaClient.getNextServerFromEureka("SEARCH-SERVICE", false);

    String uri = instanceInfo.getHomePageUrl();
    //http://localhost:9001/
    System.out.println("uri-> " + uri);

    String result = restTemplate.getForObject(uri + "/search/test", String.class);
    return result;
}

2.3 Eureka的安全性

01-eureka-server

實現Eureka認證

匯入依賴

<!-- eureka安全 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

編寫配置類

package com.qf.config;

import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

/**
 * @author ghy
 * @version 1.0
 * @date 2020-09-10
 **/
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //忽略 /eureka/** 路徑
        http.csrf().ignoringAntMatchers("/eureka/**");
        super.configure(http);
    }
}

編寫配置檔案

# 指定使用者名稱和密碼
spring:
  application:
    name: eureka-server
  security:
    user:
      name: root
      password: root

問題:

​ 當啟動 Eureka的客戶端時,出錯了

​ 原因是因為當前Eureka已經開啟安全了,那麼註冊到Eureka的服務也是需要提供認證的【使用者名稱跟密碼】

com.netflix.discovery.shared.transport.TransportException: Cannot execute request on any known server

解決方案:

​ 其他服務想註冊到Eureka上需要新增使用者名稱和密碼。【只要是註冊到Eureka中的服務都需要配置認證資訊】

# 指定eurkea服務地址
eureka:
  client:
    serviceUrl:
      #defaultZone: http://localhost:10086/eureka/
      defaultZone: http://root:[email protected]:10086/eureka/

在我們開發過程中,是很少啟動認證的,因為每次重啟都需要登入,太麻煩。

為了方便寫程式碼,我們去掉認證

01-eureka-server

  1. 註釋安全依賴
  2. 把類刪掉
  3. 把配置檔案中使用者名稱密碼相關資訊註釋掉

02-search-service 跟 03-customer-service

​ 把 defaultZone 改成原來的【不需要使用者名稱跟密碼】

當前springcloud應用過多,不方便檢視

開啟 dashboard

image-20200831172802811

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-uSxa2m5l-1607759535258)(Pictures\image-20200910114738335.png)]

2.4 Eureka的高可用

如果程式正在執行,突然Eureka宕機了。

  • 如果呼叫方訪問過一次被呼叫方了,因為本地快取了被呼叫方的資訊,Eureka的宕機不會影響到功能。

  • 如果呼叫方沒有訪問過被呼叫方,Eureka的宕機就會造成當前功能不可用。

2.4.1 搭建兩個Eureka

修改01-eureka-server的application.yml

server:
  port: ${port:10086}

spring:
  application:
    name: eureka-server


#叢集
eureka:
  instance:
    hostname: localhost
  client:
    #如果是叢集,則需要相互註冊
    rgisterWithEureka: true
    #拉取資訊
    fetchRegistry: true
    serviceUrl:
      #需要把自己註冊到對方的地址
      defaultZone: ${defaultZone:http://localhost:10087/eureka/}

在 idea 的spring-boot應用配置介面上,複製一個 EureakApplication 啟動項,改名為 EureakApplication 10087;並且在面板上啟動引數

參考下圖

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-9kz6WaKC-1607759535262)(C:\Users\ghy\AppData\Roaming\Typora\typora-user-images\image-20200910141107891.png)]

2.4.2 修改EurekaClient的application.yml檔案,讓其註冊到兩個Eureka中。

customer-service

search-service

# 指定eurkea服務地址
eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:10086/eureka/,http://localhost:10087/eureka/
2.4.3 測試

啟動 02-search-service,03-customer-service 服務

瀏覽器上重新整理eureka,就能夠看到兩個Eureka上都有了02-search-service,03-customer-service這兩個服務。

如果沒有同步,則可以選擇重新啟兩個Eureka,或者等會,Eureka節點需要時間同步資料。

2.5 Eureka的細節

EurekaClient啟動是,將自己的資訊註冊到EurekaServer上,EurekaSever就會儲存上EurekaClient的註冊資訊。

當EurekaClient呼叫服務時,本地沒有註冊資訊的快取時,去EurekaServer中去獲取註冊資訊。

EurekaClient會通過心跳的方式去和EurekaServer進行連線。(預設30s EurekaClient會發送一次心跳請求,如果超過了90s還沒有傳送心跳資訊的話,EurekaServer就認為你宕機了,將當前EurekaClient從登錄檔中移除)

eureka:
  instance:
    lease-renewal-interval-in-seconds: 30      #心跳的間隔
    lease-expiration-duration-in-seconds: 90    # 多久沒傳送,就認為你宕機了

EurekaClient會每隔30s去EurekaServer中去更新本地的登錄檔

eureka:
  client:
    registry-fetch-interval-seconds: 30 # 每隔多久去更新一下本地的登錄檔快取資訊

Eureka的自我保護機制,統計15分鐘內,如果一個服務的心跳傳送比例低於85%,EurekaServer就會開啟自我保護機制

  • 不會從EurekaServer中去移除長時間沒有收到心跳的服務。
  • EurekaServer還是可以正常提供服務的。
  • 網路比較穩定時,EurekaServer才會開始將自己的資訊被其他節點同步過去

這是觸發了Eureka的自我保護機制。當服務未按時進行心跳續約時,Eureka會統計服務例項最近15分鐘心跳續約的
比例是否低於了85%。在生產環境下,因為網路延遲等原因,心跳失敗例項的比例很有可能超標,但是此時就把服務
剔除列表並不妥當,因為服務可能沒有宕機。Eureka在這段時間內不會剔除任何服務例項,直到網路恢復正常。生
產環境下這很有效,保證了大多數服務依然可用,不過也有可能獲取到失敗的服務例項,因此服務呼叫者必須做好服
務的失敗容錯。

eureka:
  server:
    enable-self-preservation: true  # 開啟自我保護機制

*** (面試可能有)CAP定理,C - 一致性,A-可用性,P-分割槽容錯性,這三個特性在分散式環境下,只能滿足2個,而且分割槽容錯性在分散式環境下,是必須要滿足的,只能在AC之間進行權衡。

如果選擇CP,保證了一致性,可能會造成你係統在一定時間內是不可用的,如果你同步資料的時間比較長,造成的損失大。

Eureka就是一個AP的效果,高可用的叢集,Eureka叢集是無中心,Eureka即便宕機幾個也不會影響系統的使用,不需要重新的去推舉一個master,也會導致一定時間內資料是不一致。

三、服務間的負載均衡-Robbin【重點


3.1 引言

Robbin是幫助我們實現服務和服務負載均衡,Robbin屬於客戶端負載均衡

客戶端負載均衡:customer客戶模組,將2個Search模組資訊全部拉取到本地的快取,在customer中自己做一個負載均衡的策略,選中某一個服務。

服務端負載均衡:在註冊中心中,直接根據你指定的負載均衡策略,幫你選中一個指定的服務資訊,並返回。

Robbin
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-MfGWI0lD-1607759535265)(Pictures/1587977965492.png)]

3.2 Robbin的快速入門

啟動兩個search模組

​ 複製一個search模組的啟動項,啟動時加上埠號即可

在customer匯入robbin依賴

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>

配置整合RestTemplate和Robbin

@Bean
@LoadBalanced
public RestTemplate restTemplate(){
    return new RestTemplate();
}

在customer中去訪問search

@GetMapping("/test")
public String test(){

    //使用ribbon來實現負載均衡時,需要通過服務名去訪問。
    //預設的負載均衡策略為輪詢
    String result = restTemplate.getForObject("http://SEARCH-SERVICE/search/test", String.class);

    return result;
}

3.3 Robbin配置負載均衡策略

負載均衡策略

  • RandomRule:隨機策略
  • RoundRobbinRule:輪詢策略
  • WeightedResponseTimeRule:預設會採用輪詢的策略,後續會根據服務的響應時間,自動給你分配權重
  • BestAvailableRule:根據被呼叫方併發數最小的去分配

採用註解的形式【全域性】

@Bean
public IRule robbinRule(){
    return new RandomRule();
}

配置檔案去指定負載均衡的策略(推薦)

# 指定具體服務的負載均衡策略
SEARCH-SERVICE:      # 編寫服務名稱
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.WeightedResponseTimeRule  # 具體負載均衡使用的類

註釋IRule對應的配置Bean

/*@Bean
public IRule robbinRule(){
    return new RandomRule();
}*/

四、服務間的呼叫-Feign【重點


4.1 引言

Feign可以幫助我們實現面向介面程式設計,就直接呼叫其他的服務,簡化開發。

4.2 Feign的快速入門

匯入依賴

<!-- 匯入openfeign-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

引導類新增一個註解

@EnableFeignClients

建立一個介面,並且和search-service模組做對映

package com.ghy.feign;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.RequestMapping;

/**
 * @author ghy
 * @version 1.0
 * @date 2020-09-10
 **/
@FeignClient("SEARCH-SERVICE")
public interface SearchFeign {

    /**
     * 將來,這個方法被呼叫時,會通過動態代理生成代理物件去呼叫真正目標物件
     * 在對映時,需要把value屬性加上
     * @return
     */
    @RequestMapping(value = "/search/test")
    String test();

}

CustomerController

@Autowired
SearchFeign searchFeign;

@GetMapping("/test")
public String test(){
    System.out.println("CustomerController test 。。。。");
    String result = searchFeign.test();
    return result;
}

測試

瀏覽器訪問 http://localhost:8001/customer/test ,能夠正確呼叫到 search-service 模組的Controller介面的具體方法

4.3 Feign的傳遞引數方式

注意事項

  • 如果你傳遞的引數,比較複雜時,預設會採用POST的請求方式。
  • 傳遞單個引數時,推薦使用@PathVariable,如果傳遞的單個引數比較多,這裡也可以採用@RequestParam,不要省略value屬性
  • 傳遞物件資訊時,統一採用json的方式,新增@RequestBody
  • Client介面必須採用@RequestMapping

4.3.1 search-service 模組和customer-service模組

引入 lombok依賴

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>

search-service 模組和customer-service模組準備Customer實體類

package com.ghy.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @author ghy
 * @version 1.0
 * @date 2020-09-10
 **/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Customer {

    private Integer id;
    private String name;
    private Integer age;

}

在Search模組下準備三個介面

package com.qf.controller;

import com.qf.entity.Customer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;

/**
 * @author ghy
 * @version 1.0
 * @date 2020-09-09
 **/
@RestController
@RequestMapping("/search")
public class SearchController {


    //========================================
    //引數的傳遞
    @GetMapping("/{id}")
    public Customer findById(@PathVariable Integer id) {
        return new Customer(id, "@PathVariable", 1);
    }


    @GetMapping("/get")
    public Customer get(@RequestParam("id") Integer id,
                             @RequestParam("name") String name,
                             @RequestParam("age") Integer age) {
        return new Customer(id, name, 2);
    }

    @PostMapping("/save")
    public Customer save(@RequestBody Customer customer) {
        return customer;
    }
    //========================================

}

在customer-service 模組的feign介面中增加如下方法

package com.ghy.feign;

import com.ghy.entity.Customer;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;

/**
 * @author ghy
 * @version 1.0
 * @date 2020-09-10
 **/
@FeignClient("SEARCH-SERVICE")
public interface SearchFeign {


    @GetMapping("/search/{id}")
    Customer findById(@PathVariable Integer id);


    @GetMapping("/search/get")
    Customer get(@RequestParam(value = "id") Integer id,
                        @RequestParam(value = "name") String name,
                        @RequestParam(value = "age") Integer age);

    @PostMapping("/search/save")
    Customer save(@RequestBody Customer customer);

}

在customer-service 模組的Controller中增加呼叫feign介面中的方法

package com.ghy.controller;

import com.ghy.entity.Customer;
import com.ghy.feign.SearchFeign;
import com.netflix.appinfo.InstanceInfo;
import com.netflix.discovery.EurekaClient;
import com.netflix.discovery.converters.Auto;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;

import java.nio.file.FileAlreadyExistsException;

/**
 * @author ghy
 * @version 1.0
 * @date 2020-09-09
 **/
@RestController
@RequestMapping("/customer")
public class CustomerController {

    @Autowired
    SearchFeign searchFeign;

    //========================================
    //引數的傳遞
    @GetMapping("/{id}")
    public Customer findById(@PathVariable Integer id) {
        System.out.println("CustomerController findById ....");
        return searchFeign.findById(id);
    }


    @GetMapping("/get")
    public Customer get(@RequestParam("id") Integer id,
                        @RequestParam("name") String name,
                        @RequestParam("age") Integer age) {
        System.out.println("CustomerController get ....");
        return searchFeign.get(id,name,age);
    }

    @PostMapping("/save")
    public Customer save(@RequestBody Customer customer) {
        System.out.println("CustomerController save ....");
        return searchFeign.save(customer);
    }
    //========================================


}

測試

image-20200910162132628
image-20200910162219185
image-20200910162306970

4.4 Feign的Fallback

4.4.0 引言

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-wHSaRfkR-1607759535267)(Pictures\image-20200911111247662.png)]

Fallback可以幫助我們在使用Feign去呼叫另外一個服務時,如果出現了問題,走服務降級,返回一個錯誤資料,避免功能因為一個服務出現問題,全部失效。

4.4.1 FallBack方式

建立一個POJO類,實現SearchFeign介面,重寫其抽象方法,並注入到Spring容器中

package com.ghy.feign.fallback;

import com.ghy.entity.Customer;
import com.ghy.feign.SearchFeign;
import org.springframework.stereotype.Component;

/**
 * @author ghy
 * @version 1.0
 * @date 2020-09-11
 **/
@Component
public class SearchFeignFallback implements SearchFeign {
    @Override
    public String test() {
        return "feign的服務降級成功了";
    }

    @Override
    public Customer findById(Integer id) {
        return null;
    }

    @Override
    public Customer get(Integer id, String name, Integer age) {
        return null;
    }

    @Override
    public Customer save(Customer customer) {
        return null;
    }
}

修改Feign介面中的註解,新增一個屬性。

@FeignClient(value = "SEARCH-SERVICE", fallback = SearchFeignFallback.class)
public interface SearchFeign {}

新增一個配置檔案。【開啟feign跟hystrix的整合服務降級】

# feign和hystrix元件整合
feign:
  hystrix:
    enabled: true

讓搜尋模組【search-service】的test方法出現異常【模擬服務出問題了】

@RequestMapping("/test")
public String test() {
    int i=1/0;
    return "Hello Test " + port;
}

測試

訪問客戶模組的test介面 http://localhost:8001/customer/test

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-HggJmAz0-1607759535269)(Pictures\image-20200911105853736.png)]

4.4.2 FallBackFactory方式

呼叫方無法知道具體的錯誤資訊是什麼,通過FallBackFactory的方式去實現這個功能

FallBackFactory基於Fallback

建立一個POJO類,實現FallBackFactory<Feign介面實現類>,注入到Spring容器中,抽象方法中需要把Feign介面服務降級類物件返回

package com.ghy.feign.fallback.factory;

import com.ghy.feign.fallback.SearchFeignFallback;
import com.netflix.discovery.converters.Auto;
import feign.hystrix.FallbackFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * @author ghy
 * @version 1.0
 * @date 2020-09-11
 **/
@Component
public class SearchFeignFallbackFactory implements FallbackFactory<SearchFeignFallback> {

    @Autowired
    SearchFeignFallback searchFeignFallback;

    /**
     * 獲取目標介面的問題資訊
     * @param throwable
     * @return
     */
    @Override
    public SearchFeignFallback create(Throwable throwable) {
        //輸出異常堆疊資訊
        throwable.printStackTrace();
        return searchFeignFallback;
    }
}

修改SearchFeign介面中的屬性

@FeignClient(value = "SEARCH-SERVICE", fallbackFactory= SearchFeignFallbackFactory.class)

測試

訪問客戶模組的test介面 http://localhost:8001/customer/test

​ 頁面依舊是降級後結果,customer-service應用的控制檯輸出了異常資訊

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-JdJ5MziK-1607759535271)(Pictures\image-20200911110943297.png)]

五、服務的隔離及斷路器-Hystrix【重點


5.1 引言

Hystrix
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-KbFAGLZL-1607759535274)(Pictures/1588044427589.png)]

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-JwWWSQA0-1607759535275)(Pictures\image-20200911112725280.png)]

5.2 降級機制實現

匯入依賴

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>

引導類新增hystrix的一個註解

@EnableCircuitBreaker

讓findById接口出問題

編寫他的降級方法【除方法名之外,其他都跟目標方法一致】

在目標方法中加上註解,實現服務降級【@HystrixCommand(fallbackMethod = “findByIdFallback”)】

@GetMapping("/{id}")
//當前降級服務的方法必須跟目標方法保持一致,除方法名之外
@HystrixCommand(fallbackMethod = "findByIdFallback")
public Customer findById(@PathVariable Integer id) {
    System.out.println("CustomerController findById ....");
    int i=1/0;   //出問題了,希望走降級邏輯
    return searchFeign.findById(id);
}

//服務降級的方法
public Customer findByIdFallback(Integer id) {
    System.out.println("findById 出錯了,進入了降級邏輯....");
    return new Customer(id, "hystrix服務降級成功",666);
}

5、 測試一下

測試編寫了降級邏輯的介面【findById】

http://localhost:8001/customer/30

效果
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-tFQZNiEr-1607759535276)(Pictures\image-20200911113704966.png)]

5.3 執行緒隔離

如果使用Tomcat的執行緒池去接收使用者的請求,使用當前執行緒去執行其他服務的功能,如果某一個服務出現了故障,導致tomcat的執行緒大量的堆積,導致Tomcat無法處理其他業務功能。

  • Hystrix的執行緒池(預設),接收使用者請求採用tomcat的執行緒池,執行業務程式碼,呼叫其他服務時,採用Hystrix的執行緒池。
  • 訊號量,使用的還是Tomcat的執行緒池,幫助我們去管理Tomcat的執行緒池。

Hystrix的執行緒池的配置

配置資訊namevalue
執行緒隔離策略execution.isolation.strategyTHREAD
指定超時時間execution.isolation.thread.timeoutInMilliseconds1000
是否開啟超時時間配置execution.timeout.enabledtrue
超時之後是否中斷執行緒execution.isolation.thread.interruptOnTimeouttrue
取消任務後是否中斷執行緒execution.isolation.thread.interruptOnCancelfalse

程式碼實現

@HystrixProperty 具體的屬性參考 HystrixCommandProperties類中的屬性

//---------------------------
@GetMapping("/get")
@HystrixCommand(fallbackMethod = "getFallback",commandProperties = {
        //name : Hystrix執行緒池配置的屬性名
        //value : 屬性值
        @HystrixProperty(name="execution.isolation.strategy", value = "THREAD"),
        @HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds", value = "2000"),
        @HystrixProperty(name="execution.timeout.enabled", value = "true"),
})
public Customer get(@RequestParam("id") Integer id,
                    @RequestParam("name") String name,
                    @RequestParam("age") Integer age) {

    try {
        System.out.println("CustomerController get ....");
        System.out.println("執行緒: " + Thread.currentThread().getName());
        Thread.sleep(2500);//目的是為了讓當前方法超時,進入降級邏輯
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return searchFeign.get(id,name,age);
}

public Customer getFallback(Integer id,String name,Integer age) {
    System.out.println("getFallback被降級了。是執行緒隔離Hystrix執行緒池的方式進行服務降級....");
    return new Customer(id, "Hystrix執行緒池服務降級成功",666);
}

//---------------------------

測試 http://localhost:8001/customer/get?id=1&name=111&age=11111

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-ONfwXBpJ-1607759535278)(Pictures\image-20200911120251608.png)]

訊號量的配置資訊

配置資訊namevalue
執行緒隔離策略execution.isolation.strategySEMAPHORE
指定訊號量的最大併發請求數execution.isolation.semaphore.maxConcurrentRequests10

程式碼實現

//--------------- 訊號量來管理tomcat執行緒 ----------------
@PostMapping("/save")
@HystrixCommand(fallbackMethod = "saveFallback",commandProperties = {
        //name : Hystrix執行緒池配置的屬性名
        //value : 屬性值
        @HystrixProperty(name="execution.isolation.strategy", value = "SEMAPHORE"),
        @HystrixProperty(name="execution.isolation.semaphore.maxConcurrentRequests", value = "10")
})
public Customer save(@RequestBody Customer customer) {
    System.out.println("CustomerController save ....");
    System.out.println("執行緒 ---> " + Thread.currentThread().getName());
    int i=1/0;
    return searchFeign.save(customer);
}
//========================================

public Customer saveFallback(Customer customer){
    System.out.println("這是訊號量配置,出現異常,進入降級邏輯...............");
    return new Customer(customer.getId(),"訊號量配置,出現異常,進入降級邏輯", 5555);
}

測試

​ 因為是post請求,所以使用 postman測試

​ 重啟 customer-service

​ 測試介面: http://localhost:8001/customer/save

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-xbgABPqY-1607759535279)(Pictures\image-20200911162158974.png)]

控制檯能夠看到的確使用的是tomcat的執行緒

image-20200911162423707

5.4 斷路器

也叫熔斷器

5.4.1 斷路器介紹

馬丁福勒斷路器論文:https://martinfowler.com/bliki/CircuitBreaker.html

在呼叫指定服務時,如果說這個服務的失敗率達到你輸入的一個閾值,將斷路器從closed狀態,轉變為open狀態,指定服務時無法被訪問的,如果你訪問就直接走fallback方法,在一定的時間內,open狀態會再次轉變為half open狀態,允許一個請求傳送到我的指定服務,如果成功,轉變為closed,如果失敗,服務再次轉變為open狀態,會再次迴圈到half open,直到斷路器回到一個closed狀態。

斷路器
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-GdgP93wo-1607759535280)(Pictures\image-20200911163313991.png)]
5.4.2 配置斷路器的監控介面

匯入依賴

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>

在啟動引導類中添加註解

@EnableHystrixDashboard

配置一個Servlet路徑,指定上Hystrix的Servlet

@WebServlet("/hystrix.stream")
public class HystrixServlet extends HystrixMetricsStreamServlet {
}

//------------------------------------------------------------
// 在啟動類上,新增掃描Servlet的註解
@ServletComponentScan("com.ghy.servlet")

測試直接訪問 http://host:port/hystrix

http://localhost:8001/hystrix

​ 進入之後,輸入被監控的地址 http://localhost:8001/hystrix.stream

監控介面
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-NWGuvLh5-1607759535281)(Pictures\image-20200911172103719.png)]

在當前位置輸入對映好的servlet路徑

檢測效果
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-y5PwBpq5-1607759535282)(Pictures\image-20200911172012892.png)]
5.4.3 配置斷路器的屬性

斷路器的屬性(預設10s秒中之內請求數)

配置資訊namevalue
斷路器的開關circuitBreaker.enabledtrue
失敗閾值的總請求數circuitBreaker.requestVolumeThreshold20
請求總數失敗率達到%多少時circuitBreaker.errorThresholdPercentage50
斷路器open狀態後,多少秒是拒絕請求的circuitBreaker.sleepWindowInMilliseconds5000
強制讓服務拒絕請求 [A]circuitBreaker.forceOpenfalse
強制讓服務接收請求 [B]circuitBreaker.forceClosedfalse

[A] 跟 [B] 只能選一個

具體配置方式,引數參考 HystrixCommandProperties類進行配置

@GetMapping("/hello/{id}")
@HystrixCommand(fallbackMethod = "helloFallBack",commandProperties = {
        @HystrixProperty(name = "circuitBreaker.enabled",value = "true"),  //是否開啟斷路器,預設為true:開啟
        @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold",value = "10"),  //總共在10秒訪問多少次,預設為20次
        @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage",value = "50"),  //失敗的比例達到多少【百分比】開啟斷路器
        @HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds",value = "5000") //過了多少時間後,斷路器變成半開狀態
})
public String hello(@PathVariable Integer id){
    System.out.println("customer hello");
    if(id == 1) {
        int i=1/0;   //訪問至少20次,有一半以上是失敗的,則開啟斷路器
    }
    return "hello,正常了 " + id;
}

public String helloFallBack(Integer id){
    System.out.println("hello方法出異常了,進入降級邏輯");
    return id.toString() + "-> id=1,出異常,其他正常。。。。。";
}@GetMapping("/customer/{id}")
@HystrixCommand(fallbackMethod = "findByIdFallBack",commandProperties = {
    @HystrixProperty(name = "circuitBreaker.enabled",value = "true"),
    @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold",value = "10"),
    @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage",value = "70"),
    @HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds",value = "5000")
})

在目標方法中,通過id的值去判斷當前請求是否成功

當id==1時,請求失敗,當失敗比例達到閾值時,開啟斷路器,這個時候,就算訪問成功的請求,也會失敗。等到開啟斷路器的時間到了這後,會允許一個請求訪問,如果成功,則關閉斷路器,如果失敗,依舊開啟斷路器。之後迴圈以上操作

if(id == 1) {
    int i=1/0;
}

測試

  1. 先訪問一個正常的介面 http://localhost:8001/customer/hello/2, 返回正常

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-dMQpZp7u-1607759535284)(Pictures\image-20200911172440795.png)]

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-ZBhcxTa0-1607759535285)(Pictures\image-20200911172622552.png)]

  1. 再在10秒鐘之內,訪問失敗的介面10次【http://localhost:8001/customer/hello/1】,看到監控介面中,斷路器開關被打開了。

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-369jI13Q-1607759535287)(Pictures\image-20200911172749166.png)]

  1. 這個時候,即便的訪問的是正常的介面,也會失敗。即 http://localhost:8001/customer/hello/2,也出現了服務降級

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-M4ephX4s-1607759535288)(Pictures\image-20200911172833743.png)]

5.5 請求快取

5.5.1 請求快取介紹
  • 請求快取的宣告週期是一次請求

  • 請求快取是快取當前執行緒中的一個方法,將方法引數作為key,方法的返回結果作為value

  • 在一次請求中,目標方法被呼叫過一次,以後就都會被快取。

請求快取
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-U06lwvqi-1607759535291)(Pictures/1588127678866.png)]
5.5.2 請求快取的實現

建立一個Service,在Service中呼叫Search服務。

@Service
public class CustomerService {

    @Autowired
    private SearchClient searchClient;


    @CacheResult
    @HystrixCommand(commandKey = "findById")
    public Customer findById(@CacheKey Integer id) throws InterruptedException {
        return searchClient.findById(id);
    }

    @CacheRemove(commandKey = "findById")
    @HystrixCommand
    public void clearFindById(@CacheKey Integer id){
        System.out.println("findById被清空");
    }

}

使用請求快取的註解

@CacheResult:幫助我們快取當前方法的返回結果(必須@HystrixCommand配合使用)
@CacheRemove:幫助我們清除某一個快取資訊(基於commandKey)
@CacheKey:指定哪個方法引數作為快取的標識

修改Search模組的返回結果

#使用隨機值來證明會有快取
return new Customer(1,"張三",(int)(Math.random() * 100000));

編寫Filter,去構建HystrixRequestContext

@WebFilter("/*")
public class HystrixRequestContextInitFilter implements Filter {

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HystrixRequestContext.initializeContext();
        filterChain.doFilter(servletRequest,servletResponse);
    }
}

修改Controller

public Customer findById(@PathVariable Integer id) throws InterruptedException {
    System.out.println(customerService.findById(id));
    System.out.println(customerService.findById(id));
    customerService.clearFindById(id);
    System.out.println(customerService.findById(id));
    System.out.println(customerService.findById(id));
    return searchClient.findById(id);
}

測試結果

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-fd85DdUY-1607759535292)(Pictures/1588133189886.png)]

六、服務的閘道器-Zuul【重點


6.1 引言

  • 客戶端維護大量的ip和port資訊,直接訪問指定服務

  • 認證和授權操作,需要在每一個模組中都新增認證和授權的操作

  • 專案的迭代,服務要拆分,服務要合併,需要客戶端進行大量的變化

  • 統一的把安全性校驗都放在Zuul中

zuul
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-qUEnaHEN-1607759535293)(Pictures/1588145514669.png)]

6.2 Zuul的快速入門

建立Maven專案,修改為SpringBoot

匯入依賴

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>

新增一個註解

@EnableEurekaClient
@EnableZuulProxy

編寫配置檔案

# 指定Eureka服務地址
eureka:
  client:
    service-url:
      defaultZone: http://root:[email protected]:8761/eureka,http://root:[email protected]:8762/eureka

#指定服務的名稱
spring:
  application:
    name: ZUUL

server:
  port: 80

直接測試

測試效果
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-HTzbYo2U-1607759535295)(Pictures/1588148029538.png)]

6.3 Zuul常用配置資訊

6.3.1 Zuul的監控介面

匯入依賴

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

編寫配置檔案

# 檢視zuul的監控介面(開發時,配置為*,上線,不要配置)
management:
  endpoints:
    web:
      exposure:
        include: "*"

直接訪問

/actuator/routes

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-fYNGOTlB-1607759535297)(Pictures/1588157803975.png)]

6.3.2 忽略服務配置
# zuul的配置
zuul:
  # 基於服務名忽略服務,無法檢視 ,如果要忽略全部的服務  "*",預設配置的全部路徑都會被忽略掉(自定義服務的配置,無法忽略的)
  ignored-services: eureka
  # 監控介面依然可以檢視,在訪問的時候,404
  ignored-patterns: /**/search/**
6.3.3 自定義服務配置
# zuul的配置
zuul:
  # 指定自定義服務(方式一 , key(服務名):value(路徑))
#  routes:
#    search: /ss/**
#    customer: /cc/**
  # 指定自定義服務(方式二)
  routes:
    kehu:   # 自定義名稱
      path: /ccc/**     # 對映的路徑
      serviceId: customer   # 服務名稱
6.3.4 灰度釋出

新增一個配置類

@Bean
public PatternServiceRouteMapper serviceRouteMapper() {
    return new PatternServiceRouteMapper(
        "(?<name>^.+)-(?<version>v.+$)",
        "${version}/${name}");
}

準備一個服務,提供2個版本

version: v1

#指定服務的名稱
spring:
  application:
    name: CUSTOMER-${version}

修改Zuul的配置

# zuul的配置
zuul:
  # 基於服務名忽略服務,無法檢視  , 如果需要用到-v的方式,一定要忽略掉
  # ignored-services: "*"

測試

測試效果
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-G0vspNUN-1607759535298)(Pictures/1588165064217.png)]

6.4 Zuul的過濾器執行流程

客戶端請求傳送到Zuul服務上,首先通過PreFilter鏈,如果正常放行,會吧請求再次轉發給RoutingFilter,請求轉發到一個指定的服務,在指定的服務響應一個結果之後,再次走一個PostFilter的過濾器鏈,最終再將響應資訊交給客戶端。

過濾器
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-CkC33rjN-1607759535300)(Pictures/1588172828199.png)]

6.5 Zuul過濾器入門

建立POJO類,繼承ZuulFilter抽象類

@Component
public class TestZuulFilter extends ZuulFilter {}

指定當前過濾器的型別

@Override
public String filterType() {
    return FilterConstants.PRE_TYPE;
}

指定過濾器的執行順序

@Override
public int filterOrder() {
    return FilterConstants.PRE_DECORATION_FILTER_ORDER - 1;
}

配置是否啟用

@Override
public boolean shouldFilter() {
    // 開啟當前過濾器
    return true;
}

指定過濾器中的具體業務程式碼

@Override
public Object run() throws ZuulException {
    System.out.println("prefix過濾器執行~~~");
    return null;
}

測試

效果
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-ScMK8UuG-1607759535301)(Pictures/1588175034889.png)]

6.6 PreFilter實現token校驗

準備訪問路徑,請求引數傳遞token

http://localhost/v2/customer/version?token=123

建立AuthenticationFilter

@Component
public class AuthenticationFilter extends ZuulFilter {
    @Override
    public String filterType() {
        return FilterConstants.PRE_TYPE;
    }

    @Override
    public int filterOrder() {
        return PRE_DECORATION_FILTER_ORDER - 2;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() throws ZuulException {
        //..
    }
    
}

在run方法中編寫具體的業務邏輯程式碼

@Override
public Object run() throws ZuulException {
    //1. 獲取Request物件
    RequestContext requestContext = RequestContext.getCurrentContext();
    HttpServletRequest request = requestContext.getRequest();

    //2. 獲取token引數
    String token = request.getParameter("token");

    //3. 對比token
    if(token == null || !"123".equalsIgnoreCase(token)) {
        //4. token校驗失敗,直接響應資料
        requestContext.setSendZuulResponse(false);
        requestContext.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
    }
    return null;
}

測試

效果
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-9DsUZV63-1607759535303)(Pictures/1588177367016.png)]

6.7 Zuul的降級

建立POJO類,實現介面FallbackProvider

@Component
public class ZuulFallBack implements FallbackProvider {}

重寫兩個方法

@Override
public String getRoute() {
    return "*";   // 代表指定全部出現問題的服務,都走這個降級方法
}

@Override
public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
    System.out.println("降級的服務:" + route);
    cause.printStackTrace();

    return new ClientHttpResponse() {
        @Override
        public HttpStatus getStatusCode() throws IOException {
            // 指定具體的HttpStatus
            return HttpStatus.INTERNAL_SERVER_ERROR;
        }

        @Override
        public int getRawStatusCode() throws IOException {
            // 返回的狀態碼
            return HttpStatus.INTERNAL_SERVER_ERROR.value();
        }

        @Override
        public String getStatusText() throws IOException {
            // 指定錯誤資訊
            return HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase();
        }

        @Override
        public void close() {

        }

        @Override
        public InputStream getBody() throws IOException {
            // 給使用者響應的資訊
            String msg = "當前服務:" + route + "出現問題!!!";
            return new ByteArrayInputStream(msg.getBytes());
        }

        @Override
        public HttpHeaders getHeaders() {
            // 指定響應頭資訊
            HttpHeaders headers = new HttpHeaders();
            headers.setContentType(MediaType.APPLICATION_JSON);
            return headers;
        }
    };
}

測試

效果
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-sYJvYFT5-1607759535305)(Pictures/1588180538336.png)]

6.8 Zuul動態路由

建立一個過濾器

//  執行順序最好放在Pre過濾器的最後面

在run方法中編寫業務邏輯

@Override
public Object run() throws ZuulException {
    //1. 獲取Request物件
    RequestContext context = RequestContext.getCurrentContext();
    HttpServletRequest request = context.getRequest();

    //2. 獲取引數,redisKey
    String redisKey = request.getParameter("redisKey");

    //3. 直接判斷
    if(redisKey != null && redisKey.equalsIgnoreCase("customer")){
        // http://localhost:8080/customer
        context.put(FilterConstants.SERVICE_ID_KEY,"customer-v1");
        context.put(FilterConstants.REQUEST_URI_KEY,"/customer");
    }else if(redisKey != null && redisKey.equalsIgnoreCase("search")){
        // http://localhost:8081/search/1
        context.put(FilterConstants.SERVICE_ID_KEY,"search");
        context.put(FilterConstants.REQUEST_URI_KEY,"/search/1");
    }

    return null;
}

測試

效果
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-ZQpoY7I5-1607759535307)(Pictures/1588185307172.png)]

七、多語言支援-Sidecar


7.1 引言

在SpringCloud的專案中,需要接入一些非Java的程式,第三方介面,無法接入eureka,hystrix,feign等等元件。啟動一個代理的微服務,代理微服務去和非Java的程式或第三方介面交流,通過代理的微服務去計入SpringCloud的相關元件。

sidecar
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-D0ySa1pK-1607759535308)(Pictures/1588234895867.png)]

7.2 Sidecar實現

建立一個第三方的服務

建立一個SpringBoot工程,並且新增一個Controller

建立maven工程【sidecar】,修改為SpringBoot

匯入依賴

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-netflix-sidecar</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

增加引導類跟配置檔案

引導類上添加註解

@EnableSidecar

編寫配置檔案

server:
  port: 81

# 指定Eureka服務地址
eureka:
  client:
    service-url:
      defaultZone: http://root:[email protected]:8761/eureka,http://root:[email protected]:8762/eureka
# 指定服務名稱
spring:
  application:
    name: other-service


# 指定代理的第三方服務
sidecar:
  port: 7001

6、 通過customer通過feign呼叫第三方服務

效果
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-MLiQOLhC-1607759535322)(Pictures/1588237130846.png)]

八、服務間訊息傳遞-Stream


8.1 引言

Stream就是在訊息佇列的基礎上,對其進行封裝,讓咱們更方便的去操作MQ訊息佇列。

效果
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-oeJBHLoo-1607759535324)(Pictures/1588245420362.png)]

8.2 Stream快速入門

啟動RabbitMQ

消費者【search】-匯入依賴

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>

消費者-配置檔案

spring:
  # 連線RabbitMQ
  rabbitmq:
    host: 192.168.199.109
    port: 5672
    username: test
    password: test
    virtual-host: /test

消費者-監聽的佇列

public interface StreamClient {
    @Input("myMessage")
    SubscribableChannel input();
}
//-------------------------------------------------
@Component
@EnableBinding(StreamClient.class)
public class StreamReceiver {
    @StreamListener("myMessage")
    public void msg(Object msg){
        System.out.println("接收到訊息: " + msg);
    }
}

生產者-匯入依賴

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>

生產者-配置檔案

spring:
  # 連線RabbitMQ
  rabbitmq:
    host: 192.168.199.109
    port: 5672
    username: test
    password: test
    virtual-host: /test

生產者-釋出訊息

public interface StreamClient {
    @Output("myMessage")
    MessageChannel output();
}
//----------------------------------------------  在啟動類中添加註解 @EnableBinding(StreamClient.class)
@Autowired
private StreamClient streamClient;

@GetMapping("/send")
public String send(){
    streamClient.output().send(MessageBuilder.withPayload("Hello Stream!!").build());
    return "訊息傳送成功!!";
}

8.3 Stream重複消費問題

消費者工程中需要新增一個配置,指定消費者組

spring:
  cloud:
    stream:
      bindings:
        myMessage:				# 佇列名稱
          group: customer      # 消費者組

8.4 Stream的消費者手動ack

編寫配置

spring:
  cloud:
    stream:
      # 實現手動ACK
      rabbit:
        bindings:
          myMessage:
            consumer:
              acknowledgeMode: MANUAL

修改消費端方法

@StreamListener("myMessage")
public void msg(Object msg,
                @Header(name = AmqpHeaders.CHANNEL) Channel channel,
                @Header(name = AmqpHeaders.DELIVERY_TAG) Long deliveryTag) throws IOException {
    System.out.println("接收到訊息: " + msg);
    channel.basicAck(deliveryTag,false);
}

九、服務的動態配置-Config【重點


9.1 引言

  • 配置檔案分散在不同的專案中,不方便維護。

  • 配置檔案的安全問題。

  • 修改完配置檔案,無法立即生效。

config
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-dkZfOAfL-1607759535326)(Pictures/1588257620824.png)]

準備工作

1、gitee 建立一個空倉庫【名稱:cloud-config】
2、克隆倉庫到本地
3、複製 customer-demo工程中的 application.yml 配置檔案到倉庫中,改名為 customer-dev.yml
4、提交到本地,推送到遠端

9.2 搭建Config-Server

建立Maven工程,修改為SpringBoot

匯入依賴

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-config-server</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

引導類上添加註解

@EnableConfigServer

編寫配置檔案(Git的操作)

server:
  port: 10000

#config服務也需要註冊到eureka中
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka

spring:
  application:
    name: config-demo  #服務名
  cloud:
    config:
      server:
        git:
          basedir: D:\basedir   #本地倉庫路徑
          uri: https://gitee.com/cqwiu/cloud-config.git   #遠端倉庫url
          username: 你的使用者名稱		#gitee使用者名稱
          password: 你的密碼    	#gitee密碼

測試(http://localhost:port/{label}/{application}-{profile}.yml)

http://localhost:10000/master/customer-dev.yml

效果
image-20200903211910531

9.3 搭建Config-Client

就是你想要把配置交給gitee管理的具體服務,在這裡,我們以customer-demo為例

匯入依賴

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-config-client</artifactId>
</dependency>

編寫配置檔案

# 指定Eureka服務地址
eureka:
  client:
    service-url:
      defaultZone: http://root:[email protected]:8761/eureka,http://root:[email protected]:8762/eureka

#指定服務的名稱
spring:
  application:
    name: CUSTOMER-${version}
  cloud:
    config:
      discovery:
        enabled: true
        service-id: CONFIG
      profile: dev

version: v1


# CONFIG -> CUSTOMER-v1-dev.yml

修改配置名稱

修改為bootstrap.yml

編輯遠端倉庫的配置檔案 customer-dev.yml

內容如下:【其實就是把工程中存在的配置從gitee上的配置檔案中去掉】

eureka:
  instance:
    lease-renewal-interval-in-seconds: 30      #心跳的間隔
    lease-expiration-duration-in-seconds: 90    # 多久沒傳送,就認為你宕機了

server:
  port: 8001
  
spring:
  # 連線RabbitMQ
  rabbitmq:
    host: 192.168.20.128
    port: 5672
    username: demo
    password: demo
    virtual-host: /demo

SEARCH-DEMO:
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.WeightedResponseTimeRule


feign:
  hystrix:
    enabled: true

測試釋出訊息到RabbitMQ

效果
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-olStmM87-1607759535327)(Pictures/1588263245773.png)]

9.4 實現動態配置

9.4.1 實現原理
實現原理
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-MZtl1D2q-1607759535329)(Pictures/1588268131944.png)]
9.4.2 config-demo服務連線RabbitMQ

config-demo跟customer都匯入依賴

<!-- 手動重新整理向 MQ傳送訊息 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

<!-- 訊息匯流排依賴 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>

編寫配置檔案連線RabbitMQ資訊

spring:
  rabbitmq:
    host: 192.168.20.128
    port: 5672
    username: demo
    password: demo
    virtual-host: /demo
9.4.3 實現手動重新整理

編寫配置檔案

management:
  endpoints:
    web:
      exposure:
        include: "*"  #開啟bus功能

為customer新增一個controller

@RestController
@RefreshScope   //允許重新整理
public class CustomerController {

    @Value("${env}")
    private String env;

    @GetMapping("/env")
    public String env(){
        return env;
    }
}

測試

1. CONFIG在Gitee修改之後,自動拉取最新的配置資訊。
2. 其他模組需要更新的話,手動傳送一個post請求 http://ip:port/actuator/bus-refresh,不重啟專案,即可獲取最新的配置資訊
http://localhost:10000/actuator/bus-refresh
9.5.4 內網穿透

內網穿透的官網https://natapp.cn/

註冊登入

實名認證

購買一個免費的隧道。

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-i2QvDvse-1607759535330)(Pictures\image-20200903221649035.png)]


[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-ttJwPcnR-1607759535332)(Pictures\image-20200903221747079.png)]


[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-rzXrMZ1O-1607759535334)(Pictures\image-20200903221940195.png)]

配置隧道
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-hSH2kswU-1607759535335)(Pictures/1588317871876.png)]

下載客戶端,並複製config.ini檔案,在檔案中指定authtoken

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-BjCzgOyJ-1607759535336)(Pictures\image-20200903222132643.png)]


[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-CBWZFeAT-1607759535338)(Pictures\image-20200903222317525.png)]


[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-wihiktHB-1607759535339)(Pictures\image-20200903222413334.png)]


netapp軟體
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-OORgI9yL-1607759535341)(Pictures/1588317944258.png)]

啟動exe檔案,並測試使用域名訪問config介面

效果
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-JRer0Fpr-1607759535342)(Pictures/1588317929937.png)]
9.5.5 實現自動重新整理

配置Gitee中的WebHooks

配置Gitee中的WebHooks
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-Hdmj5maz-1607759535343)(Pictures\image-20200903222939794.png)]
image-20200903223122971
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-DfZqMzOj-1607759535344)(Pictures\image-20200903223203214.png)]

給Config新增一個過濾器

直接去程式碼中找到Filter
import org.springframework.stereotype.Component;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;

/**
 * create by ghy
 */
@WebFilter("/*")
public class UrlFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpServletRequest = (HttpServletRequest)request;
        HttpServletResponse httpServletResponse = (HttpServletResponse)response;

        String url = new String(httpServletRequest.getRequestURI());

        //只過濾/actuator/bus-refresh請求
        if (!url.endsWith("/actuator/bus-refresh")) {
            chain.doFilter(request, response);
            return;
        }

        //獲取原始的body
        String body = readAsChars(httpServletRequest);

        System.out.println("original body:   "+ body);

        //使用HttpServletRequest包裝原始請求達到修改post請求中body內容的目的
        CustometRequestWrapper requestWrapper = new CustometRequestWrapper(httpServletRequest);

        chain.doFilter(requestWrapper, response);

    }

    @Override
    public void destroy() {

    }

    private class CustometRequestWrapper extends HttpServletRequestWrapper {
        public CustometRequestWrapper(HttpServletRequest request) {
            super(request);
        }

        @Override
        public ServletInputStream getInputStream() throws IOException {
            byte[] bytes = new byte[0];
            ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);

            return new ServletInputStream() {
                @Override
                public boolean isFinished() {
                    return byteArrayInputStream.read() == -1 ? true:false;
                }

                @Override
                public boolean isReady() {
                    return false;
                }

                @Override
                public void setReadListener(ReadListener readListener) {

                }

                @Override
                public int read() throws IOException {
                    return byteArrayInputStream.read();
                }
            };
        }
    }

    public static String readAsChars(HttpServletRequest request)
    {

        BufferedReader br = null;
        StringBuilder sb = new StringBuilder("");
        try
        {
            br = request.getReader();
            String str;
            while ((str = br.readLine()) != null)
            {
                sb.append(str);
            }
            br.close();
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }
        finally
        {
            if (null != br)
            {
                try
                {
                    br.close();
                }
                catch (IOException e)
                {
                    e.printStackTrace();
                }
            }
        }
        return sb.toString();
    }
}


在引導類上標記 @ServletComponentScan(basePackages = “filter所在包名”)

測試

重啟config和customer工程

修改倉庫中配置檔案的內容

測試
image-20200903224332863

十、服務的追蹤-Sleuth【重點


10.1 引言

在整個微服務架構中,微服務很多,一個請求可能需要呼叫很多很多的服務,最終才能完成一個功能,如果說,整個功能出現了問題,在這麼多的服務中,如何去定位到問題的所在點,出現問題的原因是什麼。

  • Sleuth可以獲得到整個服務鏈路的資訊。

  • Zipkin通過圖形化介面去看到資訊。

  • Sleuth將日誌資訊儲存到資料庫中。

Sleuth
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-NTBbLvIO-1607759535346)(Pictures/1588325099243.png)]

10.2 Sleuth的使用

匯入依賴

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>

編寫配置檔案

logging:
  level:
    org.springframework.web.servlet.DispatcherServlet: DEBUG

測試

日誌資訊
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-OiAFMqQy-1607759535347)(Pictures/1588336398785.png)]
SEARCH:服務名稱
e9c:總鏈路id
f07:當前服務的鏈路id
false:不會將當前的日誌資訊,輸出其他系統中

10.3 Zipkin的使用

搭建Zipkin的web工程 https://zipkin.io/

version: "3.1"
services:
  zipkin:
   image: daocloud.io/daocloud/zipkin:latest
   restart: always
   container_name: zipkin
   ports:
     - 9411:9411

匯入依賴:把 sleuth 起步依賴換成 zipkin起步依賴

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>

編寫配置檔案

#指定服務的名稱
spring:
  sleuth:
    sampler:
      probability: 1   # 百分之多少的sleuth資訊需要輸出到zipkin中,1代表百分百
  zipkin:
    base-url: http://192.168.20.128:9411/  # 指定zipkin的地址

測試

測試
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-f8auGw37-1607759535348)(Pictures/1588341969002.png)]

10.4 整合RabbitMQ

匯入RabbitMQ依賴

customer修改配置檔案

spring:
  zipkin:
    sender:
      type: rabbit

修改Zipkin的資訊

version: "3.1"
services:
  zipkin:
   image: daocloud.io/daocloud/zipkin:latest
   restart: always
   container_name: zipkin
   ports:
     - 9411:9411
   environment:
     - RABBIT_ADDRESSES=192.168.20.128:5672
     - RABBIT_USER=test
     - RABBIT_PASSWORD=test
     - RABBIT_VIRTUAL_HOST=/test

測試

重啟customer

測試
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-xtwUkGZ1-1607759535349)(Pictures/1588348586217.png)]

10.5 Zipkin儲存資料到ES

重新修改zipkin的yml檔案

version: "3.1"
services:
  zipkin:
   image: daocloud.io/daocloud/zipkin:latest
   restart: always
   container_name: zipkin
   ports:
     - 9411:9411
   environment:
     - RABBIT_ADDRESSES=192.168.199.109:5672
     - RABBIT_USER=test
     - RABBIT_PASSWORD=test
     - RABBIT_VIRTUAL_HOST=/test
     - STORAGE_TYPE=elasticsearch
     - ES_HOSTS=http://192.168.199.109:9200

十一、完整SpringCloud架構圖【重點


完整架構圖
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-ZQ0nH7t4-1607759535351)(Pictures/1588351313922.png)]