一.feign与hystix结合

1.1测试环境搭建

架构如图:

非常简单,就是Order服务通过feign调用product服务的一个获取商品信息的一个接口:

package com.yang.xiao.hui.order.controller;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.context.annotation.Primary;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping; @FeignClient(name ="product",fallback =ProductHystrix.class)
@Primary
public interface ProductService {
@RequestMapping("/info/{id}")
Product getProductInfo(@PathVariable("id") Integer id);
}

@Component
public class ProductHystrix implements ProductService{
@Override
public Product getProductInfo(Integer id) {
System.out.println("被熔断;了");
Product product = new Product();
product.setName("熔断了。。。。。"); return product;
}
}

我们在order服务引入feign的依赖:

     <dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

通过下面的依赖关系图可见,已经引入了hystrix相关依赖:

根据FeignClientsConfiguration这个配置类的一个方法feignHystrixBuilder(),可以知道,要让feign跟hystrix结合,需要在application.yml配置一个feign.hystrix.enabled =true的属性

1.2 debug调试feignClient客户端的创建:

.根据上次feign的调用源码分析,可以知道,带有@FeignClient的接口都会被封装成FeignClientFactoryBean类,通过该类的getObject()方法可以获取对应的代理对象,具体源码分析,参考https://www.cnblogs.com/yangxiaohui227/p/12965340.html

断点打在FeignClientFactoryBean的getObject()方法,然后启动Order服务:(因为我的order服务的Controller注入ProductSerivce,所以启动时,会调用FeignClientFactoryBean的getObject()获取实例)

@Controller
public class OrderContorller {
@Autowired
private ProductService productService; @ResponseBody
@RequestMapping("/info/{id}")
@MyLogAnnotation
public Product getProductInfo(@PathVariable Integer id){
return productService.getProductInfo(id);
}
}

上面先调用build,在调用newInstance来创建代理对象,先看build方法:

至此代理对象创建完毕,我们看到了方法处理器是:HystrixInvocationHandler

1.3  debug调试feign调用第三方接口:

在浏览器输入http://localhost:8674/info/3,调用order服务的getProductInfo方法:

debug跟踪进去:

可见,最终是进入了HystrixInvocationHandler类的invoke方法: 方法具体如下:

 @Override
public Object invoke(final Object proxy, final Method method, final Object[] args)
throws Throwable {
// early exit if the invoked method is from java.lang.Object
// code is the same as ReflectiveFeign.FeignInvocationHandler
if ("equals".equals(method.getName())) {
try {
Object otherHandler =
args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null;
return equals(otherHandler);
} catch (IllegalArgumentException e) {
return false;
}
} else if ("hashCode".equals(method.getName())) {
return hashCode();
} else if ("toString".equals(method.getName())) {
return toString();
}

//这里创建一个command命令对象,里面实现了2个方法,一个是执行我们目标方法的run()方法,一个是执行目标方法失败后,调用我们的服务降级方法getFallcack()
HystrixCommand<Object> hystrixCommand =
new HystrixCommand<Object>(setterMethodMap.get(method)) {
@Override
protected Object run() throws Exception {
try {
return HystrixInvocationHandler.this.dispatch.get(method).invoke(args);
} catch (Exception e) {
throw e;
} catch (Throwable t) {
throw (Error) t;
}
} @Override
protected Object getFallback() {
if (fallbackFactory == null) {
return super.getFallback();
}
try {
Object fallback = fallbackFactory.create(getExecutionException());
Object result = fallbackMethodMap.get(method).invoke(fallback, args);
if (isReturnsHystrixCommand(method)) {
return ((HystrixCommand) result).execute();
} else if (isReturnsObservable(method)) {
// Create a cold Observable
return ((Observable) result).toBlocking().first();
} else if (isReturnsSingle(method)) {
// Create a cold Observable as a Single
return ((Single) result).toObservable().toBlocking().first();
} else if (isReturnsCompletable(method)) {
((Completable) result).await();
return null;
} else if (isReturnsCompletableFuture(method)) {
return ((Future) result).get();
} else {
return result;
}
} catch (IllegalAccessException e) {
// shouldn't happen as method is public due to being an interface
throw new AssertionError(e);
} catch (InvocationTargetException | ExecutionException e) {
// Exceptions on fallback are tossed by Hystrix
throw new AssertionError(e.getCause());
} catch (InterruptedException e) {
// Exceptions on fallback are tossed by Hystrix
Thread.currentThread().interrupt();
throw new AssertionError(e.getCause());
}
}
}; if (Util.isDefault(method)) {
return hystrixCommand.execute();
} else if (isReturnsHystrixCommand(method)) {
return hystrixCommand;
} else if (isReturnsObservable(method)) {
// Create a cold Observable
return hystrixCommand.toObservable();
} else if (isReturnsSingle(method)) {
// Create a cold Observable as a Single
return hystrixCommand.toObservable().toSingle();
} else if (isReturnsCompletable(method)) {
return hystrixCommand.toObservable().toCompletable();
} else if (isReturnsCompletableFuture(method)) {
return new ObservableCompletableFuture<>(hystrixCommand);
}
return hystrixCommand.execute(); //最终调用了命令行的execute方法
}

