Spring Cloud 之 Netflix Hystrix 服務容錯

本文較大篇幅引用https://www.mrhelloworld.com/hystrix-circuit-breaker/,版權歸該文章作者所有

hystrix是什麼?

Hystrix是一個用於處理分佈式系統的延遲和容錯的開源庫,在分佈式系統裏,許多依賴不可避免的會調用失敗,比 如超時、異常等,

Hystrix能夠保證在一個依賴出問題的情況下,不會導致整體服務失敗,避免級聯故障,以提高分 布式系統的彈性。

“斷路器”本身是一種開關裝置,當某個服務單元發生故障之後,通過斷路器的故障監控(類似熔斷保險絲),向調用方返回一個符合預期的、可處理的備選響應(FallBack),而不是長時間的等待或者拋出調用方無法處理的異常,

這樣就保證了服務調用方的線程不會被長時間、不必要地佔用,從而避免了故障在分佈式系統中的蔓延,乃至雪崩。

大型項目中會出現的一些問題

典型的一個案例就是服務血崩效應 我們來看一張圖:

 

 

 上圖是一條微服務調用鏈, 正常的情況我們就不必在討論了, 我們來說一下非正常情況, 假設現在 微服務H 響應 時間過長,或者微服務H直接down機瞭如圖:

 

 

 來看下上圖, 我們聯想一下上圖, 如果發生這種情況, 也就是說所有發給微服務D的請求 都會被卡在微服務H 那, 就會導致線程一直累計在這裏, 那麼其他的微服務(比如A,B,C...) 就沒有可用線程了, 導致整個服務器 崩潰,這就是服務血崩。
導致服務雪崩的情況我們來總結一下,再看看怎麼解決:程序BUG,數據不匹配,響應時間過長,服務不可用等等.....

針對上面的問題,我們來看看有哪些解決方案 :

  服務限流

  超時監控

  服務熔斷

  服務降級

  hystrix-demo 聚合工程。SpringBoot 2.2.4.RELEASESpring Cloud Hoxton.SR1

  • eureka-server:註冊中心eureka(端口8761)
  • eureka-server02:註冊中心eureka(端口8762,兩個註冊中心相互註冊,搭建過程省略)
  • product-service:商品服務,提供了 /product/{id} 接口,/product/list 接口,/product/listByIds 接口
  • order-service-rest:訂單服務,基於 Ribbon 通過 RestTemplate 調用商品服務
  • order-server-feign:訂單服務,基於 Feign 通過聲明式服務調用商品服務

 

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>product-service</artifactId>
    <version>1.0-SNAPSHOT</version>

    <!-- 繼承父依賴 -->
    <parent>
        <groupId>com.example</groupId>
        <artifactId>hystrix-demo</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <!-- 項目依賴 -->
    <dependencies>
        <!-- netflix eureka client 依賴 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <!-- spring boot web 依賴 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- lombok 依賴 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>provided</scope>
        </dependency>

        <!-- spring boot test 依賴 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

</project>
server:
  port: 7070 # 端口

spring:
  application:
    name: product-service # 應用名稱

# 配置 Eureka Server 註冊中心
eureka:
  instance:
    prefer-ip-address: true       # 是否使用 ip 地址註冊
    instance-id: ${spring.cloud.client.ip-address}:${server.port} # ip:port
  client:
    service-url:                  # 設置服務註冊中心地址
      defaultZone: http://localhost:8761/eureka/,http://localhost:8762/eureka/
package com.example.pojo;

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

import java.io.Serializable;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Product implements Serializable {

    private Integer id;
    private String productName;
    private Integer productNum;
    private Double productPrice;

}

 

package com.example.service;

import com.example.pojo.Product;

import java.util.List;

/**
 * 商品服務
 */
public interface ProductService {

    /**
     * 查詢商品列表
     *
     * @return
     */
    List<Product> selectProductList();

    /**
     * 根據多個主鍵查詢商品
     *
     * @param ids
     * @return
     */
    List<Product> selectProductListByIds(List<Integer> ids);

    /**
     * 根據主鍵查詢商品
     *
     * @param id
     * @return
     */
    Product selectProductById(Integer id);

}

實現類

package com.example.service.impl;

import com.example.pojo.Product;
import com.example.service.ProductService;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * 商品服務
 */
@Service
public class ProductServiceImpl implements ProductService {

