分布式事务方案选型矩阵

问题背景

2022年,我们团队启动了一个供应链系统的重构项目。当时的需求是:订单服务、库存服务、支付服务之间需要保证事务一致性。团队里有两个人分别推荐了不同的方案——

一位推荐 Seata AT 模式,说"开箱即用,代码零侵入";另一位推荐自研 Saga 编排,说"性能最好,腾讯都是这么做的"。

吵了两周,最后拍板用了 Seata AT。理由是:工期紧,代码侵入越小越好。

结果呢?系统上线三个月后,库存服务的 QPS 从 2000 降到了 600——因为 AT 模式的全局锁在高并发扣减库存时形成了严重的瓶颈。DBA 每周都要来问我:"你们的全局锁怎么这么多?"

六个月的二次重构,换成了 TCC 模式。团队熬了三个月,上线后库存服务的 QPS 恢复到 1800(接近理论上限),延迟从 500ms 降到了 30ms。

这次选型失误的成本:6 个月的开发人力 + 3 个月的系统不稳定期 + 无数客诉

【架构权衡】 选型没有银弹。AT/TCC/Saga/事务消息各有各的适用场景,脱离业务场景谈方案优劣毫无意义。正确的选型姿势是:先定义业务对一致性的真实需求,再评估性能压力和团队能力,最后才是在满足约束的方案中做权衡

问题定义

分布式事务的选型,本质上是在一致性、性能、开发成本三个维度上做权衡。

一致性维度描述
强一致性任何时刻,所有节点的数据完全一致
弱一致性不保证立即一致,但最终会达到一致状态
最终一致性允许短暂不一致,但保证在有限时间内收敛

在分布式系统中,CAP 定理告诉我们: Consistency(一致性)、Availability(可用性)、Partition tolerance(分区容错性)三者不可兼得。分布式事务方案,本质上是在 CP(强一致)和 AP(最终一致)之间做选择。

四维评估模型

分布式事务选型不是一道选择题,而是一道多约束优化题。我建议从四个维度评估:

flowchart TD
    A[分布式事务选型] --> B[一致性要求]
    A --> C[性能压力]
    A --> D[业务复杂度]
    A --> E[团队能力]

    B --> B1{强一致?}
    B1 -->|是| B2["→ 2PC / TCC"]
    B1 -->|否| B3["→ Saga / 事务消息"]

    C --> C1{QPS `<` 500?}
    C1 -->|是| C2["→ AT / Saga"]
    C1 -->|否| C3["→ TCC / XA"]

    D --> D1{跨语言 / 非 DB?}
    D1 -->|是| D2["→ TCC / Saga"]
    D1 -->|否| D3["→ AT / XA"]

    E --> E1{团队有 TCC 开发经验?}
    E1 -->|否| E2["→ AT (快速落地)"]
    E1 -->|是| E3["→ TCC (性能优先)"]

维度一:一致性要求

这是最关键的维度。回答这个问题:你的业务能容忍数据不一致吗?能容忍多久?

一致性要求典型业务场景推荐方案
强一致性(实时)金融转账、证券交易、账户扣款TCC / 2PC(需接受性能损失)
最终一致(秒级)电商下单、订单状态同步、库存扣减AT / Saga / 事务消息
最终一致(分钟级)日志同步、数据统计、消息通知定时任务 + 补偿

维度二:性能压力

第二个维度是系统的并发量。不同的方案在高并发下的表现差异巨大:

QPS 范围方案选择理由
QPS < 500AT / Saga / 本地消息表全局锁竞争不明显,开发成本低
500 <= QPS < 3000TCC / Saga需要注意锁持有时间,可通过异步化优化
QPS >= 3000TCC 资源预留 / 无锁 Saga避免全局锁成为瓶颈,资源预留是关键技术
⚠️

性能压力不只是看 QPS,还要看单个全局事务涉及的分支数量。如果一个全局事务涉及 10 个分支,即使每个分支只需要 10ms,全局事务的 RT 也是 100ms起步。Seata 推荐单个全局事务的分支数量控制在 20 个以内

维度三:业务复杂度

第三个维度是业务的复杂程度和资源类型:

业务特征推荐方案说明
纯 SQL 操作,同一个数据库实例AT自动解析 SQL,零侵入
跨数据库实例TCC / Saga需要业务层处理分库分表
涉及外部 HTTP/RPC 服务TCCAT 不支持外部服务调用
非 DB 资源(Redis、MQ、ES)TCC业务层控制预留/回滚
长链路业务流程(10+ 步骤)Saga 编排补偿链可能过长,TCC 不适合

维度四:团队能力

最后一个维度是团队是否具备相应的技术储备

团队情况推荐方案说明
新团队,技术储备不足本地消息表 / AT入门简单,社区文档丰富
有 Java 经验,无分布式事务经验ATSeata AT 接入成本低
有分布式事务经验TCC / Saga追求性能和可控性
多语言团队Saga / 事务消息TCC 需要多语言 SDK,Seata 已支持

完整选型矩阵

综合四个维度,以下是各方案的完整对比:

方案一致性QPS 上限代码侵入锁策略回滚方式适用场景
2PC强一致~1000全局 DB 锁自动低并发、强一致的简单场景
AT最终一致~2000全局 TC 锁自动Java + SQL,高并发以下
TCC强一致~5000业务层预留手动高并发、跨资源、外部服务
Saga最终一致~10000无锁补偿长链路业务流程
本地消息表最终一致~500轮询补偿中小规模,延迟不敏感
事务消息最终一致~3000消费幂等MQ 为主,发送方一致性
MySQL XA强一致~800全局 DB 锁自动MySQL 原生,多语言友好

【架构权衡】 在选型时,我有一个核心原则:能用最终一致性解决的问题,绝不用强一致性。因为强一致性方案(2PC/TCC)的代价是性能损耗和运维复杂度。除非你的业务明确要求"钱不能多扣/不能少扣,且必须实时一致",否则都应该优先考虑最终一致性方案。

典型场景分析

场景一:金融支付(强一致 → TCC)

金融支付是分布式事务最严苛的场景。每一笔支付必须精确扣款,不能多扣也不能少扣。

// TCC 模式实现支付扣款
@LocalTCC
public interface PaymentTccService {
    @TwoPhaseBusinessAction(
        name = "deduct",
        commitMethod = "confirm",
        rollbackMethod = "cancel"
    )
    boolean try(DeductDTO dto, BusinessActionContext ctx,
        @BusinessActionContextParameter(paramName = "accountId") Long accountId,
        @BusinessActionContextParameter(paramName = "amount") BigDecimal amount);

    boolean confirm(BusinessActionContext ctx);
    boolean cancel(BusinessActionContext ctx);
}
flowchart TD
    A[支付请求] --> B{Try: 冻结金额}
    B -->|冻结成功| C{库存 Try: 冻结库存}
    C -->|冻结成功| D{订单 Try: 创建订单}
    D -->|成功| E[Confirm 链<br/>逐个确认]
    D -->|失败| F[Cancel 链<br/>逐个回滚]
    E --> G[支付成功]
    F --> H[支付失败<br/>金额/库存已释放]
💡

金融场景中,TCC 的 Confirm 和 Cancel 都需要幂等设计。因为网络分区可能导致 Confirm 被执行多次。如果每次 Confirm 都真的扣款,用户会被多扣钱。解决方案是 Confirm 只执行"状态变更",检查账户是否已经处于"已扣款"状态。

场景二:电商下单(最终一致 → AT 或 Saga)

电商下单的一致性要求比金融支付低一些——允许短暂的状态不一致,只要最终一致即可。

// 方案 A:AT 模式(简单场景)
@GlobalTransactional
public void placeOrder(OrderDTO dto) {
    orderService.create(dto);       // 分支事务 1
    inventoryService.deduct(dto);   // 分支事务 2
    paymentService.charge(dto);     // 分支事务 3
}

// 方案 B:Saga 模式(复杂场景,如涉及外部促销系统)
public class OrderSaga {
    // Saga 编排器的每个步骤都是独立的补偿单元
    @SagaStart
    public void createOrder() { ... }

    @Compensable(compensationMethod = "cancelInventory")
    public void deductInventory() { ... }

    @Compensable(compensationMethod = "cancelPayment")
    public void chargePayment() { ... }
}

场景三:长链路履约(Saga 编排)

当业务流程涉及 10+ 个步骤时,TCC 模式会变得难以维护——每个步骤都需要写 Try/Confirm/Cancel 三个方法,光是方法数量就难以接受。

flowchart LR
    A[创建订单] --> B[预扣库存]
    B --> C[预扣优惠]
    C --> D[调用支付]
    D --> E[通知仓储]
    E --> F[分配仓库]
    F --> G[打包出库]
    G --> H[物流揽收]
    H --> I[物流运输]
    I --> J[用户签收]
    J --> K[完成履约]

    style A fill:#e1f5ff
    style K fill:#e1f5ff
    style B fill:#fff3cd
    style C fill:#fff3cd
    style D fill:#fff3cd
    style E fill:#fff3cd

这种长链路场景,Saga 是最佳选择。Saga 的核心是"正向操作 + 补偿操作",没有悬挂和空回滚的问题(因为没有 Try 阶段)。

// Saga 模式:正向操作 + 补偿操作
public class InventorySaga {

    // 正向操作:扣减库存
    public void deductInventory(Long productId, Integer quantity) {
        inventoryMapper.decrement(productId, quantity);
    }

    // 补偿操作:归还库存
    public void compensateInventory(Long productId, Integer quantity) {
        inventoryMapper.increment(productId, quantity);
    }
}
⚠️

Saga 的局限性是没有 Try 阶段,意味着正向操作失败后,补偿操作也可能失败(比如库存归还时数据库挂了)。这种情况下需要人工介入。设计 Saga 流程时,必须评估每个步骤的失败概率,并为高风险步骤设计重试策略人工补偿机制。

场景四:高并发秒杀(TCC 资源预留)

秒杀场景的核心挑战是:超卖性能。AT 模式的全局锁在秒杀场景下会被打爆,必须使用 TCC 的资源预留模式。

