☰
MySQL主从复制实战:原理、GTID配置与排错指南
2026/10/11 20:06:49 网站建设 项目流程

做后端开发这些年,MySQL 几乎是绕不开的存储底座。平时单机跑着没什么感觉,一旦业务量上来,或者你负责的系统开始有"高可用"这三个字的要求,主从复制立马就会摆到面前。

我最早接触主从配置是在一个读写分离的项目里,当时照着一篇老教程机械地敲命令,结果折腾了整整两天:不是从库 IO 线程连不上,就是 SQL 线程报 1062 主键冲突,最后连 binlog 位点都搞丢了,只能重来。后来把原理彻底吃透,才发现主从配置本身并不难,难的是理解它每一步在干什么、出了问题怎么定位。

这篇文章就围绕 MySQL 主从复制的完整配置过程,把原理、实操步骤、验证方法、日常维护和排错经验一次性说清楚。不管你是刚入行的运维,还是需要自己搭库的后端,按照下面的思路走一遍,基本能避开我当年踩过的坑。

1. 主从复制到底在解决什么问题

先说点实在的。很多人一听到"主从复制",第一反应是"让两个库数据一样",这个理解没错,但过于片面。主从复制本质上解决的是数据冗余和读写负载分离这两件事,而不是"高可用"。

1.1 读多写少的业务场景

大多数互联网业务都是读多写少:商品详情页每天被刷几百万次,下单操作可能只有几万次。如果把所有读请求都压到一台 MySQL 上,CPU 和 IO 迟早被打满。这时候主从复制就能派上用场:主库只处理写请求,从库分担读请求,应用层根据 SQL 类型做路由。这样既降低了主库压力,又让系统容量可以横向扩展——加一台从库,就多一份读能力。

1.2 数据备份与容灾

主从复制的另一个价值在于容灾。主库物理机损坏、磁盘故障、机房断电,这些极端情况谁都不想遇到,但谁也不能保证不会发生。只要有从库实时同步着数据,主库挂了之后可以立刻把从库提升为新主库,业务中断时间能压缩到分钟级甚至秒级。这比"每天凌晨备份一次,出事了恢复昨天的数据"要靠谱得多。

1.3 主从不是万能的

需要提前泼一盆冷水:主从复制不等于高可用。它解决的是"数据有备份"和"读能力扩展",如果你需要的是自动故障转移、脑裂防护这种能力,那得靠 MHA、Orchestrator 这类高可用组件来完成,单纯的复制配置做不到。另外,主从复制是异步的,从库数据在任意时刻都可能比主库落后一点,所以它不适合对一致性要求极高的场景,比如账户余额查询。

理解了这些,你就明白为什么要有主从、什么场景该用主从。下面进入正题。

2. 配置前的选型思考:GTID 还是 binlog+位点

在动手之前,必须做一个关键决策:用 GTID 复制还是传统的 binlog+位点复制。这一步选错了,后面维护会非常痛苦。

2.1 两种复制方式的原理对比

传统方式是靠文件名+偏移量定位同步位点。比如主库当前的 binlog 是mysql-bin.000012,位置在 345,从库就会告诉主库:"我从mysql-bin.000012的 345 位置开始给我发数据。"

GTID(Global Transaction Identifier)则是给每个事务一个全局唯一 ID,从库只要记录"我执行到哪个 GTID 了",主库就知道该发哪些事务给它。GTID 本质上是一个自动化的位点管理机制,它解决了传统方式最头疼的问题:找位点。

2.2 为什么我推荐直接上 GTID

传统方式最大的痛点是 failover 场景下的位点查找。主库挂了,你想把从库提升成主库,其他从库要重新指向它,就得先在新主库上找到正确的 binlog 文件名和位置,这是一件非常容易出错的事。GTID 模式下完全不用管这些,只要指定MASTER_AUTO_POSITION=1,复制位点自动协商。

另一个痛点是跳过错误。传统的SET GLOBAL sql_slave_skip_counter = 1方式在 GTID 下变了,变成了注入空事务,逻辑更清晰,定位也更容易。

