做后端这么多年,我有个很深的体会:很多项目的数据库瓶颈,根本不是SQL写得不够好,而是架构上从一开始就没给数据库留出“分身”的余地。MySQL主从复制,听起来是个老生常谈的话题,但它确实是后端工程师从“能跑”走向“能扛”的分水岭。这篇文章不打算照搬官方文档,我想用自己做项目时的真实经历,把主从复制从原理到实操、从坑点到监控,完整拆一遍。不管你是刚入行的Java后端,还是写过几年业务代码但一直没真正自己搭过主从的人,这都值得你花十几分钟看完。
1. 主从复制整体设计与思路拆解
1.1 为什么需要主从复制:后端场景中的真实痛点
先聊一个很常见的场景。你负责一个前后端分离项目,前端页面调后端接口,后端接口查MySQL。用户量小的时候,一切都很美好。但有一天运营搞了个活动,流量上来,数据库CPU直接飙到100%,接口响应从50ms变成2s。这时候很多人第一反应是“加缓存”,但缓存只能缓解读压力,如果热点数据多、更新频繁,缓存命中率并不乐观,而且缓存和数据库的一致性维护本身就是个坑。
另一个痛点就是备份。我见过不少团队直接在线上主库跑mysqldump,结果备份期间主库锁表、业务抖动,最后被运维老大骂了一顿。如果有一台从库,备份完全可以放在从库做,主库该干嘛干嘛。
还有高可用的问题。主库机器宕机,如果没有从库,你只能干等恢复;但如果有从库,至少可以把流量切到从库顶上,或者手动提主,恢复时间能缩短一个数量级。
所以主从复制解决的核心问题,归纳起来是三个:读写分离降低主库压力、备份和统计分析不影响主库、为主库故障提供容灾恢复手段。
1.2 架构选型:一主一从、一主多从,还是级联
主从复制有很多种拓扑结构,我先说最常见的几种。
一主一从最基础,适合数据量不大但需要备份、容灾或读写分离的小项目。它的优点就是简单,维护成本低,出问题也容易排查。
一主多从适合读多写少的业务。主库只负责写,多个从库分担读流量。比如一个商品详情页,一天的读请求可能有几百万,写请求可能只有几千,一台从库扛不住,就多挂几台。注意,从库不是越多越好,因为每一个从库都要从主库拉binlog,从库太多,主库的dump线程和网络I/O会变成瓶颈。我自己的经验是,从库数量超过5台以后,不如考虑引入中间层或者缓存。
级联复制就是让一台从库同时作为下一级从库的主库,也就是主库 -> 从库1 -> 从库2。这种结构适合从库特别多、或者从库跨地域的场景,因为级联节点可以分担主库的binlog分发压力。但级联的缺点也很明显:数据链路变长,延迟会叠加,排查问题也更麻烦。
还有一个容易踩坑的主主复制(双主),两边都能写,同时互相复制。这东西在生产环境我一般不推荐,除非你有非常成熟的冲突解决机制。两个主库同时写同一行数据,很容易出现主键冲突或者数据不一致,处理起来非常头大。双主真正合适的用途是“主备切换”,也就是同一时刻只有一边在写,另一边只是作为热备待命。
1.3 核心设计逻辑:基于binlog的异步事件传递
主从复制其实没有大家想象得那么神秘。你可以把它理解成一个“发布-订阅”系统:主库把每一次数据变更记录到binlog里,相当于发布了一条消息;从库专门有人去拉取这些消息,然后在本地重新执行一遍,相当于订阅并消费。
这个过程涉及三类角色:binlog、从库的I/O线程、从库的SQL线程。严谨一点说,还有一个主库的dump线程。整个流程是这样的:
- 主库上提交事务,把变更写入binlog。
- 主库的dump线程把binlog推送或等待从库来拉取。
- 从库的I/O线程从主库拉取binlog,写入本地的relay log(中继日志)。
- 从库的SQL线程读取relay log,并按顺序在从库上重放,最终数据就和主库一致了。
为什么选这种异步事件传递的方式?因为MySQL的设计原则是:写操作尽量不影响主库性能。如果主库每次写都要等从库确认,延迟和性能损耗会非常明显。异步复制在性能和一致性之间做了一个折中:主库不等待从库,所以主库写性能几乎不受影响,但代价是从库数据可能有短暂延迟。
2. 核心原理与关键细节解析
2.1 binlog:主从复制的地基
binlog是MySQL的二进制日志,记录的是数据库所有结构变更和数据变更,比如CREATE TABLE、INSERT、UPDATE、DELETE等。它有几个核心作用:主从复制、数据恢复、审计。
很多人分不清binlog和redo log的区别。redo log是InnoDB存储引擎层面的,记录的是物理页的修改,主要用于崩溃恢复;binlog是MySQL Server层面的,记录的是逻辑操作,主要用于复制和回放。两者配合,才能既保证宕机不丢数据,又能把变更同步到从库。
关于binlog的格式,必须重点说。
- STATEMENT格式:记录的是原始SQL语句。比如你执行了
UPDATE t SET name='张三' WHERE id=1,binlog里就存这条SQL。优点是日志量小,但缺点非常致命:SQL会依赖执行时的上下文,比如用了NOW()、RAND(),在主库和从库执行结果可能不一致。 - ROW格式:记录的是行变更的前镜像和后镜像,比如某一行执行UPDATE前后的值。优点是精度高,任何情况下从库重放结果都能和主库一致;缺点是日志量会变大,尤其是一张大表批量更新时,binlog文件可能膨胀得很快。
- MIXED格式:MySQL自己判断,默认用STATEMENT,遇到可能不安全的情况自动切到ROW。
我在生产环境一般直接指定binlog_format=ROW。虽然日志大了点,但换来的是数据一致性,这个交易很划算。尤其是你后面如果要用到同步工具(比如Canal、Flink CDC),ROW格式几乎是必须的,因为工具需要解析出每一行变更前后的值。
另外要记住binlog和事务的关系:binlog是在事务提交时写入的。如果事务没有提交,它的变更不会出现在binlog里。这也是为什么从库重放的时候,天然按事务边界执行,不会出现半截事务。
2.2 从库三线程协作机制
主库上的dump线程、从库上的I/O线程和SQL线程,这三个线程的协作是整个主从复制的引擎。
可以打一个通俗的比方:主库是一家出版社,每次发布新一期的杂志(binlog事件);dump线程是出版社的发行员,负责把杂志发给订阅者;从库的I/O线程是读者家的信箱管理员,负责把杂志取回来放进信箱(relay log);SQL线程则是读者本人,负责把杂志内容读完,并做笔记(重放到从库)。
为什么要拆成两个线程,而不让一个线程直接拉取并回放?设计精妙之处在于解耦。I/O线程只管从网络上拉数据,速度受限于主库和从库之间的带宽;SQL线程只管本地回放,速度受限于从库的磁盘和CPU。两者相互独立,即使SQL线程因为某条大SQL执行得很慢,I/O线程还能继续接收binlog,不会造成网络堆积。
SHOW PROCESSLIST里的几个线程状态可以确认问题出在哪。主库上看到Binlog Dump线程,说明dump线程正常;从库上看到Slave_IO_Running和Slave_SQL_Running都是Yes,才说明两个线程都在正常工作。
2.3 复制模式与一致性边界:异步、半同步、全同步
MySQL复制模式主要分三种:异步复制、半同步复制、全同步复制。
**异步复制(Async)**是默认模式。主库提交事务后直接返回成功,不等待从库确认。优点是对主库性能影响最小,但故障切换时可能丢数据。比如主库写了数据还没来得及传给从库,主库宕机了,从库提升为主库后,那条数据就彻底丢了。
**半同步复制(Semi-Sync)**在异步基础上加了一步:主库提交事务后,至少要等待一个从库确认已经收到binlog并写入relay log,才返回客户端成功。注意,半同步只要求“收到并落盘relay log”,不要求从库已经执行完,所以对性能的影响是可控的。它在性能和一致性之间做了更好的平衡,也是我个人在生产环境比较推荐的方式。
全同步复制要求所有从库都执行完才算成功,性能极差,MySQL原生并不直接支持,通常要靠MySQL Group Replication或第三方方案实现。除非是金融级强一致性场景,一般不建议。
这里有个很关键的细节:半同步复制如果等待超时,会退化成异步复制,而不是让事务失败。所以你以为自己在用半同步,实际上可能已经降级了,需要关注Rpl_semi_sync_master_status等状态变量,不然就白配置了。
2.4 主从延迟:为什么一定会存在
主从延迟是每个后端工程师都会碰到的痛点。最常见的现象是:写完数据马上读,结果读到的是旧值。
为什么会有延迟?理清几个原因:
- 网络传输耗时。从库通过I/O线程拉取binlog,网络延迟无法避免,但通常这个延迟很小,除非跨机房跨地域。
- SQL线程重放速度跟不上主库写入速度。这是延迟最大的来源。主库可以并发写,但从库在5.7之前只有一个SQL线程串行执行relay log,一旦遇到大事务或大批量写,从库就明显追不上。
- 大事务效应。一个大事务在主库可能只花了几秒,但binlog事件在从库回放时可能需要几十秒。比如批量更新几十万行,从库逐行应用,时间被拉长。
- DDL操作。一条
ALTER TABLE在从库重放可能锁表,阻塞后续同步。 - 从库自身有大量读流量,导致SQL线程能拿到的CPU、I/O资源有限。
5.7以后引入了并行复制(MTS),可以通过slave_parallel_workers参数配置多个SQL线程并行回放,能大幅缓解单线程问题。但并行复制也有约束,需要binlog事务之间没有冲突,实操中不能把并行度调到离谱。
理解了延迟为什么存在,才能对症下药。后面我会专门讲排查和优化。
3. 实操实现与核心环节配置
3.1 环境准备:用Docker快速搭一套示例环境
讲原理讲得再多,不如自己动手搭一套环境。我建议新手直接用Docker搭测试环境,比自己在本地装两份MySQL快得多,还能随意清空重来。
如果你还没装Docker,先去官网下载安装。我以前也经常看到有人搜“docker安装mysql失败”,其实大多数坑都出在端口占用、数据目录权限、容器时区这几个点上。Docker安装MySQL的完整流程,我这里直接给你一套能跑通的命令。
主库容器:
docker run -d \ --name mysql-master \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=root123 \ mysql:8.0 \ --server-id=1 \ --log-bin=mysql-bin \ --binlog-format=ROW \ --gtid-mode=ON \ --enforce-gtid-consistency=ON从库容器:
docker run -d \ --name mysql-slave \ -p 3307:3306 \ -e MYSQL_ROOT_PASSWORD=root123 \ mysql:8.0 \ --server-id=2 \ --log-bin=mysql-bin \ --binlog-format=ROW \ --gtid-mode=ON \ --enforce-gtid-consistency=ON注意几个容易出问题的地方:
server-id必须唯一,主库和从库不能一样。- 容器内的3306端口映射到宿主机时,主库用3306,从库用3307,避免冲突。
- 从库不一定非要开
log-bin,但如果从库未来可能变成主库,提前打开更省事。 - 从库最好启用
read_only,防止业务误连从库后写入数据,导致主从数据不一致。
3.2 配置主库并创建复制账号
Docker容器起来之后,进入主库容器,创建专门的复制账号。复制账号的权限不需要太大,只要REPLICATION SLAVE即可,千万别直接给所有权限。
CREATE USER 'repl'@'%' IDENTIFIED BY 'YourPassword123'; GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%'; FLUSH PRIVILEGES;验证主库状态:
SHOW MASTER STATUS;这个命令会输出类似下面的结果:
+------------------+----------+--------------+------------------+-------------------+ | File | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set | +------------------+----------+--------------+------------------+-------------------+ | mysql-bin.000003 | 157 | | | | +------------------+----------+--------------+------------------+-------------------+这里记录的就是从库开始同步的起点:binlog文件和偏移量。如果使用GTID模式,Executed_Gtid_Set也会显示。
3.3 初始化从库数据并启动同步
搭建主从的坑主要在“主从数据不一致”这一步。如果主库已经有数据了,你必须先做一次全量备份,恢复到从库,然后从备份点开始同步,不能直接空库挂主从。
用mysqldump 做一致性备份:
mysqldump -uroot -p --single-transaction --master-data=2 --all-databases > all.sql关键参数解释:
--single-transaction:在InnoDB引擎下导出时开启一个可重复读事务,保证备份期间数据一致性,同时不锁表。--master-data=2:在备份文件头部注释里写入主库当时的binlog位置,方便后面定位同步起点。--all-databases:备份所有库,避免遗漏系统库。
把备份文件拷进从库容器并导入:
docker cp all.sql mysql-slave:/tmp/all.sql docker exec -i mysql-slave sh -c 'mysql -uroot -p root123 < /tmp/all.sql'然后进入从库,配置主库连接信息并启动复制:
CHANGE MASTER TO MASTER_HOST='宿主机IP', MASTER_PORT=3306, MASTER_USER='repl', MASTER_PASSWORD='YourPassword123', MASTER_LOG_FILE='mysql-bin.000003', MASTER_LOG_POS=157; START SLAVE;如果你开启了GTID模式,配置会更简单,前提是主从两边GTID已经一致。可以通过MASTER_AUTO_POSITION=1自动定位:
CHANGE MASTER TO MASTER_HOST='宿主机IP', MASTER_PORT=3306, MASTER_USER='repl', MASTER_PASSWORD='YourPassword123', MASTER_AUTO_POSITION=1; START SLAVE;我个人建议直接用GTID,因为后续切换主从、跳过事务都方便很多。
查看同步状态:
SHOW SLAVE STATUS\G重点关注几个字段:
Slave_IO_Running: YesSlave_SQL_Running: YesSeconds_Behind_Master: 0
全部正常,说明同步已经路上。
3.4 读写分离落地:后端代码怎么配合
主从复制配好之后,还要让应用真正把读和写分流,否则等于白搭。后端层面有两种常见做法:用中间件,或者改代码。
中间件方案里比较常见的有ProxySQL、MyCat、ShardingSphere。它们对应用基本透明,SQL发到中间件,中间件自动把写请求路由到主库,读请求路由到从库。缺点是多了一层中间件,增加运维成本。
改代码的方式更直接。现在主流做法是在应用里配置两个数据源,写操作走主库,读操作走从库。以Spring Boot为例,可以通过@Transactional(readOnly = true)标识只读事务,路由到从库。也可以引入更轻量的AbstractRoutingDataSource,自己实现读写分离路由。
这里提醒一句:读写分离后,代码里最忌讳的是“写完立即读”。比如用户注册完,前端马上请求用户信息,如果读请求被路由到从库,而从库同步还没跟上,就会查到“用户不存在”。这种问题的缓解方案有很多:关键数据写完后强制读主库,或者对于一些一致性要求高的场景,等主从延迟过去再读。哪种方案好,要看业务场景和容忍度。
还有一个和前后端分离项目很相关的经验:很多项目后端接口设计时没考虑读写分离,所有接口都在同一个Service里操作同一个数据源,后面接入读写分离时各种改。早一点把读接口和写接口在代码层面区分开,后面会省很多事。
4. 常见问题与排查技巧实录
4.1 Slave_IO_Running: No 怎么办
这是从库最常见的问题。IO线程连不上主库,可能的原因有:
- 网络不通。防火墙、安全组、容器网络配置问题。
- 复制账号权限不对。
- 主库
server-id冲突,或者主库binlog没开。 - MASTER_HOST填错,或者MASTER_PORT没暴露出来。
排查思路很简单,先看报错:
SHOW SLAVE STATUS\G重点看Last_IO_Errno和Last_IO_Error这两个字段。报错信息会告诉你具体原因,大部分情况都能直接看出来。
比如常见的一个错误是Got fatal error 1236 from master when reading data from binary log,这种一般是binlog位置不对,主库binlog已经清理,或者你指定的日志文件名和位置不匹配。解决办法就是回头重新看主库的SHOW MASTER STATUS,把MASTER_LOG_FILE和MASTER_LOG_POS改对。
4.2 Slave_SQL_Running: No 怎么处理
SQL线程报错往往是因为从库重放时遇到了冲突。比如你用root账号在从库上手动插入了一条数据,然后主库又发来同样的插入,从库就会因为主键冲突重放失败。
报错字段是Last_SQL_Errno和Last_SQL_Error。排查时先把从库的SQL线程停掉,分析报错原因,人工处理好冲突数据,再重启SQL线程。MySQL 8.0里有mysqlbinlog配合START SLAVE UNTIL的跳过方式,但我不推荐新手一上来就跳过事务。跳着跳着,主从就彻底不一致了。
正确的做法是:先用pt-table-checksum这类工具对比主从数据,确认差异范围,再手动修复。修复后要把对应binlog事件跳过,然后重新确认同步位置是否追上主库。
4.3 主从延迟排查:从秒级到毫秒级
如果Seconds_Behind_Master长期不为0,就要找原因了。
先确认是不是有大事务。在主库执行SHOW PROCESSLIST和SHOW MASTER STATUS,看是否有运行时间很长的写操作。如果有,SQL线程卡在等一个超大事务重放,属于预期现象,只能等它跑完。
再检查从库的并行复制配置。5.7及以上版本可以设置:
slave_parallel_type = LOGICAL_CLOCK slave_parallel_workers = 4注意slave_parallel_type默认是DATABASE,如果有多个业务库,可以按库并行;如果是单库,必须改成LOGICAL_CLOCK才能按事务并行。这个参数很多人配置了没生效,就是因为没改类型。
还有一种隐蔽的延迟来源:从库上如果跑着定时任务、大查询、报表分析,占用了大量CPU和I/O,SQL线程自然被拖慢。解决办法是把这些任务挪到另一台专用查询从库,让主同步链路保持干净。
4.4 别忽视事务和锁对主从复制的影响
聊到主从复制,很多人会忽略它和事务、锁的关系。其实MySQL主从复制和“mysql事务处理”、“mysql锁的分类”关系非常紧密。
一个很典型的场景:主库上事务A更新了某一行,还没提交;事务B也更新同一行,被阻塞等待锁。此时binlog里不会出现事务A的变更,因为还没提交。从库看起来很清净,但实际上主库前面已经堆了一批等待锁的事务。一旦事务A提交,主库瞬间连续写入多个binlog事件,从库要追的数据量就会突然变大。
还有个大坑是DDL和锁表。ALTER TABLE操作在MySQL 8.0里多数支持在线DDL,但某些操作仍然可能锁表。锁表期间,从库SQL线程执行重放时也会拿到对应的锁,导致后续binlog事件全部排队。
所以我对团队有一个强制性要求:大表DDL必须安排在业务低峰期执行,并且提前评估从库延迟。否则主库改表10秒,从库可能卡10分钟。
4.5 日常巡检需要盯什么指标
主从复制配好不算结束,维护才是常态。我建议至少监控这几个指标:
Seconds_Behind_Master:从库延迟,超过阈值要报警。Slave_IO_Running和Slave_SQL_Running的状态。- 主库binlog大小和保留天数,避免磁盘被撑爆。
- 从库relay log堆积情况。
- 主从数据一致性,定期跑一致性校验。
另外,如果使用半同步复制,额外关注半同步的状态,确认没有静默退化成异步。
最后分享一点我的实际体会。搭建一套主从复制,跟着教程一步步走,可能半小时就能搞定。真正难的其实是两件事:第一,想清楚自己的业务到底需要哪种复制模式和拓扑结构,而不是无脑跟风;第二,建立一套持续监控和演练的机制,而不是配置完就再也不看。如果你刚开始接触主从复制,建议先在自己电脑上用Docker搭一套一主一从,老老实实把SHOW SLAVE STATUS里每个字段查一遍文档,再动手模拟一次主库宕机、从库提主的演练。这么操作过一轮之后,你对MySQL主从复制的理解会比看十篇教程都深。