Skip to content

分布式事务 ​

#分布式事务 · #2PC · #TCC · #Saga · #最终一致性 · #Seata

微服务架构下,一个业务操作往往跨越多个服务和数据库,如何保证数据一致性是核心挑战。本专题系统梳理分布式事务的理论基础与工程方案。


理论基础 ​

CAP 定理 ​

分布式系统不可能同时满足:

  • Consistency(一致性):所有节点看到相同数据
  • Availability(可用性):每个请求都能得到响应
  • Partition tolerance(分区容忍):网络分区时系统仍能运行

网络分区不可避免,实际系统在 CP 和 AP 之间选择。

BASE 理论 ​

对 ACID 的妥协:

  • Basically Available:基本可用
  • Soft state:软状态,允许中间状态
  • Eventually consistent:最终一致性

方案对比 ​

方案一致性性能侵入性适用场景
2PC/XA强一致低低传统数据库跨库
3PC强一致低低理论模型,工程少用
TCC强一致中高资金、库存等核心业务
Saga最终一致高中长事务、跨服务编排
本地消息表最终一致高中异步通知场景
事务消息最终一致高低有 MQ 基础设施
最大努力通知最终一致高低对一致性要求不高

2PC(两阶段提交) ​

为什么要两阶段? ​

假设你有一个转账操作:从 MySQL-A 扣 100 元,往 MySQL-B 加 100 元。两个操作分别在独立的数据库事务中提交,没有"跨数据库的事务"。如果 A 提交成功但 B 失败——钱凭空消失了。2PC 就是为了解决"多个参与者要么全成功、要么全失败"的问题。

mermaid
sequenceDiagram
    participant TM as "事务协调者 (TM)"
    participant RM1 as "参与者1 (MySQL-A)"
    participant RM2 as "参与者2 (MySQL-B)"

    Note over TM,RM2: Phase 1: Prepare (准备阶段)
    TM->>RM1: Prepare: 扣 100 元,准备好提交?
    RM1->>RM1: 执行 SQL,写入 undo log<br/>但事务未提交,资源锁定
    RM1-->>TM: Vote: YES (我准备好了)
    TM->>RM2: Prepare: 加 100 元,准备好提交?
    RM2->>RM2: 执行 SQL,写入 undo log<br/>事务未提交,资源锁定
    RM2-->>TM: Vote: YES

    Note over TM,RM2: 所有参与者都 YES → Phase 2: Commit
    TM->>RM1: Commit
    RM1->>RM1: COMMIT(释放锁)
    RM1-->>TM: ACK
    TM->>RM2: Commit
    RM2->>RM2: COMMIT
    RM2-->>TM: ACK

    Note over TM: 事务完成 ✅ 钱从 A→B

异常场景 ​

mermaid
sequenceDiagram
    participant TM as "TM"
    participant RM1 as "RM1"
    participant RM2 as "RM2"

    Note over TM,RM2: 场景1: Prepare 阶段有参与者 NO
    TM->>RM1: Prepare
    RM1-->>TM: YES
    TM->>RM2: Prepare
    RM2-->>TM: ❌ NO (余额不足/锁冲突)
    TM->>RM1: Abort (回滚)
    TM->>RM2: Abort
    Note over TM,RM2: 所有参与者回滚 ✅ 原子性保住

    Note over TM,RM2: 场景2: Commit 阶段部分失败 (最危险!)
    TM->>RM1: Prepare → YES
    TM->>RM2: Prepare → YES
    TM->>RM1: Commit → ACK ✅
    TM-->>TM: 💥 TM 宕机!RM2 没收到 Commit
    RM2->>RM2: 一直等待...资源锁未释放
    Note over RM2: 🔴 RM2 处于"不确定状态"<br/>不知道该 Commit 还是 Abort

2PC 的致命问题:单点故障(TM 宕机)+ 同步阻塞(参与者持有锁等待 TM 决策)。这在实际生产系统中是不可接受的——所以才有 TCC、Saga 等替代方案。


TCC(Try-Confirm-Cancel) ​

TCC 是"事务补偿"思想的代表作——它不是在数据库层面做 2PC,而是让业务代码自己定义"怎么做、怎么确认、怎么撤销"。核心是把一次业务操作拆成三个步骤:

