
聊到MySQL的可靠性和一致性翻来覆去绕不开这三个词binlog、redo log、undo log。后端面试手册里它们是常客生产环境里出过的诡异问题十有八九也和它们有关。很多人背过概念但真到了现场——日志文件飘红、备库追不上主库、崩溃恢复慢得离谱——就抓瞎了。这篇文章我从三个日志各自的职责讲起把WAL机制、两阶段提交、MVCC版本链这些容易混淆的底层逻辑拆开揉碎再带一条真实的UPDATE语句走完全程最后附上我踩过的坑和排查命令速查。适合正在准备MySQL面试的后端开发也适合被主从延迟、磁盘突增、数据恢复折磨过的DBA和运维。看完你至少能回答三个问题为什么MySQL重启不丢已提交事务binlog到底能不能删事务回滚靠的是什么1. 先搞清楚三兄弟的分工才谈得上深入理解1.1 一张表看懂三大日志很多教程喜欢从“redo是物理日志、binlog是逻辑日志、undo是回滚日志”这种定义讲起定义没错但太抽象。我用最贴近实际的话先说结论redo log是InnoDB存储引擎层的日志管的是“崩溃恢复”。它记录的是“某个数据页的某个位置被改成了什么”属于物理级别的记录。MySQL宕机了内存里没来得及刷盘的脏数据就靠它重放回来。binlog是MySQL Server层的日志管的是“主从复制”和“基于时间点的恢复”。它记录的是“数据库做了哪些变更”无论什么存储引擎都走它。备库同步、误删恢复、审计追踪全是binlog的活。undo log是InnoDB层的日志管的是“事务回滚”和“MVCC多版本控制”。它记录的是“修改之前的值”事务要回滚的时候照着它把数据改回去读多版本数据的时候顺着它往上找。这些概念可以放到一张表里对照着看日志所在层级记录内容核心作用清理方式redo logInnoDB引擎层物理页修改页号偏移量新值崩溃恢复、持久性循环覆盖checkpoint自动推进binlogServer层逻辑变更SQL或行变更主从复制、时间点恢复自动过期或手动PURGEundo logInnoDB引擎层修改前的旧值回滚、MVCCpurge线程异步清理1.2 用一个生活类比记住它们的区别我常给新人打个比方假设你是一家面馆的老板每天卖了多少碗面、收了多少钱得有本流水账这是binlog任何人想核对账目都能翻。后厨的面条和卤料是真正摆在那里的“数据”你不可能每做一碗面就把全部家当重新记一遍你只需要在脑子里留个“今天卤料用掉了三斤”的便签这是redo log为的是店突然停电时你能快速把货补齐。而undo log就是“记账前的草稿”顾客说不要了要退你得知道刚才那碗面是按什么配料做的照草稿撤掉就行。这个类比基本能对应上binlog对外可追溯、可复现redo log对内保命、防丢undo log则是给“反悔”留的后路。理解了这三者的定位后面的细节才有地方挂靠。2. redo log崩溃恢复的底牌2.1 WAL为什么是最低成本方案redo log的核心思想是WALWrite-Ahead Logging先写日志再写数据。这个顺序不是随便定的背后有一笔很实在的成本账如果每次修改都直接随机写落到磁盘的数据页上性能会惨不忍睹。数据库的更新是随机IO磁盘寻道本身就慢但redo log追加写入是顺序IO同样落盘速度能差一到两个数量级。所以InnoDB的策略是先把变更记录到redo log里告诉系统“这事已经定了”然后等后台线程慢慢把脏页刷到磁盘。这样哪怕刷盘之前数据库突然崩溃重启时靠redo log也能把没刷下去的数据页恢复出来。这就是“写入日志成功了事务才算成功”的本质原因。还有一个细节很多人忽略redo log记录的是物理页变更恢复时直接覆盖对应页的对应字节不需要重跑SQL。所以崩溃恢复看redo恢复速度取决于需要重放的日志量而不是业务复杂度。这也是为什么恢复时要关心LSNLog Sequence Number它本质上是日志的顺序编号用来标记“日志写到了哪、数据页刷到了哪”。2.2 三个刷盘参数实测差异很大redo log的写入链路是这样的事务修改先把日志写进内存里的log buffer然后由不同时机刷到磁盘上的redo log文件。控制这个“时机”的参数就是innodb_flush_log_at_trx_commit它有0、1、2三档生产环境选哪档直接关系到数据安全级别和性能。值为1每次事务提交都强制把log buffer刷到磁盘fsync。最安全任何崩溃都不丢已提交事务但每次提交都有一次磁盘同步高并发下明显拉高响应时间。值为0提交时不主动刷盘依赖后台每秒刷一次。性能最好但mysqld进程本身崩溃就可能丢最近一秒内已提交的事务。值为2提交时把日志写到操作系统缓存但不立即fsync每秒由后台统一刷磁盘。MySQL进程崩溃不丢数据只有整个操作系统断电时可能丢最近一秒的数据。我个人建议如果业务对数据安全敏感订单、支付、账户类老老实实用1如果是一些允许少量丢失的日志库、统计库可以折中选2。选0属于极限压测场景才考虑业务常规使用不太推荐。另外innodb_log_buffer_size默认16MB如果大量大事务同时提交buffer容易写满这时候即使没到提交时机也会被迫刷盘反而增加随机IO。线上如果常见“log buffer被撑爆”的告警可以适当调到64MB或更大但别指望它解决所有性能问题它只是缓冲。2.3 checkpoint、LSN和环形写redo log文件不是无限增长的它采用环形写一组固定大小的文件默认在datadir下名为ib_logfile0、ib_logfile1等写满最后一个再回到第一个覆盖。覆盖的前提是“这些日志已经没有用了”也就是对应的脏页已经全部刷到磁盘这个推进的标记就是checkpoint。checkpoint和日志写入位置之间有一段安全区这段区域的redo log是崩溃恢复要用到的。如果checkpoint停滞不前日志写入点很快会追上checkpoint这时InnoDB会被迫做一次强力刷盘来推进checkpoint表现为瞬间大量的磁盘写IO、性能毛刺。这类问题常见于磁盘太慢、脏页刷盘跟不上、或者buffer pool过大导致刷页不及时。关于redo log文件大小我用过的实例一般配置innodb_log_file_size为1GB到4GB具体看写入量。文件太小会导致频繁checkpoint文件太大会让崩溃恢复扫描时间变长。MySQL 8.0.30之后引入了innodb_redo_log_capacity把多个redo文件统一交给系统管理省去了手工调单个文件的麻烦新部署的环境建议直接看这个参数。3. binlog复制的血液和恢复的时光机3.1 row、statement、mixed三种格式怎么选binlog有三种记录格式这是主从复制领域最高频的坑位之一。statement格式记录的是原始SQL。日志量小但隐患极大同一个SQL在主库和备库执行结果可能不同。最典型的例子是NOW()、UUID()这类函数主库执行时和备库执行时取值就不一样再比如带LIMIT的UPDATE主备数据分布不同更新行数都可能对不上。这种格式我现在基本不推荐使用。row格式记录的是每行变更前后的值。优点是无脑安全主备绝对一致缺点是日志量成倍增加尤其是大范围UPDATE或DELETE可能生成巨大的binlog。mixed格式是MySQL自己判断大部分情况下用statement碰到危险场景自动切换row。看起来很智能但线上出过不少“判断失误”导致的同步问题排查成本很高。在我处理过的案例里网上各种主从不同步的求助超过一半最后都追溯到statement和mixed格式。所以我的结论很简单统一用row别纠结那点日志量。数据安全永远优先。MySQL 5.7.7之后和8.0默认就是row顺着默认走就行。3.2 两阶段提交redo和binlog怎么做到一致binlog属于Server层redo属于InnoDB层两个日志各写各的就存在一致性问题如果先写binlog、InnoDB还没来得及提交事务实际上没生效但备库已经从binlog同步执行了反过来先提交InnoDB、binlog没写成功主库事务生效了备库却少了一段数据。为了解决这个矛盾InnoDB引入了两阶段提交Two-Phase Commit。一个事务提交时走的是这样的流程InnoDB先把redo log写入磁盘状态标记为prepare准备阶段。Server层将事务的binlog写入磁盘并完成刷盘。InnoDB再把redo log状态改为commit提交阶段。关键就在这里崩溃恢复时如果发现一个事务的redo是prepare状态系统会去binlog里查这个事务的XID是否存在。binlog里能查到说明事务已经完整写入并通过了同步主库必须补上提交保证主备一致查不到说明binlog没写成功主库就回滚这个事务。这套规则保证了主库和备库最终落在一个状态上——要么两边都有要么两边都没有。理解了这一步你再去看面试题“两阶段提交的过程”就不会只是背三段话了。你还能顺带解释为什么必须走这个顺序如果颠倒过来先写binlog再写redo一旦崩溃binlog里有记录而redo没有对应状态InnoDB根本不知道有这个事务主备就分裂了。3.3 binlog到底能不能删怎么安全删这个问题的答案很明确能删但要有章法地删。binlog不是越多越好默认情况下MySQL会根据binlog_expire_logs_seconds8.0默认2592000秒也就是30天5.7里对应expire_logs_days按天计算自动清理过期文件。手动删的场景通常是磁盘快满了需要应急可以这样处理# 删除所有早于指定日志文件的binlog PURGE BINARY LOGS TO mysql-bin.000010; # 删除指定时间点之前的binlog PURGE BINARY LOGS BEFORE 2024-06-01 00:00:00; # 查看当前正在写的binlog文件 SHOW MASTER STATUS; # 列出所有存在的binlog文件 SHOW BINARY LOGS;这里有两个我踩过的坑要特别提醒。第一绝对不要用rm命令直接删binlog文件。binlog的索引文件mysql-bin.index里记录着每个文件的名字直接rm会导致索引与实际文件对不上后续flush logs或者自动清理都可能报错。第二如果备库的IO线程还在读取某个binlog这时候执行PURGE会被拒绝或者导致复制中断。所以清理前先去看备库状态SHOW SLAVE STATUS\G确认Read_Master_Log_Pos的位置别把备库还在用的文件清了。3.4 用binlog做误删恢复的真实操作说个实际场景误删了一张表的数据赶紧停掉业务写入然后要用binlog把数据捞回来。具体操作思路是这样的先把目标位置的binlog导成SQL# 将某个binlog文件中指定时间段的SQL导出 mysqlbinlog --no-defaults --start-datetime2024-07-20 10:00:00 --stop-datetime2024-07-20 10:30:00 /var/lib/mysql/mysql-bin.000015 recover.sql导出后看SQL内容找到误删的那条DELETE或TRUNCATE语句把它的“镜像逆操作”反向写出来。如果是DELETErow格式下binlog里会带上被删行的完整数据可以用mysqlbinlog --flashback类的工具转成INSERT如果没有这类工具就手工把binlog解析出的数据拼成INSERT语句再导回。这里最关键的经验是平时就要开row格式并且保留足够的binlog否则真出事的时候巧妇难为无米之炊。我还习惯在binlog里对敏感表定期做一次timetravel备份、配合定期全量备份这样恢复时只需要把全量备份恢复到出问题前的状态再用binlog追增量恢复窗口从几小时缩短到几分钟。顺带提一句主从场景里备库配置standby redo log可以在备库启用实时应用时减少IO开销、加快最新数据的应用速度我之前调过一例延迟严重的备库这个配置对缩短延迟窗口很有帮助。4. undo log回滚与MVCC的双面手4.1 undo记录了什么东西很多人都知道undo log是用来回滚的但不太清楚它具体记了什么。实际上InnoDB针对不同操作会记录不同形式的undoINSERT操作undo里记录新插入行的主键。回滚时只要根据主键把这条记录删除就行。UPDATE操作undo里记录被修改行的旧值修改前的整行数据。回滚时用旧值覆盖当前行的数据。DELETE操作在InnoDB内部其实做成了“标记删除”对应undo里记录的是“行了现在标记为已删除”的元信息真正的物理删除要等purge线程在一定条件下执行。所以回滚的本质很简单INSERT的反向操作是DELETEUPDATE的反向操作是把旧值写回去DELETE的反向操作是取消删除标记。undo log就是一个记录“反做”脚本的账本。4.2 MVCC版本链和可见性判断undo log的另一个身份是MVCC的基础设施。InnoDB的数据页上每一行记录都有两个隐藏列trx_id最近一次修改这个事务的事务ID和roll_pointer指向该行undo log版本的指针。当多事务并发地修改同一行时这行会产生一个版本链最新版本在数据页上历史版本链在undo log里通过roll_pointer串起来。读操作判断一个版本是否可见靠的是ReadView读视图。ReadView里记录了生成时刻仍在活跃的事务列表、最小活跃事务ID、最大事务ID等信息。判断规则简单说是这样某个版本的trx_id小于ReadView里的最小活跃事务ID说明这个事务在ReadView生成前已提交可见trx_id等于或大于最大事务ID说明这个事务在ReadView生成时还没开始或正在活跃不可见。REPEATABLE READ和READ COMMITTED两种隔离级别的差别也体现在这里READ COMMITTED每次查询都生成新的ReadView所以能看到其他事务新提交的变更REPEATABLE READ在事务第一次查询时生成ReadView并一直沿用所以整个事务期间看到的一致快照不变。4.3 undo膨胀和purge线程的坑undo log不会永久保留。事务提交后该事务产生的undo在“没有其他事务需要用它做MVCC判断”的前提下会被purge线程异步清理。但有两个场景会让undo疯狂膨胀。第一个是长事务。一个事务开了很长时间不提交期间的所有变更产生的undo都处于活跃状态其他并发事务如果被阻塞或需要旧版本这些undo就删不掉。我遇到过的最极端案例一条报表查询跑了四个多小时期间业务持续写入上千万行undo表空间直接顶爆了磁盘。第二个是大量并发更新。短事务再多如果更新频率极高undo的生成速度远超purge线程清理速度同样会积累。排查这类问题我会重点看SHOW ENGINE INNODB STATUS\G里的History list length这个值代表“未被清理的历史版本链长度”如果持续高位甚至一直往上涨基本可以断定有长事务或purge阻塞。MySQL 8.0之后undo表空间支持自动truncate配置innodb_undo_log_truncate和innodb_max_undo_log_size之后超过阈值会自动收缩比5.7时代好处理多了。但别指望全自动你依然需要定期监控历史版本链长度。5. 一条UPDATE语句三大日志的完整协作5.1 从开启事务到真正落盘讲完三个日志各自的事现在把它们串起来看一条UPDATE语句在MySQL内部是怎么走完全程的。假设表t有字段id和name执行UPDATE t SET name alice WHERE id 1;这条语句涉及的数据页可能不在内存中那么InnoDB会先从磁盘把这条记录所在的数据页加载到Buffer Pool。这一步之后真正的操作才展开在undo log里写入这条记录修改前的旧值name原来的值。在Buffer Pool中直接修改这行数据内存里的数据页变成新值但这个页此时是脏页还没有刷到磁盘。把“数据页XX第XX偏移量改成了新值”这条物理变更写入redo log buffer状态为prepare。事务提交时InnoDB把redo log刷盘状态标记prepare。Server层把这个事务的binlog写入磁盘完成fsync。InnoDB把redo log状态改为commit事务真正完成客户端收到成功返回。后台刷脏线程闲着没事的时候把Buffer Pool里这页数据刷到磁盘此时才算是数据真正落盘。注意看业务刚拿到“更新成功”的反馈时数据可能还在内存里只有redo log和binlog是实实在在写在磁盘上的。你不用因此焦虑——这正是WAL设计的结果日志已经保证了这个事务不会丢后台脏页早晚会落盘。5.2 崩溃了怎么救prepare/commit状态的恢复规则现在加入故障场景。假设上面这个事务提交过程中在不同时间点掉电会有什么结果这是理解MySQL崩溃恢复最核心的地方。场景一redo log还没写入磁盘就崩溃。事务根本还没到prepare状态MySQL启动后直接就当这个事务不存在回滚所有相关修改实际上这时也没有已提交事务需要救。场景二redo已经到了prepare状态、binlog还没写完就崩溃。这时启动后InnoDB会在redo里发现这个prepare事务去binlog里找它的XID找不到于是判定binlog没有完整记录这个事务为了保证主备一致回滚。场景三redo是prepare、binlog也写完并落盘了在redo状态改为commit之前崩溃。启动后InnoDB同样找到这个prepare事务去binlog里能找到对应XID说明备库后续会同步到这个事务主库必须也提交于是补一刀提交。这样主备两边最终状态一致。场景四redo已经commit之后数据页迟迟没刷盘就崩溃。启动后直接把数据页从redo里重放出来已提交事务的数据依然完整。把这四个场景记清楚以后任何“为什么崩溃不丢已提交数据”的追问你都能用“redo的preparecommit配合binlog做交叉判断”这句话接住。这也就是两阶段提交真正发挥价值的时刻。6. 实战排查清单与高频问题6.1 三张系统表的查询方法平时排查三大日志问题我首先会用这几条命令快速定位-- 查看当前binlog文件和位置 SHOW MASTER STATUS; -- 查看所有binlog文件及大小 SHOW BINARY LOGS; -- 查看InnoDB引擎状态重点看LOG信息和History list length SHOW ENGINE INNODB STATUS\G -- 查看事务是否长事务、活跃事务数量 SELECT * FROM information_schema.innodb_trx\G -- 查看undo表空间大小 SELECT * FROM information_schema.innodb_tablespaces WHERE name LIKE %undo%;手上有一张现成的速查表比临时翻文档快得多。我常用的排查命令整理如下排查目标命令/方法关键关注点当前binlog位置SHOW MASTER STATUSFile和Positionbinlog文件列表SHOW BINARY LOGS文件大小、预计过期时间redo刷盘参数SHOW VARIABLES LIKE innodb_flush_log_at_trx_commit按数据安全级别选1/2redo容量SHOW VARIABLES LIKE innodb_redo_log_capacity容量是否偏小导致频繁checkpointundo膨胀SHOW ENGINE INNODB STATUSHistory list length持续高长事务information_schema.innodb_trxtrx_started时间过老、trx_query为空主从复制状态SHOW SLAVE STATUS\GSeconds_Behind_Master、Read_Master_Log_Pos6.2 高频问题速查表这里把我在社区和工作中经常遇到的三大日志问题做个汇总binlog可以删除吗可以。用PURGE BINARY LOGS或设置过期参数自动清理禁止直接rm。删除前确认备库位置。磁盘被undo撑爆了先查长事务杀掉最老的活跃事务等待purge线程清理如果空间等不了8.0可以配置undo表空间自动truncate5.7需要重建undo表空间。redo太小导致频繁checkpoint把innodb_redo_log_capacity调大8.0或者把innodb_log_file_size调大观察写入高峰和checkpoint推进的匹配情况。备库总比主库慢先查binlog_format是不是row再检查备库是否配置了standby redo log还有备库所在磁盘IO能力。事务提交很慢优先检查innodb_flush_log_at_trx_commit和sync_binlog是不是都等于1这俩配合起来确实安全但每次提交都有两次fsync。如果实在要优化性能先评估业务能不能接受“丢最后1秒”的风险再调参数。改成row格式后binlog暴涨这是正常的row格式记录行变更日志量确实大。接受它或者从业务侧减少无谓的大范围DELETE/UPDATE而不是退回statement。最后再分享一个我个人的实操心得三大日志的监控不能只看磁盘空间和进程状态更要看“速度”。redo的写入速度是否跟得上业务高峰、binlog的生成速率和清理速率是否平衡、undo的purge效率是否正常这三个速率指标才是真正决定系统长期稳定性的东西。我习惯把SHOW MASTER STATUS、SHOW ENGINE INNODB STATUS这些采样结果落到监控系统里配合主从延迟和磁盘增长曲线一起看很多问题在发生前一周就能从趋势里嗅到味道。日志这东西平时不显山不露水但数据库的每一次断电恢复、每一笔数据找回、每一份主从一致的保证背后都是它们在默默兜底。