MySQL 中的 DML 语句执行流程,你理解的跟我一样吗?

时间:2022-07-22
本文章向大家介绍MySQL 中的 DML 语句执行流程,你理解的跟我一样吗?,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

redo log 和 bin log

在DML语句执行的过程中,主要会涉及到两个日志——redo log和bin log,而这两个日志是数据库 WAL (Write Ahead Logging,先写日志再写磁盘提高效率) 技术的两大主角。下面我来介绍一下这两个日志。

redo log(重做日志)

  • 类型:数据页级别的,记录的是物理日志 (比如某个数据做了是什么更改)。
  • 作用:确保事务的持久性,防止在数据库 crash 的时候上有脏页未写入磁盘,在重启 MySQL 的时候会根据 redo log 进行重做。
  • 产生时间:在事务开始的时候就会产生,而 redo log 的落盘不是在事务提交的时候,而是在事务执行过程中就会进行 redo log 的写入
  • 释放时间:当内存中的脏页都写入磁盘了,那么相应的 redo log 就会被覆盖

注意: 这里为什么说是覆盖是因为 redo log 写日志的特性。redo log 的大小是固定的,所以写 redo log 是循环的覆盖写,你可以理解为,一个环形文件如下图。

其中,整个环形是两个文件构成的(文件个数和文件大小你可以自己指定),两个文件像连在一起一样,其中绿色标识的是 check point ,用来表示当前日志被清理到的头(可以理解为当前有用的 redo log的头),而 write position 代表着当前写入位置(就是当前有用 redo log 的尾)。如果数据库有更新那么 write position 就会向前推,如果 write position 要追上 check point 的时候,那么数据库就会停下来将 check point 向前推(就是清理,此时就是将内存中的脏页进行写入磁盘,对应着上面的释放时间)。

bin log

bin log 默认是关闭的,需要在配置文件自己设置。

  • 类型:数据行级别的,逻辑日志 (有两种形式,一种是 statement ,记录着sql语句,另一种是 row ,记录着数据行更新前和更新后的内容)。
  • 作用:主要用于实现 MySQL 主从复制,数据备份和数据恢复。
  • 产生时间:在一个事务提交的时候会被写入磁盘。
  • 释放时间:是追加写的,所以不会被覆盖,无释放时间。

DML 的执行流程

如果你对 MySQL 的这两个日志没有了解过的话,上面的特性是很难理解的,如果结合着 DML 语句执行流程就会好理解一点,比如我现在要在数据库的表中更新 id = 1 这一行中的 value 字段。

update table set value = value + 1 where id = 1;

这个时候更新的大致流程就是这样的

  1. 首先 MySQL 的 server 层会通过调用执行器去获取指定数据行
  2. 苦差事当然交给引擎(这里是innodb)来做,InnoDB 首先会去查看当前内存中是否存在该数据行,如果存在之间从内存中取出,如果不在那么会从磁盘中 load 到内存之后再从内存中取出相应数据行。
  3. 然后将数据行进行更新并将新行写入内存中(注意此时肯定会产生脏页,后面会了解到)。
  4. 之后就会开始写日志,首先是 redo log的写入(此时进入prepare状态*)。
  5. 第二个写 bin log。
  6. 最后进行事务的提交。

注意:这里的事务提交不仅仅是简单的 commit; ,因为这里只是简单的 update 语句,自己本身就是一个事务,所以这里的 commit; 是隐式的。而这里所说的 commit; 还包含了 redo log 的状态转换——从 prepare 到 commit 状态,这是一个很重要的点,后面我会详细解释,你这里需要记住有这么一个东西。

到这里我们来简单总结一下:

DML语句的执行和两个日志——redo log、bin log有着很大的关系,因为需要提高数据库的性能,MySQL 采用了一种 WAL(先写日志再写磁盘) 技术,其中就使用到了这两个日志。主要的流程如下,MySQL会从内存中获取相应的数据行(如果没有先从磁盘 load 到内存中),然后将数据行进行更新并将新行写入内存后进行redo log的写入和 bin log 的写入,在一开始 redo log 是处于 prepare 状态,只有在 bin log 写完然后进行事务提交的时候才会处于 commit 状态

不仅仅是那么简单

这个时候你肯定有几个疑问。

redo log是如何保证事务的持久性的?(即当事务执行期间发生 crash ,redo log是如何保证 crash-safe 能力)

