tcc-transation原始碼分析與思考

土豆肉絲蓋澆飯發表於2018-04-27

tcc介紹

tcc是分散式事務的一種解決方案,即Try,Commit,Cancel

Try: 嘗試執行業務

完成所有業務檢查(一致性)

預留必須業務資源(準隔離性)
複製程式碼

Confirm: 確認執行業務

真正執行業務

不作任何業務檢查

只使用Try階段預留的業務資源

Confirm操作滿足冪等性
複製程式碼

Cancel: 取消執行業務

釋放Try階段預留的業務資源

Cancel操作滿足冪等性
複製程式碼

本文我會講解一個實現tcc思想的框架,tcc-transaction

image.png
在github還是比較火的,並且應該也有公司生產使用,熟悉它的原始碼,一方面可以提升自己眼界,擴寬自己編碼能力,另一方面,以後需要使用它的時候,難免有坑需要修改,也能為開源貢獻一份力量

tcc-transaction使用

在tcc-transaction的tcc-transaction-tutorial-sample模組,做相關配置執行即可

tcc-transaction原理

這邊我們主要講解tcc-transaction和dubbo的整合 tcc-transaction的主要原理是Aop,那麼以後面試的時候,問用Aop做什麼,就可以回答這個了,再把tcc講一下,完美的連招 作為一個處理分散式事務的框架,先來講下tcc-transaction的事務抽象

Transaction

tcc的事務,並不是資料庫的事務,而是應用層的事務,所以tcc這三個階段的操作,全部都需要我們手動實現 先看下Transaction物件的引數

    private TransactionXid xid;

    private TransactionStatus status;

    private TransactionType transactionType;

    private volatile int retriedCount = 0;

    private Date createTime = new Date();

    private Date lastUpdateTime = new Date();

    private long version = 1;

    private List<Participant> participants = new ArrayList<Participant>();

    private Map<String, Object> attachments = new ConcurrentHashMap<String, Object>();
複製程式碼
引數 解釋
xid 全域性事務id,內容相當於uuid,用來保證事務唯一性
status 事務的狀態,可以為TRYING,CONFIRMING,CANCELLING,分別對應tcc三個階段
transactionType 事務型別,可以為ROOT,BRANCH,ROOT是主事務,BRANCH是分支事務,事務的發起方法對應主事務,其他被呼叫的dubbo介面在分支事務上
retriedCount 事務重試次數,當confirm或者cancel失敗重試時增加
createTime 事務的建立時間
lastUpdateTime 事務最後一次更新時間
version 事務的版本號,樂觀鎖?
participants 事務的參與者
attachments 附加引數,暫時沒發現被用到

再看下Transaction中兩個比較重要的方法

public void commit() {

    for (Participant participant : participants) {
        participant.commit();
    }
}

public void rollback() {
    for (Participant participant : participants) {
        participant.rollback();
    }
}
複製程式碼

執行Transaction的commit或rollback方法,會對應執行所有participant的commit或rollback方法

下面講解Participant抽象

Participant

Participant是對tcc事務參與者的抽象

public class Participant implements Serializable {

    private static final long serialVersionUID = 4127729421281425247L;

    private TransactionXid xid;

    private InvocationContext confirmInvocationContext;

    private InvocationContext cancelInvocationContext;

    private Terminator terminator = new Terminator();

    Class<? extends TransactionContextEditor> transactionContextEditorClass;

    ......
}
複製程式碼

在Participant中使用InvocationContext把事務參與者的confirm和cancel方法的後設資料儲存下來

public class InvocationContext implements Serializable {

    private static final long serialVersionUID = -7969140711432461165L;
    private Class targetClass;

    private String methodName;

    private Class[] parameterTypes;

    private Object[] args;
}
複製程式碼

後設資料包括目標Class,目標方法名,目標引數列表,實際引數列表

Participant也儲存了transactionContextEditorClass,用於提取事務上下文

接下來看Participant的commit和rollback方法

 public void rollback() {
        terminator.invoke(new TransactionContext(xid, TransactionStatus.CANCELLING.getId()), cancelInvocationContext, transactionContextEditorClass);
    }

    public void commit() {
        terminator.invoke(new TransactionContext(xid, TransactionStatus.CONFIRMING.getId()), confirmInvocationContext, transactionContextEditorClass);
    }
複製程式碼

會通過terminator執行具體的操作,傳入構造好的TransactionContext,confirmInvocationContext和transactionContextEditorClass

