TCC事務原理


原文:https://yq.aliyun.com/articles/682871

本文主要介紹TCC的原理,以及從代碼的角度上分析如何實現的;不涉及具體使用示例。本文分析的是github中開源項目tcc-transaction的代碼,地址為:https://github.com/changmingxie/tcc-transaction,當然github上有多個tcc項目,但是他們原理相近,所以不過多介紹,有興趣的小伙伴自行閱讀源碼。一 TCC架構

1  架構

 

f81a815047d761053273ff2e5f33b9e603e4f713

如上圖所示:

 - 一個完整的業務活動由一個主業務服務與若干從業務服務組成。

 - 主業務服務負責發起並完成整個業務活動。

 - 從業務服務提供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解決問題的思路

5e77c9b1bbdfa607393644d3e2b3427db93b1c08

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賬戶鎖定
}

邏輯如下圖所示:

e3ff945eedac9e35269a47aa2d0efd4a9db844a1

在Try邏輯中需要確保Tom賬戶的余額足夠,並鎖定需要使用的資源(Tom、Tracy賬戶);如果這一步操作執行成功(沒有出現異常),那么將執行Confirm方法,如果執行失敗,那么將執行Cancel方法。注意Confirm、Cancel需要做好冪等。

 

 原理分析

在上面的TCC事務中,轉賬操作其實涉及六次操作,實際項目中,在任何一個步驟都可能失敗,那么當任何一個步驟失敗時,TCC框架是如何做到數據一致性的呢?

1  整體流程圖

以下為TCC的處理流程圖,他可以確保不管是在try階段,還是在confirm/cancel階段都可以確保數據的一致性。

b5b927eb63c1c76388a213f6a2825b7ae93c3a3b

從流程圖上可以看到,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實踐


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM