Spring Retry重试组件、Guava Retry重试组件

我不是女神ヾ 2023-07-08 16:22 672阅读 0赞

个人看法**:** 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中引入依赖。

    1. <!-- spring-retry -->
    2. <dependency>
    3. <groupId>org.springframework.retry</groupId>
    4. <artifactId>spring-retry</artifactId>
    5. </dependency>
    6. <!-- aop支持 -->
    7. <dependency>
    8. <groupId>org.springframework.boot</groupId>
    9. <artifactId>spring-boot-starter-aop</artifactId>
    10. </dependency>
  2. 第二步: 在某个配置类(如启动类)上,启用@EnableRetry。
    在这里插入图片描述

Spring Retry的编码式使用:

提示:编码式使用spring-retry不是主要内容,这里就简单举个例子就行了。

  1. public Object retryCoding() throws Throwable {
  2. /* * spring-retry1.3.x版本开始提供建造者模式支持了,可 * 详见https://github.com/spring-projects/spring-retry */
  3. RetryTemplate template = new RetryTemplate();
  4. // 设置重试策略
  5. SimpleRetryPolicy simpleRetryPolicy = new SimpleRetryPolicy();
  6. simpleRetryPolicy.setMaxAttempts(5);
  7. template.setRetryPolicy(simpleRetryPolicy);
  8. // 执行
  9. Object result = template.execute(
  10. new RetryCallback<Object, Throwable>() {
  11. @Override
  12. public Object doWithRetry(RetryContext context) throws Throwable {
  13. // 第一次请求,不算重试, 所以第一次请求时,context.getRetryCount()值为0
  14. throw new RuntimeException("第" + (context.getRetryCount() + 1) + "次调用失败!");
  15. }
  16. },
  17. new RecoveryCallback<Object>() {
  18. @Override
  19. public Object recover(RetryContext context) throws Exception {
  20. Throwable lastThrowable = context.getLastThrowable();
  21. return "走recover逻辑了! \t异常类是" + lastThrowable.getClass().getName()
  22. + "\t异常信息是" + lastThrowable.getMessage();
  23. }
  24. });
  25. System.out.println(result);
  26. return result;
  27. }

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

