Seata-TCC模式 原理

3,371 阅读7分钟

1 TCC原理

TCC(Try-Confirm-Cancel)将一个事务分成两阶段:

  • Try阶段:尝试锁定资源
  • Confirm阶段:如果Try阶段所有资源均锁定成功,那么执行Confirm阶段,真正的扣除资源。
  • Cancel阶段:如果Try阶段有部分资源锁定失败,那么执行Cancel阶段,回滚Try阶段锁定的资源。
    注意:除了Try阶段为主动触发外,Confirm/Cancel均有框架从自动发起。

TCC系统模型如下所示:
在这里插入图片描述

从微服务的调用过程上看TCC系统模型如下所示:
在这里插入图片描述

TCC有多种实现方式,本文仅介绍Seata的TCC模式,github上还有其他的TCC实现,例如:tcc-transaction
TCC模式和AT模式有很多相同之处,建议先了解AT模式,本文介绍原理时重点介绍其差异部分

2 TCC模式示例

TCC模式使用非常简单,这里仅摘取和实现原理相关的部分代码,并结合这些代码进行原理分析。这里的示例摘自github上seata-sample

2.1 TM系统配置

<bean class="io.seata.spring.annotation.GlobalTransactionScanner">
    <constructor-arg value="tcc-sample"/>
    <constructor-arg value="my_test_tx_group"/>
</bean>

GlobalTransactionScanner将会自动扫描@GlobalTransactional,并为其开启分布式事务。

使用时:只需要在需要开启全局事务的地方加上@GlobalTransactional注解即可,如下所示:

事务开启方:
@GlobalTransactional
public String doTransactionCommit(){
    boolean result = tccActionOne.prepare(null, 1);
    if(!result){
        throw new RuntimeException("TccActionOne failed.");
    }
    List list = new ArrayList();
    result = tccActionTwo.prepare(null, "two", list);
    if(!result){
        throw new RuntimeException("TccActionTwo failed.");
    }
    return RootContext.getXID();
}

2.2 RM提供TCC接口:

TccActionOne
public interface TccActionOne {
    @TwoPhaseBusinessAction(name = "DubboTccActionOne" , commitMethod = "commit", rollbackMethod = "rollback")
    public boolean prepare(BusinessActionContext actionContext, @BusinessActionContextParameter(paramName = "a") int a);

    public boolean commit(BusinessActionContext actionContext);

    public boolean rollback(BusinessActionContext actionContext);
}

TccActionTwo
public interface TccActionTwo {
    @TwoPhaseBusinessAction(name = "DubboTccActionTwo" , commitMethod = "commit", rollbackMethod = "rollback")
    public boolean prepare(BusinessActionContext actionContext, @BusinessActionContextParameter(paramName = "b") String b, @BusinessActionContextParameter(paramName = "c",index = 1) List list);

    public boolean commit(BusinessActionContext actionContext);

    public boolean rollback(BusinessActionContext actionContext);

}

2.3 启动seata-server

seata-server承担的是「seata-分布式事务方案」中TC的职责,即作为事务协调者,管理全局事务的生命周期。

3 Seata-TCC方案简介

3.1 Seata-TCC架构图

在这里插入图片描述
注:此图来自seata官网

3.2 分布式事务流程

在流程上,TCC模式在整个流程上与AT模式基本相同,如下所示:
在这里插入图片描述

在流程中,AT、TCC模式的主要区别在于:

  • AT模式是在DB数据源层做了代理,通过拦截SQL执行,在SQL执行前向TC中register branch;TC模式是通过@TwoPhaseBusinessAction来实现的,即框架层面扫描@TwoPhaseBusinessAction注解,通过拦截器向TC中register branch。
  • 在branch commit/rollback阶段,AT模式基于undo log执行数据回滚操作;TCC模式需要自己制定commit/rollback方法。

3.3 Seata-TCC原理分析

3.3.1 时序图

在这里插入图片描述

TCC模式的时序图和AT模式的时序图非常类似,从整体上看,仅图中用虚线框起来的部分和AT模式有差异。

3.3.2 Begin阶段

3.3.2.1 TM请求TC开启一个全局事务

此步骤和AT模式完全相同。注册成功后将xid绑定到上下文中,并传递到各个微服务中去。

3.3.2.2 TC的收到开启全局事务请求时的处理流程。

此步骤和AT模式完全相同。执行完毕后,TC中将会创建一个全局事务记录,如下所示:

[    {        "xid":"192.168.1.5:8091:2612211982926405633",        "transaction_id":2612211982926400000,        "status":1,        "application_id":"tcc-sample",        "transaction_service_group":"my_test_tx_group",        "transaction_name":"doTransactionCommit()",        "timeout":60000,        "begin_time":1618757292292,        "application_data":"NULL",        "gmt_create":44304.9501388889,        "gmt_modified":44304.9501388889    }]

3.3.3 TM执行业务逻辑

在GlobalTransactionalInterceptor拦截器中完成begin阶段以后,就会正式执行TM中的业务逻辑代码,即示例中的:

@GlobalTransactional
public String doTransactionCommit(){
    boolean result = tccActionOne.prepare(null, 1);
    if(!result){
        throw new RuntimeException("TccActionOne failed.");
    }
    List list = new ArrayList();
    result = tccActionTwo.prepare(null, "two", list);
    if(!result){
        throw new RuntimeException("TccActionTwo failed.");
    }
    return RootContext.getXID();
}

这里执行业务逻辑的时候,会RPC调用其他微服务(微服务B、微服务C)的接口,RM在执行业务逻辑的过程中将进入Register Branch阶段。

3.3.4 Register Branch阶段

这个阶段和AT模式有较大的差别,AT模式是基于DataSourceProxy,在业务sql执行前注册分支事务,TCC模式是基于@TwoPhaseBusinessAction来做到注册分支事务。

3.3.4.1 RM注册向TC分支事务

系统启动的时候,会为使用@TwoPhaseBusinessAction注解的类生成代理,即TccActionInterceptor负责处理。这个拦截器很简单,主要做了这几件事情:

  • 绑定分支类型:BranchType.TCC
  • 通过注解获取TCC方法的上下文,以方便在Confirm/Cancel阶段直接找到对应的方法。
  • 向TC注册分支事务。
    如下代码所示。
TccActionInterceptor#invoke
public Object invoke(final MethodInvocation invocation) throws Throwable {
    if (!RootContext.inGlobalTransaction() || disable || RootContext.inSagaBranch()) {
        //not in transaction
        return invocation.proceed();
    }
    Method method = getActionInterfaceMethod(invocation);
    TwoPhaseBusinessAction businessAction = method.getAnnotation(TwoPhaseBusinessAction.class);
    //try method
    if (businessAction != null) {
        //save the xid
        String xid = RootContext.getXID();
        //save the previous branchType
        BranchType previousBranchType = RootContext.getBranchType();
        //if not TCC, bind TCC branchType
        if (BranchType.TCC != previousBranchType) {
            RootContext.bindBranchType(BranchType.TCC);
        }
        try {
            Object[] methodArgs = invocation.getArguments();
            //Handler the TCC Aspect
            Map<String, Object> ret = actionInterceptorHandler.proceed(method, methodArgs, xid, businessAction, invocation::proceed);
            //return the final result
            return ret.get(Constants.TCC_METHOD_RESULT);
        }
        finally {
            //if not TCC, unbind branchType
            if (BranchType.TCC != previousBranchType) {
                RootContext.unbindBranchType();
            }
            //MDC remove branchId
            MDC.remove(RootContext.MDC_KEY_BRANCH_ID);
        }
    }
    return invocation.proceed();
}


ActionInterceptorHandler#proceed
public Map<String, Object> proceed(Method method, Object[] arguments, String xid, TwoPhaseBusinessAction businessAction, Callback<Object> targetCallback) throws Throwable {
    Map<String, Object> ret = new HashMap<>(4);

    //TCC name
    String actionName = businessAction.name();
    BusinessActionContext actionContext = new BusinessActionContext();
    actionContext.setXid(xid);
    //set action name
    actionContext.setActionName(actionName);

    //Creating Branch Record
    String branchId = doTccActionLogStore(method, arguments, businessAction, actionContext);
    actionContext.setBranchId(branchId);
    //MDC put branchId
    MDC.put(RootContext.MDC_KEY_BRANCH_ID, branchId);

    //set the parameter whose type is BusinessActionContext
    Class<?>[] types = method.getParameterTypes();
    int argIndex = 0;
    for (Class<?> cls : types) {
        if (cls.getName().equals(BusinessActionContext.class.getName())) {
            arguments[argIndex] = actionContext;
            break;
        }
        argIndex++;
    }
    //the final parameters of the try method
    ret.put(Constants.TCC_METHOD_ARGUMENTS, arguments);
    //the final result
    ret.put(Constants.TCC_METHOD_RESULT, targetCallback.execute());
    return ret;
}

