第15节:MySQL层事务提交流程简析

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 本节包含一个笔记如下:https://www.jianshu.com/writer#/notebooks/37013486/notes/50142567本节将来解释一下MySQL层详细的提交流程,但是由于能力有限,这里不可能包含全部的步骤,只是包含了一些重要的并且我学习过的步骤。

本节包含一个笔记如下:
https://www.jianshu.com/writer#/notebooks/37013486/notes/50142567


本节将来解释一下MySQL层详细的提交流程,但是由于能力有限,这里不可能包含全部的步骤,只是包含了一些重要的并且我学习过的步骤。我们首先需要来假设参数设置,因为某些参数的设置会直接影响到提交流程,我们也会逐一解释这些参数的含义。本节介绍的大部分内容都集中在函数MYSQL_BIN_LOG::prepare和MYSQL_BIN_LOG::ordered_commit之中。

image.png

一、参数设置

本部分假定参数设置为:

  • binlog_group_commit_sync_delay:0
  • binlog_group_commit_sync_no_delay_count:0
  • binlog_order_commits:ON
  • sync_binlog:1
  • binlog_transaction_dependency_tracking:COMMIT_ORDER

关于参数binlog_transaction_dependency_tracking需要重点说明一下。我们知道Innodb的行锁是在语句运行期间就已经获取,因此如果多个事务同时进入了提交流程(prepare阶段),在Innodb层提交释放Innodb行锁资源之前各个事务之间肯定是没有行冲突的,因此可以在从库端并行执行。在基于COMMIT_ORDER 的并行复制中,last commit和seq number正是基于这种思想生成的,如果last commit相同则视为可以在从库并行回放,在19节我们将解释从库判定并行回放的规则。而在基于WRITESET的并行复制中,last commit将会在WRITESET的影响下继续降低,来使从库获得更好的并行回放效果,但是它也是COMMIT_ORDER为基础的,这个下一节将讨论。我们这节只讨论基于COMMIT_ORDER 的并行复制中last commit和seq number的生成方式。

而sync_binlog参数则有两个功能:

  • sync_binlog=0:binary log不sync刷盘,依赖于OS刷盘机制。同时会在flush阶段后通知DUMP线程发送Event。
  • sync_binlog=1:binary log每次sync队列形成后都进行sync刷盘,约等于每次group commit进行刷盘。同时会在sync阶段后通知DUMP线程发送Event。注意sync_binlog非1的设置可能导致从库比主库多事务。
  • sync_binlog>1:binary log将在指定次sync队列形成后进行sync刷盘,约等于指定次group commit后刷盘。同时会在flush阶段后通知DUMP线程发送Event。

第二功能将在第17节还会进行介绍。

二、总体流程图

这里我们先展示整个流程,如下(图15-1,高清原图包含在文末原图中):

15-1.png


三、步骤解析第一阶段(图中蓝色部分)

注意:在第1步之前会有一个获取MDL_key::COMMIT锁的操作,因此FTWRL将会堵塞‘commit’操作,堵塞状态为‘Waiting for commit lock’,这个可以参考FTWRL调用的函数make_global_read_lock_block_commit。

(1.) binlog准备。将上一次COMMIT队列中最大的seq number写入到本次事务的last_commit中。可参考binlog_prepare函数。

(2.) Innodb准备。更改事务的状态为准备并且将事务的状态和XID写入到Undo中。可参考trx_prepare函数。

(3.) XID_EVENT生成并且写到binlog cache中。在第10节中我们说过实际上XID来自于query_id,早就生成了,这里只是生成Event而已。可参考MYSQL_BIN_LOG::commit函数。


四、步骤解析第二阶段(图中粉色部分)

(4.) 形成FLUSH队列。这一步正在不断的有事务加入到这个FLUSH队列。第一个进入FLUSH队列的为本阶段的leader,非leader线程将会堵塞,直到COMMIT阶段后由leader线程的唤醒。