mermaid
flowchart TB
    subgraph TCC["TCC 三阶段"]
        Try["🔵 Try 阶段<br/>预留资源 & 检查<br/>例如:冻结 100 元<br/>(金额还在账户,但不能用)"]
        Try -->|"所有参与者 Try 成功"| Confirm
        Try -->|"任一 Try 失败"| Cancel

        Confirm["🟢 Confirm 阶段<br/>确认执行<br/>例如:扣减冻结金额,转账到对方<br/>(幂等操作)"]
        Cancel["🔴 Cancel 阶段<br/>取消释放<br/>例如:解冻金额<br/>(幂等操作)"]
    end

相比于 2PC 的"数据库帮你锁定",TCC 的"业务自己控制锁"有两大优势:(1) 锁的粒度由你控制(可以只锁余额不锁整个账户行);(2) Try 阶段的锁持有时间可控(超时可以自动触发 Cancel),不会像 2PC 那样无限等待。

在 2PC 基础上增加 CanCommit 阶段和超时机制:

Phase 1: CanCommit  → 询问是否可以提交(不锁资源)
Phase 2: PreCommit  → 预提交(锁定资源)
Phase 3: DoCommit   → 正式提交

改进:参与者超时后默认提交,减少阻塞。 局限:仍无法完全解决网络分区下的不一致问题,工程中较少使用。


TCC(Try-Confirm-Cancel) ​

三个阶段 ​

阶段作用示例(转账)
Try预留资源冻结转出账户 100 元
Confirm确认提交扣减冻结金额,增加收款方余额
Cancel取消释放解冻转出账户 100 元

代码示例 ​

java
// TCC 接口定义
public interface AccountTccService {

    @TwoPhaseBusinessAction(name = "transfer",
        commitMethod = "confirm", rollbackMethod = "cancel")
    boolean tryTransfer(BusinessActionContext ctx,
                        @BusinessActionContextParameter(paramName = "accountId") String accountId,
                        @BusinessActionContextParameter(paramName = "amount") BigDecimal amount);

    boolean confirm(BusinessActionContext ctx);

    boolean cancel(BusinessActionContext ctx);
}

// Try 阶段实现
public boolean tryTransfer(BusinessActionContext ctx, String accountId, BigDecimal amount) {
    // 冻结金额:available -= amount, frozen += amount
    return accountDao.freeze(accountId, amount);
}

// Confirm 阶段实现
public boolean confirm(BusinessActionContext ctx) {
    String accountId = ctx.getActionContext("accountId");
    BigDecimal amount = ctx.getActionContext("amount");
    // 扣减冻结金额
    return accountDao.deductFrozen(accountId, amount);
}

// Cancel 阶段实现
public boolean cancel(BusinessActionContext ctx) {
    String accountId = ctx.getActionContext("accountId");
    BigDecimal amount = ctx.getActionContext("amount");
    // 解冻金额:available += amount, frozen -= amount
    return accountDao.unfreeze(accountId, amount);
}

TCC 异常场景深度解析 ​

空回滚(Empty Rollback) ​

定义:Try 阶段根本没执行(可能因为网络超时、进程崩溃),但 Cancel 被调用了。

text
时间线:
  t1: TM 发起 Try → 网络超时(RM 没收到)
  t2: TM 判定 Try 失败 → 触发全局回滚 → 发起 Cancel
  t3: RM 收到 Cancel → 但 Try 从未执行过 → 这是"空回滚"
java
// 空回滚防御:Cancel 必须判断 Try 是否执行过
public boolean cancel(BusinessActionContext ctx) {
    String xid = ctx.getXid();

    // 1. 查询事务记录表
    TccTransaction tcc = tccMapper.selectByXid(xid);
    if (tcc == null) {
        // Try 从未执行过 → 空回滚
        // 插入一条 status=CANCEL 的记录防止后续 Try 执行(防悬挂)
        tccMapper.insert(new TccTransaction(xid, "CANCEL"));
        return true; // 空回滚成功,无需实际操作
    }

    // 2. Try 执行过 → 正常回滚
    if (tcc.getStatus() == TccStatus.TRY) {
        // 执行回滚逻辑:解冻金额
        accountDao.unfreeze(accountId, amount);
        tccMapper.updateStatus(xid, TccStatus.CANCEL);
        return true;
    }

    // 3. 已 Cancel / 已 Confirm → 幂等返回成功
    return true;
}

