MySQL update 语句的执行流程


MySQL update 语句的执行流程

本文主要分享 MySQL 中 update 语句的执行流程,这其中涉及到了 MySQL 的三大日志:binlog、redo log 和 undo log。这些日志是 MySQL 实现高性能和一致性的保证,也是组建主从和主备架构的前提基础。

update 语句每次都要写磁盘吗?并不是的,这样性能太差了,根本支持不了高并发。要想理解 update 的执行流程,需要先了解下两个关键日志:redo log、binlog。

redo log

redo log 是 InnoDB 引擎特有的日志,确保了数据库的持久性和一致性,主要用于 MySQL 的崩溃恢复。

redo log 是物理日志,因为其是引擎内部日志,所以是没有 sql 语句和记录之类的概念的,记录的内容大致类似于 “在某数据页的某一偏移量上做了什么修改”。

这里你可能有疑问了:数据文件保存在磁盘,日志文件也是保存在磁盘,同样是写磁盘,为什么不直接写数据文件,还要多此一举呢?

比如执行如下 update 语句:

update account set money = money + 100 where user_id = 1;

如果是直接写数据文件,首先要读磁盘找到 user_id = 1 的记录,更新 money 字段后,再写回磁盘,属于随机读写; 如果是写日志文件,不用关心记录的具体位置,只需在日志文件后面追加日志,属于顺序写,磁盘的顺序写性能远大于随机写。

只要写了 redo log 日志,更新就算完成了;InnoDB 会在适当的时候将日志的变更刷新到磁盘的数据文件里去。

redo log 由多个文件组成,写过程类似于循环队列的循环写,如下图所示:

  • write pos 表示当前日志接下来要写的位置
  • check pos 表示已刷新到磁盘的日志的位置
  • 图中绿色部分表示接下来日志可以写入的,黄色部分表示已写入但还未刷新到磁盘的

在具体实现上,日志是先写到 buffer 的,可配置相关参数,在事务提交时将 buffer 日志刷到磁盘去; 此外,InnoDB 还有个后台线程,定时刷 buffer 到磁盘。

binlog

binlog 是 Server 层实现的,故所有引擎都可以使用,用于记录数据库的所有增删改操作。只要日志齐全,就可以用其重放出一个完全一样的数据库。

binlog 是逻辑日志,记录的是 sql 语句的原始逻辑,类似于 “给 user_id = 1 的记录的 money 字段加 100”。

binlog 日志写磁盘的过程如下图所示:

  • 为了保证事务日志的原子性,每个线程都有其单独的 cache,在事务提交时,用 write 系统调用一次性刷到 page cache 去
  • 至于何时如何调用 fsync 写到磁盘,可通过 sync_binlog 参数配置

binlog 和 redo log 的对比如下:

  redo log binlog
实现层 InnoDB Server
日志类型 物理日志 逻辑日志
写机制 循环写 追加写
用途 故障恢复 主从复制、备份还原

两阶段提交

update 语句主要做了三件事:更新 buffer pool,写 redo log,写 binlog。

由于 redo log 和 binlog 是两个独立的逻辑,为了保证日志和数据的一致性,MySQL 采用了两阶段提交机制,具体流程如下图所示:

MySQL 重启后会去扫描 redo log 文件,分三种情况进行处理:

  • redo log 里面事务完整,即有 commit 标识,则直接提交(情况 1)
  • redo log 里面事务不完整,只有 prepare,则去判断对应的 binlog 是否完整存在
    • 若是,则提交事务(情况 2)
    • 否则,回滚事务(情况 3)

假设 MySQL 分别在 update 执行过程中的各个中间时间点崩溃了,来分析下重启后是如何进行恢复的。

  • t1 时崩溃,redo log 和 binlog 都还未写,故一致;重启后内存的新数据丢失,从磁盘载入旧数据,update 执行不生效
  • t2 时崩溃,符合情况 3,回滚事务,update 执行不生效
  • t3 时崩溃,符合情况 2,提交事务,update 执行生效

可见,只要 redo log 写了 prepare 标识,且 binlog 正常写入了,此时更新操作就算生效了,后续的 commit 标识更新是否完成也不影响结果的。

参考资料