1. 引言mysql
分佈式事務主要應用領域主要體如今數據庫領域、微服務應用領域。微服務應用領域通常是柔性事務,不徹底知足ACID特性,特別是I隔離性,好比說saga不知足隔離性,主要是經過根據分支事務執行成功或失敗,執行相應的前滾的重試或者後滾的補償操做來達成全局事務的最終一致性,可是全局事務與全局事務之間沒有隔離性。算法
筆者瞭解到的分佈式事務方案有2PC的XA規範,以及Google 的percolator方案(TiDB就採用這個實現,本質上是基於全局時間戳的樂觀鎖版本校驗)。sql
mysql的XA應用場景分爲外部XA與內部XA,內部XA用於binlog與stroage engine之間,協調binlog與redo事務寫入的原子性。外部XA用於mysql節點與mysql節點之間,協調跨物理庫之間的原子性。本文主要介紹外部XA。數據庫
基於mysql的XA兩階段事務提交(2PC)分佈式事務,須要一個事務協調器(TransactionManager)來接受應用提交的全局事務(Global Transaction),全局事務通過TM的分解後,分解成多個分支事務(Branch Transaction),每一個分支事務在具體的某個mysql實例上運行,其中mysql做爲資源管理器(Resource Manager)。mvc
在實際的分佈式數據庫的分佈式事務的開發中,通常選擇DBProxy做爲TM載體,好比騰訊的TDSQL和阿里的POLARDB-X的分佈式事務方案,都是這樣的實現。分佈式
XA的2PC提交流程的主要處理邏輯在事務協調器(Transaction Manager),通常選擇DBProxy做爲TM載體,若是DBProxy用Java開發,能夠參考Atomikos的實現ide
2. XA協做流程函數
圖2.1 XA的2PC協做流程微服務
XA的2PC提交流程如圖2.1,主要分爲如下幾個步驟。性能
1) App發送start global transaction到TM,TM生成全局事務ID,xid
2) App發送global transaction語句到TM,TM根據具體的Sharding算法分解出 branch transaction,而且發送到各個mysql節點。
3) App發送commit語句到TM,TM往各個branch transaction的mysql節點發送XA prepare‘xid’語句。
4)TM收集各個Prepare語句的響應,若是各個響應都是OK,則向每一個branch transaction的mysql節點發送XA commit 'xid'語句,若是各個RM響應有不OK的,往每一個RM 上發送XA rollback 'xid'語句。
3. XA優化與異常處理
優化1:持久化事務協調階段的各個狀態
TM做爲一個單點的事務協同器,頗有可能宕機,出現單點故障。其自己的職責主要是事務協調,屬於無狀態的服務。宕機重啓後,能夠根據持久化的全局事務狀態來恢復TM的執行邏輯,因此,須要將階段的各個協調階段以及該階段中每一個RM的執行狀態持久化到獨立的DB中,多個TM共享一個持久化DB。具體的階段有,prepare階段的子階段有branch_tansaction_ send、prepare_send、prepare_ack階段,commit階段的子階段有commit_send、commit_ack階段,記錄每一個子階段每一個RM的執行狀態
優化2:並行發送語句
在branch_tansaction_ send、prepare_ send、commit_send階段,若是TM往RM發送語句是串行執行的,單個global transaction的執行時間加長,TM的TPS(每秒事務請求數)會下降,能夠在這些階段將已生成的語句,經過線程池並行發送到各個RM,TM同時同步等待語句的返回值,延時大爲下降。
異常1:TM在prepare_send階段前宕機,重啓恢復後,繼續執行prepare_send動做。
異常2:TM在prepare_send階段時宕機,可能會有部分RM收到prepare語句,部份沒有收到,重啓後,往收到prepare語句的RM發送rollback語句。
異常3:TM在prepare_ack階段記錄完各個RM的執行狀態後宕機,重啓後,根據日誌狀態發起rollback或者commit語句。
異常4:TM在commit_send階段時宕機,可能會有部分RM收到commit語句,部份沒有收到,重啓後,往沒有收到commit語句的RM發送commit語句。
異常5:TM在commit_ ack階段記錄完各個RM的執行狀態後宕機,重啓後,根據日誌狀態發起重試commit語句或者不操做。
異常6:RM超長時間沒有收到TM的rollback或者commit語句,一直持有記錄鎖,RM要有自動rollback或者commit的功能。
4. 2PC與1PC對比
XA的兩階段提交,直觀感受和RM的交互次數太多,RPC次數太多,影響單個全局事務的響應時間,TPS確定下降。可是,prepare階段有存在的意義,若是某個單機事務處於prepare狀態,一直沒有commit,mysql重啓時,進行崩潰恢復時,若是binlog中沒有該事務,對該事務進行rollback,若是有,則對該事務進行commit。
XA兩階段提交知足了事務的ACID屬性,原子性:在prepare和commit階段保障了事務的原子性。隔離性:經過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,至關於一把記錄寫鎖。
在每一個記錄上,增長一個字段全局事務ID(Xid),只有知足如下兩個條件之一方可訪問該記錄。
1)記錄上Xid是本全局事務的Xid,
2)記錄上Xid不是本全局事務ID,且該Xid已經不活躍
總結,TM和各個RM都處於徹底正常的狀況下,1PC的性能比起2PC會好,尤爲是TPS。可是在RM處於異常的場景下,例如全局事務的部分分支事務commit成功,另外一部分分支事務commit失敗。1PC的TPS可能跟2PC差很少。
5. XA各個階段的Mysql處理流程
上圖爲XA規範圖,規範中xa_open與xa_close不會頻繁調用,TM與RM要維持數據庫長鏈接,避免頻繁的建立、銷燬數據庫鏈接的開銷。
上圖5.2爲mysql內部Xa的流程圖。
xa_start與xa_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保護
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() 具體刷盤動做
分支事務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