MySQL-XA事务简介请看MySQL-XA事务(一)简介,本文从源码角度,分析XA事务的实现逻辑,文章中忽略了大部分RM的实现细节,比如,redo日志如何写入,如何落盘,如何加行锁,如何更新数据页等等。
01-XA事务涉及到的数据结构
1. 对应6个命令操作(xa start/begin,end,prepare,commit,rollback,recover)的类,如下:
Sql_cmd_xa_start
Sql_cmd_xa_end
Sql_cmd_xa_prepare
Sql_cmd_xa_commit
Sql_cmd_xa_rollback
Sql_cmd_xa_recover
这六个类是完成XA事务命令的操作入口,是对接口Sql_cmd的具体实现,MySQL XA事务的控制都是通过这6个类来完成的。

2. XA事务状态类XID_STATE,主要包含如下成员:
xa_states xa_state 表示事务状态,有如下类型:
enum xa_states {XA_NOTR=0, XA_ACTIVE, XA_IDLE, XA_PREPARED, XA_ROLLBACK_ONLY};
static const char *xa_state_names[] 对应5种状态的字符串(NON-EXISTING,ACTIVE,IDLE,PREPARED,ROLLBACK ONLY),在返回错误时,方便客户端阅读。
XID m_xid XA事务的唯一标示
bool in_recovery recovery的状态标示
uint rm_error 资源管理器RM通过此数据向事务管理器TM汇报错误。
bool m_is_binlogged 标示XA事务的二进制日志记录情况。在恢复阶段需要用到.
关于XA事务状态的转换,可以参看MySQL-XA事务(一)简介。
3. xa_option_words
表示xa命令的可选项,目前xa start/end/prepare都是不支持可选项的,只有xa commit支持ONE PHASE。
enum xa_option_words {XA_NONE, XA_JOIN, XA_RESUME, XA_ONE_PHASE,
XA_SUSPEND, XA_FOR_MIGRATE};
4. xid_t
标示一个唯一的XA事务。
typedef struct xid_t XID
定义了XID的数据格式,主要成员如下:
/**
-1 means that the XID is null
*/
long formatID;
/**
value from 1 through 64
*/
long gtrid_length;
/**
value from 1 through 64
*/
long bqual_length;
/**
distributed trx identifier. not \0-terminated.
*/
char data[XIDDATASIZE];
5. 其它相关类
class THD
class Transaction_ctx
... //太多,不一一列举了

02 XA事务处理源码分析
1. xa start过程详解
xa start命令主要作用是启动一个XA事务,并且把事务状态从XA_NOTR设置为XA_ACTIVE状态。

xa start阶段的函数调用关系如下:

在开启XA事务前,会做如下检测,对应时序图中的步骤3
1. 如果XA {START|BEGIN} xid [JOIN|RESUME]最后的选项不为XA_NONE,则报错XA选项错误,代码如下:
/* TODO: JOIN is not supported yet. */
if (m_xa_opt != XA_NONE)
my_error(ER_XAER_INVAL, MYF(0));
客户端收到报错如下:
mysql> xa start 'trx1' join;
ERROR 1398 (XAE05): XAER_INVAL: Invalid arguments (or unsupported command)
mysql> xa start 'trx1' resume;
ERROR 1398 (XAE05): XAER_INVAL: Invalid arguments (or unsupported command)
2. 如果通过XA命令开启事务时,当前XA事务状态不为XA_NOTR,则报错,代码如下:
else if (!xid_state->has_state(XID_STATE::XA_NOTR))
my_error(ER_XAER_RMFAIL, MYF(0), xid_state->state_name());
客户端报错如下:
//具体报错状态,依赖于当前线程的所处的事务状态。但是错误号是一致的。#define ER_XAER_RMFAIL 1399
ERROR 1399 (XAE07): XAER_RMFAIL: The command cannot be executed when global transaction is in the ACTIVE state
3. 如果通过XA start启动XA事务时,事务处于活跃的非XA事务上下文中,则会报错,代码如下:
else if (thd->locked_tables_mode || thd->in_active_multi_stmt_transaction())
my_error(ER_XAER_OUTSIDE, MYF(0));
客户端报错如下:
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> insert into t1(name) values('aaa');
Query OK, 1 row affected (0.00 sec)
//#define ER_XAER_OUTSIDE 1400
mysql> xa start 'trx1';
ERROR 1400 (XAE09): XAER_OUTSIDE: Some work is done outside global transaction
随后通过调用XID_STATE::start_normal_xa(xid_t const\*)开启xa事务,主要是设置xid_state中的成员变量,对应时序图中的步骤5,如下:
void start_normal_xa(const XID *xid)
{
DBUG_ASSERT(m_xid.is_null());
xa_state= XA_ACTIVE; //状态变更
m_xid.set(xid); //设置xid
in_recovery= false; //recovery状态置为false
rm_error= 0; //资源管理器错误置0
}
通过调用transaction_cache_insert,插入到事务hash缓存表中,对应时序图中的步骤6,在transaction_cache_insert处理过程中,如果存在重复,则报错,检测逻辑如下:
bool transaction_cache_insert(XID *xid, Transaction_ctx *transaction)
{
mysql_mutex_lock(&LOCK_transaction_cache);//加锁
if (my_hash_search(&transaction_cache, xid->key(),
xid->key_length())) //搜索
{
mysql_mutex_unlock(&LOCK_transaction_cache);//冲突,解锁
my_error(ER_XAER_DUPID, MYF(0));//冲突,报错
return true;
}
bool res= my_hash_insert(&transaction_cache,//无冲突,插入 (uchar*)transaction);
mysql_mutex_unlock(&LOCK_transaction_cache);//解锁
return res;
}
客户端报错内容如下:
mysql> xa start 'trx1';
ERROR 1440 (XAE08): XAER_DUPID: The XID already exists
2. XA事务数据更新
在AP操作RMS进行数据写入前,RMS需要向TM注册,见时序图中的步骤12,19,并且后注册的RM在prepare阶段会先被调用。
这里只说明一点:
XA事务中,AP直接访问RM去进行数据更新,在数据更新失败后(RM-innobase),如果innodb参数innodb_rollback_on_timeout为on,则会更新xa_state中的rm_error,用来通知TM发生错误。

3. xa end操作详解

xa end命令主要是结束一个xa事务操作,并将xa事务状态从XA_ACTIVE置为XA_IDLE,或者是XA_ROLLBACK_ONLY,这取决于Innodb参数设置innodb_rollback_on_timeout,在开启innodb_rollback_on_timeout时,如果事务操作(dml)出现锁超时,死锁等情况,xa end操作将会将XA事务状态置为XA_ROLLBACK_ONLY。
XA end操作的函数调用关系如下:
mysql_execute_command(THD*, bool)
Sql_cmd_xa_end::execute(THD*)
Sql_cmd_xa_end::trans_xa_end(THD*)
check...//各种异常检测
xid_state->set_state(XID_STATE::XA_IDLE);
正常情况下,xa end是将XA事务状态从从XA_ACTIVE置为XA_IDLE。下面讨论对于异常的处理情况,对应时序图中的步骤26。
1. 异常情况:xa end命令不可以附加任何选项,否则报错,代码如下
xa end命令不可以附加任何选项,否则报错,代码如下:
if (m_xa_opt != XA_NONE)
my_error(ER_XAER_INVAL, MYF(0));
客户端收到报错如下:
mysql> xa end 'trx1' SUSPEND;
ERROR 1398 (XAE05): XAER_INVAL: Invalid arguments (or unsupported command)
2. 执行xa end命令时,xa事务状态必须为XA_ACTIVE,否则报错,代码如下:
else if (!xid_state->has_state(XID_STATE::XA_ACTIVE))
my_error(ER_XAER_RMFAIL, MYF(0), xid_state->state_name());
客户端收到报错如下:
mysql> xa end 'trx1';
ERROR 1399 (XAE07): XAER_RMFAIL: The command cannot be executed when global transaction is in the IDLE state
mysql> xa prepare 'trx1';
Query OK, 0 rows affected (0.02 sec)
mysql> xa end 'trx1';
ERROR 1399 (XAE07): XAER_RMFAIL: The command cannot be executed when global transaction is in the PREPARED state
mysql> xa commit 'trx1';
Query OK, 0 rows affected (0.02 sec)
mysql> xa end 'trx1';
ERROR 1399 (XAE07): XAER_RMFAIL: The command cannot be executed when global transaction is in the NON-EXISTING state
3. 检测用户输入的xid是否为当前事务上下文中的xid,如果不匹配,报错。代码如下:
else if (!xid_state->has_same_xid(m_xid))
my_error(ER_XAER_NOTA, MYF(0));
客户端收到报错如下:
mysql> xa start 'trx1';
Query OK, 0 rows affected (0.00 sec)
mysql> insert into t1(name) values('aa');
Query OK, 1 row affected (0.00 sec)
mysql> xa end 'trx2';
ERROR 1397 (XAE04): XAER_NOTA: Unknown XID
4. 检测事务是否被标志为回滚,如果是的话,需要更新XA事务状态为XA_ROLLBACK_ONLY。代码如下:
bool XID_STATE::xa_trans_rolled_back(){DBUG_EXECUTE_IF("simulate_xa_rm_error", rm_error= true;);if (rm_error){switch (rm_error) //根据rm_error判断RM错误类型{case ER_LOCK_WAIT_TIMEOUT: //RM发生锁超时my_error(ER_XA_RBTIMEOUT, MYF(0));break;case ER_LOCK_DEADLOCK: //RM发生死锁my_error(ER_XA_RBDEADLOCK, MYF(0));break;default:my_error(ER_XA_RBROLLBACK, MYF(0));}xa_state= XID_STATE::XA_ROLLBACK_ONLY;}return (xa_state == XID_STATE::XA_ROLLBACK_ONLY);}
此时客户端会收到报错,并将XA事务状态设置为XA_ROLLBACK_ONLY,事务无法提交,只能回滚,符合参数innodb_rollback_on_timeout设计初衷。客户端报错内容如下:
mysql> xa start 'trx1';
Query OK, 0 rows affected (0.00 sec)
mysql> insert into t1(name) values('hhh');
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> insert into t1(name) values('hhh');
Query OK, 1 row affected (0.01 sec)
mysql> insert into t1(name) values('hhh');
Query OK, 1 row affected (0.02 sec)
mysql> xa end 'trx1';
ERROR 1613 (XA106): XA_RBTIMEOUT: Transaction branch was rolled back: took too long
3. xa prepare操作详解
在说明XA事务的prepare操作之前,先来看下,普通事务两阶段提交过程,如下图所示:

xa prepare阶段与非xa事务的提交流程类似,时序图如下:

xa prepare命令的主要作用是将事务设置为XA_PREPARE状态,包含xa prepare信息的binlog日志的落盘,innodb日志刷盘(不包含事务prepare标志),innodb中事务prepare,所以这里就存在一个问题,prepare完成后,innodb中事务prepare信息没有落盘。XA prepare主要的函数调用关系如下:
mysql_execute_command(THD*, bool)
Sql_cmd_xa_prepare::execute(THD*)
Sql_cmd_xa_prepare::trans_xa_prepare(THD*)
check //异常情况检测
ha_prepare(THD*) //进入XA事务的prepare阶段
xid_state->set_state(XID_STATE::XA_PREPARED); //设置XA事务状态
其中异常情况的检测,对应时序图中的步骤30
1. 判断XA事务状态,如果不是XA_IDLE,则报错
if (!xid_state->has_state(XID_STATE::XA_IDLE))
my_error(ER_XAER_RMFAIL, MYF(0), xid_state->state_name());
客户端收到报错如下:
mysql> xa start 'trx1';
Query OK, 0 rows affected (0.00 sec)
mysql> xa prepare 'trx1';
ERROR 1399 (XAE07): XAER_RMFAIL: The command cannot be executed when global transaction is in the ACTIVE state
2. 检测用户输入Xid和当前事务上下文中的Xid是否匹配,如果不匹配,报错,代码如下:
else if (!xid_state->has_same_xid(m_xid))
my_error(ER_XAER_NOTA, MYF(0));
客户端收到报错如下:
mysql> xa end 'trx1';
Query OK, 0 rows affected (0.00 sec)
mysql> xa prepare 'trx2';
ERROR 1397 (XAE04): XAER_NOTA: Unknown XID
下面来看xa prepare的具体实现过程,此处主要描述与非xa事务的提交过程的区别。XA事务中prepare操作,通过ha_prepare直接调用RM(binlog,innobase)的prepare接口,伪代码如下:
while (ha_info)
{
handlerton *ht= ha_info->ht();
thd->status_var.ha_prepare_count++;
if (ht->prepare)
{
if (ht->prepare(ht, thd, true)) //调用RM的prepare函数
{
ha_rollback_trans(thd, true);
error=1;
break;
}
else
{
push_warning_printf(thd, Sql_condition::SL_WARNING,
ER_ILLEGAL_HA, ER(ER_ILLEGAL_HA),
ha_resolve_storage_engine_name(ht));
}
ha_info= ha_info->next();
}
此时会先调用binlog_prepare(和注册先后顺序有关,后注册的先调用),对应时序图中的步骤33,binlog_prepare的具体实现如下:
static int binlog_prepare(handlerton *hton, THD *thd, bool all)
{
DBUG_ENTER("binlog_prepare");
if (!all) //此处是XA事务的prepare阶段,无需更新此事务的last_committed,此值和MySQL多线程回放相关,不做过多说明。
{
Logical_clock& clock= mysql_bin_log.max_committed_transaction;
thd->get_transaction()->
store_commit_parent(clock.get_timestamp());
}
DBUG_RETURN(all && is_loggable_xa_prepare(thd) ?
mysql_bin_log.commit(thd, true) : 0); //在XA事务中会调用mysql_bin_log.commit(thd, true),普通事务直接退出。
}
调用mysql_bin_log.commit(thd, true)相当于直接进入了2pc的第二个阶段,也就是commit阶段,对应时序图中的步骤34-36,这是和非XA事务进入此阶段的处理是不同的,伪代码&&分析如下:
TC_LOG::enum_result MYSQL_BIN_LOG::commit(THD *thd, bool all)
{
binlog_cache_mngr *cache_mngr= thd_get_cache_mngr(thd);
Transaction_ctx *trn_ctx= thd->get_transaction();
my_xid xid= trn_ctx->xid_state()->get_xid()->get_my_xid();
bool skip_commit= is_loggable_xa_prepare(thd);//此处skip_commit为true,ordered_commit函数中会利用这个变量跳过关于innodb层的提交处理,因为xa prepare过程是不能在innobase中进行事务提交的。
XID_STATE *xs= thd->get_transaction()->xid_state();
err= cache_mngr->trx_cache.finalize(thd, &end_evt, xs);//写入xa end/prepare log event
ordered_commit(thd, all, skip_commit)//进入order_commit,2pc的commit阶段,非XA事务不会进入到这个逻辑。
}
随后进入MYSQL_BIN_LOG::ordered_commit(THD*, bool, bool)的处理,对应时序图中的步骤36-49之间。这部分逻辑主要分为三个部分,flush,sync,commit,伪代码如下:
MYSQL_BIN_LOG::ordered_commit(THD*, bool, bool)
{
//flush stage
{
innobase_flush_logs(); //innobase事务日志刷盘,步骤36
flush_binlog_cache(); //binlog 缓存刷新到文件,步骤38
semisync_after_flush_hook();//
}
//sync stage
{
sync_binlog(); //binlog 落盘,步骤41
update_binlog_end_pos(); //通知dump线程发送binlog,步骤42,此时binlog日志将开始发送给slave实例。
}
//commit stage
{
semisync_after_sync_hook(); //调用半同步插件after_sync
// binlog->commit(); 什么都不做
//innobase_commit();//跳过.
semisync_after_commit_hook(); //调用半同步插件after_commit
}
}
再调用innobase_xa_prepare(handlerton*, THD*, bool),更新事务状态。这里需要注意的是,innobase没有把事务日志持久化(历史原因,为了innobase 事务日志的组提交),对应时序图步骤50-54。
在RMS prepare结束后,设置XA事务状态为XA_PREPARED,对应步骤56,然后XA prepare阶段结束。
3. xa commit操作详解
xa commit命令主要完成XA事务最后的提交动作,分为commit当前事务和commit其它事务。时序图如下:

主要函数调用关系如下:
mysql_execute_command(THD*, bool)Sql_cmd_xa_commit::execute(THD*)Sql_cmd_xa_commit::trans_xa_commit(THD*){if (!xid_state->has_same_xid(m_xid))//commit非此事务上下文的XA事务{//检测当前事务状态,如果不是初始化状态,则报错if (!xid_state->has_state(XID_STATE::XA_NOTR)){my_error(ER_XAER_RMFAIL, MYF(0), xid_state->state_name());DBUG_RETURN(true);}Transaction_ctx *transaction= transaction_cache_search(m_xid);//事务缓存hash表中搜索XID_STATE *xs= (transaction ? transaction->xid_state() : NULL); //获取目标事务的状态信息res= !xs || !xs->is_in_recovery(); //获取recovery状态if (res) //不存在或者in_recovery为false,报错.{my_error(ER_XAER_NOTA, MYF(0));DBUG_RETURN(true);}ha_commit_or_rollback_by_xid(thd, m_xid, !res);{innobase_commit_by_xid(handlerton*, xid_t*)//innodb层事务提交。do_binlog_xa_commit_rollback(THD*, xid_t*, bool) //写Query_log_event(XA COMMIT X'74727831',X'',1)信息MYSQL_BIN_LOG::commit(THD*, bool)MYSQL_BIN_LOG::ordered_commit(THD*, bool, bool)//flush,sync,commit。其中commit阶段会省略innodb层的事务提交.}}//以下逻辑为提交当前事务.tc_log->commit();//通过事务协调器进行事务提交操作。MYSQL_BIN_LOG::commit(THD *thd, bool all)//协调器提交do_binlog_xa_commit_rollback(THD*, xid_t*, bool) //写Query_log_event(XA COMMIT X'74727831',X'',1)信息MYSQL_BIN_LOG::ordered_commit(THD*, bool, bool)//flush,sync,commit (包括半同步相关的插件调用)}
ordered_commit的过程和prepare阶段类似,主要不同点如下:
此时强制innodb 事务日志(prepared标志)落盘
调用innodb的事务提交逻辑,释放锁等资源。
提交完成后,会从事务缓存hash表中,将此事务删除,对应时序图中的步骤83。以及XA 事务状态设置为初始化XA_NOTR。
4. xa recover操作详解
xa recover的主要命令是查看处于XA_PREPARED状态的所有XA事务。这个命令的处理过程比较简单,主要函数调用关系:
mysql_execute_command(THD*, bool)
Sql_cmd_xa_recover::execute(THD*)
Sql_cmd_xa_recover::trans_xa_recover(THD*)
xs->store_xid_info(protocol, m_print_xid_as_hex);//遍历transaction_cache哈希表,进行保存。
无论事务是否处于in_recovery状态,都会被列出来。
5. xa rollback操作详解
xa rollback的主要作用是对XA事务进行回滚,包括处于XA_IDLE/XA_PREPARED/XA_ROLLBACK_ONLY三个状态的XA事务。时序图中没有画出回滚的逻辑。
主要函数调用关系如下:
mysql_execute_command(THD*, bool)Sql_cmd_xa_rollback::execute(THD*)Sql_cmd_xa_rollback::trans_xa_rollback(THD*)if (!xid_state->has_same_xid(m_xid)){//处理非当前事务上下文的XA事务}//回滚当前事务上下文的事务check//状态检测xa_trans_force_rollback(THD*)ha_rollback_trans(THD*, bool)
XA rollback大体上分为回滚当前事务,和回滚其它事务。如下图:

5.1 回滚当前事务逻辑
函数调用关系以及解析如下:
Sql_cmd_xa_rollback::trans_xa_rollback(THD*)xa_trans_force_rollback(THD*)error= tc_log->rollback(thd, all);MYSQL_BIN_LOG::rollback(THD*, bool)do_binlog_xa_commit_rollback(THD*, xid_t*, bool){if (!xid_state->is_binlogged())return 0; //如果无binlog写入,所以回滚的时候也不需要对binlog进行处理。一般是XA_IDLE状态的事务。//如下为对处于XA_PREPARED状态的事务回滚操作,处于XA_PREPARED状态的事务binlog日志可能已经发送给了slave,所以需要写入rollback 信息通知slave进行事务回滚。char buf[XID::ser_buf_size];char query[(sizeof("XA ROLLBACK")) + 1 + sizeof(buf)];int qlen= sprintf(query, "XA %s %s", commit ? "COMMIT" : "ROLLBACK",xid->serialize(buf));Query_log_event qinfo(thd, query, qlen, false, true, true, 0, false);return mysql_bin_log.write_event(&qinfo);}if(新写入了binlog日志) //对应XA_PREPARED状态的事务回滚{error= ordered_commit(thd, all, /* skip_commit */ true); //通过ordered_commit进行binlog的提交,通知dump线程发送等。}ha_rollback_low(THD*, bool) //通过handlerton functions去调用回滚,binlog端应该不会做什么了{binlog_rollback(handlerton*, THD*, bool) //进入binlog rollback的实现{int error= 0;if (thd->lex->sql_command == SQLCOM_ROLLBACK_TO_SAVEPOINT)error= mysql_bin_log.rollback(thd, all);DBUG_RETURN(error);}innobase_rollback(handlerton*, THD*, bool)//进入innobase的rollback实现{trx_rollback_for_mysql(trx_t*)trx_rollback_low(trx_t*) //innobase内部事务回滚}}cleanup_trans_state(THD*)transaction_cache_delete(Transaction_ctx*) //从事务缓存hash表中删除
5.2 回滚其他事务逻辑
回滚非当前XA事务的逻辑如下:
mysql_execute_command(THD*, bool)Sql_cmd_xa_rollback::execute(THD*)Sql_cmd_xa_rollback::trans_xa_rollback(THD*){XID_STATE *xid_state= thd->get_transaction()->xid_state();if (!xid_state->has_same_xid(m_xid)) //回滚的是非当前事务{if (!xid_state->has_state(XID_STATE::XA_NOTR)) //当前事务上下文不为初始化状态,则报错{my_error(ER_XAER_RMFAIL, MYF(0), xid_state->state_name());DBUG_RETURN(true);}if (!xs || !xs->is_in_recin_recoveryoin_recoveryvery()){ //事务处于非recovery状态,也就是in_recovery=false,则报错.my_error(ER_XAER_NOTA, MYF(0));DBUG_RETURN(true);}ha_commit_or_rollback_by_xid(thd, m_xid, false){xarollback_handlerton(THD*, st_plugin_int**, void*){innobase_rollback_by_xid(handlerton*, xid_t*)innobase_rollback_trx(trx_t*)//innodb中事务回滚binlog_xa_rollback(handlerton*, xid_t*)binlog_xa_commit_or_rollback(THD*, xid_t*, bool) //binlog 回滚{do_binlog_xa_commit_rollback(THD*, xid_t*, bool) //写回滚日志,"XA ROLLBACK X'74727831',X'',1"MYSQL_BIN_LOG::rollback(THD*, bool){MYSQL_BIN_LOG::ordered_commit(THD*, bool, bool) //flush/sync/commit}}}}}}
6. XA事务是如何处于in_recovery状态的?
当处于XA_PREPARED状态的事务线程退出时,mysqld内部会进行如下操作:
THD::release_resources()THD::cleanup()//对于处于XA_PREPARED状态的事务,会进行事务状态信息的更改,重新插入if (trn_ctx->xid_state()->has_state(XID_STATE::XA_PREPARED)){transaction_cache_detach(trn_ctx);my_hash_delete(&transaction_cache, (uchar *)transaction);create_and_insert_new_transaction(xid_t*, bool){XID_STATE::start_recovery_xa(xid_t const*, bool){xa_state= XA_PREPARED;m_xid.set(xid);in_recovery= true;rm_error= 0;m_is_binlogged= binlogged_arg;}return my_hash_insert(&transaction_cache, (uchar*)transaction); //重新插入}}else //否则,直接删除掉{xs->set_state(XID_STATE::XA_NOTR);trans_rollback(this);transaction_cache_delete(trn_ctx);}
在下篇文章中,会描述MySQL-5.7版本xa事务与MySQL-5.6版本的区别,以及目前在使用过程中还面临的问题等,欢迎订阅。
继续阅读
本文分享自微信公众号 - MySQLLabs,如有侵权,请联系 service001@enmotech.com 删除。





