写在前面
该文参考来自 程序猿DD 的Spring Cloud 微服务实战一书,该文是作为阅读了 spring cloud hystrix 一章的读书笔记。书中版本比较老,我选择了最新稳定版的 spring cloud Greenwich.SR2 版本,该版本较书中版本有些变动。非常感谢作者提供了这么好的学习思路,谢谢!文章也参考了 Spring-cloud-netflix 的官方文档。
1. 创建请求命令
Hystrix 命令就是 HystrixCommand
,它是用来封装具体的依赖服务调用逻辑。它的文档中这样写到:“用于包装执行可能具有潜在风险的代码(通常意味着基于网络的服务调用),具有容错和超时机制,统计数据和性能指标,断路器和舱壁功能。”
该命令本质上是一个阻塞命令,但是结合 observe()
方法使用,则提供了可观察的外观。
1.1 基于继承
通过继承的方式来实现,需要注意的是 HystrixCommand
并没有提供默认的构造函数。
public class HelloCommand extends HystrixCommand<String> {
private RestTemplate restTemplate;
public HelloCommand(Setter setter, RestTemplate restTemplate){
super(setter);
this.restTemplate = restTemplate;
}
@Override
protected String run() throws Exception {
return restTemplate.getForEntity("http://HELLO-SERVICE/provider/hello", String.class).getBody();
}
@Override
protected String getFallback() {
return "我是备用值哦";
}
}
基于上面的 HelloCommand
, 我们可以选择同步或者异步的方式来执行。
@GetMapping(value="/ribbon-consumer1")
public String helloConsumer1(){
// 同步执行
HelloCommand helloCommand = new HelloCommand(HystrixCommand.Setter.withGroupKey(()->"hello"), restTemplate);
return helloCommand.execute();
}
@GetMapping(value="/ribbon-consumer2")
public String helloConsumer2() throws ExecutionException, InterruptedException {
HelloCommand helloCommand = new HelloCommand(HystrixCommand.Setter.withGroupKey(()->"hello"), restTemplate);
// 异步执行
Future<String> result = helloCommand.queue();
return result.get();
}
还有一种使用 observe()
方法的方式,我并不太清楚这种用法是否正确,因为它是 rxjava ,我不太了解。按下面代码的运行情况来看它可以是异步的。
@GetMapping(value="/ribbon-consumer3")
public String helloConsumer3() throws ExecutionException, InterruptedException {
HelloCommand helloCommand = new HelloCommand(HystrixCommand.Setter.withGroupKey(()->"hello"), restTemplate);
// observe 方法测试
Observable<String> observe = helloCommand.observe();
final String[] hello = new String[1];
observe.subscribe(new Observer<String>() {
@Override
public void onCompleted() {
System.out.println("Observer onCompleted 方法执行");
}
@Override
public void onError(Throwable throwable) {
System.out.println("我看起来发生了错误");
throwable.printStackTrace();
}
@Override
public void onNext(String s) {
hello[0] = s;
System.out.println("我收到了一个字符串:" + s);
}
});
// 这里总是返回 null,但控制台会打印上述输出语句
return hello[0];
}
注意:创建的 HystrixCommand
实例只能使用一次。
1.2 基于注解
@HystrixCommand
能够更加优雅的实现 Hystrix 命令的定义。
// 同步
@HystrixCommand(fallbackMethod = "helloFallback")
public String hello(){
return restTemplate.getForEntity("http://HELLO-SERVICE/provider/hello", String.class).getBody();
}
// 异步 需要调用返回对象的 get() 方法获取结果
@HystrixCommand(fallbackMethod = "helloFallback")
public Future<String> helloAsync(){
return new AsyncResult<String>() {
@Override
public String invoke() {
return restTemplate.getForEntity("http://HELLO-SERVICE/provider/hello", String.class).getBody();
}
};
}
private String helloFallback(){
return "访问出错";
}
该注解只能作用于方法上,观察其属性值,发现和继承方式实现的父类的构造函数差不多。
2. 定义服务降级
fallback
是 Hystrix 命令执行失败后使用的备用方法,用来实现服务降级。在基于继承的实现中,可以重写 getFallback
方法,或者重写 getFallbackMethodName
方法来指定想要的方法作为备用方法。
基于注解的实现,只需要在 fallbackMethod
参数来指定具体的服务降级实现方法。当然,我们的备用方法,仍然可能失败,我们依然可以为它实现 hystrix 命令,形成服务降级的级联。
3. 异常处理
除了 HystrixBadRequestException
异常之外,其它异常均会被 Hystrix 认为命令执行失败,并触发服务降级的处理逻辑。HystrixBadRequestException
异常的抛出无法执行服务降级逻辑,会造成服务端异常栈信息打印,客户端收到错误。
在基于注解的实现时,我们可以指定需要被忽略的异常类型,只需要设置 ignoreExceptions
参数,当抛出了指定的异常时,会被包装为 HystrixBadRequestException
抛出,这样就不会执行后续的服务降级。
3.1 异常获取
如果是继承的方式,我们可以使用如下的这种方法:
@Override
protected String run() throws Exception {
String s = null ;
s.intern();
return restTemplate.getForEntity("http://HELLO-SERVICE/provider/hello", String.class).getBody();
}
@Override
protected String getFallback() {
Throwable executionException = getExecutionException();
return "我是备用值哦";
}
@Override
public Throwable getExecutionException() {
return super.getExecutionException();
}
如果是基于注解的话,只需要为其指定的 fallback 方法,设置简单的参数即可:
@HystrixCommand(fallbackMethod = "helloFallback")
public String hello(){
String s = null ;
s.intern();
return restTemplate.getForEntity("http://HELLO-SERVICE/provider/hello", String.class).getBody();
}
private String helloFallback(Throwable e){
e.printStackTrace();
return "访问出错";
}
4. 命令名称、分组以及线程池划分
Hystrix 会根据命令组来组织和统计命令的告警、仪表盘等信息。并且默认的线程划分也是根据命令组来实现的。默认情况下,Hystrix 会让相同组名的命令使用同一个线程池。初次之外,Hystrix 提供了更加灵活的线程池划分;通过 HystrixThreadPoolKey 来对线程池进行设置。
在基于继承的实现中,通过构造函数即可完成上述参数的指定。
在基于注解的实现中,也有相应的属性,为其设置值即可。
5. 请求缓存
这里有个非常重要的点:“请求缓存是在同一请求多次访问中保证只调用一次这个服务提供者的接口,在这同一次请求第一次的结果会被缓存,保证同一请求中同样的多次访问返回结果相同。”
5.1 基于继承
Hystrix 提供了请求缓存的功能,在之前基于继承的实现中,我们只需要重载 getCacheKey 方法。
@Override
protected String getCacheKey() {
// 在同一http请求中,每次都返回缓存值,实际使用中,应根据方法参数来构造
return "all";
}
但在这样的实现中,我得到了一个错误:“Request caching is not available. Maybe you need to initialize the HystrixRequestContext?”,查看 HystrixRequestContext
,原来我们需要初始化它,那么参照文档,直接用 filter 初始化即可:
package com.duofei.filter;
import com.netflix.hystrix.strategy.concurrency.HystrixRequestContext;
import org.springframework.context.annotation.Configuration;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;
/**
* 缓存上下文初始化
* @author duofei
* @date 2019/10/26
*/
@Configuration
@WebFilter(urlPatterns = "/**")
public class RequestCacheContextFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("我Filter初始化了");
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HystrixRequestContext hystrixRequestContext = HystrixRequestContext.initializeContext();
try {
filterChain.doFilter(servletRequest, servletResponse);
}finally {
hystrixRequestContext.shutdown();
}
}
}
注意:基于 @WebFilter 注解配置 filter 时,@Configuration 注解需要有。
那么,如何清理缓存呢?
private static final HystrixCommandKey commandKey = HystrixCommandKey.Factory.asKey("helloCommandKey");
/**
* 清理缓存
* 开启请求缓存之后,我们在读的过程中没有问题,但是我们如果是写,那么我们继续读之前的缓存了
* 我们需要把之前的cache清掉
* 说明 : 1.其中getInstance方法中的第一个参数的key名称要与实际相同
* 2.clear方法中的cacheKey要与getCacheKey方法生成的key方法相同
* 3.注意我们用了commandKey是helloCommandKey,大家要注意之后new这个Command的时候要指定相同的commandKey,否则会清除不成功
*/
public static void flushRequestCache(){
HystrixRequestCache.getInstance(commandKey
, HystrixConcurrencyStrategyDefault.getInstance())
.clear("all");
}
需要注意:如果使用了 setter 的方式来构造 HystrixCommand , 那么上述定义的 commandKey 需要通过 setter 方法写入,不能通过重写 getCommandKey
的方式。
测试代码如下:
// 同步执行
HelloCommand helloCommand = new HelloCommand(HystrixCommand.Setter.withGroupKey(()->"hello"), restTemplate);
// 测试缓存
new HelloCommand(HystrixCommand.Setter.withGroupKey(()->"hello"), restTemplate).execute();
// 清除缓存
HelloCommand.flushRequestCache();
new HelloCommand(HystrixCommand.Setter.withGroupKey(()->"hello"), restTemplate).execute();
return helloCommand.execute();
最终,会执行两次 run 方法中的打印语句(观察控制台即可)。
5.2 基于缓存
提供了三个专用于请求缓存的注解:
@CacheResult
: 标记请求命令返回的结果应该被缓存,必须与@HystrixCommand
注解结合使用,它的缓存 key值会使用方法的所有参数。@CacheRemove
: 该注解用来让请求命令的缓存失效,失效的缓存根据定义的 key 决定。@CacheKey
: 该注解用来在请求命令的参数上标记,使其作为缓存的 key 值,如果没有标注则会使用所有参数。如果同时还使用了@CacheResult
和@CacheRemove
注解的cacheKeyMethod
方法指定缓存 Key 的生成,那么该注解不会生效。