(5.) 获取LOCK log 锁。

(6.) 这一步就是将FLUSH阶段的队列取出来准备进行处理。也就是这个时候本FLUSH队列就不能在更改了。可参考stage_manager.fetch_queue_for函数。

(7.) 这里事务会进行Innodb层的redo持久化,并且会帮助其他事务进行redo的持久化。可以参考MYSQL_BIN_LOG::process_flush_stage_queue函数。下面是注释和一小段代码:

  /*
    We flush prepared records of transactions to the log of storage
    engine (for example, InnoDB redo log) in a group right before
    flushing them to binary log.
  */
  ha_flush_logs(NULL, true);//做innodb redo持久化

(8.) 生成GTID和seq number,并且连同前面的last commit生成GTID_EVENT,然后直接写入到binary log中。我们注意到这里直接写入到了binary log而没有写入到binlog cache,因此GTID_EVENT是事务的第一个Event。参考函数binlog_cache_data::flush中下面一段:

trn_ctx->sequence_number= mysql_bin_log.m_dependency_tracker.step(); 
//int64 state +1
...
    if (!error)
      if ((error= mysql_bin_log.write_gtid(thd, this, &writer)))
//生成GTID 写入binary log文件
        thd->commit_error= THD::CE_FLUSH_ERROR;
    if (!error)
      error= mysql_bin_log.write_cache(thd, this, &writer);
//将其他Event写入到binary log文件

而对于seq number和last commit的取值来讲,实际上在MySQL内部维护着一个全局的结构Transaction_dependency_tracker。其中包含三种可能取值方式,如下 :

  • Commit_order_trx_dependency_tracker
  • Writeset_trx_dependency_tracker
  • Writeset_session_trx_dependency_tracker

到底使用哪一种取值方式,由参数binlog_transaction_dependency_tracking来决定的。
这里我们先研究参数设置为COMMIT_ORDER 的取值方式,对于WRITESET取值的方式下一节专门讨论。

对于设置为COMMIT_ORDER会使用Commit_order_trx_dependency_tracker的取值方式,有如下特点:

特点
每次事务提交seq number将会加1。
last commit在前面的binlog准备阶段就赋值给了每个事务。这个前面已经描述了。
last commit是前一个COMMIT队列的最大seq number。这个我们后面能看到。

其次seq number和last commit这两个值类型都为Logical_clock,其中维护了一个叫做offsets偏移量的值,用来记录每次binary log切换时sequence_number的相对偏移量。因此seq number和last commit在每个binary log总是重新计数,下面是offset的源码注释:

  /*
    Offset is subtracted from the actual "absolute time" value at
    logging a replication event. That is the event holds logical
    timestamps in the "relative" format. They are meaningful only in
    the context of the current binlog.
    The member is updated (incremented) per binary log rotation.
  */
  int64 offset;

下面是我们计算seq number的方式,可以参考Commit_order_trx_dependency_tracker::get_dependency函数。

  sequence_number=
    trn_ctx->sequence_number - m_max_committed_transaction.get_offset(); 
//这里获取seq number

我们清楚的看到这里有一个减去offset的操作,这也是为什么我们的seq number和last commit在每个binary log总是重新计数的原因。

(9.) 这一步就会将我们的binlog cache里面的所有Event写入到我们的binary log中了。对于一个事务来讲,我们这里应该很清楚这里包含的Event有:

  • QUERY_EVENT
  • MAP_EVENT
  • DML EVENT
  • XID_EVENT

注意GTID_EVENT前面已经写入到的binary logfile。这里我说的写入是调用的Linux的write函数,正常情况下它会进入图中的OS CACHE中。实际上这个时候可能还没有真正写入到磁盘介质中。

重复 7 ~ 9步 把FLUSH队列中所有的事务做同样的处理。

注意:如果sync_binlog != 1 这里将会唤醒DUMP线程进行Event的发送。

