Spring Boot 4.0 异步编程:CompletableFuture 与虚拟线程
前言
订单详情页需要客户、库存、优惠券和风控四个结果。串行调用时,总耗时接近四段下游耗时之和;并行调用可以缩短等待,但也会放大线程占用、连接池压力和失败传播范围。
本章使用 Spring Boot 4 + Java 21,把四个模拟下游调用提交到虚拟线程,再用 CompletableFuture 聚合。任何任务超过整体时限,接口会返回明确的降级结果。
读者需要完成前面的 Spring Boot 4 和并发基础章节,源码位于 dvsusan/spring-boot4-demo。
最终结果
新增接口:
GET /api/v1/chapter7/async/orders/{orderNo}正常情况下,一次请求得到客户、库存、优惠券和风控四个部分。调用超时或任务异常时,degraded=true,不会把缺少字段的对象伪装成成功。

图 1:四个下游并行执行,最终由统一的 Join 和超时策略决定返回完整或降级结果。
第 1 步:创建虚拟线程执行器
Chapter7Configuration.java 中创建专用执行器:
@Bean(destroyMethod = "close")
public ExecutorService chapter7Executor() {
// 虚拟线程适合大量阻塞式下游调用,但不能绕过连接池和下游限流。
return Executors.newVirtualThreadPerTaskExecutor();
}每个任务启动一个新虚拟线程,用完由执行器关闭。项目中必须避免每次请求都 new 一个执行器,否则并发高时会不断创建和丢弃线程池对象。
虚拟线程降低的是线程等待成本,不会让数据库、HTTP 服务或 CPU 计算自动变快。
第 2 步:并行提交四个下游任务
OrderOverviewService 的核心逻辑:
CompletableFuture<CustomerInfo> customer = async(() -> {
sleep(35);
return new CustomerInfo("C1001", "小明", "138****8001");
});
CompletableFuture<InventoryInfo> inventory = async(() -> {
sleep(50);
return new InventoryInfo("P1001", 19, true);
});
CompletableFuture<CouponInfo> coupon = async(() -> {
sleep(45);
return new CouponInfo("NEW20", 20);
});
CompletableFuture<RiskInfo> risk = async(() -> {
sleep(30);
return new RiskInfo("LOW", true);
});这里的 sleep 用来模拟网络 I/O,只为让每一段的执行顺序可观察,不代表生产耗时。
封装方法会检查线程类型:
private <T> CompletableFuture<T> async(AsyncTask<T> task) {
return CompletableFuture.supplyAsync(() -> {
if (!Thread.currentThread().isVirtual()) {
throw new IllegalStateException("订单聚合任务必须运行在虚拟线程");
}
return task.run();
}, executor);
}这段检查让错误配置直接暴露。只在日志里看到 VirtualThread 不够,接口应该有明确验收条件。
第 3 步:对整组任务设置超时
try {
CompletableFuture.allOf(customer, inventory, coupon, risk)
.orTimeout(timeout.toMillis(), TimeUnit.MILLISECONDS)
.join();
return new OrderOverview(
orderNo,
customer.join(),
inventory.join(),
coupon.join(),
risk.join(),
false
);
} catch (CompletionException exception) {
return OrderOverview.unavailable(orderNo);
}orTimeout 作用于聚合 Future 的等待过程。即使外层已经超时,底层任务仍可能继续执行,因此真实 HTTP 客户端还必须设置连接超时和读取超时,不能只依赖聚合层。
第 4 步:启动并请求接口
mvn clean package -DskipTests
java -jar demo-web/target/demo-web-1.0.0-SNAPSHOT.jar \
--spring.profiles.active=chapter7 \
--server.port=18107
curl -sS \
http://localhost:18107/api/v1/chapter7/async/orders/SO1001预期结果结构:
{
"code": "0",
"data": {
"orderNo": "SO1001",
"customer": {"customerId": "C1001", "name": "小明"},
"inventory": {"productId": "P1001", "available": 19, "sellable": true},
"coupon": {"code": "NEW20", "discount": 20},
"risk": {"level": "LOW", "allowed": true},
"degraded": false
}
}实际测试确认 /api/v1/chapter7/async/orders/SO1001 返回 200,degraded=false,库存和风控字段完整。
第 5 步:运行自动化测试
mvn -pl demo-web -am \
-Dtest=Chapter7AdvancedIntegrationTest \
-Dsurefire.failIfNoSpecifiedTests=false \
test该测试类共 4 个用例,分别覆盖虚拟线程聚合、熔断降级、游标分页和 Spring AI 演示链路。本章相关断言要求聚合结果完整且 degraded=false。
虚拟线程不等于无限并发
不要因为创建虚拟线程便宜,就为一个订单拆分几十个任务。还要考虑:
- 下游服务能承受多少并发。
- HTTP 连接池和数据库连接池是否有限。
- 单个请求超时后,底层任务能否被取消。
- 是否需要限制每个用户或接口的并行任务数。
- 聚合接口是否真的需要所有字段。
虚拟线程更适合“有很多阻塞等待”的任务,例如远程 HTTP、文件访问和同步客户端调用。CPU 密集任务仍应使用有界平台线程池。
常见问题排查
| 现象 | 原因 | 处理方式 |
|---|---|---|
| 聚合接口仍然很慢 | 任务被提交到固定小线程池 | 确认使用虚拟线程执行器 |
| 请求超时后下游仍在运行 | 只设置了 CompletableFuture 超时 | 同时设置 HTTP/数据库客户端超时 |
| 线程数暴涨 | 每请求创建执行器或无界提交 | 复用执行器并限制任务数量 |
| 数据库连接耗尽 | 并行查询超过连接池容量 | 减少并行数或调整连接池与 SQL |
| 异常被吞掉 | 只调用 join() 没有识别 CompletionException | 显式返回降级状态并记录失败任务 |
练习题
增加“优惠券可选”的场景:优惠券失败不应该让整个订单详情降级,而客户、库存和风控失败仍然返回降级。验收标准是分别让四类下游失败,结果字段能准确表现哪些数据完整、哪些缺失。
总结
异步聚合的价值是缩短互不依赖任务的等待时间,生产可靠性仍来自超时、并发上限、下游隔离和明确降级。
下一篇为远程风控调用加入限流、重试和熔断,让失败在系统内部被控制。
上一节:接口幂等、签名与防重放
下一节:Resilience4j 容错
相关阅读:日志与链路追踪
返回课程目录
13 个企业级实战项目 · 源码 + 教程 + 答疑
简历包装指导AI Agent 实战高并发·微服务Offer 导向持续更新3000+ 同行交流1V1 答疑3 天包退
覆盖高并发、微服务、分布式与 AI Agent:100万QPS短链、企业智能知识库、商城微服务、SaaS点餐、秒杀、刷题小程序、代码生成器、AI Agent 等。券后 ¥129(原价 ¥159),不满意 3 天包退。