    /**
     * 查詢商品列表
     *
     * @return
     */
    @Override
    public List<Product> selectProductList() {
        return Arrays.asList(
                new Product(1, "華爲手機", 1, 5800D),
                new Product(2, "聯想筆記本", 1, 6888D),
                new Product(3, "小米平板", 5, 2020D)
        );
    }

    /**
     * 根據多個主鍵查詢商品
     *
     * @param ids
     * @return
     */
    @Override
    public List<Product> selectProductListByIds(List<Integer> ids) {
        List<Product> products = new ArrayList<>();
        ids.forEach(id -> products.add(new Product(id, "電視機" + id, 1, 5800D)));
        return products;
    }

    /**
     * 根據主鍵查詢商品
     *
     * @param id
     * @return
     */
    @Override
    public Product selectProductById(Integer id) {
        return new Product(id, "冰箱", 1, 2666D);
    }

}
package com.example.controller;

import com.example.pojo.Product;
import com.example.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/product")
public class ProductController {

    @Autowired
    private ProductService productService;

    /**
     * 查詢商品列表
     *
     * @return
     */
    @GetMapping("/list")
    public List<Product> selectProductList() {
        return productService.selectProductList();
    }

    /**
     * 根據多個主鍵查詢商品
     *
     * @param ids
     * @return
     */
    @GetMapping("/listByIds")
    public List<Product> selectProductListByIds(@RequestParam("id") List<Integer> ids) {
        return productService.selectProductListByIds(ids);
    }

    /**
     * 根據主鍵查詢商品
     *
     * @param id
     * @return
     */
    @GetMapping("/{id}")
    public Product selectProductById(@PathVariable("id") Integer id) {
        return productService.selectProductById(id);
    }

}
package com.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
// 開啓 EurekaClient 註解,目前版本如果配置了 Eureka 註冊中心,默認會開啓該註解
//@EnableEurekaClient
public class ProductServiceApplication {

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

}
<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>order-service-rest</artifactId>
    <version>1.0-SNAPSHOT</version>

    <!-- 繼承父依賴 -->
    <parent>
        <groupId>com.example</groupId>
        <artifactId>hystrix-demo</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <!-- 項目依賴 -->
    <dependencies>
        <!-- netflix eureka client 依賴 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <!-- spring boot web 依賴 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- lombok 依賴 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>provided</scope>
        </dependency>

        <!-- spring boot test 依賴 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>
  
</project>
server:
  port: 8080 # 端口

spring:
  application:
    name: order-service-rest # 應用名稱

# 配置 Eureka Server 註冊中心
eureka:
  instance:
    prefer-ip-address: true       # 是否使用 ip 地址註冊
    instance-id: ${spring.cloud.client.ip-address}:${server.port} # ip:port
  client:
    service-url:                  # 設置服務註冊中心地址
      defaultZone: http://localhost:8761/eureka/,http://localhost:8762/eureka/
package com.example.pojo;

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

import java.io.Serializable;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Product implements Serializable {

    private Integer id;
    private String productName;
    private Integer productNum;
    private Double productPrice;

}

order

package com.example.pojo;

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

import java.io.Serializable;
import java.util.List;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Order implements Serializable {

    private Integer id;
    private String orderNo;
    private String orderAddress;
    private Double totalPrice;
    private List<Product> productList;

}
package com.example.service;

import com.example.pojo.Product;

import java.util.List;

/**
 * 商品管理
 */
public interface ProductService {

    /**
     * 查詢商品列表
     *
     * @return
     */
    List<Product> selectProductList();

    /**
     * 根據多個主鍵查詢商品
     *
     * @param ids
     * @return
     */
    List<Product> selectProductListByIds(List<Integer> ids);

    /**
     * 根據主鍵查詢商品
     *
     * @param id
     * @return
     */
    Product selectProductById(Integer id);

}

我們先使用 Ribbon 並通過 RestTemplate 來實現遠程服務的調用product服務。先講解 RestTemplate 方式的服務容錯處理。

ProductServiceImpl.java

package com.example.service.impl;

import com.example.pojo.Product;
import com.example.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import java.util.List;

/**
 * 商品管理
 */
@Service
public class ProductServiceImpl implements ProductService {

    @Autowired
    private RestTemplate restTemplate;

    /**
     * 查詢商品列表
     *
     * @return
     */
    @Override
    public List<Product> selectProductList() {
        // ResponseEntity: 封裝了返回數據
        return restTemplate.exchange(
                "http://product-service/product/list",
                HttpMethod.GET,
                null,
                new ParameterizedTypeReference<List<Product>>() {
                }).getBody();
    }

