一、CAP定理与分布式事务的哲学矛盾

在深入Seata之前,必须先理解CAP定理对分布式事务的根本性制约。CAP(Consistency一致性、Availability可用性、Partition Tolerance分区容错性)告诉我们:三选二。但在分布式系统中,分区是物理事实——网络 끊开、光纤被挖、交换机故障都是常态,不是例外。这意味着我们实际上只能在C和A之间做取舍

强一致性(C)要求所有节点在同一时刻看到相同的数据,这需要同步等待——也就是分布式事务的核心代价。可用性(A)则要求每次请求都能在有限时间内返回有效响应,不允许因等待其他节点而超时。

所以,当我们在分布式系统中说"需要强一致性事务"时,实际上是在说:"我愿意牺牲可用性来换取一致性。"这不是技术问题,是业务决策——金融支付场景必须强一致,而商品浏览场景最终一致即可。

1.1 为什么说"分布式事务是伪命题"

传统ACID事务依赖于单一数据库实例内部的物理协调机制:锁管理器、Redo日志、Undo日志、WAL(Write-Ahead Logging)——这些全部运行在共享内存和磁盘上,延迟在微秒级。当我们把事务拆到多个节点上,网络往返延迟从微秒级跳到毫秒级(局域网1-5ms,广域网50-200ms),事务持有锁的时间成百上千倍增长,数据库连接池很快耗尽。更致命的是:在等待协调的过程中,任何一个节点超时都会导致整个事务失败

核心矛盾:分布式事务试图用"协调协议"来弥补物理距离带来的不一致,但协调本身引入了新的失败点和性能瓶颈。这不是Seata的缺陷,是分布式系统的物理本质。

1.2 BASE理论:务实的最终一致

BASE(Basically Available、Soft state、Eventually consistent)是对CAP中C和A矛盾的业务化解:允许系统在不同时间看到不同的数据,但最终会收敛到一致状态。这是互联网公司处理高并发场景的主流选择。

/**
 * 业务场景对比:强一致 vs 最终一致
 * 
 * 场景A:银行转账(强一致,必须ACID)
 *   - 从账户A扣100元 → 账户B加100元
 *   - 任何中间状态(只扣没加,或只加没扣)都是不可接受的
 *   - 允许:事务时间长、吞吐低、部分节点故障时整体不可用
 * 
 * 场景B:电商下单扣库存(最终一致,可接受短时不一致)
 *   - 订单创建 → 库存预扣(本地事务)→ 异步消息确认
 *   - 允许:库存超卖5%以内(补货或赔偿解决)
 *   - 要求:最终状态必须收敛(库存不能无限扣)
 */

// 架构决策树:什么时候必须分布式事务?
// ├─ 是否跨服务?  否 → 本地事务(数据库ACID)
// ├─ 是否接受短时不一致? 是 → 最终一致(Saga/消息队列)
// ├─ 是否是金融级场景? 是 → Seata AT或TCC(接受性能代价)
// └─ 是否必须强一致且高性能? → 重新设计业务边界(避免跨库事务)

二、2PC与3PC:协调协议的原点

Two-Phase Commit(2PC)是分布式事务的教科书方案,也是Seata所有模式的底层基础。理解2PC的缺陷,才能理解Seata为什么要做AT/TCC/Saga三种模式。

2.1 2PC的执行流程

/**
 * 2PC两个阶段:
 * 
 * Phase1(Prepare):协调者向所有参与者发送Prepare请求
 *   - 参与者执行本地事务(Undo日志已写入),但不提交
 *   - 持有所需的全部锁
 *   - 返回YES(可提交)或NO(有约束违反)
 * 
 * Phase2(Commit/Rollback):协调者根据所有参与者的响应决定
 *   - 全YES → 发送Commit,所有参与者提交,释放锁
 *   - 任何NO → 发送Rollback,所有参与者回滚,释放锁
 * 
 * 关键点:Prepare阶段参与者已经持有了所有行锁!
 */

/**
 * 2PC的三个致命缺陷:
 * 
 * 缺陷1:同步阻塞(Blocking)
 *   - 从Prepare到Commit期间,所有参与者的行锁都被持有
 *   - 其他事务必须等待,极易引发死锁和连接池耗尽
 *   - 参与者数量越多,持有锁的时间越长
 * 
 * 缺陷2:单点故障(Single Point of Failure)
 *   - 协调者崩溃在Phase2刚发出Commit之前
 *   - 参与者不知道该Commit还是Rollback,会一直持有锁等待指令
 *   - 这就是"未知状态"问题:事务处于中间态,不知道该何去何从
 *   - 唯一的恢复手段是:等待协调者重启(30秒超时后?)
 * 
 * 缺陷3:数据不一致(Inconsistent State)
 *   - 如果协调者发送Commit后崩溃,只有一部分参与者收到
 *   - 收到Commit的提交了,没收到的回滚了——永久不一致
 *   - 这在网络分区场景下几乎必然发生
 */

