news 2026/9/9 6:38:21

MySQL三大日志:redo log、undo log、binlog 原理与实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MySQL三大日志:redo log、undo log、binlog 原理与实战

1. 从一次意外的宕机说起:日志系统的整体定位

没有人愿意在自己睡得正香的时候,被一条"数据库无法连接"的告警电话吵醒。我记得前几年有一次凌晨三点,线上一个支付核心库突然崩溃,当时最吓人的并不是"服务挂了",而是重启MySQL之后,那几十秒数据校验的过程——心里完全没底。后来DBA同事告诉我,其实真正让我们从崩溃中全身而退的,就是InnoDB里那套环环相扣的日志机制。所以我才决定把redo log、undo log、bin log这三兄弟彻底弄明白,而不是停留在"面试背八股"的层面。

在很多人的认知里,MySQL的日志就是"出了事回滚一下""主从同步靠binlog"这么简单。但真实情况比这复杂得多:如果你做过一次完整的故障复盘,就会发现一次小型的数据库崩溃,牵扯到的至少是三层问题——数据文件是否已经被脏页刷盘?事务是否已经提交但还没来得及写进数据文件?从库是否已经追上主库?这三个问题恰好就是undo log、redo log、bin log各自负责的领域。

1.1 为什么一条SQL会产生三种日志

先给你一个直观的场景。假设你执行了一条非常简单的更新语句:

UPDATE user SET balance = balance - 100 WHERE id = 1;

这条语句在MySQL内部会经历这样的路径:先通过SQL层解析、优化、执行,找到id=1这一行数据,然后在InnoDB层真正修改数据。如果数据页不在Buffer Pool里,先从磁盘读进来,在内存里改掉这个值。此时数据页已经"脏"了,但还没写回磁盘。

与此同时,为了保证"崩溃后能恢复""事务能回滚""主从能同步",InnoDB和binlog会各自动手:

  • redo log记下"我把id=1的balance从500改成了400",用于崩溃恢复时重放(redo)。
  • undo log记下"id=1的balance原来是500",用于事务回滚或者MVCC读旧版本。
  • binlog记下这条SQL或它的行变更结果,用于主从复制、误删恢复、离线分析。

这三份日志分工完全不同,却又在事务提交时互相配合。如果不把它们的边界搞清楚,遇到问题的时候很容易走弯路。

1.2 一个类比帮你建立整体印象

你可以把MySQL想象成一家银行柜台。客户来办业务,柜员先在叫号单上记一笔"下一步要做什么"——这就是redo log,先写下来再说;然后把客户原来账户里的余额抄在一个小本子上——这就是undo log,万一办到一半客户反悔了,照着本子改回去;最后,每天下班前把所有交易流水打包送到总行——这就是binlog,总行根据流水同步所有分行的数据。

这个类比虽然粗糙,但能帮你记住三者的核心区别:redo log是物理层面的"防丢",undo log是逻辑层面的"能回退",binlog是全局层面的"可复制、可恢复"。

2. redo log:崩溃恢复的顶梁柱

redo log可能是三兄弟里最"底层"、也最容易被忽视的。因为平时正常运行时你根本感知不到它,只有数据库异常断电、进程被杀、系统崩溃的时候,它才是真正的救世主。

2.1 WAL:先写日志,再动数据

InnoDB有一个核心设计原则叫WAL,全称Write-Ahead Logging,翻译过来就是"预写日志"。这个机制的意思很简单:在对数据页进行修改之前,必须先把本次修改的物理信息记录到redo log中,并且保证redo log先于数据页落盘。

为什么非要这么绕一圈,而不是直接在内存改完数据页立刻刷盘?原因就俩字:性能。数据页的写盘是随机I/O,按照主键或者索引把页分布在很多地方,而redo log是小块的追加写,连续I/O的速度远比随机I/O快。你可以把redo log想象成快递员手里的运单记录,他先把所有包裹的收货地址写在一个小本子上,等到攒够一大车再统一派送,比一个一个满城跑高效得多。MySQL也是这样:数据页的修改先回收到日志里,脏页的刷盘可以延后,由后台线程慢慢处理。

