2pc/3pc理论或原理

在分布式系统中,2PC(两阶段提交)3PC(三阶段提交)是保证多个节点在执行事务时达成一致性的核心协议。简单来说,它们就是为了解决“要么大家一起成功,要么大家一起失败”的问题。


一、 2PC (Two-Phase Commit) 二阶段提交

2PC 是最基础的原子提交协议,将事务的处理过程分为两个阶段:投票阶段执行阶段

1. 运行流程

  • 阶段一:表决阶段 (Prepare Phase)
    • 协调者(Coordinator)向所有参与者(Participants)发送准备请求。
    • 参与者执行事务操作,将 Undo/Redo 日志写入磁盘,但不提交
    • 参与者向协调者回复 Yes(准备就绪)或 No(失败)。
  • 阶段二:执行阶段 (Commit Phase)
    • 如果所有人都说 Yes:协调者发送 Commit 通知,参与者正式提交并释放锁。
    • 如果有人说 No 或超时:协调者发送 Rollback 通知,参与者利用 Undo 日志回滚。

2. 核心问题

  • 同步阻塞:所有参与者在等待协调者指令时都处于阻塞状态,占用资源。
  • 单点故障:如果协调者在第二阶段宕机,参与者会一直锁定资源,导致整个系统“瘫痪”。
  • 数据不一致:如果在 Commit 阶段,只有部分参与者收到了指令(由于网络分区),就会导致数据状态不一。

二、 3PC (Three-Phase Commit) 三阶段提交

为了解决 2PC 的痛点(尤其是阻塞和单点故障),3PC 引入了超时机制并增加了一个预提交阶段。

1. 运行流程

  • 阶段一:CanCommit
    • 协调者询问参与者:你们环境正常吗?能执行这个事务吗?(此时不执行具体事务逻辑,只做检查)。
  • 阶段二:PreCommit
    • 协调者根据反馈发送预执行指令。参与者执行事务操作,记录日志,但仍不真正提交。
  • 阶段三:DoCommit
    • 真正的提交阶段。

2. 3PC 的改进点

  • 引入超时机制:如果参与者在 PreCommit 后没等到协调者的指令,它会自动提交。这解决了 2PC 永久阻塞的问题。
  • 降低阻塞概率:第一阶段的询问确保了大家都有成功的可能,减少了后续资源浪费。

3. 仍然存在的问题

  • 数据不一致风险:虽然解决了阻塞,但如果网络出现分区,协调者发出了回滚指令,而某个参与者因为超时而自动提交了,数据依然会乱套。

三、 2PC vs 3PC 核心对比

特性 2PC 3PC
阶段数 2 阶段 3 阶段
核心机制 阻塞式请求 引入超时机制 + 中间预备状态
协调者故障 可能导致参与者永久阻塞 参与者可通过超时机制自动提交
数据一致性 较好,但在网络分区时有风险 牺牲了一定的一致性来换取可用性
性能 延迟较低 延迟较高(多一轮网络交互)

一句话总结:2PC 简洁但容易卡死;3PC 通过增加步骤和超时虽然防卡死,但增加了复杂度且依然无法完美解决网络分区下的数据一致性。

seata如何解决的

Seata 是阿里开源的一个分布式事务解决方案,它最核心的突破在于对 2PC(两阶段提交) 进行了深度改良。

针对传统 2PC 的高并发性能差资源锁定时间长的问题,Seata 提供了四种模式:AT、TCC、Saga 和 XA。其中,AT 模式(Automatic Transaction) 是最常用、最具代表性的方案。


一、 Seata AT 模式:对 2PC 的优化

Seata AT 模式将 2PC 的过程通过 RM(资源管理器)TC(事务协调者) 进行了自动化管理,核心逻辑如下:

1. 阶段一:业务提交(本地事务执行)

在传统的 2PC 中,第一阶段不提交事务;而在 Seata AT 中,第一阶段直接提交本地事务

  • 解析 SQL:Seata 会拦截业务 SQL。
  • 记录回滚日志(Undo Log):在提交本地事务前,Seata 会查询数据的“前镜像”(Before Image)和“后镜像”(After Image),保存到 undo_log 表中。
  • 注册全局锁:向 TC 申请该行记录的全局锁。
  • 本地提交:业务数据和 undo_log 一起在一个本地事务中提交。
  • 优势立即释放本地数据库连接和锁,极大提高了并发能力。

2. 阶段二:异步处理

  • 如果全局提交:TC 通知 RM。RM 只需要异步删除对应的 undo_log 记录即可,速度极快。
  • 如果全局回滚:RM 收到回滚请求,提取 undo_log 中的前镜像数据,通过反向 SQL 补偿数据,实现回滚。

二、 Seata 解决了 2PC 的哪些痛点?

痛点 Seata AT 的解决方案
同步阻塞 异步提交。第一阶段就释放了本地事务锁,不需要像 2PC 那样等待全局决定。
单点故障 TC 集群化。Seata 的协调者(TC)支持高可用部署,不再是单点。
运维成本 无侵入。开发者只需关注业务 SQL,Seata 自动生成回滚逻辑(对比 TCC 手写 Confirm/Cancel)。
数据孤岛 全局锁管理。通过 TC 维护全局行锁,防止不同全局事务之间的脏写。

三、 Seata 的其他模式(针对不同场景)