小结:这里创建一个command命令对象,里面实现了2个方法,一个是执行我们目标方法的run()方法,一个是执行目标方法失败后,调用我们的服务降级方法getFallcack(),直接调用了命令对象的execute方法:1.4 我们跟进命令对象的创建看看有哪些属性:

这里一大堆初始化属性,我们主要关注2个:HystrixCommandProperties.Setter 和 HystrixThreadPoolProperties.Setter,以及线程池的初始化

我们看到线程池的初始化,有个threadPoolkey,该值为服务的名称,也就是@FeignClient注解的name 值,初始化时,同时传入了一个线程池的默认配置:

那么我们先看HystrixThreadPoolProperties.Setter的属性有哪些值:

线程池属性初始化前缀是hystrix,通过上面我们可以知道,Hystrix有默认的线程参数,超过对应的线程参数就会被拒绝,那么我们就可以用该特征来做服务限流了,真对某个方法给其设置相应的最大队列属性;

直接我们查看HystrixCommandProperties的属性值:

命令创建分析完后,我们跟踪执行逻辑:hystrixCommand.execute();

这里用到了大量的RXJava相关知识,从上面代码可以知道,该方法是阻塞的:

跟进toObservable(),我们找到核心代码:

private Observable<R> applyHystrixSemantics(final AbstractCommand<R> _cmd) {
// mark that we're starting execution on the ExecutionHook
// if this hook throws an exception, then a fast-fail occurs with no fallback. No state is left inconsistent
executionHook.onStart(_cmd); /* determine if we're allowed to execute */
if (circuitBreaker.allowRequest()) { //这里判断是否允许执行目标方法,如果不允许,那就执行fallbcak方法
final TryableSemaphore executionSemaphore = getExecutionSemaphore();
final AtomicBoolean semaphoreHasBeenReleased = new AtomicBoolean(false);
final Action0 singleSemaphoreRelease = new Action0() {
@Override
public void call() {
if (semaphoreHasBeenReleased.compareAndSet(false, true)) {
executionSemaphore.release();
}
}
}; final Action1<Throwable> markExceptionThrown = new Action1<Throwable>() {
@Override
public void call(Throwable t) {
eventNotifier.markEvent(HystrixEventType.EXCEPTION_THROWN, commandKey);
}
}; if (executionSemaphore.tryAcquire()) {
try {
/* used to track userThreadExecutionTime */
executionResult = executionResult.setInvocationStartTime(System.currentTimeMillis());
return executeCommandAndObserve(_cmd)//这里是执行目标方法
.doOnError(markExceptionThrown)
.doOnTerminate(singleSemaphoreRelease)
.doOnUnsubscribe(singleSemaphoreRelease);
} catch (RuntimeException e) {
return Observable.error(e);
}
} else {
return handleSemaphoreRejectionViaFallback();//处理fallback方法
}
} else {
return handleShortCircuitViaFallback();//处理fallback方法
}
}

