那次我用 CompletableFuture,接口快了 3 倍

75 阅读3分钟

"如果你还在串行调用多个服务接口,那你可能正悄悄拖慢整个系统性能。"
本文是我在真实项目中优化一个慢接口时的实战笔记,深入剖析如何用 CompletableFuture 提升性能、处理并发、规避坑点。


7T7AlJ.png

📌 背景场景:接口被吐槽“卡成 PPT”

我们在做一个订单详情页接口 /api/order/detail,前端要展示非常多数据:

  • 订单基本信息(orderService)
  • 商品列表(productService)
  • 当前用户优惠券(couponService)
  • 用户是否为 VIP(userService)
  • 活动状态(activityService)

接口初版逻辑非常直白,顺序调用五个服务:

Order order = orderService.getOrder(orderId);List<Product> products = productService.getProducts(orderId);List<Coupon> coupons = couponService.getUserCoupons(userId);boolean isVip = userService.isVip(userId);Activity activity = activityService.getActivityByOrder(orderId);

🌧️ 问题来了:虽然每个服务单独都很快(平均 200400ms),但加起来总共要 1.52 秒,用户感觉“点击没反应”。


🧠 第一步:为什么考虑异步?

初步分析,这五个服务之间 没有强依赖关系,完全可以并发执行。于是我想到:

要不要用 Java 的 CompletableFuture 并发发起调用?

我们项目用的是 Spring Boot,配合 CompletableFuture + 自定义线程池,异步能力很强。


⚙️ 第二步:引入 CompletableFuture 重构

我们使用线程池是为了避免默认的 ForkJoinPool 被打爆,先定义一个专用线程池:

@Beanpublic Executor asyncExecutor() {    return Executors.newFixedThreadPool(10);}

然后开始重构代码逻辑:

CompletableFuture<Order> orderFuture = CompletableFuture.supplyAsync(() ->    orderService.getOrder(orderId), asyncExecutor);CompletableFuture<List<Product>> productFuture = CompletableFuture.supplyAsync(() ->    productService.getProducts(orderId), asyncExecutor);CompletableFuture<List<Coupon>> couponFuture = CompletableFuture.supplyAsync(() ->    couponService.getUserCoupons(userId), asyncExecutor);CompletableFuture<Boolean> vipFuture = CompletableFuture.supplyAsync(() ->    userService.isVip(userId), asyncExecutor);CompletableFuture<Activity> activityFuture = CompletableFuture.supplyAsync(() ->    activityService.getActivityByOrder(orderId), asyncExecutor);

然后统一等待任务完成:

CompletableFuture.allOf(orderFuture, productFuture, couponFuture, vipFuture, activityFuture).join();Order order = orderFuture.join();List<Product> products = productFuture.join();List<Coupon> coupons = couponFuture.join();Boolean isVip = vipFuture.join();Activity activity = activityFuture.join();

🚀 第三步:优化效果如何?

测试数据:

优化前串行调用优化后并发调用
平均耗时 1.8s平均耗时 650ms
P95 为 2.1sP95 为 900ms
用户投诉:高用户体验:流畅

⚠️ 注意:接口返回速度变快了,但线程池并发执行也带来新的挑战(见下一节)。


⚠️ 第四步:坑点和误区

❗ 1. 异常没处理导致接口崩了CompletableFuture 默认的异常不会传播,必须加 .exceptionally() 或 .handle() 做兜底:

orderFuture.exceptionally(ex -> {    
    log.error("orderService 调用异常", ex);    
    return null;
});

❗ 2. join() 并不会自动抛出 checked 异常.join() 会吞掉异常并用 CompletionException 包裹,调试时需要 .getCause() 查看根因。

❗ 3. 自定义线程池别乱搞

不要直接用 newFixedThreadPool(),应该配合拒绝策略 + 队列长度配置,否则容易线程爆炸。

ThreadPoolExecutor pool = new ThreadPoolExecutor(  102060, TimeUnit.SECONDS,  new LinkedBlockingQueue<>(100),  new ThreadPoolExecutor.AbortPolicy());

🧩 最佳实践建议

  • ✅ 并发异步适用于:多个耗时 IO 操作、外部接口调用、聚合查询等
  • ❌ 不适用于:CPU 密集逻辑、强依赖的顺序逻辑
  • ✅ 统一使用线程池,便于监控和管理
  • ✅ 在 .allOf() 后统一 join 结果,逻辑更清晰,异常更集中处理

📬 尾声

这次使用 CompletableFuture 优化之后,我意识到:

性能问题往往不是业务逻辑复杂,而是调用模式不对。

所以,不要一开始就排查数据库、服务性能、GC 等等,先看看你是不是把可以并行的任务写成了串行

Debug笔记 记录我在代码背后踩过的坑,也分享那些曾经提升过我效率的关键思路。

关注我,一起成为更优雅的程序员。 在这里插入图片描述