本文由《MySQL 内部两阶段提交机制深度解析》与《MySQL 内部两阶段提交(2PC)深度解析》合并整理,去重后按「背景 → 流程 → 恢复 → 性能 → 参数 → 扩展 → 实战」组织。

一、背景:为什么需要内部两阶段提交

MySQL 的存储架构分为两层:

  • Server 层:负责 SQL 解析、优化、执行,以及 binlog 的写入。
  • 引擎层(InnoDB):负责数据页的持久化与 redo log 的写入。

两层各自维护一套日志。事务提交时必须让两套日志在语义上一致;若简单「先写一个、再写另一个」而不协调,崩溃后极易出现主从不一致。MySQL 在引擎内部采用内部两阶段提交(Internal 2PC)(常以 XID 关联 redo 与 binlog),而不是分布式事务里的外部 2PC 协调者模型。

1.1 两个日志的分工

日志层级作用常见格式/形态
redo logInnoDB崩溃恢复,保证已提交变更可重做物理日志(页修改)
binlogMySQL Server主从复制、备份与按时间点恢复(PITR)逻辑日志(语句)或 ROW

为什么不能只保留一种日志?

  1. redo log 与 InnoDB 紧耦合,其它存储引擎未必具备。
  2. binlog 属于 Server 层,与引擎解耦,复制与备份都依赖它。
  3. 历史演进上 binlog 更早出现,redo 后补;二者长期并存。

1.2 若无 2PC:两种顺序都会导致主从不一致

先 redo、后 binlog,中间崩溃:

  • 重启后 InnoDB 可能认为事务已生效;
  • 从库未收到对应 binlog → 主从分歧

先 binlog、后 redo,中间崩溃:

  • 从库已执行 binlog;
  • InnoDB 侧无对应重做记录 → 主库回滚或状态滞后 → 主从分歧

因此需要一种机制:以「binlog 是否成功落盘」作为事务对外可见/可复制的一条硬界线,并与 redo 中的事务状态对齐——这就是内部 2PC 要解决的核心问题。


二、两阶段提交核心流程

两阶段提交将一次 COMMIT 拆成 PrepareCommit 两段;binlog 落盘成功通常被视为事务在复制语义上已「提交」的分水岭。

2.1 执行路径总览(含 undo)

BEGIN;
│
├─ 执行 SQL(修改 Buffer Pool 中的页)
│   ├─ 写 undo log(旧值,供回滚/MVCC)
│   └─ 变更写入内存中的 redo(随刷盘策略落盘)
│
└─ COMMIT;  ← 触发内部 2PC
    │
    ├─ [Prepare]
    │   ├─ 生成全局事务标识 XID
    │   ├─ 将相关 redo 刷到可恢复状态(策略受 innodb_flush_log_at_trx_commit 影响)
    │   └─ 在 redo 中将事务标为 prepare,并记录 XID
    │
    ├─ [写 binlog](关键分界)
    │   ├─ Server 层写入 binlog(语句或 ROW)
    │   ├─ binlog fsync(策略受 sync_binlog 影响)
    │   └─ binlog 成功持久化 ⇒ 复制侧可观测到该事务
    │
    └─ [Commit]
        ├─ 将 redo 中该 XID 从 prepare 标为 commit
        └─ 释放锁、返回客户端

2.2 与「UPDATE 一行」对照的简化流程图

UPDATE users SET balance = 900 WHERE id = 1; 为例:

客户端提交 COMMIT
    ↓
┌─────────────────────────────────────────┐
│ 1. Buffer Pool 中页已更新(脏页)         │
└─────────────────────────────────────────┘
    ↓
┌─────────────────────────────────────────┐
│ 2. Prepare:redo 记录 XID=100, prepare   │
└─────────────────────────────────────────┘
    ↓
┌─────────────────────────────────────────┐
│ 3. 写 binlog(含相同 XID),fsync        │
└─────────────────────────────────────────┘
    ↓
┌─────────────────────────────────────────┐
│ 4. Commit:redo 标记 XID=100, commit    │
└─────────────────────────────────────────┘
    ↓
返回客户端 OK

2.3 关键概念小结

概念含义
XID贯穿 redo 与 binlog 的事务标识,崩溃恢复时用来对齐两边
prepareredo 上的中间态:已做好提交准备,等待 binlog 侧「落锤」
binlog fsync成功后,即使随后崩溃,恢复逻辑也倾向提交该事务,以与复制一致
commit(redo)redo 上的终态,表示该事务在引擎侧已完成提交闭环

三、崩溃恢复(Crash Recovery)

重启时 InnoDB 扫描 redo,对仍处于 prepare 的事务,用 binlog 中是否存在对应 XID 决定提交还是回滚。

3.1 决策原则

redo 状态binlog 中是否存在该 XID恢复动作
commit-无需额外处理
prepare存在且完整提交(前滚补全 commit)
prepare不存在/不完整回滚

直觉:binlog 已有 ⇒ 从库可能已执行 ⇒ 主库必须提交binlog 没有 ⇒ 复制世界不知道该事务 ⇒ 应回滚