// 伪代码模拟2PC协调者
public class TwoPhaseCoordinator {
    private List<Participant> participants = new ArrayList<>();
    
    public boolean execute(Transaction tx) throws Exception {
        // Phase1: Prepare
        List<Future<Vote>> futures = new ArrayList<>();
        for (Participant p : participants) {
            // 异步发送Prepare给每个参与者
            futures.add(executor.submit(() -> p.prepare(tx)));
        }
        
        // 收集所有投票(设置超时防止无限等待)
        boolean allYes = true;
        for (Future<Vote> f : futures) {
            Vote v = f.get(prepareTimeout, TimeUnit.SECONDS);
            if (v == Vote.NO || v == null) { // null = 超时/异常
                allYes = false;
                break;
            }
        }
        
        // Phase2: 根据结果决定
        if (allYes) {
            broadcastCommit(tx); // 部分Commit失败 = 数据不一致!
        } else {
            broadcastRollback(tx);
        }
        return allYes;
    }
}

2.2 3PC的改进与局限

Three-Phase Commit(3PC)试图通过引入"PreCommit"阶段来解决2PC的阻塞问题:

/**
 * 3PC的三个阶段:
 * 
 * Phase1 (CanCommit):协调者询问参与者是否可以提交
 *   - 不执行事务,只是探路
 *   - 参与者在超时后默认Abort(不无限等待)
 * 
 * Phase2 (PreCommit):协调者发送PreCommit,所有参与者执行Undo日志
 *   - 仍然不提交,但已做好提交准备
 *   - 释放了部分锁(只保留关键资源的锁)
 * 
 * Phase3 (DoCommit):协调者发送DoCommit,参与者提交
 *   - 如果协调者在Phase3崩溃,参与者等待DoCommit超时后自动提交
 *   - 这解决了2PC的无限阻塞问题
 * 
 * 3PC的局限性:
 * - 仍然无法解决数据不一致(Phase3网络分区)
 * - 增加了网络往返,性能反而更差
 * - 在asynchronous网络模型下仍然不是"完美"的
 * - 工业界实际使用极少(只有部分研究性数据库使用)
 */

// 对比表格
// 特性            | 2PC        | 3PC
// ----------------|------------|---------------
// 阻塞            | 是(严重) | 改善(但不完美)
// 单点故障        | 严重       | 改善(超时机制)
// 数据不一致      | 可能       | 可能(Phase3网络分区)
// 网络往返        | 2次        | 3次
// 工业使用        | XA/JDBC    | 极少

三、Seata AT模式:无入侵的自动补偿

Seata(Simple Extensible Autonomous Transaction Architecture)是阿里开源的分布式事务解决方案,目前在Apache孵化。AT模式是Seata的核心模式,它的核心思想是:用本地事务+全局锁替代全局锁管理器,实现无侵入的事务协调

3.1 AT模式的全局锁机制

AT模式拆掉了传统2PC中"协调者持有一把全局锁"的架构,改为:

  • TC(Transaction Coordinator):Seata服务端,负责管理全局事务生命周期和全局锁
  • TM(Transaction Manager):业务侧,管理事务边界(通过@GlobalTransactional注解)
  • RM(Resource Manager):资源管理器,嵌入到每个数据库中,负责分支事务
/**
 * AT模式的工作原理:
 * 
 * 1. TM发起全局事务(TM向TC注册,获取XID)
 * 2. 每个本地业务SQL执行前,RM自动:
 *    a. 向TC申请行级X锁(如果拿不到则等待或失败)
 *    b. 记录"前镜像"(Before Image):执行SQL前的数据快照
 *    c. 执行SQL
 *    d. 记录"后镜像"(After Image):执行SQL后的数据快照
 *    e. 记录Undo Log(用于回滚)
 * 3. 全部成功后,全局提交:TC异步清理行锁和Undo Log
 * 4. 任一失败,全局回滚:TC通知所有RM用Undo Log回滚
 * 
 * 关键设计:全局锁的获取是在数据库行锁层面实现的
 * 锁信息保存在TC(Seata Server),但通过唯一ID(XID+Branch ID)管理
 * 避免了2PC中"全局锁管理器"这个单点
 */

/**
 * 快照机制详解(前镜像/后镜像)
 * 
 * 前镜像(Before Image):SELECT快照
 *   执行 DELETE FROM account WHERE id = 100;
 *   前镜像:SELECT * FROM account WHERE id = 100;  → {id:100, balance:1000}
 * 
 * 后镜像(After Image):执行后快照
 *   执行后:SELECT * FROM account WHERE id = 100;   → {id:100, balance:0}
 * 
 * Undo Log记录:
 *   {
 *     tableName: "account",
 *     beforeImage: [{id:100, balance:1000}],
 *     afterImage: [{id:100, balance:0}],
 *     sqlType: "DELETE",
 *     primaryKeys: ["id"]
 *   }
 * 
 * 回滚时:将beforeImage反向执行即可恢复
 */