flowchart TD
    A[秒杀请求] --> B[Try: 冻结库存<br/>只做 freeze<br/>不真正扣减]
    B --> C{QPS 10000?<br/>全局锁竞争?}

    C -->|AT 模式| D[全局锁排队<br/>RT 飙升<br/>用户体验差]
    C -->|TCC 模式| E[无全局锁<br/>freeze 操作本地执行<br/>RT 稳定]

    D --> F[系统崩溃<br/>雪崩]
    E --> G[高并发通过<br/>后续处理堆积请求]

TCC 的资源预留模式本质是:用业务层的"冻结"代替数据库锁。冻结操作是纯业务逻辑,不需要全局协调,因此可以支撑极高的并发。

避坑指南

坑一:不要为了技术而技术

很多团队引入分布式事务框架,纯粹是"觉得不用就落后了"。实际上,如果你的系统拆分后,不同服务之间没有强一致性需求,用最终一致性和本地补偿就够了。

判断标准:你的业务能接受"下单成功但库存还没扣"吗?如果能,就不需要分布式事务,用本地消息表或定时任务补偿即可。

坑二:不要在小系统中引入复杂框架

Seata 的运维成本不低:需要额外部署 TC Server、配置高可用、维护 undo_log 表。如果你的系统只有 3~5 个服务,日请求量不超过 1000,用本地消息表 + 定时任务完全够用。

系统规模推荐方案
单体或 2~3 个服务本地消息表 + 定时任务
3~10 个服务,中等并发AT / Saga
10+ 个服务,高并发TCC / 分层 Saga

坑三:不要混用多种方案

很多团队在同一个系统中混用了 AT、TCC、Saga 三种方案——原因是不同的业务场景选了不同的方案。这会导致:

  1. 运维复杂度爆炸:需要同时维护 AT 的 TC + TCC 的 TCC 日志 + Saga 的补偿表
  2. 调试困难:一个全局事务可能跨越 AT 和 TCC 两种模式
  3. 一致性语义混乱:团队成员对"什么场景用哪种方案"没有共识

建议:在架构设计阶段确定一种主方案,只有当该方案明显不适用时(如库存扣减用 AT 性能不够),才在特定场景引入第二种方案,并明确标注边界。

坑四:全局事务的粒度控制

无论选择哪种方案,单个全局事务涉及的分支数量越少越好。建议:

  • 单个全局事务的分支数量控制在 10 个以内(Seata 推荐 20 个以内)
  • 跨服务调用优先使用异步消息而非同步 RPC
  • 长时间-running 的业务操作(如支付回调)不要放在全局事务中
// ❌ 低效:全局事务包含大量分支
@GlobalTransactional
public void createOrder(OrderDTO dto) {
    orderService.create(dto);           // 分支 1
    inventoryService.deduct(dto);        // 分支 2
    paymentService.charge(dto);          // 分支 3
    couponService.use(dto);              // 分支 4
    pointsService.grant(dto);           // 分支 5
    messageService.notify(dto);          // 分支 6
    smsService.send(dto);               // 分支 7 ← 这个放全局事务就是浪费
}

// ✅ 高效:消息通知等异步操作移出全局事务
@GlobalTransactional
public void createOrder(OrderDTO dto) {
    orderService.create(dto);           // 分支 1
    inventoryService.deduct(dto);        // 分支 2
    paymentService.charge(dto);          // 分支 3
    couponService.use(dto);              // 分支 4
    pointsService.grant(dto);           // 分支 5
}

// 消息通知等异步执行,不参与全局事务
@Async
public void sendNotifications(OrderDTO dto) {
    messageService.notify(dto);
    smsService.send(dto);
}

开源框架对比

框架语言AT 支持TCC 支持Saga 支持XA 支持推荐度
SeataJava原生原生原生原生⭐⭐⭐⭐⭐
ShardingSphereJava原生⭐⭐⭐
HmilyJava原生⭐⭐
Apache ServiceCombJava原生原生⭐⭐⭐
MySQL XA多语言原生⭐⭐⭐

推荐 Seata 作为首选方案。原因:社区活跃(Apache 顶级项目)、功能最全(四种模式全覆盖)、文档完善、与 Spring Cloud/Dubbo 集成良好。

工程代价

维度2PCATTCCSaga本地消息表
运维成本
开发成本
排障复杂度
性能影响
跨语言支持部分

落地 Checklist

  • 明确一致性需求:与业务方确认业务能容忍的最大不一致时间
  • 评估性能压力:压测确定 QPS 峰值和平均 RT
  • 审计业务场景:识别所有涉及多服务协作的业务流程
  • 选择主方案:根据四维评估模型确定主要方案
  • 设计异常路径:每种方案都需要设计空回滚、幂等、悬挂的处理逻辑
  • 制定监控策略:全局事务成功率、RT 分布、锁等待时间
  • 编写测试用例:覆盖正常流程 + 各异常分支(超时、网络分区、服务宕机)
  • 制定回滚预案:当新方案出现问题时,如何快速回滚到之前的状态
  • 团队培训:确保所有开发人员理解所选方案的工作原理和注意事项
  • 灰度发布:先在小比例流量上验证,再全量上线