一、 一条 select 语句的执行过程

一条查询语句执行的过程,属于一条记录的过程,大致可以分为以下几个步骤:

  1. 建立客户端/服务器通信

    • 客户端发起的连接请求,通过MySQL连接器处理后,客户端将SQL查询语句发送到服务器。
  2. 查询解析

    • SQL解析器SQL解析器首先会对查询语句进行语法和词法分析,生成一个解析树(Parse Tree)。
    • 预处理器预处理器进一步检查解析树的合法性,包括表和列是否存在、名称是否正确等。
  3. 查询优化

    • 查询重写优化器可能会对解析树进行重写,例如将子查询转换为连接(JOIN),或者进行某些常见的SQL重写优化。
    • 选择执行计划优化器会生成多个执行计划,并选择其中的最优计划。这里会考虑索引的使用、表扫描的方式(全表扫描或索引扫描)等。
    • 成本估算:MySQL使用一种基于成本的优化算法,通过估算不同执行计划的代价,选择成本最低的计划。
  4. 查询执行

    • 存储引擎接口:MySQL的执行器根据优化器选择的执行计划,通过存储引擎接口调用具体的存储引擎(如InnoDB、MyISAM等)。
    • 存储引擎操作:存储引擎根据执行器的请求进行数据的读取、写入等操作。
  5. 结果返回

    • 结果集处理:执行器将存储引擎返回的数据进行处理,生成最终的结果集
    • 结果发送:最终的结果集通过网络传输返回给客户端。
  6. 缓存处理(可选)

    • 查询缓存:如果查询缓存开启且命中缓存,MySQL会直接从缓存中返回结果,而不经过上述大部分步骤。需要注意的是,MySQL 8.0 版本已移除查询缓存这一特性。

总结来说,MySQL一条查询语句的执行过程可以概括为:客户端发送SQL语句 -> 解析语句 -> 优化查询 -> 执行查询 -> 返回结果。每个步骤中都有许多细节和优化点,使得MySQL能够高效地处理各种查询请求。

引用小林图解的一张图:
1717220312018

二、一条 update 语句的执行过程

比如这条待执行的 update 语句:
UPDATE t_user SET name = 'kryiea' WHERE id = 10086;

待执行的语句执行过程:

  1. select 语句的那一套流程,update 语句也是同样会走一遍
  2. 查询到目标记录后,执行更新操作的同时会涉及对三种日志的改动,undolog、redolog、binlog

三、三种日志、MVCC、BufferPool 之间的相互配合

3.1 三种日志的主要作用

  • UndoLog 回滚日志: 是Innodb存储引擎层生成的日志,保证了事务中的原子性,主要用于事务回滚和MVCC。(撤销已经执行的修改,保证事务的原子性和一致性)
  • RedoLog 重做日志: 是Innodb存储引擎层生成的日志,保证了事务中的持久性,主要用于掉电等故障恢复。(重做已经提交的修改,保证事务的持久性)
  • BinLog 归档日志: 是 Server层 生成的日志,主要用于数据备份和主从复制。(记录和重放SQL语句,用于数据的复制和恢复)

⬇️辅助理解:
InnoDB 存储引擎的日志:

  • UndoLog 记录了此次事务 开始前 的数据状态,记录的是 更新 之前 的值
  • RedoLog 记录了此次事务 完成后 的数据状态,记录的是 更新 之后 的值

Server 层的日志:

  • BinLog 记录了完成一条更新操作后,Server 层还会生成一条 binlog,等之后事务提交的时候,会将该事物执行过程中产生的所有 binlog 统一写入 binlog文件

3.2 UndoLog 回滚日志

3.2.1 为什么需要 UndoLog

先了解隐式事务:
Innodb 引擎在执行一条增删改语句的时候,即使没有显式输入begin开启事务commit提交事务,也会自动隐式开启事务
而且执行一条 update 语句是否自动提交事务,是由 autocommit 参数决定,默认开启。

试想以下场景:
在一个事务在执行过程中,在还没有提交事务之前,如果 MySQL 发生了崩溃,要怎么回滚到事务之前的数据呢?

