一、引言
在 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同步文件

四、性能优化建议
-
合理设置
innodb_flush_log_at_trx_commit:- 对于性能要求极高但允许少量数据丢失的场景,可以设置为 2
- 对于大多数生产环境,建议保持默认值 1
-
优化 Binlog 写入:
- 使用
sync_binlog=1确保 Binlog 写入磁盘,保证主从复制的数据一致性 - 对于高并发写入场景,考虑调整
binlog_group_commit_sync_delay和binlog_group_commit_sync_no_delay_count参数
- 使用
五、总结
本文以 MySQL 8 源码为基础,深入分析了 InnoDB 引擎写入 Redo 日志和 Binlog 的时机,通过源码分析和 SQL 示例,我们了解到:
-
Redo 日志主要用于保证事务的持久性,在事务执行过程中不断写入,提交时必须刷新到磁盘。
-
Binlog 主要用于主从复制和数据恢复,在事务提交时写入,位于 Redo 日志写入之后。
-
MySQL 使用两阶段提交协议保证 Redo 日志和 Binlog 的一致性。
理解这些机制对于数据库管理员和开发人员优化数据库性能、设计高可用架构和处理故障恢复都具有重要意义。