尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Java CompletableFuture实现高并发数据库查询与结果汇总实战

Java CompletableFuture实现高并发数据库查询与结果汇总实战 1. 项目概述高并发场景下的数据汇总挑战在后台服务开发中我们经常会遇到这样的需求需要从数据库的多个表或多个数据源中并行查询数据然后将这些分散的结果进行汇总、计算最终形成一个统一的视图或报表返回给前端。如果采用传统的串行查询方式一个接一个地执行SQL那么总耗时将是所有查询耗时的累加在数据量大或查询复杂时用户体验会急剧下降接口响应时间可能长达数秒甚至更久。“并发查询数据库并做汇总处理”这个项目正是为了解决这一性能瓶颈。其核心思路是利用多线程技术将多个独立的查询任务同时发起并行执行从而将总耗时压缩到最慢的那个查询任务的耗时附近。而“多线程CompletableFuture方式”则指明了我们实现这一目标所采用的具体技术栈使用Java的CompletableFuture这一强大的异步编程工具来优雅地管理和组合多个并发任务。我经历过不少从串行改造为并发的案例效果往往是立竿见影的。比如一个需要查询用户信息、订单列表、积分明细三个接口的首页串行时可能需要200ms 300ms 150ms 650ms而并发后总耗时可能只需要350ms取决于最慢的那个查询性能提升接近一倍。这对于高并发的互联网应用来说意义重大。本篇文章我将为你彻底拆解如何使用CompletableFuture实现安全、高效的数据库并发查询与汇总涵盖设计思路、核心代码、参数调优以及我趟过的那些坑。2. 核心设计思路与方案选型2.1 为什么是CompletableFuture实现并发查询你可能首先想到Thread、Runnable或者线程池ExecutorService配合Future。这些方案当然可行但CompletableFuture提供了更高级的抽象和更强大的功能尤其在任务组合与结果处理方面。链式编程与组合能力CompletableFuture允许你将多个异步任务以函数式的方式串联thenApply、并联thenCombine或组合allOf/anyOf代码可读性远胜于对多个Future对象进行手工get和判断。异常处理更优雅提供了exceptionally、handle等方法可以非常方便地在链式调用中处理单个任务的异常而不至于让整个并发块崩溃。灵活的回调机制你可以在任务完成时触发回调thenAccept无需主动调用阻塞的get()方法这对于事件驱动型编程更友好。与线程池无缝集成可以轻松指定任务在哪个线程池中运行方便进行资源隔离和精细化调优。相比于直接使用ExecutorService.submit()返回的FutureCompletableFuture更像是一个“可完成的、带有丰富操作符的异步计算结果容器”它让异步编程的代码变得清晰、简洁且健壮。2.2 整体架构设计一个健壮的并发查询汇总流程不能只是简单地把查询丢到线程池就完事了。我们需要考虑以下几个层面任务定义与拆分明确哪些查询是彼此独立、可以并行的。例如查询用户基本信息和查询用户最近订单这两者通常没有依赖关系可以并行。而查询订单详情和查询该订单的商品列表后者依赖前者的订单ID则不适合并行更适合链式调用。线程池资源管理绝不能为每次请求都创建新线程new Thread必须使用线程池。我们需要根据业务特点IO密集型还是CPU密集型来配置核心参数核心线程数、最大线程数、队列容量、拒绝策略。异步执行与结果封装使用CompletableFuture.supplyAsync()将每个查询任务提交到线程池。每个Future对象代表一个未完成的异步查询。多任务同步与结果汇聚使用CompletableFuture.allOf()等待所有并行任务完成然后统一提取各个任务的结果。结果聚合与异常处理将汇聚的多个结果按照业务逻辑进行合并、计算、转换。同时必须妥善处理单个任务查询失败的情况避免因一个接口超时或报错导致整个汇总结果失败。超时控制必须为整个并发查询操作设置总超时时间防止因某个慢查询拖死整个线程池资源。基于以上思路我们可以勾勒出如下核心代码骨架伪代码// 1. 定义线程池 ExecutorService executor CustomThreadPool.getExecutor(); // 2. 提交并发任务 CompletableFutureUserInfo futureUser CompletableFuture.supplyAsync(() - userDao.getById(userId), executor); CompletableFutureListOrder futureOrders CompletableFuture.supplyAsync(() - orderDao.listByUser(userId), executor); CompletableFutureBigDecimal futureBalance CompletableFuture.supplyAsync(() - accountService.getBalance(userId), executor); // 3. 等待所有任务完成并汇聚结果 CompletableFutureVoid allFutures CompletableFuture.allOf(futureUser, futureOrders, futureBalance); // 4. 组合结果并处理异常 CompletableFutureUserProfileVO resultFuture allFutures.thenApply(v - { // 此处所有任务已完成正常或异常可以安全地join或getNow UserInfo user futureUser.join(); // join()不会抛受检异常但会抛CompletionException ListOrder orders futureOrders.join(); BigDecimal balance futureBalance.join(); // 聚合数据组装VO return assembleUserProfile(user, orders, balance); }).exceptionally(ex - { // 统一异常处理例如记录日志返回兜底数据或特定错误VO log.error(并发查询用户资料失败, ex); return UserProfileVO.defaultVO(); }); // 5. 带超时地获取最终结果 try { UserProfileVO finalResult resultFuture.get(3, TimeUnit.SECONDS); return finalResult; } catch (TimeoutException e) { // 超时处理取消未完成的任务记录告警 resultFuture.cancel(true); log.warn(用户资料查询超时userId: {}, userId); return UserProfileVO.timeoutVO(); } catch (InterruptedException | ExecutionException e) { // 其他异常处理 log.error(获取结果异常, e); return UserProfileVO.errorVO(); }这个骨架涵盖了核心步骤但每个环节都有大量细节和陷阱我们将在后续章节逐一拆解。3. 核心细节解析与实操要点3.1 线程池的配置艺术线程池配置不当是并发编程中最常见的性能问题根源甚至会导致服务崩溃。对于数据库查询这类IO密集型任务线程等待数据库响应的时间远大于CPU计算时间。核心参数解析核心线程数 (corePoolSize)线程池长期维持的线程数。对于IO密集型任务可以设置得相对较高。一个常见的参考公式是CPU核数 * (1 IO等待时间 / CPU计算时间)。在数据库查询场景IO等待时间很长可以简单设置为CPU核数 * 2到CPU核数 * 10之间。例如4核机器可以设置corePoolSize20。但这不是绝对的需要压测。最大线程数 (maximumPoolSize)线程池允许创建的最大线程数。当队列满后新任务会创建新线程直到达到此值。可以设置得比核心线程数大一些例如corePoolSize * 2。但要注意线程过多会导致频繁的上下文切换反而降低性能。工作队列 (BlockingQueue)用于存放等待执行的任务。常用的有LinkedBlockingQueue无界队列可能导致内存溢出和ArrayBlockingQueue有界队列。关键抉择对于并发查询汇总我强烈推荐使用有界队列如new ArrayBlockingQueue(200)。这能提供一种反压机制当系统过载时队列快速填满会触发拒绝策略避免任务无限堆积导致内存溢出。拒绝策略 (RejectedExecutionHandler)当线程池和队列都满了如何处理新任务ThreadPoolExecutor提供了几种策略AbortPolicy默认直接抛出RejectedExecutionException。在Web服务中这通常是最佳选择它能快速失败让调用方如上游服务或负载均衡感知到系统压力从而触发熔断或降级。CallerRunsPolicy由调用者线程如Tomcat的HTTP线程自己执行任务。这会导致调用线程被阻塞可能拖垮整个Web容器慎用。DiscardPolicy/DiscardOldestPolicy静默丢弃任务。这会导致业务数据丢失不推荐。一个推荐的IO密集型线程池配置示例public class QueryThreadPool { private static final int CORE_POOL_SIZE Runtime.getRuntime().availableProcessors() * 5; // 假设4核则为20 private static final int MAX_POOL_SIZE CORE_POOL_SIZE * 2; // 40 private static final int QUEUE_CAPACITY 200; private static final long KEEP_ALIVE_TIME 60L; // 空闲线程存活时间 private static final ThreadPoolExecutor EXECUTOR new ThreadPoolExecutor( CORE_POOL_SIZE, MAX_POOL_SIZE, KEEP_ALIVE_TIME, TimeUnit.SECONDS, new ArrayBlockingQueue(QUEUE_CAPACITY), new ThreadFactoryBuilder().setNameFormat(query-pool-%d).build(), // 良好命名的线程 new ThreadPoolExecutor.AbortPolicy() // 快速失败 ); public static ThreadPoolExecutor getExecutor() { return EXECUTOR; } }注意务必为线程池设置一个有意义的名称通过ThreadFactory这样在排查问题如使用jstack查看线程堆栈时你能一眼就认出哪些线程是你的查询线程极大提升排查效率。3.2 CompletableFuture的核心API实战CompletableFuture的API非常丰富这里重点介绍在并发查询汇总场景下最常用的几个。启动异步任务supplyAsync/runAsyncsupplyAsync(SupplierU supplier, Executor executor)执行一个有返回值的任务是我们最常用的方法。务必传入自定义的线程池如果不传则会使用ForkJoinPool.commonPool()这是一个被整个JVM共享的池不适合处理可能阻塞的IO任务容易影响其他框架如并行流的性能。// 正确做法 CompletableFutureUser future CompletableFuture.supplyAsync(() - userDao.query(userId), queryExecutor); // 错误做法除非你明确知道自己在做什么 CompletableFutureUser future CompletableFuture.supplyAsync(() - userDao.query(userId));结果转换thenApply当上一个阶段完成后对其结果进行转换。注意thenApply默认会在完成上一个任务的同一个线程中执行如果转换操作很重可能会阻塞该线程。如果转换也是IO操作应使用thenApplyAsync并指定线程池。CompletableFutureString futureName futureUser.thenApply(user - user.getName());结果消费thenAccept/thenRunthenAccept消费结果但不返回新值thenRun不关心结果只执行一个动作。常用于日志记录、发送消息等副作用操作。双任务组合thenCombine当两个独立的Future都完成后对它们的结果进行合并处理。这比用allOf再手动join更简洁。CompletableFutureUser futureUser ...; CompletableFutureOrder futureOrder ...; CompletableFutureUserOrderDTO dtoFuture futureUser.thenCombine(futureOrder, (user, order) - new UserOrderDTO(user, order));多任务组合allOf/anyOfallOf(CompletableFuture?... cfs)等待所有传入的Future完成。它返回一个CompletableFutureVoid。这是实现“并发查询统一汇总”的关键。anyOf(...)等待任意一个Future完成。适用于“谁快用谁”的场景如多数据源竞速查询。异常处理exceptionally/handleexceptionally(FunctionThrowable, ? extends T fn)当链中发生异常时提供兜底值或恢复逻辑。它只捕获它前面步骤的异常。handle(BiFunction? super T, Throwable, ? extends U fn)无论成功还是异常都会被执行可以同时处理正常结果和异常。一个常见的陷阱在allOf().thenApply()中直接调用各个future.join()如果某个future已经异常完成例如数据库超时join()会立即抛出CompletionException导致整个聚合失败。更稳健的做法是先用handle或exceptionally处理每个future的异常确保它们都返回一个“结果”哪怕是兜底的空对象或错误标记然后再进行聚合。4. 完整实操流程与核心代码实现让我们通过一个完整的案例来串联所有知识点实现一个“用户驾驶舱”接口需要并发查询用户信息、订单列表、消息未读数和积分余额最后汇总成一个UserDashboardVO。4.1 步骤一定义线程池与数据访问层首先我们按照3.1节的建议定义一个专用的线程池。// ThreadPoolConfig.java Configuration public class ThreadPoolConfig { Bean(name dbQueryExecutor) public ThreadPoolExecutor dbQueryExecutor() { int corePoolSize Runtime.getRuntime().availableProcessors() * 5; int maxPoolSize corePoolSize * 2; ThreadPoolExecutor executor new ThreadPoolExecutor( corePoolSize, maxPoolSize, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue(200), new ThreadFactoryBuilder().setNameFormat(db-query-%d).build(), new ThreadPoolExecutor.AbortPolicy() ); // 允许核心线程超时回收避免长期空闲占用资源根据实际情况选择 executor.allowCoreThreadTimeOut(true); return executor; } }假设我们已有对应的Mapper或Dao类这里使用MyBatis Plus示例// UserMapper.java Mapper public interface UserMapper extends BaseMapperUser { User selectByUserId(Param(userId) Long userId); } // OrderMapper.java Mapper public interface OrderMapper extends BaseMapperOrder { ListOrder selectRecentOrders(Param(userId) Long userId, Param(limit) Integer limit); } // MessageMapper.java, AccountMapper.java 类似...4.2 步骤二Service层实现并发查询这是最核心的部分。我们将在一个Service方法中编排所有并发任务。// UserDashboardServiceImpl.java Service Slf4j public class UserDashboardServiceImpl implements UserDashboardService { Autowired private UserMapper userMapper; Autowired private OrderMapper orderMapper; Autowired private MessageMapper messageMapper; Autowired private AccountMapper accountMapper; Qualifier(dbQueryExecutor) // 注入我们配置的线程池 Autowired private ThreadPoolExecutor dbQueryExecutor; Override public UserDashboardVO getDashboard(Long userId) { // 1. 提交并发查询任务 CompletableFutureUser futureUser CompletableFuture.supplyAsync(() - { log.debug(开始查询用户信息线程: {}, Thread.currentThread().getName()); return userMapper.selectByUserId(userId); }, dbQueryExecutor) .exceptionally(ex - { log.error(查询用户信息失败, userId: {}, userId, ex); return null; // 返回null作为兜底后续聚合时需判断 }); CompletableFutureListOrder futureOrders CompletableFuture.supplyAsync(() - { log.debug(开始查询订单列表); return orderMapper.selectRecentOrders(userId, 10); }, dbQueryExecutor) .exceptionally(ex - { log.error(查询订单列表失败, ex); return Collections.emptyList(); // 返回空列表兜底 }); CompletableFutureLong futureUnreadMsg CompletableFuture.supplyAsync(() - { log.debug(开始查询未读消息数); return messageMapper.countUnreadByUser(userId); }, dbQueryExecutor) .exceptionally(ex - { log.error(查询未读消息数失败, ex); return 0L; // 返回0兜底 }); CompletableFutureBigDecimal futureBalance CompletableFuture.supplyAsync(() - { log.debug(开始查询账户余额); return accountMapper.selectBalanceByUser(userId); }, dbQueryExecutor) .exceptionally(ex - { log.error(查询账户余额失败, ex); return BigDecimal.ZERO; // 返回0余额兜底 }); // 2. 使用allOf等待所有任务完成 CompletableFutureVoid allDoneFuture CompletableFuture.allOf( futureUser, futureOrders, futureUnreadMsg, futureBalance ); // 3. 聚合结果 CompletableFutureUserDashboardVO resultFuture allDoneFuture.thenApply(v - { // 此时所有future都已经完成正常或已通过exceptionally处理 User user futureUser.join(); // 因为exceptionally处理过这里join不会抛异常但可能是null ListOrder orders futureOrders.join(); // 已经是空列表或正常列表 Long unreadCount futureUnreadMsg.join(); BigDecimal balance futureBalance.join(); // 构建视图对象 UserDashboardVO vo new UserDashboardVO(); if (user ! null) { vo.setUserName(user.getName()); vo.setAvatar(user.getAvatar()); } else { vo.setUserName(用户信息获取失败); // 可以设置一个默认头像 } vo.setRecentOrders(orders.stream().map(this::convertToOrderDTO).collect(Collectors.toList())); vo.setUnreadMessageCount(unreadCount); vo.setAccountBalance(balance); vo.setLastUpdateTime(LocalDateTime.now()); return vo; }); // 4. 同步等待最终结果带超时 try { // 设置总超时时间例如2秒 return resultFuture.get(2, TimeUnit.SECONDS); } catch (TimeoutException e) { log.warn(用户驾驶舱查询超时userId: {}, userId, e); // 取消未完成的任务虽然allOf已等完但这里以防万一 resultFuture.cancel(true); // 返回一个超时状态的VO或抛出业务异常 return UserDashboardVO.timeoutDashboard(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); // 恢复中断状态 log.error(查询被中断, e); return UserDashboardVO.errorDashboard(); } catch (ExecutionException e) { log.error(查询执行过程发生异常, e); return UserDashboardVO.errorDashboard(); } } private OrderDTO convertToOrderDTO(Order order) { // 简单的转换逻辑 OrderDTO dto new OrderDTO(); dto.setOrderId(order.getId()); dto.setAmount(order.getAmount()); // ... 其他字段 return dto; } }4.3 步骤三优化与进阶技巧上面的代码已经可以工作但还有优化空间。技巧1使用thenApplyAsync避免阻塞回调线程在allDoneFuture.thenApply中聚合逻辑是在执行最后一个完成任务的线程中运行的。如果聚合逻辑很重可能会意外阻塞这个宝贵的IO线程。可以改用thenApplyAsync将聚合操作也提交到线程池可以是另一个CPU密集型的池。CompletableFutureUserDashboardVO resultFuture allDoneFuture.thenApplyAsync(v - { // 聚合逻辑... }, cpuBoundExecutor); // 使用另一个适合CPU计算的线程池技巧2更精细的异常处理与部分失败前面的例子中单个查询失败会返回兜底值整个聚合流程继续。但有时我们希望知道哪些部分失败了。可以自定义一个容器对象来包装结果和状态。Data public class QueryResultT { private boolean success; private T data; private String errorMsg; public static T QueryResultT success(T data) { QueryResultT result new QueryResult(); result.setSuccess(true); result.setData(data); return result; } public static T QueryResultT fail(String errorMsg) { QueryResultT result new QueryResult(); result.setSuccess(false); result.setErrorMsg(errorMsg); return result; } } // 在supplyAsync中使用handle返回统一结果包装 CompletableFutureQueryResultUser futureUser CompletableFuture.supplyAsync(() - userMapper.selectByUserId(userId), dbQueryExecutor) .handle((user, ex) - { if (ex ! null) { log.error(查询用户失败, ex); return QueryResult.fail(用户查询异常: ex.getMessage()); } return QueryResult.success(user); }); // 后续聚合时可以检查每个QueryResult的success状态进行更灵活的组装。技巧3利用CompletableFuture的依赖组合如果查询间有轻微依赖可以用链式调用。例如先查用户再用其部门ID去查部门信息这两个查询可以串联而非并行。CompletableFutureDepartment futureDept futureUser .thenApplyAsync(user - departmentMapper.selectById(user.getDeptId()), dbQueryExecutor) .exceptionally(ex - { log.error(查询部门信息失败, ex); return Department.defaultDept(); });5. 常见问题、性能陷阱与排查实录在实际生产中使用这套模式我踩过不少坑这里总结出来帮你避雷。5.1 问题一线程池配置不当导致服务雪崩现象在流量高峰时段接口响应时间飙升最终大量请求超时错误日志里出现大量RejectedExecutionException如果用了AbortPolicy数据库连接池也可能被打满。根因分析队列无界使用了LinkedBlockingQueue默认无界或设置了过大的队列容量。任务堆积在队列中等待时间过长上游已经超时但线程池还在处理老旧任务。最大线程数设置过低并发查询任务数超过maximumPoolSizequeueCapacity导致触发拒绝策略。数据库连接池成为瓶颈线程池开得很大但数据库连接池如HikariCP的maximumPoolSize很小导致大量线程在等待获取数据库连接造成假死。解决方案使用有界队列ArrayBlockingQueue容量根据业务可接受的平均等待时间和吞吐量设定如100-500。设置合理的拒绝策略坚持使用AbortPolicy让调用方快速失败。同时必须在全局或Controller层做好熔断降级。例如当捕获到RejectedExecutionException时直接返回一个缓存数据或简化的默认结果。线程池与连接池联动调优确保线程池最大线程数 数据库连接池最大连接数。通常连接池大小应略大于线程池最大线程数以应对连接偶尔失效等场景。例如线程池maxPoolSize40数据库连接池可以设为50。监控与告警对线程池的活跃线程数、队列大小进行监控。当队列使用率持续超过80%或活跃线程数持续高于核心线程数时发出告警。5.2 问题二未做超时控制慢查询拖死整个服务现象某个依赖的数据库查询突然变慢如全表扫描由于没有超时设置执行该查询的线程被长期占用。线程池里的线程被一个个拖住最终所有线程都在等待这个慢查询新的请求无法得到处理。根因分析CompletableFuture.get()或join()是无限期等待的。数据库查询本身也没有设置语句超时。解决方案为每个数据库查询设置超时在MyBatis中可以在Mapper语句上配置timeout属性或在数据源层面配置查询超时。为CompletableFuture组合设置总超时如4.2节代码所示使用resultFuture.get(timeout, timeUnit)。使用orTimeout方法Java 9CompletableFuture提供了更优雅的超时控制。CompletableFutureUser future CompletableFuture.supplyAsync(() - userDao.query(userId), executor) .orTimeout(1, TimeUnit.SECONDS) // 1秒超时 .exceptionally(ex - { if (ex instanceof TimeoutException) { log.warn(查询用户超时); return User.defaultUser(); } // 处理其他异常... return null; });为整个线程池的任务设置执行超时这是一个更复杂但更彻底的方法可以通过包装Runnable或使用Future.get(timeout)来实现但通常使用上述查询级和Future级的超时已经足够。5.3 问题三异常被“吞没”排查困难现象某个查询失败了但最终返回的结果里没有明显错误日志里也找不到异常堆栈。根因分析在supplyAsync的lambda表达式中直接吞了异常如try-catch后只打印日志。使用了exceptionally但只返回了兜底值没有将异常向上传递或记录足够上下文。在allOf().thenApply()里调用future.join()如果该future已异常完成join()会抛出CompletionException但如果没有被外层捕获可能在线程池的线程中默默结束。解决方案始终记录带有足够上下文的错误日志在exceptionally或handle中记录用户ID、查询类型等关键信息。区分业务异常和系统异常业务异常如用户不存在可以转换为兜底值系统异常如网络超时、数据库连接失败除了记录日志还应考虑向上抛出或转换为一个特定的错误结果让聚合层知晓是部分失败。使用自定义的QueryResult包装如4.3节技巧2所示它能清晰携带成功/失败状态和错误信息。全局的线程池异常处理器为线程池设置UncaughtExceptionHandler作为最后一道防线捕获那些未被Future处理的异常。ThreadFactory factory new ThreadFactoryBuilder() .setNameFormat(db-query-%d) .setUncaughtExceptionHandler((t, e) - log.error(线程池线程未捕获异常: {}, t.getName(), e)) .build();5.4 问题四上下文信息如MDC丢失现象在异步线程中无法获取到在父线程如HTTP请求线程中设置的TraceID、用户ID等MDCMapped Diagnostic Context信息导致日志链路追踪断裂。根因分析CompletableFuture默认不会传递线程上下文。解决方案手动传递上下文。可以在提交任务前捕获在任务执行时恢复。// 捕获当前线程的上下文以SLF4J MDC为例 MapString, String contextMap MDC.getCopyOfContextMap(); CompletableFutureUser futureUser CompletableFuture.supplyAsync(() - { // 恢复上下文 if (contextMap ! null) { MDC.setContextMap(contextMap); } try { return userMapper.selectByUserId(userId); } finally { // 清理避免污染线程池线程 MDC.clear(); } }, dbQueryExecutor);对于Spring项目可以考虑使用TaskDecorator接口来装饰线程池的Runnable自动完成上下文的传递。5.5 性能监控与调优建议上线后如何知道你的并发优化是否有效需要关注哪些指标接口平均响应时间ART与TP99对比优化前后看下降是否明显。关注TP99确保绝大多数请求受益。线程池监控activeCount活跃线程数。长期接近maximumPoolSize说明线程池可能偏小或任务执行太慢。queueSize队列中等待的任务数。长期大于0说明任务到达速率高于处理速率。completedTaskCount已完成任务数。可以计算吞吐量。数据库监控观察数据库的QPS、连接数、慢查询。并发查询会增加数据库的瞬时压力确保数据库能够承受。调优步骤基准测试在预发布环境模拟生产流量进行压测。调整核心参数先调整corePoolSize和maximumPoolSize观察线程池活跃度和队列长度变化。找到吞吐量最高、响应时间最短的平衡点。调整队列容量队列太小会导致频繁触发拒绝策略太大则增加延迟。通常设置为maximumPoolSize的2-5倍是一个起点。全链路压测在业务低峰期进行全链路压测观察从网关到服务到数据库的整体表现。最后记住一个原则并发不是银弹。它用复杂度换取了性能。在引入并发前先检查是否可以通过优化单次查询如加索引、优化SQL、引入缓存等更简单的方式解决问题。当确实需要并行多个独立IO操作时CompletableFuture线程池的方案才是一个强大而优雅的选择。
返回列表