MySQL 主从复制原理与故障处理,从原理到实战一篇讲透
MySQL 主从复制原理与故障处理,从原理到实战一篇讲透
凌晨三点,被电话吵醒:数据库报警,从库延迟超过 30 分钟,业务开始出现读写分离访问异常。
爬起来一看:
- 主库正常,QPS 20000
- 从库延迟 35 分钟
- 从库 CPU 打满,IO 等待高
最后查到原因:有一个全量表扫描的查询跑到从库了,导致从库 relay log 应用速度跟不上。
这是主从复制架构里很典型的问题。要解决这类问题,得先理解主从复制的工作原理。
MySQL 主从复制原理
三种复制方式
| 方式 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 异步复制 | 主库执行后立即返回,不等从库 | 性能最好 | 可能丢数据 |
| 半同步复制 | 主库等至少一个从库确认收到日志 | 数据安全性提高 | 有一定延迟 |
| 全同步复制 | 主库等所有从库执行完才返回 | 完全一致 | 延迟大,性能差 |
MySQL 默认是异步复制。
复制原理核心组件
┌─────────────┐ ┌─────────────┐
│ 主库 │ │ 从库 │
│ │ │ │
│ ┌───────┐ │ ────> │ ┌───────┐ │
│ │Binlog │ │ │ │Relay │ │
│ │Dump │ │ │ │Log │ │
│ │Thread │ │ │ │IO │ │
│ └───────┘ │ │ │Thread │ │
│ │ │ └───────┘ │
│ │ │ ┌───────┐ │
│ │ │ │SQL │ │
│ │ │ │Thread │ │
│ │ │ └───────┘ │
└─────────────┘ └─────────────┘
- Binlog Dump Thread:主库上运行,发送 binlog 给从库
- IO Thread:从库上运行,接收 binlog 并写入 relay log
- SQL Thread:从库上运行,读取 relay log 并执行
复制格式
binlog 有三种格式:
| 格式 | 记录内容 | 优点 | 缺点 |
|---|---|---|---|
| STATEMENT | 记录 SQL 语句 | binlog 小 | 函数、存储过程结果可能不一致 |
| ROW | 记录行变化 | 精确 | binlog 大 |
| MIXED | 混合,优先 STATEMENT | 平衡 | 可能不一致 |
-- 查看当前格式
SHOW VARIABLES LIKE 'binlog_format';
-- 设置(需要重启或重新开会话)
SET GLOBAL binlog_format = 'ROW';
建议用 ROW 格式,数据一致性更有保障。
主从复制配置
主库配置
[mysqld]
server-id = 1
log-bin = mysql-bin
binlog_format = ROW
expire-logs-days = 7
sync-binlog = 1 # 每次事务提交同步 binlog 到磁盘
从库配置
[mysqld]
server-id = 2
log-replica-updates = 1 # 5.7+ 用这个
replicate-do-db = app_db # 只复制特定库
replicate-ignore-db = mysql # 忽略特定库
relay-log = relay-bin
read-only = 1 # 从库只读(但 super 用户不受限制)
建立复制关系
-- 主库:创建复制账号
CREATE USER 'repl'@'%' IDENTIFIED BY 'repl_password';
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%';
-- 主库:获取 binlog 位置
SHOW MASTER STATUS;
-- File: mysql-bin.000123
-- Position: 456
从库执行:
CHANGE MASTER TO
MASTER_HOST = '主库IP',
MASTER_USER = 'repl',
MASTER_PASSWORD = 'repl_password',
MASTER_LOG_FILE = 'mysql-bin.000123',
MASTER_LOG_POS = 456;
START SLAVE;
SHOW SLAVE STATUS\G
常见主从问题
问题一:主从延迟
-- 从库查看延迟
SHOW SLAVE STATUS\G
-- Seconds_Behind_Master: 300 -- 延迟 5 分钟
延迟原因和解决方案:
原因 1:从库 IO 线程网络延迟
-- 查看 IO 线程状态
SHOW SLAVE STATUS\G
-- Slave_IO_State: Waiting for master to send event
-- Slave_IO_Running: Yes
-- Slave_SQL_Running: Yes
-- 查看网络延迟
SHOW SLAVE STATUS\G
-- Read_Master_Log_Pos: 12345678 -- 主库已写到位置
-- Exec_Master_Log_Pos: 12345678 -- 从库已执行位置
原因 2:从库 SQL 线程执行慢
-- 查看 SQL 线程状态
SHOW SLAVE STATUS\G
-- Relay_Log_Pos: 12345678
-- Relay_Log_Space: 87654321 -- relay log 大小异常
-- 查看从库慢查询
SHOW GLOBAL STATUS LIKE 'Slow_queries';
原因 3:大事务导致延迟
主库执行一个大事务只需要 1 秒,但 binlog 传送到从库、从库重放需要 10 分钟。
-- 主库查看当前事务
SHOW PROCESSLIST;
-- Command: Binlog Dump
-- Info: BEGIN; DELETE FROM large_table ...; COMMIT
-- 优化:拆大事务
DELETE FROM large_table WHERE id BETWEEN 1 AND 10000;
DELETE FROM large_table WHERE id BETWEEN 10001 AND 20000;
-- 分批删除
原因 4:缺少索引导致从库执行慢
-- 主库执行:0.5秒(结果集在内存)
SELECT * FROM orders WHERE user_id = 10086;
-- 从库执行:30秒(需要回表扫描大量数据)
-- 从库执行计划可能和主库不同
解决方案:
-- 在主从库都建相同索引
ALTER TABLE orders ADD INDEX idx_user_id (user_id);
问题二:从库报错跳过
-- 错误代码 1032:记录不存在
-- 错误代码 1062:主键冲突
-- 方案一:跳过错误
SET GLOBAL sql_slave_skip_counter = 1;
STOP SLAVE;
START SLAVE;
-- 方案二:在配置里忽略错误
[mysqld]
slave-skip-errors = 1032,1062
-- 方案三:手动修复
-- 找到错误的 SQL,手动在从库执行
问题三:binlog position 丢失
GTID(Global Transaction Identifier)可以解决这个问题:
[mysqld]
gtid_mode = ON
enforce_gtid_consistency = ON
GTID 复制不依赖文件位置:
CHANGE MASTER TO
MASTER_AUTO_POSITION = 1; -- 自动定位
问题四:从库 read_only 无效
-- 检查 read_only 配置
SHOW VARIABLES LIKE 'read_only';
SHOW VARIABLES LIKE 'super_read_only';
-- super_read_only 限制 super 权限用户
-- 如果需要完全只读,要开启 super_read_only
SET GLOBAL super_read_only = ON;
主从复制监控
关键监控指标
-- 延迟秒数
SHOW SLAVE STATUS\G
Seconds_Behind_Master
-- IO 线程状态
Slave_IO_Running: Yes/No
Slave_SQL_Running: Yes/No
-- 错误信息
Last_IO_Error
Last_SQL_Error
-- binlog 位置
Read_Master_Log_Pos
Exec_Master_Log_Pos
监控脚本
#!/bin/bash
# 检查主从复制状态
STATUS=$(mysql -e "SHOW SLAVE STATUS\G" | grep -E "Slave_IO_Running|Slave_SQL_Running|Seconds_Behind_Master")
if echo "$STATUS" | grep -q "Slave_IO_Running: No"; then
echo "IO thread not running"
exit 1
fi
if echo "$STATUS" | grep -q "Slave_SQL_Running: No"; then
echo "SQL thread not running"
exit 1
fi
LAG=$(mysql -e "SHOW SLAVE STATUS\G" | grep "Seconds_Behind_Master" | awk '{print $2}')
if [ "$LAG" -gt 60 ]; then
echo "Replication lag: $LAG seconds"
exit 1
fi
echo "Replication healthy"
主从切换流程
主库故障时,需要把一个从库提升为新的主库:
步骤一:确认从库数据一致
-- 在所有从库执行,选择最领先的
SHOW SLAVE STATUS\G
-- 检查 Relay_Master_Log_File 和 Exec_Master_Log_Pos
步骤二:停止所有从库复制
STOP SLAVE;
步骤三:提升候选从库
-- 在候选从库上执行
RESET SLAVE ALL; -- 清除复制关系
-- 开启 log-bin
SET GLOBAL read_only = OFF;
步骤四:通知应用切换
更新连接配置,指向新的主库。
步骤五:重建其他从库
-- 在其他从库执行,指向新的主库
CHANGE MASTER TO
MASTER_HOST = '新主库IP',
MASTER_AUTO_POSITION = 1;
START SLAVE;
读写分离最佳实践
读写分离的坑
坑一:读到旧数据
主从延迟导致写完立即读可能读到从库未同步的旧数据。
解决方案:
- 强一致读走主库
- 允许延迟读走从库
- 用 GTID + 配置延迟阈值
坑二:从库无写权限但应用配置错误
应用有时会意外写到从库。
解决方案:
- 从库配置
read_only = 1和super_read_only = 1 - 应用层做好权限分离
坑三:自增主键冲突
-- 主库
INSERT INTO orders (...) VALUES (...);
-- 从库 AUTO_INCREMENT 可能不一致
-- 解决:在所有库配置相同的自增步长
[mysqld]
auto_increment_increment = 2
auto_increment_offset = 1 -- 主库
-- 从库 offset = 2
读写分离框架
推荐使用:
- ShardingSphere:支持读写分离、数据库分片
- Vitess:YouTube 开源的数据库中间件
- MySQL Router:MySQL 官方代理
总结
主从复制是 MySQL 高可用架构的基础:
- 原理:Binlog + IO Thread + SQL Thread,异步或半同步
- 配置:server-id、binlog-format、GTID
- 延迟:监控
Seconds_Behind_Master,优化 SQL、加索引、拆大事务 - 故障处理:跳过错误、手动修复、切换主库
- 读写分离:区分强一致读和可延迟读,合理路由
遇到主从问题不要慌,按这个顺序排查:
SHOW SLAVE STATUS看状态Seconds_Behind_Master看延迟Last_IO_Error/Last_SQL_Error看错误- 根据错误类型选择跳过或修复
主从复制的本质是让多个数据库节点保持数据一致,理解了这个本质,大部分问题都能自己推导出来。