为了应对 2PC 无法覆盖的复杂场景,Seata 提供了多样化武器库:

  • TCC 模式
    • 原理:手动实现 Try-Confirm-Cancel
    • 解决:非关系型数据库、或需要跨行、跨系统的自定义补偿逻辑(如库存预留)。
  • Saga 模式
    • 原理:长事务解决方案,每个步骤有对应的补偿操作。
    • 解决:业务流程长、涉及第三方服务的场景(无法回滚对方数据库)。
  • XA 模式
    • 原理:原生支持数据库的 XA 协议。
    • 解决:对强一致性要求极高,且不希望有 undo_log 表侵入的场景。

四、 总结:核心区别

Seata 的本质是将“重”的协议(2PC)拆解为“轻”的本地事务 + “异步”的补偿机制。它牺牲了极短时间内的强一致性(变为最终一致性),换取了互联网场景下至关重要的高性能

TCC

如果说 Seata 的 AT 模式是“自动挡”,那么 TCC (Try-Confirm-Cancel) 就是“手动挡”。它不依赖于数据库的撤销日志(Undo Log),而是将分布式事务的控制权完全交给了业务代码


一、 TCC 的三个阶段

TCC 要求业务方针对每一个操作,都要实现三个方法:

1. Try(预留业务资源)

  • 核心任务:完成所有业务检查(一致性),并预留必须的业务资源。
  • 例子:在扣减 100 元余额时,Try 阶段不是直接减掉 100 元,而是冻结 100 元。
  • 状态:此时订单处于“处理中”状态。

2. Confirm(确认执行业务)

  • 核心任务:真正执行业务。只使用 Try 阶段预留的资源。
  • 特点:只要 Try 成功,Confirm 理论上一定会成功。如果失败,TC 会不断重试。
  • 例子:将冻结的 100 元正式划转走。

3. Cancel(取消执行业务)

  • 核心任务:释放 Try 阶段预留的业务资源。
  • 特点:如果 Try 阶段失败或超时,或者其他参与者失败,则执行 Cancel 进行回滚。
  • 例子:解冻那 100 元。

二、 TCC 解决的 2PC 核心痛点

  1. 性能极高:它在 Try 阶段只预留资源,不锁定数据库行。这意味着不同的事务可以并发地预留各自的资源,不会发生严重的行锁竞争。
  2. 跨数据库/服务:2PC 和 AT 模式通常要求数据库支持(如 MySQL),而 TCC 纯粹是业务层面的逻辑,你可以跨 Redis、MongoDB 甚至是调用第三方 API 进行分布式事务。
  3. 最终一致性:它通过不断重试 Confirm/Cancel,确保系统最终达到一致状态。

三、 TCC 的“三大魔咒”(必须解决的设计问题)

使用 TCC 必须手动处理以下三个异常情况,否则会产生脏数据:

异常情况 定义 解决方案
空回滚 (Empty Rollback) Try 方法还没执行(比如网络丢包),就收到了 Cancel 请求。 Cancel 时需判断 Try 是否执行过,若没执行,直接返回成功。
幂等 (Idempotence) 网络抖动导致 Confirm 或 Cancel 被多次调用。 记录事务状态控制表,确保每个方法只生效一次。
悬挂 (Hanging) Cancel 比 Try 先到了(网络延迟),导致 Try 后到并成功预留了资源,但再也没人来处理它。 Try 执行前先检查此事务是否已经 Cancel 过,若是则不执行。

四、 AT vs TCC:该选哪一个?

特性 AT 模式 TCC 模式
开发难度 极低(几乎零代码侵入) (每个接口要写 3 个方法)
性能 较高(有全局锁和本地事务损耗) 极高(完全自定义,无锁设计)
一致性 弱强一致性 最终一致性
适用场景 关系型数据库、中等并发 高并发、跨非关系型数据库、金融级核心链路

简单一句话总结: 如果你的业务对并发量要求极高(如双十一秒杀),或者涉及非关系型数据库,请选 TCC;普通的内部管理系统或 O2O 业务,选 AT 能让你少加很多班。

TCC代码实现 Demo,或者了解 Seata 是如何自动处理“空回滚”和“悬挂”

要用好 TCC,理解 Seata 如何在底层帮你处理“空回滚”“悬挂”是关键,因为这些都是分布式环境下网络抖动必然会带来的挑战。


一、 Seata 如何自动处理异常?

