重试业务场景

给你一个场景,假设你负责支付服务,需要对接外部的一个渠道,调用他们的订单查询接口。他们给你说:由于网络问题,如果我们之间交互超时了,你没有收到我的任何响应,那么按照约定你可以对这个接口发起三次重试,三次之后还是没有响应,那就应该是有问题了,你们按照异常流程处理就行。假设你不知道 Spring-retry 这个组件,那么你大概率会写出这样的代码:
@Retryable注解 - 图1
调用之后,日志的输出是这样的,一目了然,非常清晰:
@Retryable注解 - 图2
正常调用一次,重试三次,一共可以调用 4 次。在第五次调用的时候抛出异常。完全符合需求,自测也完成了,可以直接提交代码,交给测试同学了。非常完美,但是你有没有想过,这样的代码其实非常的不优雅。

重试应该是一个工具类一样的通用方法,是可以抽离出来的,剥离到业务代码之外,开发的时候我们只需要关注业务代码写的巴巴适适就行了。github.com/spring-proj…
用上 spring-retry 之后,我们上面的代码就变成了这样:
@Retryable注解 - 图3
只是加上了一个 @Retryable 注解,这玩意简直简单到令人发指。

基本使用Demo

里面涉及到三个注解:

  • @EnableRetry:加在启动类上,表示支持重试功能。
  • @Retryable:加在方法上,就会给这个方法赋能,让它有用重试的功能。
  • @Recover:重试完成后还是不成功的情况下,会执行被这个注解修饰的方法。

    maven依赖

    1. <dependency>
    2. <groupId>org.springframework.retry</groupId>
    3. <artifactId>spring-retry</artifactId>
    4. <version>1.3.1</version>
    5. </dependency>

    由于该组件是依赖于 AOP 给你的,所以还需要引入这个依赖:

    1. <dependency>
    2. <groupId>org.springframework.boot</groupId>
    3. <artifactId>spring-boot-starter-aop</artifactId>
    4. <version>2.6.1</version>
    5. </dependency>

    启动类

    1. @SpringBootApplication
    2. @EnableRetry
    3. @EnableRetry(proxyTargetClass = true)
    4. public class Application {
    5. public static void main(String[] args) {
    6. SpringApplication.run(Application.class, args);
    7. }
    8. }

    业务代码

    供了注解式开发和编程式开发的示例。我们这里主要看它的注解式开发案例: ```java @Service @Slf4j public class DoRetryService {

    @Retryable(value = Exception.class, maxAttempts = 4, backoff = @Backoff(delay = 2000L, multiplier = 1.5)) public boolean doRetry(boolean isRetry) throws Exception {

    1. log.info("开始通知下游系统");
    2. log.info("通知下游系统");
    3. if (isRetry) {
    4. throw new RuntimeException("通知下游系统异常");
    5. }
    6. return true;

    } }

  1. - interceptor:可以通过该参数,指定方法拦截器的bean名称
  2. - value:抛出指定异常才会重试
  3. - include:和value一样,默认为空,当exclude也为空时,默认所以异常
  4. - exclude:指定不处理的异常
  5. - maxAttempts:最大重试次数,默认3
  6. - backoff:重试等待策略,默认使用@Backoff@Backoffvalue默认为1000L,我们设置为2000Lmultiplier(指定延迟倍数)默认为0,表示固定暂停1秒后进行重试,如果把multiplier设置为1.5,则第一次重试为2秒,第二次为3秒,第三次为4.5秒。
  7. 最后把项目跑起来,调用一笔,确实是生效了,执行了 @Recover 修饰的方法<br /> Spring-retry 的重试策略, 简单的介绍一下其中的几种含义是啥:
  8. - AlwaysRetryPolicy:允许无限重试,直到成功,此方式逻辑不当会导致死循环
  9. - NeverRetryPolicy:只允许调用RetryCallback一次,不允许重试
  10. - SimpleRetryPolicy:固定次数重试策略,默认重试最大次数为3次,RetryTemplate默认使用的策略
  11. - TimeoutRetryPolicy:超时时间重试策略,默认超时时间为1秒,在指定的超时时间内允许重试
  12. - ExceptionClassifierRetryPolicy:设置不同异常的重试策略,类似组合重试策略,区别在于这里只区分不同异常的重试
  13. - CircuitBreakerRetryPolicy:有熔断功能的重试策略,需设置3个参数openTimeoutresetTimeoutdelegate
  14. - CompositeRetryPolicy:组合重试策略,有两种组合方式,乐观组合重试策略是指只要有一个策略允许即可以重试,悲观组合重试策略是指只要有一个策略不允许即不可以重试,但不管哪种组合方式,组合中的每一个策略都会执行
  15. <a name="DfrZW"></a>
  16. ## 指定@Recover方法
  17. spring-retry1.3.0版中@Retryable增加recover属性,从而使得再异常重试失败后可以指定补偿方法执行。从而使得我们不需要每一个用到@Recover的地方都需要新建一个类。
  18. ```java
  19. @Retryable(recover = "compensateHi")
  20. @Override
  21. public Hello hi(String name) {
  22. try {
  23. Hello hello = helloClient.hi(name);
  24. return hello;
  25. }catch (Exception e){
  26. e.printStackTrace();
  27. throw new MyHttpExcetption("");
  28. }
  29. }
  30. @Recover
  31. //Throwable throwable必须写,否则无法匹配
  32. private Hello compensateHi(Throwable throwable,String name) {
  33. System.out.println("compensateHi");
  34. try {
  35. Hello hello = helloRepository.findByName(name);
  36. return hello;
  37. }catch (Exception e){
  38. throw new MyHttpExcetption("");
  39. }
  40. }
  41. @Recover
  42. //Throwable throwable必须写,否则无法匹配
  43. private Hello compensateHi2(Throwable throwable,String name) {
  44. System.out.println("compensateHi2");
  45. try {
  46. Hello hello = helloRepository.findByName(name);
  47. return hello;
  48. }catch (Exception e){
  49. throw new MyHttpExcetption("");
  50. }
  51. }

