1. 程式人生 > >淺談分散式事務與TX-LCN

淺談分散式事務與TX-LCN

最近做專案使用到了分散式事務,下面這篇文章將給大家介紹一下對分散式事務的一些見解,並講解分散式事務處理框架TX-LCN的執行原理,初學入門,錯誤之處望各位不吝指正。

什麼情況下需要使用分散式事務?

使用的場景很多,先舉一個常見的:在微服務系統中,如果一個業務需要使用到不同的微服務,並且不同的微服務對應不同的資料庫。

打個比方:電商平臺有一個客戶下訂單的業務邏輯,這個業務邏輯涉及到兩個微服務,一個是庫存服務(庫存減一),另一個是訂單服務(訂單數加一),示意圖如下:

如果在執行這個業務邏輯時沒有使用分散式事務,當庫存與訂單其中一個出現故障時,就很可能出現這樣的情況:庫存資料庫的值減少了1,但是訂單資料庫沒有變化;或是庫存沒變化,多了一個訂單,也就是出現了資料不一致現象。

所以在類似的場合下我們要使用分散式事務,保證資料的一致性。

分散式事務的解決思路

引入:mysql中的兩階段提交策略

在談分散式事務的解決思路之前,我們先來看看單一資料來源是如何做事務處理的,我們可以從中獲取一些啟發。

我們以mysql的InnoDB引擎為例,由於mysql中有兩套日誌機制,一套是儲存層的redo log,另一套是server層的binlog,每次更新資料都要對兩個日誌進行更新。為了防止寫日誌時只寫了其中一個而沒有寫另外一個,mysql使用了一個叫兩階段提交的方式保證事務的一致性。具體是這樣的:

假設建立一個這樣的資料庫:mysql> create table T(ID int primary key, c int);


然後執行一條這樣的更新語句:mysql> update T set c=c+1 where ID=2;

這條更新語句的執行流程是這樣子的:

  1. 首先執行器會找引擎取ID=2這一行資料
  2. 拿到資料後會把資料進行+1操作,然後呼叫引擎介面把新資料寫入
  3. 引擎將資料更新到記憶體中,並將操作記錄到redo log裡,此時redo log處於prepare狀態。但它不會提交事務,只是通知執行器已經完成任務,可以隨時提交。
  4. 執行器生成這個操作的binlog,並把binlog寫入磁碟
  5. 最後執行器呼叫引擎的事務介面,把redo log改為提交狀態,更新完成。

在上述過程中,redo log寫完後沒有直接提交,而是處於prepare狀態,等通知執行器並把binlog寫完後,redo log再進行提交。這個過程就是兩階段提交,這是一個精妙的設計。

可能你會問為什麼要有兩階段提交?如果不採用兩階段提交的話,也就是使用一階段提交,那就相當於按順序執行寫redo log和binlog,如果寫完redo log 後系統出現了故障,那麼就會只有redo log記錄了操作,binlog沒有記錄,造成資料不一致;使用兩階段提交的話,假設寫完redo log後系統出現了故障,由於事務還沒有提交,所以可以順利回滾。

兩階段提交的設計還有什麼好處?首先要奠定一個概念:一個操作執行的時間越長,這個操作就越有可能失敗。打個比方,你吃飯要用20分鐘,上廁所要用1分鐘,在吃飯的過程中收到微信訊息的概率肯定比去上廁所的過程中收到微信訊息的概率大。由於在資料庫中更新操作的時間要遠大於提交事務的時間,所以先把更新操作做完,等所有耗時操作都做完最後再提交事務,能夠最大程度保證事務執行成功。

分散式事務的兩階段提交策略

根據上述的兩階段提交策略,分散式事務也可以採取類似的辦法完成事務。

在第一階段,我們要新增一個事務管理者的角色,通過它來協調各個資料來源。還是拿開頭的訂單案例講解,在執行下訂單的邏輯時,先讓各個資料庫去執行各自的事務,比如從庫存中減1,在訂單庫中加1,但是完成後不提交,只是通知事務管理者已經完成了任務。

到了第二階段,由於在階段一我們已經收到了各個資料來源是否就緒的資訊,只要有一個數據源沒有就緒,在第二階段就通知所有資料來源回滾;如果全部資料來源都已經就緒,就通知所有資料來源提交事務。

總結一下這個兩階段提交的過程就是:首先事務管理器通知各個資料來源進行操作,並返回是否準備好的資訊。等所有資料來源都準備好後,再統一發送事務提交(回滾)的通知讓各個資料來源提交事務。由於最後的提交操作耗時極短,所以操作失敗的可能性會很低。

那麼這個兩階段提交協議可能存在什麼缺點呢?很可能存在被阻塞的問題,假如其中一個數據源出現了某些問題阻塞了,既不能返回成功資訊,也不能返回失敗資訊,那麼整個事務將被阻塞。對應的策略是新增一些倒計時的操作,或者是重新發送訊息。

分散式事務框架TX-LCN