在 Seata 的 TCC 模式中,它通过一张事务状态控制表(tcc_fence_log来自动管理状态,开发者只需要开启 @TwoPhaseBusinessAction 注解即可。

1. 空回滚 (Empty Rollback)

  • 场景:由于网络原因,Try 请求丢失了,但分布式事务已经决定回滚,于是向参与者发送了 Cancel
  • 解决:Seata 在执行 Cancel 前,会先去 tcc_fence_log 表查询是否有该事务的 Try 记录。如果没有,说明 Try 没成功,Seata 会直接插入一条状态为 ROLLBACKED 的记录,并返回成功,这就是“空回滚”处理。

2. 悬挂 (Hanging)

  • 场景Cancel 请求比 Try 先到达(网络拥堵导致 Try 迟到)。如果先执行了 Cancel,后续迟到的 Try 到达并成功预留了资源,由于事务已经回滚,这部分资源将永远无法释放。
  • 解决:当 Try 请求到达时,Seata 先去 tcc_fence_log 检查。如果发现已经有了该事务的 ROLLBACKED 记录(说明 Cancel 已经跑过了),Try 方法将拒绝执行,直接返回失败。

二、 TCC 代码实现 Demo (伪代码)

假设我们有一个积分扣减的业务,我们需要定义一个 TCC 接口:

1. 定义接口

@LocalTCC
public interface PointService {
    /**
     * Try: 冻结积分
     * @TwoPhaseBusinessAction 声明 TCC 模式,并指定 Confirm 和 Cancel 的方法名
     */
    @TwoPhaseBusinessAction(name = "deductPoints", commitMethod = "confirm", rollbackMethod = "cancel")
    boolean prepareDeduct(BusinessActionContext context, @BusinessActionContextParameter(paramName = "userId") String userId, int points);

    boolean confirm(BusinessActionContext context);

    boolean cancel(BusinessActionContext context);
}

2. 业务逻辑实现

public class PointServiceImpl implements PointService {

    @Override
    public boolean prepareDeduct(BusinessActionContext context, String userId, int points) {
        // 1. 检查余额
        // 2. 扣减可用积分,增加冻结积分 (SQL: update account set points = points - 100, frozen_points = frozen_points + 100 where user_id = ?)
        return true; 
    }

    @Override
    public boolean confirm(BusinessActionContext context) {
        // 1. 从 context 获取参数
        String userId = context.getActionContext("userId").toString();
        // 2. 正式扣除冻结积分 (SQL: update account set frozen_points = frozen_points - 100 where user_id = ?)
        return true;
    }

    @Override
    public boolean cancel(BusinessActionContext context) {
        // 1. 恢复可用积分,减少冻结积分 (SQL: update account set points = points + 100, frozen_points = frozen_points - 100 where user_id = ?)
        return true;
    }
}

三、 避坑指南:设计 TCC 时的核心原则

  1. 并发可见性Try 阶段必须确保资源被独占。例如余额扣减,一定要有“冻结字段”,否则在 Confirm 前,这部分钱可能会被用户通过其他非 TCC 接口花掉。
  2. 数据轻量化ConfirmCancel 方法中,尽量只操作 Try 阶段预留的数据,不要再去做复杂的业务校验。
  3. Confirm/Cancel 必须幂等:虽然 Seata 帮你做了不少工作,但在手动编写 SQL 时,仍建议使用 update ... where status = 'TRYING' 这种自带状态检查的语句。

写订单系统的 TCC 事务,最核心的设计思想是“预留资源”

在订单场景中,通常涉及订单状态(待支付/已支付)库存(锁定/扣减)资金(冻结/扣划)。下面我以一个典型的“创建订单并扣减库存”为例,为你展示伪代码实现。


一、 核心设计:库存表结构

为了支持 TCC,库存表通常需要两个字段:

  • available_qty: 可用库存
  • frozen_qty: 冻结库存(用于 TCC 预留)

二、 TCC 接口定义

使用 Seata 的 @TwoPhaseBusinessAction 注解来定义三个阶段的方法。

@LocalTCC
public interface StockService {

    /**
     * Try: 锁定库存
     * @param actionContext 事务上下文,用于传递参数到第二阶段
     * @param productId 产品ID
     * @param count 购买数量
     */
    @TwoPhaseBusinessAction(name = "deductStock", commitMethod = "confirm", rollbackMethod = "cancel")
    boolean prepareDeduct(BusinessActionContext actionContext, 
                         @BusinessActionContextParameter(paramName = "productId") Long productId, 
                         @BusinessActionContextParameter(paramName = "count") Integer count);

    /**
     * Confirm: 真正扣减锁定库存
     */
    boolean confirm(BusinessActionContext actionContext);

    /**
     * Cancel: 释放锁定库存
     */
    boolean cancel(BusinessActionContext actionContext);
}

三、 详细业务实现

1. Try 阶段 (锁定资源)

核心逻辑:检查余额 \(\rightarrow\) 扣减可用库存 \(\rightarrow\) 增加冻结库存。

public boolean prepareDeduct(BusinessActionContext actionContext, Long productId, Integer count) {
    // SQL: UPDATE stock_tab SET available_qty = available_qty - ?, frozen_qty = frozen_qty + ? 
    //      WHERE product_id = ? AND available_qty >= ?
    int affectedRows = stockMapper.freezeStock(productId, count);
    
    if (affectedRows == 0) {
        throw new RuntimeException("库存不足,锁定失败");
    }
    return true;
}

2. Confirm 阶段 (正式扣减)

核心逻辑:减少冻结库存(因为第一阶段已经把可用库存扣过了)。

public boolean confirm(BusinessActionContext actionContext) {
    Long productId = Long.valueOf(actionContext.getActionContext("productId").toString());
    Integer count = Integer.valueOf(actionContext.getActionContext("count").toString());

    // SQL: UPDATE stock_tab SET frozen_qty = frozen_qty - ? 
    //      WHERE product_id = ? AND frozen_qty >= ?
    // 注意:这里需要保证幂等,防止重复扣减
    return stockMapper.reduceFrozenStock(productId, count) > 0;
}

3. Cancel 阶段 (回滚/释放)

核心逻辑:将冻结库存还给可用库存。

public boolean cancel(BusinessActionContext actionContext) {
    Long productId = Long.valueOf(actionContext.getActionContext("productId").toString());
    Integer count = Integer.valueOf(actionContext.getActionContext("count").toString());

    // SQL: UPDATE stock_tab SET available_qty = available_qty + ?, frozen_qty = frozen_qty - ? 
    //      WHERE product_id = ? AND frozen_qty >= ?
    // 自动处理“空回滚”:如果 Try 没执行成功,frozen_qty 会不够扣,返回 0 即可
    return stockMapper.unfreezeStock(productId, count) > 0;
}

四、 整体流程图示

为什么这样写能解决问题?

  1. 高并发:在 Try 阶段结束后,数据库事务就已经提交并释放了物理锁。如果有其他用户也要买这个商品,只要 available_qty 足够,他们可以并行修改同一行数据,而不需要互相等待。
  2. 数据安全:即便在 Confirm 阶段宕机了,Seata 的 TC(事务协调者)会不断重试 Confirm 方法。由于我们写的是 frozen_qty - count,所以数据最终一定会一致。
  3. 防止超卖:通过 available_qty >= count 的 SQL 条件判断,保证了即使并发再高也不会出现负数。

在分布式系统中,当一个订单业务涉及多个微服务(如:订单、库存、账户、积分)同时执行 TCC 时,调用顺序状态一致性就成了架构设计的核心。

通常情况下,我们采用 “同步 Try,异步 Confirm/Cancel” 的策略。


如何处理更复杂的“多个服务同时进行 TCC”时的调用顺序问题

一、 多个 TCC 服务的调用链路

在 Seata 中,通常由一个“发起者”(通常是订单服务)作为事务管理器(TM),通过 RPC 远程调用其他服务的 TCC 接口。

1. 核心流程:

  1. 开启全局事务:订单服务通过 @GlobalTransactional 开启事务。
  2. 串行/并行 Try
    • 订单服务:Try(创建预处理订单,状态为“待支付”)。
    • 库存服务:Try(锁定库存,frozen_qty + 1)。
    • 账户服务:Try(扣减余额,frozen_balance + 100)。
  3. TC 决策
    • 全部 Try 成功:TC 向所有服务发起 Confirm。此时订单状态改为“已完成”,库存和余额的冻结部分真正扣除。
    • 任意一个 Try 失败:TC 向所有服务发起 Cancel。订单状态改为“已取消”,库存和余额原路返还。

二、 深度优化:如何处理复杂的调用顺序?

在复杂的电商场景中,并不是所有 Try 都能并行的。

1. 嵌套 TCC(Nested TCC)

如果 A 服务调用 B 服务,B 服务又调用 C 服务,且三者都是 TCC 模式:

  • 原则:父服务的 Try 必须包含子服务的 Try
  • 风险:调用链路越长,第一阶段占用的系统资源(如 HTTP 连接、RPC 线程)就越多。
  • Seata 支持:Seata 自动支持这种嵌套结构,它会记录整个调用树,确保 Confirm/Cancel 按照正确的反向顺序执行。

2. 混合模式(AT + TCC)

实际生产中,有些老系统不支持 TCC,你可以混合使用:

  • 库存(高并发):用 TCC 避免长时间行锁。
  • 订单/日志(并发低):用 AT 减少代码开发量。
  • Seata 优势:Seata 支持在一个全局事务中混合不同模式,TC 会统一协调它们的提交与回滚。

三、 一个致命陷阱:Try 阶段的“过度预留”

在设计调用顺序时,一定要注意资源释放的原子性

反面教材
如果你在 Try 阶段直接扣了钱(没用冻结字段),由于 Confirm 是异步的,万一 Confirm 还没执行时用户去查余额,会发现钱少了但订单还没成功。
正确做法
永远在 Try 阶段保持“中间态”,让用户在 UI 上看到“处理中”或“冻结中”,这才是真正的最终一致性体验。


四、 总结:TCC 落地三部曲

  1. 原子性设计:数据库表必须增加“冻结/预留”相关字段。
  2. 防御性编码:必须处理空回滚、幂等、悬挂(利用 Seata 的 tcc_fence_log)。
  3. 配置优化
    • ConfirmCancel 必须尽量简单,不包含耗时的外部调用。
    • 设置合理的超时时间,防止 TC 频繁重试导致系统雪崩。

这是一份基于 Spring Cloud (Nacos/Eureka) + Feign + Seata 2.x 的 TCC 模式落地配置模板。

我们将配置分为三部分:依赖管理关键配置 (YAML)数据库表准备


基于 Spring Cloud + Seata 的完整配置模板

一、 核心依赖 (Maven)

确保微服务项目中引入了 Seata 客户端。建议版本保持一致,避免协议兼容性问题。

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
    <exclusions>
        <exclusion>
            <groupId>io.seata</groupId>
            <artifactId>seata-spring-boot-starter</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>io.seata</groupId>
    <artifactId>seata-spring-boot-starter</artifactId>
    <version>2.0.0</version>
</dependency>

二、 关键配置 (application.yml)

每个参与 TCC 事务的微服务都需要如下配置,重点在于 tx-service-group 的映射。

seata:
  enabled: true
  application-id: ${spring.application.name}
  # 事务分组:必须与 Seata Server 端配置的集群名对应
  tx-service-group: my_test_tx_group 
  service:
    vgroup-mapping:
      my_test_tx_group: default # 映射到 Seata Server 的 default 集群
  registry:
    type: nacos # 注册中心类型
    nacos:
      application: seata-server
      server-addr: 127.0.0.1:8848
      group: SEATA_GROUP
      namespace: ""
  config:
    type: nacos
  tcc:
    # 开启 TCC 防悬挂/空回滚日志记录(关键!)
    fence:
      enabled: true
      clean-period: 1h # 定期清理已完成的事务记录

三、 数据库表准备 (TCC 控制表)

这是防止“空回滚”和“悬挂”的核心。Seata 会在执行 TryConfirmCancel 时自动操作这张表。你需要在每个业务数据库(如订单库、库存库)中创建它。

-- TCC 事务控制日志表(Seata 2.0+ 标准结构)
CREATE TABLE IF NOT EXISTS `tcc_fence_log`
(
    `xid`           VARCHAR(128) NOT NULL COMMENT 'global id',
    `branch_id`     BIGINT       NOT NULL COMMENT 'branch id',
    `action_name`   VARCHAR(64)  NOT NULL COMMENT 'action name',
    `status`        TINYINT      NOT NULL COMMENT 'status(tried:1;committed:2;rollbacked:3;suspended:4)',
    `gmt_create`    DATETIME(3)  NOT NULL COMMENT 'create time',
    `gmt_modified`  DATETIME(3)  NOT NULL COMMENT 'modified time',
    PRIMARY KEY (`xid`, `branch_id`),
    KEY `idx_gmt_modified` (`gmt_modified`),
    KEY `idx_status` (`status`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;

四、 核心启动逻辑

1. 业务调用方 (TM)

在 Controller 或 Service 的入口方法上开启全局事务:

@GlobalTransactional(name = "create-order-tx", rollbackFor = Exception.class)
public void createOrder(OrderDTO order) {
    // 1. 调用库存服务 TCC (Feign)
    stockClient.prepareDeduct(order.getProductId(), order.getCount());
    // 2. 调用账户服务 TCC (Feign)
    accountClient.prepareReduce(order.getUserId(), order.getAmount());
    // 3. 订单自身逻辑...
}

2. 参与者服务 (RM)

在实现类上使用 @LocalTCC 并在方法上使用 @TwoPhaseBusinessAction(如之前伪代码所示)。


五、 落地小贴士

  1. 关于 undo_log:AT 模式需要 undo_log 表,而 TCC 模式不需要 undo_log,它只需要 tcc_fence_log
  2. 隔离性:TCC 在第一阶段会释放数据库锁,为了保证隔离性(防止脏写),建议在业务上通过状态机或冻结字段来控制数据权限。
  3. 重试机制:由于 ConfirmCancel 是异步重试的,里面的逻辑必须是幂等的。即使 Seata 帮你管了 tcc_fence_log,你的 SQL 语句最好也带上状态判断(如 where status = 'FREEZED')。

AT和TCC模式能混合用

Seata 支持在同一个全局事务(Global Transaction)中混合使用 AT 模式和 TCC 模式。

Seata 的设计初衷就是为了解决复杂的分布式场景,它通过 事务协调者(TC) 统一管理不同类型的 分支事务(Branch Transaction)


一、 为什么需要混合使用?

在真实的微服务架构中,不同的服务往往有不同的业务特性和技术背景:

  • 订单/日志服务(适合 AT):逻辑简单,并发量中等,希望代码无侵入,直接用 @GlobalTransactional 配合 AT 模式最省心。
  • 库存/支付服务(适合 TCC):并发极高,或者需要操作非关系型数据库(如 Redis、MongoDB),或者需要调用第三方接口(如支付宝、微信支付),此时必须手动实现 TCC。
  • 遗留系统:有些服务已经是写好的 TCC 接口,而新服务想用更简单的 AT 模式。

二、 混合模式的工作原理

Seata 的 TC 会根据每个分支事务注册时的 类型(BranchType) 来决定如何调度:

  1. 全局开启:入口方法标注 @GlobalTransactional
  2. 分支注册
    • 调用 AT 服务时,RM(资源管理器)自动向 TC 注册一个 AT 类型的分支。
    • 调用 TCC 服务时,RM 向 TC 注册一个 TCC 类型的分支。
  3. 二阶段提交/回滚
    • 如果全局提交:TC 通知 AT 分支异步删除 undo_log;通知 TCC 分支执行 Confirm 方法。
    • 如果全局回滚:TC 通知 AT 分支根据 undo_log 反向补偿;通知 TCC 分支执行 Cancel 方法。

三、 混合模式的代码示例(伪代码)

假设一个下单流程:订单用 AT,库存用 TCC。

// 1. 业务发起方(TM)
@Service
public class OrderBusiness {

    @GlobalTransactional // 开启全局事务
    public void createOrder(String userId, String productId, int count) {
        // AT 模式:订单服务直接操作数据库,Seata 自动拦截并记录 undo_log
        orderDAO.insert(new Order(userId, productId, count));

        // TCC 模式:调用库存服务提供的 Try 接口(prepareDeduct)
        // 内部会进行库存冻结:available_qty - count, frozen_qty + count
        stockTccClient.prepareDeduct(null, productId, count); 
    }
}

四、 混合使用的注意事项(避坑指南)

虽然技术上可行,但在落地时要注意以下三点:

1. 事务隔离性差异

  • AT 模式:通过全局锁(Global Lock)保证隔离。
  • TCC 模式:隔离性完全由你的业务代码(如“冻结字段”)保证。
  • 风险:如果 TCC 阶段没有做好资源预留(冻结),而 AT 阶段又依赖这个资源,可能会产生数据冲突。

2. 性能瓶颈

全局事务的整体性能取决于最慢的那个分支。如果 TCC 模式做得很快,但 AT 模式因为数据库大表锁竞争导致二阶段提交变慢,整个全局事务的响应时间都会拉长。

3. 配置统一

确保所有参与混合事务的微服务都连接到同一个 Seata Server (TC) 集群,并且 tx-service-group(事务分组) 映射配置正确。


五、 总结对比

维度 AT 模式分支 TCC 模式分支
数据一致性 基于数据库镜像,回滚更准确 基于业务补偿,逻辑更灵活
代码侵入 几乎零侵入 较高(需写 3 个方法)
回滚机制 自动生成 SQL 手写逻辑
适合对象 关系型数据库 (MySQL/Oracle) 任何能实现补偿逻辑的资源 (Redis/API)

raft+kafka是否可以替代分布式事务

这是一个非常硬核且深刻的问题。简单来说:Kafka(基于 Raft 的新架构)和 Seata 解决的是完全不同维度的一致性问题,它们之间无法直接替代,但可以互补。

要理解为什么不能替代,我们需要区分 “组件内部的高可用(Consensus)”“业务跨服务的一致性(Distributed Transaction)”


一、 核心区别:Raft 解决什么?Seata 解决什么?

1. Raft + Kafka (组件级:共识算法)

Kafka 现在的 KRaft 模式是用 Raft 协议替代了 ZooKeeper。

  • 目标:解决 元数据的一致性消息的不丢失
  • 场景:当 Kafka 集群中的一个 Broker 挂了,通过 Raft 协议快速选出新的 Leader,保证集群自己“不乱阵脚”,数据在多副本间是一致的。
  • 范围:它只管 Kafka 内部的数据。

2. Seata (业务级:分布式事务)

Seata 解决的是 跨库、跨服务 的原子性。

  • 目标:解决 业务操作的原子性
  • 场景:下单成功(订单库更新)+ 扣款成功(账户服务)+ 库存减一(库存服务)。
  • 范围:它横跨了多个不同的数据库和微服务。

二、 为什么 Kafka 不能替代 Seata?

如果你想用 Kafka 来实现分布式事务(即著名的“可靠消息最终一致性”方案),你会面临以下挑战:

1. 业务侵入性极高

使用 Kafka 做事务,你通常需要实现 本地消息表

  • 你必须在一个本地事务中:更新业务数据 + 插入一条消息到消息表。
  • 然后由一个后台任务异步发送到 Kafka。
  • 对比 Seata AT:Seata 只需要一个注解 @GlobalTransactional,代码几乎不用动。

2. 回滚极其困难

  • Seata:支持自动回滚(AT 模式)或业务回滚(TCC 模式)。
  • Kafka:消息一旦发出并被消费,产生的影响(如扣费)很难“撤回”。你必须手动写一套逆向业务逻辑(补偿机制)来处理失败,本质上这变成了手动实现的 Saga 模式,开发成本巨大。

3. 实时性问题

Kafka 方案是最终一致性。这意味着在消息堆积时,用户可能已经下单成功了,但过了 5 分钟库存才扣减。

  • 而在金融、库存预扣等场景,往往需要 TCC 这种准实时的强一致性感知,这正是 Seata 的强项。

三、 Raft + Kafka + Seata 的协作关系

在高级架构中,它们其实是协同工作的:

  1. 存储层(Raft):Seata Server (TC) 如果部署为高可用模式,它内部的数据库(如 MySQL 集群)或者 Seata 的某些内部组件可能会利用 Raft 保证自己不挂。
  2. 传输层(Kafka):业务系统可以使用 Kafka 发送审计日志、大数据分析。
  3. 业务层(Seata):负责订单、支付、库存之间那几个核心数据库操作的“同生共死”。

四、 总结:什么时候选谁?

需求场景 推荐方案
集群内部节点选主、日志同步 Raft (Kafka 内部已集成)
多个微服务、多个数据库的强原子性操作 Seata (AT/TCC)
解耦服务、允许延迟的最终一致性 Kafka (事务消息)
长流程、涉及第三方系统的事务 Seata (Saga)

一句话总结: Kafka 是解决“消息怎么传不丢”,Seata 是解决“逻辑怎么执行不乱”。如果你现在面临的是多个微服务数据库不一致的问题,Raft 救不了你,还是得用 Seata。

思考 Kafka 自带的“事务消息”(Transactional Messaging)是如何工作的?它虽然不能替代 Seata,但在某些异步场景下非常强大。

传统的 2PC/3PC 性能太差,而最终一致性又无法满足某些核心金融业务。请结合 Raft 协议 或者 高性能消息队列(如 Pulsar 或新版 Kafka),如何设计一个“跨机房双活”且能保证“强一致性”的写入链路

这是一个非常硬核的架构设计问题。要实现跨机房双活(Active-Active)且保证强一致性(Strong Consistency),我们必须绕过传统 2PC 的协调者单点和同步阻塞瓶颈,利用底层协议的共识(Consensus)能力来驱动上层业务。

传统的 2PC 性能差是因为它在业务层做锁竞争;而基于 Raft新版 Kafka/Pulsar 的方案是在底层日志层做顺序共识。

以下是结合 Raft 状态机同步高性能消息队列 的双活强一致性架构设计方案:


一、 核心思路:从“事务管理”转向“共识日志同步”

在双活架构中,核心矛盾是网络时延(RTT)。传统的 2PC 需要多次往返,且在提交前锁定资源。
高性能方案: 将事务抽象为一条有序日志(Log),只要日志在两个机房的多数派节点上达成共识,业务就视为写入成功。


二、 方案设计:基于 Raft 的强一致性写入链路

1. 架构组件

  • 全局序列号生成器(Global Sequencer):利用 Raft 协议在两地三中心部署,确保生成的事务 ID 是全局递增且唯一的。
  • 分布式存储层(如 TiDB 或基于 Raft 的 KV 存储):数据库不再是单机 MySQL,而是原生支持 Raft 协议的分布式数据库。
  • Leader 调度策略:通过 Raft 的 Region Leader 机制,将用户请求调度到距离其最近的 Leader 节点。

2. 写入链路流程

  1. 请求接入:机房 A 的应用接收写入请求。
  2. 日志预写(Append Log):Leader 节点将操作封装为一条 Raft Log。
  3. 跨机房同步:Leader 将 Log 同步给本机房的 Follower 和对端机房的 Follower。
  4. 多数派确认:只要本地机房的节点 + 对端机房的一个节点回执成功(满足 \(N/2 + 1\)),日志即被视为 Committed
  5. 状态机应用(Apply):各节点异步将日志应用到状态机(内存/磁盘 DB),返回用户成功。

三、 方案设计:基于 Pulsar/Kafka 的事务消息链路

如果你希望解耦业务,可以使用支持 分布式事务隔离 的消息队列。

1. 跨机房同步机制

  • Geo-Replication:Pulsar 原生支持跨机房同步。
  • 原子写入(Atomic Writes):利用 Kafka 2.x+ 的 initTransactionssendOffsetsToTransaction

2. 写入链路流程

  1. 开启消息事务:生产者向机房 A 的 Broker 发送事务消息。
  2. 同步复制:Broker 设置 acks=all,要求至少一个副本必须存在于对端机房,确保跨机房数据不丢。
  3. 提交指令:当业务本地逻辑执行完,发送 Commit
  4. 消费端保证:消费端设置 isolation.level=read_committed,确保对端机房只有在事务完全同步后才能看到数据。

四、 为什么这能超越 2PC?

维度 传统 2PC 基于 Raft / 高性能队列的共识方案
锁定时间 整个事务期间锁定数据库行 仅在日志同步阶段有微小延迟,无业务锁
阻塞模型 同步阻塞,等待所有参与者响应 多数派准入,不要求所有节点都活着
一致性等级 强一致,但易因故障导致不确定状态 线性一致性(Linearizability)
双活能力 很难实现(协调者跨机房性能极差) 天然支持,Leader 在哪,哪就是写中心

五、 金融级双活的特殊处理(关键优化)

  1. 分片(Sharding)与局部性
    将用户数据按 ID 分片。A 机房用户作为分片 1 的 Raft Leader,B 机房用户作为分片 2 的 Leader。这样 90% 的请求在本机房内就能达成多数派,只有 10% 跨机房,大幅降低 RTT 损耗。
  2. Learner 节点
    在第三个机房部署 Learner 节点,只读不参与投票,用于灾备和数据离线分析,不影响写入性能。
  3. Follower Read
    利用 ReadIndexLease Read 机制,允许在从机房进行强一致性读,实现真正的“读写双活”。

实践中这种架构能否解决分布事务

简单直接的回答是:Raft/Kafka 这种底层协议能解决“数据同步一致性”,但它不能直接替代“业务逻辑原子性”。

在实践中,这种架构通常被作为分布式事务的底层基石,而不是全部。


一、 协议层 vs. 业务层:缺了什么?

要理解为什么 Raft/Kafka 不能直接解决分布式事务,我们需要看这两个场景的区别:

  • Raft/Kafka 解决的是“副本一致性”: 只要 Leader 写入了 A=10,它能保证集群内所有节点最终(或强一致地)都变成 A=10
  • 分布式事务解决的是“跨资源原子性”: 这是一个逻辑集合。比如 账户减100红包加100。这两个操作可能在不同的 Raft 组(不同的数据库分片或不同的微服务)里。

Raft 只能保证单个 Raft 组内的一致性,无法原生保证跨组的操作。


二、 实践中的真实例子:分布式数据库 (如 TiDB / CockroachDB)

在高性能实践中,我们确实用 Raft 替代了传统的 2PC 锁等待,但它是这样分层的:

1. 底层:Raft 保证高可用

数据被切分成很多小块(Region)。每个 Region 是一个 Raft 组。这样解决了跨机房双活的数据安全自动选主问题。

2. 上层:在 Raft 之上跑 2PC (优化版)

当你要做一个跨 Region(跨服务)的事务时,依然需要类似 2PC 的逻辑,但性能极高:

  • 提议阶段:向 Raft 组 A 写入一个“预写(Lock)”记录,向 Raft 组 B 写入一个“预写(Lock)”记录。
  • 共识达成:这两个写入操作在各自的 Raft 组内通过“多数派确认”完成。
  • 决议阶段:一旦两边都写成功,事务就逻辑上提交了。

三、 为什么说这种架构“解决”了分布式事务的痛点?

虽然逻辑上还是 2PC,但有了 Raft/Kafka 架构后,性能和稳定性有了质变:

  1. 消除单点故障
    传统的 2PC 如果协调者宕机,整个事务锁死。而在 Raft 架构下,如果某个分片的 Leader 宕机,Raft 会在毫秒级选出新 Leader 恢复事务处理。
  2. 异步化性能
    Raft 允许“多数派确认”即返回。在跨机房场景下,如果你有 3 个机房(A, B, C),只要 A 和 B 确认了,就不需要等物理距离最远的 C。这解决了传统 2PC 必须等待所有节点响应的“长尾效应”。
  3. 日志即真理
    利用 Kafka 或 Raft 的顺序日志,我们可以实现 “确定性状态机”。只要日志顺序定了,不管节点怎么崩溃,重启后按顺序回放日志,最终状态一定是一致的。

四、 结论:实践中的选择

在工程实践中,我们不会用 Raft 替代 分布式事务,而是用 Raft 来构建高性能的分布式资源

  • 如果你追求超高性能和极致双活: 你应该选择 NewSQL 数据库(如 TiDB)基于 Raft 的 KV 存储。它们内部屏蔽了 Raft 和分布式事务的复杂交互,对业务透明。
  • 如果你在做微服务解耦: 你应该使用 Seata + 高可用配置。此时 Seata 的控制表(tcc_fence_log 等)可以存放在基于 Raft 的高可用数据库中。

总结: Raft 和 Kafka 提供了可靠的、有序的通道,而分布式事务是在这个通道上跑的业务逻辑。没有 Raft,分布式事务很脆弱;有了 Raft,分布式事务才能跑得又快又稳。

基于这种架构下,seata应该怎么使用

当我们将底层架构升级为基于 Raft 的共识系统(如 TiDB、CockroachDB 或高可用 Kafka/Pulsar)时,Seata 的角色从“苦力”变成了“指挥官”。

在这种架构下,Seata 不再需要担心底层数据库死锁或单点崩溃,而是专注于跨机房的业务编排。以下是实践中的最佳落地姿势:


一、 部署架构:将 Seata 挂载在共识层之上

为了配合底层的高性能,Seata 自身的存储必须也具备 Raft 级别的可靠性。

1. 存储层(Store Mode)切换为 DB

不要使用 file 模式。将 Seata 的 global_tablebranch_table 存放在支持 Raft 的分布式数据库(如 TiDB)中。

  • 好处:Seata 状态机本身实现了“跨机房双活”。即使 A 机房的 Seata Server 挂了,B 机房的 Seata Server 连上 TiDB 就能立刻接管所有未完成的事务。

2. 注册中心(Registry)跨机房同步

使用 Nacos 或 ETCD(本身基于 Raft)作为注册中心。确保微服务在调用 Seata 时,能自动感知最近的 TC 节点。


二、 模式选择:AT 还是 TCC?

在这种高性能架构下,模式的选择会发生微妙变化:

1. 优先使用 AT 模式(利用 NewSQL 的特性)

如果底层是 TiDB 等支持分布式事务的数据库,Seata 的 AT 模式性能会大幅提升。

  • 原理:TiDB 内部已经解决了行级锁的 Raft 共识。Seata 只需要在上面加一层轻量级的 undo_log
  • 适用场景:大部分 80% 的业务场景。

2. 核心链路使用 TCC(配合消息队列)

对于跨机房延迟极高的核心链路,使用 TCC + 异步 Confirm

  • 实践思路
    • Try 阶段:在本地机房的分布式数据库中预留资源。
    • Confirm 阶段:通过 Raft 协议同步的消息队列(如 Pulsar)异步通知对端机房执行。
  • 适用场景:跨国/跨长距离机房的资金划转。

三、 实战优化:如何“压榨”性能

1. 开启“全局锁查询”优化

在分布式数据库中,查询全局锁(Global Lock)开销较大。

  • 优化点:在 file.conf 或 Nacos 配置中,调高 client.rm.lock.retryInterval(重试间隔),减少对底层 Raft 组的频繁请求压力。

2. 利用 Raft 的“分片局部性”

如果你的分布式数据库按 user_id 分片,那么 Seata 的事务分组(Service Group)也应该按 user_id 映射。

  • 操作:让特定的 user_id 事务总是命中同一个机房的 Seata TC 和数据库 Leader。这样可以避免跨机房的 RPC 往返,响应速度接近单机。

四、 这种架构下的异常处理流程

当底层有了 Raft 保证,Seata 处理异常变得非常简单:

故障场景 Raft 层的表现 Seata 层的表现
单机房断电 对端机房 Raft 自动选出新 Leader,数据不丢。 Seata 发现事务超时,在存活机房发起全局回滚。
网络分区(脑裂) 少数派分区停止写入,保证数据不乱。 Seata 的 TC 处于少数派,无法拿到全局锁,事务自动失败,保护业务。
数据库慢 SQL Raft 复制延迟变大。 Seata 监控到事务执行时间过长,主动触发回滚,防止占满连接池。

五、 总结建议

在基于 Raft/Kafka 的强一致性链路上使用 Seata,你的准则是:底层管“稳”,Seata 管“快”

  1. 基础设施:Seata Server 的数据库一定要用分布式强一致数据库。
  2. 业务设计:尽量让一个事务内的所有操作落入同一个 Raft 调度周期内(即分片一致性)。
  3. 最终屏障:利用 Seata 的 @GlobalCheck 机制,在 Raft 复制完成后做最后一次数据校验。

这种架构虽然强悍,但运维复杂度较高。