MySQL事务底层实现与崩溃恢复机制详解:从Redo到Undo全链路解析

关键词:MySQL事务, Redo日志, Undo日志, WAL机制, 崩溃恢复, 事务持久性, 事务原子性, Binlog

面试中你是否被问过"MySQL如何保证事务的持久性?"、“Redo日志和Binlog有什么区别?”、"Undo日志是如何实现事务回滚的?"这些问题看似简单,但涉及MySQL底层存储引擎InnoDB的核心机制。本文将从WAL预写式日志机制出发,深入剖析Redo日志和Undo日志的实现原理,详解MySQL崩溃恢复的全过程,帮你彻底搞懂MySQL事务的底层实现。


目录

  1. MySQL事务基础
  2. WAL预写式日志机制
  3. Redo日志详解
  4. Undo日志详解
  5. Redo Log vs Binlog

1. MySQL事务基础

MySQL的事务分为显式事务隐式事务

  • 默认的事务是隐式事务:通过autocommit参数控制,默认开启
  • 显式事务由我们自己控制事务的开启、提交、回滚等操作
-- 查看autocommit设置
SHOW VARIABLES LIKE 'autocommit';

事务基本语法

事务开始

BEGIN;
-- 或
START TRANSACTION;  -- 推荐
-- 或
BEGIN WORK;

事务回滚

ROLLBACK;

事务提交

COMMIT;

2. WAL预写式日志机制

在事务的实现机制上,MySQL采用的是**WAL(Write-Ahead Logging,预写式日志)**机制。

核心思想:所有的修改都先被写入到日志中,然后再被应用到系统中。通常包含redoundo两部分信息。

日志类型作用保证的特性
Redo Log重做日志,记录数据修改后的状态事务的持久性
Undo Log撤销日志,记录数据修改前的状态事务的原子性

工作原理

  • Redo Log:每当有操作时,在数据变更之前将操作写入redo log。当发生掉电等情况时,系统可以在重启后继续操作
  • Undo Log:当变更执行到一半无法完成时,可以根据撤销日志恢复到变更前的状态

3. Redo日志详解

3.1 Redo日志的作用

3.1.1 Redo日志文件

MySQL的数据目录下默认有两个名为ib_logfile0ib_logfile1的文件,这就是redo日志。

可以通过以下启动参数调节:

参数说明默认值
innodb_log_group_home_dirredo日志文件所在的目录数据目录
innodb_log_file_size每个redo日志文件的大小48MB
innodb_log_files_in_groupredo日志文件的个数2(最大100)

写入机制:redo日志文件以组的形式出现,从ib_logfile0开始写,写满后写ib_logfile1,依此类推。如果最后一个文件也写满了,就重新转到ib_logfile0继续写(覆盖写)。

3.1.2 为什么需要Redo日志

在Buffer Pool中修改页面后,如果在事务提交后突然发生故障,内存中的数据会丢失。如何保证持久性

直接刷新数据页的问题

  1. 刷新一个完整的数据页太浪费了

    • 有时候仅仅修改了某个页面中的一个字节
    • 但InnoDB以页为单位进行磁盘IO(默认16KB)
    • 只修改一个字节就要刷新16KB的数据,显然是浪费
  2. 随机IO刷起来比较慢

    • 一个事务可能修改许多页面,这些页面可能并不相邻
    • 将修改的页面刷新到磁盘需要很多随机IO
    • 随机IO比顺序IO慢很多(尤其对机械硬盘)

Redo日志的优势

只需要记录修改了哪些东西

  • 表空间ID
  • 页号
  • 偏移量
  • 更新后的值

好处

  1. redo日志占用的空间非常小 - 只需记录位置信息和变更值
  2. redo日志是顺序写入磁盘的 - 使用顺序IO,性能更高

3.2 Redo日志格式

InnoDB针对事务对数据库的不同修改场景定义了多种类型的redo日志,绝大部分类型的redo日志都有通用的结构:

字段说明
typeredo日志的类型(约53种不同类型)
space ID表空间ID
page number页号
dataredo日志的具体内容
简单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日志在以下情况会被刷新到磁盘:

  1. 事务提交时(主要时机)

    • 通过innodb_flush_log_at_trx_commit参数控制
    • 0:事务提交时不立即同步,交给后台线程(可能丢数据)
    • 1:事务提交时同步到磁盘(默认,保证持久性)
    • 2:事务提交时写到OS缓冲区,不保证刷盘
  2. log buffer空间不足时

    • 当写入的redo日志量占满log buffer总容量的大约一半时
  3. 后台线程定期刷新

    • 大约每秒刷新一次
  4. 正常关闭服务器时

  5. 做Checkpoint时

参数配置建议

  • 对数据安全性要求高的场景(如金融系统):设置为1
  • 对性能要求高、可接受少量数据丢失的场景:设置为20

3.4 崩溃后的恢复

3.4.1 恢复机制

在服务器不挂的情况下,redo日志是累赘。但万一数据库挂了,就可以在重启时根据redo日志将页面恢复到系统崩溃前的状态。

