☰
MySQL三大日志binlog、redo log、undo log:原理、协作与实战排查
2026/10/7 3:43:03 网站建设 项目流程

聊到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或行变更)主从复制、时间点恢复自动过期或手动PURGE
undo logInnoDB引擎层修改前的旧值回滚、MVCCpurge线程异步清理

1.2 用一个生活类比记住它们的区别

我常给新人打个比方:假设你是一家面馆的老板,每天卖了多少碗面、收了多少钱,得有本流水账,这是binlog,任何人想核对账目都能翻。后厨的面条和卤料是真正摆在那里的“数据”,你不可能每做一碗面就把全部家当重新记一遍,你只需要在脑子里留个“今天卤料用掉了三斤”的便签,这是redo log,为的是店突然停电时你能快速把货补齐。而undo log就是“记账前的草稿”,顾客说不要了要退,你得知道刚才那碗面是按什么配料做的,照草稿撤掉就行。

这个类比基本能对应上:binlog对外,可追溯、可复现;redo log对内,保命、防丢;undo log则是给“反悔”留的后路。理解了这三者的定位,后面的细节才有地方挂靠。

2. redo log:崩溃恢复的底牌

2.1 WAL为什么是最低成本方案

redo log的核心思想是WAL(Write-Ahead Logging),先写日志,再写数据。这个顺序不是随便定的,背后有一笔很实在的成本账:如果每次修改都直接随机写落到磁盘的数据页上,性能会惨不忍睹。数据库的更新是随机IO,磁盘寻道本身就慢;但redo log追加写入是顺序IO,同样落盘,速度能差一到两个数量级。

所以InnoDB的策略是:先把变更记录到redo log里,告诉系统“这事已经定了”,然后等后台线程慢慢把脏页刷到磁盘。这样哪怕刷盘之前数据库突然崩溃,重启时靠redo log也能把没刷下去的数据页恢复出来。这就是“写入日志成功了,事务才算成功”的本质原因。

还有一个细节很多人忽略:redo log记录的是物理页变更,恢复时直接覆盖对应页的对应字节,不需要重跑SQL。所以崩溃恢复看redo,恢复速度取决于需要重放的日志量,而不是业务复杂度。这也是为什么恢复时要关心LSN(Log 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)。一个事务提交时走的是这样的流程:

  1. InnoDB先把redo log写入磁盘,状态标记为prepare(准备阶段)。
  2. Server层将事务的binlog写入磁盘并完成刷盘。
  3. InnoDB再把redo log状态改为commit(提交阶段)。

关键就在这里:崩溃恢复时,如果发现一个事务的redo是prepare状态,系统会去binlog里查这个事务的XID是否存在。binlog里能查到,说明事务已经完整写入并通过了同步,主库必须补上提交,保证主备一致;查不到,说明binlog没写成功,主库就回滚这个事务。这套规则保证了主库和备库最终落在一个状态上——要么两边都有,要么两边都没有。

理解了这一步,你再去看面试题“两阶段提交的过程”,就不会只是背三段话了。你还能顺带解释为什么必须走这个顺序:如果颠倒过来,先写binlog再写redo,一旦崩溃,binlog里有记录而redo没有对应状态,InnoDB根本不知道有这个事务,主备就分裂了。

3.3 binlog到底能不能删,怎么安全删

