一、简介

1、重试机制

重试机制在网络服务中非常的重要,由于网路可能存在延迟,网络抖动,网络不稳定的情况。同时在分布式服务中网络的请求的高度密集,有些服务不一定能在规定的时间内完成访问。应该请求服务需要重试几次。以保证服务请求成功。

例如对接支付接口时,因为回调比较重要,当访问失败时会进行重试,不过此时的重试机制时间是逐步加大,例如30s/1m/10m/1h等,最终到达阈值不在重试

对于重试是有场景限制的,不是什么场景都适合重试,比如参数校验不合法、写操作等(要考虑写是否幂等)都不适合重试远程调用超时、网络突然中断可以重试。在微服务治理框架中,通常都有自己的重试与超时配置,比如dubbo可以设置retries=1,timeout=500调用失败只重试1次,超过500ms调用仍未返回则调用失败。比如外部 RPC 调用,或者数据入库等操作,如果一次操作失败,可以进行多次重试,提高调用成功的可能性

2、重试机制设计、共性和原理

  • 无侵入:这个好理解,不改动当前的业务逻辑,对于需要重试的地方,可以很简单的实现

  • 可配置:包括重试次数,重试的间隔时间,是否使用异步方式等

  • 通用性:最好是无改动(或者很小改动)的支持绝大部分的场景,拿过来直接可用

  • 正常和重试优雅解耦,重试断言条件实例或逻辑异常实例是两者沟通的媒介

  • 约定重试间隔,差异性重试策略,设置重试超时时间,进一步保证重试有效性以及重试流程稳定性

  • 都使用了命令设计模式,通过委托重试对象完成相应的逻辑操作,同时内部封装实现重试逻辑

  • Spring-RetryGuava-Retryer工具都是线程安全的重试,能够支持并发业务场景的重试逻辑正确性

3、硬编码重试

本文会详细介绍Spring-RetryrGuava-Retry两个重试组件,再次之前先看一下硬编码重试方法

@Slf4j
public class OldRetry {

    public static void main(String[] args) throws Exception {
        callTry();
    }

    public static void callTry() throws Exception{
        // 最大调用次数
        int maxCount = 4;
        int retryCount = 0;
        for (int i = 0; i <= maxCount; i++) {
            if(retryCount>maxCount-1){
                log.info("第{}次调用,重试次数达到上限,抛出异常,当前retryCount:{}",i,retryCount);
                throw new Exception("重试次数达到上限");
            }

            try{
                // 模拟调用超时
                queryOrder();
                break;
            }catch (Exception e){
                retryCount++;
                log.error("第{}次调用出现异常,准备重试,当前retryCount:{}",i,retryCount);
            }


        }

    }


    private static void queryOrder() throws Exception{
        throw new TimeoutException("接口调用超时");
    }
}

二、重试框架之Spring-Retry

1、介绍

参考地址:https://github.com/spring-projects/spring-retry

Spring Retry 为 Spring 应用程序提供了声明性重试支持。它用于Spring批处理、Spring集成、Apache Hadoop(等等)。它主要是针对可能抛出异常的一些调用操作,进行有策略的重试

环境搭建首先进行pom.xml进入

<dependency>
  <groupId>org.springframework.retry</groupId>
  <artifactId>spring-retry</artifactId>
  <version>1.3.3</version>
</dependency>
<dependency>
  <groupId>org.projectlombok</groupId>
  <artifactId>lombok</artifactId>
</dependency>
<dependency>
  <groupId>org.apache.commons</groupId>
  <artifactId>commons-lang3</artifactId>
</dependency>

2、Spring-Retry的普通使用方式

2.1 Demo搭建

准备一个任务方法,我这里是采用一个随机整数,根据不同的条件返回不同的值,或者抛出异常

@Slf4j
public class RetryDemoTask {

    /**
     * 重试方法
     * @return
     */
    public static boolean retryTask(String param)  {
        log.info("收到请求参数:{}",param);

        int i = RandomUtils.nextInt(0,11);
        log.info("随机生成的数:{}",i);
        if (i == 0) {
            log.info("为0,抛出参数异常.");
            throw new IllegalArgumentException("参数异常");
        }else if (i  == 1){
            log.info("为1,返回true.");
            return true;
        }else if (i == 2){
            log.info("为2,返回false.");
            return false;
        }else{
            //为其他
            log.info("大于2,抛出自定义异常.");
            throw new RemoteAccessException("大于2,抛出远程访问异常");
        }
    }

}

