Guava-Retrying 快速构建接口服务重试机制神器

Guava-Retrying 简介

在我们的开发中,api 接口调用异常是经常会遇到的,任何接口都会有不同概率的异常情况,对于可以重入的接口,为了避免偶发性异常造成的服务的不可用,重试机制就非常有必要了。Guava-Retryiny 是一个非常灵活的重试组件,包含多种重试策略,扩展很方便。Guava-retrying 为 Google 的一个开源组件库,抽象的更彻底,不仅仅 http 请求,可针对任何重要资源服务实现重试,如数据库连接,文件资源等。

1. Maven 依赖

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

2. Retryer 介绍

Retryer 提供了构造方法,用来创建一个指定规则的 Retryer:这个方法可以通过传入尝试时间策略、停止重试策略、重试间隔等待策略、重试阻塞策略、拒绝策略等策略来指定一个请求的重试如何进行。

Retryer(@Nonnull AttemptTimeLimiter<V> attemptTimeLimiter,
  @Nonnull StopStrategy stopStrategy,
  @Nonnull WaitStrategy waitStrategy,
  @Nonnull BlockStrategy blockStrategy,
  @Nonnull Predicate<Attempt<V>> rejectionPredicate,
  @Nonnull Collection<RetryListener> listeners)
  • attemptTimeLimiter — 每次重试的最大超时,超过该超时则抛出 TimeoutException;
  • stopStrategy — 停止重试的策略;
  • waitStrategy — 重试间间隔等待策略;
  • blockStrategy — 重试间间隔线程阻塞策略;
  • rejectionPredicate — 拒绝尝试断言;
  • listeners — 重试的监听者;

2.1. AttemptTimeLimiter 接口(超时时长控制)

这个接口主要是用来控制每次重试的超时时间。

public interface AttemptTimeLimiter<V> {
 
 V call(Callable<V> var1) throws Exception;
 
}

通常来说,我们会通过 AttemptTimeLimiters 类来创建这个接口的实现。

AttemptTimeLimiters

AttemptTimeLimiters 就是一个生产 AttemptTimeLimiter 实现的工厂类,主要的方法有三个:

  • noTimeLimit() — 不限制时间
public static <V> AttemptTimeLimiter<V> noTimeLimit()
  • fixedTimeLimit() — 限制超时时间
public static <V> AttemptTimeLimiter<V> fixedTimeLimit(long duration, @Nonnull TimeUnit timeUnit)
 
public static <V> AttemptTimeLimiter<V> fixedTimeLimit(long duration, @Nonnull TimeUnit timeUnit, @Nonnull ExecutorService executorService)

两个方法通过 duration 和 timeUnit 组合实现了对调用的接口实现总的超时控制。

2.2. StopStrategy 接口(终止策略)

这个接口中只有一个方法,顾名思义,他就是用来判断是否应该停止重试。

public interface StopStrategy {

    boolean shouldStop(Attempt var1);
    
}

他传入了 Attempt 为参数,通过这个参数,我们可以获取到方法的返回值、抛出的异常、重试的次数等等。

Attempt

public interface Attempt<V> {

 V get() throws ExecutionException;
 boolean hasResult();
 boolean hasException();
 V getResult() throws IllegalStateException;
 Throwable getExceptionCause() throws IllegalStateException;
 long getAttemptNumber();
 long getDelaySinceFirstAttempt();
    
}

StopStrategys

我们也可以通过 StopStrategys 来生产 StopStrategy 对象它提供了三个 static 方法:

  • neverStop() — 不停止
public static StopStrategy neverStop()
  • stopAfterAttempt() — 在一定次数后停止
public static StopStrategy stopAfterAttempt(int attemptNumber)
  • stopAfterDelay() — 在一定超时后停止
public static StopStrategy stopAfterDelay(long duration, @Nonnull TimeUnit timeUnit)

2.3. WaitStrategy 接口(间隔策略)

