Guava Retry 重试机制流程分析

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 方法的流程符合依赖倒置原则,对于不同的处理过程(等待、停止、阻塞),符合依赖倒置原则,程序依赖于抽象接口,而不是具体实现。
相关推荐
qmx_0734 分钟前
HTB-Jerry(tomcat war文件、msfvenom)
java·web安全·网络安全·tomcat
为风而战42 分钟前
IIS+Ngnix+Tomcat 部署网站 用IIS实现反向代理
java·tomcat
技术无疆3 小时前
快速开发与维护:探索 AndroidAnnotations
android·java·android studio·android-studio·androidx·代码注入
罗政5 小时前
[附源码]超简洁个人博客网站搭建+SpringBoot+Vue前后端分离
vue.js·spring boot·后端
架构文摘JGWZ6 小时前
Java 23 的12 个新特性!!
java·开发语言·学习
拾光师7 小时前
spring获取当前request
java·后端·spring
aPurpleBerry7 小时前
neo4j安装启动教程+对应的jdk配置
java·neo4j
我是苏苏7 小时前
Web开发:ABP框架2——入门级别的增删改查Demo
java·开发语言
xujinwei_gingko7 小时前
Spring IOC容器Bean对象管理-Java Config方式
java·spring
2301_789985947 小时前
Java语言程序设计基础篇_编程练习题*18.29(某个目录下的文件数目)
java·开发语言·学习