主从复制与读写分离
问题
MySQL 主从复制的原理是什么?有哪些复制模式?如何实现读写分离?主从延迟如何解决?
答案
主从复制原理
MySQL 主从复制基于 binlog,主库将数据变更写入 binlog,从库获取并重放 binlog 来保持数据同步。
涉及 3 个线程:
| 线程 | 位置 | 职责 |
|---|---|---|
| Binlog Dump Thread | 主库 | 读取 binlog 事件,发送给从库 |
| I/O Thread | 从库 | 接收 binlog 事件,写入 Relay Log |
| SQL Thread | 从库 | 读取 Relay Log,执行 SQL 更新数据 |
复制模式
按同步方式分
| 模式 | 说明 | 优点 | 缺点 |
|---|---|---|---|
| 异步复制(默认) | 主库提交事务后立即返回,不等从库确认 | 性能最好 | 主库崩溃可能丢数据 |
| 半同步复制 | 主库等待至少一个从库确认收到 binlog 后返回 | 数据更安全 | 性能有一定损耗 |
| 全同步复制 | 主库等待所有从库都执行完后返回 | 数据最安全 | 性能最差,基本不用 |
半同步复制
MySQL 5.7+ 支持增强半同步复制(after sync):主库在 binlog 写入 Relay Log 后才提交事务(而非提交后等待),进一步保证数据一致性。
-- 安装半同步复制插件
INSTALL PLUGIN rpl_semi_sync_master SONAME 'semisync_master.so';
INSTALL PLUGIN rpl_semi_sync_slave SONAME 'semisync_slave.so';
-- 启用
SET GLOBAL rpl_semi_sync_master_enabled = 1;
SET GLOBAL rpl_semi_sync_slave_enabled = 1;
按复制格式分
取决于 binlog 格式:
| binlog 格式 | 复制方式 | 说明 |
|---|---|---|
| Statement | 基于语句复制(SBR) | 发送 SQL 语句,某些语句可能主从不一致 |
| Row | 基于行复制(RBR) | 发送行数据变更,一致性好但日志大 |
| Mixed | 混合复制(MBR) | 自动选择 SBR 或 RBR |
GTID 复制
GTID(Global Transaction ID) 是 MySQL 5.6 引入的全局事务标识,格式为 server_uuid:transaction_id。
| 对比 | 传统复制 | GTID 复制 |
|---|---|---|
| 定位方式 | binlog 文件名 + 偏移量 | GTID(全局唯一标识) |
| 故障切换 | 需要手动指定文件和位置 | 自动定位,更简单 |
| 跳过事务 | 通过 SQL_SLAVE_SKIP_COUNTER | 注入空事务跳过 |
| 一致性 | 需要手动保证 | GTID 自动检测缺失事务 |
开启 GTID 复制
-- my.cnf
-- gtid_mode = ON
-- enforce-gtid-consistency = ON
-- 从库配置
CHANGE MASTER TO
MASTER_HOST = '192.168.1.100',
MASTER_USER = 'repl',
MASTER_PASSWORD = 'password',
MASTER_AUTO_POSITION = 1; -- GTID 自动定位
读写分离
将写操作发送到主库,读操作分发到从库,分散数据库压力。
实现方式
| 方式 | 代表 | 优点 | 缺点 |
|---|---|---|---|
| 代码层 | Spring @Transactional(readOnly=true) + 动态数据源 | 灵活,无额外组件 | 侵入代码 |
| 中间件代理 | MyCat、ProxySQL、MaxScale | 对应用透明 | 多一层网络开销 |
| 框架支持 | ShardingSphere-JDBC | 功能丰富 | 学习成本 |
Spring 动态数据源(代码层实现)
// 定义数据源路由
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSourceType();
}
}
// 通过 AOP 切换数据源
@Aspect
@Component
public class DataSourceAspect {
@Before("@annotation(readOnly)")
public void switchToSlave(ReadOnly readOnly) {
DataSourceContextHolder.setDataSourceType("slave");
}
@After("@annotation(readOnly)")
public void restore() {
DataSourceContextHolder.clear();
}
}
主从延迟
延迟原因
| 原因 | 说明 |
|---|---|
| 从库单线程回放 | MySQL 5.6 前 SQL Thread 是单线程的,大事务回放慢 |
| 从库机器性能差 | 从库硬件配置低于主库 |
| 主库并发写入高 | 大量写操作导致 binlog 积压 |
| 大事务 | 一个事务涉及大量行修改,从库回放时间长 |
| DDL 操作 | ALTER TABLE 等 DDL 在从库执行时阻塞其他回放 |
| 网络延迟 | 主从之间网络状况差 |
查看主从延迟
-- 在从库执行
SHOW SLAVE STATUS\G
-- 关注 Seconds_Behind_Master 字段
-- 0 表示无延迟,NULL 表示复制中断
解决方案
| 方案 | 说明 |
|---|---|
| 并行复制 | MySQL 5.7+ 支持多线程回放(基于组提交的并行) |
| 强制走主库 | 写操作后的读请求强制走主库 |
| 等待策略 | 读请求检查 GTID 是否同步到从库,未同步则等待 |
| 半同步复制 | 保证 binlog 至少到达一个从库 |
| 缓存过渡 | 写操作后将数据写入 Redis,读请求优先读缓存 |
MySQL 5.7+ 并行复制配置
-- 从库 my.cnf
-- slave_parallel_type = LOGICAL_CLOCK -- 基于逻辑时钟的并行
-- slave_parallel_workers = 8 -- 并行线程数
-- slave_preserve_commit_order = 1 -- 保证提交顺序
MySQL 8.0 并行复制增强
MySQL 8.0.27+ 引入了 WRITESET 并行复制策略(binlog_transaction_dependency_tracking = WRITESET),基于事务修改的行集合来判断并行度,比 LOGICAL_CLOCK 并行度更高。
高可用架构
| 方案 | 说明 | 适用场景 |
|---|---|---|
| MHA | 自动主从切换,补齐 binlog 差异 | 传统主从架构 |
| MySQL Group Replication(MGR) | 基于 Paxos 协议的多节点复制 | 强一致性需求 |
| InnoDB Cluster | MGR + MySQL Router + MySQL Shell | 官方推荐方案 |
| Orchestrator | 自动故障检测和拓扑管理 | GitHub 开源 |
| ProxySQL + Keepalived | VIP 漂移 + 代理层切换 | 中小型架构 |
常见面试问题
Q1: MySQL 主从复制的原理?
答案:
基于 binlog 的复制,涉及 3 个线程:
- 主库的 Binlog Dump 线程:当从库连接时,读取 binlog 事件发送给从库
- 从库的 I/O 线程:接收主库发来的 binlog 事件,写入本地的 Relay Log(中继日志)
- 从库的 SQL 线程:读取 Relay Log,解析并执行 SQL,更新从库数据
整个过程是异步的(默认),主库提交事务后不等待从库确认。可以配置为半同步复制提高数据安全性。
Q2: 主从延迟怎么解决?
答案:
根据场景选择不同方案:
- 并行复制(最根本的解决方案):MySQL 5.7+ 开启
slave_parallel_type=LOGICAL_CLOCK,多线程回放 - 强制走主库:对实时性要求高的读(如支付后查订单),直接读主库。通常配合缓存标记:写操作后设置一个短暂的缓存标记(如 Redis key TTL 1s),读请求发现标记存在则走主库
- GTID 等待:使用
WAIT_FOR_EXECUTED_GTID_SET(gtid_set, timeout)确认从库已同步到指定 GTID - 避免大事务:将大事务拆成小事务,减少单次回放时间
Q3: 异步复制和半同步复制的区别?
答案:
- 异步复制(默认):主库提交事务后立即返回客户端,不等待从库。性能最高,但主库崩溃时如果 binlog 还没传到从库,会丢失数据
- 半同步复制:主库提交事务后等待至少一个从库确认收到 binlog(写入 Relay Log)后才返回客户端。性能有损耗(取决于网络延迟),但不会丢失已确认的数据
MySQL 5.7 的增强半同步(after sync)还保证了主库不会出现"幽灵事务"——即事务在主库可见但 binlog 未到从库的情况。
Q4: 读写分离如何实现?
答案:
三种主流方式:
- 代码层:通过 AOP + 动态数据源路由,根据注解(如
@ReadOnly)或方法名(findXxx→ 从库,saveXxx→ 主库)切换数据源。灵活但有代码侵入 - 中间件代理:如 ProxySQL、MaxScale,拦截所有 SQL,自动识别读写并路由。对应用透明但多一层网络
- ShardingSphere-JDBC:在 JDBC 层面实现读写分离路由,配置简单,功能丰富
核心注意点:
- 写操作后的立即读应走主库(避免主从延迟导致读不到刚写的数据)
- 事务内的所有操作应走主库
Q5: GTID 复制相比传统复制有什么优势?
答案:
传统复制通过 binlog 文件名 + 偏移量 定位同步位置,GTID 复制使用全局唯一的事务 ID。
核心优势:
- 故障切换简单:传统复制需要找到新主库的 binlog 文件和偏移量,GTID 自动根据 ID 定位缺失的事务
- 数据一致性:GTID 能自动检测和跳过已执行的事务,避免重复执行
- 运维友好:通过 GTID 可以清楚地知道主从之间差了哪些事务