Spring Retry重試組件、Guava Retry重試組件

個人看法 spring-retry更好


軟硬件環境 IntelliJ IDEA、SpringBoot2.2.4.RELEASE。

Spring的Retry組件

提示 spring-retry的使用方式可分爲註解式和編碼式,註解式採用代理模式依賴於AOP,而編程式則可以直接調用方法。註解式無疑更優雅,但是使用註解式的時候,要注意避免各個AOP執行順序差異帶來的問題,在這個環節的末尾,會簡單介紹如何避免這個問題。本文主要介紹的是註解式用法中基礎的常用的內容;至於spring-retry的編程式用法、spring-retry的註解式用法的其它內容可詳見https://github.com/spring-projects/spring-retry

準備工作

  1. 第一步: 在pom.xml中引入依賴。

    <!-- spring-retry -->
    <dependency>
        <groupId>org.springframework.retry</groupId>
        <artifactId>spring-retry</artifactId>
    </dependency>
    
    <!-- aop支持 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
    
  2. 第二步: 在某個配置類(如啓動類)上,啓用@EnableRetry。
    在這裏插入圖片描述

Spring Retry的編碼式使用

提示:編碼式使用spring-retry不是主要內容,這裏就簡單舉個例子就行了。

public Object retryCoding() throws Throwable {
    /*
     * spring-retry1.3.x版本開始提供建造者模式支持了,可
     * 詳見https://github.com/spring-projects/spring-retry
     */
    RetryTemplate template = new RetryTemplate();

    // 設置重試策略
    SimpleRetryPolicy simpleRetryPolicy = new SimpleRetryPolicy();
    simpleRetryPolicy.setMaxAttempts(5);
    template.setRetryPolicy(simpleRetryPolicy);

    // 執行
    Object result = template.execute(
            new RetryCallback<Object, Throwable>() {
                @Override
                public Object doWithRetry(RetryContext context) throws Throwable {
                    // 第一次請求,不算重試, 所以第一次請求時,context.getRetryCount()值爲0
                    throw new RuntimeException("第" + (context.getRetryCount() + 1) + "次調用失敗!");
                }
            },
            new RecoveryCallback<Object>() {
                @Override
                public Object recover(RetryContext context) throws Exception {
                    Throwable lastThrowable = context.getLastThrowable();
                    return "走recover邏輯了! \t異常類是" + lastThrowable.getClass().getName()
                            + "\t異常信息是" + lastThrowable.getMessage();
                }
            });
    System.out.println(result);
    return result;
}

注:1.3.x開始,spring-retry提供建造者模式支持RetryTemplate的創建了。

