Seata TCC源码剖析

1.概念

首先我们需要了解什么是TCC?

TCC 是分布式事务中的二阶段提交协议,它的全称为 Try-Confirm-Cancel,即资源预留(Try)、确认操作(Confirm)、取消操作(Cancel),他们的具体含义如下:

  1. Try:对业务资源的检查并预留;
  2. Confirm:对业务处理进行提交,即 commit 操作,只要 Try 成功,那么该步骤一定成功;
  3. Cancel:对业务处理进行取消,即回滚操作,该步骤回对 Try 预留的资源进行释放。

这是一种侵入性很高的分布式事务解决方案,上述三个操作需要业务方自己来实现,对业务系统的侵入性非常大,设计也比较复杂,但是有点在于不依赖数据库,能够实现跨数据库、跨应用资源管理,对这些不同数据访问通过侵入式的编码方式实现一个原子操作,更好地解决了在各种复杂业务场景下的分布式事务问题。

2.实际场景

在目前楼主公司的软件架构体系下,对于分布式事务领域暂无对应的解决方案。涉及多组件间的数据一致性问题常常需要开发者自己在业务逻辑层面实现兜底方案,例如引入MQ、消息表等方法来实现最终一致性,需要在业务逻辑外增加不少的开发,最重要的是还常常无法真正实现数据一致性,少数场景还是出现数据不一致的情况。

普通业务场景可以容忍少量的数据不一致,但是例如食堂消费平台这类涉及到金钱的业务那么就是零容忍的,现行较为流行的方案是TCC,整体是 两阶段提交 的模型。

目前市面上开源方案较少,包括tcc-transactionByteTCCseata等等,目前相对比较成熟的是阿里开源的seata(Seata并不完全是一个TCC事务框架,同时支持AT、TCC、XA、Saga模式),这个框架是经历过阿里生产环境的大量考验,同时也支持dubbo、spring cloud,在ORM框架支持层面,Seata 虽然是保证数据一致性的组件,但对于 ORM 框架并没有特殊的要求,像主流的Mybatis,Mybatis-Plus,Spring Data JPA, Hibernate等都支持。这是因为ORM框架位于JDBC结构的上层,而 Seata 的 AT,XA 事务模式是对 JDBC 标准接口操作的拦截和增强。

3.简单使用

假设现有一个业务需要同时使用服务 A 和服务 B 完成一个事务操作,我们在服务 A 定义该服务的一个 TCC 接口:

@LocalTCC
public interface FirstTccAction {
    /**
     * 一阶段方法
     *
     * @param context   上下文
     * @param accountNo 账户号
     * @param amount    转账金额
     * @return
     */
    @TwoPhaseBusinessAction(name = "firstTccAction", commitMethod = "commit", rollbackMethod = "rollback")
    boolean prepareMinus(BusinessActionContext context,
                         @BusinessActionContextParameter(paramName = "accountNo") String accountNo,
                         @BusinessActionContextParameter(paramName = "amount") Double amount);

    /**
     * 二阶段提交
     *
     * @param context 上下文
     * @return
     */
    boolean commit(BusinessActionContext context);

    /**
     * 二阶段回滚
     *
     * @param context 上下文
     * @return
     */
    boolean rollback(BusinessActionContext context);
}

同样,在服务 B 定义该服务的一个 TCC 接口:

@LocalTCC
public interface SecondTccAction {

    /**
     * 一阶段方法
     *
     * @param context
     * @param accountNo
     * @param amount
     */
    @TwoPhaseBusinessAction(name = "secondTccAction", commitMethod = "commit", rollbackMethod = "rollback")
    boolean prepareAdd(BusinessActionContext context,
                              @BusinessActionContextParameter(paramName = "accountNo") String accountNo,
                              @BusinessActionContextParameter(paramName = "amount") Double amount);

    /**
     * 二阶段提交
     *
     * @param context
     * @return
     */
    boolean commit(BusinessActionContext context);

    /**
     * 二阶段回滚
     *
     * @param context
     * @return
     */
    boolean rollback(BusinessActionContext context);
}

在业务所在系统中开启全局事务并执行服务 A 和服务 B 的 TCC 预留资源方法:

@GlobalTransactional
public String doTransactionCommit(){
    //服务A事务参与者
    firstTccAction.prepare(null,"A");
    //服务B事务参与者
    tccActionTwo.prepare(null,"B");
}

Seata的使用案例可参考官网的使用说明,其中包含了和各种框架的整合,包括Dubbo、nacos、mybatis等等,本文着重分析TCC的源码,更多案例请参考官网demo

4.源码分析

Seata整体架构为Server-Client,Server为独立服务需要单独部署,而Client则作为SDK引入到项目中,各Client通过SDK内置的通信模块与Server进行RPC通信,报告各分支事务的执行情况,供Server参考统一驱动分支事务的二阶段提交或回滚。

所以在进行源码分析时,既要分析Client端源码,也要分析Server端源码。

