本地消息表(可靠消息最终一致)方案是怎么实现的?
简化版
本地消息表通过「把业务操作和发消息记录放进同一个本地事务」来保证两者原子性,从而实现跨服务最终一致。做法:发起方在本地事务里既改业务数据、又往「消息表」插一条待发送消息(同一事务保证要么都成功要么都失败);然后由一个后台任务把消息表里的消息投递到 MQ,下游消费并回执,投递失败就重试。这样保证「业务成功了消息一定发得出去」,下游最终一定能收到并处理。
详细版
核心矛盾:本地操作成功了,但「发消息给 MQ」是网络操作,可能失败——如果先提交业务再发消息,发消息失败就丢了下游动作;如果先发消息再提交业务,业务回滚了消息却发出去了。两个动作无法用一个事务包住(一个是 DB、一个是 MQ)。
本地消息表的解法:把「发消息」这个动作转化成「往本地数据库的消息表插一条记录」——这就和业务操作同属一个数据库、可以放进同一个本地事务,原子性天然保证。
完整流程:
- 发起方本地事务:改业务数据 + 往
local_message表插一条「待发送」消息,一起提交。 - 后台投递任务:轮询消息表里「待发送」的消息,投递到 MQ,投递成功后把状态改为「已发送」。
- 下游消费:从 MQ 消费消息,执行自己的业务(要幂等),处理成功后回 ACK / 回执。
- 重试与补偿:投递失败或下游未回执,后台任务定时重投;超过次数告警人工介入。
完整版教学
一、为什么不能直接「业务 + 发 MQ」
设想下单后要通知积分服务加积分。最直接的写法是:本地事务提交订单 → 然后 mqProducer.send(加积分消息)。问题在于这两步不原子:
- 订单提交成功,但发 MQ 时网络抖动失败 → 积分永远不会加(消息丢了)。
- 或者先发 MQ 成功、再提交订单时数据库报错回滚 → 积分加了但订单没了(消息多发了)。
根因:数据库和 MQ 是两个独立资源,没法用一个本地事务同时保证。本地消息表就是来解这个「跨资源原子」难题的。
二、精髓:把「发消息」降级成「写本地表」
关键一步转换:不直接发 MQ,而是把消息先写进和业务同库的一张消息表。因为消息表和业务表在同一个数据库,二者的写入可以放进同一个本地事务,数据库的 ACID 天然保证「业务改了,消息记录也一定在;业务回滚了,消息记录也没了」。这就把「DB + MQ 的跨资源原子」问题,转化成了「同库两张表的本地事务」,一举解决。至于「真正投递到 MQ」这一步的可靠性,交给后台任务的重试来兜底——反正消息记录已经安全落库,大不了多试几次。
记忆点:本地消息表的核心魔法是「先把要发的消息落到自己库里」,让原子性问题回到熟悉的本地事务,再用异步重试保证消息最终发出去。
三、如何保证「一定送达且不重复」
- 不丢:消息落库了(和业务同事务),后台任务持续重投直到 MQ 确认收到,中途宕机重启后继续扫表重投——所以不会丢。
- 不重复导致错误:因为有重试,下游可能收到重复消息(MQ 本身也是至少一次投递),所以下游消费必须幂等(用消息 ID 去重表等),保证重复消费不出错。
- 消息表清理:已确认消费的消息定期归档/删除,避免表无限膨胀。
四、和事务消息(RocketMQ)的关系
事务消息是「本地消息表的 MQ 内置版」:RocketMQ 把「消息表 + 投递重试」的活儿自己做了。发起方发「半消息」(对下游不可见)→ 执行本地事务 → 根据结果 commit(半消息转正)或 rollback(丢弃)→ MQ 还会「回查」本地事务状态兜底。二者思想一致(都保证「本地事务与消息发送的原子性」),区别是:
- 本地消息表:消息状态存在自己的数据库,实现简单、通用(任何 MQ 都行),但要自己维护表和投递任务、消息表和业务表耦合。
- 事务消息:靠 MQ 的机制,不用自己建表,但需要 MQ 支持(RocketMQ),且要实现「事务回查」接口。
五、优缺点与适用
- 优点:实现简单、易理解、不依赖特定 MQ、可靠性高(消息落库 + 重试)。
- 缺点:消息表和业务库耦合(侵入业务库)、需要额外的投递任务和清理逻辑、有一定延迟(异步)。
- 适用:允许最终一致的异步场景——下单加积分、发通知、同步数据、更新非核心的关联数据。不适合要求强一致或实时的核心链路。
六、常见误区与追问
这道题不能只背概念,要把「本地消息表」放回真实分布式系统里解释:谁发起、谁协调、状态如何变化、失败后怎么兜底,以及它和性能、可用性、一致性的取舍。
| 回答层次 | 要讲清的内容 | 容易漏掉的边界 |
|---|---|---|
| 核心结论 | 本地消息表把业务写入和待发送消息放在同一个本地事务中,再异步投递 MQ 保证最终一致 | 不要停在名词解释 |
| 流程机制 | 本地事务写业务数据 -> 同事务写消息表 -> 后台扫描待发送消息 -> 投递 MQ -> 消费者幂等处理 -> 成功后标记消息完成 | 说明触发方、参与方、状态变化和兜底 |
| 工程取舍 | 创建订单和插入 outbox 消息同库同事务成功后,后台任务每 1 秒扫描未发送消息投递 MQ | 分布式事务没有免费强一致,性能、可用性和业务侵入一定要取舍 |
本地消息表 面试拆解:
1. 本地事务写业务数据
2. 同事务写消息表
3. 后台扫描待发送消息
4. 投递 MQ
5. 消费者幂等处理
6. 成功后标记消息完成
记忆钩子:先说本地事务失效的原因,再拆协调、补偿、消息、幂等、对账和回滚边界;回答时要紧扣「本地消息表」这道题,不要把相邻概念混成一段泛泛的分布式套话。
- 误区:写了消息表就不会重复消费。 投递和消费都可能重复,消费者仍要幂等。
- 误区:消息表可以不清理。 长期积累会影响扫描效率,需要归档、分区或清理。
- 误区:本地消息表适合强实时同步。 它是异步最终一致,存在投递延迟。
- 追问:为什么业务和消息要同事务? 保证业务成功时消息一定被记录,业务失败时消息不会产生。
- 追问:消息投递失败怎么办? 定时扫描重试、记录次数、告警和人工处理。
- 追问:它和事务消息区别是什么? 本地消息表靠业务库 outbox,事务消息把半消息机制交给 MQ。
七、加强记忆
本地消息表通过「业务操作 + 插入消息记录放进同一个本地事务」保证二者原子(把 DB+MQ 的跨资源难题转化为同库两表的本地事务),再由后台任务轮询消息表投递到 MQ、失败重试,下游幂等消费。核心魔法是「先把消息落到自己库里」。它保证「业务成功则消息最终一定送达」,实现最终一致。事务消息(RocketMQ 半消息+回查)是它的 MQ 内置版,思想相同。下游务必幂等。