本文梳理了Fescar生成
undoLog
的流程和源碼,項(xiàng)目不停迭代簿盅,本文源碼僅供參考防楷。
- Fescar源碼閱讀-解決分布式事務(wù)的利器
- Fescar源碼閱讀-RPC和消息
- Fescar源碼閱讀-全自動(dòng)的分布式事務(wù)AT
- Fescar源碼閱讀-神奇的UndoLog(一)
Fescar處理分布式事務(wù)采用是二段提交的方案趴泌,但是Fescar對(duì)二段提交的流程,尤其是鎖的流程進(jìn)行了改進(jìn)怠噪,大幅度提升了性能。
參考官網(wǎng)圖示:
Fescar盡可能的縮短了鎖的持有時(shí)間杜跷,本地事務(wù)在phase1結(jié)束時(shí)就可以提交傍念,并釋放本地鎖;
如果TC決議全局commit葛闷,在phase2開(kāi)始階段憋槐,全局鎖就被釋放。(當(dāng)然這又導(dǎo)致全局事務(wù)的隔離級(jí)別實(shí)際上是READ_UNCOMMITED,這方面的問(wèn)題我們后面再談淑趾,按FESCAR的理念:微服務(wù)場(chǎng)景產(chǎn)生的分布式事務(wù)阳仔,絕大部分應(yīng)用在 讀已提交 的隔離級(jí)別下工作是沒(méi)有問(wèn)題的。而實(shí)際上扣泊,這當(dāng)中又有絕大多數(shù)的應(yīng)用場(chǎng)景近范,實(shí)際上工作在 讀未提交 的隔離級(jí)別下同樣沒(méi)有問(wèn)題嘶摊。
)
這一做法的好處是極大的降低了對(duì)數(shù)據(jù)庫(kù)資源的占用時(shí)間,提升了系統(tǒng)的吞吐量评矩。
但是也引入了一個(gè)問(wèn)題:Phase1 即提交的情況下叶堆,Phase2 如何回滾?
從上文梳理的事務(wù)流程中可以看到斥杜,F(xiàn)escar通過(guò)DataSourceRM為本地事務(wù)自動(dòng)生成undolog的方式蹂空,解決了這一問(wèn)題。
聽(tīng)起來(lái)真的很神奇果录!開(kāi)發(fā)人員什么都不用管上枕,方向大膽的commit,除了問(wèn)題Fescar自動(dòng)執(zhí)行數(shù)據(jù)的反向操作弱恒,將數(shù)據(jù)回滾到事務(wù)提交前的狀態(tài)辨萍,這也太爽了!
刨根究底返弹,正本清源锈玉,作為碼農(nóng)的我們剛剛老本行,從它的源碼上看看义起,這一神奇的事情是如何做到的拉背。
SQL的執(zhí)行
JDBC中SQL的執(zhí)行是通過(guò)Statment/PreparedStatement來(lái)實(shí)現(xiàn)的。Fescar也一樣默终,通過(guò)提供了對(duì)應(yīng)的Proxy代理了具體的Statement實(shí)現(xiàn)椅棺。在Statement執(zhí)行的時(shí),搞一點(diǎn)小動(dòng)作~
下圖為PreparedStatementProxy的繼承關(guān)系齐蔽,可以看到Statment和PreparedStatement都有對(duì)應(yīng)的Proxy實(shí)現(xiàn)两疚,
代碼很簡(jiǎn)單,
PreparedStatementProxy
所有操作都委托給了ExecuteTemplate去執(zhí)行含滴。
@Override
public boolean execute() throws SQLException {
return ExecuteTemplate.execute(this, new StatementCallback<Boolean, PreparedStatement>() {
@Override
public Boolean execute(PreparedStatement statement, Object... args) throws SQLException {
return statement.execute();
}
});
}
@Override
public ResultSet executeQuery() throws SQLException {
return ExecuteTemplate.execute(this, new StatementCallback<ResultSet, PreparedStatement>() {
@Override
public ResultSet execute(PreparedStatement statement, Object... args) throws SQLException {
return statement.executeQuery();
}
});
}
ExecuteTemplate
再看看ExecuteTemplate诱渤,也很簡(jiǎn)單滴,識(shí)別當(dāng)前statement需要執(zhí)行的SQL是什么類(lèi)型(insert\update\delete等等)谈况,然后繼續(xù)委托給具體的Executor去執(zhí)行勺美。
//ExecuteTemplate#execute(SQLRecognizer, StatementProxy<S>, StatementCallback<T,S>, java.lang.Object...)
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(statementProxy, statementCallback, sqlRecognizer);
break;
default:
executor = new PlainExecutor<T, S>(statementProxy, statementCallback, sqlRecognizer);
break;
}
XXXXExecutor
看看InsertExecutor的繼承關(guān)系,其他的Executor都類(lèi)似(除了PlainExecutor不談)碑韵。
關(guān)系也非常清晰:事務(wù)處理->DML處理->Insert處理
- BaseTransactionalExecutor
主要工作:在當(dāng)前事務(wù)的Context中綁定了全局事務(wù)ID:XID赡茸。
// BaseTransactionalExecutor
@Override
public Object execute(Object... args) throws Throwable {
String xid = RootContext.getXID();
statementProxy.getConnectionProxy().bind(xid);
return doExecute(args);
}
protected abstract Object doExecute(Object... args) throws Throwable;
- AbstractDMLBaseExecutor
在statement執(zhí)行前后分別生成beforeImage、afterImage泼诱,并調(diào)用prepareUndoLog()
生成undoLog
坛掠。
beforeImage、afterImage為abstract方法,由具體的executor去實(shí)現(xiàn)屉栓。
// AbstractDMLBaseExecutor
@Override
public T doExecute(Object... args) throws Throwable {
AbstractConnectionProxy connectionProxy = statementProxy.getConnectionProxy();
if (connectionProxy.getAutoCommit()) {
// executeAutoCommitTrue和executeAutoCommitFalse類(lèi)似舷蒲,最終還是會(huì)調(diào)用executeAutoCommitFalse。
return executeAutoCommitTrue(args);
} else {
return executeAutoCommitFalse(args);
}
}
protected T executeAutoCommitFalse(Object[] args) throws Throwable {
TableRecords beforeImage = beforeImage();
T result = statementCallback.execute(statementProxy.getTargetStatement(), args);
TableRecords afterImage = afterImage(beforeImage);
// 很語(yǔ)義化的接口友多,根據(jù)sql執(zhí)行前和執(zhí)行后的數(shù)據(jù)快照牲平,定然能夠生成對(duì)應(yīng)的undolog!S蚶摹纵柿!
super.prepareUndoLog(beforeImage, afterImage);
return result;
}
protected abstract TableRecords beforeImage() throws SQLException;
protected abstract TableRecords afterImage(TableRecords beforeImage) throws SQLException;
很明顯,為了生成undoLog
启绰,InsertExecutor
昂儒、UpdateExecutor
等之類(lèi)的任務(wù)就是實(shí)現(xiàn)自己的beforeImage()
和afterImage()
,生成statement執(zhí)行前后的數(shù)據(jù)快照委可。
最終依據(jù)數(shù)據(jù)Image生成undoLog
渊跋。
undoLog
看一下如何生成undoLog
// BaseTransactionalExecutor
public void prepareUndoLog(SQLType sqlType, String tableName, TableRecords beforeImage, TableRecords afterImage) throws SQLException {
ConnectionProxy connectionProxy = statementProxy.getConnectionProxy();
TableRecords lockKeyRecords = sqlRecognizer.getSQLType() == SQLType.DELETE ? beforeImage : afterImage;
// lock key在這里生成了,由表名和主鍵組成
String lockKeys = buildLockKey(lockKeyRecords);
connectionProxy.appendLockKey(lockKeys);
SQLUndoLog sqlUndoLog = buildUndoItem(beforeImage, afterImage);
//生成的undolog最終都要交給connectionProxy,畢竟人家負(fù)責(zé)回滾呢
connectionProxy.appendUndoLog(sqlUndoLog);
}
//構(gòu)建SQLUndoLog
private SQLUndoLog buildUndoItem(SQLType sqlType, String tableName, TableRecords beforeImage, TableRecords afterImage) {
SQLUndoLog sqlUndoLog = new SQLUndoLog();
sqlUndoLog.setSqlType(sqlType);
sqlUndoLog.setTableName(tableName);
sqlUndoLog.setBeforeImage(beforeImage);
sqlUndoLog.setAfterImage(afterImage);
return sqlUndoLog;
}
// SQLUndoLog對(duì)象
public class SQLUndoLog {
// select,insert,update,delete......
private SQLType sqlType;
private String tableName;
private TableRecords beforeImage;
private TableRecords afterImage;
}
至此着倾,隨著本地SQL執(zhí)行完成拾酝,SQL對(duì)應(yīng)的undoLog也同時(shí)生成完成。
我們還注意到卡者,生成UndoLog的同時(shí)還生成了lock keys蒿囤,還記得上文的流程中,lock keys會(huì)被發(fā)送給TC的吧崇决。
對(duì)于undoLog
材诽,將跟隨本地事務(wù)一起commit,插入undo_log
表中嗽桩,同時(shí)記錄的還包括全局事務(wù)的XID和branchID
@Override
public void commit() throws SQLException {
if (context.inGlobalTransaction()) {
register();
if (context.hasUndoLog()) {
UndoLogManager.flushUndoLogs(this);// 刷入undo_log表中
}
targetConnection.commit();
report(true);
context.reset();
} else {
targetConnection.commit();
}
}
undoLog
生成完成!
AfterImage
以InsertExecutor
為例看一下AfterImage如何生成
@Override
protected TableRecords afterImage(TableRecords beforeImage) throws SQLException {
//解析SQL
SQLInsertRecognizer recogizier = (SQLInsertRecognizer)sqlRecognizer;
List<String> insertColumns = recogizier.getInsertColumns();
TableMeta tmeta = getTableMeta();
TableRecords afterImage = null;
// insert是否已經(jīng)包含pk
if (tmeta.containsPK(insertColumns)) {
// insert values including PK
List<Object> pkValues = ...//
//從SQL中將pk抽取岳守,然后使用ID查詢(xún)insert的數(shù)據(jù),這就是afterImage了
afterImage = getTableRecords(pkValues);
} else {
// PK 是自增長(zhǎng)的
Map<String, ColumnMeta> pkMetaMap = getTableMeta().getPrimaryKeyMap();
if (pkMetaMap.size() != 1) {
throw new NotSupportYetException();
}
ColumnMeta pkMeta = pkMetaMap.values().iterator().next();
if (!pkMeta.isAutoincrement()) {
throw new ShouldNeverHappenException();
}
//無(wú)需解釋了碌冶。。涝缝。
ResultSet genKeys = null;
try {
genKeys = statementProxy.getTargetStatement().getGeneratedKeys();
} catch (SQLException e) {
// java.sql.SQLException: Generated keys not requested. You need to
// specify Statement.RETURN_GENERATED_KEYS to
// Statement.executeUpdate() or Connection.prepareStatement().
if ("S1009".equalsIgnoreCase(e.getSQLState())) {
genKeys = statementProxy.getTargetStatement().executeQuery("SELECT LAST_INSERT_ID()");
} else {
throw e;
}
}
List<Object> pkValues = new ArrayList<>();
while (genKeys.next()) {
Object v = genKeys.getObject(1);
pkValues.add(v);
}
afterImage = getTableRecords(pkValues);
}
流程很清晰(updateExecutor
相對(duì)復(fù)雜扑庞,但是原理相同),根本目的都是獲取insert
數(shù)據(jù)的主鍵ID拒逮,然后把數(shù)據(jù)select
出來(lái)罐氨,形成afterImage
。
從代碼中可以看出的約束:
- 必須有主鍵
- 必須自增長(zhǎng)滩援,或者insert時(shí)直接提供ID
- 不支持批量插入(至少不含主鍵的
batch insert
是不支持的)
至此栅隐,undoLog
已經(jīng)生成,但是它何時(shí)發(fā)揮作用,怎么發(fā)揮作用租悄,這寫(xiě)問(wèn)題后續(xù)結(jié)合全局事務(wù)流程再來(lái)細(xì)看谨究。
最后附上官方的一張圖,這時(shí)候看就清楚多了泣棋。