一.SQL 语句执行流程
首先我们先从一个简单的更新语句作为切入点吧:
在执行update user set name="小陈" where id =1;的时候改sql语句的执行流程是什么?
- 首先通过连接器连接到mysql客户端,客户端为我们分配会话线程。
- 然后在经过解析器将sql语句进行解析,判断sql语句是否合法,将sql语句转化成为语法树。
- 然后在经过预处理器做语义检查(表名/列名是否存在、权限检查、视图展开、子查询处理等)。
- 然后在经过优化器进行优化,选择走不走索引,走索引的话选择那个索引;以及连表的顺序等。
- 最后经过执行器,按执行计划逐步执行 SQL,并通过存储引擎接口 去调用 InnoDB,完成真正的数据读写(查询、插入、更新、删除)。
二.日志的写入机制
日志的产生就是在执行器执行数据的读写阶段产生的。
InnoDB 是有一个 Pool Buffer 概念的,里面主要存储的是数据页(Pool Buffer 内存里面的数据页淘汰策略是近似于 LRU 算法的)。
- 首先 InnoDB 会先从数据页中查找有没有 id=1 这行数据
- 如果有的话就会直接将 name 更新为"小陈"
- 如果没有的话就会去磁盘里面加载该数据页到 Pool Buffer 之中
- 在更改之前需要将在修改之前,InnoDB 会把旧值写入 Undo Log Buffer 里面(这个后面和 MVCC 一起讲)
- 同时将该数据所在的数据页标识为脏页
此时,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。
我们需要特别注意的是 redo log 的磁盘大小是由 innodb_log_file_size、innodb_log_files_in_group控制的,第一个参数是控制每个 redo log 文件的大小、第二个是控制有多少组 redo log 文件的数量。
redolog的写入方式是循环覆盖写的方式写入的,如下图所示:
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 流程:
- 所有准备提交的事务排进 提交队列(FIFO)
- 按队列顺序,一个个把它们的 binlog cache 内容 顺序 write() 到 binlog 文件
- 这些 write() 的数据此时进入 OS page cache(文件系统缓存) ,还没
fsync到磁盘。
四.两者的区别:
看了上面的介绍的 redo log 和 undo log 执行的入盘的过程。就大概知道了两个日志是基本干什么的。
redo log 是记录的是物理日志,记录的是每个事务对数据页数据的物理的修改,undo log 是一个逻辑日志,他是追加写的,记录的是事务对某行数据下面的逻辑修改。因为是追加的写的,所以会恢复到任意时刻。
| 对比项 | redo log | binlog |
|---|---|---|
| 归属 | 存储引擎 | 层 |
| 记录内容 | 物理日志,记录页的物理修改(如页号、偏移量、修改内容) | 逻辑日志,记录的是执行的操作(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
- Binlog 已经写入并刷盘
- 系统崩溃,redo log 还没 commit
- 主库事务回滚,但从库已经执行 binlog → 主从不一致
情况 B:先写 redo log commit,再写 binlog
- redo log commit 成功
- Binlog 写入失败(或者系统崩溃)
- 主库事务已经生效,但从库没收到 binlog → 主从不一致
两阶段提交
第一阶段:Prepare 阶段
-
事务开始:
BEGIN或START TRANSACTION -
DML 操作:执行数据修改,Redo Log 实时记录物理修改到Redo Log Buffer
-
准备提交:
-
写入 Redo Log PREPARE 记录(标记事务为准备状态)
-
根据
innodb_flush_log_at_trx_commit配置决定是否刷盘:- =1:同步刷盘到磁盘(最安全)
- =2:写入 OS 缓存
- =0:每秒批量刷盘
-
第二阶段:Commit 阶段
-
写入 Binlog:
-
生成完整事务的二进制日志
-
根据
sync_binlog配置决定是否刷盘:- =1:同步刷盘到磁盘(最安全)
- =N:每 N 个事务刷盘一次
- =0:依赖 OS 调度刷盘
-
-
提交完成:
- 写入 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实战四十五讲》