分布式事务(2PC/3PC/TCC/Saga)

全面解析分布式事务核心协议:2PC 两阶段提交、3PC 三阶段提交、TCC _try-confirm-cancel 补偿模式、Saga 长事务编排,以及 Seata 框架实战与消息最终一致性方案。

单体数据库的事务通过 ACID 保证,但当数据分布在多个数据库或服务中时,如何保证跨节点的事务一致性?分布式事务是微服务架构和分库分表场景下的核心挑战。本文全面解析主流分布式事务协议和工程实践。


1. 分布式事务的核心问题

1.1 CAP 定理的约束

CAP 定理:在分布式系统中,Consistency(一致性)、Availability(可用性)、Partition Tolerance(分区容错性)三者不可兼得。

分布式事务的设计本质是在 C 和 A 之间做权衡。

1.2 典型场景

-- 下单场景(跨服务)
┌────────────┐    ┌────────────┐    ┌────────────┐
  Order           Inventory       Account   
  Service         Service         Service   
  DB: order       DB: stock       DB: acct  
└─────┬──────┘    └─────┬──────┘    └─────┬──────┘
                                        
      └─────────────────┴─────────────────┘
            需要原子性:全部成功  全部失败

2. 2PC 两阶段提交

2.1 协议流程

Phase 1 (Prepare):

Coordinator                    Participants (A/B/C)
      │                              │
      │─── 1a. Prepare Request ──►││
      │                              │
      │◄── 1b. Yes/No Response ────││
      │                              │
      │  所有回答 Yes → Phase 2 Commit
      │  任一回答 No  → Phase 2 Rollback

Phase 2 (Commit/Rollback):

      │─── 2a. Commit/Rollback ──►││
      │                              │
      │◄── 2b. ACK ────────────────││

2.2 XA 规范

XA 是 X/Open 组织提出的分布式事务标准接口:

// XA 接口
public interface XAResource {
    void start(Xid xid, int flags);      // 开始事务分支
    void end(Xid xid, int flags);        // 结束事务分支
    int prepare(Xid xid);                 // 第一阶段:准备
    void commit(Xid xid, boolean onePhase); // 第二阶段:提交
    void rollback(Xid xid);              // 第二阶段:回滚
}

MySQL XA 使用:

-- 会话 1(参与者A)
XA START 'order-xid';
INSERT INTO orders VALUES (...);
XA END 'order-xid';
XA PREPARE 'order-xid';  -- 第一阶段:准备

-- 协调器收到所有 PREPARE OK
XA COMMIT 'order-xid';    -- 第二阶段:提交
-- 或 XA ROLLBACK 'order-xid'

2.3 2PC 的问题

问题说明影响
同步阻塞参与者需锁定资源等待协调器指令性能差
单点故障协调器宕机,参与者一直悬停数据不一致风险
数据不一致协调器发出 Commit 后宕机,部分参与者没收到部分提交
脑裂网络分区导致部分提交、部分回滚严重数据错误

3. 3PC 三阶段提交

3.1 改进点:引入超时与 CanCommit

Phase 1 (CanCommit):
      │──► CanCommit? ──► 检查资源是否可用,不锁定
      │◄── Yes/No ◄────── 参与者预检查

Phase 2 (PreCommit):
      │──► PreCommit ──► 预锁定资源
      │◄── ACK ◄──────── 参与者确认

Phase 3 (DoCommit):
      │──► DoCommit ──► 真正提交
      │◄── ACK ◄─────── 参与者确认

3.2 3PC vs 2PC

维度2PC3PC
阶段数23
阻塞完全阻塞引入超时,减少阻塞时间
单点故障严重协调器宕机可通过超时继续
复杂度简单复杂
使用广泛度广泛应用(XA)较少使用

3PC 实际工程中很少使用,超时机制在复杂网络下仍然不可靠。


4. TCC(Try-Confirm-Cancel)

4.1 核心思想

TCC 将每个操作拆分为三个阶段:

阶段操作语义
Try预留资源执行业务检查,锁定/冻结资源
Confirm确认执行真正执行业务(幂等)
Cancel取消回滚释放预留资源(幂等)

4.2 电商下单示例

// 库存服务
public interface InventoryService {
    @TccMethod
    boolean tryDeduct(String productId, int count);  // 冻结库存
    
    @TccMethod
    boolean confirmDeduct(String productId, int count);  // 真正扣减
    
    @TccMethod  
    boolean cancelDeduct(String productId, int count);   // 释放冻结
}

