昨天凌晨两点,我被一条 Too many connections 的告警从睡梦中炸醒。业务库里一个统计报表的聚合查询直接把连接池打满,所有写入请求全部排队,前端商品下单超时刷了一整屏。那一刻我意识到:单库单机扛流量的日子到头了。后面的解决方案并不新鲜——上一套 MySQL 主从复制,把只读流量分流到从库,主库专心写。但真正动手的时候才发现,网上教程说得再轻巧,实操里全是细节坑。这篇文章是我自己从零搭建、排障、优化主从复制的完整记录,适合刚接触数据库的开发者,也适合准备在项目里引入读写分离的团队参考。
1. 先搞懂主从复制在解决什么问题
1.1 一台数据库机器为什么不够用了
大多数小项目初期都是单库跑,一台 MySQL 实例既负责写入又负责查询,数据量在百万级以内时问题不大。可一旦业务做起来,数据量过千万,或者出现几个特别重的分析型查询,单库的 CPU、IO、连接数都会同时告急。
我遇到过最典型的场景是:业务高峰期,运营后台有人跑了一张大表的 GROUP BY 统计,这个查询要扫描全表,直接把主库的磁盘 IO 占满,导致线上所有写操作都变慢。后来加了索引也没用,因为统计口径多、查询条件灵活,索引根本覆盖不完。归根结底,是“读请求”和“写请求”在抢同一份资源。
这时候如果你的架构只有一台 MySQL,唯一的出路就是加机器。怎么加?不是用集群软件把两台机器拼成共享存储——MySQL 自己的解法就是主从复制。
1.2 主从复制的本质:一份数据,多台机器拥有副本
主从复制的核心逻辑简单说就是:主库(Master)处理写请求,并把所有数据变更记录成一种日志;从库(Slave)通过网络拉取这份日志,在自己的机器上重新执行一遍,从而让数据与主库保持一致。
基于这个机制,它能干三件事:
- 读写分离:把
SELECT查询、报表统计、后台管理等只读流量全部导到从库,主库的连接数和 IO 压力立刻降下来。 - 容灾切换:主库宕机时,从库还持有几乎最新的数据,可以快速提升为新主库,缩短业务恢复时间。
- 热备份与大数据分析:备份操作、数据分析查询可以在从库执行,不干扰主库业务,也降低了误操作对生产的影响。
1.3 谁需要认真学这套东西
如果你是后端开发,写业务接口时多多少少会碰到“读走从库、写走主库”的需求,不懂复制机制就容易在事务里读到旧数据;如果你是专职 DBA 或运维,那主从复制更是躲不开的基本功,连主从都搭不明白,后面说高可用、读写分离都是空中楼阁;如果你是架构师,你需要理解主从复制的性能边界和延迟窗口,才能在方案选型时做出合理判断。
一句话:主从复制是 MySQL 从单机走向分布式的第一步,几乎所有后续架构都是在这个基础上长出来的。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 复制链路的核心机制:binlog 到 relay log 的生命周期
2.1 三个线程的分工协作
主从复制不是一条消息直接推过去那么简单,它靠的是主库一个线程、从库两个线程,三个线程协同工作:
主库的 Binlog Dump 线程:当从库连接上来并请求日志时,主库会为这个从库启动一个 dump 线程,负责把 binlog 文件里的内容读出来,推送给从库。
从库的 IO 线程:负责连接主库,接收 binlog 内容,并写入从库本地的中继日志。中继日志是从库自己的文件,不会直接去改数据。
从库的 SQL 线程:负责读取中继日志中的事务,在从库上逐个重放,最终写入从库的数据文件。
我用一个生活化的类比来帮助理解:主库像出版社,binlog 像发行的书籍;从库的 IO 线程是搬运工,把书从出版社搬到自己家的仓库(中继日志);SQL 线程是学习者,把仓库里的书一本一本读进去,写成自己的笔记(实际数据)。因为“搬书”和“读书”是两个独立的工种,所以 IO 和 SQL 两条链路的运行状态要分开监控,一条断了另一条可能还在照常工作。
2.2 binlog 格式怎么选
这是很多人容易忽视但影响深远的一个配置。MySQL 的 binlog 有三种格式:
| 格式 | 记录内容 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| STATEMENT | SQL 语句原文 | 日志量小,可读性好 | 非确定性函数(如 NOW()、UUID())容易导致主从数据不一致 |
旧项目兼容、日志量敏感 |
| ROW | 每一行数据的实际变更前后值 | 最安全,精确复制每一行变化 | 日志量大,尤其是大批量 UPDATE/DELETE |
生产环境首选 |
| MIXED | 前两种混合 | 平衡日志量与安全性 | 需要理解切换规则 | 兼顾两者时的折中 |
我自己在新项目里一律用 ROW。现在的磁盘和带宽都不贵,ROW 格式多出的日志量完全可以接受,但 ROW 带来的数据一致性保障却是 STATEMENT 给不了的。MySQL 8.0 的默认 binlog 格式也是 ROW,说明官方也在引导大家走向更安全的方案。补充一句,实际工作中遇到过 STATEMENT 格式下执行 UPDATE t SET a = UUID() WHERE ... 导致主从数据完全对不上的事故,排查起来非常痛苦,这一点后面讲故障排查时再展开。
2.3 relay log 的写入与回放
从库从主库拿到的 binlog 内容,会先写入本地的 relay log(中继日志)。SQL 线程读取 relay log 并逐个应用,应用完后,relay log 里已经执行过的部分可以自动清理,避免磁盘无限增长。
很多人以为只要主库产生了 binlog,从库就会立刻同步更新。实际上默认的 MySQL 复制是异步的:主库提交事务后,不会等从库确认,直接从客户端返回成功。这就意味着,从库的数据在任意时刻都可能比主库落后一点点。极端情况下如果主库突然宕机,还没来得及同步到从库的数据就会丢失。后面讲半同步复制时会说到怎么把这个风险降到最低。
3. 动手搭建一套主从:从环境准备到复制建立全流程
3.1 环境与版本规划
我这次搭建用了两台 CentOS 7.9 服务器,MySQL 版本都是 8.0.36。强调一下:生产环境做主从,主从版本最好保持一致,或者从库版本不高于主库。如果从库版本比主库高,可能会出现 binlog 里的内容从库无法解析的问题,这种兼容性坑排查起来特别费时间。
主机规划参考:
| 角色 | 主机名 | IP | MySQL 版本 | 说明 |
|---|---|---|---|---|
| 主库 | db-master | 192.168.10.10 | 8.0.36 | 只处理写请求 |
| 从库 | db-slave | 192.168.10.11 | 8.0.36 | 处理读请求与备份 |
还有一个容易忽略的规划点:防火墙要放行 3306 端口,或者在主库的授权账号里限制只允许从库 IP 连接。我从库连接主库报 Host 'xxx' is not allowed to connect 就吃过一次亏,其实是授权时写错了 IP 段。
3.2 主库配置:my.cnf 关键参数
打开主库的 my.cnf,在 [mysqld] 段下加这几项:
ini复制[mysqld]
server-id = 1
log-bin = mysql-bin
binlog_format = ROW
binlog_row_image = FULL
expire_logs_days = 7
max_binlog_size = 512M
逐条解释一下:
server-id:整个主从复制环境中每台实例的唯一标识,主库和从库必须不同,否则复制会报错。通常主库用 1,从库用 2 之类,方便辨认。log-bin:开启 binlog 的开关,文件名前缀自定。主从复制的数据来源就是它,不开启复制无从谈起。binlog_format:前面说了,用 ROW。expire_logs_days:binlog 保留时间,避免磁盘被历史日志塞满。8.0 里也可以用binlog_expire_logs_seconds,控制更精细。max_binlog_size:单个 binlog 文件最大尺寸,超过后自动滚动到新文件。
配置改完要重启 MySQL 才能生效:
bash复制systemctl restart mysqld
重启后登录 MySQL,执行:
sql复制SHOW MASTER STATUS;
如果看到了 File 和 Position 字段有值,说明 binlog 已经正常开启。
3.3 创建复制专用账号
在主库上创建一个只用于复制的账号,不要直接拿 root 去给从库连。最小权限原则走起:
sql复制CREATE USER 'repl'@'192.168.10.11' IDENTIFIED BY 'YourStrongPassword';
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'192.168.10.11';
FLUSH PRIVILEGES;
这个 REPLICATION SLAVE 权限刚好够从库拉取 binlog,不会多给。如果希望同一个账号能被多台从库使用,可以写成 'repl'@'192.168.10.%',但权限范围越小越安全。
3.4 从库初始化的关键抉择
这一步是实操里最让人纠结的地方。如果你的从库是一台全新空库,可以直接配置 CHANGE MASTER TO 让它从主库的第一个 binlog 位置开始拉取。但现实中更常见的情况是:主库已经运行很久,有存量数据,你必须先把主库的数据完整迁移到从库,然后再开始复制。
最简单可靠的方式是用 mysqldump 在主库做一次一致性逻辑备份,恢复到从库,过程中记住 binlog 位置。命令如下:
bash复制mysqldump -uroot -p \
--single-transaction \
--master-data=2 \
--all-databases \
> master_dump.sql
--single-transaction:通过 InnoDB 的事务快照实现一致性导出,不加这个参数,导出的数据可能不是同一个时间点的,导致复制起点错乱。--master-data=2:导出的 SQL 文件头会注释记录主库当时的 binlog 文件名和位置,这对定位复制起点至关重要。
把 dump 文件传到从库后,还原:
bash复制mysql -uroot -p < master_dump.sql
然后打开 dump 文件头部,找到像下面这样的注释:
sql复制-- CHANGE MASTER TO MASTER_LOG_FILE='mysql-bin.000003', MASTER_LOG_POS=154;
记下这两个值,稍后要用。
如果你的主库已经开启了 GTID(MySQL 8.0 默认开启),导出时可能还需要加 --set-gtid-purged=OFF 或额外处理 GTID 信息。这一点后面讲 GTID 时会细说。
3.5 CHANGE MASTER TO 与启动复制
在从库执行:
sql复制CHANGE MASTER TO
MASTER_HOST='192.168.10.10',
MASTER_USER='repl',
MASTER_PASSWORD='YourStrongPassword',
MASTER_LOG_FILE='mysql-bin.000003',
MASTER_LOG_POS=154;
然后启动复制线程:
sql复制START SLAVE;
检查复制状态:
sql复制SHOW SLAVE STATUS\G
重点关注两个字段:
Slave_IO_Running: YesSlave_SQL_Running: Yes
两个都是 Yes 才算正常。如果 IO 线程不是 Yes,说明从库连不上主库,或者账号密码、binlog 位置有问题;如果 SQL 线程不是 Yes,说明中继日志里的 SQL 在从库执行失败,需要接着往下排查。
3.6 数据一致性验证
配置完成后,我习惯做一次端到端验证,而不是只看两个 Yes 就收工。
在主库创建一个测试表并插入数据:
sql复制CREATE DATABASE testdb;
USE testdb;
CREATE TABLE t_user (id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50));
INSERT INTO t_user (name) VALUES ('Alice'), ('Bob');
然后到从库上查:
sql复制USE testdb;
SELECT * FROM t_user;
如果能看到刚插入的两条记录,说明整条链路已经通了。做完测试后,把测试库删掉即可,避免污染生产数据。
4. 主从延迟:最容易被低估的坑
4.1 延迟从哪来
主从复制配好之后,很多人以为就万事大吉了,结果第二天看监控,发现从库数据比主库落后了好几秒甚至几分钟。延迟的主因有三个。
第一是单一 SQL 线程回放。主库是多个客户端并发写入的,binlog 里的日志是多个事务交织在一起;但从库的 SQL 线程默认只有一个,只能一个事务一个事务地挨个执行,跟主库的并发度完全没法比,这是延迟最根本的来源。
第二是大事务。比如在主库执行一个 UPDATE 涉及 500 万行数据,这一个大事务的 binlog 传到从库后,SQL 线程要执行很久,期间从库其他数据只能排队等着,延迟瞬间飙高。
第三是主库写入压力过大。如果主库本身每秒写入量非常大,从库回放速度跟不上增量速度,延迟就会持续累积,从几秒慢慢涨到几分钟。
4.2 Seconds_Behind_Master 的正确读法
很多人习惯用 SHOW SLAVE STATUS 里的 Seconds_Behind_Master 来判断延迟,但它的含义需要正确理解:这个值表示“从库当前回放的位置”和“从库 IO 线程最近读到的主库日志位置”之间的时间差。如果从库 IO 线程本身就还没拿到最新的 binlog(比如网络阻塞),这个值也不能反映真实延迟。
有个常见误区是:Seconds_Behind_Master 为 0 不代表没有延迟,只是说明“目前追上了”,主库只要持续写入,延迟随时可能出现。另外,如果 SQL 线程因为卡死或错误停止了,这个值可能会变成 NULL,而不是一个很大的数字,监工时要注意判断。
所以我更建议用数据心跳检测来确认实际延迟:在业务表里建一张心跳表,主库定时写一条 now() 记录,从库读这条记录与本地时间对比,差值就是真实的同步延迟。这个方法比查状态字段直观得多。
4.3 并行复制怎么开
针对单线程回放的问题,MySQL 从 5.7 开始提供了基于事务的并行复制能力,8.0 里已经比较成熟。核心参数如下:
ini复制[mysqld]
slave_parallel_type = LOGICAL_CLOCK
slave_parallel_workers = 8
slave_parallel_type = LOGICAL_CLOCK:启用基于事务时间戳的并行回放。主库上有依赖关系的事务仍然不能并行,但同一时刻没有依赖的事务可以并行执行。slave_parallel_workers:SQL 线程的并行工作线程数,设置成 CPU 核数的一半左右通常效果较好,太大反而可能因为竞争导致性能下降。
我在一台 16 核的从库上把 slave_parallel_workers 从默认的 4 调到 8 后,高峰期延迟从原来的几十秒降到了几百毫秒,效果非常明显。但要注意,并行复制本质上改了 SQL 线程的执行模型,特别是有无主键的表或执行特殊 DDL 时可能引入额外的风险,建议先在测试环境压一遍再上生产。
4.4 大事务引发的延迟案例分析
有一次我在生产库变更一个表结构,直接 ALTER TABLE 一个几千万行的表。MySQL 这个操作默认会重建整张表,在主库执行了 8 分钟,从库也随后回放了 8 分钟的 binlog。那段时间从库延迟直接飙到 8 分钟,所有走从库的查询全都返回旧数据。
从那以后,遇到大表 DDL 我必用 pt-online-schema-change(Percona Toolkit 里的工具),它通过创建临时表、触发器增量同步数据的方式,让 DDL 期间只有很小的锁窗口,对主从复制的延迟影响可以控制在极低范围。如果你生产环境数据库比较大,强烈建议把这类工具纳入标准操作流程,而不是直接敲一个 ALTER TABLE。
5. 复制中断与故障恢复实战
5.1 SHOW SLAVE STATUS 关键字段解读
复制链路是网络程序,又是日志回放,运行时间长了总会出现各种异常。排查的第一件事永远是 SHOW SLAVE STATUS\G。
| 字段名 | 正常值 | 异常意义 |
|---|---|---|
| Slave_IO_Running | Yes | 从库 IO 线程无法连接主库或认证失败 |
| Slave_SQL_Running | Yes | 从库 SQL 线程执行中继日志时报错 |
| Last_IO_Errno / Last_IO_Error | 0 / 空 | IO 线程最近一次错误码与信息 |
| Last_SQL_Errno / Last_SQL_Error | 0 / 空 | SQL 线程最近一次错误码与信息 |
| Seconds_Behind_Master | 0 或很小 | 从库回放延迟 |
| Master_Log_File / Read_Master_Log_Pos | 主库 binlog 文件名/位置 | IO 线程当前读到哪里 |
| Relay_Master_Log_File / Exec_Master_Log_Pos | 与上面对应的位置 | SQL 线程执行到哪个事务 |
我最常用的排障顺序是:先看 IO 线程,再看 SQL 线程。IO 线程挂了,通常是网络或账号问题;SQL 线程挂了,通常是数据冲突或 SQL 本身执行失败。
5.2 1062 主键冲突的经典处理
从库复制中断最常见的原因之一就是 1062 Duplicate entry,也就是主键冲突。发生原因五花八门,最常见的是:主库某个表原本没有主键,后面加了一个自增主键,但主从两份数据中某些行的自增值已经不一致;或者有人在从库手动插入过数据,导致后续主库日志回放时撞上重复主键。
如果确认冲突的那条数据是从库多余的,处理方式很简单,跳过这个错误事务让它继续:
sql复制STOP SLAVE;
SET GLOBAL SQL_SLAVE_SKIP_COUNTER = 1;
START SLAVE;
但这只是绕过问题,不是解决本质。如果跳过之后马上又报同样的错,说明从库数据和主库的差异不是一两行的事,这种事后的补救会没完没了。正确姿势是:彻底对账,重做主库数据到从库。我常用 pt-table-checksum 做一致性校验,找出差异表,然后针对性重新同步差异数据。
5.3 从库数据被误改后如何重新同步
有一次同事直接在从库执行了一条错误的 UPDATE,把线上用户状态改乱了,主库的数据其实是好的。复制本身不会立刻中断,但主库后续的变更会被 SQL 线程正常应用,导致从库这条被误改的数据会一直和主库不一致,而且很难发现。
这种场景的修复思路是:把这张表的数据强制同步成主库的状态。最稳妥的方式是重新 dump 该表并恢复到从库:
bash复制mysqldump -uroot -p --single-transaction --set-gtid-purged=OFF dbname t_user > t_user.sql
mysql -uroot -p dbname < t_user.sql
恢复期间复制不会中断(因为复制线程是靠 binlog 位置继续前进的),但要注意 dump 和恢复期间从库数据可能短暂不一致。恢复完成后建议立刻用 checksum 工具验证一遍。
这里送大家一个经验:绝对不要在从库手动执行任何数据修改操作,哪怕只是“改一条测试数据”。在从库上写数据,就是在给未来的故障埋雷。
5.4 网络波动和磁盘空间
IO 线程断连是另一个高频故障。比如主库重启、从库网络闪断、防火墙策略调整,都可能导致从库 IO 线程连接中断。处理方法是直接拉起 IO 线程:
sql复制START SLAVE IO_THREAD;
MySQL 会自动从上次断开的位置继续请求日志,不会丢数据。
另外,从库的 relay log 是持续写入的,如果从库磁盘剩余空间不足,IO 线程会报错。我在监控里加了磁盘使用率告警,超过 70% 就提前处理,避免磁盘满导致复制中断。
6. 进阶:从主从走向高可用
6.1 半同步复制:主从之间的折中
前面提到默认的异步复制在主库宕机时可能丢数据,半同步复制就是来解决这个问题的。它的机制是:主库提交事务时,至少等待一个从库确认收到 binlog 并写入 relay log,才向客户端返回成功。
启用半同步需要主从两边都装插件并开启,MySQL 8.0 默认已经包含相关插件。主库上执行:
sql复制INSTALL PLUGIN rpl_semi_sync_master SONAME 'semisync_master.so';
SET GLOBAL rpl_semi_sync_master_enabled = 1;
SET GLOBAL rpl_semi_sync_master_timeout = 1000;
从库执行:
sql复制INSTALL PLUGIN rpl_semi_sync_slave SONAME 'semisync_slave.so';
SET GLOBAL rpl_semi_sync_slave_enabled = 1;
STOP SLAVE IO_THREAD;
START SLAVE IO_THREAD;
注意 rpl_semi_sync_master_timeout 这个参数很关键。它表示如果等待从库确认超过 1000 毫秒,主库会自动退化为异步复制,避免主库写性能被拖垮。生产环境建议设一个合理的超时值,比如 1000 或 2000 毫秒,不要不设。
半同步复制并不能百分之百保证不丢数据,比如在等待确认超时退化为异步的那个瞬间,从库还没拿到事务,主库就宕机了,仍然可能丢。但它显著缩小了数据丢失窗口,是成本和收益之间比较均衡的选择。
6.2 GTID 复制为什么值得迁移
传统的主从复制需要手工指定 MASTER_LOG_FILE 和 MASTER_LOG_POS,位置一旦出错就会发生重复或遗漏。GTID(Global Transaction Identifier)复制则可以自动识别事务,每个事务在全局都有唯一 ID,从库能自动判断哪些事务已经执行过、哪些需要拉取,不再需要人工定位 binlog 文件。
MySQL 8.0 默认开启了 GTID,配置里只要保留:
ini复制[mysqld]
gtid_mode = ON
enforce_gtid_consistency = ON
从库配置时就不需要指定 binlog 文件和位置了,而是用:
sql复制CHANGE MASTER TO
MASTER_HOST='192.168.10.10',
MASTER_USER='repl',
MASTER_PASSWORD='YourStrongPassword',
MASTER_AUTO_POSITION = 1;
MASTER_AUTO_POSITION = 1 让从库自动与主库协商同步点,极大简化了搭建和故障恢复的工作量。如果你现在还在用传统的位置点复制,尽早规划迁移到 GTID,收益非常明显。
6.3 主从切换的预演流程
高可用方案做得好不好,不看平时,看主库宕机时你能不能稳。我强烈建议在测试环境完整预演一遍主从切换,确认每一步的操作口令。
切换的大致流程是:
- 确认从库已经追平主库,没有明显延迟。
- 在主库执行
FLUSH TABLES WITH READ LOCK,锁住写入,再记录 binlog 位置。 - 确认从库
SHOW SLAVE STATUS中Seconds_Behind_Master为 0,或已追平到主库最后的 binlog 位置。 - 将从库的复制停掉:
STOP SLAVE。 - 提升从库为主库:可以执行
STOP SLAVE; RESET SLAVE ALL;让从库独立出来。 - 修改业务连接串,让读写请求全部指向新的主库。
- 把原主库降级为从库,重新配置复制关系,连到新的主库上。
全程最怕的是切换后才发现从库还差一个关键事务没同步。所以切换前一定要通过持续观察 Seconds_Behind_Master 和心跳表确认追平,再动手。
6.4 读写分离的落地方式
主从复制本身只负责数据同步,不负责把请求分发到不同机器。读写分离需要在应用层或代理层实现。
最简单的方式是在代码层面配置两个数据源:事务和写操作走主库数据源,纯查询走从库数据源。注意一个关键点:事务内的读操作必须走主库。因为事务里先写再读,如果读走了从库,从库可能还没同步到刚写的数据,事务内读到旧数据就会产生严重 bug。
如果不想改业务代码,可以用 MySQL Router 或 ProxySQL 这类中间件,在 SQL 层面上自动识别读写请求并分发。这种方案对业务侵入小,但增加了中间件本身的运维复杂度。
最后
从单库拆到主从,再到后来的半同步、GTID、读写分离,每一步都是在用成本换稳定性和容量。回看这一路,最大的感受是:主从复制不是“配置完就完事”的事,真正的维护工作在配置之后。你需要时刻关注从库延迟、定期做数据一致性校验、提前演练主从切换流程,甚至每隔一段时间就要重新审视复制架构是否还匹配当前的业务规模。
如果你正准备在项目里上手主从复制,我的建议就一句话:先在测试环境完整搭建一遍,再制造几次故障亲手排一遍,把报错看熟了,再上生产。等你有过几次深夜被复制中断告警叫醒、又冷静处理完的经历后,就知道这套东西的边界到底在哪。