说明:TCC 模式在 Seata 中也是遵循 TC、TM、RM 三种角色模型的


Seata TCC源码的分析我们需要分两个阶段,即Seata V1.5.1之前以及V1.5.1(包含1.5.1)之后的版本,这是由于在V1.5.1版本中Seata解决了TCC模式中的幂等、悬挂和空回滚等问题,所以源码分析时分别分析。

如图,Seata V1.5.1的feature解决了以上问题:

4.1 V1.4.2

在不支持幂等、悬挂和空回滚版本中我们以v1.4.2为基准分析。

资源管理

在 Seata 启动过程中,这个 GlobalTransactionScanner类会对注解进行扫描,该类的类结构图如下:

可以看见其继承了AbstractAutoProxyCreator类,拥有了动态代理的能力,通过重写AbstractAutoProxyCreator#wrapIfNecessary()方法来实现自定义代理逻辑。

对于需要代理的Bean对象,先判断是否需要TCC代理,其判断逻辑在AbstractedRemotingParser#isRemoting()中,TCC 接口可以是 RPC,也可以是 JVM 内部调用,对于上述demo来说,也就是JVM内部调用,具体实现类为LocalTCCRemotingParser,而该类中的判断逻辑即该Bean所实现接口上是否存在@LocalTCC注解:


所以上述demo中的FirstTccActionSecondTccAction就会被代理,在代理之前会进行资源解析,该模块专门用于解析具有 TwoPhaseBusinessAction 注解的 TCC 接口资源,同时在解析资源后注册资源到TC(Server),默认实现为io.seata.rm.tcc.remoting.parser.DefaultRemotingParser#parserRemotingServiceInfo

/**
     * parse the remoting bean info
     *
     * @param bean           the bean
     * @param beanName       the bean name
     * @param remotingParser the remoting parser
     * @return remoting desc
     */
    public RemotingDesc parserRemotingServiceInfo(Object bean, String beanName, RemotingParser remotingParser) {
        RemotingDesc remotingBeanDesc = remotingParser.getServiceDesc(bean, beanName);
        if (remotingBeanDesc == null) {
            return null;
        }
        remotingServiceMap.put(beanName, remotingBeanDesc);

        Class<?> interfaceClass = remotingBeanDesc.getInterfaceClass();
        Method[] methods = interfaceClass.getMethods();
        if (remotingParser.isService(bean, beanName)) {
            try {
                //service bean, registry resource
                Object targetBean = remotingBeanDesc.getTargetBean();
                for (Method m : methods) {
                    TwoPhaseBusinessAction twoPhaseBusinessAction = m.getAnnotation(TwoPhaseBusinessAction.class);
                    if (twoPhaseBusinessAction != null) {
                        TCCResource tccResource = new TCCResource();
                        tccResource.setActionName(twoPhaseBusinessAction.name());
                        tccResource.setTargetBean(targetBean);
                        tccResource.setPrepareMethod(m);
                        tccResource.setCommitMethodName(twoPhaseBusinessAction.commitMethod());
                        tccResource.setCommitMethod(ReflectionUtil
                            .getMethod(interfaceClass, twoPhaseBusinessAction.commitMethod(),
                                new Class[] {BusinessActionContext.class}));
                        tccResource.setRollbackMethodName(twoPhaseBusinessAction.rollbackMethod());
                        tccResource.setRollbackMethod(ReflectionUtil
                            .getMethod(interfaceClass, twoPhaseBusinessAction.rollbackMethod(),
                                new Class[] {BusinessActionContext.class}));
                        //registry tcc resource
                        DefaultResourceManager.get().registerResource(tccResource);
                    }
                }
            } catch (Throwable t) {
                throw new FrameworkException(t, "parser remoting service error");
            }
        }
        if (remotingParser.isReference(bean, beanName)) {
            //reference bean, TCC proxy
            remotingBeanDesc.setReference(true);
        }
        return remotingBeanDesc;
    }

以上方法,先调用解析类 getServiceDesc 方法对 remoting bean 进行解析,并将解析后的 remotingBeanDesc 放入 本地缓存 remotingServiceMap 中,同时调用解析类 isService 方法判断是否为发起方,如果是发起方,则解析 TwoPhaseBusinessAction 注解内容生成一个 TCCResource,并对其进行资源注册。

事务管理

1.资源注册

Seata TCC 模式的资源叫 TCCResource,其资源管理器叫 TCCResourceManager,前面讲过,当解析完 TCC 接口 RPC 资源后,如果是发起方,则会对其进行资源注册:

io.seata.rm.tcc.TCCResourceManager#registerResource

@Override
public void registerResource(Resource resource) {
    TCCResource tccResource = (TCCResource)resource;
    tccResourceCache.put(tccResource.getResourceId(), tccResource);
    super.registerResource(tccResource);
}

而实际的注册过程是通过AbstractResourceManager#registerResource方法实现的。