Spring Retry的注解式使用:

  • @Retryable默认项**:** 默认最多请求3次,默认重试时延迟1000ms再进行请求。

    • 注:重试两次, 加上本身那一次一起3次。
    • 注:默认在所有异常的情况下,都进行重试;若重试的这几次都没有成功,都出现了异常,那么最终抛出的是最后一次重试时出现的异常。
    • 示例:

      1. 被调用的方法:

        1. private int times = 0;
        2. /** * - 默认最多请求3次(注: 重试两次, 加上本身那一次一起3次) * * - 默认在所有异常的情况下,都进行重试; 若重试的这几次都没有成功,都出现了异常, * 那么最终抛出的是最后一次重试时出现的异常 */
        3. @Retryable
        4. public String methodOne() {
        5. times++;
        6. int i = ThreadLocalRandom.current().nextInt(10);
        7. if (i < 9) {
        8. if (times == 3) {
        9. throw new IllegalArgumentException("最后一次重试时, 发生了IllegalArgumentException异常");
        10. }
        11. throw new RuntimeException("times=" + times + ", 当前i的值为" + i);
        12. }
        13. return "在第【" + times + "】次调用时, 调通了!";
        14. }
      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. 被调用的方法:

        1. private int times = 0;
        2. /** * - 在尝试次数内, * 1. 如果抛出的是include里面的异常(或其子类异常),那么仍然会继续重试 * 2. 如果抛出的是include范围外的异常(或其子类异常) 或者 抛出的是 * exclude里面的异常(或其子类异常), 那么不再继续重试,直接抛出异常 * * 注意: 若抛出的异常即是include里指定的异常的子类,又是exclude里指定的异常的子类,那么 * 判断当前异常是按include走,还是按exclude走,需要根据【更短路径原则】。 * 如本例所示, RuntimeException 是 IllegalArgumentException的超类, * IllegalArgumentException 又是 NumberFormatException的超类, * 此时因为IllegalArgumentException离NumberFormatException“路径更短”, * 所以抛出的NumberFormatException按照IllegalArgumentException算,走include。 */
        3. @Retryable(include = { IllegalArgumentException.class}, exclude = { RuntimeException.class})
        4. public String methodTwo() {
        5. times++;
        6. /// if (times == 1) {
        7. /// throw new IllegalArgumentException("times=" + times + ", 发生的异常是IllegalArgumentException");
        8. /// }
        9. /// if (times == 2) {
        10. /// throw new RuntimeException("times=" + times + ", 发生的异常是RuntimeException");
        11. /// }
        12. if (times == 1) {
        13. throw new NumberFormatException("times=" + times + ", 发生的异常是IllegalArgumentException的子类");
        14. }
        15. if (times == 2) {
        16. throw new ArithmeticException("times=" + times + ", 发生的异常是RuntimeException的子类");
        17. }
        18. return "在第【" + times + "】次调用时, 调通了!";
        19. }
        20. /** * - 在尝试次数内, * 如果抛出的是exclude里面的异常(或其子类异常),那么不再继续重试,直接抛出异常 * 如果抛出的是include里面的异常(或其子类异常),那么仍然会继续重试 */
        21. @Retryable(include = { RuntimeException.class}, exclude = { IllegalArgumentException.class})
        22. public String methodTwoAlpha() {
        23. times++;
        24. if (times == 1) {
        25. throw new ArithmeticException("times=" + times + ", 发生的异常是RuntimeException的子类");
        26. }
        27. if (times == 2) {
        28. throw new NumberFormatException("times=" + times + ", 发生的异常是IllegalArgumentException的子类");
        29. }
        30. return "在第【" + times + "】次调用时, 调通了!";
        31. }
        32. /** * - 在尝试次数内, * 如果抛出的是include范围外的异常(或其子类异常),那么不再继续重试,直接抛出异常 * 如果抛出的是include里面的异常(或其子类异常),那么仍然会继续重试 */
        33. @Retryable(include = { IllegalArgumentException.class})
        34. public String methodTwoBeta() {
        35. times++;
        36. if (times == 1) {
        37. throw new NumberFormatException("times=" + times + ", 发生的异常是IllegalArgumentException的子类");
        38. }
        39. if (times == 2) {
        40. throw new ArithmeticException("times=" + times + ", 发生的异常是RuntimeException的子类");
        41. }
        42. return "在第【" + times + "】次调用时, 调通了!";
        43. }
      2. 测试方法:
        在这里插入图片描述
      3. 三个测试方法对应的输出:
        在这里插入图片描述
        在这里插入图片描述在这里插入图片描述
  • @Retryable的maxAttempts**:** maxAttempts用于指定最大尝试次数, 默认值为3。

    • 连本身那一次也会被算在内(若值为5, 那么最多重试4次, 算上本身那一次5次)。
    • 示例:

      1. 被调用的方法:

        1. private int times = 0;
        2. /** * maxAttempts指定最大尝试次数, 默认值为3. * 注:连本身那一次也会被算在内(若值为5, 那么最多重试4次, 算上本身那一次5次) */
        3. @Retryable(maxAttempts = 5)
        4. public String methodThere() {
        5. times++;
        6. if (times < 5) {
        7. throw new RuntimeException("times=" + times + ", 发生的异常是RuntimeException");
        8. }
        9. return "在第【" + times + "】次调用时, 调通了!";
        10. }
      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. 被调用的方法:

        1. import org.springframework.retry.annotation.Recover;
        2. import org.springframework.retry.annotation.Retryable;
        3. import org.springframework.stereotype.Component;
        4. /** * 目标方法:被@Retryable标记的方法 * 处理方法:被@Recover标记的方法 * * 处理方法 和 目标方法 必须满足: * 1. 处于同一个类下 * 2. 两者的参数需要保持一致 或 处理方法的参数可以多一个异常接收类(这一异常接收类必须放在第一个参数的位置) * 注:保持一致指的是参数类型保持一致,形参名可以一样可以不一样 * 3. 返回值类型需要保持一致 (或处理方法的返回值类型是目标方法的返回值类型的超类 ) * * 目标方法在进行完毕retry后,如果仍然抛出异常, 那么会去定位处理方法, 走处理方法的逻辑,定位处理方法的原则是: * - 在同一个类下,寻找和目标方法 具有 * 相同参数类型(P.S.可能会再参数列表首位多一个异常类参数)、 * 相同返回值类型 * 的标记有Recover的方法 * - 如果存在两个目标方法,他们的参数类型、返回值类型都一样, * 这时就需要主动指定对应的处理方法了, * 如:@Retryable(recover = "service1Recover") * * @author JustryDeng * @date 2020/2/25 21:40:11 */
        5. @Component
        6. public class QwerRemoteCall {
        7. private int times = 0;
        8. /// --------------------------------------------------------- @Recover基本测试
        9. @Retryable
        10. public String methodFour(Integer a, String b) {
        11. times++;
        12. throw new RuntimeException("times=" + times + ", 发生的异常是RuntimeException");
        13. }
        14. @Recover
        15. private String justryDeng(Throwable th, Integer a, String b) {
        16. return "a=" + a + ", b=" + b + "\t" + "异常类是:"
        17. + th.getClass().getName() + ", 异常信息是:" + th.getMessage();
        18. }
  1. /// 如果在@Retryable中指明了异常, 那么在@Recover中可以明确的指明是哪一种异常
  2. /// @Retryable(RemoteAccessException.class)
  3. /// public void service() {
  4. /// // ... do something
  5. /// }
  6. ///
  7. /// @Recover
  8. /// public void recover(RemoteAccessException e) {
  9. /// // ... panic
  10. /// }
  11. /// --------------------------------------------------------- @Retryable指定对应的@Recover方法
  12. /// 特别注意: @Retryable注解的recover属性, 在spring-retry的较高版本中才得以支持,
  13. /// 在本人使用的1.2.5.RELEASE版本中还暂不支持
  14. /// @Retryable(recover = "service1Recover", value = RemoteAccessException.class)
  15. /// public void service1(String str1, String str2) {
  16. /// // ... do something
  17. /// }
  18. ///
  19. /// @Retryable(recover = "service2Recover", value = RemoteAccessException.class)
  20. /// public void service2(String str1, String str2) {
  21. /// // ... do something
  22. /// }
  23. ///
  24. /// @Recover
  25. /// public void service1Recover(RemoteAccessException e, String str1, String str2) {
  26. /// // ... error handling making use of original args if required
  27. /// }
  28. ///
  29. /// @Recover
  30. /// public void service2Recover(RemoteAccessException e, String str1, String str2) {
  31. /// // ... error handling making use of original args if required
  32. /// }
  33. }
  34. 2. 测试方法:
  35. ![在这里插入图片描述][20200226235330439.png_pic_center]
  36. 3. 程序输出:
  37. ![在这里插入图片描述][20200226235350218.png_pic_center]
  • @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. 被调用的方法:

        1. private int times = 0;
        2. DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("HH:mm:ss");
        3. /** * Backoff用于指定 重试时的退避策略 * - @Retryable 或 @Retryable(backoff = @Backoff()), 那么默认延迟 1000ms后重试 * 注:第一次请求时,是马上进行的,是不会延迟的 * * 效果如: * times=1, 时间是12:02:04 * times=2, 时间是12:02:05 * times=3, 时间是12:02:06 */
        4. @Retryable(backoff = @Backoff())
        5. public String methodFive() {
        6. times++;
        7. System.err.println("times=" + times + ", 时间是" + dateTimeFormatter.format(LocalTime.now()));
        8. throw new RuntimeException("times=" + times + ", 发生的异常是RuntimeException");
        9. }
        10. /** * - delay: 延迟多久后,再进行重试。 * 注:第一次请求时,是马上进行的,是不会延迟的 * * 效果如: * times=1, 时间是11:46:36 * times=2, 时间是11:46:41 * times=3, 时间是11:46:46 */
        11. @Retryable(backoff = @Backoff(delay = 5000))
        12. public String methodFiveAlpha() {
        13. times++;
        14. System.err.println("times=" + times + ", 时间是" + dateTimeFormatter.format(LocalTime.now()));
        15. throw new RuntimeException("times=" + times + ", 发生的异常是RuntimeException");
        16. }
        17. /** * 如果不想延迟, 那么需要指定value和delay同时为0 * 注:原因可详见javadoc 或 源码 * * 效果如: * times=1, 时间是12:05:44 * times=2, 时间是12:05:44 * times=3, 时间是12:05:44 */
        18. @Retryable(backoff = @Backoff(value = 0, delay = 0))
        19. public String methodFiveBeta() {
        20. times++;
        21. System.err.println("times=" + times + ", 时间是" + dateTimeFormatter.format(LocalTime.now()));
        22. throw new RuntimeException("times=" + times + ", 发生的异常是RuntimeException");
        23. }
        24. /** * - 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 */
        25. @Retryable(maxAttempts = 5, backoff = @Backoff(delay = 3000, multiplier = 2))
        26. public String methodFiveGamma() {
        27. times++;
        28. System.err.println("times=" + times + ", 时间是" + dateTimeFormatter.format(LocalTime.now()));
        29. throw new RuntimeException("times=" + times + ", 发生的异常是RuntimeException");
        30. }
      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中引入依赖。

  1. <!-- guava retry -->
  2. <dependency>
  3. <groupId>com.github.rholder</groupId>
  4. <artifactId>guava-retrying</artifactId>
  5. <version>2.0.0</version>
  6. </dependency>

