Skip to content

第 8 讲:主从复制、读写分离、主从延迟

核心结论(10 条必记)

  1. 主从复制依赖 binlog -- 主库写变更,从库拉取并重放
  2. 从库通过 IO 线程拉取 binlog,SQL 线程重放 -- 两个线程各司其职
  3. binlog 三种格式:Statement / Row / Mixed,推荐 Row -- 更可靠,不易主从不一致
  4. 主从延迟常见原因:主库压力大、从库慢、大事务、单线程重放 -- 面试高频
  5. 读写分离可以分散压力,但要注意一致性问题 -- 写后立刻读可能读不到
  6. 核心场景要读主库,非核心场景读从库 -- 实际开发中最常用的策略
  7. 主库宕机可以手动或自动切换 -- 工具如 MHA、Orchestrator
  8. 半同步复制可以减少数据丢失 -- 至少一个从库确认收到才返回成功
  9. GTID 让主从切换更简单 -- 全局事务 ID,自动定位复制位点
  10. 并行复制可以提升从库重放速度 -- MySQL 5.7+ 支持多线程重放

一、为什么需要主从复制?

单机 MySQL 有很多问题:

问题说明
写压力集中所有写操作都打在一台机器上,容易成为瓶颈
读压力集中读写都在一台机器,高并发时扛不住
单点故障机器宕机,整个服务不可用
备份影响性能在主库做备份可能影响线上服务

解决方案:主从架构

  • 一个主库(Master):负责写
  • 一个或多个从库(Slave/Replica):负责读

好处:

  • 读写分离,分散压力
  • 从库可以做备份
  • 从库可以做容灾切换

二、主从复制的基本原理

核心依赖:binlog

简化流程

主库(Master):

  1. 执行 SQL(INSERT / UPDATE / DELETE)
  2. 把变更记录到 binlog

从库(Slave): 3. 从库有一个 IO 线程,不断拉取主库的 binlog 4. 拉取到的 binlog 写入从库的 relay log(中继日志) 5. 从库有一个 SQL 线程,读取 relay log,重放这些变更

完整流程图

主库:
  执行 SQL -> 写 binlog

从库:
  IO 线程 -> 拉取主库 binlog -> 写 relay log
  SQL 线程 -> 读 relay log -> 重放 SQL

一句话总结

主库把变更写到 binlog,从库拉取并重放,从而保持数据同步。


"重放"到底是什么意思?

一句话理解

重放就是把主库执行过的操作,在从库上再执行一遍。

用一个例子说明

假设主库执行了:

sql
INSERT INTO user(id, name, age) VALUES(1, '张三', 20);

主库的 user 表多了一行数据,同时变更被记录到 binlog。

从库同步的过程:

第1步:IO 线程从主库拉取 binlog,发现有一条变更记录
第2步:写入自己的 relay log
第3步:SQL 线程读取 relay log,发现内容是:
       在 user 表插入了一行:id=1, name='张三', age=20
第4步:SQL 线程在本地执行这个操作

从库也执行了同样的插入操作,user 表里也有了这行数据。

这个"在从库上再执行一遍"的动作,就叫重放(Replay)

为什么叫"重放"?

"重放"来自"录音回放"的概念:

主库 = 录音:每做一个操作,就像录下一段声音(写入 binlog)
从库 = 播放:拿到录音(binlog),按顺序播放一遍(重新执行)

不同 binlog 格式下,重放方式不同

格式重放什么例子
Statement重新执行 SQL 语句UPDATE user SET age = age + 1 WHERE id = 1; 从库也执行这条 SQL
Row直接按行变更修改数据binlog 记录"id=1 的 age 从 20 变成 21",从库直接改

Row 更精确,不会因为 SQL 里有 NOW() 之类的函数导致不一致。

重放的关键特点

特点说明
顺序执行按 binlog 顺序重放,不能跳过、不能乱序
异步的主库写完 binlog 就返回成功,不等从库重放完(所以会有延迟)
从库是跟随者从库不会自己产生新数据,只是重复主库的操作

