前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >MySQL 外部XA事务怎么安全恢复?

MySQL 外部XA事务怎么安全恢复?

作者头像
腾讯数据库技术
发布2023-04-03 16:40:07
1.5K0
发布2023-04-03 16:40:07
举报

PART 01

背景

InnoDB中undo段的状态

InnoDB如何安全地崩溃恢复主要通过undo log机制来保证。事务的undo日志存放在undo段中,一个事务可能拥有多个undo段,事务prepare时会将所有undo段头部的TRX_UNDO_STATE字段修改为TRX_UNDO_PREPARED,这个操作完成后(完成的标准是修改undo段状态的所有redo日志都已落盘),事务所有的修改都已经持久化,即使程序崩溃也不会丢失(不考虑硬件损坏等特殊情况)。

崩溃恢复的时候会将根据undo段的状态来决定事务的状态,以TRX_UNDO_ACTIVETRX_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

代码语言:javascript
复制
/** 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:

2. InnoDB prepare 设置事务为prepared状态(TRX_UNDO_PREPARED),保证crash后事务能正常恢复

3. binlog commit 在commit阶段写入xa prepare对应的binlog并将InnoDB中事务的状态设置为TRX_UNDO_PREPARED_IN_TC(表示XA?prepare的日志已经写入到binlog中)

代码语言:javascript
复制
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事务的状态可以是:

代码语言:javascript
复制
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. 根据事务的状态对事务进行处理(比如回滚)。

第三步的状态处理逻辑如下:

代码语言:javascript
复制
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状态的事务是直接回滚掉。修改事务最终状态的代码为:

代码语言:javascript
复制
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,它的代码如下:

代码语言:javascript
复制
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-

本文参与?腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2023-02-28,如有侵权请联系?cloudcommunity@tencent.com 删除

本文分享自 腾讯数据库技术 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与?腾讯云自媒体分享计划? ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
云数据库 MySQL
腾讯云数据库 MySQL(TencentDB for MySQL)为用户提供安全可靠,性能卓越、易于维护的企业级云数据库服务。其具备6大企业级特性,包括企业级定制内核、企业级高可用、企业级高可靠、企业级安全、企业级扩展以及企业级智能运维。通过使用腾讯云数据库 MySQL,可实现分钟级别的数据库部署、弹性扩展以及全自动化的运维管理,不仅经济实惠,而且稳定可靠,易于运维。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
http://www.vxiaotou.com