什么是事务消息?RocketMQ 的事务消息如何实现?
简化版
事务消息解决的是「本地事务和发送消息要么都成功、要么都失败」的一致性问题——比如「扣款成功」和「发送扣款通知消息」必须同时成立,不能扣了款却没发消息、或发了消息却没扣款。RocketMQ 的实现基于**「半消息 + 二次确认 + 事务回查」:先发一条半消息**(对消费者不可见)→ 执行本地事务 → 根据事务结果提交或回滚半消息 → 如果二次确认丢失,Broker 会回查本地事务状态来决定最终提交还是回滚。
详细版
RocketMQ 事务消息的完整流程:
1. 生产者发送【半消息】(half message) 到 Broker
—— 半消息已存储,但对消费者【不可见】
2. Broker 返回半消息发送成功
3. 生产者执行【本地事务】(如扣款、写库)
4. 生产者根据本地事务结果,向 Broker 发送二次确认:
- COMMIT → 半消息变为正式消息,消费者可见、可消费
- ROLLBACK → 删除半消息,消费者永远收不到
5. 如果第 4 步的确认丢失(网络问题/生产者宕机):
- Broker 会定期【回查】(check back) 生产者的本地事务状态
- 生产者实现回查接口,返回事务的最终状态
- Broker 据此决定 COMMIT 或 ROLLBACK
- 半消息:已发到 Broker 但对消费者不可见,等本地事务结果决定命运。
- 二次确认:本地事务成功则 COMMIT(消息可见),失败则 ROLLBACK(删除)。
- 事务回查:二次确认丢失时的兜底,Broker 主动问生产者「你那个事务到底成没成」。
完整版教学
一、要解决的问题:本地事务与发消息的原子性
考虑一个典型场景:用户下单后,订单服务要「创建订单(本地数据库事务)」+「发一条消息通知库存服务扣库存」。这两件事必须一致:
- 如果订单创建成功、但消息没发出去→ 库存没扣,超卖。
- 如果消息发出去了、但订单创建失败(回滚了)→ 库存被扣了,但订单不存在,数据错乱。
问题在于「操作本地数据库」和「发消息到 MQ」是两个独立系统的操作,无法用一个本地事务包起来。普通的「先写库再发消息」或「先发消息再写库」都存在中间失败导致不一致的窗口。事务消息就是为解决这个「本地事务 + 发消息」的原子性而生。
二、核心机制:半消息(Half Message)
RocketMQ 的巧妙之处在于引入半消息:
- 生产者先发一条半消息到 Broker。这条消息已经被 Broker 存储了,但对消费者不可见——消费者此刻消费不到它,它处于「待定」状态。
- Broker 确认半消息收到后,生产者再去执行本地事务(创建订单)。
- 根据本地事务的成败,生产者告诉 Broker 怎么处理这条半消息:
- 本地事务成功 → 发 COMMIT,半消息「转正」,变成正式消息,消费者能看到并消费。
- 本地事务失败 → 发 ROLLBACK,Broker 删除半消息,消费者永远看不到。
关键点:消息先发(占个位),但暂不可见;等本地事务尘埃落定,再决定这条消息是「转正」还是「删除」。 这样保证了「本地事务成功 ⟺ 消息最终可见」。
三、兜底机制:事务回查(Check Back)
上面第 3 步的「二次确认(COMMIT/ROLLBACK)」也可能失败——比如生产者执行完本地事务后、还没来得及发确认就宕机了,或者确认消息在网络中丢失了。此时 Broker 手里有一条「半消息」,却迟迟等不到确认,不知道该 COMMIT 还是 ROLLBACK。
RocketMQ 用事务回查解决:
- Broker 发现某条半消息长时间没收到二次确认,就会主动回查生产者:「你之前那个本地事务,到底成功了没?」
- 生产者需要实现一个回查接口:根据本地事务的记录(比如查订单表看订单到底建没建成),返回事务的最终状态。
- Broker 根据回查结果决定最终 COMMIT 还是 ROLLBACK。
回查会重试多次(默认最多 15 次),确保最终能确定这条半消息的命运。这就是为什么事务消息保证的是最终一致性——即使中间出故障,靠回查也能最终达成一致。
四、为什么这套机制能保证一致性
把各种失败情况过一遍:
- 半消息发送失败:本地事务还没执行,什么都没发生,无不一致。
- 本地事务失败:ROLLBACK 删除半消息,消息不可见——「事务没成,消息也没发」,一致。
- 本地事务成功、COMMIT 成功:消息转正可见——「事务成了,消息也发了」,一致。
- 本地事务成功、但 COMMIT 丢失:靠回查,生产者查到本地事务成功,返回 COMMIT——最终消息可见,一致。
- 本地事务失败、但 ROLLBACK 丢失:靠回查,生产者查到本地事务失败,返回 ROLLBACK——最终删除消息,一致。
所有分支都能收敛到「本地事务与消息状态一致」,这就是事务消息的可靠性来源。
五、注意事项与局限
- 保证的是「本地事务 → 发消息」这一端的一致性:即「本地事务成功则消息一定被消费者收到」。但下游消费者能否成功处理消息,是另一回事——消费端还需要自己保证幂等和重试(消息可能重复投递)。
- 回查接口必须幂等且可靠:生产者要能准确回答「事务成没成」,通常靠查业务表状态实现。
- 本地事务要能被查询:设计上要留下事务是否成功的痕迹(如订单表记录)供回查。
- 只保证最终一致,不是强一致:中间有短暂的「消息待定」窗口。
六、常见误区与追问
| 考点 | 正确口径 |
|---|---|
| 事务消息 | 保证本地事务和消息发送最终一致 |
| RocketMQ 流程 | 半消息、本地事务、提交/回滚、事务回查 |
| 适用 | 下单后通知、积分、库存等跨服务最终一致 |
send half message
execute local transaction
if success -> commit message
if fail -> rollback message
if unknown -> broker checks transaction status
事务消息解决的是“本地事务成功后消息必须可见,失败后消息不能被消费”。
- 误区:事务消息等于分布式强一致事务。 它通常实现最终一致,不是两阶段提交式强一致。
- 误区:半消息会被消费者立即消费。 半消息对消费者不可见,只有提交后才可投递。
- 误区:事务回查可以不实现。 本地事务结果未知时,Broker 需要回查来决定提交或回滚。
- 追问:为什么先发半消息? 先让 Broker 知道有待确认消息,避免本地事务成功后发送消息失败。
- 追问:本地事务表有什么用? 记录事务状态,供回查接口可靠返回提交/回滚结果。
- 追问:消费者还需要幂等吗? 需要,事务消息保证投递一致性,不保证消费端只执行一次。
七、加强记忆
事务消息解决「本地事务 + 发消息」的原子一致性(如扣款与发通知不能一个成一个败)。RocketMQ 用 「半消息 + 二次确认 + 事务回查」:① 先发半消息(Broker 已存但消费者不可见)→ ② 执行本地事务 → ③ 二次确认(成功发 COMMIT 让消息转正可见、失败发 ROLLBACK 删除)→ ④ 确认丢失时 Broker 回查生产者的本地事务状态(生产者实现回查接口、查业务表返回结果,重试多次)来最终决定。所有失败分支都收敛到「事务成⟺消息可见」,实现最终一致。注意消费端仍需幂等。核心:消息先占位不可见、事务定了再转正或删除、确认丢了靠回查兜底。