Skip to content

05.4 - 两阶段提交与崩溃恢复

定位:两阶段提交是 Redo Log 和 Binlog 保持一致的"同步协议"——理解它才能理解 MySQL 崩溃后如何恢复到正确状态 面试高频度:⭐⭐⭐⭐⭐ 考查方式:为什么需要 2PC、prepare 和 commit 阶段细节、崩溃恢复的三种场景、如何保证主从一致


一、这是什么?为什么需要它?

是什么

两阶段提交(2PC, Two-Phase Commit)是 MySQL 为了保证 Redo Log 和 Binlog 状态一致而设计的提交协议。

为什么需要 2PC?

如果不需要 2PC,崩溃后会出现什么问题?

sql
-- 假设事务 T 执行: UPDATE users SET age=30 WHERE id=1

-- 场景 1: 先写 Redo Log,后写 Binlog
--   写完 Redo Log 后崩溃 → Binlog 没有记录
--   → 恢复后: 数据改了 (Redo Log 重放) 但 Binlog 没记录
--   → 从库复制 Binlog → 主从不一致!

-- 场景 2: 先写 Binlog,后写 Redo Log
--   写完 Binlog 后崩溃 → Redo Log 没有记录
--   → 恢复后: 数据没改 (Redo Log 没有重放) 但 Binlog 有记录
--   → 从库会执行这个事务 → 主从不一致!

2PC 的解决方案:把提交分成两个阶段,确保 Redo Log 和 Binlog 要么都写入,要么都不写入。

核心 insight:2PC 解决的本质上是一个"分布式事务"问题——Redo Log 和 Binlog 是两个独立的写入通道,需要协调器来保证它们的一致性。


二、原理拆解

2.1 两阶段提交流程

                  2PC 提交过程

事务执行完成后,进入提交阶段:

             ┌─────────────────┐
             │ 事务开始执行...   │
             │ UPDATE ...      │
             │ UPDATE ...      │
             └────────┬────────┘


            ┌─────────────────┐
    Phase 1 │  Prepare 阶段    │
            └────────┬────────┘

             ┌───────┴────────┐
             │ Redo Log 写入   │
             │ 状态: prepare  │
             │ (事务ID+修改)   │
             └───────┬────────┘
                     │ 成功

            ┌─────────────────┐
    Phase 2 │  Commit 阶段     │
            └────────┬────────┘

             ┌───────┴────────┐
             │ 1. Binlog 写入  │ ← 此时崩溃?→ 恢复逻辑看后面
             │ 2. Binlog 刷盘  │
             └───────┬────────┘
                     │ 成功

             ┌─────────────────┐
             │ Redo Log 标记    │
             │ 状态: commit    │
             └─────────────────┘


             ┌─────────────────┐
             │ 事务提交成功 ✅   │
             └─────────────────┘

2.2 三个关键状态

事务状态转换:

                      ┌─────┐
                      │ 执行中 │ ← DML 操作
                      └──┬──┘

                      ┌──▼──┐
                      │Prepare│ ← Redo Log 写入 prepare 状态
                      └──┬──┘

                   ┌─────┴──────┐
                   │            │
               ┌───▼──┐    ┌───▼──┐
               │ Binlog│    │崩溃!│
               │ 写入  │    └──┬───┘
               └───┬──┘       │
                   │        ┌──▼──┐
               ┌───▼──┐    │恢复  │
               │Commit │    │判断  │
               └───┬──┘    │     │
                   │      ┌┴┐   ┌┴┐
               ┌───▼──┐   │Bin│ │Bin│
               │完成 ✅│   │log│ │log│
               └──────┘   │有?│ │无?│
                          └┬─┘ └┬─┘
                        ┌──┴──┐┌──┴──┐
                        │提交  ││回滚  │
                        └─────┘└─────┘

2.3 崩溃恢复的三种场景

崩溃发生在哪个阶段? → 恢复时不同的处理:

场景 A: Prepare 前崩溃
  ┌──────────┐
  │ 执行中... │ ← 还没进入 Prepare
  └──────────┘
  ⇒ 回滚 —— Redo Log 无记录,Binlog 无记录
  ⇒ 该事务的修改全部丢弃

场景 B: Prepare 后、Binlog 写入前崩溃
  ┌────────────┐
  │ Redo Log: Prepare ✅  │
  │ Binlog: ❌ 未写入     │
  └────────────┘
  ⇒ 回滚 —— Redo Log 有 prepare 但没有对应的 Binlog
  ⇒ InnoDB 认为事务未提交 → 用 Undo Log 回滚

