Vert.x 比 Spring 快,到底快在哪?

很多人以为 Vert.x 快是因为"线程池调得好",其实差在架构哲学:Spring 传统模型是一个请求一个线程(阻塞),Vert.x 是事件循环 + 非阻塞。这篇把两套线程模型拆开讲,顺带说清 WebFlux 在哪、以及到底什么时候该用谁。

Vert.x 比 Spring 快,到底快在哪?

常听到一种说法:“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 为主、并发 < 1000Spring MVC注解丰富、开发快、团队熟
要 Spring 生态,但并发 > 5000Spring 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 后端这块踩过坑,欢迎在关于页找我聊。

奇妙感 本文采用署名-非商业性使用-相同方式共享协议,转载请注明出处与作者。

目录