Skip to content

主从复制与高可用深度解析

面试官:MySQL 主从复制的原理是什么?

:主库写 binlog,从库的 IO Thread 接收并写入 Relay Log,SQL Thread 重放…

面试官:那主从延迟是怎么产生的?如何解决?

这个追问把很多人问住了。能说清「SQL Thread 单线程重放、大事务、网络延迟、并行复制」的候选人,才能真正脱颖而出。


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 有哪三种格式?各有什么优缺点?”

三种格式对比

格式记录内容优点缺点适用场景
STATEMENTSQL 语句日志量小可能主从不一致简单业务
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';

Q3:主从延迟是如何产生的?必考

Section titled “Q3:主从延迟是如何产生的?”

延迟产生的原因

主库并发写入 vs 从库单线程重放
主库:
┌─────────────────────────────┐
│ 事务1、事务2、事务3 并发执行 │
│ 时间:1 秒 │
└─────────────────────────────┘
从库(MySQL 5.6 之前):
┌─────────────────────────────┐
│ SQL Thread 单线程重放 │
│ 事务1 → 事务2 → 事务3 │
│ 时间:3 秒 │
└─────────────────────────────┘
结果:从库落后主库 2 秒

延迟的常见原因

  1. 从库单线程重放(主要原因)

    • MySQL 5.6 之前,SQL Thread 是单线程
    • 主库并发写入,从库串行重放
  2. 大事务

    -- 主库执行 10 分钟的大事务
    DELETE FROM logs WHERE create_time < '2020-01-01';
    -- 从库需要同样 10 分钟重放
  3. 从库硬件性能差

    • 从库配置低于主库
    • 从库承担读请求,负载高
  4. 网络延迟

    • 跨机房复制
    • 网络带宽不足
  5. 无主键表

    -- 从库重放时,无主键表需要全表扫描
    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 = 1
rpl_semi_sync_master_timeout = 1000 -- 超时降级为异步

3. 读写分离 + 强制走主库

// 业务层面控制
// 写操作:走主库
@Transactional
public 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 条

Q5:MySQL 高可用有哪些方案?必考

Section titled “Q5:MySQL 高可用有哪些方案?”

常见方案对比

方案原理优点缺点适用场景
主从切换手动/脚本切换简单需人工介入小规模
MHA自动故障切换成熟稳定需要额外节点中小规模
MGRPaxos 协议官方方案性能开销大中等规模
MySQL ClusterNDB 存储引擎高可用架构复杂金融场景
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 {
}
@Aspect
public class DataSourceAspect {
@Before("@annotation(readOnly)")
public void before(ReadOnly readOnly) {
DataSourceContextHolder.set("slave");
}
@After("@annotation(readOnly)")
public void after(ReadOnly readOnly) {
DataSourceContextHolder.clear();
}
}
// 使用
@ReadOnly
public 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: db
dataSources:
master:
url: jdbc:mysql://master:3306/db
slave:
url: jdbc:mysql://slave:3306/db
rules:
- !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\G
Retrieved_Gtid_Set: 3E11FA47-71CA-11E1-9E33-C80AA9429562:1-100
Executed_Gtid_Set: 3E11FA47-71CA-11E1-9E33-C80AA9429562:1-95
-- 一眼看出复制进度

配置 GTID

-- my.cnf
gtid_mode = ON
enforce_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
- 问题:主从延迟,关键业务走主库