如何解决:
如果每次在事务执行过程中,都记录下回滚时需要的信息到一个日志(undolog)里,那么在事务执行中途发生了 MySQL 崩溃后,就不用担心无法回滚到事务之前的数据,我们可以通过这个日志回滚到事务之前的数据。

3.2.2 认识 Undolog 机制

在事务没提交之前,MySQL 会先记录更新前的数据到 undolog日志文件,当事务需要回滚时,可以利用 undolog 来进行回滚。

过程如下图:
20240601142819

3.2.3 Undolog 如何记录和回滚

每当 InnoDB 引擎对一条记录进行操作时,要把回滚时需要的信息都记录到 undolog 里,比如:

  1. 插入 一条记录时,要把这条记录的主键值记下来,这样回滚时只需要把这个主键值对应的记录 delete 就好了;
  2. 删除 一条记录时,要把这条记录中的内容都记下来,这样回滚时再把由这些内容组成的记录 insert 到表中就好了;
  3. 更新 一条记录时,要把被更新的列的旧值记下来,这样回滚时再把这些列 update 为旧值就好了。

3.2.4 Undolog 日志的格式

需要了解一条记录在innodb引擎中的存储格式。

一条记录的每一次更新操作产生的 undolog 中,都有一个 roll_pointer 指针和一个 trx_id 事务id

  • 通过 trx_id 可以知道该记录是被哪个事务修改的。
  • 通过 roll_pointer 指针可以将这些 undolog 串成一个链表,这个链表就被称为版本链

版本链如下图:
1

3.2.5 Undolog + ReadView 实现 MVCC

MVCC - Multi-version concurrency control
多版本并发控制(MVCC)是一种数据库管理技术,通过维护数据的多个版本来实现并发访问,从而提高读写操作的性能和一致性。

并发访问的多个版本通过快照控制:
对于读提交可重复读隔离级别的事务来说,它们的快照读(普通 select 语句)是通过 ReadView + undolog 来实现的。
读提交可重复读快照读 区别在于创建 ReadView 的时机不同:

  • 读提交隔离级别每次执行 select 都会生成一个新的 ReadView,也意味着,事务期间的多次读取同一条数据,前后两次读的数据可能会出现不一致,因为可能这期间另外一个事务修改了该记录,并提交了事务。
  • 可重复读隔离级别:在启动事务时生成一个 ReadView,然后整个事务期间都在用这个 ReadView,这样就保证了在事务期间读到的数据都是事务启动前的记录。

如何知道版本的可见性,参考 3.2.6
通过 事务的 ReadView 里的字段 记录中的两个隐藏列 trx_id 和 roll_pointer 的比对,如果不满足可见行,就会顺着 undolog 版本链里找到满足其可见性的记录,从而控制并发事务访问同一个记录时的行为,这就叫 MVCC(多版本并发控制)

3.2.6 ReadView 机制

ReadView 的四个字段:
20240601154137
ReadView 可以理解为记录当前事务id创建时,整个数据库还有哪些其他活着的事务,记录下来,以便于判断数据的可见性。

  • creator_trx_id:代表创建当前这个 ReadView 的事务ID。
  • m_ids:表示在生成当前 ReadView 时,系统内活跃且未提交的事务ID列表。
  • min_trx_id:活跃的事务列表中最小的事务ID。
  • max_trx_id:表示在生成当前 ReadView 时,系统中要给下一个事务分配的ID值

如何判断可见性:
判定方法:事务 readview 里的字段记录中的两个隐藏列 进行对比:

  • 如果事务ReadView 中的 min_trx_id 值 >= 记录的 trx_id 值,表示这个版本的记录是在创建 ReadView 前 已经提交的事务生成的,所以该版本的记录对当前事务 可见
  • 如果事务ReadView 中的 max_trx_id 值 <= 记录的 trx_id 值,表示这个版本的记录是在创建 ReadView 后 才启动的事务生成的,所以该版本的记录对当前事务 不可见
  • 如果 事务ReadView 中的 min_trx_id < 记录的 trx_id 值 < 事务ReadView 中的 max_trx_id,需要判断 trx_id 是否在 m_ids 列表中:
    • 如果记录的 trx_idm_ids 列表中,表示生成该版本记录的活跃事务依然活跃着(还没提交事务),所以该版本的记录对当前事务 不可见
    • 如果记录的 trx_id 不在 m_ids 列表中,表示生成该版本记录的活跃事务已经被提交,所以该版本的记录对当前事务 可见

