概述

前文《SQL 优化器与执行计划:Explain 深度解读》借助 EXPLAIN 深入分析了 SQL 的执行效率与优化器的代价模型。但在实际生产环境中,即使每一条 SQL 都拥有完美的执行计划,系统的整体吞吐能力与可用性也常常受限于复制架构——主库 Binlog 的写入吞吐、从库 Relay Log 的回放速率、复制延迟引发的读写分离不一致,每一项都直指架构的命门。本文将深入主从复制的核心机制,从 Binlog 物理格式到 GTID 全局标识,从半同步的可靠性保证到并行复制的高性能回放,为你建立一套可落地的复制知识体系。

总结性引言:主从复制是 MySQL 高可用与读写分离的基石。主库通过 Binlog 将每个事务的修改事件编码为可传输的 Event,从库 I/O 线程拉取并写入 Relay Log,SQL 线程(或协调器 + Worker)负责回放。然而,异步复制存在数据丢失窗口、半同步复制可能触发超时降级、从库单线程回放会因主库的高并发写入而不断积压——这些问题的根源都深埋在复制机制的设计当中。本文从三线程模型到 GTID 自动定位,从 Binlog 物理格式选型到并行复制调优,系统地拆解主从复制的内核。

核心要点

  • 复制架构与线程模型:Binlog Dump / I/O / SQL(协调器 + Worker)的职责边界与协作流程。
  • Binlog 格式STATEMENT / ROW / MIXED 的物理存储、风险场景、一致性与日志量的权衡。
  • GTID:全局唯一标识的生成、生命周期管理与基于集合的自动故障转移。
  • 半同步复制AFTER_SYNC 等待点的源码级时序、超时降级与性能代价。
  • 并行复制:基于组提交的 LOGICAL_CLOCK 调度算法、WRITESET 的行级冲突检测与并行度调优。
  • 延迟监控Seconds_Behind_Master 的计算细节与盲区、performance_schema 精细诊断、常见延迟源的排查路径。

文章组织架构图

主从复制与GTID

1. 主从复制架构与线程模型

2. Binlog 三种格式深度对比

3. GTID:全局唯一事务标识

4. 半同步复制

5. 并行复制

6. 主从延迟监控与排查

7. 面试高频专题

架构图说明

  • 总览:全文 7 个模块从复制的基础线程模型出发,逐步深入到格式、标识、可靠性、性能加速和可观测性,最后以面试专题完成知识的闭环检验。
  • 逐模块说明:模块 1 建立复制管道的基础认知;模块 2 至模块 3 解析数据的物理表达与全局标识体系;模块 4 至模块 5 分别解决复制的“安全性”和“实时性”两大核心难题;模块 6 将理论落地到生产故障排查;模块 7 通过高频问题串联全部知识点。
  • 关键结论MySQL 复制是典型的 Binlog 生产者‑消费者模型。深入理解 Binlog 格式决策、GTID 集合协商、半同步的提交等待逻辑以及并行复制的依赖判定算法,是设计高可用数据库架构、快速定位复制故障的前提。

1. 主从复制架构与线程模型

1.1 复制的核心角色与职责边界

