3738 字
19 分钟
MySQL 主从架构与读写分离

MySQL 主从复制是构建高并发读写分离架构的基石——主库负责写入,从库承担读流量。但 Binlog 的异步传输机制天然带来主从延迟,导致”刚写完却读不到”的经典分布式问题。理解复制的三步流程、延迟的根因,以及强制路由/缓存/业务降级三种破局方案,是后端工程师的必备技能。


目录#

  1. 主从复制原理
  2. 主从延迟:根因分析
  3. 解决方案
  4. 进阶:半同步复制
  5. 常见面试题
  6. 总结

1. 主从复制原理#

1.1 架构全景#

在稍微有体量的 Go/Java 后端项目中,数据库不可能只有单机一个节点。主从架构(一主多从)将写操作全部打到主库,读操作分散到多个从库,极大提升并发读取能力。

┌─────────────────────────────────────────────────────────────────┐
│ MySQL 主从架构全景 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ │
│ │ 应用服务 │ │
│ │ (Go 项目) │ │
│ └──┬───────┬───┘ │
│ │ │ │
│ ┌──▼──┐ ┌─▼──────────────────────────┐ │
│ │ 写 │ │ 读 (负载均衡) │ │
│ └──┬──┘ └─┬──────────┬──────────┬────┘ │
│ │ │ │ │ │
│ ┌──▼───────▼──┐ ┌────▼────┐ ┌───▼──────┐ │
│ │ 主库 │ │ 从库1 │ │ 从库2 │ ... │
│ │ (Master) │ │ (Slave) │ │ (Slave) │ │
│ │ INSERT │ │ SELECT │ │ SELECT │ │
│ │ UPDATE │ │ │ │ │ │
│ │ DELETE │ └─────────┘ └──────────┘ │
│ └──────┬──────┘ │
│ │ Binlog 异步复制 │
│ ├─────────────→ 从库1 │
│ ├─────────────→ 从库2 │
│ └─────────────→ ... │
│ │
└─────────────────────────────────────────────────────────────────┘

核心分工:

角色职责特点
主库 (Master)所有写操作(INSERT/UPDATE/DELETE)单点写入,数据最新
从库 (Slave)承担大部分读操作(SELECT)可水平扩展,数据略有延迟

1.2 复制三步流程#

MySQL 的主从复制不是魔法般的瞬间同步,而是经过一个异步的、跨网络的三步流程:

主从复制三步流程:
主库 (Master) 从库 (Slave)
┌──────────────────┐ ┌──────────────────────┐
│ │ │ │
│ ① 执行 INSERT │ │ │
│ 写入 Binlog │ │ │
│ (二进制日志) │ │ │
│ │ │ │ │
│ 返回客户端成功 │ Binlog │ │
│ 主库任务结束 ✅ │ ═══════════════→ │ ② I/O 线程拉取 Binlog │
│ │ 网络传输 │ 写入 Relay Log │
│ │ │ (中继日志) │
│ │ │ │ │
│ │ │ ③ SQL 线程读取 │
│ │ │ Relay Log │
│ │ │ 逐条重放执行 │
│ │ │ 从库数据更新 ✅ │
└──────────────────┘ └──────────────────────┘
关键时间点:
T1: 主库执行完毕,返回客户端 ← 客户端认为"写入成功"
T2: Binlog 网络传输到从库
T3: SQL 线程重放完毕 ← 此时从库数据才真正一致
主从延迟 = T3 - T1(通常毫秒级 ~ 秒级)
步骤执行者产物说明
① 主库记录主库Binlog(二进制日志)主库执行完 SQL 后将修改写入 Binlog,随即返回客户端成功
② 从库拉取从库 I/O 线程Relay Log(中继日志)I/O 线程持续从主库拉取最新 Binlog,写入从库本地的 Relay Log
③ 从库重放从库 SQL 线程数据更新SQL 线程读取 Relay Log,在从库上逐条重新执行这些操作

2. 主从延迟:根因分析#

典型故障场景:代码里执行 INSERT 往主库注册用户 → 下一行代码去从库 SELECT 查这个用户 → 返回空数据 💥