// AT模式的隔离级别
// Seata默认提供"读已提交"级别的全局隔离
// 全局锁机制保证:在全局事务执行期间,
// 其他全局事务无法修改同一行数据(防止脏写)
// 但读未提交(脏读)仍然可能发生——这是AT模式与生俱来的限制

3.2 Spring Boot + Seata AT模式实战

// ============ Step1: 项目依赖 ============
// pom.xml
<dependencies>
    <!-- Spring Boot starter -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jdbc</artifactId>
    </dependency>
    <!-- Seata AT模式:自动装配 + DataSource代理 -->
    <dependency>
        <groupId>io.seata</groupId>
        <artifactId>seata-spring-boot-starter</artifactId>
        <version>2.0.0</version>
    </dependency>
    <!-- 数据库驱动(这里用MySQL) -->
    <dependency>
        <groupId>com.mysql</groupId>
        <artifactId>mysql-connector-j</artifactId>
    </dependency>
    <!-- 连接池(Seata需要能够包装DataSource) -->
    <dependency>
        <groupId>com.zaxxer</groupId>
        <artifactId>HikariCP</artifactId>
    </dependency>
</dependencies>

// ============ Step2: application.yml 配置 ============
/**
 * 关键配置说明:
 * - tx-service-group:事务分组,用于客户端发现TC服务器
 * - vgroup-mapping:事务分组 → TC集群地址的映射
 *   Seata支持基于事务分组的负载均衡,一个分组可以对应多个TC节点
 * - enable-auto-data-source-proxy:自动代理DataSource(核心!不加=AT不生效)
 * - client.rm.report-success-enable:事务成功时是否上报TC(关闭可减少网络开销)
 */
@Configuration
public class SeataConfig {
    // 如果使用Nacos作为配置中心,可以在这里注入配置
    // Seata支持Nacos/Apollo/ZooKeeper等多种配置中心
}

// ============ Step3: 全局事务业务代码 ============
/**
 * @GlobalTransactional 注解的原理:
 * 1. TM向TC注册全局事务,获取XID(全局事务ID)
 * 2. XID通过Dubbo的RpcContext或Feign的RequestHeader传播到所有服务
 * 3. 每个RM通过DataSourceProxy拦截SQL,自动注册分支事务
 * 4. TM所在服务执行完业务后,通知TC提交或回滚
 */
@Service
public class OrderServiceImpl implements OrderService {

    @Autowired
    private OrderMapper orderMapper;
    
    @Autowired
    private AccountFeignClient accountFeignClient; // Feign调用账户服务
    
    @Autowired
    private StorageFeignClient storageFeignClient;  // Feign调用库存服务

    /**
     * 分布式事务场景:创建订单
     * - 扣减账户余额(远程调用)
     * - 扣减商品库存(远程调用)
     * - 创建本地订单记录(本地数据库)
     * 
     * 任何一个环节失败,全局回滚!
     */
    @Override
    @GlobalTransactional(rollbackFor = Exception.class, name = "create-order")
    public void createOrder(OrderDTO orderDTO) {
        // 1. 远程调用:扣减账户余额
        // Feign拦截器会自动将XID传递到账户服务
        accountFeignClient.deductBalance(orderDTO.getUserId(), orderDTO.getTotalAmount());
        
        // 2. 远程调用:扣减库存
        // 库存服务通过GlobalTransactional或者全局锁保护库存行
        storageFeignClient.deductStock(orderDTO.getProductId(), orderDTO.getQuantity());
        
        // 3. 本地事务:创建订单记录
        // Seata DataSourceProxy自动拦截此SQL,注册分支事务
        Order order = new Order();
        order.setId(UUID.randomUUID().toString());
        order.setUserId(orderDTO.getUserId());
        order.setProductId(orderDTO.getProductId());
        order.setQuantity(orderDTO.getQuantity());
        order.setTotalAmount(orderDTO.getTotalAmount());
        order.setStatus("CREATED");
        order.setCreateTime(LocalDateTime.now());
        orderMapper.insert(order);
        
        // 4. 人为模拟失败(测试回滚用)
        // throw new RuntimeException("模拟业务异常,观察全局回滚!");
    }
}
AT模式最佳实践:AT模式对SQL有要求——必须支持"SELECT FOR UPDATE"的Undo Log生成。对于复杂SQL(JOIN、子查询),Seata会解析SQL生成前镜像,如果SQL不支持,AT模式会退化为手动补偿模式。

四、Seata TCC模式:业务层的精控三阶段

TCC(Try-Confirm-Cancel)是Seata的第二种模式,与AT模式的核心区别在于:TCC不依赖Undo Log,而是在业务层通过代码手动实现补偿逻辑。这让它适合任何类型的存储系统(MySQL、Redis、MongoDB、HTTP服务等)。

