异步请求处理
本章聚焦 Spring MVC 的异步请求处理机制。在讲解拦截器、异常处理等同步流程之后,必须回答一个高并发场景下的关键问题:当业务逻辑耗时较长时,Servlet 容器线程被长期占用,如何在不增加硬件的情况下提升系统吞吐量? 理解 Callable、DeferredResult 和 WebAsyncTask 的区别与适用场景,是构建高性能 Web 应用的核心能力。
定义与作用
Spring MVC 的异步请求处理,本质上是把请求的处理线程与 Servlet 容器的线程解耦。在传统的同步模式下,一个 HTTP 请求从进入到响应,全程占用同一个 Tomcat 线程;如果业务逻辑耗时 3 秒,这个线程就被阻塞 3 秒。当并发量增大时,Tomcat 线程池很快耗尽,后续请求只能排队等待,系统吞吐量急剧下降。
异步模式的核心思想是:请求到达后,Servlet 容器线程立即释放,业务逻辑在另一个线程中执行;当结果就绪后,容器重新分配线程将响应写回客户端。这样,一个 Tomcat 线程可以在 3 秒内处理数十个请求的"握手和收尾"工作,而不是被一个请求独占。
生活类比:餐厅服务员与后厨
想象一家餐厅:
- 同步模式:服务员(Tomcat 线程)点完菜后站在厨房门口等,菜做好才端给客人。高峰期 20 个服务员全在厨房门口站着,新客人进门无人接待。
- 异步模式:服务员点完菜,把订单交给后厨(异步线程池),立刻回到前台接待新客人。菜做好后,后厨按铃(结果就绪),任意一个空闲服务员去端菜(重新分配线程写响应)。同样的 20 个服务员,可以接待数倍于同步模式的客人。
这个类比的关键在于:Tomcat 线程的瓶颈不是"业务执行",而是"等待业务执行"。异步处理把"等待"从 Tomcat 线程中剥离出去。
核心原理
同步 vs 异步请求处理对比
上图展示了两种模式的本质差异:
- 同步模式:Tomcat 线程 = 业务执行线程,耗时多久就阻塞多久
- 异步模式:Tomcat 线程只负责"接收请求"和"写回响应",业务执行交给独立线程池
- 吞吐量提升:同样的 200 个 Tomcat 线程,同步模式最多并发 200 个长耗时请求;异步模式可以并发数千个
Callable 执行流程
Callable<T> 是 Spring MVC 异步处理的最简单方式。Controller 方法返回 Callable<T>,Spring MVC 自动将其提交到 TaskExecutor 执行,执行完成后自动写回响应。
关键理解:
- Callable 的执行由 Spring MVC 内部的
TaskExecutor管理,开发者无需手动创建线程 - 容器线程释放后,HTTP 连接保持打开(挂起状态),等待异步结果
- 结果就绪后,DispatcherServlet 重新被调用,但此时已有结果,直接写响应即可
DeferredResult 长轮询场景
DeferredResult<T> 比 Callable<T> 更灵活——它允许在任意线程、任意时刻设置返回值,不限于 Spring MVC 管理的线程池。这使其非常适合消息推送、长轮询、事件驱动等场景。
DeferredResult 与 Callable 的核心区别:
| 特性 | Callable<T> | DeferredResult<T> |
|---|---|---|
| 执行线程 | Spring MVC 管理的 TaskExecutor | 任意线程(可外部触发) |
| 结果设置 | 由 Callable.call() 返回值自动设置 | 手动调用 setResult() / setErrorResult() |
| 超时控制 | 通过 WebAsyncTask 包装 | 构造时直接指定超时时间 |
| 典型场景 | 耗时数据库查询、外部 HTTP 调用 | 消息推送、长轮询、事件通知 |
| 线程来源 | 框架自动分配 | 开发者完全控制 |
WebAsyncTask:带超时控制的异步包装
WebAsyncTask<T> 是对 Callable<T> 的增强包装,提供了更细粒度的超时和异常处理配置:
WebAsyncTask 允许你配置:
- 超时时间:超过指定毫秒后自动返回超时响应
- 超时回调:超时后执行自定义逻辑(如记录日志、发送告警)
- 异常回调:Callable 执行抛出异常时的处理逻辑
- 自定义线程池:指定执行 Callable 的 TaskExecutor,与框架默认池隔离
完整示例
场景
飞翔科技的电商系统"飞购"即将上线秒杀活动。CTO 大翔预估峰值 QPS 可达 10 万,但 Tomcat 线程池只配置了 200 个线程。架构师白歌分析后发现,下单接口需要调用库存服务(平均耗时 800ms),如果 200 个线程全被阻塞,系统吞吐量上限只有 250 QPS,远远不够。
白歌决定引入 Spring MVC 异步处理,让 Tomcat 线程只负责请求接入和响应写出,库存查询放到独立线程池执行。
同步方案(问题暴露)
@RestController
@RequestMapping("/orders")
public class OrderController {
@Autowired
private InventoryService inventoryService;
// 同步方式:Tomcat 线程全程阻塞
@PostMapping("/sync")
public ResponseEntity<OrderResult> createOrderSync(@RequestBody OrderRequest request) {
// 线程在此阻塞 800ms
boolean hasStock = inventoryService.checkStock(request.getSkuId());
if (!hasStock) {
return ResponseEntity.ok(new OrderResult(false, "库存不足"));
}
// ... 创建订单逻辑
return ResponseEntity.ok(new OrderResult(true, "下单成功"));
}
}
问题分析:
- 小崔压测时发现,200 线程配置下,同步模式吞吐量稳定在 240 QPS 左右,与理论计算一致
- 李眉监控告警:线程池使用率 100%,大量请求排队等待,响应时间 P99 超过 5 秒
- 黄俪前端反馈:页面转圈 5 秒后报错,用户体验极差
Callable 异步方案
@RestController
@RequestMapping("/orders")
public class OrderController {
@Autowired
private InventoryService inventoryService;
// 异步方式:Tomcat 线程立即释放
@PostMapping("/async")
public Callable<ResponseEntity<OrderResult>> createOrderAsync(
@RequestBody OrderRequest request) {
return () -> {
// 这段逻辑在 TaskExecutor 的独立线程中执行
// Tomcat 线程已经释放,可以处理其他请求
boolean hasStock = inventoryService.checkStock(request.getSkuId());
if (!hasStock) {
return ResponseEntity.ok(new OrderResult(false, "库存不足"));
}
// ... 创建订单逻辑
return ResponseEntity.ok(new OrderResult(true, "下单成功"));
};
}
}
配置自定义线程池:
@Configuration
public class AsyncConfig implements AsyncConfigurer {
@Override
@Bean(name = "mvcTaskExecutor")
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(50);
executor.setMaxPoolSize(200);
executor.setQueueCapacity(1000);
executor.setThreadNamePrefix("mvc-async-");
executor.initialize();
return executor;
}
}
WebAsyncTask 超时控制方案
@RestController
@RequestMapping("/orders")
public class OrderController {
@Autowired
private InventoryService inventoryService;
@Autowired
@Qualifier("mvcTaskExecutor")
private ThreadPoolTaskExecutor taskExecutor;
@PostMapping("/async-with-timeout")
public WebAsyncTask<ResponseEntity<OrderResult>> createOrderWithTimeout(
@RequestBody OrderRequest request) {
// 超时时间 2 秒,使用自定义线程池
WebAsyncTask<ResponseEntity<OrderResult>> asyncTask =
new WebAsyncTask<>(2000, taskExecutor, () -> {
boolean hasStock = inventoryService.checkStock(request.getSkuId());
if (!hasStock) {
return ResponseEntity.ok(new OrderResult(false, "库存不足"));
}
return ResponseEntity.ok(new OrderResult(true, "下单成功"));
});
// 超时回调
asyncTask.onTimeout(() -> {
// 记录超时日志,发送监控告警
return ResponseEntity.status(HttpStatus.REQUEST_TIMEOUT)
.body(new OrderResult(false, "系统繁忙,请稍后重试"));
});
// 异常回调
asyncTask.onError(() -> {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new OrderResult(false, "系统异常"));
});
return asyncTask;
}
}
DeferredResult 消息推送方案
秒杀活动开始时,黄俪前端需要实时显示"已售数量"。白歌设计了一个长轮询接口,当库存变化时主动推送给客户端:
@RestController
@RequestMapping("/seckill")
public class SeckillController {
// 存储所有等待中的 DeferredResult,用于广播
private final Map<String, DeferredResult<ResponseEntity<StockUpdate>>>
waitingClients = new ConcurrentHashMap<>();
// 客户端订阅库存更新(长轮询)
@GetMapping("/stock-watch/{skuId}")
public DeferredResult<ResponseEntity<StockUpdate>> watchStock(@PathVariable String skuId) {
// 超时 30 秒,超时后客户端重新发起请求
DeferredResult<ResponseEntity<StockUpdate>> result =
new DeferredResult<>(30000L);
String clientId = UUID.randomUUID().toString();
waitingClients.put(clientId, result);
// 当结果设置或超时时,从等待列表移除
result.onCompletion(() -> waitingClients.remove(clientId));
result.onTimeout(() -> waitingClients.remove(clientId));
return result;
}
// 库存变化时由外部系统调用(如消息队列消费者)
public void onStockChanged(String skuId, int remainingStock) {
StockUpdate update = new StockUpdate(skuId, remainingStock);
// 遍历所有等待中的客户端,推送更新
waitingClients.forEach((clientId, deferredResult) -> {
if (!deferredResult.isSetOrExpired()) {
deferredResult.setResult(
ResponseEntity.ok(update)
);
}
});
}
}
变化分析:
- 引入 Callable 后,小崔压测显示同样 200 个 Tomcat 线程,吞吐量从 240 QPS 提升到 8000+ QPS
- WebAsyncTask 的超时机制防止了慢查询拖垮系统,超时请求返回友好提示而非无限等待
- DeferredResult 长轮询让前端实时感知库存变化,无需频繁轮询加重服务器负担
- 李眉的监控显示 Tomcat 线程池使用率稳定在 30% 以下,系统有余量应对突发流量
易错场景与面试考点
误区一:异步处理能加快单个请求的响应速度
错误认知:"用了异步,接口从 800ms 变成 100ms。"
纠正:异步处理不减少业务执行时间,单个请求的端到端耗时甚至可能因线程切换略有增加。异步的价值在于提升系统整体吞吐量——同样的线程资源可以并发处理更多请求。如果目标是降低单个请求的延迟,应该优化业务逻辑或引入缓存,而不是异步化。
误区二:异步处理不需要考虑线程池配置
错误认知:"Spring 会自动管理异步线程,我不用关心。"
纠正:Spring MVC 默认使用 SimpleAsyncTaskExecutor,每来一个请求就创建一个新线程,在高并发下会直接导致 OOM。生产环境必须配置自定义线程池:
// 错误:使用默认执行器
public Callable<String> bad() {
return () -> { /* ... */ }; // 默认 SimpleAsyncTaskExecutor,无上限创建线程
}
// 正确:配置有界线程池
@Bean(name = "mvcTaskExecutor")
public ThreadPoolTaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(50);
executor.setMaxPoolSize(200);
executor.setQueueCapacity(1000); // 队列满后拒绝策略生效
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
return executor;
}
误区三:DeferredResult 和 Callable 可以随意混用
错误认知:"两者都是返回异步结果,用哪个都一样。"
纠正:选择依据是谁控制结果的产生时机:
- 如果结果由 Controller 方法内部的逻辑产生(如数据库查询),用 Callable
- 如果结果由外部事件触发(如消息到达、另一个系统回调),用 DeferredResult
混用的典型错误:在 Callable 里阻塞等待外部事件——这违背了异步的初衷,线程池线程被阻塞,吞吐量提升有限。
面试高频:Callable 和 DeferredResult 的区别
标准回答:
- Callable 由 Spring MVC 自动提交到 TaskExecutor 执行,结果由
call()返回值自动提供,适合内部耗时操作 - DeferredResult 允许在任意线程、任意时刻手动设置结果,适合外部事件驱动场景(消息推送、长轮询)
- WebAsyncTask 是 Callable 的增强版,支持自定义线程池、超时控制、超时回调和异常回调
- 三者共同点:都会释放 Servlet 容器线程,提升系统吞吐量
面试高频:异步请求的完整流程
标准回答:
- 请求到达 DispatcherServlet
- HandlerAdapter 检测到 Controller 返回 Callable/DeferredResult/WebAsyncTask
- 启动异步处理,将任务提交到异步线程池(Callable/WebAsyncTask)或挂起等待外部设置(DeferredResult)
- 释放 Servlet 容器线程,HTTP 连接保持打开
- 异步任务执行完成或结果通过 setResult() 设置
- DispatcherServlet 重新被调用,分配新的容器线程
- 将结果写入响应,返回客户端
小结
Spring MVC 的异步请求处理通过 Callable、DeferredResult 和 WebAsyncTask 三种机制,将 Servlet 容器线程与业务执行线程解耦。它不加快单个请求的响应速度,但能成倍提升系统吞吐量,是高并发 Web 应用的必备技术。
核心选择依据:
- 内部耗时操作 → Callable
- 外部事件驱动 → DeferredResult
- 需要超时控制 → WebAsyncTask
本章与全局的关系:本章讲解了 Spring MVC 的异步扩展机制。下一章"内容协商"将深入讲解 Spring MVC 如何根据客户端偏好自动选择响应格式(JSON/XML/HTML),以及 ContentNegotiationManager 的决策流程。