数据库的路上

MySQL 8 源码解析:InnoDB 引擎的 Redo 日志与 Binlog 写入时机与差异分析

一、引言

在 MySQL 数据库的运行机制中,Redo 日志和 Binlog 是保证数据持久性(Durability)和主从复制(Replication)的关键组成部分。理解这两种日志的写入时机和差异,对于数据库性能优化、故障恢复和复制架构设计至关重要。本文将以 MySQL 8 源码为基础,深入分析 InnoDB 引擎写入 Redo 日志和 Binlog 的时机。

二、Redo 日志与 Binlog 概述

2.1 Redo 日志(重做日志)

Redo 日志是 InnoDB 存储引擎特有的日志,用于保证事务的持久性。它记录了数据页的物理修改,确保在数据库崩溃后可以通过回放 Redo 日志来恢复数据到一致状态。Redo 日志是循环写入的,其文件大小是固定的。

2.2 Binlog(二进制日志)

Binlog 是 MySQL 服务器层的日志,用于记录数据库的逻辑更改(如 SQL 语句)。它主要用于主从复制和数据恢复。Binlog 是追加写入的,随着时间增长会产生多个文件。

三、写入时机分析

3.1 测试SQL语句

root@ test>SHOW GLOBAL VARIABLES LIKE 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit    | ON    |
+---------------+-------+
1 row in set (0.08 sec)
root@ test>show variables like 'innodb_flush_log_at_trx_commit';
+--------------------------------+-------+
| Variable_name                  | Value |
+--------------------------------+-------+
| innodb_flush_log_at_trx_commit | 1     |
+--------------------------------+-------+
1 row in set (0.09 sec)

root@ test>insert into t1 values(10,'test');

3.2 Redo 日志的写入到buffer

插入数据完成后,迷你事务提交过程中,写入 redo到日志buffer

3.3 Binlog写入到buffer

binlog写入在redo日志之后

先对binlog进行打包,然后写入到缓冲区

int THD::binlog_write_row(TABLE *table, bool is_trans, uchar const *record,
                          const unsigned char *extra_row_info) {
  // 断言当前语句已启用行格式日志,且 binlog 已打开
  assert(is_current_stmt_binlog_format_row() && mysql_bin_log.is_open());

  // 为行数据分配内存(根据表结构和记录内容)
  Row_data_memory memory(table, record);
  if (!memory.has_memory()) return HA_ERR_OUT_OF_MEM;  // 内存分配失败

  // 获取行数据缓冲区
  uchar *row_data = memory.slot(0);

  // 将记录按行格式打包(根据表结构、写入列集合等)
  size_t const len = pack_row(table, table->write_set, row_data, record,
                              enum_row_image_type::WRITE_AI);

  // 准备行事件(Write_rows_log_event)
  Rows_log_event *const ev =
      binlog_prepare_pending_rows_event<Write_rows_log_event>(
          table, server_id, len, is_trans, extra_row_info);

  if (unlikely(ev == nullptr)) return HA_ERR_OUT_OF_MEM;  // 事件创建失败

  // 将行数据写入事件缓冲区
  return ev->add_row_data(row_data, len);
}

3.3 两阶段提交:事务提交阶段

  • SQL语句执行完成后,进入到事务提交阶段

  • 两阶段提交prepare阶段 在开启binlog情况下,innodb事务提交时会使用两阶段提交方式,因为binlog也属于一个存储引擎,多个存储引擎事务,需要使用两阶段提交方式。

  • innodb 引擎prepare

  • 两阶段提交commit阶段

3.4 redo 日志刷新到磁盘

  • 写入时机 在 Binlog 刷写(Flush 阶段)前执行,确保 事务的 Redo 日志已持久化后,才将事务记录到 Binlog。这是两阶段提交(2PC)的关键步骤,避免“Binlog 已写入但 Redo 日志未持久化”导致的主从数据不一致。 ha_flush_logs(true):调用存储引擎的日志刷新接口(如 InnoDB 的 innodb_flush_log_at_trx_commit 逻辑),将内存中的 Redo 日志刷写到物理磁盘。

  • 为什么需要先刷 Redo 日志? 假设事务提交时先写 Binlog、后刷 Redo 日志,若此时数据库崩溃: Binlog 中已记录该事务(从库会复制执行),但 Redo 日志未持久化(InnoDB 崩溃恢复时会回滚未持久化的事务),导致 主从数据不一致。 通过先刷 Redo 日志、再写 Binlog,可保证: 若 Redo 日志刷盘失败,事务不会写入 Binlog,主从一致; 若 Redo 日志刷盘成功但 Binlog 写入失败,事务在存储引擎中处于“Prepared”状态,崩溃恢复时会回滚,主从一致。

  • 调用 innodb 的刷 redo 日志到磁盘的接口执行。

  • mysql8.4 版本会将刷新redo日志的动作交给后台线程 ib_log_flush 去做,当前线程选择等待。

  • debug 时暂停其他线程可以看到,当前线程底层通过POSIX 线程库中用于条件变量等待的函pthread_cond_timedwait实现线程之间的协调等待。

3.5 binlog 刷入磁盘

binlog 缓存写入到文件

  • 底层使用mysql_file_write 写入到文件
  • 最底层封装了windows和linux不同的write调用,除windows外使用POSIX标准的write接口

binlog 日志文件刷新到磁盘

*最底层封装是根据平台不同使用的刷新文件函数,默认linux使用fdatasync同步文件

四、性能优化建议

  1. 合理设置 innodb_flush_log_at_trx_commit

    • 对于性能要求极高但允许少量数据丢失的场景,可以设置为 2
    • 对于大多数生产环境,建议保持默认值 1
  2. 优化 Binlog 写入

    • 使用 sync_binlog=1 确保 Binlog 写入磁盘,保证主从复制的数据一致性
    • 对于高并发写入场景,考虑调整 binlog_group_commit_sync_delaybinlog_group_commit_sync_no_delay_count 参数

五、总结

本文以 MySQL 8 源码为基础,深入分析了 InnoDB 引擎写入 Redo 日志和 Binlog 的时机,通过源码分析和 SQL 示例,我们了解到:

  1. Redo 日志主要用于保证事务的持久性,在事务执行过程中不断写入,提交时必须刷新到磁盘。

  2. Binlog 主要用于主从复制和数据恢复,在事务提交时写入,位于 Redo 日志写入之后。

  3. MySQL 使用两阶段提交协议保证 Redo 日志和 Binlog 的一致性。

理解这些机制对于数据库管理员和开发人员优化数据库性能、设计高可用架构和处理故障恢复都具有重要意义。