--- tags: [MQ, 事务消息, 分布式事务, RocketMQ, Kafka] create time: 2026-05-24 19:52 --- # 事务消息 ## 概述 事务消息解决的核心问题是:**本地数据库操作与消息发送的原子性**。要么数据库写成功且消息发出去,要么两边都失败——不能出现"数据库写成功了但消息没发出去"或反过来的情况。RocketMQ 原生支持事务消息,Kafka 通过 Producer 事务机制实现类似效果,两者的设计哲学截然不同。 ## 正文 ### 为什么需要事务消息 考虑一个经典场景:订单服务创建订单后,需要通知库存服务扣减库存。代码可能是: ```go func CreateOrder(order Order) error { // 1. 写数据库 db.Insert(order) // 2. 发消息通知库存服务 mq.Send("topic_inventory", order) return nil } ``` 这段代码有两个致命问题: - **消息发送失败**:数据库写成功了,但 `mq.Send` 网络超时,库存不会扣减,数据不一致。 - **数据库回滚但消息已发出**:如果在事务提交前消息就发出去了,库存扣了但订单没创建。 > [!question] 思考 > 能不能把消息发送放在数据库事务里面?比如先 `BEGIN`,写库,发消息,再 `COMMIT`? 不行。消息发送是网络 IO,不在数据库事务的管辖范围内。即使你把发消息放在 `COMMIT` 之后,`COMMIT` 成功到消息发送成功之间仍然有窗口期可能失败。这就是分布式事务的经典难题。 ### 两阶段流程:半消息 → 本地事务 → 提交/回滚 RocketMQ 事务消息的核心思想是引入"半消息"(Half Message)——消息先发送到 Broker,但对 Consumer 不可见,等本地事务执行完毕后再决定是提交(Commit)还是回滚(Rollback)。 ```mermaid sequenceDiagram participant P as Producer participant B as Broker participant DB as 本地数据库 participant C as Consumer P->>B: 1. 发送半消息(Half Message) B-->>P: 返回发送结果(成功/失败) Note over P: 半消息发送成功,开始执行本地事务 P->>DB: 2. 执行本地事务(写订单表) DB-->>P: 事务结果 alt 本地事务成功 P->>B: 3a. 提交半消息(Commit) B->>C: 消息可见,正常消费 else 本地事务失败 P->>B: 3b. 回滚半消息(Rollback) Note over B: 消息被丢弃,Consumer 不可见 end style P fill:#4A90D9,color:#fff style B fill:#F5A623,color:#fff style DB fill:#6EC1E0,color:#fff style C fill:#4A90D9,color:#fff ``` 关键点:半消息写入 Broker 后存在内部 Topic(`RMQ_SYS_TRANS_HALF_TOPIC`),不会投递给 Consumer。只有 Commit 之后,消息才会被转移到真正的目标 Topic。 ### 事务状态回查 如果 Producer 发送 Commit/Rollback 时网络断了怎么办?Broker 收不到最终决定,半消息就"卡住"了。 RocketMQ 的解决方案是**事务状态回查**:Broker 定期扫描超时的半消息(默认 6 次,每次间隔递增),主动向 Producer 发起回查请求,Producer 根据本地事务执行状态返回 Commit、Rollback 或 Unknown。 ```go // 事务消息的 Producer 核心逻辑 type TransactionListener interface { // ExecuteLocalTransaction 执行本地事务 ExecuteLocalTransaction(msg *Message) LocalTransactionState // CheckLocalTransaction Broker 回查时调用 CheckLocalTransaction(msg *MessageExt) LocalTransactionState } // 示例实现 type OrderTransactionListener struct { db *sql.DB } func (l *OrderTransactionListener) ExecuteLocalTransaction(msg *Message) LocalTransactionState { // 执行本地事务:创建订单 order := parseOrder(msg.Body) err := l.db.Exec("INSERT INTO orders ...", order) if err != nil { return RollbackMessage // 失败则回滚半消息 } return CommitMessage // 成功则提交 } func (l *OrderTransactionListener) CheckLocalTransaction(msg *MessageExt) LocalTransactionState { // Broker 回查:查询订单是否已创建 orderID := msg.GetProperty("ORDER_ID") var count int l.db.QueryRow("SELECT COUNT(*) FROM orders WHERE id = ?", orderID).Scan(&count) if count > 0 { return CommitMessage // 订单存在,说明本地事务成功了 } return RollbackMessage // 订单不存在,回滚 } ``` ### Kafka 的 Producer 事务 Kafka 的思路不同——它不提供"半消息"机制,而是让 Producer 开启事务后,所有写入要么全部可见,要么全部不可见。 ```go // Kafka Producer 事务示例(confluent-kafka-go 风格) producer, _ := kafka.NewProducer(&kafka.ConfigMap{ "transactional.id": "order-service-001", // 事务 ID,用于幂等 }) producer.InitTransactions(context.Background()) producer.BeginTransaction() // 在事务中发送多条消息 producer.Produce(orderMsg, nil) producer.Produce(inventoryMsg, nil) // 提交事务——所有消息同时可见 err := producer.CommitTransaction(context.Background()) if err != nil { producer.AbortTransaction(context.Background()) } ``` Consumer 端配合 `isolation.level=read_committed`,只能读到已提交事务的消息,未提交的会被过滤掉。这和数据库的 MVCC 隔离级别是同一个思路。 ### 事务消息 vs 其他分布式事务方案 | 维度 | 事务消息 | 2PC | Saga | TCC | |------|---------|-----|------|-----| | 一致性模型 | 最终一致性 | 强一致性 | 最终一致性 | 最终一致性 | | 性能 | 高 | 低(阻塞) | 高 | 中 | | 实现复杂度 | 低 | 中 | 高 | 很高 | | 业务侵入 | 低 | 低 | 高(补偿逻辑) | 很高(三个接口) | | 适用场景 | 事件驱动、异步解耦 | 数据库分布式事务 | 长流程编排 | 资金类强一致 | | 失败处理 | 回查 + 人工介入 | 全局回滚 | 补偿事务 | Cancel + 防悬挂 | 事务消息的核心优势是**解耦**——Producer 不需要知道下游有哪些 Consumer,也不需要定义补偿逻辑。它适合"本地事务成功后通知其他服务"的场景,不适合需要跨服务同步等待结果的场景。 > [!question] 思考 > 事务消息的回查机制在网络分区时可能失败,如何保证最终一致性? 回查失败时 Broker 会按递增间隔多次重试(默认最多 6 次)。如果全部失败,半消息会进入死信队列(`RMQ_SYS_TRANS_OP_HALF_TOPIC`),运维人员可以介入处理。本质上,事务消息保证的是"最终一致性"而非"强一致性"——在极端故障下,需要人工兜底。生产环境中,建议配合对账任务定期检查本地事务和消息状态的一致性。 ### 事务消息的局限性 - **单向通知**:事务消息只保证"本地事务 + 消息发送"的原子性,不保证 Consumer 消费成功。 - **延迟**:半消息从发送到 Consumer 可见有延迟(取决于本地事务执行时间和可能的回查)。 - **不支持批量**:一次事务消息只能关联一条消息,不支持多条消息的原子发送(Kafka 的 Producer 事务支持批量)。 - **回查依赖网络**:回查机制在网络分区时可能失效,需要运维兜底。 ## 关联笔记 - [[06-高级特性/17-MQ-延迟消息与定时消息|MQ 延迟消息与定时消息]] - [[06-高级特性/19-MQ-消息过滤与路由|MQ 消息过滤与路由]] - [[05-可靠性保障/13-MQ-Exactly-Once-语义|MQ Exactly-Once 语义]] - [[12-架构与实战/46-MQ-分布式事务实践|MQ 分布式事务实践]] - [[07-主流MQ对比/24-RocketMQ|RocketMQ]]