deferredresult 通过 servlet 3.0 异步机制释放 tomcat 线程,避免阻塞;其超时单位为毫秒,需显式注册 oncompletion 回调清理资源,集群下应结合 redis 实现跨节点通知。

Spring MVC 中用 DeferredResult 实现长轮询,核心不是“让请求更快返回”,而是“不让 Tomcat 线程卡在等待上”。它把原本要占用 30 秒的请求线程,压缩成几毫秒就释放,腾出线程去接新请求,吞吐量自然提升。
为什么 DeferredResult 能释放容器线程
Tomcat 默认工作线程池(如 nio-8080-exec-1)是有限的(通常 200)。同步长轮询下,每个请求卡住 30 秒,10 个并发就占满 300 秒线程时间,极易打满。而 DeferredResult 基于 Servlet 3.0 异步规范,在 Controller 方法一返回就触发 asyncContext.startAsync(),立刻归还 Tomcat 线程;后续由业务线程(比如你自己配的线程池、消息监听器、定时任务等)调用 setResult() 或超时机制触发响应。
- 不依赖
@EnableAsync或 Spring 线程池配置,Servlet 容器异步支持开即可(Spring Boot 默认已开启) - 构造时传的超时值单位是毫秒,例如
new DeferredResult<string>(5000L)</string>表示 5 秒超时,填错成5就变成 5 毫秒超时 - 若不设超时(无参构造),请求可能永久挂起,直到客户端断连或 JVM 重启
如何安全存储和分发 DeferredResult 实例
不能用 static Map<string deferredresult></string> 直接存业务 ID,尤其在集群部署下完全失效——DeferredResult 只在当前 JVM 内有效,且存在内存泄漏风险。
Java Maven 项目审查工作流的通用输入处理。当 Java Maven 项目以 ZIP压缩包或 GitLab 仓库 URL 形式提供且需要...时使用。
- 单机场景推荐使用线程安全集合:如
ConcurrentLinkedDeque<deferredresult>></deferredresult>存待响应实例,避免遍历时发生ConcurrentModificationException - 必须注册
onCompletion()回调,在请求结束(成功、超时、异常)时显式移除自身,否则对象长期驻留堆中,GC 不掉 - 更稳妥的做法是生成一次性随机 token(如 UUID),作为 key 存入 Redis,并将
DeferredResult绑定到该 token;集群中其他节点通过 Redis 通知结果,实现跨节点唤醒
典型长轮询流程与关键回调
以“查支付结果”为例:前端发起 /queryOrderPayResult?orderId=123,后端不查库也不等,立即返回一个挂起的 DeferredResult,等支付回调到达或超时才响应。
-
onTimeout():超时后自动触发,应设置兜底值(如"timeout"),避免前端无限等待 -
onError():仅捕获onTimeout/onCompletion回调内部抛出的异常,不影响主流程;建议记录日志并设错误结果 -
onCompletion():无论何种方式结束(setResult、超时、异常),都会执行,是清理资源(如从集合移除、关闭资源)的唯一可靠时机
常见误用与规避建议
很多团队踩坑不是因为不会写,而是忽略了上下文生命周期和部署环境约束。
- 误以为要配
@EnableAsync才能用DeferredResult——其实完全无关,删掉多余配置可减少干扰 - 用
HashMap或普通ArrayList存DeferredResult,多线程操作导致ConcurrentModificationException或漏清理 - 忘记注册
onCompletion,或只在setResult处清理,导致超时/异常路径下内存持续增长 - 在单体应用里硬编码全局静态 Map,上线集群后发现请求永远收不到响应——本质是状态未共享
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南










