JAVA利用多线程进行异步接口调用的模型

收录于墨的2020~2021开发经验总结

前言

在日常开发中,我们常常会遇到这样的业务场景,业务在执行完某项操作之后,需要通知其它子系统。

例如客户下单完成之后,订单系统需要通知数据分析系统,记录下单数据;通知发货系统,开始处理发货;通知会员回馈系统,客户下了单增加了积分,可以适当的发个红包。

这些通知和客户下单这件事有关系。但是和下单是否成功并无关系。

客户希望能快速得到下单成功的页面。这些通知该如何处理呢?

比较简单的方法是,利用多线程进行异步的接口调用。

简单版

例如,业务需要调用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);
    }

END

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值