从日志的角度了解Mysql的核心机制

146 阅读13分钟

一.SQL 语句执行流程

首先我们先从一个简单的更新语句作为切入点吧:

image.png

在执行update user set name="小陈" where id =1;的时候改sql语句的执行流程是什么?

  1. 首先通过连接器连接到mysql客户端,客户端为我们分配会话线程。
  2. 然后在经过解析器将sql语句进行解析,判断sql语句是否合法,将sql语句转化成为语法树。
  3. 然后在经过预处理器做语义检查(表名/列名是否存在、权限检查、视图展开、子查询处理等)。
  4. 然后在经过优化器进行优化,选择走不走索引,走索引的话选择那个索引;以及连表的顺序等。
  5. 最后经过执行器,按执行计划逐步执行 SQL,并通过存储引擎接口 去调用 InnoDB,完成真正的数据读写(查询、插入、更新、删除)。

二.日志的写入机制

日志的产生就是在执行器执行数据的读写阶段产生的。

InnoDB 是有一个 Pool Buffer 概念的,里面主要存储的是数据页(Pool Buffer 内存里面的数据页淘汰策略是近似于 LRU 算法的)。

  1. 首先 InnoDB 会先从数据页中查找有没有 id=1 这行数据
  2. 如果有的话就会直接将 name 更新为"小陈"
  3. 如果没有的话就会去磁盘里面加载该数据页到 Pool Buffer 之中
  4. 在更改之前需要将在修改之前,InnoDB 会把旧值写入 Undo Log Buffer 里面(这个后面和 MVCC 一起讲)
  5. 同时将该数据所在的数据页标识为脏页

此时,InnoDB 也会生成一条 redo log(redo log 是物理日志,记录的是那个数据页的那个部分被更改了),此时的 redo log 日志是不会直接被刷盘到 redo log 磁盘文件里面的,而是先写入到 Redo Log Buffer 内存之中。

然后根据 MySQL 的配置参数 innodb_flush_log_at_trx_commit决定什么时候将 Redo Log Buffer 内存同步到磁盘文件上面去。其中innodb_flush_log_at_trx_commit这个常见的参数配置有三种:

参数值参数对应的实现方法
0事务提交时,不刷盘,只是把 Redo Log Buffer 的内容写到操作系统的缓存(并不是磁盘),每秒钟后台线程才会刷一次磁盘。存在丢事务风险。
1事务提交时,立刻把 Redo Log Buffer 写到操作系统缓存,并同步刷盘到磁盘 Redo Log 文件,事务最安全,但性能稍低。
2事务提交时,只写到操作系统缓存,不立即刷盘,后台线程每秒刷一次到磁盘。比1快,但断电可能丢1秒的数据。

如果不特别配置的话,innodb_flush_log_at_trx_commit这个参数默认就是1。

image.png

我们需要特别注意的是 redo log 的磁盘大小是由 innodb_log_file_sizeinnodb_log_files_in_group控制的,第一个参数是控制每个 redo log 文件的大小、第二个是控制有多少组 redo log 文件的数量。

image.png redolog的写入方式是循环覆盖写的方式写入的,如下图所示:

image.png write pos 是当前记录的位置,一边写一边后移,写到第 3 号文件末尾后就回到 0 号文件开头。checkpoint 是当前要擦除的位置,也是往后推移并且循环的,擦除记录前要把记录更新到数据文件。

这时候就有一个问题了:​​你是怎么保证 Pool Buffer 中的脏页在写入到磁盘进行持久化的时候,你的 checkpoint 就会给当前已被持久化的数据生成的 redo log 日志擦去呢?​

这就涉及到了一个东西就是:​​LSN(Log Sequence Number)​​,顾名思义就是一个全局的自增的日志 ID,当执行当前的更新操作的时候,会生成一个 LSN 写入到数据页中。

在 Buffer Pool 中,每个被修改过的脏页都会被加入到 Flush List 链表中。Flush List 链表中的脏页按照其修改的 LSN 从小到大排序(即按照修改发生的顺序排序)。LSN 也会被写入到生成的 redo log 里面,然后按照 LSN 从小到大写入到 redo log 磁盘文件里面去。

脏数据页的入磁盘是根据 Flush List 链表顺序入磁盘的,所以说 checkpoint 就能顺序的将已经入磁盘的 redo log 日志擦除去掉。

Checkpoint 的触发不只是刷盘完成​

​触发条件​​说明​
​日志空间不足​Redo Log 使用率 > 75% 时强制推进
​定时触发​默认每秒执行一次
​实例关闭​执行 Sharp Checkpoint

三.Binlog写入机制

说完了 redo log,咱们再说一下 binlog 吧。

binlog 和 redo log 不同在于,redo log 是物理日志,binlog 是逻辑日志,且 binlog 的写入方式是追加写,且 binlog 是 MySQL 的服务层生成的。

binlog 的写入是逻辑写入,但是并不是原封不动的将执行的 SQL 语句写进去,写的模式是三种格式的: binlog的写入是逻辑写入,但是并不是原封不动的将执行的sql语句写进去,写的模式是三种格式的:

​格式​​记录内容​
​STATEMENT​原始 SQL 语句文本
​ROW​每一条数据修改后的行镜像
​MIXED​混合模式:一般是 STATEMENT,有需要时自动切 ROW
row格式下保存的行镜像其实就是:这条语句修改了哪几行,每一行修改前和修改后的完整内容。所以体积要大一点。

binlog 因为是追加写的,所以就简单的多了:binlog 也是不会直接写入到 binlog 文件磁盘里面的,他也是有一个缓存的概念,但是和 redo log 不一样的是,redo log 的缓存池是全局共享的缓存池,而 binlog 的是一个事务对应一个缓存区(binglog cache)。

这个 binlog cache 写入 binlog 日志里面的也是 sync_binlog这个参数进行控制的:

​sync_binlog​​说明​
​1​事务提交后立马刷盘到binlog日志文件里面
​>1​累计多次事务后在进行刷盘

看完上面的可能就要问了:那我 sync_binlog设置为 10,就是说有累积十个事务的 binlog 之后在进行刷盘操作,你是怎么保证他是按照事务的提交顺序写入到磁盘文件的?(这个举一个例子就可以了)

事务 COMMIT 时,会经过 binlog group commit 流程

  1. 所有准备提交的事务排进 提交队列(FIFO)
  2. 按队列顺序,一个个把它们的 binlog cache 内容 顺序 write() 到 binlog 文件
  3. 这些 write() 的数据此时进入 OS page cache(文件系统缓存) ,还没 fsync 到磁盘。

四.两者的区别:

看了上面的介绍的 redo log 和 undo log 执行的入盘的过程。就大概知道了两个日志是基本干什么的。

redo log 是记录的是物理日志,记录的是每个事务对数据页数据的物理的修改,undo log 是一个逻辑日志,他是追加写的,记录的是事务对某行数据下面的逻辑修改。因为是追加的写的,所以会恢复到任意时刻。

对比项redo logbinlog
归属存储引擎
记录内容物理日志,记录页的物理修改(如页号、偏移量、修改内容)逻辑日志,记录的是执行的操作(statement 或 row image)
生命周期循环写入,空间固定追加写入,空间不断增长
恢复用途用于崩溃恢复(crash recovery)用于主从复制、增量备份、时间点恢复等

为什么 binlog 能恢复到任意时刻,但 redo log 不行?

redo log 的特点

  • redo log 是 循环写 的,容量固定。
  • 它只保证崩溃时恢复一致性,即确保已经提交的事务最终能落盘。
  • redo log 没有记录「事务提交的时间线」,也不会永久保存历史修改。
  • 因为它会被覆盖掉旧记录,所以你无法通过 redo log 回溯到过去某个时间点的数据。

binlog 的特点

  • binlog 是 追加写 的,不会覆盖旧记录

  • 记录的是每个事务的完整变更内容(row image 或 SQL 语句)。

  • 每个事务在 binlog 中的顺序就是实际提交的顺序。

  • 因为有完整的时间线和变更历史,所以可以:

    • 重放所有事务达到任意时间点(PITR,Point-In-Time Recovery)
    • 用于主从复制,从库可以按顺序重放 binlog

举一个简单的例子就是说:

  • redo log 只是告诉你「第 1234 页第 89 个字节改成了 9」
  • binlog 会告诉你「在 12:30:01 执行了 UPDATE user SET balance=balance+10 WHERE id=1
    redo log 没法知道你这个修改是属于哪一笔业务操作,也无法判断你要恢复到的「业务时刻」

说到了这里,其实还是有一个疑问就是:redolog和binlog两个日志写的顺序是随便的吗?是先写那个就可以吗?如果我先写redolog,再写binlog.先写binlog,在写redolog会有什么影响呢?

五.两阶段提交

情况 A:先写 binlog,再写 redo log commit

  1. Binlog 已经写入并刷盘
  2. 系统崩溃,redo log 还没 commit
  3. 主库事务回滚,但从库已经执行 binlog → 主从不一致

情况 B:先写 redo log commit,再写 binlog

  1. redo log commit 成功
  2. Binlog 写入失败(或者系统崩溃)
  3. 主库事务已经生效,但从库没收到 binlog → 主从不一致

两阶段提交

第一阶段:Prepare 阶段

  1. ​事务开始​​:BEGIN或 START TRANSACTION

  2. ​DML 操作​​:执行数据修改,Redo Log 实时记录物理修改到Redo Log Buffer

  3. ​准备提交​​:

    • 写入 Redo Log PREPARE 记录(标记事务为准备状态)

    • 根据 innodb_flush_log_at_trx_commit配置决定是否刷盘:

      • =1:同步刷盘到磁盘(最安全)
      • =2:写入 OS 缓存
      • =0:每秒批量刷盘

