本文首發於個人微信公衆號《andyqian》, 期待你的關注!
前言
這篇文章主要講述的是分佈式事務 seata 框架的 rm - datasource 模塊。文章會按照以下幾點進行講解:
- 簡介
- 結構
- 源碼解析
- 涉及的設計模式
- 後續的擴展
簡介
我們知道seata框架本身是事務的協調者,協調多個本地事務“符合事務的特性”,從而構成一個”分佈式的全局事務”。這也是seata框架的設計思想。看到這裏,我們應該知道,要想將多個本地事務能夠組成一個全局”事務”,就應該對本地事務進行改造,讓其在同一個“全局事務”內。rm - datasource 模塊就是一個這樣的角色,其承擔的職責包括但不限於以下幾點:
- SQL 解析與執行。
- 全局”事務”的註冊,提交與回滾。
- undo 日誌的生成與使用。
結構
rm-datasource 類圖如下所示:
別看有這麼多類以及package。別慌,這裏有很多我們的老朋友,我們一起來認識一下:
代理類:
StatementProxy,
PreparedStatementProxy,
DataSoureProxy,
ConnectionPrxoy。
這些類分別是我們熟悉的:Statement, PreparedStatement, DataSoure, Connection 的代理類。(下面會以 ConnectionPrxoy 爲例,查看其源碼。)
exec 模塊
該模塊承擔的職責是:對SQL按照不同的SQL類型進行執行,並保存beforeImage以及afterImage鏡像文件到 undo 數據結構中。(用於事務回滾)。
其主要類圖如下所示:
- 頂層父類是 Executor 接口,其定義了 executor 的方法。代碼也非常簡單,如下所示:
public interface Executor<T> {
/**
* Execute t.
*
* @param args the args
* @return the t
* @throws Throwable the throwable
*/
T execute(Object... args) throws Throwable;
}
- BaseTransactionalExecutor 爲抽象類,實現了Executor 接口中的 execute 方法。並定義了doExecute() 抽象方法。
execute 方法實現如下:
@Override
public Object execute(Object... args) throws Throwable {
//1. 如果爲Global 事務,則將XId 綁定在上下文中
if (RootContext.inGlobalTransaction()) {
String xid = RootContext.getXID();
statementProxy.getConnectionProxy().bind(xid);
}
//2. 是否需要獲取全局鎖,進行不同設置
if (RootContext.requireGlobalLock()) {
statementProxy.getConnectionProxy().setGlobalLockRequire(true);
} else {
statementProxy.getConnectionProxy().setGlobalLockRequire(false);
}
return doExecute(args);
}
/**
* 定義抽象方法,由子類自行實現。
* Do execute object.
* @param args the args
* @return the object
* @throws Throwable the throwable
*/
protected abstract Object doExecute(Object... args) throws Throwable;
在 BaseTransactionalExecutor 抽象類中。還封裝了部分公共方法,例如:
getTableMeta() (獲取表的元信息),buildLockKey (構造全局鎖的key), prepareUndoLog (執行UndoLog信息) 等等。
- PlainExecutor 表示: 未使用全局事務時的執行器。(即直接走原生的)
- AbstractDMLBaseExecutor 類繼承自 BaseTransactionalExecutor 抽象。實現了父類的 doExecute() 方法,並定義了多個抽象方法。代碼如下所示:
@Override
public T doExecute(Object... args) throws Throwable {
AbstractConnectionProxy connectionProxy = statementProxy.getConnectionProxy();
// 是否爲自動提交,
if (connectionProxy.getAutoCommit()) {
return executeAutoCommitTrue(args);
} else {
return executeAutoCommitFalse(args);
}
}
// 業務SQL 執行前的鏡像 (用於事務回滾)
protected abstract TableRecords beforeImage() throws SQLException;
// 業務SQL 執行後的鏡像。(用於事務回滾)
protected abstract TableRecords afterImage(TableRecords beforeImage) throws SQLException;
sql 模塊
該模塊承擔的職責是:識別SQL,按照不同的SQL類型定義了一系列的識別器。另外該模塊的底層引用到了alibaba.druid 開源庫。類圖如下所示:
undo 模塊
該模塊承擔的職責是:undo 對象的管理,其中包括:undo對象的編解碼,undo對象的基本操作。另外在該模塊中還包含:SQL 關鍵字檢查,(個人覺得這部分放在sql模塊裏面更合理些)。
類圖如下所示:
源碼解析
以 ConnectionPrxoy 類爲例,commit 方法代碼如下所示:
@Override
public void commit() throws SQLException {
// 如果是Global事務,則執行 processGlobalTransactionCommit方法
if (context.inGlobalTransaction()) {
processGlobalTransactionCommit();
// 是否需要獲取全局鎖
} else if (context.isGlobalLockRequire()) {
processLocalCommitWithGlobalLocks();
} else {
//本地事務直接 commit
targetConnection.commit();
}
}
- processGlobalTransactionCommit 方法代碼如下所示:
private void processGlobalTransactionCommit() throws SQLException {
try {
//1. 向 server 端進行分支事務註冊
register();
} catch (TransactionException e) {
recognizeLockKeyConflictException(e);
}
//2. 如果有 UndoLog 日誌,則將其新增到 undo_log 表中。
try {
if (context.hasUndoLog()) {
UndoLogManager.flushUndoLogs(this);
}
//3. 執行本地事務
targetConnection.commit();
} catch (Throwable ex) {
//4. 如果異常,則向 server 端 報告未提交完成。
report(false);
if (ex instanceof SQLException) {
throw new SQLException(ex);
}
}
//5. 向 server 端,報告提交完成。並將ccontext.reset()掉。
report(true);
context.reset();
}
- processLocalCommitWithGlobalLocks 方法 代碼如下所示:
private void processLocalCommitWithGlobalLocks() throws SQLException {
//1. 檢查全局鎖是否存在,如果存在,則拋出異常。
checkLock(context.buildLockKeys());
try {
//
targetConnection.commit();
} catch (Throwable ex) {
throw new SQLException(ex);
}
context.reset();
}
rollback 方法,代碼如下所示:
@Overrid
public void rollback() throws SQLException {
//1. 本地事務進行回滾
targetConnection.rollback();
//2. 如果是全局事務,且進行過分支註冊,則向server報告未提交完成。
if (context.inGlobalTransaction()) {
if (context.isBranchRegistered()) {
report(false);
}
}
context.reset();
}
report 方法代碼如下所示:
private void report(boolean commitDone) throws SQLException {
//1. 獲取重試次數,默認爲5次。可通過 report.retry.count 進行設置。
int retry = REPORT_RETRY_COUNT;
while (retry > 0) {
try {
//2. 分子報告
DefaultResourceManager.get().branchReport(BranchType.AT, context.getXid(), context.getBranchId(),
(commitDone ? BranchStatus.PhaseOne_Done : BranchStatus.PhaseOne_Failed), null);
return;
} catch (Throwable ex) {
//3. 異常時,則進行重試
LOGGER.error("Failed to report [" + context.getBranchId() + "/" + context.getXid() + "] commit done ["
+ commitDone + "] Retry Countdown: " + retry);
retry--;
// 4. 達到重試次數,以及失敗,則拋出異常。
if (retry == 0) {
throw new SQLException("Failed to report branch status " + commitDone, ex);
}
}
}
}
由於篇幅原因,其他的代理類就不詳細講解。有興趣的可以自己看看源碼。
涉及的設計模式
在該模塊中,涉及的設計模式有:
1. 代理模式
典型類有:
DataSourceProxy(數據源代理),
ConnectionProxy (連接代理),
StatementProxy (statement代理)。
2. 工廠模式
典型類有:
SQLVisitorFactory,
KeywordCheckerFactory
UndoExecutorFactory ,
UndoLogParserFactory。
3. 單例模式
典型類有:MySQLKeywordChecker。
部分代碼如下:
public static KeywordChecker getInstance() {
if (keywordChecker == null) {
synchronized (MySQLKeywordChecker.class) {
if (keywordChecker == null) {
keywordChecker = new MySQLKeywordChecker();
keywordSet = Arrays.stream(MySQLKeyword.values()).map(MySQLKeyword::name).collect(Collectors.toSet());
}
}
}
return keywordChecker;
}
4. 模板模式
典型類有:
GlobalLockTemplate,
ExecuteTemplate 。
GlobalLockTemplate 類代碼如下所示:
/**
* Execute object.
*
* @param business the business
* @return the object
* @throws Exception
*/
public Object execute(Callable<T> business) throws Exception {
Object rs = null;
try {
// add global lock declare
RootContext.bindGlobalLockFlag();
// Do Your Business
rs = business.call();
} finally {
//clean the global lock declare
RootContext.unbindGlobalLockFlag();
}
return rs;
}
後續的擴展
通過上面的介紹,我們已經非常清楚這個模塊的職責。根據社區以及框架本身的發展上來說,在後續的迭代過程中包括但不限於以下功能:
- 不同數據庫的支持。
目前僅支持MySQL,Oracle 正在支持中。 - 數據庫關鍵字檢查的完善。
- datasource 自動代理。
…
由於篇幅原因,還有很多點沒有涉及到。只能放在後面的文章中進行講解了。如果你也對分佈式事務感興趣,可以留言一起探討。
相關閱讀: