发布于2026-07-10 阅读(0)
扫一扫,手机访问
说实话,上面那段总结已经讲得很透了:DeferredResult 能避免 Tomcat 线程阻塞,核心就在于它基于 Servlet 3.0 的异步规范。请求一进入 Controller,Tomcat 的工作线程(比如那个 nio-8080-exec-N)就会被立即释放,而不是死守在原地等业务逻辑跑完。Tomcat 会把请求“移交”给它的异步调度器,之后全凭你自己的业务线程去调用 setResult() 或者等它超时,直到那一刻,HTTP 响应才会真正写回去。

不过,很多朋友在实际动手时,经常被一些“隐形门槛”绊倒。比如最常见的一个误会:以为必须配上 Spring 的 @EnableAsync 才能用。其实 DeferredResult 根本不吃这一套,它只依赖 Servlet 容器是否开启了异步支持——好消息是,Spring Boot 默认就是开着的。你费了半天劲去配置那个线程池注解,结果只是白忙一场,卡在了错误的时间点上。
我们把这个过程拆开来看。传统逻辑是:请求来了,Tomcat 从自己的线程池(默认大小 200)里捞一个线程出来,全程死盯着业务代码,直到拿到结果才放手。这就带来了一个数学问题:如果每个长轮询要卡 30 秒,那 10 个并发请求,瞬间就会占用 300 秒的线程资源,线程池很快就打满了。下一个请求只能排队等着,系统性能直线下降。
DeferredResult 的做法就聪明得多。它让请求一进来就把线程释放回去,Tomcat 转手把请求交给异步调度器。业务跑完了,或者设定的超时时间到了,再去写回响应。这里有个细节必须小心:构造 DeferredResult 时传的超时时间,单位是毫秒。比如你写 new DeferredResult(5000L),那意思是 5 秒后超时。很多人手一抖填了 5000,心里想的是 5 秒,结果代码的行为变成了 5 毫秒后超时,完全不是想要的效果。另外,如果用了无参构造,那就是默认不超时——请求可能就这么永远挂在那儿,直到 JVM 重启或者客户端主动断开连接,这绝对是个要命的坑。
DeferredResult 对象怎么存,是个大问题。千万别因为图省事,就用一个 HashMap 放在全局。这不仅仅是线程安全的问题,更是一个原则性的设计缺陷——在集群环境下,一个 JVM 里的 DeferredResult 对象,另一个节点根本看不见它。即便是在单机场景,也要考虑内存泄漏这个定时冲击波。
正确的做法是用线程安全的集合,比如 CopyOnWriteArrayList 或 ConcurrentLinkedDeque。这样你在遍历的时候,就不用提心吊胆地怕碰到 ConcurrentModificationException。更重要的是,必须注册 onCompletion() 回调。这个回调会在请求结束(不管是因为成功、超时还是异常)时触发,你要在回调里把自己从集合中移除。否则,这个对象就会一直驻留在堆内存里,GC 也没办法回收,最终导致内存泄漏。这就像是公共储物柜,你用完得把钥匙还给管理员。
DeferredResultresult = new DeferredResult<>(5000L, "timeout");
result.onCompletion(() -> results.remove(result));
results.add(result);
前端的配合也不能马虎。长轮询不是简单的发一次 fetch() 请求就完事,它的本质是一个循环:请求→等待响应→收到后立刻发起下一个。其中任何一个环节出问题,整个链路都会断掉。
第一,每次请求都必须带上一个唯一标识,比如 requestId。服务端可以拿这个来去重或者做追踪。不然,如果用户同时开了多个标签页轮询,服务端调用 setResult() 时,很可能就把数据推给了错误的请求。
第二,前端的 AJAX 请求需要显式设置 timeout。建议设得比后端 DeferredResult 的超时时间略短一点,比如后端是 5000ms,前端就设 4500ms。这样能避免一种尴尬情况:前端还在傻等,后端已经超时返回了,导致一个空的轮询间隙。
第三,一定要监听 complete(jQuery里)或 finally(fetch 里),而不仅仅是 success 或 then。因为不管是超时、网络中断还是服务端返回了个 500 错误,你都需要触发下一轮请求,否则轮询循环就停下来了。
本地跑通只是第一步,生产环境往往是集群部署。DeferredResult 只活在自己的 JVM 里。用户 A 的请求落在了节点 X,但数据更新事件却发生在节点 Y。Y 节点上把数据更新了,但 X 节点里存的那个 DeferredResult 永远不会被 setResult(),请求就卡死了。这个问题的根源,在于路牌(请求状态)和消息(更新事件)不在同一个地方。
解决办法只有两条路,没有中间态:
一是用 Redis 的 Pub/Sub 或者消息队列(比如 Kafka)来做节点间的状态同步。节点 Y 更新数据后,广播一条消息出来。所有节点都去监听这个频道,收到消息后,遍历自己本地的 DeferredResult 集合,找到对应的那个实例,调用 setResult()。
二是干脆放弃在内存里存 DeferredResult,直接上一个统一的推送中心,比如 WebSocket 网关加上 Redis 订阅。这样 DeferredResult 就可以退居二线,只充当一个轻量级的兜底方案。
最后,千万别动那种“把 DeferredResult 存到 Redis 里序列化”的念头。这玩意儿是不可序列化的,会直接抛一个 NotSerializableException 给你看。归根结底,就算用了 Redis 广播,每个节点维护好自己的 DeferredResult 列表,并严格执行 onCompletion 里的清理动作,才是王道。否则,广播来了,本地的列表却因为没清理而膨胀,内存迟早被撑爆,系统也就离崩盘不远了。
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
正版软件
正版软件
正版软件
正版软件
正版软件
1
2
3
7
8