本文将详细介绍如何利用 Seata 框架实现 TCC(Try-Confirm-Cancel)事务模式,以解决经典的电商下单场景中,订单服务与库存服务之间的数据一致性问题。

什么是TCC模式?
TCC(Try Confirm Cancel)是一种在应用层面侵入业务的两阶段提交方案,属于柔性事务范畴。其核心思想是:针对每一个业务操作,都需要配套注册一个确认操作和一个补偿(撤销)操作。
TCC事务的执行分为两个明确的阶段:
- 第一阶段 - Try(尝试): 对业务系统进行检查并预留必要的资源(通常涉及锁定或预扣)。
- 第二阶段: 根据第一阶段的结果,决定执行 Confirm 还是 Cancel。
- Confirm(确认): 执行业务确认操作,正式提交并释放预留资源。
- Cancel(取消): 执行补偿操作,取消预留的资源。

为了更好地理解,我们以一个简化的电商下单为例:用户下单购买商品,涉及“扣减库存”和“创建订单”两个步骤,这两个服务部署在不同的节点上,构成了一个典型的 分布式系统 调用场景。
假设商品A库存为100件,用户购买2件。在TCC模式下,这个过程会被拆解。
① Try 阶段
此阶段是一个初步操作,需要完成以下任务:
- 完成所有业务一致性检查。
- 预留必须的业务资源,实现准隔离性。
- 尝试执行业务。
在下单场景中,Try阶段会检查并冻结用户购买数量的库存(例如,库存从100变为98,冻结库存+2),同时创建一个状态为“待确认”的订单。

② Confirm / Cancel 阶段
此阶段根据Try阶段所有参与方是否全部正常执行,来决定最终走向。
Confirm操作: 当Try阶段所有服务都执行成功,则进入Confirm阶段,执行真正的业务提交。在我们的例子中,会释放Try阶段冻结的库存(冻结库存-2),并将订单状态更新为“成功”。Confirm操作必须保证幂等性。

Cancel操作: 当Try阶段有任何服务执行失败,则进入Cancel阶段,执行回滚补偿。在我们的例子中,会恢复被冻结的库存(库存+2,冻结库存-2),并将订单状态更新为“取消”。Cancel操作同样必须保证幂等性。

以上是TCC模式的基本概念,它为解决复杂的跨服务事务提供了一种清晰的思路。
TCC模式的三种类型?
在实际生产实践中,基于经典的TCC模型衍生出了三种常见的解决方案。
1、通用型 TCC 解决方案
这是最经典的TCC实现,所有从业务服务(如库存服务、账户服务)都同步参与主业务服务(如订单服务)的决策。

适用场景: 由于从业务服务是同步调用,其执行结果直接影响主业务决策,因此适用于执行时间确定且较短的业务。例如,电商核心的下单流程,需要订单、库存、账户三个服务强一致,要么全成功,要么全失败。

2、异步确保型 TCC 解决方案
这种方案的直接从业务服务是一个“可靠消息服务”,真正的业务逻辑(如邮件服务)通过消费消息来异步执行,从而实现解耦。

可靠消息服务自身需要实现TCC接口:Try阶段预存储消息,Confirm阶段真正投递,Cancel阶段删除消息。业务服务只需要监听消息队列,无需进行TCC改造。
适用场景: 适用于对最终一致性时间敏感度较低、且业务结果不影响主业务决策的被动型业务。例如,用户注册成功后发送欢迎邮件。

3、补偿型 TCC 解决方案
这种方案结构与通用型类似,但从业务服务只需要提供Do(执行)和Compensate(补偿)两个接口,而不是标准的三个。

