Guava学习(四)-重试

重试的使用场景

在很多业务场景中,为了排除系统中的各种不稳定因素,以及逻辑上的错误,并最大概率保证获得预期的结果,重试机制都是必不可少的。

尤其是调用远程服务,在高并发场景下,很可能因为服务器响应延迟或者网络原因,造成我们得不到想要的结果,或者根本得不到响应。这个时候,一个优雅的重试调用机制,可以让我们更大概率保证得到预期的响应。
在这里插入图片描述
通常情况下,我们会通过定时任务进行重试。例如某次操作失败,则记录下来,当定时任务再次启动,则将数据放到定时任务的方法中,重新跑一遍。最终直至得到想要的结果为止。

无论是基于定时任务的重试机制,还是我们自己写的简单的重试器,缺点都是重试的机制太单一,而且实现起来不优雅。

如何优雅地设计重试实现

一个完备的重试实现,要很好地解决如下问题:

  1. 什么条件下重试
  2. 什么条件下停止
  3. 如何停止重试
  4. 停止重试等待多久
  5. 如何等待
  6. 请求时间限制
  7. 如何结束
  8. 如何监听整个重试过程

并且,为了更好地封装性,重试的实现一般分为两步:

  1. 使用工厂模式构造重试器
  2. 执行重试方法并得到结果

一个完整的重试流程可以简单示意为:
在这里插入图片描述

guava-retrying基础用法

guava-retrying是基于谷歌的核心类库guava的重试机制实现,可以说是一个重试利器。

下面就快速看一下它的用法。

1.Maven配置

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

需要注意的是,此版本依赖的是27.0.1版本的guava。如果你项目中的guava低几个版本没问题,但是低太多就不兼容了。这个时候你需要升级你项目的guava版本,或者直接去掉你自己的guava依赖,使用guava-retrying传递过来的guava依赖。

2.实现Callable

Callable<Boolean> callable = new Callable<Boolean>() {
    public Boolean call() throws Exception {
        return true; // do something useful here
    }
};

Callable的call方法中是你自己实际的业务调用。

3.通过RetryerBuilder构造Retryer

Retryer<Boolean> retryer = RetryerBuilder.<Boolean>newBuilder()
        .retryIfResult(Predicates.<Boolean>isNull())
        .retryIfExceptionOfType(IOException.class)
        .retryIfRuntimeException()
        .withStopStrategy(StopStrategies.stopAfterAttempt(3))
        .build();

4.使用重试器执行你的业务

retryer.call(callable);

下面是完整的参考实现。

public Boolean test() throws Exception {
    //定义重试机制
    Retryer<Boolean> retryer = RetryerBuilder.<Boolean>newBuilder()
            //retryIf 重试条件
            .retryIfException()
            .retryIfRuntimeException()
            .retryIfExceptionOfType(Exception.class)
            .retryIfException(Predicates.equalTo(new Exception()))
            .retryIfResult(Predicates.equalTo(false))

            //等待策略:每次请求间隔1s
            .withWaitStrategy(WaitStrategies.fixedWait(1, TimeUnit.SECONDS))

            //停止策略 : 尝试请求6次
            .withStopStrategy(StopStrategies.stopAfterAttempt(6))

            //时间限制 : 某次请求不得超过2s , 类似: TimeLimiter timeLimiter = new SimpleTimeLimiter();
            .withAttemptTimeLimiter(AttemptTimeLimiters.fixedTimeLimit(2, TimeUnit.SECONDS))

            .build();

    //定义请求实现
    Callable<Boolean> callable = new Callable<Boolean>() {
        int times = 1;

        @Override
        public Boolean call() throws Exception {
            log.info("call times={}", times);
            times++;

            if (times == 2) {
                throw new NullPointerException();
            } else if (times == 3) {
                throw new Exception();
            } else if (times == 4) {
                throw new RuntimeException();
            } else if (times == 5) {
                return false;
            } else {
                return true;
            }

        }
    };
    //利用重试器调用请求
   return  retryer.call(callable);
}

guava-retrying实现原理

guava-retrying的核心是Attempt类、Retryer类以及一些Strategy(策略)相关的类。

Attempt

Attempt既是一次重试请求(call),也是请求的结果,并记录了当前请求的次数、是否包含异常和请求的返回值。

/**
 * An attempt of a call, which resulted either in a result returned by the call,
 * or in a Throwable thrown by the call.
 *
 * @param <V> The type returned by the wrapped callable.
 * @author JB
 */
public interface Attempt<V>

Retryer

Retryer通过RetryerBuilder这个工厂类进行构造。RetryerBuilder负责将定义的重试策略赋值到Retryer对象中。

在Retryer执行call方法的时候,会将这些重试策略一一使用。

下面就看一下Retryer的call方法的具体实现。

