Seata Server处理全局事务和分支事务启动

傅青阳一脸黑线,这个马仔太能装了。随手甩出一个阴阳转盘,元始措手不及,忽然间天旋地转,整个人被倒吊在半空,面色涨红。
回答转盘的提问,答对可重新转动指针,积累三次白色,可解除封禁。转盘上面的指针不出意外地停留在黑色面板。
元始心想,难道又是世界上最坚硬的东西?硫化碳块!男高中生!
或者是你所知的诗词中,最廉价的东西是什么?依山尽!

全局事务启动处理

收!想什么呢,我们这里可是正经的技术文章。
提问1:Seata Server如何处理全局事务的启动?
元始:首先我们知道,启动全局事务的请求是:GlobalBeginRequest。所以我们顺着Netty服务端的处理入口,最终可以找到2个关键的接口:
TCInboundHandler:事务相关的事件处理器
RMInboundHandler:资源相关的事件处理器

TCInboundHandler的第一个方法就可以看到全局事务的处理入口:

/**
 * Handle global begin response.
 *
 * @param globalBegin the global begin
 * @param rpcContext  the rpc context
 * @return the global begin response
 */
GlobalBeginResponse handle(GlobalBeginRequest globalBegin, RpcContext rpcContext);

@Override
protected void doGlobalBegin(GlobalBeginRequest request, GlobalBeginResponse response, RpcContext rpcContext)
    throws TransactionException {
    //生成xid
    response.setXid(
            // 关键代码
            core.begin(rpcContext.getApplicationId(), rpcContext.getTransactionServiceGroup(),
        request.getTransactionName(), request.getTimeout()));
    //...
}

然后启动全局事务的处理也很简单:

  1. 创建全局会话GlobalSession
  2. 生成唯一事务ID:ipAddress + ":" + port + ":" + 雪花算法ID
  3. 增加全局会话的生命周期监听器
  4. 全局会话状态修改为已启动,触发生命周期监听器的onBegin事件
  5. 发布事务启动事件
  6. 将事务ID作为返回结果,返回给客户端。

客户端的处理入口在ClientOnResponseProcessor。客户端收到事务ID后,设置全局事务状态为已启动,将事务ID写入线程缓存。

分支事务启动处理

提问2:Seata Server如何处理分支事务的启动?
元始:首先我们都知道JDBC的执行过程:

  1. 获取连接
  2. 创建Statement
  3. Statement执行SQL语句
  4. 连接提交事务或回滚事务

在这里我们贴一个最简单的Demo:

public static void main(String[] args) throws Exception {
    Connection conn = null;
    try{
        conn = DriverManager.getConnection(DB_URL, USER, PASS);
        Statement stmt = conn.createStatement();
        String sql = "UPDATE users SET age = 30 WHERE id in (100, 101)";
        stmt.executeUpdate(sql);
        conn.commit();
    } catch (SQLException e) {
         conn.rollback();
    }finally {
        if (conn!=null){
            conn.close();
        }
    }
}

由于我们的Seata的数据源是一个代理对象DataSourceProxy,从中获取的连接也是代理对象ConnectionProxy,按照上面的SQL执行过程,实际会将业务SQL先在数据库执行完成,然后在使用ConnectionProxy提交或回滚事务。
因此我们需要重点来看看ConnectionProxy#commit() 做了什么事情。

