MySQL 5.7增強了分布式事務的支援,解決了之前用戶端退出或者伺服器關閉後prepared的事務復原和伺服器當機後binlog丢失的情況。
為了解決之前的問題,MySQL5.7将外部XA在binlog中的記錄分成了兩部分,使用兩個GTID來記錄。執行prepare的時候就記錄一次binlog,執行commit/rollback再記錄一次。由于XA是分成兩部分記錄,那麼XA事務在binlog中就可能是交叉出現的。Slave端的SQL線程在apply的時候需要能夠在這些不同僚務間切換。
但MySQL XA Replication的實作隻考慮了Innodb一種事務引擎的情況,當添加其他事務引擎的時候,原本的一些代碼邏輯就會有問題。同時MySQL源碼中也存在當機導緻主備不一緻的缺陷。
當執行 XA START ‘xid’後,内部xa_state進入XA_ACTIVE狀态。
第一次記錄DML操作的時候,通過下面代碼可以看到,對普通事務在binlog的cache中第一個event記錄’BEGIN’,如果是xa_state處于XA_ACTIVE狀态就記錄’XA START xid’,xid為序列化後的。
XA END xid的執行會将xa_state設定為XA_IDLE。
當XA PREPARE xid執行的時候,binlog_prepare會通過檢查thd的xa_state是否處于XA_IDLE狀态來決定是否記錄binlog。如果在對應狀态,就會調用MYSQL_BINLOG的commit函數,記錄’XA PREPARE xid’,将之前cache的binlog寫入到檔案。
當XA COMMIT/ROLLBACK xid執行時候,調用do_binlog_xa_commit_rollback記錄’XA COMMIT/ROLLBACK xid’。
由于XA PREPARE單獨記錄binlog,那麼binlog中的events一個xa事務就可能是分隔開的。舉個例子,session1中xid為’a’的分布式事務執行xa prepare後,session2中執行并送出了xid為’z’的事務,然後xid ‘a’才送出。我們可以看到binlog events中xid ‘z’的events在’a’的prepare和commit之間。
由于XA事務在binlog中是會交叉出現的,Slave的SQL線程如果按照原本普通事務的方式重放,那麼就會出現SQL線程中還存在處于prepared狀态的事務,就開始處理下一個事務了,鎖狀态、事務狀态等會錯亂。是以SQL線程需要能夠支援這種情況下不同僚務間的切換。
SQL線程要做到能夠在執行XA事務時切換到不同僚務,需要做到server層保留原有xid的Transaction_ctx資訊,引擎層也保留原有xid的事務資訊。
server層保留原有xid的Transaction_ctx資訊是通過在prepare的時候将thd中xid的Transaction_ctx資訊從transacion_cache中detach掉,建立新的保留了XA事務資訊的Transaction_ctx放入transaction_cache中。
引擎層的實作并不是通過在prepare的時候建立新trx_t的來儲存原有事務資訊。而是在XA START的時候将原來thd中所有的engine ha_data單獨保留起來,為XA事務建立新的。在XA PREPARE的時候,再将原來的reattach回來,将XA的從thd detach掉,解除XA和thd的關聯。引擎層添加了新的接口replace_native_transaction_in_thd來支援上述操作。對于Slave的SQL線程,函數調用如下:
當XA COMMIT/ROLLBACK執行的時候,如果目前thd中沒有對應的xid,就會從transaction_cache中查找對應xid的state資訊,然後調用各個引擎的commit_by_xid/rollback_by_xid接口送出/復原XA事務。
由于XA COMMIT/XA ROLLBACK是單獨作為一部分,這部分并沒有原來XA事務涉及到庫、表的資訊,是以XA COMMIT在Slave端當slave-parallel-type為DATABASE時是無法并發執行的,在slave端強制設定mts_accessed_dbs為OVER_MAX_DBS_IN_EVENT_MTS使其串行執行。
MySQL中普通事務送出的時候,需要先在引擎中prepare,然後再寫binlog,之後再做引擎commit。但在MySQL執行XA PREPARE的時候先寫入了binlog,然後才做引擎的prepare。如果引擎在做prepare的時候失敗或者伺服器crash就會導緻binlog和引擎不一緻,主備進入不一緻的狀态。
在MySQL5.7中對模拟simulate_xa_failure_prepare的DEBUG情況做如下修改,使之模拟在Innodb引擎prepare的時候失敗。
然後運作下面的case,可以看到Master上的XA失敗後被復原。但由于這個時候已經寫入了binlog events,導緻Slave端執行了XA事務,留下一個處于prepared狀态的XA事務。
在MySQL5.7源碼中,如果在binlog和InnoDB引擎都prepare之後是不是資料就安全了呢?我們在ha_prepare函數中while循環調用完所有引擎prepare函數之後添加如下DEBUG代碼,可以控制在prepare調用結束後伺服器crash掉。
然後跑下面的testcase。可以看到即使所有引擎都prepare了,當機重新開機後XA RECOVER還是還是沒有能夠找回之前prepare的事務。而且這個時候我們檢視binlog檔案可以看到binlog已經寫成功,這也會導緻主備不一緻。很明顯,應該是InnoDB引擎丢失了prepare的日志。那麼是什麼原因導緻這個問題?感興趣的同學可以檢視int MYSQL_BIN_LOG::ordered_commit(THD *thd, bool all, bool skip_commit)和innobase中trx_prepare的代碼,看process_flush_stage_queue和flush_logs和thd->durability_property的相關邏輯。這裡不再展開詳細叙述。
上面兩個問題的修複,都可以通過先執行事務引擎的prepare操作,再調用binlog的prepare來解決。
在上面實作分析中可以看到Slave在執行XA START的時候,由于這個時候并不知道該XA事務涉及到哪些引擎,是以對所有Storage engine引擎都調用了detach_native_trx。但在XA PREPARE的時候,源碼中隻對XA涉及到的引擎調用了reattach_engine_ha_data_to_thd。對于引擎可插拔的MySQL來說,當server中不止一個事務引擎,這裡就會存在有的引擎原thd中的trx被detach後沒有被reattach。
我們可以拿支援tokudb的percona server做對應實驗。對DEBUG編譯的server,執行下面replication的testcase。該case對TokuDB做一個完整的XA事務後,再向Innodb寫入。運作該case,slave端會産生assert_fail的錯誤。因為TokuDB執行XA事務時,将Innodb的ha_data放入backup,但由于Innodb沒有參與該XA事務,是以并沒有reattach,導緻gdb可以看到assert_fail處InnoDB的ha_ptr_backup不為NULL,不符合預期。
修複問題,可以在需要reattach_engine_ha_data_to_thd的代碼處,對所有storage engine再次調用該操作。
對于不支援reattach_engine_ha_data_to_thd的事務引擎實際是不支援重放MySQL5.7新XA方式生成的binlog的,但在源碼中并沒有合适禁止操作。這就會導緻slave在apply的時候資料錯亂。
繼續使用支援tokudb的percona server做實驗。由于TokuDB并沒有實作reattach_engine_ha_data_to_thd接口,Slave在重放XA事務的時候,在TokuDB引擎中實際就在原本關聯thd的trx上操作,并沒有生成新的trx。這就會導緻資料等資訊錯亂,可以看到下面的例子。session1做了一個XA事務,插入數值1,prepare後并沒有送出。随後另一個session插入數值2,但在slave同步後,數值2無法查詢到。在session1送出了XA事務,寫入TokuDB的數值1、2才在slave端查詢到。
修複該問題,需要對沒有實作新接口的事務引擎在執行XA時候給與合适的禁止操作,同時需要支援新XA的事務引擎要實作reattach_engine_ha_data_to_thd接口。