MySQL 主从复制由三个(或更多)线程协同完成,它们分别负责“推送”、“接收”与“回放”三个独立阶段:

  1. Binlog Dump 线程(主库)

    • 启动时机:从库 I/O 线程通过 COM_REGISTER_SLAVE 命令与主库建立复制连接后,主库为每个从库连接即时创建一个 Binlog Dump 线程。
    • 核心职责:根据从库请求的起始位点(传统模式下的 (File, Pos) 或 GTID 模式下的 GTID 集合),从主库 Binlog 文件系统中定位起始 Event,然后顺序读取 Binlog Event,通过 TCP 连接持续推送给从库。读取单位是一个完整的 Event(由 event_header 中的 event_length 决定)。
    • 生命周期:只要从库复制连接不断开,该线程一直存在。当从库主动 STOP SLAVE 或网络断开时,Binlog Dump 线程退出。
    • 内部行为:Binlog Dump 线程在发送完当前已写的 Binlog 后,会进入等待状态(通过 Binlog_sender::wait_new_events),并注册 Binlog 更新通知。一旦有新的事务提交并刷写 Binlog,该线程被唤醒并立即读取新事件推送给从库,实现准实时传输。
  2. I/O 线程(从库)

    • 启动时机:从库执行 START SLAVE 后创建。
    • 核心职责:主动连接主库,请求从指定的 Binlog 位点或 GTID 集合开始接收 Event,并将接收到的原始 Event 顺序追加写入本地的 Relay Log 文件。
    • 可靠性机制:I/O 线程在每接收到一个 Event 后,都会更新内存中的 master_info 位置(或表的对应行),并在适当的时间点将最新位点刷入 master_info 持久化存储(文件或表)。当从库意外重启后,I/O 线程可以从上次已经持久化的位置继续请求,避免数据重复或丢失。
    • 与半同步的交互:当启用半同步复制时,从库 I/O 线程在将 Event 写入 Relay Log 并刷盘后,会根据配置向主库发送 ACK 确认。因此 I/O 线程的写盘速度直接决定了半同步的响应时间。
  3. SQL 线程(从库)

    • 单线程模式:只有一个 SQL 线程,顺序读取 Relay Log 中的 Event,逐个应用。其串行化行为是复制延迟的主要来源。
    • 并行复制模式(MTS, Multi‑Threaded Slave):SQL 线程演变为 Coordinator 线程,负责解析 Relay Log 中事务的依赖关系,将无冲突的事务分发给 Worker 线程池并行回放。每个 Worker 内部依然是串行执行分配给自己的事务。
    • 进度持久化:SQL 线程(或 Coordinator)在成功应用一个事务后,更新 relay_log_info 持久化存储中的回放位点。并行复制下,relay_log_info 记录的是所有 Worker 都已完成的“低水位”位点,确保重启后不会遗漏任何事务。

1.2 Relay Log 的内部结构与持久化

Relay Log 由两个物理组件构成:

  • Relay Log 文件relay‑bin.000001 等):结构与 Binlog 完全一致,由 Format_description_event 开头,随后是连续的 Query_eventRows_event 等。区别仅在于 Event 的 server_id 是主库的 server_id,而非从库自身。
  • Relay Log 索引文件relay‑bin.index):记录当前所有的 Relay Log 文件列表,供崩溃恢复时扫描。

持久化存储的演进

  • MySQL 5.6 之前,master.info 和 relay-log.info 默认以文本文件形式存储在磁盘上。由于每次更新信息都需要执行 fopen/fwrite/fclose,不仅性能较差,且在写操作中途宕机时极易损坏。
  • 从 MySQL 5.7 开始,强烈建议使用 master_info_repository=TABLE 和 relay_log_info_repository=TABLE,将位置信息保存到 mysql.slave_master_info 与 mysql.slave_relay_log_info 系统表中。表存储依托 InnoDB 的事务特性,具有原子性,崩溃恢复后不会出现位点损坏。
  • GTID 模式下,复制位点的语义从“文件+偏移”演变为“已执行的 GTID 集合”,上述两个持久化位点的故障恢复能力被 GTID 自动协商机制进一步增强。

1.3 主从复制架构图

从库

主库

1. 写入 Binlog

2. 读取 Binlog Event

3. TCP 推送

4. 写入 Relay Log

5. 解析依赖

6. 分发事务

6. 分发事务

6. 分发事务

I/O 线程

Relay Log

Coordinator 线程

Worker 1

Worker 2

Worker N

客户端事务提交

Binlog 文件

Binlog Dump 线程

图 1‑1 主从复制架构与线程模型

图表说明

  • 架构全景:图中清晰展示了“主库 Binlog Dump 推送 → 从库 I/O 接收并持久化 → Coordinator 解析依赖并分发给 Worker 并行回放”的三段式管道。
  • 数据流向与编号:每个步骤的数字对应了数据在复制系统中的完整生命周期。这种清晰的阶段划分使得我们可以对任意一个环节进行独立的吞吐量分析和瓶颈定位。
  • 线程职责解耦:Binlog Dump 线程只关心主库本地 Binlog 的读取与推送;I/O 线程只关心网络接收与 Relay Log 写入;Coordinator/Worker 只关心事务的依赖解析与执行。三者通过 TCP 缓冲区和 Relay Log 文件实现了异步解耦,即使 SQL 回放滞后,也不会阻塞主库或 I/O 线程(除非 Relay Log 磁盘空间不足)。
  • 生产启示:若从库延迟,第一步应检查 I/O 线程是否已拉取到最新 Binlog(判断瓶颈在网络/主库推送还是 SQL 回放);第二步检查 Coordinator 是否能有效并行分发(performance_schema.replication_applier_status_by_worker)。

2. Binlog 三种格式深度对比

2.1 STATEMENT 格式