Spring Retry的註解式使用

  • @Retryable默認項 默認最多請求3次,默認重試時延遲1000ms再進行請求。
    • 注:重試兩次, 加上本身那一次一起3次。
    • 注:默認在所有異常的情況下,都進行重試;若重試的這幾次都沒有成功,都出現了異常,那麼最終拋出的是最後一次重試時出現的異常。
    • 示例:
      1. 被調用的方法:
        private int times = 0;
        
        /**
         * - 默認最多請求3次(注: 重試兩次, 加上本身那一次一起3次)
         *
         * - 默認在所有異常的情況下,都進行重試; 若重試的這幾次都沒有成功,都出現了異常,
         *   那麼最終拋出的是最後一次重試時出現的異常
         */
        @Retryable
        public String methodOne() {
            times++;
            int i = ThreadLocalRandom.current().nextInt(10);
            if (i < 9) {
                if (times == 3) {
                    throw new IllegalArgumentException("最後一次重試時, 發生了IllegalArgumentException異常");
                }
                throw new RuntimeException("times=" + times + ", 當前i的值爲" + i);
            }
            return "在第【" + times + "】次調用時, 調通了!";
        }
        
      2. 測試方法:
        在這裏插入圖片描述
      3. 程序輸出:
        在這裏插入圖片描述
  • @Retryable的include與exclude 默認最多請求3次,默認重試時延遲1000ms再進行請求。
    • 在嘗試次數內:
      • 情況一:如果拋出的是include裏面的異常(或其子類異常),那麼仍然會繼續重試。
      • 情況二:如果拋出的是include範圍外的異常(或其子類異常) 或者 拋出的是exclude裏面的異常(或其子類異常), 那麼不再繼續重試,直接拋出異常。
        注:若拋出的異常即是include裏指定的異常的子類,又是exclude裏指定的異常的子類,那麼判斷當前異常是按include走,還是按exclude走,需要根據【更短路徑原則】。如下面的methodTwo方法所示, RuntimeException 是 IllegalArgumentException的超類,IllegalArgumentException 又是 NumberFormatException的超類,此時因爲IllegalArgumentException離NumberFormatException“路徑更短”,所以拋出的NumberFormatException按照IllegalArgumentException算,走include。
    • 示例:
      1. 被調用的方法:
        private int times = 0;
        
        /**
         * - 在嘗試次數內,
         *      1. 如果拋出的是include裏面的異常(或其子類異常),那麼仍然會繼續重試
         *      2. 如果拋出的是include範圍外的異常(或其子類異常)  或者 拋出的是
         *         exclude裏面的異常(或其子類異常), 那麼不再繼續重試,直接拋出異常
         *
         *      注意: 若拋出的異常即是include裏指定的異常的子類,又是exclude裏指定的異常的子類,那麼
         *            判斷當前異常是按include走,還是按exclude走,需要根據【更短路徑原則】。
         *            如本例所示, RuntimeException 是 IllegalArgumentException的超類,
         *                      IllegalArgumentException 又是 NumberFormatException的超類,
         *                      此時因爲IllegalArgumentException離NumberFormatException“路徑更短”,
         *                      所以拋出的NumberFormatException按照IllegalArgumentException算,走include。
         */
        @Retryable(include = {IllegalArgumentException.class}, exclude = {RuntimeException.class})
        public String methodTwo() {
            times++;
            /// if (times == 1) {
            ///     throw new IllegalArgumentException("times=" + times + ", 發生的異常是IllegalArgumentException");
            /// }
            /// if (times == 2) {
            ///     throw new RuntimeException("times=" + times + ", 發生的異常是RuntimeException");
            /// }
            if (times == 1) {
                throw new NumberFormatException("times=" + times + ", 發生的異常是IllegalArgumentException的子類");
            }
            if (times == 2) {
                throw new ArithmeticException("times=" + times + ", 發生的異常是RuntimeException的子類");
            }
            return "在第【" + times + "】次調用時, 調通了!";
        }
        
        /**
         * - 在嘗試次數內,
         *      如果拋出的是exclude裏面的異常(或其子類異常),那麼不再繼續重試,直接拋出異常
         *      如果拋出的是include裏面的異常(或其子類異常),那麼仍然會繼續重試
         */
        @Retryable(include = {RuntimeException.class}, exclude = {IllegalArgumentException.class})
        public String methodTwoAlpha() {
            times++;
            if (times == 1) {
                throw new ArithmeticException("times=" + times + ", 發生的異常是RuntimeException的子類");
            }
            if (times == 2) {
                throw new NumberFormatException("times=" + times + ", 發生的異常是IllegalArgumentException的子類");
            }
            return "在第【" + times + "】次調用時, 調通了!";
        }
        
        /**
         * - 在嘗試次數內,
         *      如果拋出的是include範圍外的異常(或其子類異常),那麼不再繼續重試,直接拋出異常
         *      如果拋出的是include裏面的異常(或其子類異常),那麼仍然會繼續重試
         */
        @Retryable(include = {IllegalArgumentException.class})
        public String methodTwoBeta() {
            times++;
            if (times == 1) {
                throw new NumberFormatException("times=" + times + ", 發生的異常是IllegalArgumentException的子類");
            }
            if (times == 2) {
                throw new ArithmeticException("times=" + times + ", 發生的異常是RuntimeException的子類");
            }
            return "在第【" + times + "】次調用時, 調通了!";
        }
        
      2. 測試方法:
        在這裏插入圖片描述
      3. 三個測試方法對應的輸出:
        在這裏插入圖片描述
        在這裏插入圖片描述在這裏插入圖片描述
  • @Retryable的maxAttempts maxAttempts用於指定最大嘗試次數, 默認值爲3。
    • 連本身那一次也會被算在內(若值爲5, 那麼最多重試4次, 算上本身那一次5次)。
    • 示例:
      1. 被調用的方法:
        private int times = 0;
        
        /**
         * maxAttempts指定最大嘗試次數, 默認值爲3.
         * 注:連本身那一次也會被算在內(若值爲5, 那麼最多重試4次, 算上本身那一次5次)
         */
        @Retryable(maxAttempts = 5)
        public String methodThere() {
            times++;
            if (times < 5) {
                throw new RuntimeException("times=" + times + ", 發生的異常是RuntimeException");
            }
            return "在第【" + times + "】次調用時, 調通了!";
        }
        
      2. 測試方法:
        在這裏插入圖片描述
      3. 程序輸出:
        在這裏插入圖片描述
  • @Retryable與@Recover搭配
    • 相關要點一: 我們不妨稱被@Retryable標記的方法爲目標方法,稱被@Recover標記的方法爲處理方法。那麼處理方法和目標方法必須同時滿足:
      1. 處於同一個類下。
      2. 兩者的參數類型需要匹配 或 處理方法的參數可以多一個異常接收類(這一異常接收類必須放在第一個參數的位置)。
        注:兩者的參數類型匹配即可,形參名可以一樣可以不一樣。
      3. 返回值類型需要保持一致(或處理方法的返回值類型是目標方法的返回值類型的超類)。
    • 相關要點二: 目標方法在進行完畢retry後,如果仍然拋出異常, 那麼會去定位處理方法, 走處理方法的邏輯,定位處理方法的原則是:在同一個類下,尋找和目標方法 具有相同參數類型(P.S.可能會再參數列表首位多一個異常類參數)、相同返回值類型的標記有Recover的方法。
      注:如果存在兩個目標方法,他們的參數類型、返回值類型都一樣,這時就需要主動指定對應的處理方法了,如:@Retryable(recover = “service1Recover”)。@Retryable註解的recover 屬性,在spring-retry1.3.x版本纔開始提供。
      注:如果是使用的1.3.x+版本的spring-retry推薦直接使用@Retryable(recover = "recoverMethodName")指定同類當中的處理方法的方法名
    • 示例:
      1. 被調用的方法:
        import org.springframework.retry.annotation.Recover;
        import org.springframework.retry.annotation.Retryable;
        import org.springframework.stereotype.Component;
        
        /**
         * 目標方法:被@Retryable標記的方法
         * 處理方法:被@Recover標記的方法
         *
         * 處理方法 和 目標方法 必須滿足:
         *    1. 處於同一個類下
         *    2. 兩者的參數需要保持一致  或  處理方法的參數可以多一個異常接收類(這一異常接收類必須放在第一個參數的位置)
         *       注:保持一致指的是參數類型保持一致,形參名可以一樣可以不一樣
         *    3. 返回值類型需要保持一致 (或處理方法的返回值類型是目標方法的返回值類型的超類 )
         *
         * 目標方法在進行完畢retry後,如果仍然拋出異常, 那麼會去定位處理方法, 走處理方法的邏輯,定位處理方法的原則是:
         * - 在同一個類下,尋找和目標方法 具有
         *      相同參數類型(P.S.可能會再參數列表首位多一個異常類參數)、
         *      相同返回值類型
         *      的標記有Recover的方法
         * - 如果存在兩個目標方法,他們的參數類型、返回值類型都一樣,
         *     這時就需要主動指定對應的處理方法了,
         *     如:@Retryable(recover = "service1Recover")
         *
         * @author JustryDeng
         * @date 2020/2/25 21:40:11
         */
        @Component
        public class QwerRemoteCall {
        
            private int times = 0;
        
            /// --------------------------------------------------------- @Recover基本測試
        
            @Retryable
            public String methodFour(Integer a, String b) {
                times++;
                throw new RuntimeException("times=" + times + ", 發生的異常是RuntimeException");
            }
        
            @Recover
            private String justryDeng(Throwable th, Integer a, String b) {
                return "a=" + a + ", b=" + b + "\t" + "異常類是:"
                        + th.getClass().getName() + ", 異常信息是:" + th.getMessage();
            }
        
        
            ///  如果在@Retryable中指明瞭異常, 那麼在@Recover中可以明確的指明是哪一種異常
            /// @Retryable(RemoteAccessException.class)
            /// public void service() {
            ///     // ... do something
            /// }
            ///
            /// @Recover
            /// public void recover(RemoteAccessException e) {
            ///     // ... panic
            /// }
        
        
        
            /// --------------------------------------------------------- @Retryable指定對應的@Recover方法
            /// 特別注意: @Retryable註解的recover屬性, 在spring-retry的較高版本中才得以支持,
            ///          在本人使用的1.2.5.RELEASE版本中還暫不支持
        
            /// @Retryable(recover = "service1Recover", value = RemoteAccessException.class)
            /// public void service1(String str1, String str2) {
            ///     // ... do something
            /// }
            ///
            /// @Retryable(recover = "service2Recover", value = RemoteAccessException.class)
            /// public void service2(String str1, String str2) {
            ///     // ... do something
            /// }
            ///
            /// @Recover
            /// public void service1Recover(RemoteAccessException e, String str1, String str2) {
            ///     // ... error handling making use of original args if required
            /// }
            ///
            /// @Recover
            /// public void service2Recover(RemoteAccessException e, String str1, String str2) {
            ///     // ... error handling making use of original args if required
            /// }
        
        }
        
      2. 測試方法:
        在這裏插入圖片描述
      3. 程序輸出:
        在這裏插入圖片描述
  • @Retryable的backoff @Retryable註解的backoff屬性,可用於指定重試時的退避策略。
    • 相關要點:
      1. @Retryable 或 @Retryable(backoff = @Backoff()), 那麼默認延遲 1000ms
        後重試。
      2. @Backoff的delay屬性: 延遲多久後,再進行重試。
      3. 如果不想延遲, 那麼需要指定@Backoff的value和delay同時爲0。
      4. delay與multiplier搭配使用,延遲時間 = delay * (multiplier ^ (n - 1)),其中n爲第幾次重試, n >= 1, 這裏^爲次方。
        注:第二次請求,纔算第一次重試。
    • 示例:
      1. 被調用的方法:
        private int times = 0;
        
        DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("HH:mm:ss");
        
        /**
         * Backoff用於指定 重試時的退避策略
         * - @Retryable 或 @Retryable(backoff = @Backoff()), 那麼默認延遲 1000ms後重試
         *   注:第一次請求時,是馬上進行的,是不會延遲的
         *
         *    效果如:
         *       times=1, 時間是12:02:04
         *       times=2, 時間是12:02:05
         *       times=3, 時間是12:02:06
         */
        @Retryable(backoff = @Backoff())
        public String methodFive() {
            times++;
            System.err.println("times=" + times + ", 時間是" + dateTimeFormatter.format(LocalTime.now()));
            throw new RuntimeException("times=" + times + ", 發生的異常是RuntimeException");
        }
        
        /**
         * - delay: 延遲多久後,再進行重試。
         *   注:第一次請求時,是馬上進行的,是不會延遲的
         *
         *    效果如:
         *       times=1, 時間是11:46:36
         *       times=2, 時間是11:46:41
         *       times=3, 時間是11:46:46
         */
        @Retryable(backoff = @Backoff(delay = 5000))
        public String methodFiveAlpha() {
            times++;
            System.err.println("times=" + times + ", 時間是" + dateTimeFormatter.format(LocalTime.now()));
            throw new RuntimeException("times=" + times + ", 發生的異常是RuntimeException");
        }
        
        /**
         * 如果不想延遲, 那麼需要指定value和delay同時爲0
         * 注:原因可詳見javadoc 或 源碼
         *
         *    效果如:
         *       times=1, 時間是12:05:44
         *       times=2, 時間是12:05:44
         *       times=3, 時間是12:05:44
         */
        @Retryable(backoff = @Backoff(value = 0, delay = 0))
        public String methodFiveBeta() {
            times++;
            System.err.println("times=" + times + ", 時間是" + dateTimeFormatter.format(LocalTime.now()));
            throw new RuntimeException("times=" + times + ", 發生的異常是RuntimeException");
        }
        
        /**
         * - delay: 延遲多久後,再進行重試。
         * - multiplier: 乘數因子
         *
         *   延遲時間 = delay * (multiplier ^ (n - 1)) , 其中n爲第幾次重試, n >= 1, 這裏 ^ 爲次方
         *
         * 注:第一次請求時,是馬上進行的,是不會延遲的
         * 注:第二次請求時對應第一次重試
         *
         *    效果如:
         *       times=1, 時間是12:09:14
         *       times=2, 時間是12:09:17
         *       times=3, 時間是12:09:23
         *       times=4, 時間是12:09:35
         *       times=5, 時間是12:09:59
         *     可知,延遲時間越來越大,分別是: 3 6 12 24
         */
        @Retryable(maxAttempts = 5, backoff = @Backoff(delay = 3000, multiplier = 2))
        public String methodFiveGamma() {
            times++;
            System.err.println("times=" + times + ", 時間是" + dateTimeFormatter.format(LocalTime.now()));
            throw new RuntimeException("times=" + times + ", 發生的異常是RuntimeException");
        }
        
      2. 測試方法:
        在這裏插入圖片描述
      3. 四個測試方法分別輸出:
        在這裏插入圖片描述
        在這裏插入圖片描述
        在這裏插入圖片描述
        在這裏插入圖片描述

