文章目录
- 一、前言
- 二、问题介绍、seata1.5.1版本之前的解决方案
- 1、空回滚
- 出现原因
- 解决措施
- 事务控制记录表
- try()、cancel()中获取xid、branch_id
- 2、幂等
- 出现原因
- 解决措施
- 事务控制记录表
- 3、悬挂
- 出现原因
- 解决措施
- 4、总述
- 最终的事务控制记录表
- 三、seata1.5.1版本起官方提供的解决措施
- 1、开启TCCFence
- 事务控制记录表tcc_fence_log
- 2、try时使用TCCFence
- 3、commit时使用TCCFence
- 4、cancel时使用TCCFence
- 四、总结
一、前言
更多内容见Seata专栏:https://blog.csdn.net/saintmm/category_11953405.html
至此,seata系列的内容已出:
- can not get cluster name in registry config ‘service.vgroupMapping.xx‘, please make sure registry问题解决;
- Seata Failed to get available servers: endpoint format should like ip:port 报错原因/解决方案汇总版(看完本文必解决问题)
- Seata json decode exception, Cannot construct instance of java.time.LocalDateTime报错原因/解决方案最全汇总版
- 【微服务 31】超细的Spring Cloud 整合Seata实现分布式事务(排坑版)
- 【微服务 32】Spring Cloud整合Seata、Nacos实现分布式事务案例(巨细排坑版)【云原生】
- 【微服务33】分布式事务Seata源码解析一:在IDEA中启动Seata Server
- 【微服务34】分布式事务Seata源码解析二:Seata Server启动时都做了什么
- 【微服务35】分布式事务Seata源码解析三:从Spring Boot特性来看Seata Client 启动时都做了什么
- 【微服务36】分布式事务Seata源码解析四:图解Seata Client 如何与Seata Server建立连接、通信
- 【微服务37】分布式事务Seata源码解析五:@GlobalTransactional如何开启全局事务
- 【微服务38】分布式事务Seata源码解析六:全局/分支事务分布式ID如何生成?序列号超了怎么办?时钟回拨问题如何处理?
- 【微服务39】分布式事务Seata源码解析七:图解Seata事务执行流程之开启全局事务
- 分布式事务Seata源码解析八:本地事务执行流程(AT模式下)
- 分布式事务Seata源码解析九:分支事务如何注册到全局事务
- 分布式事务Seata源码解析十:AT模式回滚日志undo log详细构建过程
- 分布式事务Seata源码解析11:全局事务执行流程之两阶段全局事务提交
- 分布式事务Seata源码解析12:全局事务执行流程之全局事务回滚
- Spring Cloud整合Seata实现TCC分布式事务模式案例
- 分布式事务Seata源码解析13:TCC事务模式实现原理
在Spring Cloud整合Seata实现TCC分布式事务模式案例一文,笔者提到Seata存在三种问题:空回滚、幂等、悬挂。
本文详细介绍一下这三种问题、相应的解决方案、以及seata1.5.1版本开始对其的解决措施。
二、问题介绍、seata1.5.1版本之前的解决方案
1、空回滚
try()未执行,cancel()执行了;即:在没有调用 TCC 资源 Try 方法的情况下,调用了二阶段的 Cancel 方法。
出现原因
因为某些原因,可能是TCC分支事务所在的服务宕机 或 网络异常,导致try()超时 / 未执行(TC直接没收到try);分支事务的执行结果为失败,会触发分支事务的回滚,从而形成了空回滚。
解决措施
空回滚问题的关键在于一阶段的try()方法未执行,解决思路就很简单:
- 如果try()方法执行了,就正常回滚;
- 如果try()方法未执行,那就是空回滚。不应该做回滚操作,直接返回操作成功即可。
我们可以维护一个事务控制记录表(tcc_fence_log
),其中有全局事务xid、分支事务branch_id;
- 一阶段 try() 方法里会插入一条记录,表示一阶段执行了。
- 二阶段 cancel() 方法中读取该记录;如果记录存在,则正常回滚;如果记录不存在,则是空回滚,直接返回成功。
事务控制记录表
CREATE TABLE IF NOT EXISTS `tcc_fence_log`
(
`xid` VARCHAR(128) NOT NULL COMMENT 'global id',
`branch_id` BIGINT NOT NULL COMMENT 'branch id',
PRIMARY KEY (`xid`, `branch_id`),
KEY `idx_status` (`status`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;
try()、cancel()中获取xid、branch_id
1> 全局事务xid获取:
String xid = RootContext.getXID();
2> 分支事务branch_id获取:
String branchId = MDC.get(RootContext.MDC_KEY_BRANCH_ID);
2、幂等
多次执行cancel() 或 confirm();
为了保证TCC二阶段提交的重试机制不会引发数据不一致,要求 TCC 的二阶段 Try、 Confirm 和 Cancel 接口保证幂等,不会重复使用或者释放资源。如果幂等没做好,很有可能导致数据不一致问题,相信大家都不想疲于手工修数据的。
出现原因
RM网络出现异常(比如:cancel()、confirm()超时),导致TC 回滚/提交时异常,TC会多次重试;
解决措施
在上述空回滚解决方案(维护一个事务控制记录表(tcc_fence_log
))的基础上,增加一个执行状态字段(status
),每次执行cancel()、confirm()时都根据xid、branchId查询相应记录的状态。
以Cancel操作为例:
- 如果没有记录,说明是空回滚,啥也不做。
- 如果记录状态是已回滚,cancel() 操作直接返回;
- 如果记录状态是已提交,cancel() 操作抛出异常,理论上这种情况永远不可能发生;
- 如果记录状态是事务初始化,cancel() 操作正常执行。
事务控制记录表
CREATE TABLE IF NOT EXISTS `tcc_fence_log`
(
`xid` VARCHAR(128) NOT NULL COMMENT 'global id',
`branch_id` BIGINT NOT NULL COMMENT 'branch id',
`status` TINYINT NOT NULL COMMENT 'status(tried:1;committed:2;rollbacked:3;suspended:4)',
PRIMARY KEY (`xid`, `branch_id`),
KEY `idx_status` (`status`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;
3、悬挂
cancel()比try()先执行;
- 因为Seata允许空回滚的原因,Cancel接口认为try接口没执行,空回滚直接返回成功;这样对于steata而言,分布式事务的二阶段cancel()已经执行成功,整个分布式事务就结束了。
- 但由于某种原因,这之后try()才真正开始执行,预留业务资源;然而这个事务已经被seata认为结束了,并且对于一个try方法预留的业务资源,只有该分布式全局事务才能使用;也就是说这个一阶段预留的资源,后面没有人处理了,对于这种情况我们称为悬挂;即业务资源预留后没法继续处理。
出现原因
在分布式事务Seata源码解析13:TCC事务模式实现原理一文中,我们聊过对于TCC分支事务而言,其会先注册分支事务,然后再执行一阶段 try进行RPC调用。
- 如果RPC调用出现网络问题,比如:网络拥堵等,RPC调用会超时,超时以后TC会通知RM回滚该分布式全局事务;
- 这时就可能出现回滚完成后,RPC 请求 才到达服务提供方开始真正执行。
解决措施
悬挂的本质问题是一阶段预留的资源后面没法处理了,所以解决思路在于一阶段的执行时机,如果二阶段执行完成,一阶段就不要再继续执行了。
- 一阶段Try()执行的时候,我们先看一下二阶段是否已执行;如果已执行,一阶段不再执行,否则正常执行;
- 在二阶段cancel() 执行时就插入一条事务控制记录,状态为悬挂;一阶段执行时判断是否可以读取到该条记录;
4、总述
- 增加事务控制表,核心三个字段:全局事务ID(tx_id)、分支事务ID(branch_id)、状态(status);
- try() 时插入记录;cancel() 时有记录则更新记录状态、没有记录则插入数据;confirm() 时更新记录状态。
最终的事务控制记录表
CREATE TABLE IF NOT EXISTS `tcc_fence_log`
(
`xid` VARCHAR(128) NOT NULL COMMENT 'global id',
`branch_id` BIGINT NOT NULL COMMENT 'branch id',
`status` TINYINT NOT NULL COMMENT 'status(tried:1;committed:2;rollbacked:3;suspended:4)',
PRIMARY KEY (`xid`, `branch_id`),
KEY `idx_status` (`status`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;
- xid -> 全局事务id
- branch_id -> TCC分支事务Id
- status -> TCC分支事务状态,包含四种状态:已尝试、已提交、已回滚、空悬挂。
三、seata1.5.1版本起官方提供的解决措施
TCC模式下的空回滚、幂等、悬挂问题,seata1.5.1版本开始进行了处理(版本说明);
有兴趣可以看一下GIthub上的pull request:https://github.com/seata/seata/pull/3545,整体解决方案和我们上面提供的一样,只是它体现在seata源码里,而上面的解决方案需要我们自己写在try、confirm、cancel的业务代码里。
具体TCC事务原理 / 源码解析见博文:分布式事务Seata源码解析13:TCC事务模式实现原理。
1、开启TCCFence
虽然seata1.5.1版本开始支持TCC空回滚、幂等、悬挂问题的解决,但默认并没有开启,需要手动开启;
开启方式如下:
- 在
@TwoPhaseBusinessAction
注解中设置useTCCFence = true;
事务控制记录表tcc_fence_log
只在@TwoPhaseBusinessAction
注解中设置useTCCFence = true;也是没法使用TCCFence的,需要在每个TCC分支事务连接的数据库中创建表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;
建表SQL出处:
- seata源码下的mysql.sql文件,如果使用Oracle、Postgresql,用另外两个文件。
2、try时使用TCCFence
ActionInterceptorHandler#proceed()方法中执行TCC事务的一阶段try()方法时,会判断是否使用TCCFence;如果使用,则尝试新增一条事务控制记录,如果新增失败、说明存在悬挂问题,直接抛出异常TCCFenceException
。
3、commit时使用TCCFence
TCCResourceManager#branchCommit()方法中执行TCC事务的二阶段commit()方法时会先根据xid和branch_id判断是否存在TCCFenceLog;
- 不存在,则抛出异常;
- 存在,并且状态是已提交,说明是重复提交,直接返回操作成功。
- 存在,并且状态是已回滚、悬挂,打印个日志,直接返回操作失败。
- 存在,并且状态是已尝试,则更新TCCFenceLog记录状态为已提交。
4、cancel时使用TCCFence
TCCResourceManager#branchRollback()方法中执行TCC事务的二阶段cancel()方法时会先根据xid和branch_id判断是否存在TCCFenceLog;
- 不存在,则插入一条状态为悬挂的记录;空回滚的场景。
- 存在,并且状态是已回滚、悬挂,说明是重复提交,直接返回操作成功。
- 存在,并且状态是已提交,打印个日志,直接返回操作失败。
- 存在,并且状态是已尝试,则更新TCCFenceLog记录状态为已回滚。
四、总结
TCC 空回滚、幂等、悬挂问题的解决方案目前是唯一的:
- 增加事务控制表,核心三个字段:全局事务ID(tx_id)、分支事务ID(branch_id)、状态(status);
- TCC分支事务状态,包含四种状态:已尝试、已提交、已回滚、空悬挂。
- try() 时插入记录;cancel() 时有记录则更新记录状态、没有记录则插入数据;confirm() 时更新记录状态。