Vert.x EventBus:一个地址,解耦所有组件
EventBus 是 Vert.x 的"神经系统",目标只有一件事——让分布式系统里各组件通过地址松耦合通信。一套 API 同时覆盖同 JVM、跨进程、跨集群,还自带三种通信模式和负载均衡。这篇把它的核心作用、三个通信模式、六个真实场景,以及一个地址怎么打通整套系统讲清楚。
如果说 Vert.x 靠”事件循环 + 非阻塞”解决了”一个请求怎么跑得更快”,那 EventBus 解决的是另一件事:一堆组件之间怎么互相说话,还能互不打听。
它被称为 Vert.x 的”神经系统”。设计目标很单纯——让系统里各个部分通过”地址(Address)“松耦合通信。发送方只管往某个地址发消息,接收方在那个地址上挂着监听,两边谁也不用认识谁。更重要的是,同一套 API 既能让同一个 JVM 里的 Verticle 通信,也能在跨 JVM 的集群里透明地通信。
如果你熟悉 Spring,可以把它类比成 ApplicationEvent 的”Pro 版”:Spring 的事件只在单个应用上下文里转,EventBus 天然支持集群、多语言和高吞吐。
一、EventBus 到底解决了什么
1. 解耦:只知道地址,不认识对方
这是它存在的根本理由。订单模块要通知库存模块扣减,传统写法订单模块得持有库存模块的引用(HTTP Client、Feign、RPC stub……)。一旦库存模块的地址或协议变了,订单模块跟着改。
EventBus 把它变成:订单模块只往 inventory.deduct 这个地址发消息,至于谁在听、听的人换了没有,它一概不管。各 Verticle 可以独立开发、独立部署、独立扩缩。
2. 统一通信抽象:一套 API,三种距离
同一行 vertx.eventBus(),在不同部署形态下表现不同,调用方无感:
- 同 JVM 内的多个 Verticle 通信;
- 同进程多线程通信;
- **跨 JVM(集群)**的分布式通信(配好 Cluster Manager 后自动生效)。
你写代码时不用关心”对方在本机还是隔壁机器”,EventBus 帮你路由。
3. 三种通信模式
这是最容易混的,先用一句话区分三个动作:
| 动作 | 方法 | 语义 | 接收方数量 |
|---|---|---|---|
| 发布/订阅 | publish(address, msg) | 广播,所有人收到 | 所有订阅者 |
| 点对点 | send(address, msg) | 轮询发给其中一个 | 一个(自动负载均衡) |
| 请求/响应 | request(address, msg, replyHandler) | 发完等对方 reply | 一个,且能拿回结果 |
// 广播:所有监听者都收到(适合"通知类"消息)
vertx.eventBus().publish("order.created", orderJson);
// 点对点:多个同地址消费者里轮询一个(适合"任务分发")
vertx.eventBus().send("image.process", taskJson);
// 请求/响应:发完等回包(适合"要结果的调用")
vertx.eventBus().<JsonObject>request("user.info", userId, reply -> {
if (reply.succeeded()) {
JsonObject user = reply.result().body();
}
});
4. 跨语言
EventBus 上的消息按 JSON 约定传输,所以 Java、Kotlin、Scala、JavaScript、Python 等写的 Verticle 可以互相通信——这对异构系统混编很实用。
二、场景一:微服务内部通信
单体拆成多个 Verticle 后,它们之间怎么协调?用地址串起来即可。
// 订单服务:只管往地址发,不关心谁在听
public class OrderVerticle extends AbstractVerticle {
@Override
public void start() {
vertx.eventBus().consumer("order.created", msg -> {
JsonObject order = (JsonObject) msg.body();
// 通知库存、支付——都是 publish/send 到地址
vertx.eventBus().send("inventory.deduct", order);
vertx.eventBus().send("payment.process", order);
});
}
}
// 库存服务:独立部署,订阅自己的地址
public class InventoryVerticle extends AbstractVerticle {
@Override
public void start() {
vertx.eventBus().consumer("inventory.deduct", msg -> {
JsonObject order = (JsonObject) msg.body();
// 扣减库存……
msg.reply("库存扣减成功");
});
}
}
多个 InventoryVerticle 实例都订阅 inventory.deduct 时,send 会自动轮询——水平扩展零成本。
三、场景二:事件驱动架构(EDA)
用户注册后,要发欢迎邮件、发券、更新统计、写审计。把这些后续动作全塞进注册方法,是经典的”上帝函数”。用 EventBus 拆开:
public class UserRegistrationVerticle extends AbstractVerticle {
@Override
public void start() {
vertx.eventBus().consumer("user.registered", msg -> {
JsonObject user = (JsonObject) msg.body();
// 只管 publish,下游各自认领
vertx.eventBus().publish("user.welcome.email", user);
vertx.eventBus().publish("user.coupon.grant", user);
vertx.eventBus().publish("user.stats.update", user);
vertx.eventBus().publish("user.audit.log", user);
});
}
}
// 邮件服务:只关心 welcome.email
public class EmailVerticle extends AbstractVerticle {
@Override
public void start() {
vertx.eventBus().consumer("user.welcome.email", msg -> {
sendWelcomeEmail(((JsonObject) msg.body()).getString("email"));
});
}
}
新增一个”注册后送积分”的功能?加一个消费者 Verticle 就行,注册主流程一行不动。这就是事件驱动最实在的好处。
四、场景三:实时数据推送(WebSocket)
股票行情、比分直播、聊天室——本质都是”服务端有更新就推给订阅客户端”。下面这个例子特意把 consumer 注册放在连接建立时做一次,而不是每次收到消息都注册(后者会泄漏 consumer):
public class StockVerticle extends AbstractVerticle {
@Override
public void start() {
vertx.createHttpServer()
.webSocketHandler(ws -> {
String clientId = ws.textHandlerID();
// 客户端订阅某只股票:为这个连接注册一个 consumer,且只注册一次
ws.textMessageHandler(symbol -> {
String address = "stock." + symbol;
vertx.eventBus().<JsonObject>consumer(address, msg -> {
ws.writeTextMessage(msg.body().encode());
});
});
ws.closeHandler(v -> { /* 连接关闭时记得 unregister,避免泄漏 */ });
})
.listen(8080);
// 行情源:周期性往地址 publish,所有订阅该地址的 WS 都收到
vertx.setPeriodic(1000, id -> {
JsonObject quote = new JsonObject()
.put("symbol", "AAPL")
.put("price", 150 + Math.random() * 10);
vertx.eventBus().publish("stock.AAPL", quote);
});
}
}
注意:
consumer注册是有去有回的——consumer()返回一个MessageConsumer,不需要时调用它的unregister()。示例里连接在closeHandler中应把对应 consumer 注销掉,否则客户端来来去去,consumer 越积越多。这是用 EventBus 做推送时最容易踩的坑。
五、场景四:集群下的负载均衡(任务队列)
图片处理、视频转码、批量计算这类重活,天然适合丢给一堆 Worker。EventBus 的 send + 多实例部署就是现成的任务队列:
// 派发器:周期性投递任务,send 自动轮询到某个 Worker
public class TaskDispatcherVerticle extends AbstractVerticle {
@Override
public void start() {
vertx.setPeriodic(1000, id -> {
JsonObject task = new JsonObject().put("imageId", "img_" + System.currentTimeMillis());
vertx.eventBus().<String>request("image.process", task, reply -> {
if (reply.succeeded()) System.out.println("done: " + reply.result().body());
});
});
}
}
// 工作节点:耗时操作包进 executeBlocking,交 Worker 线程,不挡 Event Loop
public class ImageWorkerVerticle extends AbstractVerticle {
@Override
public void start() {
vertx.eventBus().<JsonObject>consumer("image.process", msg -> {
JsonObject task = msg.body();
vertx.executeBlocking(promise -> {
promise.complete(processImage(task.getString("imageId")));
}, res -> msg.reply((String) res.result()));
});
}
}
// 部署 10 个 worker 实例,send 自动在它们之间轮询
vertx.deployVerticle(ImageWorkerVerticle.class.getName(),
new DeploymentOptions().setInstances(10));
这里有个 Vert.x 的铁律顺带提一句:耗 CPU/阻塞的操作必须进 executeBlocking,否则会卡住 Event Loop 线程,拖垮整个节点的吞吐。
六、场景五:API 网关聚合
一个仪表盘要拼用户、订单、消息、积分四块数据,各自是独立服务。用 CompositeFuture.all 并行请求再汇总:
public class ApiGatewayVerticle extends AbstractVerticle {
@Override
public void start() {
vertx.createHttpServer()
.requestHandler(req -> {
String userId = req.getParam("userId");
Future<JsonObject> userFuture = getUserInfo(userId);
Future<JsonArray> ordersFuture = getOrders(userId);
Future<JsonArray> messagesFuture = getMessages(userId);
Future<Integer> pointsFuture = getPoints(userId);
CompositeFuture.all(userFuture, ordersFuture, messagesFuture, pointsFuture)
.onSuccess(c -> req.response().end(new JsonObject()
.put("user", userFuture.result())
.put("orders", ordersFuture.result())
.put("messages", messagesFuture.result())
.put("points", pointsFuture.result()).encode()))
.onFailure(err -> req.response().setStatusCode(500).end(err.getMessage()));
})
.listen(8080);
}
private Future<JsonObject> getUserInfo(String userId) {
Promise<JsonObject> p = Promise.promise();
vertx.eventBus().<JsonObject>request("user.info", userId,
reply -> p.complete(reply.result().body()));
return p.future();
}
// getOrders / getMessages / getPoints 同理
}
四路请求并行,all 等全部完成再聚合——比串行调用快好几倍,而且每个下游又是独立扩缩的 Verticle。
七、场景六:分布式日志收集
各服务不打日志到本地文件,而是把日志事件发到总线,由专门的收集者统一落库、对错误日志告警:
public class LogCollectorVerticle extends AbstractVerticle {
@Override
public void start() {
vertx.eventBus().<JsonObject>consumer("log.collector", msg -> {
JsonObject log = msg.body();
saveToElasticsearch(log);
if ("ERROR".equals(log.getString("level"))) {
vertx.eventBus().send("alert.system", log); // 错误日志转告警
}
});
}
}
八、典型架构长什么样
┌─────────────────────────────────────┐
│ API Gateway │
│ (HTTP Server + EventBus) │
└────────────┬────────────────────────┘
│
┌────────────▼────────────────────────┐
│ Event Bus (核心总线) │
└─┬──────────┬──────────┬────────┬────┘
│ │ │ │
┌─────────▼──┐ ┌────▼────┐ ┌───▼────┐ ┌──▼─────┐
│ Order │ │Payment │ │Inventory│ │ User │
│ Service │ │Service │ │Service │ │ Service│
└────────────┘ └─────────┘ └─────────┘ └────────┘
│ │ │ │
┌────▼────┐ ┌────▼────┐ ┌────▼────┐ ┌──▼─────┐
│ DB │ │ 银行网关 │ │ Redis │ │ MongoDB│
└─────────┘ └─────────┘ └─────────┘ └────────┘
所有 Service 之间不直接引用,全走中间那条 Event Bus。网关负责把外部 HTTP 翻译成 EventBus 调用,Service 之间只认地址。
九、核心价值与边界
| 维度 | 说明 |
|---|---|
| 解耦 | 组件零直接依赖,独立演进 |
| 弹性 | 动态增减实例,自动负载均衡 |
| 可扩展 | 加功能 = 加消费者,主流程不动 |
| 性能 | 异步非阻塞;官方基准在轻负载小消息下可达百万级消息/秒的量级(实际取决于消息大小和消费逻辑,非严格压测数字,别直接当承诺) |
| 分布式 | 配 Cluster Manager 后透明支持跨 JVM |
| 多语言 | 异构 Verticle 无缝通信 |
什么时候该用:
- 微服务内部多个 Verticle 协调;
- 事件驱动架构,解耦生产者与消费者;
- 实时推送(WebSocket / SSE);
- 异步任务队列;
- API 网关聚合;
- 集中式日志 / 监控收集。
什么时候别用(换消息中间件):
- 大规模数据批量传输:上 Kafka / RabbitMQ;
- 需要持久化保证:EventBus 本身不做消息持久化,节点挂了在途消息可能丢;
- 复杂路由规则:交给专业消息中间件。
小结
EventBus 的设计哲学就三个词:简单、快速、对分布式友好。它把”组件怎么通信”这个最容易写出耦合的地方,收敛成”一个地址 + 三种动作”。在跑 Vert.x 的系统里(比如我们 FOLIO 的登录/认证模块也建立在 Vert.x 之上),EventBus 几乎就是内部通信的默认总线——会用了,模块之间的”硬连线”就自然松开了。
记住一句够用一辈子的:publish 是广播、send 是轮询、request 是等回包;consumer 用完记得 unregister。
关注我