public void registerResource(Resource resource) {
    RmNettyRemotingClient.getInstance().registerResource(resource.getResourceGroupId(), resource.getResourceId());
}

其本质上是通过获取RmNettyRemotingClient.getInstance()静态方法获取对应的Netty客户端单例,通过发送注册注册资源的RPC到Server端,供Server端保存该TCC调用的上下文环境,为后续二阶段方法执行提供上下文。


而在Server端,作为RPC的服务端,所有请求都会交由RemotingProcessor接口处理

public interface RemotingProcessor {

    /**
     * Process message
     *
     * @param ctx        Channel handler context.
     * @param rpcMessage rpc message.
     * @throws Exception throws exception process message error.
     */
    void process(ChannelHandlerContext ctx, RpcMessage rpcMessage) throws Exception;
}

当收到客户端的资源注册请求时,会交由其实现类RegRmProcessor来处理。

服务端的处理较为简单,即将RPC的channel以及TCCResource的信息封装起来保存到上下文中即可,也即完成了资源的注册。

2.事务一阶段执行

在Seata的领域模型中,各种模式都遵循相同的事务模型,而对于TCC模式,本质上是把 自定义 的分支事务纳入到全局事务的管理中。

在标记@GlobalTransactional注解的方法会被GlobalTransactionScanner#wrapIfNecessary加入GlobalTransactionalInterceptor拦截器,在代理方法之前前先执行拦截器方法。

最终会执行到TransactionalTemplate#execute

public Object execute(TransactionalExecutor business) throws Throwable {
        // 1. Get transactionInfo
        TransactionInfo txInfo = business.getTransactionInfo();
        if (txInfo == null) {
            throw new ShouldNeverHappenException("transactionInfo does not exist");
        }
        // 1.1 Get current transaction, if not null, the tx role is 'GlobalTransactionRole.Participant'.
        GlobalTransaction tx = GlobalTransactionContext.getCurrent();

        // 1.2 Handle the transaction propagation.
        ...

            // 1.3 If null, create new transaction with role 'GlobalTransactionRole.Launcher'.
            if (tx == null) {
                tx = GlobalTransactionContext.createNew();
            }

            // set current tx config to holder
            GlobalLockConfig previousConfig = replaceGlobalLockConfig(txInfo);

            try {
                // 2. If the tx role is 'GlobalTransactionRole.Launcher', send the request of beginTransaction to TC,
                //    else do nothing. Of course, the hooks will still be triggered.
                beginTransaction(txInfo, tx);

                Object rs;
                try {
                    // Do Your Business
                    rs = business.execute();
                } catch (Throwable ex) {
                    // 3. The needed business exception to rollback.
                    completeTransactionAfterThrowing(txInfo, tx, ex);
                    throw ex;
                }

                // 4. everything is fine, commit.
                commitTransaction(tx);

                return rs;
            } finally {
                //5. clear
                resumeGlobalLockConfig(previousConfig);
                triggerAfterCompletion();
                cleanUp();
            }
        } finally {
            // If the transaction is suspended, resume it.
            if (suspendedResourcesHolder != null) {
                tx.resume(suspendedResourcesHolder);
            }
        }
    }

根据源码可知,作为事务发起方首先会创建一个GlobalTransaction全局事务对象,由其驱动事务的开始、提交与回滚等操作。

在真正执行业务方法之前,先开启全局事务,默认实现为DefaultGlobalTransaction#begin

public void begin(int timeout, String name) throws TransactionException {
    if (role != GlobalTransactionRole.Launcher) {
        assertXIDNotNull();
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("Ignore Begin(): just involved in global transaction [{}]", xid);
        }
        return;
    }
    assertXIDNull();
    String currentXid = RootContext.getXID();
    if (currentXid != null) {
        throw new IllegalStateException("Global transaction already exists," +
                                        " can't begin a new global transaction, currentXid = " + currentXid);
    }
    xid = transactionManager.begin(null, null, name, timeout);
    status = GlobalStatus.Begin;
    RootContext.bind(xid);
    if (LOGGER.isInfoEnabled()) {
        LOGGER.info("Begin new global transaction [{}]", xid);
    }
}

重点为transactionManager.begin(null, null, name, timeout);方法底层通过Netty客户端执行RPC请求告知Server端开启全局事务。


Server端收到请求后,最终交由DefaultCore#begin处理:

public String begin(String applicationId, String transactionServiceGroup, String name, int timeout)
    throws TransactionException {
    GlobalSession session = GlobalSession.createGlobalSession(applicationId, transactionServiceGroup, name, timeout);
    MDC.put(RootContext.MDC_KEY_XID, session.getXid());

    session.begin();

    // transaction start event
    MetricsPublisher.postSessionDoingEvent(session, false);

    return session.getXid();
}

Seata Server开启全局事务的本质为创建一个全局Session并持久化,考虑到环境的多样性,该全局Session的持久化可以支持File、Database、Redis等三种,通过SPI的方式加载,全局Session的管理接口为TransactionStoreManager,其类图如下:

以Database的方式为例,创建全局Session的本质是在全局会话表(global_table)中插入一条记录,DataBaseTransactionStoreManager会将具体实现交由LogStoreDataBaseDAO#insertGlobalTransactionDO处理。

public boolean insertGlobalTransactionDO(GlobalTransactionDO globalTransactionDO) {
    String sql = LogStoreSqlsFactory.getLogStoreSqls(dbType).getInsertGlobalTransactionSQL(globalTable);
    Connection conn = null;
    PreparedStatement ps = null;
    try {
        int index = 1;
        conn = logStoreDataSource.getConnection();
        conn.setAutoCommit(true);
        ps = conn.prepareStatement(sql);
        ps.setString(index++, globalTransactionDO.getXid());
        ps.setLong(index++, globalTransactionDO.getTransactionId());
        ps.setInt(index++, globalTransactionDO.getStatus());
        ps.setString(index++, globalTransactionDO.getApplicationId());
        ps.setString(index++, globalTransactionDO.getTransactionServiceGroup());
        String transactionName = globalTransactionDO.getTransactionName();
        transactionName = transactionName.length() > transactionNameColumnSize ?
            transactionName.substring(0, transactionNameColumnSize) :
        transactionName;
        ps.setString(index++, transactionName);
        ps.setInt(index++, globalTransactionDO.getTimeout());
        ps.setLong(index++, globalTransactionDO.getBeginTime());
        ps.setString(index++, globalTransactionDO.getApplicationData());
        return ps.executeUpdate() > 0;
    } catch (SQLException e) {
        throw new StoreException(e);
    } finally {
        IOUtil.close(ps, conn);
    }
}

本质上是通过JDBC接口实现数据的插入,就是这么简单。


当Server成功创建全局事务后,会返回该事务的标识(XID),格式为:

ip:port:tranId

后续TCC分支事务注册时会将该分支绑定到XID代表的全局事务中,纳入到该全局事务的管理中。


前面分析了GlobalTransactionScanner会给@LocalTCC注解的类生成代理,同时给@TwoPhaseBusinessAction注解声明的方法增加TccActionInterceptor拦截器,在拦截器中实现了TCC分支事务的注册。

public class TccActionInterceptor implements MethodInterceptor, ConfigurationChangeListener {

    @Override
    public Object invoke(final MethodInvocation invocation) throws Throwable {
        // ...
        //Handler the TCC Aspect
        Map<String, Object> ret = actionInterceptorHandler.proceed(method, methodArgs, xid, businessAction, invocation::proceed);
        // ...
    }
}

public class ActionInterceptorHandler {
    /**
     * Handler the TCC Aspect
     *
     * @param method         the method
     * @param arguments      the arguments
     * @param businessAction the business action
     * @param targetCallback the target callback
     * @return map map
     * @throws Throwable the throwable
     */
    public Map<String, Object> proceed(Method method, Object[] arguments, String xid, TwoPhaseBusinessAction businessAction,
                                       Callback<Object> targetCallback) throws Throwable {
        // ...
        //Creating Branch Record
        String branchId = doTccActionLogStore(method, arguments, businessAction, actionContext);
        // ...
    }

    /**
     * Creating Branch Record
     *
     * @param method         the method
     * @param arguments      the arguments
     * @param businessAction the business action
     * @param actionContext  the action context
     * @return the string
     */
    protected String doTccActionLogStore(Method method, Object[] arguments, TwoPhaseBusinessAction businessAction,
                                         BusinessActionContext actionContext) {
        String actionName = actionContext.getActionName();
        String xid = actionContext.getXid();
        // ...
        String applicationContextStr = JSON.toJSONString(applicationContext);
        try {
            //registry branch record
            Long branchId = DefaultResourceManager.get().branchRegister(BranchType.TCC, actionName, null, xid,
                applicationContextStr, null);
            return String.valueOf(branchId);
        } catch (Throwable t) {
            String msg = String.format("TCC branch Register error, xid: %s", xid);
            LOGGER.error(msg, t);
            throw new FrameworkException(t, msg);
        }
    }
}

最终通过DefaultResourceManager来进行分支注册,发起RPC分支注册请求。


Server端收到分支事务注册请求后,根据分支类型(TCC)分发给TccCore#branchRegister方法处理。

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, () -> {
            globalSessionStatusCheck(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);
                throw new BranchTransactionException(FailedToAddBranch, String
                        .format("Failed to store branch xid = %s branchId = %s", globalSession.getXid(),
                                branchSession.getBranchId()), ex);
            }
            if (LOGGER.isInfoEnabled()) {
                LOGGER.info("Register branch successfully, xid = {}, branchId = {}, resourceId = {} ,lockKeys = {}",
                        globalSession.getXid(), branchSession.getBranchId(), resourceId, lockKeys);
            }
            return branchSession.getBranchId();
        });
    }

