PART 01
背景
InnoDB中undo段的状态
undo段处于
TRX_UNDO_ACTIVE
状态,事务将被回滚;undo段处于
TRX_UNDO_PREPARED
状态,将根据binlog的情况来决定是回滚还是提交事务。
MySQL中的XA事务
分布式事务允许多个独立的事务资源参与到一个全局的事务中,全局事务要求所有参与的事务要么都提交,要么都不提交。XA是一套分布式事务规范,本文所说的XA事务是指基于XA协议的分布式事务。XA协议下,分布式事务通常由一个全局事务管理器,一个或多个局部资源管理器,以及一个应用程序组成:
应用程序(AP):定义事务边界,并指定构成事务的操作
资源管理器(RM):提供对共享资源的访问
事务管理器(TM):为事务分配唯一标识符,监视其进度,并负责事务的提交,回滚和故障恢复

XA START,负责开启或者恢复一个XA事务,将事物状态设置为ACTIVE
XA END,将事务状态设置为IDLE状态,可通过XA START进行恢复
XA PREPARE,通知资源管理器,准备提交事务,将事务设置为PREPARED状态
XA COMMIT,通知资源管理器,提交XA事务
XA ROLLBACK,通知资源管理器,回滚XA事务
第一阶段,事务管理器发起PREPARE请求,询问所有资源管理器是否可以提交事务,资源管理器根据自身状态回复YES或者NO,在回复YES前,资源管理器会将事务持久化并设置为PREPARED状态
第二阶段,事务管理器根据前一阶段的结果来决定是提交还是回滚事务,如果所有节点均返回YES,那么通知所有节点提交事务,否则通知所有节点回滚事务
内部XA事务,事务管理器位于MySQL内部,一个事务跨多个存储引擎进行读写,就会产生内部XA事务。其中binlog是一个特殊的参与者,因此,尽管一个事务只修改一个存储引擎,由于binlog的存在,也会启动内部XA事务。崩溃恢复的时候根据binlog内容来决定InnoDB引擎中的事务是提交还是回滚,binlog中存在的XA事务,在InnoDB中会提交相应的事务,如果一个事务在binlog中不存在,那么在InnoDB层会回滚该事务。
外部XA事务,由外部的事务管理器控制,用户使用XA start, XA end,XA prepare和XA commit接口来操作XA事务,可以修改多个节点的数据。MySQL-8.0.30以前,崩溃恢复的时候MySQL对InnoDB中处于prepared状态的外部XA事务统一不做处理,因此外部XA事务不保证crash safe(即,binlog和InnoDB中的事务可能出现不一致)。
MySQL外部XA相关问题
binlog prepare
写完XA prepare的binlog后立即crash
binlog prepare如果交换binlog和InnoDB的prepare顺序
↓ crash
InnoDB prepare
此时InnoDB中事务的状态还是active,下次启动的时候active状态的事务被直接回滚,造成binlog和InnoDB不一致,进而导致主从不一致。
InnoDB prepare
↓ crash
binlog prepare
在InnoDB prepare完成后立即crash,此时InnoDB中事务的状态是prepared,而binlog中还没有对应的日志(崩溃恢复的时候不会回滚已经处于prepared状态的外部XA事务),导致binlog和InnoDB不一致。
XA prepare的顺序:InnoDB prepare,binlog prepare
仿照Previous_gtid_log_event,在binlog中新增一个event,用于记录已经处于prepared状态的XA事务的xid
崩溃恢复过程中,根据binlog中记录的xid来决定是回滚还是保留InnoDB中处于prepared状态的外部XA事务
PART 02
MySQL 8.0.30的XA PERPARE



UNDO 状态
新增一个事务undo状态 TRX_UNDO_PREPARED_IN_TC
/** contains an undo log of an prepared transaction */constexpr uint32_t TRX_UNDO_PREPARED = 6;/* contains an undo log of a prepared transaction that has been processed by the* transaction coordinator */constexpr uint32_t TRX_UNDO_PREPARED_IN_TC = 7;

Prepare顺序
binlog prepare
注意,binlog prepare不再写binlog:

2. InnoDB prepare
设置事务为prepared状态(TRX_UNDO_PREPARED),保证crash后事务能正常恢复
3. binlog commit
在commit阶段写入xa prepare对应的binlog并将InnoDB中事务的状态设置为TRX_UNDO_PREPARED_IN_TC(表示XA prepare的日志已经写入到binlog中)
for (THD *head = first; head; head = head->next_to_commit) {Thd_backup_and_restore switch_thd(thd, head);auto all = head->get_transaction()->m_flags.real_commit;// 标记事务状态为 prepared in TCtrx_coordinator::set_prepared_in_tc_in_engines(head, all);if (head->get_transaction()->m_flags.xid_written) dec_prep_xids(head);}
注意,只有外部XA事务才需要设置TRX_UNDO_PREPARED_IN_TC(内部事务不需要)。

PART 03
MySQL 8.0.30的崩溃恢复


崩溃恢复阶段,外部XA事务的状态可以是:
enum class enum_ha_recover_xa_state : int {NOT_FOUND = -1, // Trnasaction not foundPREPARED_IN_SE = 0, // Transaction is prepared in SEsPREPARED_IN_TC = 1, // Transaction is prepared in SEs and TCCOMMITTED_WITH_ONEPHASE = 2, // Transaction was one-phase committedCOMMITTED = 3, // Transaction was committedROLLEDBACK = 4 // Transaction was rolled back};
扫描最后一个binlog,如果遇到了XA_prepare_log_event,会将该event对应的xid保存起来,并设置状态为
enum_ha_recover_xa_state::PREPARED_IN_TC
(此处不考虑XA commit one phase的情况)。扫描完成后,将刚刚保存的外部XA事务的xid以及对应的状态传入InnoDB。
InnoDB根据传入的XA事务的状态以及InnoDB内部事务的undo状态修改或设置某些事务的状态。
根据事务的状态对事务进行处理(比如回滚)。
if (trx_state_eq(trx, TRX_STATE_PREPARED)) {if (trx_is_prepared_in_tc(trx)) {/* 事务处于XA prepare的第二阶段,将该事务加到XA事务状态链表中去,并修改事务状态为PREPARED_IN_TC*/xa_list.add(*trx->xid, enum_ha_recover_xa_state::PREPARED_IN_TC);} else {/*否则,将该事务加到XA事务状态链表中去,并修改事务状态为PREPARED_IN_SE*/xa_list.add(*trx->xid, enum_ha_recover_xa_state::PREPARED_IN_SE);}}
enum_ha_recover_xa_state Xa_state_list::add(XID const &xid,enum_ha_recover_xa_state state) {auto previous_state = enum_ha_recover_xa_state::NOT_FOUND;auto it = this->m_underlying.find(xid);if (it != this->m_underlying.end()) previous_state = it->second;switch (state) {case enum_ha_recover_xa_state::PREPARED_IN_SE: {if (previous_state == enum_ha_recover_xa_state::NOT_FOUND ||previous_state == enum_ha_recover_xa_state::COMMITTED ||previous_state == enum_ha_recover_xa_state::ROLLEDBACK)this->m_underlying[xid] = state;break;}case enum_ha_recover_xa_state::PREPARED_IN_TC: {if (previous_state == enum_ha_recover_xa_state::NOT_FOUND ||previous_state == enum_ha_recover_xa_state::PREPARED_IN_SE)this->m_underlying[xid] = state;break;}case enum_ha_recover_xa_state::NOT_FOUND:case enum_ha_recover_xa_state::COMMITTED:case enum_ha_recover_xa_state::COMMITTED_WITH_ONEPHASE:case enum_ha_recover_xa_state::ROLLEDBACK: {assert(false);break;}}return previous_state;}
一个事务在XA prepare时,写完binlog后立即crash,此时InnoDB中undo的状态是
TRX_UNDO_PREPARED
,server层的状态是enum_ha_recover_xa_state::PREPARED_IN_TC
,该函数不做任何处理,事务的最终状态是enum_ha_recover_xa_state::PREPARED_IN_TC
。一个事务在XA prepare时,还未来得及写binlog实例就崩溃,此时InnoDB中undo的状态是
TRX_UNDO_PREPARED
,server层的状态是enum_ha_recover_xa_state::NOT_FOUND
,事务的最终状态被设置为enum_ha_recover_xa_state::PREPARED_IN_SE
。事务的XA prepare顺利完成,该函数不做任何处理,事务的最终状态保持
enum_ha_recover_xa_state::PREPARED_IN_TC
。
bool xa::recovery::recover_one_ht(THD *, plugin_ref plugin, void *arg) {handlerton *ht = plugin_data<handlerton *>(plugin);xarecover_st *info = static_cast<struct xarecover_st *>(arg);int got;if (ht->state == SHOW_OPTION_YES && ht->recover) {while ((got = ht->recover(ht, info->list, info->len,Recovered_xa_transactions::instance().get_allocated_memroot())) >0) {// 从引擎层获取所有处于prepared状态的事务for (int i = 0; i < got; ++i) {auto &xa_trx = info->list[i];my_xid xid = xa_trx.id.get_my_xid();if (!xid) { // 处理外部XA事务::recover_one_external_trx(*info, *ht, xa_trx, external_stats);++info->found_foreign_xids;continue;}if (info->dry_run) {++info->found_my_xids;continue;}// 处理内部XA事务::recover_one_internal_trx(*info, *ht, xa_trx, xid, internal_stats);}if (got < info->len) break;}}return false;}
| 事务状态 | 处理方式 |
|---|---|
| committed/committed with one phase | commit |
| prepared in tc | set prepared in tc |
| not found/prepared in se/ rolled back | rollback |
xa prepare写binlog成功,设置undo状态为prepared in tc(engine层状态可能还未更新)
xa prepare写binlog未成功,回滚该事务
xa commit写binlog成功,提交该事务
xa commit写binlog未成功,处理方式同1,保持prepared in tc状态
xa rollback写binlog成功,回滚该事务
xa rollback写binlog未成功,处理方式同1,保持prepared in tc状态
xa commit one phase写binlog成功,提交该事务,否则回滚

PART 04
总结


MySQL 8.0.30通过新增一种undo状态,实现了crash safe的外部XA事务,读者有兴趣可自行阅读相关代码,加深理解。
-END-




