整体机制
TCC模式采用的也是两阶段提交的模型,区别于AT和XA模式,TCC模式的两阶段需要自定义实现,不依赖于数据库的事务模型和协议。
机制示例图
工作机制
TCC模式客户端使用时需要分try、commit、cancel三个部分:
- try:检查预留资源
- commit:执行真正业务的提交
- Cancel:预留资源的释放
工作机制示例图
异常场景控制
在 TCC 模型执行的过程中,还可能会出现各种异常,其中最为常见的有空回滚、幂等、悬挂等。
空回滚
出现场景:
空回滚指的是在一个分布式事务中,在没有调用参与方的 Try 方法的情况下,TM 驱动二阶段回滚调用了参与方的 Cancel 方法;一般出现原因是执行Try阶段时,try执行机器发生网络异常或者宕机。
处理方式:
新增一个 TCC 事务控制表,包含事务的 XID 和 BranchID 信息,在 Try 方法执行时插入一条记录,表示一阶段执行了,执行 Cancel 方法时读取这条记录,如果记录不存在,说明 Try 方法没有执行。
幂等
出现场景:
分支参与者在执行完commit后出现网络故障或者宕机,导致TC没有收到commit的返回结果,TC 会重复发起调用,直到二阶段执行结果成功。
处理方式:
在 TCC 事务控制表中增加一个记录状态的字段 status,该字段有 3 个值,分别为:
-tried:1
-committed:2
-rollbacked:3
二阶段 Confirm/Cancel 方法执行后,将状态改为 committed 或 rollbacked 状态。当重复调用二阶段 Confirm/Cancel 方法时,判断事务状态即可解决幂等问题。
防悬挂
出现场景:
分支参与者在执行一阶段 Try 方法时,出现网路拥堵,由于 Seata 全局事务有超时限制,执行 Try 方法超时后,TM 决议全局回滚,回滚完成后如果此时 RPC 请求才到达参与者 A,执行 Try 方法进行资源预留,从而造成悬挂。
处理方式:
当执行二阶段 Cancel 方法时,如果发现 TCC 事务控制表没有相关记录,说明二阶段 Cancel 方法优先一阶段 Try 方法执行,因此在事务控制表插入一条回滚记录;在执行一阶段Try时先读取事务控制表,若查询到回滚记录,说明出现悬挂,则停止执行业务直接返回。
集成过程
Seata客户端集成
事务执行客户端基本搭建过程与其他模式一致,全局事务发起方添加@GlobalTransactional注解即可,事务接收方需要实现自定义TTC(Try-Commit-Cancel),过程如下:
- TCC定义
- 业务层接口上添加@LocalTCC注解
- 指定执行事务一阶段的方法,在方法上添加@TwoPhaseBusinessAction,同时指定二阶段的commit方法和cancel方法。
参考代码
@LocalTCC
public interface FundManageService {
List<FundInfoEntity> queryFundInfo(FundInfoDto dto);
FundInfoEntity queryFundInfoDetail(String code);
FundInfoEntity queryFundInfoDetailForGlobal(String code);
void addFundInfo(FundInfoDto dto);
/**
* 编辑数据
* @param dto
*/
void editFundInfo(FundInfoDto dto);
/**
* 编辑数据
* @param actionContext
* @param dto
*/
@TwoPhaseBusinessAction(name = "EditFundInfoOne", commitMethod = "commit", rollbackMethod = "rollback")
void editFundInfo(BusinessActionContext actionContext, @BusinessActionContextParameter(paramName = "tx_param") FundInfoDto dto);
/**
* 减少基金池份额
* @param dto
*/
void reduceFundShr(FundInfoDto dto);
boolean commit(BusinessActionContext actionContext);
boolean rollback(BusinessActionContext actionContext);
- TCC实现
1.实现一阶段try,实现内容包含防悬挂、预留业务资源、添加全局事务日志
@Override
public void editFundInfo(BusinessActionContext actionContext, FundInfoDto dto) {
//1. 实现防悬挂
FundTransactionEntity transactionEntity = fundTransactionMapper.selectTxById(actionContext.getBranchId(), actionContext.getXid());
if (null != transactionEntity) {
logger.error("【TRY-TRY】事务已经回滚,出现悬挂,流程终止,xid:{}, branchId:{}", actionContext.getXid(), actionContext.getBranchId());
return;
}
//2. 插入事务控制记录,用于后续处理幂等和空回滚
addTxLog(dto, actionContext, FundTransactionTypeEnum.TCC_TRY.name(), FundTransactionStateEnum.STATE_TRY.name());
//3. 冻结资金份额
FundInfoEntity entity = new FundInfoEntity();
BeanUtils.copyProperties(dto, entity);
fundManageMapper.freezeFundShr(entity);
}
- 实现二阶段commit,实现内容包含保证幂等、完成业务提交、修改全局事务状态
@Override
public boolean commit(BusinessActionContext actionContext) {
//1. 基于全局事务id和分支事务id获取事务记录
FundTransactionEntity transactionEntity = fundTransactionMapper.selectTxById(actionContext.getBranchId(), actionContext.getXid());
//2. 判断是否为空,如果为空,说明一阶段的try没有执行
if (transactionEntity == null) {
logger.error("【TRY-COMMIT】获取事务记录失败,流程终止,xid:{}, branchId:{}", actionContext.getXid(), actionContext.getBranchId());
return true;
} else if (transactionEntity.getState().equals(FundTransactionStateEnum.STATE_COMMIT.name())) {
//3. 判断事务是否已经提交,若提交,流程终止,保证幂等性
logger.error("【TRY-COMMIT】事务记录已经提交,流程终止,xid:{}, branchId:{}", actionContext.getXid(), actionContext.getBranchId());
return true;
} else if (transactionEntity.getState().equals(FundTransactionStateEnum.STATE_CANCEL.name())) {
//4. 判断事务是否已经回滚,若回滚,流程终止
logger.error("【TRY-COMMIT】事务记录已经回滚,流程终止,xid:{}, branchId:{}", actionContext.getXid(), actionContext.getBranchId());
return true;
}
FundInfoDto dto = actionContext.getActionContext(TX_PARAM, FundInfoDto.class);
FundInfoEntity entity = new FundInfoEntity();
BeanUtils.copyProperties(dto, entity);
//5. 执行业务代码,完成业务
fundManageMapper.commitFundShr(entity);
//6. 调整事务日志记录为“提交”
Integer row = updateTxLog(actionContext, FundTransactionStateEnum.STATE_COMMIT.name());
Assert.state(row != null && row > 0,"【TRY-COMMIT】事务记录提交失败,事务状态已经发生变化,需要更新数据!");
return true;
}
3.实现二阶段cancel,实现内容包含防止空回滚、释放预留资源、修改全局事务状态
@Override
public boolean rollback(BusinessActionContext actionContext) {
//1. 先查询事务记录,判断是否为空(空回滚)
FundTransactionEntity transactionEntity = fundTransactionMapper.selectTxById(actionContext.getBranchId(), actionContext.getXid());
FundInfoDto dto = actionContext.getActionContext(TX_PARAM, FundInfoDto.class);
FundInfoEntity entity = new FundInfoEntity();
if (dto == null) {
return true;
}
BeanUtils.copyProperties(dto, entity);
//2. 如果为空,插入一条CANCEL类型的事务记录,并终止方案
if (transactionEntity == null) {
logger.error("【TRY-ROLLBACK】事务记录出现空回滚,流程终止,xid:{}, branchId:{}", actionContext.getXid(), actionContext.getBranchId());
this.addTxLog(dto, actionContext, FundTransactionTypeEnum.TCC_TRY.name(), FundTransactionStateEnum.STATE_CANCEL.name());
//出现空回滚以后,无论是否出现异常,都应该直接结束方法
return true;
}
//3. 如果不为空,判断事务状态是否已经回滚,如果回滚说明重复执行了回滚操作,直接结束方法
if (transactionEntity.getState().equals(FundTransactionStateEnum.STATE_CANCEL.name())) {
logger.error("【TRY-ROLLBACK】事务记录重复回滚,流程终止,xid:{}, branchId:{}", actionContext.getXid(), actionContext.getBranchId());
return true;
} else if (transactionEntity.getState().equals(FundTransactionStateEnum.STATE_COMMIT.name())) {
//4. 否则判断是否为已提交,如果是,说明流程异常,结束方法
logger.error("【TRY-ROLLBACK】事务已经提交,不允许再回滚,流程终止,xid:{}, branchId:{}", actionContext.getXid(), actionContext.getBranchId());
return true;
}
//5. 如果是初始化一阶段完成状态,则回滚修改的业务数据
fundManageMapper.unFreezeFundShr(entity);
//6. 变更事务状态为“已回滚”
Integer row = this.updateTxLog(actionContext, FundTransactionStateEnum.STATE_CANCEL.name());
Assert.state(row != null && row > 0,"【TRY-ROLLBACK】事务记录提交失败,事务状态已经发生变化,需要更新数据!");
return true;
}
特征
- 优点:TCC过程自定义实现,不依赖于数据库,可以解决各种复杂场景以及非关系型数据库类型的分布式事务问题。
- 缺点: 实现TCC过程对业务代码渗透的较深,代码编码量比较大,提高了实现难度。
参考文献
深度剖析 Seata TCC 模式
TCC 理论及设计实现指南介绍