上述Server端的处理逻辑可以概括为,根据分支注册请求携带的XID找到全局事务,然后再分支事务表(branch_table)中插入一条分支事务记录,该记录绑定了对应的全局事务信息,分支事务记录的插入实现在LogStoreDataBaseDAO#insertBranchTransactionDO,也是通过JDBC接口实现的:

public boolean insertBranchTransactionDO(BranchTransactionDO branchTransactionDO) {
    String sql = LogStoreSqlsFactory.getLogStoreSqls(dbType).getInsertBranchTransactionSQL(branchTable);
    Connection conn = null;
    PreparedStatement ps = null;
    try {
        int index = 1;
        conn = logStoreDataSource.getConnection();
        conn.setAutoCommit(true);
        ps = conn.prepareStatement(sql);
        ps.setString(index++, branchTransactionDO.getXid());
        ps.setLong(index++, branchTransactionDO.getTransactionId());
        ps.setLong(index++, branchTransactionDO.getBranchId());
        ps.setString(index++, branchTransactionDO.getResourceGroupId());
        ps.setString(index++, branchTransactionDO.getResourceId());
        ps.setString(index++, branchTransactionDO.getBranchType());
        ps.setInt(index++, branchTransactionDO.getStatus());
        ps.setString(index++, branchTransactionDO.getClientId());
        ps.setString(index++, branchTransactionDO.getApplicationData());
        return ps.executeUpdate() > 0;
    } catch (SQLException e) {
        throw new StoreException(e);
    } finally {
        IOUtil.close(ps, conn);
    }
}

这样就实现了我们前面说的将自定义的分支事务纳入到全局事务模型中了。

3.事务二阶段提交/回滚

当业务逻辑正常执行完成未抛出异常那么正常走事务提交逻辑,否则被catch同时开始回滚全局事务。

public class TransactionalTemplate {
    /**
     * Execute object.
     *
     * @param business the business
     * @return the object
     * @throws TransactionalExecutor.ExecutionException the execution exception
     */
    public Object execute(TransactionalExecutor business) throws Throwable {
        // ... 
        beginTransaction(txInfo, tx);

        Object rs;
        try {
            // Do Your Business
            rs = business.execute();
        } catch (Throwable ex) {
            // 3. The needed business exception to rollback.
            completeTransactionAfterThrowing(txInfo, tx, ex);
            throw ex;
        }

        // 4. everything is fine, commit.
        commitTransaction(tx);
        // ...
    }
}

全局事务的提交或回滚区别在于发送给Server端的指令不同而已,后续的各分支事务的二阶段执行都交由Server端来控制。


以全局提交为例,当Server端收到全局事务提交请求时,最终会分发给DefaultCore#doGlobalCommit处理:

public boolean doGlobalCommit(GlobalSession globalSession, boolean retrying) throws TransactionException {
    List<BranchSession> branchSessions = globalSession.getSortedBranches();
    Boolean result = SessionHelper.forEach(branchSessions, branchSession -> {
        // ...
        BranchStatus branchStatus = getCore(branchSession.getBranchType()).branchCommit(globalSession, branchSession);
        // ...
    }, PARALLEL_HANDLE_BRANCH && branchSessions.size() >= 2);
}

根据提交的全局事务,查询到其中的所有分支事务,遍历分支事务并进行分支事务提交。分支事务提交本质是Server(TC)与分支事务所代表的Client(RM)进行RPC交互,告知RM进行分支提交,也即调用自定义的二阶段commit方法。

具体实现为getCore(branchSession.getBranchType()).branchCommit(globalSession, branchSession);将请求委托给TCCCore的父类方法branchCommit发起RPC调用:

public BranchStatus branchCommit(GlobalSession globalSession, BranchSession branchSession) throws TransactionException {
    try {
        BranchCommitRequest request = new BranchCommitRequest();
        request.setXid(branchSession.getXid());
        request.setBranchId(branchSession.getBranchId());
        request.setResourceId(branchSession.getResourceId());
        request.setApplicationData(branchSession.getApplicationData());
        request.setBranchType(branchSession.getBranchType());
        return branchCommitSend(request, globalSession, branchSession);
    } catch (IOException | TimeoutException e) {
        throw new BranchTransactionException(FailedToSendBranchCommitRequest,
                                             String.format("Send branch commit failed, xid = %s branchId = %s", branchSession.getXid(),
                                                           branchSession.getBranchId()), e);
    }
}

发送分支提交RPC请求后,client端侧会收到请求开始分支提交处理。


客户端收到分支事务提交RPC请求后最终会交由TCCResourceManager#branchCommit处理,在该类中,通过分支事务的资源ID找到对应的TCCResouce,里面包含了TCC的各种信息,包括最重要的commit和rollback方法信息。

