1193 字
约 3 分钟
0
用 CompletableFuture 优化接口:把串行 RPC 调用改造成并行

当一个接口需要调用多个下游服务来拼装返回数据时,最容易被忽略的性能瓶颈就是:这些互不依赖的 RPC 被串行执行了。本文以笔记详情接口为例,讲解如何用 CompletableFuture 把串行调用改造成并行,让接口耗时从"相加"变成"取最大"。

一、问题:同步串行调用拖慢接口

笔记详情接口要展示发布者的昵称、头像,以及笔记正文,需要分别调用两个下游服务:

  • 用户服务:根据用户 ID 查昵称、头像;
  • KV 键值服务:根据内容 ID 查笔记正文。

改造前是同步阻塞、串行执行的:

调用户服务(耗时 T1)→ 等返回 → 调 KV 服务(耗时 T2)→ 等返回
接口总耗时 = T1 + T2

关键点在于:这两个调用之间没有数据依赖,谁先谁后都不影响结果。串行执行白白把耗时叠加了,而并行执行时总耗时只取决于最慢的那个:

同时发起两个请求
接口总耗时 ≈ max(T1, T2)

下游服务越多、单次调用越慢,并行的收益就越明显。

二、CompletableFuture 是什么

CompletableFuture 是 Future 的扩展,用于简化 Java 异步编程。相比传统 Future,它能更容易地组合和变换异步任务的结果,并提供了丰富的 API 处理错误与取消。

核心 API 一览:

API 作用
supplyAsync(任务, 线程池) 提交一个有返回值的异步任务
completedFuture(值) 返回一个"已完成"的 Future,用于兜底
allOf(f1, f2, ...) 等待所有 Future 完成,返回新的 CompletableFuture
thenApply(函数) 全部完成后对结果做转换
join() 获取结果,不抛受检异常
exceptionally(函数) 异常时返回默认值
cancel(true) 尝试取消任务

三、改造实战:笔记详情接口

用 CompletableFuture.supplyAsync 同时发起两个下游调用,allOf 等待全部完成,再在 thenApply 里用 join() 取出结果拼装 VO:

// ① 异步调用用户服务(第二个参数传入自定义线程池)
CompletableFuture<FindUserByIdRspDTO> userResultFuture = CompletableFuture
        .supplyAsync(() -> userRpcService.findById(creatorId), threadPoolTaskExecutor);

// ② 异步调用 KV 键值服务获取笔记内容
CompletableFuture<String> contentResultFuture = CompletableFuture.completedFuture(null);
if (Objects.equals(noteDO.getIsContentEmpty(), Boolean.FALSE)) {
    contentResultFuture = CompletableFuture
            .supplyAsync(() -> keyValueRpcService.findNoteContent(noteDO.getContentUuid()), threadPoolTaskExecutor);
}

// ③ allOf 等待两个异步任务都完成,thenApply 中拼接结果
CompletableFuture<FindNoteDetailRspVO> resultFuture = CompletableFuture
        .allOf(userResultFuture, contentResultFuture)
        .thenApply(s -> {
            FindUserByIdRspDTO user = userResultFuture.join();  // 取用户服务结果
            String content = contentResultFuture.join();        // 取 KV 服务结果
            // ... 构建 FindNoteDetailRspVO 返回
            return vo;
        });

// ④ 阻塞等待最终拼装结果
FindNoteDetailRspVO findNoteDetailRspVO = resultFuture.get();

三个容易被忽略的细节

  1. 为什么 supplyAsync 要传自定义线程池

    不传的话默认跑在全局共享的 ForkJoinPool.commonPool() 上,多个接口并发异步时线程互相抢占。传自定义的 ThreadPoolTaskExecutor,给"下游并发调用"一个专属、可控、有名字的线程池,这也是为什么项目里要单独配线程池配置类。

  2. completedFuture(null) 的巧思

    KV 内容可能为空(比如视频笔记没有文字正文),先给一个"已完成"的 Future 兜底,再按需决定是否真正发起调用。这样 allOf 和 join 的处理逻辑不用写两套分支,结构更统一。

  3. join() 与 get() 的区别

    get() 会抛受检异常 ExecutionException,而 join() 不抛受检异常,所以能在 thenApply 的 lambda 里直接使用,这也是示例代码选 join() 的原因。

四、与缓存优化的关系

接口性能优化通常是叠加的:

  • 缓存(Caffeine + Redis 二级缓存):解决"读数据快不快"——热点数据不落库、不穿透;
  • CompletableFuture 并行化:解决"拼装数据等不等得久"——多个下游 RPC 同时发起。

两者方向不同、互不冲突:缓存已经把单次读取压到最快,并行化则把"多次等待"压缩成"一次等待"。高并发场景下,这两板斧往往要一起上。

五、总结

  • 多个互不依赖的下游调用,应该用 CompletableFuture 并行执行,耗时从"相加"变"取最大";
  • 核心三板斧:supplyAsync 提交任务、allOf 等待完成、thenApply + join 拼装结果;
  • 别忘了传自定义线程池,别让并发任务挤在默认的公共池里;
  • 缓存优化读取、并行优化拼装,两者叠加才是接口性能的完整解法。
用 CompletableFuture 优化接口:把串行 RPC 调用改造成并行
https://www.clxhxhhr.top/posts/4593/
作者
clxstart
发布于
2026-10-10
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。