Retry轮询重试
在某些容易超时的业务场景下,通常会按照主动提交+主动轮询提交结果的模式去设计,即先将需要的信息提交到第三方接口,由于第三方接口处理较慢,无法在提交接口立即返回结果,则会返回流水号,根据流水号再去轮询查询第三方接口流水号对应的提交结果,轮询重试需要有一定的间隔时间,让第三方有充分时间进行处理。
轮询重试这个场景,比较原始的做法是,循环+Sleep,例如:
for(int i = 0;i<n;i++){
//查询
//判断是否查询到终态结果
//查询到终态则break,未查询到终态则继续
//Thread.sleep() 休眠一定时间
}
市面上已经有比较成熟的组件可以胜任这样的场景,这里比较推荐 guava-retry
-
引入依赖
<!-- 重试Retry组件--> <dependency> <groupId>com.github.rholder</groupId> <artifactId>guava-retrying</artifactId> <version>2.0.0</version> </dependency>
-
配置重试条件
package com.example.demo.utils; import com.github.rholder.retry.Attempt; import com.github.rholder.retry.RetryListener; import com.github.rholder.retry.Retryer; import com.github.rholder.retry.RetryerBuilder; import com.github.rholder.retry.StopStrategies; import com.github.rholder.retry.WaitStrategies; import com.google.common.base.Predicates; import java.util.concurrent.Callable; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; /** * @Description:重试工具类 * @Author: * @Date: 2022/10/10 10:59 */ public class RetryUtil { /** * 默认重试次数3次 */ private int maxAttempts = 3; /** * 默认等待时间3秒 */ private int waitTime = 3; /** * 默认重试等待时间单位 :秒 */ private TimeUnit timeUnit = TimeUnit.SECONDS; /** * 默认重试拦截器 */ private RetryListener retryListener = new MyRetryListener(); private RetryUtil config(int maxAttempts,int waitTime){ this.maxAttempts=maxAttempts; this.waitTime=waitTime; return this; } private Retryer init(){ Retryer<Boolean> retryer = RetryerBuilder.<Boolean>newBuilder() //设置发生异常则重试 .retryIfException() //设置返回false则重试 .retryIfResult(Predicates.<Boolean>equalTo(Boolean.FALSE)) //设置重试等待时间及单位 .withWaitStrategy(WaitStrategies.fixedWait(waitTime,timeUnit)) //设置最大重试次数 .withStopStrategy(StopStrategies.stopAfterAttempt(maxAttempts)) //设置重试拦截器 .withRetryListener(retryListener) .build(); return retryer; } public static void retry(Callable callable,int maxAttempts,int waitTime){ CompletableFuture.runAsync(() -> { Retryer retryer = new RetryUtil().config(maxAttempts, waitTime).init(); try { retryer.call(callable); } catch (Exception e) { e.printStackTrace(); } },ExecutorsUtil.executorService); } /** * 重试拦截器 */ class MyRetryListener implements RetryListener{ /** * 重试拦截记录日志 */ @Override public <V> void onRetry(Attempt<V> attempt) { long retryNumber = attempt.getAttemptNumber(); if(attempt.hasException()){ //异常重试记录 Throwable exceptionCause = attempt.getExceptionCause(); System.out.println("第" + retryNumber + "次重试调用,发生异常:" + exceptionCause.toString()); }else{ //正常重试记录 V result = attempt.getResult(); System.out.println("第" + retryNumber + "次重试调用,结果为:" + result); } } } }
重点在init() 内部配置重试策略这里,可以设置重试的条件、重试的间隔时间、重试的最大次数、重试拦截器,不仅是例子中的策略,这个组件还提供了更多多样的策略,读者可以下去自己试一下
-
跑一下
public static void main(String[] args){ long begin = System.currentTimeMillis(); System.out.println("主线程名" + Thread.currentThread().getName()); RetryUtil.retry(() -> { int randomInt = RandomUtil.randomInt(0, 10); System.out.println("重试任务获取随机数" + randomInt); System.out.println("重试任务线程名" + Thread.currentThread().getName()); return randomInt > 4; },3,3); long end = System.currentTimeMillis(); System.out.println("执行耗时:" + String.valueOf(end-begin)); /** * 输出: * 主线程名main * 执行耗时:69 * 重试任务获取随机数0 * 重试任务线程名pool-1-thread-1 * 第1次重试调用,结果为:false * 重试任务获取随机数8 * 重试任务线程名pool-1-thread-1 * 第2次重试调用,结果为:true */ }
重试任务即随机取(0,10]之前的数,然后判断是否大于4,如果不满足条件,返回false,就会重试。
分析输出的信息,重试任务设置的是异步执行,所以是由另一个线程开启重试,第n重试及结果的日志,则是重试拦截器中的逻辑打印出来的