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

173 lines
7.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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]]