PART 01
背景
InnoDB中undo段的状态
InnoDB如何安全地崩溃恢复主要通过undo log机制来保证。事务的undo日志存放在undo段中,一个事务可能拥有多个undo段,事务prepare时会将所有undo段头部的TRX_UNDO_STATE
字段修改为TRX_UNDO_PREPARED
,这个操作完成后(完成的标准是修改undo段状态的所有redo日志都已落盘),事务所有的修改都已经持久化,即使程序崩溃也不会丢失(不考虑硬件损坏等特殊情况)。
崩溃恢复的时候会将根据undo段的状态来决定事务的状态,以TRX_UNDO_ACTIVE
和TRX_UNDO_PREPARED
为例:
- undo段处于`TRX_UNDO_ACTIVE`状态,事务将被回滚;
- undo段处于`TRX_UNDO_PREPARED`状态,将根据binlog的情况来决定是回滚还是提交事务。
MySQL中的XA事务
分布式事务允许多个独立的事务资源参与到一个全局的事务中,全局事务要求所有参与的事务要么都提交,要么都不提交。XA是一套分布式事务规范,本文所说的XA事务是指基于XA协议的分布式事务。XA协议下,分布式事务通常由一个全局事务管理器,一个或多个局部资源管理器,以及一个应用程序组成:
- 应用程序(AP):定义事务边界,并指定构成事务的操作
- 资源管理器(RM):提供对共享资源的访问
- 事务管理器(TM):为事务分配唯一标识符,监视其进度,并负责事务的提交,回滚和故障恢复
MySQL的XA事务中,MySQL是资源管理器,事务管理器是连接MySQL的客户端。XA的协议主要描述了事务管理器与资源管理器之间的接口:
在MySQL中,常用的XA接口有:
- XA START,负责开启或者恢复一个XA事务,将事物状态设置为ACTIVE
- XA END,将事务状态设置为IDLE状态,可通过XA START进行恢复
- XA PREPARE,通知资源管理器,准备提交事务,将事务设置为PREPARED状态
- XA COMMIT,通知资源管理器,提交XA事务
- XA ROLLBACK,通知资源管理器,回滚XA事务
XA协议采用两阶段提交的方式来保证全局事务的原子性,两阶段提交的过程如下:
1. 第一阶段,事务管理器发起PREPARE请求,询问所有资源管理器是否可以提交事务,资源管理器根据自身状态回复YES或者NO,在回复YES前,资源管理器会将事务持久化并设置为PREPARED状态
2. 第二阶段,事务管理器根据前一阶段的结果来决定是提交还是回滚事务,如果所有节点均返回YES,那么通知所有节点提交事务,否则通知所有节点回滚事务
MySQL支持多存储引擎,为了保证binlog以及各个存储引擎之间的一致性,MySQL引入了两阶段提交,每个事务都是XA事务。这些事务按照事务管理器(两阶段提交中的协调者)所在位置可分为外部XA事务和内部XA事务:
- 内部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相关问题
在MySQL 8.0.30前,外部XA事务的XA prepare操作的处理顺序是:
binlog prepare
↓
InnoDB prepare
其中binlog prepare阶段会将XA prepare语句写入binlog,然后再将InnoDB中XA事务的状态设置为prepared,这个过程不是crash safe的(已知bug:https://bugs.mysql.com/bug.php?id=88534 ),有如下的问题:
1. 写完XA prepare的binlog后立即crash
binlog prepare
↓ crash
InnoDB prepare
此时InnoDB中事务的状态还是active,下次启动的时候active状态的事务被直接回滚,造成binlog和InnoDB不一致,进而导致主从不一致。
1. 如果交换binlog和InnoDB的prepare顺序
InnoDB prepare
↓ crash
binlog prepare
在InnoDB prepare完成后立即crash,此时InnoDB中事务的状态是prepared,而binlog中还没有对应的日志(崩溃恢复的时候不会回滚已经处于prepared状态的外部XA事务),导致binlog和InnoDB不一致。
上面的bug链接可以看到更多相关的讨论,bug报告者也提出了一种解决方法(以XA prepare 为例):
1. XA prepare的顺序:InnoDB prepare,binlog prepare
2. 仿照Previous\_gtid\_log\_event,在binlog中新增一个event,用于记录已经处于prepared状态的XA事务的xid
3. 崩溃恢复过程中,根据binlog中记录的xid来决定是回滚还是保留InnoDB中处于prepared状态的外部XA事务
MySQL社区在8.0.30中解决了这个问题,相关提交参考:https://github.com/mysql/mysql-server/commit/c1401ad ,社区的解决方法略有不同,让我们以XA prepare为例,一起来看下社区是如何解决这个问题的。
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顺序
在MySQL-8.0.30中,XA prepare的顺序是:
1. binlog prepare
注意,binlog prepare不再写binlog:
-
InnoDB prepare
设置事务为prepared状态(TRX_UNDO_PREPARED),保证crash后事务能正常恢复 -
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 TC trx_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 found PREPARED_IN_SE = 0, // Transaction is prepared in SEs PREPARED_IN_TC = 1, // Transaction is prepared in SEs and TC COMMITTED_WITH_ONEPHASE = 2, // Transaction was one-phase committed COMMITTED = 3, // Transaction was committed ROLLEDBACK = 4 // Transaction was rolled back};
崩溃恢复可以概括为以下几个步骤:
1. 扫描最后一个binlog,如果遇到了XA\_prepare\_log\_event,会将该event对应的xid保存起来,并设置状态为`enum_ha_recover_xa_state::PREPARED_IN_TC`(此处不考虑XA commit one phase的情况)。
2. 扫描完成后,将刚刚保存的外部XA事务的xid以及对应的状态传入InnoDB。
3. InnoDB根据传入的XA事务的状态以及InnoDB内部事务的undo状态修改或设置某些事务的状态。
4. 根据事务的状态对事务进行处理(比如回滚)。
第三步的状态处理逻辑如下:
-
-
-
-
-
-
-
-
-
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); }}
这里只考虑在InnoDB中已经处于prepared状态的事务,对于active状态的事务是直接回滚掉。修改事务最终状态的代码为:
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
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;}
该函数实际上是处理一些特殊情况,这里我们介绍常见的3种:
1. 一个事务在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`。
2. 一个事务在XA prepare时,还未来得及写binlog实例就崩溃,此时InnoDB中undo的状态是`TRX_UNDO_PREPARED`,server层的状态是`enum_ha_recover_xa_state::NOT_FOUND`,事务的最终状态被设置为`enum_ha_recover_xa_state::PREPARED_IN_SE`。
3. 事务的XA prepare顺利完成,该函数不做任何处理,事务的最终状态保持`enum_ha_recover_xa_state::PREPARED_IN_TC`。
这里有一个特殊情况需要说明:如果一个事务在上一个binlog文件中已经完成了prepare但还未提交,当前binlog文件中并没有该事务的XA_prepare_log_event,此时函数中的previous_state
一定是enum_ha_recover_xa_state::NOT_FOUND
,而undo状态一定是TRX_UNDO_PREPARED_IN_TC
,因此该函数会添加xid添加到全局的xid中并设置状态为enum_ha_recover_xa_state::PREPARED_IN_TC
,这里的目的是防止在后面的步骤中该事务被回滚掉。
第三步完成后MySQL获得了足够的信息,可以进行崩溃恢复的最后一步,对未决事务进行处理,可以参考函数xa::recovery::recover_one_ht
,它的代码如下:
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
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;}
该函数从引擎层获取所有处于prepared状态的事务,根据该事务是外部XA还是内部XA调用不同的处理函数。对于外部XA事务,调用recover_one_external_trx进行处理,如何处理与前面设置的事务状态有关:
事务状态 | 处理方式 |
---|---|
committed/committed with one phase | commit |
prepared in tc | set prepared in tc |
not found/prepared in se/ rolled back | rollback |
概括为如下几种情况(以下几种情况中,事务在引擎层处于prepared状态):
1. xa prepare写binlog成功,设置undo状态为prepared in tc(engine层状态可能还未更新)
2. xa prepare写binlog未成功,回滚该事务
3. xa commit写binlog成功,提交该事务
4. xa commit写binlog未成功,处理方式同1,保持prepared in tc状态
5. xa rollback写binlog成功,回滚该事务
6. xa rollback写binlog未成功,处理方式同1,保持prepared in tc状态
7. xa commit one phase写binlog成功,提交该事务,否则回滚
PART 04
总结
MySQL 8.0.30通过新增一种undo状态,实现了crash safe的外部XA事务,读者有兴趣可自行阅读相关代码,加深理解。
-END-