MySQL知识梳理(5)------主从复制与架构优化
作者:没有四次元口袋的蓝胖
日期:2026-06-23
标签:MySQL, 主从复制, 读写分离, 架构
一、主从复制原理
1.1 三线程复制流程
MySQL 主从复制依靠 三个核心线程 完成数据同步:
┌─────────────┐ Binlog ┌─────────────┐ Relay Log ┌─────────────┐
│ Master │ ──────────────► │ Slave │ ──────────────► │ Slave │
│ │ │ │ │ │
│ Binlog Dump │ │ I/O Thread │ │ SQL Thread │
│ Thread │ │ │ │ │
└─────────────┘ └─────────────┘ └─────────────┘
详细流程:
- Master 执行写操作 → 数据写入 Binlog(二进制日志)
- Binlog Dump 线程 → 监测 Binlog 变化,将新事件发送给 Slave
- Slave I/O Thread → 接收 Binlog 事件,写入 Relay Log(中继日志)
- Slave SQL Thread → 读取 Relay Log,执行 SQL 语句
- 数据同步完成 → Slave 与 Master 数据保持一致
⚡ 面试坑点 :很多同学以为从库只有一个线程,实际上是 I/O Thread + SQL Thread 配合工作,I/O 负责拉取日志,SQL 负责回放日志,两者缺一不可。
1.2 三种复制方式对比
| 复制方式 | 工作原理 | 数据一致性 | 性能 | 适用场景 |
|---|---|---|---|---|
| 异步复制 | Master 执行后不等待 Slave 确认 | ❌ 可能丢失 | ⭐⭐⭐⭐⭐ 最快 | 追求性能、可容忍少量数据不一致 |
| 半同步复制 | 等待至少一个 Slave 写入 Relay Log | ⚠️ 部分保证 | ⭐⭐⭐⭐ | 对数据有要求但不严苛的业务 |
| 全同步复制 | 等待所有 Slave 执行完成 | ✅ 完全一致 | ⭐⭐ 慢 | 金融、支付等强一致性场景 |
⚡ 面试高频追问:
- Q:半同步复制一定安全吗?
A:不一定。如果 Slave 在等待期间宕机,会退化为异步复制,存在数据丢失风险。 - Q:MySQL 默认的复制方式是什么?
A:MySQL 5.7 开始支持半同步,但默认仍是异步复制。
二、主从复制配置
2.1 Master 核心配置
ini
[mysqld]
server-id = 1 # 必选,唯一标识主从集群中的每台机器
log_bin = mysql-bin # 开启 Binlog,指定文件名
binlog_format = ROW # 推荐使用 ROW 格式,保证数据一致
sync_binlog = 1 # 每事务同步刷新到磁盘,保证 Binlog 不丢失
binlog_expire_logs_seconds = 604800 # Binlog 保留 7 天
2.2 Slave 核心配置
ini
[mysqld]
server-id = 2 # 必须与 Master 不同
relay_log = mysql-relay-bin # 中继日志文件名
read_only = ON # 从库只读(注意:root 用户仍可写)
super_read_only = ON # 推荐,彻底禁止写入
relay_log_purge = ON # 自动清理已应用的中继日志
2.3 创建复制账号
sql
-- 在 Master 上执行
CREATE USER 'repl'@'%' IDENTIFIED BY 'repl_password';
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%';
-- 查看 Master 状态(配置从库时需要)
SHOW MASTER STATUS;
-- +------------------+----------+--------------+------------------+
-- | File | Position | Binlog_Do_DB | Binlog_Ignore_DB |
-- +------------------+----------+--------------+------------------+
-- | mysql-bin.000001 | 154 | | |
-- +------------------+----------+--------------+------------------+
2.4 启动复制并查看状态
sql
-- 在 Slave 上执行
CHANGE MASTER TO
MASTER_HOST = 'master_ip',
MASTER_USER = 'repl',
MASTER_PASSWORD = 'repl_password',
MASTER_LOG_FILE = 'mysql-bin.000001',
MASTER_LOG_POS = 154;
START SLAVE;
-- 查看复制状态
SHOW SLAVE STATUS\G
-- 重点关注:Slave_IO_Running、Slave_SQL_Running、Seconds_Behind_Master
⚡ 面试坑点 :Seconds_Behind_Master 为 0 不代表没有延迟!这个值只在 SQL Thread 运行时有效,如果从库压力大或网络断开,它会显示 NULL 或不准确。
三、读写分离
3.1 什么是读写分离
将数据库的 读操作 和 写操作 分离到不同的服务器上执行:
- 写请求 → Master(唯一)
- 读请求 → 多个 Slave(可扩展)
核心价值:提升数据库并发能力,解决 "读多写少" 场景下的性能瓶颈。
3.2 三种实现方式
| 实现方式 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 应用层实现 | 在代码中手动判断读写操作 | 无额外组件,简单可控 | 侵入代码,扩展困难 |
| 框架层实现 | 如 MyBatis-Plus、ShardingSphere-JDBC | 配置化,对代码透明 | 增加依赖 |
| 代理层实现 | 如 MyCAT、ShardingSphere-Proxy、ProxySQL | 统一入口,运维友好 | 单点故障,需高可用 |
应用层示例(Spring Boot):
java
// 配置多数据源
@Bean
public DataSource masterDataSource() {
return DataSourceBuilder.create()
.url("jdbc:mysql://master:3306/db")
.build();
}
@Bean
public DataSource slaveDataSource() {
return DataSourceBuilder.create()
.url("jdbc:mysql://slave:3306/db")
.build();
}
// 使用 @ReadOnly 注解自动路由到从库
@ReadOnly(true)
public User findById(Long id) {
return userMapper.selectById(id);
}
3.3 核心注意事项
⚡ 面试高频问题:读写分离后可能遇到什么问题?如何解决?
| 问题 | 解决方案 |
|---|---|
| 主从延迟 | 强制读主库、延迟查询、读从库失败后重试主库 |
| 数据不一致 | 引入中间件路由、消息队列保证最终一致性 |
| 热点数据 | 读请求加缓存兜底 |
| 主库单点 | 主库也需要高可用(Keepalived + VIP 或 MHA/Orchestrator) |
四、主从延迟与数据一致性
4.1 五大延迟原因
┌─────────────────────────────────────────────────────────────────┐
│ 主从延迟产生原因 │
├──────────┬──────────────────────────────────────────────────────┤
│ 网络延迟 │ Binlog 传输过程中网络抖动或带宽不足 │
├──────────┼──────────────────────────────────────────────────────┤
│ 从库压力 │ 从库同时处理查询请求,CPU/IO 资源被占满 │
├──────────┼──────────────────────────────────────────────────────┤
│ 大事务 │ 主库大事务执行快,但 Binlog 传输和回放耗时长 │
├──────────┼──────────────────────────────────────────────────────┤
│ Binlog 格式 │ STATEMENT 格式比 ROW 格式日志量小,但回放可能更慢 │
├──────────┼──────────────────────────────────────────────────────┤
│ 单线程回放│ 从库 SQL Thread 单线程回放,无法利用多核 CPU │
└──────────┴──────────────────────────────────────────────────────┘
⚡ 面试坑点:大事务是生产环境最常见的延迟原因!比如 DELETE 全表、批量更新千万级数据。建议拆分为小事务分批执行。
4.2 五条解决方案
| 方案 | 说明 | 适用场景 |
|---|---|---|
| 并行复制 | 从库开启多线程回放(MySQL 5.6+) | 减少回放延迟 |
| 强制读主库 | 关键读操作走主库 | 数据一致性要求高 |
| 延迟复制 | 从库延迟 n 秒后再回放 | 误操作恢复 |
| 缓存标记法 | 写操作后缓存标记,延迟期间走主库 | 短时延迟可接受 |
| 业务优化 | 拆分大事务、使用批量操作 | 根本性解决 |
4.3 并行复制配置
sql
-- MySQL 5.7+ 推荐配置
STOP SLAVE;
-- 设置并行复制方式(LOGICAL_CLOCK 对事务型复制更友好)
SET GLOBAL slave_parallel_type = 'LOGICAL_CLOCK';
-- 设置并行 Worker 线程数(建议与 CPU 核数一致)
SET GLOBAL slave_parallel_workers = 8;
-- 开启中继日志并行写入(减少锁竞争)
SET GLOBAL relay_log_info_repository = 'TABLE';
SET GLOBAL relay_log_recovery = ON;
START SLAVE;
-- 查看并行复制状态
SHOW STATUS LIKE 'Slave_parallel_workers%';
⚡ 面试加分项 :MySQL 8.0 引入了 WRITESET 模式,基于主键哈希实现更细粒度的并行化,进一步提升复制效率。
五、分库分表
5.1 垂直拆分 vs 水平拆分
┌─────────────────────────────────────────────────────────────────┐
│ 分库分表策略 │
├─────────────────────────────┬───────────────────────────────────┤
│ 垂直拆分 │ 水平拆分 │
├─────────────────────────────┼───────────────────────────────────┤
│ 按业务模块/表结构拆分 │ 按数据行拆分 │
│ 表 → 新表 / 库 → 新库 │ 大表 → 多张小表 │
│ 减少单表/单库宽度 │ 减少单表数据量 │
│ 解决单表字段过多 │ 解决单表数据量过亿 │
└─────────────────────────────┴───────────────────────────────────┘
垂直拆分示例:
sql
-- 原始 users 表字段过多,拆分为 users 和 users_profile
CREATE TABLE users (
id BIGINT PRIMARY KEY,
username VARCHAR(50),
password VARCHAR(100),
status TINYINT
);
CREATE TABLE users_profile (
user_id BIGINT PRIMARY KEY, -- 复用主键
avatar VARCHAR(255),
bio TEXT,
phone VARCHAR(11),
address TEXT
);
5.2 四种拆分规则
| 规则 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 哈希取模 | shard_key % N |
数据分布均匀 | 扩容需迁移数据 |
| 按时间 | 按月份/年份分表 | 查询友好(冷热分离) | 热点数据集中 |
| 按地区 | 按省份/城市分库 | 业务隔离性好 | 分布不均 |
| 一致性哈希 | 哈希环 + 虚拟节点 | 扩容迁移少 | 实现复杂 |
哈希取模示例:
sql
-- 假设拆分为 4 张表:orders_0, orders_1, orders_2, orders_3
-- 计算路由:table = orders_' + (order_id % 4)
-- order_id = 1001 → orders_1
-- order_id = 1002 → orders_2
5.3 拆分带来的问题
⚡ 面试必问:分库分表后会遇到哪些新问题?
| 问题 | 说明 | 解决方案 |
|---|---|---|
| 跨表查询 | 分页、JOIN、聚合无法直接实现 | 业务层二次查询、ES 辅助 |
| 全局 ID | 自增主键无法保证唯一 | Snowflake、UUID、分布式 ID 生成器 |
| 事务一致性 | 跨库事务无法用本地事务 | 分布式事务(TCC、Saga)、最终一致性 |
| 扩容迁移 | 扩缩容需迁移数据 | 哈希环扩容、一致性哈希、倍增扩容 |
5.4 中间件选型
| 中间件 | 语言 | 特点 | 适用场景 |
|---|---|---|---|
| ShardingSphere-JDBC | Java | 轻量级、埋入应用 | Java 技术栈 |
| ShardingSphere-Proxy | Java | 透明代理、语言无关 | 多语言混合项目 |
| MyCAT | Java | 老牌成熟 | 传统项目迁移 |
| Vitess | Go | 水平扩展、云原生 | 大规模云数据库 |
| TiDB | Go | NewSQL(自动分片) | 需要强一致性场景 |
六、缓存策略
6.1 四种缓存模式
┌─────────────────────────────────────────────────────────────────┐
│ 缓存读写模式 │
├─────────────────────┬───────────────────────────────────────────┤
│ │ │
│ Cache Aside │ 业务代码同时操作缓存和数据库(最常用) │
│ (旁路缓存) │ │
│ │ │
├─────────────────────┼───────────────────────────────────────────┤
│ │ │
│ Read Through │ 缓存负责加载数据,调用方只操作缓存 │
│ (读穿透) │ │
│ │ │
├─────────────────────┼───────────────────────────────────────────┤
│ │ │
│ Write Through │ 写操作同时更新缓存和数据库 │
│ (写穿透) │ │
│ │ │
├─────────────────────┼───────────────────────────────────────────┤
│ │ │
│ Write Behind │ 异步批量写数据库 │
│ (异步回写) │ │
│ │ │
└─────────────────────┴───────────────────────────────────────────┘
Cache Aside(推荐)代码示例:
java
public User getUser(Long id) {
// 1. 先查缓存
User user = redis.get("user:" + id);
if (user != null) {
return user; // 缓存命中
}
// 2. 缓存未命中,查数据库
user = userMapper.selectById(id);
// 3. 写入缓存(设置过期时间)
if (user != null) {
redis.setex("user:" + id, 3600, user);
}
return user;
}
public void updateUser(User user) {
// 1. 先更新数据库
userMapper.updateById(user);
// 2. 删除缓存(而非更新)
redis.del("user:" + user.getId());
// 原因:更新缓存可能造成脏读,删除更安全
}
6.2 缓存穿透
问题:大量请求查询不存在的 key,绕过缓存直达数据库。
解决方案:
| 方案 | 原理 | 适用场景 |
|---|---|---|
| 布隆过滤器 | 用 Bitmap 判断 key 是否存在 | 数据可枚举 |
| 空值缓存 | 缓存空结果(设置短过期时间) | 非恶意攻击 |
java
// 布隆过滤器示例
private BloomFilter<Long> bloomFilter = BloomFilter.create(
Funnels.longFunnel(), 1000000, 0.01);
public User getUser(Long id) {
if (!bloomFilter.mightContain(id)) {
return null; // 一定不存在
}
// ... 正常查询逻辑
}
6.3 缓存击穿
问题:热点 key 过期瞬间,大量并发请求直达数据库。
解决方案:
| 方案 | 原理 | 优缺点 |
|---|---|---|
| 互斥锁 | 只允许一个请求查库 | 实现简单,牺牲并发 |
| 热点数据永不过期 | 逻辑过期,定期更新 | 数据可能短暂不一致 |
| 双缓存 | 准备两套缓存 | 实现复杂 |
java
// 互斥锁实现
public User getUserWithLock(Long id) {
String key = "user:" + id;
User user = redis.get(key);
if (user == null) {
String lockKey = "lock:" + key;
// 尝试获取锁(SETNX)
if (redis.setnx(lockKey, "1")) {
try {
user = userMapper.selectById(id);
redis.setex(key, 3600, user);
} finally {
redis.del(lockKey);
}
} else {
// 等待后重试
Thread.sleep(50);
return getUserWithLock(id);
}
}
return user;
}
6.4 缓存雪崩
问题:大量 key 同时过期或缓存服务宕机,请求全部打到数据库。
解决方案:
| 方案 | 原理 |
|---|---|
| 过期时间随机化 | TTL = baseTTL + random() |
| 多级缓存 | 本地缓存 + Redis + 数据库 |
| 服务熔断降级 | 缓存不可用时返回默认值 |
| 高可用缓存集群 | Redis Sentinel / Cluster |
七、数据库连接池
7.1 为什么需要连接池
┌─────────────┐ ┌─────────────┐
│ 应用请求 │ │ 数据库 │
│ (瞬时) │ ── 频繁建连 ──► │ (有限) │
└─────────────┘ └─────────────┘
无连接池:每次请求创建连接 → 3次握手 → SQL执行 → 4次挥手 → 连接销毁
有连接池:预先建立连接 → 请求复用 → 归还连接 → 连接复用
核心问题:
- 每次创建 TCP 连接需要 3 次握手
- 频繁创建销毁连接消耗 CPU 和内存
- 数据库最大连接数有限,不能无限创建
7.2 连接池原理
┌────────────────────────────────────────────────────────────┐
│ 连接池工作流程 │
├────────────────────────────────────────────────────────────┤
│ │
│ 初始化 ──► 预先创建 N 个连接 │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ 请求到来 │ │
│ └──────┬───────┘ │
│ ▼ │
│ ┌─ 有空闲连接? ─┐ │
│ │ │ │
│ 是 │ 否 │
│ ▼ ▼ │
│ 取出连接 连接数 < 最大值? │
│ │ 是 │ 否 │
│ ▼ ▼ ▼ │
│ 使用连接 创建新连接 等待/拒绝 │
│ │ │ │
│ ▼ ▼ │
│ 归还连接 返回连接给请求 │
│ │ │
│ ▼ │
│ 检查连接可用性 │
│ │ │
│ ▼ │
│ 存活 → 放回池中 失效 → 销毁重建 │
│ │
└────────────────────────────────────────────────────────────┘
7.3 常见连接池对比
| 连接池 | 开发公司 | 性能 | 特点 | 推荐场景 |
|---|---|---|---|---|
| HikariCP | 商业化/Spring Boot 默认 | ⭐⭐⭐⭐⭐ 最高 | 轻量、代码简洁、FastConnectionPool | 高并发场景(首选) |
| Druid | 阿里巴巴 | ⭐⭐⭐⭐ | 监控功能强、SQL 防火墙、扩展性好 | 需要监控的复杂业务 |
| DBCP | Apache | ⭐⭐⭐ | 老牌稳定 | 兼容性要求高 |
| C3P0 | - | ⭐⭐ | 功能全 | 老项目 |
HikariCP 配置示例:
yaml
spring:
datasource:
hikari:
maximum-pool-size: 20 # 最大连接数
minimum-idle: 5 # 最小空闲连接
connection-timeout: 30000 # 获取连接超时(ms)
idle-timeout: 600000 # 空闲超时(ms)
max-lifetime: 1800000 # 连接最大生命周期(ms)
connection-test-query: SELECT 1 # 连接测试SQL
⚡ 面试高频问题:连接池是不是越大越好?
答案:不是!连接数过多会导致:
- 数据库端连接数暴涨,性能下降
- 上下文切换开销增加
- 内存消耗增大
推荐配置 :连接池大小 = CPU 核数 × 2 + 磁盘数
八、思维导图速览
MySQL核心知识------主从复制与架构优化
│
├── 一、主从复制原理
│ ├── 1.1 三线程复制流程
│ │ ├── Master: Binlog Dump Thread
│ │ ├── Slave: I/O Thread → Relay Log
│ │ └── Slave: SQL Thread → 执行SQL
│ └── 1.2 三种复制方式
│ ├── 异步复制(性能高,可能丢数据)
│ ├── 半同步复制(至少一个从库确认)
│ └── 全同步复制(所有从库确认,性能低)
│
├── 二、主从复制配置
│ ├── Master 配置
│ │ ├── server-id(唯一标识)
│ │ ├── log_bin(开启Binlog)
│ │ └── binlog_format(推荐ROW)
│ ├── Slave 配置
│ │ ├── server-id(与Master不同)
│ │ ├── relay_log(中继日志)
│ │ └── read_only(从库只读)
│ └── 复制账号与状态查看
│
├── 三、读写分离
│ ├── 写 → Master / 读 → Slave
│ ├── 实现方式
│ │ ├── 应用层实现(代码判断)
│ │ ├── 框架层(ShardingSphere-JDBC)
│ │ └── 代理层(ShardingSphere-Proxy)
│ └── 注意事项:主从延迟、数据不一致
│
├── 四、主从延迟与数据一致性
│ ├── 延迟原因:网络、从库压力、大事务、单线程回放
│ ├── 解决方案:并行复制、强制读主库、延迟复制
│ └── 并行复制配置:slave_parallel_workers
│
├── 五、分库分表
│ ├── 垂直拆分(按业务/字段)
│ ├── 水平拆分(按数据行)
│ ├── 拆分规则:哈希取模、按时间、按地区、一致性哈希
│ ├── 带来的问题:跨表查询、全局ID、分布式事务、扩容迁移
│ └── 中间件:ShardingSphere、MyCAT、Vitess、TiDB
│
├── 六、缓存策略
│ ├── 四种模式:Cache Aside、Read/Write Through、Write Behind
│ ├── 缓存穿透:布隆过滤器、空值缓存
│ ├── 缓存击穿:互斥锁、热点永不过期
│ └── 缓存雪崩:过期随机化、多级缓存、熔断降级
│
└── 七、数据库连接池
├── 为什么需要:减少连接创建开销
├── 工作原理:预创建、复用、归还
└── 常见选型:HikariCP(性能最高)、Druid(监控强)
九、写在最后------学习建议
-
原理比配置更重要
面试中 "三线程如何配合"、"Binlog 和 Relay Log 的关系" 这类原理问题远比配置参数问得多。理解底层机制,才能举一反三。
-
动手实践出真知
搭建一套主从集群,实际操作
SHOW SLAVE STATUS、模拟主从延迟、测试读写分离。纸上得来终觉浅,绝知此事要躬行。 -
关联知识点串联记忆
主从复制涉及到 Binlog、事务、锁机制等知识,形成知识网络。比如思考:Binlog 的三种格式在主从复制中表现有何不同?
-
关注生产环境问题
理解 "大事务导致从库延迟"、"从库与主库数据不一致" 这些真实问题的根因和解决方案,这正是面试官考察你实战能力的关键。
-
新技术保持关注
MySQL 8.0 的增强半同步、TiDB/ CockroachDB 等 NewSQL 数据库、FTA(Fast Target Assignment)等新技术,了解它们解决了什么问题,与 MySQL 有何不同。