☰
MySQL三大日志:Undo Log、Redo Log与Binlog的底层原理与实战解析
2026/10/19 13:20:32 网站建设 项目流程

刚接触MySQL进阶知识的那段时间,我一度很困惑:既然表里的数据已经通过Buffer Pool在内存里改了,为什么事务提交时还要再多写一份Redo Log?Undo Log又说是用来回滚的,可它怎么还能支撑MVCC读?Binlog明明是Server层的日志,怎么就跟InnoDB的Redo Log绑在一起做两阶段提交了?

后来自己动手搭环境、模拟崩溃、翻源码和官方文档,才把这三者的关系慢慢理清。这篇文章就把我对Undo Log、Redo Log、Binlog的理解完整梳理一遍,重点放在“为什么需要它们”“它们各自记录了什么”“碰到崩溃时谁在保护数据”这三个问题上。无论你是准备面试、做DBA运维,还是单纯想把MySQL底层原理吃透,这篇文章都能帮你省不少绕弯的时间。

1. 为什么数据库要放着“改数据”不干,偏要先写日志

想弄明白三种日志的定位,得先搞清楚MySQL/InnoDB为什么要引入WAL(Write-Ahead Logging)这套机制。很多人把这套机制背得滚瓜烂熟,却不知道它到底解决了什么问题。

1.1 磁盘随机IO和顺序IO之间,有一条巨大鸿沟

你可以把数据库最终要落盘的“.ibd”数据文件理解为“页”的集合,每页默认16KB。如果我们每次提交事务都直接把内存里改过的页同步写回磁盘,意味着要在一个16KB的大块里随机找一个位置去修改。磁盘的随机IO通常每秒钟只能做几百次,而顺序IO每秒可以轻松做到几千上万次的级别,两者的差距可以高达两三个数量级。

这就好比你在一个巨大仓库里改货架上的商品:如果每次改一个商品都要从仓库这头走到那头,一天下来时间全耗在路上了;但如果改成“今天改了什么,先记在小本子上,下班后按区域统一整理归位”,效率高得多。WAL就是这个思路的重点体现。

1.2 “内存先改,后台慢慢刷盘”的框架

InnoDB的核心架构是:

  • 你把数据页读到Buffer Pool里,更新操作直接改内存页;
  • 这个改过的内存页就变成了“脏页”(dirty page);
  • 系统会在合适的时机,由后台的刷盘线程统一把这些脏页写回磁盘数据文件。

这里就有一个风险:事务提交成功,结果内存里的脏页还没来得及落盘,数据库突然崩溃/断电了,内存数据全没。如果没有任何日志做保障,这条提交成功的数据就是丢失的——这对数据库来说是绝对不能接受的。

1.3 WAL的本质:日志是效率设计,更是安全底线

WAL巧妙的点在于:我不急着改磁盘上的数据页,但我必须把“这次修改是什么”这件事,以追加写的方式快速记下来。追加写天然是顺序IO,成本极低;而只要这条日志落盘了,哪怕内存里的脏页全丢了,等数据库重启后也能按照日志把修改重新应用一遍。这样既保证了写入性能,又不牺牲数据安全。

理解了这套“先记日志,再改数据”,就能明白后续所有日志设计的出发点:Undo Log负责怎么撤销,Redo Log负责怎么重放,Binlog负责怎么同步。三者全都建立在这套WAL框架之上,只是记录的内容和服务的对象完全不同。

2. Undo Log:事务回滚与MVCC背后的那本“流水账”

Undo Log往往被一句话带过成“用于回滚的日志”,但它真正的分量远不止回滚。MVCC历史版本的读取,也完全依赖Undo Log支撑。

2.1 它记录的其实是“反操作”

名字叫Undo,可能让人误以为它是数据的备份或快照,实际不是。它记录的是“怎么把这次操作撤销回去”的信息。

操作类型Undo Log中记录的内容回滚时的行为
INSERT记录新插入行的主键信息定位到该行,执行删除(反向操作)
DELETE记录被删行的完整行记录内容把这条记录重新插回去(反向操作)
UPDATE记录被修改前的旧值用旧值覆盖当前的新值

也就是说,Undo Log不是把整页数据复制一遍,而是记录一个“反向SQL”所需的关键信息。

2.2 事务回滚的完整链条

