Files
cs-note/hzh/MS/03-数据一致性/02-分布式事务.md
T
2026-05-24 11:42:38 +08:00

250 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: [microservice, distributed-transactions, saga, tcc, outbox]
create time: 2026-05-05
---
# 分布式事务
## 概述
每个服务拥有独立数据库,跨服务的"一次操作"实际上涉及**多个本地事务**。如何保证这些本地事务要么全部成功、要么全部回滚,就是分布式事务要解决的问题。
```mermaid
graph LR
A["用户下单"] --> B["订单服务<br/>写库"]
B --> C["库存服务<br/>扣减"]
C --> D["支付服务<br/>扣款"]
style A fill:#e3f2fd
style B fill:#fff3e0
style C fill:#fff3e0
style D fill:#fff3e0
```
## 分布式事务方案全景
| 方案 | 一致性级别 | 性能 | 复杂度 | 适用场景 |
|------|-----------|------|--------|---------|
| **本地事务 + MQ 事件** | 最终一致 | ⭐⭐⭐⭐⭐ | ⭐ | 绝大多数场景 |
| **Saga 模式** | 最终一致 | ⭐⭐⭐ | ⭐⭐ | 长流程业务 |
| **TCC** | 强最终一致 | ⭐⭐⭐ | ⭐⭐⭐⭐ | 对一致性要求较高的场景 |
| **AT 模式 (Seata)** | 伪强一致 | ⭐⭐ | ⭐ | 不想改业务代码时 |
> [!tip] 选择策略
> **先默认用本地事务 + 异步事件(最简单、最高效)**,只有在业务明确需要 Saga 或 TCC 时才升级。80% 的场景,本地事务 + MQ 就足够了。
## 方案一:本地事务 + 消息队列
核心思想:**将"数据变更 + 发消息"合并到一个本地事务中。**
### Outbox 模式(推荐)
```mermaid
sequenceDiagram
participant App as 应用服务
participant DB as 数据库
participant Outbox as Outbox 表
participant MQ as 消息队列
participant Sub as 订阅方
App->>DB: BEGIN 事务
App->>DB: 写业务数据
App->>Outbox: 写入待发送消息
App->>DB: COMMIT
loop 定时任务
MQ->>Outbox: 扫描 status='pending'
Outbox-->>MQ: 返回消息列表
MQ->>Sub: 投递消息
MQ->>Outbox: 更新为 'sent'
end
Note right of Sub: 至少一次投递 + 消费者幂等
```
#### Outbox 表建表示例
```sql
CREATE TABLE outbox (
id BIGSERIAL PRIMARY KEY,
topic VARCHAR(255) NOT NULL, -- 消息主题
payload JSONB NOT NULL, -- 消息体
status VARCHAR(20) NOT NULL DEFAULT 'pending', -- pending / sent / failed
error_msg TEXT, -- 失败原因
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
sent_at TIMESTAMP
);
-- 加速定时扫描查询
CREATE INDEX idx_outbox_pending ON outbox (status, created_at)
WHERE status = 'pending';
```
```go
// Go 示例:出事务内同时写业务数据和 outbox 记录
tx, _ := db.Begin()
tx.Exec("INSERT INTO orders (user_id, total) VALUES ($1, $2)", userID, total)
tx.Exec(`INSERT INTO outbox (topic, payload, status)
VALUES ('order.created', $1, 'pending')`, jsonPayload)
tx.Commit()
// 后台 goroutine 轮询并推送
func outboxWorker(ctx context.Context, ticker *time.Ticker) {
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
sendPendingMessages(ctx)
}
}
}
```
#### 事务消息(RocketMQ 原生支持)
如果使用的是 RocketMQ,可以绕过 Outbox 模式直接用事务消息:
```go
// 发送事务消息
txMsg := rocketmq.NewTransactionMessage("order-created", payload)
localTx := &MyLocalTxChecker{}
// half 消息发送 → 本地事务执行 → 提交/回查
res, _ := producer.SendMessageInTransaction(txMsg, localTx)
```
RocketMQ 的事务消息流程:
1. 生产者发送 "half 消息" 到 MQ(消费者不可见)
2. 执行本地事务
3. 根据结果 Commit(消费者可见)或 Rollback(丢弃)
4. 如果步骤 2 超时,MQ 回查本地事务状态
### 消费幂等性
> [!warning] 关键保障
> 消息可能重复投递(网络超时、MQ 重投),消费者必须做到**幂等**——处理一次和处理多次的结果完全相同。
**三种常见策略:**
| 策略 | 实现方式 | 适用场景 |
|------|---------|---------|
| **数据库唯一约束** | `msg_id` 做 `UNIQUE` | 最可靠,推荐首选 |
| **Redis 去重键** | `SET dedup:{msg_id} 1 NX EX 86400` | 高吞吐场景 |
| **乐观锁版本控制** | `UPDATE SET qty = qty - N WHERE version = V` | 金额调整类操作 |
```go
// 推荐方案:利用 UNIQUE 约束做幂等保障
_, err := db.Exec(`
INSERT INTO order_events (msg_id, order_id, action, amount)
VALUES ($1, $2, $3, $4)
ON CONFLICT (msg_id) DO NOTHING
`, msgID, orderID, action, amount)
if !isUniqueViolation(err) {
log.Error("process message failed", err)
return
}
// 执行业务逻辑——到这里说明消息是新到达的
```
## 方案二:Saga 模式
Saga 适用于**跨多个服务的长流程操作**,将大事务拆成一系列本地小事务,每个步骤都有对应的补偿操作。
### 编排式 vs 编舞式
```mermaid
graph TB
subgraph ORCHESTRATION["编排式 — Coordinator 中心化"]
CO[Coordinator] --> S1[OrderSvc: Create]
CO --> S2[InventorySvc: Reserve]
CO --> S3[PaymentSvc: Charge]
S1 -.->|失败→Cancel| CO
S2 -.->|失败→Cancel| CO
S3 -.->|失败→Cancel| CO
end
subgraph CHOREOGRAPHY["编舞式 — 事件驱动"]
E1[OrderCreated] --> S11[OrderService]
S11 --> E2[StockReserved]
E2 --> S12[InventoryService]
S12 --> E3[PaymentCharged]
E3 --> S13[PaymentService]
S13 -.-> E4[PaidFailed] -.-> S11
end
```
| 维度 | 编排式 | 编舞式 |
|------|--------|--------|
| **控制流** | 中心化 Coordinator | 各服务通过事件自发响应 |
| **可观测性** | ✅ 集中管理全流程 | ❌ 流程散布在各服务 |
| **耦合度** | 依赖 Coordinator | 服务间仅感知事件 |
| **适合规模** | 5~15 步的 Saga | 简单链路 (< 5 步) |
### Saga 补偿设计原则
每个正向操作必须有对应的**反向补偿**:
| 正向操作 | 补偿操作 |
|---------|---------|
| 创建订单 | 取消订单 |
| 预留库存 | 释放库存 |
| 扣款 | 退款 |
| 发送通知 | 撤销通知(一般不需要) |
> [!warning] 补偿操作的幂等性
> 补偿操作也必须幂等——CancelOrder 可能被触发多次。用订单状态的流转来保证(如只有 PENDING 才能转到 CANCELLED)。
## 方案三:TCC (Try-Confirm-Cancel)
TCC 在每个事务步骤中实现三个接口:
```
Try: 预留资源(冻结余额 / 锁定库存)
Confirm: 确认使用资源(正式扣减 / 正式锁定)
Cancel: 释放资源(解冻余额 / 解锁库存)
```
### TCC 时序图
```mermaid
sequenceDiagram
participant Orch as Coordinator
participant O as Order Service
participant I as Inventory Service
participant P as Payment Service
Orch->>O: Try(CreateOrder)
O-->>Orch: OK
Orch->>I: Try(ReserveStock)
I-->>Orch: OK
Orch->>P: Try(ChargeBalance)
P-->>Orch: OK
Orch->>O: Confirm
Orch->>I: Confirm
Orch->>P: Confirm
Note over Orch,P: 全部 Confirm → 事务完成
```
### TCC vs Saga 对比
| 维度 | TCC | Saga |
|------|-----|------|
| 一致性强度 | 较强(资源被占用期间不允许其他事务使用) | 较弱(中间态数据可见) |
| 开发成本 | 高(每个业务方法实现 Try/Confirm/Cancel) | 低(只需正向 + 反向操作) |
| 性能 | 中(需要两阶段提交) | 中高(单阶段本地事务) |
| 适用场景 | 资金、库存等高敏感业务 | 订单流程、审批流等业务链 |
## 关联笔记
- [[03-数据一致性/01-数据库拆分]] — 数据库拆分是分布式事务的前提
- [[02-服务治理/06-容错模式]] — 熔断器和重试在分布式事务中的作用
- [[02-服务治理/05-服务间通信]] — 消息投递的一致性保障