Spring MVC如何支持异步请求处理?
Spring MVC通过
Callable、DeferredResult和SseEmitter支持异步请求。该机制释放容器线程,让后台处理耗时任务,避免阻塞,从而极大提升高并发应用的吞吐量和伸缩性。
我们来详细探讨一下 Spring MVC 如何支持异步请求处理。这是一个非常重要的特性,对于构建高并发、高吞吐量的Web应用至关重要。
1. 为什么需要异步请求处理?(解决了什么问题)
在传统的同步请求模型(Thread-Per-Request)中,Web容器(如 Tomcat)会为每个进来的HTTP请求分配一个线程。这个线程会负责处理从接收请求到返回响应的整个过程。
问题在于: 如果控制器(Controller)中的业务逻辑需要执行一个耗时的操作,比如:
- 调用一个响应缓慢的远程API。
- 执行复杂的数据库查询。
- 进行文件I/O操作。
那么,这个请求处理线程就会被阻塞(Block),直到耗时操作完成。在高并发场景下,这会导致容器的线程池被迅速耗尽。一旦所有线程都被阻塞的请求占用,新的请求就只能排队等待,甚至被拒绝,从而导致整个应用吞吐量下降,响应时间变长。
异步请求处理的核心思想就是:当遇到耗时操作时,立即释放容器的请求处理线程,让它可以去处理其他新的请求。同时,将耗时的业务逻辑交给一个后台线程(或线程池)去执行。当后台任务完成后,再通过某种机制通知容器,并将最终结果返回给客户端。
这样,少量的容器线程就可以应对大量的并发请求,极大地提高了服务器的伸缩性和资源利用率。
2. Spring MVC 异步支持的基石:Servlet 3.0+
Spring MVC的异步功能是建立在 Servlet 3.0 规范 引入的异步处理能力之上的。Servlet 3.0 允许你在一个请求处理过程中,通过调用 request.startAsync() 方法来启动异步模式。一旦进入异步模式,原始的容器线程就可以被释放,而HTTP连接会保持打开状态,等待后续的响应写入。
Spring MVC 在此基础上提供了更高层次、更易于使用的抽象。
3. Spring MVC 实现异步请求处理的核心机制
Spring MVC 主要提供了以下几种方式来实现异步请求处理:
a. Callable<V>:最简单的方式
Callable 是一个标准的 Java 并发接口,它代表一个可以返回结果的任务。当你的 Controller 方法返回一个 Callable 对象时,Spring MVC 会识别出这是一个异步请求。
工作流程:
- 控制器返回
Callable:Controller 方法执行完毕并返回一个Callable<String>(或其他类型)对象,此时方法体内的代码(创建Callable对象)是在容器的请求处理线程中执行的。 - 释放容器线程:Spring MVC 接收到
Callable后,调用request.startAsync(),然后将这个Callable任务提交给一个由 Spring 管理的TaskExecutor(一个线程池)去执行。此时,容器的请求处理线程被立即释放。 - 后台线程执行:
TaskExecutor中的一个后台线程执行Callable的call()方法中的耗时逻辑。 - 返回结果:当
call()方法执行完毕并返回结果后,Spring MVC 会将请求重新分派回 Servlet 容器。 - 恢复处理并响应:容器获得一个新的线程(或使用原来的),用
Callable的返回值继续执行后续的视图渲染或消息转换等操作,最终将响应发送给客户端。
代码示例:
@RestController
public class CallableController {
@GetMapping("/callable")
public Callable<String> processCallable() {
System.out.println("主线程 [" + Thread.currentThread().getName() + "] 进入控制器方法");
Callable<String> callable = () -> {
System.out.println("后台线程 [" + Thread.currentThread().getName() + "] 开始执行耗时任务");
Thread.sleep(2000); // 模拟耗时操作
System.out.println("后台线程 [" + Thread.currentThread().getName() + "] 完成耗时任务");
return "Task finished after 2 seconds!";
};
System.out.println("主线程 [" + Thread.currentThread().getName() + "] 返回Callable对象");
return callable;
}
}
适用场景:当你需要将一个完整的、独立的耗时任务异步化时,Callable 是最直接的选择。
b. DeferredResult<T>:更灵活、更强大的方式
DeferredResult 提供了比 Callable 更大的灵活性。它代表一个“延迟的结果”,你可以在未来的某个时间点,从任何线程中为它设置结果。
工作流程:
- 控制器返回
DeferredResult:Controller 方法创建一个DeferredResult对象并返回。通常会把这个DeferredResult对象存放到一个内存队列、Map或其他地方,以便其他线程可以访问到它。 - 释放容器线程:与
Callable类似,Spring MVC 看到DeferredResult后,会立即释放容器的请求处理线程,但保持连接开放。 - 外部事件触发:某个完全独立的线程(例如,一个消息队列监听器、一个定时任务、另一个用户的请求等)完成了某个任务。
- 设置结果:这个外部线程获取到之前存储的
DeferredResult对象,并调用其setResult()方法。 - 恢复处理并响应:调用
setResult()会触发请求被重新分派回 Servlet 容器,然后 Spring MVC 使用你设置的结果完成响应。
代码示例(经典的“长轮询”场景):
@RestController
public class DeferredResultController {
// 用一个队列来保存所有等待结果的DeferredResult
private final Queue<DeferredResult<String>> responseQueue = new ConcurrentLinkedQueue<>();
// 客户端请求,等待结果
@GetMapping("/deferred-result")
public DeferredResult<String> getUpdate() {
// 设置超时时间为5秒,超时返回指定消息
DeferredResult<String> deferredResult = new DeferredResult<>(5000L, "Request timeout.");
// 当请求完成、超时或出错时,将deferredResult从队列中移除
deferredResult.onCompletion(() -> responseQueue.remove(deferredResult));
deferredResult.onTimeout(() -> System.out.println("A request timed out."));
responseQueue.add(deferredResult);
return deferredResult;
}
// 另一个请求或后台任务,用于设置结果
@GetMapping("/set-result")
public String setResult(@RequestParam String message) {
// 遍历所有等待的请求,并为它们设置结果
for (DeferredResult<String> result : responseQueue) {
result.setResult("New update: " + message);
}
return "Result set for all waiting requests.";
}
}
Callable vs DeferredResult 的关键区别:
- 任务执行方:
Callable的任务由 Spring MVC 自动提交给TaskExecutor执行。而DeferredResult的结果是由应用中的其他线程在未来的某个时刻主动设置的。 - 控制权:
Callable是一种“委托”模式,你把任务交给Spring去执行。DeferredResult是一种“承诺”模式,你承诺未来会给出一个结果,但何时、何地、由谁给出,完全由你自己的应用逻辑决定。
适用场景:长轮询(Long-Polling)、等待外部事件(如消息队列、支付回调)、实现聊天室等需要服务器端事件驱动的场景。
c. ResponseBodyEmitter 和 SseEmitter:流式响应
这两个类用于实现服务器向客户端流式地发送数据,而不是一次性返回所有数据。这对于发送大量数据或实现服务器推送(Server-Sent Events)非常有用。
ResponseBodyEmitter: 一个通用的、可以发送任意对象序列的 Emitter。Spring 会使用配置好的HttpMessageConverter将对象转换为字节流发送。SseEmitter:ResponseBodyEmitter的子类,专门用于实现 HTML5 的 Server-Sent Events (SSE) 规范,它会自动处理事件格式。
工作流程:
- Controller 方法返回一个
Emitter对象。 - 容器线程被释放,但HTTP连接保持打开。
- 你在一个单独的线程中,可以多次调用
emitter.send()方法来发送数据片段。 - 所有数据发送完毕后,调用
emitter.complete()来关闭连接。 - 如果在处理过程中发生错误,可以调用
emitter.completeWithError()。
代码示例 (SseEmitter)
@RestController
public class SseController {
@GetMapping(path = "/sse-stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter streamEvents() {
SseEmitter emitter = new SseEmitter(Long.MAX_VALUE); // 设置一个很长的超时时间
// 在一个新线程中异步地发送事件
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.execute(() -> {
try {
for (int i = 0; i < 10; i++) {
SseEventBuilder event = SseEmitter.event()
.id(String.valueOf(i))
.name("message")
.data("Event data #" + i);
emitter.send(event);
Thread.sleep(1000);
}
emitter.complete(); // 所有事件发送完毕,关闭连接
} catch (Exception ex) {
emitter.completeWithError(ex);
}
});
executor.shutdown();
return emitter;
}
}
适用场景:实时数据推送、股票行情、在线聊天、日志监控、进度条更新等。
4. 如何配置异步支持
通常,默认配置就可以工作。但你也可以通过 WebMvcConfigurer 进行自定义配置。
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
// 设置默认的超时时间(毫秒)
configurer.setDefaultTimeout(30000);
// 配置一个自定义的线程池,用于处理Callable
configurer.setTaskExecutor(mvcTaskExecutor());
// 注册Callable和DeferredResult的拦截器
// configurer.registerCallableInterceptors(...);
// configurer.registerDeferredResultInterceptors(...);
}
@Bean
public ThreadPoolTaskExecutor mvcTaskExecutor() {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
taskExecutor.setCorePoolSize(10);
taskExecutor.setMaxPoolSize(50);
taskExecutor.setQueueCapacity(20);
taskExecutor.setThreadNamePrefix("mvc-task-");
taskExecutor.initialize();
return taskExecutor;
}
}
总结
| 机制 | 核心思想 | 结果来源 | 适用场景 |
|---|---|---|---|
Callable<V> |
将一个完整的耗时任务委托给后台线程池执行。 | 任务执行完成后,由 Spring MVC 自动获取并返回。 | 简单的异步任务,如调用外部API、复杂计算等。 |
DeferredResult<T> |
"我承诺稍后会给出结果",结果由应用逻辑在任意线程中设置。 | 由应用中的其他线程(非 Spring 管理的任务线程)主动调用 setResult()。 |
长轮询、等待外部系统事件(消息队列、回调)、跨请求通信。 |
ResponseBodyEmitter / SseEmitter |
允许在一个HTTP响应中,分多次、流式地向客户端发送数据。 | 由应用中的其他线程多次调用 send() 方法。 |
实时数据流、服务器推送(SSE)、进度更新。 |
通过这些强大的异步处理机制,Spring MVC 能够帮助开发者轻松构建出高性能、高伸缩性的现代 Web 应用程序,有效地应对高并发挑战。