接口整体性能至少提升60%,基于CompletableFuture完成并发编排,解决热门数据耗时长的问题

原创 不码不疯魔 不码不疯魔 2023-09-12 22:49 发表于四川

收录于合集

#CompletableFuture1个

#并发编排1个

#接口性能优化1个

#高频面试题11个

20

23

不疯魔不成活,大家好呀,我是科哥,江湖ID 不码不疯魔

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

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

好啦,话不多说,接下来就让我们一起先看看3个实际场景:

1.1美团-外卖订单场景

随着订单量的持续上升,美团外卖各系统服务面临的压力也越来越大。作为外卖链路的核心环节,商家端提供了商家接单、配送等一系列核心功能,业务对系统吞吐量的要求也越来越高。而商家端API服务是流量入口,所有商家端流量都会由其调度、聚合,对外面向商家提供功能接口,对内调度各个下游服务获取数据进行聚合,具有鲜明的I/O密集型(I/O Bound)特点。在当前日订单规模已达千万级的情况下,使用同步加载方式的弊端逐渐显现。

从各个服务获取数据最常见的是同步调用,如下图所示:

图片

在同步调用的场景下,接口耗时长、性能差,接口响应时长T > T1+T2+T3+……+Tn

1.2购物系统-全网比价场景

实现一个全网比价服务,比如可以从某宝、某东、某多多去获取某个商品的价格、优惠金额,并计算出实际付款金额,最终返回价格最优的平台与价格信息。这里假定每个平台获取原价格与优惠券的接口已经实现、且都是需要调用HTTP接口查询的耗时操作,接口每个耗时1s左右,等待6s才返回结果。所有的环节都是串行的,每个环节耗时加到一起,接口总耗时肯定很长。

梳理一下实现思路:

图片

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

1.3金融系统-还款扣款场景

金融项目中,有这样一个场景,我要们发起一个用户主动还款之后,需要把在途的扣款单全部暂停,但是因为用户选择还款单据可能比较多,而且暂停是调用外部资金打款系统的接口实现的,这个地方如果一条一条执行就很慢,但是如果起多线程执行的话,我就没办法知道他们每一个暂定的返回结果。我想可以实现一个这样的功能:多线程去执行暂停动作,如果都成功了,那么就推进我的主动扣款后续流程,如果暂停有任何一个接口调用失败了,那么先不推进后续流程,等下次重试。

重点掌握:CompletableFuture完成并发编排

1. 采用传统方式解决:耗时6s

2. 尝试用线程池解决:6s优化到2s

3采用CompletableFuture解决:6s优化到1s

    注意:大量干货文章,有具体的商业对接案例Word,记得关注,免费领取,标记星号每日更多干货分享哟!!!

 

01

采用传统方式解决:耗时6s

试想一下《购物系统-全网比价场景》,假如你在操作某购物APP查询的时候,等待6s才返回结果,估计会直接把APP给卸载了吧?

图片

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

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

/**
 * 传统方式【串行】获取多个平台比价信息得到最低价格平台
 */
@Override
public PriceResult findCheapestPlatAndPriceBySerial(String productName) {
    // 获取某宝的价格以及优惠
    PriceResult mouBaoPrice = computeRealPrice(HttpRequestMock.getMouBaoPrice(productName),
            HttpRequestMock.getMouBaoDiscounts(productName));
    // 获取某东的价格以及优惠
    PriceResult mouDongPrice = computeRealPrice(HttpRequestMock.getMouDongPrice(productName),
            HttpRequestMock.getMouDongDiscounts(productName));
    // 获取某多多的价格以及优惠
    PriceResult mouDuoDuoPrice = computeRealPrice(HttpRequestMock.getMouDuoDuoPrice(productName),
            HttpRequestMock.getMouDuoDuoDiscounts(productName));
    // 计算并选出实际价格最低的平台
    return Stream.of(mouBaoPrice, mouDongPrice, mouDuoDuoPrice).
            min(Comparator.comparingDouble(PriceResult::getRealPrice))
            .get();
}

运行测试结果如下:

21:55:05.199[main|1]获取某宝上 HUAWEI Mate 60的价格完成:5888.0
21:55:06.200[main|1]获取某宝上 HUAWEI Mate 60的优惠完成: -200
21:55:06.200[main|1]某宝最终价格计算完成:5688.0
21:55:07.215[main|1]获取某东上 HUAWEI Mate 60的价格完成:5999.0
21:55:08.230[main|1]获取某东上 HUAWEI Mate 60的优惠完成: -150
21:55:08.230[main|1]某东最终价格计算完成:5849.0
21:55:09.245[main|1]获取某多多上 HUAWEI Mate 60的价格完成:5399.0
21:55:10.261[main|1]获取某多多上 HUAWEI Mate 60的优惠完成: -500
21:55:10.261[main|1]某多多最终价格计算完成:4899.0
获取最优价格信息:【平台:某多多, 原价:5399.0, 折扣:0.0, 实付价:4899.0】
----- 执行耗时: 6068ms  ------

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

