Java開發(fā)利器之重試框架guava-retrying

前言

guava-retrying github地址:https://github.com/rholder/guava-retrying

guava-retrying是谷歌的Guava庫的一個小擴展衙解,允許為任意函數(shù)調(diào)用創(chuàng)建可配置的重試策略,比如與正常運行時間不穩(wěn)定的遠程服務對話的函數(shù)調(diào)用焰枢。

在日常開發(fā)中蚓峦,尤其是在微服務盛行的時代下,我們在調(diào)用外部接口時济锄,經(jīng)常會因為第三方接口超時暑椰、限流等問題從而造成接口調(diào)用失敗,那么此時我們通常會對接口進行重試荐绝,那么問題來了一汽,如何重試呢?該重試幾次呢?如果要設置重試時間超過多長時間后還不成功就不重試了該怎么做呢召夹?所幸guava-retrying為我們提供了強大而簡單易用的重試框架guava-retrying岩喷。

一、pom依賴

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

二监憎、使用示例

我們可以通過RetryerBuilder來構(gòu)造一個重試器纱意,通過RetryerBuilder可以設置什么時候需要重試(即重試時機)、停止重試策略鲸阔、失敗等待時間間隔策略偷霉、任務執(zhí)行時長限制策略

先看一個簡單的例子:


    private int invokeCount = 0;

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

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

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

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

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

輸出:

當前執(zhí)行第 1 次,num:0
當前執(zhí)行第 2 次,num:0
當前執(zhí)行第 3 次,num:0
執(zhí)行0,異常:Retrying failed to complete successfully after 3 attempts.
當前執(zhí)行第 1 次,num:1
當前執(zhí)行第 1 次,num:2
當前執(zhí)行第 2 次,num:2
當前執(zhí)行第 3 次,num:2
執(zhí)行2渔扎,異常:Retrying failed to complete successfully after 3 attempts.

三硫狞、重試時機

RetryerBuilder的retryIfXXX()方法用來設置在什么情況下進行重試,總體上可以分為根據(jù)執(zhí)行異常進行重試根據(jù)方法執(zhí)行結(jié)果進行重試兩類赞警。

3.1 根據(jù)異常進行重試

方法 描述
retryIfException() 當方法執(zhí)行拋出異常 isAssignableFrom Exception.class 時重試
retryIfRuntimeException() 當方法執(zhí)行拋出異常 isAssignableFrom RuntimeException.class 時重試
retryIfException(Predicate<Throwable> exceptionPredicate) 這里當發(fā)生異常時妓忍,會將異常傳遞給exceptionPredicate,那我們就可以通過傳入的異常進行更加自定義的方式來決定什么時候進行重試
retryIfExceptionOfType(Class<? extends Throwable> exceptionClass) 當方法執(zhí)行拋出異常 isAssignableFrom 傳入的exceptionClass 時重試

3.2 根據(jù)返回結(jié)果進行重試

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

四世剖、停止重試策略StopStrategy

停止重試策略用來決定什么時候不進行重試,其接口com.github.rholder.retry.StopStrategy笤虫,停止重試策略的實現(xiàn)類均在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

此策略將永遠重試琼蚯,永不停止酬凳,查看其實現(xiàn)類,直接返回了false

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

4.2 StopAfterAttemptStrategy

當執(zhí)行次數(shù)到達指定次數(shù)之后停止重試遭庶,查看其實現(xiàn)類:

    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

當距離方法的第一次執(zhí)行超出了指定的delay時間時停止宁仔,也就是說一直進行重試,當進行下一次重試的時候會判斷從第一次執(zhí)行到現(xiàn)在的所消耗的時間是否超過了這里指定的delay時間峦睡,查看其實現(xiàn):

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

五翎苫、重試間隔策略WaitStrategy以及重試阻塞策略BlockStrategy

這兩個策略放在一起說,它們合起來的作用就是用來控制重試任務之間的間隔時間榨了,以及如何任務在等待時間間隔時如何阻塞煎谍。也就是說WaitStrategy決定了重試任務等待多久后進行下一次任務的執(zhí)行,BlockStrategy用來決定任務如何等待龙屉。它們兩的策略工廠分別為com.github.rholder.retry.WaitStrategies和BlockStrategies呐粘。

5.1 BlockStrategy

5.1.1 ThreadSleepStrategy

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

    @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

該策略在決定任務間隔時間時唆垃,返回的是一個遞增的間隔時間,即每次任務重試間隔時間逐步遞增鳍咱,越來越長降盹,查看其實現(xiàn):

    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

顧名思義蓄坏,返回一個隨機的間隔時長,我們需要傳入的就是一個最小間隔和最大間隔丑念,然后隨機返回介于兩者之間的一個間隔時長涡戳,其實現(xiàn)為:

    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

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

    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

