告别 Redis/MQ —— ionet 分布式事件总线实战
一个熟悉的场景
你刚接到一个需求:玩家登录后,系统需要做以下事情——
- 记录登录时间
- 发放离线奖励
- 发送欢迎邮件
- 推送今日活动信息
- 更新排行榜在线状态
你的第一反应可能是:上 Redis pub/sub 或 MQ。
但这意味着:安装 Redis/RabbitMQ 服务、维护连接池、处理消息序列化、配置消费者组、担心消息丢失……原本一个简单的"通知"需求,变成了一场中间件战争。
ionet 的答案是:不需要安装任何东西。
三个概念,全部学完
ionet 的分布式事件总线只有 3 个概念:
| 概念 | 说明 | 类比 |
|---|---|---|
| EventSource(事件源) | 业务数据载体 | MQ 中的消息体 |
| Subscriber(订阅者) | 接收事件并处理业务 | MQ 中的消费者 |
| Fire(发布事件) | 发送事件给订阅者 | MQ 中的 publish |
学完这三个概念,你就可以开始使用了。
实战:玩家登录事件
第一步:定义事件源
@ProtobufClasspublicclassUserLoginEventMessage{publiclong userId;publicstaticUserLoginEventMessageof(long userId){var message =newUserLoginEventMessage(); message.userId = userId;return message;}}事件源需要添加 @ProtobufClass 注解,因为事件可能跨进程传输,需要序列化支持。
第二步:编写订阅者
@EventBusSubscriberpublicclassEmailEventBusSubscriber{@EventSubscribepublicvoidmail(UserLoginEventMessage message){ log.info("发送欢迎邮件给用户 {}", message.userId);// 实际业务:调用邮件系统发送欢迎邮件}}@EventBusSubscriberpublicclassRewardEventBusSubscriber{@EventSubscribepublicvoidcalcReward(UserLoginEventMessage message){ log.info("计算离线奖励给用户 {}", message.userId);// 实际业务:查询上次登录时间,计算奖励}}订阅者的编写规则非常简单:
- 方法必须是
public void - 方法只能有一个参数(事件源)
- 方法需要添加
@EventSubscribe注解
第三步:注册订阅者到逻辑服
publicfinalclassEmailLogicServerimplementsLogicServer{@OverridepublicvoidsettingBarSkeletonBuilder(BarSkeletonBuilder builder){ builder.addRunner((EventBusRunner)(eventBus, _)->{ eventBus.register(newEmailEventBusSubscriber());});}}第四步:在业务中发布事件
@ActionMethod(0)privateUserMessagelogin(FlowContext flowContext,LoginMessage message){// 处理登录逻辑...// 发布登录事件var event =UserLoginEventMessage.of(flowContext.getUserId()); flowContext.fire(event);returnnewUserMessage("登录成功");}完成。 就这么简单。登录方法只关心登录本身,邮件、奖励等逻辑在各自的订阅者中实现,分散在不同的逻辑服中。
四种发布粒度
ionet 提供了 4 种事件发布方式,适用于不同场景:
fireMe:仅当前逻辑服
flowContext.fireMe(event);// 异步 flowContext.fireMeSync(event);// 同步只通知当前逻辑服的 EventBus 中的订阅者。适合逻辑服内部的模块解耦。
fireLocal:当前进程所有逻辑服
flowContext.fireLocal(event); flowContext.fireLocalSync(event);如果用户逻辑服和邮件逻辑服在同一进程中启动,fireLocal 会通知两者的订阅者,但不会通知其他进程的订阅者。
fire:全局发布
flowContext.fire(event); flowContext.fireSync(event);所有订阅者都能收到——包括当前进程的、其他进程的、甚至其他机器的。
fireAny:同类型只发一个
flowContext.fireAny(event); flowContext.fireAnySync(event);当有多个同类型逻辑服实例(比如 3 个邮件逻辑服)时,fireAny 只会给其中一个发送事件。适合"只需要一个实例处理"的场景,如发放奖励——防止重复发放。
杀手级特性:零订阅者 = 零开销
这是 ionet EventBus 最精妙的设计:
如果没有任何远程订阅者,将不会触发网络请求。
这意味着什么?
假设你在登录业务中埋了一个事件发布点。即使所有相关的逻辑服都没有上线(没有任何订阅者),这行 fire 代码也不会产生任何网络开销。它只是在内存中检查:有订阅者吗?没有。结束。
这是 Redis pub/sub 和 MQ 做不到的——它们无论有没有消费者,消息都会发送到中间件服务器。
高级用例:可插拔的临时活动
利用"零订阅者 = 零开销"这个特性,你可以实现可插拔的业务逻辑。
场景:春节活动期间,玩家每日在线 60 分钟奖励一些物品。
实现方式:
- 在登录 Action 中发布事件(这个代码永远不变)
- 为活动创建一个独立的【春节活动逻辑服】,注册活动订阅者
- 活动期间:启动【春节活动逻辑服】→ 订阅者上线 → 开始处理事件
- 活动结束:关闭【春节活动逻辑服】→ 订阅者消失 → 零开销
你不需要修改任何主业务代码。 只是通过启动或关闭一个逻辑服,就实现了功能的上线和下线。
同样的思路还可以用于:
- 热更:通过事件批量推送新的配置到所有逻辑服实例
- 全局配置更新:GM 后台发布配置变更事件
- 统计数据收集:单独的统计逻辑服订阅各种业务事件
订阅者线程执行器
对于有并发安全要求的场景,ionet 允许你为订阅者指定线程执行器:
@EventBusSubscriberpublicclassMySubscriber{// 默认策略:使用用户线程执行器(与 Action 共用线程,线程安全)@EventSubscribepublicvoidonLogin(UserLoginEventMessage message){...}// 虚拟线程策略:适合 IO 密集型操作@EventSubscribe(ExecutorSelector.userVirtualExecutor)publicvoidsaveToDB(UserLoginEventMessage message){...}// 方法级线程策略:同一订阅者方法永远在同一线程@EventSubscribe(ExecutorSelector.methodExecutor)publicvoidupdateRank(UserLoginEventMessage message){...}}5 种可选策略:userExecutor(默认)、userVirtualExecutor、methodExecutor、simpleExecutor、customExecutor。
与 Redis pub/sub、MQ 的全面对比
| 特性 | ionet EventBus | Redis pub/sub | MQ (RabbitMQ 等) |
|---|---|---|---|
| 安装依赖 | 无 | Redis 服务器 | MQ 服务器 |
| 服务器费用 | 免费 | 需要 | 需要 |
| 跨进程通信 | ✅ | ✅ | ✅ |
| 跨机器通信 | ✅ | ✅ | ✅ |
| 全链路追踪 | ✅ | ❌ | ❌ |
| 零订阅者零开销 | ✅ | ❌ | ❌ |
| 线程执行器控制 | ✅ | ❌ | ❌ |
| 使用复杂度 | 3 个概念 | 需要配连接、序列化 | 需要配交换器、队列 |
小结
ionet 的分布式事件总线用 3 个概念替代了一整套中间件生态。它不只是"功能上的替代",在性能(零网络开销)、可观测性(全链路追踪)和运维成本(零安装)上都有显著优势。
更重要的是,它让"事件驱动"这种强大的架构模式变得真正低门槛——你不需要理解 Exchange、Binding、Queue、Consumer Group 这些概念,只需要知道:定义事件、写订阅者、发布事件。
更多资源
下一篇预告:[无锁并发不再难 —— ionet 线程执行器设计哲学]