吃透InnoDB日志[MySQL]:redo/undo/binlog 与两阶段提交机制

3 阅读13分钟

redo/undo/binlog 与两阶段提交机制

目录

  • 一、 undo log(回滚日志)

  • 二、redo log(重做日志)和Buffer Pool

  • 三、undo log 和redo log的区别

  • 四、 binlog(二进制日志)

    • 1、主从复制是怎么实现?
    • 2、binlog刷盘的时机
  • 五、redo log和binlog的区别

  • 六、事务的两阶段提交

    • 1、完整流程

    • 2、异常重启出现的情况

  • 七、两阶段提交出现的问题

    • 1、磁盘 I/O 次数高(性能瓶颈)
    • 2、锁竞争激烈(并发瓶颈)
    • 3、优化方案

一、 undo log(回滚日志)

理解:相当于 “后悔药”。当事务需要回滚时,就根据 undo log 里记录的旧数据,把数据恢复到修改前。

机制: 在事务没提交之前,MySQL 会先记录更新前的数据到 undo log 日志文件里面,当事务回滚时,可以利用 undo log 来进行回滚。

作用:

  • 保证事务的原子性,记录数据修改前的状态。————如果出现了错误或者用户执行了 ROLLBACK 语句,MySQL 可以利用 undo log 中的历史数据将数据恢复到事务开始之前的状态。。
  • 实现 MVCC(多版本并发控制),让不同事务可以看到不同版本的数据,从而提高并发性能。

二、redo log(重做日志)和Buffer Pool

在学习redo log之前,先了解Boffer Pool是什么,做什么的,才能更好理解redo log的掌握。

1、Buffer Pool

MySQL 的数据存在磁盘上,直接从磁盘读写很慢。所以用内存当 “缓存”,减少磁盘 I/O,提升性能。

有了 Buffer Pool(内存中的一块区域),大部分读写操作都在内存完成:

  • 读数据:先从 Buffer Pool 找,找到就直接返回;找不到再去磁盘读,顺便把数据页缓存到 Buffer Pool。
  • 改数据:先在 Buffer Pool 里改(变成 “脏页”),之后再异步刷回磁盘,避免每次修改都直接写磁盘。

(1)Buffer Pool的结构

它是一个大缓存池,按 “页” 来管理,主要缓存:数据页索引页插入缓冲页undo 页

  • 页(Page) :InnoDB 和磁盘交互的基本单位,默认 16KB。Buffer Pool 也是按页来管理的。
  • 脏页(Dirty Page) :在 Buffer Pool 里被修改过,但还没刷回磁盘的页。
  • 预读:InnoDB 会预判你可能要用到的相邻数据页,提前从磁盘读到 Buffer Pool,减少未来的磁盘 I/O。

(2)常见的问题

  1. 查一条记录,Buffer Pool 只缓存一条记录吗?

不是的,InnoDB是按“页”去加载的,所以查一条记录是把它所在的数据页读取到Buffer Pool。而如果在下一次在查这数据页的其他行,就可以直接从内存读取。

  1. 长事务为什么会让 Buffer Pool 失效?

长事务会持有旧的视图(MVCC),导致很多旧版本数据(undo 页)不能被回收。而这些旧页占着 Buffer Pool 空间,新数据页就进不来,缓存命中率下降,性能变差。

2、redo log

理解: 相当于 “备忘录”。为了避免每次数据修改都直接写入磁盘(性能太差),MySQL 先把修改记录在 redo log 里。即使数据库崩溃,重启后也能根据 redo log 恢复数据,确保已提交事务的数据不丢失。

原理: redo log 是物理日志,记录了某个数据页做了什么修改。在事务提交时将redo log持久化到磁盘上。为了避免系统中断导致脏页数据丢失,会把脏页数据以redo log记录。

它是 InnoDB 存储引擎特有的日志,采用 “WAL(Write-Ahead Logging)” 技术,即先写日志,再写磁盘。

作用:保证事务的持久性,记录数据修改后的状态。

注意!!!:被修改 Undo log页面,也需要记录对应 redo log,对undo log实现持久性的保护。

