重試利器之Guava-Retryer

AskHarries發表於2018-06-06

寫在前面

在日常開發中,我們經常會遇到需要呼叫外部服務和介面的場景。外部服務對於呼叫者來說一般都是不可靠的,尤其是在網路環境比較差的情況下,網路抖動很容易導致請求超時等異常情況,這時候就需要使用失敗重試策略重新呼叫 API 介面來獲取。重試策略在服務治理方面也有很廣泛的使用,通過定時檢測,來檢視服務是否存活(Active)。

Guava Retrying是一個靈活方便的重試元件,包含了多種的重試策略,而且擴充套件起來非常容易。

用作者的話來說:

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.

使用Guava-retrying你可以自定義來執行重試,同時也可以監控每次重試的結果和行為,最重要的基於 Guava 風格的重試方式真的很方便。

程式碼示例

  • 引入Guava-retry
<guava-retry.version>2.0.0</guava-retry.version>
<dependency>
      <groupId>com.github.rholder</groupId>
      <artifactId>guava-retrying</artifactId>
      <version>${guava-retry.version}</version>
</dependency>複製程式碼
  • 定義實現Callable介面的方法,以便Guava retryer能夠呼叫
/**
    * @desc 更新可代理報銷人介面
    * @author jianzhang11
    * @date 2017/3/31 15:17
    */
   private static Callable<Boolean> updateReimAgentsCall = new Callable<Boolean>() {
       @Override
       public Boolean call() throws Exception {
           String url = ConfigureUtil.get(OaConstants.OA_REIM_AGENT);
           String result = HttpMethod.post(url, new ArrayList<BasicNameValuePair>());
           if(StringUtils.isEmpty(result)){
              throw new RemoteException("獲取OA可報銷代理人介面異常");
           }
           List<OAReimAgents> oaReimAgents = JSON.parseArray(result, OAReimAgents.class);
           if(CollectionUtils.isNotEmpty(oaReimAgents)){
               CacheUtil.put(Constants.REIM_AGENT_KEY,oaReimAgents);
               return true;
           }
           return false;
       }
   };複製程式碼
  • 定義Retry物件並設定相關策略
Retryer<Boolean> retryer = RetryerBuilder
                .<Boolean>newBuilder()
                //丟擲runtime異常、checked異常時都會重試,但是丟擲error不會重試。
                .retryIfException()
                //返回false也需要重試
                .retryIfResult(Predicates.equalTo(false))
                //重調策略
                .withWaitStrategy(WaitStrategies.fixedWait(10, TimeUnit.SECONDS))
                //嘗試次數
                .withStopStrategy(StopStrategies.stopAfterAttempt(3))
                .build();
 
        try {
            retryer.call(updateReimAgentsCall);
        } catch (ExecutionException e) {
//            e.printStackTrace();
        } catch (RetryException e) {
            logger.error("更新可代理報銷人異常,需要傳送提醒郵件");
        }複製程式碼

簡單三步就能使用Guava Retryer優雅的實現重調方法。
接下來對其進行詳細說明:

  • RetryerBuilder是一個factory建立者,可以定製設定重試源且可以支援多個重試源,可以配置重試次數或重試超時時間,以及可以配置等待時間間隔,建立重試者Retryer例項。
  • RetryerBuilder的重試源支援Exception異常物件 和自定義斷言物件,通過retryIfExceptionretryIfResult設定,同時支援多個且能相容。
  • retryIfException,丟擲runtime異常、checked異常時都會重試,但是丟擲error不會重試。
  • retryIfRuntimeException只會在拋runtime異常的時候才重試,checked異常和error都不重試。
  • retryIfExceptionOfType允許我們只在發生特定異常的時候才重試,比如NullPointerException和IllegalStateException`都屬於runtime異常,也包括自定義的error

如:

.retryIfExceptionOfType(Error.class)// 只在丟擲error重試複製程式碼

當然我們還可以在只有出現指定的異常的時候才重試,如:

.retryIfExceptionOfType(IllegalStateException.class)  
.retryIfExceptionOfType(NullPointerException.class)複製程式碼

或者通過Predicate實現

.retryIfException(Predicates.or(Predicates.instanceOf(NullPointerException.class),  
                Predicates.instanceOf(IllegalStateException.class)))複製程式碼

retryIfResult可以指定你的Callable方法在返回值的時候進行重試,如

// 返回false重試 
.retryIfResult(Predicates.equalTo(false))  
//以_error結尾才重試 
.retryIfResult(Predicates.containsPattern("_error$"))  複製程式碼

當發生重試之後,假如我們需要做一些額外的處理動作,比如發個告警郵件啥的,那麼可以使用RetryListener。每次重試之後,guava-retrying會自動回撥我們註冊的監聽。可以註冊多個RetryListener,會按照註冊順序依次呼叫。

import com.github.rholder.retry.Attempt;  
import com.github.rholder.retry.RetryListener;  
  
import java.util.concurrent.ExecutionException;  
  
public class MyRetryListener<Boolean> implements RetryListener {  
  
    @Override  
    public <Boolean> void onRetry(Attempt<Boolean> attempt) {  
  
        // 第幾次重試,(注意:第一次重試其實是第一次呼叫)  
        System.out.print("[retry]time=" + attempt.getAttemptNumber());  
  
        // 距離第一次重試的延遲  
        System.out.print(",delay=" + attempt.getDelaySinceFirstAttempt());  
  
        // 重試結果: 是異常終止, 還是正常返回  
        System.out.print(",hasException=" + attempt.hasException());  
        System.out.print(",hasResult=" + attempt.hasResult());  
  
        // 是什麼原因導致異常  
        if (attempt.hasException()) {  
            System.out.print(",causeBy=" + attempt.getExceptionCause().toString());  
        } else {  
            // 正常返回時的結果  
            System.out.print(",result=" + attempt.getResult());  
        }  
  
        // bad practice: 增加了額外的異常處理程式碼  
        try {  
            Boolean result = attempt.get();  
            System.out.print(",rude get=" + result);  
        } catch (ExecutionException e) {  
            System.err.println("this attempt produce exception." + e.getCause().toString());  
        }  
  
        System.out.println();  
    }  
} 複製程式碼

接下來在Retry物件中指定監聽:

.withRetryListener(new MyRetryListener<>())複製程式碼

參考資料:

blog.csdn.net/aitangyong/…

segmentfault.com/a/119000000…

blog.csdn.net/aitangyong/…

baijiahao.baidu.com/s?id=157532…

www.cnblogs.com/jianzh5/p/6…

重試利器之Guava-Retryer


相關文章