4.1 Try-Confirm-Cancel三阶段设计

/**
 * TCC三阶段详解:
 * 
 * Try(预留/检查阶段)
 *   - 尝试执行所有业务操作
 *   - 进行资源检查(如库存是否充足)
 *   - 预留资源(如冻结库存:将可用库存-1,冻结库存+1)
 *   - 这一步是"假执行",只是做检查和标记
 *   - 任何资源预留失败,立即执行Cancel释放
 * 
 * Confirm(确认阶段)
 *   - 真正提交业务
 *   - 使用Try阶段预留的资源(如将冻结库存变为真正的扣减)
 *   - 只处理Try阶段成功的情况
 *   - 如果Confirm失败?TC会无限重试(幂等性保证)
 * 
 * Cancel(取消/回滚阶段)
 *   - 释放Try阶段预留的资源(如解冻库存)
 *   - 补偿Try阶段的操作
 *   - 如果Cancel失败?TC也会无限重试
 * 
 * TCC的核心挑战:业务侵入性强(需要业务方实现三个方法)
 * 但这换来的是:性能高(无Undo Log)、适用范围广(任何系统)
 */

/**
 * TCC与AT的本质区别:
 * 
 * AT模式:
 *   - 侵入性低:自动代理SQL,生成Undo Log
 *   - 性能好(无分布式锁):TC只记录锁,不阻塞SQL执行
 *   - 局限:只支持有SQL语义的场景
 * 
 * TCC模式:
 *   - 侵入性高:需要业务方实现三个方法
 *   - 性能最优:无全局锁等待,无Undo Log生成
 *   - 适用范围:任意存储系统、远程服务
 */

// TCC事务参与者接口
public interface TccAction {
    /**
     * Try阶段:预留资源
     * @param businessKey  业务键(用于定位这次预留)
     * @param reserveInfo 预留信息(如扣款金额)
     * @return true=预留成功,进入Confirm;false=预留失败,进入Cancel
     */
    boolean tryReserve(String businessKey, Object reserveInfo);
    
    /**
     * Confirm阶段:确认执行
     * @param businessKey 与Try阶段对应
     * @param confirmInfo 确认信息
     * @return 必须返回true(失败会重试直到成功)
     */
    boolean confirm(String businessKey, Object confirmInfo);
    
    /**
     * Cancel阶段:回滚/释放预留
     * @param businessKey 与Try阶段对应
     * @param cancelInfo  取消原因
     * @return 必须返回true(失败会重试直到成功)
     */
    boolean cancel(String businessKey, Object cancelInfo);
}

4.2 TCC模式实战:账户服务扣款

// ============ TCC账户服务实现 ============
/**
 * TCC模式下的账户服务设计:
 * 
 * Try阶段:检查余额是否充足,如果充足则"冻结"相应金额
 *   账户表增加 frozen_amount 字段
 *   UPDATE account SET frozen_amount = frozen_amount + ? WHERE user_id = ? 
 *   AND (balance - frozen_amount) >= ?
 *   
 * Confirm阶段:真正扣款
 *   UPDATE account SET balance = balance - frozen_reduce, 
 *                      frozen_amount = frozen_amount - frozen_reduce
 *   WHERE user_id = ?
 *   
 * Cancel阶段:解冻金额
 *   UPDATE account SET frozen_amount = frozen_amount - frozen_reduce
 *   WHERE user_id = ?
 */

@Service
@LocalTCCAnnotation // 标识为本地TCC参与者(供TM调用)
public class AccountTccServiceImpl implements AccountTccAction {
    
    @Autowired
    private AccountMapper accountMapper;

    @Override
    @TwoPhaseBusinessAction(  // 标识这是一个TCC的Try方法
        name = "deductAccountTcc",
        commitMethod = "confirm",
        rollbackMethod = "cancel"
    )
    public boolean tryReserve(
        // actionContext会由Seata框架注入,包含XID/BranchID等事务上下文
        BusinessActionContext actionContext,
        @BusinessActionContextParameter(paramName = "userId") String userId,
        @BusinessActionContextParameter(paramName = "amount") BigDecimal amount
    ) {
        String xid = actionContext.getXid();
        Long branchId = actionContext.getBranchId();
        log.info("[TCC Try] xid={}, branchId={}, userId={}, amount={}", xid, branchId, userId, amount);
        
        try {
            // 冻结金额:检查余额,冻结amount
            // 核心SQL:更新 frozen_amount = frozen_amount + amount
            // 条件:balance - frozen_amount >= amount(可用余额足够)
            int updated = accountMapper.freezeAmount(userId, amount);
            return updated > 0; // 返回true表示Try成功
        } catch (Exception e) {
            log.error("[TCC Try] 冻结失败: userId={}", userId, e);
            return false; // Try失败,TC会调用Cancel
        }
    }