事务执行过程中,每一条数据变更都会同时写一条Undo Log。假如事务执行到一半,SQL报错,或者用户主动发起了ROLLBACK,InnoDB要做的事就是:沿着这个事务产生的Undo Log,从头到尾把每一步操作反向撤销。这个操作不是直接删日志,而是把日志中的反向SQL应用到当前数据上。如果中途再次失败,还会继续沿着链路的剩余部分向前回溯,直到把这个事务产生的影响全部清干净。

2.3 MVCC:当Undo“退休”之后,它还有另一个身份

Undo Log在事务回滚之外最大的用途,是支撑MVCC多版本并发控制。

InnoDB的每行记录里,隐藏着两个关键字段:DB_TRX_ID(最近一次修改该行的事务ID)和DB_ROLL_PTR(回滚指针)。当一个旧事务在READ COMMITTED或REPEATABLE READ隔离级别下读取某一行时,如果发现这行当前的最新版本不是由自己修改的,就会顺着DB_ROLL_PTR顺着Undo Log的版本链找下去,直到找到满足可见性条件的那个历史版本。

换句话说,长事务执行期间,Undo Log的版本链会被拉扯得很长,这带来的直接后果是:

  • Undo表空间体积膨胀;
  • 查询要回溯的版本多,读取性能下降;
  • 后续的purge线程无法及时清理这些“已完成”的Undo记录,因为“看不见但可能被读”的历史版本还躺在链上。

这也是为什么生产环境里,一个长时间不提交的事务,经常导致Undo表空间蹭蹭往上涨,甚至拖垮实例。

2.4 Undo段的回收机制和8.0的改造点

Undo Log是分段管理的。历史版本只要不再被任何事务需要,就会被后台purge线程清理。MySQL 5.7及之前,Undo Log默认存放在系统表空间ibdata1中,这也是“ibdata1疯狂膨胀却无法收缩”的常见诱因之一。自MySQL 8.0起,Undo Log被独立到专门的undo表空间中,支持自动收缩,还可以配置多个undo表空间来均衡IO。

实操中,如果要判断当前是否有长事务卡住Undo,可以执行:

SELECT trx_id, trx_state, trx_started, trx_rows_locked, trx_rows_modified FROM information_schema.innodb_trx;

如果发现trx_started时间非常久,就要警惕Undo膨胀的风险了。

3. Redo Log:崩溃恢复靠它撑场,写入时机你却未必猜对

如果说Undo Log是“后悔账本”,Redo Log更像“保险柜里的重放录像”。它的问题不是“要不要记”,而是“记什么、记多少、什么时候记”。

3.1 物理日志与逻辑日志之间,Redo Log选了个折中

Binlog是逻辑日志,记录“某个时间点执行了什么SQL”;Redo Log是物理逻辑日志,本质记录的是**“在某个页的某个偏移量上,写入了什么数据”**。它不关心你的SQL语义是什么,只关心把对应数据页变成最新状态需要哪些字节层面的变更。

这带来一个好处:崩溃恢复时,InnoDB不需要重新执行SQL,只需要拿着Redo Log的数据,按物理位置覆盖到对应页上就行,恢复速度远快于重新执行逻辑SQL。

3.2 环形的文件结构:Redo Log不是无限增长的

Redo Log的文件是固定的几个ib_logfile文件组成一个环形缓存区。比如常见的配置是innodb_log_file_size=1G,两个文件,总共2G空间。日志写完最后一个文件,就会绕回第一个文件继续写。但如果checkpoint之后的新日志要覆盖还没完成刷盘的位置,就会强制推动脏页优先刷盘,让循环继续转起来。

这个环形结构决定了它不可能无限保存历史。它只负责保护“崩溃发生时,内存里有还没落盘的脏页”这个短暂窗口。只要脏页已经落盘,对应的Redo Log页面就可以被后续写入覆盖。

3.3 崩溃恢复的流程:你只需要记住一个词,重放

假设数据库在某一秒突然断电,内存里的Buffer Pool全部丢失,但Redo Log文件在磁盘上完好。InnoDB启动时会发生什么?

  • 找到最近一次checkpoint的位置;
  • 从checkpoint之后的Redo Log开始,从头到尾顺序扫描了一遍;
  • 把其中记录的数据页变更,重新应用(重放)到对应页上;
  • Buffer Pool被恢复到崩溃前的状态;
  • 再借着Undo Log把未提交的事务回滚掉。

