【架构实战】分布式事务:从CAP定理到Seata实战,一文讲透跨服务数据一致性

【架构实战】分布式事务:从CAP定理到Seata实战,一文讲透跨服务数据一致性

昨天聊了微服务架构下的 API 网关和认证授权,有个核心问题一直没展开:跨服务的数据一致性。

一个典型的事故场景:下单服务减了库存,微服务 A 通知支付服务收款,用户支付成功后,发货服务要生成物流单。如果中间任何一步失败了,怎么处理?库存减了但钱没收、钱收了但货没发------这些"半成品状态"是企业级系统里最让人头大的问题。

分布式事务,说白了就是在多个服务、多个数据库之间,保证"要么全做,要么全不做"。这个问题困扰了架构师十几年,到今天也没有银弹,但有成熟可落地的解法。这篇从 CAP 定理开始,到 TCC、Saga 模式,再到生产级框架 Seata,把完整链路讲清楚。

一、CAP 定理:理解分布式系统一致性的基础

任何讨论分布式事务的文章,不讲 CAP 定理就是在耍流氓。

CAP 定理由加州大学伯克利分校的 Eric Brewer 在 2000 年提出,由 Seth Gilbert 和 Nancy Lynch 在 2002 年证明。内容很简单:在一个分布式系统里,不可能同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition Tolerance)三者,最多同时满足两个

  • 一致性(Consistency):每次读操作都能读到最新写入的数据,所有节点看到的数据一致。
  • 可用性(Availability):每个请求都能在有限时间内得到响应,不保证数据是最新的。
  • 分区容错性(Partition Tolerance):系统能在网络分区(节点之间通信中断)的情况下继续运行。

为什么不能同时满足?因为网络分区是一定会发生的(机房断电、交换机故障、跨地域网络抖动),一旦发生,系统必须在"停服等一致"和"继续服务"之间二选一。没有第三个选项。

CP 系统:优先保证一致性,牺牲可用性。典型:ZooKeeper、HBase、Redis 主从同步模式(从节点在同步未完成时拒绝读写)。

AP 系统:优先保证可用性,牺牲一致性。典型:Cassandra、DynamoDB、Eureka(注册中心)、Redis 集群模式(从节点可读但可能是旧数据)。

现实工程选择:在网络分区必然发生的前提下,工程上通常选择 AP,然后通过补偿机制尽可能达到最终一致性。强一致性的代价太大了------跨机房同步锁在网络抖动时会让整个系统hang死,这是用户无法接受的。

二、强一致性方案:两阶段提交(2PC)

两阶段提交(Two-Phase Commit, 2PC)是最经典的强一致性协议,理解它是理解所有其他方案的基础。

2.1 流程

阶段一:Prepare(准备阶段)

协调者(Coordinator)向所有参与者(Participant)发送 Prepare 请求,询问"你们准备好了吗"。每个参与者在本地执行事务(锁定资源、写 undo/redo 日志),但不提交。然后返回 Ready 或 Abort。

阶段二:Commit(提交阶段)

  • 如果所有参与者都返回 Ready:协调者发送 Commit,所有参与者正式提交事务。
  • 如果任何一个返回 Abort:协调者发送 Rollback,所有参与者回滚。

2.2 缺陷

2PC 有三个致命问题,生产环境基本不用:

同步阻塞:Prepare 阶段参与者会锁定资源,其他事务无法访问。如果某个参与者挂了,协调者会一直等,超时后才决定回滚。这期间整个系统处于锁定状态。

单点故障:协调者是唯一的,如果它在 Commit 阶段挂了,参与者会一直锁定资源等待指令。没有协调者,谁都不敢提交,也不敢回滚。

数据不一致:如果协调者发了部分 Commit(有些发了,有些还没发就挂了),部分参与者提交了,部分没提交,数据就不一致了。

工程结论:2PC 不适合高并发互联网场景,适合对一致性要求极高、参与节点少的金融/银行核心系统(配合硬件和专网)。普通业务用 2PC 就是给自己挖坑。

三、柔性事务:Seata 的 AT 模式和 TCC 模式

3.1 什么是柔性事务

柔性事务(Flexible Transaction)是对强一致性的妥协:放弃实时强一致性,追求最终一致性(Eventually Consistency)。即允许短暂的数据不一致,但通过补偿机制,在合理时间内自行修复到一致状态。