WaitStrategy 接口只包含一个方法,顾名思义,就是间隔的时长。

public interface WaitStrategy {
 
 long computeSleepTime(Attempt var1);
 
}

WaitStrategies

同样的,我们也可以使用 WaitStrategies 类来生产 WaitStrategy, WaitStrategies 提供了非常强大而丰富的 static 方法:

  • noWait() — 不间隔
public static WaitStrategy noWait()
  • fixedWait() — 指定时间间隔
public static WaitStrategy fixedWait(long sleepTime, @Nonnull TimeUnit timeUnit)
  • frandomWait() — 随机时间间隔
public static WaitStrategy randomWait(long maximumTime, @Nonnull TimeUnit timeUnit);
 
public static WaitStrategy randomWait(long minimumTime, @Nonnull TimeUnit minimumTimeUnit, long maximumTime, @Nonnull TimeUnit maximumTimeUnit);
  • incrementingWait() — 线性递增间隔
public static WaitStrategy incrementingWait(long initialSleepTime, @Nonnull TimeUnit initialSleepTimeUnit, long increment, @Nonnull TimeUnit incrementTimeUnit)

这个方法设置了 初始间隔 和 递增步长 。

  • exponentialWait() — 指数递增间隔
public static WaitStrategy exponentialWait();
 
public static WaitStrategy exponentialWait(long maximumTime, @Nonnull TimeUnit maximumTimeUnit);
 
public static WaitStrategy exponentialWait(long multiplier, long maximumTime, @Nonnull TimeUnit maximumTimeUnit);
  • fibonacciWait() — 斐波那切数列递增间隔
public static WaitStrategy fibonacciWait();
 
public static WaitStrategy fibonacciWait(long maximumTime, @Nonnull TimeUnit maximumTimeUnit);
 
public static WaitStrategy fibonacciWait(long multiplier, long maximumTime, @Nonnull TimeUnit maximumTimeUnit);
  • exceptionWait() — 一旦抛出异常则间隔
public static <T extends Throwable> WaitStrategy exceptionWait(@Nonnull Class<T> exceptionClass, @Nonnull Function<T, Long> function)

这是 WaitStrategies 中最复杂的一个策略了,一旦上一次尝试抛出了 exceptionClass 及其子类的异常,则调用回调方法 function,以其返回值作为下一次尝试前的时间间隔。

  • join() — 合并多个策略

BlockStrategies 可以方便的创建 BlockStrategy

他只提供了一个 static 方法,一旦指定,则线程会在间隔时间内 sleep,否则不会

public static WaitStrategy join(WaitStrategy... waitStrategies)

2.4. BlockStrategy 接口(阻塞策略)

这个策略指定了线程在本次尝试后 sleep 多少毫秒。

public interface BlockStrategy {
 
 void block(long var1) throws InterruptedException;
 
}

BlockStrategies

BlockStrategies 可以方便的创建 BlockStrategy。

他只提供了一个 static 方法,一旦指定,则线程会在间隔时间内 sleep,否则不会:

public static BlockStrategy threadSleepStrategy()

2.5. Predicate 接口(断言)

Predicate 接口最重要的方法是 apply 方法,返回是否需要拒绝尝试,与 AttemptTimeLimiters 类似,Predicate 通常用 Predicates 来创建。

@FunctionalInterface
@GwtCompatible
public interface Predicate<T> extends java.util.function.Predicate<T> {
 
 @CanIgnoreReturnValue
 boolean apply(@Nullable T var1);
 
 boolean equals(@Nullable Object var1);
 
 default boolean test(@Nullable T input) { return this.apply(input); }

}

Predicates

Predicates 提供了非常丰富的 static 方法集合,可以实现各种各样的断言,甚至是断言的与或非组合。

public static <T> Predicate<T> alwaysFalse();
 
public static <T> Predicate<T> isNull();
 
public static <T> Predicate<T> notNull();
 
