MySQL 执行链路

前言

当客户端SQL语句提交给 MySQL 服务器时首先需要建立连接,连接器会与客户端进行连接,并校验用户身份信息,权限信息,如果权限通过,则通过SQL分析器对SQL语句进行语义分析(检查SQL语句是否合法,能否解析通过),分析完成后将会对SQL的执行过程进行优化,例如剔除不必要的查询条件 1=1 ,以及选择最优的索引与Where条件的字段排序。

然后交给执行器执行此SQL语句,执行器将会根据数据库所使用的存储引擎执行SQL。MySQL在设计时是分为了两层,即Server层和存储引擎层。这样做的好处就是解藕,可以根据不同的数据库去选择合适的存储引擎。

常用的InnoDB存储引擎是以页(16KB)为单位来管理存储空间的,任何的增删改差操作最终都会操作完整的一个页,会将整个页加载到 BufferPool 中并且所有的数据操作都是在 BufferPool 中完成,BufferPool 通常设置为服务器70%-80%左右的内存占用,这样数据的操作直接在内存中执行,从而提高数据操作的效率(如果每一条DML语句都要进行 IO 操作那么数据库的磁盘很容易变成瓶颈,改一条数据需要先从磁盘中读取,然后刷盘到磁盘则是2次IO,一次读一次写)。

我们通过一个例子说明,我们先假设没有 Buffer Pool,user 表里面只有一条记录,记录的 age = 1,需要执行以下三条SQL:

事务A:update user set age = 2(一读一写 2次IO)
事务B:update user set age = 3(一读一写 2次IO)
事务C:update user set age = 4(一读一写 2次IO)

每一次都需要从磁盘读取数据到内存做一次修改然后刷盘到磁盘,那么操作下来总共是6次IO。

如果加入 Buffer Pool 后:

事务A:update user set age = 2(读到 Buffer Pool 并做修改 1次读IO)
事务B:update user set age = 3(修改 Buffer Pool 中的数据 0次IO)
事务C:update user set age = 4(修改 Buffer Pool 中的数据 并刷入磁盘 1次写IO)

只有第一次需要去加载数据页,后续的操作都是直接在内存中完成,加入 Buffer Pool 之后操作下来总共是2次IO。(这里只是简单演示基本逻辑,不完全是这么简单的优化逻辑,但总的来看 Buffer Pool 就是把磁盘中的数据映射到内存中来进行操作,减少磁盘 IO 次数,但 Buffer Pool 在内存中进行数据修改操作,万一出现宕机或者断电数据很容易丢失)

查看 Buffer Pool 信息: SHOW ENGINE INNODB STATUS

此时就需要有几个很重要的 Log日志来进行辅助:Redo Log(重做日志),Bin Log (二进制日志),Undo Log(回滚日志)。

Redo Log (重做日志)

就是在数据修改之后,先将修改后的值记录到磁盘上的 Redo Log 中,就算突然断电了,Buffer Pool 中的数据全部丢失了,来电的时候也可以根据 Redo Log 恢复 Buffer Pool,这样既利用到了Buffer Pool的内存高效性,也保证了数据不会丢失,Redo log 刷盘有三种策略:

设置为0:每次事务提交不进行刷盘操作(系统默认 Master Thread 每隔1s进行一次重做日志的同步)
设置为1:每次提交事务都将进行同步,刷盘操作,这也是最保险的,因为如果这个时候崩溃了代表事物没有commit成功,因此,也不用恢复什么数据。(默认)
设置为2:表示每次事务提交时都只把 Redo Log Buffer 内容写入 Page Cache,不进行同步,由文件系统(os)自己决定什么时候同步到磁盘。

此处的 Redo Log 刷盘是顺序写入 WAL(Write-ahead logging),翻译成中文就是预写式日志,对比起随机落盘效率大幅度提高,在 KafKa,Rocketmq 等MQ队列都是采用顺序写日志来提高磁盘的写入能力。虽然磁盘顺序写已经很高效了,但是和内存操作还是有一定的差距。为了进一步提高 Redo Log 的效率,于是在内存中搞出了 Change Buffer (默认的大小为16MB,位于 BufferPool 中,可以设置占比大小百分比) 来在内存中做记录,只有事物提交才进行磁盘顺序写入。

Change Buffer  中分了很多的 Block,每个 Block 的大小为 512kb,每一个事务产生的所有 Redo Log 称为一个 Group。

Bin Log(二进制日志)

Bin Log 是记录所有数据库表结构变更(例如CREATE、ALTER TABLE…)以及表数据修改(INSERT、UPDATE、DELETE…)的二进制日志。不会记录SELECT和SHOW这类操作,因为这类操作对数据本身并没有修改,但可以通过查询通用日志来查看MySQL执行过的所有语句。

需要注意的一点是,即便 update 操作没有造成数据变化,也是会记入 Bin Log

和 Redo Log日志类似,Bin Log 也有着自己的刷盘策略,通过 sync_binlog 参数控制:

设置为0:每次提交事务前将 Bin Log 写入 OS Cache,由操作系统控制什么时候刷到磁盘

设置为1:采用同步写磁盘的方式来写 Bin Log,不使用 OS Cache 来写 Bin Log

设置为n:当每进行n次事务提交之后,调用一次 Fsync 将 OS Cache 中的Bin Log强制刷到磁盘

Bin Log有两个常用的使用场景:

  • 主从复制:MySQL Replication在 Master 端开启 Bin Log, Master 把它的二进制日志传递给 Slaves 来达到 Master-Slave 数据一致的目的。
  • 数据恢复:通过Mysql Bin Log工具来恢复数据。

