最新消息:首页公告!

一文带你深度解析 MySQL 8.0 事务提交原理

浏览 共有条评论 关键词:MySQL,事务提交
后台-系统设置-扩展变量-手机广告位-手机广告位-内容广告位一

1. 概述

MySQL 是一个插件式、支持多存储引擎架构的数据库。一方面,MySQL 支持一个事务跨多个引擎进行读写,使得数据库系统具备良好的可扩展性和灵活性;另一方面,MySQL 也支持一个事务跨多节点进行读写,通过分布式节点架构使 MySQL 消除了单点故障,提高数据库系统的可靠性和可用性。

然而,当多个引擎/节点同时访问和修改数据时,如何保证数据在各个引擎/节点之间的一致性成为了一项挑战。

本文将深入探讨 MYSQL 集群在保持数据一致性的解决方案。MySQL 集群通过 XA 事务(X/Open Distributed Transaction Processing Model,简称 X/Open DTP Model)解决了此问题。XA 事务分为内部 XA 和外部 XA 事务,本文将聚焦内部 XA 的源码实现。

 

2. XA 事务

XA 事务定义了三个参与角色(APP、TM、RM),并通过两个阶段实现分布式事务。

一文带你深度解析 MySQL 8.0 事务提交原理

图 2.1  XA 事务模型

·      XA 事务中的三个参与角色分别是:

APP(Application Program,简称 APP):应用程序,定义事务的开始和结束。

TM(Transaction Manager,简称 TM): 事务管理器,充当事务的协调者,监控事务的执行进度,负责事务的提交、回滚等。

RM(Resource Manager,简称 RM): 资源管理器,充当事务的参与者,如数据库、文件系统,提供访问资源的方式。

·      实现分布式事务的两个阶段:

阶段一: TM 向所有的 RM 发出 PREPARE 指令,RM 进行完成提交前的准备工作,并刷新相关操作日志,此时不会进行事务提交。如果在 PREPARE 指令下发过程中某一 RM 节点失败,则回滚事务,TM 向所有 RM 节点下发 ROLLBACK 指令,防止数据不一致的情况发生。

阶段二: 如果 TM 收到所有 RM 的成功消息,则 TM 向 RM 发出 COMMIT 指令,RM 向 TM 返回提交成功的消息后,TM 确认整个事务完成。如果任意一个 RM 节点 COMMIT 失败,则 TM 尝试重新下发 COMMIT 指令,尝试失败到上限次数后将返回报错,整个事务失败。

在单实例节点中,当 Server 层作为 TM,多个存储引擎作为 RM,就会产生内部 XA 事务,MySQL 利用内部事务保证了多个存储引擎的一致性。外部 XA 事务一般是针对跨多 MySQL 实例的分布式事务,因此,外部 XA 的协调者是用户的应用,参与者是 MySQL 节点。

外部 XA 事务与内部 XA 事务核心逻辑类似,同时给用户提供了一套 XA 事务的操作命令,包括 XA start,XA end,XA prepare 和 XA commit 等。

 

3. 内部 XA 事务

在单个 MYSQL 实例中,使用内部 XA 事务来解决 Server 层 Binlog 日志和 Storage 层事务日志的一致性等问题。其中,Server 层作为事务协调器,而多个存储引擎作为事务参与者。

 

3.1 协调者对象 tc_log

MySQL 启动时,包含了事务协调者的选择。如果开启了 Binlog,并且存在事务引擎,则 XA 协调器为 mysql_bin_log 对象,使用 Binlog 物理文件记录事务状态;如果关闭了 Binlog,且存在不少于 2 个事务引擎,则 XA 协调器为 tc_log_mmap 对象,使用内存结构来记录事务状态;其他情况(没有事务引擎),则不需要 XA,tc_log 设置为 tc_log_dummy 对象。

无论 tc_log_dummy 还是 mysql_bin_log 或 tc_log_mmap 都基于 TC_LOG 这个基类来实现的。TC_LOG 是一个全局指针,作为事务提交的协调器,实现了事务的 prepare,commit,rollback 等接口。

