前言
在微服务架构下,跨服务下单、扣库存、拼团等核心电商链路,普遍面临分布式事务一致性难题。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 部署与客户端配置
- 部署 Seata-Server,推荐结合 Nacos 作为注册中心、配置中心,实现服务统一管理。
- 所有微服务客户端 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 核心字段与状态枚举
- 字段详解
|
字段名 |
类型 |
约束 |
作用 |
是否允许修改 |
|
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,逻辑保持统一:
- Confirm:正式累加参团人数,判断是否满足成团条件。
6.2 订单退款反向 TCC 流程
退款属于逆向分布式事务,三阶段逻辑调整为:
- Try:冻结退款金额、锁定订单状态,防止重复退款。
- Confirm:执行真实退款、库存回补、更新订单最终状态。
七、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(已回滚)并执行回滚逻辑;若状态为3或4(悬挂),直接跳过,避免重复回滚。
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 必须遵守的硬性规则
- 注解必须完整开启防护并指定表名:
|
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 数据清理,保障系统长期稳定运行。
所有评论(0)