02

尝试用线程池解决:6s优化到2s

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

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

图片

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

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

private ExecutorService threadPool = Executors.newFixedThreadPool(6);
/**
 * 传统方式通过线程池来增加并发
 */
@Override
public PriceResult findCheapestPlatAndPriceByThreadPool(String productName) {
    Future<PriceResult> mouBaoFuture =
            threadPool.submit(() -> computeRealPrice(HttpRequestMock.getMouBaoPrice(productName),
                    HttpRequestMock.getMouBaoDiscounts(productName)));
    Future<PriceResult> mouDongFuture =
            threadPool.submit(() -> computeRealPrice(HttpRequestMock.getMouDongPrice(productName),
                    HttpRequestMock.getMouDongDiscounts(productName)));
    Future<PriceResult> mouDuoDuoFuture =
            threadPool.submit(() -> computeRealPrice(HttpRequestMock.getMouDuoDuoPrice(productName),
                    HttpRequestMock.getMouDuoDuoDiscounts(productName)));
    // 等待所有线程结果都处理完成,然后从结果中计算出最低价
    return Stream.of(mouBaoFuture, mouDongFuture, mouDuoDuoFuture)
            .map(priceResultFuture -> {
                try {
                    return priceResultFuture.get(5L, TimeUnit.SECONDS);
                } catch (Exception e) {
                    return null;
                }
            })
            .filter(Objects::nonNull)
            .min(Comparator.comparingDouble(PriceResult::getRealPrice))
            .get();
}

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

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

21:58:03.736[pool-1-thread-1|16]获取某宝上 HUAWEI Mate 60的价格完成:5888.0
21:58:03.736[pool-1-thread-3|18]获取某多多上 HUAWEI Mate 60的价格完成:5399.0
21:58:03.736[pool-1-thread-2|17]获取某东上 HUAWEI Mate 60的价格完成:5999.0
21:58:04.746[pool-1-thread-2|17]获取某东上 HUAWEI Mate 60的优惠完成: -150
21:58:04.746[pool-1-thread-3|18]获取某多多上 HUAWEI Mate 60的优惠完成: -500
21:58:04.746[pool-1-thread-1|16]获取某宝上 HUAWEI Mate 60的优惠完成: -200
21:58:04.746[pool-1-thread-2|17]某东最终价格计算完成:5849.0
21:58:04.746[pool-1-thread-3|18]某多多最终价格计算完成:4899.0
21:58:04.746[pool-1-thread-1|16]某宝最终价格计算完成:5688.0
获取最优价格信息:【平台:某多多, 原价:5399.0, 折扣:0.0, 实付价:4899.0】
----- 执行耗时: 2022ms  ------

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

03

用CompletableFuture解决:6s优化到1s

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

   获取原始价格(耗时操作)

   获取折扣优惠(耗时操作)

   得到原始价格和折扣优惠之后,计算实付价格

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

图片

我们当然可以继续使用上面提到的线程池+Future的方式,但Future在应对并行结果组合以及后续处理等方面显得力不从心,弊端明显:代码写起来会非常拖沓:先封装Callable函数放到线程池中去执行查询操作,然后分三组阻塞等待结果并计算出各自结果,最后再阻塞等待价格计算完成后汇总得到最终结果。

说到这里呢,就需要我们新的主人公CompletableFuture登场了,通过它我们可以很轻松的来完成任务的并行处理,以及各个并行任务结果之间的组合再处理等操作。我们使用CompletableFuture编写实现代码如下:

/**
 * CompletableFuture并行处理的模式
 */
