重试组件使用与原理分析(二)-guava-retrying

2022-01-04 10:50:14 浏览数 (2)

一.简介

上一篇文章我们介绍了实际项目开发中重试的应用场景,以及spring-retry原理和源码的详细介绍,那么此篇我们将会详细介绍一下另外一个常用的重试组件guava-retrying。

那么guava-retrying是什么?官方的解释是,guava重试模块提供了一个通用方法,用于重试具有特定停止、重试和异常处理功能的任意Java代码,这些功能通过guava的谓词匹配得到了增强。

接下来我们就guava-retrying的使用方式、工作原理以及源码展开介绍和分析。

二.使用方式

1.引入依赖

在项目中引入guava-retrying依赖:

代码语言:javascript复制
<dependency>
  <groupId>com.github.rholder</groupId>
  <artifactId>guava-retrying</artifactId>
  <version>2.0.0</version>
</dependency>

2.定义方法回调

利用Callable接口定义业务方法回调:

代码语言:javascript复制
Callable<Boolean> callable = new Callable<Boolean>() {
    public Boolean call() throws Exception {
        return true; // do something useful here
    }
};

该回调方法有两个点需要注意,一是返回类型,是根据具体业务场景定义,二是业务逻辑,在call方法中实现自定义重试逻辑实现(调用远程接口或者本地方法)。

3.创建重试器并执行重试

利用RetryerBuilder构建重试器并执行重试逻辑:

代码语言:javascript复制
Retryer<Boolean> retryer =  RetryerBuilder.<Boolean>newBuilder()
    .withRetryListener(new RetryListener() {
        @Override
        public <V> void onRetry(Attempt<V> attempt) {
            log.info("listener receive attempt={}",attempt);
        }
    })
    .withAttemptTimeLimiter(AttemptTimeLimiters.fixedTimeLimit(1, TimeUnit.SECONDS))
    .withStopStrategy(StopStrategies.stopAfterAttempt(3))
    .withWaitStrategy(WaitStrategies.fixedWait(2,TimeUnit.SECONDS))
    .withBlockStrategy(BlockStrategies.threadSleepStrategy())
    .retryIfException()
    .retryIfExceptionOfType(ServiceRuntimeException.class)
    .retryIfException(Predicates.equalTo(new Exception()))
    .retryIfRuntimeException()
    .retryIfResult(Predicates.equalTo(false))
    .build();
try {
    retryer.call(new callable());
} catch (ExecutionException e) {
    log.error("occur error",e);
} catch (RetryException e) {
    log.error("occur error",e);
} catch (Exception e) {
    log.error("occur error",e);
}

我们构建了一个具有最丰富的各种策略的重试器,并执行了重试逻辑。分别包含了重试监听器、重试限制器、终止策略、等待策略、阻塞策略和各种重试策略。这样我们就可以在真实场景中使用guava-retrying提供的重试能力了。

三.原理&源码分析

前边我们介绍了guava-retrying的使用方式,能够看出其使用过程和工作原理就是先根据各种策略构建一个重试器,然后使用重试器调用我们的业务逻辑回调,那么我们将参照源码来逐步分析guava-retrying的工作原理。

1.构造重试器

guava-retrying使用工厂模式创建重试器,入口是RetryerBuilder,我们逐个分析一个各种策略的构建和重试器的构建。

I)监听器

代码语言:javascript复制
/**
* Adds a listener that will be notified of each attempt that is made
*
* @param listener Listener to add
* @return <code>this</code>
*/
public RetryerBuilder<V> withRetryListener(@Nonnull RetryListener listener) {
    Preconditions.checkNotNull(listener, "listener may not be null");
    listeners.add(listener);
    return this;
}

RetryerBuilder.withRetryListener为重试器添加监听器,每次重试的时候将被通知,监听器需实现RetryListener接口,支持多次调用。

II)重试限制器

