主从复制与 GTID:半同步、并行复制

8 阅读50分钟

概述

前文《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 精细诊断、常见延迟源的排查路径。

文章组织架构图

flowchart LR
    subgraph "主从复制与GTID"
        A["1. 主从复制架构与线程模型"]
        B["2. Binlog 三种格式深度对比"]
        C["3. GTID:全局唯一事务标识"]
        D["4. 半同步复制"]
        E["5. 并行复制"]
        F["6. 主从延迟监控与排查"]
        G["7. 面试高频专题"]
    end
    A --> B --> C --> D --> E --> F --> G

架构图说明

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

1.3 主从复制架构图

flowchart LR
    subgraph 主库
        A[客户端事务提交]
        B[Binlog 文件]
        C[Binlog Dump 线程]
    end

    subgraph 从库
        D[I/O 线程]
        E[Relay Log]
        F[Coordinator 线程]
        G1[Worker 1]
        G2[Worker 2]
        G3[Worker N]
    end

    A -->|1. 写入 Binlog| B
    C -->|2. 读取 Binlog Event| B
    C -->|3. TCP 推送| D
    D -->|4. 写入 Relay Log| E
    F -->|5. 解析依赖| E
    F -->|6. 分发事务| G1
    F -->|6. 分发事务| G2
    F -->|6. 分发事务| G3

图 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 BYLIMITDELETE 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 实现行级回滚。
  • 宽表友好MINIMALNOBLOB 下,日志量可控,无需担心全表更新导致的 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)同时支持 STATEMENTROW 的解析,增加了维护成本。

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

2.4 Binlog 格式对比图

flowchart LR
    subgraph STATEMENT
        SA[SQL: UPDATE t SET c1=1 WHERE id IN...]
        SB[记录一条 SQL 文本]
    end

    subgraph ROW
        RA[Row 1: 前镜像 / 后镜像]
        RB[Row 2: 前镜像 / 后镜像]
        RC[Row N: 前镜像 / 后镜像]
    end

    subgraph MIXED
        MA{优化器判定是否安全}
        MB[安全: STATEMENT]
        MC[不安全: ROW]
    end

    MA -->|安全| MB
    MA -->|不安全| MC

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

图表说明

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

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

flowchart TD
    Start["开始配置 Binlog"]

    Start --> Q1{"系统是否为 OLTP 或有\n数据一致性严格需求?"}
    
    Q1 -- 是 --> A1["binlog_format=ROW"]
    Q1 -- 否 --> Q2{"是否仅用于归档 / 日志存储极端敏感?"}
    
    A1 --> Q3{"是否需要完整审计或 CDC?"}
    Q3 -- 是 --> A2["binlog_row_image=FULL"]
    Q3 -- 否 --> A3["binlog_row_image=MINIMAL\n(性能与日志量最佳平衡)"]
    
    Q2 -- 是 --> A4["binlog_format=STATEMENT\n(需严格审计所有 SQL)"]
    Q2 -- 否 --> A5["binlog_format=MIXED\n(过渡方案,后续必须升级)"]

    classDef decision fill:#fff4e6,stroke:#ff9800,stroke-width:2px,color:#333
    classDef action fill:#e6f7e6,stroke:#4caf50,stroke-width:2px,color:#1e4620
    classDef startNode fill:#f4f4f4,stroke:#333,stroke-width:1px

    class Start startNode
    class Q1,Q2,Q3 decision
    class A1,A2,A3,A4,A5 action

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_purgedgtid_executed 的子集,表示“事务已执行,但其 Binlog 已不再保留”。
  6. 生命周期结束gtid_purged 中的 GTID 仍然被系统记录,用于判断是否已执行过。只有当这些 GTID 通过特殊操作(如 RESET MASTER)被显式清除时,它们才会彻底消失。

3.3 GTID 生命周期状态图

