事件驱动架构听起来很优雅:服务之间不再互相调用,而是发布事件、订阅事件,各自完成自己的工作。
但很多系统从同步调用切到消息队列之后,只是把问题换了个地方。原来是接口超时,现在是消息丢失;原来是调用链太长,现在是排查链路断掉;原来是强耦合,现在是没人知道哪个消费者依赖哪个事件。
消息发出去,不等于事情完成了。
事件应该表达事实,不是命令
事件最容易被滥用成异步命令。
比如 SendEmail 更像命令,意思是”请你发邮件”;OrderPaid 更像事件,意思是”订单已经支付”。
两者差别很大。
命令关心执行者,事件关心事实。命令通常只有一个接收方,事件可以被多个系统订阅。命令失败后要明确重试责任,事件则应该让消费者基于事实自行决定要不要行动。
如果事件设计得像命令,系统表面上解耦了,实际只是把远程调用换成了队列调用。
可靠投递需要闭环
很多事故来自一个经典缝隙:数据库事务提交成功了,但消息发送失败了。
例如订单状态已经变成已支付,但支付完成事件没有发出去。下游库存、积分、通知系统都不知道这件事发生过。
解决思路通常是建立投递闭环:
- 使用 outbox 表,把业务变更和待发送事件放进同一个数据库事务。
- 后台任务扫描 outbox,负责发送和重试。
- 事件发送成功后标记状态。
- 消费端保存处理记录,避免重复消费造成副作用。
这套东西不华丽,但很管用。可靠性经常不是靠某个神奇中间件,而是靠把每一步状态留下来。
消费者必须默认会收到重复消息
消息系统的可靠性常常建立在”至少一次投递”上。
这意味着消费者必须接受一个事实:同一条消息可能被处理多次。
所以消费逻辑要有幂等设计:
- 用业务唯一键去重。
- 更新状态前检查当前状态是否合法。
- 外部副作用要有请求编号。
- 可重试操作和不可重试操作分开处理。
- 失败消息进入死信队列后要能人工排查。
不要把”消息不会重复”当成系统假设。它迟早会重复,而且通常发生在你最不想看到它重复的时候。
顺序不是免费的
事件驱动系统里,顺序是一个昂贵资源。
如果你要求所有消息全局有序,吞吐量会被严重限制。更现实的做法是只在必要范围内保证局部顺序。
比如同一个订单的事件需要按顺序处理,但不同订单之间没有必要互相等待。这个时候可以用订单 id 作为分区键,让同一实体的事件进入同一个分区。
顺序设计要问清楚:
- 哪些事件必须有序?
- 顺序范围是全局、用户级、订单级,还是某个业务实体级?
- 乱序到达时消费者能不能自我修正?
- 旧事件晚到时是否应该丢弃?
顺序越大,系统越慢;顺序越小,消费者越复杂。没有白拿的优雅。
可观测性决定你能不能救火
事件驱动架构最怕黑盒。
当一个用户说”我付款了但没收到通知”时,你需要知道:
- 业务状态是否已经提交。
- 事件是否写入 outbox。
- 消息是否发送到队列。
- 哪些消费者收到了。
- 消费是否成功。
- 失败原因是什么。
这要求事件里带上 trace id、业务 id、事件 id、版本号和发生时间。日志、指标、告警也要围绕这些字段建立。
否则系统越解耦,排查时越像在雾里找开关。
最后
事件驱动架构的价值,是把系统从同步阻塞里解放出来,让不同模块围绕业务事实独立演进。
但它不是可靠性的免费午餐。可靠投递、幂等消费、局部顺序、版本兼容、死信处理、链路追踪,一个都少不了。
真正成熟的事件驱动系统,不是”用了消息队列”,而是每一条消息从产生到消费都有迹可循,失败之后有人知道怎么把它带回正轨。