使用JAVA CompletableFuture实现流水线化的并行处理,深度实践总结

本文探讨了在后端服务中如何使用JAVA CompletableFuture优化接口响应速度,通过并行处理减少耗时。首先,文章通过一个全网比价服务的场景引入,展示了串行处理的耗时问题,然后提出并行处理的解决方案,利用线程池和Future减少接口总耗时。接着,文章详细介绍了CompletableFuture的使用,包括创建、流水线处理、异常处理和多个CompletableFuture的组合操作。此外,还提到了与Stream结合使用的注意事项,以及并发和并行的区别。最后,文章总结了选择并发或并行处理的策略,并鼓励读者分享自己的并行处理经验。
摘要由CSDN通过智能技术生成

大家好,又见面啦。

在项目开发中,后端服务对外提供API接口一般都会关注响应时长。但是某些情况下,由于业务规划逻辑的原因,我们的接口可能会是一个聚合信息处理类的处理逻辑,比如我们从多个不同的地方获取数据,然后汇总处理为最终的结果再返回给调用方,这种情况下,往往会导致我们的接口响应特别的慢。

而如果我们想要动手进行优化的时候呢,就会涉及到串行处理改并行处理的问题。在JAVA中并行处理的能力支持已经相对完善,通过对CompletableFuture的合理利用,可以让我们面对这种聚合类处理的场景会更加的得心应手。

好啦,话不多说,接下来就让我们一起来品尝下JAVA中组合式并行处理这道饕餮大餐吧。

前菜:先看个实际场景

在开始享用这顿大餐前,我们先来个前菜开开胃。

例如现在有这么个需求:

需求描述
实现一个全网比价服务,比如可以从某宝、某东、某夕夕去获取某个商品的价格、优惠金额,并计算出实际付款金额,最终返回价格最优的平台与价格信息。

📢这里假定每个平台获取原价格与优惠券的接口已经实现、且都是需要调用HTTP接口查询的耗时操作,Mock接口每个耗时1s左右。

根据最初的需求理解,我们可以很自然的写出对应实现代码:

public PriceResult getCheapestPlatAndPrice(String product) {
    // 获取某宝的价格以及优惠,并计算最终实付价格
    PriceResult mouBaoPrice = computeRealPrice(HttpRequestMock.getMouBaoPrice(product),
            HttpRequestMock.getMouBaoDiscounts(product));
    // 获取某东的价格以及优惠,并计算最终实付价格
    PriceResult mouDongPrice = computeRealPrice(HttpRequestMock.getMouDongPrice(product),
            HttpRequestMock.getMouDongDiscounts(product));
    // 获取某夕夕的价格以及优惠,并计算最终实付价格
    PriceResult mouXiXiPrice = computeRealPrice(HttpRequestMock.getMouXiXiPrice(product),
            HttpRequestMock.getMouXiXiDiscounts(product));

    // 计算并选出实际价格最低的平台
    return Stream.of(mouBaoPrice, mouDongPrice, mouXiXiPrice).
            min(Comparator.comparingInt(PriceResult::getRealPrice))
            .get();
}

一切顺利成章,运行测试下: 

05:24:53.759[main|1]获取某宝上 Iphone13的价格
05:24:54.779[main|1]获取某宝上 Iphone13的价格完成: 5199
05:24:54.779[main|1]获取某宝上 Iphone13的优惠
05:24:55.781[main|1]获取某宝上 Iphone13的优惠完成: -200
05:24:55.781[main|1]某宝最终价格计算完成:4999
05:24:55.781[main|1]获取某东上 Iphone13的价格
05:24:56.784[main|1]获取某东上 Iphone13的价格完成: 5299
05:24:56.784[main|1]获取某东上 Iphone13的优惠
05:24:57.786[main|1]获取某东上 Iphone13的优惠完成: -150
05:24:57.786[main|1]某东最终价格计算完成:5149
05:24:57.786[main|1]获取某夕夕上 Iphone13的价格
05:24:58.788[main|1]获取某夕夕上 Iphone13的价格完成: 5399
05:24:58.788[main|1]获取某夕夕上 Iphone13的优惠
05:24:59.791[main|1]获取某夕夕上 Iphone13的优惠完成: -5300
05:24:59.791[main|1]某夕夕最终价格计算完成:99
获取最优价格信息:【平台:某夕夕, 原价:5399, 折扣:0, 实付价:99】
-----执行耗时: 6122ms  ------

结果符合预期,功能一切正常,就是耗时长了点。试想一下,假如你在某个APP操作查询的时候,等待6s才返回结果,估计会直接把APP给卸载了吧