这也是为什么Redo Log总是早于Undo Log起效——必须先重放已提交的修改,才能处理未提交事务的回滚。

3.4 checkpoint和LSN:日志和数据页之间靠什么对齐

LSN(Log Sequence Number)是理解Redo Log的一把钥匙。你可以把它粗略理解成一个全局递增的日志序号,数据页和日志页都有对应的LSN。

脏页被刷盘后,这个数据页的“已落盘LSN”就会推进;系统定期计算一个全局安全的LSN,也就是checkpoint点。只要checkpoint点往前移动,代表checkpoint之前的数据,都已经安全落盘了,对应的Redo Log空间就可以被复用。这也是“崩溃恢复不用从头扫所有日志,只扫checkpoint之后的日志”的根本原因。

3.5 innodb_flush_log_at_trx_commit:三种取值对应三种安全级别

Redo Log也不是每写一条就立刻刷盘,这里有个关键参数:

参数值行为崩溃时可能造成的损失
1每次事务提交时,把Redo Log刷到磁盘不丢数据(默认值,安全金融场景推荐)
0提交时不主动刷盘,交给后台每秒刷一次最多丢1秒内的事务
2提交时把Redo Log写入系统缓存,每秒刷盘一次操作系统没崩则基本不丢;机器断电则可能丢1秒左右

有人为了性能把这个参数改成0或2,却忽略了高安全性场景对“提交即持久化”的硬要求。如果丢数据是致命的,这个参数必须保持为1,没有商量余地。

4. Binlog:Server层这本“总账”,为什么逃不开两阶段提交

Redo Log是InnoDB引擎层面的日志,而Binlog是MySQL Server层的日志。两者记录方式、生命周期、用途完全不同。但要保证“主从数据一致”和“崩溃后不丢账”,它们必须协同工作,这就引出了两阶段提交。

4.1 Binlog的“总账”性质:记录的是逻辑操作

Binlog记录的是SQL执行的逻辑结果,它不关心底层哪个页被改了,而是记录“表users在id=1的行上,把name字段从张三改成了李四”。这种日志的可读性很高,还能用于:

  • 主从复制:从库把Binlog拿过来重放,即可得到相同的结果;
  • 数据恢复:通过mysqlbinlog把某个时间点的数据变更再执行一遍;
  • 增量备份:定期基于全量+Binlog恢复某时刻数据。

由于是追加式写入的二进制文件,Binlog可以完整保留几小时甚至几天的历史记录。这和Redo Log“环形覆盖”的设计形成鲜明对比。

4.2 binlog_format:三种记录格式怎么选

Binlog有三种格式,直接影响记录内容和复制行为:

  • STATEMENT:记录SQL原文。占用空间小,但某些函数或不确定SQL在从库重放时可能出现结果不一致。
  • ROW:记录具体行的前后镜像。最安全可靠,主从一致性最强,但日志量大。
  • MIXED:混合模式,默认用STATEMENT,遇到不安全语句自动切到ROW。兼顾空间和安全,实际使用中很常见。

线上如果特别在意主从数据一致,比如存在跨库更新、随机函数、存储过程等场景,优先选择ROW格式。

4.3 两阶段提交:prepare与commit的拆分

为什么要两阶段?因为Redo Log和Binlog若各自独立写入,崩溃时可能出现“一个写了、一个没写”。比如:

  • Redo Log已经刷盘,Binlog没写:主库重启后能恢复这次提交,但从库没拿到Binlog,主从数据不一致。
  • Binlog已经写盘,Redo Log没写:从库多执行了一次修改,主库却认为事务没提交。

InnoDB通过内部XA事务协议把提交过程拆成两段:

  1. prepare阶段:事务的Redo Log先写入并刷盘,标记为prepare状态;
  2. commit阶段:把事务的Binlog写入并刷盘,再在Redo Log里标记为commit状态。

崩溃恢复时,InnoDB会检查每个处于prepare状态的事务,再看它对应的Binlog是否完整:

  • 如果Binlog完整:说明主库已经决定提交并且Binlog已落盘,恢复时强制提交该事务;保证Binlog和Redo Log一致。
  • 如果Binlog不完整或不存在:说明事务没有真正提交成功,恢复时执行回滚。

4.4 sync_binlog:提交时要不要立刻刷Binlog