    @Override
    public boolean confirm(BusinessActionContext actionContext) {
        String userId = actionContext.getActionContext("userId", String.class);
        BigDecimal amount = actionContext.getActionContext("amount", BigDecimal.class);
        log.info("[TCC Confirm] userId={}, amount={}", userId, amount);
        
        // 真正扣款:从balance扣除,从frozen_amount释放
        accountMapper.confirmDeduct(userId, amount);
        return true; // Confirm必须返回true,失败会重试
    }

    @Override
    public boolean cancel(BusinessActionContext actionContext) {
        String userId = actionContext.getActionContext("userId", String.class);
        BigDecimal amount = actionContext.getActionContext("amount", BigDecimal.class);
        log.info("[TCC Cancel] userId={}, amount={}", userId, amount);
        
        // 解冻:释放冻结的金额
        accountMapper.unfreezeAmount(userId, amount);
        return true; // Cancel必须返回true,失败会重试
    }
}

// ============ Mapper层 ============
@Mapper
public interface AccountMapper {
    
    /**
     * Try阶段:冻结金额
     * available_balance = balance - frozen_amount
     */
    @Update("UPDATE account SET frozen_amount = frozen_amount + #{amount} " +
            "WHERE user_id = #{userId} AND (balance - frozen_amount) >= #{amount}")
    int freezeAmount(@Param("userId") String userId, @Param("amount") BigDecimal amount);
    
    /**
     * Confirm阶段:确认扣款(真正从余额扣除)
     */
    @Update("UPDATE account SET " +
            "balance = balance - #{amount}, " +
            "frozen_amount = frozen_amount - #{amount} " +
            "WHERE user_id = #{userId}")
    int confirmDeduct(@Param("userId") String userId, @Param("amount") BigDecimal amount);
    
    /**
     * Cancel阶段:解冻金额(退还冻结)
     */
    @Update("UPDATE account SET frozen_amount = frozen_amount - #{amount} " +
            "WHERE user_id = #{userId}")
    int unfreezeAmount(@Param("userId") String userId, @Param("amount") BigDecimal amount);
}

// ============ 调用方:全局事务编排 ============
@Service
public class OrderTccServiceImpl {
    
    @Autowired
    private GlobalTransactionalScanner seataScanner; // 引入Seata自动配置
    
    @GlobalTransactional(rollbackFor = Exception.class)
    public void createOrderWithTcc(OrderDTO order) {
        // 账户服务使用TCC
        // 通过RPC框架(Dubbo/Feign)调用远程TCC服务
        // Seata的TCC接口支持通过RPC传播XID
        accountTccClient.tryReserve(order.getUserId(), order.getAmount());
        
        // 库存服务也使用TCC
        inventoryTccClient.tryReserve(order.getProductId(), order.getQuantity());
        
        // 本地订单创建
        orderMapper.insert(toOrder(order));
    }
}
TCC的幂等性陷阱:Confirm和Cancel可能被TC多次调用(网络问题导致超时重试),必须保证幂等。标准做法:使用BranchID作为幂等键,在数据库层面使用UPDATE WHERE条件行数=1来保证幂等。

五、Saga模式:长事务的补偿链

Saga模式是1987年提出的分布式事务模式,其核心思想是:将长事务拆成一系列本地事务,每个本地事务有对应的补偿操作。当某个步骤失败时,按照倒序依次执行补偿操作(逆向补偿)。

/**
 * Saga vs TCC vs AT 的对比:
 * 
 * Saga模式特点:
 * - 无锁设计:不获取全局锁,并发性能高
 * - 长事务友好:适合业务流程很长(10+步骤)的场景
 * - 无Undo Log:通过正向执行+逆向补偿来恢复
 * - 适用场景:业务流程系统(订单→支付→物流→库存→积分→通知)
 * 
 * Saga与TCC的核心区别:
 * - TCC有Try阶段(预检),Saga没有(直接执行)
 * - TCC通过冻结来预留,Saga通过补偿来恢复
 * - Saga适合"可补偿"的业务,TCC适合"可预留"的业务
 */

/**
 * Saga的两种补偿策略:
 * 
 * 1. 编排式Saga(Choreography)
 *    - 每个服务订阅上一个服务的事件,决定是否执行
 *    - 去中心化,适合少量服务
 *    - 缺点:服务间耦合增加,事务状态分散
 * 
 * 2. 集中式Saga(Orchestration)
 *    - 由Saga Orchestrator统一指挥各参与者的执行顺序
 *    - 中心化协调,事务状态清晰
 *    - Seata Saga采用这种模式
 */

