SpringCloud 使用 Hystrix 实现【客户端】降级
前面已经介绍了 Hystrix 服务端降级的实现方案,本篇博客将介绍 Hystrix 在客户端降级的实现方案。
由于我使用最新版的 SpringCloud(版本 2021.0.3)实现客户端降级没有成功,所以就拿相对比较稳定的 SpringCloud(版本 Hoxton.SR12)和对应的 SpringBoot(版本 2.3.12.RELEASE)来介绍演示 Hystrix 客户端降级的代码实现方案。
在本篇博客的最后会提供源代码的下载,好了,话不多说,直接代码展示。
一、搭建工程
采用 Maven 搭建 springcloud_hystrix2 父工程,下面包含 3 个子工程:
对于 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 的重点,客户端降级就是在这里实现,搭建后的结果如下:
首先列出 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 的以来。
根据服务提供者的接口,定义 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