MySQL知识梳理(5)

MySQL知识梳理(5)------主从复制与架构优化

作者:没有四次元口袋的蓝胖

日期:2026-06-23

标签:MySQL, 主从复制, 读写分离, 架构

一、主从复制原理

1.1 三线程复制流程

MySQL 主从复制依靠 三个核心线程 完成数据同步:

复制代码
┌─────────────┐     Binlog      ┌─────────────┐    Relay Log    ┌─────────────┐
│   Master    │ ──────────────► │    Slave    │ ──────────────► │   Slave     │
│             │                 │             │                 │             │
│ Binlog Dump │                 │  I/O Thread │                 │  SQL Thread │
│   Thread    │                 │             │                 │             │
└─────────────┘                 └─────────────┘                 └─────────────┘

详细流程:

  1. Master 执行写操作 → 数据写入 Binlog(二进制日志)
  2. Binlog Dump 线程 → 监测 Binlog 变化,将新事件发送给 Slave
  3. Slave I/O Thread → 接收 Binlog 事件,写入 Relay Log(中继日志)
  4. Slave SQL Thread → 读取 Relay Log,执行 SQL 语句
  5. 数据同步完成 → 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

面试高频问题:连接池是不是越大越好?

答案:不是!连接数过多会导致:

  1. 数据库端连接数暴涨,性能下降
  2. 上下文切换开销增加
  3. 内存消耗增大

推荐配置 :连接池大小 = 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(监控强)

九、写在最后------学习建议

  1. 原理比配置更重要

    面试中 "三线程如何配合"、"Binlog 和 Relay Log 的关系" 这类原理问题远比配置参数问得多。理解底层机制,才能举一反三。

  2. 动手实践出真知

    搭建一套主从集群,实际操作 SHOW SLAVE STATUS、模拟主从延迟、测试读写分离。纸上得来终觉浅,绝知此事要躬行。

  3. 关联知识点串联记忆

    主从复制涉及到 Binlog、事务、锁机制等知识,形成知识网络。比如思考:Binlog 的三种格式在主从复制中表现有何不同?

  4. 关注生产环境问题

    理解 "大事务导致从库延迟"、"从库与主库数据不一致" 这些真实问题的根因和解决方案,这正是面试官考察你实战能力的关键。

  5. 新技术保持关注

    MySQL 8.0 的增强半同步、TiDB/ CockroachDB 等 NewSQL 数据库、FTA(Fast Target Assignment)等新技术,了解它们解决了什么问题,与 MySQL 有何不同。