MySQL 5.7 以后 GTID 已经很成熟了,5.7 和 8.0 都默认支持在线开启(5.6 时代开了 GTID 就没法关,5.7 开始可以动态开关)。如果你还在用 5.6,可能要考虑升级。如果你用的是云数据库 RDS 类的产品,它通常默认就是 GTID。所以新项目我强烈建议直接用 GTID。

2.3 什么时候继续用传统方式

有一种情况可以继续用传统方式:你用的是极老的 MySQL 版本(5.5 或更早),这种情况下没有 GTID 可选。另外,如果公司内部有一套非常成熟的手工维护脚本,全部基于 binlog 文件名+位点写的,那改成 GTID 需要同步改造脚本,这种情况下可以分批迁移,不急于一时。

3. 主库配置实操:参数、账号与数据导出

这部分是干货中的干货。我假设有两台服务器,主库 IP 假设为 192.168.1.10,从库为 192.168.1.11,MySQL 版本统一为 8.0.x。双方都用 Linux 系统。

3.1 主库配置文件修改

编辑主库的/etc/my.cnf(不同发行版/安装方式路径略有差异,可以用mysql --help | grep my.cnf查找),在[mysqld]段下增加:

[mysqld] server-id = 1 log-bin = mysql-bin binlog_format = ROW gtid_mode = ON enforce_gtid_consistency = ON log_slave_updates = ON

逐项解释:

  • server-id:主从集群内每个节点的唯一标识,不能重复。主库设为1,从库建议设为2,后面再加从库依次递增。
  • log-bin:开启二进制日志。这是复制的前提,主库所有数据变更都会写入 binlog。
  • binlog_format = ROW:行级复制,记录每一行数据的变化。相比 Statement 格式,ROW 格式更安全,不会出现"同一个函数在主从执行结果不一致"的问题。
  • gtid_mode和enforce_gtid_consistency:开启 GTID 的核心开关,两个必须同时设置。
  • log_slave_updates:从库记录自己的 binlog。如果这台机器将来可能由从库变主库,或者后面要级联复制(A→B→C),这个必须开。8.0 中该参数默认开启,5.7 需要手动确认。

改完配置后重启主库 MySQL:

systemctl restart mysqld

3.2 创建复制专用账号

复制账号需要REPLICATION SLAVE权限。这里强调一点:永远不要用 root 账号做复制,权限最小化是基本功。用 root 做复制,一旦从库被入侵,主库也跟着完蛋。

CREATE USER 'repl'@'192.168.1.%' IDENTIFIED BY 'YourStrongPasswd'; GRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'repl'@'192.168.1.%'; FLUSH PRIVILEGES;

REPLICATION SLAVE是从库连接主库拉取 binlog 的权限,REPLICATION CLIENT用于执行SHOW MASTER STATUS、SHOW SLAVE STATUS这类命令、监控复制状态。主机限制我写成了网段192.168.1.%,比%安全得多。

3.3 数据一致性的核心问题

很多人配置主从失败,都是栽在这一步:先有数据,后有复制。如果主库已经有存量数据了,而你直接配置从库开始同步,从库会和主库对不上——因为新事务是在旧数据的基础上产生的,从库没有那些旧数据,重放时必然报错。

所以标准流程是:先把主库现有的数据导出来,导入到从库,然后再启动复制。

备份工具我推荐mysqldump,简单直接。执行的时候有几个关键参数:

mysqldump -uroot -p --single-transaction --set-gtid-purged=ON --all-databases --master-data=2 > dump.sql
  • --single-transaction:在 InnoDB 引擎下通过开启事务拿到一致性快照,备份过程中不影响线上写入。
  • --set-gtid-purged=ON:导出时把当前主库已经执行过的 GTID 集合写进备份文件里,这样从库导入后就知道"我之前同步到哪了"。
  • --master-data=2:在备份文件里记录主库 binlog 位点信息,虽然 GTID 模式下不是必需,但多一份信息,排查问题时很有用。

