MySQL主从复制的同步与异步之舞 🔄

48 阅读9分钟

一、开篇故事:图书馆的分馆同步 📚

想象你管理一个图书馆系统,总馆和分馆要保持图书同步:

模式1:异步复制(快递邮寄)📦

总馆:"我新进了10本书。"
分馆:"好的,记下了,晚点再上架。"
总馆:"OK,我继续接待读者。"

读者:"我要借刚进的新书。"
总馆:"可以!" ✅
分馆:"还没到呢..." ❌(延迟)

特点:
  - 总馆不等分馆,效率高
  - 但分馆会延迟

模式2:半同步复制(快递确认)📋

总馆:"我新进了10本书。"
分馆1"收到!"
总馆:"好,至少1个分馆收到了,我放心了。"
分馆23"我们还在路上..." 🚚

特点:
  - 至少1个分馆确认
  - 比异步可靠

模式3:同步复制(实时同步)⚡

总馆:"我新进了10本书。"
分馆123"都上架好了!"
总馆:"好,我才能继续。"

读者:"借新书!"
总馆、分馆:"都有!" ✅✅✅

特点:
  - 完全同步,无延迟
  - 但总馆效率低

这就是MySQL主从复制的三种模式!


二、主从复制原理 🎯

2.1 主从架构

        主库(Master)
           ↓ binlog
    ┌──────┴──────┬──────┐
    ↓             ↓      ↓
 从库1(Slave)  从库2   从库3
   ↓
应用读取

2.2 复制流程(三步走)

步骤1:主库记录binlog
  主库执行:INSERT INTO users VALUES (1, '张三');
  写入binlog:position=1234

步骤2:从库IO线程读取binlog
  从库IO线程:"主库,给我最新的binlog!"
  主库Dump线程:"给你!position 1234..."
  从库IO线程:写入relay log(中继日志)

步骤3:从库SQL线程执行relay log
  从库SQL线程:读取relay log
  从库SQL线程:执行SQL
  从库:INSERT INTO users VALUES (1, '张三');
  
完成!主从数据一致!✅

2.3 详细图解

主库(Master)
  ↓ 执行SQL
[binlog]
  ↓ 
Dump线程(发送binlog)
  ↓ 网络传输
从库(Slave)
  ↓
IO线程(接收binlog)
  ↓ 写入
[relay log]
  ↓ 读取
SQL线程(执行SQL)
  ↓
从库数据库

2.4 关键组件

组件位置作用
binlog主库记录所有修改操作
Dump线程主库发送binlog给从库
IO线程从库接收binlog,写入relay log
relay log从库中继日志,存储binlog副本
SQL线程从库读取relay log,执行SQL

三、三种复制模式详解 🔍

3.1 异步复制(Async Replication)

流程:

主库:
  1. 执行SQL
  2. 写binlog
  3. 返回成功 ← 不等从库
  
从库:
  1. IO线程拉取binlog(可能延迟)
  2. SQL线程执行(可能更延迟)

特点:

✅ 性能最高(主库不等待)
❌ 可能丢数据(主库宕机,从库还没同步)
❌ 延迟问题(从库可能落后几秒甚至几分钟)

适用场景:

- 读多写少
- 对数据一致性要求不高
- 追求性能

3.2 半同步复制(Semi-Sync Replication)

流程:

主库:
  1. 执行SQL
  2. 写binlog
  3. 等待至少1个从库确认接收binlog ← 等待!
  4. 返回成功
  
从库:
  1. IO线程接收binlog,写入relay log
  2. 发送ACK给主库 ← 确认
  3. SQL线程异步执行(主库不等)

图解:

主库                     从库1              从库2
 |                        |                  |
 |---(1) 写binlog-------->|                  |
 |                        |                  |
 |<--(2) ACK(收到)------|                  |
 |                        |                  |
 |---(3) 返回成功给客户端                    |
 |                        |                  |
 |----------------------->|----------------->|
                    从库2可能还没收到(但不影响)

配置:

-- 主库开启半同步
INSTALL PLUGIN rpl_semi_sync_master SONAME 'semisync_master.so';
SET GLOBAL rpl_semi_sync_master_enabled = 1;
SET GLOBAL rpl_semi_sync_master_timeout = 1000; -- 1秒超时

-- 从库开启半同步
INSTALL PLUGIN rpl_semi_sync_slave SONAME 'semisync_slave.so';
SET GLOBAL rpl_semi_sync_slave_enabled = 1;

特点:

✅ 比异步可靠(至少1个从库确认)
✅ 性能可接受(只等IO线程,不等SQL线程)
⚠️ 从库宕机,主库会退化为异步
❌ 仍有延迟(SQL线程异步执行)

3.3 同步复制(Sync Replication)

MySQL本身不支持真正的同步复制,但可以通过组复制(Group Replication)实现

流程:

主库:
  1. 执行SQL
  2. 写binlog
  3. 等待所有从库执行完成 ← 等很久!
  4. 返回成功