业务重试代码

@Slf4j
public class SpringRetryTemplateTest {

    /**
     * 重试间隔时间ms,默认1000ms
     * */
    private static long fixedPeriodTime = 1000L;
    /**
     * 最大重试次数,默认为3
     */
    private static int maxRetryTimes = 3;
    /**
     * 表示哪些异常需要重试,key表示异常的字节码,value为true表示需要重试
     */
    private static Map<Class<? extends Throwable>, Boolean> exceptionMap = new HashMap<>();

    public static void main(String[] args) {
        exceptionMap.put(RemoteAccessException.class,true);

        // 构建重试模板实例
        RetryTemplate retryTemplate = new RetryTemplate();

        // 设置重试回退操作策略,主要设置重试间隔时间
        FixedBackOffPolicy backOffPolicy = new FixedBackOffPolicy();
        backOffPolicy.setBackOffPeriod(fixedPeriodTime);

        // 设置重试策略,主要设置重试次数
        SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy(maxRetryTimes, exceptionMap);

        retryTemplate.setRetryPolicy(retryPolicy);
        retryTemplate.setBackOffPolicy(backOffPolicy);

        Boolean execute = retryTemplate.execute(
                //RetryCallback 重试方法
                retryContext -> {
                    boolean b = RetryDemoTask.retryTask("abc");
                    log.info("调用的结果:{}", b);
                    return b;
                },
                retryContext -> {
                    //RecoveryCallback 达到最大值的方法
                    log.info("已达到最大重试次数或抛出了不重试的异常~~~");
                    return false;
                }
        );

        log.info("执行结果:{}",execute);

    }
}
  • RetryTemplate 承担了重试执行者的角色,它可以设置SimpleRetryPolicy(重试策略,设置重试上限,重试的根源实体),FixedBackOffPolicy(固定的回退策略,设置执行重试回退的时间间隔)。

  • RetryTemplate通过execute提交执行操作,需要准备RetryCallbackRecoveryCallback 两个类实例,前者对应的就是重试回调逻辑实例,包装正常的功能操作,RecoveryCallback实现的是整个执行操作结束的恢复操作实例

  • 只有在调用的时候抛出了异常,并且异常是在exceptionMap中配置的异常,才会执行重试操作,否则就调用到excute方法的第二个执行方法RecoveryCallback

2.2 重试策略

  • NeverRetryPolicy: 只允许调用RetryCallback一次,不允许重试

  • AlwaysRetryPolicy: 允许无限重试,直到成功,此方式逻辑不当会导致死循环

  • SimpleRetryPolicy: 固定次数重试策略,默认重试最大次数为3次,RetryTemplate默认使用的策略

  • TimeoutRetryPolicy: 超时时间重试策略,默认超时时间为1秒,在指定的超时时间内允许重试

  • ExceptionClassifierRetryPolicy: 设置不同异常的重试策略,类似组合重试策略,区别在于这里只区分不同异常的重试

  • CircuitBreakerRetryPolicy: 有熔断功能的重试策略,需设置3个参数openTimeoutresetTimeoutdelegate

  • CompositeRetryPolicy: 组合重试策略,有两种组合方式,乐观组合重试策略是指只要有一个策略允许即可以重试,悲观组合重试策略是指只要有一个策略不允许即可以重试,但不管哪种组合方式,组合中的每一个策略都会执行

2.3 重试回退策略

重试回退策略,指的是每次重试是立即重试还是等待一段时间后重试。默认情况下是立即重试,如果需要配置等待一段时间后重试则需要指定回退策略BackoffRetryPolicy

  • NoBackOffPolicy: 无退避算法策略,每次重试时立即重试

  • FixedBackOffPolicy: 固定时间的退避策略,需设置参数sleeperbackOffPeriodsleeper指定等待策略,默认是Thread.sleep,即线程休眠,backOffPeriod指定休眠时间,默认1秒

  • UniformRandomBackOffPolicy: 随机时间退避策略,需设置sleeperminBackOffPeriodmaxBackOffPeriod,该策略在minBackOffPeriod,maxBackOffPeriod之间取一个随机休眠时间,minBackOffPeriod默认500毫秒,maxBackOffPeriod默认1500毫秒

  • ExponentialBackOffPolicy: 指数退避策略,需设置参数sleeperinitialIntervalmaxIntervalmultiplier,initialInterval指定初始休眠时间,默认100毫秒,maxInterval指定最大休眠时间,默认30秒,multiplier指定乘数,即下一次休眠时间为当前休眠时间*multiplier

  • ExponentialRandomBackOffPolicy: 随机指数退避策略,引入随机乘数可以实现随机乘数回退

