Skip to content

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

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

  1. redo log
  2. undo log
  3. binlog

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

也就是常说的 WAL:Write-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. prepare 和 binlog 都完整,恢复时可以把事务补成提交

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 log 和 binlog 尽量保持一致
  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 log 和 binlog 在事务提交时可能出现的不一致问题,尽量保证:

  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 log 和 binlog 一致
  5. 崩溃恢复时,已提交事务靠 redo log 恢复,未提交事务靠 undo log 回滚

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

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

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