消息重試框架 Spring-Retry 和 Guava-Retry,這個框架有點意思

一 重試框架之 Spring-Retry

Spring Retry 爲 Spring 應用程序提供了聲明性重試支持。它用於 Spring 批處理、Spring 集成、Apache Hadoop(等等)。它主要是針對可能拋出異常的一些調用操作,進行有策略的重試

1. Spring-Retry 的普通使用方式

1. 準備工作

我們只需要加上依賴:

 <dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
    <version>1.2.2.RELEASE</version>
 </dependency>

準備一個任務方法,我這裏是採用一個隨機整數,根據不同的條件返回不同的值,或者拋出異常

package com.zgd.demo.thread.retry;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomUtils;
import org.springframework.remoting.RemoteAccessException;

/**
 * @Author: zgd
 * @Description:
 */
@Slf4j
public class RetryDemoTask {


  /**
   * 重試方法
   * @return
   */
  public static boolean retryTask(String param)  {
    log.info("收到請求參數:{}",param);

    int i = RandomUtils.nextInt(0,11);
    log.info("隨機生成的數:{}",i);
    if (i == 0) {
      log.info("爲0,拋出參數異常.");
      throw new IllegalArgumentException("參數異常");
    }else if (i  == 1){
      log.info("爲1,返回true.");
      return true;
    }else if (i == 2){
      log.info("爲2,返回false.");
      return false;
    }else{
      //爲其他
        log.info("大於2,拋出自定義異常.");
        throw new RemoteAccessException("大於2,拋出遠程訪問異常");
      }
    }

}
2. 使用 SpringRetryTemplate

這裏可以寫我們的代碼了

package com.zgd.demo.thread.retry.spring;

import com.zgd.demo.thread.retry.RetryDemoTask;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.springframework.remoting.RemoteAccessException;
import org.springframework.retry.backoff.FixedBackOffPolicy;
import org.springframework.retry.policy.SimpleRetryPolicy;
import org.springframework.retry.support.RetryTemplate;

import java.util.HashMap;
import java.util.Map;

/**
 * @Author: zgd
 * @Description: spring-retry 重試框架
 */
@Slf4j
public class SpringRetryTemplateTest {

  /**
   * 重試間隔時間ms,默認1000ms
   * */
  private long fixedPeriodTime = 1000L;
  /**
   * 最大重試次數,默認爲3
   */
  private int maxRetryTimes = 3;
  /**
   * 表示哪些異常需要重試,key表示異常的字節碼,value爲true表示需要重試
   */
  private Map<Class<? extends Throwable>, Boolean> exceptionMap = new HashMap<>();


  @Test
  public void test() {
    exceptionMap.put(RemoteAccessException.class,true);

    // 構建重試模板實例
    RetryTemplate retryTemplate = new RetryTemplate();

    // 設置重試回退操作策略,主要設置重試間隔時間
    FixedBackOffPolicy backOffPolicy = new FixedBackOffPolicy();
    backOffPolicy.setBackOffPeriod(fixedPeriodTime);

    // 設置重試策略,主要設置重試次數
    SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy(maxRetryTimes, exceptionMap);

    retryTemplate.setRetryPolicy(retryPolicy);
    retryTemplate.setBackOffPolicy(backOffPolicy);

    Boolean execute = retryTemplate.execute(
            //RetryCallback
            retryContext -> {
              boolean b = RetryDemoTask.retryTask("abc");
              log.info("調用的結果:{}", b);
              return b;
            },
            retryContext -> {
              //RecoveryCallback
              log.info("已達到最大重試次數或拋出了不重試的異常~~~");
              return false;
            }
      );

    log.info("執行結果:{}",execute);

  }

}

簡單剖析下案例代碼,RetryTemplate承擔了重試執行者的角色,它可以設置SimpleRetryPolicy(重試策略,設置重試上限,重試的根源實體),FixedBackOffPolicy(固定的回退策略,設置執行重試回退的時間間隔)。

RetryTemplate通過execute提交執行操作,需要準備RetryCallbackRecoveryCallback兩個類實例,前者對應的就是重試回調邏輯實例,包裝正常的功能操作,RecoveryCallback實現的是整個執行操作結束的恢復操作實例.

只有在調用的時候拋出了異常,並且異常是在exceptionMap中配置的異常,纔會執行重試操作,否則就調用到excute方法的第二個執行方法RecoveryCallback

