ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

CompletableFuture实战:从Future到异步编排,线程池与异常处理全解析

CompletableFuture实战:从Future到异步编排,线程池与异常处理全解析 做后端这些年我遇到最多的一种“并发假象”是代码里创建了线程池、提交了任务看起来是异步了但实际上所有线程都堵在future.get()上等待结果和同步调用没有任何本质区别。真正让我下定决心全面使用 CompletableFuture 的是一次订单聚合接口的改造原来串行调用四个服务平均耗时 220ms优化后并行调用单次耗时稳定在 70ms 左右整个链路的异步编排代码写起来像流水线一样清晰。CompletableFuture 作为 Java 8 引入的异步编程工具最大的价值不光是帮你少写线程管理代码而是把“任务编排”这件事从手写状态机里解放了出来。这篇内容适合两类人一类是刚接触异步编程想搞清楚supplyAsync、thenApply、allOf这些 API 到底怎么用、用在哪另一类是已经在项目里用上了 CompletableFuture但被线程池策略、异常吞掉、超时失效这些问题坑过的同学。我会从实际项目出发把 API 原理、编排实战、线程池选型、异常处理一次讲透顺便聊几个生产环境里非常容易踩的坑。如果你接触过 Python 的 asyncio 或者 JavaScript 的 Promise你会发现异步编程的编排思想在不同语言里是相通的CompletableFuture 只是把这种思想用类型系统表达得更严格、更组合化。1. 为什么 CompletableFuture 能成为异步编排的默认答案1.1 Future 接口的三个硬伤在 CompletableFuture 出现之前Java 并发编程里做异步任务用的是 Future 接口配合 ExecutorService 提交任务。你先看一段最典型的旧写法ExecutorService executor Executors.newFixedThreadPool(4); FutureString userFuture executor.submit(() - { return userService.getUserName(userId); }); String userName userFuture.get(3, TimeUnit.SECONDS); // 阻塞等待这段代码看着是异步提交但get()一调当前线程就被挂起了异步的效果等于零。如果你想同时发起多个任务、然后用最快返回的那个结果或者等全部完成后统一处理Future 就变得非常难用。具体痛点有三个get()会阻塞线程而且一旦设置超时时间还要处理TimeoutException超时后任务其实还在后台跑线程资源白白占用。isDone()可以轮询任务是否完成但轮询本质上是一种空转白白消耗 CPU也增加了代码复杂度。多个 Future 之间无法组合。任务A的结果要传给任务B任务B的结果要跟任务C的结果合并你只能手动get()完再提交下一个一旦依赖链条变长代码就会退化成一段又一段的阻塞等待。我见过很多“号称异步”的代码点进去一看全是这种写法。核心问题不是开发者不会用并发工具而是 Future 这个抽象本身没有提供“编排”能力。1.2 CompletionStage把异步任务变成可组合的流水线CompletableFuture 在 Java 8 中出现它实现了两个接口Future和CompletionStage。CompletionStage这名字起得很准确它代表一个异步计算流程中的“阶段”每个阶段可以接收上一个阶段的结果转换后再输出给下一个阶段。打个比方这就好比工厂里的流水线每个工位只做一件事做完把半成品交给下一个工位有些工位可以并联有些工位可以设置“哪一个先完成就用哪一个”。你不需要自己在每个工位之间跑来跑去只需要把流水线搭好物料进去成品出来。CompletableFuture正是这套流水线的实现。它提供的不只是get()拿结果而是一整套方法用来表达任务之间串行依赖thenApply、thenCompose多个任务并行合并thenCombine、allOf多个任务竞争取最快applyToEither、anyOf异常兜底与恢复exceptionally、handle这些方法在英文里都以“then”开头含义就是一个阶段之后接另一个阶段。理解这一点再去看那几十个方法名就不会慌了它们全是在描述“当前阶段完成之后下一步怎么走”。很多初学者打开 CompletableFuture 的 API 文档看到四十多个方法直接劝退但只要你脑子里有“流水线”这个模型方法再多也能按功能分门别类。2. 核心 API 拆解从起点到回调整条链路应该怎么记2.1 两类起点runAsync 与 supplyAsync 怎么选CompletableFuture 创建异步任务有两个入口区别只有一个要不要返回值。// 没有返回值 CompletableFutureVoid future1 CompletableFuture.runAsync(() - { log.info(执行一些不需要返回结果的逻辑); }); // 有返回值 CompletableFutureString future2 CompletableFuture.supplyAsync(() - { return task result; });runAsync接收Runnable适合日志上报、缓存预热、消息发送这类不需要关心最终结果的场景。supplyAsync接收Supplier适合需要拿返回值继续处理的场景。绝大多数业务编排场景用的都是supplyAsync。这里还有一个容易忽略的细节这两个方法都有重载版本可以传入自定义线程池。比如supplyAsync(supplier, executor)。默认情况下使用的是ForkJoinPool.commonPool()这个默认值在生产环境里是个大坑后面我专门用一整章讲线程池问题。2.2 三类结果处理thenApply、thenAccept、thenRun 的边界任务计算完之后最常见的操作有三种把结果转换成另一个值、消费结果但不再返回、纯粹知道任务完成就行。对应的方法分别是thenApply、thenAccept、thenRun。CompletableFutureInteger future CompletableFuture .supplyAsync(() - 100) .thenApply(num - num * 2); // 结果转换返回新值 CompletableFutureVoid future2 CompletableFuture .supplyAsync(() - hello) .thenAccept(str - log.info(收到结果: {}, str)); // 消费无返回值 CompletableFutureVoid future3 CompletableFuture .supplyAsync(() - hello) .thenRun(() - log.info(任务执行完毕)); // 不管结果只执行一段逻辑记住一条主线方法名里的动词决定了它的行为。apply是“应用函数并返回新结果”accept是“接受结果但不返回”run是完全不关心结果继续跑一段逻辑。记住这个你就不容易把thenApply和thenAccept搞混。2.3 带 Async 后缀的方法执行线程的分水岭CompletableFuture 里大量方法都有带 Async 的版本比如thenApply和thenApplyAsync。这俩有什么区别这是我在面试中经常被问到的问题也是实际编码中最容易搞混的点。不带 Async 后缀的方法执行逻辑所在的线程不固定。具体来说如果调用时上游 Future 还没完成那这段逻辑会在“完成上游 Future 的那个线程”上执行如果上游已经完成了那这段逻辑会在“当前调用它的线程”上执行。带 Async 后缀的方法则不同它会强制把任务提交到指定的 Executor默认是ForkJoinPool.commonPool()去执行。我举个实际例子CompletableFutureString future CompletableFuture .supplyAsync(() - hello, executorA) .thenApply(str - str.toUpperCase()); // 大概率在 executorA 的线程上执行这里的 thenApply 没有 Async所以它会在完成 supplyAsync 的线程来自 executorA上执行。如果换成 thenApplyAsync它会重新丢给默认的 commonPool 执行如果没传 executor就跟 supplyAsync 用的线程池不是同一个了。这个区别在什么地方会出问题最典型的就是 ThreadLocal。假如你的supplyAsync里设置了某个 ThreadLocal 值然后thenApply依赖这个值做判断不带 Async 时因为和上游在同一个线程还能读到一旦你图“异步”加了个 Async线程切换了ThreadLocal 直接丢失。这个坑我见过不止一次排查起来特别费劲。2.4 一张表看清核心 API 的分类为了让你方便回忆我把常用的 API 按功能整理成了下面的表格。不需要死记硬背用到的时候知道“这一类方法存在、去文档里找带对应动词的方法就行”。功能分类核心方法作用创建起点runAsync / supplyAsync异步执行一段逻辑后者有返回值串行转换thenApply / thenCompose转换结果或连接嵌套任务消费结果thenAccept / thenRun消费结果或不关心结果合并两个任务thenCombine / thenAcceptBoth两个任务完成后合并结果两个任务取其一applyToEither / acceptEither哪个先完成用哪个异常处理exceptionally / handle / whenComplete兜底、恢复、观察聚合等待allOf / anyOf等待全部或任一任务完成3. 任务编排实战从串行依赖到并发聚合3.1 有依赖的任务怎么连thenCompose 避免嵌套地狱假设有这样一个场景先根据用户 ID 查用户信息再用用户信息里的默认收货地址查天气。第二个任务依赖第一个任务的结果这是典型的串行依赖。很多新手会写成这样CompletableFutureCompletableFutureWeather nestedFuture CompletableFuture .supplyAsync(() - userService.getUser(userId)) .thenApply(user - CompletableFuture.supplyAsync(() - weatherService.getWeather(user.getAddress())));注意到没有thenApply里返回了一个CompletableFuture整个表达式就变成了CompletableFutureCompletableFutureWeather这就是嵌套。要想拿到最终结果还得在外面再调一次join()代码变得很别扭。正确做法是用thenCompose。它的作用就是“拍平”这种嵌套把两个异步阶段连接成一个流水线CompletableFutureWeather weatherFuture CompletableFuture .supplyAsync(() - userService.getUser(userId)) .thenCompose(user - CompletableFuture.supplyAsync(() - weatherService.getWeather(user.getAddress())));thenCompose的入参是一个 Function返回值必须是 CompletionStage它会把返回的这个阶段“展开”融入当前流水线。你可以类比 JavaScript Promise 里的then它天然支持返回 Promise 并拍平。理解成“异步版本的 thenApply”也行只不过它专门用来连接另一个异步任务。3.2 无依赖的任务怎么合thenCombine 与并行聚合再来看一个常见场景商品详情页需要同时查商品基本信息、库存信息和促销信息三者互不依赖可以并行发起最终要合并成一个完整模型。CompletableFutureProduct productFuture CompletableFuture .supplyAsync(() - productService.getById(productId), executor); CompletableFutureStock stockFuture CompletableFuture .supplyAsync(() - stockService.getStock(skuId), executor); CompletableFuturePromotion promotionFuture CompletableFuture .supplyAsync(() - promotionService.getPromotion(productId), executor); CompletableFutureString resultFuture productFuture .thenCombine(stockFuture, (product, stock) - buildProductWithStock(product, stock)) .thenCombine(promotionFuture, (productWithStock, promotion) - buildFullDetail(productWithStock, promotion));thenCombine接收两个参数另一个 CompletableFuture 和一个 BiFunction。两个任务都完成之后BiFunction 会被调用把两个结果合并成一个新值。它解决的核心问题就是“两个互不依赖的异步任务如何优雅地合流”。如果合并结果后你并不想返回一个新值只是想做点事情比如通知、记录、发送消息可以用thenAcceptBoth它也接收两个结果但返回CompletableFutureVoid。3.3 竞速与聚合applyToEither、anyOf 和 allOf有些业务场景是“谁快用谁”。比如查价格时同时请求两个不同的价格服务先返回的那个作为最终结果。这时候用applyToEither最合适CompletableFuturePrice priceFromA CompletableFuture.supplyAsync(() - priceService.getPriceFromA(skuId), executor); CompletableFuturePrice priceFromB CompletableFuture.supplyAsync(() - priceService.getPriceFromB(skuId), executor); CompletableFuturePrice fastPriceFuture priceFromA.applyToEither(priceFromB, price - price);如果要同时等待多个任务全部完成用allOf。它接收多个 CompletableFuture返回CompletableFutureVoid这个 Void 只是代表“全部完成了”并不负责汇总结果。你想拿每个任务的结果还得在每个子 Future 上手动join()。要注意join()放在allOf(...).join()之后是安全的因为到那一刻所有任务都完成了再调用子任务的 join 不会阻塞。CompletableFutureString f1 CompletableFuture.supplyAsync(() - callA()); CompletableFutureString f2 CompletableFuture.supplyAsync(() - callB()); CompletableFutureString f3 CompletableFuture.supplyAsync(() - callC()); CompletableFuture.allOf(f1, f2, f3).join(); String result f1.join() f2.join() f3.join();anyOf则相反只要其中一个任务完成整个 Future 就完成返回的是最先完成的任务的结果类型是 Object。如果你确定各任务返回类型一致可以在anyOf结果上直接强转。3.4 一个完整的编排案例订单详情聚合查询把上面的知识点串起来看一个贴近真实的案例。需求根据订单 ID 查订单然后并行查询用户信息、商品明细、物流信息最后组装成订单详情 VO 返回。注意第一步和后面三步是有依赖关系的但后面三步之间互不依赖。CompletableFutureOrderDetailVO detailFuture CompletableFuture .supplyAsync(() - orderService.getOrder(orderId), executor) .thenCompose(order - { CompletableFutureUserVO userFuture CompletableFuture .supplyAsync(() - userService.getUser(order.getUserId()), executor); CompletableFutureListProductVO productFuture CompletableFuture .supplyAsync(() - productService.getProducts(order.getProductIds()), executor); CompletableFutureDeliveryVO deliveryFuture CompletableFuture .supplyAsync(() - deliveryService.getDelivery(order.getDeliveryId()), executor); return CompletableFuture .allOf(userFuture, productFuture, deliveryFuture) .thenApply(v - buildOrderDetail(order, userFuture.join(), productFuture.join(), deliveryFuture.join())); });这个方法体量看着不大但容纳了三个关键处理逻辑thenCompose解决第一步到后续任务的依赖allOf等待三个并行任务全部完成在allOf完成后的回调里使用join()取各子任务结果不会产生额外阻塞等待。整体耗时大约是“查订单时间 三个并行任务中最慢的那个时间”相比于串行四个服务的时间提升非常明显。我之前那个订单聚合接口从 220ms 优化到 70ms用的就是这种结构。代码写完之后可读性也比原来 Future 加 CountDownLatch 的实现好得多——你不用再手动维护一个计数器也不用担心并发修改 List 的线程安全问题。4. 线程池选择的坑默认的 commonPool 并不适合生产环境4.1 commonPool 的适用范围和局限CompletableFuture 的异步方法如果不传 Executor默认使用ForkJoinPool.commonPool()。commonPool 的并行度默认是CPU 核数 - 1。对于一个 8 核的机器也就是 7 个线程。问题来了如果你的异步任务里大半都是 HTTP 调用、数据库查询这类 IO 操作7 个线程很快就会被占满。而 commonPool 是全 JVM 共享的parallelStream用的也是它其他第三方库如果也用 commonPool 做并行处理大家会互相干扰。一旦 commonPool 里的线程全在阻塞等待 IO你新提交的异步任务就排在队列里等整个系统的异步能力瞬间降为零。我见过一个线上事故某服务把一批耗时任务交给 CompletableFuture.runAsync 执行没传自定义线程池高峰期 commonPool 7 个线程全被慢 IO 占住连带着parallelStream的并行流也一起拖慢接口超时率直线上升。最后改成自定义线程池问题才解决。4.2 自定义线程池的正确姿势与参数设计生产环境里我的习惯是所有 CompletableFuture 的异步任务都显式传入自定义线程池绝不依赖 commonPool。线程池参数需要根据任务类型设计不能拍脑袋。ThreadPoolExecutor bizExecutor new ThreadPoolExecutor( 8, // 核心线程数 16, // 最大线程数 60L, TimeUnit.SECONDS, // 空闲线程回收时间 new ArrayBlockingQueue(800), // 有界队列 new ThreadFactory() { private final AtomicInteger counter new AtomicInteger(); Override public Thread newThread(Runnable r) { Thread t new Thread(r, biz-async- counter.incrementAndGet()); t.setDaemon(false); return t; } }, new ThreadPoolExecutor.CallerRunsPolicy() );这里有几个点要拆开说。第一队列必须是有界的否则任务量暴增时线程池会无限积压任务内存最终被打爆。第二线程名一定要起好否则线上排查问题时你只能看到一堆 pool-7-thread-3根本不知道是哪个业务在跑。第三拒绝策略建议用CallerRunsPolicy它的含义是线程池满了之后新任务不丢弃而是由提交任务的线程直接执行。这个策略会把异步变成同步起到天然的背压效果但至少保证了任务不丢。核心线程数怎么定如果任务是 IO 密集型通常可以设为 CPU 核数的 2 倍左右比如 8 核机器设 16。如果是 CPU 密集型设成 CPU 核数 1 就够了。但这只是起点真正靠谱的做法是用压测去验证观察线程池活跃度、队列积压、RT 和错误率再调整参数。不要迷信公式。4.3 上下文传递线程切换后 traceId 怎么保住分布式系统里排查问题离不开 traceId通常放在日志框架的 MDC 里。但 CompletableFuture 的异步任务会在线程池的多个线程之间切换默认情况下 MDC 里存的 traceId 根本不会跟着传过去。结果是主线程打印了 traceId异步线程里打的日志全是空的链路断成了好几截。解决思路是包装 Executor在任务提交时把当前线程的 MDC 拷贝下来任务真正执行时再放进去执行完毕再清理public class MdcExecutor implements Executor { private final Executor delegate; public MdcExecutor(Executor delegate) { this.delegate delegate; } Override public void execute(Runnable command) { MapString, String context MDC.getCopyOfContextMap(); delegate.execute(() - { try { MDC.setContextMap(context); command.run(); } finally { MDC.clear(); } }); } }这样做一次异步线程里的日志就能和主线程串起来了。如果你用的 Spring 的 ThreadPoolTaskExecutor也可以继承后重写execute方法做同样的事。反正核心思路就一句话线程切换时手工把 ThreadLocal 里的上下文“搬运”过去。4.4 父子任务同池执行的死锁隐患这是我在代码评审里强调过多次的问题。假设你有一个线程池核心线程只有 2 个。这时你向线程池提交一个父任务父任务内部又用同一个线程池提交一个子任务然后父任务通过join()等待子任务结果。ThreadPoolExecutor pool new ThreadPoolExecutor( 2, 2, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue() ); CompletableFutureString future CompletableFuture.supplyAsync(() - { // 父任务占用了线程池里唯一的两个线程 return CompletableFuture.supplyAsync(() - child, pool).join(); }, pool);如果父任务已经占满了线程池的全部线程那子任务永远没有线程可执行会一直排在队列里。而父任务又在等子任务的结果两边互相等待这就是死锁。更隐蔽的情况是线程池没有满但并发稍高之后多个父任务把核心线程全部占住子任务全部排队系统吞吐量直接归零。解决方案也很简单父子任务不要共用一个线程池或者父任务里不要用join()阻塞等待同一个池里的子任务。设计异步链路时要清楚每个阶段跑在哪个 Executor 上避免“线程池里的任务去等待同一个线程池里的另一个任务”这种结构。5. 异常处理与超时控制别让错误在异步链路里悄悄溜走5.1 异常的传播路径与时序CompletableFuture 的异常处理和同步代码的 try-catch 有相似之处但也有一个很隐蔽的区别如果你在supplyAsync里抛了异常这个 Future 会进入异常完成状态。接下来依赖它的thenApply、thenAccept这些阶段都不会执行异常会顺着依赖链一路向下传播直到遇见exceptionally或handle这样的兜底处理或者最终在get()/join()时被抛出来。最危险的情况是整条链路从头到尾没有任何异常处理方法主线程得到了 Future 对象却从不去调 get/join这段异步逻辑里的异常就被彻底吞掉了。你只会在监控里看到某个业务数据一直没有更新但完全找不到错误日志。5.2 exceptionally、handle 与 whenComplete 的抉择三者都可以用来感知异常语义差别很大。exceptionally只在异常时执行你可以在这里返回一个降级结果让流水线继续往下走。它等价于 catch 分支。CompletableFutureString future CompletableFuture .supplyAsync(() - { throw new RuntimeException(boom); }) .exceptionally(ex - { log.error(task failed, ex); return fallback; });handle无论成功失败都会执行你需要通过入参来判断当前是正常结果还是异常对象然后返回一个新结果。它等价于 finally 判断。CompletableFutureString future CompletableFuture .supplyAsync(() - ok) .handle((result, ex) - { if (ex ! null) { return fallback; } return result.toUpperCase(); });whenComplete也无论成功失败都会执行但它就像一个观察者收到结果或异常后只做记录、埋点、通知不会改变流水线上的结果也不会吞掉异常。如果上游有异常而你没在 whenComplete 里重新抛出异常依然会沿着链向下传递。我的建议是在每个“叶子”异步任务的终点至少要挂一个exceptionally或whenComplete要么兜底要么打日志。这样即使主线程不去 get/join错误也不会凭空消失。这属于异步编程里的基本卫生习惯。5.3 get() 与 join()检查异常与不检查异常的差异这两个方法表面上看都是拿结果区别在异常处理方式。get()声明抛出InterruptedException、ExecutionException等受检异常调用时必须 try-catch 或继续往上抛join()不声明受检异常如果调用确实异常了它直接抛CompletionException这个非受检异常。在链式代码、Stream 代码里写get()会让代码非常臃肿所以我在实战中一般优先用join()。但注意join 抛出的 CompletionException 包装了一层如果你想取出原始异常需要调用getCause()。我遇到过同事排查问题只看到 CompletionException 的堆栈觉得莫名其妙往里翻一层才找到真正的业务异常。这个细节记住了能省不少排查时间。5.4 超时控制orTimeout、completeOnTimeout 与 cancel 的边界Java 9 给 CompletableFuture 增加了两个非常实用的方法orTimeout和completeOnTimeout。前者在超时后让 Future 以TimeoutException异常完成后者在超时后让 Future 以你指定的默认值完成。CompletableFutureResult future CompletableFuture .supplyAsync(() - slowService.call(), executor) .orTimeout(2, TimeUnit.SECONDS) .exceptionally(ex - defaultResult());如果你用的是 Java 8这两个方法不存在就得自己用Future.get(timeout)配合辅助手段模拟超时。所以如果你的项目还在 Java 8又依赖 CompletableFuture 做对外接口聚合超时控制这块一定要自己补上否则一旦下游服务变慢调用方会一直等下去。再说cancel()。很多人以为取消 CompletableFuture 就能中断正在执行的任务线程实际上不会。cancel()只是把这个 Future 以CancellationException完成后续阶段看过来就是异常完成。真正在跑的任务线程该执行还是继续执行不会受到任何中断信号。这和 Future 的中断行为是一致的要想真正终止任务只能靠任务内部响应中断标志位。所以我在实践中不太依赖 cancel 去做资源释放更多是在编排层面通过超时和降级来控制损失。6. 生产环境中关于 CompletableFuture 的几类高频问题6.1 任务报错但主线程毫无感知怎么排查这个问题我在前面提过但值得单独展开。症状很典型服务接口一切正常但某条数据的状态一直不变日志里也找不到任何报错。查到最后往往是一个异步任务里抛了异常而整条 CompletableFuture 链没有任何兜底处理异常被静默吞掉了。排查思路有两个方向。第一全局兜底给所有异步任务挂一个统一的whenComplete或者exceptionally至少打印 error 日志。第二监控补偿如果业务上允许可以在异步操作完成后写一张执行结果表由定时任务去核对没执行成功的自动重试或告警。这两种方式我建议都做前者保证可观测性后者保证最终一致。另外一个常见但容易忽略的点如果你的异步任务里调用了 Spring 管理的 Bean而这个 Bean 内部有事务注解那事务是失效的。因为异步线程和调用线程不是同一个事务上下文。跨线程操作数据库要么不用事务要么把需要原子性的操作移到同一个异步任务内部处理。6.2 Spring 容器里如何优雅管理异步线程池在 Spring Boot 项目里我建议把自定义线程池定义成独立的 Bean专门给 CompletableFuture 使用和 Spring 自带的Async线程池分开管理。Bean(bizAsyncExecutor) public ThreadPoolTaskExecutor bizAsyncExecutor() { ThreadPoolTaskExecutor executor new ThreadPoolTaskExecutor(); executor.setCorePoolSize(8); executor.setMaxPoolSize(16); executor.setQueueCapacity(800); executor.setThreadNamePrefix(biz-async-); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); executor.initialize(); return executor; }注入之后所有使用 CompletableFuture 的地方都显式传这个 BeanResource private ThreadPoolTaskExecutor bizAsyncExecutor; CompletableFutureUser userFuture CompletableFuture .supplyAsync(() - userService.getUser(userId), bizAsyncExecutor);这样做的好处是线程池配置集中管理压测调整参数时不用到处改代码线程名统一清晰线上日志一看就知道是哪个线程池在执行拒绝策略也有统一保障不会因为某个地方漏配而出现任务无限积压。6.3 CompletableFuture 和响应式编程怎么选经常有人问有了 CompletableFuture还需要 WebFlux、RxJava 吗我的观点是两个层级的工具。CompletableFuture 解决的是“单个异步任务的编排”它的核心价值是把链式依赖、并行合并、异常兜底这些事表达得更清晰。而响应式编程解决的是“整个调用链路的延迟计算与背压控制”它要求从数据源到消费者整条链路都是响应式类型。如果你只是想让同步接口里的几段逻辑并行起来、合并结果CompletableFuture 足够引入响应式框架反而是负担。反过来如果你的数据流本身就是持续的、需要响应式的变换与分流那再考虑 WebFlux 或者 Reactor。不要把两类工具混为一谈选型标准就看一个问题你的异步边界到底划在哪。划在方法内部用 CompletableFuture 最顺手划在整个调用链才需要真正的响应式框架。另外用 CompletableFuture 做大规模异步化改造时一定要给下游加保护。并行调用多个服务如果每个服务都变慢请求会大量堆积在等待和队列中线程池也会被打满。配合信号量或限流器控制并发度比单纯依赖线程池参数更可靠。我做异步化改造时有个习惯所有业务 CompletableFuture 的最末端一定会有一个exceptionally兜底加一个whenComplete打印结果摘要。这个习惯帮我在线上省了无数次排查时间。CompletableFuture 的编排能力很强但强工具也要求更严格的使用纪律。把线程池选对、把异常接住、把超时控住这三点做到位异步编程在绝大多数业务场景里就是可靠且优雅的。
返回列表