MySQL的事务日志并不是一个单一日志文件,而是InnoDB存储引擎为了满足事务ACID特性设计的一套日志体系。核心包含两部分:redo log(重做日志)和undo log(回滚日志)。redo log偏向物理层面,记录的是数据页上发生的具体修改,比如某个表空间某个页偏移量处的值从A变成了B;undo log则偏向逻辑层面,记录的是数据被修改之前的状态,用来在执行ROLLBACK时把数据还原回去。除此之外还有binlog,但binlog属于MySQL Server层,主要用于主从复制和基于时间点的数据恢复,其定位与InnoDB内部的事务日志有所不同。本文讨论的事务日志,主要指redo log与undo log这两类与事务本身强相关的日志。

事务日志的核心组成:redo log与undo log
redo log存在的根本原因,是为了解决磁盘随机写入效率低与数据持久性要求之间的矛盾。InnoDB的数据存储在磁盘上的表空间中,如果每次事务提交都要求把修改过的数据页实时刷盘,会产生大量随机I/O,性能会急剧下降。于是InnoDB采用了WAL(Write-Ahead Logging,日志先行)策略:事务执行时先把修改意图记录到redo log中,并保证redo log落盘成功之后才返回客户端提交成功。真正的数据页可以延迟刷盘,即使系统突然崩溃,只要redo log还在,就能在重启时根据日志重做这些修改。
undo log的作用则完全不同。它保存的是数据被修改之前的历史版本,比如一条UPDATE语句执行前,会把该行的旧值写入undo log。这样当事务执行ROLLBACK时,InnoDB可以沿着undo log逐步恢复数据。同时,undo log也是实现MVCC(多版本并发控制)的基础。普通SELECT在没有显式加锁的情况下,可以通过undo log中的旧版本构建出事务启动时的一致性快照,从而避免读写互相阻塞。
可以用一个简单的SQL事务来观察两者的配合:
START TRANSACTION; UPDATE accounts SET balance = balance - 100 WHERE id = 1; -- 执行UPDATE时: -- 1. undo log记录该行balance的旧值 -- 2. redo log记录对数据页的物理修改 COMMIT;
这段事务中,undo log负责保证万一事务回滚时余额能恢复,redo log负责保证事务提交后即使宕机余额修改也不丢失。两者记录方向相反,却在一个事务里协同工作。理解这点,是理解MySQL事务日志的第一步。
redo log的刷盘策略与性能权衡
redo log并不是写完就立刻落到磁盘。InnoDB为redo log设计了内存缓冲区,称为redo log buffer,默认大小为8MB。事务执行过程中产生的redo日志先写入这个缓冲区,再由后台线程或提交动作触发刷盘。刷盘行为由参数innodb_flush_log_at_trx_commit控制,这个参数有三个取值,直接决定了持久性与性能之间的平衡。
参数值为1时,事务提交必须把redo log buffer中的日志刷写到磁盘日志文件,并且调用fsync确保真正落盘。这是默认值,也是最安全的选择,保证已提交事务不丢失。参数值为0时,提交动作不触发刷盘,而是每秒由后台线程执行一次刷盘,如果发生崩溃,最近一秒内提交的事务可能丢失。参数值为2时,提交动作会把日志写入操作系统缓存,但不强制fsync,同样每秒fsync一次,此时MySQL进程崩溃通常不会丢日志,但操作系统崩溃或断电可能丢失最近一秒的日志。
SHOW VARIABLES LIKE 'innodb_flush_log_at_trx_commit'; SET GLOBAL innodb_flush_log_at_trx_commit = 2;
不同刷盘策略适用于不同业务场景。金融交易、订单支付这类对数据零丢失要求苛刻的系统,必须保持参数为1。而日志采集、行为埋点等允许少量丢失的场景,可以调整为2或0以换取更高的写入吞吐。需要注意的是,即使参数为0或2,redo log文件本身仍然是循环写入的,文件写满后会触发checkpoint,强制将相关脏页刷盘并覆盖旧日志。
除了提交参数,redo log的容量配置也会影响整体性能。innodb_log_file_size和innodb_log_files_in_group共同决定redo log组的总大小。如果容量太小,checkpoint会频繁发生,导致大量脏页提前刷盘,写入性能出现波动;如果容量太大,崩溃恢复时需要扫描的日志变多,恢复时间会延长。通常建议把总大小设置为足够容纳一小时左右的高峰写入量。
undo log与MVCC之间的深层关系
undo log并不像redo log那样拥有固定大小的循环文件,它存储在系统表空间中,也可以配置独立的undo表空间。每行数据被修改时,都会产生一条undo记录,这些记录通过回滚指针串联成版本链。例如一行数据被三个事务先后更新,就会产生三个历史版本,最新版本在数据页中,而旧版本全部通过undo log连接起来。当某个事务需要读取一致性快照时,InnoDB会根据ReadView判断版本链中的哪个版本对当前事务可见。
这种机制带来一个常见的性能问题:长事务导致undo log膨胀。假设一个事务开启后长时间不提交,即使它没有修改任何数据,InnoDB为了保证这个事务的ReadView有效,也不能清理之后产生的undo记录。于是其他事务不断修改数据,undo log就会无限增长,极端情况下会撑爆磁盘空间。排查方法通常是通过information_schema.innodb_trx表找到长事务,并关注history_list_length的变化趋势。
SELECT trx_id, trx_started, trx_state, trx_rows_modified FROM information_schema.innodb_trx ORDER BY trx_started;
undo log的清理依靠后台的purge线程。事务提交后,undo记录并不会立即被删除,而是先放入历史列表,purge线程会判断不再被任何ReadView需要的undo记录,然后真正清除。如果数据库上同时存在大量读事务和写事务,purge速度跟不上产生速度,同样会造成undo表空间持续增大。合理的索引设计、控制事务时长、及时提交或回滚,是避免undo膨胀的有效手段。
另一个容易混淆的点是:undo log本身也会产生redo log。也就是说,当你修改一行数据时,InnoDB要先写undo,再修改数据页,这两步产生的日志都会进入redo log。这样即使系统崩溃,undo log中记录的历史版本也能通过redo log恢复出来,从而保证回滚操作在崩溃恢复后依然有效。
崩溃恢复中事务日志的协作流程
理解了redo log和undo log各自的作用后,再来看一次完整的崩溃恢复过程。假设数据库在某个时间点发生宕机,此时内存中的redo log buffer可能尚未刷盘,数据页可能还未写入磁盘,已经提交的事务和未提交的事务混杂在一起。重启后InnoDB会先加载redo log,找到最后一个checkpoint位置,然后从该位置开始扫描之后的所有redo记录。对于已提交但数据页未刷盘的事务,redo log会将这些修改重新应用到数据页上,这一步称为前滚。
前滚完成后,数据库处于一个“所有已提交事务都已生效”的状态,但那些未提交事务的修改也可能被redo log重做了一部分。此时需要利用undo log进行回滚:InnoDB扫描undo日志,找出所有在崩溃前尚未提交的事务,将它们的修改逐一撤销。这样最终达到的状态,就是所有已提交事务的修改被保留、所有未提交事务的修改被清除,数据恢复到一致状态。
-- 模拟一个未提交事务在崩溃前执行了UPDATE START TRANSACTION; UPDATE accounts SET balance = balance - 50 WHERE id = 2; -- 此时系统崩溃,事务未提交 -- 重启后恢复流程: -- 1. redo log 前滚,将balance的修改重新应用到数据页 -- 2. undo log 回滚,将该未提交事务的balance修改撤销 -- 最终 balance 回到事务开始前的值
整个恢复过程依赖redo log与undo log的配合,也依赖checkpoint机制。checkpoint之前的日志可以认为已经不再需要,因为这些修改已经确认刷新到了磁盘数据页。checkpoint之后的日志则必须保留,用于崩溃恢复。因此redo log文件的大小和checkpoint触发频率,直接决定了崩溃恢复需要扫描的日志范围。
如果redo log文件损坏或者checkpoint信息异常,恢复过程就会失败,甚至可能需要通过备份和binlog进行更复杂的修复。所以在生产环境中,除了合理配置刷盘参数外,还应该结合binlog做完整的备份策略,定期演练恢复流程。单纯依赖redo log和undo log可以解决实例级别的崩溃恢复,但无法应对磁盘损坏、误删除等需要时间点恢复的场景。
综合来看,MySQL事务日志是一条贯穿事务生命周期的核心线索。redo log保证已提交的事务不会丢失,undo log保证未提交的事务可以被撤销,同时支撑MVCC读取一致性快照。刷盘时机、文件容量、checkpoint频率和purge速度,每一个环节都在影响数据库的可靠性、恢复时间和写入性能。理解这些机制,不仅能帮助你在遇到数据一致性问题时快速定位原因,也能在参数调优和架构设计时做出更合理的决策。