产生延迟的三层根因:

延迟的根因分析:
① 网络传输耗时
Binlog 从主库通过网络传到从库 → 跨机房/跨地域时延迟更明显
(同机房通常 < 1ms,跨地域可能数十 ms)
② SQL 线程串行重放(核心瓶颈)
主库:多个连接并发写入 → 并发度极高
从库:早期版本 SQL 线程是单线程!
┌──────────────────────────────────────────────┐
│ 主库并发写入(多线程) vs 从库重放(单线程) │
│ │
│ 连接1: INSERT ──┐ │
│ 连接2: UPDATE ──┤ Binlog ┌─ SQL线程 ──┤
│ 连接3: DELETE ──┼─────────────→│ 逐条排队 │
│ 连接4: INSERT ──┘ │ 顺序执行 │
│ └─────────────│
│ 主库 1 秒完成 1000 条 从库需要 > 1 秒 │
└──────────────────────────────────────────────┘
③ 从库自身负载
大量读请求打到从库 → CPU/IO 竞争 → SQL 线程重放变慢

核心结论:从库的数据状态永远比主库慢半拍。如果你的代码执行极快——主库刚写完,下一行代码立刻查从库——此时从库的 SQL 线程还没来得及重放到那条数据,自然查出空结果。

影响因素:

因素影响程度说明
主库写入并发量🔴 高并发越高,从库单线程重放排队越严重
网络延迟🟡 中同机房影响小,跨地域明显
从库机器性能🟡 中CPU/磁盘不如主库时重放更慢
Binlog 格式 (ROW vs STATEMENT)🟢 低ROW 格式数据量大但更安全
大事务🔴 高一个大事务的 Binlog 在从库重放耗时很长

3. 解决方案#

面对”刚写完立刻读”的强一致性场景,有三种标准打法,按推荐优先级排列。

3.1 方案 1:强制路由——写后读走主库#

最常用、最有效的技术手段。

实现原理:
同一个请求上下文 (Context) 内:
┌─────────────────────────────────────────────────────┐
│ func RegisterUser(ctx context.Context, ...) { │
│ │
│ // 写操作 → 路由到主库 │
│ db.Master().Insert(user) │
│ │
│ // ⚠️ ctx 被标记为"包含写操作" │
│ ctx = WithWriteFlag(ctx) │
│ │
│ // 后续读操作 → 检测到 WriteFlag → 也走主库 │
│ user := db.Slave().Select(...) // ← 实际走主库! │
│ │
│ } │
└─────────────────────────────────────────────────────┘
  • 实现方式:在数据库中间件(如 MyCat/ShardingSphere)或 ORM 框架层,通过 Context 传参控制——当前请求上下文中如果执行了写操作,在这个请求结束之前,后续所有读操作强制路由到主库
  • 优点:绝对的数据一致性,代码改造成本极低,无需引入额外组件
  • 缺点:会稍微增加主库的读压力(但”写后立刻读”场景占比极低,影响可忽略)

3.2 方案 2:引入缓存——Redis 扛读#

既然从库有延迟,干脆不去从库查。

双写 + 缓存优先:
┌──────────────┐ ① 写入 ┌──────────────┐
│ 应用服务 │ ─────────────→ │ 主库 MySQL │
│ │ └──────────────┘
│ │ ② 双写 ┌──────────────┐
│ │ ─────────────→ │ Redis │
└──────┬───────┘ └──────┬───────┘
│ │
│ ③ 优先读 Redis │
└───────────────────────────────┘
如果 Redis 命中 → 直接返回(无延迟问题)
如果 Redis 未命中 → 降级查从库 → 回填缓存
  • 实现方式:向主库写入数据的同时,双写一份到 Redis 缓存(或通过监听 Binlog 异步写入 Redis);后续读操作优先走 Redis
  • 优点:性能极高,大大缓解数据库压力;适合高并发读场景
  • 缺点:引入了”缓存与数据库一致性”难题,架构复杂度上升;需要考虑缓存过期/更新策略

3.3 方案 3:业务妥协——延迟跳转#