2.2 redo log的物理属性和结构

redo log本质上是物理日志,它记录的并不是"把balance减100"这类SQL语义,而是"在第X号页的偏移量Y处,把值从A改成B"这样的页级修改。这意味着它是不感知表结构的,不管你的表是什么Schema,它只关心页的字节变化。

在InnoDB里,redo log不是无穷尽的,而是采用环形写入的方式。ib_logfile0、ib_logfile1等文件组成了一个环,每个文件默认大小通常为48MB或由innodb_log_file_size参数决定。写入位置有一个write_pos,可覆盖位置有一个checkpoint,两者之间就是"当前还可以写入的空间"。一旦写满,InnoDB就必须强制把checkpoint往前推,也就是强制把对应的脏页刷到磁盘,来腾出空间给新的redo记录。

这里有一个很多人容易误解的点:redo log写满时数据库会发生"卡顿"。因为刷脏页是同步等待的,如果业务写入量很大、突然之间刷盘速度跟不上,就会出现"checkpoint推进太慢导致redo写满"的情况,这时候整个数据库的性能会被瞬间拖垮。所以在生产环境里,innodb_log_file_size不能设得太小,否则高峰期很容易出现这种"周期性抖一下"的诡异瓶颈。

2.3 redo log的刷盘时机

redo log先写进缓冲区(redo log buffer),然后由以下几种时机刷入磁盘:

  • commit时,如果innodb_flush_log_at_trx_commit=1,则每次事务提交都强制刷盘(最安全,也最慢)。
  • 如果设置为0,则表示提交时不主动刷盘,完全依赖后台线程每1秒刷一次(性能最高,但可能丢1秒数据)。
  • 如果设置为2,则表示提交时写入操作系统的page cache,由操作系统决定何时真正落盘(性能折中,服务器掉电时也可能丢数据)。

我给线上库的常规建议是保持默认的1,除非你能接受"最多丢最近一次提交的数据"这个代价。这里必须说一句大实话:很多运维为了压榨性能,把刷盘策略改成0或2,平时看起来Gap很小,但真到了断电复盘的时候,数据丢失的风险是实打实的。这不是单纯的技术选型,而是业务可用性决策。

2.4 崩溃恢复是怎么工作的

当MySQL进程崩溃之后,重启时会做两件事:回放redo log,再对未提交的事务做undo回滚。具体来说,InnoDB会扫描redo log,把那些"已经写入redo但对应的数据页还没落盘"的修改,重新应用到数据页上,这就是所谓的Crash Recovery。这个过程听起来复杂,但因为redo log连续且有序,恢复速度通常不会太慢。

恢复的详细顺序会在第5节结合binlog一起讲,因为这里还藏着一个两阶段提交的坑。

3. undo log:事务回滚与MVCC的幕后功臣

如果说redo log是为了"往前冲",那undo log就是为了"往后退"。但如果你以为undo log只是用来回滚事务,那说明还没看到它更重要的一面——它还是MVCC(多版本并发控制)能工作的基石。

3.1 undo log是逻辑日志

redo log是物理日志,undo log则必须反过来,它是逻辑日志。为什么?因为回滚的时候,你不能简单地把某个页上的字节改回原来的样子,那样可能会影响其他并发事务的修改。undo log记录的是一个反操作:

  • 如果是INSERT,就记下主键信息,回滚时删除这条记录。
  • 如果是DELETE,就记下被删除之前的那行完整数据,回滚时恢复。
  • 如果是UPDATE,就记下修改前那行的旧值,回滚时把旧值换回来。

正是因为这个"逻辑反操作"的设计,undo log可以安全地在大量并发事务之间使用,而不会互相覆盖。

3.2 回滚段与undo表空间

在InnoDB里,undo log存储在回滚段(rollback segment)中,每个回滚段又由多个undo slot组成。历史上,undo log默认存放在系统表空间ibdata1中,这也是DBA最头疼的问题之一——一旦系统表空间膨胀,想收缩非常麻烦。好在从MySQL 5.6开始,我们可以通过配置把undo log独立到单独的表空间文件里,从MySQL 8.0开始,undo log更是参数化地管理,独立表空间成为默认行为。