记录内容:原样保存引起数据变更的 SQL 语句文本。例如 UPDATE orders SET status='shipped' WHERE create_time < '2025-01-01' LIMIT 1000; 直接作为 Query_event 存入 Binlog。

优点

  • 日志体积极小,批量 DML 仅占用一个 Event。
  • 便于人工审计和排查——Binlog 中的 SQL 与业务日志基本一致。

风险场景与物理原因

  • 非确定性函数NOW()UUID()RAND()SYSDATE() 在主库和从库执行时返回值不同。当这类函数出现在 DML 的 WHERE 条件或赋值表达式中时,会直接导致主从行数据分叉。
  • 无 ORDER BY 的 LIMITDELETE FROM table LIMIT 10 取决于存储引擎的物理扫描顺序,主从可能删除不同的 10 行,产生难以发现的隐性不一致。
  • 触发器 / 存储过程:在 READ COMMITTED 隔离级别下,触发器中嵌套的同一条 SELECT 可能在主从库上返回不同结果,导致后续写入逻辑分叉。
  • 自增主键依赖:当 INSERT ... SELECT 中的 SELECT 返回行的顺序不确定时,auto_increment 分配的值可能不同,进而导致后续引用该自增值的外键数据错位。

不推荐理由:现代 OLTP 系统普遍使用 RC 隔离级别、各类不确定函数以及自动 ID 生成策略,STATEMENT 格式已经无法满足数据一致性的基本要求。MySQL 8.0 文档也已明确将其列为“可能导致数据不一致”的格式,仅在对日志量有极端限制且经过严格审计的非业务库中使用。

2.2 ROW 格式

记录内容:基于行的物理变更,使用 Table_map_event 描述表结构元数据,Update_rows_event / Delete_rows_event / Write_rows_event 记录每一行的前镜像与后镜像。

前后镜像的精确控制——binlog_row_image

  • FULL(默认):记录所有列的前后值。适用于需要完整审计、数据闪回或需要将 Binlog 作为 CDC 数据源的场景。
  • MINIMAL:仅记录主键列(用于定位行)以及发生变更的列。对于只有少数列被更新的典型 OLTP 场景,可比 FULL 减少 50%~80% 的日志量。
  • NOBLOB:与 FULL 类似,但若列类型为 BLOB 或 TEXT,仅在变更时才包含其值。适用于存在大字段但很少更新的表。

格式详解示例(伪逻辑): 假设表 t(id INT PK, c1 VARCHAR(20), c2 INT),执行 UPDATE t SET c1='new' WHERE id=1ROW 格式记录的 Update_rows_event 中包含:

  • 前镜像:[id=1, c1='old', c2=10]FULL)或 [id=1, c1='old']MINIMAL
  • 后镜像:[id=1, c1='new', c2=10]FULL)或 [id=1, c1='new']MINIMAL

从库应用时,根据前镜像中的主键 id=1 定位行,然后将其修改为后镜像的值。如果前镜像中的值与从库当前行不匹配,说明出现了复制冲突(如从库被意外写入),SQL 线程会报错。

优势总结

  • 物理确定性:基于行主键的定位和应用,彻底消除函数、上下文、执行计划带来的不确定性。
  • 闪回恢复:通过 mysqlbinlog -v 可解析出前镜像,反向构造 UPDATE/DELETE/INSERT 实现行级回滚。
  • 宽表友好MINIMAL 或 NOBLOB 下,日志量可控,无需担心全表更新导致的 Binlog 暴增(尽管仍会产生大量 Event)。

生产选择ROW 是 MySQL 8.0 的最佳实践,也是 GTID、WRITESET 并行复制、组复制(MGR)的基础依赖。

2.3 MIXED 格式

逻辑:默认使用 STATEMENT 记录,当优化器在解析 SQL 时检测到潜在的不安全因素(非确定函数、LIMIT 无排序、UDF、RC 隔离级别等),将当前语句的 Binlog 格式切换为 ROW

切换规则的局限性

  • 判定逻辑位于 THD::decide_logging_format 函数中,基于语法树分析和会话上下文。某些复杂嵌套结构(如动态 SQL、PREPARE 语句)的判断可能被绕过。
  • 在 RC 隔离级别下,MIXED 几乎将所有 DML 都记录为 ROW,完全失去了 STATEMENT 的日志量优势,同时引入混合格式带来的解析复杂性。
  • 混合 Binlog 要求下游所有消费工具(如 CDC、DTS)同时支持 STATEMENT 和 ROW 的解析,增加了维护成本。

适用场景:仅建议作为从 STATEMENT 向 ROW 迁移过程中的过渡状态,在新系统设计中应直接选用 ROW

