大家好,我是十三!欢迎来到十三Tech。

前几年一次主从切换事故——主库挂了切到从库,应用层报"刚刚提交的订单查不到"。排查发现:那个事务在主库 redo log 里已经 commit 标记,但 binlog 还没刷到磁盘,从库根本没收到。当时不懂,以为是 MySQL bug,后来研究了两阶段提交才明白——这不是 bug,是两阶段提交崩溃恢复的边界情况,跟我配的 sync_binlog 有关

这件事让我开始研究 redo log 和 binlog 怎么协作——两个独立日志,一个引擎层、一个 Server 层,怎么保证一致性?答案是两阶段提交(2PC, Two-Phase Commit)——一个诞生于分布式事务领域的经典协议,被 InnoDB 拿来解决内部多日志一致性问题。

这一篇就聊聊两阶段提交的三步流程、为什么不能简化成一步,以及崩溃恢复时怎么判断"事务到底有没有提交"。

两阶段提交:redo log 和 binlog 怎么不打架

一、为什么不能"先 redo 后 binlog"或反过来

理解两阶段提交之前,先想想——为什么不能简化?

方案 A:先写 redo log,再写 binlog。假设写完 redo log 后崩溃,binlog 没写。重启后 InnoDB 看到 redo log 有这条事务,会恢复出来——主库数据有了。但从库拿不到 binlog,不会重放这条事务——从库丢数据

方案 B:先写 binlog,再写 redo log。假设写完 binlog 后崩溃,redo log 没写。重启后 InnoDB 看 redo log 没这条事务,不恢复——主库数据没改。但从库拿到 binlog 会重放——从库多数据

两种简化方案都崩溃不一致。根因是两个日志独立写入、独立崩溃恢复,没有"协调者"保证原子性

两阶段提交的思路——把"提交"这个动作拆成两步:

  1. 第一阶段(Prepare):InnoDB 把 redo log 写盘,标记这条事务为"prepare 状态"——但还没真正 commit
  2. 第二阶段(Commit):Server 层写 binlog,写成功后再回到 redo log 把状态改成"commit"

中间任何一刻崩溃,重启时按"binlog 有没有这条事务"来决定——有就 commit、没有就 rollback。用 binlog 作为最终事实的判官

两阶段提交的三步内部结构

二、三步流程——prepare / binlog / commit

具体到代码层面,一个事务的提交分三步:

Step 1:Prepare。InnoDB 把事务的 redo log 写入 log buffer(不含 commit 标记),写到 ib_logfile。此时 redo log 里这条事务的状态是 PREPARE,事务 ID 和 XID(XID 是 binlog 跟 redo log 关联的关键)一起记录下来。

Step 2:Write binlog。Server 层把这条事务的 binlog event 写到 binlog 文件,并执行 fsync 把 binlog 刷盘。binlog event 里也带着同样的 XID。

Step 3:Commit。InnoDB 把 commit 标记写到 redo log(很快,因为只是改个状态字段),事务对客户端返回"提交成功"。

XID 是关键——它把 redo log 和 binlog 的同一条事务关联起来。崩溃恢复时,扫 redo log 找到所有 prepare 状态的事务,看它们的 XID 在 binlog 里存不存在——存在说明 binlog 已写成功,commit;不存在说明 binlog 没写成功,rollback。

时间线:
  redo prepare  ──→  binlog write+fsync  ──→  redo commit
       ↑                    ↑                    ↑
   crash → rollback    crash → ?           crash → commit

中间 crash 的判断——InnoDB 扫 redo log 拿到 prepare 事务的 XID,去 binlog 里查:找到就 commit、没找到就 rollback。用 binlog 的存在性做最终决定,这就是为什么 binlog 是"事实判官"。

三、崩溃恢复——怎么判断事务到底提交了没

两阶段提交的精髓全在崩溃恢复——这是最容易出错也是 InnoDB 设计最精巧的地方。

崩溃恢复流程

  1. 重启后扫 redo log,找到所有"prepare 状态但没 commit"的事务,收集它们的 XID 集合(记为 XID_redo
  2. 扫 binlog,拿到所有已经写入 binlog 的事务 XID 集合(记为 XID_binlog
  3. XID_redo 里每个事务:
    • 如果 XID ∈ XID_binlog → commit(binlog 已写成功,不能丢)
    • 如果 XID ∉ XID_binlog → rollback(binlog 没写,从库不会重放,主库也得回滚保持一致)

关键细节:binlog 怎么算"已写成功"?答案是看 binlog 文件里这条事务的 XID event 是否完整。binlog event 在每个事务结束时会写一个 XID_EVENT(标记事务提交),所以只要 binlog 里有完整的 XID_EVENT,就算写成功。如果 binlog 写到一半崩溃,那个不完整的 transaction 会被丢弃。

回头看那个主从切换故障——根因是 sync_binlog=0(默认值,binlog 不主动 fsync,靠 OS 刷盘)。事务在 redo log 里 commit 标记已经写了,但 binlog 还在 OS page cache 里没落盘,主库突然断电,重启后 binlog 里没这条事务,但从库早就收到 ACK 了(半同步模式下)。修复方法:sync_binlog=1 + innodb_flush_log_at_trx_commit=1——这就是著名的"双 1 配置",金融场景的标配。

崩溃恢复时怎么判断事务提交状态

"双 1"配置的代价:每次事务都要 fsync 两次(redo log + binlog),性能下降明显。生产中很多场景折中成 sync_binlog=100(每 100 次事务 fsync 一次 binlog),最多丢 100 个事务。金融场景严格双 1,日志型业务可放宽

两阶段提交真正教会我的,不是"prepare / commit 名词",而是怎么用"协调者+参与者"模式解决多副本一致性问题——这个思想不只数据库在用,分布式事务的 XA、TCC、Saga 全是它的变种。理解两阶段提交等于理解了一致性协议的核心。

下一篇讲 undo log——它是事务回滚的依据,也是 MVCC 能"读历史版本"的基础。


关于十三Tech 资深服务端研发,AI 实践者,专注分享真实可落地的技术经验。 相信 AI 是程序员的最佳搭档。

联系方式:569893882@qq.com GitHub:@TriTechAI