Files
cs-note/hhs/MQ/06-高级特性/18-MQ-事务消息.md
T
2026-05-24 20:51:06 +08:00

7.4 KiB
Raw Blame History

tags, create time
tags create time
MQ
事务消息
分布式事务
RocketMQ
Kafka
2026-05-24 19:52

事务消息

概述

事务消息解决的核心问题是:本地数据库操作与消息发送的原子性。要么数据库写成功且消息发出去,要么两边都失败——不能出现"数据库写成功了但消息没发出去"或反过来的情况。RocketMQ 原生支持事务消息,Kafka 通过 Producer 事务机制实现类似效果,两者的设计哲学截然不同。

正文

为什么需要事务消息

考虑一个经典场景:订单服务创建订单后,需要通知库存服务扣减库存。代码可能是:

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)。

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。

// 事务消息的 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 开启事务后,所有写入要么全部可见,要么全部不可见。

// 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 事务支持批量)。
  • 回查依赖网络:回查机制在网络分区时可能失效,需要运维兜底。

关联笔记