在分布式 TCC 模式中,xid(全局事务 ID)和 branchId(分支事务 ID)是 Seata 等框架识别分布式事务的核心标识,而业务参数(如订单、账户对象)的传递 / 获取,需结合框架的 Context 上下文机制实现。
saga是通过反射拿到参数给compensation 回滚方法的
为什么TCC是从上下文取的,原因:
1.Confirm/Cancel 由 Seata TC 直接调用(而非 AOP 拦截),无法捕获 Try 阶段的入参快照
2.TCC 的 Confirm/Cancel 不是像 SAGA 那样 "和正向方法静态绑定",Try/Confirm/Cancel 是 TCC 接口的三个独立方法
3.TCC 的 BranchContext 本质是 "分支事务的参数容器",解决的是 "跨阶段、跨线程、跨调用时机" 的参数传递问题:
①Try 阶段 :调用 tryWithdraw(BranchContext context, ...) 时,业务方将入参(userId/amount)存入 context,Seata 会将 context 序列化后和 branchId 绑定,存储到 TC;
②Confirm/Cancel 阶段 :TC 调用对应分支的 Confirm/Cancel 方法时,会将该分支绑定的 context 反序列化,作为入参传入方法;
③核心优势 :BranchContext 是 "和分支事务绑定的参数快照",而非 "和线程绑定",能跨时间、跨调用方传递参数 ------ 这是 SAGA 的 "全局入参快照" 做不到的。
总结原因:
TCC 的 Try/Confirm/Cancel 是三个独立、异步、分支级 的方法调用,无法像 SAGA/XA/AT 那样依赖 "方法入参快照 + 反射" 或 "线程本地存储" 传参,只能通过 BranchContext 这种 "和分支事务绑定的上下文容器",将 Try 阶段的入参精准传递到 Confirm/Cancel 阶段
以下以 Seata TCC 为例,讲清「xid/branchId 如何通过 Context 获取」「业务对象如何绑定到 Context」「落地实操步骤」,覆盖核心逻辑和代码示例:
一、核心概念先理清
| 标识 / 对象 | 作用 |
|---|---|
xid |
全局事务唯一标识,贯穿整个分布式事务(如 192.168.1.100:8091:123456) |
branchId |
分支事务 ID,每个微服务的 TCC 方法对应一个 branchId,隶属于 xid |
BranchContext |
Seata TCC 专属上下文对象,封装了 xid/branchId + 自定义业务参数 |
RootContext |
Seata 全局上下文工具类,用于在线程上下文中存储 / 获取 xid |
二、Seata TCC 中通过 Context 获取 xid/branchId + 业务对象
Seata TCC 的 try/confirm/cancel 方法默认会传入 BranchContext 参数,框架已自动填充 xid 和 branchId;业务对象则需手动绑定到 Context 中,核心分「绑定参数」和「获取参数」两步:
步骤 1:TCC 方法定义(框架自动注入 BranchContext)
Seata 要求 TCC 接口的 try/confirm/cancel 方法第一个参数必须是 BranchContext,框架会自动注入上下文(包含 xid/branchId):
java
运行
// TCC 接口(定义 try/confirm/cancel 方法)
public interface AccountTccService {
// Try 阶段:第一个参数必须是 BranchContext
boolean tryWithdraw(BranchContext context, Long userId, BigDecimal amount);
// Confirm 阶段:参数和 Try 一致,框架自动传 Context
boolean confirmWithdraw(BranchContext context, Long userId, BigDecimal amount);
// Cancel 阶段:同理
boolean cancelWithdraw(BranchContext context, Long userId, BigDecimal amount);
}
步骤 2:获取 Context 中的 xid/branchId(框架已自动填充)
在 TCC 方法实现中,可直接从 BranchContext 获取 xid 和 branchId,无需手动设置:
java
运行
@Service
public class AccountTccServiceImpl implements AccountTccService {
@Override
public boolean tryWithdraw(BranchContext context, Long userId, BigDecimal amount) {
// 1. 从 BranchContext 获取 xid(全局事务 ID)
String xid = context.getXid();
// 2. 从 BranchContext 获取 branchId(分支事务 ID)
long branchId = context.getBranchId();
// 业务逻辑:冻结金额(Try 阶段核心)
System.out.println("Try 阶段 - xid: " + xid + ", branchId: " + branchId);
accountMapper.freezeBalance(userId, amount);
return true;
}
@Override
public boolean confirmWithdraw(BranchContext context, Long userId, BigDecimal amount) {
// 同理,获取 xid/branchId
String xid = context.getXid();
// 业务逻辑:扣减冻结金额(Confirm 阶段核心)
accountMapper.deductFrozenBalance(userId, amount);
return true;
}
@Override
public boolean cancelWithdraw(BranchContext context, Long userId, BigDecimal amount) {
// 同理,获取 xid/branchId
String xid = context.getXid();
// 业务逻辑:释放冻结金额(Cancel 阶段核心)
accountMapper.releaseFrozenBalance(userId, amount);
return true;
}
}
步骤 3:传递复杂业务对象(如 OrderDTO)到 Context
如果参数是复杂对象(而非简单的 userId/amount),需手动将对象绑定到 BranchContext 的 args 中,确保 confirm/cancel 阶段能获取:
java
运行
// 发起分布式事务的入口方法(@GlobalTransactional 标记全局事务)
@Service
public class OrderServiceImpl {
@Autowired
private AccountTccService accountTccService;
@GlobalTransactional // Seata 全局事务注解,自动生成 xid 并绑定到 RootContext
public void createOrder(OrderDTO orderDTO) {
// 1. 构建 BranchContext,绑定业务对象(orderDTO)
BranchContext context = new BranchContext();
// 方式1:直接存入自定义参数(推荐,简单对象)
context.put("orderId", orderDTO.getOrderId());
context.put("userId", orderDTO.getUserId());
context.put("amount", orderDTO.getAmount());
// 方式2:序列化复杂对象存入(如 OrderDTO)
context.put("orderDTO", JSON.toJSONString(orderDTO));
// 2. 调用 TCC 的 Try 方法,传入 Context
accountTccService.tryWithdraw(context, orderDTO.getUserId(), orderDTO.getAmount());
}
}
// TCC 实现类中获取复杂对象
@Service
public class AccountTccServiceImpl implements AccountTccService {
@Override
public boolean tryWithdraw(BranchContext context, Long userId, BigDecimal amount) {
// 1. 获取框架自动填充的 xid/branchId
String xid = context.getXid();
long branchId = context.getBranchId();
// 2. 获取手动绑定的业务参数
Long orderId = (Long) context.get("orderId");
// 3. 获取序列化的复杂对象(如 OrderDTO)
String orderDTOJson = (String) context.get("orderDTO");
OrderDTO orderDTO = JSON.parseObject(orderDTOJson, OrderDTO.class);
// 业务逻辑:基于 orderDTO 执行冻结操作
accountMapper.freezeBalance(orderDTO.getUserId(), orderDTO.getAmount());
return true;
}
}
步骤 4:全局上下文 RootContext 获取 xid(补充场景)
如果在 TCC 方法外(如工具类、拦截器)需要获取 xid,可通过 Seata 的 RootContext(线程本地存储):
java
运行
// 任意位置获取 xid(需在分布式事务线程内)
String xid = RootContext.getXID();
if (xid != null) {
System.out.println("当前全局事务 ID:" + xid);
}
// 手动绑定 xid 到线程上下文(极少用,框架自动处理)
RootContext.bind("192.168.1.100:8091:123456");
// 解绑 xid
RootContext.unbind();
三、核心原理:Context 的底层实现
Seata 的 BranchContext 本质是「线程本地(ThreadLocal)存储的 Map 结构」:
- xid/branchId 自动填充 :
@GlobalTransactional注解会触发 Seata 生成 xid,并在调用 TCC 方法时,将 xid/branchId 自动写入BranchContext; - 业务参数手动传递 :
BranchContext内置Map<String, Object>,可存储任意自定义参数,本质是 "线程内的参数传递容器"; - 跨服务传递:Seata 会将 xid 封装到 RPC 调用的请求头中(如 Dubbo 的 attachment、Spring Cloud 的 Header),保证跨微服务调用时,xid 能传递到下游服务的线程上下文。
四、落地注意事项
-
参数序列化:复杂对象(如 OrderDTO)存入 Context 时,需序列化(如 JSON),避免跨服务 / 跨线程时出现 "对象无法序列化" 问题;
-
参数一致性 :
try/confirm/cancel方法的参数需保持一致,或通过 Context 传递相同的核心参数(如 orderId),避免 Confirm/Cancel 阶段参数缺失; -
避免超大参数:Context 存储的参数会随 RPC 传递,避免存入超大对象(如文件、大列表),否则会影响性能;
-
线程切换问题 :如果 TCC 方法内有异步操作(如线程池),需手动将
xid传递到异步线程(Seata 提供TccResource工具类支持):java
运行
// 异步线程中传递 xid String xid = RootContext.getXID(); executorService.submit(() -> { RootContext.bind(xid); // 手动绑定 xid 到异步线程 try { // 执行异步业务逻辑 } finally { RootContext.unbind(); // 解绑 } });
五、总结
TCC 模式中通过 Context 获取 xid/branchId 和业务对象的核心逻辑:
- 框架自动处理 :xid/branchId 由 Seata 自动注入
BranchContext,无需手动设置; - 手动传递业务参数 :复杂对象通过
BranchContext的put/get方法绑定 / 获取,或序列化后存储; - 全局上下文兜底 :非 TCC 方法内可通过
RootContext获取 xid,适配特殊场景。