Files
cs-note/hhs/MQ/12-架构与实战/46-MQ-分布式事务实践.md
T
2026-05-24 20:51:06 +08:00

224 lines
7.7 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
create time: 2026-05-24 19:52
---
# MQ 分布式事务实践
## 概述
在微服务架构下,一个业务操作往往涉及多个服务的数据变更,分布式事务成为绕不开的难题。本文介绍三种基于 MQ 的分布式事务方案:本地消息表、RocketMQ 事务消息、Saga 模式,并给出 Go 代码实现。
## 正文
### 分布式事务问题
以电商下单为例,一个"创建订单"操作至少涉及三个服务:
1. **订单服务**:创建订单记录
2. **库存服务**:扣减库存
3. **支付服务**:发起支付
这三个操作需要保证最终一致性——不能出现订单创建了但库存没扣、或者库存扣了但支付没发起的情况。传统的 2PC/XA 方案在微服务架构下性能差、可用性低,MQ 成了更实用的选择。
> [!question]
> 为什么 2PC 在微服务架构下不受欢迎?它的问题仅仅是性能差吗?
### 方案一:本地消息表 + MQ
这是最经典、最可靠的方案,核心思想是**利用本地事务保证业务操作和消息写入的原子性**。
```mermaid
graph TD
A["开始本地事务"] --> B["写入业务数据 - 订单表"]
B --> C["写入消息表 - 同一事务"]
C --> D["提交事务"]
D --> E["后台任务轮询消息表"]
E --> F["发送消息到MQ"]
F --> G["消费端处理 - 扣减库存"]
G --> H["消费端幂等校验"]
H --> I["更新消息状态为已完成"]
```
核心逻辑分三步:
1. **写入阶段**:在同一个数据库事务中,写入业务数据和消息表。这保证了两者的原子性。
2. **发送阶段**:后台任务(定时器或 binlog 监听)扫描消息表中未发送的记录,投递到 MQ。
3. **消费阶段**:消费端处理消息并做幂等校验(比如用消息 ID 去重),处理成功后回调更新消息状态。
```go
// 本地消息表方案的核心实现
type MessageRecord struct {
ID string
Topic string
Payload string
Status string // pending, sent, completed
RetryCount int
CreatedAt time.Time
}
// 1. 本地事务:写业务数据 + 消息表
func CreateOrder(db *sql.DB, order Order, msg MessageRecord) error {
tx, _ := db.Begin()
defer tx.Rollback()
// 写入订单
_, err := tx.Exec("INSERT INTO orders ...", order.ID, order.Amount)
if err != nil { return err }
// 写入消息表,同一事务
_, err = tx.Exec("INSERT INTO outbox_messages ...",
msg.ID, msg.Topic, msg.Payload, "pending")
if err != nil { return err }
return tx.Commit() // 原子提交
}
// 2. 后台任务:轮询发送
func PollAndSend(db *sql.DB, producer MQProducer) {
rows, _ := db.Query(
"SELECT id, topic, payload FROM outbox_messages WHERE status='pending' LIMIT 100")
for rows.Next() {
var msg MessageRecord
rows.Scan(&msg.ID, &msg.Topic, &msg.Payload)
if err := producer.Send(msg.Topic, msg.Payload); err == nil {
db.Exec("UPDATE outbox_messages SET status='sent' WHERE id=?", msg.ID)
}
}
}
// 3. 消费端:幂等处理
func HandleInventoryMsg(ctx context.Context, msg Message) error {
// 幂等检查:消息是否已处理过
exists, _ := db.Query("SELECT 1 FROM processed_messages WHERE id=?", msg.ID)
if exists { return nil } // 已处理,跳过
// 扣减库存
err := inventoryService.Deduct(msg.ProductID, msg.Quantity)
if err != nil { return err }
// 记录已处理
db.Exec("INSERT INTO processed_messages (id) VALUES (?)", msg.ID)
// 回调更新消息状态
db.Exec("UPDATE outbox_messages SET status='completed' WHERE id=?", msg.ID)
return nil
}
```
### 方案二:事务消息(RocketMQ)
RocketMQ 原生支持事务消息,比本地消息表更优雅。核心流程分三个阶段:
**阶段一:发送半消息**
Producer 先发送一条"半消息"(Half Message)到 Broker。这条消息对 Consumer 不可见。
**阶段二:执行本地事务**
Producer 收到半消息发送成功的确认后,执行本地业务逻辑(比如创建订单)。
**阶段三:提交或回滚**
根据本地事务的执行结果,向 Broker 发送 Commit(消息对 Consumer 可见)或 Rollback(丢弃消息)。
如果 Broker 没有收到 Commit/Rollback(比如 Producer 宕机),Broker 会主动**回查**Producer 的本地事务状态。这个回查机制是 RocketMQ 事务消息的精髓。
```go
// RocketMQ 事务消息流程
func CreateOrderWithTxMsg(producer TxProducer, order Order) error {
// 1. 发送半消息
msg := mq.NewMessage("order-topic", order.ToJSON())
result, err := producer.SendMessageInTransaction(msg, func(localMsg *mq.Message) mq.LocalTransactionState {
// 2. 执行本地事务
err := orderService.Create(order)
if err != nil {
return mq.RollbackMessage // 回滚,消息丢弃
}
return mq.CommitMessage // 提交,消息可见
})
return err
}
// 3. 事务回查(由 Broker 触发)
func CheckLocalTransaction(msg *mq.Message) mq.LocalTransactionState {
orderID := extractOrderID(msg)
order, err := orderService.Get(orderID)
if err != nil {
return mq.Unknown // 状态未知,稍后再查
}
if order.Status == "created" {
return mq.CommitMessage
}
return mq.RollbackMessage
}
```
### 方案三:Saga 模式 + MQ
Saga 适用于长事务场景,核心思想是将一个大事务拆成一系列小事务,每个小事务有对应的**补偿操作**。
Saga 有两种协调方式:
- **编排式(Orchestration)**:由一个中心协调器控制流程,按顺序调用各服务。
- **协同式(Choreography)**:各服务通过事件驱动协作,没有中心协调器,通过 MQ 传递事件。
```go
// Saga 协同式:通过事件驱动
type OrderSagaEvent struct {
Type string // OrderCreated, InventoryReserved, PaymentCompleted
OrderID string
Data interface{}
}
// 订单服务:创建订单后发布事件
func CreateOrder(order Order) {
db.Save(order)
mq.Publish("order-events", OrderSagaEvent{
Type: "OrderCreated", OrderID: order.ID, Data: order,
})
}
// 库存服务:监听订单事件,扣减库存
func OnOrderCreated(event OrderSagaEvent) {
err := inventory.Reserve(event.Data.(Order).Items)
if err != nil {
// 库存不足,发布失败事件触发补偿
mq.Publish("order-events", OrderSagaEvent{
Type: "InventoryReservationFailed", OrderID: event.OrderID,
})
return
}
mq.Publish("order-events", OrderSagaEvent{
Type: "InventoryReserved", OrderID: event.OrderID,
})
}
// 订单服务:监听库存失败事件,补偿取消订单
func OnInventoryReservationFailed(event OrderSagaEvent) {
orderService.Cancel(event.OrderID) // 补偿操作
mq.Publish("order-events", OrderSagaEvent{
Type: "OrderCancelled", OrderID: event.OrderID,
})
}
```
### 三种方案对比
| 维度 | 本地消息表 | RocketMQ 事务消息 | Saga + MQ |
|------|-----------|------------------|-----------|
| 一致性保证 | 最终一致 | 最终一致 | 最终一致 |
| 性能影响 | 中(轮询 + DB 写入) | 低(Broker 端处理) | 低(事件驱动) |
| 实现复杂度 | 中 | 低(SDK 支持) | 高(需设计补偿链) |
| 适用场景 | 通用场景 | RocketMQ 技术栈 | 长事务、跨多服务 |
| 数据一致性边界 | 同一 DB 事务 | 本地事务 + MQ | 跨多个独立服务 |
> [!question]
> 本地消息表模式下,如果消息发送成功但消费端处理失败,如何实现最终一致性?提示:想想消息表的状态管理和重试机制。
## 关联笔记
- [[44-MQ-高可用架构]]
- [[47-MQ-与微服务]]
- [[48-MQ-客户端-SDK-最佳实践]]