大家好,我是石页兄,编程界的一名小学生
上善若水,水善利万物而不争
欢迎 关注微信公众号「架构染色」&& 加群交流
一、前奏
Seata 从设计层面将事务参与者的角色分为 TC(事务协调器)、RM(资源管理器)、TM(事务管理器) ,传统的 XA 方案中, RM 是在数据库层,即依赖了 DB 的 XA 驱动能力,也会有典型的数据锁定和连接锁定的问题,为了规避 XA 带来的制约,Seata 将 RM 从 DB 层迁移出来,以中间件的形式放在应用层,完全剥离了分布式事务方案对数据库在协议支持上的要求。在 Seata 的 AT 模式中, RM 的能力在数据源做了一层代理,Seata 在这层代理中预业务 SQL 执行过程,加入分布式事务所需的逻辑,通过这种方式,Seata 真正做到了对业务代码无侵入,只需要通过简单的配置和声明,业务方就可享受 Seata 所带来的分布式事务能力;而且跟 XA 模式相比,当本地事务执行完可以立即释放本地事务锁定的资源,性能更好。
二、Seata AT 模式的顶层设计
Seata AT 模式下,一个典型的分布式事务过程如下:
- TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID。
- XID 在微服务调用链路的上下文中传播,供 RM 使用。
- RM 向 TC 注册分支事务,将其纳入 XID 对应全局事务的管辖,执行后上报分支事务状态。
- TM 向 TC 发起针对 XID 的全局提交或回滚决议。
- TC 驱动 RM 完成 XID 下管辖的全部分支事务的提交或回滚操作。
前文《【Seata 源码领读】揭秘 @GlobalTransactional 背后 TM 的黑盒操作》中描述了 TM 的能力,本篇继续介绍 RM,RM(Resource Manager): 资源管理器,控制分支事务,负责分支注册、状态汇报,并接收事务协调器 TC 的指令,驱动分支(本地)事务的提交和回滚。
传统的 XA 方案中 RM 是放在数据库层的,它依赖了数据库的 XA 驱动程序,如下图所示
但 XA 模式有两个典型的问题:
-
一个是数据锁定,XA 事务过程中,数据是被锁定的。XA 的数据锁定是数据库的内部机制维护的,所以依赖 DBA 干预数据库去解除数据锁定。
-
另一方面是连接锁定,XA 事务过程中,连接也是被锁定的。至少在两阶段提交的 prepare 之前,连接是不能释放的(因为连接断开,这个连接上的 XA 分支就会回滚,整个事务也会被迫回滚)。较之于数据的锁定(数据的锁定对于事务的隔离性是必要的机制),连接的锁定带给整个业务系统的直接影响就是,限制了并发度。
为了规避 XA 带来的制约,Seata 将 RM 从 DB 层迁移出来,以中间件的形式放在应用层,完全剥离了分布式事务方案对数据库在协议支持上的要求。
Seata AT 模式下 RM 的能力概括来说是在数据源做了一层代理,当程序执行到 DAO 层,通过 JdbcTemplate 或 Mybatis 操作 DB 时所使用的数据源实际上用的是 Seata 提供的数据源代理 DataSourceProxy,Seata 在这层代理中预业务 SQL 执行过程,加入分布式事务所需的逻辑,主要是解析 SQL,把业务数据在更新前后的数据镜像组织成回滚日志,并将 undo log 日志插入 undo_log 表中,保证每条更新数据的业务 sql 都有对应的回滚日志存在。通过这种方式,Seata 真正做到了对业务代码无侵入,只需要通过简单的配置,业务方就可以轻松享受 Seata 所带来的功能。
另外这样这样做还有其他性能好处,本地事务执行完可以立即释放本地事务锁定的资源,然后向 TC 上报分支状态。当 TM 决议全局提交时,就不需要同步调用 RM 做什么处理,委托 TC 异步调度各个 RM 分支事务删除对应的 undo log 日志即可,这个步骤非常快速地可以完成;但当 TM 决议全局回滚时,委托 TC 同步向 RM 发送回滚请求,RM 通过 XID 找到对应的 undo log 回滚日志,然后执行回滚日志完成回滚操作。
基于篇幅长度考虑,本篇先介绍AT模式下,RM在1阶段的核心能力。
三、Seata AT 模式 RM 的底层实现
3.1 关键类能力简述
1) DataSourceProxy
- 构建并注册 Resource 信息
- 初始化表的元数据信息,维前后镜像构建和二阶段回滚提供基础能力。
2)ConnectionProxy
提供增强版的 commit,增加的逻辑分两类:
- 若上下文中绑定当前全局事务的 xid,处理分支事务提交
- 向 TC 注册分支事务、使用本地事务提交业务 SQL 和 undo_log、向 TC 上报本地 commit 结果;
- 若上下文中绑定是否需要检测全局锁,处理带@GlobalLock 的本地事务提交
- 检测全局锁不存在则提交本地事务
若业务层还显式的开启了 JDBC 的事务,则提交中还有锁冲突重试机制。
3) StatmentProxy
- 解析 SQL,根据不同的 SQL 类型委托不同的执行器,构建前后镜像生成 undo_log 放置在上下文中。
- 若业务层未显式的开启 JDBC 的事务,则开启重试机制,并在执行完第一步之后,调用 ConnectionProxy 的增强版提交;
- 若业务层显式的开启 JDBC 的事务,则没有第 2 步的自动提交
3.2 鸟瞰分支事务的 1 阶段处理
Seata AT 模式下,如下图的源码检索结果可知,分支事务的执行是在 StatementProxy
、PreparedStatementProxy
的 execute
、executeQuery
、executeUpdate
等方法中的,而这些方法最终都会执行到 ExecuteTemplate#execute
方法
所以StatementProxy
和 PreparedStatementProxy
中是委托ExecuteTemplate
实现增强,执行分支事务的一阶段逻辑
下边使用伪代码,对照官方原理图,从宏观视角来描述以下分支事务的一阶段逻辑:获取链接,构建Statement
,之后执行 SQL 解析、根据 SQL 类型构建执行器,由执行器在业务 SQL 执行前后的数据快照查询并组织成 UndoLog;在提交环节有分支事务注册、UndoLog 写入并随本地事务一起 Commit、分支事务状态上报等;若遇到异常会执行本地回滚,上报异常;最后释放资源。
conProxy = mybatis#getConnection()
pareparedStatement = conProxy.PareparedStatement();
pareparedStatementProxy.execute();
ExecuteTemplate.execute
解析SQL构建 xxxExecutor,如 update 对应为 UpdateExecutor
如果autoCommit为true,设置autoCommit为false
LockRetryPolicy.execute//重试策略
AbstractDMLBaseExecutor#executeAutoCommitFalse()
beforImage()//构建前镜像
execute()//执行业务sql
afterImage()//构建后镜像
connectionProxy.commit // 增强版的提交事务
try
doCommit
register()//向TC注册分支事务,TC会检测全局锁
flushUndoLogs//undo_log刷盘
try
targetConnection.commit();//使用原始con提交本地事务
catch
report : PhaseOne_Failed //本地commit失败,向TC上报1阶段失败,抛出异常
report PhaseOne_Done //向TC 上报 本地commit成功
catch //捕获到异常就进行回滚
doRollback
targetConnection.rollback();// 执行本地回滚
report : PhaseOne_Failed //跟TC上报本地commit失败,这里似乎会重复report
pareparedStatement.close()
con.close()
3.3 详解分支事务的 1 阶段处理
1)基于执行器的设计
如果了解过 mybatis 源码,会有印象其中关键类的命名和执行流程是 xxxTemplate
-调用-> yyyExecutor
;Seata 中的设计实现也很相似,是 ExecuteTemplate
-调用-> xxxExecutor
。
ExecuteTemplate
分析上下文,构建正确的Executor
Executor
的职责- 首先判断若当前上下文与 Seata 无关(当前即不是 AT 模式的分支事务,又不用检测全局锁),直接使用原始的
Statment
执行,避免因引入 Seata 导致非全局事务中的 SQL 执行性能下降。 - 解析 SQL,解析结果有缓存,因为有些 SQL 解析会比较耗时,可能会导致在应用启动后刚开始的那段时间里处理全局事务中的 SQL 执行效率降低。
- 对于 INSERT、UPDATE、DELETE、SELECT..FOR UPDATE 等这几大类的 sql 使用对应的
Executor
进行处理,其它 SQL 直接使用原始的 Statment 执行。 - 返回执行结果,如有异常则直接抛给上层业务代码进行处理。
- 首先判断若当前上下文与 Seata 无关(当前即不是 AT 模式的分支事务,又不用检测全局锁),直接使用原始的
2)解析 sql,构建 sql 执行器
目前 Seata 1.6.1 版本中 根据 sql 的的类型封装了如INSERT
、UPDATE
、DELETE
、SELECT_FOR_UPDATE
、INSERT_ON_DUPLICATE_UPDATE
、UPDATE_JOIN
这六大类Executor
(执行器)。但从事务处理的能力上有分为 3 大类
- PlainExecutor
其中 PlainExecutor 是 原生的 JDBC 接口实现,未做任何处理,提供给全局事务中的普通的 select 查询使用
- SelectForUpdateExecutor:
Seata 的 AT 模式在本地事务之上默认支持读未提交的隔离级别,但是通过 SelectForUpdateExecutor 执行器,可以支持读已提交的隔离级别。
前面的文章我们说过用 select for update 语句来保证隔离级别为读已提交。SelectForUpdateExecutor 就是用来执行 select for update 语句的。
先通过 select 检索记录,构建出 lockKeys 发给 TC,请 TC 核实这些记录是否已经被其他事务加锁了,如果被加锁了,则根据重试策略不断重试,如果没被加锁,则正常返回查询的结果。
- DML 类的 Executor
DML 增删改类型的执行器主要在 sql 执行的前后对 sql 语句进行解析,并实现了如下两个抽象接口:
protected abstract TableRecords beforeImage() throws SQLException;
protected abstract TableRecords afterImage(TableRecords beforeImage) throws SQLException;
这两个接口便是 AT 模式下 RM 的核心能力:构建 beforeImage
,执行 sql,之后再构建 afterImag
e,通过beforeImage
和 afterImage
生成了提供回滚操作的 undo_log
日志,不同的执行器这两个接口的实现不同。
类型 | 构建前镜像 | 构建后镜像 |
---|---|---|
insert | 否 | 是 |
update | 是 | 是 |
delete | 是 | 否 |
其中构建 update
和 delete
这两类前镜像的 sql 语句的是select ... for update
,其中for update
是一种非常必要的基于本地事务排它机制的隔离保障。
3)执行器的核心方法execute
上下文中设置关键的标识信息:在ConnectionProxy
中设置全局事务的 xid,则标识后续执行分支事务;如果RootContext.requireGlobalLock()
是true
,则标识后续是处理@GlobalLock
的全局锁检测+本地事务提交。
public T execute(Object... args) throws Throwable {
// 从事务上下文中获取xid
String xid = RootContext.getXID();
if (xid != null) {
// 将xid绑定到连接的 ConnectionContext 中,由此分支事务感知其所归属的全局事务的xid
statementProxy.getConnectionProxy().bind(xid);
}
// 从上下文中获取是否需要全局锁的标记,传递给ConnectionProxy
statementProxy.getConnectionProxy().setGlobalLockRequire(RootContext.requireGlobalLock());
// 处理sql
return doExecute(args);
}
DML 类 executor 的核心逻辑在 AbstractDMLBaseExecutor#doExecute
中,这里根据是否有开启 Spring 事务而处理逻辑不通。executeAutoCommitTrue
中会自动提交。而executeAutoCommitFalse
中不执行提交,而由 Spring 事务发起commit
(调用的是ConnectionProxy
增强版的commit
)
public T doExecute(Object... args) throws Throwable {
AbstractConnectionProxy connectionProxy = statementProxy.getConnectionProxy();
if (connectionProxy.getAutoCommit()) {
// 如果AutoCommit是true,没有开启spring事务(即没有con.setAutoCommit(false)的调用)
return executeAutoCommitTrue(args);
} else {
// 如果AutoCommit是false
// 目前已知的情况是由于显示开启了事务,保障多条SQL语句的执行只在最后显式的commit提交后,才生效,
// 如声明式Spring事务@Transactional,其处理过程会由con.setAutoCommit(false);
// 如果是编程式Spring事务,需要显示调用con.setAutoCommit(false);
return executeAutoCommitFalse(args);
}
}
4) executeAutoCommitTrue
执行业务 sql,构建undo_log
并自省增强版的提交。如果有 Spring 事务开启(AutoCommit
设置为false
),则不执行这个方法,其中有 3 个关键逻辑
- 执行此方法时, Seata 框架将
AutoCommit
设置为false
,在 2.2 中主动commit
- 目的是 2.1 和 2.2 两个步骤中的所有本地 sql 同时提交,简单理解就是 业务 sql 和 Seata 框架的 undo_log 一起提交。
- 提交过程可能遇到锁冲突,在遇到锁冲突时,会有重试策略,重试逻辑中有 2 个逻辑主体:
- 2.1. 业务 sql 的执行(构造前后镜像)
- 2.2. 增强版
commit
(此时,其内部的重试策略无效),下述逻辑根据上下文是三选一- 2.2.1
processGlobalTransactionCommit()
;- 执行分支事务的提交,向 TC 申请行锁,锁冲突则进入重试逻辑
- 不冲突执行注册分支事务,提交本地事务,向 TC 上报结果
- 2.2.2
processLocalCommitWithGlobalLocks()
;- 申请到全局锁后执行本地提交,这种情况下还需要构造前后镜像嘛?
- 2.2.3
targetConnection.commit()
;- 直接提交本地事务
- 2.2.1
- 无论第 2 步成功还是失败,重置上下文,恢复自动提交
第 2 步遇冲突则重试的机制在介绍完 2.1 和 2.2 的主体逻辑后,再补充
protected T executeAutoCommitTrue(Object[] args) throws Throwable {
ConnectionProxy connectionProxy = statementProxy.getConnectionProxy();
try {
// AutoCommit设置的false,
// 目的是 2.1 和 2.2 两个步骤中的所有本地sql同时提交,简单理解就是 业务sql 和 Seata 框架的undo_log一起提交。
connectionProxy.changeAutoCommit();
// 2. 提交过程可能遇到锁冲突,在遇到锁冲突时,会有重试策略,重试逻辑中有2个逻辑主体:
return new LockRetryPolicy(connectionProxy).execute(() -> {
// 2.1 业务sql的执行(构造前后镜像)
T result = executeAutoCommitFalse(args);
// 2.2 commit(此时,其内部的重试策略无效),下述逻辑根据上下文是三选一
// 2.2.1 processGlobalTransactionCommit();
// 执行分支事务的提交,向TC申请行锁,锁冲突则进入重试逻辑
// 不冲突执行注册分支事务,提交本地事务,向TC上报结果
// 2.2.2 processLocalCommitWithGlobalLocks();
// 申请到全局锁后执行本地提交,这种情况下还需要构造前后镜像嘛?
// 2.2.3 targetConnection.commit();
// 直接提交本地事务
connectionProxy.commit();
return result;
});
} catch (Exception e) {
// when exception occur in finally,this exception will lost, so just print it here
LOGGER.error("execute executeAutoCommitTrue error:{}", e.getMessage(), e);
// isLockRetryPolicyBranchRollbackOnConflict() 默认是true,冲突时会重试,则不在这里回滚
if (!LockRetryPolicy.isLockRetryPolicyBranchRollbackOnConflict()) {
connectionProxy.getTargetConnection().rollback();
}
throw e;
} finally {
// 重置上下文
connectionProxy.getContext().reset();
// 设置为自动提交
connectionProxy.setAutoCommit(true);
}
}
5)executeAutoCommitFalse
执行业务 sql,生成前后镜像融合成 undoLog,注意此时不提交。
protected T executeAutoCommitFalse(Object[] args) throws Exception {
// 构造beforeImage
TableRecords beforeImage = beforeImage();
// 使用原始Statement 执行sql
T result = statementCallback.execute(statementProxy.getTargetStatement(), args);
// 构建afterImage
TableRecords afterImage = afterImage(beforeImage);
// 整合 beforeImage 和 afterImage 构建undoLog
prepareUndoLog(beforeImage, afterImage);
// 返回业务sql的执行结果,并未commit
return result;
}
5.1)整合 beforeImage 和 afterImage 构建 undoLog
- 根据前后镜像构建 锁 key 和 undoLog,暂存到 connectionProxy 的上下文中,在下文
commit
方法中才刷盘 - 锁 key 的构建有其规则,形如 tuser:1_a,2_b 。其中 t_user 是表名,第 1 条记录的主键是 1 和 a,第 2 条记录的逐渐是 2 和 b;即一条记录的多个主键值之间用串联 ;记录和记录之间的 key 信息用,串联;表名和主键部分用:串联
- 如果是 DELETE 语句,则使用前镜像构建锁 key
protected void prepareUndoLog(TableRecords beforeImage, TableRecords afterImage) throws SQLException {
if (beforeImage.getRows().isEmpty() && afterImage.getRows().isEmpty()) {
return;
}
if (SQLType.UPDATE == sqlRecognizer.getSQLType()) {
if (beforeImage.getRows().size() != afterImage.getRows().size()) {
throw new ShouldNeverHappenException("Before image size is not equaled to after image size, probably because you updated the primary keys.");
}
}
ConnectionProxy connectionProxy = statementProxy.getConnectionProxy();
// 如果是DELETE 语句,则使用前镜像构建锁key,
TableRecords lockKeyRecords = sqlRecognizer.getSQLType() == SQLType.DELETE ? beforeImage : afterImage;
// 一条记录的多个主键值之间用_串联;记录之间的key信息用,串联;
// 形如 t_user:1_a,2_b,第1条记录的主键是1和a,第2条记录的逐渐是2和b
String lockKeys = buildLockKey(lockKeyRecords);
if (null != lockKeys) {
// lockKeys暂存到connectionProxy的上下文中
connectionProxy.appendLockKey(lockKeys);
// 整合 beforeImage 和 afterImage 构建undoLog
SQLUndoLog sqlUndoLog = buildUndoItem(beforeImage, afterImage);
// undoLog暂存到connectionProxy的上下文中
connectionProxy.appendUndoLog(sqlUndoLog);
}
}
6)connectionProxy.commit()
增强版的提交
6.1)connectionProxy.commit()
中的重试
此处的重试很容易让人迷糊,根据上文的源码梳理可知,重试逻辑在数据源的代理中有两处。但需清楚这两个重试是互补的,即同一流程中只会有其中一个重试策略生效。
-
如果业务 SQL 的执行上下文中,没有 Spring 的事务,那么
AbstractDMLBaseExecutor.executeAutoCommitTrue
中的重试策略生效,则此处的重试策略不启用 -
如果业务 SQL 的执行上下文中,有 Spring 的事务,那么此处的重试策略生效,而
AbstractDMLBaseExecutor.executeAutoCommitTrue
不会被执行 -
此处重试所包裹的逻辑中 只有
doCommit
,没有业务 SQL 的执行以及前后镜像的构建
public void commit() throws SQLException {
try {
// 这里的重试 只有 doCommit,没有业务SQL的执行以及前后镜像的构建
// 重试策略在数据源的代理中从代码上看是有两处,这两个重试是互补的,也即同一流程中只会有其中一个重试生效。
// 首先如果业务SQL的执行上下文中,没有Spring的事务,那么AbstractDMLBaseExecutor.executeAutoCommitTrue 中的重试策略生效,则此处的重试策略不启用
// 首先如果业务SQL的执行上下文中,有Spring的事务,那么此处的重试策略生效,而 AbstractDMLBaseExecutor.executeAutoCommitTrue 不会被执行
lockRetryPolicy.execute(() -> {
doCommit();
return null;
});
} catch (SQLException e) {
// 没有自动提交,也没有被Seata调整为非自动提交(没有执行AbstractDMLBaseExecutor.executeAutoCommitTrue)
// 那么遇到Seata 增强逻辑中抛出的 SQLException 异常时,在此处执行回滚。并且抛出异常
// 否则,是由上层发起回滚。
if (targetConnection != null && !getAutoCommit() && !getContext().isAutoCommitChanged()) {
rollback();
}
throw e;
} catch (Exception e) {
throw new SQLException(e);
}
}
LockRetryController#sleep
方法中控制 重试次数(内部变量--) 和 重试间隔(普通的 sleep(xxx)),超过次数抛出异常,退出循环。
public void sleep(Exception e) throws LockWaitTimeoutException {
// prioritize the rollback of other transactions
// 重试次数控制
if (--lockRetryTimes < 0 || (e instanceof LockConflictException
&& ((LockConflictException)e).getCode() == TransactionExceptionCode.LockKeyConflictFailFast)) {
throw new LockWaitTimeoutException("Global lock wait timeout", e);
}
try {
// 通过sleep控制重试间隔
Thread.sleep(lockRetryInterval);
} catch (InterruptedException ignore) {
}
}
是否重试是有开关的,在启动时读取配置,从 1.6.1 的代码来看,未支持运行期变更,默认值是 true
// 在冲突时是否重试的开关
client.rm.lock.retry-policy-branch-rollback-on-conflict=true
重试间隔和次数可在通过配置中心做运行时变更,默认值如下:
// 重试间隔 lockRetryInterval
client.rm.lock.retry-interval=10
// 重试次数 lockRetryTimes
client.rm.lock.retry-times=30
6.2)doCommit()
中的 3 种选择
增强版的提交代码中有下述三种提交逻辑,根据上下文只选其一
- processGlobalTransactionCommit();
- 执行分支事务的提交,向 TC 申请行锁,锁冲突则向上反馈后进入上层的重试逻辑
- 不冲突执行注册分支事务,提交本地事务,向 TC 上报结果
- processLocalCommitWithGlobalLocks();
- 申请到全局锁后执行本地提交,这种情况下还需要构造前后镜像嘛?
- targetConnection.commit();
- 直接提交本地事务
ConnectionProxy#doCommit
源码如下:
private void doCommit() throws SQLException {
// xid不为空
// 如果 BaseTransactionalExecutor.execute 中 通过 statementProxy.getConnectionProxy().bind(xid) 在context绑定了xid
// 其内部是 context.bind(xid); 那么此处context.inGlobalTransaction() = true
// 则执行增强版的分支事务提交
if (context.inGlobalTransaction()) {
processGlobalTransactionCommit();
}
// 如果开发者使用@GlobalLock,则 BaseTransactionalExecutor.execute 中
// 通过statementProxy.getConnectionProxy().setGlobalLockRequire(RootContext.requireGlobalLock())
// 在context绑定了全局锁标识,那么此处context.isGlobalLockRequire() = true
// 则执行增强版的检测不到全局锁才做本地事务提交
else if (context.isGlobalLockRequire()) {
//申请到全局锁后执行本地提交
processLocalCommitWithGlobalLocks();
} else {
// 既不是分支事务,又不是@Globallock,那使用原生的本地事务提交
targetConnection.commit();
}
}
6.3)processGlobalTransactionCommit
处理分支事务的提交
前文BaseTransactionalExecutor#execute
中如果识别出有全局事务的 xid,则给ConnectionProxy
的ConnectionContext
上绑定 xid,表明数据源代理层是要做分支事务的处理。
所以如果那么此处context.inGlobalTransaction()
就等于 true,则通过processGlobalTransactionCommit
处理分支事务的提交,在这个方法中是分支事务处理核心中的核心:
- 注册分支事务,申请全局行锁,如果锁冲突则抛出异常,重试机制识别到冲突的异常后做重试处理
- undo_log 刷盘
- 执行本地事务提交,会将本地业务 sql 和 undo_log 一起提交
- 将本地事务提交的结果(1 阶段的处理结果)上报给 TC,TC 若在二阶段回滚,而分支事务上报的是 1 阶段失败了,则无需通知此分支事务做 2 阶段回滚;否则通知分支事务做 2 阶段回滚
- 重置上下文
private void processGlobalTransactionCommit() throws SQLException {
try {
// 1. 注册分支事务,申请全局行锁,如果锁冲突则抛出异常
// 有没有重复注册的情况呢?
register();
} catch (TransactionException e) {
// 如果异常code是 LockKeyConflict 和 LockKeyConflictFailFast 才重新组织抛出异常 LockConflictException
// 外部的重试管控,识别出LockConflictException后实施重试。
// 其他异常此处不处理
recognizeLockKeyConflictException(e, context.buildLockKeys());
}
try {
// 2. 写undo_log
UndoLogManagerFactory.getUndoLogManager(this.getDbType()).flushUndoLogs(this);
// 3. 执行本地事务提交,将本地业务sql和undo_log一起提交
targetConnection.commit();
} catch (Throwable ex) {
LOGGER.error("process connectionProxy commit error: {}", ex.getMessage(), ex);
// 4. 向TC上报异常,并抛出SQLException,告诉TC二阶段若回滚则此分支事务无需回滚,因为1阶段失败了。
report(false);
throw new SQLException(ex);
}
if (IS_REPORT_SUCCESS_ENABLE) {
// 4. 上报事务处理结果,告诉TC二阶段若回滚则此分支事务必须回滚,因为1阶段成功了。
report(true);
}
// 5. 重试上下文
context.reset();
}
6.4)向 TC 注册分支事务,并申请全局行锁,如果全局行锁申请成功才意味着注册成功,返回分支事务 ID,存储在上下文中。
private void register() throws TransactionException {
// 不需要回滚,或不需要全局锁,就不注册
if (!context.hasUndoLog() || !context.hasLockKey()) {
return;
}
// 向TC发送 BranchRegisterRequest 请求
Long branchId = DefaultResourceManager.get().branchRegister(BranchType.AT, getDataSourceProxy().getResourceId(),
null, context.getXid(), context.getApplicationData(),
context.buildLockKeys());
// 将branchId绑定到上下文中,同一时刻,一个con上只有一个分支事务
context.setBranchId(branchId);
}
6.5)若向 TC 注册分支事务时,因行锁冲突导致注册失败,则会抛出锁冲突的异常LockConflictException
,前边提到过重试逻辑中会识别此异常后执行重试,这个重试逻辑就在LockRetryPolicy#doRetryOnLockConflict
中。
protected <T> T doRetryOnLockConflict(Callable<T> callable) throws Exception {
LockRetryController lockRetryController = new LockRetryController();
// 循环
while (true) {
try {
return callable.call();
} catch (LockConflictException lockConflict) {
// 冲突的情况下,执行本地rollback();
onException(lockConflict);
// AbstractDMLBaseExecutor#executeAutoCommitTrue the local lock is released
if (connection.getContext().isAutoCommitChanged()
&& lockConflict.getCode() == TransactionExceptionCode.LockKeyConflictFailFast) {
// 这个转换,目前还未搞清楚用意
lockConflict.setCode(TransactionExceptionCode.LockKeyConflict);
}
// sleep方法里 重试 和 间隔控制;
// 超过次数抛出异常,退出循环
lockRetryController.sleep(lockConflict);
} catch (Exception e) {
onException(e);
throw e;
}
}
}
对于分支事务来说,这其中return callable.call();
对应的就是下图中红框圈注的内容
但需特别注意LockRetryPolicy#onException
这个方法是空的,但AbstractDMLBaseExecutor.LockRetryPolicy
重写了onException
方法,在这个方法中会清除上边重试主体执行过程暂存在上下文中的的锁 key 和 undoLog,并通过原始Connection
执行回滚。
protected void onException(Exception e) throws Exception {
ConnectionContext context = connection.getContext();
//UndoItems can't use the Set collection class to prevent ABA
//清除构建undoLog时,暂存在上下文中的的锁key 和 undoLog
context.removeSavepoint(null);
// 通过原始con 执行回滚
connection.getTargetConnection().rollback();
}
至于本地事务执行过程中发生的其他异常,业务上层会接收到该异常,至于是给 TM 模块返回成功还是失败,由业务上层实现决定,如果返回失败,则 TM 识别到异常后,会裁决对全局事务进行回滚。
四、如果跟 GlobalLock 相关
简单来说RM在数据源代理层的逻辑为
- 向 TC 查询锁是否存在,全局事务的锁还存在就通过抛异常继续重试
- 如果向 TC 查询锁不存在,则提交本地事务。
详情可查看《Seata 之 @GlobalLock 是AT模式下隔离性保障神器之一【保熟】》
五、最后说一句
我是石页兄,如果这篇文章对您有帮助,或者有所启发的话,欢迎关注笔者的微信公众号【 架构染色 】进行交流和学习。您的支持是我坚持写作最大的动力。
另外技术群中也有许多热心的大佬时常交流互助;可扫描下方笔者微信二维码,加 V 备注“加群”,期待并欢迎您的加入。