Vert.x 这个框架我断断续续用过两三年,从最开始拿它写内部工具,到后来在几个正经项目里落地,踩过的坑不算少,但总体体验是:这东西一旦理解了它的线程模型和事件驱动思路,写出来的服务在并发和资源占用上,比传统 Spring Boot 那套舒服太多。这篇入门文章我不打算给你堆概念,就按我自己当时的学习路径来,从零搭一个带 HTTP 接口、事件总线通信、MySQL 查询的 Vert.x 应用,代码全贴,注释写清楚,你照着敲一遍基本就能上手。
1. Vert.x 到底是什么,为什么值得学
1.1 它不是一个 Web 框架,而是一整套异步工具包
很多人第一次听说 Vert.x,第一反应是"又一个 Java Web 框架"。这么理解不算全错,但会严重限制你的想象力。Vert.x 官方给自己的定位是 Polyglot 异步编程工具包,也就是说它不只是用来写 HTTP 接口的,它能做 TCP/UDP 通信、消息队列消费、定时任务、分布式事件总线、反应式数据库访问、甚至内嵌一个完整的 Web 服务器。你完全可以用它写一个纯粹的 WebSocket 推送服务,也可以用它做 Kafka 的消费者,或者把它当成高性能网关的内核。
我自己最直观的感受是:用 Spring Boot 写一个服务,你要么用 Servlet 的阻塞模型,要么额外引入 WebFlux 和 Reactor,学习成本不低。Vert.x 的写法天然就是事件驱动的,你写的是回调、Future、RxJava 或者协程,这几套 API 它都有对应封装。简单说,一个 Vert.x 应用就是一组在 Event Loop 线程上运行的处理器,每个处理器通过事件总线互相通信,这种架构让它在高并发下的线程开销非常小。
1.2 核心线程模型:Event Loop 到底是怎么转的
Vert.x 的线程模型是它的灵魂。每个 Vert.x 实例默认创建 CPU 核心数(乘以一定系数)的 Event Loop 线程,这些线程是单线程模型,内核是 Netty 的 EventLoop。你的所有业务代码——HTTP 请求处理、数据库回调、定时任务回调——绝大多数都跑在这几个线程上。所谓单线程模型,就是同一个 Event Loop 上不会同时执行两个处理器,所以你不需要像传统多线程编程那样加锁。
这里有个特别需要注意的点:既然 Event Loop 线程不能阻塞,那你的业务代码里绝对不能出现 Thread.sleep、同步 JDBC 查询、死循环、大文件同步读取这类操作。一旦某个处理器把 Event Loop 线程占住,整个 Vert.x 实例上跑的其他请求全部排队等着,性能瞬间崩塌。我见过不少新手写 Vert.x 代码时顺手在处理器里调了个同步的 Redis 客户端,结果压测一上来 CPU 跑满但 QPS 几乎为零。
正确做法是:需要阻塞的操作(比如同步第三方 SDK、磁盘 IO)丢到 Worker 线程池去执行,Vert.x 提供了 executeBlocking 方法专门干这个事;需要异步的操作(HTTP 调用、数据库异步驱动、Redis 异步客户端)直接用回调,让 Event Loop 线程赶紧腾出来处理下一个事件。理解了这一点,后面所有代码你都能看明白它为什么那么写。
1.3 适合谁看,看完能到什么水平
这篇教程适合有一年以上 Java 经验、熟悉 Maven 和 IDEA 基本操作、了解 Lambda 表达式和函数式接口的读者。如果你写过 Netty 或者 NIO 相关代码会更好理解,但不是必须。另外,如果你对 Reactive Streams 或者响应式编程有一点概念,哪怕只是听说过 Flux 和 Mono,学 Vert.x 会顺畅很多。
看完这篇,你能掌握:搭建一个 Vert.x 工程、用 Router 实现 RESTful 接口、用 Event Bus 在不同业务模块之间发消息、用异步客户端操作 MySQL、处理 Json 数据、以及排查最常见的坑。这些足够你独立写一个简单的后端服务了。说实话,Vert.x 的上手门槛没有网上说的那么高,你不需要先学完整个 Reactor 生态才能动手,把最基本的 HTTP 和 Event Bus 跑通,其他的遇到再查就行。
2. 工程搭建与依赖选型
2.1 用一个最简单的 Maven 工程起步
我先说下我推荐的工程结构,不是必须严格照抄,但按照这个来,后面加模块、加代码都会比较舒服:
vertx-demo ├── pom.xml └── src └── main ├── java │ └── com │ └── demo │ ├── MainVerticle.java │ ├── HttpServerVerticle.java │ ├── DatabaseVerticle.java │ └── service │ └── UserService.java └── resources └── config.jsonpom.xml 里只需要引入一个核心依赖,Vert.x 的模块是拆分的,但大部分场景下 vertx-core 加 vertx-web 就够了。下面是我常用的依赖版本,建议直接用与你 JDK 版本匹配的稳定版,我这边用的是 JDK 11 和 Vert.x 4.4.6:
<properties> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> <vertx.version>4.4.6</vertx.version> </properties> <dependencies> <dependency> <groupId>io.vertx</groupId> <artifactId>vertx-core</artifactId> <version>${vertx.version}</version> </dependency> <dependency> <groupId>io.vertx</groupId> <artifactId>vertx-web</artifactId> <version>${vertx.version}</version> </dependency> </dependencies>这里解释一下为什么不推荐把整个 BOM 全引进来。Vert.x 的模块非常细,你全量引入会导致 jar 包膨胀,而且很多模块你用不到。等真正需要时再按需加依赖,比如后面要连 MySQL 就加 vertx-mysql-client,要发 HTTP 请求就加 vertx-web-client,要 Redis 就加 vertx-redis-client。这种按需引入的方式,能让你对每个模块的作用有更清晰的认识,排查依赖冲突也更容易。
2.2 理解 Verticle 与 Deploy 机制
Vert.x 里最核心的组件是 Verticle,你可以把它理解成 Vert.x 世界里的一个"独立任务单元"。一个 Verticle 对应一个类,可以部署多次,每次部署会占用一个独立的上下文。Verticle 有两种类型:Standard Verticle,跑在 Event Loop 线程上,适合处理 IO 密集和事件密集的任务;Worker Verticle,跑在 Worker 线程池里,适合处理 CPU 密集或阻塞密集的任务。
部署的方式非常简单:
Vertx vertx = Vertx.vertx(); vertx.deployVerticle(new MainVerticle());不过主 Verticle 里一般不会直接写业务逻辑,而是作为启动入口,通过部署子 Verticle 来组织整个应用。比如我习惯的做法是:启动时先部署 DatabaseVerticle,再部署 HttpServerVerticle,两个 Verticle 之间通过 Event Bus 通信。这种拆分的好处是职责清晰,数据库挂了不会直接拖垮 HTTP 层,后续加功能也只需要在新 Verticle 里订阅消息。注意部署是异步的,别在主线程里直接依赖部署完成后的状态,要用回调或者 Future 来处理。
2.3 配置文件读取
Vert.x 官方默认支持从 classpath 读取配置文件,我一般用 JsonObject 存配置,启动时读入内存:
JsonObject config = vertx.fileSystem() .readFileBlocking("config.json") .toJsonObject();不推荐用阻塞读取?这里用 readFileBlocking 是因为它只发生在启动阶段,此时 Task 调度还不密集,阻塞一次无伤大雅。真正跑起来的代码里,所有文件 IO 都要用异步版本。config.json 里我放 MySQL 连接信息、HTTP 端口、Event Bus 地址前缀等,这比把各种常量散落在代码里好维护得多。
3. 写第一个能用的 HTTP 服务
3.1 核心代码:Router 路由注册
现在进入正题。先看一个能跑的 HTTP 服务长什么样。我用 vertx-web 的 Router 来定义接口,它比直接手写 HttpServer requestHandler 舒服得多,支持路径参数、正则匹配、子路由、中间件,这些在真实项目中都会用到。
import io.vertx.core.AbstractVerticle; import io.vertx.core.Promise; import io.vertx.core.http.HttpServer; import io.vertx.core.http.HttpServerOptions; import io.vertx.ext.web.Router; import io.vertx.ext.web.handler.BodyHandler; public class HttpServerVerticle extends AbstractVerticle { @Override public void start(Promise<Void> startPromise) { Router router = Router.router(vertx); // 注册 BodyHandler,解析 POST/PUT 请求体为 JsonObject 或表单 router.route().handler(BodyHandler.create()); // 一个简单的 GET 接口 router.get("/api/hello").handler(ctx -> { ctx.json(new JsonObject() .put("message", "Hello Vert.x!") .put("timestamp", System.currentTimeMillis())); }); // 带路径参数的 GET 接口 router.get("/api/users/:id").handler(ctx -> { String userId = ctx.pathParam("id"); ctx.json(new JsonObject().put("userId", userId)); }); HttpServerOptions options = new HttpServerOptions() .setPort(config().getInteger("http.port", 8080)) .setCompressionSupported(true); HttpServer server = vertx.createHttpServer(options); server.requestHandler(router) .listen(ar -> { if (ar.succeeded()) { System.out.println("HTTP server started on port " + ar.result().actualPort()); startPromise.complete(); } else { startPromise.fail(ar.cause()); } }); } }这段代码里有几个细节我想单独拎出来说。第一个是 BodyHandler,你不加这个,POST 请求体根本拿不到,这个中间件会把请求体缓冲到内存或者临时文件里,然后再交给后续路由处理器。第二个是 ctx.json(),这是 RoutingContext 里自带的方法,会自动设置 Content-Type 为 application/json,并把对象序列化成 JSON 输出,不用手动写 response.end(),属于非常常用的便利方法。第三个是 startPromise,Verticle 的异步启动机制,如果你初始化的资源(连数据库、连接消息队列)是异步的,必须在真正完成后再调用 complete,否则 Vert.x 会认为启动失败。
启动入口类:
public class MainVerticle extends AbstractVerticle { @Override public void start(Promise<Void> startPromise) { vertx.deployVerticle(new DatabaseVerticle()) .compose(v -> vertx.deployVerticle(new HttpServerVerticle())) .onSuccess(id -> { System.out.println("All verticles deployed: " + id); startPromise.complete(); }) .onFailure(startPromise::fail); } }这里用了 Future 链式调用,DatabaseVerticle 先部署成功再部署 HttpServerVerticle,避免 HTTP 服务启动后数据库还没就绪导致查询报错。注意 deployVerticle 返回的是 Future ,compose 能把多个异步操作串联起来,这种写法的可读性和维护性都远好于嵌套回调。
3.2 代码里为什么必须用异步
写 Vert.x 最容易犯的错误就是"我在 Spring 里就是这么写的",然后在 Vert.x 里也来一套同步逻辑。我举个极端例子:假设你在 HTTP 请求处理器里做了一次同步的 Thread.sleep(1000),这个 Event Loop 线程就被你占用了 1 秒。如果这时候有 100 个请求同时打进来,它们会排队挨个等待,每个请求都延迟 1 秒以上,正常情况下一秒能处理几万请求的服务,现在只能处理几个。
异步代码的本质是:发起一个耗时操作(比如写数据库)之后,当前线程立刻返回事件循环,等数据库结果返回后再继续执行回调。这样同一个线程可以同时管理成千上万个 IO 任务,而不是为每个请求单独分配线程。Java 的传统线程模型里,线程切换和内存占用是很大的开销,而 Vert.x 通过事件循环把这种开销压缩到了极致。
你如果之前写过 Node.js,会发现 Vert.x 的模型和 Node 非常像。区别在于 Vert.x 可以利用多核 CPU 启动多个 Event Loop 线程,而 Node.js 默认是单线程。这也是 Vert.x 在 Java 生态里比较有竞争力的原因之一。
3.3 一个完整的 POST 接口示例
光有 GET 太单薄,我们加一个 POST 接口来演示请求体解析和参数校验。这里模拟保存用户信息,先把数据打到控制台,后面再接数据库:
router.post("/api/users").handler(ctx -> { JsonObject body = ctx.body().asJsonObject(); if (body == null || !body.containsKey("name") || !body.containsKey("age")) { ctx.response() .setStatusCode(400) .json(new JsonObject().put("error", "name and age are required")); return; } String name = body.getString("name"); int age = body.getInteger("age"); // 模拟异步保存,后续替换为数据库操作 vertx.setTimer(100, id -> { ctx.json(new JsonObject() .put("id", 1) .put("name", name) .put("age", age) .put("status", "created")); }); });注意 BodyHandler 必须注册在路由之前,否则 ctx.body() 会是 null。实际项目中我还会在 BodyHandler 后面加一个限制请求体大小的设置,防止用户传超大 JSON 把内存撑爆,默认是 -1 也就是不限制,这个在生产环境一定要显式设置。
BodyHandler bodyHandler = BodyHandler.create() .setBodyLimit(1024 * 1024); // 1MB router.route().handler(bodyHandler);4. Event Bus:让模块之间通信不再纠缠
4.1 Event Bus 三种模式
如果你只是用 Vert.x 写接口,不碰 Event Bus,那等于只用了一半功力。Event Bus 是 Vert.x 的神经系统,它允许不同 Verticle、不同模块、甚至不同 JVM 实例上的代码通过消息进行通信,解耦能力非常强。
三种模式分别是:点对点(Point-to-Point),发送方把消息发给单个消费者,如果有多个消费者则负载均衡;发布订阅(Publish-Subscribe),消息发给所有订阅者;请求-应答(Request-Reply),类似 RPC,发送时带回复地址,消费者处理完可以回传结果。
实际开发中我用得最多的就是请求-应答模式。比如 HTTP 层收到请求后,通过 Event Bus 把数据发给业务 Verticle,业务 Verticle 处理完把结果回传,HTTP 层再响应客户端。这样 HTTP 层完全不关心数据怎么来的、存哪里,业务层也不关心 HTTP 协议细节,两边只依赖一个消息地址。
4.2 请求-应答模式代码演示
还是用保存用户的例子。把 UserServiceVerticle 单独拆出来,订阅 user.save 地址:
public class UserServiceVerticle extends AbstractVerticle { @Override public void start(Promise<Void> startPromise) { vertx.eventBus().consumer("user.save", message -> { JsonObject user = (JsonObject) message.body(); int saveResult = saveToDatabase(user); if (saveResult > 0) { message.reply(new JsonObject() .put("code", 0) .put("message", "ok") .put("data", new JsonObject() .put("id", saveResult) .put("name", user.getString("name")))); } else { message.fail(500, "save failed"); } }); startPromise.complete(); } }在 HTTP 层调用时用 request 方法:
router.post("/api/users").handler(ctx -> { JsonObject body = ctx.body().asJsonObject(); if (body == null || !body.containsKey("name")) { ctx.response().setStatusCode(400).json(new JsonObject().put("error", "name is required")); return; } vertx.eventBus().request("user.save", body, reply -> { if (reply.succeeded()) { ctx.json(reply.result().body()); } else { ctx.response().setStatusCode(500).json(new JsonObject() .put("error", reply.cause().getMessage())); } }); });这个模式的好处非常明显:你的 HTTP 层处理接口时只是"发了个消息",它不需要知道是谁在处理,也不需要等处理完(当然 request 模式会异步等结果),这样当你把 user.save 的消费者从单机换成集群,或者把消费者从 Verticle 换成另一个服务,HTTP 层代码一行都不用改。Event Bus 支持集群模式,通过 Hazelcast 或 Infinispan 做集群管理器,节点之间自动广播订阅关系,这是很多分布式框架做不到的透明解耦。
4.3 异步回调的痛点与 Handler 封装
Vert.x 的普通回调写法容易产生回调地狱,尤其业务逻辑一长,嵌套三四层就非常痛苦。你大概见过这种代码:
service.a(param, res1 -> { if (res1.succeeded()) { service.b(res1.result(), res2 -> { if (res2.succeeded()) { service.c(res2.result(), res3 -> { // ... }); } }); } });这种代码的问题不只是丑,关键是错误处理非常容易遗漏,一旦中间某一步失败,后面的代码根本没机会执行。我的建议是:优先用 Future API,它把异步操作变成可组合的对象,配合 compose 和 map 能写出几乎和同步代码一样流畅的逻辑流。如果你用的是 Vert.x 4.x,可以试试它内置的 Fiber 支持,通过引入 vertx-lang-java 的协程模块,写代码时完全用同步风格,底层自动帮你切线程,这个体验是最舒服的,但需要额外学习协程的概念。
5. 连接 MySQL:异步数据库访问
5.1 引入依赖并配置连接池
Vert.x 提供了完整的异步数据库客户端,我们拿 MySQL 举例。首先在 pom.xml 加入依赖:
<dependency> <groupId>io.vertx</groupId> <artifactId>vertx-mysql-client</artifactId> <version>${vertx.version}</version> </dependency>这个客户端底层使用 Netty 实现 MySQL 协议,完全异步,不依赖 JDBC。这意味着它不会像 JDBC 那样在调用时阻塞线程,也不吃连接池的线程资源。配置连接池的方式如下:
import io.vertx.mysqlclient.MySQLClient; import io.vertx.mysqlclient.MySQLConnectOptions; import io.vertx.mysqlclient.MySQLPool; import io.vertx.sqlclient.PoolOptions; MySQLConnectOptions connectOptions = new MySQLConnectOptions() .setPort(3306) .setHost(config().getString("mysql.host", "127.0.0.1")) .setDatabase(config().getString("mysql.database", "test")) .setUser(config().getString("mysql.user", "root")) .setPassword(config().getString("mysql.password", "password")) .setCharset("utf8mb4"); PoolOptions poolOptions = new PoolOptions() .setMaxSize(20) .setMaxWaitQueueSize(100); MySQLPool pool = MySQLPool.pool(vertx, connectOptions, poolOptions);连接池大小设置是门学问。Vert.x 的异步连接池和传统 DBCP/C3P0 不太一样,因为连接请求本身是异步的,不会阻塞线程等待,所以 pool size 可以设置得更大一些,但也不能盲目调大。MySQL 服务端默认 max_connections 是 151,如果你的应用实例比较多,每个实例 20 个连接,加起来很容易打满。我自己一般先按实例数乘以 10 估算,然后通过压测微调。
5.2 异步查询的几种写法
第一种,直接查询返回 JsonObject 列表:
pool.query("SELECT id, name, age FROM users") .execute() .onSuccess(rows -> { List<JsonObject> users = new ArrayList<>(); for (Row row : rows) { users.add(new JsonObject() .put("id", row.getInteger("id")) .put("name", row.getString("name")) .put("age", row.getInteger("age"))); } // 输出或返回给调用方 }) .onFailure(err -> { System.err.println("query failed: " + err.getMessage()); });第二种,带参数查询,使用 PreparedStatement 方式防止 SQL 注入:
pool.preparedQuery("SELECT * FROM users WHERE age > ?") .execute(Tuple.of(18)) .onSuccess(rows -> { // 处理结果 }) .onFailure(err -> { // 处理错误 });这里的 Tuple 是 Vert.x SQL 客户端提供的参数封装。注意 row.getInteger("id") 是按下表映射,因为 MySQL 的 int 类型会映射成 Integer。如果是 bigint,那要用 getLong。很多坑都是从数据类型映射开始的,建议你先打印一行 rows,看下 Vert.x 自动推断的类型是不是和你预期一致,再往下做业务逻辑。
5.3 把 DAO 封装成一个独立的 Service
为了不让业务代码乱成一锅粥,我习惯把数据库操作封装成一个 UserService 类。但注意,它不是 Spring 里的单例 Bean,而是通过构造方法传入 pool 和 vertx,这样可以在数据访问层做更精细的控制:
public class UserService { private final MySQLPool pool; public UserService(MySQLPool pool) { this.pool = pool; } public Future<JsonObject> getUserById(Integer id) { Promise<JsonObject> promise = Promise.promise(); pool.preparedQuery("SELECT id, name, age FROM users WHERE id = ?") .execute(Tuple.of(id)) .onSuccess(rows -> { if (rows.rowCount() == 0) { promise.fail(new RuntimeException("user not found, id=" + id)); } else { Row row = rows.iterator().next(); promise.complete(new JsonObject() .put("id", row.getInteger("id")) .put("name", row.getString("name")) .put("age", row.getInteger("age"))); } }) .onFailure(promise::fail); return promise.future(); } }用 Future 作为返回类型的好处有三个:第一,调用方可以自由选择用回调还是用 await 还是用 compose 来消费结果;第二,Future 本身是一个值,可以被缓存、被合并、被重试;第三,它把错误处理统一到 future 的 onFailure 上,不依赖 try-catch 跨线程传播。建议你把这个模式记下来,Vert.x 4.x 大量 API 都是 Future 返回。
在 Verticle 里初始化 UserService:
public class DatabaseVerticle extends AbstractVerticle { private MySQLPool pool; private UserService userService; @Override public void start(Promise<Void> startPromise) { // 省略连接配置代码 pool = MySQLPool.pool(vertx, connectOptions, poolOptions); userService = new UserService(pool); vertx.eventBus().consumer("user.get", message -> { Integer userId = ((JsonObject) message.body()).getInteger("id"); userService.getUserById(userId).onComplete(ar -> { if (ar.succeeded()) { message.reply(ar.result()); } else { message.fail(500, ar.cause().getMessage()); } }); }); startPromise.complete(); } }这样整个链路就是:HTTP 请求 → Event Bus 消息 → UserService 异步查询 → 返回结果 → HTTP 响应。每一层之间完全解耦,你可以在不修改调用方的情况下替换 UserService 的实现,这对单元测试也非常友好。
6. 常见问题与排查技巧实录
6.1 Event Loop 被阻塞,压测 QPS 惨不忍睹
这个问题出现的频率最高。表现是:服务线上跑着,刚开始正常,某次请求一来,整个服务卡住,CPU 占用率却不高,所有的请求都像排队一样被堵住。用 jstack 查看线程栈,会看到多个线程卡在某个业务方法的同步调用上。
排查思路:先检查代码里有没有 Thread.sleep、循环等待某个锁、同步的 JDBC 或者 HTTP 客户端调用,特别注意第三方 SDK 的默认实现,很多 SDK 底层是同步的。修复方式是:要么换成异步客户端,要么用 vertx.executeBlocking 把同步操作丢到 Worker 线程池。千万别直接在 Event Loop 里硬调同步代码,这不是优化能解决的,是架构上的错误。
6.2 乱码问题:中文显示成问号
Vert.x 4.x 里 JSON 处理默认按 UTF-8 编解码,一般不会乱码。真正容易出问题的是 MySQL 连接字符集。如果你创建表的时候用了 latin1,或者连接参数没加 utf8mb4,中文数据就会变成问号。我的排查路径是:先从接口返回看是否乱码,如果接口返回正常但数据库字段乱码,那就是数据库字符集的问题;如果接口返回就乱码,那看 HTTP 响应头有没有正确设置 Content-Type。Vert.x 的 ctx.json() 会自动处理,如果你用了 ctx.response().end(string),记得手动设置:
ctx.response().putHeader("Content-Type", "application/json; charset=utf-8").end(jsonString);6.3 回调用多了,代码无法维护怎么办
回调嵌套超过三层,我就建议重构了。常见解法按优先级排:第一选择是使用 Future 链式调用,把每一步拆成返回 Future 的方法,然后 compose 串联;第二选择是引入 RxJava 3 或 Mutiny,Vert.x 官方对这两套 API 都有集成,适合复杂的数据流处理;第三选择是用协程(Fiber),vertx-lang-java 提供了 Kotlin 风格的 suspend 支持,但需要引入额外的字节码增强,团队需要学习成本。
我自己的经验是:大部分业务场景 Future 链式调用就够了,RxJava 适合做并发合并、窗口操作这类复杂场景。别一上来就上重型响应式框架,简单问题用简单方案解决才是工程化的正路。
6.4 快速定位问题的日志技巧
Vert.x 的日志默认是 JUL(java.util.logging),格式不太好看。建议接上 SLF4J 加 Logback,配置很简单,加两个依赖再加一个 logback.xml 就行。我在 logback.xml 里会专门把 io.vertx 的日志级别调到 INFO 或 WARN,把业务包的日志级别设为 DEBUG,这样既能看清业务日志,又不会被框架内部的握手、重连日志刷屏。还有,Vert.x 的异常如果没人接,会走到 Vertx 实例的异常处理器,你可以在创建 Vertx 时设置:
Vertx vertx = Vertx.vertx(new VertxOptions() .setBlockedThreadCheckInterval(5000) .setWarningExceptionTime(5000));这样任何 Event Loop 线程发生阻塞超过 5 秒,控制台就会打出 WARNING 日志提醒你,这是诊断阻塞问题的利器。
6.5 连接池耗尽导致请求超时
异步连接池因为不阻塞线程,池耗尽的表现为请求一直 pending,不会直接报错。如果 MySQL 连接数上限设置小了,高并发下新请求的取连接操作会进入 MaxWaitQueueSize,如果队列也满了,就会抛出 PoolException。出现这个问题的第一件事别急着调大 maxSize,先看是查询慢导致连接占用时间长,还是应用连接泄漏没释放。用 show processlist 看看 MySQL 侧有没有大量的 sleep 连接,如果是泄漏,多半是某个分支没有正确 close 连接或没有释放 RowSet。再检查代码里有没有用到 pool 的同时又手动创建了新的客户端连接,这种隐性连接最容易被忽略。
7. 几个让我少走弯路的实践习惯
7.1 用本地 Docker 跑 MySQL,别用本机安装
开发 Vert.x 时不建议把 MySQL 直接装在本机上。我推荐用 Docker 起一个临时 MySQL 实例,端口映射到 3306,数据库随便造,坏了直接删容器重建,不用折腾清理残留文件。一条命令搞定:
docker run --name vertx-mysql -e MYSQL_ROOT_PASSWORD=password -e MYSQL_DATABASE=test -p 3306:3306 -d mysql:87.2 压测工具选对,结果才有参考价值
Vert.x 写出来的服务性能好不好,得靠压测说话。别用浏览器刷新当压测,至少用 wrk 或者 ab。我经常用 wrk,它对 Event Loop 模型的异步服务压测效果比较真实:
wrk -t4 -c200 -d30s http://localhost:8080/api/hello注意 wrk 里的线程数最好小于等于 CPU 核心数,连接数从 50 开始逐步加,观察延迟分布,而不是只看 QPS。Vert.x 的容量规划必须关注 p99 延迟,因为异步服务的平均延迟往往很好看,但 p99 一旦恶化说明 Event Loop 已经在过载边缘了。
7.3 小步提交,接口先跑通再优化
最后一条经验:Vert.x 入门的最大障碍是"想太多"。很多初学者一上来就想设计一个完美的响应式架构,结果被各种概念折磨得放弃。我的建议是先把最简单的 HTTP 接口跑通,再加数据库,再加 Event Bus,每一步都小步快跑验证,跑通了再优化。代码能跑就是 0 到 1,优化是 1 到 100,0 到 1 这个阶段不需要完美设计,只需要让自己建立信心。
接触 Vert.x 这几年,我最喜欢的还是它那种"框架很少替你做决定"的风格。它不像 Spring Boot 那样给你安排好一切,而是提供一堆可靠的积木,让你自己组织逻辑。这种自由度对新手来说可能有点不知所措,但只要你按照上面这套路径跑一遍,把一个带数据库的小服务完整写出来,你就会发现异步编程其实没有想象中那么可怕。后续想深入的话,可以从 WebSocket、集群部署、灰度发布、实时数据推送这几个方向继续扩展,Vert.x 在实时通信这块的优势非常明显。这篇就到这里,有问题评论区聊。