Do接口直接执行完整业务逻辑,业务结果立即可见;Compensate接口用于业务补偿。其优点是对原有业务逻辑改造量小,但缺点是一阶段就执行业务,隔离性差,补偿可能失败。
适用场景: 适用于并发冲突较少或需要与外部系统交互,且执行结果会影响主业务决策的场景。
以上部分类型分析参考自 Seata 官方博客。
TCC事务模式的落地实现
Seata 不仅支持AT模式,同样对TCC模式提供了完善的支持。下面我们以电商下单场景为例,整合Seata的TCC模式。
1、演示场景
我们简化场景,仅包含订单服务和库存服务。业务逻辑如下:
- 客户端调用下单接口。
- 订单服务调用库存服务,冻结库存。
- 订单服务创建订单。
- 请求完成。
其中,订单服务是主业务服务(事务发起方),库存服务是从业务服务(事务参与方)。
若使用Seata的AT模式,伪代码如下:
@GlobalTransactional
public Result<Void> createOrder(Long productId, Long num, .....){
//1、扣库存
reduceStorage();
//2、创建订单
saveOrder();
}
但AT模式在特定场景下存在局限性,如锁定资源时间长、无法很好解决非 数据库 操作的事务等。TCC模式通过将业务拆分为两阶段,可以缩短资源锁定时间,提升性能。
TCC模式拆分思路:
1、一阶段的Try操作
预留资源。例如,在库存表中冻结相应数量的商品。
@Transactional
public boolean try(){
//冻结库存
frozenStorage();
//生成订单,状态为待确认
saveOrder();
}
注意:@Transactional开启了本地事务。若try方法内出现异常,本地事务回滚,Seata会触发全局事务回滚,执行第二阶段的cancel操作。
2、二阶段的Confirm操作
在try成功后被调用,执行最终提交。
@Transactional
public boolean confirm(){
//释放掉try操作预留的(冻结)库存
cleanFrozen();
//修改订单状态为已完成
updateOrder();
return true;
}
注意:若返回false,Seata会遵循TCC规范进行重试,直到成功。
3、二阶段的Cancel操作
在try阶段失败后被调用,执行回滚补偿。
@Transactional
public boolean cancel(){
//恢复try操作预留的(冻结)库存
rollbackFrozen();
//修改订单状态为取消
delOrder();
return true;
}
注意:若返回false,Seata会进行重试。
2、TCC事务模型的三个异常
实现TCC模型必须妥善处理以下三个经典异常:
1、空回滚
定义: 在没有调用try方法或try方法未成功执行的情况下,调用了cancel方法。
解决方案: cancel方法在执行前,需要能感知到try方法是否已执行成功。通常通过检查一张事务状态记录表来实现。
2、幂等性
定义: 由于网络等原因,confirm或cancel接口可能会被重复调用。
解决方案: confirm和cancel操作必须实现幂等性,确保多次调用与一次调用的效果相同。可通过幂等令牌或状态记录来防重。
3、悬挂
定义: 因网络拥堵,cancel请求可能早于try请求到达服务端。导致先执行了cancel,后执行了try,使得try预留的资源永远无法被确认或取消。
解决方案: try方法在执行前,需要检查cancel是否已经执行过。同样,cancel方法执行后需记录状态。
总结
针对上述三个异常,一个常见的落地解决方案是维护一张事务状态表,记录每个全局事务(XID)的执行阶段(try, confirm, cancel)。通过查询此表,可以:
- 防悬挂: try前检查是否有cancel记录。
- 防空回滚: cancel前检查是否有成功的try记录。
- 防幂等: confirm/cancel前检查是否已执行过。
Seata整合TCC实现
下面我们进入实战环节,展示如何用Seata实现TCC模式。案例源码结构如下:

项目所需的相关配置文件与SQL脚本如下:

1、TCC接口定义
在订单服务中,我们首先需要定义一个TCC接口。这里使用 Spring Boot 框架进行演示。
@LocalTCC
public interface OrderTccService {
// 一阶段的try方法
@TwoPhaseBusinessAction(name = "orderTcc", commitMethod = "commit", rollbackMethod = "rollback")
boolean tryCreate(BusinessActionContext businessActionContext,
@BusinessActionContextParameter(paramName = "userId") String userId,
@BusinessActionContextParameter(paramName = "orderId") String orderId,
@BusinessActionContextParameter(paramName = "productId") Long productId,
@BusinessActionContextParameter(paramName = "num") Long num);
// 二阶段的confirm方法
boolean commit(BusinessActionContext businessActionContext);
// 二阶段的cancel方法
boolean rollback(BusinessActionContext businessActionContext);
}
代码解析:
@LocalTCC: 声明该接口为TCC模式。
@TwoPhaseBusinessAction: 标注在try方法上,定义TCC事务名称,并指定confirm和cancel方法名。
@BusinessActionContextParameter: 将参数绑定到上下文,以便在二阶段方法中获取。
2、TCC接口实现
1. try方法实现
@Transactional
@Override
public boolean tryCreate(BusinessActionContext businessActionContext, String userId, String orderId, Long productId, Long num) {
log.info("---------开始第一阶段的事务,事务XID:{}---------", businessActionContext.getXid());
// ① 防止悬挂:查询事务记录,若已进入cancel则抛出异常
Result<TransactionalRecord> result1 = transactionalRecordFeign.getByXid(businessActionContext.getXid());
TransactionalRecord record = result1.getData();
if (Objects.nonNull(record) && Integer.valueOf(3).equals(record.getStatus()))
throw new RuntimeException("已经进入了TCC第二阶段的cancel阶段,不允许try阶段!");
// ② 冻结库存(调用库存服务)
Result<Void> result2 = storageFeign.frozen(productId, num);
// ... 检查结果
// ③ 生成订单(状态为待确认)
Order order = Order.builder()
.orderId(orderId)
.userId(userId)
.productId(productId)
.num(num)
.status(2) // 待确认状态
.createTime(new Date())
.build();
orderRepository.save(order);
// ④ 保证幂等:在工具类中记录标记
IdempotentUtil.add(getClass(), businessActionContext.getXid(), System.currentTimeMillis());
return true;
}
2. confirm方法实现
@Transactional
@Override
public boolean commit(BusinessActionContext businessActionContext) {
// ① 校验幂等,防止多次提交
if (Objects.isNull(IdempotentUtil.get(getClass(), businessActionContext.getXid()))) {
return true; // 已执行过,直接返回成功
}
log.info("---------开始第二阶段的commit事务,事务XID:{}", businessActionContext.getXid());
// ② 从上下文中获取一阶段参数
long productId = Long.parseLong(businessActionContext.getActionContext("productId").toString());
long num = Long.parseLong(businessActionContext.getActionContext("num").toString());
String orderId = businessActionContext.getActionContext("orderId").toString();
// ③ 释放冻结的库存
Result<Void> result = storageFeign.cleanFrozen(productId, num);
// ... 检查结果
// ④ 修改订单状态为已完成
Order order = Order.builder()
.orderId(orderId)
.status(3) // 已完成
.build();
orderRepository.save(order);
// ⑤ 提交成功,移出幂等标记
IdempotentUtil.remove(getClass(), businessActionContext.getXid());
return true;
}
3. cancel方法实现
@Transactional
@Override
public boolean rollback(BusinessActionContext businessActionContext) {
// ① 防止悬挂:插入一条cancel状态的事务记录
TransactionalRecord record = TransactionalRecord.builder()
.xid(businessActionContext.getXid())
.status(3) // cancel阶段
.build();
Result<TransactionalRecord> result1 = transactionalRecordFeign.add(record); // 需保证幂等
// ... 检查结果
// ② 校验幂等与空回滚:检查try阶段是否成功过
if (Objects.isNull(IdempotentUtil.get(getClass(), businessActionContext.getXid()))) {
return true; // 空回滚或已执行过
}
log.info("---------开始第二阶段的rollback事务,事务XID: {}", businessActionContext.getXid());
// ③ 获取一阶段参数
Long productId = Long.parseLong(businessActionContext.get("productId").toString());
Long num = Long.parseLong(businessActionContext.get("num").toString());
// ④ 恢复冻结的库存
Result<Void> result = storageFeign.rollBackFrozen(productId, num);
// ... 检查结果
// ⑤ 删除/取消订单(逻辑删除)
Order order = Order.builder()
.orderId(orderId)
.status(5) // 已取消
.build();
orderRepository.save(order);
// ⑥ 回滚成功,移出幂等标记
IdempotentUtil.remove(getClass(), businessActionContext.getXid());
return true;
}
3、如何防止TCC模型的三个异常?
本案例采用了混合方案来应对三大异常:
1. 幂等性与空回滚
使用一个内存中的幂等工具类(生产环境建议使用Redis等集中式存储)。
public class IdempotentUtil {
private static Table<Class<?>, String, Long> map = HashBasedTable.create();
public static void add(Class<?> clazz, String xid, Long marker) { map.put(clazz, xid, marker); }
public static Long get(Class<?> clazz, String xid) { return map.get(clazz, xid); }
public static void remove(Class<?> clazz, String xid) { map.remove(clazz, xid); }
}
- 思路: try成功时
add标记;confirm/cancel开始时get标记,若为空则说明是空回滚或已执行(幂等);confirm/cancel成功时remove标记。
2. 悬挂
依靠一张独立的事务记录表(transactional_record)。
CREATE TABLE `transactional_record` (
`id` bigint(11) NOT NULL AUTO_INCREMENT,
`xid` varchar(100) NOT NULL,
`status` int(1) DEFAULT NULL COMMENT '1. try 2 commit 3 cancel ',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
- 思路: cancel方法先插入一条
status=3的记录;try方法在执行前,查询此表检查对应XID是否已有cancel记录,若有则抛出异常防止悬挂。
4、创建订单的业务方法(事务发起方)
最后,我们需要一个事务发起方,使用@GlobalTransactional开启全局分布式事务。
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private OrderTccService orderTccService;
@GlobalTransactional
@Override
public void create(String userId, Long productId, Long num) {
// 生成订单ID,实际应用应使用分布式发号器
orderTccService.tryCreate(null, userId, UUID.randomUUID().toString(), productId, num);
}
}
5、其他配置与总结
除了上述核心代码,一个完整的Seata TCC项目还需要配置:
- Seata Server 与 Client 的配置(与AT模式类似)。
- Nacos 作为配置与注册中心。
- 各服务间的Feign调用。
- 关键: 正确配置
tx-service-group 事务组。
TCC事务模型给予了开发者更大的灵活性,但也带来了业务侵入性和需要手动处理异常情况的复杂度。通过本文的案例,我们可以看到,借助Seata框架和合理的防异常设计,能够相对清晰地实现一个健壮的TCC事务。
希望这篇关于Seata整合TCC的实战解析能帮助你更好地理解分布式事务。如果你想了解更多 后端 架构或微服务相关的技术讨论,欢迎来到技术社区交流。