悬挂(Hanging / Suspension) ​

定义:Cancel 比 Try 先到达 RM(网络乱序导致),Try 后到达时如果直接执行就会造成"已回滚的事务又被执行了"。

text
时间线:
  t1: TM 发起 Try → 网络延迟
  t2: TM 等不及 → 超时判定失败 → 发起 Cancel
  t3: Cancel 到达 RM → RM 执行 Cancel → 标记事务已取消
  t4: Try 终于到达 RM → ⚠️ 如果 RM 不判断 → 执行了 Try!
       → 但事务已经被 Cancel 了 → 永远不会有 Confirm 或 Cancel 来清理
       → 资源永远锁定(悬挂)
java
// 悬挂防御:Try 必须先检查事务是否已被 Cancel
public boolean tryTransfer(BusinessActionContext ctx, String accountId, BigDecimal amount) {
    String xid = ctx.getXid();

    // 1. 防悬挂检查:先查事务记录
    TccTransaction tcc = tccMapper.selectByXid(xid);
    if (tcc != null && tcc.getStatus() == TccStatus.CANCEL) {
        // Cancel 已经执行过 → 拒绝 Try
        log.warn("try rejected: transaction already cancelled", xid);
        return false;
    }

    // 2. 插入事务记录(唯一键约束防并发)
    try {
        tccMapper.insert(new TccTransaction(xid, TccStatus.TRY));
    } catch (DuplicateKeyException e) {
        // 并发 Try → 幂等检查
        tcc = tccMapper.selectByXid(xid);
        if (tcc.getStatus() != TccStatus.TRY) {
            return false; // 已被 Cancel
        }
        return true; // 已 Try,幂等
    }

    // 3. 执行业务:冻结金额
    accountDao.freeze(accountId, amount);
    return true;
}

TCC 状态机 ​

mermaid
stateDiagram-v2
    [*] --> TRY : Try 请求到达
    TRY --> CONFIRM : 全局事务确认
    TRY --> CANCEL : 全局事务回滚
    CANCEL --> [*]

    note right of TRY
        空回滚: Cancel 到达但未曾 TRY
        → 检查无记录 → 记录 CANCEL
    end note

    note right of CANCEL
        防悬挂: Try 到达时发现已 CANCEL
        → 拒绝 Try 执行
    end note

    note left of CONFIRM
        幂等: Confirm 可能重复到达
        → 检查状态 → 已 Confirm 则直接返回成功
    end note

事务控制表设计 ​

sql
CREATE TABLE tcc_transaction (
    id          BIGINT AUTO_INCREMENT PRIMARY KEY,
    xid         VARCHAR(128) NOT NULL UNIQUE,  -- 全局事务 ID(唯一键)
    status      TINYINT NOT NULL DEFAULT 0,    -- 0=TRY, 1=CONFIRM, 2=CANCEL
    created_at  DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at  DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    INDEX idx_status_created (status, created_at)  -- 用于定时清理超时事务
);

-- Try 时:
INSERT INTO tcc_transaction (xid, status) VALUES (?, 0);
-- ↑ 利用 UNIQUE(xid) 实现幂等 + 防悬挂

-- Cancel 先到时(空回滚):
INSERT INTO tcc_transaction (xid, status) VALUES (?, 2);
-- ↑ Try 后到时发现 status=2 → 拒绝

-- Confirm 时:
UPDATE tcc_transaction SET status = 1 WHERE xid = ? AND status = 0;
-- ↑ 幂等: 第二次 Confirm 时 status 已是 1 → 不影响任何行 → 直接返回成功

-- 定时清理超时 TRY(如 30 分钟未 Confirm/Cancel → 自动 Cancel):
UPDATE tcc_transaction SET status = 2
WHERE status = 0 AND created_at < NOW() - INTERVAL 30 MINUTE;

TCC vs 2PC:锁持有时间的对比 ​

text
2PC (数据库锁):
  Prepare → Commit 之间,数据库行锁一直持有
  如果 TM 宕机 → 锁永远不释放 → 需要 DBA 手动 KILL 事务