如果技术上不想做得太复杂,通过业务流程改造来”掩盖”延迟。

前端延迟跳转:
用户点击"注册"
│
▼
后端写入主库 ──→ 返回成功
│
▼
前端展示:"注册成功,正在为您生成档案..." (1~2 秒动画)
│
│ ← 这 2 秒足够从库完成同步
│
▼
跳转个人主页 → 查从库 → 数据已同步 ✅
  • 实现方式:用户操作后不立刻跳转到结果页,而是展示持续 1~2 秒的过渡动画,再拉取数据
  • 优点:零技术成本,用户体验甚至可以更好(有心理缓冲)
  • 缺点:只是”掩盖”延迟,没有真正解决;不适用于 API 接口场景(无 UI 可展示过渡)

3.4 方案对比#

维度强制路由(走主库)Redis 缓存业务妥协(延迟跳转)
一致性✅ 绝对一致⚠️ 缓存一致性需额外处理❌ 只是掩盖延迟
实现复杂度⭐ 低⭐⭐⭐ 高⭐ 极低
对主库压力微增(场景极少)无影响无影响
适用场景所有”写后立刻读”场景高并发读 + 可容忍短期不一致C 端产品,有 UI 页面
推荐度⭐⭐⭐ 首选⭐⭐ 按需⭐ 兜底

4. 进阶:半同步复制#

标准主从复制是异步的:主库写入 Binlog 后立刻返回,不等待从库确认。半同步复制(Semi-Sync Replication)在”异步”和”全同步”之间取了一个折中。

异步复制 vs 半同步复制:
异步复制(默认):
主库写 Binlog → 立刻返回客户端 ✅
└→ 从库异步拉取(不知何时完成)
优点:主库写入性能最高
缺点:主库宕机时可能丢失未同步的数据
半同步复制(Semi-Sync):
主库写 Binlog → 等待至少 1 个从库确认收到 Binlog → 返回客户端 ✅
└→ 从库不一定重放完了
优点:数据安全性更高(至少有一个从库有这份 Binlog)
缺点:主库写入延迟增加(等待网络往返)
对比维度异步复制半同步复制
主库写入延迟低(不等待)中(等至少 1 个从库 ACK)
数据安全性可能丢数据至少 1 个从库有 Binlog
对主库性能影响无有(网络 RTT 开销)
是否解决读延迟❌ 不解决⚠️ 只是收到 Binlog,从库不一定重放完毕

注意:半同步复制只保证 Binlog 已传输到从库,不保证 SQL 线程已经重放完毕——所以它解决的是”数据安全性”问题(主库宕机不丢数据),而非”主从读延迟”问题。


5. 常见面试题#

Q1:为什么会出现”刚写完却读不到”的情况?#

A1:MySQL 主从复制是异步的,分为三步——主库写 Binlog → 从库 I/O 线程拉取到 Relay Log → 从库 SQL 线程重放。整个过程存在网络传输耗时 + SQL 线程重放排队。代码在主库写入后立即查从库时,SQL 线程可能还没来得及重放这条数据,导致查不到。

Q2:主从延迟产生的根本原因是什么?#

A2:三个层面:

  1. 网络传输:Binlog 从主库传输到从库需要跨网络
  2. SQL 线程瓶颈(核心):从库 SQL 线程重放是串行的(早期版本单线程),而主库是多连接并发写入——主库 1 秒的并发写入量,从库可能需要更长时间才能逐条重放完毕
  3. 从库负载:大量读请求占用从库 CPU/IO,拖慢重放速度

Q3:如何解决主从延迟带来的读写不一致?#

A3:三种方案(按推荐优先级):

  1. 强制路由(首选):通过 ORM/中间件,在写操作后的同一请求上下文中,将后续读操作强制路由到主库——绝对一致,成本最低
  2. Redis 缓存:写主库的同时双写 Redis,读操作优先走缓存——性能高,但引入了缓存一致性问题
  3. 业务妥协:前端延迟跳转(过渡动画 1~2 秒),利用时间窗口掩盖延迟——零技术成本,但不适用于 API 场景

