javascript
针对Spring的Spring Retry 我发现了这样一个大家都不知道的技巧!
外部服務(wù)對于調(diào)用者來說一般都是不可靠的,尤其是在網(wǎng)絡(luò)環(huán)境比較差的情況下,網(wǎng)絡(luò)抖動很容易導(dǎo)致請求超時等異常情況,這時候就需要使用失敗重試策略重新調(diào)用 API 接口來獲取。重試策略在服務(wù)治理方面也有很廣泛的使用,通過定時檢測,來查看服務(wù)是否存活。
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一樣,默認空,當(dāng)exclude也為空時,所有異常都重試
- exclude:指定異常不重試,默認空,當(dāng)include也為空時,所有異常都重試
- maxAttemps:重試次數(shù),默認3
- backoff:重試補償機制,默認沒有
@Backoff注解
- delay:指定延遲后重試
multiplier:指定延遲的倍數(shù),比如delay=5000l,multiplier=2時,第一次重試為5秒后,第二次為10秒,第三次為20秒
@Recover注解:
當(dāng)重試到達指定次數(shù)時,被注解的方法將被回調(diào),可以在該方法中進行日志處理。需要注意的是發(fā)生的異常和入?yún)㈩愋鸵恢聲r才會回調(diào)。
3.啟用重試功能
啟動類上面添加@EnableRetry注解,啟用重試功能,或者在使用retry的service上面添加也可以,或者Configuration配置類上面。
建議所有的Enable配置加在啟動類上,可以清晰的統(tǒng)一管理使用的功能。
4.啟動服務(wù),運行測試
通過在啟動類Context調(diào)用服務(wù)看到如下打印: 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)定的遠程服務(wù)對話的策略。)
第一步引入maven坐標(biāo):
1.其主要接口及策略介紹
- Attempt:一次執(zhí)行任務(wù);
- AttemptTimeLimiter:單次任務(wù)執(zhí)行時間限制(如果單次任務(wù)執(zhí)行超時,則終止執(zhí)行當(dāng)前任務(wù));
- BlockStrategies:任務(wù)阻塞策略(通俗的講就是當(dāng)前任務(wù)執(zhí)行完,下次任務(wù)還沒開始這段時間做什么……- - BlockStrategies.THREAD_SLEEP_STRATEGY 也就是調(diào)用 Thread.sleep(sleepTime);
- RetryException:重試異常;
- RetryListener:自定義重試監(jiān)聽器,可以用于異步記錄錯誤日志;
- StopStrategy:停止重試策略,提供三種:
- StopAfterDelayStrategy:設(shè)定一個最長允許的執(zhí)行時間;比如設(shè)定最長執(zhí)行10s,無論任務(wù)執(zhí)行次數(shù),只要重試的時候超出了最長時間,則任務(wù)終止,并返回重試異常RetryException;
- NeverStopStrategy:不停止,用于需要一直輪訓(xùn)直到返回期望結(jié)果的情況;
- StopAfterAttemptStrategy:設(shè)定最大重試次數(shù),如果超出最大重試次數(shù)則停止重試,并返回重試異常;
- WaitStrategy:等待時長策略(控制時間間隔),返回結(jié)果為下次執(zhí)行時長:
- FixedWaitStrategy:固定等待時長策略;
- RandomWaitStrategy:隨機等待時長策略(可以提供一個最小和最大時長,等待時長為其區(qū)間隨機值)
- IncrementingWaitStrategy:遞增等待時長策略(提供一個初始值和步長,等待時間隨重試次數(shù)增加而增加)
- ExponentialWaitStrategy:指數(shù)等待時長策略;
- FibonacciWaitStrategy :Fibonacci 等待時長策略;
- ExceptionWaitStrategy :異常時長等待策略;
- CompositeWaitStrategy :復(fù)合時長等待策略;
2.根據(jù)結(jié)果判斷是否重試
使用場景:如果返回值決定是否要重試。
重試接口:
測試:
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... 53.根據(jù)異常判斷是否重試
使用場景:根據(jù)拋出異常類型判斷是否執(zhí)行重試。
重試接口:
測試:
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... 54.重試策略——設(shè)定無限重試
使用場景:在有異常情況下,無限重試(默認執(zhí)行策略),直到返回正常有效結(jié)果;
Retryer<String> retry = RetryerBuilder.<String>newBuilder().retryIfRuntimeException().withStopStrategy(StopStrategies.neverStop()).build();retry.call(callableWithResult());5.重試策略——設(shè)定最大的重試次數(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.等待策略——設(shè)定重試等待固定時長策略
使用場景:設(shè)定每次重試等待間隔固定為10s;
測試輸出,可以看出調(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.等待策略——設(shè)定重試等待時長固定增長策略
場景:設(shè)定初始等待時長值,并設(shè)定固定增長步長,但不設(shè)定最大等待時長;
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(); //加入Java開發(fā)交流君樣:756584822一起吹水聊天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è)定重試等待時長按指數(shù)增長策略
使用場景:根據(jù)multiplier值按照指數(shù)級增長等待時長,并設(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)⒉皇呛芏?#xff0c;好吧,查看源碼:\
@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;} //加入Java開發(fā)交流君樣:756584822一起吹水聊天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)?#xff0c;最重要的延遲時間計算方法computeSleepTime(),可以看出延遲時間計算方式
計算以2為底失敗次數(shù)為指數(shù)的值
第一步的值構(gòu)造器第一個入?yún)⑾喑?#xff0c;然后四舍五入得到延遲時間(毫秒)
通過以上分析可知入?yún)?000時間隔是應(yīng)該為2,4,8s
測試輸出,可以看出調(diào)用間隔時間 2×1000,4×1000,8×1000:
9.等待策略——設(shè)定重試等待時長按斐波那契數(shù)列策略
使用場景:根據(jù)multiplier值按照斐波那契數(shù)列增長等待時長,并設(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(); //加入Java開發(fā)交流君樣:756584822一起吹水聊天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.等待策略——組合重試等待時長策略
使用場景:當(dāng)現(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; //加入Java開發(fā)交流君樣:756584822一起吹水聊天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... 811.監(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ū)е庐惓?/span>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());}//加入Java開發(fā)交流君樣:756584822一起吹水聊天 }測試:
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()); }//加入Java開發(fā)交流君樣:756584822一起吹水聊天輸出:
2019-03-09T16:32:35.097: do something... 1 retry times=1//加入Java開發(fā)交流君樣:756584822一起吹水聊天 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.//加入Java開發(fā)交流君樣:756584822一起吹水聊天總結(jié)
兩種方式都是比較優(yōu)雅的重試策略,Spring-retry配置更簡單,實現(xiàn)的功能也相對簡單,Guava本身就是谷歌推出的精品java類庫,guava-retry也是功能非常強大,相比較于Spring-Retry在是否重試的判斷條件上有更多的選擇性,可以作為Spring-retry的補充。
最后,祝大家早日學(xué)有所成,拿到滿意offer
總結(jié)
以上是生活随笔為你收集整理的针对Spring的Spring Retry 我发现了这样一个大家都不知道的技巧!的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 凉凉!面试阿里我被Redis技术专题给搞
- 下一篇: 微信一定要绑定银行卡吗