注意,--all-databases会包含系统库。如果你的库很小,也可以只导出业务库。另外,如果主库有大量数据(几十 GB 甚至更大),mysqldump会比较慢,可以考虑用 Percona XtraBackup 做物理备份,但那个工具的介绍是另一个话题了,这里先用mysqldump保证流程能跑通。

数据导出后,把这个 dump.sql 传到从库服务器上。

4. 从库配置实操:导入数据与启动复制链路

现在轮到从库这边了。整个过程分三步:改配置、导数据、启动复制。

4.1 从库配置修改

编辑从库的/etc/my.cnf,同样在[mysqld]段下:

[mysqld] server-id = 2 log-bin = mysql-bin binlog_format = ROW gtid_mode = ON enforce_gtid_consistency = ON log_slave_updates = ON read_only = ON

注意:server-id必须和主库不同。read_only = ON是让从库拒绝非 super 权限的写操作,防止业务误写入导致主从不一致。

提示:read_only对拥有 SUPER 权限的用户无效,如果是专职 DBA 管理,可以不用管;如果开发人员也有账号,记得别给他们 SUPER 权限。

改完重启从库:

systemctl restart mysqld

4.2 导入全量备份

把主库传来的 dump.sql 导入从库:

mysql -uroot -p < dump.sql

这一步做完,从库应该已经有了和主库一致的历史数据。可以用SHOW DATABASES;简单确认一下业务库名是否存在。

4.3 配置复制链路

在从库上执行CHANGE MASTER TO语句(8.0 之后的写法稍有差异,MASTER_HOST等参数名带上了MASTER_前缀,这里用 8.0 的标准写法):

CHANGE MASTER TO MASTER_HOST='192.168.1.10', MASTER_PORT=3306, MASTER_USER='repl', MASTER_PASSWORD='YourStrongPasswd', MASTER_AUTO_POSITION=1;

关键点是MASTER_AUTO_POSITION=1,这告诉从库:别告诉我从哪个位点开始,你根据我已有的 GTID 集合自动找。启动后,从库会先和主库交换 GTID 集合,主库会把从库缺失的所有事务发给它。

然后启动复制:

START SLAVE;

4.4 验证复制是否正常

运行状态检查命令:

SHOW SLAVE STATUS\G

重点看两个线程:

Slave_IO_Running: Yes Slave_SQL_Running: Yes

两个都必须是 Yes。IO 线程负责从主库拉 binlog,SQL 线程负责把拉回来的日志在从库上重放。这俩就像一个搬运工和一个施工队,哪个停了,复制就停了。

另外两个字段也值得看:

  • Seconds_Behind_Master:从库落后主库的秒数。0 是最理想状态。
  • Retrieved_Gtid_Set和Executed_Gtid_Set:已接收和已执行的 GTID 集合。如果两个集合越来越大且不断更新,说明在持续同步。

做一次写入验证:在主库建个测试表,插几条数据,在从库查一下能不能马上看到。这里我建议用业务库来测,避免只在测试库验证、上线之后才发现业务库没同步的尴尬。

5. 复制状态监控与两个常见误区

复制搭好了,不等于一劳永逸。主从复制在日常运行中需要持续监控,我最怕看到的现象是:同事隔了一个月才来看一次SHOW SLAVE STATUS,结果发现线程早就停了,主从数据差了几十万行,还不知道从什么时候开始的。

5.1 需要重点盯的输出字段

SHOW SLAVE STATUS\G的输出有不少行,日常巡检我一般只看这几个:

字段说明关注点
Slave_IO_RunningIO 线程状态必须为 Yes
Slave_SQL_RunningSQL 线程状态必须为 Yes
Last_IO_Errno / Last_IO_ErrorIO 线程最近错误不为 0 时必须处理
Last_SQL_Errno / Last_SQL_ErrorSQL 线程最近错误不为 0 时必须处理
Seconds_Behind_Master同步延迟秒数持续增大需重点关注
Master_Log_File / Read_Master_Log_Pos已拉取到的 binlog 位点和主库对比可确认滞后量
Retrieved_Gtid_Set / Executed_Gtid_SetGTID 执行进度确认是否持续增长