Q4:半同步复制能解决主从读延迟吗?#

A4:不能。半同步复制(Semi-Sync)只保证 Binlog 已经传输到从库的 Relay Log,但不保证 SQL 线程已经重放完毕。它解决的是”主库宕机后数据不丢失”的安全性需求,而非”从库数据是否已更新”的读延迟需求。

Q5:主从架构下,从库可以写吗?#

A5:技术上可以,但绝对不推荐。如果往从库写入数据,从库的数据会和主库产生分歧,导致主从数据不一致。一旦发生主从切换,从库上的”私自有数据”会丢失或导致切换后数据错乱。标准做法是 read_only = ON 锁定从库。


6. 总结#

6.1 主从架构全景#

┌─────────────────────────────────────────────────────────────────┐
│ MySQL 主从复制全景 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ │
│ │ 主库 │ ← 写操作 (INSERT/UPDATE/DELETE) │
│ │ Master │ │
│ └────┬─────┘ │
│ │ Binlog │
│ │ (异步/半同步) │
│ ▼ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 从库1 │ │ 从库2 │ │ 从库3 │ ← 读操作 (SELECT) │
│ │ Slave │ │ Slave │ │ Slave │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ 复制流程: │
│ 主库: 执行 SQL → 写 Binlog → 返回 │
│ 从库: I/O 线程拉 Binlog → 写 Relay Log → SQL 线程重放 │
│ │
│ 核心矛盾: │
│ 写入是即时的(主库返回 = 成功) │
│ 同步是异步的(从库需要时间追上) │
│ → "刚写完读不到"是分布式系统的必然现象 │
│ │
│ 解决路径: │
│ ① 强制路由(写后读走主库)—— 首选,绝对一致 │
│ ② Redis 缓存 —— 高性能,有缓存一致性成本 │
│ ③ 业务妥协(延迟跳转)—— 零技术成本,非 API 场景可用 │
│ │
└─────────────────────────────────────────────────────────────────┘

6.2 关键设计原则#

原则说明
主写从读,各司其职主库只写,从库只读——这是读写分离架构的基本契约
复制天生异步Binlog 传输 + Relay Log 重放都需要时间,从库永远比主库慢半拍
延迟根因在 SQL 线程主库多连接并发 vs 从库单线程重放——这是速度差的本质
强制路由是首选“谁写的谁去主库读”——同一请求上下文内走主库,实现最简单、一致性最强
半同步不解决读延迟Semi-Sync 只保证 Binlog 已传输,不保证已重放
读延迟 ≠ 数据丢失主从延迟是”暂时读不到”,不是”数据丢了”——等从库追上来就能读到

6.3 快速排查清单#

排查点看什么工具/命令
主从延迟秒数Seconds_Behind_MasterSHOW SLAVE STATUS\G
复制是否正常Slave_IO_Running / Slave_SQL_RunningSHOW SLAVE STATUS\G
是否有复制错误Last_IO_Error / Last_SQL_ErrorSHOW SLAVE STATUS\G
从库是否只读read_only / super_read_onlySHOW VARIABLES LIKE '%read_only%'
Binlog 格式binlog_format (ROW/STATEMENT/MIXED)SHOW VARIABLES LIKE 'binlog_format'
半同步是否开启rpl_semi_sync_master_enabledSHOW VARIABLES LIKE '%semi_sync%'

6.4 一句口诀#

主写从读异步传,SQL 重放是瓶颈;写完立刻查主库,缓存兜底前端等。


相关链接#


本文基于 MySQL 8.0 编写,主从复制核心机制(Binlog + Relay Log + I/O/SQL 线程)在所有 5.5+ 版本中通用。

MySQL 主从架构与读写分离
https://shirone.mysqil.com/posts/mysql/mysql-主从架构与读写分离/
作者
HikariTish
发布于
2026-05-27
许可协议
CC BY-NC-SA 4.0

分享文章

生成精美分享图或复制链接,与更多人分享本文。

继续阅读

沿着主题读

基于共同的标签与分类

换条路线

从其他文章中稳定抽取

最后更新于 ,距今已过 141 天

部分内容可能已过时