特点:

✅ 完全一致,无延迟
✅ 数据最安全
❌ 性能最差(等待所有从库)
❌ 不适合生产环境

四、主从延迟问题 ⏰

4.1 什么是主从延迟?

定义: 从库的数据落后主库的时间差。

查看延迟:

-- 在从库执行
SHOW SLAVE STATUS\G

-- 关键字段
Seconds_Behind_Master: 5  ← 延迟5

4.2 延迟的危害

-- 主库
INSERT INTO users VALUES (100, '张三');
COMMIT; -- 返回成功

-- 应用立即从从库查询
SELECT * FROM users WHERE id = 100;
-- 结果:NULL ❌(从库还没同步)

-- 5秒后再查
SELECT * FROM users WHERE id = 100;
-- 结果:张三 ✅(同步完成)

真实案例:

电商下单流程:
1. 用户下单 → 写主库 ✅
2. 立即跳转订单详情页 → 读从库 ❌ 找不到订单
3. 用户:"我的订单呢?!" 😱
4. 5秒后刷新 → 从库同步完成 ✅ 订单出现
5. 用户:"???" 🤔

4.3 延迟的原因

原因1:从库单线程执行(MySQL 5.6之前)

主库:10个线程并发写入 → binlog
从库:1SQL线程串行执行 → 慢!

解决: 并行复制(MySQL 5.7+)

原因2:大事务

-- 主库执行大事务
BEGIN;
UPDATE users SET status = 1; -- 更新100万行
DELETE FROM logs WHERE id < 1000000; -- 删除100万行
COMMIT; -- 耗时10秒

-- 从库也要执行10秒!
-- 这10秒内,从库延迟至少10秒!

解决: 拆分大事务

原因3:从库负载高

从库同时承担:
  - 同步主库数据
  - 处理大量查询请求
  
→ CPU、IO资源不足
→ 同步变慢
→ 延迟增大

解决: 增加从库,分散读压力

原因4:网络延迟

主库(北京) → binlog → 从库(上海)
网络延迟:50ms

每条SQL都要传输 → 累积延迟

解决:

  • 主从部署在同一机房
  • 使用专线

原因5:binlog格式(ROW vs STATEMENT)

-- STATEMENT格式
UPDATE users SET score = score + 1 WHERE city = '北京';
-- binlog记录:UPDATE语句
-- 从库执行:同样的UPDATE(如果数据量大,很慢)

-- ROW格式
UPDATE users SET score = score + 1 WHERE city = '北京';
-- binlog记录:每一行的变化(1000行就记录1000条)
-- binlog变大,传输慢 → 延迟

五、解决主从延迟的方案 💡

方案1:并行复制(MySQL 5.7+)

原理: 从库多线程并行执行binlog

-- 配置并行复制
SET GLOBAL slave_parallel_type = 'LOGICAL_CLOCK';
SET GLOBAL slave_parallel_workers = 8; -- 8个线程

效果:

单线程:执行时间10秒
8线程:执行时间1.5秒(提升6-7倍)✅

方案2:避免大事务

-- ❌ 不好:大事务
BEGIN;
UPDATE users SET status = 1; -- 100万行
COMMIT;

-- ✅ 好:拆分小事务
FOR i IN 1..100 LOOP
    BEGIN;
    UPDATE users SET status = 1 LIMIT 10000;
    COMMIT;
END LOOP;

方案3:读写分离策略优化

// ❌ 不好:刚写完就读从库
userService.createUser(user); // 写主库
User savedUser = userService.getUser(user.getId()); // 读从库 → 可能读不到

// ✅ 好:写完读主库
@Transactional
public void createAndGet(User user) {
    userService.createUser(user); // 写主库
    User savedUser = userService.getUserFromMaster(user.getId()); // 读主库
}

// ✅ 好:延迟读取
userService.createUser(user); // 写主库
Thread.sleep(100); // 等待100ms
User savedUser = userService.getUser(user.getId()); // 读从库

// ✅ 最好:强制主库读
public User getUser(Long id, boolean forceMaster) {
    if (forceMaster) {
        return masterMapper.selectById(id); // 读主库
    }
    return slaveMapper.selectById(id); // 读从库
}

方案4:监控延迟,动态路由

@Service
public class DataSourceRouter {
    
    public User getUser(Long id) {
        // 检查从库延迟
        int delay = slaveService.getDelaySeconds();
        
        if (delay > 5) {
            // 延迟超过5秒,读主库
            return masterMapper.selectById(id);
        }
        
        // 延迟小,读从库
        return slaveMapper.selectById(id);
    }
}

方案5:使用缓存

@Service
public class UserService {
    
    @Cacheable(value = "user", key = "#id")
    public User getUser(Long id) {
        // 先查缓存,缓存没有再查数据库
        return userMapper.selectById(id);
    }
    
    @CacheEvict(value = "user", key = "#user.id")
    public void updateUser(User user) {
        userMapper.updateById(user);
        // 更新后清除缓存
    }
}