日常巡检可以用脚本定期跑SHOW SLAVE STATUS,把Slave_IO_Running和Slave_SQL_Running的结果抓出来做告警。监控平台如果能直连数据库,也可以用SHOW SLAVE STATUS的结果做指标采集。

5.2 "Seconds_Behind_Master 是 0"不等于完全实时

这里有个新手常见的误读。Seconds_Behind_Master计算的是"从库当前执行时间与主库当前时间之差",但它有个天然的缺陷:如果 IO 线程已经停了很久,SQL 线程执行完了所有已拉取的日志,这个值会显示为 0,让你以为同步正常。实际上主库早就产生了新数据,只是从库压根没拉。

所以判断同步状态,不能只看延迟秒数。更可靠的标准是对比 GTID:检查从库的Executed_Gtid_Set是否和主库的Executed_Gtid_Set(用SHOW MASTER STATUS查看)相同。如果相同,说明从库确实执行到了主库的最新事务。

5.3 别把主从延迟完全归咎于网络

排除网络因素后,从库延迟的一个大坑是大事务。比如在主库执行了一条UPDATE影响了几百万行,这条语句在 binlog 里可能是一个巨大的事务,从库重放需要很长时间。主库执行只要几秒,从库可能要执行几分钟,延迟瞬间拉大。用 ROW 格式后这个大事务在 binlog 里的体积更大,因为每一行变更都要记录。

针对这种情况,没有特别取巧的办法,只能从业务层面控制大事务,比如分批更新。另一个常见原因是从库硬件配置比主库差,如果主库是 32 核 128G,从库只有 4 核 8G,延迟几乎是必然的。从库硬件不说和主库完全一致,至少差距不能太大。

6. 从零到一踩过的坑:遇到这些错误怎么排查

主从复制的排错,是我最想分享的部分。配置过程本身是机械性的,但排错需要真正理解机制。下面几个错误是出现频率最高的,逐一说明。

6.1 主键冲突(Error 1062)

现象:Slave_SQL_Running: No,Last_SQL_Error报Duplicate entry 'xxx' for key 'PRIMARY'。

原因基本就一个:从库上有数据,而主库也恰好有这条数据,重放时发生冲突。常见场景是从库被业务误写入过数据,或者从库是从某个时间点的备份恢复的,恢复后又手动插入过本该由主库同步的数据。

处理思路:先确认冲突数据的来源。如果是从库误写导致的,那要清理掉从库上的脏数据,然后重启 SQL 线程:

STOP SLAVE; SET GTID_NEXT='冲突事务的GTID'; BEGIN; COMMIT; SET GTID_NEXT=AUTOMATIC; START SLAVE;

这是 GTID 模式下跳过单个事务的标准姿势——注入一个空事务,让 GTID 集合"假装"已经执行过。注意:这种方式只能用于确定这一条事务不需要执行的情况,绝不能批量跳过,否则主从数据会永久不一致。

6.2 日志丢失或找不到(Error 1236 / 13114)

现象:IO 线程报错,Last_IO_Error提示找不到 binlog 文件,或者Got fatal error 1236。

原因:最常见的是从库停机时间过长,主库的 binlog 已经清理掉了。MySQL 的 binlog 有保留期(expire_logs_days或 8.0 的binlog_expire_logs_seconds),如果从库断连时间超过了保留期,从库请求的 binlog 文件已经不存在,主库自然会报错。

处理方案:这种情况下,从库只能重新搭建。把主库当前数据重新导一次,从库重新CHANGE MASTER TO,然后START SLAVE。这提醒我们一个在实际运维中的教训:主库的 binlog 保留时间一定要大于从库可能的最大断连时间。尤其是那些不常被关注的从库,很多人只在故障时才想起来看它一眼,如果 binlog 刚好被清了,就只能全量重搭。