@Override
public void commit() throws SQLException {
    try {
        LOCK_RETRY_POLICY.execute(() -> {
        	// 事务提交
            doCommit();
            return null;
        });
    } catch (SQLException e) {
        // 如果失败了,本地事务回滚
        if (targetConnection != null && !getAutoCommit() && !getContext().isAutoCommitChanged()) {
            rollback();
        }
        throw e;
    } catch (Exception e) {
        throw new SQLException(e);
    }
}
private void doCommit() throws SQLException {
    if (context.inGlobalTransaction()) {
        processGlobalTransactionCommit();
    }//...
}
private void processGlobalTransactionCommit() throws SQLException {
    try {
        // 注册分支事务
        register();
    } //...
    try {
        // 解析SQL并写入undolog
        UndoLogManagerFactory.getUndoLogManager(this.getDbType()).flushUndoLogs(this);
        targetConnection.commit();
    } catch (Throwable ex) {
        LOGGER.error("process connectionProxy commit error: {}", ex.getMessage(), ex);
        // 上报分支事务提交出现异常的情况
        report(false);
        throw new SQLException(ex);
    }
    if (IS_REPORT_SUCCESS_ENABLE) {
        // 上报分支事务提交的情况
        report(true);
    }
    context.reset();
}
private void register() throws TransactionException {
    if (!context.hasUndoLog() || !context.hasLockKey()) {
        return;
    }
    // 注册分支事务
    // context.buildLockKeys() 包含了需要全局锁的主键列表
    Long branchId = DefaultResourceManager.get().branchRegister(BranchType.AT, getDataSourceProxy().getResourceId(),
        null, context.getXid(), null, context.buildLockKeys());
    context.setBranchId(branchId);
}

@Override
public Long branchRegister(BranchType branchType, String resourceId, String clientId, String xid, String applicationData, String lockKeys) throws TransactionException {
    try {
        BranchRegisterRequest request = new BranchRegisterRequest();
        request.setXid(xid);
        request.setLockKey(lockKeys);
        request.setResourceId(resourceId);
        request.setBranchType(branchType);
        request.setApplicationData(applicationData);

        BranchRegisterResponse response = (BranchRegisterResponse) RmNettyRemotingClient.getInstance().sendSyncRequest(request);
        if (response.getResultCode() == ResultCode.Failed) {
            throw new RmTransactionException(response.getTransactionExceptionCode(), String.format("Response[ %s ]", response.getMsg()));
        }
        return response.getBranchId();
    } //...
}

上面贴了一大坨代码,其实逻辑还是很简单的:

  1. 执行事务提交,可重试的;如果事务提交失败则执行事务回滚。
  2. 当前事务作为分支事务注册到Seata Server端。然后解析SQL并写入undolog(这一步在下一篇进行重点剖析),然后将本地事务直接提交,也就是此时在数据库是可以直接看到执行结果的。
  3. 如果本地事务提交失败,则上报分支事务提交出现异常的情况到Server端。然后Server端会执行全局事务回滚。
  4. 组装分支事务注册的参数,其中最重要的参数是将本次业务SQL会改动的数据行的主键集合,因为在Server端会将这些数据行进行锁定,不允许其他分支事务进行修改。
  5. 最后组装成BranchRegisterRequest请求,通过前面建立的网络连接发送到Server端。

然后Server端如何处理BranchRegisterRequest请求呢?
这里就又回到TCInboundHandler 这个事务相关的事件处理器了。跟进代码之后,我直接将关键部分代码贴出来:

@Override
protected void doBranchRegister(BranchRegisterRequest request, BranchRegisterResponse response,
                                RpcContext rpcContext) throws TransactionException {
    response.setBranchId(
            // 关键代码
        core.branchRegister(request.getBranchType(), request.getResourceId(), rpcContext.getClientId(),
            request.getXid(), request.getApplicationData(), request.getLockKey()));
}
@Override
public Long branchRegister(BranchType branchType, String resourceId, String clientId, String xid,
                           String applicationData, String lockKeys) throws TransactionException {
    GlobalSession globalSession = assertGlobalSessionNotNull(xid, false);
    return SessionHolder.lockAndExecute(globalSession, () -> {
        //...
        // 创建分支会话
        BranchSession branchSession = SessionHelper.newBranchByGlobal(globalSession, branchType, resourceId,
                applicationData, lockKeys, clientId);
        MDC.put(RootContext.MDC_KEY_BRANCH_ID, String.valueOf(branchSession.getBranchId()));
        // 分支事务需要的全局锁在这里进行锁定
        branchSessionLock(globalSession, branchSession);
        try {
            // 加入全局会话
            globalSession.addBranch(branchSession);
        } catch (RuntimeException ex) {
            branchSessionUnlock(branchSession);
            //...
        }
        //...
        return branchSession.getBranchId();
    });
}

@Override
public boolean lock() throws TransactionException {
    if (this.getBranchType().equals(BranchType.AT)) {
        // 申请全局锁
        return LockerManagerFactory.getLockManager().acquireLock(this);
    }
    return true;
}
protected List<RowLock> collectRowLocks(String lockKey, String resourceId, String xid, Long transactionId,
                                            Long branchID) {
    List<RowLock> locks = new ArrayList<RowLock>();

    String[] tableGroupedLockKeys = lockKey.split(";");
    for (String tableGroupedLockKey : tableGroupedLockKeys) {
        int idx = tableGroupedLockKey.indexOf(":");
        if (idx < 0) {
            return locks;
        }
        String tableName = tableGroupedLockKey.substring(0, idx);
        String mergedPKs = tableGroupedLockKey.substring(idx + 1);
        if (StringUtils.isBlank(mergedPKs)) {
            return locks;
        }
        String[] pks = mergedPKs.split(",");
        if (pks == null || pks.length == 0) {
            return locks;
        }
        for (String pk : pks) {
            if (StringUtils.isNotBlank(pk)) {
                // 数据行锁,锁住的是主键集合
                RowLock rowLock = new RowLock();
                rowLock.setXid(xid);
                rowLock.setTransactionId(transactionId);
                rowLock.setBranchId(branchID);
                rowLock.setTableName(tableName);
                rowLock.setPk(pk);
                rowLock.setResourceId(resourceId);
                locks.add(rowLock);
            }
        }
    }
    return locks;
}

Server端的处理过程如下:

  1. 首先创建改分支的会话。
  2. 然后针对本次要修改的数据行申请全局锁定。
  3. 所谓的数据行锁,其实就是将表的主键列表记录下来。如果后续的分支事务提交中包含了其中的主键,则返回加锁失败。
  4. 将分支会话加入到全局会话,如果出现回滚,则可以从全局会话取出所有分支会话进行回调。

SELECT FOR UPDATE的特殊处理

提问3:前面提到了业务SQL在注册分支事务之后,其实将本地的数据库事务提交了的,这时候是可以看到SQL的执行结果的。但是我们站在全局事务来看,这个时候的事务隔离其实是最差的读未提交的。所以为了解决这个问题,Seata对 SELECT FOR UPDATE 做了什么特殊处理呢?

元始:问得好,这里我们就需要回到SelectForUpdateExecutor 这个Select for update 的具体SQL执行器,在执行器提交事务之前有个循环处理

String selectPKSQL = buildSelectSQL(paramAppenderList);
while (true) {
     try {
         // #870
         // execute return Boolean
         // executeQuery return ResultSet
         rs = statementCallback.execute(statementProxy.getTargetStatement(), args);

         // Try to get global lock of those rows selected
         // 尝试为选择的行获取全局锁
         TableRecords selectPKRows = buildTableRecords(getTableMeta(), selectPKSQL, paramAppenderList);
         // 构建锁的key
         String lockKeys = buildLockKey(selectPKRows);
         if (StringUtils.isNullOrEmpty(lockKeys)) {
             break;
         }

         if (RootContext.inGlobalTransaction() || RootContext.requireGlobalLock()) {
             // Do the same thing under either @GlobalTransactional or @GlobalLock, 
             // that only check the global lock  here.
             // 检查是否拿到全局锁
             statementProxy.getConnectionProxy().checkLock(lockKeys);
         } else {
             throw new RuntimeException("Unknown situation!");
         }
         break;
     } catch (LockConflictException lce) {
         // 回滚本地事务
         if (sp != null) {
             conn.rollback(sp);
         } else {
             conn.rollback();
         }
         // trigger retry
         // 重试获取全局锁
         lockRetryController.sleep(lce);
     }
 }

简单来说就是不断重试,如果重试一定次数还未拿到全局锁,就会退出循环,宣布此次加锁失败。

元始:来来来,再给你讲个故事:
从前有座山,山里有座庙,庙里有个老和尚,长的真是俏!俏也不争春,只把春来报,待到山花烂漫时,他在丛中笑!哈哈哈!

好了干正事,我来贴一下目前我们了解的分布式事务的全貌:
Seata Server处理全局和分支事务启动

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Gemini技术窝

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值