SpringBoot-重试机制

449 阅读4分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

重试框架

重试,在项目需求中是非常常见的,例如遇到网络波动等,要求某个接口或者是方法可以最多/最少调用几次. 切记非幂等情况下慎用重试.

要想实现重试机制,自己手写一下控制流程的逻辑也可以达到重试的目的。但是这样比较麻烦,而且不具有通用性。Retry 重试框架,支持 AOP 切入的方式使用,而且能使用注解;想想,重试次数、重试延迟、重试触发条件、重试的回调方法等等我们都能很轻松结合注解以一种类似配置参数的方式去实现,十分优雅。

在 SpringBoot 中使用

① 引入依赖

<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
</dependency>

② 开启重试注解

在启动类上添加 @EnableRetry(proxyTargetClass = true) 注解.

@SpringBootApplication
@EnableRetry(proxyTargetClass = true)   //指明代理方式为CGLIB代理
public class RetryApplication {
    public static void main(String[] args) {
        SpringApplication.run(RetryApplication.class, args);
    }
}

此注解用于开启重试框架,可以修饰在 SpringBoot 启动类上面,也可以修饰在需要重试的类上.

其 proxyTargetClass 属性是 Boolean 类型,用于指明代理方式 true 表示 cglib 代理,false 表示 jdk 动态代理,默认使用 jdk 动态代理. 因为使用到了 AOP 技术.

③ 需要重试功能的方法

@Service
public class TestRetryService {
    @Retryable(value = Exception.class,maxAttempts = 3,
               backoff = @Backoff(delay = 2000,multiplier = 1.5))
    public int test(int code) throws Exception{
        System.out.println("test :" + LocalTime.now());
        if (code == 0){
            throw new Exception("情况不对!需要重试");    // 需要主动抛出异常,不然 retry 感知不到.
        }
        System.out.println("一切OK!");
        return 200;
    }

    @Recover
    public int recover(Exception e){
        System.out.println("回调方法执行!!!!");
        //记录日志,或调用其他方法
        return 400;
    }
}

以上面的例子, 说明 @Retryable 注解 :

@Retryable 被该注解标记的方法, 会使用重试机制.

  • value: 重试的触发机制, 当遇到指定的这种异常时,才触发重试.
  • maxAttempts: 重试的次数(该次数包含第一次调用, 也就是说如果设置 3 次, 调用一次后, 如果一直失败触发了重试, 那么还可以重试 2 次)
  • delay:重试的延迟时间, 也就是距离上一次重试调用的时间间隔, 单位 : 毫秒.
  • multiplier: delay 间隔时间的倍数, 也就是说, 第一次重试间隔如果是 2000ms, 那第二次重试的时候就是 2000ms * multiplier ms.
  • maxDelay : 重试次数之间的最大时间间隔, 默认为 0, 即忽略, 如果小于 delay 的设置, 则默认为 30000L.

@Recover,被该注解标记的方法是回调方法.

  • 当重试次数用完的时候, 如果还是调用失败, 就会调用该方法.
  • 该方法的入参很重要, 是作为回调的接头暗号. 异常类型需要与 Recover 方法参数类型保持一致.
  • recover 方法返回值需要与重试方法返回值保证一致.
  • 注意 : 该回调方法与重试方法写在同一个类里面.
  • 回调方法不是必须的, 当重试次数完了后, 如果还是没成功, 如果没有回调防范,那就抛出异常.

使用踩坑

踩坑 A

Retryable 方法只有直接通过代理对象调用时才会生效,通过其他方法间接调用不生效(所有基于 AOP 的注解都一样).

注意:在 Spring 中,只有需要用到 Aop 代理的类才会生成代理对象,不需要 Aop 的直接返回原生对象.

(1) 如果基于 CGLIB 动态代理时:可以代理目标类所有可继承方法. (所以推荐使用 CGLIB 代理)

  • @Retryable 或 @Recover 修饰的方法必须可以被继承(不能用 final,private 修饰)
  • @Retryable 与 @Recover 直接修饰在方法实现上

(2) 如果基于 JDK 动态代理时,只代理接口中的方法,所以需要注意:

  • @Retryable 或者@Recover 修饰的方法一定要在接口中声明
  • @Retryable 可以修饰在接口的方法声明上,也可以修饰在方法实现上
  • @Recover 注解只能修饰在接口的方法声明中(具体原因尚未分析)Recover 修饰在方法实现上时会提示: Cannot locate recovery method.
踩坑 B

由于 retry 用到了 aspect 增强,所有会有 aspect 的坑,就是方法内部调用,会使 aspect 增强失效,那么 retry 当然也会失效.

比如:

public class Demo {

    public void A() {
        B();
    }

    @Retryable(Exception.class)
    public void B() {
        throw new RuntimeException("retry...");
    }
}

这种情况下 B() 不会重试.

\