sync_binlog参数决定Binlog刷盘策略:

  • 默认值1:每次提交都刷盘,安全性最高,但性能损耗明显;
  • 为0时:交给系统决定刷盘时机,性能好但断电时可能丢失Binlog记录;
  • 为N时:每N次事务提交才刷盘一次。

为了保证不丢事务,行业内通常建议sync_binlog=1,同时将innodb_flush_log_at_trx_commit=1,也就是常说的“双1”配置。这个组合代表每次提交都会把Redo Log和Binlog都强制刷盘。确认提交成功的事务,在主库崩溃后仍然能够恢复和同步,代价是TPS会有一定下降。

4.5 组提交:双1之下性能也没那么难看的秘密

“双1”每次都刷盘,看起来会很慢,但InnoDB的组提交(group commit)机制大大缓解了这个压力。多个并发事务在提交阶段会凑在一起,一次刷盘操作可以覆盖多个事务的Redo Log/Binlog。因此,在并发量足够的场景下,“双1”的性能并没有很多人想象中那么差。如果业务并发低,性能损耗才会明显体现。

5. 一张图讲透事务提交全流程和那些绕不开的坑

把三种日志放进一个完整事务里,你会发现它们的协作关系异常清晰。这里我用文字流程来复现这个全生命周期。

5.1 一条UPDATE语句走过的一生

假设连接ID=10的会话执行:UPDATE user SET age=25 WHERE id=1;

  1. 事务开启,获取一个事务ID;
  2. InnoDB先把id=1这一行的旧数据(比如age=24)写入Undo Log;
  3. 在Buffer Pool中修改该行数据,把age从24改成25;
  4. 同时生成一条Redo Log,记录“在页X的偏移Y处,age字段从24变为25”,并把日志写入Redo Log buffer;
  5. 如果此时用户ROLLBACK,就依据Undo Log把age改回24;
  6. 如果用户COMMIT,进入两阶段提交:Redo Log先刷盘(prepare),Binlog写盘(commit),最后再在Redo Log上补标记commit;
  7. 后台刷盘线程择机把脏页写回磁盘。

步骤2和步骤4是同步记录的,步骤7是异步完成的。这就是为什么“日志先行”和“数据后写”让你既快又安全。

5.2 经典一问:Binlog能替代Redo Log吗

不能。原因有三:

  • Binlog逻辑日志,重放还要重新执行SQL计算,恢复速度慢;Redo Log物理逻辑日志,恢复时直接按字节覆盖对应数据页,快得多。
  • Binlog由Server层产生,InnoDB引擎崩溃恢复时根本不会读Binlog;Redo Log才是InnoDB自己恢复状态的依据。
  • Binlog可以记录所有引擎的变更,Redo Log只属于InnoDB。

反过来,Redo Log也不能替代Binlog,因为Redo Log是环形覆盖的,无法提供“任意时间点”的逻辑恢复能力,也没有可读性。

5.3 Redo Log和Undo Log能互相替代吗

也不能。理解这对关系可以这样想:

  • Undo Log解决的问题是“事务执行错了/我不想要了,怎么撤销”,以及“别人读旧版本数据时,去哪里找历史快照”;
  • Redo Log解决的问题是“数据库突然崩溃后,怎么把已提交但还没落盘的修改重放回来”。

一个管“撤销”,一个管“重放”,层面不同、目标不同。但有趣的是,两者都服务于事务的原子性和持久性,只是分别站在前后两个方向上。崩溃恢复时先重放Redo Log后回滚Undo Log的先后顺序,注定它们在系统内的存在方式完全不同。

5.4 长事务为什么危险:一种容易被忽视的连锁反应

长事务会同时拖垮Redo Log和Undo Log:

  • Undo Log版本链过长,读取性能下降,ibdata1或undo表空间膨胀;
  • 长事务一旦持有大量行锁,会阻塞其他事务的DML操作,严重时拖垮整个实例;
  • 长事务存在期间,purge线程清理滞后,后续版本的老数据无法回收,进一步加剧存储压力。

这类问题在开发环境里很难复现,但到了生产环境,一个忘记提交的分布式事务就能让你凌晨爬起来处理报警。

6. 三本日志的日常运维观察、常见操作与真实踩坑

原理归原理,实际运维中碰到的问题更多。“我看不到Redo Log的内容啊”“Binlog删了会不会出大事”“Undo表空间怎么又涨了”,这些都是高频问题。

6.1 怎么查看Binlog:别只会用记事本打开