bin log是如何完成数据恢复和主从复制的?

上面redo log的 prepare 和 commit 两个状态的存在意义是什么?

为什么要存在两个日志,只要一个不行吗?

为什么 WAL 技术能提高数据库性能?

下面我来慢慢回答这些问题。

bin log是如何完成数据恢复和主从复制的

首先最简单的是第二个,bin log是如何完成数据恢复和主从复制的?,看了上面的介绍大家应该也知道了,bin log有这么几个特性。

  1. 追加写,不会像 redo log 那样被覆盖
  2. 记录了完整的逻辑日志,可以利用它进行快速的数据恢复。

所以,当我们要进行数据恢复的时候可以 使用 bin log 为基础备份出一个和原库一样的备库。当我们要进行主从复制的时候,可以使用 bin log 进行 主从库的同步。

redo log是如何保证事务的持久性的

提醒一下,我这里使用的是 “双一配置”(即innodb_flush_log_at_trx_commit = 1 和 sync_binlog = 1 )。sync_binlog = 1的意思是 在事务每次提交的时候都会进行 bin log的持久化。而 innodb_flush_log_at_trx_commit = 1 的意思是事务提交的时候都将 redo log 持久化到磁盘。 所以这里的双一就是在每次事务提交的时候都会进行 redo log 和 bin log 的持久化,这两个参数的其他配置可以去参考其他文章,这里不做过多涉及。

这就不得不提到两阶段提交了,这时候还会牵扯到上面的另一个问题redo log的 prepare 和 commit 两个状态的存在意义是什么?

两阶段提交是为了在数据库发生 crash 之后重启恢复能够保证事务完整性。比如这个时候我们正在进行上面的 update 语句,然后此时数据库宕掉了。为了你好理解我在将上面的流程图拿过来。

你会发现,我这里标注了三个时刻,就是我们宕机事务可能会执行到的时刻。

首先我先将规则写在前面,你们可以对照着去理解。

  1. 如果redo log里面的事务是完整的,也就是已经有了commit标识,则直接提交
  2. 如果redo log里面的事务只有完整的prepare,则判断对应的事务binlog是否存在并完整,如果存在并完整则继续提交事务,如果不是那么回滚事务

我们来看一下上面几个时刻。

  • 时刻A:显而易见,此时日志都没写,东西都在内存中,重启肯定会回滚(就当什么事都没发生)。
  • 时刻B:此时redo log 已经写盘,但是只是处于 prepare 状态,如果这时候发生 crash ,那么 bin log还没写 and redo log 还处于 prepare 状态,此时事务会回滚。
  • 时刻C:此时 bin log 已经写盘,redo log 已写盘并处于 prepare 状态,此时事务会根据 redo log 和 bin log 继续提交。

为什么会使用两阶段提交呢?

我们可以用反证法。

如果redo log在前 bin log在后,在redo log写完之后宕机,那么重启之后主库可以根据 redo log 进行数据恢复,但是这时候因为 bin log 是没有写的,所以如果使用 bin log 进行备份,那么备库会少了这一个事务。

如果bin log在前 redo log在后,在bin log写完之后宕机,那么就会导致后面使用 bin log做备份的时候多出这个事务。

所以如果不使用 两阶段提交 ,那么就会出现 bin log备份出来的数据库和原库的数据不一致

所以redo log 结合着上面的两阶段提交就解决了,crash-safe 能力和 原库备库一致性。

为什么要存在两个日志,只要一个不行吗

你可能会想,如果不引入两个日志就没有必要进行 两阶段提交 了,这样岂不是快哉?!

我们可以继续利用反证法去证明。

如果我们只有 redo log,你知道 redo log 大小是固定且是可以被覆盖的,所以如果用来做数据备份是不可以的,因为它仅仅会记录当前内存中数据页的情况。而且 redo log是 innodb 层面的,它不是 数据库层面的,如果当你使用的另外一个数据库不是 以 innodb 作为存储引擎的话,是根本进行不了同步的

如果我们只有 bin log,我们知道bin log是数据行级别且记录的是逻辑日志,所以是没有“数据页恢复”的功能的

所以,这里我们还是需要使用 redo log 和 bin log。

redo log的 prepare 和 commit 两个状态的存在意义是什么