/**
    * 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();
       //说明: 根据attemptNumber进行循环——也就是重试多少次
       for (int attemptNumber = 1; ; attemptNumber++) {
           //说明:进入方法不等待,立即执行一次
           Attempt<V> attempt;
           try {
                //说明:执行callable中的具体业务
                //attemptTimeLimiter限制了每次尝试等待的时常
               V result = attemptTimeLimiter.call(callable);
               //利用调用结果构造新的attempt
               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);
               }
           }
       }
   }

Retryer执行过程如下:
在这里插入图片描述

guava-retrying实战

由于在订单超时取消时,需要进行回查支付记录,判断用户是否真的支付过。在调用支付是可能出现异常,需要进行重试。

自定义重试阻塞策略

默认的阻塞策略是线程休眠,这里使用自旋锁实现,不阻塞线程。

@Slf4j
@NoArgsConstructor
@Component
public class SpinBlockStrategy implements BlockStrategy {

    @Override
    public void block(long sleepTime) {

        LocalDateTime startTime = LocalDateTime.now();

        long start = System.currentTimeMillis();
        long end = start;
        log.info("线程自旋开始");

        while (end - start <= sleepTime) {
            end = System.currentTimeMillis();
        }

        //使用Java8新增的Duration计算时间间隔
        Duration duration = Duration.between(startTime, LocalDateTime.now());

        log.info("线程自旋结束 时间间隔={}", duration.toMillis());

    }
}

自定义重试监听器

RetryListener可以监控多次重试过程,并可以使用attempt做一些额外的事情。

@Slf4j
@Component
public class RetryLogListener implements RetryListener {

    @Override
    public <V> void onRetry(Attempt<V> attempt) {
        log.info("重试监听开始");

        // 第几次重试,(注意:第一次重试其实是第一次调用) 距离第一次重试的延迟 重试结果: 是异常终止, 还是正常返回
        log.info("重试次数: [{}] 重试延迟 : [{}] 是否存在异常={} 是否返回结果={}",
                attempt.getAttemptNumber(), attempt.getDelaySinceFirstAttempt()
                , attempt.hasException(),  attempt.hasResult());

        // 是什么原因导致异常
        if (attempt.hasException()) {
            log.info("异常信息={}" , attempt.getExceptionCause().toString());
        } else {
            // 正常返回时的结果
            log.info("返回结果={}" , attempt.getResult());
        }

        log.info("重试监听结束");

    }
}

自定义Exception

有些异常需要重试,有些不需要。

public class NeedRetryException extends Exception {

    public NeedRetryException(String message) {
        super("远程服务调用异常,进行重试," + message);
    }
}

动态调节重试策略

引入apollo动态调节重试策略。

@Slf4j
@Component
public class IndexWaitStrategy implements WaitStrategy {
    @Autowired
    private AppConfig appConfig;

    @Override
    public long computeSleepTime(Attempt failedAttempt) {
        //当前重试次数
        long number = failedAttempt.getAttemptNumber();
        log.info("重试时间:{}", number * appConfig.getRetryDelay());
        return number * appConfig.getRetryDelay();
    }
}

构造重试器Retryer

将上面的实现作为参数,构造Retryer。并使用Retryer进行接口重试。

@Slf4j
@Service
public class OrderRetryService {

    @Autowired
    private RecordFeign recordFeign;

    @Autowired
    private SpinBlockStrategy spinBlockStrategy;

    @Autowired
    private RetryLogListener retryLogListener;

    @Autowired
    private IndexWaitStrategy indexWaitStrategy;

    @Autowired
    private AppConfig appConfig;

    /**
     * 获取支付记录信息重试
     */
    private Retryer<TradeRecordDTO> tradeRetryer;



    @PostConstruct
    private void init() {
        tradeRetryer = RetryerBuilder.<TradeRecordDTO>newBuilder()
                //重试条件
                .retryIfExceptionOfType(NeedRetryException.class)
                .retryIfResult(Predicates.isNull())
                //等待策略:每次请求间隔1s
                //.withWaitStrategy(WaitStrategies.fixedWait(1, TimeUnit.SECONDS))
                //自定义等待策略:根据配置递增
                .withWaitStrategy(indexWaitStrategy)
                //停止策略 : 尝试请求3次
                .withStopStrategy(StopStrategies.stopAfterAttempt(appConfig.getRetryTime()))
                //时间限制 : 某次请求不得超过2s , 类似: TimeLimiter timeLimiter = new SimpleTimeLimiter();
                //.withAttemptTimeLimiter(AttemptTimeLimiters.fixedTimeLimit(appConfig.getRetryTimeLimit(), TimeUnit.SECONDS))
                //默认的阻塞策略:线程睡眠
                //.withBlockStrategy(BlockStrategies.threadSleepStrategy())
                //自定义阻塞策略:自旋锁
                .withBlockStrategy(spinBlockStrategy)
                //自定义重试监听器
                .withRetryListener(retryLogListener)
                .build();
    }

    /**
     * @Author xyhua
     * @Description 查询支付单信息
     * @Date 12:58 2020-04-08
     * @param
     * @return
     **/
    public TradeRecordDTO checkTradeRecord(String orderSn) throws ExecutionException, RetryException {
        Callable<TradeRecordDTO> recordDTOCallable = () -> {
            log.info("checkTradeRecord|查询支付单状态 orderSn:{}", orderSn);
            TradeRecordDTO tradeRecordDTO;
            try {
                tradeRecordDTO = recordFeign.getTradeRecordDTOByOrderCode(orderSn).checkResult();
                log.info("checkTradeRecord|查询支付单状态结果 tradeRecordDTO:{}", tradeRecordDTO);
            } catch (Exception e) {
                throw new NeedRetryException("订单号:" + orderSn);
            }
            return tradeRecordDTO;
        };
        return tradeRetryer.call(recordDTOCallable);
    }
}
使用中遇到的问题

Guava版本过高会让AttemptTimeLimiters.fixedTimeLimit(appConfig.getRetryTimeLimit(), TimeUnit.SECONDS)无法使用,通过降低Guava版本可以解决。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值