代码语言:javascript复制
/**
* Configures the retryer to limit the duration of any particular attempt by the given duration.
*
* @param attemptTimeLimiter to apply to each attempt
* @return <code>this</code>
*/
public RetryerBuilder<V> withAttemptTimeLimiter(@Nonnull AttemptTimeLimiter<V> attemptTimeLimiter) {
    Preconditions.checkNotNull(attemptTimeLimiter);
    this.attemptTimeLimiter = attemptTimeLimiter;
    return this;
}

配置重试器的重试周期和次数限制,默认提供的限制器有两种:

重试时间控制器

控制器名称

参数

作用

NoAttemptTimeLimit

-

无时间限制处理器,直接调用方法回调(默认)

FixedAttemptTimeLimit

duration

固定时间限制处理器,超时取消

III)终止策略

代码语言:javascript复制
/**
* Sets the stop strategy used to decide when to stop retrying. The default strategy is to not stop at all .
*
* @param stopStrategy the strategy used to decide when to stop retrying
* @return <code>this</code>
* @throws IllegalStateException if a stop strategy has already been set.
*/
public RetryerBuilder<V> withStopStrategy(@Nonnull StopStrategy stopStrategy) throws IllegalStateException {
    Preconditions.checkNotNull(stopStrategy, "stopStrategy may not be null");
    Preconditions.checkState(this.stopStrategy == null, "a stop strategy has already been set %s", this.stopStrategy);
    this.stopStrategy = stopStrategy;
    return this;
}

重试器的终止策略配置,默认不终止,guava-retrying提供了三种终止策略:

终止策略

策略名称

参数

作用

NeverStopStrategy

-

永不终止(默认)

StopAfterAttemptStrategy

maxAttemptNumber

重试超过最大次数后终止

StopAfterDelayStrategy

maxDelay

重试第一次失败时间超过最大延时后终止

IV)等待策略

代码语言:javascript复制
/**
 * Sets the wait strategy used to decide how long to sleep between failed attempts.
 * The default strategy is to retry immediately after a failed attempt.
 *
 * @param waitStrategy the strategy used to sleep between failed attempts
 * @return <code>this</code>
 * @throws IllegalStateException if a wait strategy has already been set.
 */
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;
}

重试器的等待策略,配置每次重试失败后的休眠时间,guava-retrying提供了8种等待策略:

等待策略

策略名称

参数

作用

NO_WAIT_STRATEGY

-

不休眠直接重试(默认)

FixedWaitStrategy

sleepTime

重试前休眠固定时间

RandomWaitStrategy

minimumTime,maximumTime

重试前休眠minimumTime~maximumTime之间随机时间

IncrementingWaitStrategy

initialSleepTime,increment

第一次重试休眠initialSleepTime,后续每次重试前休眠时间线性递增increment

ExponentialWaitStrategy

multiplier,maximumTime

指数增长休眠时间,2的attempTime次幂

FibonacciWaitStrategy

multiplier,maximumTime

斐波拉契增长休眠时间

ExceptionWaitStrategy

exceptionClass,function

异常休眠,特定异常休眠指定时间

CompositeWaitStrategy

waitStrategies

混合休眠时间,多个等待策略休眠时间累加

V)阻塞策略

代码语言:javascript复制
/**
 * Sets the block strategy used to decide how to block between retry attempts. The default strategy is to use Thread#sleep().
 *
 * @param blockStrategy the strategy used to decide how to block between retry attempts
 * @return <code>this</code>
 * @throws IllegalStateException if a block strategy has already been set.
 */
public RetryerBuilder<V> withBlockStrategy(@Nonnull BlockStrategy blockStrategy) throws IllegalStateException {
    Preconditions.checkNotNull(blockStrategy, "blockStrategy may not be null");
    Preconditions.checkState(this.blockStrategy == null, "a block strategy has already been set %s", this.blockStrategy);
    this.blockStrategy = blockStrategy;
    return this;
}

阻塞策略配置每次重试之前如何阻塞流程,默认是线程休眠,guava-retrying只提供了一种阻塞策略:

阻塞策略