// 订单服务编排
@Service
public class OrderTccService {
    @Autowired InventoryService inventory;
    @Autowired AccountService account;
    
    public void placeOrder(Order order) {
        // Phase 1: Try - 全部预留资源
        boolean stockOk = inventory.tryDeduct(order.getProductId(), order.getCount());
        boolean payOk = account.tryFreeze(order.getUserId(), order.getAmount());
        
        if (stockOk && payOk) {
            // Phase 2: Confirm - 全部确认
            inventory.confirmDeduct(order.getProductId(), order.getCount());
            account.confirmPay(order.getUserId(), order.getAmount());
        } else {
            // Phase 2: Cancel - 全部回滚
            if (stockOk) inventory.cancelDeduct(order.getProductId(), order.getCount());
            if (payOk) account.cancelPay(order.getUserId(), order.getAmount());
        }
    }
}

4.3 TCC 的优缺点

优点缺点
无全局锁,并发性能好业务侵入性强,需实现三阶段的业务逻辑
Try 阶段可回滚,灵活性高幂等性需业务保证
适合短事务Confirm/Cancel 需保证最终执行
相比 2PC,资源锁定时间短开发成本高

4.4 空回滚与幂等

// 空回滚:Try 超时未执行,Cancel 先到达
public boolean cancelDeduct(String productId, int count) {
    String record = getTransactionRecord();
    if (record == null) {
        // Try 未执行或已回滚 → 空回滚,直接返回成功
        return true;
    }
    // 正常回滚逻辑
}

// 幂等:Confirm/Cancel 可能被重试多次
public boolean confirmDeduct(String productId, int count) {
    if (alreadyConfirmed()) return true;  // 幂等检查
    // 确认逻辑
}

5. Saga 长事务

5.1 核心思想

Saga 将长事务拆分为本地事务序列,每个本地事务提交后立即释放资源。如果任一失败,执行补偿事务回滚已完成的步骤。

正向流程:  T1 → T2 → T3 → T4 → 完成
           │
失败回滚:  T1 → T2 → T3 ✗ → C3 → C2 → C1

5.2 两种实现模式

模式描述适用
编排式(Choreography)每个服务完成本地事务后发送事件,触发下一个服务简单流程
协调式(Orchestration)中央 Saga 协调器按顺序调用各服务复杂流程

5.3 Saga 案例:旅行预订

正向事务:
  T1: 预订航班 → COMMIT
  T2: 预订酒店 → COMMIT  
  T3: 预订租车 → COMMIT
  T4: 处理支付 → COMMIT

T3 失败时的补偿:
  C3: 取消租车预订 → 忽略(未执行)
  C2: 取消酒店预订 → COMMIT
  C1: 取消航班预订 → COMMIT

5.4 Saga 的隔离性缺失

Saga 不提供隔离性,存在更新丢失脏读风险:

T1: 扣减库存 10 → COMMIT
T2: 读取库存(看到已扣减后的值)→ 基于错误值做决策
T1 回滚 → 补偿增加库存 10

结果:T2 的决策基于了一个从未真正提交的数据 → 业务异常

缓解方案:

  • 语义锁:业务层面标记"处理中"状态
  • 悲观视图:先查后改时考虑补偿事务的可能性
  • 可交换更新:更新操作具有交换律(如金额 ± 可重排)

6. 消息最终一致性

6.1 本地消息表

-- 本地事务内同时写入业务表 + 消息表
BEGIN;
  INSERT INTO orders (...) VALUES (...);
  INSERT INTO message_queue (topic, payload, status) 
  VALUES ('inventory', '{"productId":1,"count":2}', 'PENDING');
COMMIT;

-- 定时任务扫描 PENDING 消息,投递到 MQ
-- 消费者处理成功后,消息标记为 DONE

6.2 事务消息(RocketMQ)

// RocketMQ 事务消息
TransactionMQProducer producer = new TransactionMQProducer("order_group");

// 半消息:对消费者不可见
Message msg = new Message("order_topic", "减库存", bytes);
producer.sendMessageInTransaction(msg, new LocalTransactionExecuter() {
    @Override
    public LocalTransactionState executeLocalTransactionBranch(Message msg, Object arg) {
        try {
            orderService.createOrder((Order) arg);  // 本地事务
            return LocalTransactionState.COMMIT_MESSAGE;  // 确认消息
        } catch (Exception e) {
            return LocalTransactionState.ROLLBACK_MESSAGE; // 回滚消息
        }
    }
});