场景 C: Binlog 写入后、Commit 前崩溃
  ┌────────────┐
  │ Redo Log: Prepare ✅  │
  │ Binlog: ✅ 已写入     │
  │ Redo Log: Commit ❌   ← 崩溃在这里
  └────────────┘
  ⇒ 提交 —— Redo Log 有 prepare 且 Binlog 已写入
  ⇒ InnoDB 认为事务已提交 → 补写 Redo Log commit 状态
  ⇒ 数据可见,Binlog 和主库一致 ✅

场景 D: Commit 完成后崩溃
  ┌────────────┐
  │ Redo Log: Commit ✅   │
  │ Binlog: ✅ 已写入     │
  └────────────┘
  ⇒ 无需处理 —— 事务已完整提交 ✅

2.4 Redo Log 的 prepare 和 commit 状态

Redo Log 记录格式:

prepare 状态的 Redo Log:
  [TYPE=PREPARE] [TRX_ID=100] [PAGE_NO=42] [OFFSET=128] [OLD=20] [NEW=30]

commit 状态的 Redo Log:
  [TYPE=COMMIT] [TRX_ID=100]

崩溃恢复时:

  1. 扫描所有 Redo Log 中的事务
  2. 对于每个 prepare 状态的事务,检查 Binlog 中是否有对应记录
  3. 有 → 补 commit(场景 C);没有 → 回滚(场景 B)

2.5 XID 的桥梁作用

XID(事务 ID) 是连接 Redo Log 和 Binlog 的桥梁:

Redo Log:  [TRX_PREPARE] [XID=12345]  [修改内容]
              ↑                            ↑
              │   ← 同一个 XID              │
Binlog:     [XID_EVENT: 12345]         [SQL/行数据]

崩溃恢复时:
  扫描 Redo Log → 找到 XID=12345 的 prepare 事务
  扫描 Binlog → 检查 XID=12345 是否写入
  已写入 → 提交,未写入 → 回滚

三、图解全景

              崩溃恢复决策树

MySQL 崩溃重启


扫描 Redo Log → 找出所有 prepare 状态的事务


┌──────────────────────────────────┐
│ 对于每个 prepare 事务:            │
│                                  │
│  检查 Binlog 中是否有对应 XID     │
│                                  │
│  ┌──────────┐    ┌──────────┐   │
│  │ 有 XID   │    │ 无 XID   │   │
│  └────┬─────┘    └────┬─────┘   │
│       │               │         │
│       ▼               ▼         │
│  ┌──────────┐    ┌──────────┐   │
│  │ 提交事务   │    │ 回滚事务   │  │
│  │ (补commit)│    │ (Undo回滚)│  │
│  └──────────┘    └──────────┘   │
│                                  │
│  ⇒ 保证 Redo Log 和 Binlog      │
│    最终状态一致 ✅                │
└──────────────────────────────────┘

四、实战验证

验证同步 binlog 配置

sql
-- 查看双 1 配置
SHOW VARIABLES WHERE variable_name IN (
  'innodb_flush_log_at_trx_commit',
  'sync_binlog'
);

模拟 Binlog 写入后崩溃的恢复

sql
-- 注意:以下无法在 SQL 层面直接模拟,需要理解原理

-- 查看 Binlog 中的 XID
SHOW BINLOG EVENTS IN 'mysql-bin.000001' LIMIT 10;

-- 使用 mysqlbinlog 解析查看
-- $ mysqlbinlog -v mysql-bin.000001 | grep XID

监控复制状态

sql
-- 查看主从复制是否在正常运行
SHOW SLAVE STATUS\G
-- 重点看:
--   Slave_IO_Running: Yes    ← 从库 IO 线程正常
--   Slave_SQL_Running: Yes   ← 从库 SQL 线程正常
--   Seconds_Behind_Master: 0 ← 没有延迟

五、面试视角

追问答案要点
为什么需要 2PC?保证 Redo Log 和 Binlog 的一致性。否则崩溃后可能主从不一致
prepare 阶段做了什么?写 Redo Log,标记为 prepare 状态。此时事务还没提交
commit 阶段做了什么?先写 Binlog,再标记 Redo Log 为 commit。两个都成功才算提交
Prepare 后崩溃怎么办?检查 Binlog 中是否有对应 XID:有则补提交,无则回滚
XID 的作用?连接 Redo Log 和 Binlog 的桥梁,崩溃恢复时通过 XID 匹配两个日志
双 1 配置是什么?innodb_flush_log_at_trx_commit=1 + sync_binlog=1,最安全配置

📚 相关链接

  • Redo Log:**Redo Log**
  • Undo Log:**Undo Log**
  • Binlog:**Binlog**
  • ACID 实现:**事务ACID实现**
  • ← 返回 **日志系统索引**

Knowledge4J — Java 知识库