一、 一条 select 语句的执行过程
一条查询语句执行的过程,属于读一条记录的过程,大致可以分为以下几个步骤:
-
建立客户端/服务器通信:
- 客户端发起的连接请求,通过MySQL
连接器
处理后,客户端将SQL查询语句发送到服务器。
- 客户端发起的连接请求,通过MySQL
-
查询解析:
- SQL解析器:
SQL解析器
首先会对查询语句进行语法和词法分析,生成一个解析树(Parse Tree)。 - 预处理器:
预处理器
进一步检查解析树的合法性,包括表和列是否存在、名称是否正确等。
- SQL解析器:
-
查询优化:
- 查询重写:
优化器
可能会对解析树进行重写,例如将子查询转换为连接(JOIN),或者进行某些常见的SQL重写优化。 - 选择执行计划:
优化器
会生成多个执行计划,并选择其中的最优计划。这里会考虑索引的使用、表扫描的方式(全表扫描或索引扫描)等。 - 成本估算:MySQL使用一种
基于成本的优化算法
,通过估算不同执行计划的代价,选择成本最低的计划。
- 查询重写:
-
查询执行:
- 存储引擎接口:MySQL的
执行器
根据优化器选择的执行计划,通过存储引擎接口调用具体的存储引擎(如InnoDB、MyISAM等)。 - 存储引擎操作:存储引擎根据执行器的请求进行数据的读取、写入等操作。
- 存储引擎接口:MySQL的
-
结果返回:
- 结果集处理:执行器将存储引擎返回的数据进行处理,生成最终的
结果集
。 - 结果发送:最终的结果集通过网络传输返回给客户端。
- 结果集处理:执行器将存储引擎返回的数据进行处理,生成最终的
-
缓存处理(可选):
- 查询缓存:如果查询缓存开启且命中缓存,MySQL会直接从缓存中返回结果,而不经过上述大部分步骤。需要注意的是,MySQL 8.0 版本已移除查询缓存这一特性。
总结来说,MySQL一条查询语句的执行过程可以概括为:客户端发送SQL语句 -> 解析语句 -> 优化查询 -> 执行查询 -> 返回结果
。每个步骤中都有许多细节和优化点,使得MySQL能够高效地处理各种查询请求。
引用小林图解的一张图:
二、一条 update 语句的执行过程
比如这条待执行的 update 语句:
UPDATE t_user SET name = 'kryiea' WHERE id = 10086;
待执行的语句执行过程:
select
语句的那一套流程,update
语句也是同样会走一遍- 查询到目标记录后,执行更新操作的同时会涉及对三种日志的改动,
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
来进行回滚。
过程如下图:
3.2.3 Undolog 如何记录和回滚
每当 InnoDB 引擎对一条记录进行操作时,要把回滚时需要的信息都记录到 undolog
里,比如:
- 在
插入
一条记录时,要把这条记录的主键值记下来,这样回滚
时只需要把这个主键值对应的记录delete
就好了; - 在
删除
一条记录时,要把这条记录中的内容都记下来,这样回滚
时再把由这些内容组成的记录insert
到表中就好了; - 在
更新
一条记录时,要把被更新的列的旧值记下来,这样回滚
时再把这些列update
为旧值就好了。
3.2.4 Undolog 日志的格式
需要了解一条记录在innodb引擎中的存储格式。
一条记录的每一次更新操作产生的 undolog
中,都有一个 roll_pointer 指针
和一个 trx_id 事务id
:
- 通过
trx_id
可以知道该记录是被哪个事务修改的。 - 通过
roll_pointer
指针可以将这些undolog
串成一个链表,这个链表就被称为版本链。
版本链如下图:
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 的四个字段:
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_id
在m_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 可以缓存的数据类型:
Undo 页是记录什么的
开启事务后,InnoDB 会在更新记录之前,先记录相应操作的 undolog
来保证事务的原子性。
比如:如果是 update
操作,需要把被更新的列的旧值记下来,旧值作为一条 undolog
,然后把这条 undolog
写入 BufferPool
中的 Undo页
查询一条记录,只需要缓存一条记录吗?
刚刚提到了 InnoDB 存储引擎以16KB大小的页
作为磁盘与内存交互的基本单位,所以查询一条记录的时候,会将整个页加载进 BufferPool 中,再通过页的页目录
去定位到某条具体的记录.
上面提到的 页目录 是什么?
简单来说:页目录
类似于字典的目录
,用于快速定位某条记录的大致位置。
这个问题需要了解一个 页
内部是如何组织数据的。小林图解也有提到,这里附上一张更具体的图来辅助理解。
- 一个页空间会被划分成许多部分,有:
文件头、页头、最大最小记录、用户记录、空闲空间、文件尾
等。 - 主要关注
用户记录
:存储的一行行记录会被存放在这里,记录还会进一步被分成一个个组
,每一个组内部都有一些数据记录。 - 再看到左边的
页目录
:有一个个的槽位
,其指向每一个分组内的最后一条记录。
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
:
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。
具体的配合过程:
- 记录 undolog:
- 当事务对数据库中的记录进行更新时,InnoDB 会先生成一条
undolog
,记录被修改前的数据。这个操作是为了保证在事务回滚时能够恢复到原始状态。 - 生成的 undolog 会被写入 BufferPool 中的
Undo 页面
,并在内存中进行修改。
- 记录 redolog:
- 在内存中修改
Undo 页面
后,InnoDB 还需要记录对应的redo log
。redo 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
的持久化如下图:
3.4.6 redolog buffer 的刷盘时机:
主要的几个刷盘时机:
- MySQL 正常关闭时。
- 当
redo log buffer
中记录的写入量大于 redo log buffer 内存空间的一半
时,会触发落盘。 - InnoDB 的
后台线程每隔 1 秒
,将redo log buffer
持久化到磁盘。 每次事务提交时
都将缓存在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_logfile0
和 ib_logfile1
。
日志重写方式:循环写
重做日志文件组
是以 循环写
的方式工作的,从头开始写,写到末尾就又回到开头,相当于一个环形。
先写 ib_logfile0
文件,当 ib_logfile0
文件被写满的时候,会切换至 ib_logfile1
文件,当 ib_logfile1
文件也被写满时,会切换回 ib_logfile0
文件。
图示:
3.5 BinLog 重做日志
3.5.1 Binlog 的作用
- binlog 是 MySQL 的 Server 层实现的日志,所有存储引擎都可以使用,用于备份恢复、主从复制等;
- binlog 文件是记录了
所有数据库表结构变更和表数据修改
的日志,不会记录查询类的操作,比如SELECT
和SHOW
操作。 - 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采用了多种机制来确保并发安全和一致性
- 锁机制
- 顺序写入
- 组提交 Group Commit
关键点5:
场景:写入binlog文件的过程还可以继续拆分。
- 系统调用
write()
后,会先写入内核的缓冲区page cache
,这里不涉及磁盘I/O
- 内核再通过
fsync()
持久化到磁盘,这里涉及磁盘I/O
。频繁的fsync()
会导致磁盘的I/O
升高。
关键点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 - 主从同步模型
主从同步过程:
- 写入 binlog: 主库修改数据后,写入
binlog 日志
,提交事务,更新本地存储的数据。 - 同步 binlog: 从库连接到主库后,主库会创建一个
dump 线程
,把binlog
同步到所有从库,每个从库把binlog
写到暂存日志中。 - 回放 binlog: 从库启动一个
sql 线程
去回放binlog
,去读relay log 中继日志
然后回放binlog
更新数据。
三种主从同步模式:
MySQL 默认的同步模式:异步模式
- 同步模式: 主库提交事务的线程要等待所有从库的同步成功,才返回客户端结果。性能最差了。
- 异步模式: 主库提交事务的线程不会等待 binlog 同步完成就返回客户端结果,性能最好,但是主库宕机,数据就会丢失。
- 半同步模式: 比如一主二从的集群,只要成功同步到一个从库,就立即返回数据给客户端。即使主库宕机,仍有一个从库有最新数据。
3.6 两阶段提交
3.6.1 两阶段提交的提出
思考以下问题:
事务提交后,redo log
和 binlog
都要持久化到磁盘,但是这两个是独立的逻辑,可能出现半成功的状态,这样就造成两份日志之间的逻辑不一致。
问题的场景复现:
原数据:表名 t_user
,某行记录 id = 1;name = jay
执行SQL:UPDATE t_user SET name = 'kryiea' WHERE id = 1
事务提交后:进行持久化 redolog
和 binlog
。
这两个日志的刷盘先后顺序可能会导致下面两种情况:
- 如果在将
redo log
刷入到磁盘之后, MySQL 突然宕机了,而binlog
还没有来得及写入。- MySQL 重启后,通过
redo log
能将Buffer Pool
中id = 1
这行数据的name
字段恢复到新值kryiea
。 - 但是
binlog
里面没有记录这条更新语句,在主从架构中,binlog
会被复制到从库,由于binlog
丢失了这条更新语句,从库的这一行name
字段是旧值jay
,与主库的值不一致。
- MySQL 重启后,通过
- 如果在将
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 会同时维护 binlog
与 redolog
,为了保证这两个日志的一致性,MySQL 事务提交时使用 内部 XA 事务
来保证一致性。
内部 XA 事务
由 binlog 作为协调者
,存储引擎是参与者
。
(是的,也有外部 XA 事务)
当客户端执行 commit
语句或者在自动提交
的情况下,MySQL 内部开启一个 XA 事务
, 分两阶段来完成 XA 事务
的提交 。
场景举例:
事务的提交过程有两个阶段:将 redolog
的写入拆成了两个步骤 prepare
和 commit
,中间再穿插写入 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
也没有关系,一样会被认为事务已经执行成功;
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 阶段 : 多个事务按进入的顺序将 binlog
从 cache
写入文件(不刷盘);
● sync 阶段 : 对 binlog
文件做 fsync
操作(多个事务的 binlog
合并一次刷盘);
● commit 阶段 : 各个事务按顺序做 InnoDB commit 操作
;