Fescar源碼閱讀-神奇的UndoLog(一)

本文梳理了Fescar生成undoLog的流程和源碼,項(xiàng)目不停迭代簿盅,本文源碼僅供參考防楷。



Fescar處理分布式事務(wù)采用是二段提交的方案趴泌,但是Fescar對(duì)二段提交的流程,尤其是鎖的流程進(jìn)行了改進(jìn)怠噪,大幅度提升了性能。
參考官網(wǎng)圖示:


XA-來(lái)源官網(wǎng)
Fescar-來(lái)源官網(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)两疚,

PreparedStatementProxy

代碼很簡(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處理


InsertExecutor
  • 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í)候看就清楚多了泣棋。

ATBranch

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末胶哲,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子潭辈,更是在濱河造成了極大的恐慌鸯屿,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,406評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件把敢,死亡現(xiàn)場(chǎng)離奇詭異寄摆,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)修赞,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,395評(píng)論 3 398
  • 文/潘曉璐 我一進(jìn)店門(mén)婶恼,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人榔组,你說(shuō)我怎么就攤上這事熙尉。” “怎么了搓扯?”我有些...
    開(kāi)封第一講書(shū)人閱讀 167,815評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵检痰,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我锨推,道長(zhǎng)铅歼,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,537評(píng)論 1 296
  • 正文 為了忘掉前任换可,我火速辦了婚禮椎椰,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘沾鳄。我一直安慰自己慨飘,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,536評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布译荞。 她就那樣靜靜地躺著瓤的,像睡著了一般。 火紅的嫁衣襯著肌膚如雪吞歼。 梳的紋絲不亂的頭發(fā)上圈膏,一...
    開(kāi)封第一講書(shū)人閱讀 52,184評(píng)論 1 308
  • 那天,我揣著相機(jī)與錄音篙骡,去河邊找鬼稽坤。 笑死丈甸,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的尿褪。 我是一名探鬼主播睦擂,決...
    沈念sama閱讀 40,776評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼茫多!你這毒婦竟也來(lái)了祈匙?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,668評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤天揖,失蹤者是張志新(化名)和其女友劉穎夺欲,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體今膊,經(jīng)...
    沈念sama閱讀 46,212評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡些阅,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,299評(píng)論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了斑唬。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片市埋。...
    茶點(diǎn)故事閱讀 40,438評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖恕刘,靈堂內(nèi)的尸體忽然破棺而出缤谎,到底是詐尸還是另有隱情,我是刑警寧澤褐着,帶...
    沈念sama閱讀 36,128評(píng)論 5 349
  • 正文 年R本政府宣布坷澡,位于F島的核電站,受9級(jí)特大地震影響含蓉,放射性物質(zhì)發(fā)生泄漏频敛。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,807評(píng)論 3 333
  • 文/蒙蒙 一馅扣、第九天 我趴在偏房一處隱蔽的房頂上張望斟赚。 院中可真熱鬧,春花似錦差油、人聲如沸拗军。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,279評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)食绿。三九已至,卻和暖如春公罕,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背耀销。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,395評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工楼眷, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留铲汪,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,827評(píng)論 3 376
  • 正文 我出身青樓罐柳,卻偏偏與公主長(zhǎng)得像掌腰,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子张吉,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,446評(píng)論 2 359