恢复流程

  1. 根据redo日志中的信息,确定恢复的起点和终点
  2. 将redo日志中的数据以哈希表的形式组织(同一个页面的修改放在同一个槽中)
  3. 遍历哈希表,一次性将一个页面修复好(避免随机IO)
  4. 通过各种机制避免无谓的页面修复(如已经刷新的页面)
3.4.2 为什么崩溃恢复不用Binlog?
对比项Redo LogBinlog
用途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 事务回滚的需求

事务需要保证原子性,也就是事务中的操作要么全部完成,要么什么也不做。

导致事务执行到一半的情况

  1. 服务器错误、操作系统错误、突然断电等
  2. 程序员手动执行ROLLBACK语句

回滚(Rollback):把已经修改的东西改回原先的样子,使事务看起来什么都没做。

Undo日志:为了回滚而记录的信息。每当对一条记录做改动(INSERT、DELETE、UPDATE)时,都需要把回滚时所需的东西记下来。

操作类型需要记录的信息
INSERT记录的主键值(回滚时删除该记录)
DELETE记录的内容(回滚时重新插入)
UPDATE修改前的旧值(回滚时更新为旧值)

注意:查询操作(SELECT)不会修改用户记录,所以不需要记录undo日志。

4.2 事务ID分配机制

4.2.1 分配时机

读写事务(可以对表执行增删改查):

  • 使用START TRANSACTION READ WRITE开启
  • 或使用BEGINSTART TRANSACTION默认开启

事务ID分配策略

  • 只有在事务第一次对某个表执行增、删、改操作时才会分配事务ID
  • 如果事务中全是查询语句,不会分配事务ID
4.2.2 事务ID生成机制

事务ID本质上是一个递增的数字,分配策略:

  1. 服务器在内存中维护一个全局变量
  2. 每当需要为事务分配ID时,把该变量的值当作事务ID分配,并自增1
  3. 每当变量值为256的倍数时,将该值刷新到系统表空间的页号为5的页面中的Max Trx ID属性
  4. 系统下次启动时,将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)

  • 条件:被更新的每个列,更新前后占用的存储空间一样大
  • 操作:直接在原记录的基础上修改对应列的值

先删除旧记录,再插入新记录

  • 条件:有任何一个被更新的列,更新前后占用空间大小不一致
  • 操作:
    1. 把旧记录从聚簇索引页面中真正删除(不是delete mark)
    2. 根据更新后的值创建新记录插入
    3. 如果新记录占用空间不超过旧记录,重用旧记录空间
    4. 如果页面空间不足,进行页面分裂

undo日志类型TRX_UNDO_UPD_EXIST_REC

更新主键的情况

由于聚簇索引中记录按主键值连成链表,更新主键意味着记录在聚簇索引中的位置会改变。

处理步骤

  1. 对旧记录进行delete mark操作

    • 在UPDATE语句所在事务提交前,只对旧记录做delete mark
    • 事务提交后才做purge操作
    • 原因:别的事务可能正在访问这条记录(MVCC)
  2. 创建一条新记录

    • 根据更新后各列的值创建新记录
    • 重新从聚簇索引中定位插入位置
    • 插入到对应位置

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多版本并发控制
  • 实现事务隔离级别
  • 支持事务回滚

示例:在一个事务中先插入一条记录,然后删除该记录:

  1. INSERT操作产生一条TRX_UNDO_INSERT_REC类型的undo日志
  2. DELETE操作产生一条TRX_UNDO_DEL_MARK_REC类型的undo日志
  3. 两条undo日志通过roll_pointer串成一个链表

5. Redo Log vs Binlog

特性Redo LogBinlog
设计者InnoDB引擎MySQL Server层
日志内容物理日志(页修改)逻辑日志(SQL语句)
文件大小固定(循环写)不固定(追加写)
主要用途崩溃恢复(crash-safe)数据恢复、主从复制
记录时机事务执行过程中事务提交时
崩溃恢复✅ 支持❌ 不支持
适用引擎InnoDB专用所有引擎

为什么两者都需要?

  1. Redo Log:保证事务的持久性,支持崩溃后的自动恢复
  2. Binlog:用于数据备份、恢复、主从复制,是MySQL层面的日志

两阶段提交(2PC)

为了保证Redo Log和Binlog的一致性,MySQL使用两阶段提交

  1. Prepare阶段:写入Redo Log,标记为prepare状态
  2. Commit阶段:写入Binlog,然后将Redo Log标记为commit状态

这样可以确保即使发生崩溃,也能根据Redo Log和Binlog的状态判断是否提交成功。


总结

本文深入讲解了MySQL事务的底层实现机制:

核心知识点

  1. WAL机制:先写日志再写数据,是InnoDB保证事务特性的核心
  2. Redo日志
    • 物理日志,记录数据页修改
    • 循环写入,顺序IO
    • 保证事务的持久性
    • 支持崩溃后的自动恢复
  3. Undo日志
    • 逻辑日志,记录数据修改前的状态
    • 支持事务回滚(原子性)
    • 通过版本链支持MVCC
  4. 事务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
  • 面试
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

加倍巴巴

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值