2.4 Binlog 格式对比图

MIXED

安全

不安全

优化器判定是否安全

安全: STATEMENT

不安全: ROW

ROW

Row 1: 前镜像 / 后镜像

Row 2: 前镜像 / 后镜像

Row N: 前镜像 / 后镜像

STATEMENT

SQL: UPDATE t SET c1=1 WHERE id IN...

记录一条 SQL 文本

图 2‑1 Binlog 三种格式的存储差异

图表说明

  • 物理差异STATEMENT 存储逻辑操作(SQL 文本),ROW 存储物理变更(行镜像),MIXED 由优化器动态选择。
  • 日志量与安全性权衡STATEMENT 日志量最小但安全性最差;ROW 日志量最大但提供绝对一致性;MIXED 试图折中,却引入了判定逻辑的不确定性和混合格式的运维负担。
  • 镜像粒度补充ROW 的日志量可由 binlog_row_image 进一步调节,生产推荐 MINIMAL,兼顾性能与一致性。
  • 选型结论:OLTP 系统一律推荐 ROW + binlog_row_image=MINIMAL;若遗留系统暂无法变更,使用 MIXED 并尽快迁移。

2.5 格式选择决策树(扩展)

开始配置 Binlog

系统是否为 OLTP 或有
数据一致性严格需求?

binlog_format=ROW

是否仅用于归档 / 日志存储极端敏感?

是否需要完整审计或 CDC?

binlog_row_image=FULL

binlog_row_image=MINIMAL
(性能与日志量最佳平衡)

binlog_format=STATEMENT
(需严格审计所有 SQL)

binlog_format=MIXED
(过渡方案,后续必须升级)


3. GTID:全局唯一事务标识

3.1 GTID 的结构与生成时机

GTID(Global Transaction Identifier)的标准表示格式为:


GTID = server_uuid:transaction_id

  • server_uuid:MySQL 实例首次启动时生成,基于主机的 MAC 地址和时间戳通过 UUID 算法生成,存储在数据目录下的 auto.cnf 文件中。需确保在整个复制拓扑中所有实例的 server_uuid 唯一。
  • transaction_id:该实例自启动以来已提交事务的递增计数器(注意与 InnoDB 内部事务 ID 无关)。从 1 开始,每次提交严格 +1,即使事务回滚,该编号也不会被重用。

生成时机:GTID 的分配发生在事务提交的 flush 阶段(参见前文第 3 篇《事务与锁》中的两阶段提交过程),具体位于 Binlog 的 write 之前。函数 MYSQL_BIN_LOG::write_transaction 会调用 assign_automatic_gtid 为事务分配 GTID,并将 GTID_LOG_EVENT 作为事务的第一个 Event 写入 Binlog。

3.2 GTID 生命周期与持久化

GTID 从诞生到消亡经历以下几个严格阶段:

  1. 分配:事务提交时,gtid_next 被设置为 AUTOMATIC,MySQL 生成 server_uuid:seq_no
  2. 写入 BinlogGTID_LOG_EVENT 刷入 Binlog 文件。此时 GTID 加入内存中的 gtid_executed 集合。
  3. 提交完成:InnoDB 提交后,事务已持久化。若 Binlog 还未轮转,该 GTID 仅存在于内存 gtid_executed 中。
  4. 持久化到表:后台线程或 Binlog 轮转时,将内存中新增的 GTID 周期性合并写入 mysql.gtid_executed 表。该表以 (source_uuid, interval_start, interval_end) 的形式压缩存储 GTID 集合,避免对每个 GTID 存储一行。例如 GTID 集合 uuid:1-100:102:105-200 会存储为 (uuid,1,100)(uuid,102,102)(uuid,105,200) 三行。
  5. Binlog 清理:当 Binlog 文件因超过 binlog_expire_logs_seconds 或手动 PURGE BINARY LOGS 被删除时,这些文件中包含的 GTID 将从 Binlog 文件索引中消失。此时,这些 GTID 被移至 gtid_purged 集合。gtid_purged 是 gtid_executed 的子集,表示“事务已执行,但其 Binlog 已不再保留”。
  6. 生命周期结束gtid_purged 中的 GTID 仍然被系统记录,用于判断是否已执行过。只有当这些 GTID 通过特殊操作(如 RESET MASTER)被显式清除时,它们才会彻底消失。

3.3 GTID 生命周期状态图

flush 阶段

写入 Binlog

InnoDB 提交完成

周期性刷表

