场景:线程不是无限的
设想一个网关服务,每个请求要串行调用三个下游接口,每个下游平均耗时 100ms。用传统 Spring MVC 的"一请求一线程"模型,这个线程在 300ms 里几乎全程在阻塞等待 IO,CPU 啥也没干。Tomcat 默认 200 个工作线程,意味着你最多同时扛 200 个这样的请求,第 201 个开始排队。瓶颈不在 CPU,而在"线程"这种昂贵资源被白白挂起。
WebFlux 要解决的就是这个问题:用少量线程承载大量并发连接。它的核心命题是——既然线程大部分时间在等 IO,那就别让它等,IO 没好就去干别的,好了再回来。这就是响应式 + 非阻塞的底层动机。
机制一:Reactive Streams 与背压
WebFlux 建立在 Reactor 之上,而 Reactor 实现的是 Reactive Streams 规范。这套规范只有四个接口,但精髓在于背压(backpressure)。
普通的观察者模式是 push 的:生产者有数据就推给消费者。问题是如果生产者比消费者快,数据会堆积,要么撑爆内存,要么丢数据。Reactive Streams 改成了 pull-push 混合:消费者通过 Subscription.request(n) 告诉生产者"我现在只能处理 n 个",生产者最多推 n 个。消费速度由消费者掌控。
1 | public interface Subscriber<T> { |
类比:背压就像点餐时跟厨房说"先上两个菜",而不是厨房一口气把二十个菜全端上来摆不下。这是响应式相比"裸异步回调"最本质的工程价值。
机制二:事件循环与非阻塞
WebFlux 默认跑在 Netty 上。Netty 是 Reactor 模式的事件循环:少量(通常等于 CPU 核数)EventLoop 线程,每个线程绑定一个 selector,轮询多个 channel 上的 IO 事件。一个连接没数据可读时,它的线程不会阻塞在那个连接上,而是转去处理其它就绪的连接。
1 | 传统阻塞模型: 事件循环模型: |
这套模型的威力在于:线程数和连接数解耦了。4 个 EventLoop 线程理论上能处理上万个空闲连接,因为空闲连接不占线程。
但代价是一条铁律:绝对不能在 EventLoop 线程上做阻塞操作。一旦你在事件循环里调了 Thread.sleep、JDBC 同步查询、或 block(),这个 EventLoop 线程就被卡死,它负责的成百上千个连接全部停摆。这是 WebFlux 最致命也最常见的坑。
源码视角:一条响应式链路
WebFlux 的 Controller 返回的不是数据,而是 Mono(0-1 个元素)或 Flux(0-N 个元素)——它们是惰性的发布者,只有被订阅时才执行:
1 |
|
关键点:这段代码里没有任何线程在等待。findById 发出 DB 请求后立即返回,线程去干别的;DB 响应回来时,Reactor 触发 flatMap 里的逻辑继续往下走。整条链路被编排成一系列回调,由事件循环驱动。
谁来订阅?是 WebFlux 框架本身。框架拿到你返回的 Mono,订阅它,把最终结果写回 HTTP 响应。你永远不该自己调 .block() 把它变回阻塞——那等于把响应式的好处全扔了。
工程权衡:WebFlux 不是银弹
第一个误区:以为 WebFlux 一定更快。对于 CPU 密集型任务,响应式毫无优势,反而因为调度开销更慢。它的收益只体现在 IO 密集 + 高并发 场景。低并发下,MVC 的简单同步模型吞吐和延迟往往更好,还更好调试。
第二个权衡:整条链路必须全程非阻塞,否则全盘皆输。如果你的持久层还在用阻塞 JDBC,那就必须把它丢到独立的有界弹性线程池(Schedulers.boundedElastic())上,避免污染 EventLoop:
1 | Mono.fromCallable(() -> jdbcTemplate.queryForObject(sql, ...)) |
但这么一搞,本质上又退化成了"线程池等 IO",响应式的并发优势被打了折扣。真正纯响应式需要 R2DBC 这样的非阻塞驱动配套,生态成熟度要评估。
第三个,也是最痛的:调试和心智成本。响应式的异常栈是断裂的——错误发生在某个回调里,堆栈往往指向 Reactor 内部而非你的业务代码,需要靠 checkpoint() 或 onOperatorDebug() 来还原。ThreadLocal 在响应式里也基本失效(因为执行会跨线程跳转),依赖 MDC 日志、事务上下文的代码都要改用 Reactor Context 重写。团队的学习曲线是实打实的成本。
边界:什么时候该上 WebFlux
适合:网关、BFF、大量扇出调用下游、SSE/WebSocket 长连接推送、需要扛海量空闲连接的场景。
不适合:CPU 密集计算、强依赖阻塞型中间件且无非阻塞替代、团队不熟悉响应式且业务以 CRUD 为主。后者强上 WebFlux,大概率是用十倍的复杂度换来微不足道的性能,还埋下一堆 block() 隐患。
小结
WebFlux 的本质是用事件循环把线程和连接解耦,再用 Reactive Streams 的背压让快慢两端协调速度。它真正的价值在 IO 密集高并发场景下省线程,而不是无脑提速。用好它的前提是理解那条铁律——EventLoop 上不能阻塞——以及随之而来的全链路非阻塞改造、ThreadLocal 失效、断裂栈调试这些实打实的工程代价。选型时先问一句:我的瓶颈真的是线程被 IO 挂起吗?如果不是,简单的同步模型往往是更诚实的选择。