SpringCloud 使用 Hystrix 实现【客户端】降级

前面已经介绍了 Hystrix 服务端降级的实现方案,本篇博客将介绍 Hystrix 在客户端降级的实现方案。

由于我使用最新版的 SpringCloud(版本 2021.0.3)实现客户端降级没有成功,所以就拿相对比较稳定的 SpringCloud(版本 Hoxton.SR12)和对应的 SpringBoot(版本 2.3.12.RELEASE)来介绍演示 Hystrix 客户端降级的代码实现方案。

在本篇博客的最后会提供源代码的下载,好了,话不多说,直接代码展示。


一、搭建工程

采用 Maven 搭建 springcloud_hystrix2 父工程,下面包含 3 个子工程:

image

对于 springcloud_hystrix2 父工程 pom 文件内容如下所示:

<?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.jobs</groupId>
    <artifactId>springcloud_hystrix2</artifactId>
    <packaging>pom</packaging>
    <version>1.0-SNAPSHOT</version>
    <modules>
        <module>eureka_app</module>
        <module>provider_app</module>
        <module>consumer_app</module>
    </modules>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>

    <!--spring boot-->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.12.RELEASE</version>
        <relativePath/>
    </parent>

    <!--Spring Cloud-->
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Hoxton.SR12</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
</project>

注意:Demo 的 SpringCloud 版本为 Hoxton.SR12,使用的 SpringBoot 版本为 2.3.12.RELEASE

有关【Eureka 注册中心】的搭建过程,这里就省略了,跟之前博客一模一样。


二、服务提供者搭建

对于服务提供者,本身也使用了服务端降级,具体细节不再多说,主要提供了 3 个接口:

package com.jobs.provider.controller;

import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

@RequestMapping("/provider")
@RestController
public class ProviderController {

    //该方法处理业务逻辑处理,不会发生超时,
    //因为 Hstrix 配置的【处理时间】大于【实际所需要的业务逻辑处理时间】
    @HystrixCommand(fallbackMethod = "GetTimeOutFallback", commandProperties = {
            //设置 Hystrix 的允许的业务逻辑处理超时时间为 3 秒
            //想要查找可以设置的 HystrixProperty 值,可以按两下 shift 键,
            //输入 HystrixCommandProperty,然后查看 HystrixCommandProperties 类的构造方法
            @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",
                    value = "3000")})
    @RequestMapping("/test1/{id}")
    public Map GetTimeOut(@PathVariable("id") int id) {

        //这里虽然休眠 2 秒,但是设置了 Hystrix 允许业务逻辑处理时间为 3 秒,所以不会超时
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        Map result = new HashMap();
        result.put("status", 0);
        result.put("msg", "success");
        result.put("get_id_value", id);
        result.put("version", UUID.randomUUID().toString());

        return result;
    }

    //定义 GetTimeOut 的降级方法
    //方法的返回值、参数列表、参数类型,要与原方法保持一致
    public Map GetTimeOutFallback(int id) {

        Map result = new HashMap();
        result.put("status", 1);
        result.put("msg", "fallback...");
        result.put("method", "GetError");
        result.put("get_id_value", id);
        result.put("remark", "服务端接口业务处理超时,导致降级...");

        return result;
    }

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

    //该方法会发生异常,从而调用 @HystrixCommand 方法所配置的降级方法
    @HystrixCommand(fallbackMethod = "GetErrorFallback")
    @RequestMapping("/test2/{id}")
    public Map GetError1(@PathVariable("id") int id) {

        //执行此处,会发生异常,导致调用降级方法
        int num = id / 0;

        Map result = new HashMap();
        result.put("status", 0);
        result.put("msg", "success");
        result.put("data", num);
        result.put("version", UUID.randomUUID().toString());

        return result;
    }

    //定义 GetError 的降级方法
    //方法的返回值、参数列表、参数类型,要与原方法保持一致
    public Map GetErrorFallback(int id) {

        Map result = new HashMap();
        result.put("status", 1);
        result.put("msg", "fallback...");
        result.put("method", "GetError");
        result.put("get_id_value", id);
        result.put("remark", "服务端接口发生异常,导致降级...");

        return result;
    }

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

    //该方法会发生异常,并且没有进行降级处理
    @RequestMapping("/test3/{id}")
    public Map GetError2(@PathVariable("id") int id) {

        //执行此处,会发生异常,导致调用降级方法
        int num = id / 0;

        Map result = new HashMap();
        result.put("status", 0);
        result.put("msg", "success");
        result.put("data", num);
        result.put("version", UUID.randomUUID().toString());

        return result;
    }
}