6.3 IO 线程一直显示 Connecting

现象:Slave_IO_Running: Connecting,Last_IO_Error显示连接超时或被拒绝。

按顺序排查这几项:

  1. 网络连通性:telnet 192.168.1.10 3306看端口通不通。
  2. 账号权限:确认主库上repl账号存在、主机限制配置正确。从库服务器实际出口 IP 是否在账号允许的网段内——这个很容易被忽略,跨云环境下出口 IP 不是你登录那台机器的内网 IP。
  3. 防火墙和安全组:云服务器的安全组是否放行了 3306 端口,Linux 本机防火墙(firewalld/iptables)是否拦截。
  4. 主库server-id是否配置,binlog 是否开启。SHOW MASTER STATUS如果返回空,说明主库没开 log-bin。

6.4 从库只读设置引发的坑

有同事问过我:"我明明设置了read_only = ON,为什么业务还能在从库上写入?"

原因:read_only只限制普通用户,拥有 SUPER 权限的用户不受限制。如果业务账号不小心有了 SUPER 权限,写入照样畅通无阻。8.0 还提供了super_read_only,这个连 SUPER 用户都限制写入,适合在从库上开启。但使用前要确认是否有管理任务需要临时写入从库,比如某些监控系统要写心跳表,super_read_only会把它一起挡住。

7. 进阶配置:半同步复制与多源复制的取舍

基础主从跑通之后,很多人会想更进一步。这里聊两个常见的增强方案。

7.1 半同步复制(Semisynchronous Replication)

前面反复强调,MySQL 默认的异步复制存在延迟和丢失风险。半同步复制的作用是:主库在提交事务时,会等待至少一个从库确认已经接收到 binlog,才向客户端返回成功。这能显著降低"主库宕机但事务尚未到达从库"导致的数据丢失风险。

启用方式(MySQL 8.0):

INSTALL PLUGIN rpl_semi_sync_master SONAME 'semisync_master.so'; INSTALL PLUGIN rpl_semi_sync_slave SONAME 'semisync_slave.so'; SET GLOBAL rpl_semi_sync_master_enabled = 1; SET GLOBAL rpl_semi_sync_slave_enabled = 1;

注意:半同步并不是 100% 强一致。如果在等待确认的过程中超时,主库会退化为异步模式继续提交。也就是说,它是在"可用性"和"一致性"之间做了折中。另外,如果从库只有一个且它就挂在半同步里,那这个从库挂了,主库写入性能会受到影响(等待超时期间所有写事务阻塞)。所以生产环境中至少要有两个从库,或者做好半同步超时参数的调优。

7.2 多源复制

有些场景需要把多个业务库汇聚到一个分析库,MySQL 5.7 开始支持多源复制,即一个从库从多个主库同步数据。配置上和单源几乎一样,只是每个复制通道要有不同的名字:

CHANGE MASTER TO MASTER_HOST='192.168.1.10', ... FOR CHANNEL 'channel_a'; CHANGE MASTER TO MASTER_HOST='192.168.1.12', ... FOR CHANNEL 'channel_b'; START SLAVE FOR CHANNEL 'channel_a'; START SLAVE FOR CHANNEL 'channel_b';

多源复制有个隐患:如果不同主库上有相同库名的业务库,重放时会发生覆盖冲突。所以多源复制一般用于"不同业务库合并"而不是"相同库合并"。

7.3 读写分离的正确落地方式

复制搭好之后,应用层怎么做读写分离,这是另一个常见问题。从代码层面来看,最简单的做法是配置多数据源——写操作走主库,读操作走从库。如果是 Java 生态,可以借助中间件或者框架的读写分离能力。更彻底的做法是引入数据库代理,代理层自动解析 SQL 并路由到合适的实例,应用层无感知。但无论哪种方式,都要接受一个问题:从库数据有延迟,刚写完主库的数据立刻去从库查,可能查不到。针对这个,可以在应用层对强一致场景做特殊处理,比如强制读主库。

