深度剖析一站式分散式事務方案Seata-Cient
1.背景
在之前的文章中已經介紹過Seata的總體介紹,如何使用以及Seata-Server的原理分析,有興趣的可以閱讀下面的文章:
深度剖析一站式分散式事務方案Seata-Server
解密分散式事務框架-Fescar
這篇文章會介紹Seata中另外兩個重要的角色TM
(事務管理器)和RM
(資源管理器),首先還是來看看下面這張圖:
上一個文章對於
TC
的原理已經做了詳細介紹,對於TM和RM我們看見在圖中都是屬於client
的角色,他們分別的功能如下:TM
(事務管理器):用來控制整個分散式事務的管理,發起全域性事務的Begin/Commit/Rollback
。RM(資源管理器)
:用來註冊自己的分支事務,接受TC
的Commit
或者Rollback
請求.
2.Seata-Spring
首先我們來介紹一些Seata-client
中Spring
模組,Seata
透過這個模組對自己的TM
和RM
進行初始化以及掃描AT模式和TCC模式的註解並初始化這些模式需要的資源。
在Seata
的專案中有一個spring
模組,裡面包含了我們和spring
相關的邏輯,GlobalTransactionScanner
是其中的核心類:
public class GlobalTransactionScanner extends AbstractAutoProxyCreator implements InitializingBean,ApplicationContextAware,
DisposableBean
上面程式碼是類的定義,首先它繼承了AbstractAutoProxyCreator
實現了wrapIfNecessary
方法實現我們的方法的切面代理,實現了InitializingBean
介面用於初始化我們的客戶端,實現了ApplicationContextAware
用於儲存我們的spring
容器,實現了DisposableBean
用於優雅關閉。
首先來看繼承AbstractAutoProxyCreator實現的wrapIfNecessary
protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
if (PROXYED_SET.contains(beanName)) {
return bean;
}
interceptor = null;
//check TCC proxy
if (TCCBeanParserUtils.isTccAutoProxy(bean, beanName, applicationContext)) {
//TCC interceptor, proxy bean of sofa:reference/dubbo:reference, and LocalTCC
interceptor = new TccActionInterceptor(TCCBeanParserUtils.getRemotingDesc(beanName));
} else {
Class<?> serviceInterface = SpringProxyUtils.findTargetClass(bean);
Class<?>[] interfacesIfJdk = SpringProxyUtils.findInterfaces(bean);
if (!existsAnnotation(new Class[]{serviceInterface})
&& !existsAnnotation(interfacesIfJdk)) {
return bean;
}
if (interceptor == null) {
interceptor = new GlobalTransactionalInterceptor(failureHandlerHook);
}
}
if (!AopUtils.isAopProxy(bean)) {
bean = super.wrapIfNecessary(bean, beanName, cacheKey);
} else {
AdvisedSupport advised = SpringProxyUtils.getAdvisedSupport(bean);
Advisor[] advisor = buildAdvisors(beanName, getAdvicesAndAdvisorsForBean(null, null, null));
for (Advisor avr : advisor) {
advised.addAdvisor(0, avr);
}
}
PROXYED_SET.add(beanName);
return bean;
}
Step1:檢查當前
beanName
是否已經處理過 如果處理過本次就不處理。Step2:根據註解,找到對應模式的
Inteceptor
,這裡有三種情況第一個TCC
,第二個是全域性事務管理TM的攔截器,第三個是沒有註解,如果沒有那麼直接返回即可。Step3:將對應的
interceptor
新增進入當前Bean
。
然後再看從InitializingBean
中實現的afterPropertiesSet
,也就是對Seata
的初始化:
public void afterPropertiesSet() {
initClient();
}
private void initClient() {
//init TM
TMClient.init(applicationId, txServiceGroup);
//init RM
RMClient.init(applicationId, txServiceGroup);
registerSpringShutdownHook();
}
private void registerSpringShutdownHook() {
if (applicationContext instanceof ConfigurableApplicationContext) {
((ConfigurableApplicationContext) applicationContext).registerShutdownHook();
ShutdownHook.removeRuntimeShutdownHook();
}
ShutdownHook.getInstance().addDisposable(TmRpcClient.getInstance(applicationId, txServiceGroup));
ShutdownHook.getInstance().addDisposable(RmRpcClient.getInstance(applicationId, txServiceGroup));
}
上面的程式碼邏輯比較清楚:
Step1:初始化
TM
客戶端,這裡會向Server
註冊該TM
。Step2:初始化
RM
客戶端,這裡會向Server註冊該RM
。Step3:註冊
ShutdownHook
,後續將TM
和RM
優雅關閉。
注意這裡初始化的時候會初始化兩個客戶端,分別是TM
客戶端和RM
客戶端,很多人認為TM
和RM
是用的同一個客戶端,這裡需要注意一下。
2.1 Interceptor
再上面的第一部分邏輯中我們看到我們有兩個業務核心Interceptor
,一個是GlobalTransactionalInterceptor
用來處理全域性事務的管理(開啟,提交,回滾),另外一個是TccActionInterceptor
用來處理TCC模式。熟悉Seata的朋友會問AT模式呢,為什麼只有TCC模式,這裡AT模式代表著就是自動處理事務,我們不需要有切面
2.1.1 GlobalTransactionalInterceptor
首先來看看GlobalTransactionalInterceptor#invoke:
public Object invoke(final MethodInvocation methodInvocation) throws Throwable {
Class<?> targetClass = (methodInvocation.getThis() != null ? AopUtils.getTargetClass(methodInvocation.getThis()) : null);
Method specificMethod = ClassUtils.getMostSpecificMethod(methodInvocation.getMethod(), targetClass);
final Method method = BridgeMethodResolver.findBridgedMethod(specificMethod);
final GlobalTransactional globalTransactionalAnnotation = getAnnotation(method, GlobalTransactional.class);
final GlobalLock globalLockAnnotation = getAnnotation(method, GlobalLock.class);
if (globalTransactionalAnnotation != null) {
return handleGlobalTransaction(methodInvocation, globalTransactionalAnnotation);
} else if (globalLockAnnotation != null) {
return handleGlobalLock(methodInvocation);
} else {
return methodInvocation.proceed();
}
}
Step1:從代理類中獲取到原始的
Method
Step2: 獲取
Method
中的註解Step3: 如果有
@GlobalTransactional
註解執行handleGlobalTransaction切面邏輯,這個也是我們全域性事務的邏輯。Step4: 如果有
@GlobalLock
註解,則執行handleGlobalLock切面邏輯,這個註解是用於一些非AT模式的資料庫加鎖,加上這個註解之後再執行Sql語句之前會查詢對應的資料是否加鎖,但是他不會加入全域性事務。
handleGlobalTransaction
邏輯如下:
private Object handleGlobalTransaction(final MethodInvocation methodInvocation,
final GlobalTransactional globalTrxAnno) throws Throwable {
return transactionalTemplate.execute(new TransactionalExecutor() {
@Override
public Object execute() throws Throwable {
return methodInvocation.proceed();
}
});
}
TransactionalTemplate#execute
public Object execute(TransactionalExecutor business) throws Throwable {
// 1. get or create a transaction
GlobalTransaction tx = GlobalTransactionContext.getCurrentOrCreate();
// 1.1 get transactionInfo
TransactionInfo txInfo = business.getTransactionInfo();
if (txInfo == null) {
throw new ShouldNeverHappenException("transactionInfo does not exist");
}
try {
// 2. begin transaction
beginTransaction(txInfo, tx);
Object rs = null;
try {
// Do Your Business
rs = business.execute();
} catch (Throwable ex) {
// 3.the needed business exception to rollback.
completeTransactionAfterThrowing(txInfo,tx,ex);
throw ex;
}
// 4. everything is fine, commit.
commitTransaction(tx);
return rs;
} finally {
//5. clear
triggerAfterCompletion();
cleanUp();
}
}
在handleGlobalTransaction
中將具體的實現交給了TransactionalTemplate#execute
去做了,其中具體的步驟如下:
Step1:獲取當前的全域性事務,如果沒有則建立。
Step2:獲取業務中的事務資訊包含超時時間等。
Step3:開啟全域性事務
Step4:如果有異常丟擲處理異常,rollback。
Step5:如果沒有異常那麼commit全域性事務。
Step6:清除當前事務上下文資訊。
2.1.2 `TccActionInterceptor`
我們先看看TccActionInterceptor是如何使用:
@TwoPhaseBusinessAction(name = "TccActionOne" , commitMethod = "commit", rollbackMethod = "rollback")
public boolean prepare(BusinessActionContext actionContext, int a);
public boolean commit(BusinessActionContext actionContext);
public boolean rollback(BusinessActionContext actionContext);
一般來說會定義三個方法一個是階段的try方法,另外一個是二階段的commit和rollback,每個方法的第一個引數是我們事務上下文,這裡我們不需要關心他在我們切面中會自行填充處理。
接下來我們再看看TCC相關的攔截器是如何處理的:
public Object invoke(final MethodInvocation invocation) throws Throwable {
Method method = getActionInterfaceMethod(invocation);
TwoPhaseBusinessAction businessAction = method.getAnnotation(TwoPhaseBusinessAction.class);
//try method
if(businessAction != null) {
if(StringUtils.isBlank(RootContext.getXID())){
//not in distribute transaction
return invocation.proceed();
}
Object[] methodArgs = invocation.getArguments();
//Handler the TCC Aspect
Map<String, Object> ret = actionInterceptorHandler.proceed(method, methodArgs, businessAction, new Callback<Object>(){
@Override
public Object execute() throws Throwable {
return invocation.proceed();
}
});
//return the final result
return ret.get(Constants.TCC_METHOD_RESULT);
}
return invocation.proceed();
}
Step1:獲取原始
Method
。Step2:判斷是否再全域性事務中,也就是整個邏輯服務最外層是否執行了
GlobalTransactionalInterceptor
。如果不再直接執行即可。Step3:執行
TCC
切面,核心邏輯在actionInterceptorHandler#proceed
中。
再來看看actionInterceptorHandler#proceed
這個方法:
public Map<String, Object> proceed(Method method, Object[] arguments, TwoPhaseBusinessAction businessAction, Callback<Object> targetCallback) throws Throwable {
Map<String, Object> ret = new HashMap<String, Object>(16);
//TCC name
String actionName = businessAction.name();
String xid = RootContext.getXID();
BusinessActionContext actionContext = new BusinessActionContext();
actionContext.setXid(xid);
//set action anme
actionContext.setActionName(actionName)
//Creating Branch Record
String branchId = doTccActionLogStore(method, arguments, businessAction, actionContext);
actionContext.setBranchId(branchId);
//set the parameter whose type is BusinessActionContext
Class<?>[] types = method.getParameterTypes();
int argIndex = 0;
for (Class<?> cls : types) {
if (cls.getName().equals(BusinessActionContext.class.getName())) {
arguments[argIndex] = actionContext;
break;
}
argIndex++;
}
//the final parameters of the try method
ret.put(Constants.TCC_METHOD_ARGUMENTS, arguments);
//the final result
ret.put(Constants.TCC_METHOD_RESULT, targetCallback.execute());
return ret;
}
Step1:獲取一些事務資訊,比如
TCC
名字,本次事務XID
等。Step2:建立
Branch
事務,一個是在本地的context
上下文中將它的commit
和rollback
資訊儲存起來,另一個是向我們的Seata-Server
註冊分支事務,用於後續的管理。Step3:填充方法引數,也就是我們的
BusinessActionContext
。
2.2 小結
Spring的幾個總要的內容已經剖析完畢,核心類主要是三個,一個Scanner
,兩個Interceptor
。整體來說比較簡單,Spring做的基本上也是我們客戶端一些初始化的事,接下來我們深入瞭解一下TM這個角色。
3. TM 事務管理器
在上面章節中我們講了GlobalTransactionalInterceptor
這個切面攔截器,我們知道了這個攔截器中做了我們TM應該做的事,事務的開啟,事務的提交,事務的回滾。這裡只是我們整體邏輯的發起點,其中具體的客戶端邏輯在我們的DefaultTransactionManager中,這個類中的程式碼如下所示:
public class DefaultTransactionManager implements TransactionManager {
@Override
public String begin(String applicationId, String transactionServiceGroup, String name, int timeout)
throws TransactionException {
GlobalBeginRequest request = new GlobalBeginRequest();
request.setTransactionName(name);
request.setTimeout(timeout);
GlobalBeginResponse response = (GlobalBeginResponse)syncCall(request);
return response.getXid();
}
@Override
public GlobalStatus commit(String xid) throws TransactionException {
GlobalCommitRequest globalCommit = new GlobalCommitRequest();
globalCommit.setXid(xid);
GlobalCommitResponse response = (GlobalCommitResponse)syncCall(globalCommit);
return response.getGlobalStatus();
}
@Override
public GlobalStatus rollback(String xid) throws TransactionException {
GlobalRollbackRequest globalRollback = new GlobalRollbackRequest();
globalRollback.setXid(xid);
GlobalRollbackResponse response = (GlobalRollbackResponse)syncCall(globalRollback);
return response.getGlobalStatus();
}
@Override
public GlobalStatus getStatus(String xid) throws TransactionException {
GlobalStatusRequest queryGlobalStatus = new GlobalStatusRequest();
queryGlobalStatus.setXid(xid);
GlobalStatusResponse response = (GlobalStatusResponse)syncCall(queryGlobalStatus);
return response.getGlobalStatus();
}
private AbstractTransactionResponse syncCall(AbstractTransactionRequest request) throws TransactionException {
try {
return (AbstractTransactionResponse)TmRpcClient.getInstance().sendMsgWithResponse(request);
} catch (TimeoutException toe) {
throw new TransactionException(TransactionExceptionCode.IO, toe);
}
}
}
在DefaultTransactionManager
中整體邏輯比較簡單有四個方法:
begin
:向Server
發起GlobalBeginRequest
請求,用於開啟全域性事務。commit
:向Server
發起GlobalCommitRequest
請求,用於提交全域性事務。rollback
:向Server
發起GlobalRollbackRequest
請求,用於回滾全域性事務。getStatus
:向Server
發起GlobalStatusRequest
請求,用於查詢全域性事務狀態資訊。
4. RM 資源管理器
在Seata
中目前管理RM
有兩種模式:一種是AT
模式,需要事務性資料庫支援,會自動記錄修改前快照和修改後的快照,用於提交和回滾;還有一種是TCC
模式,也可以看作是MT
模式,用於AT模式不支援的情況,手動進行提交和回滾。接下來將會深入剖析一下這兩種模式的實現原理。
4.1 AT 資源管理
AT
模式下需要使用Seata
提供的資料來源代理,其整體實現邏輯如下圖所示:
在我們的程式中執行一個sql
語句,無論你是使用mybatis
,還是直接使用jdbcTemplate
,都會遵循下面的步驟:
Step 1:從資料來源中獲取資料庫連線。
Step 2: 從連線中獲取
Statement
。Step 3: 透過Statement執行我們的
sql
語句
所以我們可以將DataSource
,Connection
,Statement
代理起來然後執行我們的一些特殊的邏輯,完成我們的AT模式。
4.1.1 DataSourceProxy
在DataSourceProxy中沒有太多的業務邏輯,只是簡單的將獲取Connection
用我們的ConnectionProxy
代理類進行了封裝,程式碼如下:
public ConnectionProxy getConnection() throws SQLException {
Connection targetConnection = targetDataSource.getConnection();
return new ConnectionProxy(this, targetConnection);
}
首先透過我們代理之前的DataSource
獲取連線,然後用ConnectionProxy
將其代理起來。
4.1.2 ConnectionProxy
ConnectionProxy
主要做三件事,第一個是生成代理的Statement
,第二個是儲存我們的連線上下文:加鎖的Key,undoLog等,第三個是代理執行我們的本地事務的commit
和rollback
。
首先來看看代理生成的Statement
:
@Override
public Statement createStatement() throws SQLException {
Statement targetStatement = getTargetConnection().createStatement();
return new StatementProxy(this, targetStatement);
}
@Override
public PreparedStatement prepareStatement(String sql) throws SQLException {
PreparedStatement targetPreparedStatement = getTargetConnection().prepareStatement(sql);
return new PreparedStatementProxy(this, targetPreparedStatement, sql);
}
這裡也是透過我們原來的連線直接生成Statement
,然後將其進行代理。
接下來看看對我們上下文的管理,大家都知道我們的一個事務其實對應的是一個資料庫連線,在這個事務中的所有sql
的undolog
和lockKey
都會在連線的上下文中記錄。如下面程式碼所示:
/**
* append sqlUndoLog
*
* @param sqlUndoLog the sql undo log
*/
public void appendUndoLog(SQLUndoLog sqlUndoLog) {
context.appendUndoItem(sqlUndoLog);
}
/**
* append lockKey
*
* @param lockKey the lock key
*/
public void appendLockKey(String lockKey) {
context.appendLockKey(lockKey);
}
這裡的程式碼很簡單,lockKey
和undolog
都是用list
儲存,直接add
即可。
當我們的本地事務完成的時候,需要呼叫Connection
的commit
或rollback
來進行事務的提交或回滾。這裡我們也需要代理這兩個方法來完成我們對分支事務的處理,先來看看commit
方法。
public void commit() throws SQLException {
if (context.inGlobalTransaction()) {
processGlobalTransactionCommit();
} else if (context.isGlobalLockRequire()) {
processLocalCommitWithGlobalLocks();
} else {
targetConnection.commit();
}
}
private void processGlobalTransactionCommit() throws SQLException {
try {
register();
} catch (TransactionException e) {
recognizeLockKeyConflictException(e);
}
try {
if (context.hasUndoLog()) {
UndoLogManager.flushUndoLogs(this);
}
targetConnection.commit();
} catch (Throwable ex) {
report(false);
if (ex instanceof SQLException) {
throw new SQLException(ex);
}
}
report(true);
context.reset();
}
Step 1:判斷
context
是否再全域性事務中,如果在則進行提交,到Step2。Step 2: 註冊分支事務並加上全域性鎖,如果全域性鎖加鎖失敗則丟擲異常。
Step 3: 如果
context
中有undolog
,那麼將Unlog
刷至資料庫。Step 4: 提交本地事務。
Step 5:報告本地事務狀態,如果出現異常則報告失敗,如果沒有問題則報告正常。
上面介紹了提交事務的流程,當context
在全域性鎖的流程中,會進行全域性鎖的查詢,這裡比較簡單就不做贅述,如果context
都沒有在上述的情況中那麼會直接進行事務提交。
對於我們rollback
來說程式碼比較簡單:
public void rollback() throws SQLException {
targetConnection.rollback();
if (context.inGlobalTransaction()) {
if (context.isBranchRegistered()) {
report(false);
}
}
context.reset();
}
Step 1:首先提交本地事務。
Step 2:判斷是否在全域性事務中。
Step 3:如果在則判斷分支事務是否已經註冊。
Step 4: 如果已經註冊那麼直接向客戶端報告該事務失敗異常。
細心的小夥伴可能發現如果我們的本地事務提交或者回滾之後失敗,那我們的分散式事務執行結果還能正確嗎?這裡完全不用擔心,再我們的服務端有完善的超時檢測,重試等機制,來幫助我們應對這些特殊情況。
4.1.3 StatementProxy
我們一般用statement
會呼叫executeXXX
方法來執行我們的sql
語句,所以在我們的Proxy
中可以利用這個方法,再執行sql
的時候做一些我們需要做的邏輯,下面看看execute
方法的程式碼:
public boolean execute(String sql) throws SQLException {
this.targetSQL = sql;
return ExecuteTemplate.execute(this, new StatementCallback<Boolean, T>() {
@Override
public Boolean execute(T statement, Object... args) throws SQLException {
return statement.execute((String) args[0]);
}
}, sql);
}
這裡直接將邏輯交給我們的ExecuteTemplate
去執行,有如下程式碼:
public static <T, S extends Statement> T execute(SQLRecognizer sqlRecognizer,
StatementProxy<S> statementProxy,
StatementCallback<T, S> statementCallback,
Object... args) throws SQLException {
if (!RootContext.inGlobalTransaction() && !RootContext.requireGlobalLock()) {
// Just work as original statement
return statementCallback.execute(statementProxy.getTargetStatement(), args);
}
if (sqlRecognizer == null) {
sqlRecognizer = SQLVisitorFactory.get(
statementProxy.getTargetSQL(),
statementProxy.getConnectionProxy().getDbType());
}
Executor<T> executor = null;
if (sqlRecognizer == null) {
executor = new PlainExecutor<T, S>(statementProxy, statementCallback);
} else {
switch (sqlRecognizer.getSQLType()) {
case INSERT:
executor = new InsertExecutor<T, S>(statementProxy, statementCallback, sqlRecognizer);
break;
case UPDATE:
executor = new UpdateExecutor<T, S>(statementProxy, statementCallback, sqlRecognizer);
break;
case DELETE:
executor = new DeleteExecutor<T, S>(statementProxy, statementCallback, sqlRecognizer);
break;
case SELECT_FOR_UPDATE:
executor = new SelectForUpdateExecutor<T, S>(statementProxy, statementCallback, sqlRecognizer);
break;
default:
executor = new PlainExecutor<T, S>(statementProxy, statementCallback);
break;
}
}
T rs = null;
try {
rs = executor.execute(args);
} catch (Throwable ex) {
if (!(ex instanceof SQLException)) {
// Turn other exception into SQLException
ex = new SQLException(ex);
}
throw (SQLException)ex;
}
return rs;
}
}
這裡是我們代理執行sql
的核心邏輯,步驟如下:
Step 1:如果不在全域性事務且不需要查詢全域性鎖,那麼就直接執行原始的
Statement
。Step 2: 如果沒有傳入
sql
識別器,那麼我們需要生成sql
識別器,這裡我們會借用Druid中對sql
的解析,我們獲取sql
的識別器,我們透過這個識別器可以獲取到不同型別的sql
語句的一些條件,比如說SQLUpdateRecognizer
是用於update
的sql
識別器,我們可以直接獲取到表名,條件語句,更新的欄位,更新欄位的值等。Step 3:根據
sql
識別器的型別,來生成我們不同型別的執行器。Step 4:透過第三步中的執行器來執行我們的sql語句。
這裡有五種Executor
:INSERT,UPDATE,DELETE
的執行器會進行undolog記錄並且記錄全域性鎖,SELECT_FOR_UPDATE
只會進行查詢全域性鎖,有一個預設的代表我們現在還不支援,什麼都不會做直接執行我們的sql
語句。
對於INSERT,UPDATE,DELETE的執行器會繼承我們的AbstractDMLBaseExecutor
:
protected T executeAutoCommitFalse(Object[] args) throws Throwable {
TableRecords beforeImage = beforeImage();
T result = statementCallback.execute(statementProxy.getTargetStatement(), args);
TableRecords afterImage = afterImage(beforeImage);
prepareUndoLog(beforeImage, afterImage);
return result;
}
protected abstract TableRecords beforeImage() throws SQLException;
protected abstract TableRecords afterImage(TableRecords beforeImage) throws SQLException;
在AbstractDMLBaseExecutor
中執行邏輯在executeAutoCommitFalse
這個方法,步驟如下:
Step 1:獲取執行當前
sql
之前所受影響行的快照,這裡beforeImage
會被不同型別的sql語句重新實現。Step 2:執行當前
sql
語句,並獲取結果。Step 3:獲取執行
sql
之後的快照,這裡的afterIamge
也會被不同型別的sql語句重新實現。Step 4:將
undolog
準備好,這裡會儲存到我們的ConnectionContext
中。
protected void prepareUndoLog(TableRecords beforeImage, TableRecords afterImage) throws SQLException {
if (beforeImage.getRows().size() == 0 && afterImage.getRows().size() == 0) {
return;
}
ConnectionProxy connectionProxy = statementProxy.getConnectionProxy();
TableRecords lockKeyRecords = sqlRecognizer.getSQLType() == SQLType.DELETE ? beforeImage : afterImage;
String lockKeys = buildLockKey(lockKeyRecords);
connectionProxy.appendLockKey(lockKeys);
SQLUndoLog sqlUndoLog = buildUndoItem(beforeImage, afterImage);
connectionProxy.appendUndoLog(sqlUndoLog);
}
準備UndoLog
的時候會獲取我們的ConnectionProxy
,將我們的Undolog
和LockKey
儲存起來,給後面的本地事務commit
和rollback
使用,上面已經講過。
4.1.4 分支事務的提交和回滾
上面的4.1.1-4.1.3都是說的是我們分散式事務的第一階段,也就是將我們的分支事務註冊到Server
,而第二階段分支提交和分支回滾都在我們的DataSourceManager
中,對於分支事務提交有如下程式碼:
public BranchStatus branchCommit(BranchType branchType, String xid, long branchId, String resourceId, String applicationData) throws TransactionException {
return asyncWorker.branchCommit(branchType, xid, branchId, resourceId, applicationData);
}
public BranchStatus branchCommit(BranchType branchType, String xid, long branchId, String resourceId, String applicationData) throws TransactionException {
if (!ASYNC_COMMIT_BUFFER.offer(new Phase2Context(branchType, xid, branchId, resourceId, applicationData))) {
LOGGER.warn("Async commit buffer is FULL. Rejected branch [" + branchId + "/" + xid + "] will be handled by housekeeping later.");
}
return BranchStatus.PhaseTwo_Committed;
}
這裡將我們的分支事務提交的資訊,放到一個佇列中,非同步去處理,也就是非同步刪除我們的undolog
資料,因為提交之後undolog
資料沒用了。
這裡有人可能會問如果當你將這個資訊非同步提交到佇列中的時候,機器當機,那麼就不會執行非同步刪除undolog
的邏輯,那麼這條undolog
是不是就會成為永久的髒資料呢?這裡Seata
為了防止這種事出現,會定時掃描某些較老的undolog資料然後進行刪除,不會汙染我們的資料。
對於我們的分支事務回滾有如下程式碼:
public BranchStatus branchRollback(BranchType branchType, String xid, long branchId, String resourceId, String applicationData) throws TransactionException {
DataSourceProxy dataSourceProxy = get(resourceId);
if (dataSourceProxy == null) {
throw new ShouldNeverHappenException();
}
try {
UndoLogManager.undo(dataSourceProxy, xid, branchId);
} catch (TransactionException te) {
if (te.getCode() == TransactionExceptionCode.BranchRollbackFailed_Unretriable) {
return BranchStatus.PhaseTwo_RollbackFailed_Unretryable;
} else {
return BranchStatus.PhaseTwo_RollbackFailed_Retryable;
}
}
return BranchStatus.PhaseTwo_Rollbacked;
}
這裡會先獲取到我們的資料來源,接下來呼叫我們的重做日誌管理器的undo
方法進行日誌重做,undo
方法較長這裡就不貼上來了,其核心邏輯是查詢到我們的undolog
然後將裡面的快照在我們資料庫進行重做。
4.2 TCC 資源管理
TCC
沒有AT
模式資源管理這麼複雜,部分核心邏輯在之前的Interceptor
中已經講解過了,比如二階段方法的儲存等。這裡主要看看TCC
的分支事務提交和分支事務回滾,在TCCResourceManager
中有:
public BranchStatus branchCommit(BranchType branchType, String xid, long branchId, String resourceId,
String applicationData) throws TransactionException {
TCCResource tccResource = (TCCResource) tccResourceCache.get(resourceId);
if (tccResource == null) {
throw new ShouldNeverHappenException("TCC resource is not exist, resourceId:" + resourceId);
}
Object targetTCCBean = tccResource.getTargetBean();
Method commitMethod = tccResource.getCommitMethod();
if (targetTCCBean == null || commitMethod == null) {
throw new ShouldNeverHappenException("TCC resource is not available, resourceId:" + resourceId);
}
boolean result = false;
//BusinessActionContext
BusinessActionContext businessActionContext =
getBusinessActionContext(xid, branchId, resourceId, applicationData);
Object ret = commitMethod.invoke(targetTCCBean, businessActionContext);
LOGGER.info("TCC resource commit result :" + ret + ", xid:" + xid + ", branchId:" + branchId + ", resourceId:" +
resourceId);
if (ret != null && ret instanceof TwoPhaseResult) {
result = ((TwoPhaseResult) ret).isSuccess();
} else {
result = (boolean) ret;
}
return result ? BranchStatus.PhaseTwo_Committed : BranchStatus.PhaseTwo_CommitFailed_Retryable;
}
步驟如下:
Step 1:首先查詢當前服務是否有該
TCC
資源,如果沒有丟擲異常。Step 2:然後找到我們的TCC物件和對應的
commit
方法。Step 3:然後執行我們的
commit
方法。Step 4:最後將結果返回給我們的
Server
,由Server
決定是否重試。
這裡的branchRollback
方法也比較簡單,這裡就不做過多分析了。
總結
透過上面分析我們知道,Seata
的初始化是依賴Spring
去進行,我們的全域性事務的開啟/提交/回滾都是依賴我們的TM事務管理器,而我們的分支事務的管理是依靠我們的RM
,其中提供了兩個模式AT
和TCC
,AT
模式必須使用資料庫,其核心實現是實現資料來源的代理,將我們自己的邏輯注入進去。而我們的TCC能彌補我們沒有使用資料庫的情況,將提交和回滾都交由我們自己實現,其核心實現邏輯是依賴將一個資源的二階段的方法和我們的目標物件在我們的資源上下文中儲存下來,方便我們後續使用。
最後如果大家對分散式事務感興趣,歡迎大家使用並閱讀Seata
的程式碼,並給我們提出建議。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31555607/viewspace-2644804/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 深度剖析一站式分散式事務方案Seata(Fescar)-Server分散式Server
- 深度剖析一站式分散式事務方案 Seata(Fescar)-Server分散式Server
- 深度剖析Saga分散式事務分散式
- 深度剖析分散式事務效能分散式
- 深度剖析分散式事務之 AT 與 XA 對比分散式
- 分散式事務解決方案分散式
- 分散式事務處理方案,微服事務處理方案分散式
- SpringCloud 分散式事務解決方案SpringGCCloud分散式
- 深入剖析分散式事務一致性分散式
- 分散式事務(2)---強一致性分散式事務解決方案分散式
- 分散式事務,強一致性方案有哪些?|分散式事務系列(二)分散式
- TX-LCN分散式事務使用方案分散式
- 常用的分散式事務解決方案分散式
- 分散式事務解決方案彙總分散式
- 分散式事務解決方案--GTS(二)分散式
- 分散式事務解決方案--GTS(一)分散式
- MSSQL server分散式事務解決方案SQLServer分散式
- 分散式事務(一)—分散式事務的概念分散式
- 分散式事務解決方案(五)【TCC型方案】分散式
- 分散式事務解決方案(一)【介紹】分散式
- 面試必備的分散式事務方案面試分散式
- 分散式事務的概念和解決方案Seate分散式
- 分散式事務(3)---RocketMQ實現分散式事務原理分散式MQ
- 微服務架構及分散式事務解決方案微服務架構分散式
- 分散式事務和分散式hash分散式
- 分散式事務(4)---RocketMQ實現分散式事務專案分散式MQ
- 分散式事務解決方案(四)【最大努力通知】分散式
- 基於RocketMq的分散式事務解決方案MQ分散式
- 分散式事務解決方案——柔性事務與服務模式分散式模式
- 微服務分散式事務4種解決方案實戰微服務分散式
- 理解分散式事務分散式
- 分散式事務概述分散式
- 聊聊分散式事務分散式
- seata 分散式事務分散式
- 分散式系統(三)——分散式事務分散式
- 分散式事務~從seata例項來學習分散式事務分散式
- 基於可靠訊息方案的分散式事務(二):Java中的事務分散式Java
- 分散式事務方案 - 最終一致性分散式