基本概念:
TCC(Try-Confirm-Cancel)分布式事务模型相对于 XA 等传统模型,其特征在于它不依赖 RM 对分布式事务的支持,
而是通过对业务逻辑的分解来实现分布式事务。
TCC与AT模式相同,也是二阶段提交,但是TCC对业务代码侵入性很强
TCC模式下,所有事务都要手动实现Try,Confirm,Cancel三个方法
TCC 模型认为对于业务系统中一个特定的业务逻辑 ,其对外提供服务时,必须接受一些不确定性,
即对业务逻辑初步操作的调用仅是一个临时性操作,调用它的主业务服务保留了后续的取消权。
如果主业务服务认为全局事务应该回滚,它会要求取消之前的临时性操作,这就对应从业务服务的取消操作。
而当主业务服务认为全局事务应该提交时,它会放弃之前临时性操作的取消权,这对应从业务服务的确认操作。
每一个初步操作,最终都会被确认或取消。因此,针对一个具体的业务服务,TCC 分布式事务模型需要业务系统提供
三段业务逻辑:
1. 初步操作 Try:完成所有业务检查,预留必须的业务资源。
2. 确认操作 Confirm:真正执行的业务逻辑,不做任何业务检查,只使用 Try 阶段预留的业务资源。
因此,只要 Try 操作成功,Confirm 必须能成功。另外,Confirm 操作需满足幂等性,保证一笔分布式事务能且只能成功一次。
3. 取消操作 Cancel:释放 Try 阶段预留的业务资源。同样的,Cancel 操作也需要满足幂等性。
工作机制:
一阶段
TM
TM处理流程和AT模式下一样:
1.开启全局事务,向TC注册全局事务并返回XID
2.如果业务执行成功,通知TC全局事务提交
3.如果业务执行失败,通知TC全局事务回滚
4.清除内存中XID
RM
注册分支事务,获取branchId。并将方法调用时的上下文发送给TC。执行业务逻辑
二阶段
TM
TM通知TC全局提交/回滚。TC通知各分支事务。RM处理消息逻辑是RMHandlerTCC里。
RM
提交和回滚逻辑一样:
1.用TC发过来的ResourceId查到TCResource,找到commit/rollback方法并执行
2.通知TC执行结果
常见异常 :
最常见的主要是这三种异常,空回滚、幂等、悬挂。
1 空回滚:
空回滚就是对于一个分布式事务,在没有调用 TCC 资源 Try 方法的情况下,调用了二阶段的 Cancel 方法,
Cancel 方法需要识别出这是一个空回滚,然后直接返回成功。
< try 未执行,Cancel 执行 >
什么样的情形会造成空回滚呢?注册分支事务是在调用 RPC 时,Seata 框架的切面会拦截到该次调用请求,
先向 TC 注册一个分支事务,然后才去执行 RPC 调用逻辑。如果 RPC 调用逻辑有问题,
比如调用方机器宕机、网络异常,都会造成 RPC 调用失败,即未执行 Try 方法。但是分布式事务已经开启了,
需要推进到终态,因此,TC 会回调参与者二阶段 Cancel 接口,从而形成空回滚。
解决:
需要一张额外的事务控制表,其中有分布式事务 ID 和分支事务 ID,第一阶段 Try 方法里会插入一条记录,
表示一阶段执行了。Cancel 接口里读取该记录,如果该记录存在,则正常回滚;如果该记录不存在,则是空回滚。
2 幂等:
幂等就是对于同一个分布式事务的同一个分支事务,重复去调用该分支事务的第二阶段接口,
因此,要求 TCC 的二阶段 Confirm 和 Cancel 接口保证幂等,不会重复使用或者释放资源。如果幂等控制没有做好,
很有可能导致资损等严重问题。
< 多次 执行 Confirm 或 Cancel>
什么样的情形会造成重复提交或回滚?从图中可以看到,提交或回滚是一次 TC 到参与者的网络调用。因此,
网络故障、参与者宕机等都有可能造成参与者 TCC 资源实际执行了二阶段防范,但是 TC 没有收到返回结果的情况,
这时,TC 就会重复调用,直至调用成功,整个分布式事务结束。
解决:
事务控制表的每条记录关联一个分支事务,那我们完全可以在这张事务控制表上加一个状态字段,
用来记录每个分支事务的执行状态。
状态字段有三个值,分别是初始化、已提交、已回滚。Try 方法插入时,是初始化状态。
二阶段 Confirm 和 Cancel 方法执行后修改为已提交或已回滚状态。当重复调用二阶段接口时,
先获取该事务控制表对应记录,检查状态,如果已执行,则直接返回成功;否则正常执行。
3.悬挂:
悬挂就是对于一个分布式事务,其二阶段 Cancel 接口比 Try 接口先执行。
因为允许空回滚的原因,Cancel 接口认为 Try 接口没执行,空回滚直接返回成功,对于 Seata 框架来说,
认为分布式事务的二阶段接口已经执行成功,整个分布式事务就结束了。但是这之后 Try 方法才真正开始执行。
< Confirm 在 try 之前>
什么样的情况会造成悬挂呢?按照前面所讲,在 RPC 调用时,先注册分支事务,再执行 RPC 调用,
如果此时 RPC 调用的网络发生拥堵,通常 RPC 调用是有超时时间的,RPC 超时以后,发起方就会通知 TC 回滚该分布式事务,
可能回滚完成后,RPC 请求才到达参与者,真正执行,从而造成悬挂。
解决:
可以在二阶段执行时插入一条事务控制记录,状态为已回滚,这样当一阶段执行时,先读取该记录,
如果记录存在,就认为二阶段已经执行,进行 空 try ,否则二阶段没执行,正常执行 try。
TCC模式 需要的数据库表:
创建数据库: seata-server
建表 Sql语句 :
SET FOREIGN_KEY_CHECKS=0; DROP TABLE IF EXISTS `branch_table`; CREATE TABLE `branch_table` ( `branch_id` bigint(20) NOT NULL, `xid` varchar(128) NOT NULL, `transaction_id` bigint(20) DEFAULT NULL, `resource_group_id` varchar(32) DEFAULT NULL, `resource_id` varchar(256) DEFAULT NULL, `branch_type` varchar(8) DEFAULT NULL, `status` tinyint(4) DEFAULT NULL, `client_id` varchar(64) DEFAULT NULL, `application_data` varchar(2000) DEFAULT NULL, `gmt_create` datetime(6) DEFAULT NULL, `gmt_modified` datetime(6) DEFAULT NULL, PRIMARY KEY (`branch_id`), KEY `idx_xid` (`xid`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; DROP TABLE IF EXISTS `global_table`; CREATE TABLE `global_table` ( `xid` varchar(128) NOT NULL, `transaction_id` bigint(20) DEFAULT NULL, `status` tinyint(4) NOT NULL, `application_id` varchar(32) DEFAULT NULL, `transaction_service_group` varchar(32) DEFAULT NULL, `transaction_name` varchar(128) DEFAULT NULL, `timeout` int(11) DEFAULT NULL, `begin_time` bigint(20) DEFAULT NULL, `application_data` varchar(2000) DEFAULT NULL, `gmt_create` datetime DEFAULT NULL, `gmt_modified` datetime DEFAULT NULL, PRIMARY KEY (`xid`), KEY `idx_gmt_modified_status` (`gmt_modified`,`status`), KEY `idx_transaction_id` (`transaction_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; DROP TABLE IF EXISTS `lock_table`; CREATE TABLE `lock_table` ( `row_key` varchar(128) NOT NULL, `xid` varchar(96) DEFAULT NULL, `transaction_id` bigint(20) DEFAULT NULL, `branch_id` bigint(20) NOT NULL, `resource_id` varchar(256) DEFAULT NULL, `table_name` varchar(32) DEFAULT NULL, `pk` varchar(36) DEFAULT NULL, `gmt_create` datetime DEFAULT NULL, `gmt_modified` datetime DEFAULT NULL, PRIMARY KEY (`row_key`), KEY `idx_branch_id` (`branch_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;