如何在業(yè)務(wù)邏輯當中優(yōu)雅引入重試機制

為什么要引入重試機制

我們首先看看正常的業(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>

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容