TCC 模式
概述
TCC 是分布式事务中的两阶段提交协议,它的全称为 Try-Confirm-Cancel,即资源预留(Try)、确认操作(Confirm)、取消操作(Cancel)。Try:对业务资源的检查并预留;Confirm:对业务处理进行提交,只要 Try 成功,那么该步骤一定成功;Cancel:对业务处理进行回滚,对 Try 预留的资源进行释放。
TCC 是一种侵入式的分布式事务解决方案,以上三个操作都需要业务系统自行实现,对业务系统有着非常大的侵入性,设计相对复杂,但优点是完全不依赖数据库,能够实现跨数据库、跨应用的资源管理,对这些不同数据的访问通过侵入式的编码方式实现一个原子操作,更好解决了各种复杂业务场景下的分布式事务问题。
TCC 模式最大的优势是效率高。在 try 阶段的锁定资源并不是真正意义上的锁定,而是提交了本地事务,将资源预留到中间状态,不需要阻塞等待,因此效率比其它模式更高。而且采用异步提交的方式,try 阶段执行成功后,启动定时任务异步执行 confirm/cancel 方法。
Seata TCC模式1.5.1版本的优化
代码演示
假设现在有一个业务需要同时使用服务 A 和服务 B 完成一个事务操作。
对服务 A 定义一个 TCC 接口:
public interface TccActionOne {
@TwoPhaseBusinessAction(name = "DubboTccActionOne", commitMethod = "commit", rollbackMethod = "rollback")
public boolean prepare(BusinessActionContext actionContext, @BusinessActionContextParameter(paramName = "a") String a);
public boolean commit(BusinessActionContext actionContext);
public boolean rollback(BusinessActionContext actionContext);
}
对服务 B 定义一个 TCC 接口:
public interface TccActionTwo {
@TwoPhaseBusinessAction(name = "DubboTccActionTwo", commitMethod = "commit", rollbackMethod = "rollback", useTCCFence = true)
public boolean prepare(BusinessActionContext actionContext, @BusinessActionContextParameter(paramName = "b") String b);
public boolean commit(BusinessActionContext actionContext);
public boolean rollback(BusinessActionContext actionContext);
}
在业务系统中开启全局事务并执行服务 A 和服务 B 的 TCC 预留资源方法:
@GlobalTransactional
public String doTransactionCommit() {
tccActionOne.prepare(null, "one");
tccActionTwo.prepare(null, "two");
}
以上就是使用 Seata TCC 模式实现一个全局事务的例子。
使用 @GlobalTransactional
注解开启一个全局事务,而服务 A 和服务 B 的 TCC 接口为事务参与者,Seata 会把一个 TCC 接口当成一个 Resource,也叫做 TCC Resource。
TCC 接口可以是 RPC,也可以是 JVM 内部调用,这也意味着一个 TCC 接口,会有发起方和调用方两个身份。在以上例子中,TCC 接口在服务 A 和服务 B 中是发起方,在业务系统中是调用方。
Seata 启动时会对 TCC 接口进行扫描并解析,如果 TCC 接口是一个发起方,则在 Seata 启动时会向 TC 注册 TCC Resource,每个 TCC Resource 都有一个资源 id;如果 TCC 接口是一个调用方,则 Seata 会代理调用方,代理会拦截 TCC 接口的调用,即每次调用 Try 方法,会向 TC 注册一个分支事务,接着才执行原来的 RPC 调用。
当全局事务决议提交/回滚时,TC 会通过分支注册的资源 id 回调到对应参与者服务中执行 TCC Resource 的 Confirm/Cancel 方法。
原理
1.5.1版本之前
TM 开启全局事务时,RM 需要向 TC 发送注册信息,TC 保存分支事务的状态。TM 请求提交/回滚时,TC 需要向 RM 发送提交或者回滚信息。这样包含两个分支事务的分布式事务中,TC 与 RM 之间有四次 RPC。
1.5.1版本
TM 开启全局事务时,RM 不再需要向 TC 发送注册消息,而是把分支事务状态保存在了本地。TM 向 TC 发送提交或回滚消息后,RM 异步线程首先查出本地保存的未提交分支事务,然后向 TC 发送消息获取(本地分支事务)所在的全局事务状态,来决定是提交还是回滚本地事务。
这样优化后,RPC 次数减少了 50%,性能大幅提升。
处理异常
在 TCC 模型执行的过程中,可能会出现各种异常,最常见的有空回滚、幂等、悬挂等。
可以对 @TwoPhaseBusinessAction
注解的 useTCCFence
属性设置为 true,开启保护机制来解决这些问题。
并且需要在 MySQL 中创建 tcc_fence_log 表。
CREATE TABLE IF NOT EXISTS `tcc_fence_log`
(
`xid` VARCHAR(128) NOT NULL COMMENT 'global id',
`branch_id` BIGINT NOT NULL COMMENT 'branch id',
`action_name` VARCHAR(64) NOT NULL COMMENT 'action name',
`status` TINYINT NOT NULL COMMENT 'status(tried:1;committed:2;rollbacked:3;suspended:4)',
`gmt_create` DATETIME(3) NOT NULL COMMENT 'create time',
`gmt_modified` DATETIME(3) NOT NULL COMMENT 'update time',
PRIMARY KEY (`xid`, `branch_id`),
KEY `idx_gmt_modified` (`gmt_modified`),
KEY `idx_status` (`status`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;
处理空回滚问题
空回滚指在一个分布式事务中,在没有调用参与者的 Try 方法的情况下,TM 驱动二阶段回滚调用了参与者的 Cancel 方法。
比如全局事务开启后,参与者 A 分支注册完成之后会执行参与者一阶段 RPC 方法,如果此时参与者所在的机器发生宕机,网络异常都会造成 RPC 调用失败,即参与者一阶段方法没有成功执行,但是此时全局事务已经开启,最终必须要走向结束状态,在全局事务回滚时会调用参与者的 Cancel 方法,从而造成空回滚。
如何解决空回滚问题?在一阶段执行 Try 方法时,往 tcc_fence_log 表里插入一条记录(包括事务的 XID 和 BranchID 信息),status 字段值为 TRIED。在二阶段执行 Cancel 方法时判断对应记录是否存在,如果不存在,则不执行回滚操作。
处理幂等问题
幂等问题指 TC 没有收到分支事务的响应,需要进行重试,因此 Confirm/Cancel 接口需要支持幂等处理,即不会产生资源重复提交或者释放。
比如参与者执行完二阶段操作之后,由于网络抖动或者宕机问题,会造成 TC 收不到参与者执行二阶段操作的返回结果,则 TC 会重复发起调用,直到成功收到二阶段操作的执行结果。
如何解决幂等问题?提交事务时首先会判断 tcc_fence_log 表中是否有对应的记录,如果没有则插入一条记录;如果有则判断事务状态,如果状态是 COMMITED,就不会再次提交。回滚事务的逻辑与之相似。
处理悬挂问题
悬挂问题指 二阶段 Cancel 方法比一阶段 Try 方法优先执行,由于允许空回滚的原因,执行完二阶段 Cancel 方法之后直接空回滚返回成功,此时全局事务已经结束,但是 Try 方法随后执行,就会造成一阶段 Try 方法预留的资源永远无法提交和释放了。
比如执行一阶段 Try 方法时,出现网络拥堵,由于 Seata 全局事务有超时限制,执行 Try 方法超时后,TM 决议全局回滚,回滚完成后如果此时 RPC 请求才到达参与者,然后参与者执行 Try 方法进行资源预留,从而造成悬挂。
如何解决悬挂问题?当执行二阶段 Cancel 方法时,先判断 tcc_fence_log 表是否存在当前 xid 的记录,如果不存在则插入一条记录,状态是 SUSPENDED,并且不执行回滚操作。后面执行 Try 方法时首先会向 tcc_fence_log 表插入一条当前 xid 的记录,这样会造成主键冲突,从而避免了悬挂问题。