跳到主要内容
约 11 分钟8 张卡

MySQL 复制

MySQL 复制把源库二进制日志中的变化传到副本,再由副本应用这些变化。它可以提供读副本和故障接管的基础,但复制本身不等于自动切换,也不保证任意副本随时都能读到刚提交的数据。

解释复制时,先区分三件事:事务在哪个节点提交,日志是否已经传到副本,副本是否已经应用。很多读写分离和切换问题,正是把这三步误当成了同一件事。本文以 MySQL 8.0 的传统源库与副本复制为准。

1. 面试核心​

先记住这几条,再读正文
  • 源库通过 binlog 发送变化,副本的接收线程写入 relay log,应用线程再执行其中的事务。卡 · 日志与线程
  • 异步复制的源库提交不等待副本确认;半同步等待约定数量的副本把事务日志写入 relay log 并落盘,不等待副本应用完成。卡 · 异步与半同步确认
  • 半同步的两个等待点区别在源库提交顺序:AFTER_SYNC 在存储引擎提交前等待,AFTER_COMMIT 在提交后等待。卡 · 异步与半同步确认
  • GTID 标识事务,自动定位根据事务集合寻找缺失日志;它不产生丢失日志,也不替代选主和旧主隔离。卡 · 复制位置与 GTID
  • 复制延迟要区分传输落后、应用落后和应用停止;Seconds_Behind_Source=0 不能独立证明副本与源库完全一致。卡 · 复制延迟
  • 写后立即读可以读源库,或等待目标副本应用写入对应的 GTID;固定等待一段时间不能保证已经追上。卡 · 读写分离与写后读
  • 副本查到的库存不能直接当作扣减资格。库存检查与扣减必须在权威写入端原子完成,读到新值也不能代替并发约束。卡 · 综合题
  • 切换需要核对候选副本包含哪些事务、阻止旧主继续写入,并处理在途请求的未知结果。卡 · 副本初始化与故障切换

2. 日志与线程​

二进制日志(binary log,简称 binlog)保存用于复制和恢复的数据库变化事件。它属于 MySQL Server 的日志机制,与 InnoDB 为崩溃恢复使用的 redo log、为回滚和旧版本读取使用的 undo log 职责不同。

这些日志怎样配合本地提交与崩溃恢复,见 MySQL 事务日志。下面只沿跨节点复制解释传输和应用。

一条写入到副本可读,经过这些环节:

  1. 源库处理事务,并把需要复制的事件记录到 binlog。
  2. 源库的 binlog dump 线程把日志发送给已连接的副本。
  3. 副本的 I/O 接收线程读取事件,写入本地 relay log,也就是中继日志。
  4. 副本的 SQL 应用线程读取 relay log 并执行事务;配置并行复制时,由协调线程分配给多个 worker。
  5. 副本提交相应事务之后,新建立且允许看到该版本的查询才可以读到它。

接收与应用分开,能够让网络接收继续进行,而不必每收到一条事务都等待其执行完成。但这也意味着“接收线程正常”不能证明数据已经更新,“两个线程都在运行”也不能证明没有积压。

2.1 日志格式​

MySQL 的 binlog 可以按语句或行变化记录。语句格式记录需要重执行的语句;行格式记录行变化;混合格式根据语句条件选择记录方式。

行格式减少了副本重新求值时因环境或非确定性行为产生差异的风险,但大量行更新可能生成更多日志。语句格式不能简单理解为“更省,所以更好”:需要核对语句能否在副本产生相同结果。MySQL 8.0 默认使用行格式,分析具体环境时仍应查看 binlog_format。

并行应用也不意味着可以任意打乱冲突事务。worker 的可并行程度受事务依赖与配置影响;一个大事务、热点行锁或较慢的存储仍可能成为瓶颈。

3. 复制位置与 GTID​

文件位置复制用“binlog 文件名与偏移量”指出从哪里继续。这个位置属于某个源库的日志文件,切换源库时需要正确换算和衔接。