那么问题来了,Bin Log 和 Redo Log 都是记录的修改之后的值,这两者有什么区别呢?有Redo Log 为什么还需要 Bin Log 呢?

区分 Bin Log 和 Redo Log

Bin Log 属于MySQL Server 层,Redo Log 属于 Engine 层

Bin Log 的所有引擎都可用,Redo Log  InnoDB 独有

Bin Log 记录的是逻辑操作,Redo Log 记录的是更新的内容

Bin Log 是追加写形成多个文件,Redo Log 是固定大小的几个文件循环写

事务执行时多个操作,一直向 Redo Log 中写入,最后提交时才写入 Bin Log

Undo Log(回滚日志)

在数据修改的时候,不仅记录了Redo Log,还记录了相对应的 Undo Log,如果因为某些原因导致事务失败或回滚了,可以借助该 Undo Log 进行回滚。Undo Log是采用段 (segment) 的方式来记录的,每个Undo Log 操作在记录的时候占用一个Undo Log Segment。作用:保存了事务发生之前的数据的一个版本,可以用于回滚,同时可以提供多版本并发控制下的读。需要注意的是,Undo Log 默认存在全局表空间里面,你可以简单的理解成 Undo Log 也是记录在一个 MySQL 的表里面,插入一条 Undo Log 和插入一条普通数据是类似。也就是说,写 Undo Log 的过程中同样也是要写入 Redo Log 的。

执行链路

经过了上面基础知识的铺垫,我们来看一下整个SQL的执行链路:

0、这里跳过连接器,解析器,优化器步骤(上面已经描述过)。

1、首先 Undo Log 会记录此次事物的回滚信息(事物ID,回滚指针)以便于事物进行回滚以及对应的事物隔离 MVCC。

2、然后再操作 BufferPool 中的数据,在操作的同时也要同步把数据操作信息添加到 Change Buffer。而如果这个数据页还没有在内存中的话,在不影响数据一致性的前提下,InnoDB 会先将这些更新操作缓存在 Change buffer 中,这样就不需要从磁盘中读入这个数据页了。在下次查询需要访问这个数据页的时候,将数据页读入内存,然后执行 Change buffer 中与这个页有关的操作。(但某些情况下也需要先将数据页读入内存后再做操作)

3、数据变更操作信息添加到 Change Buffer 中后,操作信息也要同步刷盘到 Redo Log 日志中进行持久存储,以防止宕机导致操作数据丢失。(这里的同步是表示事物提交后才考虑刷盘到磁盘中,事物没提交刷盘没意义,MySQL 默认配置 Redo Log 同步刷盘,如果改成异步刷盘,在突然宕机或掉电情况则会出现数据丢失,内存中 Change Buffer 记录的信息都会丢失)

4、具体的 SQL 操作也需要添加到 Bin Log Cache 中,但 Bin Log Cache 只是记录的逻辑操作信息(具体执行的 DML DDL DQL 语句),没有记录数据变更信息。 Bin Log Cache 是存储在每个线程中的一块空间默认 32K 用于指定存储整个事物生成的 Bin Log Event 的内存大小,对于大事物来讲很可能超过这个参数的设置,则需要开启 Bin Log 临时文件用于存储。

5、客户端事物提交,则进行二阶段提交。首先刷盘 Redo Log 日志记录, 并修改 Redo Log 中的记录的状态为 prepare,然后刷盘 Bin Log 日志记录,Bin Log 刷盘成功后将会把对应的 Redo Log 状态从 prepare 改为 commit。因为必须保持两边日志保持一致性。如果两份日志数据不一致就会导致主从数据不一致,以及各种问题。此时就可以响应客户端事物提交成功,但此时数据(脏页)并未落盘,依然存储在BufferPool中,只是对应的记录日志已经全部成功。

6、MySQL 服务器会异步刷盘(合适的刷页时机)内存中 BufferPool 的脏页数据,只有把脏页数据刷入磁盘中,自此数据才算真正落盘。在异步刷盘中如果出现任何宕机或断电场景,启动后 InnoDB 都可以根据 Redo Log 重做之前的 BufferPool 数据。

合适的刷页时机

1、Redo Log 写不下了。Redo Log 作为持久化的保障,每次更新操作都必须记录,如果 Redo Log 写不下了,那么就必须进行刷页,将数据同步到磁盘中,然后移动 Redo Log 的 CheckPoint 指针,空出新的位置,如果此时内存不紧张,那么刷完的页不需要淘汰出内存。

2、内存放不下数据页了。查询操作会将磁盘中的数据页读入内存,然后再进行返回,如果内存放不下数据页了,那么就必须淘汰最久未使用的数据页,如果该数据页是脏页,那么必须刷进磁盘然后淘汰。

3、数据库空闲时。如果数据库认为当前是一个空闲状态时,那么就会对脏页进行刷页,此时内存如果不紧张,那么刷完后不进行淘汰。

4、数据库正常关闭时。数据库关闭的时候,也会进行刷页,将之前做的修改持久化到硬盘。

总结

MySQL通过一系列日志的顺序写入来提高持久化性能,并且通过 BufferPool 组件把磁盘中的数据映射到内存中进行DML操作,大幅度提高了吞吐性能,并且采用了异步刷盘来规避了不同表,行数据修改导致磁盘随机写入的低性能问题。

文章目录

随心笔记

技术无止境 创新不停驻