Skip to content

MySQL 日志、事务提交与崩溃恢复

很多人学 MySQL 日志时,会把它记成几个名词:

  1. redo log
  2. undo log
  3. binlog

但如果只是背名字,往往还是很容易卡在关键区别上:

  1. 它们分别解决什么问题
  2. 为什么一个写操作要记这么多日志
  3. redo logbinlog 到底有什么区别
  4. 两阶段提交是为了解决什么
  5. 数据库宕机后,为什么有的事务能恢复,有的不能

这篇文章就专门把这条线讲清楚。

1. 🌟 先记结论:三类日志分别负责什么

如果只想抓住最核心的区别,可以直接记:

  1. undo log:为了回滚和 MVCC
  2. redo log:为了崩溃恢复
  3. binlog:为了主从复制和归档恢复

它们不是重复设计,而是在解决三个不同的问题:

  1. 事务还没完成时,怎么撤销已经做过的修改
  2. 数据页还没来得及刷盘时,机器宕了怎么恢复
  3. 这次变更怎么同步给从库,或者之后做数据回放

2. 为什么 MySQL 不能只靠数据页本身

很多人第一次接触数据库时,会自然地想:既然最终数据都在表文件里,那直接改数据页不就行了?

问题在于,磁盘写很慢,而且一次事务提交不可能要求:

  1. 立刻把所有相关数据页同步写盘
  2. 还要保证宕机后绝对不丢
  3. 同时保持高并发吞吐

如果每次提交都强制把所有脏页立刻刷盘,代价会非常高。

所以数据库采用了更符合工程现实的做法:

  1. 先在内存页里修改
  2. 把关键日志写下来
  3. 数据页在合适时机再刷盘

这背后的核心思想就是:先写日志,再落数据页。

也就是常说的 WALWrite-Ahead Logging

3. undo log:解决“怎么回退”和“怎么看旧版本”

3.1 它在解决什么问题

undo log 主要服务两个目标:

  1. 事务回滚
  2. MVCC 的一致性读

你可以把它理解成:在修改数据之前,把旧版本记下来。

这样如果事务失败了,就可以回滚;如果别的事务需要看旧版本,也能顺着版本链找到历史状态。

3.2 一个简单例子

假设账户余额原本是:

text
balance = 100

事务把它改成:

text
balance = 150

那数据库在改之前,通常会把旧值 100 相关信息记到 undo log 里。

这样:

  1. 如果事务回滚,可以恢复成 100
  2. 如果其他事务当前不能看到新版本,也可以读到旧版本 100

3.3 它和 MVCC 的关系

MVCC 之所以能做到“普通 select 不加锁也能读到一致性数据”,靠的并不只是事务 ID,还包括:

  1. 记录里的隐藏列
  2. undo log 形成的版本链
  3. Read View

所以你可以把 undo log 理解成:MVCC 的历史版本仓库。

3.4 注意点

  1. undo log 不只是给回滚用
  2. 长事务会让旧版本长期不能清理
  3. 版本链太长会让查询和回收成本上升

这也是为什么工程上一直强调:尽量避免长事务。

4. redo log:解决“宕机后怎么把已经提交的修改补回来”

4.1 它在解决什么问题

数据库更新时,真正的数据页通常先改内存里的 Buffer Pool,并不会每次提交都立刻落盘。

这时问题就来了:如果事务已经提交,但数据页还没刷到磁盘,机器突然宕机怎么办?

这就是 redo log 要解决的事。

它记录的是:某些数据页做过哪些修改,宕机恢复时可以重做。

4.2 为什么 redo log 比直接刷盘更划算

因为直接刷整页很重,而写日志通常更顺序、更紧凑。

所以数据库更常见的做法是:

  1. 内存页先改
  2. redo log 先按策略落盘
  3. 数据页稍后再刷盘

这样可以同时兼顾:

  1. 提交速度
  2. 崩溃恢复能力
  3. 磁盘写入效率

4.3 redo log 的核心价值

一句话总结就是:只要 redo log 足够可靠,数据页就不必在提交瞬间强制刷盘。

