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

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

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

一.简介

    上一篇文章我们介绍了实际项目开发中重试的应用场景,以及spring-retry原理和源码的详细介绍,那么此篇我们将会详细介绍一下另外一个常用的重试组件guava-retrying。     那么guava-retrying是什么?官方的解释是,guava重试模块提供了一个通用方法,用于重试具有特定停止、重试和异常处理功能的任意Java代码,这些功能通过guava的谓词匹配得到了增强。 接下来我们就guava-retrying的使用方式、工作原理以及源码展开介绍和分析。

二.使用方式

1.引入依赖

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


  com.github.rholder
  guava-retrying
  2.0.0

复制代码

2.定义方法回调

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

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

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

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

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

Retryer retryer =  RetryerBuilder.newBuilder()
    .withRetryListener(new RetryListener() {
        @Override
        public  void onRetry(Attempt 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)监听器

public RetryerBuilder withRetryListener(@Nonnull RetryListener listener) {
    Preconditions.checkNotNull(listener, "listener may not be null");
    listeners.add(listener);
    return this;
}
复制代码

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

II)重试限制器

public RetryerBuilder withAttemptTimeLimiter(@Nonnull AttemptTimeLimiter attemptTimeLimiter) {
    Preconditions.checkNotNull(attemptTimeLimiter);
    this.attemptTimeLimiter = attemptTimeLimiter;
    return this;
}
复制代码

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

重试时间控制器控制器名称参数作用NoAttemptTimeLimit-无时间限制处理器,直接调用方法回调(默认)FixedAttemptTimeLimitduration固定时间限制处理器,超时取消

III)终止策略

public RetryerBuilder 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-永不终止(默认)StopAfterAttemptStrategymaxAttemptNumber重试超过最大次数后终止StopAfterDelayStrategymaxDelay重试第一次失败时间超过最大延时后终止

IV)等待策略

public RetryerBuilder 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-不休眠直接重试(默认)FixedWaitStrategysleepTime重试前休眠固定时间RandomWaitStrategyminimumTime,maximumTime重试前休眠minimumTime~maximumTime之间随机时间IncrementingWaitStrategyinitialSleepTime,increment第一次重试休眠initialSleepTime,后续每次重试前休眠时间线性递增incrementExponentialWaitStrategymultiplier,maximumTime指数增长休眠时间,2的attempTime次幂FibonacciWaitStrategymultiplier,maximumTime斐波拉契增长休眠时间ExceptionWaitStrategyexceptionClass,function异常休眠,特定异常休眠指定时间CompositeWaitStrategywaitStrategies混合休眠时间,多个等待策略休眠时间累加

V)阻塞策略

public RetryerBuilder 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)重试策略

public RetryerBuilder retryIfException() {
    rejectionPredicate = Predicates.or(rejectionPredicate, new ExceptionClassPredicate(Exception.class));
    return this;
}

public RetryerBuilder retryIfRuntimeException() {
    rejectionPredicate = Predicates.or(rejectionPredicate, new ExceptionClassPredicate(RuntimeException.class));
    return this;
}

public RetryerBuilder retryIfExceptionOfType(@Nonnull Class exceptionClass) {
    Preconditions.checkNotNull(exceptionClass, "exceptionClass may not be null");
    rejectionPredicate = Predicates.or(rejectionPredicate, new ExceptionClassPredicate(exceptionClass));
    return this;
}

public RetryerBuilder retryIfException(@Nonnull Predicate exceptionPredicate) {
    Preconditions.checkNotNull(exceptionPredicate, "exceptionPredicate may not be null");
    rejectionPredicate = Predicates.or(rejectionPredicate, new ExceptionPredicate(exceptionPredicate));
    return this;
}

public RetryerBuilder retryIfResult(@Nonnull Predicate resultPredicate) {
    Preconditions.checkNotNull(resultPredicate, "resultPredicate may not be null");
    rejectionPredicate = Predicates.or(rejectionPredicate, new ResultPredicate(resultPredicate));
    return this;
}
复制代码

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

重试策略策略名称参数作用ExceptionClassPredicateException.class接收到Exception(或子类)异常会重试ExceptionClassPredicateRuntimeException.class接收到RuntimeException(或子类)异常会重试ExceptionClassPredicateexceptionClass接收到自定义异常(或子类)重试ResultPredicateresultPredicate接收到自定义类型重试

VII)构造重试器

public Retryer build() {
    AttemptTimeLimiter theAttemptTimeLimiter = attemptTimeLimiter == null ? AttemptTimeLimiters.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(theAttemptTimeLimiter, theStopStrategy, theWaitStrategy, theBlockStrategy, rejectionPredicate, listeners);
}
复制代码

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

2.执行重试

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

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

V result = attemptTimeLimiter.call(callable);
复制代码

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

II)通知监听器

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

    通知监听器执行了重试。

III)重试策略裁定

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

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

private static class OrPredicate implements Predicate, Serializable {
private final List> components;

private OrPredicate(List> 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)终止策略裁定

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

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

V)等待和阻塞

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

分类:

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/5697358.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-17
下一篇 2022-12-17

发表评论

登录后才能评论

评论列表(0条)

保存