在 MySQL 的事务执行过程中,binlog 和 redo log(重做日志)协同工作来确保事务的持久性和数据一致性。MySQL 使用一种称为 【两阶段提交】 的机制来确保这两个日志之间的一致性,以避免在崩溃时出现数据不一致的情况。 在我前面的 博客 中也有详细的介绍这两种日志,感兴趣的可以点击链接查阅。
1. redo log 和 binlog 的作用
redo log(重做日志):它是 InnoDB 存储引擎的日志,主要用于实现 事务的持久性
和 崩溃恢复
。当事务在 MySQL 中被提交时,数据并不会立即写入磁盘,而是先写入内存页,再通过 redo log 记录修改。在系统崩溃后,InnoDB 会通过 redo log 恢复未写入磁盘的事务,保证事务的持久性(即满足 事务的 ACID 中的 D:Durability)。
binlog(二进制日志):它是 MySQL Server 层的日志,记录所有导致数据修改的 SQL 语句。binlog 主要用于 主从复制
和 基于时间点的恢复
,使得主库和从库保持一致。
由于这两个日志分别在 MySQL Server 层和 InnoDB 存储引擎层,MySQL 需要确保这两个日志的状态在事务提交时保持一致。
为什么会发生这些不一致?
redo log 负责在主库崩溃时恢复数据,因此它影响的是 主库的数据持久性。
binlog 主要用于 主从复制,确保事务的操作可以复制到从库,从而影响 从库的数据一致性。
由于这两份日志分别负责不同的功能,如果在事务提交过程中,redo log 和 binlog 的刷盘过程不同步(即出现了 “半成功” 状态),就会导致主库和从库之间的数据不一致:
当 redo log 刷入磁盘成功,但 binlog 未写入磁盘时:主库的数据更新成功,但由于 binlog 丢失,从库不会执行相应的更新。
当 binlog 刷入磁盘成功,但 redo log 未写入磁盘时:从库的数据更新成功,但由于主库没有完成事务提交,主库回滚了这次更新。
这两种情况下,主库和从库的数据都会发生不一致,原因是 redo log 和 binlog 分别控制了主库的事务持久性和从库的事务一致性,如果它们之间的操作没有严格同步,主从数据就可能出现不一致。
看下面第2节的例子。
2. 为什么需要两个阶段提交(保持两种日志的一致)
假设 id =1这行数据的字段 name 的值原本是’lisi’,然后执行 UPDATE user SET name = ‘zhangsan’ WHERE id = 1;如果在持久化redo log 和 binlog 两个日志的过程中,出现了半成功状态,那么就有两种情况
情况一:redo log 刷入磁盘成功,但 binlog 没有写入磁盘
过程
- 执行 UPDATE user SET name = ‘zhangsan’ WHERE id = 1;,首先将事务的修改(name 从lisi 变为 zhangsan)写入 redo log 的 prepare 状态并刷入磁盘。
- 此时,redo log 已经确保数据的持久性,即便 MySQL 发生崩溃,重启后依然能通过 redo log 恢复内存中的数据,将 name 恢复到新的值 zhangsan。
- 但在 redo log 刷入磁盘之后,MySQL 在还没有来得及写入 binlog 的时候宕机。
重启后的结果:
- 主库:由于 redo log 已经持久化,事务恢复时会根据 redo log 的内容将 id = 1 这一行的 name 字段恢复到新值 zhangsan。
- 从库:binlog 没有写入,因此在主库宕机之前,这条更新语句没有记录在 binlog 中。从库通过复制机制读取的是主库上次宕机前的 binlog,由于 binlog 丢失了这条更新语句,从库的 name 字段仍然是旧值 lisi。
问题:主库的 name 值是 zhangsan,从库的 name 值是 lisi,导致主从数据不一致。
=====================================================================================================
情况二:binlog 刷入磁盘成功,但 redo log 没有刷入磁盘
过程:
- 执行 UPDATE user SET name = ‘zhangsan’ WHERE id = 1;,先将 SQL 语句记录在 binlog 中,并成功将 binlog 刷入磁盘。
- 在 binlog 刷入磁盘后,MySQL 发生崩溃,而此时 redo log 还没有提交成功,即 redo log 还处于 prepare 状态,没有完成持久化。
重启后的结果:
- 主库:由于 redo log 还没有提交,所以在崩溃恢复时,MySQL 发现该事务并没有真正提交,因此会回滚这次事务。重启后,主库的 id = 1 这一行 name 字段会保持旧值 lisi。
- 从库:由于 binlog 已经刷入磁盘,并且在主从复制时会将 binlog 传递给从库,所以从库会执行 binlog 中记录的这条更新语句,结果是从库将 id = 1 这一行的 name 字段更新为zhangsan。
问题:主库的 name 值是lisi,而从库的 name 值是 zhangsan,同样会导致主从数据不一致。
3. 两阶段提交的执行过程
在事务提交过程中,MySQL 的 binlog 和 InnoDB 的 redo log 需要通过两阶段提交机制来保持一致性。这个过程大致如下:
【图片来自:小林coding】
阶段 1:准备阶段(Prepare Phase)
-
记录 redo log:当一个事务执行并准备提交时,InnoDB 会将事务的修改先记录到 redo log 的 prepare 状态。这时的 redo log 还没有真正提交,但已经将所有修改持久化到了日志中。该状态确保在崩溃恢复时,系统能够根据这个 redo log 的 prepare 状态判断事务的执行情况。
-
写入内存:事务的数据修改通常首先在内存中操作(内存页或缓冲池),此时数据还没有被写入磁盘,但 redo log 的准备记录确保了这些修改可以在崩溃后恢复。
-
等待 binlog 的同步:在这一步,InnoDB 已经做好了写入日志的准备,但事务还没有完全提交,系统等待 binlog 的同步。
阶段 2:提交阶段(Commit Phase)
-
写入 binlog:MySQL 将该事务的修改操作(如 INSERT、UPDATE、DELETE)记录到 binlog 中,并根据配置(例如 sync_binlog)决定是否将 binlog 刷盘写入磁盘。这一步是为了确保在主从复制时,能够完整地同步所有操作。
-
提交 redo log:当 binlog 写入成功后,将 redo log 的状态从 prepare 改为 commit,并将日志数据刷入磁盘。这一过程确保事务的持久化和一致性。
-
事务提交完成:此时,事务的所有数据修改已经成功写入到 redo log 和 binlog 中,事务被正式提交。
4. 为什么两阶段提交不会出现重启后不一致的情况
【图片来自:小林coding】
不管是时刻 A(redo log 已经写入磁盘, binog 还没写入磁盘),还是时刻 B(redo log 和 binlog 都已经写入磁盘,还没写入 commit 标识)崩溃,此时的redo log 都处于 prepare 状态。
在 MySQL 重启后会按顺序扫描 redo log 文件,碰到处于 prepare 状态的 redo log,就拿着 redo log 中的XID 去 binlog 查看是否存在此 XID:
- 如果 binlog 中没有当前内部 XA 事务的 XID,说明 redolog 完成刷盘,但是 binlog 还没有刷盘,则回滚事务。对应时刻 A 崩溃恢复的情况。
- 如果 binlog 中有当前内部 XA 事务的 XID,说明 redolog 和 binlog 都已经完成了刷盘,则提交事务对应时刻 B 崩溃恢复的情况。
可以看到,对于处于 prepare 阶段的 redo log,即可以提交事务,也可以回滚事务,这取决于是否能在binlog 中查找到与 redo log 相同的 XID,如果有就提交事务,如果没有就回滚事务。这样就可以保证redo log 和 binlog 这两份日志的一致性了。
所以说,两阶段提交是以 binlog 写成功为事务提交成功的标识,因为 binlog 写成功了,就意味着能在binlog 中查找到与 redo log 相同的 XID。