該策略是由方法執(zhí)行異常來決定是否重試任務之間進行間隔等待脯倚,以及間隔多久渔彰。

    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返回的間隔時長相加就是最終的間隔時間植榕。查看其實現(xiàn):

   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有點相似再沧,間隔時間都是隨著重試次數(shù)的增加而遞增的,不同的是尊残,F(xiàn)ibonacciWaitStrategy是按照斐波那契數(shù)列來進行計算的炒瘸,使用這個策略時,我們需要傳入一個乘數(shù)因子和最大間隔時長寝衫,其實現(xiàn)就不貼了

5.2.7 ExponentialWaitStrategy

這個與IncrementingWaitStrategy顷扩、FibonacciWaitStrategy也類似,間隔時間都是隨著重試次數(shù)的增加而遞增的慰毅,但是該策略的遞增是呈指數(shù)級遞增隘截。查看其實現(xiàn):

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

六、重試監(jiān)聽器RetryListener

當發(fā)生重試時汹胃,將會調(diào)用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())
            // 設置最大執(zhí)行次數(shù)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() + "次執(zhí)行");
        }
    }

輸出:

第1次執(zhí)行
第2次執(zhí)行
第3次執(zhí)行

七统台、重試原理

其實到這一步之后雕擂,實現(xiàn)原理大概就很清楚了啡邑,就是由上述各種策略配合從而達到了非常靈活的重試機制贱勃。在這之前我們看一個上面沒說的東東-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這個類包含了任務執(zhí)行次數(shù)、任務執(zhí)行異常、任務執(zhí)行結(jié)果贵扰、以及首次執(zhí)行任務至今的時間間隔仇穗,那么我們后續(xù)的不管重試時機、還是其他策略都是根據(jù)此值來決定戚绕。

接下來看關鍵執(zhí)行入口Retryer#call:

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

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

            // 這個就是決定是否要進行重試的地方纹坐,如果不進行重試直接返回結(jié)果,執(zhí)行成功就返回結(jié)果舞丛,執(zhí)行失敗就返回異常
            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);
                }
            }
        }

八谷誓、總結(jié)

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末鸵钝,一起剝皮案震驚了整個濱河市糙臼,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌恩商,老刑警劉巖变逃,帶你破解...
    沈念sama閱讀 210,978評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異痕届,居然都是意外死亡韧献,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,954評論 2 384
  • 文/潘曉璐 我一進店門研叫,熙熙樓的掌柜王于貴愁眉苦臉地迎上來锤窑,“玉大人,你說我怎么就攤上這事嚷炉≡▎” “怎么了?”我有些...
    開封第一講書人閱讀 156,623評論 0 345
  • 文/不壞的土叔 我叫張陵申屹,是天一觀的道長绘证。 經(jīng)常有香客問我,道長哗讥,這世上最難降的妖魔是什么嚷那? 我笑而不...
    開封第一講書人閱讀 56,324評論 1 282
  • 正文 為了忘掉前任,我火速辦了婚禮杆煞,結(jié)果婚禮上魏宽,老公的妹妹穿的比我還像新娘腐泻。我一直安慰自己,他們只是感情好队询,可當我...
    茶點故事閱讀 65,390評論 5 384
  • 文/花漫 我一把揭開白布派桩。 她就那樣靜靜地躺著,像睡著了一般蚌斩。 火紅的嫁衣襯著肌膚如雪铆惑。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,741評論 1 289
  • 那天送膳,我揣著相機與錄音员魏,去河邊找鬼。 笑死叠聋,一個胖子當著我的面吹牛逆趋,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播晒奕,決...
    沈念sama閱讀 38,892評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼闻书,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了脑慧?” 一聲冷哼從身側(cè)響起魄眉,我...
    開封第一講書人閱讀 37,655評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎闷袒,沒想到半個月后坑律,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,104評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡囊骤,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年晃择,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片也物。...
    茶點故事閱讀 38,569評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡宫屠,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出滑蚯,到底是詐尸還是另有隱情浪蹂,我是刑警寧澤,帶...
    沈念sama閱讀 34,254評論 4 328
  • 正文 年R本政府宣布告材,位于F島的核電站坤次,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏斥赋。R本人自食惡果不足惜缰猴,卻給世界環(huán)境...
    茶點故事閱讀 39,834評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望疤剑。 院中可真熱鬧滑绒,春花似錦胰舆、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,725評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽棘幸。三九已至焰扳,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間误续,已是汗流浹背吨悍。 一陣腳步聲響...
    開封第一講書人閱讀 31,950評論 1 264
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留蹋嵌,地道東北人育瓜。 一個月前我還...
    沈念sama閱讀 46,260評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像栽烂,于是被迫代替她去往敵國和親躏仇。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,446評論 2 348

推薦閱讀更多精彩內(nèi)容