(10.) 这一步还会判断binary log是否需要切换,并且设置一个切换标记。依据就是整个队列每个事务写入的Event总量加上现有的binary log大小是否超过了max_binlog_size。可参考MYSQL_BIN_LOG::process_flush_stage_queue函数,如下部分:

 if (total_bytes > 0 && my_b_tell(&log_file) >= (my_off_t) max_size)
    *rotate_var= true; //标记需要切换

但是注意这里是先将所有的Event写入binary log,然后才进行的判断。因此对于大事务来讲其Event肯定都包含在同一个binary log中。

到这里FLUSH阶段就结束了。


五、步骤解析第三阶段(图中紫色部分)

(11.) FLUSH队列加入到SYNC队列。第一个进入的FLUSH队列的leader为本阶段的leader。其他FLUSH队列加入SYNC队列,且其他FLUSH队列的leader会被LOCK sync堵塞,直到COMMIT阶段后由leader线程的唤醒。

(12.) 释放LOCK log。

(13.) 获取LOCK sync。

(14.) 这里根据参数delay的设置来决定是否等待一段时间。我们从图中我们可以看出如果delay的时间越久那么加入SYNC队列的时间就会越长,也就可能有更多的FLUSH队列加入进来,那么这个SYNC队列的事务就越多。这不仅会提高sync效率,并且增大了GROUP COMMIT组成员的数量(因为last commit还没有更改,时间拖得越长那么一组事务中事务数量就越多),从而提高了从库MTS的并行效率。但是缺点也很明显可能导致简单的DML语句时间拖长,因此不能设置过大,下面是我简书中的一个案列就是因为delay参数设置不当引起的,如下:
https://www.jianshu.com/p/bfd4a88307f2

参数delay一共包含两个参数如下:

  • binlog_group_commit_sync_delay:通过人为的设置delay时长来加大整个GROUP COMMIT组中事务数量,并且减少进行磁盘刷盘sync的次数,但是受到binlog_group_commit_sync_no_delay_count的限制。单位为1/1000000秒,最大值1000000也就是1秒。
  • binlog_group_commit_sync_no_delay_count:在delay的时间内如果GROUP COMMIT中的事务数量达到了这个设置就直接跳出等待,而不需要等待binlog_group_commit_sync_delay的时长。单位是事务的数量。

(15.) 这一步就是将SYNC阶段的队列取出来准备进行处理。也就是这个时候SYNC队列就不能再更改了。这个队列和FLUSH队列并不一样,事务的顺序一样但是数量可能不一样。

(16.) 根据sync_binlog的设置决定是否刷盘。可以参考函数MYSQL_BIN_LOG::sync_binlog_file,逻辑也很简单。

到这里SYNC阶段就结束了。

注意:如果sync_binlog = 1 这里将会唤醒DUMP线程进行Event的发送。


六、步骤解析第四阶段(图中黄色部分)

(17.) SYNC队列加入到COMMIT队列。第一个进入的SYNC队列的leader为本阶段的leader。其他SYNC队列加入COMMIT队列,且其他SYNC队列的leader会被LOCK commit堵塞,直到COMMIT阶段后由leader线程的唤醒。

(18.) 释放LOCK sync。

(19.) 获取LOCK commit。

(20.) 根据参数binlog_order_commits的设置来决定是否按照队列的顺序进行Innodb层的提交,如果binlog_order_commits=1 则按照队列顺序提交则事务的可见顺序和提交顺序一致。如果binlog_order_commits=0 则下面21步到23步将不会进行,也就是这里不会进行Innodb层的提交。

(21.) 这一步就是将COMMIT阶段的队列取出来准备进行处理。也就是这个时候COMMIT队列就不能在更改了。这个队列和FLUSH队列和SYNC队列并不一样,事务的顺序一样,数量可能不一样。

注意:如果rpl_semi_sync_master_wait_point参数设置为‘AFTER_SYNC’,这里将会进行ACK确认,可以看到实际的Innodb层提交操作还没有进行,等待期间状态为‘Waiting for semi-sync ACK from slave’。

