強悍的Spring之Spring Retry

強悍的Spring之Spring Retry


在日常開發(fā)中,我們經(jīng)常會遇到需要調(diào)用外部服務和接口的場景。外部服務對于調(diào)用者來說一般都是不可靠的,尤其是在網(wǎng)絡環(huán)境比較差的情況下,網(wǎng)絡抖動很容易導致請求超時等異常情況,這時候就需要使用失敗重試策略重新調(diào)用 API 接口來獲取。重試策略在服務治理方面也有很廣泛的使用,通過定時檢測,來查看服務是否存活。

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一樣,默認空,當exclude也為空時,所有異常都重試
  • exclude:指定異常不重試,默認空,當include也為空時,所有異常都重試
  • maxAttemps:重試次數(shù),默認3
  • backoff:重試補償機制,默認沒有

@Backoff注解

  • delay:指定延遲后重試
  • multiplier:指定延遲的倍數(shù),比如delay=5000l,multiplier=2時,第一次重試為5秒后,第二次為10秒,第三次為20秒

@Recover注解:
當重試到達指定次數(shù)時,被注解的方法將被回調(diào),可以在該方法中進行日志處理。需要注意的是發(fā)生的異常和入?yún)㈩愋鸵恢聲r才會回調(diào)。

@Service
public class RemoteService {

@Retryable(value = {Exception.class}, maxAttempts = 5, backoff = @Backoff(delay = 5000L, multiplier = 1))
public void call() {
    System.out.println(LocalDateTime.now() + ": do something...");
    throw new RuntimeException(LocalDateTime.now() + ": 運行調(diào)用異常");
}

@Recover
public void recover(Exception e) {
    System.out.println(e.getMessage());
}

3.啟用重試功能

啟動類上面添加@EnableRetry注解,啟用重試功能,或者在使用retry的service上面添加也可以,或者Configuration配置類上面。

建議所有的Enable配置加在啟動類上,可以清晰的統(tǒng)一管理使用的功能。

@SpringBootApplication
@EnableRetry
public class App {
    public static void main(String[] args) throws Exception {
        ConfigurableApplicationContext context = SpringApplication.run(App.class, args);
        System.out.println("Start app success.");
        RemoteService bean = context.getBean(RemoteService.class);
        bean.call();
    }
}

4.啟動服務,運行測試

通過在啟動類Context調(diào)用服務看到如下打印:

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)定的遠程服務對話的策略。)

第一步引入maven坐標:

 <dependency>
    <groupId>com.github.rholder</groupId>
    <artifactId>guava-retrying</artifactId>
    <version>2.0.0</version>
</dependency>

1.其主要接口及策略介紹

  • Attempt:一次執(zhí)行任務;
  • AttemptTimeLimiter:單次任務執(zhí)行時間限制(如果單次任務執(zhí)行超時,則終止執(zhí)行當前任務);
  • BlockStrategies:任務阻塞策略(通俗的講就是當前任務執(zhí)行完,下次任務還沒開始這段時間做什么……),默認策略為:BlockStrategies.THREAD_SLEEP_STRATEGY 也就是調(diào)用 Thread.sleep(sleepTime);
  • RetryException:重試異常;
  • RetryListener:自定義重試監(jiān)聽器,可以用于異步記錄錯誤日志;
  • StopStrategy:停止重試策略,提供三種:
  • StopAfterDelayStrategy :設定一個最長允許的執(zhí)行時間;比如設定最長執(zhí)行10s,無論任務執(zhí)行次數(shù),只要重試的時候超出了最長時間,則任務終止,并返回重試異常RetryException;
  • NeverStopStrategy :不停止,用于需要一直輪訓直到返回期望結果的情況;
  • StopAfterAttemptStrategy :設定最大重試次數(shù),如果超出最大重試次數(shù)則停止重試,并返回重試異常;
  • WaitStrategy:等待時長策略(控制時間間隔),返回結果為下次執(zhí)行時長:
  • FixedWaitStrategy:固定等待時長策略;
  • RandomWaitStrategy:隨機等待時長策略(可以提供一個最小和最大時長,等待時長為其區(qū)間隨機值)
  • IncrementingWaitStrategy:遞增等待時長策略(提供一個初始值和步長,等待時間隨重試次數(shù)增加而增加)
  • ExponentialWaitStrategy:指數(shù)等待時長策略;
  • FibonacciWaitStrategy :Fibonacci 等待時長策略;
  • ExceptionWaitStrategy :異常時長等待策略;
  • CompositeWaitStrategy :復合時長等待策略;