    /**
     * 根據多個主鍵查詢商品
     *
     * @param ids
     * @return
     */
    @Override
    public List<Product> selectProductListByIds(List<Integer> ids) {
        StringBuffer sb = new StringBuffer();
        ids.forEach(id -> sb.append("id=" + id + "&"));
        return restTemplate.getForObject("http://product-service/product/listByIds?" + sb.toString(), List.class);
    }

    /**
     * 根據主鍵查詢商品
     *
     * @param id
     * @return
     */
    @Override
    public Product selectProductById(Integer id) {
        return restTemplate.getForObject("http://product-service/product/" + id, Product.class);
    }

}

OrderService.java

package com.example.service;

import com.example.pojo.Order;

public interface OrderService {

    /**
     * 根據主鍵查詢訂單
     *
     * @param id
     * @return
     */
    Order selectOrderById(Integer id);

    /**
     * 根據主鍵查詢訂單
     *
     * @param id
     * @return
     */
    Order queryOrderById(Integer id);

    /**
     * 根據主鍵查詢訂單
     *
     * @param id
     * @return
     */
    Order searchOrderById(Integer id);

}

OrderServiceImpl.java

package com.example.service.impl;

import com.example.pojo.Order;
import com.example.service.OrderService;
import com.example.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.Arrays;

@Service
public class OrderServiceImpl implements OrderService {

    @Autowired
    private ProductService productService;

    /**
     * 根據主鍵查詢訂單
     *
     * @param id
     * @return
     */
    @Override
    public Order selectOrderById(Integer id) {
        return new Order(id, "order-001", "中國", 22788D,
                productService.selectProductList());
    }

    /**
     * 根據主鍵查詢訂單
     *
     * @param id
     * @return
     */
    @Override
    public Order queryOrderById(Integer id) {
        return new Order(id, "order-002", "中國", 11600D,
                productService.selectProductListByIds(Arrays.asList(1, 2)));
    }

    /**
     * 根據主鍵查詢訂單
     *
     * @param id
     * @return
     */
    @Override
    public Order searchOrderById(Integer id) {
        return new Order(id, "order-003", "中國", 2666D,
                Arrays.asList(productService.selectProductById(5)));
    }

}
package com.example.controller;

import com.example.pojo.Order;
import com.example.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/order")
public class OrderController {

    @Autowired
    private OrderService orderService;

    /**
     * 根據主鍵查詢訂單-調用商品服務 /product/list
     *
     * @param id
     * @return
     */
    @GetMapping("/{id}/product/list")
    public Order selectOrderById(@PathVariable("id") Integer id) {
        return orderService.selectOrderById(id);
    }

    /**
     * 根據主鍵查詢訂單-調用商品服務 /product/listByIds
     *
     * @param id
     * @return
     */
    @GetMapping("/{id}/product/listByIds")
    public Order queryOrderById(@PathVariable("id") Integer id) {
        return orderService.queryOrderById(id);
    }

    /**
     * 根據主鍵查詢訂單-調用商品服務 /product/{id}
     *
     * @param id
     * @return
     */
    @GetMapping("/{id}/product")
    public Order searchOrderById(@PathVariable("id") Integer id) {
        return orderService.searchOrderById(id);
    }

}
package com.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@SpringBootApplication
public class OrderServiceRestApplication {

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

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

}

我們先來解釋一下降級,降級是當我們的某個微服務響應時間過長,或者不可用了,講白了也就是那個微服務調用不了了,我們不能把錯誤信息返回出來,或者讓他一直卡在那裏,所以要在準備一個對應的策略(一個方法)當發生 這種問題的時候我們直接調用這個方法來快速返回這個請求,不讓他一直卡在那 。

講了這麼多,我們來看看具體怎麼操作: 我們剛剛說了某個微服務調用不了了要做降級,也就是說,要在調用方做降級(不然那個微服務都down掉了再做 降級也沒什麼意義了) 比如說我們 order調用product那麼就在order做降級

服務消費者 pom.xml 添加 hystrix 依賴。

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

服務消費者業務層代碼添加服務降級規則。

然後在我們的需要做服務降級方法上面加入註解@HystrixCommand(fallbackMethod就是我們剛剛說的方法的名字)

import com.example.pojo.Product;
import com.example.service.ProductService;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

