资讯动态

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

发布时间:2026/9/9 6:38:23 来源:尧图企业网站定制
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层解析、优化、执行找到id1这一行数据然后在InnoDB层真正修改数据。如果数据页不在Buffer Pool里先从磁盘读进来在内存里改掉这个值。此时数据页已经脏了但还没写回磁盘。与此同时为了保证崩溃后能恢复事务能回滚主从能同步InnoDB和binlog会各自动手redo log记下我把id1的balance从500改成了400用于崩溃恢复时重放redo。undo log记下id1的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_commit1则每次事务提交都强制刷盘最安全也最慢。如果设置为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_commit1配合可以确保一个事务在提交时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-datetime2024-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层之间采用的是两阶段提交协议。简单描述一下流程事务在InnoDB内修改数据生成redo log并把状态标记为PREPARE。事务最终提交时先写binlog此时事务的binlog已落盘。再告诉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的id100所在数据页。在修改数据页前先将这次页修改写入redo log buffer。在undo log中写入一条旧值记录id100原来的balance是500。执行COMMIT时先把redo log刷入磁盘状态为PREPARE。生成一条binlog写入binlog cache并刷盘。InnoDB收到commit确认把redo log状态更新为COMMIT整个事务正式结束。如果在这个流程中的第4步和第5步之间崩溃重启时发现redo有prepare记录但binlog没有对应事务这个事务就会被回滚。如果在第5步之后、第6步之前崩溃重启时发现binlog里有这个事务但redo还停在prepareInnoDB就会相信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视业务写入量避免日志轮转太快导致频繁checkpointbinlog_formatROW安全第一数据恢复更容易binlog_row_imageFULL记录完整前后镜像方便排查和恢复innodb_undo_tablespaces3MySQL 5.7分散undo表空间均衡I/O这套组合是安全优先的配置尤其适合金融、支付、订单这类不能丢数据的业务。如果你做的是日志系统、评论系统这类允许少量丢失的新鲜数据场景可以在刷盘策略上适当放宽比如innodb_flush_log_at_trx_commit0但你要很清楚哪怕只丢一秒也是实打实的数据丢失。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也要提交这个事务。Q2redo log和binlog都能做崩溃恢复区别是什么redo log是InnoDB层的物理日志解决实例崩溃后数据页与Buffer Pool不一致的问题binlog是Server层的逻辑日志用于主从复制、按时间点恢复等场景。两者的核心区别在于描述层面不同物理页变更 vs 逻辑操作结果以及它们记录的时机不同redo边改边记binlog提交时才写。Q3为什么不能只用binlog做崩溃恢复因为binlog并不包含所有数据页变更的完整物理细节而且InnoDB崩溃恢复是在存储引擎层面、面向数据页的binlog逻辑上重放的话效率太低也无法保证数据页和索引结构处于完全一致的状态。Q4undo 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设置为0binlog写入大部分只停留在操作系统缓存里真正落盘延迟严重从库自然拉不到最新数据。最后改成sync_binlog1情况立刻好转。第三个是误以为undo log只在回滚时才写。不少新人以为UPDATE执行后再ROLLBACK才会产生undo log其实不是。只要事务执行了修改undo log就会立即写入不管后面是提交还是回滚。如果事务特别大修改了上千万行哪怕最终提交undo日志的写入量也非常可观。所以设计批量操作时要尽量控制每个事务的规模别让一条UPDATE扫全表。实际工作中把三种日志的运行机制吃透真的能让你在故障场景里少熬夜。一次崩溃可能只有几分钟但如果不懂redo的刷盘时机、binlog的格式选择、undo的purge机制你很可能要花几个小时甚至几天才能恢复线上环境。希望这篇文章能帮你把这三个基础构件彻底焊死在脑子里下次再碰到数据库故障至少心里有底知道该从哪里下手。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价