跳转到内容
123xiao | 无名键客

《Java 中基于 CompletableFuture 的异步编排实战:并行调用、超时控制与异常兜底》

字数: 0 阅读时长: 1 分钟

Java 中基于 CompletableFuture 的异步编排实战:并行调用、超时控制与异常兜底

在 Java 服务端开发里,只要你做过聚合接口、网关层、推荐系统、用户画像或者报表查询,就一定碰到过这样的问题:

  • 一个接口要同时调用多个下游服务
  • 某个下游慢了,整体响应就被拖垮
  • 某个依赖偶发报错,结果整个接口直接 500
  • 想并发提速,但线程池一不小心又把自己打挂了

我自己第一次大规模用 CompletableFuture 做聚合接口时,最开始也只是把同步调用机械地改成异步,结果吞吐没怎么涨,问题倒是多了一堆:线程池用错、异常被吞、超时没兜底、主线程提前结束……后来才意识到,CompletableFuture 真正的价值不是“异步”两个字,而是异步编排能力

这篇文章就从实战角度,带你完整走一遍:

  • 如何并行调用多个下游
  • 如何做超时控制
  • 如何做异常兜底
  • 如何避免常见坑
  • 如何把它用得既快又稳

背景与问题

先看一个典型场景:商品详情页接口。

假设前端请求 /product/detail?id=1001,服务端需要拼装出完整页面数据,分别调用:

  1. 商品基础信息服务
  2. 库存服务
  3. 价格服务

如果你用同步串行写法,大概是这样:

Product product = productService.getProduct(id);
Inventory inventory = inventoryService.getInventory(id);
Price price = priceService.getPrice(id);
return new ProductDetail(product, inventory, price);

问题很直接:

  • 三次远程调用串行执行,总耗时约等于三者之和
  • 任意一个服务慢,就拖慢整个接口
  • 任意一个服务报错,可能直接失败

如果每个服务平均 200ms,那么串行总耗时大约是:

200ms + 200ms + 200ms = 600ms

而这三个服务其实互相独立,本来就适合并行。

这时候,CompletableFuture 就很合适。


前置知识与环境准备

本文示例基于:

  • JDK 8+
  • 推荐理解线程池基础
  • 知道 Future 和阻塞等待的基本概念

需要提前说明一个版本点:

  • JDK 8 没有 orTimeoutcompleteOnTimeout
  • JDK 9+ 才提供这两个 API

所以文中我会给出:

  • 通用编排思路
  • JDK 9+ 的优雅写法
  • 兼容 JDK 8 的替代写法思路

核心原理

1. CompletableFuture 解决的不是“开线程”,而是“组织任务关系”

很多人刚接触时会把它理解成:

“哦,就是异步执行一个任务。”

但更关键的是它能表达任务之间的关系:

  • 并行执行allOf
  • 任意一个先返回anyOf
  • 成功后继续处理thenApply / thenCompose
  • 合并多个结果thenCombine
  • 异常恢复exceptionally / handle
  • 结果兜底completeOnTimeout

也就是说,它不是单个“异步任务”,而是一张异步依赖图

2. 三类常见编排模式

模式一:并行聚合

多个互不依赖的任务同时发起,最后统一汇总。

flowchart LR
    A[请求进入] --> B[查询商品]
    A --> C[查询库存]
    A --> D[查询价格]
    B --> E[汇总结果]
    C --> E
    D --> E

模式二:串联依赖

前一个任务结果作为后一个任务输入。

比如先查用户,再根据用户等级查权益。

flowchart TD
    A[查用户信息] --> B[提取会员等级]
    B --> C[查询权益服务]
    C --> D[组装响应]

模式三:超时与异常兜底

调用失败或超时,不一定要让整个请求失败,可以返回默认值或降级值。

stateDiagram-v2
    [*] --> 调用下游
    调用下游 --> 成功返回: 正常完成
    调用下游 --> 超时: 超过阈值
    调用下游 --> 异常: 抛出异常
    超时 --> 默认值兜底
    异常 --> 默认值兜底
    成功返回 --> [*]
    默认值兜底 --> [*]

第一个可落地示例:把串行调用改成并行调用

先写一个完整可运行的例子。为了方便本地执行,我用 sleep 模拟远程 RPC 调用。

实战代码(可运行)

import java.util.concurrent.*;

public class CompletableFutureParallelDemo {