PURGE LOGS

RESET MASTER

事务执行中
GTID 未分配

GTID 分配
加入内存 gtid_executed

GTID_LOG_EVENT 持久化到 Binlog

事务已提交
GTID 在 gtid_executed

持久化到 mysql.gtid_executed

Binlog 文件删除

GTID 移入 gtid_purged

GTID 完全清除

图 3‑1 GTID 生命周期状态图

图表说明

  • 全生命周期:清晰展示 GTID 从分配、写入 Binlog,到刷入系统表,再到 Binlog 清理后移入 gtid_purged 的完整链路。每个阶段都有对应的元数据存储位置。
  • 关键状态转换gtid_executed 表是 GTID 的权威持久化记录,即使所有 Binlog 均被清理,实例依然知道哪些 GTID 已执行。这对于从库初始化和故障恢复至关重要。
  • gtid_purged 的双重角色:它既是已执行 GTID 的子集,也是“Binlog 已丢失”的声明。从库初始化时,必须通过 gtid_purged 知道主库上哪些 GTID 已不可重新获取。
  • 运维实践:了解生命周期有助于正确处理因 gtid_purged 设置不当导致的 ER_MASTER_FATAL_ERROR_READING_BINLOG 错误。

3.4 GTID 模式下的自动定位机制

传统位点复制要求 DBA 手动指定 MASTER_LOG_FILE 和 MASTER_LOG_POS,在故障切换时极易因计算偏差导致数据丢失或重复。GTID 模式通过 MASTER_AUTO_POSITION=1 实现了自动协商:

  1. 从库 I/O 线程发送自身的 gtid_executed 集合(SHOW VARIABLES LIKE 'gtid_executed' 的压缩形式)给主库。
  2. 主库计算自身 gtid_executed 与从库发来的集合的差集,得到从库缺失的 GTID 集合。
  3. 主库 Binlog Dump 线程扫描自身的 Binlog 索引,定位包含第一个缺失 GTID 的 Binlog 文件,并从该文件的开始位置进行传输。
  4. 由于 GTID 的全局唯一性,主库会自动跳过从库已经执行过的事务,不会重复发送。

该机制彻底消除了手工维护位点带来的操作风险,是构建自动故障切换(如 Orchestrator、MHA 切换)的基础。

3.5 GTID 关键变量与表的操作示例


-- 查看 GTID 执行状态 SHOW VARIABLES LIKE 'gtid_mode'; -- ON / ON_PERMISSIVE / OFF / OFF_PERMISSIVE SHOW VARIABLES LIKE 'enforce_gtid_consistency'; -- ON / OFF / WARN -- 查看当前实例的 GTID 集合 SELECT @@GLOBAL.gtid_executed; SELECT @@GLOBAL.gtid_purged; -- 查看 mysql.gtid_executed 表内容 SELECT * FROM mysql.gtid_executed; -- 输出示例: -- +--------------------------------------+----------------+--------------+ -- | source_uuid | interval_start | interval_end | -- +--------------------------------------+----------------+--------------+ -- | 3E11FA47-71CA-11E1-9E33-C80AA9429562 | 1 | 100 | -- | 3E11FA47-71CA-11E1-9E33-C80AA9429562 | 105 | 105 | -- +--------------------------------------+----------------+--------------+ -- 表示该实例已执行 uuid:1-100 和 uuid:105 的事务(uuid:101-104 缺失或为其他实例产生) -- 设置 gtid_purged(通常在从库初始化时) RESET MASTER; SET GLOBAL gtid_purged = '3E11FA47-71CA-11E1-9E33-C80AA9429562:1-100';

解读gtid_purged 只能在 gtid_executed 为空且无 Binlog 时设置(或使用 RESET MASTER 清空),用于告知从库“这些 GTID 对应的事务已经在主库上执行过,但其 Binlog 已不可得,请直接从后续 GTID 开始拉取”。

3.6 GTID 复制配置要点(扩展)

在搭建 GTID 复制时,除了基本的 gtid_mode=ON 和 enforce_gtid_consistency=ON,还需特别注意:

  • 主从的 server_id 必须不同,server_uuid 自动生成且不应冲突。
  • 使用 mysqldump 备份时需指定 --set-gtid-purged=ON(默认),以便备份文件在从库导入时自动设置 gtid_purged,为 MASTER_AUTO_POSITION=1 做好准备。
  • 若从库由备份恢复且 gtid_purged 已设置,CHANGE MASTER TO MASTER_AUTO_POSITION=1 后启动复制即
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