理解redo log和undo log的协作方式,是分析MySQL事务行为、定位数据不一致以及设计可靠备份方案的基础。很多对InnoDB只停留在概念层面的同学,在遇到crash recovery失效或者MVCC读到旧数据的问题时,往往不知道从哪里下手。本文会从两个日志的物理存储结构开始,逐步拆解它们在一个事务生命周期里的交互顺序,并给出实际可操作的观察方法。

redo log与undo log在存储层上的根本差异
redo log是一组固定大小的文件,默认在数据目录下以ib_logfile0、ib_logfile1命名,总大小由innodb_log_file_size和innodb_log_files_in_group共同决定。它采用环形覆盖写的方式,每个日志块包含一个全局递增的日志序列号LSN。写入redo log的内容不是SQL语句,而是对数据页中某个偏移量的物理修改描述,例如“在第10号表空间的第88号页偏移120处写入4个字节0x00000005”。这种设计保证了redo记录足够简单,能在崩溃后快速重放,但缺点是无法直接看出这次修改是事务A还是事务B发起的,也无法知道修改前该位置的值是什么。
undo log则存放在系统表空间或独立的undo表空间中,它是一条条以undo segment为组织的链表结构。每个insert、update、delete操作在修改数据页之前,都会先把对应的回滚信息写入undo log。undo log记录的是逻辑变化,比如“把这行主键为100的数据的name列从张三改回李四”,而不是物理字节级的patch。正因为undo log保留了旧值,事务回滚和MVCC的读视图才能获得一致的历史版本。undo log的生命周期比redo log长得多,只有确认没有任何活跃事务或快照需要引用该undo记录时,才能被purge线程回收。
一个常见的误解是认为redo log里也包含undo log的副本,所以undo log丢失了也能靠redo恢复。实际上,redo log确实会记录undo page的修改,但这只是为了保证undo page本身在崩溃后不损坏,并不能替代undo log的逻辑含义。比如崩溃前事务已经写了一条undo记录到undo page,同时生成了该undo page变更的redo记录。如果undo page的落盘没有完成,redo可以重放让undo page恢复成写过的状态,但undo记录里保存的旧值信息是逻辑内容,redo无法凭空创造出来。
事务提交时redo与undo的写入顺序和刷盘策略
以一个最简单的UPDATE t SET name='王五' WHERE id=1为例,假设磁盘上该行原来的name值是李四。事务开始后,InnoDB首先在id=1所在数据页上找到这条记录,把它的旧值(李四)以及事务ID写入undo log,并同步生成对应的redo log来描述undo page的写入。此时undo log和它的redo都还停留在内存的buffer pool和redo log buffer中,并没有强制落盘。
接着,InnoDB修改数据页,把name改成王五,同时生成该数据页修改的redo log追加到redo log buffer。注意,数据页的修改虽然发生在内存中,但对应的脏页不会立刻刷盘;redo log buffer中的内容则会根据innodb_flush_log_at_trx_commit参数的设置,在事务提交时决定是否同步到磁盘上的redo log文件。当该参数为1时,每次提交都调用fsync刷盘,崩溃后最多丢失最后一个未提交事务的修改;为0时每秒刷一次,性能最高但可能丢失1秒内的已提交事务;为2时写入操作系统缓存但不强制fsync,MySQL进程崩溃数据不丢,但服务器断电可能丢失。
很多资料强调“先写redo log再写数据页”,容易让人误以为redo log必须在修改数据页之前落盘。实际上,修改数据页这个动作发生在内存中,而redo log buffer也是内存中的结构,两者几乎同时发生。真正有顺序约束的是:脏页刷盘必须在对应的redo log落盘之后。因为如果脏页已经写到磁盘,而它的redo log还停留在内存中丢失了,那么这个数据页的修改就失去了崩溃保护。InnoDB通过内部的checkpoint机制和LSN比较来保证这一点,而不是简单地按照“先redo后page”的顺序执行。
undo log的刷盘时机相对宽松,只要在对应的数据页修改之前,undo log能在崩溃后通过redo恢复出来即可。因此InnoDB并不会在每次修改前把undo log强制刷盘,而是让undo page作为普通数据页参与buffer pool的管理。当事务提交时,undo log会标记为可清理,但真正的物理删除要等purge线程确认没有其他事务需要读取该历史版本。
利用引擎状态和日志工具剖析事务执行痕迹
想要直观看到redo log和undo log的协作情况,可以使用SHOW ENGINE INNODB STATUS命令。输出中的LOG部分会展示当前redo log的LSN范围、最后检查点位置、以及已经刷盘的LSN。例如:
SHOW ENGINE INNODB STATUS\G
在输出里找到LOG段落,可以看到类似下面的内容:
--- LOG --- Log sequence number 356789012 Log buffer assigned up to 356789012 Log buffer completed up to 356789012 Log written up to 356789012 Log flushed up to 356789012 Added dirty pages up to 356789012 Pages flushed up to 356782000 Last checkpoint at 356780000 ...
这里的Log sequence number是当前redo log的最大LSN,Last checkpoint表示已经完成刷盘的数据页对应的LSN。如果Log flushed up to和Log sequence number之间差距持续很大,说明redo log刷盘跟不上写入速度,可能需要调大redo log buffer或优化磁盘性能。对于undo log,SHOW ENGINE INNODB STATUS中的TRANSACTIONS段落会列出活跃事务持有的undo记录数量、回滚段使用情况,帮助判断是否存在长事务导致undo空间膨胀的问题。
如果需要分析已提交事务的完整链路,包括它修改了哪些行以及修改前的值,可以结合binlog和undo log的解析工具。binlog是MySQL Server层的逻辑日志,记录了SQL语句或行变更事件,通过mysqlbinlog工具可以直接读取。而undo log的解析相对复杂,通常需要借助第三方工具如undrop-for-innodb或者使用InnoDB的崩溃恢复源码进行定制分析。一个简化版的做法是把binlog中的Row事件当作事务的最终状态,再结合innodb引擎状态中的undo信息推断回滚情况。
下面模拟一个事务并观察它的binlog输出。先开启binlog并执行一个更新:
SET SESSION binlog_format = 'ROW'; START TRANSACTION; UPDATE employees SET salary = salary * 1.1 WHERE department = 'engineering'; COMMIT;
然后使用mysqlbinlog解析该binlog文件,观察Row_query和Table_map以及Update_rows事件。虽然binlog不直接展示undo log内容,但Update_rows事件里的before image和after image分别对应了修改前后的行数据,这和undo log中保存的逻辑旧值在语义上是一致的,可以作为交叉验证的依据。
mysqlbinlog --verbose --base64-output=DECODE-ROWS mysql-bin.000042
分析结果时,要注意binlog的before image来自查询执行时的行快照,而undo log中的旧值可能因为多个事务交错修改而出现中间状态。因此不能简单地把binlog的before image当作undo log的唯一来源,还需要结合事务隔离级别和锁信息综合判断。实践中更稳妥的方式是在测试环境开启innodb_undo_log_truncate和innodb_undo_tablespaces,配合performance_schema中的事务历史表来追踪每次修改对应的undo消耗量。
redo log和undo log的协作不是靠某一条显式的绑定关系,而是通过事务ID和LSN在两个日志体系间建立关联。每个undo记录头部存储了生成该记录时的LSN,而数据页中的每行记录又含有最近修改它的事务ID和指向undo记录的roll pointer。当崩溃恢复时,InnoDB从redo log重放所有已提交但未刷盘的数据页修改,然后扫描undo log,把未提交事务的修改回滚。这个顺序决定了redo log必须先于undo log完成重放,否则回滚过程会找不到对应的数据页状态。