// Seata Saga状态机配置示例(JSON/JSON5格式)
/**
 * Seata Saga的状态机设计器通过JSON描述事务流程:
 * 
 * 状态机 = 一系列状态(ServiceState/ChoiceState/SubStateMachine)
 * 状态之间通过流转(transitions)连接
 * 异常通过CompensationTrigger触发补偿
 */
{
  "Name": "orderSagaStateMachine",
  "Comment": "订单创建Saga长事务",
  "StartState": "CreateOrder",
  "States": {
    "CreateOrder": {
      "Type": "ServiceTask",    // 调用服务
      "ResourceName": "orderService:create",  // 服务标识
      "CompensationMethod": "orderService:cancel", // 补偿方法
      "Next": "PayOrder",
      "Status": {
        "Success": "SU",       // 成功则流转
        "Fail": "FA",          // 失败则补偿
        "Compensate": "CO"
      },
      "Input": [
        "$.#context.xid",      // 引用事务上下文中的xid
        "$.#context.userId",
        "$.#context.orderInfo"
      ]
    },
    "PayOrder": {
      "Type": "ServiceTask",
      "ResourceName": "paymentService:pay",
      "CompensationMethod": "paymentService:refund",
      "Next": "DeductInventory",
      "Catch": [               // 异常处理
        {
          "Next": "CompensateOrder",
          "Exceptions": ["*"]  // 捕获所有异常
        }
      ]
    },
    "DeductInventory": {
      "Type": "ServiceTask",
      "ResourceName": "inventoryService:deduct",
      "CompensationMethod": "inventoryService:restore",
      "Next": "GrantCredits"
    },
    "GrantCredits": {
      "Type": "ServiceTask",
      "ResourceName": "creditService:grant",
      "CompensationMethod": "creditService:revoke",
      "End": true              // 正常结束
    }
  }
}

六、Seata高可用部署与性能优化

生产环境中,Seata Server(TC)必须是集群部署,否则TC就是整个系统的单点。

6.1 TC集群部署架构

/**
 * Seata TC高可用部署模式:
 * 
 * 架构:TC集群(多节点)+ DB(全局事务会话存储)
 * 
 * 注册中心选型:
 * - Nacos(推荐):支持服务发现+配置中心,适合阿里技术栈
 * - Eureka:纯服务发现,适合Spring Cloud体系
 * - ZooKeeper:CP模型,数据一致性好
 * - Consul:HashiCorp生态,运维友好
 * 
 * 数据库选型:
 * - MySQL(推荐):Seata原生支持,事务数据量适中
 * - PostgreSQL:适合数据量大的场景,支持JSON类型
 * - Redis(实验性):超高性能,但需要接受数据丢失风险
 * 
 * 会话存储(Transaction Store):
 * - Seata在TC内存中维护全局事务状态
 * - 定期持久化到DB(默认5秒),用于故障恢复
 * - 极端情况下最多丢失最近5秒的事务(可配置)
 */

/**
 * Seata集群部署配置示例(registry.conf)
 */

// registry.conf - 使用Nacos作为注册/配置中心
registry {
  type = "nacos"   // 注册中心类型
  
  nacos {
    application = "seata-server"
    serverAddr = "nacos-headless.namespace:8848"  // Kubernetes内用Headless Service
    group = "SEATA_GROUP"
    namespace = "seata-namespace"
    cluster = "default"
    // Nacos集群模式下,serverAddr填写VIP或DNS
  }
}

config {
  type = "nacos"
  nacos {
    serverAddr = "nacos-headless.namespace:8848"
    namespace = "seata-namespace"
    group = "SEATA_GROUP"
    dataId = "seataServer"
  }
}

// ============ application.yml(TC服务端) ============
spring:
  application:
    name: seata-server
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://mysql-master:3306/seata?useUnicode=true&rewriteBatchedStatements=true
    username: seata
    password: ${MYSQL_PASSWORD}
    hikari:
      maximum-pool-size: 50   // TC数据库连接池不宜过大
      minimum-idle: 10

server:
  port: 8091

# TC核心性能配置
seata:
  server:
    # TC的Netty通信线程池(处理RM/TM的请求)
    # 并发高时需要调大,默认CPU核数*2
    max-concurrent-client-sessions: 16
    # Raft集群通信端口(TC间心跳/选主)
    raft-server-port: 9091
    # 会话信息持久化间隔(越小恢复越快,但DB压力大)
    store-session-port: 5000
    #undo_log表分片(高并发写入时避免单表热点)
    #在store.db横表模式下,表名支持sharding表达式
    
  # 客户端连接数限制(防止RM过多导致TC过载)
  server.max-connection-idle-timeout: 30
  server.max-connection-idle-times: 3

6.2 客户端(TM/RM)性能调优

/**
 * Seata客户端(业务服务)性能调优:
 */