Seata(Simple Extensible Autonomous Transaction Architecture)是阿里巴巴开源的分布式事务解决方案,在国内生产环境使用非常广泛。它提供了四种模式:

  • AT 模式:自动处理,对业务无侵入(基于 undo log 逆向 SQL)
  • TCC 模式:Try-Confirm-Cancel,三阶段编程式补偿
  • Saga 模式:长事务编排,适合长链路、多步骤
  • XA 模式:强一致性,基于 XA 协议(需数据库支持)

国内工程实践的主流选择:AT 模式(常规业务)+ TCC 模式(高并发金融场景)。

3.2 AT 模式:自动化的数据补偿

AT 模式是 Seata 最省心的模式,业务代码几乎不用改。它的工作原理:

一阶段解析 SQL :Seata 的 DataSource Proxy 拦截业务 SQL,确定操作类型(UPDATE/INSERT/DELETE)和表名。根据主键生成逆向 SQL(undo log),写入 undo_log 表。

二阶段提交:提交成功后,异步删除 undo log。由于全局锁已释放,其他事务可以并发处理。

异常回滚 :发生异常时,Seata 从 undo_log 表读取逆向 SQL,执行补偿,恢复到修改前的状态。全局锁由 TC(Transaction Coordinator)管理。

yaml 复制代码
# seata-server.yml(TC 服务端配置)
server:
  port: 8091

store:
  mode: db
  db:
    datasourceType: druid
    dbType: mysql
    driverClassName: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://mysql:3306/seata?useUnicode=true
    username: root
    password: root
xml 复制代码
<!-- 各业务服务的依赖 -->
<dependency>
    <groupId>com.seata</groupId>
    <artifactId>seata-spring-boot-starter</artifactId>
    <version>1.7.0</version>
</dependency>
yaml 复制代码
# application.yml(业务服务配置)
seata:
  tx-service-group: my_tx_group
  enable-auto-data-source-proxy: true
  config:
    type: nacos
    nacos:
      namespace: seata
      serverAddr: nacos:8848
  registry:
    type: nacos
    nacos:
      application: seata-server
      serverAddr: nacos:8848

业务代码完全不用改,Seata 自动拦截 DataSource 操作:

java 复制代码
@Service
public class OrderServiceImpl implements OrderService {

    @GlobalTransactional(name = "create-order", rollbackFor = Exception.class)
    @Override
    public void createOrder(CreateOrderRequest request) {
        // 1. 创建订单记录
        Order order = new Order();
        order.setId(UUID.randomUUID().toString());
        order.setUserId(request.getUserId());
        order.setAmount(request.getAmount());
        order.setStatus("PENDING");
        orderMapper.insert(order);

        // 2. 调用库存服务扣减库存(feign远程调用)
        storageClient.deduct(request.getProductId(), request.getQuantity());

        // 3. 调用账户服务扣款
        accountClient.deduct(request.getUserId(), request.getAmount());

        // 4. 更新订单状态
        order.setStatus("PAID");
        orderMapper.updateById(order);

        // 任何一步抛异常,Seata 自动回滚全部
    }
}

只需要加一个 @GlobalTransactional 注解,Seata 自动管理分布式事务。一阶段的 undo log 由框架自动生成、回滚时执行,二阶段也是框架处理。

3.3 TCC 模式:精准控制的补偿

AT 模式依赖 undo log,适用于大多数场景。但如果需要精准控制每个阶段的业务逻辑,或者对性能要求极高(如每秒数万笔交易),就需要 TCC 模式。

TCC 把每个操作拆成三个阶段:

Try(预留):尝试执行,锁定资源,但不做最终确认。比如冻结库存、预扣金额。

Confirm(确认):所有 Try 都成功后,执行真正的业务操作。比如真正减库存、真正扣款。

Cancel(取消):任何一个 Try 失败,执行补偿,释放资源。比如解冻库存、退还预扣金额。

java 复制代码
@LocalTCC
public interface StorageTccService {

    @TwoPhaseBusinessAction(name = "deductStorage", commitMethod = "confirm", rollbackMethod = "cancel")
    boolean tryDeduct(
            @BusinessActionContextParameter(paramName = "productId") String productId,
            @BusinessActionContextParameter(paramName = "quantity") int quantity,
            @BusinessActionContextParameter(paramName = "xid") String xid
    );

