大家好,又见面啦。
在项目开发中,后端服务对外提供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个主要步骤:
- 获取原始价格(耗时操作)
- 获取折扣优惠(耗时操作)
- 得到原始价格和折扣优惠之后,计算实付价格
这3个步骤中,第1、2两个耗时操作也是相对独立的,如果也能并行处理的话,响应时长上应该又会缩短一些,即如下的处理流程:
我们当然可以继续使用上面提到的线程池+Future
的方式,但Future
在应对并行结果组合以及后续处理等方面显得力不从心,弊端明显:
代码写起来会非常拖沓:先封装
Callable
函数放到线程池中去执行查询操作,然后分三组阻塞等待