public BranchStatus branchCommit(BranchType branchType, String xid, long branchId, String resourceId,
                                     String applicationData) throws TransactionException {
    TCCResource tccResource = (TCCResource)tccResourceCache.get(resourceId);
    if (tccResource == null) {
        throw new ShouldNeverHappenException(String.format("TCC resource is not exist, resourceId: %s", resourceId));
    }
    Object targetTCCBean = tccResource.getTargetBean();
    Method commitMethod = tccResource.getCommitMethod();
    if (targetTCCBean == null || commitMethod == null) {
        throw new ShouldNeverHappenException(String.format("TCC resource is not available, resourceId: %s", resourceId));
    }
    try {
        //BusinessActionContext
        BusinessActionContext businessActionContext = getBusinessActionContext(xid, branchId, resourceId,
                                                                               applicationData);
        Object ret = commitMethod.invoke(targetTCCBean, businessActionContext);
        LOGGER.info("TCC resource commit result : {}, xid: {}, branchId: {}, resourceId: {}", ret, xid, branchId, resourceId);
        boolean result;
        if (ret != null) {
            if (ret instanceof TwoPhaseResult) {
                result = ((TwoPhaseResult)ret).isSuccess();
            } else {
                result = (boolean)ret;
            }
        } else {
            result = true;
        }
        return result ? BranchStatus.PhaseTwo_Committed : BranchStatus.PhaseTwo_CommitFailed_Retryable;
    } catch (Throwable t) {
        String msg = String.format("commit TCC resource error, resourceId: %s, xid: %s.", resourceId, xid);
        LOGGER.error(msg, t);
        return BranchStatus.PhaseTwo_CommitFailed_Retryable;
    }
}

显而易见,客户端二阶段提交的实现就是通过反射实现commit或者rollback方法的调用,然后向Server报告执行结果。

最终Server汇集各分支事务的执行结果进行最后全局和分支事务的清理工作,至此整个事务结束。

4.2 V1.5.1

在支持幂等、悬挂和空回滚版本中我们以v1.5.1为基准分析。

在 TCC 模型执行的过程中,还可能会出现各种异常,其中最为常见的有空回滚、幂等、悬挂等。下面我讲下 Seata 是如何处理这三种异常的。

幂等

幂等问题指的是 TC 重复进行二阶段提交,因此 Confirm/Cancel 接口需要支持幂等处理,即不会产生资源重复提交或者重复释放。

那么幂等问题是如何产生的呢?

如上图所示,参与者 A 执行完二阶段之后,由于网络抖动或者宕机问题,会造成 TC 收不到参与者 A 执行二阶段的返回结果,TC 会重复发起调用,直到二阶段执行结果成功。

空回滚

空回滚指的是在一个分布式事务中,在没有调用参与方的 Try 方法的情况下,TM 驱动二阶段回滚调用了参与方的 Cancel 方法。

那么空回滚是如何产生的呢?

如上图所示,全局事务开启后,参与者 A 分支注册完成之后会执行参与者一阶段 RPC 方法,如果此时参与者 A 所在的机器发生宕机,网络异常,都会造成 RPC 调用失败,即参与者 A 一阶段方法未成功执行,但是此时全局事务已经开启,Seata 必须要推进到终态,在全局事务回滚时会调用参与者 A 的 Cancel 方法,从而造成空回滚。

悬挂

悬挂指的是二阶段 Cancel 方法比 一阶段 Try 方法优先执行,由于允许空回滚的原因,在执行完二阶段 Cancel 方法之后直接空回滚返回成功,此时全局事务已结束,但是由于 Try 方法随后执行,这就会造成一阶段 Try 方法预留的资源永远无法提交和释放了。

那么悬挂是如何产生的呢?

如上图所示,在执行参与者 A 的一阶段 Try 方法时,出现网路拥堵,由于 Seata 全局事务有超时限制,执行 Try 方法超时后,TM 决议全局回滚,回滚完成后如果此时 RPC 请求才到达参与者 A,执行 Try 方法进行资源预留,从而造成悬挂。

解决

Seata的解決我们可以参看GitHub上对于该问题的讨论。

也正如问题讨论这样,Seata通过增加一张状态控制表(tcc_fence_log)来解决这些问题。

事务一阶段执行

在源码层面上来看,新版本在@TwoPhaseBusinessAction注解中新增useTCCFence属性,作为开启TCC处理幂等、空回滚、悬挂问题的开关,默认关闭。

当开启useTCCFence后,当分支事务开始执行时,开始尝试插入一条记录到状态控制表(tcc_fence_log)中。

tcc_fence_log 建表语句如下(MySQL 语法):

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;
public class ActionInterceptorHandler {
    public Object proceed(Method method, Object[] arguments, String xid, TwoPhaseBusinessAction businessAction,
                                       Callback<Object> targetCallback) throws Throwable {
        // ...
        if (businessAction.useTCCFence()) {
            // Use TCC Fence, and return the business result
            return TCCFenceHandler.prepareFence(xid, Long.valueOf(branchId), actionName, targetCallback);
        } else {
            //Execute business, and return the business result
            return targetCallback.execute();
        }
        // ...
    }
}