这个问题的答案很明确:能删,但要有章法地删。binlog不是越多越好,默认情况下MySQL会根据binlog_expire_logs_seconds(8.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-datetime="2024-07-20 10:00:00" --stop-datetime="2024-07-20 10:30:00" /var/lib/mysql/mysql-bin.000015 > recover.sql

导出后看SQL内容,找到误删的那条DELETE或TRUNCATE语句,把它的“镜像逆操作”反向写出来。如果是DELETE,row格式下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针对不同操作会记录不同形式的undo:

  • INSERT操作:undo里记录新插入行的主键。回滚时只要根据主键把这条记录删除就行。
  • UPDATE操作:undo里记录被修改行的旧值(修改前的整行数据)。回滚时用旧值覆盖当前行的数据。
  • DELETE操作:在InnoDB内部其实做成了“标记删除”,对应undo里记录的是“行了,现在标记为已删除”的元信息,真正的物理删除要等purge线程在一定条件下执行。

所以回滚的本质很简单:INSERT的反向操作是DELETE,UPDATE的反向操作是把旧值写回去,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。这一步之后,真正的操作才展开:

  1. 在undo log里写入这条记录修改前的旧值(name原来的值)。
  2. 在Buffer Pool中直接修改这行数据,内存里的数据页变成新值,但这个页此时是脏页,还没有刷到磁盘。
  3. 把“数据页XX第XX偏移量改成了新值”这条物理变更写入redo log buffer,状态为prepare。
  4. 事务提交时,InnoDB把redo log刷盘,状态标记prepare。
  5. Server层把这个事务的binlog写入磁盘,完成fsync。
  6. InnoDB把redo log状态改为commit,事务真正完成,客户端收到成功返回。
  7. 后台刷脏线程闲着没事的时候,把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的prepare+commit配合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和Position
binlog文件列表SHOW BINARY LOGS文件大小、预计过期时间
redo刷盘参数SHOW VARIABLES LIKE 'innodb_flush_log_at_trx_commit'按数据安全级别选1/2
redo容量SHOW VARIABLES LIKE 'innodb_redo_log_capacity'容量是否偏小导致频繁checkpoint
undo膨胀SHOW ENGINE INNODB STATUSHistory list length持续高
长事务information_schema.innodb_trxtrx_started时间过老、trx_query为空
主从复制状态SHOW SLAVE STATUS\GSeconds_Behind_Master、Read_Master_Log_Pos

6.2 高频问题速查表

这里把我在社区和工作中经常遇到的三大日志问题做个汇总:

  1. binlog可以删除吗?可以。用PURGE BINARY LOGS或设置过期参数自动清理,禁止直接rm。删除前确认备库位置。
  2. 磁盘被undo撑爆了?先查长事务,杀掉最老的活跃事务,等待purge线程清理;如果空间等不了,8.0可以配置undo表空间自动truncate,5.7需要重建undo表空间。
  3. redo太小导致频繁checkpoint?把innodb_redo_log_capacity调大(8.0)或者把innodb_log_file_size调大,观察写入高峰和checkpoint推进的匹配情况。
  4. 备库总比主库慢?先查binlog_format是不是row,再检查备库是否配置了standby redo log,还有备库所在磁盘IO能力。
  5. 事务提交很慢?优先检查innodb_flush_log_at_trx_commit和sync_binlog是不是都等于1,这俩配合起来确实安全,但每次提交都有两次fsync。如果实在要优化性能,先评估业务能不能接受“丢最后1秒”的风险再调参数。
  6. 改成row格式后binlog暴涨?这是正常的,row格式记录行变更,日志量确实大。接受它,或者从业务侧减少无谓的大范围DELETE/UPDATE,而不是退回statement。

最后再分享一个我个人的实操心得:三大日志的监控,不能只看磁盘空间和进程状态,更要看“速度”。redo的写入速度是否跟得上业务高峰、binlog的生成速率和清理速率是否平衡、undo的purge效率是否正常,这三个速率指标才是真正决定系统长期稳定性的东西。我习惯把SHOW MASTER STATUS、SHOW ENGINE INNODB STATUS这些采样结果落到监控系统里,配合主从延迟和磁盘增长曲线一起看,很多问题在发生前一周就能从趋势里嗅到味道。日志这东西,平时不显山不露水,但数据库的每一次断电恢复、每一笔数据找回、每一份主从一致的保证,背后都是它们在默默兜底。

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

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

立即咨询