1. 首页
  2. 面试专题
  3. 文章列表
通用专题 Java/后端开发 消息一致性与 Outbox 2026-06-14

本地事务之后再发消息,为什么总会留下不一致窗口

业务入库和消息发送无法天然处在同一个本地事务里,Outbox 模式用事件表、投递器和幂等消费把不一致窗口变成可恢复流程。

后端项目里经常有这样的流程:订单创建成功后发一条消息,库存系统、积分系统或通知系统再去消费。面试官追问时,真正难点不在“会不会用 MQ”,而在本地数据库事务和消息发送之间总有一个缝隙。

如果先写数据库,再发消息,数据库提交后服务宕机,消息可能没发出去。如果先发消息,再写数据库,消费者可能看到一个数据库里还不存在或最终回滚的业务对象。把两步调换顺序解决不了根本问题,因为数据库和消息队列通常不在同一个本地事务里。

双写问题不能靠侥幸顺序解决

很多代码看起来很自然:service 方法里先保存订单,再调用 mqTemplate.send。正常情况下没问题,但异常总是发生在边界上。网络抖动、进程重启、消息发送超时、事务提交失败,都可能让两边状态不一致。

分布式事务可以在某些场景里使用,但成本、性能和依赖都比较重。多数互联网后端更常见的是接受最终一致,然后把“可能失败”设计成可重试、可观测、可补偿的流程。Outbox 模式就是这个思路。

Outbox 把消息变成业务事务的一部分

Outbox 的核心做法是:在同一个数据库本地事务里,既写业务表,也写一条待发送事件到 outbox 表。只要业务事务提交成功,事件记录也一定存在;如果事务回滚,事件也不会出现。

随后由后台投递器扫描 outbox 表,把待发送事件投递到消息队列。发送成功后标记状态;发送失败则保留记录,稍后重试。这样消息发送不再依赖业务请求现场一次成功,而是变成一个可靠的后台交付过程。

它解决的是丢消息,不是只发一次

Outbox 通常保证的是至少投递一次,而不是天然只投递一次。投递器可能发送成功后还没来得及更新状态就宕机,恢复后会再次发送同一条事件。因此消费者必须幂等。

幂等消费可以靠业务唯一键、事件 ID、状态机版本、去重表或数据库唯一约束实现。比如订单已取消的事件重复到达,消费者应该识别当前状态,而不是再次扣库存或重复发券。很多消息系统事故不是生产者没发,而是消费者假设消息只会来一次。

事件表也需要治理

Outbox 表不是随便加一张表就结束。它要有事件类型、业务 ID、唯一事件 ID、状态、重试次数、下次重试时间、错误原因和创建时间。投递器要控制批量大小、失败退避、并发锁定和老数据归档。

如果事件量很大,也可以考虑基于数据库变更日志的 CDC 方案,由日志订阅组件把业务表变化转成消息。但 CDC 同样要处理顺序、过滤、重复和回放。方案不同,工程问题不会消失。

面试里这样讲更稳

可以先指出双写窗口:数据库提交和消息发送不是一个原子操作。然后给出 Outbox:业务数据和事件表同事务写入,后台投递器负责发送,消费者通过幂等承接重复。最后补充可观测性:积压量、失败次数、最老未发送事件时间都要监控。

这道题考的不是 MQ API,而是你是否知道一致性问题不能靠“正常情况下会成功”来设计。