Seata currently supports AT mode, XA mode, TCC mode and SAGA mode. The previous article talked more about the non-invasive AT mode. Today, let's take a look AT the TCC mode submitted in phase II.
What is TCC
TCC is a two-phase commit protocol in distributed transactions. Its full name is Try Confirm Cancel, that is, resource reservation (Try), Confirm and Cancel. Their specific meanings are as follows:
- Try: check and reserve business resources;
- Confirm: submit the business processing, i.e. commit operation. As long as Try succeeds, this step must succeed;
- Cancel: cancel the business processing, that is, rollback. This step releases the resources reserved by Try.
TCC is an intrusive distributed transaction solution. The above three operations need to be implemented by the business system. It is very invasive to the business system and the design is relatively complex. However, the advantage is that TCC does not rely on the database at all and can realize cross database and cross application resource management, For these different data access, an atomic operation is realized through intrusive coding, which better solves the problem of distributed transactions in various complex business scenarios.

Seata TCC mode
The principle of Seata TCC mode is consistent with that of general TCC mode. Let's use Seata TCC mode to implement a distributed transaction:
Assuming that an existing business needs to use service A and service B to complete A transaction operation at the same time, we define A TCC interface of the service in service A:
public interface TccActionOne { @TwoPhaseBusinessAction(name = "DubboTccActionOne", commitMethod = "commit", rollbackMethod = "rollback") public boolean prepare(BusinessActionContext actionContext, @BusinessActionContextParameter(paramName = "a") String a); public boolean commit(BusinessActionContext actionContext); public boolean rollback(BusinessActionContext actionContext); }
Similarly, define a TCC interface of the service in service B:
public interface TccActionTwo { @TwoPhaseBusinessAction(name = "DubboTccActionTwo", commitMethod = "commit", rollbackMethod = "rollback") public void prepare(BusinessActionContext actionContext, @BusinessActionContextParameter(paramName = "b") String b); public void commit(BusinessActionContext actionContext); public void rollback(BusinessActionContext actionContext); }
In the system where the business is located, start the global transaction and execute the TCC resource reservation method of service A and service B:
@GlobalTransactional public String doTransactionCommit() { //Service A transaction participant tccActionOne.prepare(null, "one"); //Service B transaction participant tccActionTwo.prepare(null, "two"); }
The above is an example of using Seata TCC mode to implement A global transaction. It can be seen that the TCC mode also uses @ GlobalTransactional annotation to start A global transaction, and the TCC interfaces of service A and service B are transaction participants. Seata will treat A TCC interface as A Resource, also known as TCC Resource.
The TCC interface can be an RPC or an internal call of the JVM, which means that A TCC interface has two identities: the initiator and the caller. In the above example, the TCC interface is the initiator in service A and service B and the caller in the system where the business is located. If the TCC interface is A Dubbo RPC, the caller is A dubbo:reference and the initiator is A dubbo:service.

When Seata starts, it will scan and parse the TCC interface. If the TCC interface is a publisher, it will register TCC resources with TC when Seata starts, and each TCC Resource has a resource ID; If the TCC interface is a caller, the Seata proxy caller, like the AT mode, the proxy will intercept the call of the TCC interface, that is, each time the Try method is called, a branch transaction will be registered with the TC, and then the original RPC call will be executed.
When the global transaction is committed / rolled back, TC will call back to the corresponding participant service through the resource ID registered in the branch, and execute the Confirm/Cancel method of TCC Resource.
How does Seata implement TCC mode
It can be seen from the above Seata TCC model that the TCC mode also follows the three role models of TC, TM and RM in Seata. How to implement the TCC mode in these three role models? I summarize its main implementation into resource parsing, resource management and transaction processing.
Resource resolution
Resource resolution is to resolve and register the TCC interface. As mentioned earlier, the TCC interface can be a PRC or an internal call of the JVM. In the Seata TCC module, there is a remoting module, which is specially used to resolve the TCC interface resources with TwoPhaseBusinessAction annotation:

RemotingParser interfaces mainly include isRemoting, isReference, isService, getServiceDesc and other methods. The default implementation is DefaultRemotingParser, and other respective RPC protocol parsing classes are executed in DefaultRemotingParser. Seata has implemented the parsing of RPC protocols of Dubbo, HSF, SofaRpc and LocalTCC, and has SPI scalability, In the future, you are welcome to provide more RPC protocol parsing classes for Seata.
During the startup of Seata, a GlobalTransactionScanner annotation is used to scan, and the following methods will be executed:
io.seata.spring.util.TCCBeanParserUtils#isTccAutoProxy
The purpose of this method is to judge whether the bean has been proxied by TCC. In the process, it will first judge whether the bean is a remoting bean. If so, it will call the getServiceDesc method to parse the remoting bean. At the same time, it will judge that if it is an initiator, it will register its resources:
io.seata.rm.tcc.remoting.parser.DefaultRemotingParser#parserRemotingServiceInfo
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(interfaceClass.getMethod(twoPhaseBusinessAction.commitMethod(), twoPhaseBusinessAction.commitArgsClasses())); tccResource.setRollbackMethodName(twoPhaseBusinessAction.rollbackMethod()); tccResource.setRollbackMethod(interfaceClass.getMethod(twoPhaseBusinessAction.rollbackMethod(), twoPhaseBusinessAction.rollbackArgsClasses())); // set argsClasses tccResource.setCommitArgsClasses(twoPhaseBusinessAction.commitArgsClasses()); tccResource.setRollbackArgsClasses(twoPhaseBusinessAction.rollbackArgsClasses()); // set phase two method's keys tccResource.setPhaseTwoCommitKeys(this.getTwoPhaseArgs(tccResource.getCommitMethod(), twoPhaseBusinessAction.commitArgsClasses())); tccResource.setPhaseTwoRollbackKeys(this.getTwoPhaseArgs(tccResource.getRollbackMethod(), twoPhaseBusinessAction.rollbackArgsClasses())); //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; }
In the above methods, first call the getServiceDesc method of the parsing class to parse the remoting bean, put the parsed remotingBeanDesc into the remotingServiceMap of the local cache, and call the isService method of the parsing class to judge whether it is the initiator. If it is the initiator, parse the annotation content of TwoPhaseBusinessAction to generate a TCCResource, And register its resources.
resource management
1. Resource registration
The resource of Seata TCC mode is called TCCResource, and its resource manager is called TCCResource manager. As mentioned earlier, after parsing the RPC resource of the TCC interface, if it is the initiator, it will register the resource:
io.seata.rm.tcc.TCCResourceManager#registerResource
public void registerResource(Resource resource) { TCCResource tccResource = (TCCResource)resource; tccResourceCache.put(tccResource.getResourceId(), tccResource); super.registerResource(tccResource); }
TCC resource contains the relevant information of the TCC interface and will be cached locally. Continue to call the parent class registerResource method (encapsulating the communication method) to register with TC. The resourceId of TCC resource is actionName, and actionName is the name in @ TwoParseBusinessAction annotation.
2. Resource commit / rollback
io.seata.rm.tcc.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); // ... ... ret = commitMethod.invoke(targetTCCBean, args); // ... ... 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; } }
When TM decides to submit the second phase, TC will call back the resource ID registered by the branch to the corresponding participant (i.e. the initiator of TCC interface) service to execute the Confirm/Cancel method of TCC Resource.
The resource manager will find the corresponding TCCResource in the local cache according to the resourceId, and find the corresponding BusinessActionContext context according to xid, branchId, resourceId and applicationData. The executed parameters are in the context. Finally, execute the method of obtaining commit in TCC resource for two-phase submission.
The two-stage rollback is similar.
transaction processing
As mentioned earlier, if the TCC interface is a caller, the Seata TCC proxy will be used to intercept the caller and register the branch before calling the real RPC method.
Execution method io seata. spring. util. Tccbeanparserutils#istccautoproxy not only parses the TCC interface resources, but also judges whether the TCC interface is the caller. If it is the caller, it returns true:
io.seata.spring.annotation.GlobalTransactionScanner#wrapIfNecessary