TCC (业务锁):
  Try → Confirm/Cancel 之间,只有"业务层面的软锁"
  超时可以自动 Cancel → 不需要 DBA 介入
  锁的范围由业务代码控制(可以只锁余额,不锁整行)

XA 2PC 协调者故障的完整分析 ​

2PC 的致命盲区:协调者在 Commit 阶段宕机 ​

mermaid
sequenceDiagram
    participant TM as "协调者 (TM)"
    participant RM1 as "MySQL-A"
    participant RM2 as "MySQL-B"

    Note over TM,RM2: Phase 1: Prepare
    TM->>RM1: Prepare → YES
    TM->>RM2: Prepare → YES

    Note over TM,RM2: Phase 2: Commit — 协调者部分发送后宕机
    TM->>RM1: Commit → RM1 COMMIT 成功
    TM-->>TM: 💥 协调者宕机!

    Note over RM2: ⚠️ RM2 处于"不确定状态"<br/>Prepare 了但不知道要 Commit 还是 Abort<br/>行锁一直持有 → 阻塞所有其他事务!

    Note over TM,RM2: 恢复后(如果 TM 有 WAL 日志):
    TM->>TM: 从 WAL 恢复 → 发现事务 X 的状态是 COMMITTING
    TM->>RM2: 重发 Commit → RM2 COMMIT ✅

三种恢复策略 ​

策略原理优点缺点
WAL 恢复TM 写 WAL 日志记录事务状态,宕机恢复后重放可靠TM 需要持久化存储
参与者互询RM2 不确定时,询问其他 RM:"你们 commit 了吗?"无需 TM 恢复需 RM 间网络互通
超时 + 补偿RM2 超时后自动 Abort,事后通过补偿机制修复不阻塞可能违反原子性

Seata 如何避免 2PC 的单点故障 ​

text
Seata AT 模式的改进:

1. TM (Transaction Manager) → 无状态,任意实例可接管
   - 全局事务状态存储在 Seata Server (TC) 中
   - TC 使用数据库/Redis 持久化事务日志

2. TC (Transaction Coordinator) → 高可用部署
   - TC 集群(多实例 + DB 共享存储)
   - 宕机后其他 TC 实例从 DB 读取事务日志继续处理

3. RM (Resource Manager) → 无阻塞
   - 第一阶段已提交本地事务(与 2PC 不同!)
   - 行锁在第一阶段就已释放
   - 回滚时用 undo_log 生成反向 SQL 补偿
   - → 即使 TC 宕机 10 分钟,RM 也不会锁表 10 分钟

关键区别:
  2PC: Prepare 锁定 → Commit 释放 → TM 宕机 = 锁永远不释放
  Seata AT: 第一阶段提交 → 立即释放锁 → TC 宕机只延迟回滚,不阻塞业务
mermaid
flowchart TB
    subgraph TwoPC["2PC 问题"]
        P["Prepare<br/>锁住资源"] --> Crash["TM 宕机"]
        Crash --> Block["资源永久锁定<br/>需要 DBA 手工介入"]
    end

    subgraph Seata["Seata AT 改进"]
        Commit1["第一阶段: 本地提交<br/>+ 写 undo_log"] --> Crash2["TC 宕机"]
        Crash2 --> Recovery["TC 恢复后<br/>根据 undo_log 补偿"]
        Recovery --> OK["资源从不阻塞 ✅"]
    end

    style Block fill:#f44336,color:#fff
    style OK fill:#4CAF50,color:#fff

Seata TC 高可用架构 ​

┌─────────────────────────────────────────────────┐
│                  Seata TC 集群                    │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐      │
│  │ TC-1     │  │ TC-2     │  │ TC-3     │      │
│  └────┬─────┘  └────┬─────┘  └────┬─────┘      │
│       └──────────────┼──────────────┘            │
│                      │ 共享 DB 存储事务日志       │
│              ┌───────┴───────┐                   │
│              │ global_table   │                  │
│              │ branch_table   │                  │
│              │ lock_table     │                  │
│              └───────────────┘                   │
└─────────────────────────────────────────────────┘ |

配置:
  registry.type = "nacos"  # 服务发现
  config.type   = "nacos"  # 配置中心
  store.mode    = "db"     # 事务日志持久化到 DB