GTID(全局事务标识)给事务一个在复制拓扑内唯一的标识,通常写成 源服务器 UUID:事务序号。同一事务复制到其他节点时保留这个标识,已经提交过相同 GTID 的节点会跳过重复应用。

例如 aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa:1-8 表示该源服务器的第 1 到第 8 个事务。副本已有 1-6,集合差可以指出还缺哪些事务:

SELECT GTID_SUBTRACT(
'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa:1-8',
'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa:1-6'
) AS missing;

结果是 aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa:7-8。GTID 是事务身份,不是物理文件偏移,也不是跨所有服务器统一递增的时间戳。

启用 GTID 自动定位后,副本在连接时报告已接收或已执行的事务集合,源库从仍保留的 binlog 中发送缺失事务。应用阶段再根据已执行集合避免重复。必须同时满足 GTID 配置与日志保留条件;源库已经清理掉副本需要的日志时,仅有 GTID 无法恢复数据,需要其他有效日志或重新建立副本。

GTID 自动跳过的是同一日志事务,不能替代业务幂等。客户端把同一次付款重新提交为一个新的数据库事务,会获得另一个 GTID,仍可能重复扣款。

4. 异步与半同步确认​

异步复制允许源库在不等待副本确认的情况下完成提交并回复客户端。源库永久故障时,尚未传出的事务可能无法从副本恢复,因此“写入已成功”和“切换到任意副本不丢数据”不是同一个保证。

半同步复制通过插件让源库等待指定数量的副本确认。副本在事务事件写入 relay log 并刷到磁盘后发送确认;此时还没有要求应用线程执行并提交该事务。

MySQL 8.0 默认的半同步等待点是 AFTER_SYNC:源库同步 binlog 后,等待副本确认,再提交存储引擎并返回结果。AFTER_COMMIT 则先提交存储引擎,再等待确认,等待期间其他会话已经可能在源库看到该事务。

半同步有几个边界必须一起说明:

  • 等待的是配置要求数量的副本,默认是一个,不是所有副本。
  • 确认对应日志落盘,不保证立即从该副本读到数据。
  • 等待超时后,源库会退回异步模式;符合恢复条件后再回到半同步。
  • 写延迟增加了网络往返与副本日志落盘的等待,不能只比较吞吐而不看延迟。

因此不能仅凭“开启半同步”就宣布任何切换都零丢失。需要核对当时是否实际处于半同步、哪些副本确认了日志、候选副本是否完整,并与故障模型和切换规则配套。

5. 复制延迟​

复制延迟是写入从源库提交到目标副本可用之间的滞后。先找落后发生在哪一段,再决定优化手段。

观察可能的方向进一步证据
接收位置长期不前进网络、认证、源库发送、日志缺失接收线程状态和 Last_IO_Error
已收到日志持续增多,但执行进度落后大事务、锁等待、磁盘或 CPU、并行度不足worker 状态、锁等待、事务耗时
应用线程停止约束冲突、数据不一致、执行错误Last_SQL_Error 与各 worker 错误
延迟波动与写高峰一致长期产生速度超过应用能力日志增长率、应用吞吐与队列趋势

MySQL 8.0.22 及以后可以用 SHOW REPLICA STATUS 查看复制状态,旧问法中的 SHOW SLAVE STATUS 对应旧名称。关注 Replica_IO_Running、Replica_SQL_Running、接收与执行 GTID 集合以及错误信息,不只看一个延迟数字。

Seconds_Behind_Source 基于事件时间和应用进度估算时间差。当接收线程本身落后、应用线程已追上当前收到的日志时,它仍可能为 0。监控应结合线程存活、日志位置或 GTID 差,以及业务心跳,不能把 0 当作数据一致性的证明。

增加 worker 只可能改善可并行的应用工作;网络故障、冲突大事务和错误停机不会因此自动消失。也不要为了恢复绿色指标直接跳过错误事务,先确认丢掉它会破坏什么数据关系。