策略名称

参数

作用

ThreadSleepStrategy

-

线程休眠(默认)

VI)重试策略

代码语言:javascript复制
/**
 * Configures the retryer to retry if an exception (i.e. any <code>Exception</code> or subclass
 * of <code>Exception</code>) is thrown by the call.
 *
 * @return <code>this</code>
 */
public RetryerBuilder<V> retryIfException() {
    rejectionPredicate = Predicates.or(rejectionPredicate, new ExceptionClassPredicate<V>(Exception.class));
    return this;
}
/**
 * Configures the retryer to retry if a runtime exception (i.e. any <code>RuntimeException</code> or subclass
 * of <code>RuntimeException</code>) is thrown by the call.
 *
 * @return <code>this</code>
 */
public RetryerBuilder<V> retryIfRuntimeException() {
    rejectionPredicate = Predicates.or(rejectionPredicate, new ExceptionClassPredicate<V>(RuntimeException.class));
    return this;
}
/**
 * Configures the retryer to retry if an exception of the given class (or subclass of the given class) is
 * thrown by the call.
 *
 * @param exceptionClass the type of the exception which should cause the retryer to retry
 * @return <code>this</code>
 */
public RetryerBuilder<V> retryIfExceptionOfType(@Nonnull Class<? extends Throwable> exceptionClass) {
    Preconditions.checkNotNull(exceptionClass, "exceptionClass may not be null");
    rejectionPredicate = Predicates.or(rejectionPredicate, new ExceptionClassPredicate<V>(exceptionClass));
    return this;
}
/**
 * Configures the retryer to retry if an exception satisfying the given predicate is
 * thrown by the call.
 *
 * @param exceptionPredicate the predicate which causes a retry if satisfied
 * @return <code>this</code>
 */
public RetryerBuilder<V> retryIfException(@Nonnull Predicate<Throwable> exceptionPredicate) {
    Preconditions.checkNotNull(exceptionPredicate, "exceptionPredicate may not be null");
    rejectionPredicate = Predicates.or(rejectionPredicate, new ExceptionPredicate<V>(exceptionPredicate));
    return this;
}
/**
 * Configures the retryer to retry if the result satisfies the given predicate.
 *
 * @param resultPredicate a predicate applied to the result, and which causes the retryer
 *                        to retry if the predicate is satisfied
 * @return <code>this</code>
 */
public RetryerBuilder<V> retryIfResult(@Nonnull Predicate<V> resultPredicate) {
    Preconditions.checkNotNull(resultPredicate, "resultPredicate may not be null");
    rejectionPredicate = Predicates.or(rejectionPredicate, new ResultPredicate<V>(resultPredicate));
    return this;
}

重试策略也就是配置哪些异常类型或者自定义返回类型需要重试,每个重试策略之间都是或的关系,guava-retrying定义并提供了一下几种重试策略:

重试策略

策略名称

参数

作用

ExceptionClassPredicate

Exception.class

接收到Exception(或子类)异常会重试

ExceptionClassPredicate

RuntimeException.class

接收到RuntimeException(或子类)异常会重试

ExceptionClassPredicate

exceptionClass

接收到自定义异常(或子类)重试

ResultPredicate

resultPredicate

接收到自定义类型重试

VII)构造重试器

代码语言:javascript复制
/**
 * Builds the retryer.
 *
 * @return the built retryer.
 */
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);
}

如果没有传入重试限制器,则默认使用AttemptTimeLimiters.<V>noTimeLimit();如果没有定义终止策略,则默认使用永不终止策略;如果没有定义等待策略,则默认使用无需等待策略;如果没有定义阻塞策略,则默认使用线程阻塞策略,最有使用重试限制器、终止策略、等待策略、阻塞策略、重试策略和监听器创建重试器。

2.执行重试

构建完重试器之后,就会调用重试逻辑,我们看一下重试逻辑的核心代码:

代码语言:javascript复制
/**
 * Executes the given callable. If the rejection predicate
 * accepts the attempt, the stop strategy is used to decide if a new attempt
 * must be made. Then the wait strategy is used to decide how much time to sleep
 * and a new attempt is made.
 *
 * @param callable the callable task to be executed
 * @return the computed result of the given callable
 * @throws ExecutionException if the given callable throws an exception, and the
 *                            rejection predicate considers the attempt as successful. The original exception
 *                            is wrapped into an ExecutionException.
 * @throws RetryException     if all the attempts failed before the stop strategy decided
 *                            to abort, or the thread was interrupted. Note that if the thread is interrupted,
 *                            this exception is thrown and the thread's interrupt status is set.
 */
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);
            }
        }
    }
}

执行给定的调用,如果重试策略接受重试,则使用停止策略来决定是否必须进行新的尝试,然后,使用等待策略来决定睡眠时间,并进行新的尝试。我们按照顺序也逐个分析一下重试逻辑的原理:

I)执行逻辑

代码语言:javascript复制
V result = attemptTimeLimiter.call(callable);

在构造重试器逻辑中我们知道限制器有两种,分别是FixedAttemptTimeLimit和NoAttemptTimeLimit,前者是带超时时间的重试后者是直接调用Callable的call方法。

II)通知监听器

代码语言:javascript复制
for (RetryListener listener : listeners) {
    listener.onRetry(attempt);
}

通知监听器执行了重试。

III)重试策略裁定

代码语言:javascript复制
if (!rejectionPredicate.apply(attempt)) {
    return attempt.get();
}

如果不满足重试策略直接返回结果,这里的重试策略谓词是多种重试策略并集,任何一种满足就认为满足:

代码语言:javascript复制
/** @see Predicates#or(Iterable) */
private static class OrPredicate<T> implements Predicate<T>, Serializable {
private final List<? extends Predicate<? super T>> components;

private OrPredicate(List<? extends Predicate<? super T>> components) {
  this.components = components;
}
@Override
public boolean apply(@Nullable T t) {
  // Avoid using the Iterator to avoid generating garbage (issue 820).
  for (int i = 0; i < components.size(); i  ) {
    if (components.get(i).apply(t)) {
      return true;
    }
  }
  return false;
}
//省略
}

IV)终止策略裁定

代码语言:javascript复制
if (stopStrategy.shouldStop(attempt)) {
    throw new RetryException(attemptNumber, attempt);
}

如果命中终止策略,则直接抛重试异常终止流程,终止策略这里不再表述。

V)等待和阻塞

代码语言:javascript复制
else {
        long sleepTime = waitStrategy.computeSleepTime(attempt);
        try {
            blockStrategy.block(sleepTime);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RetryException(attemptNumber, attempt);
        }
    }

如果没有命中终止策略,则进入等待和阻塞,等待策略和阻塞策略结合使用,先根据等待策略计算出需要休眠的时间,然后调用阻塞策略阻塞相应时间。

如果for循环体重没有异常终止或者正常返回,那么进入下一次重试,直到资格耗尽(或者无限重试)。

四.优缺点

使用过guava-retrying或者分析过其源码你会发现,guava-retrying重试组件特别轻量级,核心类就那几个,并且使用简单设计优雅,但是它也存在缺点,和spring-retry一样我们也枚举一下guava-retrying的优缺点:

优点

  1. 策略丰富并且支持自定义
  2. 使用简单
  3. 设计优雅

缺点

  1. 不支持注解
  2. 侵入业务代码
  3. 重复性强

总结

本篇从使用和源码维度详细介绍了guava-retrying使用方式和实现原理,以及其优缺点,当然我们从翻阅guava-retrying重试组件源码的过程中也学到了很多东西,优雅的设计、面向接口编程的炉火纯青的使用、以及高度结构化的编码方式,这些东西是我从其他框架中很少见到的。当然回归到本篇主题,希望通过本篇文章以及上一篇文章对重试组件的介绍,对大家在重试组件的选型、使用和理解上有所帮助。

0 人点赞