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)常见的问题
- 查一条记录,Buffer Pool 只缓存一条记录吗?
不是的,InnoDB是按“页”去加载的,所以查一条记录是把它所在的数据页读取到Buffer Pool。而如果在下一次在查这数据页的其他行,就可以直接从内存读取。
- 长事务为什么会让 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 表示当前要擦除的位置。
当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 步)
-
写入 Binlog:主库写 binlog 日志,提交事务,并更新本地存储数据。
- 客户端在主库执行写操作(INSERT/UPDATE/DELETE)。
- 主库先把变更写入 binlog 文件,再提交事务、更新本地数据。
- 然后返回客户端 “操作成功”。
-
同步 Binlog:把 binlog 复制到所有从库上,每个从库把 binlog 写到暂存日志中。
- 从库启动一个专门的 I/O 线程,连接主库的
log dump线程。 - 主库的
log dump线程把 binlog 推送给从库。 - 从库的 I/O 线程把收到的 binlog 写入本地的 relay log(中继日志) 。
- 从库启动一个专门的 I/O 线程,连接主库的
-
回放 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(准备阶段)
- 协调者(事务管理器 )先让 InnoDB 把 redo log 写到磁盘,并标记为
prepare状态(表示 “我准备好了,可以提交”)。 - 然后,协调者等待所有参与者(主要是 InnoDB)都回复 “我准备好了”。
阶段二:Commit(提交阶段)
- 协调者收到所有参与者的 “OK” 后,才会让 Server 层把 binlog 写到磁盘。
- 最后,再让 InnoDB 把 redo log 的状态从
prepare改成commit(表示 “事务真正完成了”)。
注意!!! :
如果在 Prepare 阶段宕机:重启后,发现 redo log 是 prepare 但 binlog 没写,就回滚事务。
如果在 Commit 阶段宕机:重启后,发现 binlog 已经写了,就提交事务;如果 binlog 没写,就回滚。
1、完整流程
阶段一:Prepare(准备阶段)
-
写入 redo log(prepare 状态):
- InnoDB 将事务 ID(XID)写入 redo log,并把事务状态标记为
prepare。 - 触发
innodb_flush_log_at_trx_commit = 1,强制将 redo log 持久化到磁盘。
- InnoDB 将事务 ID(XID)写入 redo log,并把事务状态标记为
-
返回 “OK”:
- InnoDB 向 Server 层(binlog)回复 “我准备好了”。
此时,事务处于 “半成功” 的状态:redo log 已经落盘,但因为 binlog 还没写。
阶段二:Commit(提交阶段)
-
写入 binlog:
- Server 层将事务 ID(XID)和事务内容写入 binlog。
- 触发
sync_binlog = 1,强制将 binlog 持久化到磁盘。
-
返回 “OK”:
- binlog 向 InnoDB 回复 “我也准备好了”。
-
最终提交:
- InnoDB 将 redo log 的事务状态从
prepare更新为commit。 - 这个状态更新不需要刷盘,只需写入文件系统的 page cache 即可,因为只要 binlog 成功落盘,就认为事务成功了。
- InnoDB 将 redo log 的事务状态从
-
返回客户端:
- Server 层向客户端返回 “事务提交成功”。
2、异常重启出现的情况
MySQL 异常重启后,通过两阶段提交的 事务ID(XID )来判断事务命运:
场景 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