乐途乐途
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
  • 学习路径
  • 第1章 SpringMVC概述与DispatcherServlet

    • 本章导读:Spring MVC概述与DispatcherServlet
    • Spring MVC 是什么
    • MVC 设计模式
    • 前端控制器模式
    • DispatcherServlet
    • 核心组件协作
  • 第2章 控制器与请求映射

    • 本章导读:控制器与请求映射
    • Controller
    • RestController
    • RequestMapping
    • GetMapping
    • PostMapping
    • PutMapping
    • DeleteMapping
    • PathVariable
    • RESTful
    • 请求映射原理
  • 第3章 请求参数获取与转换

    • 本章导读:请求参数获取与转换
    • RequestParam
    • RequestBody
    • RequestHeader
    • CookieValue
    • Model
    • ModelAttribute
    • 数据绑定原理
    • 数据校验
  • 第4章 响应数据与视图解析

    • 本章导读:响应数据与视图解析
    • ResponseBody
    • ResponseEntity
    • ModelAndView
    • ViewResolver
    • HttpMessageConverter
    • forward与redirect
  • 第5章 拦截器过滤器与跨域

    • 本章导读:拦截器、过滤器与跨域
    • HandlerInterceptor
    • WebMvcConfigurer
    • CrossOrigin
    • 登录验证实战
  • 第6章 文件上传与异常处理

    • 本章导读:文件上传与异常处理
    • MultipartFile
    • 文件下载
    • ExceptionHandler
    • ControllerAdvice
    • RestControllerAdvice
    • ResponseStatus
  • 第7章 高级特性与最佳实践

    • 本章导读:高级特性与最佳实践
    • SessionAttributes
    • SessionAttribute
    • RedirectAttributes
    • MockMvc测试
    • 国际化
    • 最佳实践
  • 第8章 扩展与异步机制

    • 本章导读:扩展与异步机制
    • 异步请求处理
    • 自定义参数解析器
    • 内容协商

异步请求处理

本章聚焦 Spring MVC 的异步请求处理机制。在讲解拦截器、异常处理等同步流程之后,必须回答一个高并发场景下的关键问题:当业务逻辑耗时较长时,Servlet 容器线程被长期占用,如何在不增加硬件的情况下提升系统吞吐量? 理解 Callable、DeferredResult 和 WebAsyncTask 的区别与适用场景,是构建高性能 Web 应用的核心能力。


定义与作用

Spring MVC 的异步请求处理,本质上是把请求的处理线程与 Servlet 容器的线程解耦。在传统的同步模式下,一个 HTTP 请求从进入到响应,全程占用同一个 Tomcat 线程;如果业务逻辑耗时 3 秒,这个线程就被阻塞 3 秒。当并发量增大时,Tomcat 线程池很快耗尽,后续请求只能排队等待,系统吞吐量急剧下降。

异步模式的核心思想是:请求到达后,Servlet 容器线程立即释放,业务逻辑在另一个线程中执行;当结果就绪后,容器重新分配线程将响应写回客户端。这样,一个 Tomcat 线程可以在 3 秒内处理数十个请求的"握手和收尾"工作,而不是被一个请求独占。

生活类比:餐厅服务员与后厨

想象一家餐厅:

  • 同步模式:服务员(Tomcat 线程)点完菜后站在厨房门口等,菜做好才端给客人。高峰期 20 个服务员全在厨房门口站着,新客人进门无人接待。
  • 异步模式:服务员点完菜,把订单交给后厨(异步线程池),立刻回到前台接待新客人。菜做好后,后厨按铃(结果就绪),任意一个空闲服务员去端菜(重新分配线程写响应)。同样的 20 个服务员,可以接待数倍于同步模式的客人。

这个类比的关键在于:Tomcat 线程的瓶颈不是"业务执行",而是"等待业务执行"。异步处理把"等待"从 Tomcat 线程中剥离出去。


核心原理

同步 vs 异步请求处理对比

上图展示了两种模式的本质差异:

  1. 同步模式:Tomcat 线程 = 业务执行线程,耗时多久就阻塞多久
  2. 异步模式:Tomcat 线程只负责"接收请求"和"写回响应",业务执行交给独立线程池
  3. 吞吐量提升:同样的 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 的区别

标准回答:

  1. Callable 由 Spring MVC 自动提交到 TaskExecutor 执行,结果由 call() 返回值自动提供,适合内部耗时操作
  2. DeferredResult 允许在任意线程、任意时刻手动设置结果,适合外部事件驱动场景(消息推送、长轮询)
  3. WebAsyncTask 是 Callable 的增强版,支持自定义线程池、超时控制、超时回调和异常回调
  4. 三者共同点:都会释放 Servlet 容器线程,提升系统吞吐量

面试高频:异步请求的完整流程

标准回答:

  1. 请求到达 DispatcherServlet
  2. HandlerAdapter 检测到 Controller 返回 Callable/DeferredResult/WebAsyncTask
  3. 启动异步处理,将任务提交到异步线程池(Callable/WebAsyncTask)或挂起等待外部设置(DeferredResult)
  4. 释放 Servlet 容器线程,HTTP 连接保持打开
  5. 异步任务执行完成或结果通过 setResult() 设置
  6. DispatcherServlet 重新被调用,分配新的容器线程
  7. 将结果写入响应,返回客户端

小结

Spring MVC 的异步请求处理通过 Callable、DeferredResult 和 WebAsyncTask 三种机制,将 Servlet 容器线程与业务执行线程解耦。它不加快单个请求的响应速度,但能成倍提升系统吞吐量,是高并发 Web 应用的必备技术。

核心选择依据:

  • 内部耗时操作 → Callable
  • 外部事件驱动 → DeferredResult
  • 需要超时控制 → WebAsyncTask

本章与全局的关系:本章讲解了 Spring MVC 的异步扩展机制。下一章"内容协商"将深入讲解 Spring MVC 如何根据客户端偏好自动选择响应格式(JSON/XML/HTML),以及 ContentNegotiationManager 的决策流程。

上一页
本章导读:扩展与异步机制
下一页
自定义参数解析器