MySQL 的二进制日志(binlog)、重做日志(redolog)以及保证它们之间数据一致性的关键机制——两阶段提交(2PC, Two-Phase Commit)。
核心概念
首先,我们必须理解这两个日志的根本区别:
特性 | 重做日志 (Redo Log) | 二进制日志 (Binlog) |
---|---|---|
所属层 | InnoDB 存储引擎层 实现的日志 | MySQL Server 层 实现的日志,所有存储引擎都可以使用 |
日志类型 | 物理日志 记录的是“在某个数据页上做了什么修改” (例如:给第xx表空间的第yy数据页的第zz偏移量写了什么数据) |
逻辑日志 记录的是语句的原始逻辑(Statement)或行更改后的值(Row) (例如: UPDATE t SET c=c+1 WHERE id=2; 或 更新前后两行的数据) |
用途 | 崩溃恢复 (Crash Recovery) 确保事务的持久性 (Durability)。在数据库宕机重启后,将已提交但未刷盘的数据重新应用(redo)到数据页。 |
主从复制 (Replication) & 数据恢复 (Point-in-Time Recovery) 记录所有更改数据库数据的操作,用于将数据同步到从库,或基于时间点的数据恢复。 |
写入方式 | 循环写 空间固定,写满后回头覆盖(但覆盖前必须确保对应脏页已刷盘)。 |
追加写 文件不断增长,通过 max_binlog_size 控制单个文件大小,写满后切换到下一个文件。 |
刷盘时机 | 由 innodb_flush_log_at_trx_commit 参数控制,事务提交时可以选择如何刷盘,对数据安全性至关重要。 |
由 sync_binlog 参数控制,事务提交后可以选择如何刷盘,影响主从数据一致性。 |
为什么需要两阶段提交 (2PC)?
想象一下,如果只有一个日志,或者两个日志独立写入,会有什么问题?
不一致的场景:
-
先写 binlog,后写 redolog:
- 假设 binlog 写完,但 redolog 还没写完,数据库崩溃了。
- 重启后:InnoDB 发现 redolog 中这个事务不完整,会将其回滚。
- 后果:binlog 里已经记录了这个事务,但数据库实际数据没有改变。如果用这个 binlog 去搭建从库或恢复数据,从库的数据会比主库多,造成数据不一致。
-
先写 redolog,后写 binlog:
- 假设 redolog(prepare状态)写完,但 binlog 还没写完,数据库崩溃了。
- 重启后:InnoDB 发现 redolog 是 prepare 状态,会去检查 binlog 是否完整。
- 如果 binlog 不完整(这个事务没写进去),InnoDB 会回滚该事务。
- 后果:redolog 中的事务被回滚了。但如果 binlog 已经写完了呢?
- 另一种情况:redolog(prepare)和 binlog 都写完了,但在 redolog commit 前崩溃。
- 重启后:InnoDB 发现 redolog 是 prepare 状态且 binlog 是完整的,它会自动提交该事务。
- 看起来没问题? 但如果 binlog 没写完,事务就被回滚了。数据库内部数据一致,但 binlog 缺失了这个事务。用这个 binlog 恢复的从库或数据库会缺少这个事务,造成数据不一致。
为了解决这两个日志之间的一致性问题,MySQL 引入了内部的两阶段提交协议。它的核心思想是将事务的提交过程拆分成了两个阶段,让两个日志的写入“纠缠”在一起,形成一个原子操作。
两阶段提交 (2PC) 详细流程
两阶段提交将一个事务的提交过程分为 Prepare 和 Commit 两个阶段。下图直观地展示了其工作流程与崩溃恢复逻辑:
flowchart TD
A[事务执行UPDATE语句] --> B[InnoBuffer Pool中修改数据页<br>生成Redo Log并写入Log Buffer]B --> C[用户执行COMMIT]
C --> Stage1[阶段一: Prepare]subgraph Stage1[阶段一: Prepare]direction LRS1[将Log Buffer中的Redo Log<br>强制刷盘(fsync)] --> S2[将Redo Log记录的事务状态<br>标记为'PREPARE']
endStage1 --> Stage2[阶段二: Commit]subgraph Stage2[阶段二: Commit]direction LRT1[写Binlog到文件] --> T2[将Binlog文件强制刷盘(fsync)<br>(依赖sync_binlog参数设置)]T2 --> T3[将Log Buffer中的Redo Log(含COMMIT标记)<br>强制刷盘(fsync)]
endStage2 --> D[事务提交完成<br>返回用户成功]C --> CrashPoint1[崩溃点A<br>(Redo Log Prepare前)]
CrashPoint1 --> Recovery1[重启恢复: 回滚事务]Stage1 --> CrashPoint2[崩溃点B<br>(Binlog写入前)]
CrashPoint2 --> Recovery2[重启恢复: 回滚事务]T2 --> CrashPoint3[崩溃点C<br>(Redo Log Commit前)]
CrashPoint3 --> Recovery3subgraph Recovery3[重启恢复: 检查Binlog]R1[根据XID查找Binlog] --> R2{Binlog是否存在且完整?}R2 -- 是 --> R3[提交事务]R2 -- 否 --> R4[回滚事务]
end
结合流程图,我们来看每个阶段的具体工作:
第一阶段:Prepare(准备阶段)
- 用户发出
COMMIT
请求。 - InnoDB 开始进行两阶段提交。
- 写 redolog:将当前事务产生的 redolog 从 log buffer 写入磁盘(
fsync
操作,取决于innodb_flush_log_at_trx_commit
设置),并将日志记录的状态标记为PREPARE
。此时,事务还没有真正提交。
第二阶段:Commit(提交阶段)
- 写 binlog:将事务的二进制日志(binlog)从 binlog cache 写入到磁盘上的 binlog 文件(
fsync
操作,取决于sync_binlog
设置)。 - 写 redolog (commit标记):在刚才写入的 redolog 中打上一个
COMMIT
标记,表示事务正式提交。(注意:在 MySQL 5.7 及以后,为了优化性能,这一步的刷盘操作有时可以被省略,依赖于binlog
是否已经成功持久化)。
注意:步骤 1 和 2 之间的时刻,就是流程图中的“崩溃点C”,是崩溃恢复时判断事务状态的关键。
崩溃恢复 (Crash Recovery) 过程
这是两阶段提交最精彩的部分。数据库重启时,会进入恢复模式:
- 扫描 redolog:数据库启动时,InnoDB 会扫描最后的 redolog 文件,收集所有处于
PREPARE
状态的事务(XID
)。 - 对于每一个
PREPARE
状态的事务(XID
):- 情况一:Binlog 完整(提交事务)
- 如果发现该事务的
XID
也存在于 binlog 中(说明在崩溃前,binlog 已经成功写入并刷盘),那么 InnoDB 会认为这个事务是有效的,应该被提交。 - 操作:重新写下一条 redolog 记录,状态为
COMMIT
,然后完成事务的提交(应用修改到数据页)。
- 如果发现该事务的
- 情况二:Binlog 不完整(回滚事务)
- 如果在该事务对应的 binlog 中找不到完整的
XID
记录(说明在崩溃前,binlog 可能没写完),那么 InnoDB 会认为这个事务是无效的,应该被回滚。 - 操作:根据 undolog 来回滚该事务在 Prepare 阶段所做的所有修改。
- 如果在该事务对应的 binlog 中找不到完整的
- 情况一:Binlog 完整(提交事务)
通过这个恢复机制,它完美地解决了上述两种不一致的场景:
- 保证了只要 binlog 里有的,redolog 最终也一定会提交 -> 主从数据一致。
- 保证了 binlog 里没有的,redolog 即使 prepare 了也会被回滚 -> 主从数据一致。
总结与要点
-
目的:两阶段提交的唯一目的是保证 binlog 和 redolog 这两个日志的逻辑一致性,从而确保:
- 主从复制的数据一致性。
- 崩溃恢复后,基于 binlog 的点播恢复和数据库内部数据的一致性。
-
角色:
- Redo Log (Prepare):表示“我准备好了,我保证我的修改能力是有效的”。
- Binlog (Write & Fsync):表示“我记录了,这个操作可以对外发布了”。
- Redo Log (Commit):表示“好的,基于 binlog 的记录,我正式提交”。
-
性能影响:两阶段提交意味着一次事务提交需要多次刷盘(
fsync
),这是 MySQL 写操作的主要性能瓶颈所在。优化手段就是调整innodb_flush_log_at_trx_commit
和sync_binlog
参数,在性能和数据安全性之间做出权衡。
简单来说,两阶段提交是 MySQL 协调 Server 层和 InnoDB 存储引擎层,使用两种不同性质的日志,最终达成数据一致性目标的精妙协议。