主从复制与读写分离
主从复制是 MySQL 高可用的地基:读写分离、备份、故障切换、数据分析都建立在它之上。但它也是最容易被"配通了就不管"的组件——直到某天从库延迟几小时,或者主从数据悄悄不一致。这篇文章按"原理 → 搭建 → 读写分离 → 延迟治理 → 故障切换"展开,命令可直接执行。基准 MySQL 8.0,5.7 的差异单独标注。
一、复制的基本模型
主从复制是异步的,三个线程协作:
- 主库 Binlog Dump 线程:每个从库连接对应一个,负责把 binlog 发给从库。
- 从库 IO 线程:接收 binlog 事件,写入本地 relay log。
- 从库 SQL 线程:读 relay log,重放为实际的数据变更。
数据流是:主库执行事务 → 写 binlog → 提交 → dump 线程推送 → 从库 IO 线程写 relay log → SQL 线程重放。
注意:MySQL 8.0 中 SQL 线程默认是多线程的(
replica_parallel_workers,8.0.27 前叫slave_parallel_workers),5.7 默认是 0(即单线程)。这是 8.0 复制性能提升的关键,升级后必须显式配置并行度。
二、binlog 三种格式
| 格式 | 内容 | 优点 | 缺点 |
| --- | --- | --- | --- |
| STATEMENT | 原始 SQL | 日志小 | 函数、UUID()、LIMIT 不确定时主从不一致 |
| ROW | 每行的前后镜像 | 绝对一致,可做闪回 | 日志大,批量更新膨胀 |
| MIXED | 按语句自动切换 | 折中 | 行为不易预期 |
8.0 默认是 ROW,5.7 默认也是 ROW(5.7.7 起)。生产环境必须用 ROW,理由是确定性与可恢复性——STATEMENT 下 NOW()、RAND()、自增锁行为都可能让从库产生不同结果。
SHOW VARIABLES LIKE 'binlog_format';
SHOW VARIABLES LIKE 'binlog_row_image'; -- 默认 FULL,可改 MINIMAL 减小日志
SHOW VARIABLES LIKE 'binlog_expire_logs_seconds'; -- 8.0 默认 2592000(30 天)
5.7 用 expire_logs_days(默认 0,即不过期),8.0 改为秒级变量 binlog_expire_logs_seconds,旧变量在 8.0.11 后废弃。
三、GTID 与位点复制
传统复制靠 MASTER_LOG_FILE + MASTER_LOG_POS 定位,主库切换后要重新计算位点,容易出错。GTID(Global Transaction Identifier)给每个事务一个全局唯一编号,格式为 server_uuid:transaction_id,从库只需告诉主库"我已执行到哪",自动定位。
# 主库与从库都要配置
[mysqld]
server_id = 1 # 每个实例唯一
log_bin = /var/log/mysql/mysql-bin
binlog_format = ROW
gtid_mode = ON
enforce_gtid_consistency = ON
# 从库额外配置
relay_log = /var/log/mysql/relay-bin
read_only = ON
super_read_only = ON
replica_parallel_workers = 8
replica_parallel_type = LOGICAL_CLOCK
replica_preserve_commit_order = ON
8.0 推荐 replica_parallel_type = LOGICAL_CLOCK(基于组提交的并行),5.7 中该参数叫 slave_parallel_type,且 LOGICAL_CLOCK 从 5.7.2 起可用。replica_preserve_commit_order 保证并行重放后提交顺序与主库一致,避免从库出现"后提交的事务先可见"。
四、搭建主从(GTID 方式)
1. 主库准备
-- 创建复制账号
CREATE USER 'repl'@'%' IDENTIFIED BY 'Repl@2026';
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%';
FLUSH PRIVILEGES;
-- 确认 GTID 已执行集合
SHOW MASTER STATUS\G
输出示例:
*************************** 1. row ***************************
File: mysql-bin.000003
Position: 157
Binlog_Do_DB:
Binlog_Ignore_DB:
Executed_Gtid_Set: 3e11fa47-71ca-11ee-9c2f-0242ac110002:1-42
2. 主库做一致性备份
mysqldump -uroot -p \
--single-transaction \
--master-data=2 \
--set-gtid-purged=ON \
--routines --triggers --events \
--databases shop > /tmp/shop.sql
--single-transaction 利用 RR 快照保证一致性,不锁表,只对 InnoDB 有效。--master-data=2 会把位点以注释形式写入备份文件(8.0.26 起建议用 --source-data=2,旧参数仍兼容)。
3. 从库导入并启动复制
mysql -uroot -p < /tmp/shop.sql
CHANGE REPLICATION SOURCE TO
SOURCE_HOST = '10.0.1.10',
SOURCE_PORT = 3306,
SOURCE_USER = 'repl',
SOURCE_PASSWORD = 'Repl@2026',
SOURCE_AUTO_POSITION = 1,
SOURCE_CONNECT_RETRY = 10,
SOURCE_RETRY_COUNT = 3;
START REPLICA;
SHOW REPLICA STATUS\G
注意:8.0.23 起
CHANGE MASTER TO改名为CHANGE REPLICATION SOURCE TO,START SLAVE改为START REPLICA,SHOW SLAVE STATUS改为SHOW REPLICA STATUS。旧语法仍可用但会告警;5.7 只有旧语法。运维脚本跨版本时必须兼容两套。
关键状态字段:
Replica_IO_Running: Yes
Replica_SQL_Running: Yes
Seconds_Behind_Source: 0
Retrieved_Gtid_Set: 3e11fa47-...:1-42
Executed_Gtid_Set: 3e11fa47-...:1-42
Last_IO_Error:
Last_SQL_Error:
两个 Running 都是 Yes、Seconds_Behind_Source 为 0、两个 GTID 集合相等,才算正常。5.7 中对应字段名是 Slave_IO_Running、Seconds_Behind_Master。
五、读写分离
方案一:应用层路由
用 Spring 的 AbstractRoutingDataSource 配合 AOP,按方法注解切换数据源。
public class RoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContext.isReadOnly() ? "slave" : "master";
}
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ReadOnly {
}
这种方式简单,但要自己处理"写后立即读"的一致性问题。
方案二:中间件
ShardingSphere-Proxy、MyCat、ProxySQL 在代理层解析 SQL,自动把 SELECT 路由到从库。优点是业务无侵入、支持强制走主库的 hint;缺点是增加一跳网络、代理本身成为需要高可用的组件。
ShardingSphere-JDBC 的读写分离配置:
spring:
shardingsphere:
rules:
readwrite-splitting:
data-sources:
rw-ds:
write-data-source-name: master
read-data-source-names: slave0,slave1
load-balancer-name: round-robin
load-balancers:
round-robin:
type: ROUND_ROBIN
写后读一致性
主从异步复制下,写完立刻读从库可能读不到。三种处理:
- 强制走主库:写操作后的 N 秒内,或同一事务/同一请求内,读走主库。
- 按 GTID 等待:记录写入返回的 GTID,读从库前等待
WAIT_FOR_EXECUTED_GTID_SET超时。 - 会话粘滞:同一会话固定走主库一段时间。
-- 在从库上等待指定 GTID 集合执行完成,超时 1 秒
SELECT WAIT_FOR_EXECUTED_GTID_SET('3e11fa47-71ca-11ee-9c2f-0242ac110002:43', 1);
返回值 0 表示已追上,1 表示超时。这是比"睡 100 ms"精确得多的方案。
六、主从延迟的成因与治理
Seconds_Behind_Source 的计算方式是"从库当前时间 - 正在重放的事件的原始时间戳",只在 SQL 线程运行时有效,SQL 线程停了会显示 NULL。
常见成因:
| 成因 | 表现 | 治理 |
| --- | --- | --- |
| 大事务 | 单事务重放几分钟 | 拆分批量更新,控制单事务行数 |
| 从库单线程重放 | 主库并发高时堆积 | 开并行复制,LOGICAL_CLOCK |
| 从库硬件弱于主库 | 持续落后 | 从库配置不低于主库 |
| 从库承担大量查询 | SQL 线程抢不到 CPU/IO | 读写分离的从库与备份从库分开 |
| 无主键表 | ROW 格式下全表扫描定位行 | 所有表必须有主键 |
| DDL | 大表 ALTER 阻塞重放 | 用 gh-ost / pt-osc 在线变更 |
无主键表是隐藏杀手:ROW 格式下,从库重放 UPDATE/DELETE 时如果没有主键,只能全表扫描匹配行,一条语句可能扫几百万行。检查方法:
SELECT t.TABLE_SCHEMA, t.TABLE_NAME
FROM information_schema.TABLES t
LEFT JOIN information_schema.TABLE_CONSTRAINTS c
ON c.TABLE_SCHEMA = t.TABLE_SCHEMA
AND c.TABLE_NAME = t.TABLE_NAME
AND c.CONSTRAINT_TYPE = 'PRIMARY KEY'
WHERE t.TABLE_TYPE = 'BASE TABLE'
AND t.TABLE_SCHEMA NOT IN ('mysql','sys','information_schema','performance_schema')
AND c.CONSTRAINT_NAME IS NULL;
监控延迟建议用 pt-heartbeat,它不依赖 Seconds_Behind_Source,通过在主库定时写心跳表、在从库读时间差,能测出真实延迟:
# 主库持续写心跳
pt-heartbeat --daemonize --update --database heartbeat \
--host 10.0.1.10 --user repl --password 'Repl@2026'
# 从库查看延迟
pt-heartbeat --monitor --database heartbeat \
--host 10.0.1.11 --user repl --password 'Repl@2026'
七、故障切换
主库宕机后,需要选一个延迟最小的从库提升为主:
-- 1. 确认从库已追平
SHOW REPLICA STATUS\G -- Seconds_Behind_Source = 0
-- 2. 停止复制并提升
STOP REPLICA;
RESET REPLICA ALL;
-- 3. 关闭只读
SET GLOBAL super_read_only = OFF;
SET GLOBAL read_only = OFF;
其余从库指向新主:
STOP REPLICA;
CHANGE REPLICATION SOURCE TO
SOURCE_HOST = '10.0.1.11',
SOURCE_AUTO_POSITION = 1;
START REPLICA;
GTID 模式下不需要计算位点,这是它最大的价值。生产环境应使用 MHA、Orchestrator 或云厂商的托管切换,并配合 VIP / DNS 切换,避免应用改配置。
八、常见坑 / 最佳实践
server_id必须全局唯一。重复会导致复制中断且报错隐晦,云环境克隆实例时尤其容易踩。- 所有表必须有主键。这是 ROW 格式复制的前提,也是延迟和一致性的保障。上线前用上面的 SQL 做一次全库扫描。
- 控制单事务大小。批量
UPDATE或DELETE一次几十万行,会造成秒级到分钟级的延迟尖刺。按 500~1000 行分批提交。 - 从库开启
super_read_only。只开read_only时,有SUPER权限的账号仍能写入,是主从数据不一致的常见来源。8.0 默认账号权限模型下更要注意。 - 读写分离要有强制走主库的开关。写后读、强一致查询、报表校验必须能指定主库,否则会出现"刚保存就查不到"的用户投诉。
九、小结
- 复制由 dump 线程、IO 线程、SQL 线程协作完成,异步复制天然存在延迟窗口。
- binlog 用
ROW格式,保证确定性与可恢复性;GTID 让主库切换无需计算位点。 - 8.0 的
CHANGE REPLICATION SOURCE TO、SHOW REPLICA STATUS与 5.7 的MASTER/SLAVE语法不通用,脚本要兼容。 - 读写分离的核心难点是写后读一致性,用强制主库或
WAIT_FOR_EXECUTED_GTID_SET解决。 - 延迟治理的关键是并行复制、拆大事务、消除无主键表,监控用
pt-heartbeat而不是Seconds_Behind_Source。
- 点赞
- 收藏
- 关注作者
评论(0)