Guava Retry的使用:

  1. 比起Spring Retry的使用, Guava Retry的使用方式相对简单,这里仅给出一个简单的使用示例,更多细节可详见[https://github.com/rholder/guava-retrying][https_github.com_rholder_guava-retrying]。

简单使用示例

  1. import com.github.rholder.retry.RetryException;
  2. import com.github.rholder.retry.Retryer;
  3. import com.github.rholder.retry.RetryerBuilder;
  4. import com.github.rholder.retry.StopStrategies;
  5. import java.io.IOException;
  6. import java.util.Arrays;
  7. import java.util.concurrent.Callable;
  8. import java.util.concurrent.ExecutionException;
  9. import java.util.concurrent.ThreadLocalRandom;
  10. import java.util.zip.DataFormatException;
  11. /** * Guava Retry简单使用示例 * * @author JustryDeng * @date 2020/2/25 21:40:11 */
  12. public class XyzRemoteCall {
  13. /** * guava retry组件 使用测试 * * 提示:泛型 对应 要返回的数据的类型。 */
  14. public static void jd() {
  15. // 创建callable, 在call()方法里面编写相关业务逻辑
  16. Callable<Object[]> callable = new Callable<Object[]>() {
  17. int times = 0;
  18. @Override
  19. public Object[] call() throws Exception {
  20. // business logic
  21. times++;
  22. if (times == 1) {
  23. throw new RuntimeException();
  24. }
  25. if (times == 2) {
  26. throw new Exception();
  27. }
  28. // 随机一个数[origin, bound)
  29. int randomNum = ThreadLocalRandom.current().nextInt(1, 5);
  30. if (randomNum == 1) {
  31. throw new DataFormatException("call()抛出了检查异常DataFormatException");
  32. } else if (randomNum == 2) {
  33. throw new IOException("call()抛出了检查异常IOException");
  34. } else if (randomNum == 3) {
  35. throw new RuntimeException("call()抛出了运行时异常RuntimeException");
  36. }
  37. return new Object[]{ "邓沙利文", "亨得帅", "邓二洋", "JustryDeng"};
  38. }
  39. };
  40. // 创建重试器
  41. Retryer<Object[]> retryer = RetryerBuilder.<Object[]>newBuilder()
  42. /* * 指定什么条件下触发重试 * * 注:这里,只要callable中的call方法抛出的异常是Throwable或者 * 是Throwable的子类,那么这里都成立,都会进行重试。 */
  43. .retryIfExceptionOfType(Throwable.class)
  44. /// .retryIfException()
  45. /// .retryIfRuntimeException()
  46. /// .retryIfExceptionOfType(@Nonnull Class<? extends Throwable> exceptionClass)
  47. /// .retryIfException(@Nonnull Predicate<Throwable> exceptionPredicate)
  48. /// .retryIfResult(@Nonnull Predicate<V> resultPredicate)
  49. // 设置两次重试之间的阻塞策略(如: 设置线程sleep、设置自旋锁等等)
  50. ///.withBlockStrategy()
  51. // 设置监听器 (这个监听器可用于监听每次请求的结果信息, 并作相应的逻辑处理。 如: 统计、预警等等)
  52. ///.withRetryListener()
  53. // 设置延时策略, 每次重试前,都要延时一段时间,然后再发起请求。(第一次请求,是不会被延时的)
  54. ///.withWaitStrategy()
  55. // 设置停止重试的策略(如:这里设置的是三次请求后, 不再重试)
  56. .withStopStrategy(StopStrategies.stopAfterAttempt(3))
  57. .build();
  58. try {
  59. Object[] result = retryer.call(callable);
  60. System.err.println(Arrays.toString(result));
  61. /* * call()方法抛出的异常会被封装到RetryException或ExecutionException中, 进行抛出 * 所以在这里,可以通过 e.getCause()获取到call()方法实际抛出的异常 */
  62. } catch (RetryException|ExecutionException e) {
  63. System.err.println("call()方法抛出的异常, 实际是" + e.getCause());
  64. e.printStackTrace();
  65. }
  66. }
  67. }

Spring Retry重试组件、Guava Retry重试组件简单梳理完毕 !

^_^ 如有不当之处,欢迎指正

^_^ 参考连接
https://github.com/spring-projects/spring-retry

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

^_^ 测试代码托管连接
https://github.com/JustryDeng/CommonRepository…

^_^ 本文已经被收录进《程序员成长笔记》 ,笔者JustryDeng

发表评论

表情:
评论列表 (有 0 条评论,672人围观)

还没有评论,来说两句吧...

相关阅读

    相关 Spring Retry机制

    在调用第三方接口或者使用mq时,会出现网络抖动,连接超时等网络异常,所以需要重试。为了使处理更加健壮并且不太容易出现故障,后续的尝试操作,有时候会帮助失败的操作最后执行成功。例