    boolean confirm(BusinessActionContext context);

    boolean cancel(BusinessActionContext context);
}

@Service
public class StorageTccServiceImpl implements StorageTccService {

    @Autowired
    private StorageMapper storageMapper;

    @Override
    @Transactional
    public boolean tryDeduct(String productId, int quantity, String xid) {
        // Try 阶段:检查库存是否足够,如果足够则预扣(冻结)
        Storage storage = storageMapper.selectByProductId(productId);
        if (storage.getAvailable() < quantity) {
            throw new BusinessException("库存不足");
        }
        // 冻结 quantity 库存(available 减少,frozen 增加)
        storageMapper.freezeStock(productId, quantity);
        return true;
    }

    @Override
    @Transactional
    public boolean confirm(BusinessActionContext context) {
        // Confirm 阶段:正式扣减冻结的库存
        String productId = context.getActionContext("productId", String.class);
        int quantity = context.getActionContext("quantity", Integer.class);
        storageMapper.confirmFrozen(productId, quantity);
        return true;
    }

    @Override
    @Transactional
    public boolean cancel(BusinessActionContext context) {
        // Cancel 阶段:释放冻结的库存
        String productId = context.getActionContext("productId", String.class);
        int quantity = context.getActionContext("quantity", Integer.class);
        storageMapper.unfreezeStock(productId, quantity);
        return true;
    }
}

TCC 的关键设计原则:

幂等性:Confirm 和 Cancel 都会重试,必须设计成幂等的(重复执行结果相同)。用主键+状态机判断,或者用 Redis 记录 xid 做去重。

空回滚:Try 没执行成功(网络超时),Cancel 被调用了。此时不应该报错,应该查一下有没有这个 xid 的记录,没有就跳过。

悬挂:Cancel 比 Try 先执行(比如网络问题 Try 超时了,但 Cancel 消息先到了)。此时 Confirm 不会执行,Cancel 做了空回滚之后,要防止 Try 最后又成功执行了。

四、Saga 模式:长事务编排

当事务链路非常长(10+ 步骤),且每一步都有明确的正向和逆向操作时,TCC 的复杂度会急剧上升。Saga 模式是更适合的选择。

Saga 把长事务拆成一系列本地事务,每个本地事务都有对应的补偿操作。执行顺序由编排器(Choreography 或 Orchestration)决定。

编排模式(Saga Orchestrator):有一个中心编排器定义执行顺序。

java 复制代码
@SagaStart
public class OrderSaga {

    @Autowired
    private SagaController sagaController;

    public void execute(CreateOrderRequest request) {
        sagaController.execute(new Step[] {
            new Step("createOrder", "创建订单", this::createOrderStep, this::createOrderCompensate),
            new Step("deductStorage", "扣减库存", this::deductStorageStep, this::deductStorageCompensate),
            new Step("deductAccount", "扣减账户", this::deductAccountStep, this::deductAccountCompensate),
            new Step("sendMessage", "发送通知", this::sendMessageStep, this::sendMessageCompensate),
        });
    }
}

事件驱动模式(Saga Choreography):每个服务通过消息队列(MQ)通知下一个服务,不需要中心编排器。服务之间通过事件解耦,适合松耦合的微服务架构,但调试和追踪困难。

五、生产环境落地:避坑指南

5.1 全局锁的性能问题

Seata 的 AT 模式在 Update 操作时会加全局锁,这是性能的主要瓶颈。高并发场景下,如果一个全局锁持有时间过长,会阻塞其他涉及同一行数据的事务。

优化策略:

  • 拆分热点数据:不要把库存全放一张表,按商品ID哈希分表,让不同商品的扣减操作不竞争同一把锁。
  • 读写分离:AT 模式默认读写都在主库,可以考虑配置只读库分担读压力(Seata 支持)。
  • 降级方案:库存扣减这种强一致性场景用 TCC,查询类场景直接读从库,不过 Seata。

5.2 TCC 空回滚和悬挂的防御代码

这是生产代码里最容易漏掉的部分,一定要写:

java 复制代码
@Override
public boolean cancel(BusinessActionContext context) {
    String xid = context.getXid();
    String productId = (String) context.getActionContext("productId");
    Integer quantity = (Integer) context.getActionContext("quantity");

    // 空回滚防御:查询是否存在 try 记录
    // 如果没有,说明 try 根本没执行,直接返回成功
    if (!frozenLogMapper.existsByXidAndProductId(xid, productId)) {
        log.info("空回滚检测: xid={}, productId={},跳过", xid, productId);
        return true;
    }

    // 幂等防御:检查是否已经 cancel 过
    if (frozenLogMapper.isCanceled(xid, productId)) {
        return true;
    }

    // 执行解冻
    storageMapper.unfreezeStock(productId, quantity);
    frozenLogMapper.markCanceled(xid, productId);
    return true;
}

5.3 事务日志的可靠性

Seata 的事务协调器(TC Server)状态存储推荐用数据库(MySQL/PostgreSQL),而不是文件或 Redis。数据库有 ACID 保证,TC Server 重启后能恢复到正确状态。Redis 存储在服务器重启时可能丢数据,导致事务状态不一致。

5.4 监控告警

分布式事务出了问题是很难排查的,因为涉及多个服务。几个必须监控的指标:

  • 全局事务成功率:突然下降说明有异常
  • 分支事务超时率:超时会导致全局回滚
  • TC Server 的活跃会话数:爆了说明并发过高
  • 补偿动作频率:Cancel/Confirm 频繁执行说明业务失败率高

六、选型建议:什么时候用什么模式

场景 推荐模式 原因
大多数业务场景(库存扣减、订单创建) AT 模式 对业务无侵入,改造成本最低
高并发金融支付,强一致性要求 TCC 模式 精准控制,性能好
超长链路(10+步骤),事件驱动架构 Saga 模式 避免长时间锁定
强一致性要求,数据库支持 XA XA 模式 两阶段提交,数据库原生支持

一个系统的不同业务模块完全可以混用不同模式:订单创建用 AT 模式(通用),支付清算用 TCC 模式(金融级精准),长账期对账用 Saga 模式(链路长)。

七、总结

分布式事务的核心矛盾:强一致性 vs 高可用。没有银弹,只有取舍。

  • CAP 定理告诉我们,分区一定会发生,要在做 CP 还是 AP 之间做选择。
  • 2PC 强一致但有致命缺陷(同步阻塞、单点故障),不适合互联网高并发场景。
  • AT 模式最省心,适合 80% 的业务场景,Seata 自动处理回滚。
  • TCC 模式精准可控,适合金融支付类高一致性场景,但需要写三阶段补偿代码。
  • Saga 模式适合长链路编排,MQ 驱动解耦,但调试困难。

工程实践里,绝大多数场景用 AT 模式就够了 。只有在真正遇到性能瓶颈或强一致性要求时,才值得为 TCC 的复杂度买单。记住:最合适的架构是能够解决你当前问题的最简单方案,而不是技术最先进的那一个。


我是做架构的,关注我,一起搞定分布式系统里的那些坑。

相关推荐
独孤九剑打醒他1 小时前
RGB‑TOT 全架构系统仿真:红外波长簇光通信完整验证
架构
这个DBA有点耶2 小时前
从库延迟的“二次放大”效应:一次大事务,拖垮整个读写分离
数据库·mysql·架构
张宇Joaquin3 小时前
高性能模板库Eigen架构分析及性能优化实践
性能优化·架构
AI产品测评官4 小时前
AI 招聘系统的三代技术演进:从 DOM 注入到视觉语义读取的架构拆解
人工智能·架构·求职招聘
lakernote5 小时前
图解 Kafka Consumer 常用 API:poll、seek、pause、wakeup 到底在控制什么?
分布式·kafka·linq
语核科技5 小时前
知识管理系统的技术选型:传统知识库与AI原生知识库的架构差异
架构·ai-native
梦奇不是胖猫5 小时前
从小餐厅 到 理解 互联网架构
架构
m0_640602448 小时前
2026 年餐饮收银系统前后端技术实现——核心架构与原理详解
后端·微服务·云原生·架构
数据库小学妹8 小时前
集群与分布式啥区别?从主从集群到分布式实战
分布式·分布式数据库·数据库架构·集群·分库分表·主从集群·集群与分布式