项目实战:使用JUC的CompletableFuture执行任务,并根据返回值执行异步回调

前言

程序猿大吉在实施的威逼之下又有了新的需求,翻译成技术语言,大致是这样的:

不停地调用一个远程接口(成千上万次)。该接口会返回一串id,并拿着这个id回写我们本地的数据库。

这个远程接口响应时间特别久,大概要1到3s。而一旦接口返回一串id,并将id回写到本地数据库,这个过程比较短,只需要0.05s左右。

所以我想到了将查询远程接口封装成一个函数,将回写本地数据库封装成一个函数,将异常处理封装成一个函数。这样可以最大程度解耦。

在保证效率的情况下必须是用线程池,使用线程池的话,有两种方法解决该需求:

  • 方法一:使用自带的 ExecutorService 建立一个线程池,并且调用其 submit方法开线程。在单个线程里串行执行 调用远程接口 -> 回写数据库状态 -> 异常处理
  • 方法二:使用JUC的CompletableFuture建立一个线程池,调用其supplyAsync方法开线程,在这个线程里只执行 【调用远程接口】的方法,然后根据该线程调用完成的返回结果 回调封装好的 【回写数据库】方法。如果返回结果异常,就回调封装好的【异常处理】方法

这两种方法在执行顺序上其实是完全一致的,效率也差不多。如果考虑代码可读性,则可以选用方法一。但是方法二在逻辑上更说得通,且在多人协作开发的情况下,线程功能更加单一,耦合度要低一些。

最终选用了方法二作为开发方案。下面就来重点讨论 CompletableFuture 的技术实现。

建立线程池

使用 CompletableFuture 开启一个线程,该方法可以传入自定义线程池,否则就用默认的线程池。

通过以下Java代码建立一个线程池:

线程池创建构造方法传入的几个参数,是根据业务量而定的。根据业务数量,将线程池的参数调整如下:

//定义线程池
    public static final ExecutorService threadPool =
            new ThreadPoolExecutor(
                    20,
                    50,
                    60L,
                    TimeUnit.SECONDS,
                    new ArrayBlockingQueue<>(50),
                    new ThreadPoolExecutor.AbortPolicy());

其中常驻线程数量调整到了20,最大线程数量是50,如果超出则采取丢弃策略。

开启线程

使用CompletableFuture.supplyAsync开启一个线程。为什么用supplyAsync这个方法呢?因为该方法可以获取返回值。

CompletableFuture创建线程的时候可以传入自定义线程池,否则它就使用默认线程池,刚刚已经创建好线程池了,那就把刚刚创建的线程池传入。

代码如下(threadPool就是刚刚创建好的线程池):

//该方法可以传入自定义线程池,否则就用默认的线程池;
            CompletableFuture<Integer> thread = CompletableFuture.supplyAsync(
                    () -> callRemoteApi(), threadPool);

该线程里只干一件事:就是调用远程API。

获取线程返回值,根据返回值调用回写数据库

上面的线程执行结束后,会返回一个id,然后我们根据该id查询数据库。具体用到的方法是 whenCompleteAsync:

//线程池执行完毕,如果成功了,则回写,如果失败了,就执行失败处理逻辑
            thread.whenCompleteAsync(
                    (result,throwable)-> {
    
    
                        if (result != -10086) //如果线程池返回了调用远程API成功的结果,那么就调用回写数据库的方法
                            writeBack(result);
                        else
                            exceptionHandle(result);//执行失败处理逻辑
                    }
            );

该方法是一个异步回调方法,线程池会回调我们定义好的 【回写数据库】函数。

最后看 whenCompleteAsync 这个方法本身:

方法不以 Async 结尾,意味着 Action 使用相同的线程执行,而 Async 可能会使用其他线程
执行(如果是使用相同的线程池,也可能会被同一个线程选中执行)

介绍到这里,理论知识应该没问题了。下面看完整Demo代码:

完整Demo代码


import java.util.ArrayList;
import java.util.Random;
import java.util.concurrent.*;


/*
    该测试结果完全满足需求。
    下面代码的目标:使用线程池调用远程API,远程API会返回一串id
    远程API有一定概率返回失败,返回失败的状态码总是 -10086

    线程池接收API返回结果,如果成功,就将该id回写数据库
    如果失败,就调用异常处理逻辑。

    API参考资料:https://www.cnblogs.com/wuwuyong/p/15496841.html
*/
public class E06_CompletableFuture_AsyncCallback {
    
    

    //定义线程池
    //和实施讨论并且根据业务数量,将线程池的参数调整如下:有20个常驻线程。
    public static final ExecutorService threadPool =
            new ThreadPoolExecutor(
                    20,
                    50,
                    60L,
                    TimeUnit.SECONDS,
                    new ArrayBlockingQueue<>(50),
                    new ThreadPoolExecutor.AbortPolicy());


    public static void main(String[] args) {
    
    


        //一共需要发送15次远程请求,向数据库回写15次状态
        for (int i = 0; i < 15; i++) {
    
    

            //该方法可以传入自定义线程池,否则就用默认的线程池;
            CompletableFuture<Integer> thread = CompletableFuture.supplyAsync(
                    () -> callRemoteApi(), threadPool);

            //线程池执行完毕,如果成功了,则回写,如果失败了,就执行失败处理逻辑
            thread.whenCompleteAsync(
                    (result,throwable)-> {
    
    
                        if (result != -10086) //如果线程池返回了调用远程API成功的结果,那么就调用回写数据库的方法
                            writeBack(result);
                        else
                            exceptionHandle(result);//执行失败处理逻辑
                    }
            );

        }


    }
    //调用一个远程API,该API会返回一个id
    private static Integer callRemoteApi(){
    
    
        try {
    
    
            //模拟长任务:调用时间。该调用时间比较慢,长达三秒
            Thread.sleep(3000L);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }

        Random random = new Random();
        int i = random.nextInt(10);
        if (judgeSuccess()) {
    
    
            System.out.println("已经调用远程API接口,此次调用成功,返回的id是:"+i);
            return i;
        } else {
    
    
            System.out.println("已经调用远程API接口,此次调用失败");
            return -10086;
        }
    }

    private static void writeBack(int id) {
    
    
        //回写状态到数据库是一个短任务(相对而言)
        System.out.println("已经将id为:" + id + "的单据,回写状态到数据库");
    }

    //如果远程调用失败了就进行异常处理。
    private static void exceptionHandle(int code){
    
    
        System.out.println("本次调用失败,已经调用失败处理逻辑。错误码为:"+code);
    }

    //调用远程接口有50%的概率失败。这个方法用于判断是否接口调用成功
    private static boolean judgeSuccess() {
    
    
        Random random = new Random();
        int i = random.nextInt(10);
        if (i <= 5) {
    
    
            return true;
        } else {
    
    
            return false;
        }
    }
}

后记

这只是一个简单的demo。而CompletableFuture最强大的地方在于他能够编排,决定各个线程执行次序,等待线程执行结果等等。可以让复杂的操作利用多个线程完成。详见https://www.cnblogs.com/wuwuyong/p/15496841.html 这篇博客,里面提供了更详尽的例子。

猜你喜欢

转载自blog.csdn.net/weixin_44757863/article/details/122370050