从事MySQL运维和架构设计的人,迟早要跟日志打交道。很多人被问到MySQL里有哪些日志时,第一反应是binlog,再想想可能还有error log。但真正决定事务能否“不丢、不重、不乱”的,是binlog、redo log、undo log这三兄弟。我之前排查过几次夜间数据异常,最后都回到日志链路本身:谁在什么时候写了什么日志,谁在崩溃后根据日志做了哪些操作。这篇文章就把这三种日志的分工、写日志顺序、事务配合、参数配置和常见坑一起梳理清楚,适合正在理解MySQL事务原理的开发者,也适合被线上日志问题折磨的运维同学参考。
1. 三种日志的分工:谁在记账,谁在备份,谁在后悔
1.1 Binlog:MySQL Server层的“流水账本”
Binlog是MySQL Server层生成的逻辑日志,记录的是“发生了什么变更”:某条语句把某行数据从旧值改成了新值,或者某个表结构被修改了。它跟具体存储引擎无关,MyISAM、InnoDB都可以用,所以承担了两个非常重要的任务:主从复制和基于时间点的数据恢复。
我在实际排查问题时,习惯把binlog理解成记账本。它的特点是可以完整回放,只要能从某个全量备份往后重放binlog,就能把数据库恢复到任意一个历史时刻。记账本记的是逻辑操作,所以记录的是SQL描述或行变更前后镜像,而不是磁盘页上具体字节的变化。
binlog有三种格式。statement格式记录原始SQL,日志量小,但遇到NOW()、UUID()这类不确定函数时,主从执行结果可能不一致;row格式记录每行变更前、变更后的镜像,最安全,但日志量明显变大;mixed格式由MySQL判断,可能混用,在某些场景下仍存在不确定性。除非有非常明确的理由,否则我建议线上用row格式,虽然日志会膨胀,但换来了可预测的复制结果和更可靠的数据恢复能力。确认binlog是否开启时,检查log_bin参数;实际文件通常在datadir下的binlog.*文件中,也可以单独指定目录存放。
1.2 Redo Log:InnoDB引擎的“损坏保险单”
Redo log是InnoDB存储引擎层的物理日志,记录的是“某个数据页在某个偏移量发生了什么修改”,比如页号1000、偏移量28处写入了某个值。它的作用不是给人看的,而是给崩溃恢复用的:提交成功的变更,即使数据页还没来得及刷到磁盘,只要redo log里留下了记录,实例重启后就可以重新应用修改,保证事务不丢。
这里必须理解WAL(Write-Ahead Logging)的核心思想。把随机写入数据页的操作,转换成顺序追加写日志文件,先把这次变更加在日志上,等一个合适的时机再去更新数据页。顺序写比随机写快得多,这是InnoDB选择WAL而不是每次直接刷数据页的原因。redo log是固定大小循环写的,可以理解成一块环形缓冲:写满后覆盖最旧的、已经保证落盘的部分,所以它不是一个无限保留的归档,而是“最近一段时间所有变更的收据”。
早期版本里,redo log是ib_logfile0、ib_logfile1等文件,现代MySQL 8.0.30之后改为#innodb_redo目录下动态管理文件,但本质没变。如果你发现实例参数中innodb_log_file_size和innodb_log_files_in_group仍然存在,那是旧版参数;新版本更推荐直接关注innodb_redo_log_capacity。
1.3 Undo Log:事务回滚和MVCC的“后悔药”
Undo log也是InnoDB引擎层的日志,但它记录的是“修改之前的数据版本”。事务执行中如果需要回滚,InnoDB就根据undo log里记录的旧值,把数据页恢复到事务开始前的样子。同时,多版本并发控制(MVCC)也依赖undo log:在一个事务执行过程中,另一个事务做普通读时,看不到尚未提交的新改动,只能通过undo log回溯到旧版本。
所以说undo log不是“日志坏了才用”,它本身是事务并发能力的地基。它跟redo log有一个非常明确的分工:redo log保证已提交事务不丢,undo log保证未提交事务或被回滚的事务能撤得干净。崩溃恢复时,InnoDB会利用redo log重放所有已提交的修改;对于崩溃前尚未提交的事务,则利用undo log把数据页回滚到之前的状态。正因如此,长事务会导致undo log不断累积,因为这些旧版本还需要被其他正在运行的事务读取,不能立刻清理。
2. 日志写入顺序:先写日志再写数据,还要玩两阶段提交
2.1 WAL原理:为什么顺序写日志比随机写数据页快
我经常用一个很生活化的比喻解释WAL。你去银行办存款,柜员不会立刻把账本所有页都重新写一遍,而是先在流水单上记一笔“某时刻存入多少钱”,然后等夜深人静再统一把总账更新。流水单写入是顺序追加,总账更新则可能涉及多个页的随机读写。如果每次操作都立刻把账本更新好,性能会差很多,而且万一写一半断电,账目就乱了。MySQL的redo log就是这个流水单,它记录的是即将发生或已经发生的所有物理变更。
在InnoDB中,数据变更先进入缓存(Buffer Pool)中的内存页,同时生成redo log并写入redo log buffer。事务提交时,是否需要把redo log buffer强制刷到磁盘,由参数innodb_flush_log_at_trx_commit决定。如果为1,每次事务提交都把redo log刷到磁盘,性能有损耗但最安全;如果为2,每次提交只写入操作系统缓存,由操作系统稍后落盘,性能好,但主机断电时会丢最近一小段时间的已提交事务;如果为0,则是每秒定时刷盘,性能最好,但MySQL进程崩溃也可能丢最多一秒的事务。线上追求不丢数据时,必须把redo log参数和sync_binlog=1一起配好。
2.2 两阶段提交流程:binlog和redo log如何保证一致
这是MySQL事务提交里最容易绕晕的部分,也是架构面试里最常考的点。为什么需要两阶段提交?因为binlog属于Server层,redo log属于InnoDB引擎层,它们各自维护自己的轨迹,如果不做协调,就会产生一个可怕的问题:某个事务在存储引擎里已经提交了,但binlog没写或者写了一半;主库重启后引擎数据是新的,但从库还在等binlog同步,最后两边数据不一致。
InnoDB使用两阶段提交(Two-Phase Commit)解决这个问题。流程大致这样:事务执行过程中,InnoDB不断写入redo log,先不提交。等到事务需要提交时,第一步,把事务标记为prepare状态并刷redo log;第二步,Server层把本次事务的binlog写入并刷盘;第三步,InnoDB把事务标记为commit状态,同时把binlog的文件名和写入位置记录到redo log中,然后返回成功。
为什么要把binlog的位置记进redo log?因为崩溃恢复时,InnoDB看到了prepare状态的事务,需要通过binlog里是否存在对应记录来判断是否需要提交。如果binlog里有这个事务且完整,说明Server层已经确认提交,InnoDB就应该把这个事务正式提交掉。如果binlog里没有,说明事务根本没走到提交这一步,应当回滚。这套规则保证了binlog和实际数据永远能对上,主从复制才可靠。
2.3 崩溃恢复的依据:prepare和commit怎么判断
实战中如果看到redo log里有一堆prepare状态但不清楚事务是否提交,可以这样理清判断逻辑:先看对应binlog,找到这个事务最后的位置,如果事件链完整,包括事务边界事件,就执行commit;如果binlog里没有匹配记录,就执行rollback。这就是为什么binlog格式为row时更容易解析,statement格式要判断某个事务边界相对复杂,但内部机制仍能保证。
为了减少磁盘I/O,MySQL会把多个事务的提交动作合并成一次fsync,这就是组提交(group commit)。你可以粗略理解为:一个班的学生排队交作业,老师把所有作业收齐后统一归档,比一个学生交一次立刻归档更高效。组提交让高并发下的提交性能提高不少,但原理上仍为每个事务维护两阶段日志状态。
3. 一条UPDATE语句的日志流水账
3.1 从begin到commit,日志都经历了什么
拿一个具体例子说话:UPDATE users SET age=30 WHERE id=100,假设这一行原来的age是20。事务开始后,InnoDB不会立刻改数据页,而是先做一系列日志动作:
- 在undo log中记录旧值,即“id=100这一行age原来是20”,并把这条undo记录加入该事务的回滚段。
- 在Buffer Pool中更新内存里的数据页,把age改为30,同时标记该页为脏页。
- 生成一条redo log,记录“页X上,id=100这行age字段从20写成30”,状态为prepare,并写入redo log buffer。
- 如果客户端执行commit,Server层先生成该事务的binlog事件,写入binlog缓存,最终刷盘。
- binlog刷盘成功后,InnoDB再把这个事务标记为commit,redo log也刷盘到位,事务才算真正提交。
这个顺序很关键:commit返回给客户端之前,binlog和redo log都已经是持久化状态,也就是“日志先落盘,数据页后落盘”。所以哪怕下一秒操作系统断电,重启后InnoDB也能从redo log里把这次修改恢复回来;而从库也能从binlog里拿到该事务同步执行。
3.2 回滚、脏页刷盘与checkpoint如何推进
写完日志后,数据页并不是马上落盘。脏页由后台线程按策略异步刷盘,刷盘时机受很多因素影响,比如脏页比例、空闲内存、系统IO能力等。为了保证redo log循环写时不会覆盖掉还没落盘的修改,InnoDB维护了checkpoint机制。checkpoint的位置表示“这个点之前的脏页已经刷盘,redo log里对应的修改已经可以覆盖”。redo log中write_pos往前写,checkpoint_pos往前追,两者之间就是当前还在使用的日志区域。
如果业务写入量很大,而后台刷脏页跟不上,write_pos就会逼近checkpoint,最终出现redo log没有空间可写的状态。此时InnoDB会强制加速刷脏页,整个系统更新操作就会明显变慢。理解了这点,再去看监控里的“redo log空间等待”告警,就不会只想着加日志文件,而是同时检查刷盘能力是不是瓶颈。
未提交事务如果发生回滚,InnoDB顺着undo log链往回找旧版本,然后把数据页上的值还原。注意回滚也有日志,同样会写redo log覆盖旧数据的状态,所以回滚本身依然需要WAL保护,避免回滚到一半又崩溃。
3.3 主从复制时binlog如何被二次消费
主库在每次事务提交后,binlog里会出现一条或多条事件记录。从库用一个专用线程连接到主库,从指定binlog文件和偏移量拉取日志,写入到自己的relay log。接着从库的SQL线程读取relay log中的事件,在本机执行。整个过程就是把逻辑日志在另一台实例上重放一遍。
主从复制最怕的是binlog事件到了从库,但执行结果不确定。这也是我坚持用row格式的原因。row格式记录的是变更前后的行镜像,从库直接应用镜像即可,不会因为SQL语句使用了函数、随机数或非确定性表达式而产生偏差。如果你看到主从延迟或数据不一致,优先检查binlog_format和binlog_row_image这些参数是否符合预期。
另外,binlog文件是有边界的,每个binlog文件写满后自动切换到下一个,max_binlog_size决定了切换阈值。从库拉取时如果主库把binlog提前清理了,就会报“got fatal error 1236”,这时候只能考虑重建从库或从备份补数据。
4. 三种日志的配置与运维实战
4.1 关键参数怎么设:从保证不丢到追求性能
配置日志参数前,先问自己:这个业务能不能接受事务提交成功后丢失已提交数据。如果答案是不能,那binlog侧必须保证sync_binlog=1,redo log侧必须保证innodb_flush_log_at_trx_commit=1。这两个参数同时为1,才能做到事务提交后,binlog和redo log都已经落盘。只开一个不行:比如redo刷盘但binlog没刷,从库复制可能延迟或丢失;binlog刷了但redo没刷,主库崩溃可能丢提交。
追求性能时可以牺牲一些安全性,比如把redo log的刷盘参数改成2,再配合sync_binlog的批量提交策略。但改之前要想清楚,如果机房断电,最近约1秒的已提交事务可能全部丢失,主从也可能从此不一致。很多团队在测试环境用0,生产环境用1,个人建议:除非有专门的硬件或异地容灾兜底,否则生产环境老老实实用1。
binlog还要关注保存时长。老版本参数是expire_logs_days,新版本推荐binlog_expire_logs_seconds,单位是秒。建议保留时间至少要覆盖一次完整备份周期加上容错时间,比如每天全备,就保留至少48小时;如果要做基于时间点恢复,最好保留到备份任务确认上传成功之后。
redo log容量方面,8.0.30以前的版本要同时看innodb_log_file_size和innodb_log_files_in_group,两者相乘是总日志容量。8.0.30以后直接用innodb_redo_log_capacity,默认100MB,很多写入密集的小型业务会发现默认值太小,建议先观察写入峰值,再设置成1GB左右,避免频繁触发脏页强制刷盘。
4.2 日志故障排查:损坏、写满、复制中断
日志相关的故障一般分几类,按现场常见程度列一下。
第一类是binlog空间写满或清理不及时,导致磁盘满。binlog增长极快时,先看是否有大事务或row格式下整表更新产生巨大日志,同时检查保留时长参数。如果磁盘已经满了,可以手工purge旧的binlog,但前提是确认已经没有从库依赖这些文件,否则主从复制会中断。
第二类是redo log写满,业务更新卡住。处理思路是:先找到阻塞源头,如果是某个大事务持有大量脏页,等它提交;如果写入量持续超过刷盘能力,则考虑增大redo log容量并检查磁盘IO能力。不要一看到日志写满就反复重启,重启会把问题带到恢复阶段,反而更慢。
第三类是复制中断。从库上执行SHOW REPLICA STATUS查看Last_IO_Error和Last_SQL_Error。IO线程报1236说明主库binlog已经被清理,或者从库偏移量不连续。SQL线程报1062说明主库执行过同一个事务而从库又应用了一次,常见于手动介入后从库位点被改乱。处理前先备份现场信息,再判断是跳过错误还是重建从库。一旦决定重建,用全量备份加binlog重新追平,是最稳妥的路线。
4.3 误删数据后用binlog做时间点恢复
误删数据是binlog最能体现价值的时候。前提是binlog开着、格式是row、备份链完整。恢复思路是:先找到事故时间点,把所有binlog从最近一次可靠全备的位置重放到误操作前的位置,导出成临时实例或直接导入到目标库。
常用命令大致是这样:
- 找到最近全量备份,将其恢复到临时实例或直接恢复数据文件。
- 用mysqlbinlog把备份点之后的binlog解析成SQL,比如:
mysqlbinlog --start-position=1534 --stop-position=2687 /data/binlog/binlog.000015 > recover.sql- 检查recover.sql内容确认没有误删语句,再执行导入。
这里有个关键坑:如果误操作之后,后续还有很多正常写入,直接把“最近全量备份+后续binlog”恢复到原库,会使备份时间点之后的正常业务写入全部被覆盖,造成更大损失。更稳妥的做法是用临时实例先恢复出误操作前的数据,把需要恢复的表导出,再倒回线上库。恢复操作一定要在变更审批和允许停服的窗口内进行。
5. 常见问题与踩坑实录
5.1 binlog能不能替代redo log
不能。虽然两者都记录变更,但角度完全不同。binlog是Server层的逻辑日志,记录了完整的SQL或行镜像,可以保留很久、用于复制和恢复。redo log是引擎层的物理日志,循环写、容量有限、主要用于快速崩溃恢复。如果只有binlog,实例崩溃时,磁盘上数据页可能停留在很早的状态,InnoDB没办法只靠逻辑重放把所有数据页恢复到内存和磁盘一致的最近状态,因为崩溃恢复需要按页物理修补。反过来,如果只有redo log,数据库就没法做时间点恢复,也没法向从库提供持续的逻辑变更流。所以它们不是互相替代的关系,而是缺一不可的配合关系。
补充一点:事务提交时,binlog和redo log都需要落盘,但数据页不需要。因此,你可以在系统正常运行时看到binlog文件的生成时间和实际数据文件更新时间明显不同。这个不同正是WAL设计的结果。
5.2 redo log写满时更新卡住怎么办
前面提到redo log是环形空间,如果write_pos追到checkpoint,InnoDB必须强制刷脏页以推进checkpoint,才能继续写日志。此时几乎所有更新都会阻塞,表现为监控里磁盘IO等待上升、更新线程堆积。处理流程是:先查是否有超长事务或一次性变更大量数据的大事务,这些事务占用大量redo log空间且迟迟不提交;再查脏页比例是否过高,刷盘线程是否被其他IO操作拖住;最后才是考虑增大日志容量。
我曾经遇到一个案例,某个业务每天凌晨批量跑一个回写任务,update语句一下子命中几千万行,事务迟迟不提交,redo log被基本填满,其他小事务也一起堵住。后来优化方式是把大事务拆成多个小事务分批提交,降低单个事务产生的redo log和undo log量,同时在凌晨把刷盘策略调得激进一些,问题才缓解。比起单纯把redo log调大,控制事务大小往往更有效。
5.3 undo log暴涨背后的长事务
undo log无限增长,最终会造成undo表空间膨胀,也会导致历史版本链过长,查询变慢。最常见的元凶就是一个没有提交的长事务。开启事务后,一直不提交也不回滚,它看到的“快照”就像给历史版本链加了一把锁,purge线程无法清理那些旧版本。
排查方法很简单:
SELECT trx_id, trx_started, trx_mysql_thread_id, trx_rows_locked, trx_rows_modified FROM information_schema.innodb_trx WHERE trx_state = 'RUNNING';如果trx_started时间非常早,那基本就是它了。再用trx_mysql_thread_id去processlist里找到对应会话,确认后kill掉。长事务结束后,undo log清理也需要时间,不需要频繁重启。如果是MySQL 8.0,undo表空间自动truncate机制会逐步回收;旧版本可能需要手动收缩,但没有安全的在线收缩方案,风险较大,不推荐随意操作。
我排查时还会顺手看performance_schema里的statement事件,确认这个长事务是不是在反复执行同一条SQL:比如在同一个条件下循环update,每次只改少量行但事务一直不提交。这种场景比单纯“select放着一小时”更容易踩中undo暴涨。
最后分享一个个人习惯。每次遇到和日志相关的线上问题,我不会急着改参数,而是在白板上把三条链路画出来:redo log从记录物理变更到崩溃恢复,binlog从事务提交到主从重放,undo log从旧版本到回滚和MVCC。把“谁在什么时机写、谁在异常时读取、谁负责清理”标清楚,再对照SHOW ENGINE INNODB STATUS里的日志序号、从库复制位点、事务列表,绝大多数问题都能快速定位。这个思路也适用于刚入门的人,先理解日志的关系,再去记那些参数细节,会轻松很多。