從一個線上問題分析binlog與內部XA事務提交過程

京東發表於2018-12-05
1.   問題

業務上新增一條訂單記錄,使用者接收到BinLake拉取的MySQL從庫資料訊息後,馬上根據訊息內的訂單號去查詢同一個MySQL從庫,發現有些時候無法查到該條資料,等待大約500ms~1000ms後再去查詢資料庫,可以查詢到該條資料。

注: BinLake為京東商城資料庫技術部自研的一套訂閱和消費MySQL資料庫binlog的元件,本例所描述的問題是業務方希望根據訂閱的binlog來獲取實時訂單等業務訊息。

2.  Binlog與內部XA

2.1  XA的概念

XA(分散式事務)規範主要定義了(全域性)事務管理器(TM: Transaction Manager)和(區域性)資源管理器(RM: Resource Manager)之間的介面。XA為了實現分散式事務,將事務的提交分成了兩個階段:也就是2PC (tow phase commit),XA協議就是透過將事務的提交分為兩個階段來實現分散式事務。

  • 兩階段

1)prepare 階段

事務管理器向所有涉及到的資料庫伺服器發出prepare"準備提交"請求,資料庫收到請求後執行資料修改和日誌記錄等處理,處理完成後只是把事務的狀態改成"可以提交",然後把結果返回給事務管理器。即:為prepare階段,TM向RM發出prepare指令,RM進行操作,然後返回成功與否的資訊給TM。

2)commit 階段

事務管理器收到回應後進入第二階段,如果在第一階段內有任何一個資料庫的操作發生了錯誤,或者事務管理器收不到某個資料庫的回應,則認為事務失敗,回撤所有資料庫的事務。資料庫伺服器收不到第二階段的確認提交請求,也會把"可以提交"的事務回撤。如果第一階段中所有資料庫都提交成功,那麼事務管理器向資料庫伺服器發出"確認提交"請求,資料庫伺服器把事務的"可以提交"狀態改為"提交完成"狀態,然後返回應答。即:為事務提交或者回滾階段,如果TM收到所有RM的成功訊息,則TM向RM發出提交指令;不然則發出回滾指令。

  • 外部與內部XA

MySQL中的XA實現分為:外部XA和內部XA。前者是指我們通常意義上的分散式事務實現;後者是指單臺MySQL伺服器中,Server層作為TM(事務協調者,通常由binlog模組擔當),而伺服器中的多個資料庫例項作為RM,而進行的一種分散式事務,也就是MySQL跨庫事務;也就是一個事務涉及到同一條MySQL伺服器中的兩個innodb資料庫(目前似乎只有innodb支援XA)。內部XA也可以用來保證redo和binlog的一致性問題。

2.2.  redo與binlog的一致性問題

我們MySQL為了相容其它非事務引擎的複製,在server層面引入了 binlog, 它可以記錄所有引擎中的修改操作,因而可以對所有的引擎使用複製功能; 然而這種情況會導致redo log與binlog的一致性問題;MySQL透過內部XA機制解決這種一致性的問題。

第一階段:InnoDB prepare, write/sync redo log;binlog不作任何操作;

第二階段:包含兩步,1> write/sync Binlog; 2> InnoDB commit (commit in memory);

當然在5.6之後引入了組提交的概念,可以在IO效能上進行一些提升,但總體的執行順序不會改變。

當第二階段的第1步執行完成之後,binlog已經寫入,MySQL會認為事務已經提交併持久化了(在這一步binlog就已經ready並且可以傳送給訂閱者了)。在這個時刻,就算資料庫發生了崩潰,那麼重啟MySQL之後依然能正確恢復該事務。在這一步之前包含這一步任何操作的失敗都會引起事務的rollback。

第二階段的第2步大部分都是記憶體操作,比如釋放鎖,釋放mvcc相關的read view等等。MySQL認為這一步不會發生任何錯誤,一旦發生了錯誤那就是資料庫的崩潰,MySQL自身無法處理。這個階段沒有任何導致事務rollback的邏輯。在程式執行層面,只有這一步完成之後,事務導致變更才能透過API或者客戶端查詢體現出來。

下面的一張圖,說明了MySQL在何時會將binlog傳送給訂閱者。

從一個線上問題分析binlog與內部XA事務提交過程理論上來說,也可以在commit階段完成之後再將binlog傳送給訂閱者,但這樣會增大主從延遲的風險。

3.    相關程式碼

