MySQL三大日志(binlog、redo log和undo log)

MySQL 日志 主要包括错误日志、查询日志、慢查询日志、事务日志、二进制日志几大类。其中,比较重要的还要属二进制日志binlog(归档日志)和事务日志redo log(重做日志)undo log(回滚日志)

redo log(重做日志)

redo log是物理日志,他记录的是某个数据页做了什么修改,属于InnoDB存储引擎内部的日志,主要用于保证不丢数据(持久性)

具体流程:

  1. MySQL以页为单位从磁盘中读取数据,加载出来的数据叫数据页,会放入到Buffer Pool中。
  2. 后续查询都是先从Buffer Pool中读取数据,如果数据页不在Buffer Pool中才会从磁盘中读取。
  3. 更新表数据时也是如此,发现Buffer Pool里存在要更新的数据,就直接在Buffer Pool里更新。
  4. 然后会把“在某个数据页上做了什么修改”记录到重做日志缓存(redo log buffer)中,接着刷盘到redo log文件中。

刷盘时机

刷盘时机指的是redo log buffer中的日志什么时候刷盘到磁盘中,MySQL提供了三种刷盘方式:

  1. 事务提交时(最核心):当事务提交时,MySQL会将redo log buffer中的日志刷盘到磁盘中,以确保事务的持久性。可以通过innodb_flush_log_at_trx_commit参数来控制刷盘的行为。
  2. redo log buffer空间不足时
    • 当redo log buffer已用空间达到50%时,后台线程会主动将这部分日志刷新到磁盘中。
    • 如果因为大事务或IO繁忙导致buffer被写满,那么所有试图写入redo log buffer的线程都会被阻塞,并强制进行一次同步刷盘。
  3. 触发检查点(Checkpoint)时:Checkpoint是InnoDB为了缩短崩溃恢复时间而设计的核心机制。
    • Checkpoint 是一个“安全位置标记”,表示在这个位置之前的 redo log 已经不再需要用于崩溃恢复。换句话说:Checkpoint之前的修改都已经真正写入了数据文件(.ibd)。Checkpoint之后的日志,才是恢复时需要重放的。
    • Checkpoint是InnoDB中用于标记redo log安全位置的机制。表示在该位置之前的日志对应的数据页已经刷盘,因此不再需要用于崩溃恢复。它的作用是保证 redo log 可以循环覆盖,同时避免日志空间耗尽。
  4. 后台线程周期性刷新:InnoDB后台的master thread大约每秒执行一次例行任务,其中就包括将redo log buffer中的日志刷新到磁盘。这个机制是innodb_flush_log_at_trx_commit设置为0或2时的主要持久化保障。
  5. 正常关闭服务器:当MySQL服务器正常关闭时,会将redo log buffer中的日志刷盘到磁盘中,以确保数据的完整性。
  6. binlog切换时:当开启binlog后,在MySQL采用innodb_flush_log_at_trx_commit=1sync_binlog=1的双一配置下,为了保证redo log和binlog之间状态的一致性(用于崩溃恢复或主从复制),在binlog文件写满或者手动执行flush logs进行切换时,会触发redo log的刷盘动作。

innodb_flush_log_at_trx_commit参数

innodb_flush_log_at_trx_commit参数控制着redo log的刷盘行为,主要有以下三个取值:

  1. 值为0:表示事务提交时不进行刷盘。
  2. 值为1:表示事务提交时进行刷盘(默认值)。
  3. 值为2:表示事务提交时都只把log buffer里的redo log内容写入page cache(文件系统缓存)。page cache是专门用来缓存文件的,这里被缓存的文件就是redo log文件。

另外,InnoDB后台线程每隔1秒就会把redo log buffer中的内容写到page cache,然后调用fsync刷盘。

日志文件组

InnoDB的redo log文件是由多个日志文件组成的,称为日志文件组(log file group)。

每个日志文件的大小由innodb_log_file_size参数控制。日志文件组中的日志文件数量由innodb_log_files_in_group参数控制。