这里我们还得引出一个点,我们上面提到了 redo log 的落盘是在事务执行过程中。那么,redo log究竟具体在什么时候会进行日志的持久化呢?

具体有三种

  1. redo log buffer占用的空间要达到 innodb_log_buffer_size一半的时候,会有后台线程主动将日志写盘。注意,由于这个事务并没有提交,所以这个写盘动作只是write,而没有调用fsync,也就是只留在了文件系统的page cache
  2. 后台线程会做每秒的轮询将 redo log buffer write到文件系统的page cache并调用 fsync 进行写盘
  3. 并行的事务提交的时候,顺带将这个事务的redo log buffer持久化到磁盘

这里我们提到了两个很关键的词:redo log bufferpage cache

那么这个redo log buffer 和 page cache 又是干什么的呢?这里我们需要将一下 redo log 的三种形态。如下图

你可以想一下,一个事务会有多个 DML 语句,而每次 DML 语句都进行写盘会进行大量的系统调用导致资源浪费和时间浪费,所以每次 DML 语句的时候只是会将 日志先缓存到内存中的 redo log buffer 中去,而最终调用 commit 的时候会将 redo log buffer 中的内容写入磁盘。

而 page cache 的存在是为了加快 fsync 系统调用的速度,我们知道每次事务 commit 的时候都会进行两次 fsync 调用(双一配置),而主要的 redo log 一般会提早进行 write 到文件系统缓冲中,所以这样会加快写盘速度。

在这里,我放了一张加入“缓存”的DML更新流程的图。

其中 bin log cache你也可以理解为缓存,而且因为bin log是逻辑日志,所以一个事务的bin log是不能被拆开的,所以我们的 bin log cache 是存放在每个线程的空间里的,相互独立。如下图

注意:redo log在最后只是 write 进行了写入文件系统的 page cache 中是因为这个时候已经可以保证 crash-safe能力了,就不需要再额外进行写盘操作了,如果不理解可以结合上面的两阶段提交规则去理解。

所以这里 redo log 的两种状态其实也是两阶段提交的重要组成部分,我们可以知道,在bin log未写盘之前 redo log会先进行写盘,但是这次写盘的状态还只是 prepare 状态,只有在bin log 写完之后 才会最终将状态变为 commit,并且这里不再进行写盘操作,而是通过后面进行写盘的时候顺便写入。

为什么 WAL 技术能提高数据库性能

我们这个时候可能还会有一个疑惑,在“双一配置”下,每次事务的提交都需要进行两次 fsync 系统调用,这样对于数据库的压力会是很大的。

我们知道 WAL 技术能提高数据库性能的一个原因是——日志文件是顺序写的,磁盘的顺序写要比随机写快很多。但是对于每次事务进行两次系统调用这点,WAL 有没有做什么优化呢?

答案是有的,试想一下,如果存在多个事务并发的情况下,此时会出现多个事务的 redo log buffer都已经写好,这时候 InnoDB 会使用 LSN(log sequence number)日志逻辑序列号,LSN 是单调递增的,用来对应 redo log的写入点,每次写入 length 长度的 redo log,LSN 就会增加 length。

比如此时有,三个并发事务trx1,trx2,trx3。我们可以看到 trx1 是第一个到达的,而 trx1 要进行写盘的时候已经有三个事务在队列中了,所以此时 trx1 去写盘的时候带的 LSN 就会变成 200,那么此时进行写盘,就会将trx1,trx2,trx3都写入磁盘中了,这里仅进行了一次系统调用。

所以这里 WAL 技术会对一些需要系统调用写盘的地方进行一些优化,尽量减少IO。对于这个问题就可以总结为两点:

  1. 通过日志的顺序写提高磁盘效率
  2. 通过组提交减少系统调用

总结

这里我们主要介绍了在 MySQL 中 一条 DML 语句是如何执行的,redo log 、bin log又是如何和 DML 语句、事务联系在一起的。其中还介绍了 redo log的三种形态和两种提交状态,bin log的线程cache,LSN组提交实现等。

总的来说就是 MySQL 在进行 DML 语句的时候会先写日志缓存(为了事务多个 DML 语句而不多次进行写盘操作),等到事务提交的时候会进行日志的真正落盘(“双一配置”),其中还使用了两阶段提交加上redo log的两种提交状态来实现 crash-safe能力 和 redo log,bin log 的同步