Vert.x 实战(一):从技术选型到可运行的订单服务

Vert.x 实战(一):从技术选型到可运行的订单服务

Elvis Lv2

这套教程最初的问题是:怎样用 Java 写一个需要同时等待库存、支付等下游结果的订单接入层?请求处理期间大部分时间不在计算,而在等待网络 I/O。如果为每个请求长期占用一个平台线程,线程数量、上下文切换和内存都会逐渐成为成本。

本系列用一个简化的订单服务验证 Vert.x 的编程模型。项目固定使用 JDK 21、Vert.x 5.0.0 和 Gradle 9.2.1。它是可运行的教学项目,不是可以直接上线的电商系统;库存、支付和持久化会被简化,但超时、失败传播和测试不能省略。

先判断 Vert.x 是否适合

Vert.x 适合以下工作负载:

  • 同时维护较多 HTTP、WebSocket 或消息连接;
  • 一次请求需要并行调用多个下游服务;
  • 业务可以使用异步 API,团队愿意明确处理失败和超时;
  • 希望保留 Java 生态,又不想把每个连接绑定到一个平台线程。

它不是所有 Java 服务的默认答案。大量同步 JDBC、复杂报表计算或强依赖线程本地变量的旧系统,直接迁移只会把阻塞藏进 Event Loop。普通 CRUD 服务如果现有 Spring MVC 已经稳定,也没有必要仅为追求“响应式”而重写。

这个项目要解决什么

第一版接口只有三个动作:

1
2
3
POST /orders       创建订单
GET /orders/:id 查询订单
GET /health 存活检查

HTTP 层不直接保存订单,而是通过 Event Bus 把命令交给 OrderVerticle。这种拆分让协议适配与业务状态分开,后续可以替换 HTTP、增加 WebSocket 通知,或者把订单消费者部署为多个实例。

1
HTTP request -> HttpVerticle -> Event Bus -> OrderVerticle -> reply

当前版本使用内存 Map,因此重启后数据会丢失。这是刻意保留的限制:前几章只观察 Verticle、Context 和 Event Bus,不用数据库连接池掩盖核心行为。

Event Loop 带来的约束

Vert.x 并不会让代码自动变快。它的关键约束是:Event Loop 上的 Handler 必须尽快返回。如果在 Handler 中执行慢 SQL、Thread.sleep、同步 HTTP 请求或大段 CPU 计算,同一个 Context 上后续事件都会被拖住。

因此系列中会坚持三条规则:

  1. 网络和存储调用使用异步客户端;
  2. 每个异步步骤显式处理失败和超时;
  3. 性能结论必须附测试环境、命令和原始结果,没有实测就不写数字。

建立可复现的工程

教程代码最常见的问题不是业务逻辑,而是读者无法还原作者的环境。本项目固定以下基线:

1
2
3
4
源码目标版本:JDK 21
Vert.x:5.0.0
Gradle Wrapper:9.2.1
测试:JUnit 5 + Vert.x JUnit 5 + AssertJ

构建文件使用 Vert.x BOM,避免 Core、Web、Web Client 等模块版本不一致:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}

dependencies {
implementation(platform("io.vertx:vertx-stack-depchain:5.0.0"))
implementation("io.vertx:vertx-core")
implementation("io.vertx:vertx-web")

testImplementation("io.vertx:vertx-junit5")
testImplementation("org.junit.jupiter:junit-jupiter:5.11.4")
testRuntimeOnly("org.junit.platform:junit-platform-launcher:1.11.4")
}

我的机器运行 GraalVM JDK 25.0.3。最初使用 Gradle 8.14.3 时,Wrapper 在启动阶段失败;升级到 Gradle 9.2.1 后恢复正常。源码仍由 Toolchain 以 Java 21 为目标。构建工具使用的 JVM 与源码目标版本是两个不同问题。

项目目录保持简单:

1
2
3
4
5
6
7
vertx-serials/
├── build.gradle.kts
├── settings.gradle.kts
├── gradlew
└── src/
├── main/java/dev/elvis/orders/
└── test/java/dev/elvis/orders/

先执行测试,再启动服务:

1
2
3
./gradlew test
./gradlew run
curl -i http://localhost:8080/health

用两个 Verticle 划分职责

HttpVerticle 负责 HTTP 协议,OrderVerticle 负责订单命令与状态。入口类按依赖顺序部署:

1
2
3
4
5
6
7
Vertx vertx = Vertx.vertx();
vertx.deployVerticle(new OrderVerticle())
.compose(ignored -> vertx.deployVerticle(new HttpVerticle()))
.onFailure(error -> {
error.printStackTrace();
vertx.close();
});

先注册订单消费者,再开放 HTTP 端口。若两个部署并行执行,服务可能已经接收请求,但 Event Bus 消费者尚未就绪。

Vert.x 5.0.0 的 AbstractVerticle 异步生命周期通过 Promise<Void> 表达:

1
2
3
4
5
6
7
8
9
10
11
@Override
public void start(Promise<Void> startPromise) {
vertx.createHttpServer()
.requestHandler(router)
.listen(config().getInteger("http.port", 8080))
.onSuccess(server -> {
this.server = server;
startPromise.complete();
})
.onFailure(startPromise::fail);
}

只有端口监听成功才完成启动 Promise。端口占用时部署会失败,应用不会打印错误的“启动成功”。停止时也要等待 Server 关闭:

1
2
3
4
5
6
7
8
@Override
public void stop(Promise<Void> stopPromise) {
if (server == null) {
stopPromise.complete();
} else {
server.close().onComplete(stopPromise);
}
}

一个普通 Verticle 的 Handler 默认在关联 Context 上调度。它降低了同一实例内状态并发访问的复杂度,但不意味着共享对象天然线程安全,也不意味着可以随意增加实例数。当前订单 Map 只用于教学;真实服务应使用持久化存储和数据库唯一约束。

到这里我们得到一个能构建、能测试、能正确报告启动失败的最小服务。下一篇会加入 Event Bus、库存预留和 Future 编排。

  • 标题: Vert.x 实战(一):从技术选型到可运行的订单服务
  • 作者: Elvis
  • 创建于 : 2025-11-16 17:40:05
  • 更新于 : 2026-01-01 12:00:00
  • 链接: https://qianwj.github.io/2025/11/16/vertx-series-1/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。