3、常见问题[面试]

(1)为什么redo log 要写到磁盘,数据也要写磁盘?

首先写入redo log 是用了追加的操作,所以磁盘操作是顺序写;而磁盘写入操作是先找位置再写入属于随机写。磁盘的顺序写随机写的开销更小,提高磁盘的性能。

(2)产生的 redo log 是直接写入磁盘的吗?

首先redo log 本身是有自己的缓存的,会把redo log写入到自己的缓存中,再持久化到磁盘。

(3)redo log 什么时候刷盘?

  • MySQL 正常关闭时;
  • 当 redo log buffer 中记录的写入量大于 redo log buffer 内存空间的一半时,会触发落盘;
  • InnoDB 的后台线程每隔 1 秒,将 redo log buffer 持久化到磁盘。
  • 每次事务提交时都将缓存在 redo log buffer 里的 redo log 直接持久化到磁盘

(4)redo log 文件写满了怎么办?

首先InnoDB有 1 个重做日志文件组( redo log Group),重做日志文件组由有 2 个 redo log 文件组成。在重做日志组中,每个 redo log File 的大小是固定且一致的。

重做日志文件组是以两个redo log循环写入的方式环形工作的。 write pos 表示 redo log 当前记录写到的位置,用 checkpoint 表示当前要擦除的位置。

image-20260308170851406.png

当write pos 追上了 checkpoint,就说明 redo log 文件满了,这个时候就会SQL阻塞,redo log文件停止写入磁盘,然后标记redo log的哪些记录可以删除,把旧的记录进行删除,最后恢复运行。所以用户要按业务场景设置文件大小,避免redo log文件满了。

三、undo log 和redo log的区别

undo log 记录了此次事务「修改前」的数据状态,记录的是更新之前的值,主要用于事务回滚,保证事务的原子性。

redo log 记录了此次事务「修改后」的数据状态,记录的是更新之后的值,主要用于事务崩溃恢复,保证事务的持久性。

四、 binlog(二进制日志)

binlog 文件是记录了所有数据库表结构变更和表数据修改的日志,不会记录查询类的操作。----相当于 “数据库的黑匣子”。它记录了所有 “修改了什么” 的逻辑操作,而不是物理数据页的变化。

产生过程:MySQL 在完成一条更新操作后,Server 层还会生成一条 binlog,等到事务提交的时候,会将该事物执行过程中产生的所有 binlog 统一写 入 binlog 文件。

作用:记录数据库中所有的修改操作,用于数据复制(主从同步)和数据恢复。

!!!所以如果数据库数据被删除,使用binlog文件恢复

————binlog 文件保存的是全量的日志,也就是保存了所有数据变更的情况。

1、主从复制是怎么实现?

简要概括:

  • 读写分离:写操作走主库,读操作走从库,大大减轻主库压力。
  • 数据备份:从库可以作为主库的实时备份,提高容灾能力。
  • 高可用:主库挂了,可以快速把从库提升为新主库,减少 downtime。

主从复制:主库把自己的变更日志(binlog)发给从库,从库重放这些日志,从而和主库保持数据一致。

(1)核心流程(3 步)

  1. 写入 Binlog:主库写 binlog 日志,提交事务,并更新本地存储数据。

    • 客户端在主库执行写操作(INSERT/UPDATE/DELETE)。
    • 主库先把变更写入 binlog 文件,再提交事务、更新本地数据。
    • 然后返回客户端 “操作成功”。
  2. 同步 Binlog:把 binlog 复制到所有从库上,每个从库把 binlog 写到暂存日志中。

    • 从库启动一个专门的 I/O 线程,连接主库的 log dump 线程。
    • 主库的 log dump 线程把 binlog 推送给从库。
    • 从库的 I/O 线程把收到的 binlog 写入本地的 relay log(中继日志)
  3. 回放 Binlog:回放 binlog,并更新存储引擎中的数据。

    • 从库再启动一个 SQL 线程,读取 relay log。
    • 按顺序重放日志里的 SQL 语句,更新自己的数据。
    • 最终实现主从数据一致。

