原创 不码不疯魔 不码不疯魔 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】即可获取所有资料,大量的商用级方案已经更新,记得关注,免费领取,标记星号哟!!!