該功能由 [`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
}
```