整个过程默认是异步的:主库提交事务后,不用等从库同步完,就直接返回客户端,性能高但有数据丢失风险。

在完成主从复制之后,你就可以在写数据时只写主库,在读数据时只读从库

注意:一般情况下,一个主库配 2~3 个从库

而从库越多,主库的 CPU、内存、网络带宽消耗就越大,反而会拖慢主库性能。

(2)复制模型

1.同步复制(全同步)

主库必须等所有从库都同步成功,才返回客户端。

优点:数据绝对一致,不会丢。

缺点:性能极差,任何一个从库挂了,整个写操作就卡住。

2. 异步复制(默认)

主库提交事务后,直接返回客户端,不管从库有没有同步完。

优点:性能最好,主库不受从库影响。

缺点:主库宕机时,可能有未同步的 binlog,导致数据丢失。

适合对性能要求高、能接受少量数据丢失的场景。

3. 半同步复制(MySQL 5.7+ 推荐)

主库只需要等至少一个从库同步成功,就返回客户端。(生产环境的首选方案)

优点:在性能和数据安全之间做了平衡。只要有一个从库活着,就不会丢数据。

缺点:比异步慢一点,但比全同步快得多。

2、binlog刷盘的时机

(1)写入binlog的原理

事务执行过程中,先把日志写到 binlog cache(Server 层的 cache),事务提交的时候,再把 binlog cache 写到 binlog 文件中。

因为有一个线程只能同时有一个事务在执行的设定,而一个事务的 binlog 是不能被拆开的要保证一次性写入。

MySQL 给每个线程分配了一片内存用于缓冲 binlog 叫 binlog cache,参数 binlog_cache_size是控制内存大小。当超出参数大小会暂存到磁盘。

在事务提交的时候,执行器把 binlog cache 里的完整事务写入到 binlog 文件中,并清空 binlog cache。

(2)刷入磁盘

在事务提交的时候,才会将cache刷入磁盘。

五、redo log和binlog的区别

binlog是 MySQL Server 层的日志。

redo log是InnoDB引擎实现的日志。

文件格式不同:

binlog 有 3 种格式类型,分别是 STATEMENT(默认格式)、ROW、 MIXED

redo log 是物理日志

写入方式不同:

binlog 是追加写,写满文件就创建新的文件进行写入。

redo log 是循环写。

用途不同:

binlog 用于备份恢复、主从复制;

redo log 用于掉电等故障恢复。

六、事务的两阶段提交

每个阶段都由协调者(Coordinator)和参与者(Participant)共同完成。

在事务执行后,记录的 binlog 会被保存到 binlog cache后,到事务提交的两个阶段:

阶段一:Prepare(准备阶段)
  1. 协调者(事务管理器 )先让 InnoDB 把 redo log 写到磁盘,并标记为 prepare 状态(表示 “我准备好了,可以提交”)。
  2. 然后,协调者等待所有参与者(主要是 InnoDB)都回复 “我准备好了”。
阶段二:Commit(提交阶段)
  1. 协调者收到所有参与者的 “OK” 后,才会让 Server 层把 binlog 写到磁盘。
  2. 最后,再让 InnoDB 把 redo log 的状态从 prepare 改成 commit(表示 “事务真正完成了”)。

注意!!! :

如果在 Prepare 阶段宕机:重启后,发现 redo log 是 prepare 但 binlog 没写,就回滚事务。

如果在 Commit 阶段宕机:重启后,发现 binlog 已经写了,就提交事务;如果 binlog 没写,就回滚。

1、完整流程

阶段一:Prepare(准备阶段)

  1. 写入 redo log(prepare 状态):

    • InnoDB 将事务 ID(XID)写入 redo log,并把事务状态标记为 prepare
    • 触发 innodb_flush_log_at_trx_commit = 1,强制将 redo log 持久化到磁盘。
  2. 返回 “OK”:

    • InnoDB 向 Server 层(binlog)回复 “我准备好了”。