六、主从复制配置实战 🛠️

6.1 主库配置

# my.cnf
[mysqld]
# 开启binlog
log-bin=mysql-bin
server-id=1  # 主库ID,唯一

# binlog格式
binlog-format=ROW  # 推荐ROW

# binlog过期时间(天)
expire_logs_days=7

# 半同步配置(可选)
rpl_semi_sync_master_enabled=1
rpl_semi_sync_master_timeout=1000  # 1秒超时

创建复制用户:

-- 主库创建复制账号
CREATE USER 'repl'@'%' IDENTIFIED BY 'password';
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%';
FLUSH PRIVILEGES;

-- 查看主库状态
SHOW MASTER STATUS;
-- 记录File和Position

6.2 从库配置

# my.cnf
[mysqld]
server-id=2  # 从库ID,唯一,不能与主库相同

# 半同步配置(可选)
rpl_semi_sync_slave_enabled=1

# 并行复制(MySQL 5.7+)
slave_parallel_type=LOGICAL_CLOCK
slave_parallel_workers=8

# relay log
relay-log=relay-bin

配置主从关系:

-- 从库配置
CHANGE MASTER TO
  MASTER_HOST='192.168.1.100',
  MASTER_USER='repl',
  MASTER_PASSWORD='password',
  MASTER_LOG_FILE='mysql-bin.000001',  -- 主库的File
  MASTER_LOG_POS=154;  -- 主库的Position

-- 启动从库复制
START SLAVE;

-- 查看从库状态
SHOW SLAVE STATUS\G

-- 关键字段:
-- Slave_IO_Running: Yes  ← IO线程运行中
-- Slave_SQL_Running: Yes ← SQL线程运行中
-- Seconds_Behind_Master: 0 ← 无延迟

七、主从切换(故障转移)🔄

7.1 主库宕机场景

主库💀 → 从库1升级为主库

步骤:
1. 停止所有从库复制
2. 选择一个从库升级为主库
3. 其他从库指向新主库
4. 应用切换到新主库

7.2 手动切换

-- 1. 从库1:停止复制
STOP SLAVE;

-- 2. 从库1:升级为主库
RESET MASTER;

-- 3. 从库2、3:指向新主库(从库1)
STOP SLAVE;
CHANGE MASTER TO
  MASTER_HOST='新主库IP',
  MASTER_USER='repl',
  MASTER_PASSWORD='password',
  MASTER_LOG_FILE='mysql-bin.000001',
  MASTER_LOG_POS=154;
START SLAVE;

-- 4. 应用切换数据源

7.3 自动切换(MHA、Orchestrator)

MHA(Master High Availability):

功能:
1. 监控主库健康状态
2. 自动故障检测
3. 自动选举新主库
4. 自动切换
5. 数据补偿(确保数据不丢失)

八、面试高频问题 🎤

Q1: MySQL主从复制的原理是什么?

答: 三步走:

  1. 主库记录binlog:所有修改操作记录到binlog
  2. 从库IO线程拉取binlog:写入relay log
  3. 从库SQL线程执行relay log:重放SQL,实现同步

Q2: 异步、半同步、同步复制的区别?

答:

  • 异步:主库不等从库,性能高但可能丢数据
  • 半同步:主库等至少1个从库确认接收binlog(不等执行),平衡性能和可靠性
  • 同步:主库等所有从库执行完成,最可靠但性能差

Q3: 主从延迟的原因和解决方案?

答: 原因:

  1. 从库单线程执行(老版本)
  2. 大事务
  3. 从库负载高
  4. 网络延迟

解决:

  1. 并行复制(MySQL 5.7+)
  2. 拆分大事务
  3. 增加从库,分散压力
  4. 写完读主库或使用缓存

Q4: 如何判断主从是否同步?

答:

SHOW SLAVE STATUS\G

关键字段:
- Slave_IO_Running: Yes(IO线程正常)
- Slave_SQL_Running: Yes(SQL线程正常)
- Seconds_Behind_Master: 0(无延迟)

Q5: 主库宕机如何处理?

答:

  1. 手动切换:选一个从库升级为主库
  2. 自动切换:使用MHA、Orchestrator等工具
  3. 确保数据不丢失:半同步复制 + 数据补偿

九、总结口诀 📝

主从复制三步走,
binlog、relay、SQL。
主库记录binlog,
从库拉取并执行。

异步模式性能高,
半同步更可靠。
同步复制太慢了,
生产环境少用到。

主从延迟是大坑,
原因要分清。
大事务要拆分,
并行复制来帮忙。

写完立即读主库,
或者用缓存挡。
监控延迟很重要,
动态路由更智能!

参考资料 📚


下期预告: 144-MySQL的binlog、redolog、undolog的作用和区别 📝


编写时间:2025年
作者:技术文档小助手 ✍️
版本:v1.0

愿你的主从永远同步! 🔄✨