基于本文回答

播面 播面

文图音视,全方位拆解八股文
0
评论

Spring MVC如何支持异步请求处理?

知识点图片

Spring MVC通过CallableDeferredResultSseEmitter支持异步请求。该机制释放容器线程,让后台处理耗时任务,避免阻塞,从而极大提升高并发应用的吞吐量和伸缩性。

我们来详细探讨一下 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 会识别出这是一个异步请求。

工作流程:

  1. 控制器返回 Callable:Controller 方法执行完毕并返回一个 Callable<String>(或其他类型)对象,此时方法体内的代码(创建Callable对象)是在容器的请求处理线程中执行的。
  2. 释放容器线程:Spring MVC 接收到 Callable后,调用 request.startAsync(),然后将这个 Callable 任务提交给一个由 Spring 管理的 TaskExecutor(一个线程池)去执行。此时,容器的请求处理线程被立即释放
  3. 后台线程执行TaskExecutor 中的一个后台线程执行 Callablecall() 方法中的耗时逻辑。
  4. 返回结果:当 call() 方法执行完毕并返回结果后,Spring MVC 会将请求重新分派回 Servlet 容器。
  5. 恢复处理并响应:容器获得一个新的线程(或使用原来的),用 Callable 的返回值继续执行后续的视图渲染或消息转换等操作,最终将响应发送给客户端。

代码示例:

java
@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 更大的灵活性。它代表一个“延迟的结果”,你可以在未来的某个时间点,从任何线程中为它设置结果。

工作流程:

  1. 控制器返回 DeferredResult:Controller 方法创建一个 DeferredResult 对象并返回。通常会把这个 DeferredResult 对象存放到一个内存队列、Map或其他地方,以便其他线程可以访问到它。
  2. 释放容器线程:与 Callable 类似,Spring MVC 看到 DeferredResult 后,会立即释放容器的请求处理线程,但保持连接开放。
  3. 外部事件触发:某个完全独立的线程(例如,一个消息队列监听器、一个定时任务、另一个用户的请求等)完成了某个任务。
  4. 设置结果:这个外部线程获取到之前存储的 DeferredResult 对象,并调用其 setResult() 方法。
  5. 恢复处理并响应:调用 setResult() 会触发请求被重新分派回 Servlet 容器,然后 Spring MVC 使用你设置的结果完成响应。

代码示例(经典的“长轮询”场景):

java
@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. ResponseBodyEmitterSseEmitter:流式响应

这两个类用于实现服务器向客户端流式地发送数据,而不是一次性返回所有数据。这对于发送大量数据或实现服务器推送(Server-Sent Events)非常有用。

  • ResponseBodyEmitter: 一个通用的、可以发送任意对象序列的 Emitter。Spring 会使用配置好的 HttpMessageConverter 将对象转换为字节流发送。
  • SseEmitter: ResponseBodyEmitter 的子类,专门用于实现 HTML5 的 Server-Sent Events (SSE) 规范,它会自动处理事件格式。

工作流程:

  1. Controller 方法返回一个 Emitter 对象。
  2. 容器线程被释放,但HTTP连接保持打开。
  3. 你在一个单独的线程中,可以多次调用 emitter.send() 方法来发送数据片段。
  4. 所有数据发送完毕后,调用 emitter.complete() 来关闭连接。
  5. 如果在处理过程中发生错误,可以调用 emitter.completeWithError()

代码示例 (SseEmitter)

java
@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 进行自定义配置。

java
@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 应用程序,有效地应对高并发挑战。

00:00
00:00