对于服务提供者提供的接口:

  • /provider/test1/{id} 接口,故意休眠 2 秒,但服务端配置超时时间为 3 秒,所以不会发生服务端降级
  • /provider/test1/{id} 接口,故意代码运行抛出异常,所以在服务端会产生降级
  • /provider/test1/{id} 接口,故意代码运行抛出异常,但是没有提供降级方法

三、客户端消费者搭建

这个是本篇博客 Demo 的重点,客户端降级就是在这里实现,搭建后的结果如下:

image

首先列出 pom 文件的内容:

<?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">
    <parent>
        <artifactId>springcloud_hystrix2</artifactId>
        <groupId>com.jobs</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>consumer_app</artifactId>

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

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

        <!--引入 openfeign 的依赖,已经包含了 hystrix 的依赖-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
    </dependencies>
</project>

对于客户端降级的实现方式,采用 openfeign 来进行实现,非常简单。由于 SpringCloud 在 2020 版本之前,openfeign 已经包含了 Hystrix 的以来,所以客户端在引入了 openfeign 的依赖后,就不需要再引入 hystrix 的以来。

image

根据服务提供者的接口,定义 feign 的接口方法:

package com.jobs.consumer.feignclient;

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

import java.util.Map;

/**
 * 使用 feign 的步骤如下:
 * 1. 在 SpringBoot 的启动类上,增加 @EnableFeignClients 注解
 * 2. 定义 feign 声明式接口,添加注解 @FeignClient,设置 value 值为【服务提供者的】应用名称
 * 3. 定义接口方法,方法的参数列表和返回值,需要跟服务提供者的接口方法保持一致,方法名称无所谓
 * 4. 在 ConsumerController 中注入该接口对象,调用接口方法完成远程调用
 */

@FeignClient(value = "PROVIDER-APP", fallbackFactory = ProviderAppClientImpl.class)
public interface ProviderAppClient {

    @RequestMapping("/provider/test1/{id}")
    Map GetServerTimeoutData(@PathVariable("id") int id);

    @RequestMapping("/provider/test2/{id}")
    Map GetServerErrorData(@PathVariable("id") int id);

    @RequestMapping("/provider/test3/{id}")
    Map GetServerExpectionData(@PathVariable("id") int id);
}

在上面的接口中,fallbackFactory 属性指定了一个类,该类的作用就是当调用 feign 接口访问远程服务出现问题时,就会调用 fallbackFactory 类中的降级方法,从而实现客户端降级,因此我们需要定义这个降级处理方法类 ProviderAppClientImpl

package com.jobs.consumer.feignclient;

import org.springframework.cloud.openfeign.FallbackFactory;
import org.springframework.stereotype.Component;

import java.util.HashMap;
import java.util.Map;

/**
 * 使用 Feign 实现客户端降级的方案:
 * 1. 定义类去实现 FallbackFactory<自己定义的 Feign 接口>
 * 2. 使用 @Component 注解将该类的 Bean 加入 Spring 容器中
 */

@Component
public class ProviderAppClientImpl implements FallbackFactory<ProviderAppClient> {

    @Override
    public ProviderAppClient create(Throwable cause) {
        //记录错误异常信息,比如打印到控制台上
        System.out.println(cause);

        return new ProviderAppClient() {
            @Override
            public Map GetServerTimeoutData(int id) {
                Map result = new HashMap();
                result.put("status", 1);
                result.put("msg", "调用服务端 test1 接口,降级了111111...");
                result.put("get_id_value", id);

                return result;
            }

            @Override
            public Map GetServerErrorData(int id) {
                Map result = new HashMap();
                result.put("status", 1);
                result.put("msg", "调用服务端 test2 接口,发生了降级222222...");
                result.put("get_id_value", id);

                return result;
            }

            @Override
            public Map GetServerExpectionData(int id) {
                Map result = new HashMap();
                result.put("status", 1);
                result.put("msg", "调用服务端 test3 接口,导致降级333333...");
                result.put("get_id_value", id);

                return result;
            }
        };
    }
}

然后就可以在 ConsumerController 类中调用 feign 接口的方法,调用远程服务接口进行测试

package com.jobs.consumer.controller;

