关键词:MySQL事务, Redo日志, Undo日志, WAL机制, 崩溃恢复, 事务持久性, 事务原子性, Binlog
面试中你是否被问过"MySQL如何保证事务的持久性?"、“Redo日志和Binlog有什么区别?”、"Undo日志是如何实现事务回滚的?"这些问题看似简单,但涉及MySQL底层存储引擎InnoDB的核心机制。本文将从WAL预写式日志机制出发,深入剖析Redo日志和Undo日志的实现原理,详解MySQL崩溃恢复的全过程,帮你彻底搞懂MySQL事务的底层实现。
目录
1. MySQL事务基础
MySQL的事务分为显式事务和隐式事务:
- 默认的事务是隐式事务:通过
autocommit参数控制,默认开启 - 显式事务由我们自己控制事务的开启、提交、回滚等操作
-- 查看autocommit设置
SHOW VARIABLES LIKE 'autocommit';
事务基本语法
事务开始:
BEGIN;
-- 或
START TRANSACTION; -- 推荐
-- 或
BEGIN WORK;
事务回滚:
ROLLBACK;
事务提交:
COMMIT;
2. WAL预写式日志机制
在事务的实现机制上,MySQL采用的是**WAL(Write-Ahead Logging,预写式日志)**机制。
核心思想:所有的修改都先被写入到日志中,然后再被应用到系统中。通常包含redo和undo两部分信息。
| 日志类型 | 作用 | 保证的特性 |
|---|---|---|
| Redo Log | 重做日志,记录数据修改后的状态 | 事务的持久性 |
| Undo Log | 撤销日志,记录数据修改前的状态 | 事务的原子性 |
工作原理:
- Redo Log:每当有操作时,在数据变更之前将操作写入redo log。当发生掉电等情况时,系统可以在重启后继续操作
- Undo Log:当变更执行到一半无法完成时,可以根据撤销日志恢复到变更前的状态
3. Redo日志详解
3.1 Redo日志的作用
3.1.1 Redo日志文件
MySQL的数据目录下默认有两个名为ib_logfile0和ib_logfile1的文件,这就是redo日志。
可以通过以下启动参数调节:
| 参数 | 说明 | 默认值 |
|---|---|---|
innodb_log_group_home_dir | redo日志文件所在的目录 | 数据目录 |
innodb_log_file_size | 每个redo日志文件的大小 | 48MB |
innodb_log_files_in_group | redo日志文件的个数 | 2(最大100) |
写入机制:redo日志文件以组的形式出现,从ib_logfile0开始写,写满后写ib_logfile1,依此类推。如果最后一个文件也写满了,就重新转到ib_logfile0继续写(覆盖写)。
3.1.2 为什么需要Redo日志
在Buffer Pool中修改页面后,如果在事务提交后突然发生故障,内存中的数据会丢失。如何保证持久性?
直接刷新数据页的问题:
-
刷新一个完整的数据页太浪费了
- 有时候仅仅修改了某个页面中的一个字节
- 但InnoDB以页为单位进行磁盘IO(默认16KB)
- 只修改一个字节就要刷新16KB的数据,显然是浪费
-
随机IO刷起来比较慢
- 一个事务可能修改许多页面,这些页面可能并不相邻
- 将修改的页面刷新到磁盘需要很多随机IO
- 随机IO比顺序IO慢很多(尤其对机械硬盘)
Redo日志的优势:
只需要记录修改了哪些东西:
- 表空间ID
- 页号
- 偏移量
- 更新后的值
好处:
- redo日志占用的空间非常小 - 只需记录位置信息和变更值
- redo日志是顺序写入磁盘的 - 使用顺序IO,性能更高
3.2 Redo日志格式
InnoDB针对事务对数据库的不同修改场景定义了多种类型的redo日志,绝大部分类型的redo日志都有通用的结构:
| 字段 | 说明 |
|---|---|
| type | redo日志的类型(约53种不同类型) |
| space ID | 表空间ID |
| page number | 页号 |
| data | redo日志的具体内容 |
简单Redo日志类型
针对物理日志(在页面的某个偏移量处写入数据):
| 类型 | 说明 |
|---|---|
MLOG_1BYTE (type=1) | 在页面偏移量处写入1字节 |
MLOG_2BYTE (type=2) | 在页面偏移量处写入2字节 |
MLOG_4BYTE (type=4) | 在页面偏移量处写入4字节 |
MLOG_8BYTE (type=8) | 在页面偏移量处写入8字节 |
复杂Redo日志类型
对于INSERT等复杂操作,可能涉及:
- 更新B+树的叶子节点页面
- 更新B+树的非叶子节点页面
- 创建新的页面(页面分裂)
核心原则:redo日志会把事务在执行过程中对数据库所做的所有修改都记录下来,在系统崩溃重启后可以把事务所做的任何修改都恢复出来。
3.3 Redo日志写入过程
3.3.1 Redo Log Block和日志缓冲区
InnoDB把生成的redo日志都放在大小为512字节的块(block)中。
服务器启动时向操作系统申请一大片称之为redo log buffer的连续内存空间(日志缓冲区),默认大小为16MB(可通过innodb_log_buffer_size设置)。这片内存空间被划分成若干个连续的redo log block。
3.3.2 Redo日志刷盘时机
redo日志在以下情况会被刷新到磁盘:
-
事务提交时(主要时机)
- 通过
innodb_flush_log_at_trx_commit参数控制 - 0:事务提交时不立即同步,交给后台线程(可能丢数据)
- 1:事务提交时同步到磁盘(默认,保证持久性)
- 2:事务提交时写到OS缓冲区,不保证刷盘
- 通过
-
log buffer空间不足时
- 当写入的redo日志量占满log buffer总容量的大约一半时
-
后台线程定期刷新
- 大约每秒刷新一次
-
正常关闭服务器时
-
做Checkpoint时
参数配置建议:
- 对数据安全性要求高的场景(如金融系统):设置为1
- 对性能要求高、可接受少量数据丢失的场景:设置为2或0
3.4 崩溃后的恢复
3.4.1 恢复机制
在服务器不挂的情况下,redo日志是累赘。但万一数据库挂了,就可以在重启时根据redo日志将页面恢复到系统崩溃前的状态。
恢复流程:
- 根据redo日志中的信息,确定恢复的起点和终点
- 将redo日志中的数据以哈希表的形式组织(同一个页面的修改放在同一个槽中)
- 遍历哈希表,一次性将一个页面修复好(避免随机IO)
- 通过各种机制避免无谓的页面修复(如已经刷新的页面)
3.4.2 为什么崩溃恢复不用Binlog?
| 对比项 | Redo Log | Binlog |
|---|---|---|
| 用途 | MySQL自己使用,保证crash-safe能力 | 人工恢复数据,主从复制 |
| 层级 | InnoDB引擎特有 | MySQL Server层实现,所有引擎可用 |
| 日志类型 | 物理日志(记录数据页修改) | 逻辑日志(记录SQL语句) |
| 写入方式 | 循环写(固定大小,覆盖旧日志) | 追加写(保存全量日志) |
| 恢复速度 | 快(直接应用到数据页) | 慢(需要重新执行SQL) |
| 崩溃恢复 | 可以(有checkpoint机制) | 不可以(无法判断哪些已入表) |
核心区别:
Binlog虽然拥有全量日志,但没有一个标志让InnoDB判断哪些数据已经写入磁盘,哪些还没有。例如:
记录1:给 ID=2 这一行的 c 字段加1
记录2:给 ID=2 这一行的 c 字段加1
如果在记录1入表后、记录2未入表时数据库crash,只通过binlog无法判断这两条记录哪条已写入磁盘。
但redo log不一样,只要刷入磁盘的数据,都会从redo log中抹掉。数据库重启后,直接把redo log中的数据恢复至内存即可。
一句话概括:
- binlog:用作人工恢复数据、主从复制
- redo log:MySQL自己使用,用于保证数据库崩溃时的事务持久性
4. Undo日志详解
4.1 事务回滚的需求
事务需要保证原子性,也就是事务中的操作要么全部完成,要么什么也不做。
导致事务执行到一半的情况:
- 服务器错误、操作系统错误、突然断电等
- 程序员手动执行ROLLBACK语句
回滚(Rollback):把已经修改的东西改回原先的样子,使事务看起来什么都没做。
Undo日志:为了回滚而记录的信息。每当对一条记录做改动(INSERT、DELETE、UPDATE)时,都需要把回滚时所需的东西记下来。
| 操作类型 | 需要记录的信息 |
|---|---|
| INSERT | 记录的主键值(回滚时删除该记录) |
| DELETE | 记录的内容(回滚时重新插入) |
| UPDATE | 修改前的旧值(回滚时更新为旧值) |
注意:查询操作(SELECT)不会修改用户记录,所以不需要记录undo日志。
4.2 事务ID分配机制
4.2.1 分配时机
读写事务(可以对表执行增删改查):
- 使用
START TRANSACTION READ WRITE开启 - 或使用
BEGIN、START TRANSACTION默认开启
事务ID分配策略:
- 只有在事务第一次对某个表执行增、删、改操作时才会分配事务ID
- 如果事务中全是查询语句,不会分配事务ID
4.2.2 事务ID生成机制
事务ID本质上是一个递增的数字,分配策略:
- 服务器在内存中维护一个全局变量
- 每当需要为事务分配ID时,把该变量的值当作事务ID分配,并自增1
- 每当变量值为256的倍数时,将该值刷新到系统表空间的页号为5的页面中的
Max Trx ID属性 - 系统下次启动时,将
Max Trx ID加载到内存,加上256后赋值给全局变量
特点:
- 先被分配ID的事务得到较小的ID
- 后被分配ID的事务得到较大的ID
- 保证整个系统中事务ID值是递增的
4.3 Undo日志的格式
为了实现事务的原子性,InnoDB在实际进行增、删、改一条记录时,都需要先把对应的undo日志记下来。
undo日志编号:从0开始编号,第0号、第1号、…、第n号,称为undo no。
undo日志存储:被记录到类型为FIL_PAGE_UNDO_LOG的页面中,可以从系统表空间或专门的undo表空间分配。
4.3.1 INSERT操作对应的Undo日志
插入记录时,需要记录该记录的主键信息,回滚时删除该记录即可。
undo日志类型:TRX_UNDO_INSERT_REC
聚簇索引记录的隐藏列:
- trx_id:对该记录做改动的事务ID
- roll_pointer:指向记录对应的undo日志的指针
4.3.2 DELETE操作对应的Undo日志
删除记录的过程分为两个阶段:
阶段一:delete mark(删除标记)
- 将记录的
delete_mask标识位设置为1 - 记录处于中间状态,仍在正常记录链表中
- 在删除语句所在的事务提交前,记录一直处于中间状态(为了MVCC)
阶段二:purge(清理)
- 事务提交后,专门线程将记录从正常记录链表移除
- 加入到垃圾链表(PAGE_FREE指向)
- 调整页面统计信息
undo日志类型:TRX_UNDO_DEL_MARK_REC
版本链:通过old roll_pointer可以找到记录在修改之前对应的undo日志。
4.3.3 UPDATE操作对应的Undo日志
InnoDB对更新主键和不更新主键两种情况有不同处理:
不更新主键的情况
就地更新(In-Place Update):
- 条件:被更新的每个列,更新前后占用的存储空间一样大
- 操作:直接在原记录的基础上修改对应列的值
先删除旧记录,再插入新记录:
- 条件:有任何一个被更新的列,更新前后占用空间大小不一致
- 操作:
- 把旧记录从聚簇索引页面中真正删除(不是delete mark)
- 根据更新后的值创建新记录插入
- 如果新记录占用空间不超过旧记录,重用旧记录空间
- 如果页面空间不足,进行页面分裂
undo日志类型:TRX_UNDO_UPD_EXIST_REC
更新主键的情况
由于聚簇索引中记录按主键值连成链表,更新主键意味着记录在聚簇索引中的位置会改变。
处理步骤:
-
对旧记录进行delete mark操作
- 在UPDATE语句所在事务提交前,只对旧记录做delete mark
- 事务提交后才做purge操作
- 原因:别的事务可能正在访问这条记录(MVCC)
-
创建一条新记录
- 根据更新后各列的值创建新记录
- 重新从聚簇索引中定位插入位置
- 插入到对应位置
undo日志:
- delete mark操作前:记录
TRX_UNDO_DEL_MARK_REC类型的undo日志 - 插入新记录时:记录
TRX_UNDO_INSERT_REC类型的undo日志 - 每更新一条记录的主键值,会记录2条undo日志
4.4 版本链的形成
每次对记录进行改动,都会记录一条undo日志。每条undo日志都有roll_pointer属性(INSERT操作除外),可以将这些undo日志连起来,串成一个链表,这就是版本链。
版本链的作用:
- 支持MVCC多版本并发控制
- 实现事务隔离级别
- 支持事务回滚
示例:在一个事务中先插入一条记录,然后删除该记录:
- INSERT操作产生一条
TRX_UNDO_INSERT_REC类型的undo日志 - DELETE操作产生一条
TRX_UNDO_DEL_MARK_REC类型的undo日志 - 两条undo日志通过
roll_pointer串成一个链表
5. Redo Log vs Binlog
| 特性 | Redo Log | Binlog |
|---|---|---|
| 设计者 | InnoDB引擎 | MySQL Server层 |
| 日志内容 | 物理日志(页修改) | 逻辑日志(SQL语句) |
| 文件大小 | 固定(循环写) | 不固定(追加写) |
| 主要用途 | 崩溃恢复(crash-safe) | 数据恢复、主从复制 |
| 记录时机 | 事务执行过程中 | 事务提交时 |
| 崩溃恢复 | ✅ 支持 | ❌ 不支持 |
| 适用引擎 | InnoDB专用 | 所有引擎 |
为什么两者都需要?
- Redo Log:保证事务的持久性,支持崩溃后的自动恢复
- Binlog:用于数据备份、恢复、主从复制,是MySQL层面的日志
两阶段提交(2PC):
为了保证Redo Log和Binlog的一致性,MySQL使用两阶段提交:
- Prepare阶段:写入Redo Log,标记为prepare状态
- Commit阶段:写入Binlog,然后将Redo Log标记为commit状态
这样可以确保即使发生崩溃,也能根据Redo Log和Binlog的状态判断是否提交成功。
总结
本文深入讲解了MySQL事务的底层实现机制:
核心知识点:
- WAL机制:先写日志再写数据,是InnoDB保证事务特性的核心
- Redo日志:
- 物理日志,记录数据页修改
- 循环写入,顺序IO
- 保证事务的持久性
- 支持崩溃后的自动恢复
- Undo日志:
- 逻辑日志,记录数据修改前的状态
- 支持事务回滚(原子性)
- 通过版本链支持MVCC
- 事务ID:递增分配,用于标识事务和实现MVCC
实际应用建议:
- innodb_flush_log_at_trx_commit:根据业务对数据安全的要求选择(1最安全,0性能最好)
- redo日志文件大小:根据写入量调整,避免频繁切换
- 理解崩溃恢复机制:有助于理解为什么redo log是物理日志,为什么比binlog更适合崩溃恢复
面试高频问题:
- MySQL如何保证事务的持久性?(Redo Log + WAL)
- MySQL如何保证事务的原子性?(Undo Log)
- Redo Log和Binlog的区别?(物理vs逻辑、循环vs追加、崩溃恢复能力)
- 为什么崩溃恢复用Redo Log而不用Binlog?(无法判断哪些已入表)
- Undo日志是如何支持MVCC的?(版本链、roll_pointer)
- 更新主键和不更新主键的Undo日志有什么区别?(1条vs2条)
希望这篇文章能帮助你深入理解MySQL事务的底层实现!如果觉得有帮助,欢迎点赞、收藏、关注~
推荐标签:
- MySQL
- InnoDB
- Redo Log
- Undo Log
- 事务
- 崩溃恢复
- WAL
- 面试
1115

被折叠的 条评论
为什么被折叠?



