前置知识: MySQL

两阶段提交

2 min高级

MySQL InnoDB 两阶段提交(2PC)机制详解:保证 Redo Log 与 Binlog 一致性、崩溃恢复流程与XA事务。

1. 两阶段提交的必要性

1.1 问题背景

InnoDB 的 Redo Log 和 Server 层的 Binlog 是两个独立的日志系统。如果不协调写入顺序,崩溃后会导致数据不一致:

plaintext
主库执行: UPDATE accounts SET balance = balance - 100 WHERE id = 1;

情况1: Redo Log 写入成功,Binlog 未写入 → 主库已扣款,从库未扣款
情况2: Binlog 写入成功,Redo Log 未写入 → 主库未扣款,从库已扣款

1.2 两阶段提交方案

将事务提交分为 Prepare 和 Commit 两个阶段,中间插入 Binlog 写入:

plaintext
阶段1 (Prepare):  写 Redo Log,标记为 prepare 状态
阶段间:           写 Binlog
阶段2 (Commit):   写 Redo Log,标记为 commit 状态

2. 两阶段提交流程

2.1 详细执行步骤

Commit 阶段

9. 写 Redo Log commit 标记

10. 释放行锁,事务对外可见

11. 释放 Undo Log 标记为可清理

Binlog 写入阶段

7. 将 Binlog Cache 写入 Binlog File

8. 根据 sync_binlog 设置决定是否 fsync

Prepare 阶段

4. 将 Redo Log Buffer 刷盘 fsync

5. Redo Log 中标记事务为 XA_PREPARE

6. 持有行锁,事务对外不可见

事务执行阶段

1. 执行 SQL,修改数据页 Buffer Pool

2. 生成 Undo Log 写入 Undo Tablespace

3. 生成 Redo Record 写入 Redo Log Buffer

Commit 阶段

9. 写 Redo Log commit 标记

10. 释放行锁,事务对外可见

11. 释放 Undo Log 标记为可清理

Binlog 写入阶段

7. 将 Binlog Cache 写入 Binlog File

8. 根据 sync_binlog 设置决定是否 fsync

Prepare 阶段

4. 将 Redo Log Buffer 刷盘 fsync

5. Redo Log 中标记事务为 XA_PREPARE

6. 持有行锁,事务对外不可见

事务执行阶段

1. 执行 SQL,修改数据页 Buffer Pool

2. 生成 Undo Log 写入 Undo Tablespace

3. 生成 Redo Record 写入 Redo Log Buffer

mermaid diagram

2.2 组提交优化

多个事务同时提交时,可以合并 fsync 操作:

事务A

Flush Stage 一次fsync

事务B

事务C

Sync Stage 一次fsync

Commit Stage 顺序commit

事务A

Flush Stage 一次fsync

事务B

事务C

Sync Stage 一次fsync

Commit Stage 顺序commit

mermaid diagram

阶段1(Flush):多个事务的 Redo Log 一起 fsync;阶段2(Sync):多个事务的 Binlog 一起 fsync;阶段3(Commit):依次标记 commit

3. 崩溃恢复

3.1 恢复流程

MySQL 重启时,InnoDB 扫描 Redo Log 进行崩溃恢复:

plaintext
1. 从 Checkpoint 点开始扫描 Redo Log
2. 重做(Redo):重放所有已提交事务的修改
3. 回滚(Undo):撤销所有未提交事务的修改
4. 处理 XA PREPARE 状态的事务:
   a. 检查 Binlog 中是否有该事务的记录
   b. 有 → 提交事务(commit)
   c. 无 → 回滚事务(rollback)

3.2 各种崩溃场景分析

崩溃时机Redo Log 状态Binlog 状态恢复动作
Prepare 之前无记录无记录无需恢复
Prepare 之后、Binlog 之前prepare无记录回滚事务
Binlog 之后、Commit 之前prepare有记录提交事务
Commit 之后commit有记录已完成,无需处理

3.3 Binlog 完整性判断

sql
-- MySQL 通过 XID(事务ID)匹配 Redo Log 和 Binlog
-- 每个 Binlog 事务组以 XID event 结尾

-- Binlog 中的事务格式:
-- BEGIN
-- ... (行变更事件)
-- XID 12345  ← 事务标识

-- 恢复时:在 Binlog 中查找 XID=12345
-- 找到 → 事务完整,提交
-- 找不到 → 事务不完整,回滚

4. XA 事务

4.1 外部 XA 事务

MySQL 支持 X/Open XA 规范,实现跨数据库的分布式事务:

sql
-- XA 事务语法
XA START 'txn1';
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
XA END 'txn1';
XA PREPARE 'txn1';   -- 第一阶段:准备
-- 此时可以查询事务状态
XA RECOVER;
XA COMMIT 'txn1';    -- 第二阶段:提交
-- 或 XA ROLLBACK 'txn1';  -- 第二阶段:回滚

4.2 XA 事务状态机

plaintext
START → END → PREPARE → COMMIT
                ↓
            ROLLBACK

状态: ACTIVE → IDLE → PREPARED → COMMITTED
                              → ROLLED_BACK

4.3 XA 事务的注意事项

  • 悬挂事务:PREPARE 后未 COMMIT 也未 ROLLBACK,占用锁资源
  • 超时处理:服务端没有内建的 XA 超时自动回滚,需应用层兜底并监控悬挂
  • 监控:定期执行 XA RECOVER 检查悬挂事务
sql
-- 查看悬挂事务
XA RECOVER;

-- 手动回滚悬挂事务
XA ROLLBACK 'txn1';

5. 半同步复制与两阶段提交

5.1 半同步复制对两阶段提交的影响

plaintext
异步复制:    主库提交 → 返回客户端 → 从库异步拉取 Binlog
半同步复制:  主库提交 → 等待至少1个从库确认收到 Binlog → 返回客户端

半同步复制在 Binlog 写入后增加了一个等待步骤:

plaintext
Prepare → Binlog → 等待从库ACK → Commit

5.2 After Sync vs After Commit

模式等待时机数据安全性能
After SyncBinlog 写入后、Commit 前主库崩溃不丢数据(无损半同步)相近
After CommitCommit 后主库崩溃可能丢数据、可现”幻影读”相近
sql
-- 5.7+ 默认 After Sync(变量自 8.0.26 更名,旧名 rpl_semi_sync_master_wait_point 已随 8.4 移除)
SET GLOBAL rpl_semi_sync_source_wait_point = AFTER_SYNC;

5.3 After Sync 的优势

plaintext
After Sync 流程:
1. Prepare
2. 写 Binlog
3. 等待从库 ACK  ← 在 Commit 之前
4. Commit

如果主库在步骤3后崩溃:
- 从库已收到 Binlog → 从库会提交
- 主库未 Commit → 恢复时检查 Binlog 完整 → 提交
- 数据一致!