為什么要引入重試機制
我們首先看看正常的業(yè)務(wù)系統(tǒng)交互流程,就像下面圖中所示一樣,我們自己開發(fā)的系統(tǒng)通過HTTP接口或者通過RPC去訪問其他業(yè)務(wù)系統(tǒng),其他系統(tǒng)在沒出現(xiàn)任何問題的情況下會返回給我們需要的數(shù)據(jù),狀態(tài)為success。
但大家在日常的開發(fā)工作當中應(yīng)該碰到過不少這樣的問題:自己應(yīng)用因為業(yè)務(wù)需求需要調(diào)其他關(guān)聯(lián)應(yīng)用的接口或二方包,而其他應(yīng)用的接口穩(wěn)定性不敢過分恭維,老是出一些莫名奇妙的幺蛾子,比如由于接口暫時升級維護導致的短暫不可用,又或者網(wǎng)絡(luò)抖動因素導致的單次接口請求失敗。
諸如此類的麻煩問題會因為業(yè)務(wù)強依賴致使我們自己維護的系統(tǒng)也跟著陷入一種不穩(wěn)定的狀態(tài)(當然這個強依賴是沒有辦法的事情,畢竟業(yè)務(wù)之間需要解耦獨立開發(fā)維護)。
所以也就是說重試的使用場景大多是因為我們的系統(tǒng)依賴了其他的業(yè)務(wù),或者是由于我們自己的業(yè)務(wù)需要通過網(wǎng)絡(luò)請求去獲取數(shù)據(jù)這樣的場景。既然一次請求結(jié)果的狀態(tài)非常不可控、不穩(wěn)定,那么一個非常自然的想法就是多試幾次,就能很好的避開網(wǎng)絡(luò)抖動或其他關(guān)聯(lián)應(yīng)用暫時down機維護帶來的系統(tǒng)不可用問題。
當然,這里也有幾個引入重試機制以后需要考慮的問題。
- 我們應(yīng)該重試幾次?
- 每次重試的間隔設(shè)置為多少合適?
- 如果所有重試機會都用完了還是不成功怎么辦?下面我們就這幾個問題展開分析一下。
重試幾次合適
通常來說我們單次重試所面臨的情況就如上面我們分析的一樣,有很大的不可確定性,那到底多少次是比較合理的次數(shù)呢?這個就要“具體業(yè)務(wù)具體分析”了,但一般來說3次重試就差不多可以滿足大多數(shù)業(yè)務(wù)需求了,當然,這是需要結(jié)合后面要說的重試間隔一起討論的。為什么說3次就基本夠了呢,因為如果被請求系統(tǒng)實在處于長時間不可用狀態(tài)。我們重試多次是沒有什么意義的。
重試間隔設(shè)置為多少合適
如果重試間隔設(shè)置得太小,可能被調(diào)用系統(tǒng)還沒來得及恢復過來我們就又發(fā)起調(diào)用,得到的結(jié)果肯定還是Fail;如果設(shè)置的太大,我們自己的系統(tǒng)就會犧牲掉不少數(shù)據(jù)時效性。所以,重試間隔也要根據(jù)被調(diào)用的系統(tǒng)平均恢復時間去正確估量,通常而言這個平均恢復時間很難統(tǒng)計到,所以一般的經(jīng)驗值是3至5分鐘。
重試機會用完以后依舊Fail怎么辦
這種情況也是需要認真考慮的,因為不排除被調(diào)用系統(tǒng)真的起不來的情況,這時候就需要采取一定的補償措施了。首先要做的就是在我們自己的系統(tǒng)增加錯誤報警機制,這樣我們才能即時感知到應(yīng)用發(fā)生了不可自恢復的調(diào)用異常。其次就是在我們的代碼邏輯中加入觸發(fā)手動重試的開關(guān),這樣在發(fā)生異常情況以后我們就可以方便的修改觸發(fā)開關(guān)然后手動重試。
在這里還有一個非常重要的問題需要考慮,那就是接口調(diào)用的冪等性問題,如果接口不是冪等的,那我們手動重試的時候就很容易發(fā)生數(shù)據(jù)錯亂相關(guān)的問題。
Spring重試工具包
Spring為我們提供了原生的重試類庫,我們可以方便地引入到工程當中,利用它提供的重試注解,沒有太多的業(yè)務(wù)邏輯侵入性。如下,我們先引入依賴包。
<dependency> <groupId>org.springframework.retry</groupId> <artifactId>spring-retry</artifactId></dependency><dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId></dependency>
然后在啟動類或者配置類上添加@EnableRetry注解,并在需要重試的方法上添加@Retryable注解。
@Retryablepublic String hello(){ long times = helloTimes.incrementAndGet(); log.info("hello times:{}", times); if (times % 4 != 0){ log.warn("發(fā)生異常,time:{}", LocalTime.now() ); thrownew HelloRetryException("發(fā)生Hello異常"); } return"hello " + nameService.getName();}
更詳細的用法和屬性大家參閱Spring Retry的文檔就好了,解釋的非常清楚。這里要說的一點是,Spring的重試機制也還是存在一定的不足,只支持對異常進行捕獲,而無法對返回值進行校驗。
Guava Retry
相比Spring Retry,Guava Retry具有更強的靈活性,可以根據(jù)返回值校驗來判斷是否需要進行重試。我們依然需要先引入它的依賴包。
<dependency> <groupId>com.github.rholder</groupId> <artifactId>guava-retrying</artifactId> <version>2.0.0</version></dependency>
在用的時候也很簡單,先創(chuàng)建一個Retryer實例,然后使用這個實例對需要重試的方法進行調(diào)用,可以通過很多方法來設(shè)置重試機制,比如使用retryIfException來對所有異常進行重試,使用retryIfExceptionOfType方法來設(shè)置對指定異常進行重試,使用retryIfResult來對不符合預期的返回結(jié)果進行重試,使用retryIfRuntimeException方法來對所有RuntimeException進行重試。
@Testpublic void guavaRetry() { Retryer<String> retryer = RetryerBuilder.<String>newBuilder() .retryIfExceptionOfType(HelloRetryException.class) .retryIfResult(StringUtils::isEmpty) .withWaitStrategy(WaitStrategies.fixedWait(3, TimeUnit.SECONDS)) .withStopStrategy(StopStrategies.stopAfterAttempt(3)) .build(); try { retryer.call(() -> helloService.hello()); } catch (Exception e){ e.printStackTrace(); }}
相比Spring,Guava Retry提供了幾個核心特性。
- 可以設(shè)置任務(wù)單次執(zhí)行的時間限制,如果超時則拋出異常。
- 可以設(shè)置重試監(jiān)聽器,用來執(zhí)行額外的處理工作。
- 可以設(shè)置任務(wù)阻塞策略,即可以設(shè)置當前重試完成,下次重試開始前的這段時間做什么事情。
- 可以通過停止重試策略和等待策略結(jié)合使用來設(shè)置更加靈活的策略,比如指數(shù)等待時長并最多10次調(diào)用,隨機等待時長并永不停止等等。
小結(jié)
上面針對我們?yōu)槭裁匆胫卦嚈C制,引入重試機制需要思考的幾個核心問題,以及為重試機制提供良好支持的工具類庫都分別作了簡單介紹,相信大家在今后的開發(fā)工作中遇到類似場景也能駕輕就熟地使用思考了。
我們?nèi)粘9ぷ髦杏泻芏唷按蟆钡臉I(yè)務(wù)場景需要我們集中精力去突破、去思考,但也有很多類似的“小”點需要我們?nèi)ゴ虼⒊酝?,大家共勉?/p>