tcc介紹
tcc是分散式事務的一種解決方案,即Try,Commit,Cancel
Try: 嘗試執行業務
完成所有業務檢查(一致性)
預留必須業務資源(準隔離性)
複製程式碼
Confirm: 確認執行業務
真正執行業務
不作任何業務檢查
只使用Try階段預留的業務資源
Confirm操作滿足冪等性
複製程式碼
Cancel: 取消執行業務
釋放Try階段預留的業務資源
Cancel操作滿足冪等性
複製程式碼
本文我會講解一個實現tcc思想的框架,tcc-transaction
在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
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包含的方法以及屬性如下
變數介紹
- 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
方法介紹
- 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,這個方法與用於主事務的建立
- propagationNewBegin
public Transaction propagationNewBegin(TransactionContext transactionContext) {
Transaction transaction = new Transaction(transactionContext);
transactionRepository.create(transaction);
registerTransaction(transaction);
return transaction;
}
複製程式碼
這個方法用於從主事務的上下文建立分支事務,xid保持不變,事務型別變化
- 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
- 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,而是不刪除資料庫事務記錄,之後會有定時任務進行掃描重試,後面會講到這個定時任務
- 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);
}
}
複製程式碼
- 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");
}
}
}
複製程式碼
事務結束,從棧中彈出結束的事務。
- 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註解的方法分三種
- Root方法,就是這次事務的入口方法
- Normal方法,在Root方法呼叫的dubbo介面方法
- 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操作
思考
分散式事務解決方案
下面列舉一些分散式事務解決方案的特性
- 傳統的二/三階段提交 這種解決方案會佔用資料庫事務資源,在網際網路公司很少使用
- 非同步確保型事務 基於可靠訊息的最終一致性,可以非同步,但資料絕對不能丟,而且一定要記賬成功 這個難道是依賴mq支援的事務特性?
- 最大努力通知型事務 按規律進行通知,不保證資料一定能通知成功,但會提供可查詢操作介面進行核對 目前專案比較常用的方式
- tcc 適用於實時性要求比較高,資料必須可靠的場景
我的理解
依照我目前的工作經驗,現在公司對分散式事務的解決方案一般是上述的第三種方法,但是一些對實時性要求比較高,資料必須可靠的場景我們還是可以考慮使用tcc的,但是也沒必要全盤tcc,可以和最大努力通知型事務一起使用
對於tcc還有一個疑問,在高併發情況下,在mq的模式下,由於是非同步,能夠保證訊息最終被消費掉,並且消費速率穩定,而tcc這種模式,會不會導致介面資源不夠用,介面資源都佔用滿,導致不斷的try失敗。
由此可見tcc的使用難度不止在業務使用方式上,對於一些極限的場景,需要有經驗的人來分析tcc該使用在多大範圍內。但是如果是併發量不大的專案,大家可以試著使用。
朋友們,使用或沒使用過tcc的,請留下你的想法。