3.2.7 UndoLog 如何刷盘的

  • Undolog数据页 的刷盘策略是一样的,都需要通过 Redolog 保证持久化
  • Buffer pool 中有 Undo 页,对 Undo 页的修改也都会记录到 Redolog
  • Redolog 会每秒刷盘,提交事务时也会刷盘,数据页Undo 页都是靠这个机制保证持久化的。

3.3 Buffer Pool 缓冲池

3.3.1 Buffer Pool 的意义

场景:
MySQL 的数据都是存在磁盘中的,那么我们要更新一条记录的时候,得先要从磁盘读取该记录,然后在内存中修改这条记录。

那修改完这条记录是选择直接写回到磁盘,还是选择缓存起来呢:
这也是 Buffer Pool 的意义。
当然是缓存起来好,这样下次有查询语句命中了这条记录,直接读取缓存中的记录,就不需要从磁盘获取数据了。

有了Buffer Pool后:

  • 当读取数据时,如果数据存在于 BufferPool 中,客户端就会直接读取 BufferPool 中的数据,否则再去磁盘中读取。
  • 当修改数据时,如果数据存在于 Buffe Pool 中,那直接修改 BufferPool 中数据所在的页,然后将其页设置为脏页(该页的内存数据和磁盘上的数据已经不一致),为了减少磁盘I/O,不会立即将脏页写入磁盘,后续由后台线程选择一个合适的时机将脏页写入到磁盘。

Buffer Pool 属于哪一层:
属于:Innodb引擎层。
区别:不是本文开头讨论 select 语句执行过程中提到的缓存,那个是在 server 层的。

3.3.2 Buffer Pool 缓存什么

InnoDB中磁盘与内存的交互基本单位:
InnoDB 会把存储的数据划分为若干个,以页作为磁盘和内存交互的基本单位,一个页的默认大小为 16KB。因此,BufferPool 同样需要按页来划分,使用与存储引擎一样的基本单位。

在 MySQL 启动的时候:
InnoDB 会为 BufferPool 申请一片连续的内存空间,然后按照默认的 16KB 的大小划分出一个个的页, BufferPool 中的页就叫做缓存页
此时这些缓存页都是空闲的,之后随着程序的运行,会有磁盘上的页被加载缓存到 BufferPool 中的缓存页。

在 MySQL 启动完成的时候:
由于是先申请了一片连续的内存空间但没写入具体数据,所以可以观察到使用的虚拟内存空间很大,而使用到的物理内存空间却很小。
这是因为只有这些虚拟内存被访问后,操作系统才会触发缺页中断,申请物理内存,接着将虚拟地址和物理地址建立映射关系。

BufferPool 可以缓存的数据类型:
mysql-BufferPool.drawio

Undo 页是记录什么的
开启事务后,InnoDB 会在更新记录之前,先记录相应操作的 undolog 来保证事务的原子性

比如:如果是 update 操作,需要把被更新的列的旧值记下来,旧值作为一条 undolog,然后把这条 undolog 写入 BufferPool 中的 Undo页

查询一条记录,只需要缓存一条记录吗?
刚刚提到了 InnoDB 存储引擎以16KB大小的页作为磁盘与内存交互的基本单位,所以查询一条记录的时候,会将整个页加载进 BufferPool 中,再通过页的页目录去定位到某条具体的记录.

上面提到的 页目录 是什么?
简单来说:页目录类似于字典的目录,用于快速定位某条记录的大致位置。

这个问题需要了解一个 内部是如何组织数据的。小林图解也有提到,这里附上一张更具体的图来辅助理解。

  • 一个页空间会被划分成许多部分,有:文件头、页头、最大最小记录、用户记录、空闲空间、文件尾等。
  • 主要关注用户记录:存储的一行行记录会被存放在这里,记录还会进一步被分成一个个,每一个组内部都有一些数据记录。
  • 再看到左边的页目录:有一个个的槽位,其指向每一个分组内的最后一条记录。