(22.) 在Innodb层提交之前必须要更改last_commit了。COMMIT队列中每个事务都会去更新它,如果大于则更改,小于则不变。可参考Commit_order_trx_dependency_tracker::update_max_committed函数,下面是这一小段代码:

{
  m_max_committed_transaction.set_if_greater(sequence_number);
//如果更大则更改
}

(23.) COMMIT队列中每个事务按照顺序进行Innodb层的提交。可参考innobase_commit函数。

这一步Innodb层会做很多动作,比如:

  • Readview的更新
  • Undo的状态的更新
  • Innodb 锁资源的释放

完成这一步,实际上在Innodb层事务就可以见了。我曾经遇到过一个由于leader线程唤醒本组其他线程出现问题而导致整个commit操作hang住,但是在数据库中这些事务的修改已经可见的案例。

循环22~23直到COMMIT队列处理完。

注意:如果rpl_semi_sync_master_wait_point参数设置为‘AFTER_COMMIT’,这里将会进行ACK确认,可以看到实际的Innodb层提交操作已经完成了,等待期间状态为‘Waiting for semi-sync ACK from slave’。

(24.) 释放LOCK commit。

到这里COMMIT阶段就结束了。


七、步骤解析第五阶段(图中绿色部分)

(25.) 这里leader线程会唤醒所有的组内成员,各自进行各自的操作了。

(26.) 每个事务成员进行binlog cache的重置,清空cache释放临时文件。

(27.) 如果binlog_order_commits设置为0,COMMIT队列中的每个事务就各自进行Innodb层提交(不按照binary log中事务的的顺序)。

(28.) 根据前面第10步设置的切换标记,决定是否进行binary log切换。

(29.) 如果切换了binary log,则还需要根据expire_logs_days的设置判断是否进行binlog log的清理。


八、总结

  • 整个过程我们看到生成last commit和seq number的过程并没有其它的开销,但是下一节介绍的基于WRITESET的并行复制就有一定的开销了。
  • 我们需要明白的是FLUSH/SYNC/COMMIT每一个阶段都有一个相应的队列,每个队列并不一样。但是其中的事务顺序却是一样的,是否能够在从库进行并行回放完全取决于准备阶段获取的last_commit,这个我们将在第19节详细描述。
  • 对于FLUSH/SYNC/COMMIT三个队列事务的数量实际有这样关系,即COMMIT队列>=SYNC队列>=FLUSH队列。如果压力不大它们三者可能相同且都只包含一个事务。
  • 从流程中可以看出基于COMMIT_ORDER 的并行复制如果数据库压力不大的情况下可能出现每个队列都只有一个事务的情况。这种情况就不能在从库并行回放了,但是下一节我们讲的基于WRITESET的并行复制却可以改变这种情况。
  • 这里我们也更加明显的看到大事务的Event会在提交时刻一次性的写入到binary log。如果COMMIT队列中包含了大事务,那么必然堵塞本队列中的其它事务提交,后续的提交操作也不能完成。我认为这也是MySQL不适合大事务的一个重要原因。

第15节结束