比较经典的问题是你在大事务里跑了很久,然后回滚了,结果undo文件还是那么大。这是很正常的,因为undo log文件里虽然有大量"可复用"的空间,但物理文件大小不会自动收缩。我在实际运维时遇到过几次这种情况,处理方式是用一个新的undo表空间替换旧的,或者接收磁盘多占用一些这样的事实,而不是强行删文件——强行删除undo文件会让数据库直接崩溃,这个坑千万别踩。

3.3 undo log与MVCC的版本链

MVCC的实现,本质上是靠undo log构成的历史版本链。每个数据行上会有一个隐藏的DB_TRX_ID列,记录最近一次修改它的事务ID。当其他事务需要读取旧版本数据时,就沿着这个事务ID找到对应的undo log记录,一层一层往上回溯,直到拿到符合自己快照可见性的版本。

这个机制被广泛用于普通SELECT语句的隔离级别控制中。在可重复读隔离级别下,事务开启后持有的ReadView一旦确定,普通查询就只会看到这个ReadView已经提交的版本,即使数据后来被其他事务改了,也能通过undo log读旧版本,从而保证同一个事务里多次查询结果一致。

到这里你就能理解一个反直觉的现象:有时候你以为UPDATE之后回滚了,数据"恢复原样"了,但其实旧版本并没有完全消失,它可能还藏在undo log的版本链里,被一些长事务的ReadView引用着。这也是为什么大事务、长事务会让undo log不断膨胀的原因——版本链不能随便截断,因为可能还有人在读旧版本。

3.4 purge线程是怎么回收undo的

既然undo log不能无限膨胀,InnoDB就安排了专门的purge线程来清理那些"任何事务都看不见的旧版本"。它依赖一个叫做history list的数据结构,里面记录了可以被清理的undo记录。事务提交后,只要没有其他事务需要访问这些旧版本,purge线程就会把它们删掉,同时回收对应的回滚段空间。

但这里有一个很容易被忽略的生产风险:如果出现一个超长时间运行的事务,它可能一直持有一个早期的ReadView,导致大量undo log无法被purge,数据库整体空间膨胀,查询性能下降。我遇到过的最极端案例是,一个开发人员在测试环境开着事务不提交,结果把线上共享实例的undo表空间撑到上百G。这种问题的排查思路很简单:

-- 查看是否有长时间未提交的事务 SELECT trx_id, trx_state, trx_started, trx_mysql_thread_id FROM information_schema.innodb_trx ORDER BY trx_started;

一旦发现运行时间异常长的事务,先找到对应的会话,确认是否可以通过KILL解决。别指望自动化工具能替你分析业务要不要保留事务,得先搞清楚这个事务为什么还没结束。

4. bin log:全局逻辑日志,复制与数据追踪的基石

前面两位都是InnoDB存储引擎层面的日志,binlog则不一样,它是MySQL Server层产生的逻辑日志。这一层级的差别非常关键,因为它意味着bin log不关心你的表到底用的是InnoDB还是MyISAM,它只记录"逻辑上的数据变更结果"。

4.1 binlog的三种格式

binlog有三种格式:STATEMENT、ROW和MIXED。这三种格式的选择,几乎决定了你后续排障、恢复数据、做数据同步时的难易程度。

格式记录内容优点缺点
STATEMENT原始SQL语句日志量小,可读性强某些非确定性函数、存储过程可能导致主从数据不一致
ROW每一行变更的前后镜像最安全,主从一致性有保障日志量大,尤其大批量UPDATE时
MIXED由MySQL自动判断兼顾两者需要理解自动判断逻辑,某些边缘情况仍可能出现意外

我个人在生产环境里基本都是强制使用ROW格式。虽然它的日志体积更大,但从库数据一致性、数据回放的安全性、以及后续做数据闪回(比如用binlog2sql)来说都省心太多。曾经有个同事用STATEMENT格式跑一条带UUID()的UPDATE,结果主库和从库生成的UUID完全不同,整个复制链路的校验全炸了。这种问题在ROW格式下根本不会出现。