Binlog是二进制文件,直接打开必然乱码。推荐的做法:

  • 在MySQL里执行SHOW BINLOG EVENTS,可查看当前Binlog文件里的具体事件列表;
  • 用mysqlbinlog工具解析成可读SQL:
mysqlbinlog --base64-output=decode-rows -vv /var/lib/mysql/binlog.000123

这条命令尤其适合ROW格式,可以把行的前后镜像都展示出来,对排查误操作和数据恢复很有用。如果只想看某个时间段的日志,可以加上--start-datetime和--stop-datetime参数。

6.2 Binlog日志可以删除吗?安全删除姿势

答案是“可以,但要分情况”。Binlog是增量恢复和主从复制的数据源,盲目删除可能导致:

  • 从库还没把某个文件拉取完,你把它删了,主从复制直接断掉;
  • 想按时间点恢复数据时,发现关键时间段的Binlog缺失。

安全做法:

  • 确认所有从库的复制进度都已经越过了待删除的Binlog文件;
  • 用系统自带参数清理:
PURGE BINARY LOGS TO 'binlog.000456'; -- 删除该文件之前的日志 PURGE BINARY LOGS BEFORE NOW() - INTERVAL 7 DAY; -- 删除7天前的日志

或者直接配置过期自动清理:

SET GLOBAL binlog_expire_logs_seconds = 604800; -- MySQL 8.0 SET GLOBAL expire_logs_days = 7; -- MySQL 5.7及之前

注意MySQL 8.0里expire_logs_days已废弃,用binlog_expire_logs_seconds。

6.3 观察Redo Log的状态和大小

想看Redo Log写入情况,最方便的是:

SHOW ENGINE INNODB STATUS;

其中的LOG段会展示当前LSN、已刷盘LSN、checkpoint等指标。如果发现Log sequence number和Last checkpoint at之间的差值长期接近Redo Log总容量上限,说明日志即将被覆盖而脏页来不及刷盘,这时候要考虑:

  • 调大innodb_log_file_size(MySQL 8.0里是innodb_redo_log_capacity);
  • 优化长时间运行的查询或事务;
  • 检查存储盘IO能力是否成为刷盘瓶颈。

MySQL 8.0从8.0.30起,Redo Log改为由innodb_redo_log_capacity统一管理,默认100MB,可在线调整,比早期版本更灵活。

6.4 真实踩坑:一次“Binlog缺失”导致的恢复失败

我之前在一个测试环境做过一次模拟崩实验:把实例直接kill -9,重启后一切正常,主从也没问题。后来想测试时间点恢复,翻历史Binlog时发现,binlog文件在凌晨被自动清理过,而全量备份是三天前的,中间隔了一天空档。也就是说,我根本没有办法把数据恢复到那个被误删的时间点。

这次之后,我把Binlog保留策略改成了按天保留15天,同时把备份任务和Binlog清理任务排成依赖关系,确保“备份成功后,才允许清理之前的Binlog”。如果你维护核心业务库,建议把这个流程固化下来,不要指望“应该还没被清”。

6.5 关于“三本日志”的一个有效观察顺序

新手接着看日志,很容易一头雾水。我的建议是从一个简单事务开始,逐步观察:

  1. 开启general_log,记录所有SQL;
  2. 手动执行一条UPDATE;
  3. 查看Binlog中的记录内容;
  4. 看information_schema.innodb_trx,观察事务状态;
  5. 用SHOW ENGINE INNODB STATUS看Redo Log的LSN变化;
  6. 模拟一次kill -9,重启后对比数据变化。

这套实验流程比看任何文档都更能建立直觉。学底层原理,最好的方式不是背概念,而是亲手制造一次故障,看日志怎么把你从坑里捞出来。

我个人在实际调优中感受最深的一点:不要把三种日志割裂开理解。它们之所以存在,是围绕同一个核心目标——在保证性能的前提下,让数据库尽量不丢数据、不让主从不分家。理解了WAL这条主线,再去看各种参数和异常现象,基本都能顺着逻辑推导出原因,而不是靠记忆硬背。

最后分享一个排查小技巧:当线上出现“事务提交成功了,但从库查不到数据”这样的诡异问题时,先去查show master status和从库的show slave status里Exec_Master_Log_Pos是否差距持续拉大,再考虑是不是某种极端情况下Binlog本身没记录完整。把日志类型和时间线画出来对比,80%的疑难杂症都能找到线索。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询