1.系列文章
- 你必须懂也可以懂的微服务系列一:服务注册
- 你必须懂也可以懂的微服务系列二:服务反注册
- 你必须懂也可以懂的微服务系列三:服务调用
- 你必须懂也可以懂的微服务系列四:服务发现与负载均衡
- 你必须懂也可以懂的微服务系列五:服务熔断
2.前言
网络抖动是系统调用过程中发生最频繁、持续时间较短的一种故障,如果服务调用方在出现超时情况下不做任何处理,那么就会导致某次请求失败,其实这种失败是可以通过某种手段来进行解决,也就是本文要介绍的技术重试
3.服务重试
3.1 捕获IO异常
Object executeAndDecode(RequestTemplate template, Options options) throws Throwable {
Request request = targetRequest(template);
try {
response = client.execute(request, options);
// ensure the request is set. TODO: remove in Feign 12
response = response.toBuilder()
.request(request)
.requestTemplate(template)
.build();
} catch (IOException e) {
// 1.抛出可重试异常
throw errorExecuting(request, e);
}
// ...省略部分代码
}
static FeignException errorExecuting(Request request, IOException cause) {
return new RetryableException(
-1,
format("%s executing %s %s", cause.getMessage(), request.httpMethod(), request.url()),
request.httpMethod(),
cause,
null, request);
}
在进行远程调用时捕获IO异常,一旦发生IO异常,对外抛出可重试异常
RetryableException
3.2 重试条件判断
@Override
public Object invoke(Object[] argv) throws Throwable {
RequestTemplate template = buildTemplateFromArgs.create(argv);
Options options = findOptions(argv);
Retryer retryer = this.retryer.clone();
while (true) {
try {
return executeAndDecode(template, options);
} catch (RetryableException e) {
try {
retryer.continueOrPropagate(e);
} catch (RetryableException th) {
Throwable cause = th.getCause();
if (propagationPolicy == UNWRAP && cause != null) {
throw cause;
} else {
throw th;
}
}
continue;
}
}
}
重试器Retryer默认实现
Retryer NEVER_RETRY = new Retryer() {
@Override
public void continueOrPropagate(RetryableException e) {
throw e;
}
@Override
public Retryer clone() {
return this;
}
};
默认情况不会进行重试,会将异常进行抛出,要想实现重试功能,需要手动指定重试器
feign:
client:
config: # 参考FeignClientProperties中的config
resilience4j-provider-service: # 调用服务名称
connectTimeout: 3000 # 连接超时时间
readTimeout: 3000 # 读取超时时间
retryer: feign.Retryer.Default # 指定重试策略
3.3 重试实现
public void continueOrPropagate(RetryableException e) {
// 1.是否超过最大重试次数,如果超过则抛出异常
if (this.attempt++ >= this.maxAttempts) {
throw e;
} else {
long interval;
// 2.获取下一次重试间隔时间
if (e.retryAfter() != null) {
interval = e.retryAfter().getTime() - this.currentTimeMillis();
if (interval > this.maxPeriod) {
interval = this.maxPeriod;
}
if (interval < 0L) {
return;
}
} else {
interval = this.nextMaxInterval();
}
try {
// 3.休眠间隔时间
Thread.sleep(interval);
} catch (InterruptedException var5) {
Thread.currentThread().interrupt();
throw e;
}
this.sleptForMillis += interval;
}
}
重试逻辑实现也很简单,其逻辑如下:
- 先判断是否超过最大重试次数
- 超过则直接抛出异常
- 未超过则计算下次重试时间间隔
- 休眠间隔时间后再次发起远程调用
3.4 重试自定义实现
如果默认重试器Retryer无法满足业务需求,你也可以通过自定义实现重试器来达到自己的目的
public class RpcRetryer implements Retryer {
private final int maxAttempts;
private final long period;
private final long maxPeriod;
int attempt;
long sleptForMillis;
public RpcRetryer() {
this(100, SECONDS.toMillis(1), 4);
}
public RpcRetryer(long period, long maxPeriod, int maxAttempts) {
this.period = period;
this.maxPeriod = maxPeriod;
this.maxAttempts = maxAttempts;
this.attempt = 1;
}
protected long currentTimeMillis() {
return System.currentTimeMillis();
}
@Override
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();
throw e;
}
sleptForMillis += interval;
}
/**
* Calculates the time interval to a retry attempt. <br>
* The interval increases exponentially with each attempt, at a rate of nextInterval *= 1.5
* (where 1.5 is the backoff factor), to the maximum interval.
*
* @return time in nanoseconds from now until the next attempt.
*/
long nextMaxInterval() {
long interval = (long) (period * Math.pow(1.5, attempt - 1));
return Math.min(interval, maxPeriod);
}
@Override
public Retryer clone() {
return new RpcRetryer(period, maxPeriod, maxAttempts);
}
}
自定义重试器实现完成后,只需要在配置文件中进行指定即可
feign:
client:
config: # 参考FeignClientProperties中的config
resilience4j-provider-service: # 调用服务名称
retryer: com.boot.cloud.openfeign.client.retry.RpcRetryer # 指定重试策略
4. 结语
相信你在读完系列文章后会对服务注册、服务反注册、服务调用、服务发现与负载均衡、服务熔断、服务重试有一个大致的认识,也希望该系列文章对你学习微服务、实践微服务有些许帮助。