6. 读写分离与写后读​

读写分离把写入交给源库,把一部分读取分配到副本,用副本分担读取压力。它增加读容量,也把复制延迟带入用户体验:刚创建的订单可能在源库存在,从副本查询却暂时为空。

处理方式取决于业务要求:

  • 对必须读到本次写入结果的操作,直接把后续读取路由到源库,并使用能看到该提交的新查询。
  • 对允许短暂旧值的列表或统计,明确可接受的延迟范围,再读副本。
  • 已准确取得写事务 GTID 的系统,可以在选定副本等待该 GTID 应用完成,再建立读取所需的新快照。

等待操作可以使用 WAIT_FOR_EXECUTED_GTID_SET(gtid_set, timeout)。返回 0 表示目标集合已经执行,返回 1 表示超时;调用方还应处理非法参数和执行错误,不能把未成功的调用当作已经追上。第二个参数是秒,必须非负,省略或设为 0 会无限等待;在线请求应有明确超时及超时后的路由或失败策略。

等待成功也要正确使用:它不替换已经建立的旧事务快照,也不证明所有更晚的写入都已复制。应等待本次操作需要的事务范围,随后按业务要求发起新读取。固定 sleep(1) 则没有检查复制位置,负载和故障变化后仍可能读到旧值。

库存扣减还需要更强的约束。假设副本显示剩余 1 件,两个请求都读到这个值,再分别到源库直接减一,就可能超卖。即使两个请求都改读源库,也可能同时通过一次独立查询,因此只解决复制延迟仍然不够。应在权威写入端把检查与扣减放在同一条条件更新或正确加锁的事务中,并核对影响行数;具体机制见热点行。展示库存可以按业务允许读取副本,真正决定能否下单的操作不能依赖这份旧快照。

7. 副本初始化与故障切换​

新副本先取得一致的数据快照及其复制位置,再追赶快照之后的日志。只有数据、位置和增量日志能够衔接,副本才是完整的;不能随意复制一份正在变化的数据目录,再从任意位置开始重放。

故障切换至少需要处理以下几项:

  1. 确认候选副本已经接收和执行哪些事务,必要时完成日志应用;不能只比较 GTID 数量判断内容包含关系。
  2. 隔离旧源库的写入能力,避免旧库恢复后和新库同时接受写入。
  3. 改变写入口并让其余副本跟随新源库,重新核对复制状态。
  4. 处理客户端在途请求:没有收到结果的写入可能已完成,重试应遵守业务幂等规则。

GTID 能帮助识别事务和重新连接,不能代替选主、网络隔离和入口切换。通用确认与切换关系见 主备复制。

复制也不能替代备份。误删除通常会继续复制到副本;恢复历史状态需要可用备份和连续日志,见 灾难恢复。

8 · 面试题卡片​

本页题目与参考回答

8.1 MySQL 主从复制经过哪些日志和线程?​

考察重点:binlog、relay log、接收与应用阶段的职责。

相关内容:日志与线程。

参考回答

源库记录 binlog,由发送线程传给副本;副本接收线程写 relay log,应用线程再执行并提交其中的事务。 并行复制还包含协调线程和多个 worker,能否并行受事务依赖影响。

接收日志与应用事务是两步。接收正常只能说明传输路径在工作,不代表查询已经能看到相应变化。binlog、redo 和 undo 的用途也不同,不能互换。

8.2 半同步复制确认成功后,为什么副本仍可能读不到刚写的数据?​

考察重点:日志持久化、应用提交、确认数量与超时降级。

相关内容:异步与半同步确认。

参考回答

半同步确认的是约定数量副本已把事务事件写入 relay log 并落盘,不是已经应用提交。 因此应用线程还没追上,或读取了另一个未追上的副本时,仍可能得到旧值。

此外,半同步等待超时会退回异步;判断故障保证时要检查实际运行状态和候选副本,不能只看配置开关。

8.3 AFTER_SYNC 与 AFTER_COMMIT 在什么时候等待副本确认?​