图 3.1 TC_LOG 类关系图

mysql_bin_log,tc_log_mmap 和 tc_log_dummy 作为协调者的基本逻辑如下:

mysql_bin_log作为协调者:
prepare:ha_prepare_low
commit:write-binlog + ha_comit_low
tc_log_mmap作为协调者:
prepare:ha_prepare_low
commit:wrtie-xid + ha_commit_low
tc_log_dummy作为协调者:
prepare:ha_prepare_low
commit:ha_commit_low
 

其中 tc_log_dummy 不会记录事务日志,只是做简单的转发,将 Server 层的调用路由到 Storage 层调用。tc_log_mmap 是一个标准的事务协调者实现,它会创建一个名为 tc.log 的日志并使用操作系统的内存映射(memory-map,mmap)机制将内容映射到内存中,tc.log 文件中分为一个一个 PAGE,每个 PAGE 上有多个 XID(X/Open transaction IDentifier,全局事务唯一 ID)。Binlog 同样基于 TC_LOG 来实现事务协调者功能,会递增生成 mysql-binlog.xxxx 的文件,每个文件中包含多个事务产生的 Binlog event,并在 Binlog event 中包含 XID。tc_log_mmap 和 Binlog 都基于 XID 来确定事务是否已提交。

本文主要关注于如何通过内部 XA 保证 Binlog 和 Redo log 的一致性,即以 Binlog 作为协调器的场景,这里的 Binlog 既是协调者也是参与者。

 

3.2 事务提交过程

如图 3.2 为一个事务的执行过程,当客户端发出 COMMIT 指令时,MYSQL 内部将通过 Prepare 和 Commit 两个阶段完成事务的提交。

图 3.2 事务提交过程

Prepare 阶段,事务的 Undo log 设置为 prepare 状态,写 Prepare Log(Prepare 阶段产生的 Redo Log),将事务状态设为 TRX_PREPARED,写 XID(事务 ID 号)到 Redo Log,同时把 Redo Log 刷新到磁盘中。

Commit 阶段,Binlog 写入文件并刷盘,同时也会把 XID 写入到 Binlog。调用引擎的 Commit 完成事务的提交,同时会对事务的 Undo log 从 prepare 状态设置为提交状态(可清理状态),写 Commit Log(Commit 阶段产生的 Redo log),释放锁、read view 等,最后将事务状态设置为 TRX_NOT_STARTED 状态。

两阶段提交保证了事务在多个引擎之间的原子性,以 Binlog 写入成功作为事务提交的标志。

在崩溃恢复中,是以 Binlog 中的 XID 和 Redo log 中的 XID 进行比较,XID 在 Binlog 里存在则提交,不存在则回滚。我们来看崩溃恢复时具体的情况:

情况一:写入 Redo log 后,处于 Prepare 状态的时候崩溃了,此时:

由于 Binlog 还没写,Redo log 处于 Prepare 状态还没提交,所以崩溃恢复的时候,这个事务会回滚,此时 Binlog 还没写,所以也不会传到备库。

情况二:假设写完 Binlog 之后崩溃了,此时:

Redo log 中的日志是不完整的,处于 Prepare 状态,还没有提交,那么恢复的时候,首先检查 Binlog 中的事务是否完整(事务 XID 在 Binlog 里中存在,标志该事务已经完成),如果事务完整,则直接提交事务,否则回滚事务。

情况三:假设 Redo log 处于 Commit 状态的时候崩溃了,如果 Binlog 中的事务完整,那么会重新写入 Commit 标志,并完成提交,否则回滚事务。由此可见,两阶段提交能够确保数据的一致性。

一般常用的 SQL 语句都是通过公共接口 mysql_execute_command 来执行,我们来分析该接口执行的流程:

mysql_execute_command
{
   switch (command)
   {
        case SQLCOM_COMMIT
                trans_commit();
                break;
   }
   if thd->is_error()  //语句执行报错
     trans_rollback_stmt(thd);
  else
trans_commit_stmt(thd); 
}

MySQL 的 Server 层有两个提交函数 trans_commit_stmt()和 trans_commit()。前者在每个语句执行完成时调用,一般标记语句的结束。而后者是在整个事务真正提交的时候调用,一般对应显示执行 COMMIT 语句,或开启一个新事务 BEGIN/START TRANSCATION,或执行一条非临时表的 DDL 语句等场景。

 

3.3 多语句事务提交

多语句事务提交一般指 BEGIN/COMMIT 显示事务,主要逻辑在 trans_commit()中,以下是具体实现:

 
// mysql层进行的事务提交
int ha_commit_trans(THD *thd, bool all, bool ignore_global_read_lock) {
  Transaction_ctx *trn_ctx = thd->get_transaction();
  // all为true,意味着当前是事务级提交范围,否则是语句级提交范围
  Transaction_ctx::enum_trx_scope trx_scope = all ? Transaction_ctx::SESSION : Transaction_ctx::STMT ;
  // 获得注册在当前事务的引擎列表,在trans_register_ha()中初始化
  Ha_trx_info *ha_info = trn_ctx->ha_trx_info(trx_scope);
  // 当前注册的可读可写存储引擎的数量,只有事务引擎支持读写
    uint rw_ha_count = 0;
    // 检查是否可以跳过两阶段提交机制
    rw_ha_count = ha_check_and_coalesce_trx_read_only(thd, ha_info, all);
    trn_ctx->set_rw_ha_count(trx_scope, rw_ha_count);
  // Prepare 阶段
  if (!trn_ctx->no_2pc(trx_scope) && (trn_ctx->rw_ha_count(trx_scope) > 1))
    error = tc_log->prepare(thd, all);
  }
  // Commit 阶段
 if (error || (error = tc_log->commit(thd, all))) {
    ha_rollback_trans(thd, all);
    goto end;
  }
}

 

协调者如何确认是否走 2PC(两阶段提交)逻辑?

这里主要根据事务修改是否涉及多个引擎来决定,即函数 ha_check_and_coalesce_trx_read_only()。特殊的是,如果打开 Binlog,Binlog 也会作为参与者而被考虑在内,最终协调者会统计事务中涉及修改的参与者数量。如果数量超过 1 个,则进行 2PC 提交流程。

当满足以上条件,进入 Prepare 阶段,调用 Binlog 协调器的 prepare 接口。Prepare 阶段,Binlog Prepare 接口没什么可做,而 InnoDB Prepare 接口主要做的事情就是修改事务和 Undo 段的状态,以及记录 XID。

InnoDB Prepare 接口会把内存中事务对象的状态修改为 TRX_STATE_PREPARED,并将事务对应 Undo 段在内存中的对象状态修改为 TRX_UNDO_PREPARED。然后,把 XID 信息写入当前事务对应日志组的 Undo Log Header 中的 XID 区域。修改 TRX_UNDO_STATE 字段值和写入 XID,这两个操作都要修改 Undo 页。修改 Undo 页之前,会先记录相应的 Redo 日志。最后,刷事务更新产生的 Redo 日志。

