微服务系列之Hystrix服务容错(三)

服务熔断
服务熔断一般是指软件系统中,由于某些原因使得服务出现了过载现象,为防止造成整个系统故障,从而采用的一种保护措施,所以很多地方把熔断亦称为过载保护。
在这里插入图片描述
添加依赖
服务消费者 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 雪崩处理

环境准备
我们在父工程下再创建一个 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 接口。

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 服务容错知识点就讲解结束了。

发布了0 篇原创文章 · 获赞 0 · 访问量 997

猜你喜欢

转载自blog.csdn.net/qq_41490913/article/details/105106595