工作中会遇到一些流程冗长的业务逻辑,为了保证整个链路能够走通需要有一些容错率,所以重试也就必不可少了,很多人重试可能就是写个for或者while循环来进行重试,那么有没有稍微优雅一些的做法呢?这就涉及到两个常见的重试方案:Guava Retry与Spring Retry,我们一一介绍一下

项目Demo

https://github.com/fzustone/retry

环境

Guava 31.1.1

Spring Boot 2.4.4

JDK 8

Guava Retry

官方仓库

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

从仓库的提交记录来看这个项目已经很久没有更新过了上一次提交是五年前,其中有些依赖Guava其他包的API已经无法使用,比如The problem with 'AttemptTimeLimiters.fixedTimeLimit' ,这里的SimpleTimeLimiter类的构造函数已经变成私有方法需要使用create方法声明新对象。

Maven

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

示例

public static void main(String[] args) {
   //定义重试机制
   Retryer<Double> retryer = RetryerBuilder.<Double>newBuilder()
         //retryIf 重试条件
         .retryIfException()
         .retryIfResult(Objects::isNull)
         .retryIfException(throwable -> Objects.equals(throwable, new Exception()))
         .retryIfExceptionOfType(Exception.class)
         .retryIfRuntimeException()

         //时间限制 : 某次请求不得超过2s  该方法因SimpleTimeLimiter构造函数变更已失效无法使用
         //.withAttemptTimeLimiter(AttemptTimeLimiters.fixedTimeLimit(2, TimeUnit.SECONDS))
         //停止策略 : 尝试请求6次
         .withStopStrategy(StopStrategies.stopAfterAttempt(6))
         //等待策略:每次请求间隔1s
         .withWaitStrategy(WaitStrategies.fixedWait(1, TimeUnit.SECONDS))
         //如何实现重试的时间间隔,默认的阻塞策略:线程睡眠
         .withBlockStrategy(BlockStrategies.threadSleepStrategy())
         //自定义重试监听器,在call方法调用结束执行
         .withRetryListener(new CustomRetryListener())
         .build();

   //定义请求实现
   Callable<Double> callable = () -> {
      Calculator calculator = new Calculator();
      return calculator.divide(1, 0);

   };


   //利用重试器调用请求
   try {
      retryer.call(callable);
   } catch (RetryException | ExecutionException e) {
      LOGGER.error("重试失败,出现异常", e);
   }
}
public class CustomRetryListener implements RetryListener {
   private static final Logger LOGGER = LoggerFactory.getLogger(CustomRetryListener.class);

   @Override
   public <V> void onRetry(Attempt<V> attempt) {

      // 第几次重试(注意:第一次重试其实是第一次调用)
      LOGGER.info("retry time : [{}]", attempt.getAttemptNumber());

      // 距离第一次重试的延迟
      LOGGER.info("retry delay : [{}]", attempt.getDelaySinceFirstAttempt());

      // 是否因异常终止
      if (attempt.hasException()) {
         LOGGER.info("causeBy:", attempt.getExceptionCause());
      }
      // 正常返回时的结果
      if (attempt.hasResult()) {
         LOGGER.info("result={}", attempt.getResult());
      }

   }
}
public class Calculator {
   private static final Logger LOGGER = LoggerFactory.getLogger(Calculator.class);

   public double divide(double a, double b) throws Exception {
      if (b == 0) {
         LOGGER.error("被除数不能为0");
         throw new Exception("被除数是0");
      }
      return a / b;
   }
}

重试的代码并不复杂这里我们直接翻call()源码看看,可以大概知道它的实现其实是一个for循环,大致流程:

1.进入for循环,设定循环次数
2.调用实际的业务方法得到结果
3.包装结果,以在监听器使用
4.循环调用监听器方法
5.判断结果是否在重试的条件范围中

优缺点

优点

  1. 可重试的条件多,不仅仅可以针对异常还可以针对结果
  2. 可自定义等待与阻塞策略

缺点

  1. 导致重试的异常会被重新包装成RetryException,无法得到具体堆栈,只能在监听器获取详情

Spring Retry

官方仓库

https://github.com/spring-projects/spring-retry

Maven

<!-- Aop依赖 -->
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

<!-- 重试机制 -->
<dependency>
   <groupId>org.springframework.retry</groupId>
   <artifactId>spring-retry</artifactId>
</dependency>

spring-retry版本为1.3.1

示例

