Guava Retry 重试机制源码分析
在进行重试时,我们要考虑以下几点:
- 何时启动重试机制(发生异常、网络延迟等);
- 何时结束重试(调用成功);
- 重试的时间间隔是多久,即计算等待时间(固定时间、指数退避等);
- 如何进行等待(Thread.sleep)
而在 Guava Retry 库中都提供了这些的解决方法。
一、Guava Retry 重试机制使用
引入 guava-retry 库:
java
代码解读
复制代码
<dependency> <groupId>com.github.rholder</groupId> <artifactId>guava-retrying</artifactId> <version>2.0.0</version> </dependency>
示例代码:
java
代码解读
复制代码
@Slf4j public class FixIntervalRetryStrategy implements RetryStrategy<RpcResponse> { @Override public RpcResponse doRetry(Callable<RpcResponse> callable) throws Exception { Retryer<RpcResponse> retryer = RetryerBuilder.<RpcResponse>newBuilder() .retryIfExceptionOfType(Exception.class) .withStopStrategy(StopStrategies.stopAfterAttempt(3)) .withWaitStrategy(WaitStrategies.fixedWait(3, TimeUnit.SECONDS)) .withRetryListener(new RetryListener() { @Override public <V> void onRetry(Attempt<V> attempt) { // 监听重试机制 log.info("重试次数:{}", attempt.getAttemptNumber()); } }) .build(); return retryer.call(callable); } }
- retryIfExceptionOfType:在发生什么类型的异常时进行重试;
- withStopStrategy:何时结束重试;
- withWaitStrategy:每次等待多久后进行再次重试;
- withRetryListener:定义重试的监听器;
1)对于何时进行重试,Guava 的 RetryerBuilder 类给我们提供了很多方法:
java
代码解读
复制代码
// 在发生Exception异常时触发重试 public RetryerBuilder<V> retryIfException() { rejectionPredicate = Predicates.or(rejectionPredicate, new ExceptionClassPredicate<V>(Exception.class)); return this; } // 在发生RuntimeException异常时触发重试 public RetryerBuilder<V> retryIfRuntimeException() { rejectionPredicate = Predicates.or(rejectionPredicate, new ExceptionClassPredicate<V>(RuntimeException.class)); return this; } // 在发生指定Exception异常时触发重试 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; } 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; } 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; }
2)对于重试间隔,Guava 的 WaitStrategies 给我们提供了很多种策略:
- FixedWaitStrategy:固定时间进行重试;
- RandomWaitStrategy:随机等待时间进行重试;
- IncrementingWaitStrategy:根据失败次数计算等待时间
- ExponentialWaitStrategy:随着指数倍数增长;
- FibonacciWaitStrategy:随着失败尝试次数的增加,等待时间按照斐波那契数列增长;
- CompositeWaitStrategy:这个策略类的特点是将多个不同的等待策略组合在一起,并按顺序将这些策略的等待时间相加,以计算总的等待时间;
- ExceptionWaitStrategy:根据指定异常自定义过期时间;
3)对于何时结束重试,Guava 的 StopStrategies 给我们提供了很多种策略:
- StopAfterDelayStrategy:在延迟多久后停止重试;
- StopAfterAttemptStrategy:在重试指定次数后停止重试;
- NeverStopStrategy:从不停止重试;
4)对于等待调用,Guava 只提供了一个类 ThreadSleepStrategy:
java
代码解读
复制代码
@Immutable private static class ThreadSleepStrategy implements BlockStrategy { @Override public void block(long sleepTime) throws InterruptedException { Thread.sleep(sleepTime); } }
- 实质上是调用 Thread.sleep 方法进行阻塞等待;
二、重试机制的核心流程
2.1 构建 Retry 类
我们先看看 RetryBuilder 的 build 方法,它是如何构建出 Retry 的:
java
代码解读
复制代码
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); }
- 该方法会设置停止策略,阻塞策略,等待策略以及 theAttemptTimeLimiter,其中 theAttemptTimeLimiter 的作用是确保重试方法调用时不会超过指定时间限制,避免阻塞进程,它可以设置为无限制等待或者指定时间等待。
- 我们还需要注意 rejectionPredicate 这个参数,这个参数是判断是否进行重试的关键。
rejectionPredicate 的构建我们需要 RetryBuilder 类里面的方法:
java
代码解读
复制代码
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; } public static <T extends @Nullable Object> Predicate<T> or( Predicate<? super T> first, Predicate<? super T> second) { return new OrPredicate<>(Predicates.<T>asList(checkNotNull(first), checkNotNull(second))); } // 這個是OrPredicate的構造函數 private OrPredicate(List<? extends Predicate<? super T>> components) { this.components = components; }
- 通过将Predicate的判断实现类封装到 components 数组中,然后遍历该数组查找符合条件当前重试条件的一个,如果没有符合条件意味着当前不需要重试;
其中 Predicate 接口的实现类是 ExceptionClassPredicate,是 RetryBuilder 的内部类:
java
代码解读
复制代码
private static final class ExceptionClassPredicate<V> implements Predicate<Attempt<V>> { private Class<? extends Throwable> exceptionClass; public ExceptionClassPredicate(Class<? extends Throwable> exceptionClass) { this.exceptionClass = exceptionClass; } @Override public boolean apply(Attempt<V> attempt) { // 判断当前重试中是否存在异常 if (!attempt.hasException()) { return false; } return exceptionClass.isAssignableFrom(attempt.getExceptionCause().getClass()); } }
- Attempt 是当前重试动作的抽象;
2.2 执行重试流程
创建出 Retry 后,我们看看 Retry 的 call 方法:
java
代码解读
复制代码
public V call(Callable<V> callable) throws ExecutionException, RetryException { long startTime = System.nanoTime(); // 循环记录重试次数 for (int attemptNumber = 1; ; attemptNumber++) { Attempt<V> attempt; try { // 执行callable接口方法 V result = attemptTimeLimiter.call(callable); // 封装重试结果 attempt = new ResultAttempt<V>(result, attemptNumber, TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime)); } catch (Throwable t) { // 重试执行如果发生异常就封装到ExceptionAttempt中 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); } } } }
该方法就是调用重试机制核心方法,它的主流程包括以下步骤:
- 执行重试方法,判断当前是否存在异常信息决定是否创建 ExceptionAttempt;
- 接着调用所有重试监听器;
- 根据 Attempt 的 hasException 方法,在 rejectionPredicate 类中判断是否需要继续重试;
- 若需要重试,则先根据 stopStrategy 判断当前重试是否需要结束;
- 如果不需要结束,则再根据 waitStrategy 计算出当前需要暂停的时间;
- 然后调用 blockStrategy 进行睡眠等待;
三、收获
- 通过策略模式,提供不同的等待策略,停止策略供用户选择,同时用户可以自定义策略,只要实现特定的接口即可;
- call 方法的流程符合依赖倒置原则,对于不同的处理过程(等待、停止、阻塞),符合依赖倒置原则,程序依赖于抽象接口,而不是具体实现。