
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.6properties maven.compiler.source11/maven.compiler.source maven.compiler.target11/maven.compiler.target vertx.version4.4.6/vertx.version /properties dependencies dependency groupIdio.vertx/groupId artifactIdvertx-core/artifactId version${vertx.version}/version /dependency dependency groupIdio.vertx/groupId artifactIdvertx-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(PromiseVoid 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()屬于非常常用的便利方法。第三個是 startPromiseVerticle 的異步啟動機制如果你初始化的資源連數據庫、連接消息隊列是異步的必須在真正完成后再調用 complete否則 Vert.x 會認為啟動失敗。啟動入口類public class MainVerticle extends AbstractVerticle { Override public void start(PromiseVoid 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(PromiseVoid 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 groupIdio.vertx/groupId artifactIdvertx-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 - { ListJsonObject 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 FutureJsonObject getUserById(Integer id) { PromiseJsonObject 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 里初始化 UserServicepublic class DatabaseVerticle extends AbstractVerticle { private MySQLPool pool; private UserService userService; Override public void start(PromiseVoid 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; charsetutf-8).end(jsonString);6.3 回調用多了代碼無法維護怎么辦回調嵌套超過三層我就建議重構了。常見解法按優先級排第一選擇是使用 Future 鏈式調用把每一步拆成返回 Future 的方法然后 compose 串聯第二選擇是引入 RxJava 3 或 MutinyVert.x 官方對這兩套 API 都有集成適合復雜的數據流處理第三選擇是用協程Fibervertx-lang-java 提供了 Kotlin 風格的 suspend 支持但需要引入額外的字節碼增強團隊需要學習成本。我自己的經驗是大部分業務場景 Future 鏈式調用就夠了RxJava 適合做并發合并、窗口操作這類復雜場景。別一上來就上重型響應式框架簡單問題用簡單方案解決才是工程化的正路。6.4 快速定位問題的日志技巧Vert.x 的日志默認是 JULjava.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_PASSWORDpassword -e MYSQL_DATABASEtest -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 到 1000 到 1 這個階段不需要完美設計只需要讓自己建立信心。接觸 Vert.x 這幾年我最喜歡的還是它那種框架很少替你做決定的風格。它不像 Spring Boot 那樣給你安排好一切而是提供一堆可靠的積木讓你自己組織邏輯。這種自由度對新手來說可能有點不知所措但只要你按照上面這套路徑跑一遍把一個帶數據庫的小服務完整寫出來你就會發現異步編程其實沒有想象中那么可怕。后續想深入的話可以從 WebSocket、集群部署、灰度發布、實時數據推送這幾個方向繼續擴展Vert.x 在實時通信這塊的優勢非常明顯。這篇就到這里有問題評論區聊。