但是要注意的是,在异常重试失败进行匹配是,方法的第一个参数必须是Throwable或其子类,否则就算@Retryable中recover属性指定也无法匹配。

  1. @Recover的注解的方法第一个参数需要是 @Retryable 所捕获的重试异常类型;必须是第一个参数
  2. @Recover注解的方法的 返回值必须 和 @Retryable注解的方法返回值一致

    注意:

    但是这里也暴露了一个 Spring-retry 的弊端,就是必须要通过抛出异常的方式来触发相关业务。

听着好像也是没有毛病,但是你想想一下,假设渠道方说如果我给你返回一个 500 的 ErrorCode,那么你也可以进行重试。这样的业务场景应该也是比较多的。如果你要用 Spring-retry 会怎么做?是不是得写出这样的代码:

  1. if(errorCode==500){
  2. throw new Exception("手动抛出异常");
  3. }

注解属性含义Retryable

  1. @Target({ ElementType.METHOD, ElementType.TYPE })
  2. @Retention(RetentionPolicy.RUNTIME)
  3. @Documented
  4. public @interface Retryable {
  5. /**
  6. * 为重试方法应用重试拦截器的bean名称。与其他属性互斥
  7. */
  8. String interceptor() default "";
  9. /**
  10. * 可以重试的异常类型。与includes属性同义。默认值为空(并且如果exclude也是空的话,
  11. * 所有的异常都会重试)
  12. */
  13. Class<? extends Throwable>[] value() default {};
  14. /**
  15. * 同上
  16. */
  17. Class<? extends Throwable>[] include() default {};
  18. /**
  19. * 与include含义相反
  20. */
  21. Class<? extends Throwable>[] exclude() default {};
  22. /**
  23. * 统计报告的唯一标签。如果没有提供,调用者可以选择忽略它,或者提供一个默认值。
  24. *
  25. * @return the label for the statistics
  26. */
  27. String label() default "";
  28. /**
  29. * 标识重试有状态的:即异常重新抛出,但是重试策略使用相同的策略应用于后续的具有相同参数的
  30. * 调用。如果为false那么可重试的异常不会重新抛出。
  31. */
  32. boolean stateful() default false;
  33. /**
  34. * 尝试的最大次数(包含第一次失败),默认为3
  35. */
  36. int maxAttempts() default 3;
  37. /**
  38. * 返回一个求尝试最大次数值的表达式(包含第一次失败),默认为3
  39. * 重写 {@link #maxAttempts()}。
  40. * @since 1.2
  41. */
  42. String maxAttemptsExpression() default "";
  43. /**
  44. * 为正重试的动作指定backoff属性。默认没有backoff,但是在两次尝试之间暂定一下是一个很好的想法
  45. * (即使代价是阻塞线程)
  46. */
  47. Backoff backoff() default @Backoff();
  48. /**
  49. * 在{@code SimpleRetryPolicy.canRetry()}返回true之后指定一个计算表达式 - 可用来有条件的取消重试。
  50. * 仅在调用抛出一个异常后。求值的root对象为上一次的异常 {@code Throwable}。
  51. * 可以引用上下文中的其他beans。
  52. * 例如:
  53. * {@code "message.contains('you can retry this')"}.
  54. * and
  55. * {@code "@someBean.shouldRetry(#root)"}.
  56. * @since 1.2
  57. */
  58. String exceptionExpression() default "";
  59. }

