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
↓ crash
InnoDB prepare
此时InnoDB中事务的状态还是active,下次启动的时候active状态的事务被直接回滚,造成binlog和InnoDB不一致,进而导致主从不一致。
如果交换binlog和InnoDB的prepare顺序
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 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
};
扫描最后一个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-