數據庫分佈式事務XA規範介紹及Mysql底層實現機制【原創】

1. 引言mysql

分佈式事務主要應用領域主要體如今數據庫領域、微服務應用領域。微服務應用領域通常是柔性事務,不徹底知足ACID特性,特別是I隔離性,好比說saga不知足隔離性,主要是經過根據分支事務執行成功或失敗,執行相應的前滾的重試或者後滾的補償操做來達成全局事務的最終一致性,可是全局事務與全局事務之間沒有隔離性。算法

筆者瞭解到的分佈式事務方案有2PCXA規範,以及Google percolator方案(TiDB就採用這個實現,本質上是基於全局時間戳的樂觀鎖版本校驗)。sql

mysqlXA應用場景分爲外部XA與內部XA,內部XA用於binlogstroage engine之間,協調binlogredo事務寫入的原子性。外部XA用於mysql節點與mysql節點之間,協調跨物理庫之間的原子性。本文主要介紹外部XA數據庫

基於mysqlXA兩階段事務提交(2PC)分佈式事務,須要一個事務協調器(TransactionManager)來接受應用提交的全局事務(Global Transaction),全局事務通過TM的分解後,分解成多個分支事務(Branch Transaction),每一個分支事務在具體的某個mysql實例上運行,其中mysql做爲資源管理器(Resource Manager)。mvc

在實際的分佈式數據庫的分佈式事務的開發中,通常選擇DBProxy做爲TM載體,好比騰訊的TDSQL和阿里的POLARDB-X的分佈式事務方案,都是這樣的實現。分佈式

XA2PC提交流程的主要處理邏輯在事務協調器(Transaction Manager),通常選擇DBProxy做爲TM載體,若是DBProxyJava開發,能夠參考Atomikos的實現ide

 

2. XA協做流程函數

 

 2.1   XA2PC協做流程微服務

  XA2PC提交流程如圖2.1,主要分爲如下幾個步驟。性能

1) App發送start global transactionTMTM生成全局事務IDxid

2) App發送global transaction語句到TMTM根據具體的Sharding算法分解出 branch transaction,而且發送到各個mysql節點。

3) App發送commit語句到TMTM往各個branch transactionmysql節點發送XA preparexid語句。

4)TM收集各個Prepare語句的響應,若是各個響應都是OK,則向每一個branch transactionmysql節點發送XA commit 'xid'語句,若是各個RM響應有不OK往每一個RM     發送XA rollback 'xid'語句。

 

3. XA優化與異常處理

優化1:持久化事務協調階段的各個狀態

  TM做爲一個單點的事務協同器,頗有可能宕機,出現單點故障。其自己的職責主要是事務協調,屬於無狀態的服務。宕機重啓後,能夠根據持久化的全局事務狀態來恢復TM的執行邏輯,因此,須要將階段的各個協調階段以及該階段中每一個RM的執行狀態持久化到獨立的DB中,多個TM共享一個持久化DB。具體的階段有,prepare階段的子階段有branch_tansaction_ sendprepare_sendprepare_ack階段,commit階段的子階段有commit_sendcommit_ack階段,記錄每一個子階段每一個RM的執行狀態

 

優化2:並行發送語句

  在branch_tansaction_ sendprepare_ sendcommit_send階段,若是TMRM發送語句是串行執行的,單個global transaction的執行時間加長,TMTPS(每秒事務請求數)會下降,能夠在這些階段將已生成的語句,經過線程池並行發送到各個RMTM同時同步等待語句的返回值,延時大爲下降。

 

異常1TMprepare_send階段前宕機,重啓恢復後,繼續執行prepare_send動做。

異常2TMprepare_send階段時宕機,可能會有部分RM收到prepare語句,部份沒有收到,重啓後,往收到prepare語句RM發送rollback語句。

異常3TMprepare_ack階段記錄完各個RM的執行狀態後宕機,重啓後,根據日誌狀態發起rollback或者commit語句。

異常4TMcommit_send階段時宕機,可能會有部分RM收到commit語句,部份沒有收到,重啓後,往沒有收到commit語句RM發送commit語句。

異常5TMcommit_ ack階段記錄完各個RM的執行狀態後宕機,重啓後,根據日誌狀態發起重試commit語句或者不操做。

異常6RM超長時間沒有收到TMrollback或者commit語句,一直持有記錄鎖,RM要有自動rollback或者commit的功能。

 

4. 2PC1PC對比

  XA的兩階段提交,直觀感受和RM的交互次數太多,RPC次數太多,影響單個全局事務的響應時間,TPS確定下降。可是,prepare階段有存在的意義,若是某個單機事務處於prepare狀態,一直沒有commitmysql重啓時,進行崩潰恢復時,若是binlog中沒有該事務,對該事務進行rollback,若是有,則對該事務進行commit

  XA兩階段提交知足了事務的ACID屬性,原子性:在preparecommit階段保障了事務的原子性。隔離性:經過mysql原生的記錄鎖,作到讀寫隔離。持久性:基於mysql單機事務的redo實現了持久性。一致性:基於mysql單機事務。

  若是放棄prepare階段,只有commit階段,全局事務的原子性沒法保障,例如這個場景,全局事務的部分分支事務commit成功,另外一部分分支事務commit失敗,此時全局事務就處於既不能commit成功,也不能rollback成功,由於已經成功commit的分支事務沒法rollback

  即便經過解析binlog,生成反向SQL進行補償達到rollback的效果,此時也會多產生一次交互,RPC次數和兩階段提交是同樣的了。可是此時又引起一個新問題,全局事務的隔離性難以保障,由於另外一個全局事務2可能會修改此時全局事務1的已經commit了的記錄,而全局事務1正在反向補償同一條已經commit了的記錄。

  即便經過如下方法達到了隔離性,只知足Read Commited隔離級別,Repeated Read等隔離級別沒有實現,並且隔離的粒度比較大,記錄上的Xid,至關於一把記錄寫鎖。

  在每一個記錄上,增長一個字段全局事務IDXid),只有知足如下兩個條件之一方可訪問該記錄。

  1)記錄上Xid是本全局事務的Xid

  2)記錄上Xid不是本全局事務ID,且該Xid已經不活躍

   總結,TM和各個RM都處於徹底正常的狀況下,1PC的性能比起2PC會好,尤爲是TPS。可是在RM處於異常的場景下,例如全局事務的部分分支事務commit成功,另外一部分分支事務commit失敗。1PCTPS可能2PC差很少

  