我们可以根据自己的应用场景和需求,使用不同的策略,不过一般使用默认的就足够了。

2.4 其他扩展

配置重试策略和退避策略

@Configuration
public class SpringRetryConfig {

    @Bean("retryTemplateFixed")
    public RetryTemplate retryTemplateFixed() {
        // 1.重试策略
        // 触发条件
        Map<Class<? extends Throwable>, Boolean> exceptionMap = new HashMap<>();
        exceptionMap.put(RemoteAccessException.class, true);

        // 重试次数设置为3次
        int maxAttempts = 3;
        SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy(maxAttempts, exceptionMap);

        // 2.重试间隔设置为1秒
        FixedBackOffPolicy backOffPolicy = new FixedBackOffPolicy();
        backOffPolicy.setBackOffPeriod(1000);

        // 3.构造RetryTemplate
        RetryTemplate retryTemplate = new RetryTemplate();
        retryTemplate.setRetryPolicy(retryPolicy);
        retryTemplate.setBackOffPolicy(backOffPolicy);
        return retryTemplate;
    }

    @Bean("retryTemplate")
    public RetryTemplate retryTemplate() {
        // 定义简易重试策略,最大重试次数为3次,重试间隔为3s
        return RetryTemplate.builder()
                .maxAttempts(3)
                .fixedBackoff(3000)
                .retryOn(RuntimeException.class)
                .build();
    }


    /**
     * spring retry支持的重试策略和退避策略
    */
    @Bean("retryTemplateDemo")
    public RetryTemplate retryTemplateDemo() {
        // 1.重试策略
        // 不重试
        NeverRetryPolicy neverRetryPolicy = new NeverRetryPolicy();

        // 无限重试
        AlwaysRetryPolicy alwaysRetryPolicy = new AlwaysRetryPolicy();

        // 设置不同异常的重试策略,类似组合重试策略,区别在于这里只区分不同异常的重试
        ExceptionClassifierRetryPolicy exceptionClassifierRetryPolicy = new ExceptionClassifierRetryPolicy();
        final Map<Class<? extends Throwable>, RetryPolicy> policyMap = new HashMap<>(3);
        policyMap.put(IOException.class, alwaysRetryPolicy);
        policyMap.put(InterruptedIOException.class, neverRetryPolicy);
        policyMap.put(UnknownHostException.class, neverRetryPolicy);
        exceptionClassifierRetryPolicy.setPolicyMap(policyMap);

        // 固定次数重试,默认最大重试次数为5次,RetryTemplate默认重试策略
        SimpleRetryPolicy simpleRetryPolicy = new SimpleRetryPolicy();
        simpleRetryPolicy.setMaxAttempts(5);

        // 超时时间重试,默认超时时间为1秒,在指定的超时时间内重试
        TimeoutRetryPolicy timeoutRetryPolicy = new TimeoutRetryPolicy();
        timeoutRetryPolicy.setTimeout(3000);

        /*
         * 组合重试策略,有两种组合方式:
         *  1.悲观默认重试,有不重试的策略则不重试。
         *  2.乐观默认不重试,有需要重试的策略则重试。
         */
        CompositeRetryPolicy compositeRetryPolicy = new CompositeRetryPolicy();
        compositeRetryPolicy.setOptimistic(true);
        compositeRetryPolicy.setPolicies(new RetryPolicy[]{simpleRetryPolicy, timeoutRetryPolicy});

        // 有熔断功能的重试
        CircuitBreakerRetryPolicy circuitBreakerRetryPolicy = new CircuitBreakerRetryPolicy(compositeRetryPolicy);
        // 5s内失败10次,则开启熔断
        circuitBreakerRetryPolicy.setOpenTimeout(5000);
        // 10s之后熔断恢复
        circuitBreakerRetryPolicy.setResetTimeout(10000);

        // 2.退避策略(上一次执行失败之后,间隔多久进行下一次重试)
        // 立即重试
        NoBackOffPolicy noBackOffPolicy = new NoBackOffPolicy();

        // 固定时间后重试,默认1s
        FixedBackOffPolicy fixedBackOffPolicy = new FixedBackOffPolicy();
        fixedBackOffPolicy.setBackOffPeriod(1000);

        // 随机时间后重试(如下:从500ms到1500ms内取一个随机时间后进行重试)
        UniformRandomBackOffPolicy uniformRandomBackOffPolicy = new UniformRandomBackOffPolicy();
        uniformRandomBackOffPolicy.setMinBackOffPeriod(500);
        uniformRandomBackOffPolicy.setMaxBackOffPeriod(1500);

        // 指数退避策略(如下:初始休眠时间100ms,最大休眠时间30s,下一次休眠时间为当前休眠时间*2)
        ExponentialBackOffPolicy exponentialBackOffPolicy = new ExponentialBackOffPolicy();
        exponentialBackOffPolicy.setInitialInterval(100);
        exponentialBackOffPolicy.setMaxInterval(30000);
        exponentialBackOffPolicy.setMultiplier(2);

        // 随机指数退避策略
        ExponentialRandomBackOffPolicy exponentialRandomBackOffPolicy = new ExponentialRandomBackOffPolicy();
        exponentialRandomBackOffPolicy.setInitialInterval(100);
        exponentialRandomBackOffPolicy.setMaxInterval(30000);
        exponentialRandomBackOffPolicy.setMultiplier(2);

        // 3.return
        RetryTemplate retryTemplate = new RetryTemplate();
        retryTemplate.setRetryPolicy(circuitBreakerRetryPolicy);
        return retryTemplate;
    }
}