public Object invoke(TransactionContext transactionContext, InvocationContext invocationContext, Class<? extends TransactionContextEditor> transactionContextEditorClass) {


        if (StringUtils.isNotEmpty(invocationContext.getMethodName())) {

            try {
                //從spring容器中獲取對應事務參與者實現
                Object target = FactoryBuilder.factoryOf(invocationContext.getTargetClass()).getInstance();

                Method method = null;
                //獲取對應方法反射物件
                method = target.getClass().getMethod(invocationContext.getMethodName(), invocationContext.getParameterTypes());
                //設定上下文
                FactoryBuilder.factoryOf(transactionContextEditorClass).getInstance().set(transactionContext, target, method, invocationContext.getArgs());
                //反射呼叫
                return method.invoke(target, invocationContext.getArgs());

            } catch (Exception e) {
                throw new SystemException(e);
            }
        }
        return null;
    }
複製程式碼

在terminator的呼叫過程中,我們需要理解的一點的是,Participant其實分兩種,第一種是本地的Participant,直接反射呼叫即可,第二種是遠端的Participant,也就是呼叫的是Dubbo介面,反射呼叫的同時會執行遠端對等端的介面邏輯,所以這裡需要用到transactionContextEditorClass來設定上下文資訊,傳遞到遠端dubbo介面中去

TransactionContext

image.png
TransactionContext的儲存了全域性事務id和事務狀態,在呼叫事務參與者Participant的confirm或cancel方法時會傳遞過去

TransactionContextEditor

public interface TransactionContextEditor {

    public TransactionContext get(Object target, Method method, Object[] args);

    public void set(TransactionContext transactionContext, Object target, Method method, Object[] args);

}
複製程式碼

TransactionContextEditor用於呼叫事務參與者方法時,設定和獲取需要傳遞的TransactionContext,目前有2種實現,DefaultTransactionContextEditor和DubboTransactionContextEditor DefaultTransactionContextEditor從方法引數裡面提取TransactionContext物件

@Override
        public TransactionContext get(Object target, Method method, Object[] args) {
            //獲取TransactionContext在args中的位置
            int position = getTransactionContextParamPosition(method.getParameterTypes());

            if (position >= 0) {
                return (TransactionContext) args[position];
            }

            return null;
        }
複製程式碼

DubboTransactionContextEditor從Dubbo Rpc上下文提取TransactionContext物件

@Override
    public TransactionContext get(Object target, Method method, Object[] args) {
        //從Dubbo Rpc上下文獲取
        String context = RpcContext.getContext().getAttachment(TransactionContextConstants.TRANSACTION_CONTEXT);

        if (StringUtils.isNotEmpty(context)) {
            return JSON.parseObject(context, TransactionContext.class);
        }

        return null;
    }
複製程式碼

TransactionManager

TransactionManager用來控制Transaction的生命週期,Transaction的改變使用TransactionRepository同步到第三方儲存,一般使用mysql資料庫儲存 TransactionManager包含的方法以及屬性如下

image.png

變數介紹

  1. transactionRepository
 private TransactionRepository transactionRepository;
複製程式碼

TransactionRepository用於對Transaction的持久化操作,如果是JDBC實現,其實就是對一張Transaction表的CRUD,這張表主要用於補償任務 2. CURRENT

private static final ThreadLocal<Deque<Transaction>> CURRENT = new ThreadLocal<Deque<Transaction>>();
複製程式碼

這是一個雙向佇列,在這個類主要用作棧,用來處理事務的巢狀,因為是ThreadLocal,所以針對每個執行緒都是獨立的 3. executorService

 private ExecutorService executorService;
複製程式碼

執行緒池,用於非同步執行commit或者cancel

方法介紹

  1. registerTransaction
private void registerTransaction(Transaction transaction) {

    if (CURRENT.get() == null) {
        CURRENT.set(new LinkedList<Transaction>());
    }

    CURRENT.get().push(transaction);
}
複製程式碼

把transaction設定到ThreadLocal物件中去,push方法對應入棧 2. begin public Transaction begin() {

Transaction transaction = new Transaction(TransactionType.ROOT);
transactionRepository.create(transaction);
registerTransaction(transaction);
return transaction;
複製程式碼

} 開啟事務,同步到repository,註冊到ThreadLocal,這個方法與用於主事務的建立

  1. propagationNewBegin
public Transaction propagationNewBegin(TransactionContext transactionContext) {

    Transaction transaction = new Transaction(transactionContext);
    transactionRepository.create(transaction);

    registerTransaction(transaction);
    return transaction;
}
複製程式碼