講了這麼多理論的知識,下面講解一款真正應用在生產中的分散式事務框架TX-LCN的執行原理。(典型的分散式事務框架不止TX-LCN,比如還有阿里的GTS,不過GTS是收費的,TX-LCN是開源的)

我們先看一下官方文件中給出的執行原理示意圖:

思路和我們上面講的兩階段分散式事務處理流程差不多(有小不同),核心步驟分為3步:

  1. 建立事務組:在事務發起方開始執行業務程式碼之前先呼叫TxManager建立事務組物件,然後拿到事務表示GroupId的過程。簡單來說就是對這次下訂單的操作在事務管理中心裡建立一個物件,拿到一個id。
  2. 加入事務組:參與方在執行完業務方法後,將該模組的事務資訊通知給TxManager的操作。也就是指各個資料來源(各個服務)完成操作後,和事務管理中心說一聲,註冊一下自己。
  3. 通知事務組:發起方執行業務程式碼後,將發起方執行結果狀態通知給TxManager,TxManager將根據事務最終狀態和事務組的資訊來通知相應的參與模組提交或回滾事務,並返回結果給事務發起方。和客戶打交道的下訂單服務會收到減庫存和加訂單是否成功訊息,它會把這兩個訊息通知給事務管理者,事務管理者根據情況通知兩個庫存服務提交事務或回滾事務。

目前發現網上有一篇不錯的TX-LCN執行原始碼分析文章: https://blog.csdn.net/cgj296645438/article/details/93860384

文章中跟著原始碼走一遍會發現和上面的流程圖差不多,落實到程式碼中有一些精彩的地方,比如:


public Object runTransaction(DTXInfo dtxInfo, BusinessCallback business) throws Throwable {
 
        if (Objects.isNull(DTXLocalContext.cur())) {
            DTXLocalContext.getOrNew();
        } else {
            return business.call();
        }
 
        log.debug("<---- TxLcn start ---->");
        DTXLocalContext dtxLocalContext = DTXLocalContext.getOrNew();
        TxContext txContext;
        // ---------- 保證每個模組在一個DTX下只會有一個TxContext ---------- //
        if (globalContext.hasTxContext()) {
            // 有事務上下文的獲取父上下文
            txContext = globalContext.txContext();
            dtxLocalContext.setInGroup(true);
            log.debug("Unit[{}] used parent's TxContext[{}].", dtxInfo.getUnitId(), txContext.getGroupId());
        } else {
            // 沒有的開啟本地事務上下文
            txContext = globalContext.startTx();
        }
        
        //......
}

這段程式碼保證了每個模組下只會有一個TxContext,換個說法就是假設一個業務邏輯不是操作不同的資料來源,而是對同一個資料來源執行多次相同的操作,那麼該資料來源對應的模組在DTX下會只有一個TxContext

LCN的事務協調機制

LCN的口號是:LCN並不生產事務,LCN只是本地事務的協調工。大家肯定會有個疑問,它不生產事務,那麼它是怎麼控制各個模組在完成事務的邏輯操作之後不馬上提交,而是等到TxManager最後一起通知各模組提交的呢?

因為每個模組都是一個TxClient,每個TxClient下都有一個連線池,是框架自定義的連線池,對Connection使用靜態代理的方式進行包裝。

public class LcnConnectionProxy implements Connection {
 
    private Connection connection;
 
    public LcnConnectionProxy(Connection connection) {
        this.connection = connection;
    }
 
    /**
     * notify connection
     *
     * @param state transactionState
     * @return RpcResponseState RpcResponseState
     */
    public RpcResponseState notify(int state) {
        try {
            
            if (state == 1) {
                log.debug("commit transaction type[lcn] proxy connection:{}.", this);
                connection.commit();
            } else {
                log.debug("rollback transaction type[lcn] proxy connection:{}.", this);
                connection.rollback();
            }
            connection.close();
            log.debug("transaction type[lcn] proxy connection:{} closed.", this);
            return RpcResponseState.success;
        } catch (Exception e) {
            log.error(e.getLocalizedMessage(), e);
            return RpcResponseState.fail;
        }
    }
 
    @Override
    public void setAutoCommit(boolean autoCommit) throws SQLException {
        connection.setAutoCommit(false);
    }
    
    //......
}

連線池在沒有接收到通知事務之前會一直佔有著這次分散式事務的連線資源。等到最後TxManager通知TxClient時,TxClient才會去執行相應的提交或回滾。所以LCN的事務協調機制相當於是攔截了一下連線池,控制了連線的事務提交。

LCN的事務補償機制

由於我們不能保證事務每次都正常執行,如果在執行某個業務方法時,本應該執行成功的操作卻因為伺服器掛機或網路抖動等問題導致事務沒有正常提交,這種場景就需要通過補償來完成事務。

在這種情況下TxManager會做一個標示;然後返回給發起方。告訴他本次事務有存在沒有通知到的情況,然後TxClient再次執行該次請求事務。


參考資料:
極客時間丁奇Mysql實戰與尚學堂視訊配套資