Vert.x 比 Spring 快,到底快在哪?
很多人以为 Vert.x 快是因为"线程池调得好",其实差在架构哲学:Spring 传统模型是一个请求一个线程(阻塞),Vert.x 是事件循环 + 非阻塞。这篇把两套线程模型拆开讲,顺带说清 WebFlux 在哪、以及到底什么时候该用谁。
常听到一种说法:“Vert.x 性能好,是因为它的线程池调得好。“这话只说对了一半,而且是最不重要的那一半。
Vert.x 真正快的原因,是”事件循环 + 非阻塞”这套架构,而不是线程池参数。 Spring 传统模型(Spring MVC + Tomcat)是一个请求占一个线程、线程被 I/O 阻塞;Vert.x 是一个 Event Loop 线程轮转处理成千上万个连接,永不阻塞。这是两种完全不同的设计哲学。下面把它们拆开。
Spring 传统模型:一个请求一个线程
Spring MVC 跑在 Tomcat 上,Tomcat 的 NIO Connector 负责接连接,但业务逻辑(Controller / Service)是阻塞式的——每个请求从进来到返回,独占一个线程。
Tomcat 线程池(默认 200 线程)
│
├─ 请求 A → 线程1:查 DB(阻塞)→ 调外部 API(阻塞)→ 返回
├─ 请求 B → 线程2:查 DB(阻塞)→ ...
└─ ... 最多 200 个并发,第 201 个排队
代码长这样,看起来很顺,但问题藏在 userRepository.findById(id) 这一行里——线程在这里被卡住,什么都干不了:
@GetMapping("/api/user/{id}")
public User getUser(@PathVariable String id) {
User user = userRepository.findById(id); // 线程阻塞,等 DB
String profile = httpClient.get(".../" + id); // 线程继续阻塞,等 HTTP
return user;
}
代价很直接:200 个线程 × 约 1MB 栈 = 200MB 只花在线程栈上;阻塞期间线程空转等 I/O;并发上限被线程数卡死。这是”线程-per-request”模型的天花板。
Vert.x 模型:Event Loop 永不阻塞
Vert.x 的线程分三类,分工明确:
- Event Loop 线程:数量少(通常等于 CPU 核心数),处理 I/O 事件和回调,绝对不能阻塞;
- Worker 线程:专门跑真正阻塞的代码(JDBC、同步文件 I/O 等);
- 内部线程:Vert.x 自己用。
核心约束就一条:Event Loop 上永远不阻塞。一个连接进来,读事件被快速处理,需要查库就丢给 Worker 线程池,Event Loop 立刻回去处理下一个连接。等 Worker 干完了,再回到 Event Loop 发响应。
public class VertxHandler implements Handler<RoutingContext> {
public void handle(RoutingContext ctx) {
dbClient.query("SELECT * FROM users WHERE id = ?", id, queryRes -> {
if (queryRes.succeeded()) {
webClient.get(80, "api.example.com", "/profile/" + id)
.send(httpRes -> {
if (httpRes.succeeded()) {
ctx.response().end(buildResponse(queryRes, httpRes));
}
});
}
});
// handle() 立即返回!Event Loop 已经去处理别的连接了
}
}
配置也不过几行:
VertxOptions options = new VertxOptions()
.setEventLoopPoolSize(Runtime.getRuntime().availableProcessors()) // 通常 = CPU 核数
.setWorkerPoolSize(20) // 处理阻塞操作的线程数,按实际调
.setInternalBlockingPoolSize(10);
为什么能扛住高并发
把 Event Loop 简化成伪代码,就明白它为什么”省”:
void run() {
while (true) {
int ready = selector.selectNow(); // 非阻塞,只看"就绪"的
if (ready > 0) {
for (SelectionKey key : selector.selectedKeys()) {
if (key.isReadable()) handleRead(key); // 快进快出,不阻塞
}
}
Runnable task;
while ((task = taskQueue.poll()) != null) {
task.run(); // 任务也必须"快",耗时的一律丢 Worker 池
}
// 需要阻塞?交给 Worker 线程池,Event Loop 自己绝不卡
}
}
关键点:没有线程创建/销毁开销,几乎没有上下文切换。Spring 那 200 个线程大部分时间在”等 I/O”时是被闲置浪费的;Vert.x 的 4 个 Event Loop 在等 I/O 的间隙一直在处理别的连接。所以同样是 1 万并发,Spring 卡在线程数上,Vert.x 轻松轮转。
量级参考(非严格压测,只说明趋势):同样简单接口,纯文本响应下 Spring MVC 约八千级 QPS、WebFlux 约三万级、Vert.x 约四万级;差距主要来自”阻塞 vs 非阻塞”和抽象层数。复杂业务里差距会缩小,但”非阻塞”在高并发长连接场景的优势是结构性的。
最容易踩的坑:在 Event Loop 里阻塞
Vert.x 最反直觉也最容易写错的一点:
vertx.eventBus().consumer("address", message -> {
// ❌ 致命:Event Loop 线程里直接调阻塞 DB,整个事件循环被卡死
String r = blockingDatabaseCall();
message.reply(r);
// ✅ 正确:丢给 Worker 线程池
vertx.executeBlocking(promise -> {
promise.complete(blockingDatabaseCall());
}, false, asyncResult -> { // false = 同一地址有序执行
message.reply(asyncResult.result());
});
});
executeBlocking 的第二参数 false 表示同一个消费者地址上的阻塞任务按顺序执行(避免竞态),true 则允许并发。我一般默认 false,除非确认任务间无依赖。
Spring 的现代化答案:WebFlux
说 Vert.x 快,不代表 Spring 没还手之力。Spring WebFlux 用 Reactor 做响应式,底层 Netty 也是事件循环,线程模型跟 Vert.x 同宗:
@GetMapping("/reactive/users/{id}")
public Mono<User> getUser(@PathVariable String id) {
return userRepository.findById(id) // R2DBC 非阻塞
.flatMap(user -> webClient.get()
.uri("/api/profile/{id}", id)
.retrieve().bodyToMono(String.class)
.map(profile -> { user.setProfile(profile); return user; }));
}
区别在生态和心智负担:WebFlux 你还在 Spring 全家桶里(Security、Data、Cloud 一套带走),但代码要改成 Mono / Flux 的响应式写法,有一定门槛;Vert.x 更底层、更”裸”,优化更彻底,但回调嵌套写多了就是”回调地狱”,得靠 Future / Promise 链或 RxJava 来抚平。
到底什么时候用谁
我自己的判断标准(结合在 FOLIO 认证模块上用 Vert.x 3.9.1 跑生产的经验):
| 场景 | 选什么 | 理由 |
|---|---|---|
| 传统企业应用、CRUD 为主、并发 < 1000 | Spring MVC | 注解丰富、开发快、团队熟 |
| 要 Spring 生态,但并发 > 5000 | Spring WebFlux | 响应式 + 全套 Spring 能力 |
| 实时推送、IM、IoT、大量长连接、极致吞吐 | Vert.x | 事件循环 + 非阻塞,长连接几乎不占线程 |
| 主体是传统业务,局部要扛高并发 | Spring MVC + Vert.x 混合 | 各管一段,别硬凑 |
混合架构很常见,也最务实:Spring 管 REST 和业务,Vert.x 在另一个端口专接 WebSocket / 实时推送,互不拖累。
@Configuration
public class Hybrid {
@Bean public Vertx vertx() {
return Vertx.vertx(new VertxOptions().setEventLoopPoolSize(4).setWorkerPoolSize(20));
}
@Bean public HttpServer vertxWs(Vertx vertx) {
return vertx.createHttpServer()
.webSocketHandler(this::handleWs) // 8081:实时
.listen(8081);
}
// 8080 仍交给 Spring MVC 管传统 REST
}
一句话收尾
Vert.x 不是”线程池调得更好”,而是用事件循环把”等 I/O”的空闲时间彻底消灭了——一个线程能服务上万个连接,而 Spring 传统模型为每个并发请求都押了一个被阻塞的线程。选型别盲信基准数字:中低并发、要快上线,Spring MVC 足够;高并发长连接、要榨性能,才上 Vert.x 或 WebFlux。如果你也在 Java 后端这块踩过坑,欢迎在关于页找我聊。
关注我