import com.jobs.consumer.feignclient.ProviderAppClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;

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

    /*
    客户端服务消费者,会在两种情况下发生降级:
    1 客户端这边,调用服务端接口,服务端接口出现异常,服务端没有降级逻辑
    2 客户端调用远程服务接口,等待远程响应时间超时(
    --- 对于 Hystrix 来说,默认等待时间为 1 秒,否则就会超时
    --- 可以在 yml 中禁用 Hystrix 超时控制,配置 feign 的 readtimeout 解决问题

    注意,注意:
    客户端服务消费者,自己本身代码出现了异常,不会触发客户端降级
     */

    @Autowired
    private ProviderAppClient providerAppClient;

    //客户端调用服务端接口,服务端业务处理超过了 1 秒(服务端接口正常无异常)
    //客户端等待接口响应,本博客 demo 配置了 feign 最大等待时间为 1 秒,所以超时导致客户端降级
    //可以通过配置 feign 针对具体的服务名称(如 PROVIDER-APP)的 readtimeout 属性
    @RequestMapping("/test1/{id}")
    public Map GetTest1(@PathVariable("id") int id) {

        Map result = providerAppClient.GetServerTimeoutData(id);
        return result;
    }

    //服务端接口会发生异常,在服务端【有降级处理逻辑】,返回服务端接口降级处理结果
    //所以客户端这边,就不会发生降级,直接返回服务端降级后的结果
    @RequestMapping("/test2/{id}")
    public Map GetTest2(@PathVariable("id") int id) {

        Map result = providerAppClient.GetServerErrorData(id);
        return result;
    }

    //服务端接口会发生异常,在服务端【没有降级处理逻辑】
    //
    @RequestMapping("/test3/{id}")
    public Map GetTest3(@PathVariable("id") int id) {

        Map result = providerAppClient.GetServerExpectionData(id);
        return result;
    }

    //在客户端这边的代码发生异常,不会调用降级方法
    //只有是通过 feign 调用远程接口出现问题时,才会出现客户端降级
    @RequestMapping("/test4/{id}")
    public Map GetTest4(@PathVariable("id") int id) {

        int num = id / 0;

        Map result = providerAppClient.GetServerErrorData(id);
        return result;
    }
}

最后我们还需要在 application.yml 文件中增加以下配置:

  • 启动 feign 有关 hystrix 的降级熔断功能,配置 feign 的超时等待时间(默认是 1 秒)
  • 配置 hystrix 的超时等待时间(默认是 1 秒)
# 启用 feign 有关 hystrix 的降级熔断功能
feign:
  circuitbreaker:
    enabled: true
  client:
    config:
      # 采用 feign 来控制客户端等待超时时间
      PROVIDER-APP:
        connecttimeout: 1000
        # 可以将此配置,修改为 3000 来测试 /test1 接口
        # 这样就不会出现客户端等待响应超时,导致客户端降级
        readtimeout: 1000

#也可以通过配置 Hystrix 的默认超时时间,解决超时问题
hystrix:
  command:
    # 对于 Hystrix 默认超时等待时间为 1 秒
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 1000
    # 要想通过 Hystrix 单独控制一个方法,可以采用以下格式
    # feign接口名称#方法名称(参数类型,参数类型...) 的格式
    ProviderAppClient#GetServerTimeoutData(int):
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 1000

注意:上面有关 feign 和 hystrix 配置的超时时间都是 1 秒(默认值)

客户端在 2 种情况下,会发生客户端的降级:(注意客户端本身代码出现异常,不会触发客户端降级)

  • 客户端调用服务端的接口,服务端接口响应超时
  • 客户端调用服务端的接口,服务端接口出现异常

客户端提供了 4 个接口,用于验证客户端降级效果:

  • /consumer/test1/{id} 接口,由于配置的超时时间是 1 秒,在访问服务端的 /provider/test1/{id} 接口时,服务端接口业务处理逻辑是 2 秒(正常运行没有异常),所以导致客户端接口等待响应超时,导致客户端降级。需要在客户端的 yml 配置文件中,同时修改 feign 的 PROVIDER-APP 下 readtimeout 属性和 hystrix 的 ProviderAppClient#GetServerTimeoutData(int) 下的 timeoutInMilliseconds 属性,比如设置为 3 秒(大于服务端接口相应时间),就不会再发生客户端降级了。
  • /consumer/test2/{id} 接口,调用服务端接口时,服务端接口发生了异常,但是服务端进行了降级处理,所以不会导致客户端发生降级,会直接返回服务端降级处理后的结果。
  • /consumer/test3/{id} 接口,调用服务端接口时,服务端接口发生了异常,且服务端没有进行降级处理,从而导致客户端发生降级。
  • /consumer/test4/{id} 接口,客户端本身在执行代码时,出现异常,不会触发客户端降级,直接报错。


OK,到此为止,所有工程已经搭建完毕,与之前博客重复的代码细节,这里就省略了。你可以下载本篇博客的源代码进行详细查看。最后可以运行 Demo 程序,调用客户端提供的接口,验证服务端降级效果。本篇博客的 Demo 源代码经过详细测试无误。

本篇博客的 Demo 源代码下载地址为:https://files.cnblogs.com/files/blogs/699532/springcloud_hystrix2.zip

posted @ 2022-09-04 22:57  乔京飞  阅读(10294)  评论(0编辑  收藏  举报