数据库分布式事务XA规范介绍及Mysql底层实现机制【原创】

1. 引言

分布式事务主要应用领域主要体现在数据库领域、微服务应用领域。微服务应用领域一般是柔性事务,不完全满足ACID特性,特别是I隔离性,比如说saga不满足隔离性,主要是通过根据分支事务执行成功或失败,执行相应的前滚的重试或者后滚的补偿操作来达成全局事务的最终一致性,但是全局事务与全局事务之间没有隔离性。

笔者了解到的分布式事务方案有2PCXA规范,以及Google percolator方案(TiDB就采用这个实现,本质上是基于全局时间戳的乐观锁版本校验)。

mysqlXA应用场景分为外部XA与内部XA,内部XA用于binlogstroage engine之间,协调binlogredo事务写入的原子性。外部XA用于mysql节点与mysql节点之间,协调跨物理库之间的原子性。本文主要介绍外部XA

基于mysqlXA两阶段事务提交(2PC)分布式事务,需要一个事务协调器(TransactionManager)来接受应用提交的全局事务(Global Transaction),全局事务经过TM的分解后,分解成多个分支事务(Branch Transaction),每个分支事务在具体的某个mysql实例上运行,其中mysql作为资源管理器(Resource Manager)。

在实际的分布式数据库的分布式事务的开发中,一般选择DBProxy作为TM载体,比如腾讯的TDSQL和阿里的POLARDB-X的分布式事务方案,都是这样的实现。

XA2PC提交流程的主要处理逻辑在事务协调器(Transaction Manager),一般选择DBProxy作为TM载体,如果DBProxyJava开发,可以参考Atomikos的实现

 

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

 

posted on 2020-07-20 16:27  huyutian  阅读(1108)  评论(0编辑  收藏  举报