该功能由 [`Guava-Retrying`](https://github.com/rholder/guava-retrying) 源改而来
> 在很多业务场景中,为了排除系统中的各种不稳定因素,以及逻辑上的错误,并最大概率保证获得预期的结果,重试机制都是必不可少的。
> 尤其是调用远程服务,在高并发场景下,很可能因为服务器响应延迟或者网络原因,造成我们得不到想要的结果,或者根本得不到响应。这个时候,一个优雅的重试调用机制,可以让我们更大概率保证得到预期的响应。
[TOC]
# 如何优雅地设计重试实现
1. 什么条件下重试
2. 什么条件下停止
3. 如何停止重试
4. 停止重试等待多久
5. 如何等待
6. 请求时间限制
7. 如何结束
8. 如何监听整个重试过程
# 源码修改
AttemptTimeLimiters.java 调用原版guava18 SimpleTimeLimiter()
改为
AttemptTimeLimiters.java 调用升级guava29 SimpleTimeLimiter(ExecutorService)
****
# 构造重试
继续上一章网络重试,自定义如下:
```
Retryer<RawResponse> retry = RetryerBuilder.<RawResponse>newBuilder()
// 重试条件
.retryIfException()
.retryIfResult(reRetryPredicate)
// 等待策略:请求间隔1s
.withWaitStrategy(WaitStrategies.fixedWait(1, TimeUnit.SECONDS))
// 停止策略:尝试请求3次
.withStopStrategy(StopStrategies.stopAfterAttempt(3))
// 时间限制:请求限制2s
.withAttemptTimeLimiter(AttemptTimeLimiters.fixedTimeLimit(2, TimeUnit.SECONDS))
// 重试监听
.withRetryListener(reRetryListener)
//
.build();
```
重试条件
```
// 抛出runtime异常、checked异常时都会重试,但是抛出error不会重试
.retryIfException()
// 抛出runtime异常时重试,checked异常和error都不重试。
.retryIfRuntimeException()
// 特定异常时重试
.retryIfExceptionOfType(NullPointerException.class)
.retryIfException(Predicates.or(Predicates.instanceOf(NullPointerException.class),Predicates.instanceOf(IOException.class)))
// 返回指定结果时重试
.retryIfResult(Predicates.equalTo(false))
```
了解重试条件后,通过自定义结果、异常可以灵活的应用在业务当中
*****
网络请求:如当返回不等于200时重试
```
protected Predicate<RawResponse> reRetryPredicate = raw -> {
return raw.statusCode() != 200;
};
```
业务逻辑:如当返回result为空时、当返回值=-3时(统一返回章节中-3=重试)
```
protected Predicate<Result> reRetryPredicate = result -> {
if (ObjectUtils.isEmpty(result)) {
return true;
} else if (result.getCode() == -3) {
return true;
}
return false;
};
```
# 测试示例
```
@Test
public void retry() {
// 构造重试
Retryer<Result> retry = RetryerBuilder.<Result>newBuilder()
// 重试条件
.retryIfException()
// 返回指定结果时重试
.retryIfResult((@Nullable Result result) -> {
if (ObjectUtils.isEmpty(result)) {
return true;
} else if (result.getCode() == -3) {
return true;
}
return false;
})
// 等待策略:每次请求间隔1s
.withWaitStrategy(WaitStrategies.fixedWait(1, TimeUnit.SECONDS))
// 停止策略 : 尝试请求2次
.withStopStrategy(StopStrategies.stopAfterAttempt(2))
// 时间限制 : 请求限制2s
.withAttemptTimeLimiter(AttemptTimeLimiters.fixedTimeLimit(5, TimeUnit.SECONDS))
.withRetryListener(new RetryListener() {
@Override
public <V> void onRetry(Attempt<V> attempt) {
long number = attempt.getAttemptNumber();
long delay = attempt.getDelaySinceFirstAttempt();
boolean isError = attempt.hasException();
boolean isResult = attempt.hasResult();
if (attempt.hasException()) {
if (attempt.getExceptionCause().getCause() instanceof RunException) {
RunException runException = (RunException) attempt.getExceptionCause().getCause();
log.warn("onException causeBy:{} {}", runException.getErrorCode(), runException.getMessage());
} else {
log.warn("onException causeBy:{}", attempt.getExceptionCause().toString());
}
} else {
if (attempt.hasResult()) {
try {
V result = attempt.get();
if (result instanceof Result) {
log.warn("onRetry number:{} error:{} result:{} statusCode:{} delay:{}", number, isError, isResult,
((Result) result).getCode(), delay);
}
} catch (ExecutionException e) {
log.error("onResult exception:{}", e.getCause().toString());
throw new RunException(RunExc.RETRY, "test retry");
}
}
}
}
})
.build();
try {
Result result = retry.call(() -> {
// 构造请求
RequestBuilder req = Requests.post(url).params(Parameter.of("configKey", "appLaunch"));
// 请求重试
RawResponse response = RequestsHelper.retry(req);
// 获取结果
TestRetryResponse result1 = response.readToJson(TestRetryResponse.class);
// 验证结果,如果结果正确则返回,错误则重试
if (result1.getCode() == 0) {
return R.succ(result1.getData());
} else {
return R.fail(Result.RETRY, result1.getMsg());
}
});
// 验证结果,如果结果正确则返回,错误则重试
log.info(JSON.toJSONString(R.succ(result.getData())));
} catch (ExecutionException | RetryException e) {
throw new RunException(RunExc.RETRY, "test retry");
}
}
```
# 验证结果
述示例代码可知,远程请求调用,对业务、网络均模拟重试操作。这类是比较常见的业务场景。在某些特殊场景下需要对某些请求、业务需要做重试。如:
网络A:重试3次,每次等待1秒,限制2秒
业务B:重试2次,每次等待1秒,限制5秒
1、当网络A异常时:
```
[FastBoot][ WARN][08-11 14:23:53]-->[pool-6-thread-1:1079595][onRetry(HttpRetryer.java:76)] | - onRetry number:1 error:false result:true statusCode:404 delay:180
[FastBoot][ WARN][08-11 14:23:54]-->[pool-6-thread-1:1080701][onRetry(HttpRetryer.java:76)] | - onRetry number:2 error:false result:true statusCode:404 delay:1285
[FastBoot][ WARN][08-11 14:23:55]-->[pool-6-thread-1:1081792][onRetry(HttpRetryer.java:76)] | - onRetry number:3 error:false result:true statusCode:404 delay:2375
[FastBoot][ WARN][08-11 14:23:55]-->[http-nio-9090-exec-10:1081793][onRetry(ApIController.java:129)] | - onException causeBy:2000 请求错误:http retry error
[FastBoot][ WARN][08-11 14:23:56]-->[pool-6-thread-1:1082901][onRetry(HttpRetryer.java:76)] | - onRetry number:1 error:false result:true statusCode:404 delay:98
[FastBoot][ WARN][08-11 14:23:58]-->[pool-6-thread-1:1084004][onRetry(HttpRetryer.java:76)] | - onRetry number:2 error:false result:true statusCode:404 delay:1200
[FastBoot][ WARN][08-11 14:23:59]-->[pool-6-thread-1:1085138][onRetry(HttpRetryer.java:76)] | - onRetry number:3 error:false result:true statusCode:404 delay:2334
[FastBoot][ WARN][08-11 14:23:59]-->[http-nio-9090-exec-10:1085139][onRetry(ApIController.java:129)] | - onException causeBy:2000 请求错误:http retry error
[FastBoot][ERROR][08-11 14:23:59]-->[http-nio-9090-exec-10:1085143][runException(GlobalExceptionAdvice.java:134)] | - runException ......
com.xiesx.FastBoot.core.exception.RunException: 重试失败:test retry error
```
2、当网络A异常时,业务B限制2秒时(注意:这里只打印2个,因为B做了限制2秒,A会重试1秒\*3次,超出B限制)
```
[FastBoot][ WARN][08-11 14:23:53]-->[pool-6-thread-1:1079595][onRetry(HttpRetryer.java:76)] | - onRetry number:1 error:false result:true statusCode:404 delay:180
[FastBoot][ WARN][08-11 14:23:54]-->[pool-6-thread-1:1080701][onRetry(HttpRetryer.java:76)] | - onRetry number:2 error:false result:true statusCode:404 delay:1285
[FastBoot][ WARN][08-11 14:23:55]-->[http-nio-9090-exec-10:1081793][onRetry(ApIController.java:129)] | - onException causeBy:2000 请求错误:http retry error
[FastBoot][ WARN][08-11 14:23:56]-->[pool-6-thread-1:1082901][onRetry(HttpRetryer.java:76)] | - onRetry number:1 error:false result:true statusCode:404 delay:98
[FastBoot][ WARN][08-11 14:23:58]-->[pool-6-thread-1:1084004][onRetry(HttpRetryer.java:76)] | - onRetry number:2 error:false result:true statusCode:404 delay:1200
[FastBoot][ WARN][08-11 14:23:59]-->[http-nio-9090-exec-10:1085139][onRetry(ApIController.java:129)] | - onException causeBy:2000 请求错误:http retry error
[FastBoot][ERROR][08-11 14:23:59]-->[http-nio-9090-exec-10:1085143][runException(GlobalExceptionAdvice.java:134)] | - runException ......
com.xiesx.FastBoot.core.exception.RunException: 重试失败:test retry error
```
3、网络A正常:业务B错误(A重试1次、B重复2次)
```
[FastBoot][ WARN][08-11 14:40:18]-->[pool-7-thread-1:2064857][onRetry(HttpRetryer.java:76)] | - onRetry number:1 error:false result:true statusCode:200 delay:1397
[FastBoot][ WARN][08-11 14:40:19]-->[http-nio-9090-exec-4:2065102][onRetry(ApIController.java:138)] | - onRetry number:1 error:false result:true statusCode:-3 delay:1642
[FastBoot][ WARN][08-11 14:40:20]-->[pool-7-thread-1:2066165][onRetry(HttpRetryer.java:76)] | - onRetry number:1 error:false result:true statusCode:200 delay:49
[FastBoot][ WARN][08-11 14:40:20]-->[http-nio-9090-exec-4:2066166][onRetry(ApIController.java:138)] | - onRetry number:2 error:false result:true statusCode:-3 delay:2707
[FastBoot][ERROR][08-11 14:40:20]-->[http-nio-9090-exec-4:2066168][runException(GlobalExceptionAdvice.java:134)] | - runException ......
com.xiesx.FastBoot.core.exception.RunException: 重试失败:test retry error
```
4、网络A、B正常 (AB重复1次、即首次)
```
[FastBoot][ WARN][08-11 14:48:38]-->[pool-8-thread-1:2564941][onRetry(HttpRetryer.java:76)] | - onRetry number:1 error:false result:true statusCode:200 delay:159
[FastBoot][ WARN][08-11 14:48:38]-->[http-nio-9090-exec-1:2564946][onRetry(ApIController.java:138)] | - onRetry number:1 error:false result:true statusCode:0 delay:164
```
重试成功返回:
```
{
"code":0,
"msg":"操作成功",
"data":{
},
"success":true
}
```
重试失败返回:
```
{
"code": 7000,
"msg": "重试失败:test retry error",
"success": false
}
```