第二阶段:Commit 阶段

  1. ​写入 Binlog​​:

    • 生成完整事务的二进制日志

    • 根据 sync_binlog配置决定是否刷盘:

      • =1:同步刷盘到磁盘(最安全)
      • =N:每 N 个事务刷盘一次
      • =0:依赖 OS 调度刷盘
  2. ​提交完成​​:

    • 写入 Redo Log COMMIT 记录(标记事务为已提交状态)
    • 返回客户端提交成功

六.崩溃恢复机制

当mysql进程挂的时候,在进行恢复和回滚的时候,会扫描Redolog和Binlog ,主要依赖下面的决策(这里需要举一个例子):

redo 状态binlog 存在?恢复决策
prepare继续提交(重做)
prepare回滚
commit重做

七.Undo Log 与事务回滚

上面已经详细的说明了 redo log 和 bin log 的写入机制以及时机,在事务进行回滚的时候,他是如何知道该事务上一个事务是什么呢?这就需要我们的 undo log 了.

undo log 记录的信息​​大概如下:

  • 首先记录的是该事务的操作类型:更新、删除、增加
  • 如果是更新的话记录被修改列在修改前的值(旧值),以及主键值(用于定位行)
  • 如果是删除操作的话会保留删除前所有的旧数据
  • 如果是新增操作的话就记录一个主键值就可以了
  • 此外事务 ID(trx_id):执行该操作的事务 ID
  • 回滚指针(roll_ptr):指向该行上一个版本的 Undo Log 记录,形成版本链.

MySQL 对于每张表都有几个隐藏字段:

struct row {
    DB_TRX_ID db_trx_id;      // 最后修改的事务ID
    DB_ROLL_PTR db_roll_ptr;  // 指向Undo Log的指针
    DB_ROW_ID db_row_id;      // 行ID(隐式主键)
    // 用户数据列...
};

定位事务修改​​:

  • InnoDB 通过事务 ID(trx_id)来标识一个事务。每个事务在开始时会被分配一个唯一的事务 ID
  • 在事务执行过程中,每次对数据进行修改(INSERT、UPDATE、DELETE)时,都会记录一个 Undo Log,并且将这个 Undo Log 的指针(roll_ptr)保存在数据行的隐藏字段 db_roll_ptr 中。同时,数据行也会记录修改它的事务 ID(db_trx_id)
  • 同一个事务内的多个修改操作,会形成一个 Undo Log 链,每个 Undo Log 记录都包含指向前一个版本的指针(prev_roll_ptr),从而构成一个版本链

回滚过程​​:

  • 从当前事务的最后一次修改开始(即最新的 Undo Log 记录),按照操作的逆序(后进先出)依次应用 Undo Log 进行回滚

  • 对于每个修改操作:

    • UPDATE:将数据行恢复到修改前的状态(使用 Undo Log 中记录的旧值)
    • DELETE:实际上执行一个 INSERT 操作,将删除的行重新插入表中(使用 Undo Log 中记录的完整行数据)
    • INSERT:执行 DELETE 操作,删除之前插入的行(因为插入的行在回滚时应该被删除)
  • 每回滚一个操作,就释放该操作持有的锁(如行锁)

  • 回滚完成后,该事务产生的 Undo Log 不再需要,会被标记为可清理状态

  • 后台的 Purge 线程会负责清理这些 Undo Log,释放空间

八、总结:事务从开始到结束的完整生命周期

1. 事务开始阶段

  • 客户端发送 BEGIN或 START TRANSACTION命令
  • MySQL 分配事务 ID,初始化各种日志缓冲区

2. DML 操作执行阶段

  • 执行数据修改操作(INSERT/UPDATE/DELETE)
  • 生成对应的 Undo Log 记录,形成版本链
  • 数据页修改,标记为脏页,加入 Flush List
  • 实时生成 Redo Log 记录物理修改

3. 准备提交阶段(Prepare)

  • 写入 Redo Log PREPARE 记录
  • 根据 innodb_flush_log_at_trx_commit配置决定 Redo Log 刷盘策略

4. 正式提交阶段(Commit)

  • 生成 Binlog 日志
  • 根据 sync_binlog配置决定 Binlog 刷盘策略
  • 写入 Redo Log COMMIT 记录
  • 返回客户端提交成功

5. 后续处理阶段

  • 脏页通过 Checkpoint 机制逐步刷盘
  • 无用的 Undo Log 由 Purge 线程清理
  • Binlog 文件根据过期策略清理

6. 异常处理(崩溃恢复)

  • 扫描所有 Prepare 状态的 Redo Log
  • 通过 Binlog 判断事务是否应该提交
  • 完成未提交事务的回滚或已提交事务的重做

7. 日志清理

  • Redo Log 循环覆盖写入
  • Binlog 追加写入,按策略清理过期文件
  • Undo Log 由 Purge 线程清理已提交事务的版本

这样完整的流程保证了 MySQL 的事务特性(ACID),即使在系统崩溃的情况下也能保证数据的一致性和持久性。

参考文章:《MySQL实战四十五讲》