    private static final ExecutorService EXECUTOR = new ThreadPoolExecutor(
            4, 8,
            60, TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(100),
            new ThreadFactory() {
                private int index = 1;
                @Override
                public Thread newThread(Runnable r) {
                    return new Thread(r, "biz-exec-" + index++);
                }
            },
            new ThreadPoolExecutor.CallerRunsPolicy()
    );

    public static void main(String[] args) {
        long start = System.currentTimeMillis();

        CompletableFuture<String> productFuture = CompletableFuture.supplyAsync(() -> getProduct(1001L), EXECUTOR);
        CompletableFuture<Integer> inventoryFuture = CompletableFuture.supplyAsync(() -> getInventory(1001L), EXECUTOR);
        CompletableFuture<Double> priceFuture = CompletableFuture.supplyAsync(() -> getPrice(1001L), EXECUTOR);

        CompletableFuture<ProductDetail> detailFuture = CompletableFuture.allOf(
                productFuture, inventoryFuture, priceFuture
        ).thenApply(v -> new ProductDetail(
                productFuture.join(),
                inventoryFuture.join(),
                priceFuture.join()
        ));

        ProductDetail detail = detailFuture.join();

        long cost = System.currentTimeMillis() - start;
        System.out.println("result = " + detail);
        System.out.println("cost = " + cost + " ms");

        EXECUTOR.shutdown();
    }

    static String getProduct(Long id) {
        sleep(200);
        return "商品-" + id;
    }

    static Integer getInventory(Long id) {
        sleep(300);
        return 99;
    }

    static Double getPrice(Long id) {
        sleep(250);
        return 199.0;
    }

    static void sleep(long ms) {
        try {
            Thread.sleep(ms);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException("thread interrupted", e);
        }
    }

    static class ProductDetail {
        private final String productName;
        private final Integer inventory;
        private final Double price;

        public ProductDetail(String productName, Integer inventory, Double price) {
            this.productName = productName;
            this.inventory = inventory;
            this.price = price;
        }

        @Override
        public String toString() {
            return "ProductDetail{" +
                    "productName='" + productName + '\'' +
                    ", inventory=" + inventory +
                    ", price=" + price +
                    '}';
        }
    }
}

这段代码做了什么?

核心点有三个:

  1. 使用 supplyAsync 并发提交三个任务
  2. allOf 等待全部完成
  3. thenApply 里统一汇总结果

为什么这里用 join() 而不是 get()

两者都能取结果,但区别在异常处理:

  • get() 抛受检异常:InterruptedExceptionExecutionException
  • join() 抛非受检异常:CompletionException

在编排代码里,join() 通常更顺手,代码更干净。

实际耗时会是多少?

如果三个任务分别耗时 200ms、300ms、250ms,那么并行后总耗时约等于:

max(200, 300, 250) ≈ 300ms

这就是并行编排最直接的收益。


核心原理:常用 API 应该怎么选

很多人 API 名字都看过,但一写代码就乱。这里给一个实用版理解。

runAsync vs supplyAsync

  • runAsync:无返回值
  • supplyAsync:有返回值
CompletableFuture<Void> f1 = CompletableFuture.runAsync(() -> doTask(), executor);
CompletableFuture<String> f2 = CompletableFuture.supplyAsync(() -> queryData(), executor);

thenApply vs thenCompose

  • thenApply:对结果做同步映射
  • thenCompose:把“返回 Future 的任务”摊平

比如:

CompletableFuture<User> userFuture = CompletableFuture.supplyAsync(() -> getUser(1L), executor);

CompletableFuture<String> levelFuture = userFuture.thenApply(user -> user.getLevel());

而如果第二步本身又是异步调用:

CompletableFuture<String> rightsFuture = userFuture.thenCompose(
        user -> CompletableFuture.supplyAsync(() -> getRights(user.getLevel()), executor)
);

如果这里误用 thenApply,你会得到一个嵌套类型:

CompletableFuture<CompletableFuture<String>>

这就是典型坑。

thenCombine vs allOf

  • thenCombine:适合两个任务结果合并
  • allOf:适合多个任务统一等待

示例:

CompletableFuture<String> a = CompletableFuture.supplyAsync(() -> "A", executor);
CompletableFuture<String> b = CompletableFuture.supplyAsync(() -> "B", executor);