4.        int MYSQL_BIN_LOG::ordered_commit(THD *thd, bool all, bool skip_commit) {  

5.           .....  

6.           //進入flush stage  

7.           change_stage(thd, Stage_manager::FLUSH_STAGE, thd, NULL, &LOCK_log);  

8.           ....  

9.           //通知底層儲存引擎日誌刷盤  

10.        process_flush_stage_queue(&total_bytes, &do_rotate, &wait_queue);    

11.        .....  

12.        //將各個執行緒的binlogcache寫到檔案中  

13.        flush_cache_to_file(&flush_end_pos);  

14.        ....   

15.        //進入到Sync stage  

16.        change_stage(thd, Stage_manager::SYNC_STAGE, wait_queue, &LOCK_log,  

17.                        &LOCK_sync));  

18.        //binlog fsync落盤  

19.        sync_binlog_file(false)  

20.        //通知binlog傳送執行緒,有新的binlog落盤可以傳送到訂閱者了  

21.        update_binlog_end_pos(tmp_thd->get_trans_pos());  

22.        //進入commit state  

23.        change_stage(thd, Stage_manager::COMMIT_STAGE, final_queue,  

24.                          leave_mutex_before_commit_stage, &LOCK_commit);  

25.       

26.        ....  

27.        //事務狀態提交  

28.        process_commit_stage_queue(thd, commit_queue);  

29.        ....  

30.       

31.     } 

其中,在update_binlog_end_pos之後,binlog傳送執行緒就已經可以讀取最新的binlog傳送給訂閱者了。當訂閱者收到這些binlog之後如果process_commit_stage_queue因為系統排程等原因還未執行完成,那麼訂閱者碰巧在此時發起問題中所描述的查詢,就會發生查詢不到的情況。

下面我們看一下process_commit_stage_queue都做了什麼。

在process_commit_stage_queue會分別呼叫到binlog的commit方法binlog_commit和innodb的commit函式trx_commit_in_memory。

1.      static int binlog_commit(handlerton *, THD *, bool) {  

2.        DBUG_ENTER("binlog_commit");  

3.        /* 

4.          Nothing to do (any more) on commit. 

5.         */  

6.        DBUG_RETURN(0);  

7.      }  

在binlog_commit中什麼也不做,因為跟binlog有關的操作前面都已經做完了。

最後看一下儲存引擎innodb的trx_commit_in_memory都幹了什麼。

1.      static void trx_commit_in_memory(  

2.          trx_t *trx,       /*!< in/out: transaction */  

3.          const mtr_t *mtr, /*!< in: mini-transaction of 

4.                            trx_write_serialisation_history(), or NULL if 

5.                            the transaction did not modify anything */  

6.          bool serialised)  

7.      /*!< in: true if serialisation log was 

8.      written */  

9.      {  

10.        ....  

11.        //釋放鎖  

12.        lock_trx_release_locks(trx);  

13.      

14.        ut_ad(trx_state_eq(trx, TRX_STATE_COMMITTED_IN_MEMORY));  

15.      

16.        .....  

17.        //釋放mvcc相關的read view  

18.        if (trx->read_only || trx->rsegs.m_redo.rseg == NULL) {  

19.          MONITOR_INC(MONITOR_TRX_RO_COMMIT);  

20.          if (trx->read_view != NULL) {  

21.            trx_sys->mvcc->view_close(trx->read_view, false);  

22.          }  

23.      

24.        } else {  

25.          ut_ad(trx->id > 0);  

26.          MONITOR_INC(MONITOR_TRX_RW_COMMIT);  

27.        }  

28.      }  

29.      ....  

30.      //清理insert操作相關的undo log(注意,此時只有insertundo需要清理)  

31.      if (mtr != NULL) {  

32.        if (trx->rsegs.m_redo.insert_undo != NULL) {  

33.          trx_undo_insert_cleanup(&trx->rsegs.m_redo, false);  

34.        }  

35.      

36.        if (trx->rsegs.m_noredo.insert_undo != NULL) {  

37.          trx_undo_insert_cleanup(&trx->rsegs.m_noredo, true);  

38.        }  

39.    }  

這一步完成之後,在執行時刻事務的變更才能被查詢到。但需要記住,MySQL在binlog落盤成功後就認為事務的持久化已經完成。

4.總結

在binlog落盤之後,MySQL就會認為事務的持久化已經完成(在這個時刻之後,就算資料庫發生了崩潰都可以在重啟後正確的恢復該事務)。但是該事務產生的資料變更被別的客戶端查詢出來還需要在commit全部完成之後。MySQL會在binlog落盤之後會立即將新增的binlog傳送給訂閱者以儘可能的降低主從延遲。但由於多執行緒時序等原因,當訂閱者在收到該binlog之後立即發起一個查詢操作,可能不會查詢到任何該事務產生的資料變更(因為此時該事務所處執行緒可能尚未完成最後的commit步驟)。

如果應用需要根據binlog作為一些業務邏輯的觸發點,還是需要考慮引入一些延時重試機制或者重新考慮合適的實現架構。

相關文章