public static <T> Predicate<T> not(Predicate<T> predicate);
 
public static <T> Predicate<T> and(Iterable<? extends Predicate<? super T>> components);
 
public static <T> Predicate<T> and(Predicate... components);
 
public static <T> Predicate<T> and(Predicate<? super T> first, Predicate<? super T> second);
 
public static <T> Predicate<T> or(Iterable<? extends Predicate<? super T>> components);
 
public static <T> Predicate<T> or(Predicate... components);
 
public static <T> Predicate<T> or(Predicate<? super T> first, Predicate<? super T> second);
 
public static <T> Predicate<T> equalTo(@NullableDecl T target);
 
public static Predicate<Object> instanceOf(Class<?> clazz);
 
public static Predicate<Class<?>> subtypeOf(Class<?> clazz);
 
public static <T> Predicate<T> in(Collection<? extends T> target);
 
public static <A, B> Predicate<A> compose(Predicate<B> predicate, Function<A, ? extends B> function);
 
public static Predicate<CharSequence> containsPattern(String pattern);
 
public static Predicate<CharSequence> contains(Pattern pattern);

2.6. wrap() — 让线程池中的线程也拥有重试功能

上面提到,Retryer 具有 call 方法,只要设置好重试策略,将相应方法封装为 Callable 接口的实现,传入 Retryer 的 call 方法,即可按照预定的重试策略调用对应的方法。

但是,如果我们的方法不是直接执行,而是需要放入线程池中呢?Retryer 提供了 wrap 接口实现将方法的重试策略封装到一个 Callable 实现中,从而让我们可以直接通过线程池调用:

public Retryer.RetryerCallable<V> wrap(Callable<V> callable)

3. Retryer 创建工具

3.1.  RetryerBuilder 类

由于 Retryer 类构造方法参数较多,较为复杂,而使用 RetryerBuilder 要更加简洁明了,也是更加常用的方式.如下:

Retryer<Boolean> retryer = RetryerBuilder.<Boolean>newBuilder()
                // 如果Callable结果为null,继续重试
                .retryIfResult(Predicates.<Boolean>isNull())
                // IOException,继续重试
                .retryIfExceptionOfType(IOException.class)
                // RuntimeException,继续重试
                .retryIfRuntimeException()
                // 指定重试策略,重试三次后停止
                .withStopStrategy(StopStrategies.stopAfterAttempt(3))
                .build();

4. Guava-Retrying 实战

RetryerBuilder 构建重试的触发异常条件、重试等待策略和重试停止策略。重试业务逻辑通过实现 callable 接口自行封装,可支持任何定制需求。

  1. RetryerGuavaTemplate 类实现
public class RetryerGuavaTemplate {

    /**
     * http请求重试封装
     *
     * @param callable        执行方法
     * @param retryExceptions 发生指定异常时重试,可为null
     * @param <T>             返回结果
     * @return
     */
    public <T> T retryHttpWrapper(Callable<T> callable, List<Class<? extends Throwable>> retryExceptions) {
        RetryerBuilder<T> builder = RetryerBuilder.<T>newBuilder()
                // 发生ConnectException异常时重试
                .retryIfExceptionOfType(ConnectException.class)
                // 重试的等待策略 初始等待1s,每次递增1s。如:第一次1s,第二次2s,第三次3s,以此类推...
                .withWaitStrategy(WaitStrategies.incrementingWait(1, TimeUnit.SECONDS, 1, TimeUnit.SECONDS))
                // 重试3次后停止
                .withStopStrategy(StopStrategies.stopAfterAttempt(3));
        // 重试异常添加
        if (retryExceptions != null && retryExceptions.size() > 0) {
            for (Class<? extends Throwable> e : retryExceptions) {
                builder.retryIfExceptionOfType(e);
            }
        }
        Retryer<T> retryer = builder.build();
        try {
            return retryer.call(callable);
        } catch (ExecutionException | RetryException e) {
            throw new RuntimeException(e);
        }
    }

