549 lines
19 KiB
Markdown
549 lines
19 KiB
Markdown
---
|
||
tags: [microservice, distributed-transactions, saga, tcc, outbox, seata, idempotency, dlq]
|
||
create time: 2026-05-17 15:00
|
||
---
|
||
|
||
# 分布式事务
|
||
|
||
## 概述
|
||
|
||
每个服务拥有独立数据库,跨服务的"一次操作"实际上涉及**多个本地事务**。如何保证这些本地事务要么全部成功、要么全部回滚,就是分布式事务要解决的问题。
|
||
|
||
```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)
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### 消费幂等性
|
||
|
||
> [!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
|
||
}
|
||
// 执行业务逻辑——到这里说明消息是新到达的
|
||
```
|
||
|
||
#### 事务消息 vs Outbox
|
||
|
||
> [!question] 思考一下
|
||
> Outbox 模式有一个天然缺点:定时轮询有延迟(通常秒级)。如果你需要**毫秒级的延迟敏感型通知**(如支付结果实时推送到用户端),该怎么办?
|
||
|
||
如果使用的是 RocketMQ,可以绕过 Outbox 模式直接用事务消息:
|
||
|
||
```go
|
||
// 发送事务消息
|
||
txMsg := rocketmq.NewTransactionMessage("order-created", payload)
|
||
localTx := &MyLocalTxChecker{}
|
||
|
||
// half 消息发送 → 本地事务执行 → 提交/回查
|
||
res, _ := producer.SendMessageInTransaction(txMsg, localTx)
|
||
```
|
||
|
||
RocketMQ 事务消息的完整交互流程:
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant Producer as 生产者
|
||
participant Broker as MQ Broker
|
||
participant DB as 业务数据库
|
||
participant Consumer as 消费者
|
||
|
||
Producer->>Broker: ① 发送 Half Message (消费者不可见)
|
||
Broker-->>Producer: ACK
|
||
|
||
Producer->>DB: ② 执行本地事务
|
||
alt 本地事务成功
|
||
Producer->>Broker: ③a Commit (消息对消费者可见)
|
||
else 本地事务失败
|
||
Producer->>Broker: ③b Rollback (消息丢弃)
|
||
end
|
||
|
||
Note over Producer,Broker: ④ 如果步骤 2 超时未回复
|
||
Broker->>Producer: ④ 回查请求 (CheckLocalTx)
|
||
Producer->>DB: 查询本地事务状态
|
||
DB-->>Producer: 返回 COMMIT / ROLLBACK
|
||
Producer->>Broker: 回查结果
|
||
```
|
||
|
||
> [!important] 回查机制的意义
|
||
> 当生产者执行本地事务过程中发生崩溃或网络中断,Broker 收不到 Commit/Rollback 指令,就会通过回查主动询问生产者。这确保了 **half 消息最终一定会收敛到可见或被丢弃**,不会出现悬空状态。
|
||
|
||
**可靠性要点清单:**
|
||
- ✅ 业务数据库和 Outbox 必须在**同一个本地事务**中写入
|
||
- ✅ 消费者必须有幂等保护(否则至少一次投递 = 无限次重复)
|
||
- ✅ 定时扫描任务需要设置最大重试次数,超过后进入人工处理
|
||
- ✅ 生产环境建议配置 **DLQ(死信队列)**,将连续消费失败的消息归档
|
||
|
||
## 方案二: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)。
|
||
|
||
### 编排式实现示例 (Go)
|
||
|
||
编排式的核心是一个 **Saga Coordinator**,它维护着当前步骤的执行状态和已完成的正向/反向操作列表:
|
||
|
||
```go
|
||
// Step 定义一个 Saga 步骤
|
||
type Step struct {
|
||
Name string
|
||
Execute func(ctx context.Context) error // 正向操作
|
||
Compensation func(ctx context.Context) error // 补偿操作
|
||
}
|
||
|
||
// Orchestrator 编排器
|
||
type Orchestrator struct {
|
||
steps []Step
|
||
executed []string // 已完成步骤名,用于补偿时逆向遍历
|
||
}
|
||
|
||
func (o *Orchestrator) AddStep(name string, exec, compens func(context.Context) error) {
|
||
o.steps = append(o.steps, Step{Name: name, Execute: exec, Compensation: compens})
|
||
}
|
||
|
||
// Execute 按顺序执行所有步骤,任一步骤失败则逆向补偿
|
||
func (o *Orchestrator) Execute(ctx context.Context) error {
|
||
for i, step := range o.steps {
|
||
if err := step.Execute(ctx); err != nil {
|
||
log.Error("step failed", z.String("step", step.Name), z.Err(err))
|
||
// 逆向补偿:从后往前执行已完步骤的补偿操作
|
||
o.rollback(ctx, i-1)
|
||
return fmt.Errorf("saga failed at %s: %w", step.Name, err)
|
||
}
|
||
o.executed = append(o.executed, step.Name)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (o *Orchestrator) rollback(ctx context.Context, upTo int) {
|
||
for i := len(o.executed) - 1; i >= 0 && i <= upTo; i-- {
|
||
// 找到对应步骤的补偿函数并执行
|
||
step := o.steps[i]
|
||
if err := step.Compensation(ctx); err != nil {
|
||
log.Warn("compensation also failed — need manual intervention", z.String("step", step.Name))
|
||
// 补偿失败必须告警!人工介入处理
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
使用方式——构建一个下单 Saga:
|
||
|
||
```go
|
||
saga := &Orchestrator{}
|
||
|
||
saga.AddStep("create_order",
|
||
func(ctx context.Context) error { return orderSvc.Create(ctx, req) }, // 正向
|
||
func(ctx context.Context) error { return orderSvc.Cancel(ctx, orderID) }, // 反向
|
||
)
|
||
saga.AddStep("reserve_stock",
|
||
func(ctx context.Context) error { return stockSvc.Reserve(ctx, productIDs) },
|
||
func(ctx context.Context) error { return stockSvc.Release(ctx, productIDs) },
|
||
)
|
||
saga.AddStep("charge_payment",
|
||
func(ctx context.Context) error { return paySvc.Charge(ctx, amount) },
|
||
func(ctx context.Context) error { return paySvc.Refund(ctx, orderID) },
|
||
)
|
||
|
||
err := saga.Execute(ctx)
|
||
```
|
||
|
||
> [!note] 关键理解:补偿的局限性
|
||
>
|
||
> - **补偿不是撤销**:取消订单不会把商品退回到原始状态,而是创建一条反向业务记录
|
||
> - **补偿可能失败**:如果退款接口也挂了,你需要重试 + 告警 + 人工介入机制
|
||
> - **时间窗口问题**:如果正向前向到了第 5 步、补偿到了第 2 步时第 2 步也失败了,第 3~4 步的正向操作已经发生且无法撤销
|
||
|
||
## 方案三: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) | 低(只需正向 + 反向操作) |
|
||
| 性能 | 中(需要两阶段提交) | 中高(单阶段本地事务) |
|
||
| 适用场景 | 资金、库存等高敏感业务 | 订单流程、审批流等业务链 |
|
||
|
||
## 方案四:AT 模式 (Seata)
|
||
|
||
> [!question] 什么时候该用 Seata?
|
||
>
|
||
> 如果你的团队**不愿或没有能力为每个业务方法编写 TCC 接口**,但又有跨库事务需求——Seata 的 AT 模式就是为此设计的。它是"零侵入"的最强候选方案。
|
||
|
||
Seata AT 模式的核心原理是 **全局锁 + 二阶段提交**,业务代码不需要任何改动:
|
||
|
||
```
|
||
第一阶段(Global Lock):
|
||
1. 拦截 SQL → 解析前后镜像
|
||
2. 申请全局锁(锁住被修改的行)
|
||
3. 本地事务执行(含 undo_log 写入)
|
||
4. 提交本地事务(全局锁暂不释放)
|
||
|
||
第二阶段(Global Commit/Rollback):
|
||
Commit: 异步删除 undo_log,释放全局锁
|
||
Rollback: 利用 undo_log 生成反向 SQL,回滚并释放全局锁
|
||
```
|
||
|
||
### AT 模式时序图
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant TC as Transaction Coordinator<br/>(Seata Server)
|
||
participant TM as Transaction Manager<br/>(应用 A)
|
||
participant RS1 as Resource 1<br/>(订单 DB)
|
||
participant RS2 as Resource 2<br/>(库存 DB)
|
||
|
||
TM->>TC: 开启全局事务 (xid)
|
||
TM->>RS1: BEGIN; UPDATE orders SET ...
|
||
Note over RS1: 自动捕获 BEFORE/AFTER 镜像<br/>写入 undo_log
|
||
|
||
TM->>RS2: BEGIN; UPDATE stock SET qty = qty - 1
|
||
Note over RS2: 同样写 undo_log
|
||
|
||
TM->>RS1: COMMIT (本地事务完成)
|
||
TM->>RS2: COMMIT (本地事务完成)
|
||
|
||
TM->>TC: 报告二阶段提交完成
|
||
TC-->>RS1: 异步清理 undo_log
|
||
TC-->>RS2: 异步清理 undo_log
|
||
```
|
||
|
||
### AT 与 XA 的区别
|
||
|
||
很多人混淆 AT 和 XA——它们都是两阶段提交,但实现方式完全不同:
|
||
|
||
| 维度 | XA 模式 | AT 模式 (Seata) |
|
||
|------|---------|----------------|
|
||
| **锁粒度** | 数据库级(长事务锁) | 行级(短期全局锁) |
|
||
| **隔离级别** | 需要 SERIALIZABLE | 基于脏读 + 全局锁 |
|
||
| **性能** | 较差(锁持有时间长) | 较好(本地事务快速提交后异步清理) |
|
||
| **侵入性** | 需配置 DataSource Proxy | 零侵入,自动代理 JDBC |
|
||
|
||
### 使用注意事项
|
||
|
||
```go
|
||
// Seata Go SDK — 几乎零侵入
|
||
import "github.com/seata/seata-sdk-go-client/tx"
|
||
|
||
func CreateOrder(ctx context.Context, req *Request) error {
|
||
// 只需添加这一个注解
|
||
tx.GlobalTransaction()
|
||
|
||
// 下面的代码完全不变——Seata 自动代理数据源
|
||
orderRepo.Create(ctx, &order)
|
||
stockRepo.Decrease(ctx, productID, qty)
|
||
|
||
return nil
|
||
}
|
||
```
|
||
|
||
> [!warning] 生产环境避坑清单
|
||
>
|
||
> 1. **undo_log 表必须建**:`seata_undo_log` 是 Seata 自动创建的,用于存储前后镜像。务必保证这个表可用(通常和业务库在同一实例)
|
||
> 2. **不要混用本地事务和全局事务**:同一个方法里同时出现 `@Transactional` 和 `@GlobalTransactional` 会导致行为不可预测
|
||
> 3. **异常处理**:全局事务中抛出的异常会被 Seata 捕获并触发回滚,确保异常能正确向上传递
|
||
> 4. **性能考量**:全局锁虽然比 XA 短,但在高并发场景下仍然是瓶颈。**优先用 MQ 最终一致,实在做不到再上 AT**
|
||
> 5. **只支持部分数据库**:MySQL、PostgreSQL、Oracle 支持良好;TiDB 等 NewSQL 数据库兼容性有限
|
||
|
||
> [!tip] 选型建议
|
||
>
|
||
> AT 模式最适用的场景:**遗留系统微服务化改造**。当原有单体拆分成多个服务时,原有的 `@Transactional` 跨库调用突然失效,引入 AT 模式可以快速过渡——等业务稳定后再逐步迁移到 MQ 事件驱动架构。
|
||
|
||
## 补充保障:重试与死信队列
|
||
|
||
分布式系统中,**没有哪个组件永远可靠**。消息消费失败、网络闪断、下游超时——这些都不可避免。你需要一套完整的错误处理链路:
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
MQ["消息队列"] --> C["消费者"]
|
||
C --> FAIL{消费成功}
|
||
FAIL -- 是 --> OK["正常结束"]
|
||
FAIL -- 否 --> RETRY{重试次数未满}
|
||
RETRY -- 是 --> DELAY["延迟重试<br/>指数退避"]
|
||
DELAY --> C
|
||
RETRY -- 否 --> DLQ["进入死信队列<br/>人工介入"]
|
||
|
||
DLQ --> ALERT["告警通知"]
|
||
DLQ --> REPLAY["手动重放或修复后补发"]
|
||
```
|
||
|
||
### 三种重试策略对比
|
||
|
||
| 策略 | 适用场景 | 示例 |
|
||
|------|---------|------|
|
||
| **立即重试** | 瞬时故障(连接池未就绪) | 等 100ms 再试 3 次 |
|
||
| **延迟重试** | 需要时间窗口等待恢复 | 1s → 2s → 4s → 8s |
|
||
| **定时任务补偿** | 批量漏掉的消息 | 每 5 分钟扫描 pending 表 |
|
||
|
||
### 死信队列 (DLQ) 设计
|
||
|
||
```go
|
||
// 消费者主循环:带重试和 DLQ 保护
|
||
func (c *consumer) Process(ctx context.Context, msg Message) error {
|
||
for attempt := 0; attempt < c.maxRetries; attempt++ {
|
||
err := c.handle(ctx, msg)
|
||
if err == nil {
|
||
return nil // 成功
|
||
}
|
||
|
||
if isRetryable(err) {
|
||
time.Sleep(time.Duration(attempt+1) * time.Second)
|
||
continue // 重试
|
||
}
|
||
|
||
// 非可重试错误,直接进 DLQ
|
||
return c.sendToDLQ(msg, err)
|
||
}
|
||
|
||
// 超过最大重试次数,进 DLQ
|
||
return c.sendToDLQ(msg, fmt.Errorf("exhausted %d retries", c.maxRetries))
|
||
}
|
||
```
|
||
|
||
> [!quote] 运维心态
|
||
>
|
||
> "一个没有死信队列的消息消费者,就像一辆没有备胎的车——任何一次不可预期的故障都会让你抛锚在路上。"
|
||
|
||
## 如何选择分布式事务方案?
|
||
|
||
> [!question]- 决策树
|
||
>
|
||
> 面对跨服务的数据一致性问题,你不需要每次都重新选型。按照下面的思路逐层判断即可:
|
||
|
||
```mermaid
|
||
flowchart TD
|
||
START["跨服务写操作"] --> Q1{核心资金或资产}
|
||
|
||
Q1 -- 是且要求强一致 --> TCC["TCC"]
|
||
Q1 -- 否 --> Q2{流程步数超过5}
|
||
|
||
Q2 -- 是长流程 --> SAGA["Saga"]
|
||
Q2 -- 否简单链路 --> Q3{容忍秒级延迟}
|
||
|
||
Q3 -- 可以 --> MQ["本地事务+MQ(Outbox)"]
|
||
Q3 -- 不可以毫秒级 --> TXMSG["RocketMQ事务消息"]
|
||
|
||
Q3 -- 无法接受最终一致 --> SEATA["Seata AT模式"]
|
||
|
||
TCC --> END["完成选择"]
|
||
SAGA --> END
|
||
MQ --> END
|
||
TXMSG --> END
|
||
SEATA --> WARNING["过渡方案,后续应迁移至事件驱动架构"]
|
||
WARNING --> END
|
||
|
||
style TCC fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
|
||
style SAGA fill:#fff3e0,stroke:#ff9800,stroke-width:2px
|
||
style MQ fill:#e3f2fd,stroke:#1976d2,stroke-width:2px
|
||
style TXMSG fill:#e3f2fd,stroke:#1976d2,stroke-width:2px
|
||
style SEATA fill:#fce4ec,stroke:#e91e63,stroke-width:2px
|
||
```
|
||
|
||
### 决策速查矩阵
|
||
|
||
| 你的业务特征 | 推荐方案 | 理由 |
|
||
|-------------|---------|------|
|
||
| 日订单量百万级,通知类场景 | Outbox + MQ | 性能最好,开发成本最低 |
|
||
| 支付/清算/账务核心链路 | TCC | 资源占用期间不允许其他事务修改 |
|
||
| 审批流、物流状态流转(多步) | Saga 编排式 | 流程清晰,补偿机制完备 |
|
||
| 已有 RocketMQ,对延迟敏感 | 事务消息 | 绕过 Outbox 轮询延迟 |
|
||
| 遗留系统快速拆分 | Seata AT | 零侵入,快速过渡 |
|
||
| 只需要"最终一致" | 本地事务 + MQ | 80% 的场景这就是答案 |
|
||
|
||
> [!tip] 黄金法则
|
||
>
|
||
> **"能用异步事件解决的,不要用两阶段提交;能本地事务 + MQ 解决的,不要上框架。"**
|
||
>
|
||
> 复杂度越高,出问题的概率越大。每一层抽象都引入了新的故障点——协调器挂了怎么办?补偿逻辑写错了怎么发现?回滚又失败了谁来做?保持简单是最好的工程纪律。
|
||
|
||
## 关联笔记
|
||
|
||
- [[03-数据一致性/01-数据库拆分]] — 数据库拆分是分布式事务的前提
|
||
- [[02-服务治理/06-容错模式]] — 熔断器和重试在分布式事务中的作用
|
||
- [[02-服务治理/05-服务间通信]] — 消息投递的一致性保障
|