测试

@SpringBootTest
@Slf4j
public class RetryTest {

    // 注入RetryTemplate
    @Resource
    private RetryTemplate retryTemplateFixed;

    @Test
    public void test() {
        // 执行
        Boolean execute = retryTemplateFixed.execute(
                // 重试回调
                retryContext -> {
                    System.out.println(new Date());
                    boolean b = RetryTask.retryTask("abc");
                    log.info("调用的结果:{}", b);
                    return b;
                },
                // 恢复回调(达到最大重试次数,或者抛出不满足重试条件的异常)
                retryContext -> {
                    log.info("已达到最大重试次数或抛出了不重试的异常~~~");
                    return false;
                }
        );

        log.info("执行结果:{}",execute);

    }

}

3、Spring-Retry注解式(推荐)

3.1 注解介绍

下面注解方法为常用方法,具体可以自己探索

@EnableRetry

表示是否开始重试组件

属性类型默认值说明
proxyTargetClassbooleanfalse指示是否要创建基于子类的(CGLIB)代理,而不是创建标准的基于Java接口的代理

@Retryable

标注此注解的方法在发送异常时会进行重试

属性类型默认值说明
interceptorString“”将interceptor的bean名称应用到retryable(),和其他的属性互斥
includeClass[]{}哪些异常可以触发重试 ,默认为空
excludeClass[]{}哪些异常将不会触发重试,默认为空,如果和include属性同时为空,则所有的异常都将会触发重试
valueClass[]{}可重试的异常类型
labelString“”统计报告的唯—标签。如果没有提供,调用者可以选择忽略它,或者提供默认值
maxAttemptsint3尝试的最大次数(包括第一次失败),默认为3次
backoff@Backoff@Backoff()@Backoff @Backoff()指定用于重试此操作的backoff属性。默认为空

@Backoff

属性类型默认值说明
delaylong0如果不设置则默认使用1000 ms等待重试,和value同义词
maxDelaylong0最大重试等待时间
multiplierlong0用于计算下一个延迟延迟的乘数(大于0生效)
randombooleanfalse随机重试等待时间

3.2 注解式实战

因为注解需要用到切面,所以需要引入依赖

<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-aspects</artifactId>
  <version>5.3.22</version>
</dependency>

配置注解式重试方法

@Service
@Slf4j
public class SpringRetryDemo   {

    /**
     * 重试所调用方法
     * @param param
     * @return
     */
    @Retryable(value = {RemoteAccessException.class},maxAttempts = 3,backoff = @Backoff(delay = 2000L,multiplier = 2))
    public boolean call(String param){
        return RetryDemoTask.retryTask(param);
    }

    /**
     * 达到最大重试次数,或抛出了一个没有指定进行重试的异常
     * recover 机制
     * @param e 异常
     */
    @Recover
    public boolean recover(Exception e,String param) {
        log.error("达到最大重试次数,或抛出了一个没有指定进行重试的异常:",e);
        return false;
    }
    