/**
 * 商品管理
 */
@Service
public class ProductServiceImpl implements ProductService {

    @Autowired
    private RestTemplate restTemplate;

    /**
     * 根據主鍵查詢商品
     *
     * @param id
     * @return
     */
    // 聲明需要服務容錯的方法
    // 服務降級
    @HystrixCommand(fallbackMethod = "selectProductByIdFallback")
    @Override
    public Product selectProductById(Integer id) {
        return restTemplate.getForObject("http://product-service/product/" + id, Product.class);
    }

    // 託底數據
    private Product selectProductByIdFallback(Integer id) {
        return new Product(id, "託底數據", 1, 2666D);
    }

}

服務消費者啓動類開啓熔斷器註解。

package com.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

// 開啓熔斷器註解 2 選 1,@EnableHystrix 封裝了 @EnableCircuitBreaker
// @EnableHystrix
@EnableCircuitBreaker
@SpringBootApplication
public class OrderServiceRestApplication {

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

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

}

@EnableHystrix 或者@EnableCircuitBreaker(他們之間是一個繼承關係,2個註解所描述的內容是 完全一樣的) 

訪問:http://localhost:9090/order/3/product 結果如下:

 

 關閉服務提供者,再次訪問:http://localhost:9090/order/3/product 結果如下:

 

 通過結果可以看到,服務降級已經啓用。當 Provider 不可用時返回託底數據,直到服務可用快速恢復。

我們在服務提供者的該方法上添加2秒延時,

@GetMapping("/{id}")
    public Product selectProductById(@PathVariable("id") Integer id) throws InterruptedException {
        Thread.sleep(2000);
        return productService.selectProductById(id);
    }

再重啓服務提供者product-service測試一次

 

 可能有些同學有疑問, 我這裏什麼都沒幹, 就讓他休眠了一下 , 怎麼就知道我這裏超時了呢? 因爲hystrix他有默認的超時監聽,當你這個請求默認超過了1秒鐘就會超時

當然,這個可以配置的,至於怎麼配 置,待會兒我會把一些配置統一列出來

講了這麼多, 這個降級到底有什麼用呢?

第一, 他可以監聽你的請求有沒有超時,

第二,報錯了他這裏直接截斷了沒有讓請求一直卡在這裏

其實降級還有一個好處, 就是當你的系統馬上迎來大量的併發(雙十一秒殺這種 或者促銷活動) 這時候如果發現系 統馬上承載不了這麼大的併發時, 可以考慮先關閉一些不重要的微服務(在降級方法裏面返回一個比較友好的信 息),

把資源讓給主微服務,總結一下就是 整體資源快不夠了,忍痛將某些服務先關掉,待渡過難關,再開啓回來。

 

服務熔斷一般是指軟件系統中,由於某些原因使得服務出現了過載現象,爲防止造成整個系統故障,從而採用的一種保護措施,所以很多地方把熔斷亦稱爲過載保護。

 其實熔斷,就好像我們生活中的跳閘一樣, 比如說你的電路出故障了,爲了防止出現 大型事故 這裏直接切斷了你的電源以免意外繼續發生, 把這個概念放在我們程序上也是如此, 當一個微服務調用多 次出現問題時(默認是10秒內20次當然 這個也能配置),hystrix就會採取熔斷機制,不再繼續調用你的方法(會 在默認5秒鐘內和電器短路一樣,5秒鐘後會試探性的先關閉熔斷機制,但是如果這時候再失敗一次{之前是20次} 那麼又會重新進行熔斷) 而是直接調用降級方法,這樣就一定程度上避免了服務雪崩的問題

服務消費者業務層代碼添加服務熔斷規則。

package com.example.service.impl;

import com.example.pojo.Product;
import com.example.service.ProductService;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import com.netflix.hystrix.contrib.javanica.conf.HystrixPropertiesManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

/**
 * 商品管理
 */
@Service
public class ProductServiceImpl implements ProductService {

    @Autowired
    private RestTemplate restTemplate;