使用spring retry註解式時,避免多個AOP代理導致可能出現的問題

  • 情景說明 就像@Transactional與@CacheEvict標註在同一個方法上、@Transactional與synchronized標註在同一個方法上一樣,在併發情況下,會出現問題(會出現什麼問題、怎麼解決出現的問題可詳見《程序員成長筆記(第二部)》相關章節)。如果@Transactional和@Retryable同時標註在了同一個方法上,那是不是也會出問題呢,從原理分析,肯定是會出現問題的,如下面的錯誤示例。

  • 錯誤示例

    • 某個service實現如圖:
      在這裏插入圖片描述
    • 調用一次該方法前的表:
      在這裏插入圖片描述
    • 調用一次該方法後的表:
      在這裏插入圖片描述        這裏只是拿事務AOP與重試AOP舉的一個例子,重點是說,在多個AOP同時作用於同一個方法時,應該考慮各個AOP之間的執行順序問題;更好的辦法是儘量避免多個AOP作用於同一個切點。
  • 正確示例(避免方式) 將重試機制那部分代碼,單獨放在一個類裏面,避免多個AOP作用於同一個切點
    在這裏插入圖片描述        這個時候,哪怕仍然通過@EnableTransactionManagement(order = Ordered.HIGHEST_PRECEDENCE)把事務的AOP優先級調到了最高,也不會有什麼影響了,也不會出現上面錯誤示例中多條數據的問題了。
    注:避免方式較多(如主動控制各個AOP直接的執行順序、避免多個AOP作用於同一個切點等),推薦使用避免多個AOP作用於同一個切點。


