【Seata源码领读】揭秘 @GlobalTransactional 背后 RM 的黑盒操作之一阶段【保熟】

大家好,我是石页兄,编程界的一名小学生

上善若水,水善利万物而不争

欢迎 关注微信公众号「架构染色」&& 加群交流

一、前奏

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 模式有两个典型的问题:

  1. 一个是数据锁定,XA 事务过程中,数据是被锁定的。XA 的数据锁定是数据库的内部机制维护的,所以依赖 DBA 干预数据库去解除数据锁定。

  2. 另一方面是连接锁定,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

  1. 构建并注册 Resource 信息
  2. 初始化表的元数据信息,维前后镜像构建和二阶段回滚提供基础能力。

2)ConnectionProxy

提供增强版的 commit,增加的逻辑分两类:

  1. 若上下文中绑定当前全局事务的 xid,处理分支事务提交
  • 向 TC 注册分支事务、使用本地事务提交业务 SQL 和 undo_log、向 TC 上报本地 commit 结果;
  1. 若上下文中绑定是否需要检测全局锁,处理带@GlobalLock 的本地事务提交
  • 检测全局锁不存在则提交本地事务

若业务层还显式的开启了 JDBC 的事务,则提交中还有锁冲突重试机制。

3) StatmentProxy

  1. 解析 SQL,根据不同的 SQL 类型委托不同的执行器,构建前后镜像生成 undo_log 放置在上下文中。
  2. 若业务层未显式的开启 JDBC 的事务,则开启重试机制,并在执行完第一步之后,调用 ConnectionProxy 的增强版提交;
  3. 若业务层显式的开启 JDBC 的事务,则没有第 2 步的自动提交

3.2 鸟瞰分支事务的 1 阶段处理

Seata AT 模式下,如下图的源码检索结果可知,分支事务的执行是在 StatementProxyPreparedStatementProxyexecuteexecuteQueryexecuteUpdate 等方法中的,而这些方法最终都会执行到 ExecuteTemplate#execute 方法

所以StatementProxyPreparedStatementProxy 中是委托ExecuteTemplate实现增强,执行分支事务的一阶段逻辑

下边使用伪代码,对照官方原理图,从宏观视角来描述以下分支事务的一阶段逻辑:获取链接,构建Statement,之后执行 SQL 解析、根据 SQL 类型构建执行器,由执行器在业务 SQL 执行前后的数据快照查询并组织成 UndoLog;在提交环节有分支事务注册、UndoLog 写入并随本地事务一起 Commit、分支事务状态上报等;若遇到异常会执行本地回滚,上报异常;最后释放资源。

image.png

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

  1. ExecuteTemplate 分析上下文,构建正确的 Executor
  2. Executor 的职责
    • 首先判断若当前上下文与 Seata 无关(当前即不是 AT 模式的分支事务,又不用检测全局锁),直接使用原始的 Statment 执行,避免因引入 Seata 导致非全局事务中的 SQL 执行性能下降。
    • 解析 SQL,解析结果有缓存,因为有些 SQL 解析会比较耗时,可能会导致在应用启动后刚开始的那段时间里处理全局事务中的 SQL 执行效率降低。
    • 对于 INSERT、UPDATE、DELETE、SELECT..FOR UPDATE 等这几大类的 sql 使用对应的Executor进行处理,其它 SQL 直接使用原始的 Statment 执行。
    • 返回执行结果,如有异常则直接抛给上层业务代码进行处理。

image.png

2)解析 sql,构建 sql 执行器

目前 Seata 1.6.1 版本中 根据 sql 的的类型封装了如INSERTUPDATEDELETESELECT_FOR_UPDATEINSERT_ON_DUPLICATE_UPDATEUPDATE_JOIN 这六大类Executor(执行器)。但从事务处理的能力上有分为 3 大类

  1. PlainExecutor

其中 PlainExecutor 是 原生的 JDBC 接口实现,未做任何处理,提供给全局事务中的普通的 select 查询使用

  1. SelectForUpdateExecutor:

Seata 的 AT 模式在本地事务之上默认支持读未提交的隔离级别,但是通过 SelectForUpdateExecutor 执行器,可以支持读已提交的隔离级别。

前面的文章我们说过用 select for update 语句来保证隔离级别为读已提交。SelectForUpdateExecutor 就是用来执行 select for update 语句的。

先通过 select 检索记录,构建出 lockKeys 发给 TC,请 TC 核实这些记录是否已经被其他事务加锁了,如果被加锁了,则根据重试策略不断重试,如果没被加锁,则正常返回查询的结果。

image.png

  1. DML 类的 Executor

DML 增删改类型的执行器主要在 sql 执行的前后对 sql 语句进行解析,并实现了如下两个抽象接口:

protected abstract TableRecords beforeImage() throws SQLException;

protected abstract TableRecords afterImage(TableRecords beforeImage) throws SQLException;

这两个接口便是 AT 模式下 RM 的核心能力:构建 beforeImage,执行 sql,之后再构建 afterImage,通过beforeImageafterImage 生成了提供回滚操作的 undo_log 日志,不同的执行器这两个接口的实现不同。

类型 构建前镜像 构建后镜像
insert
update
delete

其中构建 updatedelete 这两类前镜像的 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 个关键逻辑

  1. 执行此方法时, Seata 框架将 AutoCommit设置为false,在 2.2 中主动 commit
    • 目的是 2.1 和 2.2 两个步骤中的所有本地 sql 同时提交,简单理解就是 业务 sql 和 Seata 框架的 undo_log 一起提交。
  2. 提交过程可能遇到锁冲突,在遇到锁冲突时,会有重试策略,重试逻辑中有 2 个逻辑主体:
  • 2.1. 业务 sql 的执行(构造前后镜像)
  • 2.2. 增强版commit(此时,其内部的重试策略无效),下述逻辑根据上下文是三选一
    • 2.2.1 processGlobalTransactionCommit();
      • 执行分支事务的提交,向 TC 申请行锁,锁冲突则进入重试逻辑
      • 不冲突执行注册分支事务,提交本地事务,向 TC 上报结果
    • 2.2.2 processLocalCommitWithGlobalLocks();
      • 申请到全局锁后执行本地提交,这种情况下还需要构造前后镜像嘛?
    • 2.2.3 targetConnection.commit();
      • 直接提交本地事务
  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

  1. 根据前后镜像构建 锁 key 和 undoLog,暂存到 connectionProxy 的上下文中,在下文commit方法中才刷盘
  2. 锁 key 的构建有其规则,形如 tuser:1_a,2_b 。其中 t_user 是表名,第 1 条记录的主键是 1 和 a,第 2 条记录的逐渐是 2 和 b;即一条记录的多个主键值之间用串联 ;记录和记录之间的 key 信息用,串联;表名和主键部分用:串联
  3. 如果是 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 种选择

增强版的提交代码中有下述三种提交逻辑,根据上下文只选其一

  1. processGlobalTransactionCommit();
  • 执行分支事务的提交,向 TC 申请行锁,锁冲突则向上反馈后进入上层的重试逻辑
  • 不冲突执行注册分支事务,提交本地事务,向 TC 上报结果
  1. processLocalCommitWithGlobalLocks();
  • 申请到全局锁后执行本地提交,这种情况下还需要构造前后镜像嘛?
  1. 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,则给ConnectionProxyConnectionContext上绑定 xid,表明数据源代理层是要做分支事务的处理。

所以如果那么此处context.inGlobalTransaction()就等于 true,则通过processGlobalTransactionCommit处理分支事务的提交,在这个方法中是分支事务处理核心中的核心:

  1. 注册分支事务,申请全局行锁,如果锁冲突则抛出异常,重试机制识别到冲突的异常后做重试处理
  2. undo_log 刷盘
  3. 执行本地事务提交,会将本地业务 sql 和 undo_log 一起提交
  4. 将本地事务提交的结果(1 阶段的处理结果)上报给 TC,TC 若在二阶段回滚,而分支事务上报的是 1 阶段失败了,则无需通知此分支事务做 2 阶段回滚;否则通知分支事务做 2 阶段回滚
  5. 重置上下文
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 备注“加群”,期待并欢迎您的加入。

参考:

猜你喜欢

转载自juejin.im/post/7234066866271830073
rm
今日推荐