CompletableFuture<String> c = a.thenCombine(b, (x, y) -> x + y);

如果是 3 个以上异步任务,通常 allOf 更自然。


实战升级:超时控制与异常兜底

真实业务里,并行只是第一步。更关键的是:

不能因为一个下游超时或报错,把整个聚合接口拖死。

下面用一个更贴近生产的例子来写。

需求:

  • 商品服务正常返回
  • 库存服务偶发异常,返回默认库存 0
  • 价格服务可能超时,超过 300ms 时返回默认价格 -1
import java.util.concurrent.*;

public class CompletableFutureTimeoutDemo {

    private static final ExecutorService EXECUTOR = Executors.newFixedThreadPool(4);

    public static void main(String[] args) {
        long start = System.currentTimeMillis();

        CompletableFuture<String> productFuture = CompletableFuture
                .supplyAsync(() -> getProduct(1001L), EXECUTOR)
                .exceptionally(ex -> {
                    log("product service error: " + ex.getMessage());
                    return "默认商品";
                });

        CompletableFuture<Integer> inventoryFuture = CompletableFuture
                .supplyAsync(() -> getInventory(1001L), EXECUTOR)
                .exceptionally(ex -> {
                    log("inventory service error: " + ex.getMessage());
                    return 0;
                });

        CompletableFuture<Double> priceFuture = CompletableFuture
                .supplyAsync(() -> getPrice(1001L), EXECUTOR)
                .completeOnTimeout(-1.0, 300, TimeUnit.MILLISECONDS)
                .exceptionally(ex -> {
                    log("price service error: " + ex.getMessage());
                    return -1.0;
                });

        CompletableFuture<ProductDetail> resultFuture = CompletableFuture
                .allOf(productFuture, inventoryFuture, priceFuture)
                .thenApply(v -> new ProductDetail(
                        productFuture.join(),
                        inventoryFuture.join(),
                        priceFuture.join()
                ));

        ProductDetail detail = resultFuture.join();

        long cost = System.currentTimeMillis() - start;
        System.out.println(detail);
        System.out.println("cost = " + cost + " ms");

        EXECUTOR.shutdown();
    }

    static String getProduct(Long id) {
        sleep(100);
        return "商品-" + id;
    }

    static Integer getInventory(Long id) {
        sleep(150);
        throw new RuntimeException("库存服务不可用");
    }

    static Double getPrice(Long id) {
        sleep(500);
        return 299.0;
    }

    static void sleep(long ms) {
        try {
            Thread.sleep(ms);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException(e);
        }
    }

    static void log(String msg) {
        System.out.println("[LOG] " + msg);
    }

    static class ProductDetail {
        private final String productName;
        private final Integer inventory;
        private final Double price;

        public ProductDetail(String productName, Integer inventory, Double price) {
            this.productName = productName;
            this.inventory = inventory;
            this.price = price;
        }

        @Override
        public String toString() {
            return "ProductDetail{" +
                    "productName='" + productName + '\'' +
                    ", inventory=" + inventory +
                    ", price=" + price +
                    '}';
        }
    }
}

这里的编排顺序很关键

  • 任务先异步发起
  • 每个任务在局部链路上做异常兜底
  • 有需要的任务做超时控制
  • 最后再统一汇总

这样做的好处是:

  • 某个任务失败,不影响别的任务继续执行
  • 汇总阶段拿到的是已经降级后的结果
  • 主流程代码更稳定

completeOnTimeoutorTimeout 的区别

这两个 API 容易混淆:

completeOnTimeout(value, timeout, unit)

超时后直接返回默认值。

适合:

  • 默认库存为 0
  • 默认价格为 -1
  • 默认推荐列表为空列表

orTimeout(timeout, unit)

超时后让 Future 异常完成。

适合:

  • 想明确感知超时异常
  • 需要上层决定是否兜底
  • 需要区分“业务异常”和“超时异常”

示例:

CompletableFuture<Double> priceFuture = CompletableFuture
        .supplyAsync(() -> getPrice(1001L), EXECUTOR)
        .orTimeout(300, TimeUnit.MILLISECONDS)
        .exceptionally(ex -> {
            log("price timeout or error: " + ex.getMessage());
            return -1.0;
        });

更像生产代码的写法:封装一个带超时和兜底的异步调用器

如果每个地方都手写 supplyAsync(...).orTimeout(...).exceptionally(...),代码会重复。更实用的方式是封装一下。

