你知道 MySQL update 语句背后藏着哪些不可告人的秘密?

681 阅读8分钟

本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

前面的文章我们已经对MySQL的查询语句的执行流程进行了说明,感兴趣的可以去看看:

面试官:MySQL 是如何执行一条查询语句的?

本篇文章我们来聊聊 MySQL更新语句的执行原理。 更新流程和查询流程有什么不同呢?

基本流程也是一致的,也就是说,它也要经过解析器、优化器的处理,最后交给执行器。

区别就在于拿到符合条件的数据之后的操作。

养成好习惯,先三连走一波,码字不易,感谢你们的每一个点赞、关注和收藏。

缓冲池Buffer Pool

首先,InnnoDB 的数据都是放在磁盘上的,存储引擎要操作数据,必须先把磁盘里面的数据加载到内存里面。

这里就有个问题,是不是我们需要的数据多大,我们就一次从磁盘加载多少数据到内存呢?

磁盘 I/O 的读写相对于内存的操作来说是很慢的。如果我们需要的数据分散在磁盘的不同的地方,那就意味着会产生很多次的 I/O 操作。

所以,无论是操作系统的文件管理系统也好,还是存储引擎也好,都有一个预读取的概念。也就是说,当磁盘上的一块数据被读取的时候,很有可能它附近的位置也会马上被读取到,这个就叫做局部性原理。那么这样,我们干脆每次多读取一点,而不是用多少读多少。

我们设定了一个存储引擎从磁盘读取数据到内存的最小的单位,叫做页。操作系统也有页的概念。操作系统的页大小一般是 4K,而在 InnoDB 里面,这个最小的单位默认是 16KB 大小,它是一个逻辑单位。

我们要操作的数据就在这样的页里面,数据所在的页叫数据页。我们对于数据页的操作,不是每次都直接操作磁盘,因为磁盘的速度太慢了。

使用了一种缓冲池的技术,也就是把磁盘读到的页放到一块内存区域里面。下一次读取 相同的页,先判断是不是在这个内存区域里面,如果是,就直接读取,不用再次访问磁盘。这个内存区域就叫 Buffer Pool。

接下来让我们先来看下缓冲池在整个 mysql 架构里处于什么样的地方,有一个宏观的认识。

修改数据的时候,先修改缓冲池(Buffer Pool)里面的页。内存的数据页和磁盘数据不一致的时候,我们把它叫做脏页。InnoDB 里面有专门的后台线程把 Buffer Pool 的数据写入到磁盘,每隔一段时间就一次性地把多个修改写入磁盘,这个动作就叫做刷脏。

InnoDB 内存结构

上图中可以看到很多的文件(redo log、binlog、undo log),下面我们就来分析一下它们。

redo log

思考一个问题:如果 Buffer Pool 里面的脏页还没有刷入磁盘时,数据库宕机或者重启,这些数据将会丢失。

为了避免这个问题,InnoDB 把所有对页面的修改操作专门写入一个日志文件,并且在数据库启动时从这个文件进行恢复操作(实现 crash-safe)——用它来实现事务的持久性。

image-20211013184428262

这个文件就是磁盘的 redo log(叫做重做日志),对应于/var/lib/mysql/目录下的 ib_logfile0ib_logfile1,每个 48M。

这 种 日 志 和 磁 盘 配 合 的 整 个 过 程 , 其 实 就 是 MySQL 里 的 WAL 技 术(Write-Ahead Logging),它的关键点就是先写日志,再写磁盘。

问题:同样是写磁盘,为什么不直接写到 db file 里面去?为什么先写日志再写磁盘?

我们先来了解一下随机 I/O 和顺序 I/O 的概念。

如果我们所需要的数据是随机分散在磁盘上不同页的不同扇区中,那么找到相应的数据需要等到磁臂旋转到指定的页,然后盘片寻找到对应的扇区,才能找到我们所需要 的一块数据,一次进行此过程直到找完所有数据,这个就是随机 IO,读取数据速度较慢。

