使用 Guava-Retry 優雅的實現重處理

大家好,我是陳哥~

在日常開發中,尤其是在微服務盛行的時代下,我們在調用外部接口時,經常會因爲第三方接口超時、限流等問題從而造成接口調用失敗,那麼此時我們通常會對接口進行重試,那麼問題來了,如何重試呢?該重試幾次呢?如果要設置重試時間超過多長時間後還不成功就不重試了該怎麼做呢?所幸 guava-retrying 爲我們提供了強大而簡單易用的重試框架 guava-retrying。

guava-retrying 是谷歌的 Guava 庫的一個小擴展,允許爲任意函數調用創建可配置的重試策略,比如與正常運行時間不穩定的遠程服務對話的函數調用。

在前面陳哥也介紹過一種重試框架 Spring-RetrySpring Boot 優雅的實現重處理功能

1. pom 依賴

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

2. 使用示例

我們可以通過 RetryerBuilder 來構造一個重試器,通過 RetryerBuilder 可以設置什麼時候需要重試(即重試時機)、停止重試策略、失敗等待時間間隔策略、任務執行時長限制策略

先看一個簡單的例子:

    private int invokeCount = 0;

    public int realAction(int num) {
        invokeCount++;
        System.out.println(String.format("當前執行第 %d 次,num:%d", invokeCount, num));
        if (num <= 0) {
            throw new IllegalArgumentException();
        }
        return num;
    }

    @Test
    public void guavaRetryTest001() {
        Retryer<Integer> retryer = RetryerBuilder.<Integer>newBuilder()
            // 非正數進行重試
            .retryIfRuntimeException()
            // 偶數則進行重試
            .retryIfResult(result -> result % 2 == 0)
            // 設置最大執行次數3次
            .withStopStrategy(StopStrategies.stopAfterAttempt(3)).build();

        try {
            invokeCount=0;
            retryer.call(() -> realAction(0));
        } catch (Exception e) {
            System.out.println("執行0,異常:" + e.getMessage());
        }

        try {
            invokeCount=0;
            retryer.call(() -> realAction(1));
        } catch (Exception e) {
            System.out.println("執行1,異常:" + e.getMessage());
        }

        try {
            invokeCount=0;
            retryer.call(() -> realAction(2));
        } catch (Exception e) {
            System.out.println("執行2,異常:" + e.getMessage());
        }
    }

輸出:

當前執行第 1 次,num:0
當前執行第 2 次,num:0
當前執行第 3 次,num:0
執行0,異常:Retrying failed to complete successfully after 3 attempts.
當前執行第 1 次,num:1
當前執行第 1 次,num:2
當前執行第 2 次,num:2
當前執行第 3 次,num:2
執行2,異常:Retrying failed to complete successfully after 3 attempts.

3. 重試時機

RetryerBuilder 的 retryIfXXX() 方法用來設置 ** 在什麼情況下進行重試,總體上可以分爲根據執行異常進行重試根據方法執行結果進行重試兩類。關注公衆號:“碼猿技術專欄”,回覆關鍵詞:“1111” 獲取阿里內部 Java 調優手冊

3.1 根據異常進行重試

AarCGQ

3.2 根據返回結果進行重試

retryIfResult(@Nonnull Predicate resultPredicate) 這個比較簡單,當我們傳入的 resultPredicate 返回 true 時則進行重試

4. 停止重試策略 StopStrategy

停止重試策略用來決定什麼時候不進行重試,其接口 com.github.rholder.retry.StopStrategy,停止重試策略的實現類均在 com.github.rholder.retry.StopStrategies 中,它是一個策略工廠類。

public interface StopStrategy {

    /**
     * Returns <code>true</code> if the retryer should stop retrying.
     *
     * @param failedAttempt the previous failed {@code Attempt}
     * @return <code>true</code> if the retryer must stop, <code>false</code> otherwise
     */
    boolean shouldStop(Attempt failedAttempt);
}

4.1 NeverStopStrategy

