在spring cloud体系项目中,引入的重试机制保证了高可用的同时,也会带来一些其它的问题,如幂等操作或一些没必要的重试。
今天就来分别分析一下 FeignClient 和 Ribbon 重试机制的实现原理和区别,主要分为三点:

1)FeignClient重试机制分析
2)Ribbon重试机制分析
3)FeignClient和Ribbon重试机制的区别于联系

1)FeignClient 重试机制分析:

FeignClient 重试机制的实现原理相对简单。首先看一下feignClient处理请求的拦截类:SynchronousMethodHandler,看一下该类中的代理方法invoke

 @Override
public Object invoke(Object[] argv) throws Throwable {
//生成处理请求模板
RequestTemplate template = buildTemplateFromArgs.create(argv);
//获取重试配置类
Retryer retryer = this.retryer.clone();
while (true) {
try {
return executeAndDecode(template);
} catch (RetryableException e) {
//在异常里执行是否重试方法
retryer.continueOrPropagate(e);
if (logLevel != Logger.Level.NONE) {
logger.logRetry(metadata.configKey(), logLevel);
}
continue;
}
}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

上面的默认重试配置Retryer,在其构造方法中,默认的请求次数为5次,如下:

 public Default() {
this(100, SECONDS.toMillis(1), 5);
}
  • 1
  • 2
  • 3
  • 4

判断是否重试的算法如下:

public void continueOrPropagate(RetryableException e) {
//重试次数大于最大请求次数,抛出异常
if (attempt++ >= maxAttempts) {
throw e;
} long interval;
if (e.retryAfter() != null) {
interval = e.retryAfter().getTime() - currentTimeMillis();
if (interval > maxPeriod) {
interval = maxPeriod;
}
if (interval < 0) {
return;
}
} else {
interval = nextMaxInterval();
}
try {
Thread.sleep(interval);
} catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
}
sleptForMillis += interval;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

如果要关闭或者要重写 feignClient重试机制 的话,可以自定义feignRetryer,在方法中不做重试,直接抛出异常。配置如下:

/**
* @author zhangshukang
*/
@Configuration
public class FeignConfig {
@Bean
Retryer feignRetryer() {
return new Retryer() {
@Override
//在这里重写 continueOrPropagate算法,可自定义处理方式。这里直接抛出异常,相当于不重试。
public void continueOrPropagate(RetryableException e) {
throw e;
}
@Override
public Retryer clone() {
return this;
}
};
}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

2)Ribbon重试机制分析:

首先看一下我们ribbon常用的配置,已经配置用到的地方:

ribbon:
ReadTimeout: 0
ConnectTimeout: 10
MaxAutoRetries: 1
MaxAutoRetriesNextServer: 2
OkToRetryOnAllOperations: false
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

这里从字面意思可以看出:
retrySameServer:重试相同实例,对应MaxAutoRetries
retryNextServer:重试下一实例,对应MaxAutoRetriesNextServer
retryEnabled:重试所有操作,对应OkToRetryOnAllOperations

这里声明一点,关于feignClient如何整合ribbon负载均衡的,之前的博客已经有完整的分析:
《SpringCloud | Feign如何整合Ribbon进行负载均衡的?》,所以下面就跳过整合部分,直接分析负载均衡模块。

public T executeWithLoadBalancer(final S request, final IClientConfig requestConfig) throws ClientException {
//获取重试机制配置:RequestSpecificRetryHandler,继续跟进该方法...
RequestSpecificRetryHandler handler = getRequestSpecificRetryHandler(request, requestConfig);
//这里很关键,很明显采用了命令模式,ribbon负载均衡的配置在这里传给LoadBalancerCommand类
LoadBalancerCommand<T> command = LoadBalancerCommand.<T>builder()
.withLoadBalancerContext(this)
.withRetryHandler(handler)
.withLoadBalancerURI(request.getUri())
.build(); try {
return command.submit(
new ServerOperation<T>() {
@Override
public Observable<T> call(Server server) {
URI finalUri = reconstructURIWithServer(server, request.getUri());
S requestForServer = (S) request.replaceUri(finalUri);
try {
return Observable.just(AbstractLoadBalancerAwareClient.this.execute(requestForServer, requestConfig));
}
catch (Exception e) {
return Observable.error(e);
}
}
})
.toBlocking()
.single();
} catch (Exception e) {
Throwable t = e.getCause();
if (t instanceof ClientException) {
throw (ClientException) t;
} else {
throw new ClientException(e);
}
} }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
    @Override
public RequestSpecificRetryHandler getRequestSpecificRetryHandler(
RibbonRequest request, IClientConfig requestConfig) {
//这里如果配置了OkToRetryOnAllOperations为true,则所有的请求都进行重试。默认为false
if (this.clientConfig.get(CommonClientConfigKey.OkToRetryOnAllOperations,
false)) {
return new RequestSpecificRetryHandler(true, true, this.getRetryHandler(),
requestConfig);
}
//如果没配置的话,如果不是get请求,就关闭重试
if (!request.toRequest().method().equals("GET")) {
return new RequestSpecificRetryHandler(true, false, this.getRetryHandler(),
requestConfig);
}
else {
//如果是get请求,则开启重试。
return new RequestSpecificRetryHandler(true, true, this.getRetryHandler(),
requestConfig);
}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

上述代码是对请求类型进行区分,哪些重试,哪些不重试。
区别就在于第二个参数,来看一下第二个参数具体哪里用到了,继续跟进代码如下:

    public boolean isRetriableException(Throwable e, boolean sameServer) {
//如果手动配置了所有请求都重试,或者get请求时,这里开启重试。
if(this.okToRetryOnAllErrors) {
return true;
} else if(e instanceof ClientException) {
ClientException ce = (ClientException)e;
return ce.getErrorType() == ErrorType.SERVER_THROTTLED?!sameServer:false;
} else {
return this.okToRetryOnConnectErrors && this.isConnectionException(e);
}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

刚刚上面提到了命令模式,属于RxJava的内容,事件驱动机制,有兴趣的可以自行研读。这里看一下上面命令模式执行类具体怎么用的:

public Observable<T> submit(final ServerOperation<T> operation) {
final ExecutionInfoContext context = new ExecutionInfoContext(); if (listenerInvoker != null) {
try {
listenerInvoker.onExecutionStart();
} catch (AbortExecutionException e) {
return Observable.error(e);
}
} //这两个变量,上面已经提到了,重试机制的关键
final int maxRetrysSame = retryHandler.getMaxRetriesOnSameServer();
final int maxRetrysNext = retryHandler.getMaxRetriesOnNextServer(); // 利用RxJava生成一个Observable用于后面的回调
Observable<T> o =
//选择具体的server进行调用
(server == null ? selectServer() : Observable.just(server))
.concatMap(new Func1<Server, Observable<T>>() {
@Override
// Called for each server being selected
public Observable<T> call(Server server) {
context.setServer(server);
//获取这个server调用监控记录,用于各种统计和LoadBalanceRule的筛选server处理
final ServerStats stats = loadBalancerContext.getServerStats(server); //获取本次server调用的回调入口,用于重试同一实例的重试回调
Observable<T> o = Observable
.just(server)
.concatMap(new Func1<Server, Observable<T>>() {
@Override
public Observable<T> call(final Server server) {
context.incAttemptCount();
loadBalancerContext.noteOpenConnection(stats); if (listenerInvoker != null) {
try {
listenerInvoker.onStartWithServer(context.toExecutionInfo());
} catch (AbortExecutionException e) {
return Observable.error(e);
}
} final Stopwatch tracer = loadBalancerContext.getExecuteTracer().start(); ......省略部分代码 }
});
//设置针对同一实例的重试回调
if (maxRetrysSame > 0)
o = o.retry(retryPolicy(maxRetrysSame, true));
return o;
}
});
//设置重试下一个实例的回调
if (maxRetrysNext > 0 && server == null)
o = o.retry(retryPolicy(maxRetrysNext, false));
//异常回调
return o.onErrorResumeNext(new Func1<Throwable, Observable<T>>() {
@Override
public Observable<T> call(Throwable e) {
if (context.getAttemptCount() > 0) {
if (maxRetrysNext > 0 && context.getServerAttemptCount() == (maxRetrysNext + 1)) {
e = new ClientException(ClientException.ErrorType.NUMBEROF_RETRIES_NEXTSERVER_EXCEEDED,
"Number of retries on next server exceeded max " + maxRetrysNext
+ " retries, while making a call for: " + context.getServer(), e);
}
else if (maxRetrysSame > 0 && context.getAttemptCount() == (maxRetrysSame + 1)) {
e = new ClientException(ClientException.ErrorType.NUMBEROF_RETRIES_EXEEDED,
"Number of retries exceeded max " + maxRetrysSame
+ " retries, while making a call for: " + context.getServer(), e);
}
}
if (listenerInvoker != null) {
listenerInvoker.onExecutionFailed(e, context.toFinalExecutionInfo());
}
return Observable.error(e);
}
});
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82

上述代码典型的RxJava风格。

接下来是关键。o为Observable实例,类似于生产者,上面代码为Observable回调逻辑。上面有两行关键的代码:
o = o.retry(retryPolicy(maxRetrysSame, true));
o = o.retry(retryPolicy(maxRetrysNext, false));

首先看一下 retryPolicy 方法,这个就是 ribbon 重试算法的逻辑了,来看一下的实现:

 private Func2<Integer, Throwable, Boolean> retryPolicy(final int maxRetrys, final boolean same) {
return new Func2<Integer, Throwable, Boolean>() {
@Override
public Boolean call(Integer tryCount, Throwable e) {
if (e instanceof AbortExecutionException) {
return false;
}
//判断是否继续重试
if (tryCount > maxRetrys) {
return false;
} if (e.getCause() != null && e instanceof RuntimeException) {
e = e.getCause();
}
//进入异常处理
return retryHandler.isRetriableException(e, same);
}
};
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

上述代码是Ribbon判断是否重试的实现,根据我们配置的变量次数,进行判断,有异常则进入异常处理。
整体的重试机制就是将 LoadBalancerCommand 类中 retryPolicy 的重试实现逻辑,传入RxJava Observable对象的o.retry()方法,该方法接收的参数的就是一个Function:

public final Observable<T> retry(Func2<Integer, Throwable, Boolean> predicate) {
return nest().lift(new OperatorRetryWithPredicate<T>(predicate));
}
  • 1
  • 2
  • 3

最后回过头看这两行代码,逻辑大致清晰许多,来看一下执行顺序:

o = o.retry(retryPolicy(maxRetrysSame, true));
o = o.retry(retryPolicy(maxRetrysNext, false));
  • 1
  • 2

执行顺序:

1)首先会先执行下面一行代码,获取负载均衡的重试配置,然后进行负载均衡,选取实例。

2)再执行上面一行代码,获取执行单个服务的重试配置,最后再执行具体的业务逻辑。

3)FeignClient 和 Ribbon重试区别与联系:

疑问:一个http请求,如果feign和ribbon都配置了重试机制,异常情况下一共会请求多少次?

经过上面的分析,请求总次数 n 为feignClient和ribbon配置参数的笛卡尔积:

n(请求总次数)=feign(默认5次) * (MaxAutoRetries+1) * (MaxAutoRetriesNextServer+1)

注意:+1是代表ribbon本身默认的请求。

其实二者的重试机制相互独立,并无联系。但是因为用了feign肯定会用到ribbon,所以feign的重试机制相对来说比较鸡肋,自己feignClient的时候一般会关闭该功能。ribbon的重试机制默认配置为0,也就是默认是去除重试机制的,建议不要修改。如果配置不当,会因为幂等请求带来数据问题。所以建议关闭二者的重试功能。
如果开启的话,建议合理配置Hystrix的超时时间,在一些没必要的重试请求执行时,根据Hystrix的超时时间,快速失败,结束重试。

友链:探果网

SpringCloud | FeignClient和Ribbon重试机制区别与联系的更多相关文章

  1. Ribbon重试机制与Hystrix熔断机制的配置问题1

    Ribbon超时与Hystrix超时问题,为了确保Ribbon重试的时候不被熔断,我们就需要让Hystrix的超时时间大于Ribbon的超时时间,否则Hystrix命令超时后,该命令直接熔断,重试机制 ...

  2. Ribbon重试机制与Hystrix熔断机制的配置问题

    Ribbon超时与Hystrix超时问题,为了确保Ribbon重试的时候不被熔断,我们就需要让Hystrix的超时时间大于Ribbon的超时时间,否则Hystrix命令超时后,该命令直接熔断,重试机制 ...

  3. ribbon重试机制

    我们使用Spring Cloud Ribbon实现客户端负载均衡的时候,通常都会利用@LoadBalanced来让RestTemplate具备客户端负载功能,从而实现面向服务名的接口访问. 下面的例子 ...

  4. Spring Cloud学习 之 Spring Cloud Ribbon 重试机制及超时设置不生效

    今天测了一下Ribbon的重试跟超时机制,发现进行的全局超时配置一直不生效,配置如下: ribbon: #单位ms,请求连接的超时时间,默认1000 ConnectTimeout: 500 #单位ms ...

  5. springcloud之Feign、ribbon设置超时时间和重试机制的总结

    一 超时时间配置 如果在一个微服务当中对同一个接口同时配置了Hystrix与ribbon两个超时时间,则在接口调用的时候,两个计时器会同时读秒. 比如,访问一个接口需要2秒,你的ribbon配置的超时 ...

  6. SpringCloud微服务实战——搭建企业级开发框架(十三):OpenFeign+Ribbon实现高可用重试机制

      Spring Cloud OpenFeign 默认是使用Ribbon实现负载均衡和重试机制的,虽然Feign有自己的重试机制,但该功能在Spring Cloud OpenFeign基本用不上,除非 ...

  7. Zuul + Ribbon 脱离Eureka完成负载均衡+重试机制

    Zuul + Ribbon 脱离Eureka完成负载均衡+重试机制 因为没有注册中心,所以需要网关对下游服务做负载均衡,然后果断集成Ribbon.中间遇到很多坑,最后终于解决了. 其实Ribbon里面 ...

  8. springcloud超时重试机制的先后顺序

    https://blog.csdn.net/zzzgd_666/article/details/83314833

  9. Spring Cloud重试机制与各组件的重试总结

    SpringCloud重试机制配置 首先声明一点,这里的重试并不是报错以后的重试,而是负载均衡客户端发现远程请求实例不可到达后,去重试其他实例. ? 1 2 3 4 5 6 7 8 @Bean @Lo ...

随机推荐

  1. ci高级使用方法篇之连接多个数据库

    在我们的项目中有时可能须要连接不止一个数据库.在ci中怎样实现呢? 我们在本地新建了两个数据库,例如以下截图所看到的: 改动配置文件database.php文件为例如以下格式(读者依据自己数据库的情况 ...

  2. 解决 maven 项目启动 提示 class not find

    第一种方法: 项目 --> .classpath <classpathentry exported="true" kind="con" path=& ...

  3. Oracle查询备注信息

    查询表的备注信息: SELECT TABLE_NAME, TABLE_TYPE, COMMENTS FROM USER_TAB_COMMENTS WHERE TABLE_NAME = 'MR_DEPT ...

  4. HTTP协议是如何通信的

    一.什么是HTTP协议 HTTP协议是HyperText Transfer Protocol的缩写,即超文本传输协议.是由w3c(万维网联盟)制定的一种应用层协议,用来定义浏览器与web服务器之间如何 ...

  5. 关于LoadRunner的迭代

    通过用lr做负载压力测试过程发现,如果设定不同的action迭代次数,每次得出的结果是不同的,曲线的表现形式也是不同的.这点就使我们会感觉困惑,为什么要设置action的迭代次数?以及对于不同的应用系 ...

  6. C#:确保绑定到同一数据源的多个控件保持同步

    下面的代码示例演示如何使用 BindingSource 组件,将三个控件(两个文本框控件和一个 DataGridView 控件)绑定到 DataSet 中的同一列.该示例演示如何处理BindingCo ...

  7. 【LeetCode】137. Single Number II (3 solutions)

    Single Number II Given an array of integers, every element appears threetimes except for one. Find t ...

  8. &&和;和||符号的意思

    http://www.cnblogs.com/xuxm2007/archive/2011/01/16/1936836.html在命令行可以一次执行多个命令,有以下几种:   1.每个命令之间用;隔开 ...

  9. 【java设计模式】之 代理(Proxy)模式

    代理模式的核心作用就是通过代理,控制对对象的访问.这跟实际中是一样的,比如说明星都有经纪人,这就是一个代理,比如有人要找某明星拍戏,那么首先处理这事的是他的经纪人,虽然拍戏需要自己拍,但是拍戏前后的一 ...

  10. Maven + Eclipse + Tomcat - 开启项目调试之旅(转载)

    本文的读者需要拥有一些Maven基础知识和实践,如果没有,请直接绕过或者先看一些关于Maven教程,比如Juven翻译的<Maven权威指南>,google一下便知. 开门见山,首先抛出一 ...