前言

在微服务架构下,跨服务下单、扣库存、拼团等核心电商链路,普遍面临分布式事务一致性难题。Seata 作为主流开源分布式事务框架,提供 AT、TCC、SAGA、XA 多种模式,其中 TCC 模式 凭借高性能、高可控的特性,成为电商高并发核心链路的首选方案。

本文将完整讲解 Seata TCC 核心原理、架构、落地代码、事务规范,重点详解官方 tcc_fence_log 防护表结构、幂等机制与使用规范,解决业务唯一键与事务表解耦问题,同时深入分析 TCC 与 AT 模式的隔离级别、脏读问题,并给出生产环境可直接落地的解决方案,全文代码可直接上线使用。

一、分布式事务基础:TCC 核心概念

1.1 TCC 定义

TCC 是Try-Confirm-Cancel三阶段补偿型柔性分布式事务,依靠三阶段动作实现跨服务数据一致性,Seata 框架负责全局事务编排、上下文传递、事务状态记录、幂等与异常兜底。

  • Try(资源检查 & 预占用):第一阶段,完成业务合法性校验、资源预锁定,不执行最终业务逻辑,仅临时占用资源。
  • Confirm(确认提交):所有分支 Try 执行成功后触发,执行真实业务提交,释放锁定状态。
  • Cancel(回滚撤销):任意分支 Try 失败、超时、异常时触发,执行补偿逻辑,回滚所有预占用资源。

1.2 TCC 与 AT 模式选型对比

Seata 两大主流模式特性与适用场景对比如下:

模式

特点

适用场景

AT

无代码侵入、框架自动生成 undo_log、自动回滚

普通增删改、低并发非核心业务

TCC

代码有侵入、手动实现三阶段补偿、性能更高、业务可控性强

高并发核心链路:下单、扣库存、拼团、支付、分账

选型建议:电商核心交易、库存、资金链路优先使用 TCC;普通查询、非核心管理接口使用 AT 或本地事务。

1.3 TCC 典型业务适用场景

  • 下单链路:创建订单 + 扣减库存 + 生成支付流水
  • 拼团链路:开团/参团、团单锁定 + 库存预扣
  • 逆向链路:订单退款、库存回补、资金退回
  • 跨服务抵扣:优惠券、积分、多商户分账

二、Seata TCC 整体架构与环境准备

2.1 架构角色

Seata 分布式事务包含三大核心角色,协同完成全局事务调度:

  • TC(Transaction Coordinator 事务协调器):Seata-Server 服务端,全局事务管理者,记录事务状态,下发 Confirm / Cancel 指令。
  • TM(Transaction Manager 事务管理器):业务应用入口,负责开启、关闭全局事务。
  • RM(Resource Manager 资源管理器):各个微服务分支,注册分支事务,执行 Try / Confirm / Cancel 三阶段逻辑。

2.2 标准执行流程

  • TM 开启全局事务,生成唯一全局事务XID,透传至所有参与微服务。
  • 调用各个分支 RM,执行第一阶段 Try 逻辑,完成资源预锁定。
  • 所有分支 Try 执行成功 → TC 通知所有分支执行 Confirm 提交。
  • 任意分支 Try 失败、超时、异常 → TC 通知所有分支执行 Cancel 回滚补偿。
  • Seata 全程依靠 tcc_fence_log 表记录事务状态,自动处理幂等、空回滚、悬挂三大问题。

2.3 项目依赖(Spring Boot 2.7.x)

引入 Seata 官方 Starter 依赖:

XML
<!-- Seata 依赖 -->
<dependency>
    <groupId>io.seata</groupId>
    <artifactId>seata-spring-boot-starter</artifactId>
    <version>1.6.1</version>
</dependency>

2.4 Seata Server 部署与客户端配置

  1. 部署 Seata-Server,推荐结合 Nacos 作为注册中心、配置中心,实现服务统一管理。
  2. 所有微服务客户端 application.yml 统一配置:

YAML
seata:
  application-id: business-service
  tx-service-group: business_tx_group
  service:
    vgroup-mapping:
      business_tx_group: default
    grouplist:
      default: 127.0.0.1:8091 # Seata TC 服务地址
  registry:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
      namespace: public
  config:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
      namespace: public

