第 8 讲:主从复制、读写分离、主从延迟
核心结论(10 条必记)
- 主从复制依赖 binlog -- 主库写变更,从库拉取并重放
- 从库通过 IO 线程拉取 binlog,SQL 线程重放 -- 两个线程各司其职
- binlog 三种格式:Statement / Row / Mixed,推荐 Row -- 更可靠,不易主从不一致
- 主从延迟常见原因:主库压力大、从库慢、大事务、单线程重放 -- 面试高频
- 读写分离可以分散压力,但要注意一致性问题 -- 写后立刻读可能读不到
- 核心场景要读主库,非核心场景读从库 -- 实际开发中最常用的策略
- 主库宕机可以手动或自动切换 -- 工具如 MHA、Orchestrator
- 半同步复制可以减少数据丢失 -- 至少一个从库确认收到才返回成功
- GTID 让主从切换更简单 -- 全局事务 ID,自动定位复制位点
- 并行复制可以提升从库重放速度 -- MySQL 5.7+ 支持多线程重放
一、为什么需要主从复制?
单机 MySQL 有很多问题:
| 问题 | 说明 |
|---|---|
| 写压力集中 | 所有写操作都打在一台机器上,容易成为瓶颈 |
| 读压力集中 | 读写都在一台机器,高并发时扛不住 |
| 单点故障 | 机器宕机,整个服务不可用 |
| 备份影响性能 | 在主库做备份可能影响线上服务 |
解决方案:主从架构
- 一个主库(Master):负责写
- 一个或多个从库(Slave/Replica):负责读
好处:
- 读写分离,分散压力
- 从库可以做备份
- 从库可以做容灾切换
二、主从复制的基本原理
核心依赖:binlog
简化流程
主库(Master):
- 执行 SQL(INSERT / UPDATE / DELETE)
- 把变更记录到 binlog
从库(Slave): 3. 从库有一个 IO 线程,不断拉取主库的 binlog 4. 拉取到的 binlog 写入从库的 relay log(中继日志) 5. 从库有一个 SQL 线程,读取 relay log,重放这些变更
完整流程图
主库:
执行 SQL -> 写 binlog
从库:
IO 线程 -> 拉取主库 binlog -> 写 relay log
SQL 线程 -> 读 relay log -> 重放 SQL一句话总结
主库把变更写到 binlog,从库拉取并重放,从而保持数据同步。
"重放"到底是什么意思?
一句话理解
重放就是把主库执行过的操作,在从库上再执行一遍。
用一个例子说明
假设主库执行了:
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 语句。
-- 主库执行:
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())时,主从可能不一致 |
-- 主从不一致的例子:
UPDATE user SET update_time = NOW() WHERE id = 1;
-- 主库执行时是一个时间,从库重放时又是另一个时间2. Row(行模式)
binlog 里记录的是数据行的变更。
-- 主库执行:
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 线程重放 |
怎么判断主从延迟?
在从库执行:
SHOW SLAVE STATUS\G重点看 Seconds_Behind_Master:从库落后主库的秒数。0 = 基本同步,几十/几百 = 延迟严重。
五、主从延迟怎么解决?
| 方案 | 说明 |
|---|---|
| 优化主库 | 避免大事务、分批写入、控制写入 TPS |
| 提升从库硬件 | 更好的 CPU / SSD,增加内存 |
| 开启并行复制 | MySQL 5.7+ 支持 slave_parallel_workers = 4 |
| 分库分表 | 单库写压力太大时考虑拆分 |
| 业务层容忍延迟 | 统计报表、非核心查询可以接受短时间延迟 |
| 读主库 | 必须读到最新数据时直接读主库 |
并行复制配置示例:
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,才返回成功。减少数据丢失风险。
-- 主库
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 怎么生成
- 分库分表后的查询问题
- 扩容怎么做
- 分布式事务怎么处理