此策略將永遠重試,永不停止,查看其實現類,直接返回了 false

@Override
public boolean shouldStop(Attempt failedAttempt) {
 return false;
}

4.2 StopAfterAttemptStrategy

當執行次數到達指定次數之後停止重試,查看其實現類:

    private static final class StopAfterAttemptStrategy implements StopStrategy {
        private final int maxAttemptNumber;

        public StopAfterAttemptStrategy(int maxAttemptNumber) {
            Preconditions.checkArgument(maxAttemptNumber >= 1, "maxAttemptNumber must be >= 1 but is %d", maxAttemptNumber);
            this.maxAttemptNumber = maxAttemptNumber;
        }

        @Override
        public boolean shouldStop(Attempt failedAttempt) {
            return failedAttempt.getAttemptNumber() >= maxAttemptNumber;
        }
    }

4.3 StopAfterDelayStrategy

當距離方法的第一次執行超出了指定的 delay 時間時停止,也就是說一直進行重試,當進行下一次重試的時候會判斷從第一次執行到現在的所消耗的時間是否超過了這裏指定的 delay 時間,查看其實現:

   private static final class StopAfterAttemptStrategy implements StopStrategy {
        private final int maxAttemptNumber;

        public StopAfterAttemptStrategy(int maxAttemptNumber) {
            Preconditions.checkArgument(maxAttemptNumber >= 1, "maxAttemptNumber must be >= 1 but is %d", maxAttemptNumber);
            this.maxAttemptNumber = maxAttemptNumber;
        }

        @Override
        public boolean shouldStop(Attempt failedAttempt) {
            return failedAttempt.getAttemptNumber() >= maxAttemptNumber;
        }
    }

5. 重試間隔策略、重試阻塞策略

這兩個策略放在一起說,它們合起來的作用就是用來控制重試任務之間的間隔時間,以及如何任務在等待時間間隔時如何阻塞。也就是說 WaitStrategy 決定了重試任務等待多久後進行下一次任務的執行,BlockStrategy 用來決定任務如何等待。它們兩的策略工廠分別爲 com.github.rholder.retry.WaitStrategies 和 BlockStrategies。關注公衆號:“碼猿技術專欄”,回覆關鍵詞:“1111” 獲取阿里內部 Java 調優手冊

5.1 BlockStrategy

5.1.1 ThreadSleepStrategy

這個是 BlockStrategies,決定如何阻塞任務,其主要就是通過 Thread.sleep() 來進行阻塞的,查看其實現:

    @Immutable
    private static class ThreadSleepStrategy implements BlockStrategy {

        @Override
        public void block(long sleepTime) throws InterruptedException {
            Thread.sleep(sleepTime);
        }
    }

5.2 WaitStrategy

5.2.1 IncrementingWaitStrategy

該策略在決定任務間隔時間時,返回的是一個遞增的間隔時間,即每次任務重試間隔時間逐步遞增,越來越長,查看其實現:

    private static final class IncrementingWaitStrategy implements WaitStrategy {
        private final long initialSleepTime;
        private final long increment;

        public IncrementingWaitStrategy(long initialSleepTime,
                                        long increment) {
            Preconditions.checkArgument(initialSleepTime >= 0L, "initialSleepTime must be >= 0 but is %d", initialSleepTime);
            this.initialSleepTime = initialSleepTime;
            this.increment = increment;
        }

        @Override
        public long computeSleepTime(Attempt failedAttempt) {
            long result = initialSleepTime + (increment * (failedAttempt.getAttemptNumber() - 1));
            return result >= 0L ? result : 0L;
        }
    }

該策略輸入一個起始間隔時間值和一個遞增步長,然後每次等待的時長都遞增 increment 時長。

5.2.2 RandomWaitStrategy