    /**
     * 根據主鍵查詢商品
     *
     * @param id
     * @return
     */
    // 聲明需要服務容錯的方法
    // 服務熔斷
    @HystrixCommand(commandProperties = {
            // 當請求符合熔斷條件觸發 fallbackMethod 默認 20 個
            @HystrixProperty(name = HystrixPropertiesManager.CIRCUIT_BREAKER_REQUEST_VOLUME_THRESHOLD,
                    value = "10"),
            // 請求錯誤率大於 50% 就啓動熔斷器,然後 for 循環發起重試請求,當請求符合熔斷條件觸發 fallbackMethod
            @HystrixProperty(name = HystrixPropertiesManager.CIRCUIT_BREAKER_ERROR_THRESHOLD_PERCENTAGE,
                    value = "50"),
            // 熔斷多少秒後去重試請求,默認 5s
            @HystrixProperty(name = HystrixPropertiesManager.CIRCUIT_BREAKER_SLEEP_WINDOW_IN_MILLISECONDS,
                    value = "5000"),
    }, fallbackMethod = "selectProductByIdFallback")
    @Override
    public Product selectProductById(Integer id) {
        System.out.println("-----selectProductById-----"
                + LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_TIME));
        // 模擬查詢主鍵爲 1 的商品信息會導致異常
        if (1 == id)
            throw new RuntimeException("查詢主鍵爲 1 的商品信息導致異常");
        return restTemplate.getForObject("http://product-service/product/" + id, Product.class);
    }

    // 託底數據
    private Product selectProductByIdFallback(Integer id) {
        return new Product(id, "託底數據", 1, 2666D);
    }

}

服務消費者啓動類開啓熔斷器註解。

這個東西光筆記不太好測試,只能你們自己去測試了

 

我們在父工程下再創建一個 Consumer 項目這次是基於 Feign 實現聲明式服務調用。

服務提供者添加 openfeign 依賴,openfeign 默認集成了 hystrix 依賴。

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>order-service-feign</artifactId>
    <version>1.0-SNAPSHOT</version>

    <!-- 繼承父依賴 -->
    <parent>
        <groupId>com.example</groupId>
        <artifactId>hystrix-demo</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <!-- 項目依賴 -->
    <dependencies>
        <!-- netflix eureka client 依賴 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <!-- spring cloud openfeign 依賴 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <!-- spring boot web 依賴 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- lombok 依賴 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>provided</scope>
        </dependency>

        <!-- spring boot test 依賴 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

</project>

服務提供者需要開啓 Feign 對於 Hystrix 的支持。

server:
  port: 9091 # 端口

spring:
  application:
    name: order-service-feign # 應用名稱

# 配置 Eureka Server 註冊中心
eureka:
  instance:
    prefer-ip-address: true       # 是否使用 ip 地址註冊
    instance-id: ${spring.cloud.client.ip-address}:${server.port} # ip:port
  client:
    service-url:                  # 設置服務註冊中心地址
      defaultZone: http://localhost:8761/eureka/,http://localhost:8762/eureka/

# Feign 開啓 Hystrix 支持
feign:
  hystrix:
    enabled: true
package com.example.pojo;

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

import java.io.Serializable;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Product implements Serializable {

    private Integer id;
    private String productName;
    private Integer productNum;
    private Double productPrice;

}

Order.java

package com.example.pojo;

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

import java.io.Serializable;
import java.util.List;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Order implements Serializable {

    private Integer id;
    private String orderNo;
    private String orderAddress;
    private Double totalPrice;
    private List<Product> productList;

}
package com.example.service;

import com.example.fallback.ProductServiceFallback;
import com.example.pojo.Product;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;

import java.util.List;

// 聲明需要調用的服務和服務熔斷處理類
@FeignClient(value = "product-service", fallback = ProductServiceFallback.class)
public interface ProductService {

    /**
     * 查詢商品列表
     *
     * @return
     */
    @GetMapping("/product/list")
    List<Product> selectProductList();

    /**
     * 根據多個主鍵查詢商品
     *
     * @param ids
     * @return
     */
    @GetMapping("/product/listByIds")
    List<Product> selectProductListByIds(@RequestParam("id") List<Integer> ids);

    /**
     * 根據主鍵查詢商品
     *
     * @param id
     * @return
     */
    @GetMapping("/product/{id}")
    Product selectProductById(@PathVariable("id") Integer id);

}

OrderService.java

package com.example.service;

import com.example.pojo.Order;

public interface OrderService {

    /**
     * 根據主鍵查詢訂單
     *
     * @param id
     * @return
     */
    Order selectOrderById(Integer id);

    /**
     * 根據主鍵查詢訂單
     *
     * @param id
     * @return
     */
    Order queryOrderById(Integer id);

    /**
     * 根據主鍵查詢訂單
     *
     * @param id
     * @return
     */
    Order searchOrderById(Integer id);

}