MySQL8.0.30及之后的版本中这两个参数已经被废弃,即使指定也是用来计算innodb_redo_log_capacity的。日志文件组的文件数固定为32,文件大小则为innodb_redo_log_capacity/32


binlog(归档日志)

binlog是MySQL Server层的日志,不属于某个存储引擎。它记录的是逻辑操作

比如:

1
update user set age = 30 where id = 1;

binlog记录的是SQL或行变更信息,而不是物理页修改。

它有两个核心作用:

  1. 主从复制:从库就是读取主库的binlog,然后重放。
  2. 数据恢复:比如误删数据,可以用binlog做point-in-time recovery(基于时间点恢复)。

redo log 是用来“崩溃恢复”,
binlog 是用来“复制和归档”。

binlog格式

binlog有三种格式,可以使用binlog_format参数进行设置:

  • statement:记录SQL语句的文本形式。
  • row:记录行级别的变更信息,包含了被修改的行的具体数据。
  • mixed:MySQL会根据具体情况自动选择使用statement或row格式。

它们的区别核心只有一个问题:记录“SQL语句”还是记录“数据变化”?

statement(基于语句)

记录的是原始 SQL。

比如:

1
update user set age = 30 where id = 1;

binlog 里记录的就是这条 SQL。

优点:

  • 日志体积小
  • 写入快

问题:

  • 如果SQL执行结果依赖环境,主从可能不一致

典型例子:

1
update user set create_time = now();

主库执行时间和从库执行时间不同。

或者:

1
update user set id = id + 1 limit 1;

如果没有明确order by,主从选中的行可能不同。

statement本质风险是:依赖确定性。

row(基于行)

不记录SQL,而是记录“数据行的变化”。

比如刚才的 update:

binlog 会记录:

  • 修改前的行数据
  • 修改后的行数据

主库改哪一行,从库就精确改哪一行。

优点:

  • 最安全
  • 不会因函数、时间、limit 等产生不一致

缺点:

  • 日志体积大
  • 大批量更新会产生大量 binlog

这是当前默认推荐格式。

mixed(混合模式)

MySQL自动判断:

  • 能安全用statement就用statement
  • 可能不安全就用row

它试图在性能和安全之间折中。

但在生产中:

大多数系统直接使用row。

因为:

复制一致性 > 日志体积。

对比

statement:记录“怎么做”
row:记录“做了什么”
mixed:自动选择

写入机制

事务执行时,先把日志写到binlog cache,事务提交时再把binlog cache中的日志写入磁盘上的binlog文件。

一个事物的binlog不能被拆开,所以系统会给每个线程分配一个块内存作为binlog cache,可以通过binlog_cache_size参数设置这个缓存的大小。

binlog日志刷盘流程如下:

  • 上图的write,是指把日志写入到文件系统的page cache,并没有把数据持久化到磁盘,所以速度比较快
  • 上图的fsync,才是将数据持久化到磁盘的操作
  • writefsync的时机,可以由参数sync_binlog控制,默认是1。
    • 为0的时候,表示每次提交事务都只write,由系统自行判断什么时候执行fsync。
    • 为1的时候,表示每次提交事务都要执行一次fsync。
    • 还可以设置为N(N>1),表示每次提交事务都write,但每N次提交才执行一次fsync

两阶段提交

redo log(重做日志)让InnODB存储引擎有了崩溃恢复能力。
binlog(归档日志)保证了MySQL集群架构的数据一致性。

redo log在事务执行过程中可以不断写入,但binlog只有在事务提交时才写入。

redo log和binlog分属两个系统层级,如何保证它们的一致性?

redo log 属于 InnoDB(存储引擎层,物理日志)
binlog 属于 Server 层(逻辑日志)

如果只成功写了其中一个,就会产生灾难。

为什么必须两阶段?

设想两个失败场景。

场景 1:

  • redo log 写成功
  • binlog 写失败
  • 此时崩溃

重启后:

InnoDB 根据 redo log 恢复数据
但 binlog 没有记录这次修改

如果做主从复制,从库永远不知道这次修改。

主从数据不一致。

场景 2:

  • binlog 写成功
  • redo log 写失败
  • 崩溃

主库恢复后没有这条数据
但从库会执行 binlog

再次不一致。

所以必须保证:

  • 要么两个都成功
  • 要么两个都失败

MySQL 中的两阶段流程

不是抽象协议,而是具体执行顺序。

第一阶段:prepare 阶段

  1. InnoDB 写 redo log
  2. 将 redo log 标记为 prepare 状态
  3. 刷盘(保证持久)

此时事务还未真正提交。

第二阶段:commit 阶段

  1. 写 binlog
  2. binlog 刷盘
  3. 通知 InnoDB 将 redo log 标记为 commit

至此事务完成。

崩溃恢复时怎么判断

重启时 InnoDB 会检查 redo log 中的状态:

如果是 commit 状态 → 直接提交
如果是 prepare 状态 → 检查 binlog

  • binlog 中有这条事务 → 提交
  • binlog 中没有 → 回滚

通过这个机制实现最终一致。

面试标准回答

MySQL 为了保证redo logbinlog的一致性,采用两阶段提交机制。

第一阶段:InnoDB写redo log并标记为prepare
第二阶段:写binlog并刷盘,然后将redo log标记为commit

崩溃恢复时,如果redo log处于prepare状态,会通过binlog判断事务是否真正提交,从而保证两种日志的一致性。


undo log(回滚日志)

undo log —— 保证可回滚 + MVCC

undo log也是InnoDB的日志,但它是逻辑日志,记录的是修改前的值

两个作用:

第一,事务回滚
比如:

1
update user set age = 30 where id = 1;

如果执行一半你rollback了怎么办?

undo log里记录了age修改前的值,比如25。回滚时就把25写回去。

第二,MVCC(多版本并发控制)

REPEATABLE READ隔离级别下,一个事务可以读到“历史版本”的数据。这个历史版本就是通过undo log形成的版本链。

每次修改都会生成一个undo版本,串成链表。
查询时通过Read View判断可见性,决定读哪个版本。


redo log和undo log的区别

undo log 解决的是“事务自己后悔”
redo log 解决的是“系统突然死亡”

两者关注的不是同一个风险。

时间维度不同

undo log 面向的是:事务还活着,但决定撤销。

redo log 面向的是:数据库进程已经崩溃,需要恢复到一致状态。

一个是主动回退。
一个是被动重建。

数据方向不同

undo log 记录的是“旧值”(before image)。

它让数据可以往回走。

比如:
age: 20 → 30

undo 里保存 20。
回滚时把 20 写回去。
这是时间倒流。

redo log 记录的是“修改操作”。

它让数据可以往前补。

比如:
age 从 20 改成 30
redo 记录的是这次物理变更。

如果崩溃时数据页还没落盘,
重启时通过 redo 把修改重新做一遍。

这是时间补偿。

触发场景不同

undo log 使用场景:

  • 事务 rollback
  • MVCC 读历史版本

redo log 使用场景:

  • 宕机后 crash recovery
  • WAL 保证持久性

undo 是逻辑一致性工具。
redo 是持久性保障工具。

为什么两个都需要?

假设只有 undo,没有 redo。

事务提交成功,数据在内存,
还没刷盘就宕机。
重启后,数据丢了。
undo 解决不了。

假设只有 redo,没有 undo。

事务执行到一半要回滚,
怎么办?
redo 只记录“怎么改”,
没有旧值,回不去。

数据库必须同时具备:

  • 向后退的能力(undo)
  • 向前补的能力(redo)

总结

MySQL InnoDB引擎使用redo log(重做日志)保证事务的持久性,使用undo log(回滚日志)来保证事务的原子性

MySQL数据库的数据备份、主备、主主、主从都离不开binlog,需要依靠binlog来同步数据,保证数据一致性。