📖 本篇简介:主从复制是 MySQL 高可用的基石,但它会出两类要命的问题:一是"主从延迟"(从库追不上主库),二是"脑裂"(主库挂了、从库被提升,但旧主库又活过来,两边同时写)。本篇用 10 个现场场景,带你学会定位主从延迟、安全做故障切换、处理切换时数据没追上、理解脑裂成因与恢复、半同步复制选型,以及复制中断修复。示例基于 MySQL 8.0,学完你能把高可用切换讲出"脑裂防护"这一层深度。
目录
- 1 本篇导读与主从高可用问题全景
- 2 场景一:主从延迟突然飙高如何定位原因
- 3 场景二:Seconds_Behind_Master 为 0 真的同步了吗
- 4 场景三:主库宕机如何做高可用切换
- 5 场景四:切换时从库还没追上主库怎么办
- 6 场景五:脑裂是怎么发生的有什么危害
- 7 场景六:脑裂后怎么恢复数据一致
- 8 场景七:半同步复制如何提升数据安全
- 9 场景八:MHA 与 Orchestrator 怎么选
- 10 场景九:主从延迟导致读到旧数据怎么处理
- 11 场景十:主从复制中断报错如何修复
- 12 主从延迟与高可用速查表
- 13 常见坑与注意事项
- 14 练习题与自测
1 本篇导读与主从高可用问题全景
主从架构的两个核心问题,面试和线上都高频出现:
- 主从延迟:主库写入后,binlog 传到从库并回放需要时间,从库数据暂时落后。延迟小时无感,延迟大了会"读到旧数据",切换时还会丢数据。
- 脑裂(Split-Brain):主库故障,高可用组件把从库提升为新主;但旧主库其实没彻底死(只是网络分区),它又开始接受写入。结果两个"主库"同时写,数据分叉、冲突。
先记住主从复制的基本链路:主库写 binlog → 从库 IO 线程拉取写 relay log → 从库 SQL 线程回放 relay log。延迟就发生在"回放"这一步。
-- 本示例基于 MySQL 8.0
-- 先看从库的同步状态和延迟
SHOW REPLICA STATUS\G -- 8.0 用 REPLICA,5.7 是 SLAVE
-- 关键列:
-- Replica_IO_Running / Replica_SQL_Running 两个线程是否都 Yes
-- Seconds_Behind_Master 从库落后主库多少秒
-- Executed_Gtid_Set 从库已执行的 GTID
2 场景一:主从延迟突然飙高如何定位原因
场景描述:平时主从延迟只有几十毫秒,突然飙到几百秒,从库数据严重落后。怎么定位是哪里慢了?
考察点:考察主从延迟的根因排查——延迟不是单一原因。
解答思路:
主从延迟常见原因按出现频率排:
-- 本示例基于 MySQL 8.0
-- 1. 看从库两个线程状态:IO 线程还是 SQL 线程慢
SHOW REPLICA STATUS\G
-- Replica_IO_Running=Yes 说明 binlog 传输正常
-- 如果 SQL 回放慢,延迟通常是"大事务"或"从库性能差"
1. 主库大事务:一个 UPDATE 更新了几百万行,binlog 是一条,从库回放这条要很久,期间延迟猛涨;
2. 从库性能差:从库机器 IO/CPU 比主库弱,回放跟不上;
3. 从库承担了读压力:从库又当复制又扛读,回放被读请求挤占资源;
4. 并行复制没开:从库单线程回放(老模式),写入稍多就延迟。
-- 本示例基于 MySQL 8.0
-- 8.0 开启基于组提交的并行复制,从库多线程回放,大幅降延迟
SHOW VARIABLES LIKE 'replica_parallel_type'; -- LOGICAL_CLOCK
SHOW VARIABLES LIKE 'replica_parallel_workers'; -- 并行回放线程数
定位延迟时,还有一个实用手段:直接在主库找"现在正在执行的大事务"。
-- 本示例基于 MySQL 8.0
-- 查主库上执行时间长、影响行数多的事务,它往往就是从库延迟的元凶
SELECT trx_id, trx_started,
TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS age_sec,
trx_rows_modified, trx_query
FROM information_schema.innodb_trx
ORDER BY trx_rows_modified DESC
LIMIT 5;
-- trx_rows_modified 很大的事务,就是从库要慢慢回放的那条
追问:为什么大事务会造成主从延迟?
因为 binlog 里一个大事务是一个整体,从库回放时要么不做、要么全做完,期间整个从库都落后。主库几秒写完,从库可能要回放几分钟。
3 场景二:Seconds_Behind_Master 为 0 真的同步了吗
场景描述:监控显示 Seconds_Behind_Master=0,你以为从库追平了。但切流后还是丢了数据。为什么这个指标会骗人?
考察点:考察对主从延迟指标的深入理解,这是容易被忽视的坑。
解答思路:
Seconds_Behind_Master 有两个盲区:
1. 它测的是 SQL 线程回放的时间差。如果 IO 线程已经断了(binlog 传不过来),这个值可能显示 NULL 或 0,不代表真同步;
2. 它是"瞬时值",主库瞬间写入高峰时延迟高,写完又回落,采样可能刚好采到 0。
更可靠的判断是看 GTID 是否一致:
-- 本示例基于 MySQL 8.0
-- 在主库看已执行的 GTID
SHOW MASTER STATUS; -- 看 Executed_Gtid_Set
-- 在从库看已执行的 GTID
SHOW REPLICA STATUS\G -- 看 Executed_Gtid_Set
-- 两者一致,才是真正追平;Seconds_Behind_Master=0 只是参考
# 本示例为 sqlite3 演示:模拟用 GTID 集合判断主从是否追平
# 主库已写 GTID 集合 vs 从库已执行集合
master = {"g1","g2","g3","g4"} # 主库执行到 g4
replica = {"g1","g2","g3"} # 从库只到 g3,还差 g4
pending = master - replica
print("从库还差:", pending) # 输出 {'g4'},说明没追平
上面脚本实跑会发现从库还差 g4,即便 Seconds_Behind_Master 显示 0。
追问:怎么精确判断从库追上了某个具体事务?
用 GTID:等从库的 Executed_Gtid_Set 包含了主库刚写入的事务 GTID,才是真的读到最新数据。4 场景三:主库宕机如何做高可用切换
场景描述:主库突然宕机,业务全部写失败。你有一个从库,现在要手动把从库提升为主库。完整步骤是什么?
考察点:考察手动主从切换的标准流程,这是 DBA 基本功。
解答思路:
手动切换(假设旧主库已彻底失联)按下面步骤:
-- 本示例基于 MySQL 8.0
-- 1. 确认旧主库真的挂了(别误判,否则会脑裂)
-- 2. 在从库上:先确认已追平能追的所有 relay log
STOP REPLICA;
-- 3. 把从库提升为主库:关闭只读、开启写
SET GLOBAL read_only = OFF;
SET GLOBAL super_read_only = OFF;
-- 4. 记录新主库位点/GTID,让其他从库指向新主
CHANGE REPLICATION SOURCE TO SOURCE_HOST='新主库IP';
START REPLICA;
关键顺序:先确认旧主真死、再提升新主、最后让其他从库改指向。整个过程要快,但不能省"确认旧主死透"这一步——省了它就会脑裂。
一次干净的手动切换,时间线大概是这样,面试时能按时间线讲出来很加分:
| 时间点 | 动作 | 目的 |
|---|---|---|
| T+0 | 监控告警主库失联 | 开始应急 |
| T+1min | 确认旧主真死,防火墙隔离旧主 | 防脑裂 |
| T+2min | 检查从库追上情况 | 评估丢数据 |
| T+3min | 从库提升为新主,关只读 | 恢复写入 |
| T+4min | 其他从库指向新主 | 重建拓扑 |
| T+5min | 应用切到新主 VIP | 业务恢复 |
-- 本示例基于 MySQL 8.0
-- 切换前最后确认:从库是否还能追到主库的最新位点
-- 如果旧主只是网络问题但还活着,先把它彻底隔离(防火墙/停机)再切
追问:为什么切换要先确认旧主彻底死?
如果旧主其实活着只是网络不通,你又提升了新主,两个主同时写就脑裂了。所以要么隔离旧主,要么用 fencing(隔离旧主网络)防止它复活后继续写。
5 场景四:切换时从库还没追上主库怎么办
场景描述:主库宕机时,从库还落后主库 100 秒数据。现在要立刻切,这 100 秒数据怎么办?
考察点:考察切换时的数据取舍——RPO(恢复点目标)的权衡。
解答思路:
这是个两难:等从库追平,业务一直不可写;不等,就丢那 100 秒数据。
标准做法:
1. 如果旧主库磁盘还能读:尝试把旧主 binlog 里没同步的部分抢救出来,补到新主;
2. 如果旧主彻底坏了:只能接受丢这部分数据(RPO > 0),业务层做最终一致补偿;
3. 平时用半同步复制(见场景七):让主库至少等一个从库收到 binlog 才返回成功,把丢失窗口缩到极小。
-- 本示例基于 MySQL 8.0
-- 切换前看从库落后多少,评估丢数据量
SHOW REPLICA STATUS\G
-- 看 Executed_Gtid_Set 与旧主 binlog 最新位点差多少
-- 如果业务能忍短暂不可写,等从库追平再切(RPO=0)
-- 如果业务必须立刻恢复,接受少量数据丢失(RPO>0)先切
追问:金融业务能接受丢这 100 秒数据吗?
不能。金融场景必须上半同步复制 + 多从库,把 RPO 压到接近 0;甚至用多副本一致性协议(如 MGR)。互联网一般业务可以接受秒级丢失。
6 场景五:脑裂是怎么发生的有什么危害
场景描述:听说"脑裂"是高可用最可怕的事故,到底它是怎么发生的?危害有多大?
考察点:考察脑裂的成因与危害,这是高可用设计的核心考点。
解答思路:
脑裂的经典场景:
正常:应用 → 主库A → 从库B
故障:A 所在机房网络抖动,A 本身没死机,但高可用组件连不上 A
→ 组件误判 A 挂了,把 B 提升为新主
→ 但 A 其实还活着,还在接受旧连接的写入
→ 现在 A 和 B 都是主库,两边同时写
危害:两个主库数据各自分叉,同一行在两边被改成不同值,binlog 冲突,后续怎么都合不回来,要人工逐行比对修复,数据可能永久丢失。
-- 本示例基于 MySQL 8.0
-- 脑裂防护核心:fencing(隔离旧主)
-- 提升新主前,必须先把旧主"干掉"或网络隔离,让它绝对不能再写
-- 常见手段:
-- 1. STONITH(Shoot The Other Node In The Head):直接关机旧主
-- 2. 防火墙切断旧主网络,让它无法对外提供服务
-- 3. 旧主启动时自动检测:发现不是新主就强制 read_only
追问:为什么"网络分区"比"主库真挂"更危险?
真挂了旧主不会复活;网络分区时旧主活着但失联,组件误判后双主并存,才是脑裂。所以脑裂本质是"误判 + 旧主复活"。
7 场景六:脑裂后怎么恢复数据一致
场景描述:已经脑裂了,旧主和新主都写了一段时间。现在要恢复成单主,怎么处理两边分叉的数据?
考察点:考察脑裂后的恢复流程,理解其代价。
解答思路:
脑裂恢复的原则是"确定谁为准,丢弃另一边",因为自动合并冲突数据几乎不可能:
1. 先停掉一个主:立即把旧主设为只读或下线,止住两边继续分叉;
2. 确认新主:以新提升的主库为准(业务已经切过来了);
3. 抢救旧主独有数据:旧主上有、新主上没有的写入,导出出来人工核对,决定要不要补回去;
4. 重建从库:以新主为基准,重新搭建所有从库,恢复拓扑。
-- 本示例基于 MySQL 8.0
-- 第一步:立刻把旧主降级为只读,停止它继续写(止血)
SET GLOBAL super_read_only = ON;
-- 第二步:对比新旧主的 binlog/GTID,找出旧主独有的事务
-- SHOW BINARY LOGS; 对比两边 Executed_Gtid_Set 的差集
-- 第三步:以新主为准,旧主重新作为从库指向新主
CHANGE REPLICATION SOURCE TO SOURCE_HOST='新主IP';
START REPLICA;
-- 注意:如果旧主有新主没有的数据,直接 change 会冲突,要先清理掉
追问:脑裂能完全避免吗?
不能完全避免,但能用 fencing + 多数派投票(如 MGR 要多数节点同意才能选主)把概率降到极低。脑裂恢复是下策,预防才是上策。
脑裂恢复时最棘手的是"两边各有新数据"——比如用户在旧主改了地址,又在新主下了订单,两边都变了,没法自动判断哪个对。这就是为什么脑裂事故的恢复往往要业务团队一起逐笔核对,耗时几天。这也再次说明:脑裂是要靠预防避免的事故,不是能轻松修复的小问题。
8 场景七:半同步复制如何提升数据安全
场景描述:默认异步复制下,主库写成功就返回,binlog 还没传到从库主库就挂了,数据丢了。怎么提升数据安全?
考察点:考察半同步复制(semi-sync)的原理与权衡。
解答思路:
异步复制的问题:主库写 binlog 后立刻返回成功,不等从库确认。这时主库宕机,这条写就丢了。半同步复制让主库至少等一个从库收到 binlog(写到 relay log)才返回成功:
-- 本示例基于 MySQL 8.0
-- 主库安装并开启半同步插件
INSTALL PLUGIN rpl_semi_sync_source SONAME 'semisync_source.so';
SET GLOBAL rpl_semi_sync_source_enabled = 1;
-- 至少等 1 个从库确认收到 binlog
SET GLOBAL rpl_semi_sync_source_wait_for_replicas = 1;
-- 从库开启半同步
INSTALL PLUGIN rpl_semi_sync_replica SONAME 'semisync_replica.so';
SET GLOBAL rpl_semi_sync_replica_enabled = 1;
权衡:半同步提高了数据安全(RPO≈0),但主库写入要等从库 ACK,延迟略增。如果从库都挂了,主库会超时后自动降级回异步,不能因为等从库把主库写挂。
-- 本示例基于 MySQL 8.0
-- 超时降级:从库一直不 ACK,超过这个毫秒数就退回异步,保主库可用
SET GLOBAL rpl_semi_sync_source_timeout = 1000;
追问:半同步就等于零数据丢失吗?
接近零,但严格说从库收到 binlog 到回放成可读还有一步;要更严格的话用 MGR(组复制)多数派确认。
9 场景八:MHA 与 Orchestrator 怎么选
场景描述:要搭一套自动高可用,了解到 MHA、Orchestrator、MGR 几个方案,怎么选?
考察点:考察高可用方案选型的对比能力。
解答思路:
| 方案 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| MHA | 监控+故障时自动提升从库 | 成熟、广泛使用 | 需额外管理节点,维护老 |
| Orchestrator | 拓扑发现+自动故障转移 | 可视化、拓扑管理强 | 架构较复杂 |
| MGR 组复制 | 多主共识协议 | 无单点、强一致 | 性能开销大,运维门槛高 |
-- 本示例基于 MySQL 8.0
-- MGR(Group Replication)是 8.0 原生的高可用方案,基于 Paxos 变种
-- 组内多数派(如 3 台里 2 台)同意才算提交,脑裂时少数派自动只读
SHOW VARIABLES LIKE 'group_replication_group_name';
-- MGR 从根上缓解脑裂:少数派节点无法形成多数,自动拒绝写入
选型建议:中小团队用 Orchestrator 或云厂商托管高可用;对数据一致性要求极高、有运维能力的,考虑 MGR。
追问:MGR 为什么能防脑裂?
因为它要多数派节点达成共识才能提交和选主。脑裂时少数派那一侧凑不齐多数,自动变成只读,写不进去,从机制上杜绝了双主写。
10 场景九:主从延迟导致读到旧数据怎么处理
场景描述:读写分离后,用户刚下单,刷新订单列表却看不到刚下的单——主从延迟读到了旧数据。怎么解?
考察点:考察读写分离一致性问题,这是线上最常见的延迟症状。
解答思路:
按业务对一致性的要求分级处理:
-- 本示例基于 MySQL 8.0
-- 方案1:读己之写——刚写过的数据,接下来的读强制走主库
-- 应用层记录"这个用户刚写过",短时间内其读请求路由到主库
-- 方案2:等待从库追平再读(GTID 方式)
-- 客户端记录刚写入的 GTID,读从库前先等它追上
-- WAIT_FOR_EXECUTED_GTID_SET('刚写的GTID', 超时秒数);
SELECT WAIT_FOR_EXECUTED_GTID_SET('aaaaaaaa-...:12345', 3);
-- 上面这行会阻塞直到从库执行到该 GTID,最多等 3 秒
其他方案:非关键读容忍延迟(商品详情无所谓);关键读("我的订单")走主库。
追问:所有读都走主库不就没延迟问题了吗?
那就失去读写分离意义了,主库扛不住读洪峰。要按业务分级:关键读走主、普通读走从。
11 场景十:主从复制中断报错如何修复
场景描述:从库突然 Replica_SQL_Running=NO,复制中断了,报错 1062 Duplicate entry。怎么修?
考察点:考察复制中断的常见错误与处理。
解答思路:
先看具体错误:
-- 本示例基于 MySQL 8.0
-- 看复制报错详情
SHOW REPLICA STATUS\G
-- 重点列:Last_Error,看是哪个事务、什么错
1062 Duplicate entry 说明从库上已经有这条数据了(可能被手工写过),回放时主键冲突。处理:跳过这个事务继续:
-- 本示例基于 MySQL 8.0
-- 方式1:跳过一个事务(GTID 模式)
SET GTID_NEXT = 'aaaaaaaa-...:12346'; -- 指定要跳过的 GTID
BEGIN; COMMIT; -- 空事务占位
SET GTID_NEXT = 'AUTOMATIC';
START REPLICA;
-- 方式2:老的跳过方式(5.7 及以前)
-- SET GLOBAL sql_slave_skip_counter = 1;
-- START SLAVE;
但跳过事务是有损操作,跳过意味着这条变更在从库永久丢失。要先评估这个事务重不重要,重要的话手工补数据再跳。
# 本示例为 sqlite3 演示:复制中断修复流程的状态机
states = ["IO正常", "SQL报错中断"]
# 修复动作:定位错误 → 评估是否有损 → 跳过/补数据 → 重启复制
print("修复流程: 定位 Last_Error → 判断 1062 主键冲突 → 跳过该 GTID → START REPLICA")
print("原则: 跳过=有损, 重要事务先手工补数据")
追问:复制中断为什么要立刻处理?
因为中断期间主库新数据不会同步到从库,延迟越拉越大;且从库一旦延迟太久,恢复后追平要很久,期间从库读的都是旧数据。
12 主从延迟与高可用速查表
| 现象 | 大概率原因 | 第一反应 |
|---|---|---|
| 延迟突然飙高 | 主库大事务 / 从库读压力 | 查大事务、从库并行复制 |
| Seconds_Behind=0 但丢数据 | 指标骗人,看 GTID | 对比 Executed_Gtid_Set |
| 主库宕机要切 | 确认旧主死透再提升 | 隔离旧主 + 提升从库 |
| 切换时从库落后 | 大事务没回放完 | 抢救旧主 binlog 或接受丢数据 |
| 脑裂 | 误判+旧主复活 | 立即停一个主,以新主为准 |
| 读到旧数据 | 主从延迟 | 读己之写走主库 |
| 复制中断 1062 | 主键冲突 | 评估后跳过 GTID |
| 高可用方案 | 脑裂防护 | 数据安全 |
|---|---|---|
| 异步复制 | 靠 fencing | 主库宕机可能丢数据 |
| 半同步复制 | 靠 fencing | 至少一从收到 binlog |
| MGR 组复制 | 多数派机制天然防裂 | 多数派共识,强一致 |
13 常见坑与注意事项
1. Seconds_Behind_Master=0 就放心切:它是瞬时值且 IO 断时会骗人,切前必须对比 GTID。
2. 不确认旧主死透就切:这是脑裂的直接原因,切换前必须隔离旧主。
3. 从库又复制又扛重读:读压力挤占回放资源,延迟越来越大。
4. 复制中断不及时修:延迟越拉越大,从库读的都是旧数据。
5. 无脑跳过复制错误:跳过 GTID 是有损的,重要数据要先补。
6. 半同步不设超时降级:从库全挂后主库一直等 ACK,写被拖死。
7. 切换不演练:真到主库宕机时手忙脚乱,平时要做切换演练。
14 练习题与自测
练习 1:主从延迟突然从毫秒飙到几百秒,列出至少三个可能原因。
参考答案:主库大事务、从库性能差/扛了读压力、并行复制没开。
练习 2:为什么 Seconds_Behind_Master=0 不可全信?怎么准确判断追平?
参考答案:它是 SQL 线程瞬时值,IO 断时会假 0;准确判断要对比主从的 Executed_Gtid_Set 是否一致。
练习 3:脑裂是怎么发生的?防护核心是什么?
参考答案:主库网络分区但没死,组件误判提升新主,旧主复活后双写;防护核心是 fencing,提升新主前必须隔离旧主。
练习 4:半同步复制相比异步复制解决了什么问题?代价是什么?
参考答案:解决主库宕机丢最后几条已提交数据的问题(RPO≈0);代价是写入要等从库 ACK,延迟略增,需设超时降级保主库。
练习 5(自测):口述主库宕机手动切换的完整步骤,并说明每一步如何防止脑裂。
自测要点:能否说出"确认旧主死透并隔离→提升从库→其他从库改指向",且强调 fencing 防旧主复活。
下一篇预告:第 26 章 数据库选型与架构设计
文章回复
0 条公开回复