OrderServiceImpl.java

package com.example.service.impl;

import com.example.pojo.Order;
import com.example.service.OrderService;
import com.example.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.Arrays;

@Service
public class OrderServiceImpl implements OrderService {

    @Autowired
    private ProductService productService;

    /**
     * 根據主鍵查詢訂單
     *
     * @param id
     * @return
     */
    @Override
    public Order selectOrderById(Integer id) {
        return new Order(id, "order-001", "中國", 22788D,
                productService.selectProductList());
    }

    /**
     * 根據主鍵查詢訂單
     *
     * @param id
     * @return
     */
    @Override
    public Order queryOrderById(Integer id) {
        return new Order(id, "order-002", "中國", 11600D,
                productService.selectProductListByIds(Arrays.asList(1, 2)));
    }

    /**
     * 根據主鍵查詢訂單
     *
     * @param id
     * @return
     */
    @Override
    public Order searchOrderById(Integer id) {
        return new Order(id, "order-003", "中國", 2666D,
                Arrays.asList(productService.selectProductById(5)));
    }

}

ProductServiceFallback.java

package com.example.fallback;

import com.example.pojo.Product;
import com.example.service.ProductService;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * 服務熔斷降級處理
 */
@Component
public class ProductServiceFallback implements ProductService {

    // 查詢商品列表接口的託底數據
    @Override
    public List<Product> selectProductList() {
        return Arrays.asList(
                new Product(1, "託底數據-華爲手機", 1, 5800D),
                new Product(2, "託底數據-聯想筆記本", 1, 6888D),
                new Product(3, "託底數據-小米平板", 5, 2020D)
        );
    }

    // 根據多個主鍵查詢商品接口的託底數據
    @Override
    public List<Product> selectProductListByIds(List<Integer> ids) {
        List<Product> products = new ArrayList<>();
        ids.forEach(id -> products.add(new Product(id, "託底數據-電視機" + id, 1, 5800D)));
        return products;
    }

    // 根據主鍵查詢商品接口的託底數據
    @Override
    public Product selectProductById(Integer id) {
        return new Product(id, "託底數據", 1, 2666D);
    }

}
package com.example.controller;

import com.example.pojo.Order;
import com.example.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/order")
public class OrderController {

    @Autowired
    private OrderService orderService;

    /**
     * 根據主鍵查詢訂單-調用商品服務 /product/list
     *
     * @param id
     * @return
     */
    @GetMapping("/{id}/product/list")
    public Order selectOrderById(@PathVariable("id") Integer id) {
        return orderService.selectOrderById(id);
    }

    /**
     * 根據主鍵查詢訂單-調用商品服務 /product/listByIds
     *
     * @param id
     * @return
     */
    @GetMapping("/{id}/product/listByIds")
    public Order queryOrderById(@PathVariable("id") Integer id) {
        return orderService.queryOrderById(id);
    }

    /**
     * 根據主鍵查詢訂單-調用商品服務 /product/{id}
     *
     * @param id
     * @return
     */
    @GetMapping("/{id}/product")
    public Order searchOrderById(@PathVariable("id") Integer id) {
        return orderService.searchOrderById(id);
    }

}

服務消費者啓動類開啓 @EnableFeignClients 註解。

package com.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
// 開啓 FeignClients 註解
@EnableFeignClients
public class OrderServiceFeignApplication {

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

}
http://localhost:9091/order/5/product

正常顯示頁面

 

 添加2秒睡眠時間,再次請求

 

 我們已經可以通過 Feign 實現服務降級處理,

但是服務不可用時如果我們想要捕獲異常信息該如何實現?接下來一起學習一下

通過 fallbackFactory 屬性聲明服務熔斷降級處理類。

package com.example.service;

import com.example.fallback.ProductServiceFallbackFactory;
import com.example.pojo.Product;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;

import java.util.List;

// 聲明需要調用的服務和服務熔斷處理類
@FeignClient(value = "product-service", fallbackFactory = ProductServiceFallbackFactory.class)
public interface ProductService {

    /**
     * 查詢商品列表
     *
     * @return
     */
    @GetMapping("/product/list")
    List<Product> selectProductList();

    /**
     * 根據多個主鍵查詢商品
     *
     * @param ids
     * @return
     */
    @GetMapping("/product/listByIds")
    List<Product> selectProductListByIds(@RequestParam("id") List<Integer> ids);