重点三个方法:circuitBreaker.allowRequest()   /executeCommandAndObserve(_cmd)  / handleSemaphoreRejectionViaFallback()

先看熔断器核心业务方法:circuitBreaker.allowRequest()

我们看看isOpen()方法的逻辑:

       @Override
public boolean isOpen() {
if (circuitOpen.get()) { //如果已经是打开的,那就返回true
// if we're open we immediately return true and don't bother attempting to 'close' ourself as that is left to allowSingleTest and a subsequent successful test to close
return true;
} // we're closed, so let's see if errors have made us so we should trip the circuit open
HealthCounts health = metrics.getHealthCounts(); //获取统计对象,每个command对象的key值就会有一个统计对象,也就是每个请求方法对应一个对象 // check if we are past the statisticalWindowVolumeThreshold
if (health.getTotalRequests() < properties.circuitBreakerRequestVolumeThreshold().get()) { //在一个时间窗口内,默认10s,请求总次数要达到配置的最小请求数(默认20次)
// we are not past the minimum volume threshold for the statisticalWindow so we'll return false immediately and not calculate anything
return false;
} if (health.getErrorPercentage() < properties.circuitBreakerErrorThresholdPercentage().get()) { /在一个时间窗口内,默认10s,请求失败率要达到最小配置百分比(默认50%)
return false;
} else {
// our failure rate is too high, trip the circuit //执行到这里说明失败率过高
if (circuitOpen.compareAndSet(false, true)) {
// if the previousValue was false then we want to set the currentTime //即使之前是关闭状态也要将其改为打开状态
circuitOpenedOrLastTestedTime.set(System.currentTimeMillis());//设置最近打开时间,用于半打开状态的设置
return true;
} else {
// How could previousValue be true? If another thread was going through this code at the same time a race-condition could have
// caused another thread to set it to true already even though we were in the process of doing the same
// In this case, we know the circuit is open, so let the other thread set the currentTime and report back that the circuit is open
return true;
}
}
} }

小结:isOpen()在默认情况下,如果10s内请求次数达到20次,失败率达到百分之50,则熔断器将会打开

接着我们分析半打开方法:allowSingleTest();

public boolean allowSingleTest() {
long timeCircuitOpenedOrWasLastTested = circuitOpenedOrLastTestedTime.get(); //上次最近打开熔断器的时间
// 1) if the circuit is open
// 2) and it's been longer than 'sleepWindow' since we opened the circuit
           //熔断器如果是打开状态,并且最近打开时间距离当前时间已经超过了设置的时间(默认5s)
           if (circuitOpen.get() && System.currentTimeMillis() > timeCircuitOpenedOrWasLastTested + properties.circuitBreakerSleepWindowInMilliseconds().get()) {
// We push the 'circuitOpenedTime' ahead by 'sleepWindow' since we have allowed one request to try.
// If it succeeds the circuit will be closed, otherwise another singleTest will be allowed at the end of the 'sleepWindow'.
if (circuitOpenedOrLastTestedTime.compareAndSet(timeCircuitOpenedOrWasLastTested, System.currentTimeMillis())) { //更新最近打开时间,方便下一个5s进行判断
// if this returns true that means we set the time so we'll return true to allow the singleTest
// if it returned false it means another thread raced us and allowed the singleTest before we did
return true;
}
}
return false;
}

一张图总结:

至此,熔断器核心逻辑分析完毕了,我们回到之前目标方法的逻辑,继续跟踪executeCommandAndObserve(_cmd);

前面我们分析过,创建Command对象时,实现了run方法,和getFallback()方法,上图可见,run方法被调用了,而run方法里面有我们的目标方法

接着我们分析执行目标方法失败后,是如何调用getFallback方法的: handleSemaphoreRejectionViaFallback():

由此可见,这里最终调用了Command对象的getFallback()方法;

执行失败的原因有:执行异常,超时,熔断器打开,线程数超过最大限制,请求异常

