我们日常用Spring Boot写的RestController
,感觉上就是一个简单的方法,但它背后其实有一套复杂的网络服务在支撑。一个HTTP请求到底是怎么从用户的浏览器,穿过层层网络,最终抵达我们代码里的Controller方法的?理解这个过程,特别是Tomcat和Netty这两种主流服务器的处理方式,对我们写出更高性能的应用很有帮助。
Tomcat模型:一个请求,一个线程,简单直接
我们先聊聊大家最熟悉的Spring MVC on Tomcat组合。
Tomcat的NIO模型,本质上是一种高度优化的“单Reactor多线程”实现。它的内部角色分工很明确:有一小队Acceptor
线程,专门负责在门口迎接新的TCP连接。连接一旦建立,Acceptor
不会亲自服务,而是把这个连接交给Poller
线程。
Poller
线程也只有少数几个,它像个雷达,不断扫描所有已连接的通道,看看哪个通道上送来了数据。一旦发现某个连接有数据可读,Poller
也不会自己去读写,而是把这个“可读”的信号打包成一个任务,扔给一个庞大的Worker
线程池。
接下来就是重头戏了。Worker
线程池里的一个工作线程会领走这个任务,并且负责到底。这个线程会先去网络通道里把请求数据读出来,这个读取的过程是阻塞的。读完后,它把字节流解析成我们熟悉的HttpServletRequest
对象,然后请求就进入了Spring的处理流程,最终调用到我们的Controller方法。
这里最关键的一点是,我们写的业务代码,包括查询数据库、调用其他服务等所有操作,全都在这个Worker
线程上同步执行。执行完了,再由这个线程把响应数据写回给用户。所以,这种模式的好处是编程模型非常简单,写代码就像写单机程序一样,思路很直观。但缺点也很明显,系统的并发能力,直接受限于Worker
线程池的大小。
Netty模型:事件驱动,少数线程支撑海量连接
再来看看Spring WebFlux on Netty这个组合,它的玩法就完全不一样了。
Netty是标准的“主从Reactor多线程”架构。它也有一个专门负责接客的BossGroup
,通常就一个线程,工作很专一,只管接收连接,然后把连接转手扔给WorkerGroup
。
WorkerGroup
里包含了一组EventLoop
线程,数量通常和CPU核心数差不多。一个连接被分配给某个EventLoop
之后,这个连接的整个生命周期就和这个线程绑定了。从数据读取、解码成HttpRequest
对象,再到分发给WebFlux框架,所有事情都由这一个EventLoop
线程亲力亲为。
当请求最终到达我们的Controller方法时,代码依然是运行在这个EventLoop
线程上的。这就带来一个严格的约束:绝对不能有任何阻塞操作。因为一旦这个线程被阻塞,它负责的所有其他连接就全都动不了了。
所以,我们必须返回Mono
或Flux
这类响应式类型,并通过异步方式去调用下游。EventLoop
线程在发起数据库查询或者RPC调用后,会立即返回去处理其他连接上的事件,而不是原地等待。当下游服务返回结果时,会通过一个回调事件,重新唤醒这个EventLoop
线程,让它继续完成后续的数据处理和响应。这种事件驱动的模式,使得极少数的线程就能管理海量的并发连接。
核心差异与如何选择
为了更直观地看出差别,我们看一个表格。
特性维度 | Spring MVC on Tomcat | Spring WebFlux on Netty |
---|---|---|
线程模型 | 一个请求一个Worker线程 | 少数I/O线程处理所有连接 |
编程范式 | 同步、阻塞 | 异步、非阻塞 (响应式) |
资源占用 | 线程多,内存开销大 | 线程少,内存开销小 |
核心风险 | 线程池耗尽 | 阻塞I/O线程 |
适用场景 | 通用CRUD、CPU密集型业务 | 高并发、I/O密集型业务(如网关) |
这两种模型的性能差异,在处理I/O密集的场景时会被无限放大。比如,处理1000个并发请求,每个请求都要等待1秒的网络I/O。在Tomcat里,如果Worker
线程池大小是200,那200个线程会立刻被占满并阻塞,剩下的800个请求只能排队。而在Netty里,哪怕只有8个EventLoop
线程,也能轻松应对,因为它们从不等待。
那么,我们到底该怎么选?
其实很简单,看你的业务场景。如果你的应用是I/O密集型的,比如微服务网关、消息推送中台,需要用有限的服务器资源应对海量的并发连接,那么Netty/WebFlux是更好的选择,它能最大化系统吞吐能力。
反过来,如果你的应用是业务逻辑复杂、并发量可控的传统CRUD系统,那Tomcat/MVC的同步模型会让开发、调试和排查问题变得简单直观得多。在这种场景下,开发效率和可维护性的重要性,往往比压榨那一点硬件性能更重要。
总的来说,Tomcat用线程池隔离了阻塞,让编程更简单;Netty用事件驱动压榨硬件性能,让并发更高。两者没有绝对的优劣,理解它们背后的设计思想,才能在合适的场景做出正确的选择。