guava-retrying-灵活重试机制的解决方案

299 阅读2分钟

背景

业务上我们常会需要一些重试的场景,比如一些对推送成功率要求较高的场景,或者调用三方接口超时的场景等。自己编写一套通用的重试代码会比较繁琐,所以就可以选用guava-retrying实现灵活的重试机制。

实践

首先我们需要引入maven依赖。

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

然后就可以愉快的使用guava-retrying进行重试了。

先定义重试器,通过构建者模式,封装各类条件和策略。

Retryer<Boolean> retryer = RetryerBuilder.<Boolean>newBuilder()
                .retryIfResult(aBoolean -> Objects.equals(aBoolean, false))
                .withWaitStrategy(WaitStrategies.incrementingWait(1, TimeUnit.SECONDS, 2, TimeUnit.SECONDS))
                .withStopStrategy(StopStrategies.stopAfterAttempt(3))
                .build();

支持三种策略

五种重试条件

定义好重试器后,就可以执行业务代码。

        try {
            retryer.call(() -> {
                System.out.println("开始执行");
                // 判断标志物是否还存在
                return false;
            });
        } catch (Exception e) {
            System.err.println(e);
        }

需要传入的是Callable的函数式接口,所以是需要返回值,retryIfResult可以通过返回值判断是否需要重试。

源码解析

了解了如何使用,还是要看一下具体是如何实现的,学习一下他们的设计思路。关于构建者模式的链式调用,我之前已经有过一篇文章,就不在多赘述了。

我们先看一下build方法,主要是将策略,条件等进行初始化处理。

    public Retryer<V> build() {
        AttemptTimeLimiter<V> theAttemptTimeLimiter = attemptTimeLimiter == null ? AttemptTimeLimiters.<V>noTimeLimit() : attemptTimeLimiter;
        StopStrategy theStopStrategy = stopStrategy == null ? StopStrategies.neverStop() : stopStrategy;
        WaitStrategy theWaitStrategy = waitStrategy == null ? WaitStrategies.noWait() : waitStrategy;
        BlockStrategy theBlockStrategy = blockStrategy == null ? BlockStrategies.threadSleepStrategy() : blockStrategy;

        return new Retryer<V>(theAttemptTimeLimiter, theStopStrategy, theWaitStrategy, theBlockStrategy, rejectionPredicate, listeners);
    }

我们的策略和条件都只能设置一个,如果相同的策略设置了两个以上,则会抛出错误。

    public RetryerBuilder<V> withWaitStrategy(@Nonnull WaitStrategy waitStrategy) throws IllegalStateException {
        Preconditions.checkNotNull(waitStrategy, "waitStrategy may not be null");
        Preconditions.checkState(this.waitStrategy == null"a wait strategy has already been set %s"this.waitStrategy);
        this.waitStrategy = waitStrategy;
        return this;
    }

我们再看一下call方法如何实现。

    public V call(Callable<V> callable) throws ExecutionException, RetryException {
        long startTime = System.nanoTime();
        for (int attemptNumber = 1; ; attemptNumber++) {
            Attempt<V> attempt;
            try {
                V result = attemptTimeLimiter.call(callable);
                attempt = new ResultAttempt<V>(result, attemptNumber, TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime));
            } catch (Throwable t) {
                attempt = new ExceptionAttempt<V>(t, attemptNumber, TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime));
            }

            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);
                }
            }
        }
    }

整体的代码很简单,但是很清晰,基本上定时任务的每个点,都可以进行扩展,每次结果的监听,拒绝的策略,停止的策略,睡眠时间的策略,以及最后的阻塞粗略。状态的流转都是通过ResultAttempt和ExceptionAttempt进行控制。

查看ResultAttempt的构造函数

        public ResultAttempt(R result, long attemptNumber, long delaySinceFirstAttempt) {
            this.result = result;
            this.attemptNumber = attemptNumber;
            this.delaySinceFirstAttempt = delaySinceFirstAttempt;
        }
                @Override
        public R get() throws ExecutionException {
            return result;
        }

查看ExceptionAttempt的构造函数

        public ExceptionAttempt(Throwable cause, long attemptNumber, long delaySinceFirstAttempt) {
            this.e = new ExecutionException(cause);
            this.attemptNumber = attemptNumber;
            this.delaySinceFirstAttempt = delaySinceFirstAttempt;
        }
        
        @Override
        public R get() throws ExecutionException {
            throw e;
        }        

最大的区别就是一个返回了执行的结果,一个返回了异常信息。

所以当判断条件不满足时,如果ExceptionAttempt则直接抛出异常。

            if (!rejectionPredicate.apply(attempt)) {
                return attempt.get();
            }

总结

guava-retrying是一个简单,但是扩展性很高的重试工具类,可以很好的适配业务上的各种重试场景。