這個方法用於從主事務的上下文建立分支事務,xid保持不變,事務型別變化

  1. propagationExistBegin
public Transaction propagationExistBegin(TransactionContext transactionContext) throws NoExistedTransactionException {
    Transaction transaction = transactionRepository.findByXid(transactionContext.getXid());

    if (transaction != null) {
        transaction.changeStatus(TransactionStatus.valueOf(transactionContext.getStatus()));
        registerTransaction(transaction);
        return transaction;
    } else {
        throw new NoExistedTransactionException();
    }
}
複製程式碼

這個方法用於從事務上下文同步事務狀態到ThreadLocal

  1. commit
public void commit(boolean asyncCommit) {
    //從threadlocal得到當前事務
    final Transaction transaction = getCurrentTransaction();

    transaction.changeStatus(TransactionStatus.CONFIRMING);
    //資料庫更新transaction
    transactionRepository.update(transaction);

    if (asyncCommit) {
        try {
            Long statTime = System.currentTimeMillis();
            //通過執行緒池非同步執行事務提交
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    commitTransaction(transaction);
                }
            });
            logger.debug("async submit cost time:" + (System.currentTimeMillis() - statTime));
        } catch (Throwable commitException) {
            logger.warn("compensable transaction async submit confirm failed, recovery job will try to confirm later.", commitException);
            throw new ConfirmingException(commitException);
        }
    } else {
        //同步執行事務提交
        commitTransaction(transaction);
    }
}
複製程式碼

這個方法執行事務的commit,實際事務提交在commitTransaction中執行,會執行每個事務參與者的commit方法

private void commitTransaction(Transaction transaction) {
    try {
        //呼叫事務參與者的提交方法
        transaction.commit();
        //事務結束,在資料庫刪除當前事務,如果commit異常,不會把資料庫內事務記錄刪除,為了重試補償
        transactionRepository.delete(transaction);
    } catch (Throwable commitException) {
        logger.warn("compensable transaction confirm failed, recovery job will try to confirm later.", commitException);
        throw new ConfirmingException(commitException);
    }
}
複製程式碼

看了這段邏輯後,我們可以發現,在commit失敗的時候,並不會觸發rollback,而是不刪除資料庫事務記錄,之後會有定時任務進行掃描重試,後面會講到這個定時任務

  1. rollback
public void rollback(boolean asyncRollback) {

    final Transaction transaction = getCurrentTransaction();
    transaction.changeStatus(TransactionStatus.CANCELLING);
    
    transactionRepository.update(transaction);

    if (asyncRollback) {

        try {
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    rollbackTransaction(transaction);
                }
            });
        } catch (Throwable rollbackException) {
            logger.warn("compensable transaction async rollback failed, recovery job will try to rollback later.", rollbackException);
            throw new CancellingException(rollbackException);
        }
    } else {

        rollbackTransaction(transaction);
    }
}
複製程式碼

這個方法用於執行事務的回滾邏輯,和commit方法類似,在rollbackTransaction方法中,會執行每個事務參與者的rollback方法

private void rollbackTransaction(Transaction transaction) {
    try {
        //呼叫事務參與者的提交方法
        transaction.rollback();
         //事務結束,在資料庫刪除當前事務,如果rollback異常,不會把資料庫內事務記錄刪除,為了重試補償
        transactionRepository.delete(transaction);
    } catch (Throwable rollbackException) {
        logger.warn("compensable transaction rollback failed, recovery job will try to rollback later.", rollbackException);
        throw new CancellingException(rollbackException);
    }
}
複製程式碼
  1. cleanAfterCompletion
public void cleanAfterCompletion(Transaction transaction) {
    if (isTransactionActive() && transaction != null) {
        Transaction currentTransaction = getCurrentTransaction();
        if (currentTransaction == transaction) {
            //棧操作,後進先出
            CURRENT.get().pop();
        } else {
            throw new SystemException("Illegal transaction when clean after completion");
        }
    }
}
複製程式碼

事務結束,從棧中彈出結束的事務。

  1. enlistParticipant
public void enlistParticipant(Participant participant) {
        Transaction transaction = this.getCurrentTransaction();
        transaction.enlistParticipant(participant);
        transactionRepository.update(transaction);
    }
複製程式碼

給事務繫結事務參與者並同步到repository

接下來講下核心的兩個切面,這兩個切面把上面的所有元件串聯在一起使用