protected String doTccActionLogStore(Method method, Object[] arguments, TwoPhaseBusinessAction businessAction, BusinessActionContext actionContext) {
    String actionName = actionContext.getActionName();
    String xid = actionContext.getXid();
    //
    Map<String, Object> context = fetchActionRequestContext(method, arguments);
    context.put(Constants.ACTION_START_TIME, System.currentTimeMillis());

    //init business context
    initBusinessContext(context, method, businessAction);
    //Init running environment context
    initFrameworkContext(context);
    actionContext.setActionContext(context);

    //init applicationData
    Map<String, Object> applicationContext = new HashMap<>(4);
    applicationContext.put(Constants.TCC_ACTION_CONTEXT, context);
    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);
    }
}

protected void initBusinessContext(Map<String, Object> context, Method method,
                                       TwoPhaseBusinessAction businessAction) {
    if (method != null) {
        //the phase one method name
        context.put(Constants.PREPARE_METHOD, method.getName());
    }
    if (businessAction != null) {
        //the phase two method name
        context.put(Constants.COMMIT_METHOD, businessAction.commitMethod());
        context.put(Constants.ROLLBACK_METHOD, businessAction.rollbackMethod());
        context.put(Constants.ACTION_NAME, businessAction.name());
    }
}

这个过程需要注意:TCC在此阶段会将commitMethod、rollbackMethod、请求上下文信息保存下来。

3.3.4.2 TC受理RM的Register Branch流程

这个过程和AT模式基本相同,暂不累述。分支事务注册成功时,TC中branch_table中将多出一条分支事务记录。

[    {        "branch_id":2612211982926400000,        "xid":"192.168.1.5:8091:2612211982926405633",        "transaction_id":2612211982926400000,        "resource_group_id":"NULL",        "resource_id":"DubboTccActionOne",        "branch_type":"TCC",        "status":0,        "client_id":"tcc-sample:127.0.0.1:58584",        "application_data":{            "actionContext":{                "a":1,                "action-start-time":1618757292549,                "sys::prepare":"prepare",                "sys::rollback":"rollback",                "sys::commit":"commit",                "host-name":"192.168.1.5",                "actionName":"DubboTccActionOne"            }        },        "gmt_create":"2021-04-18 22:48:12.705895",        "gmt_modified":"2021-04-18 22:48:12.705895"    },    {        "branch_id":2612211982926400000,        "xid":"192.168.1.5:8091:2612211982926405633",        "transaction_id":2612211982926400000,        "resource_group_id":"NULL",        "resource_id":"DubboTccActionTwo",        "branch_type":"TCC",        "status":0,        "client_id":"tcc-sample:127.0.0.1:58584",        "application_data":{            "actionContext":{                "b":"two",                "action-start-time":1618757293189,                "c":"c2",                "sys::prepare":"prepare",                "sys::rollback":"rollback",                "sys::commit":"commit",                "host-name":"192.168.1.5",                "actionName":"DubboTccActionTwo"            }        },        "gmt_create":"2021-04-18 22:48:12.7058959",        "gmt_modified":"2021-04-18 22:48:12.705895"    }]

3.3.5 Global Commit/RollBack阶段

这个过程和AT模式基本相同,暂不累述。

3.3.5.1 TM通知TC进行Global Commit/Rollback阶段

这个过程和AT模式基本相同,暂不累述。

3.3.5.2 TC进行Global Commit/Rollback阶段

这个阶段的前半部分和AT模式基本一致,

3.3.6 RM进行Branch Commit/Rollback阶段

RM收到Branch Commit/Rollback请求以后,会交给AbstractRMHandler处理,接着AbstractRMHandler将请求委派给TCCResourceManager处理。
Branch Commit/Rollback阶段主要包括以下内容:

  • 从缓存中获取TCCResource
  • 从TCCResource获取commitMethod/rollbackMethod
  • 获取请求执行的上下文
  • 执行commit/rollback方法
    branchCommit代码如下,rollbackMethod流程基本类似。
TCCResourceManager#branchCommit
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;
    }
}

4 TCC实践经验

此部分针对所有TCC方案。

4.1 防悬挂控制

原因分析:Try阶段超时(拥堵),分布式事务回滚触发Cancel,Cancel可能比Try先到达。
解决方案:允许空回滚,且要拒绝回滚后的Try操作。
在这里插入图片描述

此图来自网络,比较懒不想自己画

4.2 幂等控制

分布式开发中,任何写操作都需要进行幂等控制,TCC的分布式事务也是。
在这里插入图片描述
此图来自网络,比较懒不想自己画

5 参考文档

Seata AT 模式

分布式事务 Seata 及其三种模式详解 | Meetup#3 回顾

柔性事务解决方案之TCC

Seata 原理

Seata-AT模式

Seata-TCC模式

Seata-Saga模式

Seata-XA模式

TCC-Transaction原理