1.4  如何修改默认的参数配置:如,我想更改线程池的配置,或者滑动窗口时间,@FeignClient注解里面并没有更改hystrix的相关配置,那么我们回顾下ProductService接口的创建过程:

我们创建命令对象时:

由此可知,我们创建的HystrixCommand时,要用到一个属性配置对象,该对象其实就是创建代理对象时,由HystrixFeign提供,但该属性是直接new出来的,所以我们唯有替换掉HystrixFeign

我们发现,容器中没有HystrixFeign.Builder对象时就才会创建Feign.Builder,那么我们就给他注入一股Feign.Builder,在里面给它设置setterFactory属性:

package com.yang.xiao.hui.order.controller;

import com.netflix.hystrix.HystrixCommand;
import com.netflix.hystrix.HystrixCommandGroupKey;
import com.netflix.hystrix.HystrixCommandKey;
import com.netflix.hystrix.HystrixThreadPoolProperties;
import feign.Feign;
import feign.Target;
import feign.hystrix.HystrixFeign;
import feign.hystrix.SetterFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope; import java.lang.reflect.Method; @Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ HystrixCommand.class, HystrixFeign.class })
public class FeignConfig { @Bean
@Scope("prototype")
@ConditionalOnProperty(name = "feign.hystrix.enabled")
public Feign.Builder feignHystrixBuilder() {
HystrixFeign.Builder builder = HystrixFeign.builder();
SetterFactory setterFactory= new SetterFactory(){ @Override
public HystrixCommand.Setter create(Target<?> target, Method method) {
String groupKey = target.name();
String commandKey = Feign.configKey(target.type(), method);
return HystrixCommand.Setter
.withGroupKey(HystrixCommandGroupKey.Factory.asKey(groupKey))
.andCommandKey(HystrixCommandKey.Factory.asKey(commandKey))
.andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter().withCoreSize(100).withMaxQueueSize(200)); }
};
builder.setterFactory(setterFactory); return builder;
}
}

//需要更改任何属性就在HystrixCommand.Setter对象加,我们这里更改了线程池的核心线程数为100,最大队列数为200,启动order服务,重新调用方法:

已经变更了;

思考: 上面的配置是对所有的服务都起效的,如果我们需要对不同的服务,如库存服务,客户服务,等有不同的配置,如何处理:

方案一,在主启动类中,将上面的配置类的扫描排除,然后在@FeignClient注解中添加该配置,原理在feign源码里说过,在创建feign对象时,如果子容器不存在某个bean,会到父,容器中找;那么我们的思路是让父容器扫描不到:

此时再次测试:

方案二:在配置类那里根据不同服务返回不同设置,甚至可以根据不同方法返回不同设置,因为Feign.Builder对象是多例的:

package com.yang.xiao.hui.order.controller;

import com.netflix.hystrix.HystrixCommand;
import com.netflix.hystrix.HystrixCommandGroupKey;
import com.netflix.hystrix.HystrixCommandKey;
import com.netflix.hystrix.HystrixThreadPoolProperties;
import feign.Feign;
import feign.Target;
import feign.hystrix.HystrixFeign;
import feign.hystrix.SetterFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope; import java.lang.reflect.Method; @Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ HystrixCommand.class, HystrixFeign.class })
public class FeignConfig { @Bean
@Scope("prototype")
@ConditionalOnProperty(name = "feign.hystrix.enabled")
public Feign.Builder feignHystrixBuilder() {
HystrixFeign.Builder builder = HystrixFeign.builder();
SetterFactory setterFactory= new SetterFactory(){ @Override
public HystrixCommand.Setter create(Target<?> target, Method method) {
String groupKey = target.name();
String commandKey = Feign.configKey(target.type(), method);
HystrixThreadPoolProperties.Setter setter = HystrixThreadPoolProperties.Setter().withCoreSize(100).withMaxQueueSize(200);
if("product".equals(groupKey)){ //如果是商品服务就用下面配置
setter=HystrixThreadPoolProperties.Setter().withCoreSize(50).withMaxQueueSize(50);
}
return HystrixCommand.Setter
.withGroupKey(HystrixCommandGroupKey.Factory.asKey(groupKey))
.andCommandKey(HystrixCommandKey.Factory.asKey(commandKey))
.andThreadPoolPropertiesDefaults(setter); }
};
builder.setterFactory(setterFactory); return builder;
}
}

