250 lines
7.7 KiB
Markdown
250 lines
7.7 KiB
Markdown
|
|
---
|
|||
|
|
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-服务间通信]] — 消息投递的一致性保障
|