5. 🌟 binlog:解决“怎么复制”和“怎么做归档恢复”

5.1 它在解决什么问题

binlog 是 MySQL Server 层的逻辑日志,最主要用于:

  1. 主从复制
  2. 数据归档
  3. 基于时间点恢复

它更关心的是:这次事务从业务语义上做了什么变更。

5.2 🌟 它和 redo log 的本质区别

最容易混的是这两个:

  1. redo log:InnoDB 层,偏物理恢复,面向崩溃恢复
  2. binlog:Server 层,偏逻辑变更,面向复制和归档

所以它们不是谁替代谁,而是:一个负责把本机数据救回来,一个负责把变更传出去或留档。

5.3 常见 binlog 格式

常见有三种:

  1. statement
  2. row
  3. mixed

statement

记录 SQL 语句本身。

优点是:

  1. 日志量可能更小

缺点是:

  1. 某些非确定性语句复制时可能不稳定

row

记录每一行实际变成了什么。

优点是:

  1. 复制更准确
  2. 可重放性更强

缺点是:

  1. 日志量可能更大

现在实际业务里,很多场景会更偏向 row

6. redo log、undo log、binlog 到底怎么配合

如果你把一次更新简化来看,大致会经历下面几步:

  1. 事务开始
  2. 先生成 undo log
  3. 修改内存中的数据页
  4. 记录 redo log
  5. 事务提交时写入 binlog
  6. 按策略把日志真正刷盘
  7. 数据页稍后再刷盘

这三者解决的问题完全不同:

mermaid
flowchart LR
    A[更新一行数据] --> B[undo log<br/>记录旧版本]
    A --> C[redo log<br/>记录可重做修改]
    A --> D[binlog<br/>记录逻辑变更]
    B --> E[回滚 / MVCC]
    C --> F[崩溃恢复]
    D --> G[主从复制 / 归档恢复]

7. 🌟 为什么会有两阶段提交

7.1 背景问题

如果一个事务既要写 redo log,又要写 binlog,那就必须把一个关键问题想清楚:

如果只写成功了一部分,宕机了怎么办?

因为这两份日志分别服务不同目标:

  1. redo log 关系到本机恢复
  2. binlog 关系到复制和归档

如果它们状态不一致,就会出问题。

7.2 一个典型不一致场景

假设没有两阶段提交:

  1. 事务已经把 redo log 写好了并标记提交
  2. binlog 还没写成功
  3. 这时机器宕机

恢复后,本机会认为事务已经提交,因为 redo log 在。

但从库拿不到这次变更,因为 binlog 没有。

这样主从数据就可能不一致。

反过来,如果 binlog 写了,但 redo log 没提交,也会有另一种不一致风险。

7.3 两阶段提交的大致思路

核心流程可以简化成:

  1. 先写 redo log prepare
  2. 再写 binlog
  3. 最后把 redo log 标记为 commit

这样恢复时就可以根据日志状态判断:

  1. 只有 prepare,但没有完整 binlog,通常不能算真正提交
  2. preparebinlog 都完整,恢复时可以把事务补成提交

7.4 为什么它能降低不一致问题

因为它给“提交”这件事引入了一个中间状态。

数据库不再简单地认为:redo log 一写完就绝对提交。

而是先进入 prepare 状态,等 binlog 也准备好后,再真正完成提交。

8. 两阶段提交的流程图

mermaid
sequenceDiagram
    participant TX as 事务
    participant InnoDB as InnoDB
    participant Binlog as Binlog

    TX->>InnoDB: 写 undo log,修改 Buffer Pool
    TX->>InnoDB: 写 redo log prepare
    TX->>Binlog: 写 binlog
    TX->>InnoDB: 写 redo log commit
    Note over TX,Binlog: 到这里事务才算完整提交

9. 🌟 宕机恢复时数据库大致会做什么

很多人会把“崩溃恢复”想成一句空话,其实可以拆成两个方向:

  1. 把该恢复的修改补回来
  2. 把不该保留的修改撤掉

大体可以这样理解:

  1. 已提交但数据页没刷盘的事务,靠 redo log 重做
  2. 未提交的事务,靠 undo log 回滚