// innodb prepare,innodb层事务准备阶段
static void trx_prepare(trx_t *trx) /*!< in/out: transaction */
{
  lsn_t lsn = 0;
  // 对于系统和undo表空间回滚段,如果有更新需要持久化到redo中
  if (trx->rsegs.m_redo.rseg != nullptr && trx_is_redo_rseg_updated(trx)) {
    // lsn = mtr.commit_lsn(); 开启第一个mtr,并返回写入redo log buffer后的最新位点,提交时刻对应的lsn
    lsn = trx_prepare_low(trx, &trx->rsegs.m_redo, false);
  }
  // 对于临时表空间回滚段,如果有更新不需要持久化到redo中
  if (trx->rsegs.m_noredo.rseg != nullptr && trx_is_temp_rseg_updated(trx)) {
    trx_prepare_low(trx, &trx->rsegs.m_noredo, true);
  }
  // 更新事务和事务系统状态信息
  trx->state = TRX_STATE_PREPARED;
  trx_sys->n_prepared_trx++;
  // 释放RC及以下隔离级别的GAP lock
  if (trx->isolation_level <= TRX_ISO_READ_COMMITTED) {
    trx->skip_lock_inheritance = true;
    lock_trx_release_read_locks(trx, true);
  }
  switch (thd_requested_durability(trx->mysql_thd)) {
    // thd初始化时默认设置为HA_REGULAR_DURABILITY
case HA_REGULAR_DURABILITY:
   trx->ddl_must_flush = false;
      // redolog刷新
      trx_flush_log_if_needed(lsn, trx);
  }
}
 

 

紧接着进入 2PC 的 Commit 阶段,trans_commit()调用 binlog 协调器的 MYSQL_BIN_LOG::Commit()接口,功能集中在 MYSQL_BIN_LOG::ordered_commit()函数中。到了 Commit 阶段,一个事务就已经接近尾声了。写操作(包括增、删、改)已经完成,内存中的事务状态已经修改,Undo 状态也已经修改,XID 信息也已经写入 Undo Log Header,Prepare 阶段产生的 Redo 日志已经写入到 Redo 日志文件。剩余的收尾工作,包括 Redo 日志刷盘、事务的 Binlog 日志从临时存放点拷贝到 Binlog 日志文件、Binlog 日志文件刷盘以及 InnoDB 事务提交。

 
// tc_log->commit ==> MYSQL_BIN_LOG::commit()
MYSQL_BIN_LOG::commit()
//  这个函数很重要,它包含了binlog组提交三步曲,
int MYSQL_BIN_LOG::ordered_commit(THD *thd, bool all, bool skip_commit) {
    //1:Flush Stag:按照事务提交的顺序,先刷Redo log到磁盘,然后把每个事务产生的 binlog 日志从临时存放点拷贝到 binlog 日志文件缓存中
    flush_error = process_flush_stage_queue(&total_bytes, &do_rotate, &wait_queue); 
    //2: Sync Stage: binlog 日志刷盘之前会进入等待过程,目的是为了攒到更多的binlog日志后,合并IO单次刷盘
sync_binlog_file(false);//binlog fsync to disk
    //3: Commit Stage: 各线程按序提交事务    process_commit_stage_queue(thd, commit_queue);
 }

 

Redo Binlog 日志刷盘都涉及到磁盘 IO。如果每提交一个事务,都把该事务中的 Redo 日志、Binlog 日志刷盘,那么就会涉及到很多小数据量的 IO 操作,但是频繁的小数量 IO 操作非常消耗磁盘的读写性能。

为了提高磁盘 IO 效率并进一步提升事务的提交效率,MySQL 从 5.6 开始引入了 Binlog 日志组提交功能。该功能将事务的 Commit 阶段细分为 3 个子阶段。对于每个子阶段,都可以有多个事务同时处于该子阶段,写日志和刷盘操作可以合并。

·      Flush 子阶段,先将 Redo 日志刷盘,接着将所有的 binlog caches 写入到 binlog 文件缓存中。

·      Sync 子阶段,对 binlog 文件缓存做 fsync 操作,多个线程的 binlog 合并为一次刷盘。

·      Commit 子阶段,依次将 redolog 中已经 prepare 的事务在引擎层提交,commit 阶段不用刷盘,因为 flush 阶段中的 redolog 刷盘已经足够保证数据库崩溃时的数据安全了。当前 Commit 子阶段主要包含了 InnoDB 层的事务提交,真正执行事务提交入口函数为 trx_commit_low()。trx_commit_low()主要分成两个部分 trx_write_serialisation_history()和 trx_commit_in_memory()。trx_write_serialisation_history()处理整个事务执行过程中所使用 insert/update 的回滚段的收尾工作。trx_commit_in_memory()在内存中设置事务提交的标志 trx->state = TRX_STATE_COMMITTED_IN_MEMORY,本事务的数据可以即刻被其他事务可见;在设置事务提交已经完成的标志后,才会释放当前事务的 Read View 和事务过程中所持有的 table lock 和 record lock,清除 trx_sys 系统中的当前事务等。

 

