mysql的两阶段提交
MySQL 使用两阶段提交(2PC)机制(主要发生在存储引擎层如 InnoDB 和 Server 层的 Binlog 之间)是为了确保事务在涉及多个独立组件(尤其是 Binlog 和存储引擎的 Redo Log)时,仍然能严格满足 ACID 特性中的原子性(Atomicity)和持久性(Durability),特别是在发生系统崩溃等故障的情况下。
使用两阶段提交的核心原因在于 Binlog 和 Redo Log 是 MySQL 中两个独立且关键的部分,它们有不同的职责,但共同保障了数据的完整性和可恢复性:
Binlog (Binary Log):
作用:记录所有对数据库结构或内容进行修改的语句(Statement 模式)或数据行的变更(Row 模式或 Mixed 模式)。主要用于:
主从复制 (Replication):从库(Slave)通过读取主库(Master)的 Binlog 来重放变更,保持数据同步。
时间点恢复 (Point-in-Time Recovery, PITR):结合全量备份和 Binlog,可以将数据库恢复到任意历史时间点。
写入时机:在事务提交时写入(具体是
COMMIT
之前)。性质:是 Server 层的逻辑日志,记录的是数据库操作的逻辑变化。
Redo Log (InnoDB 特有):
作用:记录物理页级别的修改。用于崩溃恢复 (Crash Recovery)。当数据库异常重启时,InnoDB 过重放 Redo Log 中未持久化到数据文件(.ibd)的修改,保证事务的持久性(Durability)。
写入时机:在事务执行过程中,数据页被修改后,会先写入 Redo Log Buffer,然后按策略(如事务提交时)刷盘(
fsync
)。性质:是存储引擎层的物理日志,记录的是数据页的物理变化。
关键问题:如何保证 Binlog 和 Redo Log 的一致性?
事务的原子性要求:一个事务中的所有操作,要么全部生效(持久化),要么全部不生效(像没发生过一样)。在 MySQL 架构中,一个事务的提交涉及:
将事务的修改写入存储引擎的数据页和 Redo Log(保证存储引擎内部的持久性)。
将事务的修改记录写入 Binlog(保证复制和 PITR 的完整性)。
如果 MySQL 没有使用两阶段提交,在事务提交过程中发生崩溃,可能导致 Binlog 和 Redo Log 记录不一致,从而破坏原子性和持久性:
场景一:先写 Binlog,后提交存储引擎(写 Redo Log)
COMMIT
语句开始执行。Binlog 写入成功(磁盘
fsync
)。在 InnoDB 真正提交(写 Commit 标记到 Redo Log 并
fsync
)之前,数据库崩溃。后果:
Binlog 中有该事务的记录。从库会应用这个事务(数据已同步到从库),或者做 PITR 时会包含这个事务。
InnoDB 未提交。崩溃恢复时,Redo Log 中没有该事务的 Commit 标记,事务会被回滚。主库上该事务的修改丢失。
问题:主库数据丢失,但从库/PITR 恢复后数据存在。主从不一致!数据不一致! 违反了原子性和持久性。
场景二:先提交存储引擎(写 Redo Log),后写 Binlog
COMMIT
语句开始执行。InnoDB 提交成功(Redo Log Commit 标记写入并
fsync
)。在写入 Binlog 之前,数据库崩溃。
后果:
InnoDB 已提交。崩溃恢复后,事务修改有效存在于主库。
Binlog 中没有该事务的记录。从库不会应用这个事务(数据缺失),PITR 也不会包含这个事务。
问题:主库有数据,但从库/PITR 恢复后没有该数据。主从不一致!数据不一致! 同样违反了原子性和持久性(从复制和恢复的角度看,数据没有持久化到整个系统)。
两阶段提交如何解决这个问题?
两阶段提交将事务的提交过程分为两个明确的阶段:
Prepare 阶段 (第一阶段):
InnoDB 将事务的状态设置为
PREPARE
,并将包含此状态的 Redo Log 记录强制刷盘(fsync
)。此时,事务的修改已经安全地持久化在 Redo Log 中(但尚未最终提交),可以保证崩溃后能恢复出这个“准备中”的事务。
注意:Binlog 此时还没有写入
Commit 阶段 (第二阶段):
MySQL Server (Binlog) 将事务的所有修改写入 Binlog 文件,并强制刷盘(
fsync
)。这一步确保了 Binlog 的持久性。一旦 Binlog 写入并刷盘成功,MySQL Server 通知 InnoDB 进行最终提交。
InnoDB 将事务的状态设置为
COMMIT
(通常是在 Redo Log 中写入一个 Commit 标记),并将此记录刷盘。这一步相对较快,因为主要的修改数据已经在 Prepare 阶段持久化了。
崩溃恢复时的处理 - 关键!
当数据库从崩溃中恢复时,InnoDB 会检查 Redo Log:
只有
COMMIT
标记的事务:直接重做(Redo)修改到数据文件。这些事务是完整的,Binlog 肯定也写成功了(因为 Commit 阶段在 Binlog 写完后才发生)。只有
PREPARE
标记的事务(没有COMMIT
):这是两阶段提交的核心。InnoDB 需要去检查 Binlog:如果该事务对应的 Binlog 记录存在且完整:说明 Prepare 阶段成功,Commit 阶段在写 Binlog 之后、写 InnoDB Commit 标记之前崩溃了。Binlog 已持久化,所以 InnoDB 执行提交(写入 Commit 标记并重做修改)。
如果该事务对应的 Binlog 记录不存在或不完整:说明 Prepare 阶段成功,但 Commit 阶段在写 Binlog 之前或中途崩溃了。Binlog 未持久化,所以 InnoDB 执行回滚(Undo 修改)。
通过这种机制,两阶段提交确保了:
原子性:一个事务要么同时在主库(通过 Redo Log 恢复)和 Binlog(复制/PITR)中生效,要么同时在两者中消失。不会出现一个生效另一个不生效的中间状态。
持久性:一旦客户端收到
COMMIT
成功的响应,数据一定同时安全地持久化在 Redo Log 和 Binlog 中,即使之后发生崩溃。数据一致性:主库的数据状态和 Binlog 记录的状态严格一致。
主从一致性:Binlog 是主从同步的基础。保证 Binlog 和主库数据一致,是保证主从数据一致性的前提。
MySQL 采用两阶段提交是为了协调其内部两个独立的日志系统(Binlog 和 InnoDB Redo Log)在事务提交过程中的操作。它通过在崩溃恢复阶段根据 Binlog 的完整性来决定是提交还是回滚处于 PREPARE
状态的事务,从而严格保证了无论提交过程中何时发生崩溃,事务在存储引擎层和 Binlog 层最终的状态都是一致的。这是实现数据库高可靠性和数据一致性的基石,尤其是对于主从复制和灾难恢复至关重要。