五、connectionProxy分支事务注册

所有文章

https://www.cnblogs.com/lay2017/p/12485081.html

 

正文

上一篇关于DataSourceProxy的文章中,我们看到了一个DataSourceProxy在构造的时候将作为Resource注册到ResourceManager并通过RPC注册到Server端。

而我们知道Connection表示的是与DataSource的通信连接,seata对Connection也进行了代理。DataSourceProxy的getConnection方法中将获取到该代理类。我们看看DataSourceProxy的getConnection方法

@Override
public ConnectionProxy getConnection() throws SQLException {
    Connection targetConnection = targetDataSource.getConnection();
    // 实例化一个代理类
    return new ConnectionProxy(this, targetConnection);
}

可以看到,从DataSourceProxy中get出来的Connection并不是原始对象,而是经过ConnectionProxy代理包装以后的对象。

ConnectionProxy不仅包含了Connection,也包含了当前的DataSourceProxy对象,从而间接包含DataSource。

 

ConnectionProxy的类图

了解ConnectionProxy的方法之前,我们先来看看它的UML类图结构

类图比较简单,AbstractConnectionProxy直接实现Connection接口,且包含了DataSourceProxy和原始Connection的成员变量

public abstract class AbstractConnectionProxy implements Connection {
    /**
     * 数据源代理对象
     */
    protected DataSourceProxy dataSourceProxy;
    /**
     * 原始Connection对象
     */
    protected Connection targetConnection;

    // 省略
}   

ConnectionProxy直接继承于AbstractConnectionProxy,主要扩展了commit方法和rollback方法。

 

commit注册分支事务

我们跟进ConnectionProxy的commit方法

@Override
public void commit() throws SQLException {
    try {
        // 重试
        LOCK_RETRY_POLICY.execute(() -> {
            // 提交
            doCommit();
            return null;
        });
    } catch (SQLException e) {
        throw e;
    } catch (Exception e) {
        throw new SQLException(e);
    }
}

commit方法加上了一个重试执行,跟进doCommit

private void doCommit() throws SQLException {
    if (context.inGlobalTransaction()) {
        processGlobalTransactionCommit();
    } else if (context.isGlobalLockRequire()) {
        processLocalCommitWithGlobalLocks();
    } else {
        targetConnection.commit();
    }
}

这里区分了三种情况,如果没有全局事务,没有全局锁,那么就直接调用原始的commit,提交本地事务即可。

我们这里以全局事务为例,进入processGlobalTransactionCommit看看

private void processGlobalTransactionCommit() throws SQLException {
    try {
        // 注册分支事务
        register();
    } catch (TransactionException e) {
        recognizeLockKeyConflictException(e, context.buildLockKeys());
    }

    try {
        // 如果由undoLog
        if (context.hasUndoLog()) {
            // 插入到undo_log表中
            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);
    }
    // 清除当前ConnectionProxy中的xid、branchId、undoLog buffer之类的,本次提交结束
    context.reset();
}

processGlobalTransactionCommit方法首先将RPC到Server端注册分支事务,Server端返回一个branchId到当前ConnectionContext当中。

如果注册分支事务失败?直接向上抛出异常,由发起全局事务的地方捕获,并进行全局事务的回滚。

如果分支事务顺利注册成功,那么就将undo_log插入到本地的undo_log表中,且提交本地事务。

如果本地事务提交失败,意味着一阶段执行失败,那么就通过report方法通知Server端。

如果成功呢?若开启通知的话,也通过report方法通知一阶段成功。

最后清除当前Connection的上下文事务相关数据,本次结束。

 

最后,再看看report方法吧

private void report(boolean commitDone) throws SQLException {
    int retry = REPORT_RETRY_COUNT;
    while (retry > 0) {
        try {
            DefaultResourceManager.get().branchReport(BranchType.AT, context.getXid(), context.getBranchId(), commitDone ? BranchStatus.PhaseOne_Done : BranchStatus.PhaseOne_Failed, null);
            return;
        } catch (Throwable ex) {
            LOGGER.error("Failed to report [" + context.getBranchId() + "/" + context.getXid() + "] commit done ["
                + commitDone + "] Retry Countdown: " + retry);
            retry--;

            if (retry == 0) {
                throw new SQLException("Failed to report branch status " + commitDone, ex);
            }
        }
    }
}

report中通过ResourceManager把分支事务的执行情况上报,如果本地事务提交,那么就报一阶段完成,如果没有提交,那么就报一阶段提交失败。

 

小结:到这里我们回顾一下ConnectionProxy的commit方法干了啥?其实就是注册branch到global Transaction当中,作为其中一个分支存在。

且本地事务将会直接提交,而不用等待全局事务提交才提交,这样对于资源来说减少了锁定时间。

 

rollback通知

前面我们说commit注册分支事务,并提交本地事务。那么如果执行失败呢?将会调用rollback方法,我们再看看rollback做了啥

@Override
public void rollback() throws SQLException {
    // 回滚本地事务
    targetConnection.rollback();
    // 如果在全局事务当中
    if (context.inGlobalTransaction()) {
        // 如果分支事务已经注册
        if (context.isBranchRegistered()) {
            // 发送一阶段失败通知
            report(false);
        }
    }
    // 清理上下文
    context.reset();
}

rollback中,先是回滚了本地事务。然后判断是否在全局事务当中,且当前分支事务已经提交,如果是的话发送一阶段的回滚到Server端,最后清理上下文。

 

总结

我们看到commit和rollback的实现逻辑里,本地的事务都会预先commit或者rollback,不会等待全局事务一起commit或者rollback。

分支事务如果已经注册成功,那么将会通过ResourceManager来report给Server端一阶段的成功或者失败的结果。那如果分支事务注册不成功呢?将会向上抛出异常,由全局事务的发起者捕获到,并发起全局事务回滚

总得来说,ConnectionProxy里面的commit和rollback尽量在程序正常的情况下进行分支事务的处理,而要保证最终一致性主要还是得由全局事务在Server端的二阶段处理逻辑为主。

 

posted @ 2020-03-12 22:18  __lay  阅读(1108)  评论(0编辑  收藏  举报