vault backup: 2026-05-24 20:51:06
This commit is contained in:
@@ -0,0 +1,223 @@
|
||||
---
|
||||
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-最佳实践]]
|
||||
Reference in New Issue
Block a user