import java.util.concurrent.*;
import java.util.function.Supplier;

public class AsyncHelper {

    private final Executor executor;

    public AsyncHelper(Executor executor) {
        this.executor = executor;
    }

    public <T> CompletableFuture<T> callAsync(
            Supplier<T> supplier,
            long timeout,
            TimeUnit unit,
            T fallback,
            String taskName) {

        return CompletableFuture
                .supplyAsync(supplier, executor)
                .completeOnTimeout(fallback, timeout, unit)
                .exceptionally(ex -> {
                    System.out.println("[WARN] " + taskName + " failed: " + ex.getMessage());
                    return fallback;
                });
    }
}

使用方式:

import java.util.concurrent.*;

public class AsyncHelperDemo {

    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(4);
        AsyncHelper helper = new AsyncHelper(executor);

        CompletableFuture<String> userFuture =
                helper.callAsync(() -> getUserName(1L), 200, TimeUnit.MILLISECONDS, "默认用户", "user-service");

        CompletableFuture<Integer> scoreFuture =
                helper.callAsync(() -> getScore(1L), 200, TimeUnit.MILLISECONDS, 0, "score-service");

        CompletableFuture<String> result = userFuture.thenCombine(scoreFuture,
                (user, score) -> "user=" + user + ", score=" + score);

        System.out.println(result.join());

        executor.shutdown();
    }

    static String getUserName(Long userId) {
        sleep(100);
        return "Alice";
    }

    static Integer getScore(Long userId) {
        sleep(300);
        return 100;
    }

    static void sleep(long ms) {
        try {
            Thread.sleep(ms);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException(e);
        }
    }
}

这种封装的好处是:

  • 调用侧只关心业务逻辑
  • 超时、异常、日志策略统一
  • 降级行为可配置

逐步验证清单

如果你想把示例真正吃透,我建议按这个顺序自己跑一遍。

验证 1:先看并行耗时

把三个任务都改成正常返回,分别睡眠:

  • 200ms
  • 300ms
  • 250ms

观察总耗时是否接近 300ms,而不是 750ms。

验证 2:让一个任务抛异常

例如库存服务抛异常,确认:

  • 聚合接口没有整体失败
  • 最终库存值变成兜底值 0

验证 3:让一个任务超时

把价格服务改成 500ms,超时时间设成 300ms,确认:

  • 最终价格变成 -1
  • 整体接口不会一直阻塞到 500ms

验证 4:去掉自定义线程池

故意不传 executor,使用默认线程池,观察:

  • 日志线程名变化
  • 在压测场景下的稳定性风险

这一步很重要,因为很多线上问题就是从“先偷懒用默认线程池”开始的。


常见坑与排查

这部分我会重点说一些很容易在项目里踩到的点。

坑 1:默认使用 ForkJoinPool,结果线程池行为不可控

如果你这样写:

CompletableFuture.supplyAsync(() -> queryData());

那么默认会使用 ForkJoinPool.commonPool()

问题是:

  • 它是全局共享的
  • 你很难隔离不同业务
  • 如果任务里有阻塞 IO,commonPool 可能被拖慢
  • 出了问题很难定位是谁把线程池占满了

建议:业务代码里尽量显式传入自定义线程池。


坑 2:把 IO 密集型任务丢到不合适的线程池

远程调用、数据库查询、HTTP 请求,大多数都属于阻塞型 IO
这类任务如果线程数太少,会排队;线程数太大,又会导致上下文切换严重。

排查思路:

  • 看线程池核心参数
  • 看队列积压
  • 看任务平均耗时和超时率
  • 看拒绝策略是否生效

常见配置思路:

  • CPU 密集型:线程数接近 CPU 核数
  • IO 密集型:线程数通常大于 CPU 核数,但不要无脑放大

坑 3:allOf() 不会直接给你聚合结果

CompletableFuture.allOf(f1, f2, f3) 的返回类型是:

CompletableFuture<Void>

它只表示“都完成了”,不会自动把结果组装好
所以你还得自己 join() 各个子任务结果。

这也是很多新手第一次用时最困惑的地方。


坑 4:异常被包装,日志看起来像“丢了真相”

join() 抛的是 CompletionException,真实异常通常在 getCause() 里。

比如:

try {
    future.join();
} catch (CompletionException e) {
    System.out.println("root cause = " + e.getCause());
}

