主从复制与高可用深度解析
面试官:MySQL 主从复制的原理是什么?
你:主库写 binlog,从库的 IO Thread 接收并写入 Relay Log,SQL Thread 重放…
面试官:那主从延迟是怎么产生的?如何解决?
这个追问把很多人问住了。能说清「SQL Thread 单线程重放、大事务、网络延迟、并行复制」的候选人,才能真正脱颖而出。
链式追问一:主从复制原理
Section titled “链式追问一:主从复制原理”Q1:MySQL 主从复制的流程是什么?必考
Section titled “Q1:MySQL 主从复制的流程是什么?”复制流程:
主库(Master) 从库(Slave)───────────── ────────────1. 执行事务2. 写 binlog ──── binlog ──→ IO Thread 接收 3. 写 Relay Log(中继日志) 4. SQL Thread 读取 Relay Log 5. 重放 SQL,更新从库数据三个关键线程:
| 线程 | 作用 | 位置 |
|---|---|---|
| Dump Thread | 发送 binlog 给从库 | 主库 |
| IO Thread | 接收 binlog,写入 Relay Log | 从库 |
| SQL Thread | 读取 Relay Log,重放事务 | 从库 |
详细流程:
1. 主库执行事务 ┌─────────────────────────────┐ │ BEGIN; │ │ UPDATE users SET age = 30; │ │ COMMIT; │ └─────────────────────────────┘ ↓2. 写入 binlog ┌─────────────────────────────┐ │ binlog.000001: │ │ - UPDATE users SET age=30 │ └─────────────────────────────┘ ↓3. Dump Thread 发送 binlog ┌─────────────────────────────┐ │ 通过网络发送给从库 │ └─────────────────────────────┘ ↓4. IO Thread 接收并写入 Relay Log ┌─────────────────────────────┐ │ Relay Log: │ │ - UPDATE users SET age=30 │ └─────────────────────────────┘ ↓5. SQL Thread 重放事务 ┌─────────────────────────────┐ │ 执行 UPDATE users SET age=30│ └─────────────────────────────┘Q2:binlog 有哪三种格式?各有什么优缺点?高频
Section titled “Q2:binlog 有哪三种格式?各有什么优缺点?”三种格式对比:
| 格式 | 记录内容 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| STATEMENT | SQL 语句 | 日志量小 | 可能主从不一致 | 简单业务 |
| ROW | 行数据变化 | 数据一致 | 日志量大 | 生产环境 |
| MIXED | 混合模式 | 兼顾两者 | 复杂 | 折中方案 |
详解:
1. STATEMENT(语句模式)
# binlog 内容UPDATE users SET last_login = NOW() WHERE id = 1;
问题:- NOW() 在主库和从库执行时间不同- 主从数据不一致2. ROW(行模式,推荐)
# binlog 内容### UPDATE users.users### WHERE### @1=1 (id)### SET### @2='2024-01-01 10:00:00' (last_login)
优势:- 记录具体的数据变化- 主从数据完全一致3. MIXED(混合模式)
默认使用 STATEMENT特殊情况(如 NOW()、UUID())自动切换到 ROW配置方式:
-- 查看当前格式SHOW VARIABLES LIKE 'binlog_format';
-- 设置为 ROW 模式(推荐)SET GLOBAL binlog_format = 'ROW';链式追问二:主从延迟
Section titled “链式追问二:主从延迟”Q3:主从延迟是如何产生的?必考
Section titled “Q3:主从延迟是如何产生的?”延迟产生的原因:
主库并发写入 vs 从库单线程重放
主库:┌─────────────────────────────┐│ 事务1、事务2、事务3 并发执行 ││ 时间:1 秒 │└─────────────────────────────┘
从库(MySQL 5.6 之前):┌─────────────────────────────┐│ SQL Thread 单线程重放 ││ 事务1 → 事务2 → 事务3 ││ 时间:3 秒 │└─────────────────────────────┘
结果:从库落后主库 2 秒延迟的常见原因:
-
从库单线程重放(主要原因)
- MySQL 5.6 之前,SQL Thread 是单线程
- 主库并发写入,从库串行重放
-
大事务
-- 主库执行 10 分钟的大事务DELETE FROM logs WHERE create_time < '2020-01-01';-- 从库需要同样 10 分钟重放 -
从库硬件性能差
- 从库配置低于主库
- 从库承担读请求,负载高
-
网络延迟
- 跨机房复制
- 网络带宽不足
-
无主键表
-- 从库重放时,无主键表需要全表扫描UPDATE users SET age = 30 WHERE name = 'Alice';-- 性能极差
查看主从延迟:
-- 在从库执行SHOW SLAVE STATUS\G
-- 关键字段Seconds_Behind_Master: 10 -- 延迟 10 秒Q4:如何解决主从延迟问题?高频
Section titled “Q4:如何解决主从延迟问题?”解决方案:
1. 并行复制(MySQL 5.6+)
MySQL 5.6:按库并行┌─────────────────────────────┐│ db1 的事务并行重放 ││ db2 的事务并行重放 │└─────────────────────────────┘限制:单库仍为单线程
MySQL 5.7:按组提交并行(推荐)┌─────────────────────────────┐│ 同一组提交的事务可并行重放 ││ 配置: ││ slave_parallel_workers = 4 ││ slave_parallel_type = LOGICAL_CLOCK │└─────────────────────────────┘效果:延迟降低 80%+2. 半同步复制
异步复制(默认):主库 → binlog → 从库(异步)主库提交成功,不等待从库
半同步复制:主库 → binlog → 从库主库等待至少一个从库确认收到 binlog主库才提交成功
配置:plugin-load = "rpl_semi_sync_master=semisync_master.so"rpl_semi_sync_master_enabled = 1rpl_semi_sync_master_timeout = 1000 -- 超时降级为异步3. 读写分离 + 强制走主库
// 业务层面控制// 写操作:走主库@Transactionalpublic void updateUser(User user) { userDao.update(user); // 主库}
// 读操作:走从库public User getUser(Long id) { return userDao.findById(id); // 从库}
// 强制走主库(对实时性要求高的场景)@Master // 自定义注解public User getUserFromMaster(Long id) { return userDao.findById(id); // 主库}4. 分库分表
单库写入压力大 → 分库分表每个库独立复制 → 降低单库延迟5. 优化大事务
-- ❌ 大事务DELETE FROM logs WHERE create_time < '2020-01-01'; -- 删除 1000 万条
-- ✅ 分批删除DELETE FROM logs WHERE create_time < '2020-01-01' LIMIT 1000;-- 循环执行,每次删除 1000 条链式追问三:高可用架构
Section titled “链式追问三:高可用架构”Q5:MySQL 高可用有哪些方案?必考
Section titled “Q5:MySQL 高可用有哪些方案?”常见方案对比:
| 方案 | 原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 主从切换 | 手动/脚本切换 | 简单 | 需人工介入 | 小规模 |
| MHA | 自动故障切换 | 成熟稳定 | 需要额外节点 | 中小规模 |
| MGR | Paxos 协议 | 官方方案 | 性能开销大 | 中等规模 |
| MySQL Cluster | NDB 存储引擎 | 高可用 | 架构复杂 | 金融场景 |
| Proxy + 主从 | 代理层切换 | 对应用透明 | 代理层单点 | 大规模 |
详解:
1. MHA(Master High Availability)
架构:┌─────────────────────────────┐│ MHA Manager(监控节点) │└─────────────────────────────┘ ↓ 监控┌─────────────────────────────┐│ Master(主库) │└─────────────────────────────┘ ↓ 复制┌──────────┐ ┌──────────┐│ Slave1 │ │ Slave2 │└──────────┘ └──────────┘
故障切换流程:1. MHA Manager 检测到主库宕机2. 从 Slave1 和 Slave2 中选择数据最新的作为新主库3. 其他从库指向新主库4. VIP 漂移到新主库5. 应用连接新主库
切换时间:10-30 秒2. MGR(MySQL Group Replication)
架构(单主模式):┌─────────────────────────────┐│ Primary(主节点) ││ - 接收写请求 │└─────────────────────────────┘ ↓ Paxos 协议┌──────────┐ ┌──────────┐│ Secondary│ │ Secondary││ (只读) │ │ (只读) │└──────────┘ └──────────┘
原理:- 基于 Paxos 协议- 多数节点确认后才提交- 自动故障检测和切换
优势:- 官方方案,无需第三方工具- 自动故障切换- 强一致性
劣势:- 性能开销大(Paxos 协议)- 网络要求高- 最多支持 9 个节点3. Proxy + 主从(如 ShardingSphere-Proxy)
架构:┌─────────────────────────────┐│ 应用 │└─────────────────────────────┘ ↓┌─────────────────────────────┐│ Proxy(代理层) ││ - 读写分离 ││ - 故障切换 │└─────────────────────────────┘ ↓┌──────────┐ ┌──────────┐│ Master │ │ Slave │└──────────┘ └──────────┘
优势:- 对应用透明- 统一管理- 支持分库分表
劣势:- Proxy 单点(需高可用)- 增加一跳延迟Q6:如何实现读写分离?高频
Section titled “Q6:如何实现读写分离?”实现方式:
1. 应用层实现
// 使用动态数据源public class DynamicDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { return DataSourceContextHolder.get(); }}
// 切换数据源@Target({ElementType.METHOD, ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)public @interface ReadOnly {}
@Aspectpublic class DataSourceAspect { @Before("@annotation(readOnly)") public void before(ReadOnly readOnly) { DataSourceContextHolder.set("slave"); }
@After("@annotation(readOnly)") public void after(ReadOnly readOnly) { DataSourceContextHolder.clear(); }}
// 使用@ReadOnlypublic User getUser(Long id) { return userDao.findById(id); // 走从库}2. 中间件实现
ShardingSphere-Proxy:┌─────────────────────────────┐│ 应用 ││ jdbc:mysql://proxy:3307/db │└─────────────────────────────┘ ↓┌─────────────────────────────┐│ ShardingSphere-Proxy ││ - 解析 SQL ││ - SELECT → Slave ││ - INSERT/UPDATE/DELETE → Master │└─────────────────────────────┘ ↓┌──────────┐ ┌──────────┐│ Master │ │ Slave │└──────────┘ └──────────┘
配置:schemaName: dbdataSources: master: url: jdbc:mysql://master:3306/db slave: url: jdbc:mysql://slave:3306/dbrules: - !READWRITE_SPLITTING dataSources: ds_0: writeDataSourceName: master readDataSourceNames: - slave读写分离的问题:
问题1:主从延迟导致读不到最新数据解决方案:- 强制走主库(关键业务)- 延迟检测(Seconds_Behind_Master < 1s 才读从库)
问题2:事务内的读操作解决方案:- 事务内所有操作走主库- Spring @Transactional 自动处理Q7:GTID 是什么?有什么优势?加分
Section titled “Q7:GTID 是什么?有什么优势?”GTID(Global Transaction ID):全局事务ID,为每个事务分配唯一标识。
格式:
GTID = source_id:transaction_id
示例:3E11FA47-71CA-11E1-9E33-C80AA9429562:23↑ ↑server_uuid 事务序号传统复制 vs GTID 复制:
| 对比项 | 传统复制 | GTID 复制 |
|---|---|---|
| 定位事务 | 通过 binlog 文件名 + 位置 | 通过 GTID |
| 切换主库 | 需要手动指定 binlog 位置 | 自动查找 |
| 搭建从库 | 需要锁表备份 | 无需锁表 |
| 故障恢复 | 复杂 | 简单 |
GTID 优势:
1. 故障切换简单传统方式:CHANGE MASTER TO MASTER_HOST='new_master', MASTER_LOG_FILE='mysql-bin.000003', MASTER_LOG_POS=12345; -- 需要手动查找
GTID 方式:CHANGE MASTER TO MASTER_HOST='new_master', MASTER_AUTO_POSITION=1; -- 自动定位
2. 搭建从库简单mysqldump --single-transaction --master-data=2 --triggers --routines --all-databases > backup.sql-- 备份文件中自动包含 GTID 信息
3. 复制状态清晰SHOW SLAVE STATUS\GRetrieved_Gtid_Set: 3E11FA47-71CA-11E1-9E33-C80AA9429562:1-100Executed_Gtid_Set: 3E11FA47-71CA-11E1-9E33-C80AA9429562:1-95-- 一眼看出复制进度配置 GTID:
-- my.cnfgtid_mode = ONenforce_gtid_consistency = ON
-- 查看是否启用SHOW VARIABLES LIKE 'gtid_mode';总结:主从复制与高可用的核心要点
Section titled “总结:主从复制与高可用的核心要点”主从复制原理:- 三个线程:Dump Thread、IO Thread、SQL Thread- binlog 三种格式:STATEMENT、ROW、MIXED(推荐 ROW)- 异步复制导致延迟
主从延迟解决:- 并行复制(MySQL 5.7+)- 半同步复制- 读写分离 + 强制走主库- 分库分表
高可用方案:- MHA:成熟稳定,中小规模- MGR:官方方案,中等规模- Proxy + 主从:大规模,对应用透明
读写分离:- 应用层实现:动态数据源- 中间件实现:ShardingSphere-Proxy- 问题:主从延迟,关键业务走主库