8. 关于数据一致性验证与日常巡检清单

最后分享一套我在实际维护中固定下来的巡检清单。主从搭建完、运行一周后,我会按下面的顺序检查一次,之后每个月例行执行。

8.1 数据一致性巡检

工具层面,MySQL 官方生态里有pt-table-checksum(Percona Toolkit 家族),它能对主从数据做一致性校验,找出哪些表的数据不一致。它的原理是在主库上按行做 checksum 计算,把计算结果和从库对比。这个工具跑起来有一定开销,建议在业务低峰期执行。

手工巡检时,可以对关键大表做COUNT(*)对比。但这里提醒一下,COUNT(*)只对比了行数,行数一致不代表数据一致——某一行数据被改了但行数没变,这种差异COUNT(*)发现不了。所以重要表的校验还是推荐pt-table-checksum,或者定期抽样的CHECKSUM TABLE。

8.2 日常巡检清单

我自己的巡检表大概长这样:

  1. 检查SHOW SLAVE STATUS\G,确认两个线程是 Yes。
  2. 检查Last_IO_Error和Last_SQL_Error是否为空。
  3. 检查Executed_Gtid_Set是否等于主库SHOW MASTER STATUS的Executed_Gtid_Set。
  4. 检查磁盘空间,binlog 增长是否异常。从库开启 binlog 的情况下,空间占用会一直增长。
  5. 检查主从机器的负载和慢查询,从库延迟很多时候是慢查询抢占了 IO。
  6. 周期性的数据一致性校验,先用pt-table-checksum,有差异再找具体表。

8.3 主库 binlog 保留时长

这个是最容易忽略的隐患。主库的 binlog 如果保留太短,从库断连时间稍长就会触发前文说的 1236 错误。如果保留太长,磁盘空间又受不了。8.0 中可以通过binlog_expire_logs_seconds设置保留时长,比如设置 7 天(604800 秒)。这个值要根据你从库的最大容忍停机时间来决定,而不是凭感觉定。

配置示例:

[mysqld] binlog_expire_logs_seconds = 604800

8.4 从库宕机后的恢复流程

从库机器重启后,如果 MySQL 是开机自启动的,通常复制线程也会自动恢复。但也有恢复不了的情况,尤其是异常断电或磁盘错误。这时候按照下面的步骤处理:

  1. 登录从库,执行SHOW SLAVE STATUS\G,看报错信息。
  2. 如果是网络闪断导致的 IO 线程中断,执行START SLAVE;即可,通常会自动重连。
  3. 如果 SQL 线程报错,根据错误类型决定是跳过事务还是重新搭建。
  4. 如果 IO 线程报 1236(binlog 文件不存在),这种情况只能重搭从库,做法就是本文第 4 节的全量流程重新走一遍。

8.5 重搭从库的正确姿势

重搭从库时,有些细节可以提高效率:先记录当前主库的 GTID 集合,用mysqldump全量备份,导入从库后启动复制。因为 dump 文件里带了SET GLOBAL GTID_PURGED信息,从库启动复制时会自动从 GTID 集合之后的位置开始拉取,无需手动指定位点。

这里特别提醒:如果备份文件很大,导入耗时长,可以先把 dump 文件用 gzip 压缩再传到从库,网络传输会快很多:

gzip dump.sql scp dump.sql.gz root@192.168.1.11:/data/ gunzip dump.sql.gz mysql -uroot -p < dump.sql

我在实际项目中用这套流程重搭过多次从库,最小的库几十秒搞定,几十 GB 的库压缩后传输也能明显节省时间成本。

主从配置不是"敲完命令就结束"的事情,它更考验的是持续维护的能力。希望这篇文章能把原理到实操的每个环节都讲透,让你在配置主从时不只是照着命令抄,而是知道每一步为什么这么做、出了问题时往哪个方向排查。如果严格按照上面的流程走下来,再遇到主从问题,你应该能在几分钟内定位到根因,而不是像我当年那样抓瞎一整天。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询