把土办法下沉到 MQ
本地消息表方案讲给团队听,有人说:土是真的土——每个要发消息的业务都要建一张消息表、养一个扫描任务,十个业务十张表。RocketMQ 的回应是:「先存后投」的逻辑我替你做,搬进消息服务器内部,这就是事务消息(Transactional Message)。应用侧只剩「执行本地事务」一件事,消息表和扫描任务全部消失。
半消息:先锁事实,再干活
事务消息的核心概念是半消息(Half Message)——一条「暂扣」在 MQ 里、消费者看不见的消息。完整流程四步:
1 生产者 → MQ:发送半消息(消费者不可见)
2 MQ:半消息持久化,回复「收到」
3 生产者:执行本地事务(订单落库)
4 生产者 → MQ:commit(消息转正,开始投递)
或 rollback(删除半消息)对照本地消息表看透本质:半消息就是 MQ 帮你管理的那条「待发送」记录——第 2 步 MQ 确认落盘,相当于消息记录已提交;第 3、4 步的顺序保证了「要么消息和业务一起成立,要么半消息被删掉」。顺序题的两难被结构化解开了:先让消息「半」着,业务成了再转正。
回查:进程挂了的兜底
流程走到第 3 步,生产者本地事务提交成功,还没来得及发 commit 就崩了——半消息悬在 MQ 里,转正还是删除?MQ 不知道,但它会问:定时对悬而未决的半消息发起回查(Check Local Transaction),生产者收到回查后查自己的本地事务状态,回答 commit、rollback 或 unknown(unknown 则稍后继续查)。
关键在「查什么」:回查要能从业务数据本身判断事务做没做成——订单表里有没有这笔单、状态是什么。这要求业务记录可查、状态语义清晰(成功/失败不能都是同一个模糊态)。有团队偷懒在内存里记个标记应付回查,进程一重启标记蒸发,回查永远 unknown,半消息等满最大回查次数后默认 rollback——消息丢了。回查逻辑不是可选项,是这个方案的地基。
与本地消息表对比
| 维度 | 本地消息表 | 事务消息 |
|---|---|---|
| 消息暂存 | 业务库里的消息表 | MQ 内部半消息 |
| 应用侧工作 | 写消息表 + 扫描任务 | 本地事务 + 回查实现 |
| DB 开销 | 每笔多一次消息表写入 | 无额外写入 |
| MQ 绑定 | 任何 MQ 都行 | RocketMQ(或同类实现) |
| 排查视角 | 查自己的表,直白 | 查 MQ 控制台 + 回查日志 |
别高估它的保真范围
两个常见误解纠正:一,事务消息只管发送端——保证「业务成功则消息必达」,消费端该重试重试、该幂等幂等,一样不能少;二,它不是 ACID 事务——MQ 和数据库之间没有统一提交点,靠的是「先半后全 + 回查」的收敛协议,中间存在短暂的双向不确定,最终一致而已。另外 Kafka 原生没有这套机制(它的事务是另一回事——流处理 exactly-once),Kafka 体系想要可靠消息,本地消息表仍是首选。
小结
事务消息一句话:半消息锁事实、本地事务定乾坤、回查兜进程挂——把本地消息表下沉进 MQ,省表省任务,代价是绑定 RocketMQ 与一份可靠的回查实现。发送端到 MQ 的链路闭环了,但有一类场景连 MQ 都嫌重:给用户发个到账短信,通知失败了总不能无限重试把 MQ 塞满——下一篇:最大努力通知,通知到为止的艺术。
评论 (0)