As shown in the figure, when the GlobalTransactionalScanner scans the TCC interface caller (Reference), it will cause the TccActionInterceptor to intercept it, and the TccActionInterceptor implements the MethodInterceptor.
In TccActionInterceptor, ActionInterceptorHandler type will also be called to execute interception processing logic. Transaction related processing is in ActionInterceptorHandler#processed method:
public Object proceed(Method method, Object[] arguments, String xid, TwoPhaseBusinessAction businessAction, Callback<Object> targetCallback) throws Throwable { //Get action context from arguments, or create a new one and then reset to arguments BusinessActionContext actionContext = getOrCreateActionContextAndResetToArguments(method.getParameterTypes(), arguments); //Creating Branch Record String branchId = doTccActionLogStore(method, arguments, businessAction, actionContext); // ... ... try { // ... ... return targetCallback.execute(); } finally { try { //to report business action context finally if the actionContext.getUpdated() is true BusinessActionContextUtil.reportContext(actionContext); } finally { // ... ... } } }
Above, before executing the first phase of the TCC interface, the doTccActionLogStore method will be called for branch registration, and TCC related information such as parameters will be placed in the context, which will be used for resource submission / rollback mentioned above.
How to control exceptions
During the execution of TCC model, various exceptions may occur, among which the most common are empty rollback, idempotent, suspension, etc. Let me talk about how Seata handles these three exceptions.
How to handle empty rollback
What is an empty rollback?
Empty rollback means that in a distributed transaction, TM driven two-phase rollback calls the Cancel method of the party without calling the Try method of the party.
So how does empty rollback come about?

As shown in the above figure, after the global transaction is started, the participant A branch registration will execute the participant A phase I RPC method. If the machine of participant A goes down and the network is abnormal, the RPC call will fail, that is, the participant A phase I method is not successfully executed, but the global transaction has been started, and Seata must advance to the final state, When the global transaction is rolled back, the Cancel method of participant A will be called, resulting in an empty rollback.
To prevent empty rollback, you must identify it as an empty rollback in the Cancel method. How does Seata do it?
Seata's approach is to add a new TCC transaction control table, which contains the XID and BranchID information of the transaction. When the Try method is executed, a record is inserted to indicate that the first stage has been executed. When the Cancel method is executed, this record is read. If the record does not exist, it indicates that the Try method has not been executed.
How to deal with idempotent
Idempotent problem refers to the repeated two-stage submission of TC. Therefore, the Confirm/Cancel interface needs to support idempotent processing, that is, there will be no repeated submission or release of resources.
So how does the idempotent problem arise?

As shown in the above figure, after participant A executes phase II, due to network jitter or downtime, TC will not receive the return result of participant A's execution of phase II, and TC will repeatedly initiate the call until the execution result of phase II is successful.
How does Seata deal with the idempotent problem?
Similarly, a record status field status is added to the TCC transaction control table. This field has three values, namely:
- tried: 1
- committed: 2
- rollbacked: 3
After the two-stage Confirm/Cancel method is executed, change the state to committed or rolled back. When the two-stage Confirm/Cancel method is called repeatedly, the idempotent problem can be solved by judging the transaction state.
How to handle suspension
Suspension means that the two-stage Cancel method is executed prior to the one-stage Try method. Because empty rollback is allowed, the direct empty rollback returns success after the two-stage Cancel method is executed. At this time, the global transaction has ended. However, due to the subsequent execution of the Try method, the resources reserved by the one-stage Try method can never be committed and released.
So how did the suspension come about?

As shown in the above figure, when executing the one-stage Try method of participant A, there is network congestion. Due to the timeout limit of Seata global transactions, TM decides to roll back the global transaction after the execution of Try method times out. After the rollback is completed, if the RPC request reaches participant A at this time, execute Try method for resource reservation, resulting in suspension.
What does Seata do with suspension?
Add a status to the status field status in the TCC transaction control table:
- suspended: 4
When the two-stage Cancel method is executed, if it is found that there are relevant records in the TCC transaction control table, it indicates that the two-stage Cancel method takes precedence over the one-stage Try method, so a record with status=4 is inserted. When the one-stage Try method is executed later, it is judged that status=4, it indicates that the two-stage Cancel has been executed, And return false to prevent the successful execution of the one-stage Try method.