顧名思義,返回一個隨機的間隔時長,我們需要傳入的就是一個最小間隔和最大間隔,然後隨機返回介於兩者之間的一個間隔時長,其實現爲:

    private static final class RandomWaitStrategy implements WaitStrategy {
        private static final Random RANDOM = new Random();
        private final long minimum;
        private final long maximum;

        public RandomWaitStrategy(long minimum, long maximum) {
            Preconditions.checkArgument(minimum >= 0, "minimum must be >= 0 but is %d", minimum);
            Preconditions.checkArgument(maximum > minimum, "maximum must be > minimum but maximum is %d and minimum is", maximum, minimum);

            this.minimum = minimum;
            this.maximum = maximum;
        }

        @Override
        public long computeSleepTime(Attempt failedAttempt) {
            long t = Math.abs(RANDOM.nextLong()) % (maximum - minimum);
            return t + minimum;
        }
    }

5.2.3 FixedWaitStrategy

該策略是返回一個固定時長的重試間隔。查看其實現:

    private static final class FixedWaitStrategy implements WaitStrategy {
        private final long sleepTime;

        public FixedWaitStrategy(long sleepTime) {
            Preconditions.checkArgument(sleepTime >= 0L, "sleepTime must be >= 0 but is %d", sleepTime);
            this.sleepTime = sleepTime;
        }

        @Override
        public long computeSleepTime(Attempt failedAttempt) {
            return sleepTime;
        }
    }

5.2.4 ExceptionWaitStrategy

該策略是由方法執行異常來決定是否重試任務之間進行間隔等待,以及間隔多久。

    private static final class ExceptionWaitStrategy<T extends Throwable> implements WaitStrategy {
        private final Class<T> exceptionClass;
        private final Function<T, Long> function;

        public ExceptionWaitStrategy(@Nonnull Class<T> exceptionClass, @Nonnull Function<T, Long> function) {
            this.exceptionClass = exceptionClass;
            this.function = function;
        }

        @SuppressWarnings({"ThrowableResultOfMethodCallIgnored", "ConstantConditions", "unchecked"})
        @Override
        public long computeSleepTime(Attempt lastAttempt) {
            if (lastAttempt.hasException()) {
                Throwable cause = lastAttempt.getExceptionCause();
                if (exceptionClass.isAssignableFrom(cause.getClass())) {
                    return function.apply((T) cause);
                }
            }
            return 0L;
        }
    }

5.2.5 CompositeWaitStrategy

這個沒啥好說的,顧名思義,就是一個策略的組合,你可以傳入多個 WaitStrategy,然後所有 WaitStrategy 返回的間隔時長相加就是最終的間隔時間。查看其實現:

   private static final class CompositeWaitStrategy implements WaitStrategy {
        private final List<WaitStrategy> waitStrategies;

        public CompositeWaitStrategy(List<WaitStrategy> waitStrategies) {
            Preconditions.checkState(!waitStrategies.isEmpty(), "Need at least one wait strategy");
            this.waitStrategies = waitStrategies;
        }

        @Override
        public long computeSleepTime(Attempt failedAttempt) {
            long waitTime = 0L;
            for (WaitStrategy waitStrategy : waitStrategies) {
                waitTime += waitStrategy.computeSleepTime(failedAttempt);
            }
            return waitTime;
        }
    }

5.2.6 FibonacciWaitStrategy

這個策略與 IncrementingWaitStrategy 有點相似,間隔時間都是隨着重試次數的增加而遞增的,不同的是,FibonacciWaitStrategy 是按照斐波那契數列來進行計算的,使用這個策略時,我們需要傳入一個乘數因子和最大間隔時長,其實現就不貼了

5.2.7 ExponentialWaitStrategy

這個與 IncrementingWaitStrategy、FibonacciWaitStrategy 也類似,間隔時間都是隨着重試次數的增加而遞增的,但是該策略的遞增是呈指數級遞增。查看其實現:

    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", multiplier);
            Preconditions.checkArgument(maximumWait >= 0L, "maximumWait must be >= 0 but is %d", maximumWait);
            Preconditions.checkArgument(multiplier < maximumWait, "multiplier must be < maximumWait but is %d", multiplier);
            this.multiplier = multiplier;
            this.maximumWait = maximumWait;
        }

        @Override
        public long computeSleepTime(Attempt failedAttempt) {
            double exp = Math.pow(2, failedAttempt.getAttemptNumber());
            long result = Math.round(multiplier * exp);
            if (result > maximumWait) {
                result = maximumWait;
            }
            return result >= 0L ? result : 0L;
        }
    }