4.2 binlog是怎么保证"恰好一次"的语义的

相比redo log的循环写,binlog是追加写,文件写完一个就换下一个,文件名一般形如mysql-bin.000001。它同样不感知数据页的物理状态,只记录事务提交后产生的逻辑结果。这里有一个核心概念叫"binlog cache",每个事务在提交前,先写到线程私有的binlog cache里,事务提交时才统一写入文件。

MySQL提供了一个大参数sync_binlog,决定提交时binlog是否强制刷盘。默认值是1,即每次事务提交都刷盘。它和innodb_flush_log_at_trx_commit=1配合,可以确保一个事务在提交时,redo log和binlog都真正落盘,从而把"提交成功但日志丢失"的概率降到最低。

4.3 binlog在数据恢复和主从复制中的角色

主从复制的过程其实就是"从库拉取主库的binlog,然后在本地重放"的过程。从库通过IO线程把主库binlog拉到本地relay log,再由SQL线程去执行。这个过程看似简单,但有一个极其重要的细节:binlog里并没有记录"这是第几个事务",而是通过坐标(文件名+偏移量)来定位同步位点。所以从库中断之后,你经常需要看Relay_Log_File和Exec_Master_Log_Pos这种字段。

数据恢复场景中,binlog的价值就更大的。比如凌晨不小心删了一张表,如果备份是前一天凌晨的,那么从备份恢复后,还需要把备份点之后的所有binlog重新回放,才能把数据追回到"误删前的一瞬间"。这里经常涉及一个工具叫mysqlbinlog,用法大概是:

mysqlbinlog --stop-datetime="2024-06-01 10:00:00" mysql-bin.000008 | mysql -uroot -p

不过我要提醒一句:如果你要精确跳过某条误操作的SQL,直接用时间点回放是不可靠的。更稳妥的做法是用ROW格式配合binlog2sql先把目标SQL解析出来,确认之后再决定是闪回还是跳过。

5. 三类日志的分工与协作:两阶段提交的完整流程

讲到这里,可能有人会问:既然redo log负责崩溃恢复,binlog负责复制,它们各管各的不就行了?为什么需要配合?答案在于"提交事务"这个瞬间,如果redo log和binlog其中一个写成功了,另一个还没写,崩溃恢复时就会出现主从数据不一致或者主库自己都恢复不了的情况。

5.1 事务提交时的两阶段提交

InnoDB和Server层之间采用的是两阶段提交协议。简单描述一下流程:

  1. 事务在InnoDB内修改数据,生成redo log,并把状态标记为PREPARE。
  2. 事务最终提交时,先写binlog(此时事务的binlog已落盘)。
  3. 再告诉InnoDB把redo log状态从PREPARE更新为COMMIT。

为什么要这么做?因为崩溃可能发生在任意一步之间。如果写完prepare的redo后还没写binlog就崩溃,重启后会回滚这个事务;如果先写了binlog但还没来得及把redo标记为commit,那么重启时会去扫描binlog,如果发现这个事务的binlog已经存在,就继续把它提交,否则回滚。这样就能保证redo log和binlog的一致性。

5.2 一组日志的生命周期案例

我们以一个简单事务为例:

BEGIN; UPDATE account SET balance = balance - 100 WHERE id = 100; COMMIT;

此时各个日志的状态大致如下:

  • InnoDB收到更新请求,在Buffer Pool中修改account的id=100所在数据页。
  • 在修改数据页前,先将这次页修改写入redo log buffer。
  • 在undo log中写入一条旧值记录,id=100原来的balance是500。
  • 执行COMMIT时,先把redo log刷入磁盘,状态为PREPARE。
  • 生成一条binlog,写入binlog cache并刷盘。
  • InnoDB收到commit确认,把redo log状态更新为COMMIT,整个事务正式结束。

如果在这个流程中的第4步和第5步之间崩溃,重启时发现redo有prepare记录但binlog没有对应事务,这个事务就会被回滚。如果在第5步之后、第6步之前崩溃,重启时发现binlog里有这个事务,但redo还停在prepare,InnoDB就会"相信"binlog里的记录,继续把这个事务提交。这就是两阶段提交最核心的价值。

