This paper mainly introduces the principle of TCC and analyzes how to achieve it from the point of view of code. No specific example is provided. This article examines tCC-Transaction, an open source project on Github. Of course, there are several TCC projects on Github, but their principles are similar, so I will not introduce more, interested partners to read the source code.
1 TCC architecture
1.1 the principle
- A complete business activity consists of a master business service and several slave business services.
- The master business service is responsible for initiating and completing the entire business activity.
- Provide TCC business operations from business services.
- The business activity manager controls the consistency of business activities by registering actions within a business activity and performing confirm actions when the business activity is committed and cancel actions when the business activity is canceled.
TCC is similar to 2PC/3PC, except that TCC’s transaction control is at the business code level, while 2PC/3PC is at the resource level.
1.2 Specifications for each stage
A TCC transaction consists of two phases: the Try phase and the Confirm/Cancel phase. From the logical model of TCC, we can see that the core idea of TCC is to check and reserve resources in the try phase to ensure that resources are available in the confirm phase, which can ensure the successful execution of the confirm phase to the greatest extent.
1.2.1 Try: Attempts to execute services
- Complete all business checks (consistency)
- Reserve required business resources (quasi-isolation)
1.2.2 Confirm: Confirm the service
- Actually doing business
- No operational checks are made
- Use only the service resources reserved in the Try phase
- Confirm operations must be idempotent
1.2.2 Cancel: Cancels services
- Release service resources reserved during the Try phase
- The Cancel operation must be idempotent
2 TCC source code analysis
In the above TCC transaction, the transfer operation actually involves six operations. In the actual project, any step may fail. So when any step fails, how does the TCC framework achieve data consistency?
2.1 Overall Flow chart
The following is a TCC process flow chart that ensures data consistency during both the try and Confirm/Cancel phases.
Can see from the diagram, TCC is dependent on a transaction record, before starting the TCC affairs tags to create the record, and then continued to update the record in TCC’s each link, so you can know that link transaction execution, as a failure, also according to retry this data to determine the current stage, And decide what to do. The cancel and COMMIT methods must be idempotent because there is retry logic for failure. In distributed development, idempotent should be implemented wherever writing is involved.
2.2 TCC core processing logic
With the @compensable annotation, the transferTry method is first invoked in the proxy class. There are two interceptors that apply to @compensable in TCC. They are: Main logic in the Interceptor CompensableTransactionInterceptor (TCC), ResourceCoordinatorInterceptor (processing resources related matters).
CompensableTransactionInterceptor# interceptCompensableMethod is the core of the TCC processing logic. InterceptCompensableMethod encapsulates the request data, prepare the way for TCC affairs, source code is as follows: public Object interceptCompensableMethod(ProceedingJoinPoint pjp) throws Throwable { Method method = CompensableMethodUtils.getCompensableMethod(pjp); Compensable compensable = method.getAnnotation(Compensable.class); Propagation propagation = compensable.propagation(); TransactionContext transactionContext = FactoryBuilder.factoryOf(compensable.transactionContextEditor()).getInstance().get(pjp.getTarget(), method, pjp.getArgs()); boolean asyncConfirm = compensable.asyncConfirm(); boolean asyncCancel = compensable.asyncCancel(); boolean isTransactionActive = transactionManager.isTransactionActive(); if (! TransactionUtils.isLegalTransactionContext(isTransactionActive, propagation, transactionContext)) { throw new SystemException("no active compensable transaction while propagation is mandatory for method " + method.getName()); } MethodType methodType = CompensableMethodUtils.calculateMethodType(propagation, isTransactionActive, transactionContext); switch (methodType) { case ROOT: return rootMethodProceed(pjp, asyncConfirm, asyncCancel); case PROVIDER: return providerMethodProceed(pjp, transactionContext, asyncConfirm, asyncCancel); default: return pjp.proceed(); }}Copy the code
RootMethodProceed is TCC and core processing logic that implements Try, Confirm, and Cancel execution.
private Object rootMethodProceed(ProceedingJoinPoint pjp, boolean asyncConfirm, boolean asyncCancel) throws Throwable {
Object returnValue = null;
Transaction transaction = null;
try {
transaction = transactionManager.begin();
try {
returnValue = pjp.proceed();
} catch (Throwable tryingException) {
if (isDelayCancelException(tryingException)) {
transactionManager.syncTransaction();
} else {
logger.warn(String.format("compensable transaction trying failed. transaction content:%s", JSON.toJSONString(transaction)), tryingException);
transactionManager.rollback(asyncCancel);
}
throw tryingException;
}
transactionManager.commit(asyncConfirm);
} finally {
transactionManager.cleanAfterCompletion(transaction);
}
return returnValue;
}
Copy the code
What we see in this method is that the @compensable annotation (try) is executed first, and the rollback (cancel) is executed if an exception is thrown, or the commit (cancel) is executed otherwise.
2.3 Exception Handling Process
Given that exceptions can occur during try, Cancel, and Confirm, the system can either return to the original (untransferred) state or reach the final (transferred) state if any step fails. Let’s discuss how consistency is ensured at the TCC code level.
2.3.1 the Begin
In the previous code, you can see that TCC starts a transaction with transactionManager.begin() before executing a try. The core of the begin method is:
- Create a record of where the transaction has been executed.
- Register the current Transaction with TransactionManager and use this Transaction to commit or rollback during confirm or Cancel. TransactionManager# begin method
public Transaction begin() {
Transaction transaction = new Transaction(TransactionType.ROOT);
transactionRepository.create(transaction);
registerTransaction(transaction);
return transaction;
}
Copy the code
CachableTransactionRepository# create create a used to identify the record of transactions executed link, and then put the transaction in the central district cache. The code is as follows:
@Override
public int create(Transaction transaction) {
int result = doCreate(transaction);
if (result > 0) {
putToCache(transaction);
}
return result;
}
Copy the code
CachableTransactionRepository have multiple subclasses (FileSystemTransactionRepository JdbcTransactionRepository RedisTransactionRepository, ZooKeeperTransactionRepository), through these classes can implement record db, file, redis, zk solution, etc.
2.3.2 Commit/rollback
In the commit and rollback, have such a line of code, used to update the transaction status: transactionRepository. Update (transaction); This line marks the current transaction status as COMMIT /rollback, and if it fails, an exception will be thrown and subsequent Confirm/Cancel methods will not be executed. If successful, the confirm/ Cancel method is executed.
2.3.3 the Scheduler
If the try/commit/ ROLLBACK process fails, the request (transferTry method) will be returned immediately. TCC introduced the retry mechanism here, that is, the failed task is executed through the scheduled program query and then the compensation operation is performed. TransactionRecovery#startRecover queries all abnormal transactions and processes them one by one. Note that there is a maximum number of retries for a retry operation. If the maximum number of retries is exceeded, the transaction will be ignored.
public void startRecover() { List<Transaction> transactions = loadErrorTransactions(); recoverErrorTransactions(transactions); } private List<Transaction> loadErrorTransactions() { long currentTimeInMillis = Calendar.getInstance().getTimeInMillis(); TransactionRepository transactionRepository = transactionConfigurator.getTransactionRepository(); RecoverConfig recoverConfig = transactionConfigurator.getRecoverConfig(); return transactionRepository.findAllUnmodifiedSince(new Date(currentTimeInMillis - recoverConfig.getRecoverDuration() * 1000)); } private void recoverErrorTransactions(List<Transaction> transactions) { for (Transaction transaction : transactions) { if (transaction.getRetriedCount() > transactionConfigurator.getRecoverConfig().getMaxRetryCount()) { logger.error(String.format("recover failed with max retry count,will not try again. txid:%s, status:%s,retried count:%d,transaction content:%s", transaction.getXid(), transaction.getStatus().getId(), transaction.getRetriedCount(), JSON.toJSONString(transaction))); continue; } if (transaction.getTransactionType().equals(TransactionType.BRANCH) && (transaction.getCreateTime().getTime() + transactionConfigurator.getRecoverConfig().getMaxRetryCount() * transactionConfigurator.getRecoverConfig().getRecoverDuration() * 1000 > System.currentTimeMillis())) { continue; } try { transaction.addRetriedCount(); if (transaction.getStatus().equals(TransactionStatus.CONFIRMING)) { transaction.changeStatus(TransactionStatus.CONFIRMING); transactionConfigurator.getTransactionRepository().update(transaction); transaction.commit(); transactionConfigurator.getTransactionRepository().delete(transaction); } else if (transaction.getStatus().equals(TransactionStatus.CANCELLING) || transaction.getTransactionType().equals(TransactionType.ROOT)) { transaction.changeStatus(TransactionStatus.CANCELLING); transactionConfigurator.getTransactionRepository().update(transaction); transaction.rollback(); transactionConfigurator.getTransactionRepository().delete(transaction); } } catch (Throwable throwable) { if (throwable instanceof OptimisticLockException || ExceptionUtils.getRootCause(throwable) instanceof OptimisticLockException) { logger.warn(String.format("optimisticLockException happened while recover. txid:%s, status:%s,retried count:%d,transaction content:%s", transaction.getXid(), transaction.getStatus().getId(), transaction.getRetriedCount(), JSON.toJSONString(transaction)), throwable); } else { logger.error(String.format("recover failed, txid:%s, status:%s,retried count:%d,transaction content:%s", transaction.getXid(), transaction.getStatus().getId(), transaction.getRetriedCount(), JSON.toJSONString(transaction)), throwable); }}}}Copy the code
3 Advantages and disadvantages of TCC
At present, the most stable and reliable solutions to solve distributed transactions include TCC, 2PC/3PC and final consistency. These three schemes have their own advantages and disadvantages and have their own applicable scenarios. Let’s briefly discuss the main advantages and disadvantages of TCC.
3.1 Main advantages of TCC:
Because the Try phase checks and reserves resources, the Confirm phase usually succeeds. Resource locking is performed in service code and does not block the DB. Therefore, it has no impact on DB performance. TCC has high real-time performance. All DB write operations are concentrated in confirm, and the results of write operations are returned in real time (the failure is slightly delayed due to the execution time of the timing program).
3.2 Main disadvantages of TCC:
As you can see from the source code analysis, because of transaction state management, there will be multiple DB operations, which will cost some performance and make the overall TCC transaction time longer. The more parties involved in a transaction, the more complex the code in Try, Confirm, Cancel becomes, and the less reusable it is (mainly relative to the final consistency scheme). In addition, the more parties involved, the longer the processing time of these stages, the higher the possibility of failure.
4 Related Documents
Principle of Seata
Seata – AT mode
Seata – TCC mode
Seata – Saga mode
Seata – XA mode
TCC ws-transaction principle