如果想要使用Spring Retry需要先注解开启,在配置类上添加注解 @EnableRetry

例如:

@EnableRetry
@SpringBootApplication
public class RetryApplication {

   public static void main(String[] args) {
      SpringApplication.run(RetryApplication.class, args);
   }

}
private static final Logger LOGGER = LoggerFactory.getLogger(SpringRetry.class);

@Retryable(value = Exception.class, maxAttempts = 5, backoff = @Backoff(delay = 2000L, multiplier = 1.5))
public double test1(double a, double b) throws Exception {

   if (b == 0) {
      LOGGER.error("被除数不能为0");
      throw new Exception("被除数是0");
   }
   return a / b;
}

@Recover
public double recover1(RuntimeException ex) {
   LOGGER.info("执行recover1方法");
   throw ex;
}

先来看下关键的注解@Retryable

public @interface Retryable {
//指定recover方法的方法名,1.2的版本没有这个属性
    String recover() default "";
//重试拦截器bean名称,用于可重试方法。与其他属性互斥。
    String interceptor() default "";
//指定处理的异常类
    Class<? extends Throwable>[] value() default {};
//指定处理的异常类和value一样,默认为空,当exclude也为空时,默认所有异常
    Class<? extends Throwable>[] include() default {};
//指定异常不处理,默认空,当include也为空时,默认所有异常
    Class<? extends Throwable>[] exclude() default {};
//标签,在上下文可以从属性名为RetryContext.NAME获取
//https://stackoverflow.com/questions/60822379/how-can-i-use-retryablelabel-for-logging-or-monitoring-purposes
    String label() default "";
//表示重试是有状态的标志:即,异常被重新引发,但是重试策略与相同的策略应用于具有相同参数的后续调用。如果为false,则不会重新引发可重试的异常。
//针对@Transactional此类需要事务回滚,为true那么重试不会被触发,会直接进行回滚,
//反之进行重试
    boolean stateful() default false;
//最大重试次数。默认3次
    int maxAttempts() default 3;
//表达式形式的最大次数 例如maxAttemptsExpression = "${max.retry.attempts}"
    String maxAttemptsExpression() default "";
//见下文
    Backoff backoff() default @Backoff;
//异常的表达式形式
    String exceptionExpression() default "";
//监听器
    String[] listeners() default {};
}

关于stateful属性可以参见Github的issue:stateful @Retryable doesn't retry以及Spring文档

关于maxAttemptsExpression属性可见:Spring Retry not working and getting exception for maxAttemptsExpression value

//等同于delay 指定延迟后重试,默认为1000毫秒
long value() default 1000;

long delay() default 0;
//重试之间的最大等待时间 默认为0即忽略
long maxDelay() default 0;
//指定延迟倍数,默认为0,表示固定暂停1秒后进行重试
double multiplier() default 0;
//表达式形式的延迟时间
String delayExpression() default "";
//表达式形式的重试之间的最大等待时间
String maxDelayExpression() default "";
//表达式形式的指定延迟倍数,默认为0,表示固定暂停1秒后进行重试
String multiplierExpression() default "";
//是否随机化,让延迟时间落得更均匀
boolean random() default false;

random的用途如下:

wait time = delay * (1.0D + random.nextFloat() * (multiplier - 1.0D))if(delay > maxDelay){
    delay = maxDelay
}else{ 
    delay = delay * multiplier
}

优缺点

优点

  1. 注解形式,使用方便

缺点

  1. 只能处理Throwable类型的异常的重试

避坑指南

在1.3之前的版本如果一个类里有多个方法需要重试是无法指定recover方法的,现在的版本可以指定方法名,但是要注意的是recover方法必须符合

  1. 入参的第一个参数是异常变量,剩余的变量与业务方法类型一致
  2. 如果业务方法入参有基本类型的变量,recover方法需要转成包装类型才能被识别到,但是recover的出参则不能转成包装类型否则会被过滤

@Retryable重试不生效

https://www.jianshu.com/p/f6b539a36b93

一个需要进行AOP增强的类,外部调用methodA()且该方法中调用methodB(),调用methodB()不会执行AOP的增强逻辑。真正执行methodA()的是目标对象,那么methodA()中调用methodB()就是目标对象的方法而不是代理对象的,也就自然不会执行AOP的增强逻辑。所以内部调用时要调用代理对象的方法。

参考:

https://medium.com/@vmoulds01/springboot-retry-random-backoff-136f41a3211a

https://juejin.cn/post/6844903582022516744