當然, 重試策略還有很多種, 回退策略也是:

重試策略

重試回退策略

重試回退策略,指的是每次重試是立即重試還是等待一段時間後重試。

默認情況下是立即重試,如果需要配置等待一段時間後重試則需要指定回退策略BackoffRetryPolicy

我們可以根據自己的應用場景和需求,使用不同的策略,不過一般使用默認的就足夠了。

上面的代碼的話, 我簡單的設置了重試間隔爲 1 秒,重試的異常是RemoteAccessException,下面就是測試代碼的情況:    重試第二次成功的情況:

重試一次以後,遇到了沒有指出需要重試的異常,直接結束重試,調用retryContext

重試了三次後,達到了最大重試次數,調用retryContext

2. Spring-Retry 的註解使用方式

既然是 Spring 家族的東西,那麼自然就支持和 Spring-Boot 整合

1. 準備工作

依賴:

 <dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
    <version>1.2.2.RELEASE</version>
 </dependency>

 <dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.1</version>
 </dependency>
2. 代碼

在 application 啓動類上加上@EnableRetry的註解

@EnableRetry
public class Application {
 ...
}

爲了方便測試,我這裏寫了一個SpringBootTest的測試基類,需要使用SpringBootTest的只要繼承這個類就好了

package com.zgd.demo.thread.test;

/**
 * @Author: zgd
 * @Description:
 */

import com.zgd.demo.thread.Application;
import lombok.extern.slf4j.Slf4j;
import org.junit.After;
import org.junit.Before;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

/**
 * @Author: zgd
 * @Date: 18/09/29 20:33
 * @Description:
 */
@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
@Slf4j
public class MyBaseTest {


  @Before
  public void init() {
    log.info("----------------測試開始---------------");
  }

  @After
  public void after() {
    log.info("----------------測試結束---------------");
  }

}

我們只要在需要重試的方法上加@Retryable,在重試失敗的回調方法上加@Recover,下面是這些註解的屬性

建一個 service 類

package com.zgd.demo.thread.retry.spring;

import com.zgd.demo.thread.retry.RetryDemoTask;
import com.zgd.demo.thread.test.MyBaseTest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.remoting.RemoteAccessException;
import org.springframework.retry.ExhaustedRetryException;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Recover;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Component;

/**
 * @Author: zgd
 * @Description:
 */
@Service
@Slf4j
public class SpringRetryDemo   {

 /**
   * 重試所調用方法
   * @param param
   * @return
   */
  @Retryable(value = {RemoteAccessException.class},maxAttempts = 3,backoff = @Backoff(delay = 2000L,multiplier = 2))
  public boolean call(String param){
      return RetryDemoTask.retryTask(param);
  }

  /**
   * 達到最大重試次數,或拋出了一個沒有指定進行重試的異常
   * recover 機制
   * @param e 異常
   */
  @Recover
  public boolean recover(Exception e,String param) {
    log.error("達到最大重試次數,或拋出了一個沒有指定進行重試的異常:",e);
    return false;
  }

}

然後我們調用這個 service 裏面的 call 方法

package com.zgd.demo.thread.retry.spring;

import com.zgd.demo.thread.test.MyBaseTest;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * @Author: zgd
 * @Description:
 */
@Component
@Slf4j
public class SpringRetryDemoTest extends MyBaseTest {

  @Autowired
  private SpringRetryDemo springRetryDemo;

  @Test
  public void retry(){
    boolean abc = springRetryDemo.call("abc");
    log.info("--結果是:{}--",abc);
  }

}

這裏我依然是RemoteAccessException的異常才重試,@Backoff(delay = 2000L,multiplier = 2))表示第一次間隔 2 秒,以後都是次數的 2 倍, 也就是第二次 4 秒,第三次 6 秒.

來測試一下:

遇到了沒有指定重試的異常, 這裏指定重試的異常是@Retryable(value = {RemoteAccessException.class}..., 所以拋出參數異常IllegalArgumentException的時候,直接回調 @Recover 的方法

重試達到最大重試次數時,調用@Recover的方法

重試到最後一次沒有報錯,返回 false

二 重試框架之 Guava-Retry

Guava retryer 工具與 spring-retry 類似,都是通過定義重試者角色來包裝正常邏輯重試,但是 Guava retryer 有更優的策略定義,在支持重試次數和重試頻度控制基礎上,能夠兼容支持多個異常或者自定義實體對象的重試源定義,讓重試功能有更多的靈活性。

Guava Retryer 也是線程安全的,入口調用邏輯採用的是Java.util.concurrent.Callable的 call 方法,示例代碼如下:

pom.xml 加入依賴

  <!-- https://mvnrepository.com/artifact/com.github.rholder/guava-retrying -->
    <dependency>
        <groupId>com.github.rholder</groupId>
        <artifactId>guava-retrying</artifactId>
        <version>2.0.0</version>
    </dependency>

更改一下測試的任務方法

package com.zgd.demo.thread.retry;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomUtils;
import org.springframework.remoting.RemoteAccessException;

/**
 * @Author: zgd
 * @Description:
 */
@Slf4j
public class RetryDemoTask {


  /**
   * 重試方法
   * @return
   */
  public static boolean retryTask(String param)  {
    log.info("收到請求參數:{}",param);

    int i = RandomUtils.nextInt(0,11);
    log.info("隨機生成的數:{}",i);
    if (i < 2) {
      log.info("爲0,拋出參數異常.");
      throw new IllegalArgumentException("參數異常");
    }else if (i  < 5){
      log.info("爲1,返回true.");
      return true;
    }else if (i < 7){
      log.info("爲2,返回false.");
      return false;
    }else{
      //爲其他
        log.info("大於2,拋出自定義異常.");
        throw new RemoteAccessException("大於2,拋出自定義異常");
      }
    }

}

Guava

這裏設定跟 Spring-Retry 不一樣,我們可以根據返回的結果來判斷是否重試,比如返回 false 我們就重試

package com.zgd.demo.thread.retry.guava;

import com.github.rholder.retry.*;
import com.zgd.demo.thread.retry.RetryDemoTask;
import org.junit.Test;
import org.springframework.remoting.RemoteAccessException;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;

/**
 * @Author: zgd
 * @Description:
 */
public class GuavaRetryTest {


  @Test
  public void fun01(){
    // RetryerBuilder 構建重試實例 retryer,可以設置重試源且可以支持多個重試源,可以配置重試次數或重試超時時間,以及可以配置等待時間間隔
    Retryer<Boolean> retryer = RetryerBuilder.<Boolean> newBuilder()
            .retryIfExceptionOfType(RemoteAccessException.class)//設置異常重試源
            .retryIfResult(res-> res==false)  //設置根據結果重試
            .withWaitStrategy(WaitStrategies.fixedWait(3, TimeUnit.SECONDS)) //設置等待間隔時間
            .withStopStrategy(StopStrategies.stopAfterAttempt(3)) //設置最大重試次數
            .build();

    try {
      retryer.call(() -> RetryDemoTask.retryTask("abc"));
    } catch (Exception e) {
      e.printStackTrace();
    }
  }

}

運行測試一下

遇到了我們指定的需要重試的異常,進行重試,間隔是 3 秒

重試次數超過了最大重試次數

返回爲 true,直接結束重試

遇到了沒有指定重試的異常,結束重試

返回 false,重試

我們可以更靈活的配置重試策略,比如:

如:

retryIfExceptionOfType(NullPointerException.class)// 只在拋出空指針異常重試
// 返回false重試  
.retryIfResult(Predicates.equalTo(false))   

//以_error結尾才重試  
.retryIfResult(Predicates.containsPattern("_error$"))

//返回爲空時重試
.retryIfResult(res-> res==null)
.withRetryListener(new RetryListener {      
 @Override    
   public <T> void onRetry(Attempt<T> attempt) {  
               logger.error("第【{}】次調用失敗" , attempt.getAttemptNumber());  
          } 
 }
)

總結

spring-retry 和 guava-retry 工具都是線程安全的重試,能夠支持併發業務場景的重試邏輯正確性。兩者都很好的將正常方法和重試方法進行了解耦,可以設置超時時間、重試次數、間隔時間、監聽結果、都是不錯的框架。

但是明顯感覺得到,guava-retry 在使用上更便捷,更靈活,能根據方法返回值來判斷是否重試,而 Spring-retry 只能根據拋出的異常來進行重試。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/rFaiO9yxsWbHuKD2pOs94w