此时,事务处于 “半成功” 的状态:redo log 已经落盘,但因为 binlog 还没写。


阶段二:Commit(提交阶段)

  1. 写入 binlog:

    • Server 层将事务 ID(XID)和事务内容写入 binlog。
    • 触发 sync_binlog = 1,强制将 binlog 持久化到磁盘。
  2. 返回 “OK”:

    • binlog 向 InnoDB 回复 “我也准备好了”。
  3. 最终提交:

    • InnoDB 将 redo log 的事务状态从 prepare 更新为 commit
    • 这个状态更新不需要刷盘,只需写入文件系统的 page cache 即可,因为只要 binlog 成功落盘,就认为事务成功了。
  4. 返回客户端:

    • Server 层向客户端返回 “事务提交成功”。

2、异常重启出现的情况

MySQL 异常重启后,通过两阶段提交的 事务ID(XID )来判断事务命运:

image-20260308181624103.png

场景 1:在时刻 A 崩溃

----(redo log 已刷盘,binlog 未刷盘)

  • 重启后,扫描到处于 prepare 状态的 redo log。
  • 拿着 redo log 里的 XID 去 binlog 里找,找不到
  • 结论:事务未完成,回滚
场景 2:在时刻 B 崩溃

----(redo log 和 binlog 都已刷盘,redo log 还是 prepare)

  • 重启后,扫描到处于 prepare 状态的 redo log。
  • 拿着 redo log 里的 XID 去 binlog 里找,找到了
  • 结论:事务已完成,提交

为什么这么要设计?

  • binlog 是主从复制和数据恢复的依据。
  • 如果 binlog 已经写了,从库(或用 binlog 恢复的库)就会执行这条事务。
  • 所以主库重启时,也必须提交这条事务,才能保证主从数据一致。

七、两阶段提交出现的问题

1. 磁盘 I/O 次数高(性能瓶颈)

每个事务提交,至少触发 2 次 fsync 刷盘

2. 锁竞争激烈(并发瓶颈)

一个事务从 prepare 到 commit 全程持有锁,其他事务必须排队;并发高时,锁等待严重,性能不佳。

3、优化方案

MySQL 引入了 binlog 组提交(group commit)机制: 当有多个事务提交的时候,会将多个 binlog 刷盘操作合并成一个,从而减少磁盘 I/O 的次数

组提交的三个阶段

组提交将 Commit 阶段拆分为 Flush、Sync、Commit 三个队列,每个队列独立加锁,实现了阶段间的并行执行。

1. Flush 阶段(支持 redo log 组提交)

多个事务按进入的顺序将 binlog 从 cache 写入文件(不刷盘)

  • Leader 机制:第一个进入队列的事务成为 Leader,后续事务为 Follower。
  • 批量写 redo log:Leader 代表整个队列,将所有事务的 redo log 一次性刷盘(prepare 状态)。
  • 批量写 binlog:Leader 将队列中所有事务的 binlog 批量写入文件系统缓存(write 操作,不刷盘)。
  • 作用:将 redo log 的刷盘从 prepare 阶段延迟到这里,实现了 redo log 的组提交。
2. Sync 阶段(支持 binlog 组提交)

对 binlog 文件做 fsync 操作(多个事务的 binlog 合并一次刷盘)

  • 等待时机:Leader 不会立即刷盘,而是等待一小段时间(由 binlog_group_commit_sync_delay 控制),或者等待队列中事务数达到阈值(由 binlog_group_commit_sync_no_delay_count 控制)。
  • 批量刷盘:时机成熟后,Leader 调用一次 fsync,将队列中所有事务的 binlog 从文件缓存一次性刷到磁盘。
  • 作用:将多个事务的 binlog 刷盘合并为一次,大幅减少 I/O。
3. Commit 阶段

各个事务按顺序做 InnoDB commit 操作

  • 顺序提交:Leader 按顺序调用 InnoDB 接口,将队列中所有事务的 redo log 状态从 prepare 更新为 commit
  • 作用:完成事务的最终提交,为下一组事务腾出空间。

参考资料:MySQl小林coding