5.3 生产环境中如何通过日志定位问题

排查问题时,我最常用的几个命令:

-- 查看当前redo log配置 SHOW VARIABLES LIKE 'innodb_log_file_size'; SHOW VARIABLES LIKE 'innodb_flush_log_at_trx_commit'; -- 查看binlog相关信息 SHOW BINARY LOG STATUS; SHOW MASTER STATUS;

定位主从延迟或复制中断时,一般先看从库的复制状态:

SHOW SLAVE STATUS\G

重点观察Slave_IO_Running、Slave_SQL_Running、Last_SQL_Errno和Last_SQL_Error。如果是binlog解析错误,往往需要结合mysqlbinlog工具去看具体在哪一个event上出错。多数情况下,从库报错是数据不一致(比如插入主键冲突、更新不存在的行),这时候优先考虑基于GTID的复制模式下如何跳过错误或重放事务。

6. 参数调优与面试高频问题实战

文章最后这部分,我想把三类日志从"理论理解"落到"实际能抄走"的层面。包括配置参数的推荐值、日常监控指标、以及面试里高频出现的几道题。

6.1 关键参数如何设置

如果你负责的库允许我建议一套相对稳妥的参数组合,我会这样设置:

参数推荐值说明
innodb_flush_log_at_trx_commit1每次提交刷redo,保证不丢已提交事务
sync_binlog1每次提交刷binlog,保证主从一致
innodb_log_file_size1GB~4GB(视业务写入量)避免日志轮转太快导致频繁checkpoint
binlog_formatROW安全第一,数据恢复更容易
binlog_row_imageFULL记录完整前后镜像,方便排查和恢复
innodb_undo_tablespaces3(MySQL 5.7)分散undo表空间,均衡I/O

这套组合是"安全优先"的配置,尤其适合金融、支付、订单这类不能丢数据的业务。如果你做的是日志系统、评论系统这类允许少量丢失的新鲜数据场景,可以在刷盘策略上适当放宽,比如innodb_flush_log_at_trx_commit=0,但你要很清楚哪怕只丢一秒,也是实打实的数据丢失。

6.2 日常巡检要看哪些指标

在日常运维中,我建议至少盯住这几个点:

  • redo log的写入量和写入速率,突然的飙升往往意味着有大事务或者产生大量更新。
  • MySQL状态里的Innodb_os_log_pending_writes,如果长期非零甚至持续增长,说明日志写入存在瓶颈。
  • Binlog文件的大小和数量,注意磁盘空间是否被binlog撑满。生产环境中我曾经就因为binlog保留时间过长直接把磁盘写爆过,所以建议设置expire_logs_days或binlog_expire_logs_seconds,同时做好定时备份归档。
  • history list length,这个值过大意味着undo purge进度滞后,可能是有长事务。可以用以下命令粗看:
SHOW ENGINE INNODB STATUS;

在TRANSACTIONS段找到History list length,如果数值百万级以上,基本可以断定有大型未提交事务拖住了purge线程。

6.3 高频面试题如何答到点子上

讲理论容易,回答面试题时容易"管中窥豹"。这里我列几道高频题和我给出的逻辑:

Q1:一个UPDATE语句在崩溃后是怎么恢复的?

答:重启后InnoDB先扫描redo log,把已提交但未落盘的数据页修改重放一遍;然后扫描undo log,把未提交的事务回滚,最终数据处于一个一致状态。结合binlog的话,还要考虑两阶段提交,如果binlog里已经有这个事务,那么即使redo里只到prepare,也要提交这个事务。

Q2:redo log和binlog都能做崩溃恢复,区别是什么?

redo log是InnoDB层的物理日志,解决实例崩溃后数据页与Buffer Pool不一致的问题;binlog是Server层的逻辑日志,用于主从复制、按时间点恢复等场景。两者的核心区别在于描述层面不同(物理页变更 vs 逻辑操作结果),以及它们记录的时机不同(redo边改边记,binlog提交时才写)。

Q3:为什么不能只用binlog做崩溃恢复?

