“事务提交了,数据就一定不会丢”——这句承诺背后,是 InnoDB 三套日志在精密协作。redo log 保证崩溃不丢已提交数据,undo log 支撑回滚与 MVCC,binlog 负责主从复制与归档恢复。三者职责不同、归属不同(前两者是 InnoDB 引擎层的,binlog 是 MySQL Server 层的),却必须通过"两阶段提交"协同一致。搞混它们,就理解不了为什么 MySQL 既能崩溃恢复又能主从同步。
场景:一次 UPDATE 的生与死
执行 UPDATE account SET balance = balance - 100 WHERE id = 1,MySQL 不会立刻把数据页写回磁盘。磁盘随机写很慢,如果每次更新都同步刷盘,性能不可接受。InnoDB 的策略是:先改内存里的数据页(Buffer Pool),把"我改了什么"顺序写进 redo log,就算提交成功。 真正的数据页之后由后台线程慢慢刷回磁盘。
问题来了:如果数据页还没刷盘,机器就断电了怎么办?这正是 redo log 存在的意义。
redo log:崩溃恢复的底气
redo log 记录的是物理层面的修改——“把某个表空间某个页的某个偏移量改成某值”。它是顺序写的,磁盘顺序写远快于随机写,这就是 InnoDB 的核心性能技巧:把随机的数据页落盘,转化成顺序的日志落盘,即 WAL(Write-Ahead Logging,预写日志)。
redo log 大小固定,是循环写的环形结构:
1 | write pos(当前写到哪) |
write pos 追上 checkpoint 时,必须先推进 checkpoint(把脏页刷盘)才能继续写,此时会出现写入卡顿。
崩溃重启后,InnoDB 拿 redo log 重放(redo)那些"已提交但数据页未落盘"的修改,数据就回来了。控制 redo 刷盘时机的关键参数是 innodb_flush_log_at_trx_commit:
1 | = 1(默认):每次提交都把 redo 刷到磁盘 —— 最安全,不丢数据 |
生产环境对数据安全有要求的,必须是 1。这是性能与持久性最直接的权衡旋钮。
undo log:回滚与多版本的源泉
如果说 redo 是"重做",undo 就是"撤销"。修改一行前,InnoDB 先把旧值记进 undo log。事务回滚时,照着 undo 把数据改回去。
但 undo 的价值远不止回滚——它同时是 MVCC 的版本来源。前文讲过,一行的历史版本就串在 undo log 里,快照读靠它找到"对自己可见的旧版本"。所以 undo 不能在事务提交后立刻删除,必须等到没有任何 Read View 还可能引用它时,才由 purge 线程清理。这也是为什么长事务会让 undo 膨胀。
注意:undo log 的产生本身也会被 redo log 记录。因为 undo 也是数据,崩溃恢复时也得能重建出来——这体现了 redo 作为"最底层物理日志"的地位。
binlog:复制与时间机器
binlog 是 Server 层的逻辑日志,记录的是逻辑层面的变更(哪条语句、或哪行的前后镜像),与存储引擎无关(即便用 MyISAM 也有 binlog)。它有三种格式:
1 | STATEMENT:记原始 SQL,省空间,但 NOW()、UUID() 等非确定函数主从会不一致 |
binlog 是追加写的,写满一个文件换下一个,永不覆盖。它支撑两大能力:主从复制(从库拉取主库 binlog 重放)和基于时间点的恢复(用全量备份 + binlog 回放到任意时刻)。这就是为什么误删数据后能"找回来"——前提是 binlog 没被清掉。
两阶段提交:让两套日志一致
redo(引擎层)和 binlog(Server 层)必须保持一致:不能出现"redo 记了但 binlog 没记"(主从数据丢失),也不能"binlog 记了但 redo 没记"(主库崩溃恢复后丢这条,从库却有)。InnoDB 用 两阶段提交(2PC) 解决:
1 | 1. prepare 阶段:写 redo log,标记为 prepare 状态 |
崩溃恢复时的判定逻辑很精妙:
1 | - redo 处于 commit 状态 → 直接提交 |
binlog 是否完整,成了"这个事务到底算不算提交"的最终裁决依据。这套机制保证了 crash-safe 的同时,主从不会分叉。
工程权衡与踩坑
双 1 配置。 高可靠场景的黄金组合是 innodb_flush_log_at_trx_commit = 1 配合 sync_binlog = 1(每次提交都 fsync binlog)。两个都为 1 才真正不丢数据,但每次提交两次 fsync,是性能开销最大的配置。能容忍极端情况丢少量数据的场景,可把 sync_binlog 调成 100~1000 提升吞吐。
组提交(group commit)缓解 fsync 压力。 既然每次 fsync 都慢,InnoDB 把多个并发事务的日志攒成一批一次性 fsush,用一次 fsync 服务多个事务,大幅提升高并发下的提交吞吐。这是双 1 配置下还能扛住高并发的关键。
redo log 不宜过小。 设得太小,checkpoint 被频繁逼着推进,脏页刷盘抖动剧烈,TPS 出现周期性掉坑。一般给得足够大,让 checkpoint 平滑。
别混淆 redo 和 binlog 的恢复能力。 redo 只能做"崩溃恢复",恢复到崩溃前最后一刻的状态,它是循环写的、历史会被覆盖,做不了"恢复到三天前"。要回到任意历史时间点,只能靠 binlog + 备份。两者一个保命于当下,一个负责追溯历史,缺一不可。
小结
三套日志各司其职:redo 用 WAL 把随机写变顺序写、保崩溃不丢;undo 撑起回滚与 MVCC 的多版本;binlog 是 Server 层的逻辑流水,喂养主从复制与时间点恢复。两阶段提交以 binlog 的完整性为准绳,把引擎层与 Server 层的日志钉死在同一个真相上。理解了这三者的分工与协作,你就理解了 MySQL"提交即不丢、宕机能恢复、主从不分叉"承诺的全部底层逻辑。