如果你只打印顶层异常,可能只看到:

java.util.concurrent.CompletionException

却看不到真正的业务错误。

排查时一定要展开 cause。


坑 5:局部兜底和全局兜底混用,导致问题被“悄悄吃掉”

例如:

CompletableFuture<Integer> inventoryFuture = CompletableFuture
        .supplyAsync(() -> getInventory(1001L), EXECUTOR)
        .exceptionally(ex -> 0);

这样确实能保住主流程,但代价是:

  • 错误被吞掉了
  • 如果日志没打全,排查会非常困难
  • 监控也可能感知不到真实失败率

更稳妥的做法是:

  • 兜底前打日志
  • 上报监控
  • 标记降级来源

坑 6:超时只是 Future 超时,不代表底层任务一定停止了

这是个很容易误解的点。

例如:

future.completeOnTimeout(defaultValue, 300, TimeUnit.MILLISECONDS)

300ms 后调用方确实拿到了默认值,但底层那个任务可能还在继续跑
如果底层是 HTTP 调用、数据库查询或复杂计算,它未必会被立即中断。

这意味着:

  • 超时配置不能替代下游自身超时
  • HTTP client、数据库驱动、RPC 框架也要设置超时
  • 否则会出现“调用方超时了,但资源还在后台被持续占用”

这个坑我当时踩过,表面上接口很快返回了,结果线程池里的慢任务越积越多,最后把整个实例拖慢。


安全/性能最佳实践

这一节尽量讲能直接拿去用的建议。

1. 一定要使用自定义线程池,并按业务隔离

不要所有异步任务共用一个线程池。

建议至少按场景拆分:

  • 用户信息查询线程池
  • 商品聚合线程池
  • 报表导出线程池

好处:

  • 防止一个慢业务拖垮另一个业务
  • 便于监控和限流
  • 更方便容量规划

2. 线程池参数别拍脑袋,至少结合这几个指标

要看:

  • 平均任务耗时
  • 峰值 QPS
  • 下游超时阈值
  • 队列长度
  • 拒绝率

一个简单经验:

如果聚合接口峰值 200 QPS,每次需要并发打 3 个下游,那么瞬时异步任务量可能明显高于接口 QPS,本质上要按“子任务规模”估算。


3. 每个下游都要有独立超时

不要只在最外层接口做总超时。

更合理的做法是:

  • 商品服务超时 200ms
  • 库存服务超时 100ms
  • 价格服务超时 150ms

因为不同服务的重要性和稳定性不同。
超时策略应该是业务化的,不该一刀切。


4. 降级值要有业务语义

兜底不是随便写个 null-1 就完事。

例如:

  • 库存查不到:是否应该显示 0
  • 价格超时:是否可以显示“价格加载中”?
  • 推荐失败:返回空列表是否会误导用户?

建议:

  • 和产品/业务确认降级语义
  • 前后端约定清楚字段含义
  • 必要时增加 degraded=true 标记

5. 区分“可降级字段”和“不可降级字段”

不是所有字段都适合兜底。

例如:

  • 商品名称:通常不可缺失
  • 价格:很多场景不可错误展示
  • 推荐列表:通常可为空
  • 用户积分:某些页面可延迟加载

可执行建议:

  • 对核心字段使用 orTimeout + fail fast
  • 对非核心字段使用 completeOnTimeout + fallback

6. 补齐日志、指标和链路追踪

异步一多,排查难度会明显上升。建议至少做三件事:

  • 记录任务开始/结束/异常日志
  • 统计超时次数、异常次数、降级次数
  • 把 traceId 透传到异步线程中

如果没有这些可观测性,异步问题通常会变成“偶发、难复现、看不懂”。


7. 不要在异步链中做重阻塞操作

比如你本来是为了并行提速,结果在某个 thenApply 里又做了一次很重的同步阻塞调用,这样会让收益大打折扣。

常见反例:

future.thenApply(result -> {
    callRemoteServiceSynchronously();
    return convert(result);
});

如果后续步骤还是异步远程调用,应优先考虑 thenCompose


一个完整的生产思路图

把上面的建议串起来,大致是这样:

sequenceDiagram
    participant Client as Client
    participant API as 聚合接口
    participant TP as 业务线程池
    participant P as 商品服务
    participant I as 库存服务
    participant R as 价格服务

    Client->>API: 请求商品详情
    API->>TP: 提交商品任务
    API->>TP: 提交库存任务
    API->>TP: 提交价格任务

    TP->>P: 异步调用
    TP->>I: 异步调用
    TP->>R: 异步调用

    P-->>TP: 正常返回
    I-->>TP: 异常
    R-->>TP: 超时

    TP-->>API: 商品=正常值
    TP-->>API: 库存=默认值
    TP-->>API: 价格=默认值

    API-->>Client: 聚合结果(部分降级)

JDK 8 怎么办?

如果你还在 JDK 8,orTimeoutcompleteOnTimeout 没法直接用。通常有两个方案:

方案一:借助调度线程池 + 手动超时 Future

思路是:

  • 正常发起业务任务
  • 再用 ScheduledExecutorService 延迟触发一个超时 Future
  • applyToEither 谁先完成用谁

示意代码:

import java.util.concurrent.*;
import java.util.function.Supplier;

public class Jdk8TimeoutHelper {

    private static final ScheduledExecutorService SCHEDULER = Executors.newScheduledThreadPool(1);

    public static <T> CompletableFuture<T> withTimeout(
            Supplier<T> supplier,
            Executor executor,
            long timeout,
            TimeUnit unit,
            T fallback) {

        CompletableFuture<T> taskFuture = CompletableFuture.supplyAsync(supplier, executor);
        CompletableFuture<T> timeoutFuture = new CompletableFuture<>();

        SCHEDULER.schedule(() -> timeoutFuture.complete(fallback), timeout, unit);

        return taskFuture.applyToEither(timeoutFuture, t -> t)
                .exceptionally(ex -> fallback);
    }
}

这不是最优雅,但在 JDK 8 里很常见,也足够实用。

方案二:把超时控制下沉到 RPC/HTTP 客户端

其实很多时候更推荐这样做:

  • HTTP 客户端设置连接超时、读超时
  • RPC 框架设置调用超时
  • 数据库连接池设置查询超时

因为真正可靠的超时控制,最好离资源最近


常见排查路径

线上接口“偶发变慢”时,可以按下面顺序排查:

1. 看整体耗时分布

  • 是平均慢,还是 P99 慢?
  • 是所有接口慢,还是某个聚合接口慢?

2. 看线程池状态

重点关注:

  • 活跃线程数
  • 队列积压长度
  • 拒绝次数
  • 最大线程数是否长期打满

3. 看下游依赖耗时

异步编排只是把等待并行化,不会凭空让下游变快。
如果某个服务本身已经慢到 800ms,再漂亮的编排也救不了根因。

4. 看是否发生大面积降级

如果大量请求都走 fallback,虽然接口“没挂”,但业务效果可能已经显著下降。

5. 看超时是否只发生在聚合层

如果聚合层 300ms 就兜底,但底层连接还卡 3 秒,那说明:

  • 编排层超时有了
  • 底层资源超时没设好

这是非常典型的“看起来稳,其实在流血”。


总结

CompletableFuture 最值得掌握的,不是某个 API 名字,而是这套思路:

  1. 把独立调用并行化
  2. 为每个子任务设置超时
  3. 在子任务维度做异常兜底
  4. 最后统一汇总结果
  5. 线程池、日志、监控必须配套

如果你现在正准备在项目里落地,我建议先按这个最小闭环做:

  • 用自定义线程池
  • supplyAsync 发起多个独立调用
  • allOf 汇总
  • completeOnTimeoutorTimeout 控制时延
  • exceptionally 做明确降级
  • 打好异常日志和降级日志

最后给一个边界判断:

  • 如果只是少量异步聚合CompletableFuture 很合适
  • 如果已经出现复杂 DAG、批量任务编排、分布式工作流,那就要考虑更专业的编排方案,而不是无限堆 CompletableFuture

对于中型 Java 服务来说,CompletableFuture 往往是“投入产出比很高”的那一档技术:不重,但够实用;不玄学,但能明显提升响应性能和系统韧性。只要线程池、超时和兜底这三件事你处理对了,它就能很好地成为聚合接口的主力工具。


分享到:

上一篇
《从浏览器指纹到接口签名:一次中级 Web 逆向反爬参数还原实战》
下一篇
《Spring Boot 中基于 JWT 与 Spring Security 的前后端分离登录鉴权实战与权限设计》