深入浅出guava-retrying,新的重试机制

406 阅读4分钟

文章目录

背景

项目中使用阿里云进行文件上传等操作,有时候上传会因为网络问题导致超时;或者在调用一些三方的操作上会导致超时,这时候应该在程序中进行重试.平常重试我们会使用try-catch然后一些判断去进行重试,写起来极其不优雅,不方便.guava-retrying就是为了解决以上痛点的.

使用

可以先考到测试项目,执行一下基本就了解了

首先引入:

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

简单的使用案例:

import com.github.rholder.retry.*;
import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;

/**
 * @ClassName RetryerTest
 * @Author yida
 * @Date 2021/8/18 5:08 下午
 * @Description RetryerTest
 */
@Slf4j
public class RetryerTest {

    private static int i = 0;

    public static void main(String[] args) throws Exception {
        System.out.println("请求结束返回:"+test());
    }

    public static Integer test() throws Exception {
        Retryer<Integer> retryer = RetryerBuilder.<Integer>newBuilder()
                // 抛出runtime异常、checked异常时都会重试,但是抛出error不会重试
                .retryIfException()
                //3秒后重试
                .withWaitStrategy(WaitStrategies.fixedWait(3000, TimeUnit.MILLISECONDS))
                //最多重试次数,后面可接入apollo
                .withStopStrategy(StopStrategies.stopAfterAttempt(3))
                    // 执行时间不超过5秒
                .withAttemptTimeLimiter(AttemptTimeLimiters.fixedTimeLimit(5, TimeUnit.SECONDS))
                // 回调监听,不管成功失败都会执行listener
                .withRetryListener(new RetryListener() {
                    @Override
                    public <Integer> void onRetry(Attempt<Integer> attempt) {
                        System.out.println("listener开始....");

                        // 第几次重试,(注意:第一次重试其实是第一次调用)
                        System.out.print("[重试的次数]time=" + attempt.getAttemptNumber());

                        // 距离第一次重试的延迟
                        System.out.print(",重试的延迟delay=" + attempt.getDelaySinceFirstAttempt());

                        // 重试结果: 是异常终止, 还是正常返回
                        System.out.print(",hasException=" + attempt.hasException());
                        System.out.print(",hasResult=" + attempt.hasResult());

                        // 是什么原因导致异常
                        if (attempt.hasException()) {
                            System.out.print(",causeBy=" + attempt.getExceptionCause().toString());
                        } else {
                            // 正常返回时的结果
                            System.out.print(",result=" + attempt.getResult());
                        }

                        // bad practice: 增加了额外的异常处理代码
                        try {
                            Integer result = attempt.get();
                            System.out.println("在listener里面处理结果,result get=" + result);
                        } catch (Exception e) {
                            System.out.println("在listener里面处理异常exception." + e.getCause().toString());
                        }
                        System.out.println("listener结束....\n\r\n\r");
                    }
                })
                .build();
        Integer call = retryer.call(() -> {
            System.out.println("同一规则,可以执行多份代码");
            return 0;
        });

       return retryer.call(() -> {
            // 前两次模拟异常,第三次成功
            if (i >= 2) {
                return 0;
            } else {
                i++;
            }
            try {
                // 模拟请求异常
                return 1 / 0;
            } catch (Exception e) {
                // 打印异常
                System.out.println("执行代码,请求失败" + e);
                throw e;
            }
        });

    }

}

返回的结果

同一规则,可以执行多份代码
listener开始....
[重试的次数]time=1,重试的延迟delay=2,hasException=false,hasResult=true,result=0在listener里面处理结果,result get=0
listener结束....


执行代码,请求失败java.lang.ArithmeticException: / by zero
listener开始....
[重试的次数]time=1,重试的延迟delay=0,hasException=true,hasResult=false,causeBy=java.lang.ArithmeticException: / by zero在listener里面处理异常exception.java.lang.ArithmeticException: / by zero
listener结束....


执行代码,请求失败java.lang.ArithmeticException: / by zero
listener开始....
[重试的次数]time=2,重试的延迟delay=3006,hasException=true,hasResult=false,causeBy=java.lang.ArithmeticException: / by zero在listener里面处理异常exception.java.lang.ArithmeticException: / by zero
listener结束....


listener开始....
[重试的次数]time=3,重试的延迟delay=6011,hasException=false,hasResult=true,result=0在listener里面处理结果,result get=0
listener结束....


请求结束返回:0

Process finished with exit code 0

Api详解

通过RetryerBuilder创建Retryer对象,使用Retryer对象执行call方法.Retryer对象可以执行多个call方法,不过是按照顺序执行的.所以一般使用都是通过RetryerBuilder新建Retryer对象去执行.
RetryerBuilder是一个factory建立者,能够定制设置重试源且能够支持多个重试源,能够配置重试次数或重试超时时间,以及能够配置等待时间间隔,建立重试者Retryer实例。
RetryerBuilder的重试源支持Exception异常对象 和自定义断言对象,经过retryIfException 和retryIfResult设置,同时支持多个且能兼容。

RetryerBuilder的部分代码:

public class RetryerBuilder<V> {
    private AttemptTimeLimiter<V> attemptTimeLimiter;
    private StopStrategy stopStrategy;
    private WaitStrategy waitStrategy;
    private BlockStrategy blockStrategy;
    private Predicate<Attempt<V>> rejectionPredicate = Predicates.alwaysFalse();
    private List<RetryListener> listeners = new ArrayList<RetryListener>();

    private RetryerBuilder() {
    }

    public static <V> RetryerBuilder<V> newBuilder() {
        return new RetryerBuilder<V>();
    }
// 省略

retryIfException,抛出runtime异常、checked异常时都会重试,可是抛出error不会重试。

retryIfRuntimeException只会在抛runtime异常的时候才重试,checked异常和error都不重试。

.retryIfExceptionOfType(NullPointerException.class)
                .retryIfExceptionOfType(IllegalStateException.class)

retryIfExceptionOfType容许咱们只在发生特定异常的时候才重试,好比NullPointerException和IllegalStateException都属于runtime异常,也包括自定义的error;

当发生重试以后,可使用RetryListener进行监听.能够注册多个RetryListener,会按照注册顺序依次调用。测试代码写的很清楚了.

项目中使用

可以通过aop对指定方法进行重试.
新建自定义重试注解:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface RetryAnnotation {
  
}

然后写个切面对retryAnnotation进行切面处理.


@Component
@Aspect
@Slf4j
public class RetryAspect {

    @Around("@annotation(com.study.springbootplus.config.RetryAnnotation)")
    public Object process(ProceedingJoinPoint joinPoint) throws Exception {

        // 参数可以改到配置文件中
        Retryer<Object> retryer = RetryerBuilder.newBuilder()
                // 抛出runtime异常、checked异常时都会重试,但是抛出error不会重试
                .retryIfException()
                //3秒后重试
                .withWaitStrategy(WaitStrategies.fixedWait(3000, TimeUnit.MILLISECONDS))
                //最多重试次数
                .withStopStrategy(StopStrategies.stopAfterAttempt(3)).build();

        Object[] parameters = joinPoint.getArgs();


        return retryer.call(() -> {
            try {
                return joinPoint.proceed();
            } catch (Throwable ex) {
                log.error("RetryAspect processed error,parameters={},ex={}", JSON.toJSON(parameters), ex);
                if (ex instanceof Exception) {
                    throw (Exception) ex;
                } else {
                    throw new Exception(ex);
                }
            }
        });
    }

}