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
为例:
TRX_UNDO_ACTIVE
状态,事务将被回滚;TRX_UNDO_PREPARED
状态,将根据binlog的情况来决定是回滚还是提交事务。MySQL中的XA事务
分布式事务允许多个独立的事务资源参与到一个全局的事务中,全局事务要求所有参与的事务要么都提交,要么都不提交。XA是一套分布式事务规范,本文所说的XA事务是指基于XA协议的分布式事务。XA协议下,分布式事务通常由一个全局事务管理器,一个或多个局部资源管理器,以及一个应用程序组成:
MySQL的XA事务中,MySQL是资源管理器,事务管理器是连接MySQL的客户端。XA的协议主要描述了事务管理器与资源管理器之间的接口:
在MySQL中,常用的XA接口有:
XA协议采用两阶段提交的方式来保证全局事务的原子性,两阶段提交的过程如下:
MySQL支持多存储引擎,为了保证binlog以及各个存储引擎之间的一致性,MySQL引入了两阶段提交,每个事务都是XA事务。这些事务按照事务管理器(两阶段提交中的协调者)所在位置可分为外部XA事务和内部XA事务:
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 ),有如下的问题:
binlog prepare
↓ crash
InnoDB prepare
此时InnoDB中事务的状态还是active,下次启动的时候active状态的事务被直接回滚,造成binlog和InnoDB不一致,进而导致主从不一致。
InnoDB prepare
↓ crash
binlog prepare
在InnoDB prepare完成后立即crash,此时InnoDB中事务的状态是prepared,而binlog中还没有对应的日志(崩溃恢复的时候不会回滚已经处于prepared状态的外部XA事务),导致binlog和InnoDB不一致。
上面的bug链接可以看到更多相关的讨论,bug报告者也提出了一种解决方法(以XA prepare 为例):
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的顺序是:
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 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};
崩溃恢复可以概括为以下几个步骤:
enum_ha_recover_xa_state::PREPARED_IN_TC
(此处不考虑XA commit one phase的情况)。第三步的状态处理逻辑如下:
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种:
TRX_UNDO_PREPARED
,server层的状态是enum_ha_recover_xa_state::PREPARED_IN_TC
,该函数不做任何处理,事务的最终状态是enum_ha_recover_xa_state::PREPARED_IN_TC
。TRX_UNDO_PREPARED
,server层的状态是enum_ha_recover_xa_state::NOT_FOUND
,事务的最终状态被设置为enum_ha_recover_xa_state::PREPARED_IN_SE
。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状态):
PART 04
总结
MySQL 8.0.30通过新增一种undo状态,实现了crash safe的外部XA事务,读者有兴趣可自行阅读相关代码,加深理解。
-END-