DeferredResult 使用指南
DeferredResult 是 Spring MVC 提供的异步 HTTP 响应机制。Controller 方法可以先返回一个「占位结果」,在稍后的任意线程中再调用 setResult() 完成真正的 HTTP 响应。
适用于「请求发起快、结果产出慢」的场景:等待第三方回调、消息队列响应、长轮询、跨服务异步通知等。HTTP 请求线程只负责「注册等待 + 触发业务」,真正的响应在异步事件到达后再写出。
为什么需要 DeferredResult
-
问题背景
以「提交任务并等待外部系统回调结果」为例,典型时序:
- 客户端调用 API 提交请求
- 服务端向外部系统发起调用(RPC、消息、Webhook 等)
- 外部系统异步处理
- 外部系统通过回调或事件通知服务端「处理完成」
- 服务端将结果返回给客户端
第 2~4 步往往需要数秒甚至更久。如果 Controller 在 Servlet 线程里同步阻塞等待,Tomcat 工作线程会被大量占用,高并发时容易线程池耗尽。
-
DeferredResult 做了什么
DeferredResult让 Spring MVC 在 Controller 返回后立即释放 Servlet 线程,但保持 HTTP 连接不关闭。后续任意线程调用setResult(data)时,Spring 会发起一次 ASYNC dispatch,将响应写回客户端。不需要在 Controller 里自己创建线程池 。
完成响应的线程可以来自消息监听、@Async 任务、定时任务、Netty 事件循环等已有异步来源。
-
请求生命周期
同步请求: request → Filter → Servlet → Controller → 响应 → 结束 DeferredResult: 第一次 dispatch: request → Filter → Controller → return DeferredResult → 释放 Servlet 线程 (HTTP 连接仍保持打开) 等待异步结果 ... 第二次 dispatch(ASYNC): 用已 set 的结果完成响应 → 结束
标准用法
-
基本模式
@GetMapping("/order/{orderId}") public DeferredResult<ResponseEntity<OrderResult>> getOrderResult( @PathVariable String orderId) { DeferredResult<ResponseEntity<OrderResult>> result = new DeferredResult<>(10_000L); // 10 秒超时 result.onTimeout(() -> result.setResult(ResponseEntity.status(504) .body(new OrderResult(orderId, "TIMEOUT", null)))); result.onError(throwable -> result.setResult(ResponseEntity.internalServerError().build())); orderService.submitAndWait(orderId, (code, data) -> { if (code == 0) { result.setResult(ResponseEntity.ok(data)); } else { result.setResult(ResponseEntity.badRequest().body(data)); } }); return result; // 立即返回,释放 Servlet 线程 } -
唤醒链路(通用)
HTTP 请求线程 → 创建 DeferredResult,注册到 Holder → 触发异步业务(发消息、调外部 API 等) → return result(释放 Tomcat 线程) 外部事件线程(MQ 消费者 / @Async / 回调接口 / 定时任务) → 业务处理完成 → holder.completeOne() 或 completeAll() → deferredResult.setResult() → HTTP 响应返回客户端唤醒
setResult()的线程,通常不是当初处理 HTTP 请求的那个线程 ,而是承载异步事件的那条线程。
API 说明
| 方法 | 说明 |
|---|---|
new DeferredResult<>(timeoutMs) |
创建实例,超时后触发 onTimeout |
setResult(value) |
设置成功结果,完成 HTTP 响应(仅可调用一次) |
setErrorResult(value) |
设置错误结果 |
onTimeout(Runnable) |
超时回调,应在此做资源清理 |
onCompletion(Runnable) |
无论成功、失败或超时,最终都会执行 |
onError(Consumer<Throwable>) |
设置结果过程中抛出异常时触发 |
常见误区
| 误区 | 事实 |
|---|---|
| 用了 DeferredResult 就必须在 Controller 里开线程池 | 不需要;任何线程调用 setResult() 即可 |
| 返回 DeferredResult 等于业务完全异步 | 仅「等待结果」异步;前置校验、DB 查询等仍占用 Servlet 线程 |
| 没有新建线程池就等于没用 | 异步完成可来自 MQ、回调、@Async、定时任务等 |
| 超时会自动取消后台任务 | onTimeout 只影响 HTTP 层;业务侧需自行实现取消逻辑 |
setResult 可以多次调用 |
只能调用一次,重复调用会抛 IllegalStateException |
线程池规划
一个后端服务tomcat有一个线程池,自己的某个业务(IO密集型)又存在一个线程池,另外的多个业务(CPU密集型)又需要很多个线程池。那服务启动的线程池岂不是很多?
全局来看,应该如何合理的规划一个服务的线程池数量呢?如何平衡性能?
核心原则:线程池按“阻塞类型”分组,而不是按“业务模块”分组。
IO阻塞时间越长,线程池可以(且应该)设置得越大。 这两者是严格的正相关关系。
- IO密集型业务(查DB、调RPC、读文件)→ 共用 1个大的IO线程池
- CPU密集型业务(加密、压缩、序列化)→ 共用 1个小的CPU线程池
- 特殊隔离业务(如关键支付链路、批量导出)→ 最多再开 1个独立池用于隔离