5XA各個階段的Mysql處理流程

 

 上圖爲XA規範規範中xa_openxa_close不會頻繁調用,TMRM要維持數據庫長鏈接,避免頻繁的建立、銷燬數據庫鏈接的開銷。

 

上圖5.2mysql內部Xa的流程圖。

xa_startxa_end起到標識分支事務的做用,具體由mysql服務端Sql_cmd_xa_start::trans_xa_start()函數與Sql_cmd_xa_end::trans_xa_end()函數實現

Sql_cmd_xa_start::trans_xa_start() thd->get_transaction()->xid_state設置爲XID_STATE::XA_ACTIVE狀態

Sql_cmd_xa_end::trans_xa_end()檢查thd->get_transaction()->xid_state必須爲XID_STATE::XA_ACTIVE狀態

 

6. mysql源碼跟蹤

xa_prepare內部函數調用流程

 1 mysql_execute_command()
 2 case SQLCOM_XA_PREPARE:
 3    res= lex->m_sql_cmd->execute(thd);
 4     Sql_cmd_xa_prepare::execute(THD *thd)
 5       Sql_cmd_xa_prepare::trans_xa_prepare(THD *thd)
 6         ha_prepare(THD *thd)
 7           innobase_xa_prepare
 8             trx_prepare_for_mysql(trx_t* trx)
 9                trx_prepare()
10                   trx_prepare_low()
11                     trx_undo_set_state_at_prepare() 修改undolog狀態爲prepare狀態
12                       mlog_write_ulint() 寫redo buffer
13                         mtr_commit(&mtr)將redo buffer寫入redo log file,並將髒頁掛載在buffer pool的flushlist,能夠看出寫undo segment也須要redo保護
View Code

 

xa_commit內部流程

 1 mysql_execute_command()
 2 case SQLCOM_XA_COMMIT: 
 3 res= lex->m_sql_cmd->execute(thd);
 4   Sql_cmd_xa_commit::execute(THD *thd)
 5     Sql_cmd_xa_commit::trans_xa_commit(THD *thd)
 6       MYSQL_BIN_LOG::commit
 7         ha_commit_low
 8           innobase_commit
 9             innobase_commit_low
10                 trx_commit_for_mysql()
11                   trx_commit()
12                     trx_commit_low()
13                     trx_commit_in_memory()
14                       lock_trx_release_locks() 釋放事務的記錄鎖
15                          trx_flush_log_if_needed() 刷新redo buffer到redo log
16                            log_write_up_to(lsn, flush);
17                               log_write_flush_to_disk_low() 具體刷盤動做
View Code

 

分支事務update處理流程

 1 mysql_execute_command()
 2    case SQLCOM_UPDATE:
 3      res= lex->m_sql_cmd->execute(thd);
 4        Sql_cmd_update::execute(THD *thd)
 5         try_single_table_update
 6            open_tables_for_query(THD *thd, TABLE_LIST *tables, uint flags)
 7            open_and_process_table
 8                open_table()
 9             mysql_update
10                table->init_cost_model()
11               ha_innobase::info
12                  ha_innobase::info_low獲取統計信息
13                test_quick_select()根據代價模型,獲取開銷最低的表訪問方式,如range\table scan\index scan
14                  ha_innobase::try_semi_consistent_read(true),請求存儲引擎開啓半一致性讀,在update 或者delete的語句中。
15                  init_read_record設置數據掃描方法,如rr_quick,rr_sequential
16                    handler::ha_rnd_init
17                      ha_innobase::rnd_init,初始化c
18                  rr_sequential
19                    handler::ha_rnd_next掃描一條記錄
20                      ha_innobase::rnd_next() table scan讀取第一條記錄
21                        row_search_mvcc() 
22                           sel_set_rec_lock() 在一條記錄上加鎖
23                             lock_clust_rec_read_check_and_lock在彙集索引上加記錄鎖
24                               lock_rec_lock加記錄鎖
25                  handler::ha_update_row
26                    binlog_log_row 
27                      THD::binlog_update_row記錄row格式的binlog
28                    ha_innobase::update_row(old_row,new_row)
29                    row_upd_clust_rec() 更新彙集索引記錄
30                      trx_undo_report_row_operation() 記錄undo信息
31                        trx_undo_assign_undo() 分配回滾段
32                        trx_undo_page_report_modify() 在回滾段中記錄彙集索引的更改
33                    row_upd_rec_in_place() 更新操做寫入彙集索引
34                           row_upd_rec_in_place_log()更新操做寫入redo buffer
35                      mtr_t::commit() 將redo buffer寫入redo日誌文件,並將髒頁掛載在buffer pool的flushlist
View Code
相關文章
相關標籤/搜索