原文:https://yq.aliyun.com/articles/682871
本文主要介紹TCC的原理,以及從代碼的角度上分析如何實現的;不涉及具體使用示例。本文分析的是github中開源項目tcc-transaction的代碼,地址為:https://github.com/changmingxie/tcc-transaction,當然github上有多個tcc項目,但是他們原理相近,所以不過多介紹,有興趣的小伙伴自行閱讀源碼。一 TCC架構
1 架構
如上圖所示:
- 一個完整的業務活動由一個主業務服務與若干從業務服務組成。
- 主業務服務負責發起並完成整個業務活動。
- 從業務服務提供TCC型業務操作。
- 業務活動管理器控制業務活動的一致性,它登記業務活動中的操作,並在業務活動提交時進行confirm操作,在業務活動取消時進行cancel操作
TCC和2PC/3PC很像,不過TCC的事務控制都是業務代碼層面的,而2PC/3PC則是資源層面的。
2 各階段規范
TCC事務其實主要包含兩個階段:Try階段、Confirm/Cancel階段。
從TCC的邏輯模型上我們可以看到,TCC的核心思想是,try階段檢查並預留資源,確保在confirm階段有資源可用,這樣可以最大程度的確保confirm階段能夠執行成功。
1) try-嘗試執行業務
完成所有業務檢查(一致性)
預留必須業務資源(准隔離性)
2) confirm-確認執行業務
真正執行業務
不作任何業務檢查
只使用Try階段預留的業務資源
Confirm操作必須保證冪等性
3) cancel-取消執行業務
釋放Try階段預留的業務資源
Cancel操作必須保證冪等性
二 一個示例
下面通過一個示例來討論TCC事務。Tom需要給Tracy轉10元,當使用TCC解決這種事務時,應該如何去做呢?
1 面臨的主要問題
我們考慮一下這個轉賬過程面臨的問題:
- 需要確保Tom賬戶余額不少於10元。
- 需要確保賬戶余額的正確性,例如:假設Tom只有10元錢,但是Tom同時給Tracy、Angle轉賬10元;Tom給其他人轉賬時,也可能收到其他人轉過來的錢,此時賬戶的余額不能出現錯亂(Tracy賬戶也面臨過類似的問題)
- 當並發量比較大時,要能夠確保性能。
2 TCC解決問題的思路
TCC解決分布式事物的思路是,一個大事務拆解成多個小事務。
3 TCC處理邏輯
使用TCC事務時,偽代碼如下所示:
@Compensable(confirmMethod = "transferConfirm", cancelMethod = "transferCancel")
@Transactional
public void transferTry(long fromAccountId, long toAccountId, int amount) {
//檢查Tom賬戶
//鎖定Tom賬戶
//鎖定Tracy賬戶
}
@Transactional
public void transferConfirm(long fromAccountId, long toAccountId, int amount) {
//tom賬戶-10元
//tracy賬戶+10元
}
@Transactional
public void transferCancel(long fromAccountId, long toAccountId, int amount) {
//解除Tom賬戶鎖定
//接觸Tracy賬戶鎖定
}
邏輯如下圖所示:
在Try邏輯中需要確保Tom賬戶的余額足夠,並鎖定需要使用的資源(Tom、Tracy賬戶);如果這一步操作執行成功(沒有出現異常),那么將執行Confirm方法,如果執行失敗,那么將執行Cancel方法。注意Confirm、Cancel需要做好冪等。
三 原理分析
在上面的TCC事務中,轉賬操作其實涉及六次操作,實際項目中,在任何一個步驟都可能失敗,那么當任何一個步驟失敗時,TCC框架是如何做到數據一致性的呢?
1 整體流程圖
以下為TCC的處理流程圖,他可以確保不管是在try階段,還是在confirm/cancel階段都可以確保數據的一致性。
從流程圖上可以看到,TCC依賴於一條事務處理記錄,在開始TCC事務前標記創建此記錄,然后在TCC的每個環節持續更新此記錄的狀態,這樣就可以知道事務執行到那個環節了,當一次執行失敗,進行重試時同樣根據此數據來確定當前階段,並判斷應該執行什么操作。
因為存在失敗重試的邏輯,所以cancel、commit方法必須實現冪等。其實在分布式開發中,凡是涉及到寫操作的地方都應該實現冪等。
2 TCC核心處理邏輯
因為使用了@Compensable注解,所以當調用transferTry方法前,首先進入代理類中。在TCC中有兩個Interceptor會對@Compensable標注的方法生效,他們分別是:CompensableTransactionInterceptor(TCC主要邏輯在此Interceptor中完成)、ResourceCoordinatorInterceptor(處理資源相關的事宜)。
CompensableTransactionInterceptor#interceptCompensableMethod是TCC的核心處理邏輯。interceptCompensableMethod封裝請求數據,為TCC事務做准備,源碼如下:
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();
}
}
rootMethodProceed是TCC和核心處理邏輯,實現了對Try、Confirm、Cancel的執行,源碼如下,重點注意標紅加粗部分:
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;
}
在這個方法中我們看到,首先執行的是@Compensable注解標注的方法(try),如果拋出異常,那么執行rollback方法(cancel),否則執行commit方法(cancel)。
3 異常處理流程
考慮到在try、cancel、confirm過程中都可能發生異常,所以在任何一步失敗時,系統都能夠要么回到最初(未轉賬)狀態,要么到達最終(已轉賬)狀態。下面討論一下TCC代碼層面是如何保證一致性的。
1) Begin
在前面的代碼中,可以看到執行try之前,TCC通過transactionManager.begin()開啟了一個事務,這個begin方法的核心是:
- 創建一個記錄,用於記錄事務執行到那個環節了。
- 注冊當前事務到TransactionManager中,在confirm、cancel過程中可以使用此Transaction來commit或者rollback。
TransactionManager#begin方法
public Transaction begin() {
Transaction transaction = new Transaction(TransactionType.ROOT);
transactionRepository.create(transaction);
registerTransaction(transaction);
return transaction;
}
CachableTransactionRepository#create創建一個用於標識事務執行環節的記錄,然后將transaction放到緩存中區。代碼如下:
@Override
public int create(Transaction transaction) {
int result = doCreate(transaction);
if (result > 0) {
putToCache(transaction);
}
return result;
}
CachableTransactionRepository有多個子類(FileSystemTransactionRepository、JdbcTransactionRepository、RedisTransactionRepository、ZooKeeperTransactionRepository),通過這些類可以實現記錄db、file、redis、zk等的解決方案。
2) Commit/rollback
在commit、rollback中,都有這樣一行代碼,用於更新事務狀態:
transactionRepository.update(transaction);
這行代碼將當前事務的狀態標記為commit/rollback,如果失敗會拋出異常,不會執行后續的confirm/cancel方法;如果成功,才會執行confirm/cancel方法。
3) Scheduler
如果在try/commit/rollback過程中失敗了,請求(transferTry方法)將會立即返回,TCC在這里引入了重試機制,即通過定時程序查詢執行失敗的任務,然后進行補償操作。具體見:
TransactionRecovery#startRecover查詢所有異常事務,然后逐個進行處理。注意重試操作有一個最大重試次數的限制,如果超過最大重試次數,此事務將會被忽略。
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);
}
}
}
}
四 TCC優缺點
目前解決分布式事務的方案中,最穩定可靠的方案有:TCC、2PC/3PC、最終一致性。這三種方案各有優劣,有自己的適用場景。下面我們簡單討論一下TCC主要的優缺點。
1 TCC的主要優點有
因為Try階段檢查並預留了資源,所以confirm階段一般都可以執行成功。
資源鎖定都是在業務代碼中完成,不會block住DB,可以做到對db性能無影響。
TCC的實時性較高,所有的DB寫操作都集中在confirm中,寫操作的結果實時返回(失敗時因為定時程序執行時間的關系,略有延遲)。
2 TCC的主要缺點有
從源碼分析中可以看到,因為事務狀態管理,將產生多次DB操作,這將損耗一定的性能,並使得整個TCC事務時間拉長。
事務涉及方越多,Try、Confirm、Cancel中的代碼就越復雜,可復用性就越底(這一點主要是相對最終一致性方案而言的)。另外涉及方越多,這幾個階段的處理時間越長,失敗的可能性也越高。
五 相關文檔
TCC-Transaction源碼以及使用文檔參考:https://github.com/changmingxie/tcc-transaction
最終一致性解決方案,參考《RocketMQ實踐》