2.5 TCC 核心防护表:tcc_fence_log(官方原始标准结构)

2.5.1 基础说明

Seata 1.4+ 版本正式推出 TCC 防悬挂、空回滚、幂等防护机制,官方标准表名为 tcc_fence_log,早期文档简写为 tcc_fence,二者为同一张表。
该表核心字段、联合主键、状态枚举由框架硬编码固定,禁止修改,是 TCC 事务正常运行的基础。

关键说明:tcc_fence_log 与业务表主键、业务唯一键(如本文订单表 order_no完全解耦。事务幂等由 xid+branch_id 联合主键保证,业务数据唯一性由业务表自身索引保证,两套规则各司其职,互不干扰。

2.5.2 官方原始表结构(MySQL)

SQL
CREATE TABLE IF NOT EXISTS `tcc_fence_log`
(
    `xid`           VARCHAR(128)  NOT NULL COMMENT 'Seata 全局事务ID',
    `branch_id`     BIGINT        NOT NULL COMMENT 'Seata 分支事务ID',
    `action_name`   VARCHAR(64)   NOT NULL COMMENT 'TCC 动作名称,对应@TwoPhaseBusinessAction的name属性',
    `status`        TINYINT       NOT NULL COMMENT '事务状态:1-尝试中,2-已提交,3-已回滚,4-已悬挂',
    `gmt_create`    DATETIME(3)   NOT NULL COMMENT '记录创建时间(毫秒精度)',
    `gmt_modified`  DATETIME(3)   NOT NULL COMMENT '记录更新时间(毫秒精度)',
    -- 联合主键:框架核心依赖,绝对不可删除、修改、调整顺序
    PRIMARY KEY (`xid`, `branch_id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT 'Seata TCC 事务防护日志表';

2.5.3 核心字段与状态枚举

  1. 字段详解

字段名

类型

约束

作用

是否允许修改

xid

VARCHAR(128)

非空

Seata 全局事务唯一ID,由TC生成

禁止

branch_id

BIGINT

非空

单个全局事务下,各微服务分支唯一ID

禁止

action_name

VARCHAR(64)

非空

绑定TCC三阶段动作名称,区分不同TCC服务

禁止

status

TINYINT

非空

事务状态码,框架硬编码解析

禁止

gmt_create

DATETIME(3)

非空

记录创建时间,用于数据清理、问题排查

禁止

gmt_modified

DATETIME(3)

非空

记录最后更新时间

禁止

状态枚举(固定值,业务不可自定义)

  • 1:STATUS_TRIED 一阶段 Try 执行完成
  • 2:STATUS_COMMITTED 二阶段 Confirm 执行完成
  • 3:STATUS_ROLLBACKED 二阶段 Cancel 执行完成
  • 4:STATUS_SUSPENDED 空回滚/悬挂标记

2.5.4 合法扩展规则(不破坏原始结构)

原始核心结构严禁改动,仅可新增业务扩展字段、普通索引,用于链路追踪、问题排查,不影响框架运行:

SQL
-- 扩展版本(保留官方原始结构,仅追加字段/索引,电商推荐使用)
CREATE TABLE IF NOT EXISTS `tcc_fence_log`
(
    `xid`           VARCHAR(128)  NOT NULL COMMENT 'Seata 全局事务ID',
    `branch_id`     BIGINT        NOT NULL COMMENT 'Seata 分支事务ID',
    `action_name`   VARCHAR(64)   NOT NULL COMMENT 'TCC 动作名称',
    `status`        TINYINT       NOT NULL COMMENT '事务状态:1-尝试中,2-已提交,3-已回滚,4-已悬挂',
    `gmt_create`    DATETIME(3)   NOT NULL COMMENT '记录创建时间(毫秒精度)',
    `gmt_modified`  DATETIME(3)   NOT NULL COMMENT '记录更新时间(毫秒精度)',
    -- 自定义扩展字段(可选,用于关联订单号、业务单号)
    `biz_no`        VARCHAR(64)   DEFAULT NULL COMMENT '业务单号(订单号/团单号)',
    `biz_type`      TINYINT      DEFAULT NULL COMMENT '业务类型:1-下单 2-退款 3-拼团',
    -- 官方原始联合主键(保留不变)
    PRIMARY KEY (`xid`, `branch_id`),
    -- 自定义普通索引(可选,优化查询、清理效率)
    KEY `idx_gmt_modified` (`gmt_modified`),
    KEY `idx_biz_no` (`biz_no`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT 'Seata TCC 事务防护日志表';

2.5.5 配置启用防护能力

@TwoPhaseBusinessAction 注解中开启 TCC 防护,框架才会自动读写 tcc_fence_log

Java
@TwoPhaseBusinessAction(
        name = "stockTccAction",
        commitMethod = "stockConfirm",
        rollbackMethod = "stockCancel",
        useTCCFence = true,  // 开启TCC防护
        tccFenceConfig = @io.seata.rm.tcc.api.TccFence(
                tccFenceTable = "tcc_fence_log" // 指定防护表名
        )
)

2.5.6 运维规范

  • 每个参与 TCC 的微服务,独立创建 tcc_fence_log 表;
  • 该表属于日志表,建议配置定时任务,自动清理7~30天历史数据,避免数据膨胀;
  • 严禁修改原始字段、主键、状态值,仅可做字段追加。

三、Seata TCC 核心注解与编码规范

3.1 核心注解说明

  • @GlobalTransactional:TM 全局事务入口注解,标记方法为分布式事务起点。
  • @LocalTCC:标记当前接口/类为 TCC 分支服务。
  • @TwoPhaseBusinessAction:定义二阶段动作,绑定 Try、Confirm、Cancel 方法,内置 tcc_fence_log 自动防护逻辑。

3.2 三阶段强制编码规范

  • Try 阶段:仅做参数校验、资源预锁定,不执行最终业务;必须保证原子性。
  • Confirm 阶段:纯提交逻辑,禁止新增业务判断;必须保证原子性。
  • Cancel 阶段:纯补偿回滚逻辑,反向释放资源;必须保证原子性。
  • 禁止在 Try 阶段执行不可逆操作(消息推送、第三方支付调用等)。
  • 分层幂等设计:tcc_fence_log(xid+branch_id) 负责分布式事务重试幂等;业务表唯一索引(如 order_no)负责外部请求幂等,二者解耦。

四、TCC 完整实战代码(下单+扣库存场景)

参团下单 + 库存锁定典型场景为例,拆分两个微服务:订单服务库存服务
订单表 order_no 为唯一约束、非主键,与 tcc_fence_log 完全解耦,无需改造事务表。

4.1 库存服务实现

4.1.1 库存表设计

SQL
CREATE TABLE `shop_stock` (
  `id` bigint NOT NULL COMMENT '商品ID(主键)',
  `stock_num` int NOT NULL DEFAULT '0' COMMENT '真实可售库存',
  `frozen_num` int NOT NULL DEFAULT '0' COMMENT 'TCC冻结库存',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

4.1.2 库存 Mapper 接口

Java
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
import com.xxx.entity.Stock;

public interface StockMapper {

    @Select("select id,stock_num,frozen_num from shop_stock where id = #{goodsId}")
    Stock selectById(@Param("goodsId") Long goodsId);

    /** 锁定库存:增加冻结数量 */
    @Update("update shop_stock set frozen_num = frozen_num + #{num} where id = #{goodsId}")
    void lockStock(@Param("goodsId") Long goodsId, @Param("num") Integer num);

    /** 正式扣减库存:冻结转真实扣减 */
    @Update("update shop_stock set stock_num = stock_num - #{num}, frozen_num = frozen_num - #{num} where id = #{goodsId}")
    void realDeductStock(@Param("goodsId") Long goodsId, @Param("num") Integer num);

    /** 解锁库存:归还冻结数量 */
    @Update("update shop_stock set frozen_num = frozen_num - #{num} where id = #{goodsId}")
    void unLockStock(@Param("goodsId") Long goodsId, Integer num);
}

4.1.3 库存实体类

Java
public class Stock {
    private Long id;
    private Integer stockNum;
    private Integer frozenNum;

    // getter & setter
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public Integer getStockNum() {
        return stockNum;
    }

    public void setStockNum(Integer stockNum) {
        this.stockNum = stockNum;
    }

    public Integer getFrozenNum() {
        return frozenNum;
    }

    public void setFrozenNum(Integer frozenNum) {
        this.frozenNum = frozenNum;
    }
}

4.1.4 库存 TCC 接口定义

Java
import io.seata.rm.tcc.api.BusinessActionContext;
import io.seata.rm.tcc.api.BusinessActionContextParameter;
import io.seata.rm.tcc.api.LocalTCC;
import io.seata.rm.tcc.api.TwoPhaseBusinessAction;

@LocalTCC
public interface StockTccService {

    /**
     * 一阶段 Try:锁定/预扣库存
     * @param businessActionContext 事务上下文
     * @param goodsId 商品ID
     * @param num 购买数量
     */
    @TwoPhaseBusinessAction(
            name = "stockTccAction",
            commitMethod = "stockConfirm",
            rollbackMethod = "stockCancel",
            useTCCFence = true,
            tccFenceConfig = @io.seata.rm.tcc.api.TccFence(
                    tccFenceTable = "tcc_fence_log"
            )
    )
    boolean stockTry(BusinessActionContext businessActionContext,
                     @BusinessActionContextParameter(paramName = "goodsId") Long goodsId,
                     @BusinessActionContextParameter(paramName = "num") Integer num);

    /**
     * 二阶段 Confirm:正式扣减库存
     */
    boolean stockConfirm(BusinessActionContext businessActionContext);

    /**
     * 二阶段 Cancel:解锁库存、回滚
     */
    boolean stockCancel(BusinessActionContext businessActionContext);
}

4.1.5 库存 TCC 实现类

Java
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import io.seata.rm.tcc.api.BusinessActionContext;
import javax.annotation.Resource;

@Service
public class StockTccServiceImpl implements StockTccService {

    @Resource
    private StockMapper stockMapper;

    // ========== 一阶段 Try:预扣、锁定库存 ==========
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean stockTry(BusinessActionContext context, Long goodsId, Integer num) {
        // 1. 校验商品与库存合法性
        Stock stock = stockMapper.selectById(goodsId);
        if (stock == null || stock.getStockNum() < num) {
            throw new RuntimeException("商品库存不足");
        }
        // 2. 预扣库存:增加冻结数量,真实库存不变
        stockMapper.lockStock(goodsId, num);
        return true;
    }

    // ========== 二阶段 Confirm:正式扣减库存 ==========
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean stockConfirm(BusinessActionContext context) {
        Long goodsId = Long.valueOf(context.getActionContext("goodsId").toString());
        Integer num = Integer.valueOf(context.getActionContext("num").toString());
        // 正式扣减冻结库存,减少真实库存
        stockMapper.realDeductStock(goodsId, num);
        return true;
    }

    // ========== 二阶段 Cancel:解锁库存、回滚 ==========
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean stockCancel(BusinessActionContext context) {
        Long goodsId = Long.valueOf(context.getActionContext("goodsId").toString());
        Integer num = Integer.valueOf(context.getActionContext("num").toString());
        // 归还冻结库存,恢复数据
        stockMapper.unLockStock(goodsId, num);
        return true;
    }
}

4.2 订单服务实现

4.2.1 订单表设计

order_no 为唯一约束、非主键,业务唯一性由本表唯一索引保证,与 tcc_fence_log 无关联:

SQL
CREATE TABLE `order_info` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `order_no` varchar(64) NOT NULL COMMENT '订单编号(唯一约束,非主键)',
  `user_id` bigint NOT NULL COMMENT '用户ID',
  `status` tinyint NOT NULL DEFAULT '0' COMMENT '订单状态:0-预创建 1-正常订单 -1-已作废',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_order_no` (`order_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT '订单表';

4.2.2 订单实体类

Java
import java.util.Date;

public class Order {
    private Long id;
    private String orderNo;
    private Long userId;
    private Integer status;
    private Date createTime;

    // getter & setter
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getOrderNo() {
        return orderNo;
    }

    public void setOrderNo(String orderNo) {
        this.orderNo = orderNo;
    }

    public Long getUserId() {
        return userId;
    }

    public void setUserId(Long userId) {
        this.userId = userId;
    }

    public Integer getStatus() {
        return status;
    }

    public void setStatus(Integer status) {
        this.status = status;
    }

    public Date getCreateTime() {
        return createTime;
    }

    public void setCreateTime(Date createTime) {
        this.createTime = createTime;
    }
}

4.2.3 订单 Mapper 接口

Java
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Update;

public interface OrderMapper {

    @Insert("insert into order_info(order_no,user_id,status,create_time) values(#{orderNo},#{userId},#{status},now())")
    void insert(com.xxx.entity.Order order);

    @Update("update order_info set status = #{status} where order_no = #{orderNo}")
    void updateStatus(@Param("orderNo") String orderNo, @Param("status") Integer status);

    @Delete("delete from order_info where order_no = #{orderNo}")
    void deleteByOrderNo(@Param("orderNo") String orderNo);
}

4.2.4 订单 TCC 接口定义

Java
import io.seata.rm.tcc.api.BusinessActionContext;
import io.seata.rm.tcc.api.BusinessActionContextParameter;
import io.seata.rm.tcc.api.LocalTCC;
import io.seata.rm.tcc.api.TwoPhaseBusinessAction;

@LocalTCC
public interface OrderTccService {

    @TwoPhaseBusinessAction(
            name = "orderTccAction",
            commitMethod = "orderConfirm",
            rollbackMethod = "orderCancel",
            useTCCFence = true,
            tccFenceConfig = @io.seata.rm.tcc.api.TccFence(tccFenceTable = "tcc_fence_log")
    )
    boolean orderTry(BusinessActionContext context,
                     @BusinessActionContextParameter("orderNo") String orderNo,
                     @BusinessActionContextParameter("userId") Long userId);

    boolean orderConfirm(BusinessActionContext context);

    boolean orderCancel(BusinessActionContext context);
}

4.2.5 订单 TCC 实现类

Java
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import io.seata.rm.tcc.api.BusinessActionContext;
import javax.annotation.Resource;

@Service
public class OrderTccServiceImpl implements OrderTccService {

    @Resource
    private OrderMapper orderMapper;

    // Try:创建预订单(临时状态)
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean orderTry(BusinessActionContext context, String orderNo, Long userId) {
        // 创建预订单,状态标记为 0-待确认
        Order order = new Order();
        order.setOrderNo(orderNo);
        order.setUserId(userId);
        order.setStatus(0);
        order.setCreateTime(new Date());
        orderMapper.insert(order);
        return true;
    }

    // Confirm:订单转为正式有效状态
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean orderConfirm(BusinessActionContext context) {
        String orderNo = context.getActionContext("orderNo").toString();
        // 更新订单状态为 1-正常有效订单
        orderMapper.updateStatus(orderNo, 1);
        return true;
    }

    // Cancel:作废/删除预订单,回滚
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean orderCancel(BusinessActionContext context) {
        String orderNo = context.getActionContext("orderNo").toString();
        // 删除预订单 或 更新为作废状态
        orderMapper.deleteByOrderNo(orderNo);
        return true;
    }
}

4.3 全局事务入口(TM 层)

Java
import io.seata.spring.annotation.GlobalTransactional;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.UUID;

@RestController
@RequestMapping("/group")
public class GroupController {

    @Resource
    private OrderTccService orderTccService;
    @Resource
    private StockTccService stockTccService;

    /**
     * 参团下单 全局事务入口
     */
    @GlobalTransactional(rollbackFor = Exception.class)
    @PostMapping("/join")
    public String joinGroup(Long goodsId, Integer num, Long userId) {
        String orderNo = UUID.randomUUID().toString().replace("-", "");
        // 执行订单分支 Try
        orderTccService.orderTry(null, orderNo, userId);
        // 执行库存分支 Try
        stockTccService.stockTry(null, goodsId, num);
        // 所有Try成功,Seata自动执行Confirm;出现异常自动执行Cancel
        return "参团下单成功";
    }
}

 Seata TC 全局事务表

(global_table/branch_table/lock_table) 这三张是 Seata-Server(TC 事务协调器)的全局事务存储表,全局仅建一套,所有微服务、所有数据库共用,不需要在业务库重复创建。

 AT 模式的 undo_log 表 

仅 AT 模式需要每个业务库单独创建,TCC 完全不依赖 undo_log。

TCC 专属防护表

 tcc_fence_log 所有参与 TCC 分支的业务数据库,都必须单独创建该表。 原理:tcc_fence_log 是 RM(资源管理器)侧本地表,用于当前微服务 / 当前数据源的分支事务幂等、空回滚、悬挂防护,每个数据库实例 / 业务库独立维护自身分支事务状态,不能跨库共用。

五、补充:双重幂等保障(order_no 非主键场景)

当前架构存在两层独立幂等防护,分工明确,互不干扰:

  • 业务层幂等
    订单表通过 uk_order_no 唯一索引,保证同一订单号不会重复创建订单,拦截重复下单请求。
  • 分布式事务幂等
    tcc_fence_log 通过 xid+branch_id 联合主键,保证分布式事务三阶段方法不重复执行

组合异常场景处理:

  • 同一事务多次重试:tcc_fence_log 拦截,不会重复执行业务逻辑;
  • 不同事务、相同订单号:数据库唯一索引拦截,拒绝插入;
  • 空回滚/悬挂:由防护表自动处理,和订单号无关。

六、拓展:拼团、退款场景 TCC 逻辑补充

6.1 拼团业务扩展

在订单+库存基础上,新增团单分支 TCC,逻辑保持统一:

  • Try:锁定团单人数、标记团单为预占用状态。
  • Confirm:正式累加参团人数,判断是否满足成团条件。
  • Cancel:恢复团单人数,取消预占用状态。

6.2 订单退款反向 TCC 流程

退款属于逆向分布式事务,三阶段逻辑调整为:

  • Try:冻结退款金额、锁定订单状态,防止重复退款。
  • Confirm:执行真实退款、库存回补、更新订单最终状态。
  • Cancel:解除资金与订单冻结,恢复原有数据。

七、TCC 经典问题解决方案(空回滚、悬挂、幂等)

7.1 防护机制整体说明

本项目已使用 Seata 官方标准 tcc_fence_log 防护表,并在注解中开启 useTCCFence = true。该表基于 xid + branch_id 联合主键,由框架自动实现 分布式事务重试拦截、幂等控制、空回滚、悬挂 三大能力。

7.2 幂等性重点说明

在已正确部署并配置 tcc_fence_log 的前提下:
针对 Seata 内部事务重试场景,Confirm、Cancel、Try 方法无需手动编写幂等逻辑

7.2.1 底层执行逻辑

  • Try 阶段
    框架执行前查询防护表,无记录则插入状态为1(尝试中)的数据,再执行业务逻辑;若因网络、服务重试重复调用,主键冲突会直接拦截,不会重复执行业务代码。
  • Confirm 阶段
    框架查询事务状态,状态为1则更新为2(已提交)并执行提交逻辑;若状态已是2,直接跳过业务代码,天然幂等。
  • Cancel 阶段
    框架查询事务状态,状态为1则更新为3(已回滚)并执行回滚逻辑;若状态为34(悬挂),直接跳过,避免重复回滚。

7.2.2 框架无法覆盖的场景

tcc_fence_log 仅拦截同一个全局事务的重试操作,以下场景框架不生效,必须依靠业务自身规则保障:

  • 外部重复请求:前端、网关重复提交,会生成全新全局XID,防护表无法拦截,依靠订单表uk_order_no唯一索引拦截重复订单。
  • 跨事务重复操作:同一订单在多个不同全局事务中被操作(如重复发起退款),属于业务重复操作,不属于事务重试,框架不处理。
  • 方法内部原子性:框架只防重复执行,不保证方法内逻辑原子,因此本地事务注解不可省略。要求:Try/Confirm/Cancel 方法必须添加本地事务 @Transactional(rollbackFor = Exception.class),保证单阶段内部数据一致性。

7.3 空回滚、悬挂处理

  • 空回滚:分支未执行 Try,直接收到 Cancel 指令 → 防护表无对应记录,直接跳过回滚逻辑。
  • 悬挂:Cancel 先执行,后续延迟收到 Try 请求 → 表中标记为已回滚,拦截后续 Try 执行。

7.4 编码规范与误区纠正

7.4.1 无需编写的代码

不要在 Try/Confirm/Cancel 内部手动增加状态查询、幂等标记、重复判断,该部分由 tcc_fence_log 全权接管。

7.4.2 必须遵守的硬性规则

  1. 注解必须完整开启防护并指定表名:

Java
@TwoPhaseBusinessAction(
        name = "orderTccAction",
        commitMethod = "orderConfirm",
        rollbackMethod = "orderCancel",
        useTCCFence = true,
        tccFenceConfig = @io.seata.rm.tcc.api.TccFence(tccFenceTable = "tcc_fence_log")
)

  • 三方法必须保留 @Transactional(rollbackFor = Exception.class) 本地事务,保证单阶段数据原子性;
  • 严格遵循 TCC 规范:Try 只做资源预占,Confirm/Cancel 仅做纯提交/回滚,不掺杂复杂业务判断;
  • 业务唯一性依旧依靠数据库唯一索引兜底。

7.4.3 常见误区

误区:有了 tcc_fence_log,可以不加本地事务

❌ 错误:框架只管 “重复执行”,不管 “执行失败回滚”,本地事务是数据原子性基础,必须保留。

误区:所有重复请求都能被拦截

❌ 错误:仅拦截同一个分布式事务的重试;全新事务、外部重复请求,需要业务层处理。

误区:可以简化 Confirm/Cancel 逻辑

❌ 错误:依旧要遵循 TCC 三阶段设计规范,保证补偿逻辑可正常回滚 / 提交。

7.5 总结

  • 存在 tcc_fence_log 并正确配置后,分布式事务层面的幂等由框架实现,Confirm、Cancel 无需手动做幂等
  • 外部重复请求、跨事务操作,仍需业务索引/状态机兜底;
  • 本地事务、TCC 编码规范为强制要求,不可简化。

八、Seata TCC 与 AT 隔离级别、脏读分析

8.1 前置基础概念

  • 数据库原生隔离级别:MySQL InnoDB 默认 可重复读(RR),仅控制单库本地事务。
  • Seata 定位:应用层分布式事务框架,不会修改数据库原生隔离级别
  • 脏数据/脏读:读取到未提交、后续会被回滚的中间临时数据。

8.2 AT 模式隔离与脏读分析

8.2.1 隔离能力

底层沿用 MySQL 本地隔离级别,Seata 额外实现全局读隔离,依靠 SELECT FOR UPDATE 全局锁 + undo_log 快照实现控制。

8.2.2 脏读结论

正常使用下 AT 模式不会产生脏读, 另外查询接口时采用全局锁取读。AT此时视为读已提交。

8.2.3 风险边界

绕过 Seata 代理、直连数据库、跨实例查询,隔离机制失效。

8.3 TCC 模式隔离与脏读分析

8.3.1 隔离能力

TCC 无框架层面全局隔离机制,隔离能力依赖 MySQL 本地隔离 + 业务层状态设计。

8.3.2 脏读结论

TCC 天然存在脏读风险:Try 阶段数据落地,Try ~ Confirm/Cancel 区间会读到中间态数据。

8.3.3 典型脏读场景

  • 正常流程:Try → Confirm,数据转为正式,无影响。
  • 回滚流程:Try 落库后触发 Cancel,中间窗口产生脏读。
  • 超时回滚:事务挂起,脏数据存活时间变长。

8.4 TCC 下三类数据异常

  • 脏读:读取到待回滚的中间数据(最常见)。
  • 不可重复读:同一查询先后读到中间态、正式态/回滚态数据。
  • 幻读:批量查询时,临时订单新增/删除,导致查询条数前后不一致。

8.5 业务影响

  • 库存展示虚假缺货;
  • 临时订单/团单被统计、推送;
  • 报表读取临时数据,导致对账失真。

九、生产级 TCC 脏读解决方案

方案1:业务状态隔离(首选,成本最低)

  • 订单查询强制过滤 status != 0(过滤预创建临时订单);
  • 可售库存计算公式:可售库存 = 真实库存 - 冻结库存
  • 团单列表过滤临时状态。

方案2:行级悲观锁

Try 阶段使用 SELECT ... FOR UPDATE 加行锁,控制并发写,缓解数据混乱。

方案3:缩短中间态生命周期

合理设置 Seata 全局事务超时时间,优化接口性能,压缩中间数据存活时间。

方案4:分布式全局锁

使用 Redisson 实现串行化访问,隔离性最强;高并发、秒杀场景慎用

Java
// 伪代码示例
// RLock lock = redissonClient.getLock("lock:goods:" + goodsId);
// lock.lock();
// try {
//     // 执行查询 + Try 逻辑
// } finally {
//     lock.unlock();
// }

方案5:查询口径分层

  • 前台业务查询:仅查询有效正式数据;
  • 后台运维查询:可查询全量数据;
  • 定时报表:安排在业务低峰期执行。

十、AT 与 TCC 模式综合对比

特性

Seata AT 模式

Seata TCC 模式

框架全局隔离

支持,自带全局锁+快照防脏读

不支持,依赖业务设计

脏读风险

正常使用无脏读

天然存在脏读风险

中间态数据

无对外可见中间态

Try 生成中间态数据

代码侵入

极低

性能

中等

适用场景

普通业务、低并发、高隔离

核心交易、高并发、可接受短期中间态

十一、生产环境部署、调优与运维规范

11.1 Seata 服务端部署

  • 生产环境集群部署 Seata TC,保证高可用。
  • 事务日志持久化至 MySQL,防止服务宕机丢失事务状态。

11.2 性能调优

  • 链路拆分:核心交易使用 TCC,非核心业务使用 AT/本地事务。
  • 控制 TCC 分支数量:分支越多,事务耗时越长,尽量精简调用链路。
  • 合理配置 default_global_transaction_timeout 全局事务超时时间。

11.3 运维重点

  • 接入 Seata 监控面板,监控全局事务、分支事务成功率、耗时。
  • 定时清理 tcc_fence_log 历史数据,避免表数据膨胀。
  • 监控异常事务,及时告警处理。

十二、TCC 模式优缺点总结

优点

  • 不依赖数据库 undo_log,适配分库分表、MySQL 集群场景。
  • 补偿逻辑完全自定义,业务可控性极强。
  • 纯内存+数据库逻辑,性能优异,支撑电商高并发核心链路。
  • 兼容性广,不绑定底层存储。

缺点

  • 代码侵入性强,每一个业务场景都需要手动实现 Try/Confirm/Cancel。
  • 开发工作量翻倍,对开发规范要求极高。
  • 需要额外维护 tcc_fence_log 事务表,增加运维成本。
  • 自带中间态数据,需要业务层适配解决脏读问题。

十三、最终落地总结

  • 选型建议:微服务电商核心链路(下单、拼团、支付、退款)优先使用 TCC;普通查询、管理接口使用 AT 模式。
  • 表规范:严格使用 Seata 官方 tcc_fence_log 原始结构,仅可追加业务字段,禁止修改核心字段、主键与状态值。
  • 幂等设计:部署 tcc_fence_log 并开启防护后,Try/Confirm/Cancel 无需手动实现事务级幂等;外部重复请求依靠业务唯一索引、状态机兜底。
  • 隔离方案:TCC 不要依赖框架做隔离,以业务状态过滤为核心,搭配行锁、分层查询解决脏读问题。
  • 运维重点:做好事务监控与 tcc_fence_log 数据清理,保障系统长期稳定运行。
Logo

电商企业物流数字化转型必备!快递鸟 API 接口,72 小时快速完成物流系统集成。全流程实战1V1指导,营造开放的API技术生态圈。

更多推荐