核心Aspect

在使用tcc-transaction的時候,我們需要對開啟tcc事務的方法加上@Compensable註解,這個註解可以設定以下引數

引數 解釋
propagation 事務傳播性,包含REQUIRED(必須存在事務,不存在,建立),SUPPORTS(有事務的話在事務內執行),MANDATORY(必須存在事務),REQUIRES_NEW(不管是否存在,建立新的事務)
confirmMethod confirm階段對應的方法
cancelMethod cancel階段對應的方法
transactionContextEditor 設定對應transactionContextEditor
asyncConfirm 是否非同步confirm
asyncCancel 是否非同步cancel

@Compensable註解的引數會在下面兩個切面中使用到

ConfigurableTransactionAspect

ConfigurableTransactionAspect主要用來控制Transaction的生命週期,內部通過CompensableTransactionInterceptor實現

@Pointcut("@annotation(org.mengyun.tcctransaction.api.Compensable)")
public void compensableService() {

}

@Around("compensableService()")
public Object interceptCompensableMethod(ProceedingJoinPoint pjp) throws Throwable {

    return compensableTransactionInterceptor.interceptCompensableMethod(pjp);
}
複製程式碼

直接看interceptCompensableMethod方法

public Object interceptCompensableMethod(ProceedingJoinPoint pjp) throws Throwable {

    //解析@Compensable註解
    Method method = CompensableMethodUtils.getCompensableMethod(pjp);
    Compensable compensable = method.getAnnotation(Compensable.class);
    Propagation propagation = compensable.propagation();
    //獲取上下文,如果是Root,不會存在上下文,Transaction都還沒建立
    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());
    }

    //計算方法型別,Root對應主事務入口方法,Provider對應遠端提供者方的方法,Normal是主事務內消費者方的方法(是代理方法)
    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();
    }
}
複製程式碼

在tcc事務內被@Compensable註解的方法分三種

  1. Root方法,就是這次事務的入口方法
  2. Normal方法,在Root方法呼叫的dubbo介面方法
  3. Provider方法,對應dubbo介面方法的遠端實現 被註解的方法都是try的邏輯,confirm和cancel邏輯配置在@Compensable註解引數中

對被@Compensable註解的方法執行切面邏輯的時候,會根據這三種方法型別做不同處理 對於Root方法,執行rootMethodProceed的邏輯

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;
}
複製程式碼

會在被切方法(對應try邏輯)執行前,開啟事務,try邏輯執行成功,通過transactionManager的commit方法執行每個事務參與者的commit邏輯,如果try失敗,通過transactionManager執行每個參與者的rollback邏輯。

對於Provider方法

private Object providerMethodProceed(ProceedingJoinPoint pjp, TransactionContext transactionContext, boolean asyncConfirm, boolean asyncCancel) throws Throwable {

    Transaction transaction = null;
    try {

        switch (TransactionStatus.valueOf(transactionContext.getStatus())) {
            case TRYING:
                //使用transactionContext建立分支事務
                transaction = transactionManager.propagationNewBegin(transactionContext);
                //執行被切方法邏輯
                return pjp.proceed();
            case CONFIRMING:
                try {
                    //更新事務狀態
                    transaction = transactionManager.propagationExistBegin(transactionContext);
                    //提交事務,不走切面的方法
                    transactionManager.commit(asyncConfirm);
                } catch (NoExistedTransactionException excepton) {
                    //the transaction has been commit,ignore it.
                }
                break;
            case CANCELLING:

                try {
                    //更新事務狀態
                    transaction = transactionManager.propagationExistBegin(transactionContext);
                    //回滾事務,不走切面的方法
                    transactionManager.rollback(asyncCancel);
                } catch (NoExistedTransactionException exception) {
                    //the transaction has been rollback,ignore it.
                }
                break;
        }

    } finally {
        //清理資源
        transactionManager.cleanAfterCompletion(transaction);
    }

    Method method = ((MethodSignature) (pjp.getSignature())).getMethod();

    //對於 confirm和 cancel 返回空值
    //主要針對原始型別做處理,因為不能為null
    return ReflectionUtils.getNullValue(method.getReturnType());
}
複製程式碼

可以看到在provider型別方法的切面,也就是遠端的Participant,如果transaction的status為trying,會通過transactionManager.propagationNewBegin建立分支事務並執行被切方法邏輯,如果是status為confirming或者canceling,會呼叫對應confirm或cancel配置的方法,跳過被切方法