可以看到会交由TCCFenceHandler.prepareFence处理,先尝试插入记录到状态表中,在执行TCC一阶段业务方法。

public static Object prepareFence(String xid, Long branchId, String actionName, Callback<Object> targetCallback) {
    return transactionTemplate.execute(status -> {
        try {
            Connection conn = DataSourceUtils.getConnection(dataSource);
            boolean result = insertTCCFenceLog(conn, xid, branchId, actionName, TCCFenceConstant.STATUS_TRIED);
            LOGGER.info("TCC fence prepare result: {}. xid: {}, branchId: {}", result, xid, branchId);
            if (result) {
                return targetCallback.execute();
            } else {
                throw new TCCFenceException(String.format("Insert tcc fence record error, prepare fence failed. xid= %s, branchId= %s", xid, branchId),
                        FrameworkErrorCode.InsertRecordError);
            }
        } catch (TCCFenceException e) {
            if (e.getErrcode() == FrameworkErrorCode.DuplicateKeyException) {
                LOGGER.error("Branch transaction has already rollbacked before,prepare fence failed. xid= {},branchId = {}", xid, branchId);
                addToLogCleanQueue(xid, branchId);
            }
            status.setRollbackOnly();
            throw new SkipCallbackWrapperException(e);
        } catch (Throwable t) {
            status.setRollbackOnly();
            throw new SkipCallbackWrapperException(t);
        }
    });
}

insertTCCFenceLog方法底层通过JDBC接口实现数据的插入,而且交由spring提供的TransactionTemplate实现事务的管理,所以保证了状态控制表和一阶段try方法在同一事务内执行,由数据库底层保证数据一致性。

事务二阶段提交/回滚
幂等

在 commit/cancel 阶段,因为 TC 没有收到分支事务的响应,需要进行重试,这就要分支事务支持幂等。

我们看一下新版本是怎么解决的。下面的代码在 TCCResourceManager 类:

@Override
public BranchStatus branchCommit(BranchType branchType, String xid, long branchId, String resourceId,
                                 String applicationData) throws TransactionException {
    TCCResource tccResource = (TCCResource)tccResourceCache.get(resourceId);
    //省略判断
    Object targetTCCBean = tccResource.getTargetBean();
    Method commitMethod = tccResource.getCommitMethod();
    //省略判断
    try {
        //BusinessActionContext
        BusinessActionContext businessActionContext = getBusinessActionContext(xid, branchId, resourceId,
            applicationData);
        Object[] args = this.getTwoPhaseCommitArgs(tccResource, businessActionContext);
        Object ret;
        boolean result;
        //注解 useTCCFence 属性是否设置为 true
        if (Boolean.TRUE.equals(businessActionContext.getActionContext(Constants.USE_TCC_FENCE))) {
            try {
                result = TCCFenceHandler.commitFence(commitMethod, targetTCCBean, xid, branchId, args);
            } catch (SkipCallbackWrapperException | UndeclaredThrowableException e) {
                throw e.getCause();
            }
        } else {
            //省略逻辑
        }
        LOGGER.info("TCC resource commit result : {}, xid: {}, branchId: {}, resourceId: {}", result, xid, branchId, resourceId);
        return result ? BranchStatus.PhaseTwo_Committed : BranchStatus.PhaseTwo_CommitFailed_Retryable;
    } catch (Throwable t) {
        //省略
        return BranchStatus.PhaseTwo_CommitFailed_Retryable;
    }
}

从代码中可以看到,提交事务时首先会判断 tcc_fence_log 表中是否已经有记录,如果有记录,则判断事务执行状态并返回。这样如果判断到事务的状态已经是 STATUS_COMMITTED,就不会再次提交,保证了幂等。如果 tcc_fence_log 表中没有记录,则插入一条记录,供后面重试时判断。

Rollback 的逻辑跟 commit 类似,逻辑在类 TCCFenceHandler 的 rollbackFence 方法。

空回滚

Seata 的解决方案是在 try 阶段 往 tcc_fence_log 表插入一条记录,status 字段值是 STATUS_TRIED,在 Rollback 阶段判断记录是否存在,如果不存在,则不执行回滚操作。代码如下:

//TCCFenceHandler 类
public static Object prepareFence(String xid, Long branchId, String actionName, Callback<Object> targetCallback) {
    return transactionTemplate.execute(status -> {
        try {
            Connection conn = DataSourceUtils.getConnection(dataSource);
            boolean result = insertTCCFenceLog(conn, xid, branchId, actionName, TCCFenceConstant.STATUS_TRIED);
            LOGGER.info("TCC fence prepare result: {}. xid: {}, branchId: {}", result, xid, branchId);
            if (result) {
                return targetCallback.execute();
            } else {
                throw new TCCFenceException(String.format("Insert tcc fence record error, prepare fence failed. xid= %s, branchId= %s", xid, branchId),
                        FrameworkErrorCode.InsertRecordError);
            }
        } catch (TCCFenceException e) {
            //省略
        } catch (Throwable t) {
            //省略
        }
    });
}

