WebFlux 还在用,但选型的理由变了
虚拟线程把「高并发」这张牌从 WebFlux 手里拿走了。
2026 年,Spring Boot 4 把 spring.threads.virtual.enabled 默认拨到 true,Tomcat 和 Jetty 的请求处理线程、@Async 任务、定时任务全部默认走虚拟线程。这意味着一个写同步代码、用普通 JDBC 驱动的项目,在高并发场景下的吞吐量已经和 WebFlux 处在同一档位。而它不需要 Mono、Flux、flatMap,不需要把整条调用链写成异步风格,也不需要为「Mono<Void> 的空完成为什么不触发回调」这类问题花掉一个下午。
我自己的这套 CMS 就跑在 WebFlux + R2DBC 上。写这篇文章的时候它正在深圳的一台机器上接收请求。所以下面不是在替任何一方站台——我是在回答一个自己也得面对的问题:当初选 WebFlux 的那些理由,到今天还剩几条站得住。
已经失效的理由:高并发
2018 年前后选 WebFlux,最常见的理由是「平台线程太贵」。一个 Tomcat 线程占 1MB 栈内存,200 个并发连接就要 200 个线程,I/O 等得越久、线程浪费越多。WebFlux 用事件循环把并发模型翻了一面:少量线程、非阻塞 I/O、同进程能撑住数万连接。
虚拟线程改变了这个等式。JDK 21 正式引入、JDK 25 成为 LTS 之后,虚拟线程的栈不在 JVM 堆上、可以创建数百万个、I/O 阻塞时自动让出载体线程。一个虚拟线程等数据库返回的时候,它占用的资源接近于零——和 Reactor 把请求拆成多个回调的本质效果相同,但写出来是同步风格。
Spring Boot 4 把这个能力直接打开:不用改代码、不用换框架、不用把 @Controller 改成返回 Mono,只要 JDK 版本够,默认就走虚拟线程。阿里云的一篇迁移总结里把它列为第一个坑——「之前引入 Reactor/WebFlux 响应式框架的理由可能不再成立」。
如果你当初选 WebFlux 的理由是「扛住更多并发」,这个理由在 2026 年已经不成立了。 JDBC + 虚拟线程在同等硬件上能给出同一量级的吞吐,代码复杂度低一个数量级。
仍然成立的三条理由
一、流式响应:SSE 和 WebSocket
我的 CMS 里有一个 AI 助手,模型的回复是逐 token 流式输出的。这条链路用的是 Server-Sent Events——服务端持续往客户端推文本片段,直到回复结束。
WebFlux 处理 SSE 是原生的:Flux<ServerSentEvent<String>> 就是一个流,框架负责把每个元素推下去、处理背压、在客户端断开时取消上游订阅。整条链从 AI 模型的 HTTP 流式响应到浏览器的 EventSource,全程非阻塞。
虚拟线程 + Spring MVC 也能做 SSE——SseEmitter 早就存在。但它是一个命令式的、需要手动管理完成和超时的对象,没有背压概念,也没有「上游取消时自动通知下游」的链路。连接数少的时候看不出区别,连接数上去之后,手动管理的复杂度会追上甚至超过 Reactor 的声明式写法。
如果你的系统有流式输出需求——AI 回复、实时通知、数据推送——WebFlux 在这条路上的工具链仍然更完整。
二、R2DBC:数据库访问这一层没有虚拟线程等价物
这是最容易被忽略的一条。虚拟线程解决的是「一个请求占一个线程等 I/O」的问题,但它要求底下的驱动是阻塞的——虚拟线程在 socket.read() 上阻塞时才会让出载体线程。
R2DBC 不走这条路。它是真正的异步数据库驱动:连接获取、查询执行、结果集遍历全程非阻塞,不占任何线程等待数据库响应。在一个 WebFlux 项目里,从 HTTP 入口到数据库查询再回到 HTTP 响应,整条链路都不阻塞线程。
JDBC 配合虚拟线程也能做到「不浪费线程」,但两者有一个实际差异:R2DBC 的连接池可以在相同数量的物理连接上承载更高的并发查询,因为查询在等待数据库返回时不占线程、不占连接上的阻塞位。在高并发短查询的场景下(比如 CMS 的文章列表、分类查询),这个差异是可测量的。
更重要的是生态一致性:WebFlux 的 @Transactional 走的是 ReactiveTransactionManager,安全框架走的是 ReactiveSecurityContextHolder,整个请求上下文都在 Reactor 的 Context 里传递。如果数据库这一层换回 JDBC,你就得在虚拟线程和 Reactor 之间选一个统一的上下文传递机制——而两者不兼容。
三、背压:一个被低估的能力
背压(backpressure)是 Reactor 的核心概念:下游处理不过来时,能通知上游减速。在绝大多数 CRUD 场景里你用不到它——请求进来、查库、返回,速率是匹配的。
但在两类场景里背压是救命的:
- 数据导入/导出:一次导几万条记录,如果生产速度远大于消费速度,没有背压就会把内存撑爆。Reactor 的
Flux天然按订阅者的需求发数据。 - 流式 AI 调用:模型输出 token 的速度和下游处理(渲染、存储、审核)的速度不总是一致的。背压让整条链路在速率不匹配时不会崩溃,而是自动调节。
虚拟线程不提供背压。它能让你在高并发下不浪费线程,但无法让生产者和消费者自动协商速率。
已经在跑的 WebFlux 项目怎么办
先说结论:没有充分的理由不要迁。
迁移的成本不在 API 风格的转换——把 Mono<T> 改成 T 是机械操作,IDE 能帮大半。真正的成本在三处:
- R2DBC → JDBC:你得选一个 JDBC 连接池(HikariCP 是默认选择),配好虚拟线程下的参数。HikariCP 的
maximumPoolSize在虚拟线程下的推荐值和平台线程时代不同——虚拟线程能撑住更多并发等待,但连接池的物理连接数仍然受数据库上限约束。 - Reactor Context → ThreadLocal:WebFlux 用
Context传递请求级别的状态(用户身份、事务上下文、链路追踪),虚拟线程世界用ThreadLocal。两者的传递模型完全不同——Context 是显式的、跟着操作符走;ThreadLocal 是隐式的、跟着线程走。所有依赖 Context 的中间件、切面、工具类都要改。 - Spring AI 的流式调用:如果你的项目接了 Spring AI 的
Flux<String>流式响应(我的 CMS 就接了),这一层迁到虚拟线程后需要换成SseEmitter或类似的命令式流,调试和错误处理的方式都会变。
如果项目跑着没出问题、团队对 Reactor 已经熟悉,留在 WebFlux 上的维护成本低于迁移成本。 Reactor 目前没有正式的 EOL 时间表,Spring Framework 7 仍然完整支持 WebFlux。
新项目怎么选:一张判据表
| 场景 | 推荐 | 理由 |
|---|---|---|
| CRUD 为主、无流式需求 | MVC + 虚拟线程 | 代码简单、生态完整、调试方便 |
| 有 SSE / WebSocket 流式输出 | WebFlux | 原生流支持、背压、自动取消 |
| 数据导入导出、大批量流处理 | WebFlux | 背压防止内存溢出 |
| 已选定 R2DBC 做数据访问 | WebFlux | 生态一致性,换回 JDBC 代价大 |
| 团队对 Reactor 不熟悉 | MVC + 虚拟线程 | 学习成本是实打实的工期 |
| 高并发但无上述特征 | MVC + 虚拟线程 | 吞吐量已经够用,复杂度更低 |
最后一行值得展开说:「高并发但无上述特征」在 2024 年以前几乎一定选 WebFlux,在 2026 年应该选虚拟线程。这不是 WebFlux 变差了,是另一条路追上了。
一点收尾
我自己的选择是继续跑 WebFlux。不是因为高并发——那个理由确实已经失效了。是因为 AI 助手的流式回复需要 SSE、R2DBC 的非阻塞数据访问和 Reactor 的上下文传递已经深入整套代码、而迁移到虚拟线程的收益在可预见的请求量下几乎为零。
选型的理由变了,但留下来的理由反而比当初更硬了——因为它们不再是「高并发」这种泛泛的需求,而是和具体业务特征绑在一起的。
如果你的项目也在 WebFlux 上,不妨对着上面那张表过一遍:留下来的理由是哪几条,失效的是哪几条。想清楚之后,无论留还是迁,都比被社区舆论推着走要稳。
本文作者:Aryee 发布时间:2026-10-03 08:27
本文出处:ARYEE.cn 固定链接:https://www.aryee.cn/archives/webflux-virtual-threads-2026.html
转载或引用请连这一行一起带走;文中的代码与结论按当时环境成立,组件升级后请以官方文档为准。