對於normal型別 直接呼叫,normal型別的方法是封裝了對遠端dubbo介面方法呼叫邏輯的本地代理方法,所以直接執行即可

ConfigurableCoordinatorAspect

ConfigurableCoordinatorAspect主要是為了設定事務的參與者,在一個事務內,每個被@Compensable註解的方法都是事務參與者

@Pointcut("@annotation(org.mengyun.tcctransaction.api.Compensable)")
public void transactionContextCall() {

}

@Around("transactionContextCall()")
public Object interceptTransactionContextMethod(ProceedingJoinPoint pjp) throws Throwable {
    return resourceCoordinatorInterceptor.interceptTransactionContextMethod(pjp);
}
複製程式碼

相關邏輯封裝在ResourceCoordinatorInterceptor的interceptTransactionContextMethod方法中

public Object interceptTransactionContextMethod(ProceedingJoinPoint pjp) throws Throwable {

    //得到當前事務
    Transaction transaction = transactionManager.getCurrentTransaction();

    if (transaction != null) {

        switch (transaction.getStatus()) {
            case TRYING:
                //只需要在trying的時候把參與者資訊提取出來,設定到transaction中去
                enlistParticipant(pjp);
                break;
            case CONFIRMING:
                break;
            case CANCELLING:
                break;
        }
    }

    //執行目標方法
    return pjp.proceed(pjp.getArgs());
}
複製程式碼

在trying階段會把所有參與者加入到事務中去,對於Root方法,建立主事務,加入的參與者會包括Root方法對應本地參與者以及Normal方法對應的遠端參與者,對於Provider方法,通過主事務上下文建立分支事務,加入的參與者包括Provider方法對應的本地參與者以及它包含的Normal方法對應的遠端參與者。這裡的遠端參與者又可以開啟新的分支事務。層級多了,勢必會產生效能問題。

接下來看enlistParticipant如何生成參與者物件

private void enlistParticipant(ProceedingJoinPoint pjp) throws IllegalAccessException, InstantiationException {

    //獲取@Compensable資訊
    Method method = CompensableMethodUtils.getCompensableMethod(pjp);
    if (method == null) {
        throw new RuntimeException(String.format("join point not found method, point is : %s", pjp.getSignature().getName()));
    }
    Compensable compensable = method.getAnnotation(Compensable.class);

    String confirmMethodName = compensable.confirmMethod();
    String cancelMethodName = compensable.cancelMethod();

    Transaction transaction = transactionManager.getCurrentTransaction();
    TransactionXid xid = new TransactionXid(transaction.getXid().getGlobalTransactionId());

    if (FactoryBuilder.factoryOf(compensable.transactionContextEditor()).getInstance().get(pjp.getTarget(), method, pjp.getArgs()) == null) {
        //設定事務上下文到Editor,Editor用來統一提取上下文,這邊對應設定dubbo的rpc上下文中去
        //這邊的上下文設定後就會呼叫try邏輯
        FactoryBuilder.factoryOf(compensable.transactionContextEditor()).getInstance().set(new TransactionContext(xid, TransactionStatus.TRYING.getId()), pjp.getTarget(), ((MethodSignature) pjp.getSignature()).getMethod(), pjp.getArgs());
    }

    Class targetClass = ReflectionUtils.getDeclaringType(pjp.getTarget().getClass(), method.getName(), method.getParameterTypes());

    //目前的用法,其實只要儲存呼叫引數就行,因為具體執行confirm和cancel都是根據transaction的status來判斷的
    //confirm的呼叫上下文
    InvocationContext confirmInvocation = new InvocationContext(targetClass,
            confirmMethodName,
            method.getParameterTypes(), pjp.getArgs());

    //cancel的呼叫上下文
    InvocationContext cancelInvocation = new InvocationContext(targetClass,
            cancelMethodName,
            method.getParameterTypes(), pjp.getArgs());

    Participant participant =
            new Participant(
                    xid,
                    confirmInvocation,
                    cancelInvocation,
                    compensable.transactionContextEditor());

    //把participant設定到transaction,並且同步到持久化儲存
    transactionManager.enlistParticipant(participant);

}
複製程式碼

通過從@Compensable註解配置的資訊以及當前Transaction來配置Participant。 在Participant設定到Transaction後,會執行pjp.proceed(pjp.getArgs()),也就執行了對應try邏輯的被切方法

ConfigurableCoordinatorAspect的邏輯會在ConfigurableTransactionAspect後執行,這和它們設定的order有關,小的order先執行,後切入