    /**
     * 达到最大重试次数,或抛出了一个没有指定进行重试的异常
     * recover 机制
     * @param e 异常
     */
    @Recover
    public boolean recover(Exception e,String param) {
        log.error("达到最大重试次数,或抛出了一个没有指定进行重试的异常:",e);
        return false;
    }
    
    /**
     * 可以自定义异常的兜底方案,注意返回方法要对,否则会失效,会优先匹配子类
    */
    @Recover
    public boolean recover1(RuntimeException npe, String param){
        log.error("达到最大重试次数,或抛出了一个空指针异常:");
        return true;
    }

}

进行测试,发现可以成功重试

@SpringBootTest
@Slf4j
class RetryApplicationTests {

  @Autowired
  private SpringRetryDemo springRetryDemo;

  @Test
  public void test01(){
    boolean abc = springRetryDemo.call("abc");
    log.info("--结果是:{}--",abc);
  }

}

4、监听重试过程

4.1 简介

  • 通过实现RetryListener接口,重写open、close、onError这三个方法,既可以完成对重试过程的追踪,也可以添加额外的处理逻辑;

  • 通过继承RetryListenerSupport,也可以从open、close、onError这三个方法中,选择性的重写

普通方式使用时(注解方式不需要),在实例化RetryTemplate时,配置上该RetryListener实例即可:retryTemplate.setListeners(new RetryListener[] {retryListenerTemplate});另外每个RetryTemplate可以注册多个监听器,其中onOpen、onClose方法按照注册顺序执行,onError按照注册顺序的相反顺序执行

4.2 实现RetryListener接口

@Slf4j
@Component
public class RetryListenerTemplate implements RetryListener {
    // 进入重试前调用
    @Override
    public <T, E extends Throwable> boolean open(RetryContext retryContext, RetryCallback<T, E> retryCallback) {
        log.info("--------------------------进入重试方法(实现)--------------------------");
        return true;
    }

    // 重试结束后调用
    @Override
    public <T, E extends Throwable> void close(RetryContext retryContext, RetryCallback<T, E> retryCallback, Throwable throwable) {
        log.info("--------------------------重试方法结束(实现)--------------------------");
    }

    // 捕获到异常时调用
    @Override
    public <T, E extends Throwable> void onError(RetryContext retryContext, RetryCallback<T, E> retryCallback, Throwable throwable) {
        log.info("--------------------------第" + retryContext.getRetryCount() + "次重试(实现)--------------------------");
        log.error(throwable.getMessage(), throwable);
    }
}

4.3 继承RetryListenerSupport

@Component
@Slf4j
public class RetryListenerTemplate extends RetryListenerSupport {

    @Override
    public <T, E extends Throwable> boolean open(RetryContext context, RetryCallback<T, E> callback) {
        log.info("--------------------------进入重试方法(继承)--------------------------");
        return super.open(context, callback);
    }

    @Override
    public <T, E extends Throwable> void close(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
        log.info("--------------------------重试方法结束(继承)--------------------------");
        super.close(context, callback, throwable);
    }

    @Override
    public <T, E extends Throwable> void onError(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
        log.info("--------------------------第" + context.getRetryCount() + "次重试(继承)--------------------------");
        log.error(throwable.getMessage(), throwable);
        super.onError(context, callback, throwable);
    }
}

三、重试框架之Guava-Retrying

1、介绍

源码地址:https://github.com/rholder/guava-retrying

Guava retryer工具与spring-retry类似,都是通过定义重试者角色来包装正常逻辑重试,但是Guava retryer有更优的策略定义,在支持重试次数和重试频度控制基础上,能够兼容支持多个异常或者自定义实体对象的重试源定义,让重试功能有更多的灵活性。

Guava Retryer也是线程安全的,入口调用逻辑采用的是Java.util.concurrent.Callable的call方法

首先需要引入依赖

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

2、Guava-Retrying普通使用方式(官方)

2.1 Demo实战

首先创建一个服务类

@Slf4j
public class RetryDemoTask {

    /**
     * 重试方法
     * @return
     */
    public static boolean retryTask(String param)  {
        log.info("收到请求参数:{}",param);

        int i = RandomUtils.nextInt(0,11);
        log.info("随机生成的数:{}",i);
        if (i < 2) {
            log.info("小于2,抛出参数异常.");
            throw new IllegalArgumentException("参数异常");
        }else if (i  < 5){
            log.info("小于5,返回true.");
            return true;
        }else if (i < 7){
            log.info("小于7,返回false.");
            return false;
        }else{
            //为其他
            log.info("大于等于7,抛出自定义异常.");
            throw new RemoteAccessException("大于等于7,抛出自定义异常");
        }
    }

}

