**考点分析:**这道题表面问的是"同步",实际上考察的是对 MySQL 复制体系的理解深度与工程落地经验。
- 能否清晰区分主库 与从库的角色分工和数据流向;
- 是否掌握复制核心链路:Binlog → Relay Log 以及各复制线程的职责;
- 能否说清 STATEMENT / ROW / MIXED 三种 Binlog 格式的差异与取舍;
- 能否把复制机制落到真实业务:读写分离、数据备份、高可用切换、异地容灾;
- 是否具备生产实践意识:复制延迟、一致性、GTID、半同步复制与主从切换。
一、标准回答
MySQL 的主从同步,通常也叫主从复制(Replication) ,是指把一台 MySQL 实例(主库,Master )上的数据变更记录下来,由另一台或多台实例(从库,Slave)主动拉取这些变更并在本地重放,从而使从库数据尽可能与主库保持一致。
它的核心作用是:将写入与读取分离,提升系统的并发处理能力和可用性。具体体现在以下几个方面:
- 读写分离:主库负责写操作,从库负责读操作,分摊数据库压力;
- 数据备份:在从库上执行备份,避免影响主库的正常业务;
- 高可用:主库故障时,级联切换从库接管服务,缩短恢复时间;
- 数据分发:把数据复制到多个节点、多个机房,支撑报表分析和异地容灾。
从特点上看,MySQL 主从复制默认是异步复制 ,数据流向是单向的 (主库到从库),并且整个过程基于二进制日志 Binlog 实现。异步意味着主库提交事务后不会强制等待从库确认,因此从库可能存在短暂延迟。
二、核心原理
根据 MySQL 官方文档的描述,复制过程可以概括为:主库把数据变更写入二进制日志,从库持续从主库读取这些日志事件,并在本地重新执行。整个链路涉及三类角色和两个关键日志文件。
2.1 整体复制链路
- 主库写 Binlog:主库执行事务提交后,把变更以事件形式顺序写入二进制日志 Binlog;
- Binlog Dump 线程发送日志 :主库为每个已连接的从库启动一个 Binlog Dump 线程,把 Binlog 事件推送给从库;
- 从库 I/O 线程写入 Relay Log :从库的 I/O 线程 接收 Binlog 事件,并写入本地的 中继日志(Relay Log);
- 从库 SQL 线程重放 :从库的 SQL 线程 读取 Relay Log 中的事件并执行,完成数据回放。
MySQL 5.7 之后还引入了基于库的多线程复制(MTS),允许多个 SQL 线程并行回放不同数据库的事件,显著降低复制延迟。
2.2 三种 Binlog 格式
主库记录 Binlog 时可以选择不同格式,这直接影响复制的一致性和日志体积。
| 格式 | 记录内容 | 优点 | 缺点 |
|---|---|---|---|
| STATEMENT | 记录执行的 SQL 语句 | 日志量小、可读性强 | UUID、NOW() 等函数可能导致主从结果不一致 |
| ROW | 记录每一行数据的前后变化 | 一致性强、便于恢复 | 日志量大,尤其是批量更新 |
| MIXED | 默认 STATEMENT,需要时自动切换为 ROW | 兼顾日志量与会一致性 | 依赖 MySQL 的判断策略 |
2.3 半同步复制与 GTID
默认异步复制存在主库已提交、从库尚未收到的窗口,一旦主库宕机可能丢失这部分数据。半同步复制通过插件让主库在提交前等待至少一个从库确认收到日志,典型参数如下:
sql
-- 主库安装并开启半同步插件
INSTALL PLUGIN rpl_semi_sync_master SONAME 'semisync_master.so';
SET GLOBAL rpl_semi_sync_master_enabled = 1;
-- 从库开启半同步插件
INSTALL PLUGIN rpl_semi_sync_slave SONAME 'semisync_slave.so';
SET GLOBAL rpl_semi_sync_slave_enabled = 1;
GTID(Global Transaction Identifier) 则为每个事务分配全局唯一编号,形如 server_uuid:transaction_id。基于 GTID 的复制不依赖文件名和位点,主从切换后从库能自动续传,避免手工指定位置出错。
三、应用场景
3.1 日常开发场景
- 读写分离:查询接口走从库,写入接口走主库,降低主库读压力;
- 慢查询与数据核对:在从库执行复杂统计或对账,减少对主库的影响;
- 本地开发调试:把生产数据复制到从库,供开发环境模拟真实数据。
3.2 企业真实场景
- 高可用架构:配合 MHA、Orchestrator、MGR 等中间件实现故障自动检测和主从切换;
- 在线备份:使用 XtraBackup 等工具在从库做物理备份,主库无需停写;
- 异地容灾:跨机房建立从库,主中心故障后由异地节点接管;
- 数据订阅与同步:通过 Canal、Maxwell 解析从库 Binlog,把数据同步到 ES、Redis 或大数据平台。
四、使用方式
下面以典型的"一主一从 + Java 读写分离"为例,说明从搭建复制到业务侧使用主从同步的完整流程。
4.1 配置主从实例
先在主从两台的 MySQL 配置文件中设置不同的 server-id,并开启 Binlog:
text
# 主库 my.cnf
[mysqld]
server-id = 1
log-bin = mysql-bin
binlog_format = ROW
从库 my.cnf
[mysqld]
server-id = 2
relay-log = relay-bin
在主库创建专用复制账号,并查看当前 Binlog 位点:
sql
-- 主库执行
CREATE USER 'repl'@'%' IDENTIFIED BY 'Repl@123456';
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%';
FLUSH PRIVILEGES;
SHOW MASTER STATUS;
在从库执行 CHANGE MASTER TO 指定主库信息,然后启动复制:
sql
-- 从库执行
CHANGE MASTER TO
MASTER_HOST='192.168.1.10',
MASTER_PORT=3306,
MASTER_USER='repl',
MASTER_PASSWORD='Repl@123456',
MASTER_LOG_FILE='mysql-bin.000001',
MASTER_LOG_POS=154;
START SLAVE;
SHOW SLAVE STATUS\G
4.2 Java 实现读写分离
业务侧可以通过 Spring 的 AbstractRoutingDataSource 根据线程上下文动态选择主库或从库。首先实现路由数据源和上下文持有器:
java
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
// 根据当前线程上下文返回数据源 key
public class ReadWriteRoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSourceType();
}
}
public class DataSourceContextHolder {
private static final ThreadLocal<String> CONTEXT = new ThreadLocal<>();
public static void setDataSourceType(String type) {
CONTEXT.set(type);
}
public static String getDataSourceType() {
return CONTEXT.get();
}
public static void clear() {
CONTEXT.remove();
}
}
接着在配置类中注册主库、从库两个真实数据源,并把它们交给路由数据源管理:
java
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class DataSourceConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.master")
public DataSource masterDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties(prefix = "spring.datasource.slave")
public DataSource slaveDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
public DataSource routingDataSource(DataSource masterDataSource,
DataSource slaveDataSource) {
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("MASTER", masterDataSource);
targetDataSources.put("SLAVE", slaveDataSource);
ReadWriteRoutingDataSource routing = new ReadWriteRoutingDataSource();
routing.setDefaultTargetDataSource(masterDataSource);
routing.setTargetDataSources(targetDataSources);
return routing;
}
}
业务代码在执行写操作前切换到主库,执行读操作前切换到从库,并在 finally 中清理上下文:
java
import org.springframework.stereotype.Service;
@Service
public class OrderService {
public Long createOrder(Order order) {
DataSourceContextHolder.setDataSourceType("MASTER");
try {
// 写操作:INSERT
return orderMapper.insert(order);
} finally {
DataSourceContextHolder.clear();
}
}
public Order getOrder(Long id) {
DataSourceContextHolder.setDataSourceType("SLAVE");
try {
// 读操作:SELECT
return orderMapper.selectById(id);
} finally {
DataSourceContextHolder.clear();
}
}
}
4.3 执行流程与注意事项
- 应用启动时加载主库
masterDataSource和从库slaveDataSource; - 路由数据源把
MASTER、SLAVE两个 key 与真实数据源绑定; - 每次数据库操作前,代码通过
ThreadLocal写入目标数据源 key; - MyBatis 或 JDBC 获取连接时,路由数据源根据 key 返回对应连接;
- 操作结束后清理
ThreadLocal,避免线程复用导致串数据源。
注意事项:
- 读己之写问题:刚写入主库的数据可能还没同步到从库,此时立即查询可能读不到,关键场景应强制走主库;
- 事务切换问题:必须保证同一事务内使用同一数据源,避免事务中途切到从库造成数据错乱;
- 从库故障降级:从库不可用时应自动降级到主库,保证查询可用;
- 连接池隔离:主从数据源使用独立连接池,防止互相影响;
- 延迟监控 :持续观察
Seconds_Behind_Master,对延迟敏感的读请求需有兜底策略。
五、扩展延伸
5.1 技术对比
| 复制方式 | 同步强度 | 数据丢失风险 | 性能影响 |
|---|---|---|---|
| 异步复制 | 主库不等待从库 | 存在丢失窗口 | 小 |
| 半同步复制 | 至少一个从库确认 | 明显降低 | 中等 |
| 全同步(MGR) | 多数派节点确认 | 低 | 较大 |
5.2 优缺点
- 优点:实现简单、部署成熟、扩展性好,通过加从库即可线性提升读能力;
- 缺点:默认异步存在数据延迟和不一致窗口,写能力无法随从库扩容,主库仍是单点写入;
- 与主主复制对比:主从是单向复制,适合读写分离;主主双向复制虽然可互写,但容易产生写冲突,通常需要谨慎处理自增 ID 分配。
5.3 实际开发注意事项
- 大事务拆分:单个大事务会产生大量 Binlog,容易造成从库延迟;
- DDL 操作 :大表 DDL 应使用
gh-ost或pt-online-schema-change,避免阻塞复制; - 幂等性:从库重放依赖事务幂等,业务侧不要依赖绝对实时一致;
- 切换演练:定期演练主从切换,验证数据连续性、GTID 配置和中间件路由是否正常。
六、面试追问
追问 1:主从延迟是如何产生的?怎么解决?
回答思路: 延迟主要来自单线程重放、大事务、从库压力大和网络抖动。标准答案要点: 开启并行复制(MTS)、将 binlog_format 设为 ROW 提升并行度、拆解大事务、从库专机专用、监控 Seconds_Behind_Master,必要时对强一致读强制走主库。
追问 2:主从复制如何保证一致性?半同步是怎么工作的?
回答思路: 先区分最终一致与强一致。标准答案要点: 默认异步是最终一致;半同步复制要求主库提交前等待至少一个从库把日志写入 Relay Log 并确认,按等待时机分为 AFTER_SYNC 和 AFTER_COMMIT,其中 AFTER_SYNC 能进一步降低丢数据风险。
追问 3:GTID 是什么?它比传统位点复制好在哪里?
回答思路: GTID 是给每个事务分配全局唯一标识。标准答案要点: 传统方式基于 MASTER_LOG_FILE 和 MASTER_LOG_POS,切换后容易定位错误;GTID 让从库能自动判断哪些事务已执行、哪些缺失,主从切换和故障恢复更可靠、更简单。
追问 4:读写分离下如何解决"读己之写"问题?
回答思路: 核心是让刚写入的数据可被读到。**标准答案要点:**方案包括:关键操作后强制读主库、记录写后主从延迟阈值内走主库、使用缓存短暂接管读请求、或引入能感知复制进度的中间件自动路由。
追问 5:主库宕机后如何切换?如何避免数据丢失?
回答思路: 从监控、切换、补偿三层回答。**标准答案要点:**先确认从库复制位点达到主库宕机前位置,选择数据最新的从库提升为主库;使用半同步复制和 GTID 尽量减少丢数据;切换后通过 Binlog 补齐、关闭旧主库写入并重定向流量,最后把旧主库重建为从库。