最佳实践 ​

  1. Seata AT 优先:不需要手写 TCC 三接口,零侵入
  2. TCC 用于特定场景:资金、库存等需要 Try 阶段显式锁定资源的业务
  3. Saga 用于长流程:跨多个服务的长时间事务
  4. 监控 TC 健康状态:TC 宕机会延迟回滚但不阻塞业务,仍需告警
  5. 定时扫描 undo_log:清理过期数据,防止表膨胀

Saga 模式 ​

核心思想 ​

将长事务拆分为多个本地事务,每个本地事务有对应的补偿操作。正向执行失败时,逆序执行补偿。

T1 → T2 → T3 → T4(成功)
T1 → T2 → T3(失败)→ C3 → C2 → C1(补偿回滚)

编排模式 vs 事件驱动 ​

mermaid
flowchart LR
    subgraph Orchestration["协调器编排 (Orchestration)"]
        direction TB
        O_Saga["Saga 协调器"]
        O_S1["创建订单"]
        O_S2["冻结库存"]
        O_S3["扣减余额"]

        O_Saga -->|"1. 执行"| O_S1
        O_S1 -->|"成功"| O_Saga
        O_Saga -->|"2. 执行"| O_S2
        O_S2 -->|"成功"| O_Saga
        O_Saga -->|"3. 执行"| O_S3

        O_S3 -->|"失败!"| O_Saga
        O_Saga -->|"逆序补偿"| O_S2
        O_Saga -->|"逆序补偿"| O_S1
    end

    subgraph Choreography["事件编排 (Choreography)"]
        direction TB
        C_Order["订单服务"]
        C_Stock["库存服务"]
        C_Pay["支付服务"]

        C_Order -->|"事件: OrderCreated"| C_Stock
        C_Stock -->|"事件: StockReserved"| C_Pay
        C_Pay -->|"事件: PaymentFailed"| C_Order
        C_Stock -->|"事件: StockReleased"| C_Order
    end
维度协调器编排事件编排
流程可见性✅ 中心协调器,流程一目了然❌ 分散在各服务中,全局难追踪
耦合度🟡 与 Saga 协调器耦合✅ 各服务仅通过事件解耦
调试难度✅ 低(单点追踪)❌ 高(需分布式追踪)
扩展性🟡 新增步骤需改协调器✅ 新增消费者即可
适合订单类流程(步骤固定)异步通知(步骤易扩展)

Seata 框架 — AT 模式深度 ​

AT 模式原理 ​

Seata AT(Automatic Transaction)是使用最广泛的模式——它不需要你写 TCC 的三接口,而是自动解析 SQL 生成回滚日志:

mermaid
sequenceDiagram
    participant TM as "Seata TM"
    participant RM1 as "RM1 (DB-A)"
    participant RM2 as "RM2 (DB-B)"

    Note over TM,RM2: 全局事务开始
    TM->>TM: 生成全局事务 XID

    Note over TM,RM2: 分支事务1: 扣库存
    RM1->>RM1: 1. 执行前:SELECT stock WHERE id=1 → beforeImage: stock=10
    RM1->>RM1: 2. 执行业务SQL:UPDATE stock SET stock=9
    RM1->>RM1: 3. 执行后:SELECT stock WHERE id=1 → afterImage: stock=9
    RM1->>RM1: 4. 本地事务提交:UPDATE + INSERT undo_log(含beforeImage/afterImage)
    RM1->>TM: 分支1提交成功

    Note over TM,RM2: 分支事务2: 扣余额 → 失败!
    RM2->>RM2: UPDATE account → 数据库异常
    RM2-->>TM: 分支2失败

    Note over TM,RM2: 全局回滚
    TM->>RM1: 回滚分支1
    RM1->>RM1: 查询 undo_log: beforeImage stock=10
    RM1->>RM1: 执行反向SQL:UPDATE stock SET stock=10 WHERE stock=9
    RM1->>RM1: 删除 undo_log 记录
    RM1-->>TM: 回滚完成

undo_log 表结构:

sql
CREATE TABLE undo_log (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    branch_id BIGINT NOT NULL,
    xid VARCHAR(128) NOT NULL,
    rollback_info LONGBLOB NOT NULL,  -- JSON: {beforeImage:..., afterImage:...}
    log_status INT NOT NULL,           -- 0=正常, 1=已全局完成
    INDEX idx_xid_branch(xid, branch_id)
);