// application.yml(业务服务)
seata:
  # 事务分组:客户端通过事务分组发现TC
  # 一个事务分组可以包含多个TC节点,实现客户端侧负载均衡
  tx-service-group: my-tx-group
  
  # 事务分组 → TC集群的映射关系
  # 格式:tx-service-group:cluster-name
  service:
    vgroup-mapping:
      my-tx-group: default  # TC集群名(在Nacos中注册的名字)
    
    # 全局事务超时时间(默认60秒)
    # 业务执行超过这个时间,TC会强制回滚
    # 设置太短会导致长查询被误杀,设置太长会导致锁持有时间过长
    global-branch-scheduling-try-times: 3  # 分支调用重试次数
    
  # RM端的undo_log表优化(AT模式必须)
  client:
    rm:
      # 异步删除undo_log(事务提交后不立即删除,减少主事务延迟)
      # 删除任务由后台线程异步执行
      async-commit: true
      # undo_log表保留时间(秒),用于异常排查
      undolog-delete-records-time: 604800  # 7天
      
      # 报告成功的全局事务是否上报TC(关闭可减少约50%的TC通信)
      # 适用场景:读多写少、全局事务成功率高的场景
      report-success-enable: false
      
      # 并发连接数(同一TC节点上的RM连接数)
      lock-retry-interval: 10ms   # 申请全局锁失败后的重试间隔
      lock-retry-times: 30        # 申请全局锁的最大重试次数

  # TM端
  tm:
    # 批量处理全局事务注册(减少TC通信次数)
    defer-client-handler-append: true

/**
 * 高并发场景下的AT模式性能问题排查:
 * 
 * 问题1:全局锁竞争激烈
 *   表现:多个全局事务同时修改同一行数据,lock-retry频繁
 *   解决:业务层面拆分热点数据(分桶、分片)
 * 
 * 问题2:undo_log写入成为瓶颈
 *   表现:数据库CPU高,undo_log表写入量大
 *   解决:undo_log异步批量写入、清理频率调高
 * 
 * 问题3:TC成为单点瓶颈
 *   表现:全局事务响应时间长,TC进程CPU高
 *   解决:TC水平扩容(集群),客户端vgroup-mapping配置多个节点
 */

七、实战:订单+库存+账户的分布式事务场景

完整的分布式事务场景:用户下单购买商品,

// ============ 场景描述 ============
/**
 * 业务场景:
 * 用户下单 -> 扣减账户余额(远程)-> 扣减库存(远程)-> 创建本地订单
 * 
 * 技术选型:Seata AT模式
 * - 账户服务:远程Feign调用,AT模式保护远程数据修改
 * - 库存服务:远程Feign调用,AT模式保护远程数据修改
 * - 订单服务:本地数据库,AT模式自动代理DataSource
 * 
 * 全局事务边界:createOrder() 方法
 * - TM向TC注册全局事务,获取XID
 * - XID通过Feign Header自动传播到 account-service 和 inventory-service
 * - 任一环节失败,全局回滚
 */

// ============ 订单服务 ============
@Service
public class OrderService {
    
    @Autowired
    private OrderMapper orderMapper;
    
    @Autowired
    private AccountClient accountClient;
    
    @Autowired
    private InventoryClient inventoryClient;

    /**
     * 创建订单(全局事务入口)
     * 
     * 事务流程:
     * 1. @GlobalTransactional 拦截,生成XID
     * 2. account-client 调用账户服务:TM注册分支事务,AT代理扣款SQL
     * 3. inventory-client 调用库存服务:TM注册分支事务,AT代理扣库存SQL
     * 4. orderMapper.insert() 本地事务:Seata DataSourceProxy代理INSERT
     * 5. 全部成功:TC异步清理全局锁
     * 6. 任一失败:TC通知所有RM回滚(使用Undo Log)
     */
    @GlobalTransactional(rollbackFor = Exception.class, name = "create-order-tx")
    public Order createOrder(CreateOrderRequest request) {
        // ① 扣减账户余额(跨服务)
        // Feign拦截器注入 X-TTID 请求头到HTTP Header
        // 账户服务Filter解析XID,注册分支事务到TC
        log.info("开始扣减账户余额: userId={}, amount={}", request.getUserId(), request.getTotalAmount());
        accountClient.deductBalance(request.getUserId(), request.getTotalAmount());
        
        // ② 扣减商品库存(跨服务)
        // 库存服务通过 Seata GlobalLock 机制保证行锁
        log.info("开始扣减库存: productId={}, quantity={}", request.getProductId(), request.getQuantity());
        inventoryClient.deductStock(request.getProductId(), request.getQuantity());
        
        // ③ 创建本地订单记录(本地事务)
        // Seata DataSourceProxy 自动拦截 INSERT SQL
        // 生成前镜像(空)-> 执行INSERT -> 生成后镜像 -> 写入undo_log
        Order order = new Order();
        order.setId(UUID.randomUUID().toString());
        order.setUserId(request.getUserId());
        order.setProductId(request.getProductId());
        order.setQuantity(request.getQuantity());
        order.setTotalAmount(request.getTotalAmount());
        order.setStatus("PAID");
        order.setCreateTime(LocalDateTime.now());
        orderMapper.insert(order);
        
        log.info("订单创建成功: orderId={}", order.getId());
        return order;
    }
}