二.hystix用于controller层

前面分析的是feign与hystrix结合的源码,hystix还可以用于controller层:

2.1 添加依赖:

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

2.2 修改controller

2.3.主启动类添加@EnableHystrix或@EnableCircuitBreaker  @EnableHystrix注解里面包含了@EnableCircuitBreaker所以他们功能一致

2.4 源码分析:
从EnableHystrix注解开始:

根据上面分析,springboot会加载类路径下,META-INF/spring.factories文件,找到key为org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker的类

由此可见org.springframework.cloud.netflix.hystrix.HystrixCircuitBreakerConfiguration这个是我们要找的配置类

跟踪到此,我们看到了最终会注入一个切面类HystrixCommandAspect,我们根进看看

调用contorller方法,debug跟踪:

进入create方法:

继续跟进HystrixCommandFactory.getInstance().create(metaHolder);

由此可见,最终也是将一个方法封装成了一个Command对象,跟前面分析feign的调用逻辑基本一致了

至此,调用逻辑跟之前feign结合hystrix时已经一致了,因此不再重复分分析

既然controller可以使用hystrix,如果feign调用过程也使用了hystirx,那么我们可以通过在application.yml配置文件中设置feign.hystrix.enabled=false,保留controller层的hystrix逻辑

总结: Hystix 可以实现熔断,服务降级,限流三大功能;

1.熔断的理解:就是前面判断断路器是否开还是关,跟保险丝作用一样

2. 服务降级的理解: 就是服务熔断后,执行fallback方法,这就是服务降级了

3. 限流的理解:就是里面的线程池最大队列数

=

hystrix 源码分析以及属性的配置的更多相关文章

  1. Spring IOC 容器源码分析 - 填充属性到 bean 原始对象

    1. 简介 本篇文章,我们来一起了解一下 Spring 是如何将配置文件中的属性值填充到 bean 对象中的.我在前面几篇文章中介绍过 Spring 创建 bean 的流程,即 Spring 先通过反 ...

  2. 使用react全家桶制作博客后台管理系统 网站PWA升级 移动端常见问题处理 循序渐进学.Net Core Web Api开发系列【4】:前端访问WebApi [Abp 源码分析]四、模块配置 [Abp 源码分析]三、依赖注入

    使用react全家桶制作博客后台管理系统   前面的话 笔者在做一个完整的博客上线项目,包括前台.后台.后端接口和服务器配置.本文将详细介绍使用react全家桶制作的博客后台管理系统 概述 该项目是基 ...

  3. mybatis源码分析--如何加载配置及初始化

    简介 Mybatis 是一个持久层框架,它对 JDBC 进行了高级封装,使我们的代码中不会出现任何的 JDBC 代码,另外,它还通过 xml 或注解的方式将 sql 从 DAO/Repository ...

  4. 5.2 spring5源码--spring AOP源码分析二--切面的配置方式

    目标: 1. 什么是AOP, 什么是AspectJ 2. 什么是Spring AOP 3. Spring AOP注解版实现原理 4. Spring AOP切面原理解析 一. 认识AOP及其使用 详见博 ...

  5. 【源码分析】FastJson全局配置日期格式导致@JSONField(format = "yyyy-MM-dd")注解失效

    出现的问题 我全局配置的时间格式是:yyyy-MM-dd HH:mm:ss @JSONField注解配置的时间格式是:yyyy-MM-dd 最终的返回结果是:yyyy-MM-dd HH:mm:ss 问 ...

  6. Spring源码分析-从@ComponentScan注解配置包扫描路径到IoC容器中的BeanDefinition,经历了什么(一)?

    阅前提醒 全文较长,建议沉下心来慢慢阅读,最好是打开Idea,点开Spring源码,跟着下文一步一步阅读,更加便于理解.由于笔者水平优先,编写时间仓促,文中难免会出现一些错误或者不准确的地方,恳请各位 ...

  7. [dev][ipsec][dpdk] strongswan/dpdk源码分析之ipsec算法配置过程

    1 简述 storngswan的配置里用一种固定格式的字符串设置了用于协商的预定义算法.在包协商过程中strongswan将字符串转换为固定的枚举值封在数据包里用于传输. 协商成功之后,这组被协商选中 ...

  8. dubbo源码分析7——dubbo的配置解析_与spring的整合

    dubbo的配置其实就是建立在spring的命名空间的配置机制之上的.在dubbo的jar包的META-INF目录下会有spring.handlers这个文件,用来配置spring的命名空间和解析类的 ...

  9. STL源码分析之第二级配置器

    前言 第一级是直接调用malloc分配空间, 调用free释放空间, 第二级三就是建立一个内存池, 小于128字节的申请都直接在内存池申请, 不直接调用malloc和free. 本节分析第二级空间配置 ...