    /**
     * 根據主鍵查詢商品
     *
     * @param id
     * @return
     */
    @GetMapping("/product/{id}")
    Product selectProductById(@PathVariable("id") Integer id);

}

實現 FallbackFactory 接口。

package com.example.fallback;

import com.example.pojo.Product;
import com.example.service.ProductService;
import feign.hystrix.FallbackFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * 服務熔斷降級處理可以捕獲異常
 */
@Component
public class ProductServiceFallbackFactory implements FallbackFactory<ProductService> {

    // 獲取日誌,在需要捕獲異常的方法中進行處理
    Logger logger = LoggerFactory.getLogger(ProductServiceFallbackFactory.class);

    @Override
    public ProductService create(Throwable throwable) {
        return new ProductService() {
            // 查詢商品列表接口的託底數據
            @Override
            public List<Product> selectProductList() {
                logger.error("product-service 服務的 selectProductList 方法出現異常,異常信息如下:"
                        + throwable);
                return Arrays.asList(
                        new Product(1, "託底數據-華爲手機", 1, 5800D),
                        new Product(2, "託底數據-聯想筆記本", 1, 6888D),
                        new Product(3, "託底數據-小米平板", 5, 2020D)
                );
            }

            // 根據多個主鍵查詢商品接口的託底數據
            @Override
            public List<Product> selectProductListByIds(List<Integer> ids) {
                logger.error("product-service 服務的 selectProductListByIds 方法出現異常,異常信息如下:"
                        + throwable);
                List<Product> products = new ArrayList<>();
                ids.forEach(id -> products.add(new Product(id, "託底數據-電視機" + id, 1, 5800D)));
                return products;
            }

            // 根據主鍵查詢商品接口的託底數據
            @Override
            public Product selectProductById(Integer id) {
                logger.error("product-service 服務的 selectProductById 方法出現異常,異常信息如下:"
                        + throwable);
                return new Product(id, "託底數據", 1, 2666D);
            }
        };
    }

}

訪問:http://localhost:9091/order/1/product/list 結果如下:

 

 控制檯打印結果:

 

 hystrix相關配置:

hystrix.command.default和hystrix.threadpool.default中的default爲默認CommandKey

Command Properties
Execution相關的屬性的配置:
hystrix.command.default.execution.isolation.strategy 隔離策略,默認是Thread, 可選Thread|Semaphore

hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds 命令執行超時時間,默認1000ms

hystrix.command.default.execution.timeout.enabled 執行是否啓用超時,默認啓用true
hystrix.command.default.execution.isolation.thread.interruptOnTimeout 發生超時是是否中斷,默認true
hystrix.command.default.execution.isolation.semaphore.maxConcurrentRequests 最大併發請求數,默認10,該參數當使用ExecutionIsolationStrategy.SEMAPHORE策略時纔有效。如果達到最大併發請求數,請求會被拒絕。理論上選擇semaphore size的原則和選擇thread size一致,但選用semaphore時每次執行的單元要比較小且執行速度快(ms級別),否則的話應該用thread。
semaphore應該佔整個容器(tomcat)的線程池的一小部分。
Fallback相關的屬性
這些參數可以應用於Hystrix的THREAD和SEMAPHORE策略