失敗補償機制

對於失敗的Confirm和Cancel操作,會有補償任務進行重試,具體實現類為RecoverScheduledJob,在這個類的init方法會啟動quartz任務

public void init() {

    try {
        MethodInvokingJobDetailFactoryBean jobDetail = new MethodInvokingJobDetailFactoryBean();
        //設定定時任務執行的物件和方法
        jobDetail.setTargetObject(transactionRecovery);
        jobDetail.setTargetMethod("startRecover");
        jobDetail.setName("transactionRecoveryJob");
        jobDetail.setConcurrent(false);
        jobDetail.afterPropertiesSet();

        CronTriggerFactoryBean cronTrigger = new CronTriggerFactoryBean();
        cronTrigger.setBeanName("transactionRecoveryCronTrigger");
        //設定cron表示式
        cronTrigger.setCronExpression(transactionConfigurator.getRecoverConfig().getCronExpression());
        cronTrigger.setJobDetail(jobDetail.getObject());
        cronTrigger.afterPropertiesSet();

        scheduler.scheduleJob(jobDetail.getObject(), cronTrigger.getObject());
        scheduler.start();

    } catch (Exception e) {
        throw new SystemException(e);
    }
}
複製程式碼

在這個方法裡會使用RecoverConfig的配置初始化定時任務,定時任務具體的執行邏輯使用MethodInvokingJobDetailFactoryBean的targetObject和targetMethod配置,對應為transactionRecovery的startRecover方法,我們來看下這個方法

public void startRecover() {
        //獲取所有沒被處理的transaction
        List<Transaction> transactions = loadErrorTransactions();
        //根據規則處理這些transaction
        recoverErrorTransactions(transactions);
    }
複製程式碼

分別看下上述兩個方法的邏輯

private List<Transaction> loadErrorTransactions() {


        long currentTimeInMillis = Calendar.getInstance().getTimeInMillis();

        TransactionRepository transactionRepository = transactionConfigurator.getTransactionRepository();
        RecoverConfig recoverConfig = transactionConfigurator.getRecoverConfig();

        //獲取在RecoverDuration間隔之前未完成的transaction
        return transactionRepository.findAllUnmodifiedSince(new Date(currentTimeInMillis - recoverConfig.getRecoverDuration() * 1000));
    }
複製程式碼
 private void recoverErrorTransactions(List<Transaction> transactions) {


    for (Transaction transaction : transactions) {

        //重試次數超過上限的Transaction不再執行補償
        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();

            //對超時的confiring操作重試
            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)//對超時的Canceling操作重試,或者Root超時的trying進行cancel操作
                    || 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);
            }
        }
    }
}
複製程式碼

注意一點,trying階段不會重試,失敗未處理,會觸發canceling操作

思考

分散式事務解決方案

下面列舉一些分散式事務解決方案的特性

  1. 傳統的二/三階段提交 這種解決方案會佔用資料庫事務資源,在網際網路公司很少使用
  2. 非同步確保型事務
    tcc-transation原始碼分析與思考
    基於可靠訊息的最終一致性,可以非同步,但資料絕對不能丟,而且一定要記賬成功 這個難道是依賴mq支援的事務特性?
  3. 最大努力通知型事務
    tcc-transation原始碼分析與思考
    按規律進行通知,不保證資料一定能通知成功,但會提供可查詢操作介面進行核對 目前專案比較常用的方式
  4. tcc
    tcc-transation原始碼分析與思考
    適用於實時性要求比較高,資料必須可靠的場景

我的理解

依照我目前的工作經驗,現在公司對分散式事務的解決方案一般是上述的第三種方法,但是一些對實時性要求比較高,資料必須可靠的場景我們還是可以考慮使用tcc的,但是也沒必要全盤tcc,可以和最大努力通知型事務一起使用

對於tcc還有一個疑問,在高併發情況下,在mq的模式下,由於是非同步,能夠保證訊息最終被消費掉,並且消費速率穩定,而tcc這種模式,會不會導致介面資源不夠用,介面資源都佔用滿,導致不斷的try失敗。

由此可見tcc的使用難度不止在業務使用方式上,對於一些極限的場景,需要有經驗的人來分析tcc該使用在多大範圍內。但是如果是併發量不大的專案,大家可以試著使用。

朋友們,使用或沒使用過tcc的,請留下你的想法。

資源連結

tcc-transaction git地址
微服務架構的分散式事務解決方案

相關文章