📖 本篇简介:主从复制(Replication)是 MySQL 高可用架构的基石——它让一台主库的数据实时同步到多台从库,从而分担读压力、提供故障切换能力。本篇系统讲解 binlog 三种格式、主从搭建流程、GTID 复制、半同步复制、主从延迟监控与故障切换方案。学完本篇,你将能独立搭建一套一主两从的复制环境,并能在主库宕机时把业务切到从库。本系列示例基于 MySQL 8.0 语法。
目录
- 1 主从复制原理
- 2 Binlog 三种格式
- 3 主从搭建实操
- 4 GTID 复制
- 5 半同步复制
- 6 主从延迟监控与处理
- 7 故障切换方案
- 8 主从复制速查表
- 9 常见坑与注意事项
- 10 练习题与参考答案
1 主从复制原理
主从复制的核心思路:主库把所有写操作记录到 binlog(二进制日志),从库拉取主库的 binlog 并在本地重放,从而保持与主库数据一致。整个过程由三个线程协作完成:
| 线程 | 所在节点 | 职责 |
|---|---|---|
| Binlog Dump Thread | 主库 | 把 binlog 发给从库 |
| I/O Thread | 从库 | 接收主库发来的 binlog,写到本地 relay log(中继日志) |
| SQL Thread | 从库 | 读取 relay log,重放 SQL,更新本地数据 |
复制流程可以简化成:
主库 ──写 binlog──> Binlog Dump
│
▼ 网络传输
从库 I/O Thread ──写 relay log──> SQL Thread ──重放──> 从库数据
MySQL 8.0 中 SQL Thread 已经从单线程升级为并行复制(基于组提交),默认可以并行重放多个数据库的事务,从库追平速度大幅提升。
1.1 异步复制 vs 同步复制
默认的主从复制是异步复制(Async Replication):主库写完 binlog 就返回成功,不等从库接收。优点是性能好,缺点是主库宕机时,可能有一部分 binlog 还没传到从库,数据就丢了。
如果业务不能接受丢数据,就要用半同步复制(Semi-sync)——主库至少等一台从库确认收到 binlog 才返回成功。这是本章后面要讲的重点。
1.2 并行复制
MySQL 5.7 之前从库 SQL 线程是单线程的,主库每秒写 1000 个事务,从库一秒只能重放 100 个,延迟越拉越大。MySQL 8.0 的并行复制(基于 WRITESET)让"不修改同一行的事务"可以在从库并行重放,并发能力接近主库。
-- 查看并行复制模式
SHOW VARIABLES LIKE 'binlog_transaction_dependency_tracking';
-- 默认 COMMIT_ORDER;WRITESET 模式并行度更高,但要求主库开 binlog 行镜像
生产环境开 WRITESET 的前提:主库 binlog 必须是 ROW 格式(本篇推荐的格式),并且 binlog_row_image=FULL。
2 Binlog 三种格式
binlog 是主从复制的源头。它有三种记录格式,直接影响从库重放的正确性和体积:
| 格式 | 记录内容 | 优点 | 缺点 |
|---|---|---|---|
| STATEMENT(SBR) | 原始 SQL 语句 | 日志小、可读性好 | 函数(NOW()、UUID())在从库执行结果可能不同 |
| ROW(RBR) | 每一行的前后镜像 | 绝对正确 | 日志体积大(一次 UPDATE 百万行就记百万行) |
| MIXED | 默认 STATEMENT,遇到不确定函数自动切 ROW | 兼顾 | 调试稍麻烦 |
-- 查看当前 binlog 格式
SHOW VARIABLES LIKE 'binlog_format';
-- 设置为 ROW(生产强烈推荐)
SET GLOBAL binlog_format = 'ROW';
生产环境几乎都用 ROW。理由:STATEMENT 下 UPDATE ... WHERE create_time = NOW() 这种 SQL 在从库执行时 NOW() 已经不是同一时刻,会造成主从不一致。ROW 格式直接记"id=100 这行从 X 改成 Y",绝对正确。
2.1 查看 binlog
-- 查看所有 binlog 文件
SHOW BINARY LOGS;
-- 查看当前正在写的 binlog
SHOW MASTER STATUS;
-- 解析某个 binlog 的内容(系统命令 mysqlbinlog)
-- mysqlbinlog --start-datetime="2026-10-07 10:00:00" /var/lib/mysql/binlog.000001
SHOW MASTER STATUS 输出里的 File 和 Position 两个字段,是从库"从哪里开始拉 binlog"的坐标——搭建主从时必须告诉从库。
3 主从搭建实操
下面演示一台主库(192.168.1.10)搭一台从库(192.168.1.11)的完整流程。
3.1 主库配置
[mysqld]
# 主库唯一 server_id(从库不能重复)
server_id = 100
# 开启 binlog
log_bin = /var/lib/mysql/binlog
# binlog 格式用 ROW
binlog_format = ROW
# 只同步需要的库(可选)
binlog_do_db = shop
# binlog 过期天数(自动清理)
expire_logs_days = 7
配置完重启 MySQL,在主库上创建复制账号:
-- 创建从库复制专用账号,只允许从 192.168.1.11 连接
CREATE USER 'repl'@'192.168.1.11' IDENTIFIED BY 'Repl@2026';
-- 授予复制权限
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'192.168.1.11';
FLUSH PRIVILEGES;
-- 锁表,确保数据一致性(只读锁,不阻塞读)
FLUSH TABLES WITH READ LOCK;
-- 记录当前 binlog 坐标(File + Position),搭建时要用
SHOW MASTER STATUS;
-- 假设输出:binlog.000003 1572
-- 此时用 mysqldump 备份主库数据到从库,备份完再 UNLOCK
-- mysqldump -uroot -p --single-transaction --master-data=2 shop > shop.sql
UNLOCK TABLES;
3.2 从库配置
[mysqld]
# 从库 server_id 必须与主库不同
server_id = 101
# 开启 relay log
relay_log = /var/lib/mysql/relaylog
# 从库默认只读(超级用户除外)
read_only = ON
# 防止从库被误写
super_read_only = ON
把主库导出的 shop.sql 导入从库,然后在从库上指定主库坐标:
-- 告诉从库:主库在哪里、用什么账号、从哪个 binlog 坐标开始拉
CHANGE REPLICATION SOURCE TO
SOURCE_HOST='192.168.1.10',
SOURCE_USER='repl',
SOURCE_PASSWORD='Repl@2026',
SOURCE_LOG_FILE='binlog.000003',
SOURCE_LOG_POS=1572;
-- MySQL 8.0 之前的旧语法是 MASTER_HOST/MASTER_LOG_FILE 等,仍兼容
-- 启动复制
START REPLICA;
-- 查看从库状态
SHOW REPLICA STATUS\G
SHOW REPLICA STATUS\G 输出里关键看两行:
Replica_IO_Running: Yes:I/O 线程正常拉 binlog;Replica_SQL_Running: Yes:SQL 线程正常重放。
两个都是 Yes 才算搭好了。
4 GTID 复制
传统主从要手动记 binlog 文件和位置(File + Position),主库换了、binlog 轮转过,坐标就乱了。GTID(Global Transaction Identifier)是 MySQL 5.6 引入的全局事务编号,每个事务在主库执行时自动分配一个全局唯一 ID,从库自动识别"我已经执行到哪个 GTID",不用再手动记坐标。
4.1 开启 GTID
[mysqld]
# 主库和从库都要开
gtid_mode = ON
enforce_gtid_consistency = ON
开启后,从库搭建就简化成:
-- 从库:直接告诉它主库地址,自动找位置
CHANGE REPLICATION SOURCE TO
SOURCE_HOST='192.168.1.10',
SOURCE_USER='repl',
SOURCE_PASSWORD='Repl@2026',
SOURCE_AUTO_POSITION=1; -- 关键:自动定位 GTID
START REPLICA;
4.2 查看 GTID
-- 主库已经执行过的 GTID
SHOW MASTER STATUS;
-- Executed_Gtid_Set 字段显示已执行的所有 GTID
-- 从库已经接收到、重放过的 GTID
SHOW REPLICA STATUS\G
-- Retrieved_Gtid_Set / Executed_Gtid_Set
GTID 的好处:主库宕机、切到新主库时,从库只要 CHANGE SOURCE 指过去,自动知道"新主库上哪些事务我已经有了",跳过不重复执行。这是 MHA、Orchestrator 等高可用方案能自动切换的基础。
4.3 用 Python 模拟 GTID 集合对比(本机实跑)
GTID 本质上是一个全局唯一的事务编号,格式是 uuid:seq。从库判断"我有没有执行过某个 GTID",就是在自己的已执行集合里查一下。
# 模拟主库和从库的 GTID 集合
master_executed = {"host1:1", "host1:2", "host1:3", "host2:1", "host2:2"}
slave_executed = {"host1:1", "host1:2", "host2:1"}
# 从库还需要从主库补哪些事务
missing = master_executed - slave_executed
print("从库还缺的 GTID =", sorted(missing))
print("从库已有的 GTID =", sorted(slave_executed))
实跑输出:
从库还缺的 GTID = ['host1:3', 'host2:2']
从库已有的 GTID = ['host1:1', 'host1:2', 'host2:1']
这个集合差就是从库启动后要从主库拉取并执行的事务。自动定位(SOURCE_AUTO_POSITION=1)的本质就是:从库把自己的 GTID 集合告诉主库,主库只发集合差集里的事务。
5 半同步复制
异步复制的风险是主库宕机时丢数据。半同步(Semi-sync)让主库至少等一台从库"收到 binlog 并写入 relay log"才返回成功,牺牲一点性能换数据安全。
5.1 安装半同步插件
-- 主库安装半同步模块(Master 端)
INSTALL PLUGIN rpl_semi_sync_source SONAME 'semisync_source.so';
SET GLOBAL rpl_semi_sync_source_enabled = 1;
-- 从库安装半同步模块(Replica 端)
INSTALL PLUGIN rpl_semi_sync_replica SONAME 'semisync_replica.so';
SET GLOBAL rpl_semi_sync_replica_enabled = 1;
-- 主库至少等几个从库确认
SET GLOBAL rpl_semi_sync_source_wait_for_replicas = 1;
-- 超时时间:等不到从库确认就降级为异步(默认 10s)
SET GLOBAL rpl_semi_sync_source_timeout = 10000;
5.2 半同步的降级机制
如果所有从库都挂了、主库等不到确认,超过 timeout 后,主库会自动降级为异步复制,继续服务,不会一直堵着。等从库恢复后,再自动切回半同步。这种"宁可慢、不可丢"的模式是金融、订单类业务的标配。
6 主从延迟监控与处理
主从复制最大的痛点是主从延迟(Replication Lag)——主库已经执行了 1000 条事务,从库才重放了 800 条。如果读流量走从库,用户就会看到"刚下单却查不到订单"。
6.1 查看延迟
-- 在从库上查看
SHOW REPLICA STATUS\G
-- 关键看:
-- Seconds_Behind_Master: 3 (落后主库多少秒)
Seconds_Behind_Master 的原理是:从库 SQL 线程正在重放的事务的"主库执行时间"和当前系统时间之差。这个值为 0 不代表完全一致,只能粗略参考。
更准确的方式是在主库写入一个时间戳表,从库查询这行的时间戳与当前时间差:
-- 主库上建心跳表
CREATE TABLE repl_heartbeat (ts DATETIME);
-- 主库定时(每秒)写一次
-- UPDATE repl_heartbeat SET ts = NOW();
-- 从库查询:当前时间 - 这行的 ts 就是真实延迟
SELECT TIMESTAMPDIFF(SECOND, ts, NOW()) AS lag FROM repl_heartbeat;
6.2 延迟常见原因
| 原因 | 表现 | 解决 |
|---|---|---|
| 从库单线程重放(老版本) | 大事务卡很久 | 升级 8.0 并行复制 |
| 从库性能差(CPU/IO 弱) | 延迟随写入波动 | 从库硬件升级 |
| 大事务(一次性 UPDATE 百万行) | 延迟突然飙升 | 拆小事务、分批提交 |
| 从库开了慢查询、备份 | 抢占 IO | 错峰、隔离 |
| 主库 binlog 格式是 ROW 大事务 | 从库重放慢 | 拆事务 |
6.3 减少延迟的手段
1. 主库写大事务拆小:一次提交 100 行,不要一次提交 100 万行;
2. 从库开并行复制(MySQL 8.0 默认基于 WRITESET 并行);
3. 从库硬件不弱于主库,最好是 SSD;
4. 关键读("刚下单查订单")强制走主库,不要为了分担压力把这种强一致读也走从库。
7 故障切换方案
主库宕机后,要把业务切到新主库,这个过程叫"故障切换"(Failover)。常见方案:
| 方案 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 手动切换 | DBA 手动 CHANGE MASTER | 简单 | 慢、依赖人 |
| MHA | 自动选最从从库提升为主 | 成熟 | 要装 Perl、维护复杂 |
| Orchestrator | Go 写的自动切换工具 | Web 界面、可视化 | 要单独部署 |
| 云厂商 RDS | 云自带高可用 | 零运维 | 锁定云 |
| MySQL InnoDB Cluster | 官方 MGR + Router | 官方推荐 | 学习成本高 |
7.1 切换的核心步骤
手动切换一次主库的步骤(以主库挂了、从库 192.168.1.11 提升为新主库为例):
1. 确认老主库真的挂了(避免脑裂);
2. 在从库上执行 STOP REPLICA;
3. 确认从库已经追完所有 relay log(Seconds_Behind_Master=0);
4. 在从库上 RESET MASTER 或清理旧复制配置;
5. 把应用连接指向新主库;
6. 原来的其他从库重新 CHANGE SOURCE TO 指向新主库。
-- 新主库(原来的从库):解除只读
SET GLOBAL read_only = OFF;
SET GLOBAL super_read_only = OFF;
-- 其他从库:改指向新主库
CHANGE REPLICATION SOURCE TO
SOURCE_HOST='192.168.1.11',
SOURCE_USER='repl',
SOURCE_PASSWORD='Repl@2026',
SOURCE_AUTO_POSITION=1;
START REPLICA;
7.2 脑裂问题
最危险的情况:老主库其实没挂,只是网络断了。这时候你把业务切到新主库,老主库又"复活"继续接收写,两边都在写,数据就分叉了(脑裂)。
解决:
- 切换前用第三方手段(比如 VIP 漂移脚本、云负载均衡)确认老主库真的不可达;
- 老主库恢复后,先
RESET MASTER再以从库身份加入新主库,不要直接对外服务。
8 主从复制速查表
| 命令/视图 | 作用 |
|---|---|
| SHOW MASTER STATUS | 主库当前 binlog 坐标 |
| SHOW REPLICA STATUS\G | 从库 IO/SQL 线程状态 |
| START REPLICA | 启动从库复制 |
| STOP REPLICA | 暂停从库复制 |
| SHOW BINARY LOGS | 列出主库所有 binlog |
| CHANGE REPLICATION SOURCE TO ... | 配置主库坐标 |
| Seconds_Behind_Master | 从库延迟秒数 |
| gtid_executed | 当前已执行的 GTID 集合 |
| mysqlbinlog 命令行 | 解析 binlog 文件 |
| rpl_semi_sync_source_enabled | 主库半同步开关 |
关键参数速记:
| 参数 | 作用 | 推荐值 |
|---|---|---|
| server_id | 实例唯一编号 | 主从不重复 |
| log_bin | 开启 binlog | ON |
| binlog_format | binlog 格式 | ROW |
| gtid_mode | 开启 GTID | ON |
| relay_log | 中继日志路径 | 独立磁盘 |
| read_only | 从库只读 | ON |
| expire_logs_days | binlog 保留天数 | 7 |
9 常见坑与注意事项
坑 1:主从 server_id 一样。
如果主从 server_id 配重复,从库会报错、拒绝复制。每个实例必须唯一,一般用 IP 末段做编号。
坑 2:从库开了 read_only 但超级用户还能写。
read_only=ON 只限制普通用户,root 超级用户依然能写。要同时开 super_read_only=ON,连 root 都不能写。
坑 3:binlog 格式用 STATEMENT。
线上跑了几年 STATEMENT,某天发现从库数据错了,追了半天是 NOW() / UUID() 函数导致的。生产一律 ROW。
坑 4:大事务导致主从延迟飙升。
一次 UPDATE orders SET ... WHERE create_time < '2025-01-01' 刷了 500 万行,主库一秒完成,从库重放要半小时。大事务必须拆小,比如按主键区间分批。
坑 5:以为从库绝对只读。
从库上不小心执行了一个写操作(比如手工修数据),这行就在主从之间分叉了,后续复制可能直接报错。从库写操作要通过主库代理。
坑 6:脑裂。
网络抖动时自动切换,老主库又活过来,两边都在写。一定要有"老主库复活后自动降级为从库"的机制,不能让它继续接流量。
坑 7:binlog 没设过期,磁盘被撑爆。
主库一直写 binlog 不清理,几个月后磁盘满、MySQL 挂掉。必须设 expire_logs_days 或 binlog_expire_logs_seconds。
10 练习题与参考答案
练习 1:主从复制的三个线程分别叫什么、在主还是从、做什么?
<details>
<summary>参考答案</summary>
Binlog Dump Thread(主库):把 binlog 发给从库;I/O Thread(从库):接收 binlog 写到 relay log;SQL Thread(从库,8.0 已并行化):读 relay log 重放 SQL。
</details>
练习 2:为什么生产环境 binlog 推荐用 ROW 格式?
<details>
<summary>参考答案</summary>
STATEMENT 格式记录原始 SQL,像 NOW()、UUID()、RAND() 这类函数在主从执行时结果不同,会导致主从不一致。ROW 格式记录每一行的前后镜像,从库直接按镜像改,绝对正确。代价是日志体积大,但磁盘现在便宜,正确性优先。
</details>
练习 3:从库 Seconds_Behind_Master 显示 100 秒,你会怎么排查和处理?
<details>
<summary>参考答案</summary>
1. 先看从库是否在重放大事务:SHOW REPLICA STATUS 看 Executed_Gtid_Set 是否在推进;
2. 查主库最近是否有大事务:SHOW BINLOG EVENTS;
3. 看从库 CPU/IO 是否打满;
4. 长期延迟要从库硬件升级、开并行复制、把大事务拆小。
</details>
练习 4:半同步复制相比异步复制,牺牲了什么、换来了什么?
<details>
<summary>参考答案</summary>
牺牲了主库的写入响应时间(要等从库确认收到 binlog);换来了"主库宕机时已提交事务不丢"的可靠性。当从库不可用时,半同步会超时降级为异步,避免主库被卡死。
</details>
练习 5:主库宕机要切到从库,怎么避免脑裂?
<details>
<summary>参考答案</summary>
切换前用第三方监控/HA 软件确认老主库真的不可达;老主库恢复后不能直接对外服务,要先清空本地 binlog、以从库身份加入新主库;应用层通过 VIP 或代理(如 ProxySQL)统一入口,切换时只改指向,不让应用直连老主库。
</details>
下一篇预告:第 23 章 分库分表与分区
文章回复
0 条公开回复