在 Rollback 阶段的处理逻辑如下:

//TCCFenceHandler 类
public static boolean rollbackFence(Method rollbackMethod, Object targetTCCBean,
                                    String xid, Long branchId, Object[] args, String actionName) {
    return transactionTemplate.execute(status -> {
        try {
            Connection conn = DataSourceUtils.getConnection(dataSource);
            TCCFenceDO tccFenceDO = TCC_FENCE_DAO.queryTCCFenceDO(conn, xid, branchId);
            // non_rollback
            if (tccFenceDO == null) {
                //不执行回滚逻辑
                return true;
            } else {
                if (TCCFenceConstant.STATUS_ROLLBACKED == tccFenceDO.getStatus() || TCCFenceConstant.STATUS_SUSPENDED == tccFenceDO.getStatus()) {
                    LOGGER.info("Branch transaction had already rollbacked before, idempotency rejected. xid: {}, branchId: {}, status: {}", xid, branchId, tccFenceDO.getStatus());
                    return true;
                }
                if (TCCFenceConstant.STATUS_COMMITTED == tccFenceDO.getStatus()) {
                    if (LOGGER.isWarnEnabled()) {
                        LOGGER.warn("Branch transaction status is unexpected. xid: {}, branchId: {}, status: {}", xid, branchId, tccFenceDO.getStatus());
                    }
                    return false;
                }
            }
            return updateStatusAndInvokeTargetMethod(conn, rollbackMethod, targetTCCBean, xid, branchId, TCCFenceConstant.STATUS_ROLLBACKED, status, args);
        } catch (Throwable t) {
            status.setRollbackOnly();
            throw new SkipCallbackWrapperException(t);
        }
    });
}

updateStatusAndInvokeTargetMethod 方法执行的 sql 如下:

update tcc_fence_log set status = ?, gmt_modified = ?
    where xid = ? and  branch_id = ? and status = ? ;

可见就是把 tcc_fence_log 表记录的 status 字段值从 STATUS_TRIED 改为 STATUS_ROLLBACKED,如果更新成功,就执行回滚逻辑。

悬挂

Seata 解决这个问题的方法是执行 Rollback 方法时先判断 tcc_fence_log 是否存在当前 xid 的记录,如果没有则向 tcc_fence_log 表插入一条记录,状态是 STATUS_SUSPENDED,并且不再执行回滚操作。代码如下:

public static boolean rollbackFence(Method rollbackMethod, Object targetTCCBean,
                                    String xid, Long branchId, Object[] args, String actionName) {
    return transactionTemplate.execute(status -> {
        try {
            Connection conn = DataSourceUtils.getConnection(dataSource);
            TCCFenceDO tccFenceDO = TCC_FENCE_DAO.queryTCCFenceDO(conn, xid, branchId);
            // non_rollback
            if (tccFenceDO == null) {
                //插入防悬挂记录
                boolean result = insertTCCFenceLog(conn, xid, branchId, actionName, TCCFenceConstant.STATUS_SUSPENDED);
                //省略逻辑
                return true;
            } else {
                //省略逻辑
            }
            return updateStatusAndInvokeTargetMethod(conn, rollbackMethod, targetTCCBean, xid, branchId, TCCFenceConstant.STATUS_ROLLBACKED, status, args);
        } catch (Throwable t) {
            //省略逻辑
        }
    });
}

而后面执行 try 阶段方法时首先会向 tcc_fence_log 表插入一条当前 xid 的记录,这样就造成了主键冲突。代码如下:

//TCCFenceHandler 类
public static Object prepareFence(String xid, Long branchId, String actionName, Callback<Object> targetCallback) {
    return transactionTemplate.execute(status -> {
        try {
            Connection conn = DataSourceUtils.getConnection(dataSource);
            boolean result = insertTCCFenceLog(conn, xid, branchId, actionName, TCCFenceConstant.STATUS_TRIED);
            //省略逻辑
        } catch (TCCFenceException e) {
            if (e.getErrcode() == FrameworkErrorCode.DuplicateKeyException) {
                LOGGER.error("Branch transaction has already rollbacked before,prepare fence failed. xid= {},branchId = {}", xid, branchId);
                addToLogCleanQueue(xid, branchId);
            }
            status.setRollbackOnly();
            throw new SkipCallbackWrapperException(e);
        } catch (Throwable t) {
            //省略
        }
    });
}

注意:queryTCCFenceDO 方法 sql 中使用了 for update,这样就不用担心 Rollback 方法中获取不到 tcc_fence_log 表记录而无法判断 try 阶段本地事务的执行结果了。


参考:https://seata.io/zh-cn/blog/seata-tcc

posted @ 2024-01-02 15:24  liuershi  阅读(399)  评论(0)    收藏  举报