目录
前言:
上一篇文章我们分析了seata-At强一致性的分布式事务框架,在高并发下性能不及seata-tcc。我们本篇文件分析一下seata-tcc的使用和需要注意的地方。
seata-Tcc简介
2019 年 3 月份,Seata 开源了 TCC 模式,该模式由蚂蚁金服贡献。TCC 模式需要用户根据自己的业务场景实现 Try、Confirm 和 Cancel 三个操作;事务发起方在一阶段执行 Try 方式,在二阶段提交执行 Confirm 方法,二阶段回滚执行 Cancel 方法。
TCC 三个方法描述:
- Try:资源的检测和预留;
- Confirm:执行的业务操作提交;要求 Try 成功 Confirm 一定要能成功;
- Cancel:预留资源释放;
TCC 是一种侵入式的分布式事务解决方案,以上三个操作都需要业务系统自行实现,对业务系统有着非常大的入侵性,设计相对复杂,但优点是 TCC 完全不依赖数据库,能够实现跨数据库、跨应用资源管理,对这些不同数据访问通过侵入式的编码方式实现一个原子操作,更好地解决了在各种复杂业务场景下的分布式事务问题
Seata-Tcc底层原理
Seata TCC 模式跟通用型 TCC 模式原理一致,我们先来使用 Seata TCC 模式实现一个分布式事务
我们先来分析一个最简单的使用seata-tcc的分布式事务的案例,我们的服务A的代码
**
* 这里定义tcc的接口
* 一定要定义在接口上
* 我们使用springCloud的远程调用
* 那么这里使用LocalTCC便可
*
* @author tanzj
*/
@LocalTCC
public interface TccService {
/**
* 定义两阶段提交
* name = 该tcc的bean名称,全局唯一
* commitMethod = commit 为二阶段确认方法
* rollbackMethod = rollback 为二阶段取消方法
* BusinessActionContextParameter注解 传递参数到二阶段中
*
* @param params -入参
* @return String
*/
@TwoPhaseBusinessAction(name = "insert", commitMethod = "commitTcc", rollbackMethod = "cancel")
String insert(
@BusinessActionContextParameter(paramName = "params") Map<String, String> params
);
/**
* 确认方法、可以另命名,但要保证与commitMethod一致
* context可以传递try方法的参数
*
* @param context 上下文
* @return boolean
*/
boolean commitTcc(BusinessActionContext context);
/**
* 二阶段回滚方法
*
* @param context 上下文
* @return boolean
*/
boolean cancel(BusinessActionContext context);
}
业务服务首先开启了一个全局的事务,我们的业务应用调用A服务
@Override
@GlobalTransactional(timeoutMills = 60000 * 4)
@Transactional
public String insertTcc(Map<String, String> params) {
log.info("------------------> xid = " + RootContext.getXID());
//操作我们当前的数据库
tmDAO.insert(params);
//通过rpc远程调用其他业务服务
tccFeign.insertTCC(params);
//throw new RuntimeException("TCC服务测试回滚");
return "success";
}
以上就是使用 Seata TCC 模式实现一个全局事务的例子,可以看出,TCC 模式同样使用 @GlobalTransactional
注解开启全局事务。完整的代码可以参考我上一篇文章分布式事务seata-At使用 详解,去下载源码和搭建环境。
我们用一张图来描述一下这个过程
1.我们的第一步我们的TM服务,还有TCC接口所在的服务会通过nacos和我们的Tc服务进行服务注册和发现。
2.seata会对扫描注解@GlobalTransactional 进行扫描操作,判断他是一个业务发起方。在执行真正的rpc方法之前,进行一个拦截,然后向Tc注册一个分支事务。同时还会讲一些上下文参数一起发给TC服务。上图中的操作4用使用到这些相关参数对指定的Tc进行全局提交或者回滚
3.seata还会对我们的@LocalTCC进行扫描,发现他是一个TCC接口,然后会对它进行解析,同时再解析@TwoPhaseBusinessAction这个注解,把它封装成一个TCCResource对象,将它注册给TC服务。
4.TCCResource
包含了 TCC 接口的相关信息,同时会在本地进行缓存。继续调用父类 registerResource
方法(封装了通信方法)向 TC 注册,TCC 资源的 resourceId 是 actionName,actionName 就是 @TwoParseBusinessAction
注解中的 name。
5.我们分析上图中的5和6:
当 TM 决议二阶段提交,TC 会通过分支注册的的资源 ID 回调到对应参与者(即 TCC 接口发起方)服务中执行 TCC Resource 的 Confirm/Cancel 方法。
资源管理器中会根据 resourceId 在本地缓存找到对应的 TCCResource
,同时根据 xid、branchId、resourceId、applicationData 找到对应的 BusinessActionContext
上下文,执行的参数就在上下文中。最后,执行 TCCResource
中获取 commit
的方法进行二阶段提交。回滚也是类似
分布式事务存在的一些问题
空回滚、幂等、悬挂等
空回滚:空回滚指的是在一个分布式事务中,在没有调用参与方的 Try 方法的情况下,TM 驱动二阶段回滚调用了参与方的 Cancel 方法。
那么空回滚是如何产生的呢?
如上图所示,全局事务开启后,参与者 A 分支注册完成之后会执行参与者一阶段 RPC 方法,如果此时参与者 A 所在的机器发生宕机,网络异常,都会造成 RPC 调用失败,即参与者 A 一阶段方法未成功执行,但是此时全局事务已经开启,Seata 必须要推进到终态,在全局事务回滚时会调用参与者 A 的 Cancel 方法,从而造成空回滚。
要想防止空回滚,那么必须在 Cancel 方法中识别这是一个空回滚,Seata 是如何做的呢?
Seata 的做法是新增一个 TCC 事务控制表,包含事务的 XID 和 BranchID 信息,在 Try 方法执行时插入一条记录,表示一阶段执行了,执行 Cancel 方法时读取这条记录,如果记录不存在,说明 Try 方法没有执行。
幂等:幂等问题指的是 TC 重复进行二阶段提交,因此 Confirm/Cancel 接口需要支持幂等处理,即不会产生资源重复提交或者重复释放。
那么幂等问题是如何产生的呢?
如上图所示,参与者 A 执行完二阶段之后,由于网络抖动或者宕机问题,会造成 TC 收不到参与者 A 执行二阶段的返回结果,TC 会重复发起调用,直到二阶段执行结果成功。
Seata 是如何处理幂等问题的呢?
同样的也是在 TCC 事务控制表中增加一个记录状态的字段 status,该字段有有 3 个值,分别为:
- tried:1
- committed:2
- rollbacked:3
二阶段 Confirm/Cancel 方法执行后,将状态改为 committed 或 rollbacked 状态。当重复调用二阶段 Confirm/Cancel 方法时,判断事务状态即可解决幂等问题。
悬挂:悬挂指的是二阶段 Cancel 方法比 一阶段 Try 方法优先执行,由于允许空回滚的原因,在执行完二阶段 Cancel 方法之后直接空回滚返回成功,此时全局事务已结束,但是由于 Try 方法随后执行,这就会造成一阶段 Try 方法预留的资源永远无法提交和释放了。
那么悬挂是如何产生的呢?
如上图所示,在执行参与者 A 的一阶段 Try 方法时,出现网路拥堵,由于 Seata 全局事务有超时限制,执行 Try 方法超时后,TM 决议全局回滚,回滚完成后如果此时 RPC 请求才到达参与者 A,执行 Try 方法进行资源预留,从而造成悬挂。
Seata 是怎么处理悬挂的呢?
在 TCC 事务控制表记录状态的字段 status 中增加一个状态:
- suspended:4
当执行二阶段 Cancel 方法时,如果发现 TCC 事务控制表没有相关记录,说明二阶段 Cancel 方法优先一阶段 Try 方法执行,因此插入一条 status=4 状态的记录,当一阶段 Try 方法后面执行时,判断 status=4 ,则说明有二阶段 Cancel 已执行,并返回 false 以阻止一阶段 Try 方法执行成功。