hystrix.command.default.fallback.isolation.semaphore.maxConcurrentRequests 如果併發數達到該設置值,請求會被拒絕和拋出異常並且fallback不會被調用。默認10
hystrix.command.default.fallback.enabled 當執行失敗或者請求被拒絕,是否會嘗試調用hystrixCommand.getFallback() 。默認true
Circuit Breaker相關的屬性
hystrix.command.default.circuitBreaker.enabled 用來跟蹤circuit的健康性,如果未達標則讓request短路。默認true
hystrix.command.default.circuitBreaker.requestVolumeThreshold 一個rolling window內最小的請求數。如果設爲20,那麼當一個rolling window的時間內(比如說1個rolling window是10秒)收到19個請求,即使19個請求都失敗,也不會觸發circuit break。默認20
hystrix.command.default.circuitBreaker.sleepWindowInMilliseconds 觸發短路的時間值,當該值設爲5000時,則當觸發circuit break後的5000毫秒內都會拒絕request,也就是5000毫秒後纔會關閉circuit。默認5000
hystrix.command.default.circuitBreaker.errorThresholdPercentage錯誤比率閥值,如果錯誤率>=該值,circuit會被打開,並短路所有請求觸發fallback。默認50
hystrix.command.default.circuitBreaker.forceOpen 強制打開熔斷器,如果打開這個開關,那麼拒絕所有request,默認false
hystrix.command.default.circuitBreaker.forceClosed 強制關閉熔斷器 如果這個開關打開,circuit將一直關閉且忽略circuitBreaker.errorThresholdPercentage
Metrics相關參數
hystrix.command.default.metrics.rollingStats.timeInMilliseconds 設置統計的時間窗口值的,毫秒值,circuit break 的打開會根據1個rolling window的統計來計算。若rolling window被設爲10000毫秒,則rolling window會被分成n個buckets,每個bucket包含success,failure,timeout,rejection的次數的統計信息。默認10000
hystrix.command.default.metrics.rollingStats.numBuckets 設置一個rolling window被劃分的數量,若numBuckets=10,rolling window=10000,那麼一個bucket的時間即1秒。必須符合rolling window % numberBuckets == 0。默認10
hystrix.command.default.metrics.rollingPercentile.enabled 執行時是否enable指標的計算和跟蹤,默認true
hystrix.command.default.metrics.rollingPercentile.timeInMilliseconds 設置rolling percentile window的時間,默認60000
hystrix.command.default.metrics.rollingPercentile.numBuckets 設置rolling percentile window的numberBuckets。邏輯同上。默認6
hystrix.command.default.metrics.rollingPercentile.bucketSize 如果bucket size=100,window=10s,若這10s裏有500次執行,只有最後100次執行會被統計到bucket裏去。增加該值會增加內存開銷以及排序的開銷。默認100
hystrix.command.default.metrics.healthSnapshot.intervalInMilliseconds 記錄health 快照(用來統計成功和錯誤綠)的間隔,默認500ms
Request Context 相關參數
hystrix.command.default.requestCache.enabled 默認true,需要重載getCacheKey(),返回null時不緩存
hystrix.command.default.requestLog.enabled 記錄日誌到HystrixRequestLog,默認true

Collapser Properties 相關參數
hystrix.collapser.default.maxRequestsInBatch 單次批處理的最大請求數,達到該數量觸發批處理,默認Integer.MAX_VALUE
hystrix.collapser.default.timerDelayInMilliseconds 觸發批處理的延遲,也可以爲創建批處理的時間+該值,默認10
hystrix.collapser.default.requestCache.enabled 是否對HystrixCollapser.execute() and HystrixCollapser.queue()的cache,默認true

ThreadPool 相關參數
線程數默認值10適用於大部分情況(有時可以設置得更小),如果需要設置得更大,那有個基本得公式可以follow:
requests per second at peak when healthy × 99th percentile latency in seconds + some breathing room
每秒最大支撐的請求數 (99%平均響應時間 + 緩存值)
比如:每秒能處理1000個請求,99%的請求響應時間是60ms,那麼公式是:
(0.060+0.012)

基本得原則時保持線程池儘可能小,他主要是爲了釋放壓力,防止資源被阻塞。
當一切都是正常的時候,線程池一般僅會有1到2個線程激活來提供服務

hystrix.threadpool.default.coreSize 併發執行的最大線程數,默認10
hystrix.threadpool.default.maxQueueSize BlockingQueue的最大隊列數,當設爲-1,會使用SynchronousQueue,值爲正時使用LinkedBlcokingQueue。該設置只會在初始化時有效,之後不能修改threadpool的queue size,除非reinitialising thread executor。默認-1。
hystrix.threadpool.default.queueSizeRejectionThreshold 即使maxQueueSize沒有達到,達到queueSizeRejectionThreshold該值後,請求也會被拒絕。因爲maxQueueSize不能被動態修改,這個參數將允許我們動態設置該值。if maxQueueSize == -1,該字段將不起作用
hystrix.threadpool.default.keepAliveTimeMinutes 如果corePoolSize和maxPoolSize設成一樣(默認實現)該設置無效。如果通過plugin(https://github.com/Netflix/Hystrix/wiki/Plugins)使用自定義實現,該設置纔有用,默認1.
hystrix.threadpool.default.metrics.rollingStats.timeInMilliseconds 線程池統計指標的時間,默認10000
hystrix.threadpool.default.metrics.rollingStats.numBuckets 將rolling window劃分爲n個buckets,默認10

 

至此 Hystrix 服務容錯知識點就講解結束了。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章