三、binlog 的三种格式

这是面试高频,也是理解主从复制的关键。

1. Statement(语句模式)

binlog 里记录的是原始 SQL 语句

sql
-- 主库执行:
UPDATE user SET age = age + 1 WHERE id = 1;

-- binlog 里记录:
UPDATE user SET age = age + 1 WHERE id = 1;

从库重放时,也执行同样的 SQL。

优点缺点
binlog 体积小SQL 里有不确定因素(NOW()UUID()RAND())时,主从可能不一致
sql
-- 主从不一致的例子:
UPDATE user SET update_time = NOW() WHERE id = 1;
-- 主库执行时是一个时间,从库重放时又是另一个时间

2. Row(行模式)

binlog 里记录的是数据行的变更

sql
-- 主库执行:
UPDATE user SET age = 21 WHERE id = 1;

-- binlog 里记录(简化表示):
-- id=1: age 从 20 变成 21
优点缺点
不会因为 SQL 不确定性导致主从不一致binlog 体积更大
更精确一次更新很多行时,binlog 会很大

3. Mixed(混合模式)

MySQL 根据情况自动选择:

  • 通常用 Statement
  • 如果 SQL 有不确定因素,自动切换成 Row

兼顾体积和准确性。

推荐

大多数生产环境推荐用 Row 格式,因为更可靠,不容易主从不一致,binlog 体积问题可以接受。


四、主从延迟:为什么会发生?

主从复制不是实时的,通常有延迟。

什么是主从延迟?

主库已经写入的数据,从库还没来得及同步过来。

常见原因

原因说明
主库写入压力大主库 TPS 很高,binlog 生成速度快,从库重放跟不上
从库硬件配置差CPU / IO 性能不如主库,重放慢
大事务一次更新几十万行,binlog 很大,从库重放时间长
从库单线程重放MySQL 5.6 之前从库单线程重放,主库多线程并发写(5.7+ 支持并行复制)
网络延迟主从之间网络不稳定,binlog 传输慢
从库有大查询慢查询可能影响 SQL 线程重放

怎么判断主从延迟?

在从库执行:

sql
SHOW SLAVE STATUS\G

重点看 Seconds_Behind_Master:从库落后主库的秒数。0 = 基本同步,几十/几百 = 延迟严重。


五、主从延迟怎么解决?

方案说明
优化主库避免大事务、分批写入、控制写入 TPS
提升从库硬件更好的 CPU / SSD,增加内存
开启并行复制MySQL 5.7+ 支持 slave_parallel_workers = 4
分库分表单库写压力太大时考虑拆分
业务层容忍延迟统计报表、非核心查询可以接受短时间延迟
读主库必须读到最新数据时直接读主库

并行复制配置示例:

sql
slave_parallel_workers = 4  -- 从库用 4 个线程并行重放
slave_parallel_type = LOGICAL_CLOCK

六、读写分离

什么是读写分离?

  • 写操作(INSERT / UPDATE / DELETE)打到主库
  • 读操作(SELECT)打到从库

架构示意

应用
  |
  +-- 写请求 -> 主库
  |
  +-- 读请求 -> 从库 1 / 从库 2 / 从库 3

好处

好处说明
分散压力读远多于写,从库分担读压力
主库专注写主库不被大量读查询拖累
横向扩展增加从库就能提升读能力

怎么实现读写分离?

方案说明
应用层判断代码里区分写操作用主库连接,读操作用从库连接
中间件MyCat、ShardingSphere、ProxySQL、Atlas 自动路由
ORM 框架支持有些 ORM 框架支持主从配置,自动路由

七、读写分离的一致性问题

这是面试高频,也是实际开发中的痛点。

问题场景

用户刚注册(写主库),然后立刻跳转到个人中心(读从库)。如果此时主从有延迟,从库还没同步到新数据,用户会看到"404 用户不存在"。