因为binlog并不包含所有数据页变更的完整物理细节,而且InnoDB崩溃恢复是在存储引擎层面、面向数据页的,binlog逻辑上"重放"的话效率太低,也无法保证数据页和索引结构处于完全一致的状态。

Q4:undo log在MVCC里具体是怎么发挥作用的?

每次修改都会在undo log里留下旧版本记录,形成一个版本链。普通SELECT在可重复读隔离级别下,通过ReadView判断哪些版本可见,不可见的版本就顺着版本链往前找,直到找到符合可见性规则的版本。所以undo log不只是"反悔撤销",它还承担了并发场景下的"历史快照"职责。

6.4 我踩过的几个真实坑

最后分享三个非常具体的坑,算是拿真金白银换来的教训。

第一个是误删了undo表空间文件导致的启动失败。有一次我在一台测试机上发现磁盘空间快满了,看到ibdata1文件很大,手一贱就想删掉它来回收空间。结果MySQL起不来,错误日志里直接提示找不到undo文件。后来才明白,undo文件里还保留着很多活跃事务的历史版本,是不能被随意删除的。正确做法是确保没有长事务后,通过ALTER INSTANCE或迁移undo表空间来整理空间,而不是直接rm。

第二个是binlog和redo log的刷盘顺序造成的主从数据延迟。当时我在从库上执行了FLUSH LOGS之后,主库又连续写入大量binlog,导致从库IO线程一直追不上。排查发现不是网络问题,而是主库的sync_binlog设置为0,binlog写入大部分只停留在操作系统缓存里,真正落盘延迟严重,从库自然拉不到最新数据。最后改成sync_binlog=1,情况立刻好转。

第三个是误以为undo log只在回滚时才写。不少新人以为UPDATE执行后再ROLLBACK才会产生undo log,其实不是。只要事务执行了修改,undo log就会立即写入,不管后面是提交还是回滚。如果事务特别大,修改了上千万行,哪怕最终提交,undo日志的写入量也非常可观。所以设计批量操作时,要尽量控制每个事务的规模,别让一条UPDATE扫全表。

实际工作中,把三种日志的运行机制吃透,真的能让你在故障场景里少熬夜。一次崩溃可能只有几分钟,但如果不懂redo的刷盘时机、binlog的格式选择、undo的purge机制,你很可能要花几个小时甚至几天才能恢复线上环境。希望这篇文章能帮你把这三个基础构件彻底焊死在脑子里,下次再碰到数据库故障,至少心里有底,知道该从哪里下手。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 6:38:17

用PLC和组态王给洗衣机换脑:顺序控制系统实战

很多人可能觉得洗衣机就是个日用家电,顶多拆开来换换电机电容,跟PLC八竿子打不着。但如果你把一台普通波轮洗衣机当成一套典型的顺序控制系统来看,它其实包含了电机正反转、水位检测、定时控制、状态切换这些在工业现场天天遇到的基本逻辑。我…

作者头像 李华
网站建设 2026/9/9 6:36:40

VISA仪器控制例程详解:从环境搭建到实战排坑

简介:VISA控制仪器的例程是一份针对测试测量与自动化领域的VISA编程实例包,面向需要控制USB、LAN、GPIB、COM接口仪器的开发者。包内共15个文件,压缩后195KB,包含C源代码文件、Visual C工程文件(dsp/dsw)、…

作者头像 李华
网站建设 2026/9/9 6:35:56

大模型API报错排查指南:401/403/404/429/500状态码一次讲清

深夜两点,告警群里突然刷出一排报错日志,清一色全是401。我第一反应是API Key过期了,翻了一圈配置却发现Key根本没换,最后查出来是服务重启后环境变量没加载进来。这种场面,接过大模型API的开发者十有八九都经历过。大…

作者头像 李华
网站建设 2026/9/9 6:35:47

水洼个数:DFS、BFS与并查集三种解法详解

水洼个数,一道练DFS/BFS/并查集的好题“3378:练65.1 水洼个数”,如果你是在信息学竞赛教材或者OJ题库上看到这个编号,那大概率是经典题 Lake Counting 的变体。题目本身不复杂,给一个 N 行 M 列的网格图,每…

作者头像 李华