相关实践学习
如何快速连接云数据库RDS MySQL
本场景介绍如何通过阿里云数据管理服务DMS快速连接云数据库RDS MySQL,然后进行数据表的CRUD操作。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助     相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
26天前
|
SQL 存储 缓存
MySQL执行流程
本文介绍了MySQL的执行流程,分为server层和引擎层。server层包含连接器、查询缓存、解析器、预处理器、优化器等组件,负责SQL的接收、解析、优化及执行;引擎层负责数据的存储与读取。文章详细解释了各组件的功能,如连接器负责用户身份认证,查询缓存提高查询效率,解析器进行SQL的词法和语法分析,预处理器验证表和字段的存在性,优化器选择最优执行计划,最终由查询执行引擎完成查询并将结果返回给客户端。
MySQL执行流程
|
4天前
|
SQL 关系型数据库 MySQL
MySQL事务日志-Undo Log工作原理分析
事务的持久性是交由Redo Log来保证,原子性则是交由Undo Log来保证。如果事务中的SQL执行到一半出现错误,需要把前面已经执行过的SQL撤销以达到原子性的目的,这个过程也叫做"回滚",所以Undo Log也叫回滚日志。
MySQL事务日志-Undo Log工作原理分析
|
13天前
|
SQL 安全 关系型数据库
【MySQL基础篇】事务(事务操作、事务四大特性、并发事务问题、事务隔离级别)
事务是MySQL中一组不可分割的操作集合,确保所有操作要么全部成功,要么全部失败。本文利用SQL演示并总结了事务操作、事务四大特性、并发事务问题、事务隔离级别。
【MySQL基础篇】事务(事务操作、事务四大特性、并发事务问题、事务隔离级别)
|
19天前
|
SQL 关系型数据库 MySQL
MySQL进阶突击系列(04)事务隔离级别、AICD、CAP、BASE原则一直搞不懂? | 看这篇就够了
本文详细介绍了数据库事务的四大特性(AICD原则),包括原子性、隔离性、一致性和持久性,并深入探讨了事务并发问题与隔离级别。同时,文章还讲解了分布式系统中的CAP理论及其不可能三角关系,以及BASE原则在分布式系统设计中的应用。通过具体案例和图解,帮助读者理解事务处理的核心概念和最佳实践,为应对相关技术面试提供了全面的知识准备。
|
3月前
|
存储 SQL 关系型数据库
MySQL的事务隔离级别
【10月更文挑战第17天】MySQL的事务隔离级别
137 43
|
2月前
|
关系型数据库 MySQL
mysql事务特性
原子性:一个事务内的操作统一成功或失败 一致性:事务前后的数据总量不变 隔离性:事务与事务之间相互不影响 持久性:事务一旦提交发生的改变不可逆
|
2月前
|
关系型数据库 MySQL 数据库
MySQL事务隔离级别及默认隔离级别的设置
在数据库系统中,事务隔离级别是一个关键的概念,它决定了事务在并发执行时如何相互隔离。MySQL提供了四种事务隔离级别,每种级别都解决了不同的并发问题。本文将详细介绍这些隔离级别以及MySQL的默认隔离级别。
|
2月前
|
存储 SQL NoSQL
|
15天前
|
存储 Oracle 关系型数据库
数据库传奇:MySQL创世之父的两千金My、Maria
《数据库传奇:MySQL创世之父的两千金My、Maria》介绍了MySQL的发展历程及其分支MariaDB。MySQL由Michael Widenius等人于1994年创建,现归Oracle所有,广泛应用于阿里巴巴、腾讯等企业。2009年,Widenius因担心Oracle收购影响MySQL的开源性,创建了MariaDB,提供额外功能和改进。维基百科、Google等已逐步替换为MariaDB,以确保更好的性能和社区支持。掌握MariaDB作为备用方案,对未来发展至关重要。
40 3
|
15天前
|
安全 关系型数据库 MySQL
MySQL崩溃保险箱:探秘Redo/Undo日志确保数据库安全无忧!
《MySQL崩溃保险箱:探秘Redo/Undo日志确保数据库安全无忧!》介绍了MySQL中的三种关键日志:二进制日志(Binary Log)、重做日志(Redo Log)和撤销日志(Undo Log)。这些日志确保了数据库的ACID特性,即原子性、一致性、隔离性和持久性。Redo Log记录数据页的物理修改,保证事务持久性;Undo Log记录事务的逆操作,支持回滚和多版本并发控制(MVCC)。文章还详细对比了InnoDB和MyISAM存储引擎在事务支持、锁定机制、并发性等方面的差异,强调了InnoDB在高并发和事务处理中的优势。通过这些机制,MySQL能够在事务执行、崩溃和恢复过程中保持
43 3

推荐镜像

更多