测试类

@SpringBootTest
@Slf4j
public class GuavaRetryTest {

    @Test
    public void fun01(){
        // RetryerBuilder 构建重试实例 retryer,可以设置重试源且可以支持多个重试源,可以配置重试次数或重试超时时间,以及可以配置等待时间间隔
        Retryer<Boolean> retryer = RetryerBuilder.<Boolean> newBuilder()
                .retryIfExceptionOfType(RemoteAccessException.class)//设置异常重试源
                .retryIfResult(res-> res==false)  //设置根据结果重试
                .withWaitStrategy(WaitStrategies.fixedWait(3, TimeUnit.SECONDS)) //设置等待间隔时间
                .withStopStrategy(StopStrategies.stopAfterAttempt(3)) //设置最大重试次数
                .withRetryListener(new RetryListener() {
                    @Override
                    public <V> void onRetry(Attempt<V> attempt) {
                        log.info("第【{}】次重试调用",attempt.getAttemptNumber());
                    }
                })
                .withBlockStrategy(l -> {
                    log.info("重试阻塞时间:{}",l);
                })
                .build();
        try {
            retryer.call(() -> RetryDemoTask.retryTask("abc"));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
}

2.2 重试机制

RetryerBuilder的retryIfXXX()方法用来设置在什么情况下进行重试,总体上可以分为根据执行异常进行重试根据方法执行结果进行重试两类。

方法描述
retryIfException()抛出 runtime 异常、checked 异常时都会重试,但是抛出 error 不会重试
retryIfRuntimeException()会在抛 runtime 异常的时候才重试,checked 异常和error 都不重试
retryIfException(Predicate exceptionPredicate)这里当发生异常时,会将异常传递给exceptionPredicate,那我们就可以通过传入的异常进行更加自定义的方式来决定什么时候进行重试
retryIfExceptionOfType(Class<? extends Throwable> exceptionClass)许我们只在发生特定异常的时候才重试,比如NullPointerException 和 IllegalStateException 都属于 runtime 异常,也包括自定义的error
retryIfResult(@Nonnull Predicate resultPredicate)传入的resultPredicate返回true时则进行重试

2.3 停止重试相关策略

StopStrategy

停止重试策略用来决定什么时候不进行重试,其接口com.github.rholder.retry.StopStrategy,停止重试策略的实现类均在com.github.rholder.retry.StopStrategies中,它是一个策略工厂类

  • NeverStopStrategy:此策略将永远重试,永不停止

  • StopAfterAttemptStrategy:当执行次数到达指定次数之后停止重试

  • StopAfterDelayStrategy:当距离方法的第一次执行超出了指定的delay时间时停止,也就是说一直进行重试,当进行下一次重试的时候会判断从第一次执行到现在的所消耗的时间是否超过了这里指定的delay时间,查看其实现

WaitStrategy

  • IncrementingWaitStrategy:在决定任务间隔时间时,返回的是一个递增的间隔时间,即每次任务重试间隔时间逐步递增,越来越长

  • RandomWaitStrategy:返回一个随机的间隔时长,我们需要传入的就是一个最小间隔和最大间隔,然后随机返回介于两者之间的一个间隔时长

  • FixedWaitStrategy:返回一个固定时长的重试间隔

  • ExceptionWaitStrategy:由方法执行异常来决定是否重试任务之间进行间隔等待,以及间隔多久

  • FibonacciWaitStrategy:与IncrementingWaitStrategy有点相似,间隔时间都是随着重试次数的增加而递增的,不同的是,FibonacciWaitStrategy是按照斐波那契数列来进行计算的,使用这个策略时,我们需要传入一个乘数因子和最大间隔时长

  • ExponentialWaitStrategy:与IncrementingWaitStrategy、FibonacciWaitStrategy也类似,间隔时间都是随着重试次数的增加而递增的,但是该策略的递增是呈指数级递增

  • WaitStrategy:随机时间间隔以及不等待

RetryListener

当发生重试时,将会调用RetryListener的onRetry方法,此时我们可以进行比如记录日志等额外操作

3、Guava-Retrying注解式(非官方)

因为注解需要用到切面,所以需要引入依赖

<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-aspects</artifactId>
  <version>5.3.22</version>
</dependency>

创建自定义注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Retry {
    //指定异常时重试  由于pjp.proceed();需要try catch 异常,会有问题暂时没找到解决方法,先注释
    Class[] exceptionClass() default {};

    //出现Exception时重试
    boolean retryIfException() default false;

    //程序出现RuntimeException异常时重试
    boolean retryIfRuntimeException() default false;

    //重试次数
    int attemptNumber() default 3;

    //重试间隔 ms
    long waitStrategySleepTime() default 1000;

    //持续时间; 期间
    long duration() default 0;

    //返回值为指定字符串时重试
    String returnResult() default "willRetryString";

    //返回值为false时重试(默认关闭)  不支持同时设置指定返回字符串重试
    boolean closeReturnFalseRetry() default true;

}

创建AOP切面方法增强

@Aspect
@Component
@Slf4j
public class RetryAspect {
    @Pointcut("@annotation(com.example.retry.guavaRetrying.Retry)")
    public void MethodCallConstraintPointcut(){
    }

    @Around(value = "@annotation(com.example.retry.guavaRetrying.Retry)")
    public Object monitorAround(ProceedingJoinPoint point) throws Throwable {
        log.info("进入重试切面");
        Method method = null;
        //判断注解是否method 上
        if (point.getSignature() instanceof MethodSignature) {
            MethodSignature signature = (MethodSignature) point.getSignature();
            method = signature.getMethod();
        } else {
            return null;
        }
        Retry annotation = method.getDeclaredAnnotation(Retry.class);
        //重试时间,重试次数
        if (annotation.duration() <= 0 && annotation.attemptNumber() <= 1) {
            return point.proceed();
        }

        //不能设置开启returnFalse重试 和指定返回值重试
        if(!annotation.closeReturnFalseRetry() && !"willRertyString".equals(annotation.returnResult())){
            return point.proceed();
        }


        // 构建builder
        RetryerBuilder builder = RetryerBuilder.newBuilder();

        //重试次数
        if (annotation.attemptNumber() > 0) {
            builder.withStopStrategy(StopStrategies.stopAfterAttempt(annotation.attemptNumber()));
        }
        //退出策略
        if (annotation.duration() > 0) {
            builder.withStopStrategy(StopStrategies.stopAfterDelay(annotation.duration(), TimeUnit.MILLISECONDS));
        }

        //重试间隔等待策略
        if (annotation.waitStrategySleepTime() > 0) {
            builder.withWaitStrategy(WaitStrategies.fixedWait(annotation.waitStrategySleepTime(), TimeUnit.MILLISECONDS));
        }

        //停止重试的策略
        if (annotation.exceptionClass().length > 0) {
            for (Class<?> retryThrowable : annotation.exceptionClass()) {
                if (retryThrowable != null && Throwable.class.isAssignableFrom(retryThrowable)) {
                    builder.retryIfExceptionOfType(retryThrowable);
                }
            }
        }

        //RuntimeException时重试
        if (annotation.retryIfRuntimeException()){
            builder.retryIfRuntimeException();

        }
        if (annotation.retryIfException()){
            builder.retryIfException();
        }
        if (!"willRertyString".equals(annotation.returnResult())){
            builder.retryIfResult(s -> Objects.equals(s, annotation.returnResult()));
        }

        if (!annotation.closeReturnFalseRetry()){
            builder.retryIfResult(aBoolean -> Objects.equals(aBoolean, annotation.closeReturnFalseRetry()));
        }

        Method finalMethod = method;
        return builder.build().call(() -> {
            try {
                log.info("执行切面 "+   finalMethod.getName());
                return point.proceed();
            } catch (Throwable throwable) {
                if (throwable instanceof Exception) {
                    throw (Exception) throwable;
                } else {
                    throw new Exception(throwable);
                }
            }
        });
    }
}

创建方法

@Component
@Slf4j
public class AopTest {

    @Retry(retryIfException = true, waitStrategySleepTime = 1200,attemptNumber = 5)
    public void test() throws RemoteException {
        int x=0;
        try {
            // 格式化时间
            SimpleDateFormat sdf = new SimpleDateFormat();
            // a为am/pm的标记
            sdf.applyPattern("yyyy-MM-dd HH:mm:ss a");
            // 获取当前时间
            Date date = new Date();
            // 输出已经格式化的现在时间(24小时制)
            log.info("现在时间:" + sdf.format(date));
            int a=1/x;
        } catch (Exception e) {
            log.error("发生异常错误,错误原因:",e);
            throw new RemoteException("发生异常错误");
        }
    }

}

最后测试

@SpringBootTest
@Slf4j
public class GuavaRetryTest {

    @Autowired
    AopTest aopTest;
    @Test
    public void test() {
        try {
            aopTest.test();
        } catch (Exception e) {
            log.error("测试错误");
        }
    }
}

四、源码简析

1、Spring-Retry源码简析

参考文章:https://mp.weixin.qq.com/s/VA6KScOzSkGfZ65jUbVR6w

2、Guava-Retrying源码简析

实现原理大概就是由上述各种策略配合从而达到了非常灵活的重试机制

public interface Attempt<V> {

    /**
     * Returns the result of the attempt, if any.
     *
     * @return the result of the attempt
     * @throws ExecutionException if an exception was thrown by the attempt. The thrown
     *                            exception is set as the cause of the ExecutionException
     */
    public V get() throws ExecutionException;

    /**
     * Tells if the call returned a result or not
     *
     * @return <code>true</code> if the call returned a result, <code>false</code>
     *         if it threw an exception
     */
    public boolean hasResult();

    /**
     * Tells if the call threw an exception or not
     *
     * @return <code>true</code> if the call threw an exception, <code>false</code>
     *         if it returned a result
     */
    public boolean hasException();

    /**
     * Gets the result of the call
     *
     * @return the result of the call
     * @throws IllegalStateException if the call didn't return a result, but threw an exception,
     *                               as indicated by {@link #hasResult()}
     */
    public V getResult() throws IllegalStateException;

    /**
     * Gets the exception thrown by the call
     *
     * @return the exception thrown by the call
     * @throws IllegalStateException if the call didn't throw an exception,
     *                               as indicated by {@link #hasException()}
     */
    public Throwable getExceptionCause() throws IllegalStateException;

    /**
     * The number, starting from 1, of this attempt.
     *
     * @return the attempt number
     */
    public long getAttemptNumber();

    /**
     * The delay since the start of the first attempt, in milliseconds.
     *
     * @return the delay since the start of the first attempt, in milliseconds
     */
    public long getDelaySinceFirstAttempt();
}

通过接口方法可以知道Attempt这个类包含了任务执行次数、任务执行异常、任务执行结果、以及首次执行任务至今的时间间隔,那么我们后续的不管重试时机、还是其他策略都是根据此值来决定。接下来看关键执行入口Retryer##call

public V call(Callable<V> callable) throws ExecutionException, RetryException {
    long startTime = System.nanoTime();
    
    // 执行次数从1开始
    for (int attemptNumber = 1; ; attemptNumber++) {
        Attempt<V> attempt;
        try {
            // 尝试执行
            V result = attemptTimeLimiter.call(callable);
            
            // 执行成功则将结果封装为ResultAttempt
            attempt = new Retryer.ResultAttempt<V>(result, attemptNumber, TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime));
        } catch (Throwable t) {
            // 执行异常则将结果封装为ExceptionAttempt
            attempt = new Retryer.ExceptionAttempt<V>(t, attemptNumber, TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime));
        }
        // 这里将执行结果传给RetryListener做一些额外事情
        for (RetryListener listener : listeners) {
            listener.onRetry(attempt);
        }
        // 这个就是决定是否要进行重试的地方,如果不进行重试直接返回结果,执行成功就返回结果,执行失败就返回异常
        if (!rejectionPredicate.apply(attempt)) {
            return attempt.get();
        }
        
        // 到这里,说明需要进行重试,则此时先决定是否到达了停止重试的时机,如果到达了则直接返回异常
        if (stopStrategy.shouldStop(attempt)) {
            throw new RetryException(attemptNumber, attempt);
        } else {
            // 决定重试时间间隔
            long sleepTime = waitStrategy.computeSleepTime(attempt);
            try {
                // 进行阻塞
                blockStrategy.block(sleepTime);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                throw new RetryException(attemptNumber, attempt);
            }
        }
    }

参考文章:

重试组件 Spring Retry

重试框架Guava-Retry和spring-Retry

确实很优雅,所以我要扯下这个注解的神秘面纱

使用 Guava-Retry 优雅的实现重处理

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