Guava的Retry組件

準備工作:在pom.xml中引入依賴

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

Guava Retry的使用

       比起Spring Retry的使用, Guava Retry的使用方式相對簡單,這裏僅給出一個簡單的使用示例,更多細節可詳見https://github.com/rholder/guava-retrying

簡單使用示例

import com.github.rholder.retry.RetryException;
import com.github.rholder.retry.Retryer;
import com.github.rholder.retry.RetryerBuilder;
import com.github.rholder.retry.StopStrategies;

import java.io.IOException;
import java.util.Arrays;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ThreadLocalRandom;
import java.util.zip.DataFormatException;

/**
 * Guava Retry簡單使用示例
 *
 * @author JustryDeng
 * @date 2020/2/25 21:40:11
 */
public class XyzRemoteCall {

    /**
     * guava retry組件 使用測試
     *
     * 提示:泛型 對應 要返回的數據的類型。
     */
    public static void jd() {
        // 創建callable, 在call()方法裏面編寫相關業務邏輯
        Callable<Object[]> callable = new Callable<Object[]>() {
            int times = 0;
            @Override
            public Object[] call() throws Exception {
                // business logic
                times++;
                if (times == 1) {
                    throw new RuntimeException();
                }
                if (times ==  2) {
                    throw new Exception();
                }
                // 隨機一個數[origin, bound)
                int randomNum = ThreadLocalRandom.current().nextInt(1, 5);
                if (randomNum == 1) {
                    throw new DataFormatException("call()拋出了檢查異常DataFormatException");
                } else if (randomNum == 2) {
                    throw new IOException("call()拋出了檢查異常IOException");
                } else if (randomNum == 3) {
                    throw new RuntimeException("call()拋出了運行時異常RuntimeException");
                }
                return new Object[]{"鄧沙利文", "亨得帥", "鄧二洋", "JustryDeng"};
            }
        };

        // 創建重試器
        Retryer<Object[]> retryer = RetryerBuilder.<Object[]>newBuilder()
                /*
                 * 指定什麼條件下觸發重試
                 *
                 * 注:這裏,只要callable中的call方法拋出的異常是Throwable或者
                 *    是Throwable的子類,那麼這裏都成立,都會進行重試。
                 */
                .retryIfExceptionOfType(Throwable.class)
                /// .retryIfException()
                /// .retryIfRuntimeException()
                /// .retryIfExceptionOfType(@Nonnull Class<? extends Throwable> exceptionClass)
                /// .retryIfException(@Nonnull Predicate<Throwable> exceptionPredicate)
                /// .retryIfResult(@Nonnull Predicate<V> resultPredicate)

                // 設置兩次重試之間的阻塞策略(如: 設置線程sleep、設置自旋鎖等等)
                ///.withBlockStrategy()
                // 設置監聽器 (這個監聽器可用於監聽每次請求的結果信息, 並作相應的邏輯處理。 如: 統計、預警等等)
                ///.withRetryListener()
                // 設置延時策略, 每次重試前,都要延時一段時間,然後再發起請求。(第一次請求,是不會被延時的)
                ///.withWaitStrategy()
                // 設置停止重試的策略(如:這裏設置的是三次請求後, 不再重試)
                .withStopStrategy(StopStrategies.stopAfterAttempt(3))
                .build();
        try {
            Object[] result = retryer.call(callable);
            System.err.println(Arrays.toString(result));
        /*
         * call()方法拋出的異常會被封裝到RetryException或ExecutionException中, 進行拋出
         * 所以在這裏,可以通過 e.getCause()獲取到call()方法實際拋出的異常
         */
        } catch (RetryException|ExecutionException e) {
            System.err.println("call()方法拋出的異常, 實際是" + e.getCause());
            e.printStackTrace();
        }
    }
}


Spring Retry重試組件、Guava Retry重試組件簡單梳理完畢 !


^_^ 如有不當之處,歡迎指正

^_^ 參考連接
         https://github.com/spring-projects/spring-retry

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

^_^ 測試代碼託管連接
         https://github.com/JustryDeng/CommonRepository…

^_^ 本文已經被收錄進《程序員成長筆記(七)》,筆者JustryDeng

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章