假设我们已经找到了第一块数据,并且其他所需的数据就在这一块数据后边,那么就不需要重新寻址,可以依次拿到我们所需的数据,这个就叫顺序 IO。

刷盘是随机 I/O,而记录日志是顺序 I/O(连续写的),顺序 I/O 效率更高。因此先把修改写入日志文件,在保证了内存数据的安全性的情况下,可以延迟刷盘时机,进而提升系统吞吐。

这个 redo log 有什么特点?

  1. redo log 是 InnoDB 存储引擎实现的,并不是所有存储引擎都有。支持崩溃恢复是 InnoDB 的一个特性。

  2. 不是记录数据页更新之后的状态,而是记录这个页做了什么改动,属于物理日志。

  3. redo log 的大小是固定的,前面的内容会被覆盖,一旦写满,就会触发 redo log 到磁盘的同步,以便腾出空间记录后面的修改。

除了 redo log 之外,还有一个跟修改有关的日志,叫做 undo log。redo log 和 undo log 与事务密切相关,统称为事务日志。

undo log

undo log(撤销日志或回滚日志)记录了事务发生之前的数据状态(不包括 select)。如果修改数据时出现异常,可以用 undo log 来实现回滚操作(保持原子性)。

在执行 undo 的时候,仅仅是将数据从逻辑上恢复至事务之前的状态,而不是从物理页面上操作实现的,属于逻辑格式的日志。

undo Log 的数据默认在系统表空间 ibdata1 文件中,因为共享表空间不会自动收缩,也可以单独创建一个 undo 表空间。

有了这些日志之后,我们来总结一下一个mysql更新语句执行的流程,这是一个简化的过程。name 原值为 mayun

update user set name = 'jiangwang' where id=1;
  1. 事务开始,从内存或磁盘取到这条数据,返回给 Server 的执行器;

  2. 执行器修改这一行数据的值为 jiangwang;

  3. 记录 name=mayun 到 undo log;

  4. 记录 name=jiangwang 到 redo log;

  5. 调用存储引擎接口,在内存(Buffer Pool)中修改 name=jiangwang;

  6. 事务提交。

内存和磁盘之间,工作着很多后台线程,什么是后台线程呢?

后台线程的主要作用是负责刷新内存池中的数据和把修改的数据页刷新到磁盘。后台线程分为:master threadIO threadpurge threadpage cleaner thread

Binlog

除了 InnoDB 架构中的日志文件,MySQL 的 Server 层也有一个日志文件,叫做 binlog,它可以被所有的存储引擎使用。

binlog 以事件的形式记录了所有的 DDL 和 DML 语句(因为它记录的是操作而不是数据值,属于逻辑日志),可以用来做主从复制和数据恢复

跟 redo log 不一样,它的文件内容是可以追加的,没有固定大小限制。

在开启了 binlog 功能的情况下,我们可以把 binlog 导出成 SQL 语句,把所有的操作重放一遍,来实现数据的恢复。

binlog 的另一个功能就是用来实现主从复制,它的原理就是从服务器读取主服务器的 binlog,然后执行一遍。

有了这两个日志之后,我们来看一下一条更新语句是怎么执行的(redo 不能一次写入了):

image-20211013192144811

例如一条语句:update user set name='小马' where id=1;

  1. 先查询到这条数据,如果有缓存,也会用到缓存。

  2. 把 name 改成小马,然后调用引擎的 API 接口,写入这一行数据到内存,同时记录 redo log。这时 redo log 进入 prepare 状态,然后告诉执行器,执行完成了,可以随时提交。

  3. 执行器收到通知后记录 binlog,然后调用存储引擎接口,设置 redo log为 commit 状态。

  4. 更新完成。

小结

MySQL的更新语句的执行流程的原理,上面也已经说得很清楚了,最后我们总结一下重点:

  1. 先记录到内存(buffer pool),再写日志文件。

  2. 记录 redo log 分为两个阶段(prepare 和 commit)。

  3. 存储引擎和 server 分别记录不同的日志。

  4. 先记录 redo,再记录 binlog。