3.2 典型崩溃点

崩溃时机redobinlog结果
Prepare 前无 prepare回滚,等价于未提交
Prepare 后、binlog 前prepare回滚
binlog 后、redo commit 前prepare提交
commit 完成后commit正常,无需特殊处理

3.3 时间线示例(与 XID 对照)

正常提交:

1
2
3
4
T1 改 Buffer Pool
T2 redo: XID=100 prepare
T3 binlog: XID=100
T4 redo: XID=100 commit

Prepare 后崩溃(无 binlog): 恢复时找不到 XID → 回滚,balance 维持旧值。

binlog 已写入后崩溃(redo 仍为 prepare): 恢复找到 XID → 提交,与从库对齐。


四、性能:组提交(Group Commit)

两阶段路径上,redo 与 binlog 的 fsync 往往是吞吐瓶颈。MySQL 5.6+ 的 binlog 组提交把多个事务的刷盘合并,显著减少 fsync 次数。

4.1 三子阶段(队列协调)

1
2
3
[Flush]  多个事务 binlog 写入 page cache(未必立即 fsync),第一个进队者为 leader
[Sync]   leader 代表整队做一次 fsync
[Commit] 按序通知各事务在 InnoDB 侧完成 commit

4.2 效果(示意)

1
2
无组提交:10 个并发事务 ≈ 10 次 fsync
有组提交:10 个并发事务 ≈ 1 次 fsync(理想情况)

五、关键参数与「双1」

5.1 innodb_flush_log_at_trx_commit

行为安全性能
0每秒刷盘,提交不一定刷
1每次提交刷 redo最高相对低
2提交写 OS cache,按策略刷

5.2 sync_binlog

行为
0由 OS 决定刷盘时机
1每次提交 fsync binlog(最安全
N每 N 次提交刷一次(折中)

5.3 生产常见「双1」

1
2
3
[mysqld]
innodb_flush_log_at_trx_commit = 1
sync_binlog = 1

可最大限度避免提交成功但日志未落盘导致的主从不一致;代价是磁盘 I/O 压力上升,需结合 SSD、组提交与业务吞吐评估。

5.4 组提交相关调优(高并发可酌情调)

参数含义思路
binlog_group_commit_sync_delay等待更多事务入队的微秒级延迟默认 0;高并发可尝试小幅增加以换批量 fsync
binlog_group_commit_sync_no_delay_count队列达到该事务数则不再等待与上一参数配合

六、与 undo log 的关系

内部 2PC 对齐的是 redo 与 binlogundo 仍在事务执行阶段参与(回滚与 MVCC),职责不同:

日志层级主要职责
undoInnoDB回滚、MVCC 版本链
redoInnoDB崩溃恢复(已提交需前滚)
binlogServer复制与 PITR

事务提交后 undo 不再用于回滚该事务,但历史版本仍可能被读视图引用,由 purge 异步回收。


七、生产案例(为何强调双1)

7.1 主库宕机后「从库比主库多」

  • binlog 已持久化,从库已应用;主库在 redo commit 前崩溃。
  • 恢复时:prepare + binlog 存在 ⇒ 提交 ⇒ 主从重新对齐。

7.2 sync_binlog = 0 时崩溃

  • binlog 可能留在 page cache 未落盘,主库恢复后按 redo/binlog 对齐逻辑可能回滚,而从库若已收到部分事件,易出现分歧。
  • 生产一般推荐 sync_binlog=1innodb_flush_log_at_trx_commit=1 配套,除非能明确接受风险并有额外防护。

八、实战验证

8.1 查看 redo / binlog 与 XID

1
2
SHOW VARIABLES LIKE 'innodb_log%';
SHOW ENGINE INNODB STATUS\G
1
2
mysqlbinlog /path/to/mysql-bin.000001 | head
# 可见 COMMIT 前的 Xid = ...

8.2 粗暴模拟崩溃(仅测试环境)

1
2
3
4
# 事务提交后
kill -9 $(pidof mysqld)
systemctl start mysqld
tail -f /var/log/mysqld.log   # 观察 crash recovery

8.3 主从一致性抽查

1
2
-- 主、从分别执行相同查询,结果应一致
SELECT balance FROM users WHERE id = 1;

8.4 监控参考

1
2
3
SHOW GLOBAL STATUS LIKE 'Com_commit';
SHOW GLOBAL STATUS LIKE 'Innodb_log_writes';
SHOW GLOBAL STATUS LIKE 'Binlog_cache_use';

九、小结

  1. 内部 2PC 解决的是 redo 与 binlog 的提交语义对齐,核心是 XID「binlog 是否落盘」 的恢复决策。
  2. 崩溃恢复:binlog 有 ⇒ 提交;binlog 无 ⇒ 回滚——从而尽量维持复制一致性
  3. 性能:组提交合并 fsync;安全innodb_flush_log_at_trx_commit=1sync_binlog=1 是生产默认基线。
  4. undo 负责事务级回滚与 MVCC,与 2PC 分工不同但同属事务实现

参考资料