Spring Cloud 系列之 Netflix Hystrix 服務容錯(三)

本篇文章爲系列文章,未讀前幾集的同學請猛戳這裏:

本篇文章講解 Hystrix 的服務熔斷和服務降級以及基於 Feign 的服務熔斷處理。


服務熔斷

點擊鏈接觀看:服務熔斷視頻(獲取更多請關注公衆號「哈嘍沃德先生」)

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

添加依賴

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

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

業務層

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

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 = {
            // 10s 內請求數大於 10 個就啓動熔斷器,當請求符合熔斷條件觸發 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);
    }

}

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 searchOrderById(Integer id) {
        return new Order(id, "order-003", "中國", 2666D,
                // 爲了方便測試直接使用訂單 ID 作爲參數
                Arrays.asList(productService.selectProductById(id)));
    }

}

啓動類

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

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);
    }

}

測試

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

-----selectProductById-----22:47:12.463
-----selectProductById-----22:47:17.677
-----selectProductById-----22:47:22.894

通過結果可以看到,服務熔斷已經啓用。每 5 秒會去重試一次 Provider 如果重試失敗繼續返回託底數據,如此反覆直到服務可用,然後關閉熔斷快速恢復。

服務降級

吃雞遊戲相信大家應該都有所耳聞,這個遊戲落地的時候什麼東西都沒有,裝備都是需要自己去主動搜索或者通過擊殺其他隊伍而獲取。所以,在這個遊戲中就涉及到一個揹包的問題,揹包的大小決定了能攜帶的物資數量,總共分爲三級,在你沒有拿到更高級的揹包之前,你只能將最重要的裝備留在身邊。其實服務降級,就是這麼回事,再看一個例子。

大家都見過女生旅行吧,大號的旅行箱是必備物,平常走走近處綽綽有餘,但一旦出個遠門,再大的箱子都白搭了,怎麼辦呢?常見的情景就是把物品拿出來分分堆,比了又比,最後一些非必需品的就忍痛放下了,等到下次箱子夠用了,再帶上用一用。而服務降級,就是這麼回事,整體資源快不夠了,忍痛將某些服務先關掉,待渡過難關,再開啓回來。

觸發條件

  • 方法拋出非 HystrixBadRequestException 異常;
  • 方法調用超時;
  • 熔斷器開啓攔截調用;
  • 線程池/隊列/信號量跑滿。

添加依賴

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

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

業務層

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

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);
    }

}

測試

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

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

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

Feign 雪崩處理

點擊鏈接觀看:Feign 雪崩處理視頻(獲取更多請關注公衆號「哈嘍沃德先生」)

環境準備

我們在父工程下再創建一個 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

實體類

Product.java

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;

}

消費服務

ProductService.java

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);
    }

}

控制層

OrderController.java

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);
    }

}

捕獲服務異常

我們已經可以通過 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<T> 接口。

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 結果如下:

控制檯打印結果:

ERROR 17468 --- [ HystrixTimer-1] c.e.f.ProductServiceFallbackFactory      : product-service 服務的 selectProductListByIds 方法出現異常,異常信息如下:com.netflix.hystrix.exception.HystrixTimeoutException

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

本文采用 知識共享「署名-非商業性使用-禁止演繹 4.0 國際」許可協議

大家可以通過 分類 查看更多關於 Spring Cloud 的文章。


🤗 您的點贊轉發是對我最大的支持。

📢 掃碼關注 哈嘍沃德先生「文檔 + 視頻」每篇文章都配有專門視頻講解,學習更輕鬆噢 ~

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