前言
在日常开发中,我们常常会遇到这样的业务场景,业务在执行完某项操作之后,需要通知其它子系统。
例如客户下单完成之后,订单系统需要通知数据分析系统,记录下单数据;通知发货系统,开始处理发货;通知会员回馈系统,客户下了单增加了积分,可以适当的发个红包。
这些通知和客户下单这件事有关系。但是和下单是否成功并无关系。
客户希望能快速得到下单成功的页面。这些通知该如何处理呢?
比较简单的方法是,利用多线程进行异步的接口调用。
简单版
例如,业务需要调用A、B、C三个业务,
private void taskA() {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("do task a");
}
private void taskB() {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("do task b");
}
private final static AtomicInteger taskCTryTimes = new AtomicInteger(1);
private void taskC() {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (taskCTryTimes.getAndIncrement() < 3) {
System.out.println("do task c fail");
throw new RuntimeException("taskC fail");
}
System.out.println("do task c");
}
工具类如下:
/**
* 执行任务
*
* @author hengyumo
* @param executorService ExecutorService
* @param runnable Runnable
*/
public static void doJob(ExecutorService executorService, java.lang.Runnable runnable) {
executorService.submit(runnable);
}
我们不关心任务是否是成功的,那么可以直接这样操作:
@Test
public void testAsyncJob() throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(5);
ConcurrentUtil.doJob(executorService, this::taskA);
ConcurrentUtil.doJob(executorService, this::taskB);
ConcurrentUtil.doJob(executorService, this::taskC);
executorService.shutdown();
//noinspection ResultOfMethodCallIgnored
executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.DAYS);
}
结果:
do task b
do task a
do task c fail
可以看到,这里任务C失败了,其原因可能是超时、网络波动、数据库查询慢等等原因。但不管怎么说它都是失败了。
我们只并没有处理失败,这对下游服务并不公平,为了能保证我们的消息能稳妥的递送给其它服务。
我们可以增加重试机制:
重试机制
@FunctionalInterface
public interface Runnable {
void run() throws Throwable;
}
/**
* 执行任务,设置重试次数,如果超出重试次数则会放弃任务
*
* @author hengyumo
* @param executorService ExecutorService
* @param runnable Runnable
* @param taskDesc 任务描述
* @param tryTimes 重试次数
* @param delay 延时(毫秒),每失败一次,都会等待延时之后才进行重试
*/
public static void doJob(ExecutorService executorService, Runnable runnable,
String taskDesc, int tryTimes, int delay) {
Assert.isTrue(tryTimes > 0, "重试次数需要大于0");
Assert.isTrue(delay > 0, "延时(毫秒)需要大于0");
// 认为无异常既是成功
Callable<Boolean> callable = () -> {
try {
runnable.run();
} catch (Throwable throwable) {
throwable.printStackTrace();
return false;
}
return true;
};
// 监听线程
java.lang.Runnable watch = () -> {
Future<Boolean> future;
int hasTry = 0;
boolean success =false;
while (hasTry < tryTimes) {
future = executorService.submit(callable);
try {
if (future.get()) {
success = true;
break;
}
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
hasTry ++;
log.info("{} 失败,重试次数:{}", taskDesc, hasTry);
try {
Thread.sleep(delay);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.info("{} 执行完成,success:{},重试次数:{}", taskDesc, success, hasTry);
};
executorService.submit(watch);
}
通过这个方法实现了超时重试:
@Test
public void testAsyncJobWithRetry() throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(5);
ConcurrentUtil.doJob(executorService, this::taskA, "taskA", 1, 500);
ConcurrentUtil.doJob(executorService, this::taskB, "taskB", 1, 500);
ConcurrentUtil.doJob(executorService, this::taskC, "taskC", 3, 500);
// shutdown():停止接收新任务,原来的任务继续执行,
// 因为ConcurrentUtil.doJob里边还要往线程池里加任务,所以这里shutdown不行
// executorService.shutdown();
// 5s 未执行完成,则终止所有线程
if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) {
executorService.shutdownNow();
}
}
测试结果:
do task b
do task a
19:17:08.016 [pool-1-thread-1] INFO cn.hengyumo.dawn.utils.ConcurrentUtil - taskA 执行完成,success:true,重试次数:0
19:17:08.016 [pool-1-thread-2] INFO cn.hengyumo.dawn.utils.ConcurrentUtil - taskB 执行完成,success:true,重试次数:0
do task c fail
java.lang.RuntimeException: taskC fail
at cn.hengyumo.dawn.example.ConcurrentTest.taskC(ConcurrentTest.java:178)
at cn.hengyumo.dawn.utils.ConcurrentUtil.lambda$doJob$0(ConcurrentUtil.java:41)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1130)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:630)
at java.base/java.lang.Thread.run(Thread.java:832)
19:17:08.525 [pool-1-thread-5] INFO cn.hengyumo.dawn.utils.ConcurrentUtil - taskC 失败,重试次数:1
do task c fail
java.lang.RuntimeException: taskC fail
at cn.hengyumo.dawn.example.ConcurrentTest.taskC(ConcurrentTest.java:178)
at cn.hengyumo.dawn.utils.ConcurrentUtil.lambda$doJob$0(ConcurrentUtil.java:41)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1130)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:630)
at java.base/java.lang.Thread.run(Thread.java:832)
19:17:09.549 [pool-1-thread-5] INFO cn.hengyumo.dawn.utils.ConcurrentUtil - taskC 失败,重试次数:2
do task c
19:17:10.579 [pool-1-thread-5] INFO cn.hengyumo.dawn.utils.ConcurrentUtil - taskC 执行完成,success:true,重试次数:2
可以看到,taskC失败了两次,第三次才成功了。
这个重试还可以这么写,更加的简洁:
重试简洁写法
public static void doJobSimple(ExecutorService executorService, Runnable runnable,
String taskDesc, int tryTimes, int delay) {
Assert.isTrue(tryTimes > 0, "重试次数需要大于0");
Assert.isTrue(delay > 0, "延时(毫秒)需要大于0");
// 监听线程
java.lang.Runnable watch = () -> {
int hasTry = 0;
boolean success = false;
while (hasTry < tryTimes) {
try {
runnable.run();
success = true;
break;
} catch (Throwable throwable) {
throwable.printStackTrace();
}
hasTry ++;
log.info("{} 失败,重试次数:{}", taskDesc, hasTry);
try {
Thread.sleep(delay);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.info("{} 执行完成,success:{},重试次数:{}", taskDesc, success, hasTry);
};
executorService.submit(watch);
}