解决方案

方案说明适用场景
强制读主库直接读主库用户注册后查询、下单后查订单、改密码后验证
延迟一小段时间前端等一下再查询业务能接受的场景
用缓存写操作同时更新缓存,读操作先读缓存高频热点数据
标记路由Session/Cookie 标记"刚写过",一定时间内强制读主库写后读场景
异步通知主库写完后通过消息队列通知应用,再去读从库对一致性要求高

实际中最常用的组合

  • 核心写后读:读主库
  • 非核心查询:读从库
  • 高频热点数据:Redis 缓存

八、主库宕机怎么办?

这是高可用的核心问题。

1. 手动切换(不推荐)

管理员手动把某个从库提升为主库,修改应用配置指向新主库。

2. 自动故障切换

用工具自动检测主库是否宕机,自动把从库提升为主库。

工具说明
MHA(Master High Availability)经典方案
Orchestrator自动化拓扑管理
MySQL Router + MySQL InnoDB Cluster官方方案

切换过程

1. 检测主库宕机(心跳检测、连接失败)
2. 选择新主库(通常选数据最新的从库)
3. 提升从库为主库
4. 其他从库指向新主库
5. 应用连接指向新主库

切换过程中的问题

问题说明
数据丢失主库还有部分 binlog 没同步给从库
双主问题切换不彻底,可能同时有两个主库导致数据冲突

解决:半同步复制

主库写入 binlog 后,至少等一个从库确认收到 binlog,才返回成功。减少数据丢失风险。

sql
-- 主库
INSTALL PLUGIN rpl_semi_sync_master SONAME 'semisync_master.so';
SET GLOBAL rpl_semi_sync_master_enabled = 1;

-- 从库
INSTALL PLUGIN rpl_semi_sync_slave SONAME 'semisync_slave.so';
SET GLOBAL rpl_semi_sync_slave_enabled = 1;

九、GTID 是什么?

GTID 是 MySQL 5.6+ 引入的全局事务 ID

传统复制的问题

传统复制基于 binlog 文件位置(文件名 + position),切换主库时很难准确定位"从哪里开始复制"。

GTID 解决什么?

每个事务有一个全局唯一 ID。从库可以根据 GTID 自动定位,不需要手动指定 binlog 文件和位置。切换主库时更方便、更可靠。

GTID 让主从切换更简单、更安全。


十、面试高频题

Q1:主从复制原理是什么?

主库把变更写入 binlog,从库的 IO 线程拉取 binlog 并写入 relay log,从库的 SQL 线程读取 relay log 并重放,从而实现数据同步。

Q2:binlog 三种格式有什么区别?

Statement 记录 SQL 语句,体积小但可能不一致;Row 记录行变更,更可靠但体积大;Mixed 自动选择。生产推荐 Row。

Q3:主从延迟有哪些原因?

主库写入压力大、从库硬件差、大事务、从库单线程重放、网络延迟、从库有大查询。

Q4:读写分离的一致性问题怎么解决?

核心场景读主库、用缓存、标记路由、异步通知、业务容忍延迟。

Q5:主库宕机怎么办?

手动或自动切换,工具如 MHA/Orchestrator,可以用半同步复制减少数据丢失,GTID 让切换更简单。


十一、练习题

练习 1

为什么生产环境推荐用 Row 格式的 binlog,而不是 Statement?

练习 2

用户刚下单(写主库),然后立刻查订单详情(读从库),可能看不到数据。你有哪些解决方案?

练习 3

主从延迟很严重,从库落后主库 100 秒。可能有哪些原因?怎么优化?


下一讲预告

第 9 讲:分库分表原理、策略、常见问题

  • 为什么需要分库分表
  • 垂直拆分 vs 水平拆分
  • 分片键怎么选
  • 全局 ID 怎么生成
  • 分库分表后的查询问题
  • 扩容怎么做
  • 分布式事务怎么处理

基于 VitePress 构建