接着我们下面来看看exceptionExpression, 一样也是写SpEL表达式

  1. @Retryable(value = IllegalAccessException.class, exceptionExpression = "message.contains('test')")
  2. public void service4(String exceptionMessage) throws IllegalAccessException {
  3. log.info("do something... {}", LocalDateTime.now());
  4. throw new IllegalAccessException(exceptionMessage);
  5. }
  6. @Retryable(value = IllegalAccessException.class, exceptionExpression = "#{message.contains('test')}")
  7. public void service4_3(String exceptionMessage) throws IllegalAccessException {
  8. log.info("do something... {}", LocalDateTime.now());
  9. throw new IllegalAccessException(exceptionMessage);
  10. }

上面的表达式exceptionExpression = “message.contains(‘test’)”的作用其实是获取到抛出来exception的message(调用了getMessage()方法),然后判断message的内容里面是否包含了test字符串,如果包含的话就会执行重试。所以如果调用方法的时候传入的参数exceptionMessage中包含了test字符串的话就会执行重试。
但这里值得注意的是, Spring Retry 1.2.5之后exceptionExpression是可以省略掉#{…}

还可以在表达式中执行一个方法,前提是方法的类在spring容器中注册了,@retryService其实就是获取bean name为retryService的bean,然后调用里面的checkException方法,传入的参数为#root,它其实就是抛出来的exception对象。一样的也是可以省略#{…}

  1. @Retryable(value = IllegalAccessException.class, exceptionExpression = "#{@retryService.checkException(#root)}")
  2. public void service5(String exceptionMessage) throws IllegalAccessException {
  3. log.info("do something... {}", LocalDateTime.now());
  4. throw new IllegalAccessException(exceptionMessage);
  5. }
  6. @Retryable(value = IllegalAccessException.class, exceptionExpression = "@retryService.checkException(#root)")
  7. public void service5_1(String exceptionMessage) throws IllegalAccessException {
  8. public boolean checkException(Exception e) {
  9. log.error("error message:{}", e.getMessage());
  10. return true; //返回true的话表明会执行重试,如果返回false则不会执行重试

https://juejin.cn/post/7094613787973517343
详细使用:https://blog.csdn.net/u010597819/article/details/108301369
详细使用:https://www.jb51.net/article/245471.htm
源码解析:https://juejin.cn/post/7054024739001466916