3、MySQL Binlog 核心原理(下):Binlog的写入机制、两阶段提交、与Redo Log的协作

上一章我们聊了 Binlog 的格式和基本结构。这一章,咱们深入底层,看看 Binlog 到底是怎么写进去的,以及它和 InnoDB 的 Redo Log 是怎么配合完成事务提交的。

说白了,这部分是 MySQL 高可用和数据一致性的基石。你要是搞懂了这块,后面看 Debezium 的运作机制,就会觉得豁然开朗。

3.1 Binlog 的写入机制:从内存到磁盘

Binlog 的写入,不是一条 SQL 执行完就立刻刷盘的。它有一套缓冲机制,目的是平衡性能和数据安全。

我刚开始接触 MySQL 内核时,也以为 Binlog 是实时写的。后来翻了源码才发现,没那么简单。

3.1.1 写入流程

  1. 事务执行阶段:事务中的 DML 操作,先修改内存中的数据页,并记录 Redo Log。
  2. 事务提交阶段:在事务提交时,MySQL 会生成该事务的 Binlog 事件,并写入到一个叫做 binlog_cache 的内存缓冲区中。
  3. 写入 Binlog 文件:在事务提交的某个阶段,将 binlog_cache 中的内容写入到操作系统的 binlog 文件(实际上是文件系统的 Page Cache)。
  4. 刷盘(fsync):根据配置参数,决定何时将 Page Cache 中的数据真正持久化到磁盘。

核心参数:sync_binlog

  • sync_binlog=0:由操作系统决定何时刷盘。性能最好,但服务器宕机时可能丢失最多一个事务的 Binlog。
  • sync_binlog=1:每次事务提交都强制刷盘。最安全,但性能最差。
  • sync_binlog=N:每 N 个事务提交后刷盘一次。折中方案。

我个人习惯,在生产环境里,如果对数据一致性要求极高,会设置 sync_binlog=1。但如果业务量巨大,且能容忍秒级的数据丢失,可以适当调大 N 值。

3.1.2 Binlog Cache 与事务

每个客户端连接,在事务执行期间,都会有自己的 binlog_cache。这个缓存的大小由 binlog_cache_size 控制。

如果事务产生的 Binlog 事件太大,超过了这个缓存,MySQL 会使用临时文件来存储。嗯,这里要注意,频繁使用临时文件会影响性能。

监控建议:

你可以通过 SHOW STATUS LIKE 'Binlog_cache_%'; 来查看缓存的使用情况。如果 Binlog_cache_disk_use 的值很高,说明你的 binlog_cache_size 可能设置得太小了。

3.2 两阶段提交:Binlog 与 Redo Log 的握手协议

这是 MySQL 保证 crash-safe 的核心机制。说白了,就是确保 Binlog 和 Redo Log 的状态最终一致。

为什么需要两阶段提交?

你想想看,一个事务提交时,需要写两份日志:Redo Log(InnoDB 的)和 Binlog(Server 层的)。如果写完 Redo Log 后,在写 Binlog 之前系统崩溃了,那恢复时就会出问题——Redo Log 说事务提交了,但 Binlog 里没有,主从复制就会丢数据。

反过来,如果先写 Binlog,再写 Redo Log,写 Binlog 后崩溃了,Binlog 里有记录,但 Redo Log 没有,从库可能会多出数据。

所以,MySQL 引入了两阶段提交。

3.2.1 两阶段提交的流程

我把这个过程拆解成三步,你一看就明白:

阶段 动作 状态
Prepare 阶段 InnoDB 将 Redo Log 写入磁盘,并将事务状态标记为 prepare。 Redo Log 已持久化,但事务未提交。
写 Binlog 阶段 MySQL Server 层将事务的 Binlog 事件写入文件,并刷盘(取决于 sync_binlog)。 Binlog 已写入,但事务仍未最终提交。
Commit 阶段 InnoDB 将 Redo Log 中的事务状态从 prepare 改为 commit。 事务正式提交,数据对外可见。

这个流程,我用一张图来展示,会更直观:

MySQL 两阶段提交流程 客户端 Server 层 InnoDB 引擎 步骤 1: Prepare 阶段 Redo Log 写入,状态标记为 prepare 步骤 2: 写 Binlog 阶段 Binlog 事件写入文件并刷盘 步骤 3: Commit 阶段 Redo Log 状态改为 commit 关键点: 任一阶段崩溃,恢复时 通过比对两日志状态

3.3 崩溃恢复:如何保证数据不丢

两阶段提交的设计,就是为了应对崩溃恢复。MySQL 在重启后,会扫描 Redo Log 和 Binlog,根据它们的状态来决定事务是提交还是回滚。

具体的恢复逻辑是这样的:

  • 情况一:Redo Log 中事务状态为 prepare,且对应的 Binlog 已经完整写入并刷盘。此时,MySQL 认为事务是完整的,会在恢复时执行 提交 操作。
  • 情况二:Redo Log 中事务状态为 prepare,但对应的 Binlog 不完整或不存在。此时,MySQL 认为事务是不完整的,会在恢复时执行 回滚 操作。
  • 情况三:Redo Log 中事务状态为 commit。直接提交,无需额外处理。

我曾经踩过的坑:

有一次,我在一个高负载的 MySQL 实例上,把 sync_binlog 设置成了 0,结果机房断电了。重启后,发现有几条 Binlog 记录丢失了,导致从库的数据和主库不一致。那次之后,我再也不敢在核心业务库上把 sync_binlog 设为 0 了。

3.4 Binlog 与 Redo Log 的协作:Debezium 的视角

理解了上面的机制,你就能明白 Debezium 为什么能捕获到完整的数据变更了。

Debezium 作为 MySQL 的 Slave,它读取的是 Binlog。而 Binlog 的写入,是在两阶段提交的 第二阶段 完成的。这意味着,只要 Debezium 读取到了 Binlog 事件,就说明该事务的 Redo Log 已经处于 prepare 状态,并且 Binlog 已经刷盘。

换句话说,Debezium 看到的数据变更,都是已经“半提交”的,不会读到未提交的脏数据。这保证了数据变更的 一致性。

关键结论:

Binlog 的写入顺序,就是事务提交的顺序。Debezium 通过读取 Binlog,就能以事务为单位,有序地捕获到数据库的所有数据变更。

好了,这一章的内容就到这里。Binlog 的写入机制和两阶段提交,是理解 MySQL 高可用和数据一致性的关键。搞懂了这些,你就能更好地理解 Debezium 是如何工作的了。