事务消息流程:

  1. 发送半消息(对消费者不可见)
  2. 执行本地事务
  3. 本地事务成功 → 确认消息(消费者可见)
  4. 本地事务失败 → 回滚消息(消费者不可见)
  5. 如果本地事务执行状态未知 → MQ 回调查询

6.3 最大努力通知

服务A ──► 消息队列 ◄── 服务B(消费处理)
   │           │
   │◄──────────┘(失败重试,有限次数)
   │
   └── 定时对账系统(A/B 数据一致性校验)

适用于:对实时性要求不高、可事后对账补偿的场景(如支付回调)。


7. Seata 框架实战

7.1 Seata 架构

┌─────────────────────────────────────────────────────────┐
│                      Seata Server                       │
│  ┌──────────────┐  ┌──────────────┐  ┌─────────────┐  │
│  │ TC           │  │ TM           │  │ RM          │  │
│  │ Transaction  │  │ Transaction  │  │ Resource    │  │
│  │ Coordinator  │  │ Manager      │  │ Manager     │  │
│  │ 事务协调器    │  │ 事务管理器    │  │ 资源管理器   │  │
│  └──────────────┘  └──────────────┘  └─────────────┘  │
└─────────────────────────────────────────────────────────┘

TC: 独立部署,维护全局事务状态
TM: 在应用端(@GlobalTransactional 注解处)
RM: 在各参与服务的资源层(代理数据源)

7.2 AT 模式(Automatic Transaction)

Seata AT 模式对业务零侵入,自动代理数据源:

@Service
public class OrderService {
    @GlobalTransactional(name = "create-order", rollbackFor = Exception.class)
    public void createOrder(Order order) {
        orderService.create(order);        // RM: order_db
        inventoryService.deduct(order);     // RM: inventory_db  
        accountService.debit(order);        // RM: account_db
    }
}

AT 模式原理:

  1. 一阶段:执行业务 SQL,记录 UNDO_LOG(前镜像/后镜像)
  2. 二阶段-成功:异步删除 UNDO_LOG
  3. 二阶段-回滚:用 UNDO_LOG 中的前镜像恢复数据
-- UNDO_LOG 表(自动创建)
CREATE TABLE undo_log (
    branch_id BIGINT NOT NULL,
    xid VARCHAR(128) NOT NULL,
    context VARCHAR(128) NOT NULL,
    rollback_info LONGBLOB NOT NULL,
    log_status INT NOT NULL,
    log_created DATETIME(6) NOT NULL,
    log_modified DATETIME(6) NOT NULL,
    PRIMARY KEY (branch_id, xid)
);

7.3 Seata 四种模式对比

模式原理侵入性性能适用场景
AT自动代理,记录 UNDO_LOG简单 CRUD,首推
TCC手撕三阶段接口复杂业务,性能要求高
Saga状态机引擎业务流程长
XA标准 2PC强一致要求

8. 方案选型指南

                          事务类型
                              │
              ┌───────────────┼───────────────┐
              │               │               │
          短事务          长事务            最终一致
              │               │               │
              ▼               ▼               ▼
        ┌─────────┐    ┌─────────┐    ┌─────────────┐
        │ 2PC/XA  │    │  Saga   │    │ 消息+本地表  │
        │  TCC    │    │ 状态机   │    │ 最大努力通知 │
        │ Seata-AT│    │         │    │             │
        └─────────┘    └─────────┘    └─────────────┘
              │               │               │
              ▼               ▼               ▼
        强一致性        最终一致性         最终一致性
        性能较低        吞吐高             吞吐最高
        实现简单        实现复杂           实现中等
场景推荐方案
单服务多数据源Seata AT / XA
高并发短事务TCC
长业务流程Saga
跨异构系统消息最终一致性
金融支付2PC/XA + 对账

9. 总结

分布式事务没有银弹:

一致性 ←─────── 强度 ─────────→ 性能
 │                              │
2PC/XA ──► TCC ──► Saga ──► 消息最终一致性
 │                              │
强                          弱/最终一致
 │                              │
低性能                      高性能

工程实践建议:

  1. 能用单机事务就不用分布式事务(合理拆分避免跨服务事务)
  2. 能用最终一致性就不用强一致(消息 + 对账是最佳实践)
  3. 必须强一致时选择 Seata AT(低侵入、AT 模式足够覆盖 80% 场景)
  4. 监控补偿任务,确保悬挂事务最终完成

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「数据库」更多文章

  1. 备份恢复与高可用方案
  2. 数据库性能监控与诊断
  3. NewSQL 选型对比