<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          使用 guava-retrying 實(shí)現(xiàn)靈活的重試機(jī)制

          共 2662字,需瀏覽 6分鐘

           ·

          2020-12-16 21:55

          我們的后端業(yè)務(wù)系統(tǒng)可能會出現(xiàn)接口調(diào)用失敗、網(wǎng)絡(luò)擁塞超時、任務(wù)執(zhí)行失敗、系統(tǒng)錯誤等異常情況,需要進(jìn)行重試操作。但某些場景下我們對重試有特殊要求,比如延遲重試、降頻重試等,此時自己編寫重試代碼會很繁瑣,在 Java 中,可以使用 guava-retrying 幫我們實(shí)現(xiàn)靈活的重試機(jī)制。
          guava-retrying 簡介
          guava-retrying 是一個線程安全的 Java 重試類庫,提供了一種通用方法去處理任意需要重試的代碼,可以方便靈活地控制重試次數(shù)、重試時機(jī)、重試頻率、停止時機(jī)等,并具有異常處理功能。
          GitHub地址:https://github.com/rholder/guava-retrying
          有意思的是,這個項(xiàng)目最初源于 Jean-Baptiste Nizet 在 guava 倉庫下的評論。

          guava-retrying 入門

          下面通過一個場景幫助大家快速入門 guava-retrying,再具體講解其更多用法。
          作者在 GitHub 提供了入門代碼,先通過 maven 或 gradle 引入:
          maven引入代碼:
          <dependency><groupId>com.github.rholdergroupId><artifactId>guava-retryingartifactId><version>2.0.0version>dependency>
          gradle引入代碼:
          compile "com.github.rholder:guava-retrying:2.0.0"
          假定我們需要調(diào)用一個qps限制很低的第三方接口,如果調(diào)用失敗,需要依次在失敗后的第10s、30s、60s進(jìn)行降頻重試。
          如果不使用框架,實(shí)現(xiàn)邏輯大致如下:
          // 調(diào)用接口boolean result;AtomicInteger atomicInteger = new AtomicInteger(0);int sleepNum = 10000;
          while(!result && atomicInteger.get() < 4) { atomicInteger.incrementAndGet(); result = thirdApi.invoke(); Thread.sleep(sleepNum); sleepNum += sleepNum * atomicInteger.get();}
          雖然看起來代碼行數(shù)并不多,只需要自己定義計數(shù)器、計算休眠時間等,但是再考慮到異常處理、異步等情況,重試邏輯的代碼占整體代碼的比重太大了(真正的業(yè)務(wù)邏輯只有 thirdApi.invoke 對么?)。如果業(yè)務(wù)中多處需要重試,還要反復(fù)編寫類似的代碼,而這不應(yīng)該是開發(fā)者關(guān)心的。
          guava-retrying 為我們封裝了一套很好的通用重試方法,來試下用它實(shí)現(xiàn)上述邏輯:
          Callable<Boolean> callable = () -> {return thirdApi.invoke(); // 業(yè)務(wù)邏輯};
          // 定義重試器Retryer<Boolean> retryer = RetryerBuilder.<Boolean>newBuilder() .retryIfResult(Predicates.<Boolean>isNull()) // 如果結(jié)果為空則重試 .retryIfExceptionOfType(IOException.class) // 發(fā)生IO異常則重試 .retryIfRuntimeException() // 發(fā)生運(yùn)行時異常則重試 .withWaitStrategy(WaitStrategies.incrementingWait(10, TimeUnit.SECONDS, 10, TimeUnit.SECONDS)) // 等待 .withStopStrategy(StopStrategies.stopAfterAttempt(4)) // 允許執(zhí)行4次(首次執(zhí)行 + 最多重試3次) .build();
          try { retryer.call(callable); // 執(zhí)行} catch (RetryException | ExecutionException e) { // 重試次數(shù)超過閾值或被強(qiáng)制中斷 e.printStackTrace();}
          分析上述代碼:
          1. 首先定義了一個 Callable 任務(wù),其中執(zhí)行我們需要重試的業(yè)務(wù)邏輯。
          2. 通過 RetryerBuilder 構(gòu)造重試器,構(gòu)造包含如下部分:
          • 重試條件 retryIfResult、retryIfExceptionOfType、retryIfRuntimeException
          • 重試等待策略(延遲)withWaitStrategy
          • 重試停止策略 withStopStrategy
          • 阻塞策略、超時限制、注冊重試監(jiān)聽器(上述代碼未使用)
          3. 通過 retryer.call 執(zhí)行任務(wù)
          4. 當(dāng)重試次數(shù)超過設(shè)定值或者被強(qiáng)制中斷時,會拋出異常,需要捕獲處理
          通過上述代碼我們定義了一個重試器來實(shí)現(xiàn)降頻重試機(jī)制。顯然這種方式相較自己實(shí)現(xiàn)重試來說具有如下優(yōu)點(diǎn):
          1. 對代碼的侵入性更小
          2. 更直觀,改動方便
          3. 可復(fù)用重試器至多個任務(wù)(代碼段)
          RetryerBuilder 方法介紹
          RetryerBuilder 用于構(gòu)造重試器,是整個 guava-retrying 庫的核心,決定了重試的行為,下面詳細(xì)介紹 RetryerBuilder 的方法。
          通過 newBuilder 方法獲取 RetryerBuilder 實(shí)例,通過 build 方法構(gòu)造 Retryer:
          RetryerBuilder<V> newBuilder()Retryer<V> build()
          可以通過下面的方法改變重試器的行為。

          重試條件

          1. 根據(jù)執(zhí)行結(jié)果判斷是否重試 retryIfResult
          RetryerBuilder<V> retryIfResult(@Nonnull Predicate<V> resultPredicate)
          2. 發(fā)生異常時重試
          // 發(fā)生任何異常都重試retryIfException()// 發(fā)生 Runtime 異常都重試RetryerBuilder retryIfRuntimeException()// 發(fā)生指定 type 異常時重試RetryerBuilder retryIfExceptionOfType(@Nonnull Classextends Throwable> exceptionClass)// 匹配到指定類型異常時重試RetryerBuilder retryIfException(@Nonnull Predicate exceptionPredicate)
          等待策略
          等待策略可以控制重試的時間間隔,通過 withWaitStrategy 方法注冊等待策略:
          RetryerBuilder<V> withWaitStrategy(@Nonnull WaitStrategy waitStrategy) throws IllegalStateException
          WaitStrategy 是等待策略接口,可通過 WaitStrategies 的方法生成該接口的策略實(shí)現(xiàn)類,共有7種策略:
          1. FixedWaitStrategy:固定等待時長策略,比如每次重試等待5s
          // 參數(shù):等待時間,時間單位WaitStrategy fixedWait(long sleepTime, @Nonnull TimeUnit timeUnit) throws IllegalStateException
          2. RandomWaitStrategy:隨機(jī)等待時長策略,每次重試等待指定區(qū)間的隨機(jī)時長
          // 參數(shù):隨機(jī)上限,時間單位WaitStrategy randomWait(long maximumTime, @Nonnull TimeUnit timeUnit)// 參數(shù):隨機(jī)下限,下限時間單位,隨機(jī)上限,上限時間單位WaitStrategy randomWait(long minimumTime,                        @Nonnull TimeUnit minimumTimeUnit,long maximumTime,                        @Nonnull TimeUnit maximumTimeUnit)
          3. IncrementingWaitStrategy:遞增等待時長策略,指定初始等待值,然后重試間隔隨次數(shù)等差遞增,比如依次等待10s、30s、60s(遞增值為10)
          // 參數(shù):初始等待時長,初始值時間單位,遞增值,遞增值時間單位WaitStrategy incrementingWait(long initialSleepTime,                              @Nonnull TimeUnit initialSleepTimeUnit,long increment,                              @Nonnull TimeUnit incrementTimeUnit)
          4. ExponentialWaitStrategy:指數(shù)等待時長策略,指定初始值,然后每次重試間隔乘2(即間隔為2的冪次方),如依次等待 2s、6s、14s。可以設(shè)置最大等待時長,達(dá)到最大值后每次重試將等待最大時長。
          // 無參數(shù)(默認(rèn)初始值為1)WaitStrategy exponentialWait()// 參數(shù):最大等待時長,最大等待時間單位(默認(rèn)初始值為1)WaitStrategy exponentialWait(long maximumTime, @Nonnull TimeUnit maximumTimeUnit)// 參數(shù):初始值,最大等待時長,最大等待時間單位WaitStrategy exponentialWait(long multiplier, long maximumTime, @Nonnull TimeUnit maximumTimeUnit)
          5. FibonacciWaitStrategy :斐波那契等待時長策略,類似指數(shù)等待時長策略,間隔時長為斐波那契數(shù)列。
          // 無參數(shù)(默認(rèn)初始值為1)WaitStrategy fibonacciWait()// 參數(shù):最大等待時長,最大等待時間單位(默認(rèn)初始值為1)WaitStrategy fibonacciWait(long maximumTime, @Nonnull TimeUnit maximumTimeUnit)// 參數(shù):最大等待時長,最大等待時間單位(默認(rèn)初始值為1)WaitStrategy fibonacciWait(long multiplier, long maximumTime, @Nonnull TimeUnit maximumTimeUnit)
          6. ExceptionWaitStrategy:異常時長等待策略,根據(jù)出現(xiàn)的異常類型決定等待的時長
          // 參數(shù):異常類型,計算等待時長的函數(shù)extends Throwable> WaitStrategy exceptionWait(@Nonnull Class exceptionClass,@Nonnull Function function)
          7. CompositeWaitStrategy :復(fù)合時長等待策略,可以組合多個等待策略,基本可以滿足所有等待時長的需求
          // 參數(shù):等待策略數(shù)組WaitStrategy join(WaitStrategy... waitStrategies)
          阻塞策略
          阻塞策略控制當(dāng)前重試結(jié)束至下次重試開始前的行為,通過 withBlockStrategy 方法注冊阻塞策略:
          RetryerBuilder<V> withBlockStrategy(@Nonnull BlockStrategy blockStrategy) throws IllegalStateException
          BlockStrategy 是等待策略接口,可通過 BlockStrategies 的方法生成實(shí)現(xiàn)類,默認(rèn)只提供一種策略 ThreadSleepStrategy:
          @Immutableprivate static class ThreadSleepStrategy implements BlockStrategy {
          @Overridepublic void block(long sleepTime) throws InterruptedException { Thread.sleep(sleepTime); }}
          很好理解,除了睡眠,阻塞著啥也不干。

          停止策略

          停止策略決定了何時停止重試,比如限制次數(shù)、時間等,通過 withStopStrategy 方法注冊等待策略:
          RetryerBuilder<V> withStopStrategy(@Nonnull StopStrategy stopStrategy) throws IllegalStateException
          可通過 StopStrategies 的方法生成 StopStrategy 接口的策略實(shí)現(xiàn)類,共有3種策略:
          1. NeverStopStrategy:永不停止,直到重試成功
          2. StopAfterAttemptStrategy:指定最多重試次數(shù),超過次數(shù)拋出 RetryException 異常
          3. StopAfterDelayStrategy:指定最長重試時間,超時則中斷當(dāng)前任務(wù)執(zhí)行且不再重試,并拋出 RetryException 異常

          超時限制

          通過 withAttemptTimeLimiter 方法為任務(wù)添加單次執(zhí)行時間限制,超時則中斷執(zhí)行,繼續(xù)重試。
          RetryerBuilder<V> withAttemptTimeLimiter(@Nonnull AttemptTimeLimiter<V> attemptTimeLimiter)
          默認(rèn)提供了兩種 AttemptTimeLimiter:
          1. NoAttemptTimeLimit:不限制執(zhí)行時間
          2. FixedAttemptTimeLimit:限制執(zhí)行時間為固定值

          監(jiān)聽器

          可以通過 withRetryListener 方法為重試器注冊***,每次重試結(jié)束后,會按注冊順序依次回調(diào) Listener 的 onRetry 方法,可在其中獲取到當(dāng)前執(zhí)行的信息,比如重試次數(shù)等。
          示例代碼如下:
          import com.github.rholder.retry.Attempt;import com.github.rholder.retry.RetryListener;
          public class MyRetryListener<T> implements RetryListener {
          @Overridepublic <T> void onRetry(Attempt<T> attempt) {// 第幾次重試,(注意:第一次重試其實(shí)是第一次調(diào)用)System.out.print("[retry]time=" + attempt.getAttemptNumber());
          // 距離第一次重試的延遲System.out.print(",delay=" + attempt.getDelaySinceFirstAttempt());
          // 重試結(jié)果: 是異常終止, 還是正常返回System.out.print(",hasException=" + attempt.hasException());System.out.print(",hasResult=" + attempt.hasResult());
          // 是什么原因?qū)е庐惓?/span>if (attempt.hasException()) {System.out.print(",causeBy=" + attempt.getExceptionCause().toString()); } else {// 正常返回時的結(jié)果System.out.print(",result=" + attempt.getResult()); } }}
          看下原理
          顧名思義,guava-retrying 依賴 guava 庫,如作者所說,源碼中大量依賴 guava 的 Predicates(斷言)來判斷是否繼續(xù)重試。
          通過方法、對象名也可以看出,該庫主要使用了策略模式、構(gòu)造器模式和觀察者模式(Listener),對調(diào)用方非常友好。
          從哪兒開始執(zhí)行任務(wù)就從哪兒開始看,直接打開 Retryer 類的 call 方法:
          public V call(Callable callable) throws ExecutionException, RetryException {long startTime = System.nanoTime(); // 1. 記錄開始時間,用于后續(xù)的時間計算for (int attemptNumber = 1; ; attemptNumber++) {        Attempt attempt;try {            V result = attemptTimeLimiter.call(callable); // 2. 執(zhí)行callable任務(wù),得到attempt            attempt = new ResultAttempt(result, attemptNumber, TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime));        } catch (Throwable t) {            attempt = new ExceptionAttempt(t, attemptNumber, TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime));        }
          for (RetryListener listener : listeners) { // 3. 如果有***,通知 listener.onRetry(attempt); }
          if (!rejectionPredicate.apply(attempt)) { // 4. 如果執(zhí)行callable出現(xiàn)異常,則返回異常的attemptreturn attempt.get(); }if (stopStrategy.shouldStop(attempt)) { // 5. 根據(jù)停止策略判斷是否停止重試throw new RetryException(attemptNumber, attempt); // 若停止,拋出異常 } else {long sleepTime = waitStrategy.computeSleepTime(attempt); // 6. 根據(jù)等待策略計算休眠時間try { blockStrategy.block(sleepTime); // 7. 根據(jù)阻塞策略決定休眠行為,默認(rèn)為sleep } catch (InterruptedException e) { Thread.currentThread().interrupt();throw new RetryException(attemptNumber, attempt); } } }}
          這個方法邏輯很清晰,可以結(jié)合作者的注釋閱讀,主要流程如下:
          1. 記錄開始時間,便于后續(xù)判斷是否超過限制時間
          2. 通過 attemptTimeLimiter 執(zhí)行 callable 任務(wù),得到 attempt。attempt 代表著每次執(zhí)行,記錄了如執(zhí)行結(jié)果、執(zhí)行次數(shù)、距離第一次執(zhí)行的延遲時間、異常原因等信息。
          • 如果 attemptTimeLimiter 是 NoAttemptTimeLimit,則直接調(diào)用 callable.call 執(zhí)行。
          • 如果 attemptTimeLimiter 是 FixedAttemptTimeLimit,則調(diào)用 timeLimiter.callWithTimeout 限制執(zhí)行時間。
          3. 通知監(jiān)聽器,進(jìn)行一些回調(diào)操作
          4. rejectionPredicate 默認(rèn)為 alwaysFalse,如果執(zhí)行 callable 出現(xiàn)異常,則 rejectionPredicate 會返回異常的 attempt
          rejectionPredicate = Predicates.or(rejectionPredicate, new ExceptionClassPredicate(RuntimeException.class));
          5. 根據(jù)停止策略判斷是否停止重試,若停止,拋出 RetryException 異常表示最終重試失敗
          6. 根據(jù)等待策略計算休眠時間
          7. 根據(jù)阻塞策略決定休眠行為,默認(rèn)為 Thread.sleep(躺著啥也不干)
          就是這樣,該庫能夠?qū)崿F(xiàn)靈活的重試,并不復(fù)雜,有興趣的同學(xué)可以去看下源碼~

          有道無術(shù),術(shù)可成;有術(shù)無道,止于術(shù)

          歡迎大家關(guān)注Java之道公眾號


          好文章,我在看??

          瀏覽 50
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产内射一级毛片农民工 | 日本久久二区 | 国产淫秽视频免费 | 成人在线精品 | 色五月婷婷操逼 |