mysql-4.drawio

3.3.3 Buffer Pool 刷盘策略

深入学习文章推荐:(十二)MySQL之内存篇:深入探寻数据库内存与Buffer Pool的奥妙! - 竹子爱熊猫

3.4 RedoLog 重做日志

3.4.1 为什么需要 Redolog

试想以下场景:
Buffer Pool 是基于内存的,而内存总是不可靠,万一断电重启,还没来得及落盘的脏页数据就会丢失。

解决方案:
采用 WAL (Write-Ahead Logging) 技术。

  • MySQL 的写操作并不是立刻写到磁盘上,而是先写日志文件,然后在合适的时间再将新的记录写到磁盘上。
  • 在事务提交时,数据库系统只需确保 redolog 已经写入磁盘,而数据页可以稍后再写入。

解决方案的场景:
当有一条记录需要更新的时候,InnoDB 引擎会先更新 Buffer Pool 中的数据页,同时将该页标记为脏页。然后,将本次对这个页修改后的数据状态redo log 的形式记录下来并写入 redo log 文件,这时更新操作就算完成了。

到这里,只是完成了 redo log 文件的刷盘,但还未完成将最新的数据记录(脏的数据页)刷盘到存储表数据的文件 xxx.ibd 中。简而言之:日志文件是最新的,但数据库文件还不是最新的,还需要完成最后的 Buffer Pool --> 磁盘 操作,才能完成持久化存储。

后续,InnoDB 引擎会在适当的时候,由后台线程将缓存在 Buffer Pool 的脏页刷新到磁盘里,这就是 WAL 的核心思想。

图示,第4步的细节参考 3.4.5
mysql-1WAL.drawio

3.4.2 认识 Redolog 机制

redolog 是物理日志,记录了某个数据页做了什么修改

如何记录这个 “修改”:
格式:AAA表空间 中的 BBB数据页 的 CCC偏移量 的地方做了 DDD的更新,每当执行一个事务就会产生这样的一条或者多条物理日志。

WAL:
在事务提交时,只要先将 redo log 持久化到磁盘即可,可以不需要等到将缓存在 Buffer Pool 里的脏页数据持久化到磁盘。

如何保证事务的持久性:
当系统崩溃时,虽然脏页数据没有持久化,但是 redolog 已经持久化,MySQL 能在重启后根据 redolog 的日志内容,将所有数据恢复到最新的状态。

3.4.3 Redolog 与 Undolog 的配合

配合场景:
开启事务后,InnoDB 在进行更新操作之前,首先会记录相应的 undolog。

如果是更新操作,InnoDB 需要将被更新的列的旧值记下来,也就是生成一条 undolog。这个 undolog 会被写入 Buffer Pool 中的 Undo 页面。
然后在 BufferPool 中完成数据页的更新,标记该页为脏页,并且记录对于的 redolog。

具体的配合过程:

  1. 记录 undolog:
  • 当事务对数据库中的记录进行更新时,InnoDB 会先生成一条 undolog,记录被修改前的数据。这个操作是为了保证在事务回滚时能够恢复到原始状态。
  • 生成的 undolog 会被写入 BufferPool 中的 Undo 页面,并在内存中进行修改。
  1. 记录 redolog:
  • 在内存中修改 Undo 页面后,InnoDB 还需要记录对应的 redo logredo log 记录的是对数据页的物理修改操作,是用来在系统崩溃后进行数据恢复的。
  • 具体来说,redo log 会记录对数据页的修改操作细节,包括何时修改、修改了哪些数据等。这些信息会先写入 redolog buffer 中,并在适当的时机(例如事务提交时)刷写到磁盘上的 redo log 文件中。

Redolog Buffer 与 Buffer Pool
不一样! Redolog Buffer 是 redolog 自己的缓存 具体细节往下看。

3.4.4 Redolog与数据分开写入磁盘的必要性