6. 重試監聽器 RetryListener

當發生重試時,將會調用 RetryListener 的 onRetry 方法,此時我們可以進行比如記錄日誌等額外操作。

    public int realAction(int num) {
        if (num <= 0) {
            throw new IllegalArgumentException();
        }
        return num;
    }

    @Test
    public void guavaRetryTest001() throws ExecutionException, RetryException {
        Retryer<Integer> retryer = RetryerBuilder.<Integer>newBuilder().retryIfException()
            .withRetryListener(new MyRetryListener())
            // 設置最大執行次數3次
            .withStopStrategy(StopStrategies.stopAfterAttempt(3)).build();
        retryer.call(() -> realAction(0));

    }

    private static class MyRetryListener implements RetryListener {

        @Override
        public <V> void onRetry(Attempt<V> attempt) {
            System.out.println("第" + attempt.getAttemptNumber() + "次執行");
        }
    }

輸出:

第1次執行
第2次執行
第3次執行

7. 重試原理

其實到這一步之後,實現原理大概就很清楚了,就是由上述各種策略配合從而達到了非常靈活的重試機制。在這之前我們看一個上面沒說的東東 - Attempt

public interface Attempt<V> {
    public V get() throws ExecutionException;

    public boolean hasResult();
    
    public boolean hasException();

    public V getResult() throws IllegalStateException;

    public Throwable getExceptionCause() throws IllegalStateException;

    public long getAttemptNumber();

    public long getDelaySinceFirstAttempt();
}

通過接口方法可以知道 Attempt 這個類包含了任務執行次數、任務執行異常、任務執行結果、以及首次執行任務至今的時間間隔,那麼我們後續的不管重試時機、還是其他策略都是根據此值來決定。

接下來看關鍵執行入口 Retryer##call:

    public V call(Callable<V> callable) throws ExecutionException, RetryException {
        long startTime = System.nanoTime();
        
        // 執行次數從1開始
        for (int attemptNumber = 1; ; attemptNumber++) {
            Attempt<V> attempt;
            try {
                // 嘗試執行
                V result = attemptTimeLimiter.call(callable);
                
                // 執行成功則將結果封裝爲ResultAttempt
                attempt = new Retryer.ResultAttempt<V>(result, attemptNumber, TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime));
            } catch (Throwable t) {
                // 執行異常則將結果封裝爲ExceptionAttempt
                attempt = new Retryer.ExceptionAttempt<V>(t, attemptNumber, TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime));
            }

            // 這裏將執行結果傳給RetryListener做一些額外事情
            for (RetryListener listener : listeners) {
                listener.onRetry(attempt);
            }

            // 這個就是決定是否要進行重試的地方,如果不進行重試直接返回結果,執行成功就返回結果,執行失敗就返回異常
            if (!rejectionPredicate.apply(attempt)) {
                return attempt.get();
            }
            
            // 到這裏,說明需要進行重試,則此時先決定是否到達了停止重試的時機,如果到達了則直接返回異常
            if (stopStrategy.shouldStop(attempt)) {
                throw new RetryException(attemptNumber, attempt);
            } else {
                // 決定重試時間間隔
                long sleepTime = waitStrategy.computeSleepTime(attempt);
                try {
                    // 進行阻塞
                    blockStrategy.block(sleepTime);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    throw new RetryException(attemptNumber, attempt);
                }
            }
        }

8. 總結

通篇下來可以看到其實核心實現並不難,但是此框架通過建造者模式和策略模式組合運用,提供了十分清晰明瞭且靈活的重試機制,其設計思路還是值得借鑑學習!

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