2.根據(jù)結果判斷是否重試

使用場景:如果返回值決定是否要重試。

重試接口:

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);
            if (counter < 5) {
                return "james";
            }
            return "kobe";
        }
    };
}

測試:

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... 5

3.根據(jù)異常判斷是否重試

使用場景:根據(jù)拋出異常類型判斷是否執(zhí)行重試。
重試接口:

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);
            if (counter < 5) {
                throw new RuntimeException("Run exception");
            }
            return "kobe";
        }
    };
}

測試:

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... 5

4.重試策略——設定無限重試

使用場景:在有異常情況下,無限重試(默認執(zhí)行策略),直到返回正常有效結果;

 Retryer<String> retry = RetryerBuilder.<String>newBuilder()
            .retryIfRuntimeException()
            .withStopStrategy(StopStrategies.neverStop())
            .build();
    retry.call(callableWithResult());

5.重試策略——設定最大的重試次數(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.等待策略——設定重試等待固定時長策略

使用場景:設定每次重試等待間隔固定為10s;

 public static void main(String[] args) throws ExecutionException, RetryExceptio{
    Retryer<String> retry = RetryerBuilder.<String>newBuilder()
            .retryIfRuntimeException()
            .withStopStrategy(StopStrategies.stopAfterAttempt(4))
            .withWaitStrategy(WaitStrategies.fixedWait(10, TimeUnit.SECONDS))
            .build();
    retry.call(callableWithResult());
}

測試輸出,可以看出調(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.等待策略——設定重試等待時長固定增長策略

場景:設定初始等待時長值,并設定固定增長步長,但不設定最大等待時長;

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();

    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ù)增長策略

使用場景:根據(jù)multiplier值按照指數(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)⒉皇呛芏?,好吧,查看源碼:

@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;
    }

    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)部類,構造器中校驗入?yún)?,最重要的延遲時間計算方法computeSleepTime(),可以看出延遲時間計算方式

  1. 計算以2為底失敗次數(shù)為指數(shù)的值
  2. 第一步的值構造器第一個入?yún)⑾喑?,然后四舍五入得到延遲時間(毫秒)

通過以上分析可知入?yún)?000時間隔是應該為2,4,8s

測試輸出,可以看出調(diào)用間隔時間 2×1000,4×1000,8×1000:

2019-03-09T19:11:23.905: do something... 1
2019-03-09T19:11:25.908: do something... 2
2019-03-09T19:11:29.908: do something... 3
2019-03-09T19:11:37.909: do something... 4
Exception in thread "main" com.github.rholder.retry.RetryException: Retrying failed to complete successfully after 4 attempts.

9.等待策略——設定重試等待時長按斐波那契數(shù)列策略

使用場景:根據(jù)multiplier值按照斐波那契數(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();

    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.等待策略——組合重試等待時長策略

使用場景:當現(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;

    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... 8

11.監(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());
    // 重試結果: 是異常終止, 還是正常返回
    System.out.println("hasException=" + attempt.hasException());
    System.out.println("hasResult=" + attempt.hasResult());
    // 是什么原因?qū)е庐惓?    if (attempt.hasException()) {
        System.out.println("causeBy=" + attempt.getExceptionCause());
    } else {
        // 正常返回時的結果
        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());
    }
}

測試:

    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());
}

輸出:

2019-03-09T16:32:35.097: do something... 1
retry times=1
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.

總結

兩種方式都是比較優(yōu)雅的重試策略,Spring-retry配置更簡單,實現(xiàn)的功能也相對簡單,Guava本身就是谷歌推出的精品java類庫,guava-retry也是功能非常強大,相比較于Spring-Retry在是否重試的判斷條件上有更多的選擇性,可以作為Spring-retry的補充。

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

相關閱讀更多精彩內(nèi)容

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