3.4 单语句事务提交

从 SQL 的执行过程分析可以看到,无论执行何种语句,最后都会执行 trans_commit_stmt(),即单语句提交函数。如果当前是单语句事务,一般指 AUTOCOMMIT 为 ON 的场景,那么会走事务提交逻辑,即 ha_commit_trans()函数。额外考虑到 COMMIT 和 DDL 语句等已经在调用 trans_commit_stmt()之前将事务提交,所以在这里只需要标记语句结束即可。

// 执行单语句事务提
bool trans_commit_stmt(THD *thd, bool ignore_global_read_lock) {
  int res = false;
  // 单语句事务,需要走2PC提交逻辑
  if (thd->get_transaction()->is_active(Transaction_ctx::STMT)) {
    res = ha_commit_trans(thd, false, ignore_global_read_lock);
  } else if (tc_log)
    // COMMIT/DDL等,只需要走引擎层提交逻辑,置为false,只标识语句结束,跳过真正提交阶段
    res = tc_log->commit(thd, false);
  thd->get_transaction()->reset(Transaction_ctx::STMT);
  return res;
}

 

ha_commit_trans()最后会走到 innobase_commit()中,innobase_commit()中的参数 commit_trx 控制是否真的进行存储引擎层的提交处理,trans_commit_stmt()里会设置 commit_trx 为 0,允许跳过事务提交。

这里的判断逻辑是,只有当 commit_trx= 1 或者设置 autocommit=1 的情况下,才会真正进入事务提交逻辑。而多语句事务对应的 trans_commit()函数里会设置 commit_trx=1,进入 innobase_commit_low()执行真正的事务提交逻辑。

 
/** 在innodb层提交一个事务
thd:需要提交事务的会话
commit_trx:true,需要提交事务。false,跳过事务提交。 
 */
static int innobase_commit(handlerton *hton, THD *thd, bool commit_trx) 
{
  trx_t *trx = check_trx_exists(thd);
  // innobase_commi仅在“真正的”commit时被调用,而且在每个语句之后(走trans_commit_stmt()函数)也被调用,因此这里需要will_commit判断是否要真正去提交事务。
  bool will_commit =
      commit_trx ||
      (!thd_test_options(thd, OPTION_NOT_AUTOCOMMIT | OPTION_BEGIN)); // autocommit=1且不在显示事务块中
  if (will_commit) {
    /* 在显示提交commit,或者autocommit=1、且不在显示事务块内*/
    innobase_commit_low(trx);
  } else {
    /* 其他情况,我们只是标记SQL语句结束,不做事务提交 */
    trx_mark_sql_stat_end(trx);
  }
  return 0;
}

4. 总结

本文从多语句/单语句事务提交原理角度出发,介绍了 MySQL 的两阶段提交协议。在 prepare 阶段,InnoDB 把数据更新到内存后记录 Redo log,此时 Redo log 的状态为 prepare 状态;在 Commit 阶段,Server 生成 Binlog 后落盘,InnoDB 把刚写入的 Redo log 状态更新为 commit 状态。两阶段提交保证了事务在多个引擎和 Binlog 之间的原子性,同样保证了通过备份和 Binlog 恢复出的数据库和原数据库的数据一致性。

转载请注明:可思数据 » 一文带你深度解析 MySQL 8.0 事务提交原理

后台-系统设置-扩展变量-手机广告位-手机广告位-内容广告位二
留言与评论(共有 条评论)
昵称:
匿名发表 登录账号
                 
   
验证码:
后台-系统设置-扩展变量-手机广告位-手机广告位-内容广告位三