考察重点:源库 binlog 持久化、存储引擎提交和客户端确认的先后关系。

相关内容:异步与半同步确认。

参考回答

AFTER_SYNC 在 binlog 同步落盘后、存储引擎提交前等待确认;AFTER_COMMIT 在存储引擎提交后等待确认。 两者都在完成相应等待后才向本次提交的客户端返回结果,但 AFTER_COMMIT 等待期间,其他会话已经可能看到源库上提交的数据。

MySQL 8.0 的默认等待点是 AFTER_SYNC。无论选择哪个点,副本确认的都是事务事件写入 relay log 并落盘,不能据此推断应用线程已完成;超时退回异步的边界也仍然存在。

8.4 GTID 自动定位解决什么问题,缺失的 binlog 能靠它恢复吗?​

考察重点:事务身份、集合差、日志保留与自动定位。

相关内容:复制位置与 GTID。

参考回答

GTID 让节点按事务集合识别已经收到或执行的内容,源库据此发送缺少的事务,不必人工换算另一源库的文件偏移。 相同 GTID 已提交后可以跳过重复应用。

GTID 只标识事务,不包含恢复该事务所需的全部日志。需要的 binlog 已被清理时,必须从其他有效日志或快照恢复,不能靠标识本身补出变化。

8.5 复制延迟怎样定位,Seconds_Behind_Source 为 0 就完全同步了吗?​

考察重点:传输和应用进度、线程错误与监控局限。

相关内容:复制延迟。

参考回答

先区分接收落后、应用积压和应用停止,再看对应线程状态、错误及事务位置。 应用慢可能来自大事务、锁、资源或并行限制;接收落后则要查网络、源库和日志可用性。

Seconds_Behind_Source 为 0 不能单独证明同步。接收线程落后但应用已追上收到的日志时,也可能显示 0;需要结合源库位置、GTID 集合和业务心跳。

8.6 读写分离后,怎样保证创建订单后能立即查到它?​

考察重点:写后读要求、事务位置、等待超时与快照。

相关内容:读写分离与写后读。

参考回答

可以把这次后续读取路由到源库,或等待目标副本应用创建事务的 GTID,再发起能看到该提交的新查询。 等待必须有超时和明确的超时处理;不能用固定休眠证明副本已经追上。

即使目标 GTID 已执行,读取仍放在旧的 RR 快照内,也可能看到旧状态。读取位置、已执行事务范围和快照条件需要一起满足。

8.7 从副本读取库存再到源库扣减,怎样避免复制延迟导致超卖?​

题型:综合题

考察重点:陈旧读取、并发检查与扣减、展示数据和交易决策的分工。

相关内容:读写分离与写后读、热点行。

参考回答

扣减资格必须由权威写入端的原子操作决定,不能因为副本显示有库存就直接扣减。 可以在源库使用带库存条件的 UPDATE,把条件判断与扣减合在一条语句内,并根据影响行数确认是否成功;涉及多条记录时,应设计相应事务与锁定规则。

把查询改到源库只能减少陈旧读取,独立的“先查、后改”仍可能让多个请求同时通过检查。副本可用于允许旧值的展示,但最终下单必须遵守库存约束,后续重试还要避免重复扣减。

8.8 MySQL 源库故障后,GTID 能否自动保证切换不丢数据?​

考察重点:确认范围、候选副本、旧主隔离与未知写入结果。

相关内容:副本初始化与故障切换。

参考回答

不能,GTID 只帮助识别事务和定位缺失内容。 是否丢数据取决于已确认事务是否存在于可恢复副本,以及切换选择了哪个副本;异步模式下,源库已提交的事务可能尚未复制。

切换还需阻止旧主继续接受写入,更新写入口并重建复制关系。客户端结果未知的请求可能已成功,后续重试必须处理重复副作用。

9. 参考资料​

来源:Coding 101 原文 · 免费阅读,引用请保留出处。内容使用说明