// ============ 账户服务(远程,AT模式) ============
@RestController
@RequestMapping("/account")
public class AccountController {
    
    @Autowired
    private AccountService accountService;

    /**
     * 扣减余额接口
     * Seata的GlobalLock+AT模式保护
     * 
     * 与本地AT的区别:需要显式获取全局锁(SELECT FOR UPDATE)
     * 通过@GlobalTransactional或@GlobalLock注解声明
     */
    @GlobalTransactional(rollbackFor = Exception.class)
    @PostMapping("/deduct")
    public Result<Void> deductBalance(@RequestParam String userId, 
                                       @RequestParam BigDecimal amount) {
        accountService.deduct(userId, amount);
        return Result.success();
    }
}

@Service
public class AccountService {
    
    @Autowired
    private AccountMapper accountMapper;

    /**
     * 扣减余额(AT模式)
     * SQL:UPDATE account SET balance = balance - ? WHERE user_id = ? AND balance >= ?
     * 
     * Seata DataSourceProxy 拦截此SQL:
     * 1. 前镜像:SELECT balance FROM account WHERE user_id = ?(记录扣款前余额)
     * 2. 执行:UPDATE account SET balance = balance - ?
     * 3. 后镜像:SELECT balance FROM account WHERE user_id = ?(记录扣款后余额)
     * 4. 写入undo_log:{before: 1000, after: 900}
     * 
     * 全局提交:异步删除undo_log(不阻塞)
     * 全局回滚:反向执行UPDATE(从900恢复到1000)
     */
    public void deduct(String userId, BigDecimal amount) {
        int affected = accountMapper.deductBalance(userId, amount);
        if (affected == 0) {
            throw new BizException("余额不足或账户不存在: " + userId);
        }
    }
}

@Mapper
public interface AccountMapper {
    // AT模式自动拦截:生成前/后镜像 + 写入undo_log
    @Update("UPDATE account SET balance = balance - #{amount} " +
            "WHERE user_id = #{userId} AND balance >= #{amount}")
    int deductBalance(@Param("userId") String userId, @Param("amount") BigDecimal amount);
}

// ============ 库存服务(远程,AT模式) ============
@Service
public class InventoryService {
    
    @Autowired
    private InventoryMapper inventoryMapper;

    /**
     * 扣减库存(AT模式)
     * SQL:UPDATE inventory SET stock = stock - ? WHERE product_id = ? AND stock >= ?
     */
    public void deductStock(String productId, Integer quantity) {
        int affected = inventoryMapper.deductStock(productId, quantity);
        if (affected == 0) {
            throw new BizException("库存不足或商品不存在: " + productId);
        }
    }
}

@Mapper
public interface InventoryMapper {
    @Update("UPDATE inventory SET stock = stock - #{quantity} " +
            "WHERE product_id = #{productId} AND stock >= #{quantity}")
    int deductStock(@Param("productId") String productId, @Param("quantity") Integer quantity);
}
实战选型建议:账户扣款用TCC(资金不能有任何歧义),库存扣减用TCC(超卖零容忍),普通业务逻辑用AT(开发效率最高)。切忌教条主义——没有最好的模式,只有最适合业务的模式。

八、架构选型决策树

最后总结一下如何在不同场景下做技术选型:

/**
 * 分布式事务选型决策树:
 * 
 * Q1: 业务是否可以接受最终一致?
 *   是 → Q2
 *   否 → 必须强一致,参考以下
 * 
 * Q2: 涉及的系统类型是什么?
 *   纯数据库操作(MySQL/PostgreSQL)→ AT模式
 *   混合系统(DB+Redis+MQ+外部API)→ TCC模式
 *   长流程业务(10+步骤)→ Saga模式
 * 
 * Q3: 性能要求?
 *   高并发(万级TPS+)→ TCC > Saga > AT
 *   普通业务(千级TPS)→ AT模式(开发效率最高)
 *   金融级 → TCC + 多IDC容灾
 * 
 * Q4: 开发成本能接受多少?
 *   低(快速迭代)→ AT模式(无业务侵入)
 *   中等(核心链路)→ TCC模式(关键业务)
 *   高(长流程)→ Saga模式(业务流程引擎)
 */

// 模式对比总结
// 特性              | AT模式        | TCC模式        | Saga模式
// -----------------|--------------|---------------|------------
// 业务侵入性         | 无            | 高            | 中等
// 性能              | 中等          | 高            | 高
// 全局锁            | 有            | 无            | 无
// 回滚方式          | Undo Log     | 手动补偿       | 补偿服务
// 适用场景          | 数据库操作    | 任意系统       | 长流程
// 失败恢复          | 自动          | 手动幂等       | 状态机驱动
// 开发成本          | 低            | 高            | 中等
// 理论成熟度         | 成熟          | 成熟          | 成熟
// 生态              | Apache顶级项目| Apache顶级项目| Apache顶级项目