1 principle of TCC

TCC (try-confirm-cancel) divides a transaction into two phases:

  • Try phase: Attempts to lock resources
  • Confirm phase: If all resources in the Try phase are locked successfully, perform the Confirm phase to deduct resources.
  • Cancel phase: If some resources fail to be locked during the Try phase, perform the Cancel phase to roll back the resources locked during the Try phase. Note: Except for the Try phase, which is actively triggered, Confirm/Cancel is automatically initiated by the framework.

The TCC system model is as follows:

The TCC system model can be seen from the call process of microservices as follows:

TCC can be implemented in a variety of ways. This article only introduces Seata’s TCC mode, but there are other TCC implementations available on Github. For example, TCC-Transaction TCC mode and AT mode have many similarities

2 TCC mode example

TCC mode is very simple to use. Here, only part of the code related to the implementation principle is selected, and the principle analysis is carried out with these codes. The example here is taken from Seata-sample on Github

2.1 TM System Configuration

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

GlobalTransactionScanner will automatically scan @GlobalTransactional and open distributed transactions for it.

When in use: Simply annotate @GlobalTransactional where you want to enable global transactions, as shown below:

@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(); }Copy the code

2.2 RM Provides the TCC Interface:

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);

}
Copy the code

2.3 start seata – server

Seata-server takes on the responsibilities of TCS in the “SEATa-Distributed Transaction Solution”, that is, as the transaction coordinator, to manage the life cycle of global transactions.

3 Introduction of Seata-TCC scheme

3.1 SEATA-TCC architecture diagram



Note: This figure is from seATA website

3.2 Distributed transaction flow

In terms of process, TCC mode is basically the same as AT mode in the whole process, as shown below:

In the process, the main differences between AT and TCC modes are:

  • AT mode is to do the proxy in the DB data source layer, by intercepting SQL execution, register branch in the TC before SQL execution; TC mode is implemented via @TwophaseBusinessAction, which is a framework level scan of @TwophaseBusinessAction annotations to register Branch in TCS via interceptors.
  • In the Branch COMMIT/ROLLBACK phase, data is rolled back based on undo log in AT mode. In TCC mode, you need to make your own COMMIT /rollback methods.

3.3 SeATA-TCC principle analysis

3.3.1 sequence diagram

The sequence diagram of TCC mode is very similar to that of AT mode. On the whole, only the part marked with dotted lines is different from that of AT mode.

3.3.2 rainfall distribution on 10-12 the Begin stage

3.3.2.1TM requests TC to start a global transaction

This step is exactly the same as in AT mode. After successful registration, the XID is bound to the context and passed to the individual microservices.

3.3.2.2 Process for TC receiving a Global transaction Request

This step is exactly the same as in AT mode. After execution, the TC will create a global transaction record as follows:

[{" 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}]Copy the code

3.3.3 TM performs business logic

After finish the begin stage in GlobalTransactionalInterceptor blocker, will formally implement the business logic code in TM, namely in the sample:

@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(); }Copy the code

When the service logic is executed, RPC calls the interfaces of other microservices (microservices B and C). RM enters the Register Branch phase during the service logic execution.

3.3.4 Register Branch Phase

This stage is quite different from the AT mode. In THE AT mode, branch transactions are registered before business SQL execution based on DataSourceProxy, while in the TCC mode, branch transactions are registered based on @twophaseBusinessAction.

3.3.4.1 RM Registers branch transactions with TCS

When the system starts up, it generates a proxy for classes using the @TwophaseBusinessAction annotation, which is handled by the TccActionInterceptor. The interceptor is very simple and does a few things:

  • The binding BranchType is branchtype.tcc
  • Get the context of the TCC method through annotations to find the corresponding method directly during the Confirm/Cancel phase.
  • Register branch transactions with TCS. See the code below.
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()); }}Copy the code

Note that TCC saves commitMethod, rollbackMethod, and request context information at this stage.

3.3.4.2 TC Accepts RM Register Branch Process

This process is basically the same as the AT model and will not be covered for the moment. If a branch transaction is successfully registered, another branch transaction record is added to the BRANch_table of the TC.

[{" 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"}]Copy the code

3.3.5 Global Commit/RollBack Phase

This process is basically the same as the AT model and will not be covered for the moment.

3.3.5.1 TM Notifies TCS to Perform Global Commit/Rollback

This process is basically the same as the AT model and will not be covered for the moment.

3.3.5.2 Performing the Global Commit/Rollback Phase on TCS

The first half of this phase is basically the same as the AT pattern,

3.3.6 RM Started the Branch Commit/Rollback phase

Upon receiving the Branch Commit/Rollback request, RM submitted it to the AbstractRMHandler. The AbstractRMHandler then delegates the request to the TCCResourceManager. The Branch Commit/Rollback phase includes the following:

  • Get the TCCResource from the cache
  • From TCCResource commitMethod/rollbackMethod
  • Gets the context in which the request is executed
  • The branchCommit code is as follows, and the rollbackMethod process is similar.
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; }}Copy the code

4 practical experience in TCC

This section is for all TCC schemes.

4.1 Anti-suspension control

Cause analysis: The Try phase timed out (congestion), and the distributed transaction rollback triggers Cancel, which may arrive before the Try.

Solution: Allow empty rollback and reject the Try operation after rollback.

This graph comes from the network, rather lazy do not want to draw their own

4.2 Idempotent control

In distributed development, any write operation requires idempotent control, as do TCC’s distributed transactions.



This graph comes from the network, rather lazy do not want to draw their own

5 Reference Documents

Seata AT mode

Distributed transaction Seata and its three modes, rounding | Meetup# 3 review

TCC for flexible transaction solutions

Principle of Seata

Seata – AT mode

Seata – TCC mode

Seata – Saga mode

Seata – XA mode

TCC ws-transaction principle