能提高数据库的写性能:

  • 顺序写入:
    • redo log 的写入是顺序写入,采用在文件尾部追加写入文件的方式,这样可以减少磁盘的寻道时间和旋转延迟,从而提高写入速度。
    • 顺序写入的性能通常比随机写入高
  • 随机写入:
    • 数据页的写入通常是随机写入。随机写入是指数据写入磁盘的不同位置,写入速度较慢。
    • 通过将数据页首先写入缓冲池(Buffer Pool),然后在适当时机批量写入磁盘,可以减少随机写入的频率和次数。

3.4.5 Redolog 的刷盘策略

执行一个事务的过程中,产生的 redo log 也不是直接写入磁盘的,因为这样会产生大量的 I/O 操作,而且磁盘的运行速度远慢于内存。
所以,redo log 也有自己的缓存 redo log buffer ,每当产生一条 redo log 时,会先写入到 redo log buffer,后续再持久化到磁盘.

redo log buffer 默认大小 16 MB,可以通过 innodb_log_Buffer_size 参数动态的调整大小,增大它的大小可以让 MySQL 处理大事务时不必写入磁盘(提高了写入磁盘的阈值),进而提升写 IO 性能。

redo log buffer 的持久化如下图:
20240602172013

3.4.6 redolog buffer 的刷盘时机:

主要的几个刷盘时机:

  1. MySQL 正常关闭时。
  2. redo log buffer 中记录的写入量大于 redo log buffer 内存空间的一半时,会触发落盘。
  3. InnoDB 的后台线程每隔 1 秒,将 redo log buffer 持久化到磁盘。
  4. 每次事务提交时都将缓存在 redo log buffer 里的 redo log 直接持久化到磁盘(这个策略可由 innodb_flush_log_at_trx_commit 参数控制)。