AT 模式的边界:只支持关系型数据库(MySQL/Oracle/PostgreSQL),不支持 NoSQL(MongoDB/Redis等)。因为它依赖 JDBC 拦截 SQL 并解析"修改前/修改后"的数据快照,这要求 SQL 的语义是确定的标准 DML。

协调器模式(Orchestration):

python
# Saga 协调器
class OrderSaga:
    steps = [
        SagaStep(action=create_order, compensate=cancel_order),
        SagaStep(action=reserve_stock, compensate=release_stock),
        SagaStep(action=deduct_balance, compensate=refund_balance),
        SagaStep(action=send_notification, compensate=None),  # 无需补偿
    ]

    def execute(self):
        completed = []
        for step in self.steps:
            try:
                step.action()
                completed.append(step)
            except Exception:
                # 逆序补偿
                for s in reversed(completed):
                    if s.compensate:
                        s.compensate()
                raise SagaRollbackException()

事件编排模式(Choreography):

OrderService → [OrderCreated] → StockService → [StockReserved] → PaymentService → [PaymentCompleted]

各服务监听事件自行决策,无中心协调器,但流程追踪困难。

Saga vs TCC ​

维度SagaTCC
隔离性无,可能读到中间状态有,Try 阶段锁定资源
性能高,无资源锁定中,Try 阶段锁资源
实现复杂度中高(需实现三个接口)
适用场景长流程、跨多服务短事务、强一致性要求

本地消息表 ​

流程 ​

sql
-- Step 1: 业务操作 + 写消息在同一个本地事务
BEGIN;
UPDATE orders SET status = 'paid' WHERE id = 1;
INSERT INTO outbox (id, event_type, payload, status, created_at)
    VALUES ('msg_001', 'ORDER_PAID', '{"orderId":1}', 'NEW', NOW());
COMMIT;
python
# Step 2: 定时任务扫描并发送
def send_outbox_messages():
    messages = db.query("SELECT * FROM outbox WHERE status = 'NEW' AND created_at < NOW() - INTERVAL 5 SECOND")
    for msg in messages:
        try:
            mq.send(msg.event_type, msg.payload)
            db.execute("UPDATE outbox SET status = 'SENT' WHERE id = ?", msg.id)
        except Exception:
            pass  # 下次重试

# Step 3: 定期清理已发送消息
def cleanup_outbox():
    db.execute("DELETE FROM outbox WHERE status = 'SENT' AND created_at < NOW() - INTERVAL 7 DAY")

Seata 框架 ​

模式支持 ​

模式说明
AT自动补偿,基于 SQL 解析生成回滚日志(最常用)
TCC手动实现 Try/Confirm/Cancel
Saga长事务编排
XA标准 XA 协议

AT 模式原理 ​

1. 解析 SQL,记录修改前的数据快照(beforeImage)
2. 执行 SQL
3. 记录修改后的数据快照(afterImage)
4. 生成 undo_log 记录
5. 提交本地事务 + undo_log(同一事务)

回滚时:根据 undo_log 中的 beforeImage 恢复数据

使用示例 ​

java
@GlobalTransactional(timeoutMills = 30000, name = "create-order")
public void createOrder(OrderDTO order) {
    // 扣减库存(远程调用)
    stockService.deduct(order.getProductId(), order.getCount());

    // 扣减余额(远程调用)
    accountService.debit(order.getUserId(), order.getAmount());

    // 创建订单(本地)
    orderDao.create(order);
}
// Seata 自动管理全局事务,任一步骤失败自动回滚所有参与者

最佳实践 ​

  1. 优先选择最终一致性:强一致性代价高,大多数业务可接受短暂不一致
  2. 幂等是基础:无论哪种方案,参与者都必须支持幂等
  3. 超时处理:设置合理超时,避免事务长时间悬挂
  4. 监控告警:对悬挂事务、补偿失败建立告警机制
  5. 人工兜底:极端情况下提供人工介入入口

相关专题 ​

批注模式

💬 文章评论

暂无评论,来说点什么吧 👇

编程学习笔记