随机推荐

  1. 学习一下 JVM (二) -- 学习一下 JVM 中对象、String 相关知识

    一.JDK 8 版本下 JVM 对象的分配.布局.访问(简单了解下) 1.对象的创建过程 (1)前言 Java 是一门面向对象的编程语言,程序运行过程中在任意时刻都可能有对象被创建.开发中常用 new ...

  2. 区块链入门到实战(21)之以太坊(Ethereum) – 分布式应用(DApp)

    作用:用户交互 分布式应用(DApp)是运行在区块链之上的应用程序,支持区块链网络中用户之间的交互. DApp(decentralized application)的后端代码运行在区块链网络上,这个可 ...

  3. Java BigDecimal使用指南

    提起BigDecimal,相信大家都使用过,之所以总结这篇呢,是因为最近发现项目中使用的不是太规范,在某些场景下甚至出现代码抛出异常的情况, 所以就总结了这篇,希望大家在使用时,可以少踩一些坑. 1. ...

  4. Alink漫谈(二十) :卡方检验源码解析

    Alink漫谈(二十) :卡方检验源码解析 目录 Alink漫谈(二十) :卡方检验源码解析 0x00 摘要 0x01 背景概念 1.1 假设检验 1.2 H0和H1是什么? 1.3 P值 (P-va ...

  5. e3mall商城的归纳总结3之后台商品节点、认识nginx

    一  后台商品节点 大家都知道后台创建商品的时候需要选择商品的分类,而这个商品的分类就就像一棵树一样,一层包含一层又包含一层.因此这里用的框架是easyUiTree.该分类前端使用的是异步加载模式(指 ...

  6. 英文ubuntu中的乱码,输入法问题 、mint字体发虚

    英文ubuntu文本文件默认编码是utf-8,windows下是gbk,所以产生乱码问题. 1.前言 运行命令查看系统编码 $locale 结果如下: LANG=en_US.UTF-8 LANGUAG ...

  7. CVPR2020 面向密集多角度物体检测的动态修正网络(DRN)

    论文链接:https://arxiv.org/pdf/2005.09973.pdf code:https://github.com/Anymake/DRN_CVPR2020 文章概要: 本文是中科院自 ...

  8. [BUUOJ记录] [ACTF2020 新生赛]Include

    本题主要考查了利用php://filter伪协议进行文件包含 进入题目根据Tip进入正题,可以看到URL中存在文件包含(题目名也很直接) 首先考虑 "php://input"伪协议 ...

  9. 17_Python的常用模块

    1.随机数模块 random 1.随机小数 import random # (0,1)随机取浮点数 random.random() # 0.17988578778011 # (1, 3)取指定范围的浮 ...

  10. Python 零基础快速入门!

    “人生苦短,我学python”是编程届的名言.用python写小脚本的便捷性,让很多其他语言的学习者把python当作辅助语言.拥有了某一个语言的功底,再来学习另外一种语言应该是十分快速的.编程理念都 ...