梳理下前面代码的实现思路:

 

所有的环节都是串行的,每个环节耗时加到一起,接口总耗时肯定很长。

但实际上,每个平台之间的操作是互不干扰的,那我们自然而然的可以想到,可以通过多线程的方式,同时去分别执行各个平台的逻辑处理,最后将各个平台的结果汇总到一起比对得到最低价格。

所以整个执行过程会变成如下的效果:

为了提升性能,我们采用线程池来负责多线程的处理操作,因为我们需要得到各个子线程处理的结果,所以我们需要使用 Future来实现: 

public PriceResult getCheapestPlatAndPrice2(String product) {
    Future<PriceResult> mouBaoFuture =
        threadPool.submit(() -> computeRealPrice(HttpRequestMock.getMouBaoPrice(product),
                                                 HttpRequestMock.getMouBaoDiscounts(product)));
    Future<PriceResult> mouDongFuture =
        threadPool.submit(() -> computeRealPrice(HttpRequestMock.getMouDongPrice(product),
                                                 HttpRequestMock.getMouDongDiscounts(product)));
    Future<PriceResult> mouXiXiFuture =
        threadPool.submit(() -> computeRealPrice(HttpRequestMock.getMouXiXiPrice(product),
                                                 HttpRequestMock.getMouXiXiDiscounts(product)));
    
    // 等待所有线程结果都处理完成,然后从结果中计算出最低价
    return Stream.of(mouBaoFuture, mouDongFuture, mouXiXiFuture)
        .map(priceResultFuture -> {
            try {
                return priceResultFuture.get(5L, TimeUnit.SECONDS);
            } catch (Exception e) {
                return null;
            }
        })
        .filter(Objects::nonNull)
        .min(Comparator.comparingInt(PriceResult::getRealPrice))
        .get();
}

上述代码中,将三个不同平台对应的Callable函数逻辑放入到ThreadPool中去执行,返回Future对象,然后再逐个通过Future.get()接口阻塞获取各自平台的结果,最后经比较处理后返回最低价信息。

执行代码,可以看到执行结果与过程如下:

05:42:24.270[pool-1-thread-1|12]获取某宝上 Iphone13的价格
05:42:24.270[pool-1-thread-2|13]获取某东上 Iphone13的价格
05:42:24.270[pool-1-thread-3|14]获取某夕夕上 Iphone13的价格
05:42:25.291[pool-1-thread-2|13]获取某东上 Iphone13的价格完成: 5299
05:42:25.291[pool-1-thread-3|14]获取某夕夕上 Iphone13的价格完成: 5399
05:42:25.291[pool-1-thread-1|12]获取某宝上 Iphone13的价格完成: 5199
05:42:25.291[pool-1-thread-2|13]获取某东上 Iphone13的优惠
05:42:25.291[pool-1-thread-3|14]获取某夕夕上 Iphone13的优惠
05:42:25.291[pool-1-thread-1|12]获取某宝上 Iphone13的优惠
05:42:26.294[pool-1-thread-2|13]获取某东上 Iphone13的优惠完成: -150
05:42:26.294[pool-1-thread-3|14]获取某夕夕上 Iphone13的优惠完成: -5300
05:42:26.294[pool-1-thread-1|12]获取某宝上 Iphone13的优惠完成: -200
05:42:26.294[pool-1-thread-2|13]某东最终价格计算完成:5149
05:42:26.294[pool-1-thread-3|14]某夕夕最终价格计算完成:99
05:42:26.294[pool-1-thread-1|12]某宝最终价格计算完成:4999
获取最优价格信息:【平台:某夕夕, 原价:5399, 折扣:0, 实付价:99】
-----执行耗时: 2119ms  ------

结果与第一种实现方式一致,但是接口总耗时从6s下降到了2s,效果还是很显著的。但是,是否还能再压缩一些呢?

基于上面按照平台拆分并行处理的思路继续推进,我们可以看出每个平台内的处理逻辑其实可以分为3个主要步骤:

  1. 获取原始价格(耗时操作)
  2. 获取折扣优惠(耗时操作)
  3. 得到原始价格和折扣优惠之后,计算实付价格

这3个步骤中,第1、2两个耗时操作也是相对独立的,如果也能并行处理的话,响应时长上应该又会缩短一些,即如下的处理流程:

 

我们当然可以继续使用上面提到的线程池+Future的方式,但Future在应对并行结果组合以及后续处理等方面显得力不从心,弊端明显:

代码写起来会非常拖沓:先封装Callable函数放到线程池中去执行查询操作,然后分三组阻塞等待

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值