flowchart 
    A[事务执行中<br>GTID 未分配] -->|flush 阶段| B[GTID 分配<br>加入内存 gtid_executed]
    B -->|写入 Binlog| C[GTID_LOG_EVENT 持久化到 Binlog]
    C -->|InnoDB 提交完成| D[事务已提交<br>GTID 在 gtid_executed]
    D -->|周期性刷表| E[持久化到 mysql.gtid_executed]
    D -->|PURGE LOGS| F[Binlog 文件删除]
    F --> G[GTID 移入 gtid_purged]
    G -->|RESET MASTER| H[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_FILEMASTER_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=ONenforce_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 后启动复制即可;若未设置,需手动将主库的 gtid_purged 赋值给从库,确保从库不会请求已经被清理的 Binlog。

4. 半同步复制

4.1 异步复制的数据丢失窗口(再强调)

默认异步复制下,主库执行 Commit 的顺序如下(简化):

InnoDB Prepare → Binlog Write & fsync → InnoDB Commit → 返回客户端 "Success"

在这一流程中,Binlog 持久化后存储引擎即提交,事务就此确认。而从库 I/O 线程可能数秒后才拉取到该事务的 Binlog。如果主库在返回客户端成功后突然宕机且不可恢复,则该事务虽然已向客户端确认,但在从库上并不存在。此时若将从库提升为主库,已确认的事务永久丢失。这是所有异步复制系统固有的数据丢失风险。

4.2 AFTER_COMMIT 与 AFTER_SYNC 的源码级时序对比

半同步插件通过介入 commit 阶段,在主库上增加一个“等待从库 ACK”的步骤。关键在于等待点(wait point)的位置:

AFTER_COMMIT(旧版等待点)

  • 等待点位于 InnoDB Commit 之后,返回客户端之前。
  • 时序:Binlog Write & fsync → InnoDB Commit → 等待从库 ACK(超时阈值) → 返回客户端
  • 致命缺陷:InnoDB 已提交,锁资源已全部释放。其他会话此时已能读取到该事务的变更。如果主库在等待 ACK 期间宕机,该事务在从库上可能尚未被接收。故障切换后,其他会话先前读到的“已提交”数据在新主库上消失,违反了数据库的持久性(Durability)定义。

AFTER_SYNC(MySQL 8.0 默认推荐)

  • 等待点位于 Binlog Write & fsync 之后,InnoDB Commit 之前。
  • 时序:Binlog Write & fsync → 等待从库 ACK → InnoDB Commit → 返回客户端
  • 安全保证:在 InnoDB 提交之前等待从库确认,意味着“如果事务最终提交,则其 Binlog 至少已存在于主库和至少一个从库的磁盘上”。主库在等待期间宕机,由于事务尚未提交,不会向客户端返回成功,故障切换后该事务在语义上从未存在过,不会出现“已确认事务丢失”。
  • 隔离性优势:等待期间,事务尚未向存储引擎提交,其他会话完全看不到该事务的数据,避免了 AFTER_COMMIT 下可能发生的幻读。

性能代价对比:两种等待点都需要付出一次网络往返的延迟,但 AFTER_SYNC 额外占用了一个事务的提交锁持有时间(等待时,事务的提交锁未释放),在高并发下可能略微增加锁竞争。不过,大多数场景下安全性提升的收益远大于这一微小代价。

4.3 半同步复制时序图(AFTER_SYNC)

sequenceDiagram
    participant Client as 客户端
    participant Master as 主库
    participant SlaveIO as 从库 I/O 线程

    Client->>Master: COMMIT
    Master->>Master: 1. Binlog Write & fsync
    Master->>SlaveIO: 2. 推送 Binlog Event
    SlaveIO-->>Master: 3. ACK (已写入 Relay Log)
    Note over Master: 等待 ACK 阶段<br>rpl_semi_sync_master_timeout
    Master->>Master: 4. InnoDB Commit
    Master->>Client: 5. OK (事务提交成功)

图 4‑1 半同步复制 AFTER_SYNC 时序图

图表说明

  • 时序与等待点:等待点精确位于 Binlog 持久化后、InnoDB 提交前。主库在该窗口内阻塞,直到从库 ACK 或超时。
  • ACK 的发送条件:从库 I/O 线程在将 Binlog Event 写入 Relay Log 并完成 fsync(由 rpl_semi_sync_slave_enabled=1sync_relay_log 控制)后,立即向主库发送 ACK。
  • 超时与降级:若等待时间超过 rpl_semi_sync_master_timeout(默认 10000ms),主库放弃等待,降级为异步复制,继续执行 InnoDB Commit 并返回客户端,以避免主库被从库拖死。
  • 性能影响量化:每个事务提交增加一个 RTT 网络延迟。同机房典型 RTT 为 0.2~0.5ms,事务吞吐量大约下降 20%~40%;跨机房 RTT 30ms 以上时,半同步将导致吞吐量急剧下降,不推荐使用。

4.4 半同步插件的安装、监控与故障排查(扩展)

-- 安装半同步插件
INSTALL PLUGIN rpl_semi_sync_master SONAME 'semisync_master.so';
INSTALL PLUGIN rpl_semi_sync_slave SONAME 'semisync_slave.so';

-- 启用(写入 my.cnf 保证重启后生效)
SET GLOBAL rpl_semi_sync_master_enabled = 1;
SET GLOBAL rpl_semi_sync_slave_enabled = 1;
SET GLOBAL rpl_semi_sync_master_timeout = 1000; -- 建议 1 秒

-- 从库重启 I/O 线程以生效
STOP SLAVE IO_THREAD;
START SLAVE IO_THREAD;

-- 监控主库端状态
SHOW STATUS LIKE 'Rpl_semi_sync_master%';
-- 重点关注:
-- Rpl_semi_sync_master_status       :ON 表示当前在半同步模式,OFF 表示降级
-- Rpl_semi_sync_master_yes_tx       :成功收到 ACK 的事务数
-- Rpl_semi_sync_master_no_tx        :未收到 ACK 的事务数(no_tx / (yes_tx+no_tx) 为降级比例)
-- Rpl_semi_sync_master_timeouts     :超时降级次数,持续增长需告警
-- Rpl_semi_sync_master_tx_avg_wait_time :平均等待时间(微秒),反映网络延迟

故障排查:如果 Rpl_semi_sync_master_status 频繁在 ON/OFF 间切换,或 timeouts 持续增长,应检查:

  • 从库 I/O 线程是否正常运行。
  • 从库 Relay Log 磁盘写入性能是否成为瓶颈(iostat 查看磁盘延迟)。
  • 主从网络延迟和丢包率。

5. 并行复制

5.1 单 SQL 线程的局限性

在 MySQL 5.6 之前,从库只有一个 SQL 线程串行应用事务。主库多核并发提交时,Binlog 中事务的记录顺序是交错的,但从库只能逐个执行,回放速度远低于主库写入速度。这种情况在主库 TPS 超过数千时尤为明显——从库 Relay Log 越积越多,延迟不断拉大。并行复制的核心目标就是打破这种串行瓶颈。

5.2 基于组提交的 LOGICAL_CLOCK 调度原理

MySQL 5.7 起引入 LOGICAL_CLOCK 调度器,其理论基础是:在主库上进行组提交的事务之间不存在锁冲突,因此它们在从库上也可以并行回放。

主库生成依赖标记: 每个事务在写入 Binlog 时,被赋予两个关键的整数标记:

  • sequence_number:全局递增的事务序列号。每提交一个事务,该值加 1,写入 Binlog 的 Gtid_log_eventsequence_number 字段。
  • last_committed:当前事务开始提交时,上一个已完成组提交的 leader 事务的 sequence_number。其含义是:所有 sequence_number <= last_committed 的事务已经全部提交完毕,当前事务不会与它们存在锁冲突。last_committed 也记录在 Binlog Event 的公共头部。

组提交窗口的标记行为: 在组提交过程中,MySQL 会将一批并发到达 flush 阶段的事务集中刷写 Binlog。对于这一批事务,它们的 last_committed 全部设置为该组第一个事务的 sequence_number - 1。因此,同一组内所有事务的 last_committed 相等,且小于它们各自的 sequence_number。这意味着它们之间没有依赖,可以并行回放。

示例

事务sequence_numberlast_committed
T110099A
T210199A
T310299A
T4103102B
  • T1, T2, T3 属于同一组(组 A),last_committed 均为 99。从库 Coordinator 看到后,判断它们之间无依赖,可并行执行。
  • T4 的 last_committed=102,需要等待 sequence_number=102(即 T3)执行完毕后方可开始。因此 T4 与 T3 之间存在依赖(T4 必须等 T3 完成)。

从库调度算法: Coordinator 线程维护一个 commit_order_queue(提交顺序队列),Worker 线程从队列中领取无依赖的事务执行。调度逻辑的核心伪代码如下:

if 当前事务.last_committed <= last_executed_sequence_number:
    该事务可立即分发给空闲 Worker
else:
    该事务必须等待,直到 last_executed_sequence_number >=  last_committed

5.3 WRITESET 冲突检测——打破组提交边界的更细粒度并行

COMMIT_ORDER 的局限:并行度完全取决于主库的组提交密度。若主库业务模型为大量小事务但彼此不冲突,由于组提交窗口长度有限,它们可能被标记在不同的组中,从而在从库上被视为有依赖,无法并行。

WRITESET 的工作原理

  1. 提取 writeset:主库在事务提交时,提取该事务所修改的所有行的主键或非空唯一键,对每个键值计算哈希(XXHASH64),生成一个 writeset 集合。
  2. 冲突检测:MySQL 维护一个全局的 writeset 历史记录(大小由 binlog_transaction_dependency_history_size 控制)。当前事务提交时,将其 writeset 与历史记录中近期事务的 writeset 进行交集比对:
    • 若无交集,该事务与历史中的事务无实际冲突,将其 last_committed 设置为与历史中最旧的无冲突事务的 sequence_number,从而大幅降低依赖。
    • 若有交集,则按照 COMMIT_ORDER 的规则设定 last_committed
  3. 效果WRITESET 突破了组提交窗口的物理限制,使逻辑上无冲突但物理上不在同一组提交的事务也能被标记为可并行,并行度可提升数倍。

生效条件(必须全部满足)

  • binlog_format=ROW(需要精确的行级修改信息)
  • transaction_write_set_extraction=XXHASH64(开启 writeset 提取)
  • 被修改的表必须具有主键或非空唯一键,否则 writeset 退化为 COMMIT_ORDER

5.4 并行复制配置与 Worker 状态监控

-- 从库配置(my.cnf)
slave_parallel_type = LOGICAL_CLOCK
slave_parallel_workers = 8          -- 建议核数的 1~2 倍,最大不超过 16
slave_preserve_commit_order = ON   -- 保证从库提交顺序与主库一致,避免 gap lock 问题

-- 主库配置(开启 WRITESET)
binlog_transaction_dependency_tracking = WRITESET
transaction_write_set_extraction = XXHASH64
binlog_format = ROW

-- 查看 Worker 执行状态(精细到每个线程)
SELECT
    worker_id,
    thread_id,
    service_state AS state,
    last_applied_transaction AS last_gtid,
    applying_transaction AS current_gtid,
    TIMESTAMPDIFF(MICROSECOND, 
        applying_transaction_start_apply_time, 
        NOW()) / 1000000 AS exec_seconds
FROM performance_schema.replication_applier_status_by_worker;

-- 若发现某 Worker 的 exec_seconds 显著大于其他 Worker,说明该 Worker 可能分配到
-- 大事务或正在等待锁,需进一步通过 SHOW PROCESSLIST 或 innodb_lock_waits 排查。

5.5 并行复制调度依赖示意图(新增)

flowchart LR
    subgraph Binlog 事务序列
        T1[T1 seq=100 lc=99]
        T2[T2 seq=101 lc=99]
        T3[T3 seq=102 lc=99]
        T4[T4 seq=103 lc=102]
    end

    subgraph 依赖关系
        T1 & T2 & T3 -->|无依赖, 可并行| Workers
        T4 -->|依赖 T3 完成| Worker_X
    end

图 5‑1 LOGICAL_CLOCK 依赖判定示意图

图表说明

  • 依赖判定:T1、T2、T3 的 last_committed 均为 99,小于彼此最小的 seq,因此三者之间无依赖,可被 Coordinator 同时分发给不同的 Worker 执行。T4 的 last_committed=102,必须等待 seq=102 的 T3 完成才能执行。
  • 并行度来源:组提交窗口的大小决定了同一组内可并行的事务数量。WRITESET 则通过更精确的冲突检测,将“假性依赖”进一步消解。
  • slave_preserve_commit_order 的作用:即使 T1、T2、T3 并行执行完成,Coordinator 也会确保它们按照原始的 seq 顺序(100→101→102)向存储引擎提交,从而避免因提交乱序导致的间隙锁冲突。

6. 主从延迟监控与排查

6.1 Seconds_Behind_Master 的深度解读

该字段是 SHOW SLAVE STATUS 中最常用的延迟指标,但常被误读:

计算逻辑

  • 当前 SQL 线程正在处理的 Relay Log Event 的头部包含一个 timestamp,该时间戳由主库在写入 Binlog 时记录。
  • Seconds_Behind_Master = 当前从库系统时间 - Event.timestamp

三大盲区

  1. I/O 延迟盲区:该值仅反映 SQL 线程与 Relay Log 中最后一个 Event 的时间差。如果 I/O 线程因网络问题还未来得及拉取主库最新的 Binlog,则 Seconds_Behind_Master 会稳定在 0,但实际上从库与主库存在数秒甚至更长的数据滞后。
  2. 大事务盲区:当 SQL 线程正在执行一个巨大的事务(如耗时 300s 的批量 UPDATE),在回放的整个过程中,Relay Log 的读取指针停在该大事务的开始位置。此时 Seconds_Behind_Master 计算的是该大事务在主库的提交时间与当前从库时间的差值,会随着时间流逝而线性增长,但并不反映大事务还需要多长时间才能执行完。
  3. NULL 盲区:当 SQL 线程未运行、或刚启动还未读取到任何 Event 时,该值为 NULL,常被误判为“完全同步”。

6.2 基于 GTID 的精确延迟量化

更精确的方法是直接比较主从的 gtid_executed 集合:

-- 主库
SELECT @@GLOBAL.gtid_executed;
-- 从库
SELECT @@GLOBAL.gtid_executed;
-- 计算差集即为尚未同步的事务 GTID 列表

也可配合 performance_schema 中的时间戳字段精确计算每个事务的端到端延迟:

SELECT
    worker_id,
    last_applied_transaction,
    last_applied_transaction_end_apply_time,
    last_applied_transaction_original_commit_timestamp,
    TIMESTAMPDIFF(MICROSECOND,
        last_applied_transaction_original_commit_timestamp,
        last_applied_transaction_end_apply_time) / 1000000 AS total_delay_sec
FROM performance_schema.replication_applier_status_by_worker;

其中 original_commit_timestamp 来自主库提交事务时写入 Binlog 的 original_commit_timestamp 字段(微秒精度),end_apply_time 是从库应用完毕的时间,二者之差为事务从主库提交到从库执行完毕的精确端到端延迟。

6.3 常见延迟原因与排查路径(表格详化)

延迟原因现象诊断命令 / 方法解决方案
主库大事务从库延迟突然飙升,Seconds_Behind_Master 持续增大,主库慢查询日志中出现扫描千万行的 DMLSHOW PROCESSLIST 在主库查找长事务;mysqlbinlog 解析最新的 Binlog 检查大 Event拆分大事务为每批 1000~5000 行;业务低峰执行
从库硬件瓶颈CPU 使用率长期 > 80%,IO 利用率 > 90%,SQL 线程被资源限制top, iostat, SHOW ENGINE INNODB STATUS 检查信号量等待升级从库规格;扩容 innodb_buffer_pool_size 减少磁盘读
从库读负载过高从库承担大量读流量,与 SQL 线程争抢 CPU 和 IOSHOW PROCESSLIST 查看大量用户读查询;slow_query_log 分析增加从库节点做读负载均衡;使用缓存层分担读压
无主键表ROW 格式下,从库对无主键表的每行变更都会引发全表扫描SELECT * FROM information_schema.tables WHERE table_schema='xxx' AND table_name NOT IN (SELECT DISTINCT table_name FROM information_schema.table_constraints WHERE constraint_type='PRIMARY KEY')为所有表添加显式主键(哪怕是自增 ID)
并行复制配置不当Worker 线程空闲,但 Coordinator 分发缓慢performance_schema.replication_applier_status_by_worker 确认 worker 状态调整 slave_parallel_workers 数量(4~8);开启 WRITESET 降低假性依赖
DDL 阻塞Relay Log 中遇到 ALTER TABLE,SQL 线程被 DDL 长时间占用,后续 DML 全部堆积SHOW SLAVE STATUSRelay_Log_Pos 长时间不变使用 pt-oscgh-ost 在主库在线执行 DDL;对于从库阻塞,可临时跳过大事务或等待 DDL 完成

7. 面试高频专题

(以下每道题均独立成块,结合正文深度展开)

Q1:MySQL 主从复制的三个线程分别是什么?各自的职责和内部关键行为有哪些?

一句话回答:主库 Binlog Dump 线程负责推送 Binlog Event,从库 I/O 线程负责接收并写入 Relay Log,从库 SQL 线程(或 Coordinator + Worker)负责回放事务。

详细解释

  • Binlog Dump 线程:主库侧,根据从库请求的位点或 GTID 集合,顺序读取本地 Binlog 文件,将每个完整的 Event 通过 TCP 发送。该线程通过注册 Binlog 更新通知实现准实时推送,并在无新 Event 时 sleep 等待。
  • I/O 线程:从库侧,连接主库后,将接收到的 Event 顺序写入 Relay Log 文件,并周期性持久化 master_info(位点或 GTID 集合)。在半同步复制中,它还负责在写入 Relay Log 后立即向主库发送 ACK。
  • SQL 线程:单线程模式下,串行读取 Relay Log Event 并执行。MTS 模式下,转为 Coordinator,解析每个事务的 last_committedsequence_number,将无冲突事务分发给 Worker 线程池并行回放。

多角度追问

  1. 为什么需要将拉取和回放分离? —— 解耦网络传输与 SQL 执行,避免因一个慢 SQL 回放堵塞 Binlog 的接收,进而导致主库 Binlog 积压或网络缓冲溢出。
  2. Relay Log 与 Binlog 在物理格式上有什么异同? —— 格式完全相同,均由 MySQL Binlog Event 构成。区别在于 server_id 保留为主库的 server_id,且 Relay Log 有专门的 relay-log.info 记录回放进度。
  3. I/O 线程是如何知道从哪里开始接收的? —— 传统模式从 master.info 记录的 (File, Pos) 开始;GTID 模式通过发送 gtid_executed 集合与主库协商缺失的 GTID 集合自动确定起始位置。
  4. 如果从库重启,SQL 线程如何知道从 Relay Log 的哪里继续? —— 依据 relay-log.info 记录的位点。GTID 模式下,即使 relay-log.info 丢失,从库也可以通过 gtid_executed 集合与主库重新协商,并从 Relay Log 中解析 GTID 实现更可靠的恢复。

加分回答:在 MySQL 8.0 中,slave_preserve_commit_order=ON 使得 Coordinator 在 Worker 并行执行的基础上,保障最终提交顺序与主库一致。这需要 Coordinator 维护一个“提交队列”,某个事务必须在所有 seq 小于它的前置事务全部提交后才能提交,从而避免 Gap Lock 引发的死锁。


Q2:Binlog 的三种格式分别是什么?为什么 ROW 格式是生产环境的首选?

一句话回答STATEMENT 记录 SQL 文本,ROW 记录每行的前后镜像,MIXED 自动切换。ROW 以物理确定性彻底消除不一致风险,是生产第一选择。

详细解释

  • STATEMENT:日志量小但安全性差,非确定性函数、无排序 LIMIT、RC 隔离级别下均可能产生主从不一致。
  • ROW:基于主键定位行,物理应用变更,从库结果与主库绝对一致。支持闪回恢复、CDC 和 WRITESET 并行复制。
  • MIXED:默认 STATEMENT,优化器判定不安全时自动切换 ROW。因判定逻辑存在盲区且混合格式增加运维复杂度,已不推荐。

多角度追问

  1. ROW 格式的日志量太大怎么办? —— 使用 binlog_row_image=MINIMAL,只记录主键和变更列,通常可减少 50% 以上的日志量。对存储空间和网络传输都有明显优化。
  2. STATEMENT 格式下,NOW() 为什么会导致不一致? —— 因为 NOW() 在主库和从库的执行时间点不同,返回的时间戳不同。如果这个值被写入数据表,则主从数据产生永久分叉。
  3. MIXED 在 RC 隔离级别下实际行为是什么? —— RC 级别下,MySQL 认为 STATEMENT 格式天生不安全(因为锁定读取的语句会受其他并发提交的影响,导致主从执行路径不同),所以几乎所有 DML 都会自动切换为 ROW 记录。此时 MIXED 已名存实亡。
  4. 如何将一个使用 STATEMENT 格式的老系统平滑迁移到 ROW? —— 先将会话级 binlog_format 改为 MIXED 测试运行,确认无问题后再全局变更为 MIXED,最后在业务低峰期直接切换到 ROW,同时监控从库延迟和日志增长。

加分回答:对于需要构建实时数据管道(如 Canal、Maxwell 监听 Binlog)的系统,ROW 格式提供了完整的前后镜像,使得下游可以轻松获取到变更前后的所有数据,无需解析 SQL 反推变化,这是 STATEMENT 无法做到的。


Q3:GTID 是什么?它如何简化故障切换?其内部存储结构是怎样的?

一句话回答:GTID 是 server_uuid:transaction_id 组成的全局唯一事务标识。它通过集合差集计算,让从库自动定位缺失事务,完全免去手动位点维护。

详细解释

  • GTID 由实例的 server_uuid 和事务在该实例上的递增编号组成,保证在复制拓扑中全局唯一。
  • 故障切换时,从库连接新主库并发送自己的 gtid_executed 集合。新主库计算差集,将缺失的事务从 Binlog 中补发。避免了传统模式下手动找位点可能导致的“数据丢失”或“重复应用”两大风险。
  • mysql.gtid_executed 表以区间压缩方式存储 GTID,提高存储效率。gtid_purged 标记已清理 Binlog 的 GTID,避免新从库请求不存在的事务。

多角度追问

  1. GTID 模式下如何跳过错误事务? —— 不能使用 SQL_SLAVE_SKIP_COUNTER。必须通过设置 gtid_next 为要跳过的 GTID,然后执行 BEGIN; COMMIT; 注入一个空事务,将其加入 gtid_executed,然后重启复制即可。
  2. gtid_purged 设置错误会有什么后果? —— 如果设置得比实际已执行的 GTID 少,主库可能会重复发送已应用的事务,导致主键冲突或数据错乱;如果设置得比实际多,则从库会认为某些已执行的事务无需执行,造成数据空洞。因此设置前必须用 RESET MASTER 清空现有 GTID 集合。
  3. GTID 的 transaction_id 达到最大值怎么办? —— 它是 64 位整数,即使每秒提交 10 万事务,也需要数百万年才会用完。MySQL 8.0 引入了 64 位 seq_no,理论上永不耗尽。
  4. GTID 模式对日常运维的 DDL 有什么影响? —— 会强制禁止 CREATE TABLE ... SELECT 等混合 DDL/DML 语句,要求 DDL 和 DML 分离,这反过来也促进了更安全的变更习惯。

加分回答:GTID 的集合协商机制是 MGR(组复制)的核心基础。MGR 通过 Paxos 协议对 GTID 进行全局排序与冲突检测,可以说,没有 GTID 就没有 MySQL 的分布式高可用集群。


Q4:半同步复制与异步复制的核心区别是什么?AFTER_SYNC 如何保证数据不丢失?

一句话回答:异步复制主库提交后不等待从库,存在数据丢失窗口;半同步复制在 Binlog 刷盘后、InnoDB 提交前等待从库 ACK,确保提交的事务至少在两个节点上存在 Binlog。

详细解释

  • 异步复制:Binlog fsync → InnoDB Commit → 返回成功,主库若在返回成功后宕机,则已确认事务在从库上可能不存在。
  • AFTER_SYNCBinlog fsync → 等待从库 ACK → InnoDB Commit → 返回成功。在 InnoDB 提交前,该事务的 Binlog 已复制到从库并落盘。如果主库在等待 ACK 期间宕机,由于事务尚未提交,不会向客户端返回成功,切换后无丢失。

多角度追问

  1. AFTER_COMMIT 有什么致命问题? —— 等待点位于 InnoDB Commit 之后,其他会话可能已经读取到该事务的结果并据此做了外部响应(如发送短信)。主库宕机后,从库并无该事务,出现“已读未存”的严重数据一致性事故。
  2. 半同步复制对性能的影响有多大? —— 每个事务增加一个 RTT 网络延迟。同机房 0.3ms 延迟下,高并发 OLTP 的 TPS 通常会下降 30% 左右。跨机房 RTT 50ms 则吞吐量可能降至单机 QPS 的 1/50,几乎不可用。
  3. 超时降级后如何快速恢复? —— 当从库重新连接并确认后,主库的 Rpl_semi_sync_master_status 自动恢复为 ON,无需人工干预。但降级期间的已提交事务可能已经只存在于主库,因此需要配合监控(Rpl_semi_sync_master_no_tx)及时告警。
  4. 是否可以将超时设置为无穷大,强制等待? —— 虽然可以设置极大值,但强烈不推荐。一旦所有从库故障,主库将完全挂起,等同于分布式系统中的“缺乏可用性”。因此必须保留超时降级作为可用性的保险。

加分回答AFTER_SYNC 的实现利用了 MySQL 两阶段提交的 flushcommit 之间的空隙。将等待 ACK 插入到 sync 之后、commit 之前,是 MySQL 复制团队经过大量论证后找到的完美安全切入点,既保证了持久性又避免了隔离性异常。


Q5:并行复制的原理是什么?LOGICAL_CLOCKWRITESET 分别是如何决定事务依赖的?

一句话回答:并行复制利用主库组提交事务间的无锁特性,通过 Binlog 中的 last_committedsequence_number 判定依赖,将无冲突事务分配给多个 Worker 并行回放。WRITESET 进一步通过行级哈希检测,打破组提交窗口的假性依赖。

详细解释

  • LOGICAL_CLOCK:主库组提交时,同一批事务共享相同的 last_committed,在从库上被视为可并行。依赖判断条件为:如果事务的 last_committed 小于已经完成的最高 sequence_number,则可分派。
  • WRITESET:在主库提交时提取行修改的 writeset 哈希,与近期事务的 writeset 比对。如果无交集,则将 last_committed 改写为更早的值,从而消除逻辑依赖,大幅提升并行度。

多角度追问

  1. 为什么不直接让所有事务都并行? —— 因为有些事务修改了相同的行,有先后依赖。如果不加区分地并行,会导致数据错乱(例如后提交的事务覆盖了先提交事务的修改)。因此必须通过 last_committed 或 writeset 来保证冲突事务的顺序。
  2. WRITESET 对表有什么要求?为什么必须要有主键? —— writeset 的提取依赖行标识的哈希。如果没有主键或非空唯一键,MySQL 无法唯一标识一行数据,也就无法构建 writeset,只能退化为 COMMIT_ORDER
  3. slave_preserve_commit_order 为什么在并行复制中很重要? —— 并行回放的事务可能乱序提交。如果一个读查询在从库上通过间隙锁扫描,可能因提交顺序与主库不同而读到不一致的数据甚至发生死锁。开启该参数后,Coordinator 保证提交顺序与 Binlog 一致,解决了此问题。
  4. 如何衡量并行复制的效果? —— 观察 performance_schema.replication_applier_status_by_worker,如果各 Worker 的 applying_transaction 进度均匀推进,说明并行度良好;如果只有一个 Worker 忙碌其余空闲,说明存在严重的依赖链或大事务,并行度失效。

加分回答binlog_transaction_dependency_history_size 控制 writeset 历史记录的大小。默认 25000 个事务。增大该值可以让冲突检测参考更长时间窗口的历史,进一步提升并行度,但会增加内存占用。对于大并发且冲突极少的场景,可以适当加大。


Q6:Seconds_Behind_Master 为 0 是否代表完全同步?为什么?

一句话回答:不是。它只反映 SQL 线程与 Relay Log 中最后一个 Event 的时间差,无法体现 I/O 线程的延迟,在大事务执行期间也会虚高。

详细解释:该值 = 当前从库时间 - SQL 线程正在处理的 Event 的时间戳。如果 I/O 线程因网络问题未拉取到新 Event,Relay Log 中最后一个 Event 的时间戳可能已经是数秒之前,SQL 线程处理完该 Event 后,差值显示为 0,但实际从库与主库存在数据滞后。此外,当 SQL 线程正在执行长事务时,该值会随现实时间线性增长,但不反映事务还要多久完成。

多角度追问

  1. 什么情况下 Seconds_Behind_Master 为 NULL? —— SQL 线程未运行、I/O 线程未连接、刚启动复制还未读取任何 Event、或 Coordinator 未运行时。
  2. 如何更精确地监控主从延迟? —— 推荐使用 pt-heartbeat 工具在主库上定期写入时间戳行,从库计算当前时间与该行的时间差,可精确到毫秒且不受 Event 时间戳滞后影响。也可对比主从 gtid_executed 集合差。
  3. 在并行复制场景下,这个值是怎么计算的? —— 它基于最慢的 Worker 当前所执行事务的 Event 时间戳。因此即使大多数 Worker 已完成,有一个 Worker 缓慢,该值依然偏高。
  4. 有没有其他监控表可以更细粒度查看延迟? —— performance_schema.replication_applier_status_by_worker 中的 last_applied_transaction_original_commit_timestampend_apply_time 可以给出每个事务的精确延迟,适合定位慢 Worker。

加分回答:许多监控平台默认只展示 Seconds_Behind_Master,这会产生“延迟为 0 就无风险”的假象。生产环境下务必配合 pt-heartbeat 或 GTID 差集进行双指标监控,构建主从延迟的真值视图。


Q7:主从延迟突然增大,可能的原因有哪些?如何构建排查路径?

一句话回答:常见原因包括主库大事务、从库硬件不足、从库读负载过高、无主键表、并行复制失效、DDL 阻塞。排查应遵循“定位瓶颈环节 → 分析根因事务/资源 → 靶向处理”的路径。

详细解释

  1. 定位瓶颈环节:先确认是 I/O 线程滞后(对比主从 GTID 差集与 Retrieved_Gtid_Set)还是 SQL 线程滞后(Retrieved_Gtid_Set 接近主库但 Executed_Gtid_Set 落后)。若是后者,查看 replication_applier_status_by_worker 找出执行时间最长的 Worker。
  2. 分析根因事务:如果是 Worker 某一大事务长时间执行,可关联 SHOW PROCESSLIST 查看其 SQL;如果是锁等待,检查 innodb_lock_waits;如果是表无主键,通过 information_schema 查找。
  3. 检查资源与配置topiostat 查看 CPU/IO;检查 slave_parallel_workersWRITESET 是否生效;检查从库慢查询日志是否有很多长查询与 SQL 线程争抢资源。

多角度追问

  1. 如何判断是不是大事务导致的? —— 在主库侧,SELECT * FROM information_schema.innodb_trx WHERE trx_started < NOW() - INTERVAL 30 SECOND; 查找长时间未提交的事务。同时从库 replication_applier_status_by_worker 中会有单个 Worker 的执行时间远远大于其他 Worker。
  2. 如果所有 Worker 都在忙碌且等同一个锁,说明什么? —— 说明 Relay Log 中遇到了 DDL 语句(如 ALTER TABLE),它需要获取表级排他锁,导致后续所有 DML 事务全部阻塞。只能等待 DDL 执行完,或使用 pt-osc 在主库规避。
  3. 并行复制明明开了,延迟却还很大,是什么原因? —— 可能是由于 slave_parallel_workers 过小,或者 COMMIT_ORDER 下并行度不足。检查 performance_schema.replication_applier_status_by_worker 中各 Worker 是否均匀忙碌。若是假性依赖多,可开启 WRITESET
  4. 从库硬件资源还很空闲,为什么仍然延迟? —— 可能是单线程模式下遇到 I/O 延迟瓶颈(如磁盘 await 高),或者网络延迟导致 I/O 线程拉取不及时。需要逐层排查。

加分回答:应急处理时,如果确认是某个大事务导致延迟,且业务允许临时跳过,可以在从库使用 STOP SLAVE SQL_THREAD,然后通过 START SLAVE UNTIL SQL_AFTER_GTIDS='跳过的事务GTID' 略过该事务。但务必备份并评估数据影响,跳过的事务需要通过其他手段补偿数据。


Q8:如何从传统位点复制平滑迁移到 GTID 复制?

一句话回答:通过在线的四步法将 gtid_modeOFF → OFF_PERMISSIVE → ON_PERMISSIVE → ON,全程无需停机(但需暂时禁止 DDL),所有节点完成切换后启用 MASTER_AUTO_POSITION=1

详细解释

  1. OFF → OFF_PERMISSIVE:新事务产生 GTID,但允许复制匿名事务。此时所有从库依然可以正常复制。
  2. OFF_PERMISSIVE → ON_PERMISSIVE:新事务必须为 GTID,但从库仍可接收匿名事务。需等待 Ongoing_anonymous_transaction_count 状态为零,确保所有异步复制的匿名事务都已执行完毕。
  3. ON_PERMISSIVE → ON:所有事务均为 GTID,复制也必须使用 GTID。切换完毕,从库可使用 MASTER_AUTO_POSITION=1
  4. 切换顺序:先切所有从库,最后切主库。每一步切换后,观察 SHOW SLAVE STATUSAuto_Position 和 GTID 状态,确保无误后再进行下一步。

多角度追问

  1. 迁移过程中为什么要暂时禁止 DDL? —— DDL 会隐式提交,容易在中间状态产生匿名事务与 GTID 事务交叉,导致 Ongoing_anonymous_transaction_count 难以归零。
  2. 如果拓扑中有中间从库(级联复制)怎么处理? —— 需要从最底层的从库开始,逐级向上切换。每个中间节点作为其下游的主库,必须在上游切换到 GTID 后,自己也要切换到相应的模式。
  3. 迁移完成后,还需保留 master_info 文件吗? —— 可以保留作为冗余,但 GTID 模式下自动定位不再依赖它。如果使用 master_info_repository=TABLE,可以直接清除不再需要。
  4. 如果在迁移过程中发生故障怎么办? —— 每个模式都允许旧格式的事务继续执行,因此可以安全回退到上一步(需确保未超过临界点)。关键是在每个步骤确认 Ongoing_anonymous_transaction_count=0

加分回答:MySQL 8.0.23 后,对于 MGR 环境,支持更简洁的在线迁移。但在经典异步/半同步复制中,上述四步法仍然是官方推荐且经过大规模验证的稳妥路径。


Q9:为什么 GTID 模式下 CREATE TABLE ... SELECT 被禁止?

一句话回答:该语句是一个原子操作但内部会生成两个独立事务(DDL + DML),GTID 无法为其分配一个唯一的全局事务标识,违反了 GTID 一致性原则。

详细解释

  • 在 MySQL 内部,CREATE TABLE ... SELECT 实际执行了两个动作:① 创建一个新表(DDL,隐式提交之前的事务,开启新事务并快速提交);② 将 SELECT 的结果逐行插入新表(DML,形成另一个事务)。
  • 但这是作为一条 SQL 原子语句提交给用户的,用户认为它是一个事务。GTID 要求“每个事务必须有一个 GTID”,但这一个原子语句却产生两个 GTID,导致无法映射。
  • 因此 enforce_gtid_consistency=ON 时,MySQL 直接拒绝执行该语句并报错 ERROR 1786 (HY000): CREATE TABLE ... SELECT is forbidden when @@GLOBAL.ENFORCE_GTID_CONSISTENCY = 1.

多角度追问

  1. 替代方案是什么? —— 拆分为 CREATE TABLE LIKE(或手动定义结构)+ INSERT INTO ... SELECT。两个独立语句会被分配各自的 GTID,完全兼容。
  2. 除了这个语句,GTID 还禁止哪些操作? —— 事务内部使用 CREATE TEMPORARY TABLEDROP TEMPORARY TABLE(临时表操作不写 Binlog,无法复制);事务内同时更新事务表(InnoDB)和非事务表(MyISAM)。
  3. enforce_gtid_consistency 设置为 WARN 的作用是什么? —— 允许执行这些危险语句,但会在错误日志中写入警告。用于迁移期间捕捉应用代码中潜在的违规 SQL。
  4. 为什么 Binlog 不能为这种混合语句设计专门的 GTID 映射? —— 因为这会在 Binlog 中引入非原子性的记录,严重破坏复制状态机的简洁性,导致下游所有依赖 Binlog 的系统(如 CDC、备份恢复)都需要识别并处理这种特殊记录,得不偿失。

加分回答:这个限制实际上推动了将 DDL 和 DML 严格分离的良好工程习惯。许多企业也在这一限制下建立了“禁止在业务代码中执行 DDL”的规范,所有表结构变更统一由 DBA 工具执行,进一步保障了生产安全。


Q10(故障排查题):线上一个从库 Seconds_Behind_Master 持续增大,Slave_SQL_Running=YesSlave_IO_Running=Yes,但 performance_schema 显示 Worker 线程均处于忙碌状态,如何分析并解决?

一句话回答:Worker 全部忙碌表示 SQL 回放能力已打满但依然跟不上写入速度,大概率是大事务、无主键表导致的全表扫描或并行度不足。需定位最慢 Worker、检查锁等待和表结构,从根因 SQL 或并行配置入手解决。

详细排查步骤

  1. 区分 I/O 延迟与 SQL 延迟:对比主库 @@GLOBAL.gtid_executed 与从库 Retrieved_Gtid_SetExecuted_Gtid_Set。若 Retrieved_Gtid_Set 接近主库但 Executed_Gtid_Set 滞后,则是纯 SQL 回放慢;若 Retrieved_Gtid_Set 滞后,则 I/O 也有问题。
  2. 定位最慢 Worker:查询 performance_schema.replication_applier_status_by_worker,按 applying_transaction_start_apply_time 排序,找出 exec_seconds 最大的 Worker,记录其 current_gtid
  3. 检查锁等待:查询 information_schema.innodb_lock_waits,观察最慢 Worker 的线程是否在等待其他线程释放锁(可能是另一个 Worker 或用户读查询)。
  4. 分析根因表与事务:使用 mysqlbinlog 解析当前 GTID 对应的 Binlog 片段,获取该事务的具体 SQL。检查涉及的表是否有主键、是否为大范围扫描或大规模 DML。
  5. 检查并行配置:确认 slave_parallel_type=LOGICAL_CLOCKslave_parallel_workers 是否合理,binlog_transaction_dependency_tracking 是否为 WRITESET,以及表是否有主键(无主键则 WRITESET 降级)。
  6. 检查从库资源与负载top 观察 CPU 与 IO 是否饱和。若有大量用户读查询在从库运行,可能需要限流或扩展从库。

多角度追问

  1. 如果确认为大事务导致一个 Worker 缓慢,其他 Worker 空闲,如何应急? —— 首先评估业务影响,若可接受跳过大事务,执行 STOP SLAVE SQL_THREAD; SET gtid_next='<problem_gtid>'; BEGIN; COMMIT; SET gtid_next='AUTOMATIC'; START SLAVE SQL_THREAD; 注入空事务跳过。之后务必进行数据补偿。
  2. 如果所有 Worker 均匀忙碌,延迟仍持续增大,是什么原因? —— 说明回放总量超过并行能力。原因可能为:①主库写入 TPS 过高;②从库硬件资源已达瓶颈;③并行度已满但依赖链导致实际并发不足。可尝试增加 Worker 数量、开启 WRITESET,或升级硬件。
  3. 如何验证是否是无主键表导致的全表扫描? —— 在从库 SHOW ENGINE INNODB STATUS 中会看到大量的 row locks 等待和高频的 reads。同时,检查最慢 Worker 当前执行的 SQL 是否涉及无主键表。
  4. 紧急情况下能否在从库上直接 kill 掉慢 Worker? —— Worker 线程可以通过 KILL 终止,但可能导致当前事务回滚且 Relay Log 指针回退,Coordinator 会重新分配该事务,无法根本解决问题,并可能加剧延迟。应在复制层面临时跳过,而非杀掉线程。
  5. 如何在未来预防此类延迟? —— 实施大事务拆分规范,强制所有表必须具有主键,使用 WRITESET 优化并行度,建立基于 pt-heartbeat 或 GTID 差集的延迟告警,以及定期演练应急跳过流程。

加分回答:一个常被忽略的细节是 slave_pending_jobs_size_max 参数,它控制 Coordinator 可分配的最大任务内存。如果该值设置过小,导致大事务无法被分发给 Worker,Coordinator 会反复尝试分配而阻塞。检查 SHOW SLAVE STATUS 中的 Slave_SQL_Running_State,若显示 Waiting for Slave Workers to process their queues,可能与此有关,可适当增大。


主从复制速查表

类别关键参数 / 命令用途
复制状态SHOW SLAVE STATUS\GIO/SQL 线程状态、延迟、错误信息
主库位点SHOW MASTER STATUS当前 Binlog 文件及偏移量
GTID 模式gtid_mode=ON, enforce_gtid_consistency=ON开启 GTID 复制
自动定位MASTER_AUTO_POSITION=1GTID 模式下自动协商起始位点
Binlog 格式binlog_format=ROW, binlog_row_image=MINIMAL生产推荐配置,兼顾一致性与日志量
半同步启用rpl_semi_sync_master_enabled=1主库启动半同步
半同步超时rpl_semi_sync_master_timeout=1000超时后退化为异步,防止主库阻塞
并行复制类型slave_parallel_type=LOGICAL_CLOCK基于组提交的并行调度
Worker 数量slave_parallel_workers=8并行执行线程数
冲突检测binlog_transaction_dependency_tracking=WRITESET行级冲突检测,提升并行度
提交顺序保持slave_preserve_commit_order=ON保证从库提交顺序与主库一致
Worker 监控performance_schema.replication_applier_status_by_worker每 Worker 的事务执行详情
半同步监控SHOW STATUS LIKE 'Rpl_semi_sync%'半同步事务统计与状态
GTID 集合查询SELECT @@GLOBAL.gtid_executed获取已执行 GTID 集合
跳过 GTID 事务SET gtid_next='UUID:N'; BEGIN; COMMIT;注入空事务跳过错误事务
延迟精确监控pt-heartbeat通过心跳表实现毫秒级主从延迟监控

故障排查快速路径

  1. SHOW SLAVE STATUS\G → 确认 IO/SQL 线程状态,检查 Last_Error
  2. 主从 @@GLOBAL.gtid_executed 对比 → 量化延迟规模
  3. performance_schema.replication_applier_status_by_worker → 定位慢 Worker
  4. information_schema.innodb_lock_waits → 检查锁等待
  5. 主库慢查询日志 + SHOW PROCESSLIST → 溯源根因 SQL

延伸阅读

  • 《高性能MySQL》第4版,第 9-10 章:复制与扩展
  • MySQL 8.0 官方文档:Replication
  • MySQL 8.0 官方文档:Using GTIDs
  • MySQL 8.0 复制源码关键文件:sql/rpl_binlog_sender.cc, sql/rpl_slave.cc, sql/rpl_parallel.cc