了解一个主要参数 innodb_flush_log_at_trx_commit:

  • innodb_flush_log_at_trx_commit = 0
    每次事务提交时 ,还是将 redo log 留在 redo log buffer 中 ,该模式下在事务提交时不会主动触发写入磁盘的操作。
  • innodb_flush_log_at_trx_commit = 1 这是默认值。
    每次事务提交时,都 将缓存在 redo log buffer 里的 redo log 直接持久化到磁盘 ,这样可以保证 MySQL 异常重启之后数据不会丢失。
  • innodb_flush_log_at_trx_commit = 2
    每次事务提交时,都只是缓存在 redo log buffer 里的 redo log 写到 redo log 文件,注意并不意味着写入到了磁盘 ,因为操作系统的文件系统中有个 Page Cache(Page Cache 是专门用来缓存文件数据的,所以写入 redo log文件意味着写入到了操作系统的文件缓存。

3.4.7 Redolog 日志重写

问题背景:
redolog 文件写满了/文件过大怎么办?

解决方案 - 日志重写:
默认情况下,InnoDB 存储引擎有 1 个重做日志文件组 redo log Group,由 2 个 redolog 文件 组成,分别是:ib_logfile0ib_logfile1

日志重写方式:循环写
重做日志文件组是以 循环写 的方式工作的,从头开始写,写到末尾就又回到开头,相当于一个环形。
先写 ib_logfile0 文件,当 ib_logfile0 文件被写满的时候,会切换至 ib_logfile1 文件,当 ib_logfile1 文件也被写满时,会切换回 ib_logfile0 文件。

图示:
20240602173429

3.5 BinLog 重做日志

3.5.1 Binlog 的作用

  • binlog 是 MySQL 的 Server 层实现的日志,所有存储引擎都可以使用,用于备份恢复、主从复制等;
  • binlog 文件是记录了所有数据库表结构变更和表数据修改的日志,不会记录查询类的操作,比如 SELECTSHOW 操作。
  • binlog 是追加写,写满一个文件,就创建一个新的文件继续写,不会覆盖以前的日志,保存的是全量的日志。

产生 binlog 的场景:
MySQL 在完成一条更新操作后,Server 层会生成一条 binlog,将其写到 binlog cache(Server 层的 cache),等之后事务提交的时候,会将该事务执行过程中产生的所有 binlog 统一写入 binlog 文件

3.5.2 Binlog 刷盘策略

MySQL 给每一个处理线程分配了一片内存用于缓冲 binlog,该内存叫 binlog cache

事务执行过程中,先把日志写到 binlog cache,事务提交的时候,再把 binlog cache 写到 binlog 文件中。

关键点1:
一个事务的 binlog 是不能被拆开的,因此无论这个事务有多大(比如有很多条语句),也要保证一次性写入。

MySQL 设定一个处理线程只能同时有一个事务在执行,所以每当执行一个 begin/start transaction 的时候,就会默认提交上一个事务。

关键点2:
场景:什么时候 binlog cache 会写到 binlog 文件

回答:在事务提交的时候,执行器把 binlog cache 里的完整事务写入到 binlog 文件中,并清空 binlog cache

关键点3:
每一个线程都有自己的binlog cache,最终写入同一个Binlog文件

关键点4:
场景:如关键点3所提到的 最终写入同一个Binlog文件,那这里的并发问题如何解决?

回答:MySQL采用了多种机制来确保并发安全和一致性

  1. 锁机制
  2. 顺序写入
  3. 组提交 Group Commit

关键点5:
场景:写入binlog文件的过程还可以继续拆分。

  • 系统调用 write()后,会先写入内核的缓冲区 page cache,这里不涉及磁盘I/O
  • 内核再通过 fsync() 持久化到磁盘,这里涉及磁盘I/O。频繁的 fsync() 会导致磁盘的I/O 升高。

20240603001559

关键点6:
fsync() 的频率由参数 sync_binlog 控制

  • sync_binlog = 0 是默认值
    表示每次提交事务都只 write,不 fsync,后续交由操作系统决定何时将数据持久化到磁盘。
  • sync_binlog = 1
    表示每次提交事务都会 write,然后马上执行 fsync,最多丢失一个事务的 binlog
  • sync_binlog > 1
    表示每次提交事务都 write,但累积 N 个事务后才 fsync

3.5.3 Binlog - 主从同步模型

主从同步过程:

  1. 写入 binlog: 主库修改数据后,写入 binlog 日志,提交事务,更新本地存储的数据。
  2. 同步 binlog: 从库连接到主库后,主库会创建一个 dump 线程,把 binlog 同步到所有从库,每个从库把 binlog 写到暂存日志中。
  3. 回放 binlog: 从库启动一个 sql 线程去回放 binlog,去读 relay log 中继日志然后回放 binlog 更新数据。
    20240603002541

三种主从同步模式:
MySQL 默认的同步模式:异步模式

  • 同步模式: 主库提交事务的线程要等待所有从库的同步成功,才返回客户端结果。性能最差了。
  • 异步模式: 主库提交事务的线程不会等待 binlog 同步完成就返回客户端结果,性能最好,但是主库宕机,数据就会丢失。
  • 半同步模式: 比如一主二从的集群,只要成功同步到一个从库,就立即返回数据给客户端。即使主库宕机,仍有一个从库有最新数据。

3.6 两阶段提交

3.6.1 两阶段提交的提出

思考以下问题:
事务提交后,redo logbinlog 都要持久化到磁盘,但是这两个是独立的逻辑,可能出现半成功的状态,这样就造成两份日志之间的逻辑不一致。

问题的场景复现:

原数据:表名 t_user,某行记录 id = 1;name = jay
执行SQL:UPDATE t_user SET name = 'kryiea' WHERE id = 1
事务提交后:进行持久化 redologbinlog

这两个日志的刷盘先后顺序可能会导致下面两种情况:

  • 如果在将 redo log 刷入到磁盘之后, MySQL 突然宕机了,而 binlog 还没有来得及写入。
    • MySQL 重启后,通过 redo log 能将 Buffer Poolid = 1 这行数据的 name 字段恢复到新值 kryiea
    • 但是 binlog 里面没有记录这条更新语句,在主从架构中,binlog 会被复制到从库,由于 binlog 丢失了这条更新语句,从库的这一行 name 字段是旧值 jay,与主库的值不一致。
  • 如果在将 binlog 刷入到磁盘之后, MySQL 突然宕机了,而 redo log 还没有来得及写入 。
    • 由于 redo log 还没写,崩溃恢复以后这个事务无效,所以 id = 1 这行数据的 name 字段还是旧值 jay
    • binlog 里面记录了这条更新语句,在主从架构中,binlog 会被复制到从库,从库执行了这条更新语句,那么这一行 name 字段是新值 kryiea,与主库的值不一致。

问题的解决方案: 两阶段提交

3.6.2 两阶段提交的概念

两阶段提交其实是分布式事务一致性协议,它可以保证多个逻辑操作要不全部成功,要不全部失败,不会出现半成功的状态。

两阶段提交把单个事务的提交拆分成了 2 个阶段,分别是准备(Prepare)阶段提交(Commit)阶段 ,每个阶段都由协调者(Coordinator)参与者(Participant)共同完成。

协调者与参与者之间的协作:
例子来自小林图解。

举个拳击比赛的例子,两位拳击手(参与者)开始比赛之前,裁判(协调者)会在中间确认两位拳击手的状态,类似于问你准备好了吗?

  • 准备阶段 : 裁判(协调者)会依次询问两位拳击手(参与者)是否准备好了,然后拳击手听到后做出应答,如果觉得自己准备好了,就会跟裁判说准备好了;如果没有自己还没有准备好(比如拳套还没有带好),就会跟裁判说还没准备好。
  • 提交阶段 : 如果两位拳击手(参与者)都回答准备好了,裁判(协调者)宣布比赛正式开始,两位拳击手就可以直接开打;如果任何一位拳击手(参与者)回答没有准备好,裁判(协调者)会宣布比赛暂停,对应事务中的回滚操作。

3.6.3 两阶段提交的过程

在 MySQL 的 InnoDB 存储引擎中,开启 binlog 的情况下,MySQL 会同时维护 binlogredolog,为了保证这两个日志的一致性,MySQL 事务提交时使用 内部 XA 事务 来保证一致性。

内部 XA 事务binlog 作为协调者存储引擎是参与者

(是的,也有外部 XA 事务)

当客户端执行 commit 语句或者在自动提交的情况下,MySQL 内部开启一个 XA 事务, 分两阶段来完成 XA 事务的提交 。

场景举例:
事务的提交过程有两个阶段:将 redolog 的写入拆成了两个步骤 preparecommit,中间再穿插写入 binlog

具体如下:

  • prepare 阶段 :将 XID(内部 XA 事务的 ID) 写入到 redo log,同时将 redo log 对应的事务状态设置为 prepare,然后将 redo log 持久化到磁盘(innodb_flush_log_at_trx_commit = 1 的作用)

  • commit 阶段 :把 XID 写入到 binlog,然后将 binlog 持久化到磁盘(sync_binlog = 1 的作用),接着调用引擎的提交事务接口,将 redo log 状态设置为 commit,此时该状态并不需要持久化到磁盘,只需要 write 到文件系统的 page cache 中就够了。因为只要 binlog 写磁盘成功,就算 redo log 的状态还是 prepare 也没有关系,一样会被认为事务已经执行成功

20240603005031

3.6.4 两阶段提交有什么问题

两阶段提交虽然保证了两个日志文件的数据一致性,但是性能很差。

主要有两个方面的影响:

  • 磁盘 I/O 次数高 : 对于“双1”配置,每个事务提交都会进行两次 fsync(刷盘),一次是 redo log 刷盘,另一次是 binlog 刷盘
  • 锁竞争激烈 : 两阶段提交虽然能够保证单事务两个日志的内容一致,但在多事务的情况下,却不能保证两者的提交顺序一致,因此,在两阶段提交的流程基础上,还需要加一个来保证提交的原子性,从而保证多事务的情况下,两个日志的提交顺序一致。

3.6.5 对两阶段提交加强:加入组提交

MySQL 引入了组提交(group commit)机制,当有多个事务提交的时候,会将多个 binlog 刷盘操作合并成1个,从而减少磁盘 I/O 的次数。

引入了组提交机制后,prepare 阶段不变,只针对 commit 阶段,将 commit 阶段拆分为3个过程:
flush 阶段 : 多个事务按进入的顺序binlogcache 写入文件(不刷盘);
sync 阶段 :binlog 文件做 fsync 操作(多个事务的 binlog 合并一次刷盘);
commit 阶段 : 各个事务按顺序InnoDB commit 操作

20240603010701