    public static void main(String[] args) {
        RetryerGuavaTemplate retryerGuavaTemplate = new RetryerGuavaTemplate();
        retryerGuavaTemplate.retryHttpWrapper(() -> {
            System.out.println(LocalTime.now());
            System.out.println("execute the retryer call.");
            throw new RuntimeException("retryer test...");
        }, Lists.newArrayList(RuntimeException.class));
    }

}
  1. 测试结果

测试代码中重试次数为3次,重试等待策略初始等待1s,每增加一次递增1s。通过测试日志打印,与预期吻合。

Guava-Retrying 快速构建接口服务重试机制神器

5. Guava-Retrying 结合 AOP 实现

  1. 添加 pom 文件依赖
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
  1. 新建 @Retryer 标签类
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Retryer {

    // 指定时间间隔
    long waitMsec() default 0;

    // 重试的异常
    Class[] retryThrowable() default {};

    //在一定超时后停止
    long maxDelayMsec() default 0;

    // 最大重试次数
    int maxAttempt() default 0;

}
  1. 新建一个 AOP 切面
@Slf4j
@Aspect
@Component
public class RetryerAspect {

    @Around(value = "@annotation(Retryer)")
    public Object monitorAround(ProceedingJoinPoint pjp) throws Throwable {
        Method method;
        if (pjp.getSignature() instanceof MethodSignature) {
            MethodSignature signature = (MethodSignature) pjp.getSignature();
            method = signature.getMethod();
        } else {
            log.error("Monitor Annotation not at a method {}", pjp);
            return null;
        }
        Retryer retryerAnnotation = method.getDeclaredAnnotation(Retryer.class);
        if (retryerAnnotation.maxDelayMsec() <= 0 && retryerAnnotation.maxAttempt() <= 1) {
            return pjp.proceed();
        }
        RetryerBuilder retryer = RetryerBuilder.newBuilder();
        if (retryerAnnotation.waitMsec() > 0) {
            retryer.withWaitStrategy(fixedWait(retryerAnnotation.waitMsec(), TimeUnit.MILLISECONDS));
        }
        if (retryerAnnotation.retryThrowable().length > 0) {
            for (Class retryThrowable : retryerAnnotation.retryThrowable()) {
                if (retryThrowable != null && Throwable.class.isAssignableFrom(retryThrowable)) {
                    retryer.retryIfExceptionOfType(retryThrowable);
                }
            }
        }
        if (retryerAnnotation.maxDelayMsec() > 0) {
            retryer.withStopStrategy(StopStrategies.stopAfterDelay(retryerAnnotation.maxDelayMsec(), TimeUnit.MILLISECONDS));
        }
        if (retryerAnnotation.maxAttempt() > 0) {
            retryer.withStopStrategy(StopStrategies.stopAfterAttempt(retryerAnnotation.maxAttempt()));
        }
        String retrylog = pjp.getTarget().getClass().getCanonicalName() + "." + method.getName();
        return retryer.build().call(() -> {
            try {
                log.info("<RETRYER>" + retrylog);
                return pjp.proceed();
            } catch (Throwable throwable) {
                if (throwable instanceof Exception) {
                    throw (Exception) throwable;
                } else {
                    throw new Exception(throwable);
                }
            }
        });
    }

}
  1. 使用我们自定义的 @Retryer 实现接口自动重试
    @Retryer(retryThrowable = Exception.class, maxAttempt = 3, waitMsec = 2000)
    public Object retryerTest(String hello) {
        Map<String, String> result = null;
        try {
            System.out.println(LocalDateTime.now());
            result.put("name", hello);
        } catch (Exception e) {
            throw e;
        }
        return "";
    }

原文始发于微信公众号(白菜说技术):Guava-Retrying 快速构建接口服务重试机制神器

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/172844.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!