bigywater.tech
Spring · 48

异步请求:Callable 与 DeferredResult

返回 Callable 和 DeferredResult 到底发生了什么?拦截器为什么被调用两次?超时设成 100 毫秒为什么等了将近一秒?超时之后慢任务还会继续跑吗?异步到底省了什么?这篇在 Spring Boot 4.0.8 的 Spring MVC 7.0.9 上打印线程名和派发类型逐个验证。

2 次
一个 Callable 请求,拦截器 preHandle 的调用次数(REQUEST 和 ASYNC)
1161 → 1 ms
Tomcat 只有 2 个线程时,并发的慢请求期间一个快请求的等待时间(同步 → Callable)
984 ms
DeferredResult 只设了 100 ms 超时,实际最长等了多久

Callable

实测 · 本机执行输出
  GET /sync → 200 sync-done (363 ms)
     拦截器 preHandle [REQUEST] /sync 在 http-nio-9601-exec-2
     /sync 方法在 http-nio-9601-exec-2
     拦截器 postHandle [REQUEST]
     拦截器 afterCompletion [REQUEST]
  GET /callable → 200 callable-done (330 ms)
     拦截器 preHandle [REQUEST] /callable 在 http-nio-9601-exec-3
     /callable 控制器方法在 http-nio-9601-exec-3,返回 Callable
     拦截器 afterConcurrentHandlingStarted(原线程释放前)
     /callable 的 Callable 在 task-1 执行
     拦截器 preHandle [ASYNC] /callable 在 http-nio-9601-exec-4
     拦截器 postHandle [ASYNC]
     拦截器 afterCompletion [ASYNC]

原线程释放,Callable 在 task 线程跑,完成后以 ASYNC 重新派发。

DeferredResult

实测 · 本机执行输出
  请求 /deferred 发出 0.5 秒后,客户端还在等响应: True
  /deferred 最终 → 200 deferred-ok (总耗时 521 ms)
     拦截器 preHandle [REQUEST] /deferred 在 http-nio-9602-exec-1
     /deferred 控制器方法在 http-nio-9602-exec-1,返回 DeferredResult(id=d1)
     拦截器 afterConcurrentHandlingStarted(原线程释放前)
     /complete 在 http-nio-9602-exec-3 完成 DeferredResult(d1) → true
     拦截器 preHandle [ASYNC] /deferred 在 http-nio-9602-exec-4
     拦截器 postHandle [ASYNC]
     拦截器 afterCompletion [ASYNC]
  GET /from-timer → 200 from-timer-done (213 ms)
     拦截器 preHandle [REQUEST] /from-timer 在 http-nio-9602-exec-5
     /from-timer 控制器方法在 http-nio-9602-exec-5
     拦截器 afterConcurrentHandlingStarted(原线程释放前)
     my-timer 线程里 setResult
     拦截器 preHandle [ASYNC] /from-timer 在 http-nio-9602-exec-6
     拦截器 postHandle [ASYNC]
     拦截器 afterCompletion [ASYNC]

由任意线程、任意请求完成,谁来完成都可以。

超时

实测 · 本机执行输出
  GET /deferred-timeout?ms=500 → 200 timeout-fallback (1035 ms)
     onTimeout 回调在 http-nio-9603-exec-3
     onCompletion 回调在 http-nio-9603-exec-3
  GET /deferred-timeout-default → 500 [AsyncRequestTimeoutException] null (829 ms)
     onTimeout 回调在 http-nio-9603-exec-5
     @ExceptionHandler 收到 AsyncRequestTimeoutException 在 http-nio-9603-exec-5
  GET /callable-timeout → 500 [AsyncRequestTimeoutException] null (1828 ms)
     慢 Callable 的 sleep 被中断了(InterruptedException),线程 task-1
     @ExceptionHandler 收到 AsyncRequestTimeoutException 在 http-nio-9603-exec-7
  (等 3.2 秒再看慢 Callable 的下场)
     慢 Callable 的 sleep 被中断了(InterruptedException),线程 task-1

有兜底值就返回兜底值,没有就抛 AsyncRequestTimeoutException;超时的 Callable 被中断。

超时的精度

实测 · 本机执行输出
  DeferredResult 超时 100 ms,6 次(每次开始前错开一点时间)实际等了(ms):[984, 864, 687, 519, 333, 132]
  DeferredResult 超时 300 ms,6 次(每次开始前错开一点时间)实际等了(ms):[1009, 862, 676, 526, 327, 1158]
  DeferredResult 超时 700 ms,6 次(每次开始前错开一点时间)实际等了(ms):[997, 862, 1688, 1524, 1337, 1149]

实际等待时间像是对齐到 1 秒的节拍,亚秒级的超时靠不住。

异常

实测 · 本机执行输出
  GET /callable-ex → 500 [IllegalStateException] callable 里抛的 (46 ms)
     拦截器 preHandle [REQUEST] /callable-ex 在 http-nio-9605-exec-2
     拦截器 afterConcurrentHandlingStarted(原线程释放前)
     拦截器 preHandle [ASYNC] /callable-ex 在 http-nio-9605-exec-3
     @ExceptionHandler 收到 IllegalStateException 在 http-nio-9605-exec-3
     拦截器 afterCompletion [ASYNC]
  GET /deferred-ex → 500 [IllegalArgumentException] setErrorResult 设的 (115 ms)
     拦截器 preHandle [REQUEST] /deferred-ex 在 http-nio-9605-exec-4
     拦截器 afterConcurrentHandlingStarted(原线程释放前)
     拦截器 preHandle [ASYNC] /deferred-ex 在 http-nio-9605-exec-5
     @ExceptionHandler 收到 IllegalArgumentException 在 http-nio-9605-exec-5
     拦截器 afterCompletion [ASYNC]

异步里的异常在重新派发之后走异常处理器。

线程占用

实测 · 本机执行输出
  10 个并发 /sync(每个 300ms):总耗时 1.58 s,状态码都是 200:True;期间另一个 /inflight 等了 1161 ms
  Callable 实际执行在 8 个不同的线程上:['task-1', 'task-2', 'task-3', 'task-4', 'task-5', 'task-6', 'task-7', 'task-8']
  10 个并发 /block-sleep(Callable,每个 300ms):总耗时 0.64 s,状态码都是 200:True;期间另一个 /inflight 等了 1 ms

异步把等待从 Tomcat 线程挪到另一个池子,池子的大小同样会成为瓶颈。

分步演示

异步请求
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK 21.0.12(Homebrew) / Spring Boot 4.0.8(Spring Framework 7.0.9、Tomcat 11.0.24) / Spring Cloud 2025.1.3;用 javac 编译、java 直接运行,依赖由 Maven 从 Maven Central 解析;数字每次运行会有小幅抖动。用 Spring Boot 4.0.8 的 SpringApplication(spring-boot-starter-webmvc,内嵌 Tomcat 11.0.24,JDK 21 平台线程);拦截器是 AsyncHandlerInterceptor,每一行由应用自己打印线程名和 DispatcherType;并发测试把 server.tomcat.threads.max 和 min-spare 设为 2
    • 没有验证:WebAsyncTask 与自定义任务执行器;spring.threads.virtual.enabled=true 时 Callable 的行为(见第 60 篇);客户端中途断开时 DeferredResult 的处理;超时检测节拍的来源(只看到了 1 秒量级的现象,没有读 Tomcat 代码确认);反应式返回类型(Flux、Mono)在 MVC 里的支持;异步请求的 Filter 行为
    京ICP备2026067502号-1京公网安备11010502063487号