強悍的Spring之Spring Retry
在日常開發(fā)中,我們經(jīng)常會遇到需要調(diào)用外部服務和接口的場景。外部服務對于調(diào)用者來說一般都是不可靠的喳魏,尤其是在網(wǎng)絡環(huán)境比較差的情況下撵枢,網(wǎng)絡抖動很容易導致請求超時等異常情況,這時候就需要使用失敗重試策略重新調(diào)用 API 接口來獲取凡涩。重試策略在服務治理方面也有很廣泛的使用,通過定時檢測疹蛉,來查看服務是否存活活箕。
Spring異常重試框架Spring Retry
Spring Retry支持集成到Spring或者Spring Boot項目中,而它支持AOP的切面注入寫法可款,所以在引入時必須引入aspectjweaver.jar包育韩。
1.引入maven依賴
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
<version>1.1.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.6</version>
</dependency>
2.添加@Retryable和@Recover注解
@Retryable注解,被注解的方法發(fā)生異常時會重試
- value:指定發(fā)生的異常進行重試
- include:和value一樣闺鲸,默認空筋讨,當exclude也為空時,所有異常都重試
- exclude:指定異常不重試摸恍,默認空悉罕,當include也為空時,所有異常都重試
- maxAttemps:重試次數(shù)立镶,默認3
- backoff:重試補償機制蛮粮,默認沒有
@Backoff注解
- delay:指定延遲后重試
- multiplier:指定延遲的倍數(shù),比如delay=5000l,multiplier=2時谜慌,第一次重試為5秒后然想,第二次為10秒,第三次為20秒
@Recover注解:
當重試到達指定次數(shù)時欣范,被注解的方法將被回調(diào)变泄,可以在該方法中進行日志處理。需要注意的是發(fā)生的異常和入?yún)㈩愋鸵恢聲r才會回調(diào)恼琼。
@Service
public class RemoteService {
@Retryable(value = {Exception.class}, maxAttempts = 5, backoff = @Backoff(delay = 5000L, multiplier = 1))
public void call() {
System.out.println(LocalDateTime.now() + ": do something...");
throw new RuntimeException(LocalDateTime.now() + ": 運行調(diào)用異常");
}
@Recover
public void recover(Exception e) {
System.out.println(e.getMessage());
}
3.啟用重試功能
啟動類上面添加@EnableRetry注解妨蛹,啟用重試功能,或者在使用retry的service上面添加也可以晴竞,或者Configuration配置類上面蛙卤。
建議所有的Enable配置加在啟動類上,可以清晰的統(tǒng)一管理使用的功能。
@SpringBootApplication
@EnableRetry
public class App {
public static void main(String[] args) throws Exception {
ConfigurableApplicationContext context = SpringApplication.run(App.class, args);
System.out.println("Start app success.");
RemoteService bean = context.getBean(RemoteService.class);
bean.call();
}
}
4.啟動服務颤难,運行測試
通過在啟動類Context調(diào)用服務看到如下打印:
2019-03-09T15:22:12.781: do something...
2019-03-09T15:22:17.808: do something...
2019-03-09T15:22:22.835: do something...
2019-03-09T15:22:27.861: do something...
2019-03-09T15:22:32.887: do something...
2019-03-09T15:22:32.887: 運行調(diào)用異常
基于guava的重試組件Guava-Retryer
直接看組件作者對此組件的介紹:
This is a small extension to Google’s Guava library to allow for the creation of configurable retrying strategies for an arbitrary function call, such as something that talks to a remote service with flaky uptime.(這是對Google的guava庫的一個小擴展神年,允許為任意函數(shù)調(diào)用創(chuàng)建可配置的重試策略,例如與運行時間不穩(wěn)定的遠程服務對話的策略行嗤。)
第一步引入maven坐標:
<dependency>
<groupId>com.github.rholder</groupId>
<artifactId>guava-retrying</artifactId>
<version>2.0.0</version>
</dependency>
1.其主要接口及策略介紹
- Attempt:一次執(zhí)行任務已日;
- AttemptTimeLimiter:單次任務執(zhí)行時間限制(如果單次任務執(zhí)行超時,則終止執(zhí)行當前任務)栅屏;
- BlockStrategies:任務阻塞策略(通俗的講就是當前任務執(zhí)行完飘千,下次任務還沒開始這段時間做什么……),默認策略為:BlockStrategies.THREAD_SLEEP_STRATEGY 也就是調(diào)用 Thread.sleep(sleepTime);
- RetryException:重試異常栈雳;
- RetryListener:自定義重試監(jiān)聽器护奈,可以用于異步記錄錯誤日志;
- StopStrategy:停止重試策略哥纫,提供三種:
- StopAfterDelayStrategy :設定一個最長允許的執(zhí)行時間霉旗;比如設定最長執(zhí)行10s,無論任務執(zhí)行次數(shù)磺箕,只要重試的時候超出了最長時間,則任務終止抛虫,并返回重試異常RetryException松靡;
- NeverStopStrategy :不停止,用于需要一直輪訓直到返回期望結(jié)果的情況建椰;
- StopAfterAttemptStrategy :設定最大重試次數(shù)雕欺,如果超出最大重試次數(shù)則停止重試,并返回重試異常棉姐;
- WaitStrategy:等待時長策略(控制時間間隔)屠列,返回結(jié)果為下次執(zhí)行時長:
- FixedWaitStrategy:固定等待時長策略;
- RandomWaitStrategy:隨機等待時長策略(可以提供一個最小和最大時長伞矩,等待時長為其區(qū)間隨機值)
- IncrementingWaitStrategy:遞增等待時長策略(提供一個初始值和步長笛洛,等待時間隨重試次數(shù)增加而增加)
- ExponentialWaitStrategy:指數(shù)等待時長策略;
- FibonacciWaitStrategy :Fibonacci 等待時長策略乃坤;
- ExceptionWaitStrategy :異常時長等待策略苛让;
- CompositeWaitStrategy :復合時長等待策略;
2.根據(jù)結(jié)果判斷是否重試
使用場景:如果返回值決定是否要重試湿诊。
重試接口:
private static Callable<String> callableWithResult() {
return new Callable<String>() {
int counter = 0;
public String call() throws Exception {
counter++;
System.out.println(LocalDateTime.now() + ": do something... " + counter);
if (counter < 5) {
return "james";
}
return "kobe";
}
};
}
測試:
public static void main(String[] args) {
Retryer<String> retry = RetryerBuilder.<String>newBuilder()
.retryIfResult(result -> !result.contains("kobe")).build();
retry.call(callableWithResult());
}
輸出:
2019-03-09T15:40:23.706: do something... 1
2019-03-09T15:40:23.710: do something... 2
2019-03-09T15:40:23.711: do something... 3
2019-03-09T15:40:23.711: do something... 4
2019-03-09T15:40:23.711: do something... 5
3.根據(jù)異常判斷是否重試
使用場景:根據(jù)拋出異常類型判斷是否執(zhí)行重試狱杰。
重試接口:
private static Callable<String> callableWithResult() {
return new Callable<String>() {
int counter = 0;
public String call() throws Exception {
counter++;
System.out.println(LocalDateTime.now() + ": do something... " + counter);
if (counter < 5) {
throw new RuntimeException("Run exception");
}
return "kobe";
}
};
}
測試:
public static void main(String[] args) throws ExecutionException, RetryException{
Retryer<String> retry = RetryerBuilder.<String>newBuilder()
.retryIfRuntimeException()
.withStopStrategy(StopStrategies.neverStop())
.build();
retry.call(callableWithResult());
}
輸出:
2019-03-09T15:53:27.682: do something... 1
2019-03-09T15:53:27.686: do something... 2
2019-03-09T15:53:27.686: do something... 3
2019-03-09T15:53:27.687: do something... 4
2019-03-09T15:53:27.687: do something... 5
4.重試策略——設定無限重試
使用場景:在有異常情況下,無限重試(默認執(zhí)行策略)厅须,直到返回正常有效結(jié)果仿畸;
Retryer<String> retry = RetryerBuilder.<String>newBuilder()
.retryIfRuntimeException()
.withStopStrategy(StopStrategies.neverStop())
.build();
retry.call(callableWithResult());
5.重試策略——設定最大的重試次數(shù)
使用場景:在有異常情況下,最多重試次數(shù),如果超過次數(shù)則會拋出異常错沽;
private static Callable<String> callableWithResult() {
return new Callable<String>() {
int counter = 0;
public String call() throws Exception {
counter++;
System.out.println(LocalDateTime.now() + ": do something... " + counter);
throw new RuntimeException("Run exception");
}
};
}
測試:
public static void main(String[] args) throws ExecutionException, RetryException{
Retryer<String> retry = RetryerBuilder.<String>newBuilder()
.retryIfRuntimeException()
.withStopStrategy(StopStrategies.stopAfterAttempt(4))
.build();
retry.call(callableWithResult());
}
輸出:
2019-03-09T16:02:29.471: do something... 1
2019-03-09T16:02:29.477: do something... 2
2019-03-09T16:02:29.478: do something... 3
2019-03-09T16:02:29.478: do something... 4
Exception in thread "main" com.github.rholder.retry.RetryException: Retrying failed to complete successfully after 4 attempts.
6.等待策略——設定重試等待固定時長策略
使用場景:設定每次重試等待間隔固定為10s簿晓;
public static void main(String[] args) throws ExecutionException, RetryExceptio{
Retryer<String> retry = RetryerBuilder.<String>newBuilder()
.retryIfRuntimeException()
.withStopStrategy(StopStrategies.stopAfterAttempt(4))
.withWaitStrategy(WaitStrategies.fixedWait(10, TimeUnit.SECONDS))
.build();
retry.call(callableWithResult());
}
測試輸出,可以看出調(diào)用間隔是10S:
2019-03-09T16:06:34.457: do something... 1
2019-03-09T16:06:44.660: do something... 2
2019-03-09T16:06:54.923: do something... 3
2019-03-09T16:07:05.187: do something... 4
Exception in thread "main" com.github.rholder.retry.RetryException: Retrying failed to complete successfully after 4 attempts.
7.等待策略——設定重試等待時長固定增長策略
場景:設定初始等待時長值甥捺,并設定固定增長步長抢蚀,但不設定最大等待時長;
public static void main(String[] args) throws ExecutionException, RetryException {
Retryer<String> retry = RetryerBuilder.<String>newBuilder()
.retryIfRuntimeException()
.withStopStrategy(StopStrategies.stopAfterAttempt(4))
.withWaitStrategy(WaitStrategies.incrementingWait(1, SECONDS, 1, SECONDS))
.build();
retry.call(callableWithResult());
}
測試輸出镰禾,可以看出調(diào)用間隔時間遞增1秒:
2019-03-09T18:46:30.256: do something... 1
2019-03-09T18:46:31.260: do something... 2
2019-03-09T18:46:33.260: do something... 3
2019-03-09T18:46:36.260: do something... 4
Exception in thread "main" com.github.rholder.retry.RetryException: Retrying failed to complete successfully after 4 attempts.
8.等待策略——設定重試等待時長按指數(shù)增長策略
使用場景:根據(jù)multiplier值按照指數(shù)級增長等待時長皿曲,并設定最大等待時長;
public static void main(String[] args) throws ExecutionException, RetryExceptio{
Retryer<String> retry = RetryerBuilder.<String>newBuilder()
.retryIfRuntimeException()
.withStopStrategy(StopStrategies.stopAfterAttempt(4))
.withWaitStrategy(WaitStrategies.exponentialWait(1000, 10,SECONDS))
.build();
retry.call(callableWithResult());
}
這個重試策略和入?yún)⒉皇呛芏庹欤冒墒讨ィ榭丛创a:
@Immutable
private static final class ExponentialWaitStrategy implements WaitStrategy {
private final long multiplier;
private final long maximumWait;
public ExponentialWaitStrategy(long multiplier, long maximumWait) {
Preconditions.checkArgument(multiplier > 0L, "multiplier must be > 0 but is %d", new Object[]{Long.valueOf(multiplier)});
Preconditions.checkArgument(maximumWait >= 0L, "maximumWait must be >= 0 but is %d", new Object[]{Long.valueOf(maximumWait)});
Preconditions.checkArgument(multiplier < maximumWait, "multiplier must be < maximumWait but is %d", new Object[]{Long.valueOf(multiplier)});
this.multiplier = multiplier;
this.maximumWait = maximumWait;
}
public long computeSleepTime(Attempt failedAttempt) {
double exp = Math.pow(2.0D, (double)failedAttempt.getAttemptNumber());
long result = Math.round((double)this.multiplier * exp);
if(result > this.maximumWait) {
result = this.maximumWait;
}
return result >= 0L?result:0L;
}
}
通過源碼看出ExponentialWaitStrategy是一個不可變的內(nèi)部類哈恰,構(gòu)造器中校驗入?yún)ⅲ钪匾难舆t時間計算方法computeSleepTime(),可以看出延遲時間計算方式
- 計算以2為底失敗次數(shù)為指數(shù)的值
- 第一步的值構(gòu)造器第一個入?yún)⑾喑伺乇幔缓笏纳嵛迦氲玫窖舆t時間(毫秒)
通過以上分析可知入?yún)?000時間隔是應該為2,4瓦灶,8s
測試輸出烁登,可以看出調(diào)用間隔時間 2×1000,4×1000易阳,8×1000:
2019-03-09T19:11:23.905: do something... 1
2019-03-09T19:11:25.908: do something... 2
2019-03-09T19:11:29.908: do something... 3
2019-03-09T19:11:37.909: do something... 4
Exception in thread "main" com.github.rholder.retry.RetryException: Retrying failed to complete successfully after 4 attempts.
9.等待策略——設定重試等待時長按斐波那契數(shù)列策略
使用場景:根據(jù)multiplier值按照斐波那契數(shù)列增長等待時長附较,并設定最大等待時長,斐波那契數(shù)列:1潦俺、1拒课、2、3事示、5早像、8、13肖爵、21卢鹦、34、……
public static void main(String[] args) throws ExecutionException, RetryException {
Retryer<String> retry = RetryerBuilder.<String>newBuilder()
.retryIfRuntimeException()
.withStopStrategy(StopStrategies.stopAfterAttempt(4))
.withWaitStrategy(WaitStrategies.fibonacciWait(1000, 10, SECONDS))
.build();
retry.call(callableWithResult());
}
同樣劝堪,看源碼可知計算可知延遲時間為斐波那契數(shù)列和第一入?yún)⒌某朔e(毫秒)
public long computeSleepTime(Attempt failedAttempt) {
long fib = this.fib(failedAttempt.getAttemptNumber());
long result = this.multiplier * fib;
if(result > this.maximumWait || result < 0L) {
result = this.maximumWait;
}
return result >= 0L?result:0L;
}
測試輸出法挨,可看出間隔調(diào)用為1×1000,1×1000幅聘,2×1000:
2019-03-09T19:28:43.903: do something... 1
2019-03-09T19:28:44.909: do something... 2
2019-03-09T19:28:45.928: do something... 3
2019-03-09T19:28:47.928: do something... 4
Exception in thread "main" com.github.rholder.retry.RetryException: Retrying failed to complete successfully after 4 attempts.
10.等待策略——組合重試等待時長策略
使用場景:當現(xiàn)有策略不滿足使用場景時凡纳,可以對多個策略進行組合使用。
public static void main(String[] args) throws ExecutionException, RetryException {
Retryer<String> retry = RetryerBuilder.<String>newBuilder()
.retryIfRuntimeException()
.withStopStrategy(StopStrategies.stopAfterAttempt(10))
.withWaitStrategy(WaitStrategies.join(WaitStrategies.exponentialWait(1000, 100, SECONDS)
, WaitStrategies.fixedWait(2, SECONDS)))
.build();
retry.call(callableWithResult());
}
同樣帝蒿,看源碼才能理解組合策略是什么意思:
public long computeSleepTime(Attempt failedAttempt) {
long waitTime = 0L;
WaitStrategy waitStrategy;
for(Iterator i$ = this.waitStrategies.iterator(); i$.hasNext(); waitTime += waitStrategy.computeSleepTime(failedAttempt)) {
waitStrategy = (WaitStrategy)i$.next();
}
return waitTime;
}
可看出組合策略其實按照多個策略的延遲時間相加得到組合策略的延遲時間荐糜。exponentialWait的延遲時間為2,4,8暴氏,16延塑,32...,fixedWait延遲為2答渔,2关带,2,2沼撕,2...,所以總的延遲時間為4宋雏,6,10务豺,18磨总,34...
測試輸出:
2019-03-09T19:46:45.854: do something... 1
2019-03-09T19:46:49.859: do something... 2
2019-03-09T19:46:55.859: do something... 3
2019-03-09T19:47:05.859: do something... 4
2019-03-09T19:47:23.859: do something... 5
2019-03-09T19:47:57.860: do something... 6
2019-03-09T19:49:03.861: do something... 7
2019-03-09T19:50:45.862: do something... 8
11.監(jiān)聽器——RetryListener實現(xiàn)重試過程細節(jié)處理
使用場景:自定義監(jiān)聽器,分別打印重試過程中的細節(jié)笼沥,未來可更多的用于異步日志記錄蚪燕,亦或是特殊處理。
public class MyRetryListener implements RetryListener {
@Override
public <V> void onRetry(Attempt<V> attempt) {
System.out.println(("retry times=" + attempt.getAttemptNumber()));
// 距離第一次重試的延遲
System.out.println("delay=" + attempt.getDelaySinceFirstAttempt());
// 重試結(jié)果: 是異常終止, 還是正常返回
System.out.println("hasException=" + attempt.hasException());
System.out.println("hasResult=" + attempt.hasResult());
// 是什么原因?qū)е庐惓? if (attempt.hasException()) {
System.out.println("causeBy=" + attempt.getExceptionCause());
} else {
// 正常返回時的結(jié)果
System.out.println("result=" + attempt.getResult());
}
// 增加了額外的異常處理代碼
try {
Object result = attempt.get();
System.out.println("rude get=" + result);
} catch (ExecutionException e) {
System.out.println("this attempt produce exception." + e.getCause());
}
}
測試:
public static void main(String[] args) throws ExecutionException, RetryException {
Retryer<String> retry = RetryerBuilder.<String>newBuilder()
.retryIfRuntimeException()
.withStopStrategy(StopStrategies.stopAfterAttempt(2))
.withRetryListener(new MyRetryListener())
.build();
retry.call(callableWithResult());
}
輸出:
2019-03-09T16:32:35.097: do something... 1
retry times=1
delay=128
hasException=true
hasResult=false
causeBy=java.lang.RuntimeException: Run exception
this attempt produce exception.java.lang.RuntimeException: Run exception
2019-03-09T16:32:35.102: do something... 2
retry times=2
delay=129
hasException=true
hasResult=false
causeBy=java.lang.RuntimeException: Run exception
this attempt produce exception.java.lang.RuntimeException: Run exception
Exception in thread "main" com.github.rholder.retry.RetryException: Retrying failed to complete successfully after 2 attempts.
總結(jié)
兩種方式都是比較優(yōu)雅的重試策略奔浅,Spring-retry配置更簡單馆纳,實現(xiàn)的功能也相對簡單,Guava本身就是谷歌推出的精品java類庫汹桦,guava-retry也是功能非常強大鲁驶,相比較于Spring-Retry在是否重試的判斷條件上有更多的選擇性,可以作為Spring-retry的補充营勤。