@Override
public PriceResult findCheapestPlatAndPriceByParallel(String productName) {
    // 获取并计算某宝的最终价格
    CompletableFuture<PriceResult> mouBao =
            CompletableFuture.supplyAsync(() -> HttpRequestMock.getMouBaoPrice(productName))
                    .thenCombine(CompletableFuture.supplyAsync(() -> HttpRequestMock.getMouBaoDiscounts(productName)),
                            this::computeRealPrice);
    // 获取并计算某东的最终价格
    CompletableFuture<PriceResult> mouDong =
            CompletableFuture.supplyAsync(() -> HttpRequestMock.getMouDongPrice(productName))
                    .thenCombine(CompletableFuture.supplyAsync(() -> HttpRequestMock.getMouDongDiscounts(productName)),
                            this::computeRealPrice);
    // 获取并计算某多多的最终价格
    CompletableFuture<PriceResult> mouDuoDuo =
            CompletableFuture.supplyAsync(() -> HttpRequestMock.getMouDuoDuoPrice(productName))
                    .thenCombine(CompletableFuture.supplyAsync(() -> HttpRequestMock.getMouDuoDuoDiscounts(productName)),
                            this::computeRealPrice);

    // 排序并获取最低价格
    return Stream.of(mouBao, mouDong, mouDuoDuo)
            .map(CompletableFuture::join)
            .sorted(Comparator.comparingDouble(PriceResult::getRealPrice))
            .findFirst()
            .get();
}

看下执行结果符合预期,而接口耗时则降到了1s(因为我们依赖的每一个查询实际操作的接口耗时都是模拟的1s,所以这个结果已经算是此复合接口能达到的极限值了)

22:02:43.054[ForkJoinPool.commonPool-worker-6|21]获取某多多上 HUAWEI Mate 60的优惠完成: -500
22:02:43.054[ForkJoinPool.commonPool-worker-3|18]获取某东上 HUAWEI Mate 60的价格完成:5999.0
22:02:43.054[ForkJoinPool.commonPool-worker-2|17]获取某宝上 HUAWEI Mate 60的优惠完成: -200
22:02:43.054[ForkJoinPool.commonPool-worker-5|20]获取某多多上 HUAWEI Mate 60的价格完成:5399.0
22:02:43.054[ForkJoinPool.commonPool-worker-1|16]获取某宝上 HUAWEI Mate 60的价格完成:5888.0
22:02:43.054[ForkJoinPool.commonPool-worker-4|19]获取某东上 HUAWEI Mate 60的优惠完成: -150
22:02:43.054[ForkJoinPool.commonPool-worker-5|20]某多多最终价格计算完成:4899.0
22:02:43.054[ForkJoinPool.commonPool-worker-1|16]某宝最终价格计算完成:5688.0
22:02:43.054[ForkJoinPool.commonPool-worker-4|19]某东最终价格计算完成:5849.0
获取最优价格信息:【平台:某多多, 原价:5399.0, 折扣:0.0, 实付价:4899.0】
----- 执行耗时: 1019ms  ------

好啦,大家应该能够看出来串行并行处理逻辑的区别、以及并行处理逻辑的实现策略了吧?这里我们应该也可以看出CompletableFuture在应对并行处理场景下的强大优势。

04

总结:CompletableFuture底层原理分析

CompletableFuture是ava8中引入的一个新特性,它提供了一种简单的方法来实现异步编程和任务组合。他的底层实现主要涉及到了几个重要的技术手段,如Completion链式异步处理、事件驱区动ForkJoinPool线程池、以及CountDownLatch控制计算状态、通过CompletionException捕获异常等。

CompletableFuture内部采用了一种链式的结构来处理异步计算的结果,每个CompletableFuture都有一个与之关联的Completion链,它可以包含多个Completion阶段,每个阶段都代表一个异步操作,并且可以指定它所依赖的前一个阶段的计算结果。(在CompletableFuture类中,定义了一个内部类Completion,它表示Completion链的一个阶段,其中包含了前一个阶段的计算结果、下一个阶段的计算操作以及执行计算操作的线程池等信息。)

CompletableFuture还使用了一种事件驱动的机制来处理异步计算的完成事件。在一CompletableFuture对象上注册的Completion阶段完成后,它会触发一个完成事件,然后CompletableFuture对象会执行与之关联的下一个Completion阶段。

CompletableFuture的异步计算是通过线程池来实现的。CompletableFuture在内部使用了一个ForkJoinPool线程池来执行异步任务。当我们创建一个CompletableFuture对象时,它会在内部创建一个任务,并提交到ForkJoinPool中去执行。

在CompletableFuture的异步计算过程中,会先执行前一个阶段的计算,然后将计算结果传递给下一个阶段。在执行计算过程中,会通过CompletableFuture内部的CountDownLatch来控制计算的完成状态,并通过CompletableFuture内部的CompletionException来捕获计算过程中出现的异常。

收获

商业案例源码+文档资料

图片

图片

商业案例源码+笔记文档,怎么获取?

点击下方公众号进入关注,后台回复【CompletableFuture】即可获取所有资料,大量的商用级方案已经更新,记得关注,免费领取,标记星号哟!!!

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值