1.原始代码逻辑
前端传递一个快递单号的集合,后端校验当前用户是否有权限操作->校验快递单号是否已存在->调用第三方API查询快递数据->将调用结果保存并返回展示
问题:功能限制每次最多可以传十个快递单号,后台最初是通过循环调用的方式去查询API,API的响应时间是200ms~2s,也就是说服务商处已经同步有快递数据的情况下可以在两秒左右完成查询,如果没有数据的话则时间会超过十秒,严重影响工作人员使用。
2.优化一
1.将快递单号用in的方式去数据库查询,然后拿到一个以入库的快递数据集合,减少对数据库的查询
2.配置RestTemplate整合HttpClient连接池
3.将串行的API查询转为并行查询。
实现方式:遍历快递单号集合创建异步线程去调用api,使用java的同步CountDownLatch使线程结果同步完成,再返回结果的集合即可
LocalDateTime now = LocalDateTime.now();
List<CourierBatchInDTO> batchInDTOS = new LinkedList<>();
CountDownLatch countDownLatch = new CountDownLatch(batchParam.size());
for (ExpressScanParam param : batchParam) {
taskExecutor.execute(()->courierInByBatch(userId, siteNameMap, user, expressMap, now, batchInDTOS, param,countDownLatch));
}
try {
//保证之前的所有的线程都执行完成,才会走下面的;
// 这样就可以在下面拿到所有线程执行完的集合结果
countDownLatch.await();
} catch (Exception e) {
log.error("阻塞异常:"+e.getMessage());
throw new BusinessException("系统入库异常,请联系管理员!");
}
return batchInDTOS;
最初的优化方案就是新建一个返回结果对像集合,再遍历单号集合异步调用线程,最后将结果对象集合返回给前端,测试时发现因为每个线程的执行时间不一致,导致其实只拿到了最先执行的线程的结果,所以需要使用CountDownLatch来控制线程同步完成。
参考文章:https://baijiahao.baidu.com/s?id=1648081908157830415&wfr=spider&for=pc
3.优化二
逛B站时看到一个视频介绍如何写一个并行调用模板,其中就介绍了使用CompletionService一边生成任务一边获取返回值,然后就想可以使用这样的方式来处理并行调用API的问题。
ExecutorCompletionService<CourierBatchInDTO> completionService = new ExecutorCompletionService<>(taskExecutor);
for (ExpressScanParam param : batchParam) {
completionService.submit(()->courierInByBatch(userId, siteNameMap, user, expressMap, now, param));
}
try {
for (ExpressScanParam param : batchParam) {
batchInDTOS.add(completionService.poll(3L, TimeUnit.SECONDS).get());
}
} catch (Exception e) {
log.error("阻塞异常:"+e.getMessage());
throw new BusinessException("系统入库异常,请联系管理员!");
}
return batchInDTOS;
4.优化三
使用jdk8中提供的并行流parallelStream来实现
List<CourierBatchInDTO> batchInDTOS = batchParam.parallelStream()
.map(param -> courierInByBatch(userId, siteNameMap, user, expressMap, now, param))
.collect(Collectors.toList());
注意:
使用parallelStream
和CountDownLatch
都可以实现并发处理,但它们之间有一些重要的区别:
-
API和编程模型不同:
parallelStream
是Java集合框架提供的一种高级API,它使集合内部的元素能够并行处理,而不需要程序员显式创建和管理线程。它更为抽象和易于使用,通常适用于处理集合内的元素。CountDownLatch
是Java的多线程编程工具,它是显式创建的对象,用于协调多个线程的执行。它需要程序员自行编写线程管理和控制逻辑,因此更为底层和灵活,可用于更复杂的并发控制场景。
-
适用场景:
parallelStream
适用于集合内元素的并行处理,例如在列表、集合或数组上执行相同的操作。它是一种函数式编程风格,适合简单的数据处理任务。CountDownLatch
适用于更复杂的并发场景,如等待多个线程完成后再执行某些操作,或者在多个线程之间进行精细的协调。它通常用于多个线程之间的协同工作。
-
线程管理:
parallelStream
隐藏了线程管理的复杂性,Java会自动创建和管理线程池,你只需要提供操作的逻辑。这对于简单的数据处理任务很方便。CountDownLatch
需要显式创建和管理线程,你需要自行创建线程、启动线程并在需要时等待线程完成。
-
线程控制:
parallelStream
不提供显式的线程控制机制。你不能轻松实现等待多个任务完成后继续执行的逻辑。CountDownLatch
允许你明确地等待多个线程完成(通过countDown
和await
方法),从而实现更复杂的线程协作。
总之,选择使用parallelStream
还是CountDownLatch
取决于你的具体需求。如果你只需简单地并行处理集合中的元素,那么parallelStream
可能更方便。但如果你需要更精细的线程协调和控制,或者需要等待多个任务完成后再执行其他操作,那么CountDownLatch
可能更适合。