所以崩溃恢复并不是只靠一份日志,而是:redo log 负责向前补,undo log 负责向后撤。

10. 一个完整视角:从事务提交到崩溃恢复

把整个过程串起来,可以这样记:

  1. 更新前先留旧版本,保证能回滚,也保证 MVCC 能看历史
  2. 更新时先改内存页,提高写入效率
  3. 提交前把 redo log 按策略写稳,保证宕机后能补做
  4. 同时写 binlog,保证复制和归档能力
  5. 通过两阶段提交让 redo logbinlog 尽量保持一致
  6. 宕机恢复时,已提交事务用 redo log 恢复,未提交事务用 undo log 回滚

这条线如果能真正讲顺,MySQL 日志这一块基本就有了比较完整的理解。

11. 最容易混淆的几个问题

11.1 🌟 redo log 和 binlog 有什么区别

可以从这几个维度理解:

  1. 所在层次不同:redo log 属于 InnoDB,binlog 属于 MySQL Server
  2. 作用不同:redo log 用于崩溃恢复,binlog 用于复制和归档
  3. 记录方式不同:redo log 更偏物理恢复信息,binlog 更偏逻辑变更

11.2 🌟 为什么 MySQL 既要 redo log 又要 binlog

因为两者解决的问题不同:

  1. redo log 解决本机宕机恢复
  2. binlog 解决主从复制和归档回放

如果只有其中一个,能力就不完整。

11.3 🌟 undo log 是不是只用来回滚

不是。

它除了支持回滚,还支撑了 MVCC 的旧版本读取。

11.4 🌟 两阶段提交是为了解决什么

是为了解决 redo logbinlog 在事务提交时可能出现的不一致问题,尽量保证:

  1. 本机恢复状态正确
  2. 主从复制状态也正确

11.5 🌟 崩溃恢复依赖哪些日志

更完整的理解是:

  1. 已提交事务的数据补做主要依赖 redo log
  2. 未提交事务的回滚主要依赖 undo log
  3. binlog 更主要面向复制和归档,不是 InnoDB 崩溃恢复的核心日志

12. 一段压缩版说明

如果只想用一段话把这三类日志串起来,可以这样概括:

undo log 主要用于事务回滚和 MVCC,它保存的是修改前的旧版本信息;redo log 主要用于崩溃恢复,因为 InnoDB 更新数据时通常先改内存页,再把可重做的修改写入 redo log,只要 redo log 安全落盘,宕机后就能把已提交但未来得及刷盘的数据补回来;binlog 是 MySQL Server 层的逻辑日志,主要用于主从复制和归档恢复。因为一个事务提交时既涉及 redo log,又涉及 binlog,所以 MySQL 通过两阶段提交尽量保证两者状态一致,避免本机恢复成功但从库没同步到,或者反过来的不一致问题。

13. 工程上几个容易被忽略的点

13.1 长事务会拖累 undo 清理

长事务会让旧版本长期不能回收,带来:

  1. undo 膨胀
  2. 版本链变长
  3. 查询和回收变慢

13.2 不要把 binlog 当成 redo log 的替代品

二者职责不同。

很多人一看到“日志”就会混成一类,但工程上它们分别服务不同目标。

13.3 事务提交成功不等于数据页已经落盘

实际写项目时,更多是这样,很多时候提交成功意味着:

  1. 关键日志已经按策略写稳
  2. 数据页可以稍后刷盘

这正是数据库能兼顾性能和持久性的关键。

14. 总结

如果把这篇文章压缩成最核心的几句话,就是:

  1. undo log 负责“能撤销”和“能回看”
  2. redo log 负责“宕机后能补做”
  3. binlog 负责“把变更传出去和留档”
  4. 两阶段提交负责尽量让 redo logbinlog 一致
  5. 崩溃恢复时,已提交事务靠 redo log 恢复,未提交事务靠 undo log 回滚

真正理解 MySQL 日志,不是记住三个名词就结束了,而是要看清:

一次事务写入,为什么要同时面对回滚、恢复、复制这三类问题。

基于 VitePress 构建的个人技术笔记。