主从复制与读写分离:数据一致性怎么保证?

摘要:从一次"用户刚下单就查不到订单"的线上故障出发,深度剖析MySQL主从复制原理与读写分离架构。通过图解binlog的三种格式、三个线程的协作过程,揭秘异步复制、半同步复制、组提交的区别。详解主从延迟的5种根因和6种解决方案,配合ShardingSphere、MyCat等中间件的实战配置,以及强制主库、延迟补偿、二次读取等数据一致性保障手段,让你彻底搞懂"为什么读写分离会数据不一致"、"如何在性能和一致性之间权衡"等核心问题。


💥 翻车现场

周五晚上8点,哈吉米正在吃晚饭,手机突然疯狂震动。

客服主管:@哈吉米 紧急!大量用户反馈下单后查不到订单!
哈吉米:???怎么可能,我看看日志!

用户A(user_id=10086):
20:05:32 - 下单成功(order_id=100001)
20:05:33 - 查询订单列表 → 空!
20:05:35 - 再次查询 → 看到了!

用户B(user_id=10087):
20:06:15 - 下单成功(order_id=100002)
20:06:16 - 查询订单详情 → 订单不存在!
20:06:18 - 刷新页面 → 出现了!

哈吉米:"卧槽,下单后立马查不到,过几秒就能查到了?"

紧急回滚后,哈吉米查看架构图:

          【应用服务器】
               ↓
        写操作 ↓ 读操作
               ↓
    MySQL主库  →  MySQL从库1
      (写)    同步   (读)
               ↘
                 MySQL从库2
                    (读)

哈吉米:"我上周刚上的读写分离,难道是主从延迟?"

第二天早上,南北绿豆和阿西噶阿西来了。

南北绿豆:"典型的主从延迟问题!你写入主库,立马读从库,但主从还没同步完成。"
阿西噶阿西:"而且我猜你用的是异步复制,根本不管从库有没有同步成功。"
哈吉米:"那怎么办?改成同步复制?"
南北绿豆:"不是这么简单!来,我给你讲讲主从复制的原理。"


🤔 主从复制的原理:binlog的奇妙旅程

南北绿豆在白板上画了一个流程图。

主从复制的完整流程

                  【主库 Master】
                        ↓
              1. 执行SQLUPDATEINSERT)
                        ↓
              2. 写入binlog(二进制日志)
                        ↓
              3. 提交事务
                        ↓
         ┌──────────────┴──────────────┐
         ↓                              ↓
  【Dump线程】                    【主库业务】
    读取binlog                    继续处理请求
         ↓
    通过网络发送
         ↓
  【从库 Slave】
         ↓
  【IO线程】接收binlog
         ↓
  写入relay log(中继日志)
         ↓
  【SQL线程】读取relay log
         ↓
  执行SQL,写入从库
         ↓
  从库数据更新完成

三个关键线程

线程所在位置作用
Dump线程主库读取binlog,发送给从库
IO线程从库接收binlog,写入relay log
SQL线程从库读取relay log,执行SQL

哈吉米:"所以主从复制是异步的?主库不等从库同步完就返回了?"

阿西噶阿西:"对!这就是异步复制的特点,性能好但可能丢数据。"


详细时序图

时间轴:
T1: 主库执行 INSERT INTO order (order_id, user_id) VALUES (100001, 10086);
T2: 主库写入binlog
T3: 主库提交事务 → 返回"插入成功"给应用
    ↓
    此时从库还没同步!
    ↓
T4: Dump线程读取binlog(主库)
T5: 通过网络发送binlog(可能有延迟)
T6: IO线程接收binlog(从库)
T7: IO线程写入relay log(从库)
T8: SQL线程读取relay log(从库)
T9: SQL线程执行INSERT(从库)
T10: 从库数据更新完成

应用查询时间轴:
T3.5: 应用查询从库(SELECT * FROM order WHERE order_id = 100001)
      → 查不到!(因为从库还在T6-T9的同步过程中)

南北绿豆:"看到了吗?T3主库就返回成功了,但T10从库才真正写入,中间有7个步骤的延迟!"


主从延迟的来源

阿西噶阿西:"主从延迟来自这几个环节:"

环节延迟来源典型耗时
网络传输主库 → 从库的网络延迟1-10ms(局域网)
50-200ms(跨地域)
IO线程写入relay log写入磁盘1-5ms
SQL线程执行从库执行SQL取决于SQL复杂度
锁等待从库有其他查询占用锁0-1000ms+
大事务主库一个大事务,从库要完整执行可能几秒甚至几分钟

总延迟 = 网络 + 写入 + 执行 + 锁等待 + 大事务

哈吉米:"所以主从延迟不可避免?"

南北绿豆:"对!但可以优化到很小(10ms以内)。"


📊 binlog的三种格式:ROW vs STATEMENT vs MIXED

阿西噶阿西:"主从复制靠binlog,但binlog有三种格式,选错了会出大问题!"

格式1️⃣:STATEMENT(语句复制)

定义:记录原始SQL语句。

示例

-- 主库执行
UPDATE account SET balance = balance + 100 WHERE user_id = 10086;

-- binlog记录(STATEMENT格式)
UPDATE account SET balance = balance + 100 WHERE user_id = 10086;

-- 从库执行
UPDATE account SET balance = balance + 100 WHERE user_id = 10086;

优点

  • ✅ binlog体积小(只记录SQL)
  • ✅ 网络传输快

缺点

  • ❌ 某些SQL可能导致主从数据不一致

危险案例

-- 主库执行(假设当前时间是 2024-10-07 20:05:32)
INSERT INTO log (user_id, create_time) 
VALUES (10086, NOW());

-- binlog记录
INSERT INTO log (user_id, create_time) 
VALUES (10086, NOW());  -- NOW()是函数,不是具体值

-- 从库执行(假设执行时间是 2024-10-07 20:05:35,延迟了3秒)
INSERT INTO log (user_id, create_time) 
VALUES (10086, NOW());  -- NOW()在从库上是20:05:35!

结果:
主库:create_time = 2024-10-07 20:05:32
从库:create_time = 2024-10-07 20:05:35  ← 数据不一致!

其他危险函数

函数问题
NOW()主从执行时间不同
UUID()每次生成的UUID不同
RAND()随机数不同
USER()主从用户可能不同
@@auto_increment_increment自增步长可能不同

哈吉米:"卧槽,这不是坑吗?"

南北绿豆:"所以现在基本不用STATEMENT格式了。"


格式2️⃣:ROW(行复制,推荐)

定义:记录每一行数据的实际变化。

示例

-- 主库执行
UPDATE account SET balance = balance + 100 WHERE user_id = 10086;
-- 假设user_id=10086的balance原本是500

-- binlog记录(ROW格式,简化表示)
### UPDATE `account`
### WHERE
###   user_id = 10086
###   balance = 500      -- 记录修改前的值(用于回滚)
### SET
###   balance = 600      -- 记录修改后的值(具体数字)

优点

  • ✅ 数据一致性强(记录的是实际值,不是函数)
  • ✅ 可以精确恢复数据(闪回)
  • ✅ 不受函数影响(NOW()、UUID()等)

缺点

  • ❌ binlog体积大(每行都记录)
  • ❌ 批量操作binlog巨大

批量操作的坑

-- 主库执行
UPDATE account SET balance = balance + 100;  -- 更新了100万行

-- STATEMENT格式的binlog
UPDATE account SET balance = balance + 100;  -- 只有1条SQL,很小

-- ROW格式的binlog
### UPDATE `account` WHERE user_id=1 SET balance=600
### UPDATE `account` WHERE user_id=2 SET balance=750
### UPDATE `account` WHERE user_id=3 SET balance=820
### ... (100万条记录)

结果:ROW格式的binlog可能有几百MB!

阿西噶阿西:"所以ROW格式虽然安全,但批量操作时binlog会很大,传输慢,从库同步慢。"


格式3️⃣:MIXED(混合模式,智能选择)

定义:MySQL自动选择STATEMENT或ROW。

规则

  • 普通SQL → 用STATEMENT(体积小)
  • 有不确定函数的SQL → 用ROW(保证一致性)

示例

-- 这条SQL用STATEMENT
UPDATE account SET balance = 600 WHERE user_id = 10086;

-- 这条SQL用ROW(因为有NOW())
INSERT INTO log (user_id, create_time) VALUES (10086, NOW());

优点

  • ✅ 兼顾性能和一致性
  • ✅ 自动选择

缺点

  • ⚠️ 有时候MySQL判断不准确

三种格式对比

格式优点缺点适用场景
STATEMENTbinlog小,传输快某些SQL主从不一致❌ 不推荐
ROW一致性强,可精确恢复binlog大,批量操作慢推荐(默认)
MIXED自动平衡判断可能不准⚠️ 可选

查看和设置binlog格式

-- 查看当前binlog格式
SHOW VARIABLES LIKE 'binlog_format';

-- 设置binlog格式(全局)
SET GLOBAL binlog_format = 'ROW';

-- 设置binlog格式(当前会话)
SET SESSION binlog_format = 'ROW';

南北绿豆:"现在MySQL 8.0默认就是ROW格式,保证数据一致性。"


🔧 复制模式对比:异步 vs 半同步 vs 组提交

哈吉米:"既然异步复制有延迟,能不能改成同步复制?"

阿西噶阿西:"有三种模式,各有优缺点。"


模式1️⃣:异步复制(Asynchronous Replication,默认)

流程

主库执行SQL
  ↓
写入binlog
  ↓
提交事务 → 立即返回"成功"
  ↓
后台异步发送binlog给从库

时序图

时间轴:
T1: 主库写入binlog
T2: 主库提交事务 → 返回"成功"(此时从库还没收到)
T3: Dump线程发送binlog
T4: 从库接收并执行

问题:T2和T4之间有延迟,如果T2.5查询从库,查不到数据

优点

  • ✅ 性能最好(主库不等从库)
  • ✅ 主库不受从库影响(从库挂了,主库正常)

缺点

  • ❌ 主从延迟
  • ❌ 主库宕机可能丢数据(binlog还没发送给从库)

适用场景:对一致性要求不高,追求性能


模式2️⃣:半同步复制(Semi-Synchronous Replication)

流程

主库执行SQL
  ↓
写入binlog
  ↓
发送binlog给从库
  ↓
等待至少1个从库确认接收 ← 关键!
  ↓
提交事务 → 返回"成功"

时序图

时间轴:
T1: 主库写入binlog
T2: 发送binlog给从库
T3: 从库IO线程接收binlog,写入relay log
T4: 从库发送ACK确认 → 主库收到
T5: 主库提交事务 → 返回"成功"(此时从库已经接收)
T6: 从库SQL线程执行SQL

注意:T5时从库虽然接收了binlog,但还没执行(T6才执行)
所以仍有小延迟!

优点

  • ✅ 数据更安全(至少1个从库收到binlog)
  • ✅ 主库宕机不丢数据(从库有完整binlog)

缺点

  • ❌ 性能下降(主库要等从库确认)
  • ❌ 仍有延迟(从库接收≠从库执行)

配置方法

-- 主库安装半同步插件
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;

-- 设置超时时间(10秒内从库没确认,降级为异步)
SET GLOBAL rpl_semi_sync_master_timeout = 10000;

-- 从库开启半同步
SET GLOBAL rpl_semi_sync_slave_enabled = 1;

超时降级

如果10秒内从库没确认(网络故障、从库宕机):
主库自动降级为异步复制,保证主库可用

哈吉米:"所以半同步也不能保证实时一致?"

南北绿豆:"对!因为从库接收binlog ≠ 从库执行完SQL,中间还有SQL线程执行的时间。"


模式3️⃣:组提交(Group Commit)

定义:多个事务的binlog一起发送,减少网络开销。

流程

事务1提交 → 写入binlog
事务2提交 → 写入binlog
事务3提交 → 写入binlog
  ↓
攒够一批(或达到时间阈值)
  ↓
一次性发送给从库

优点

  • ✅ 减少网络开销(批量发送)
  • ✅ 提升主从复制性能

配置方法

-- 开启binlog组提交
SET GLOBAL binlog_group_commit_sync_delay = 1000;   -- 延迟1ms攒一批
SET GLOBAL binlog_group_commit_sync_no_delay_count = 100;  -- 攒够100个事务就发送

三种模式对比

模式性能一致性数据安全适用场景
异步复制⭐⭐⭐⭐⭐⭐⭐⭐⭐非核心业务,追求性能
半同步复制⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐金融、支付等核心业务
组提交⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐配合半同步使用

阿西噶阿西:"实际项目中,推荐半同步+组提交,兼顾性能和一致性。"


🚨 主从延迟的5种原因

南北绿豆:"主从延迟不只是网络问题,还有很多隐藏原因。"

原因1️⃣:从库配置低

主库:8核16G SSD
从库:2核4G HDD  ← 配置太差

结果:主库1秒写入1万条,从库1秒只能执行5000条,越积越多

解决方案:从库配置不能比主库差太多,至少要达到主库的70%


原因2️⃣:主库写入过快

主库:秒杀活动,1秒写入10万条
从库:SQL线程是单线程,1秒只能执行5万条

结果:延迟越来越大

查看延迟

-- 从库执行
SHOW SLAVE STATUS\G

Seconds_Behind_Master: 15  ← 延迟15秒!

解决方案:开启并行复制(MySQL 5.7+)

-- 从库开启并行复制(多个SQL线程)
SET GLOBAL slave_parallel_type = 'LOGICAL_CLOCK';
SET GLOBAL slave_parallel_workers = 4;  -- 4个SQL线程

原因3️⃣:大事务阻塞

主库执行大事务:
START TRANSACTION;
UPDATE order SET status = 1;  -- 更新100万行,执行了30秒
COMMIT;

从库:
必须等这个大事务完整执行完(30秒),其他事务都要等待

解决方案

  • 避免大事务(拆成小批次)
  • 业务高峰期不执行批量操作
-- ❌ 错误写法
UPDATE order SET status = 1 WHERE create_time < '2024-01-01';  -- 100万行

-- ✅ 正确写法(分批执行)
UPDATE order SET status = 1 
WHERE create_time < '2024-01-01' 
LIMIT 1000;  -- 每次1000行,循环执行

原因4️⃣:锁等待

从库的SQL线程执行:
UPDATE account SET balance = 600 WHERE user_id = 10086;

但从库上有个慢查询正在执行:
SELECT * FROM account WHERE user_id = 10086 FOR UPDATE;  -- 加了锁,还没释放

结果:SQL线程被阻塞,等待锁释放

排查方法

-- 从库执行,查看锁等待
SELECT * FROM information_schema.INNODB_LOCKS;
SELECT * FROM information_schema.INNODB_LOCK_WAITS;

解决方案

  • 从库只做读操作,不要有写操作
  • 慢查询加超时限制

原因5️⃣:网络抖动

主库到从库的网络延迟:
正常:5ms
抖动:500ms(丢包、重传)

结果:binlog传输变慢

排查方法

# 测试主从网络延迟
ping 从库IP

# 测试网络带宽
iperf -c 从库IP

解决方案

  • 主从部署在同一机房
  • 使用高速网络(万兆网卡)
  • 跨地域用专线

延迟原因总结

原因现象解决方案
从库配置低延迟持续增长升级从库硬件
主库写入快高峰期延迟大开启并行复制
大事务突然延迟几十秒拆分大事务
锁等待延迟不稳定从库避免写操作
网络抖动延迟波动大主从同机房

💡 主从延迟的6种解决方案

哈吉米:"知道原因了,怎么解决主从数据不一致的问题?"

阿西噶阿西:"有6种方案,各有适用场景。"


方案1️⃣:强制走主库(最简单)

思路:写入后立即读取,强制查主库。

代码实现

@Service
public class OrderService {
    
    @Autowired
    private OrderMapper orderMapper;
    
    @Transactional
    public void createOrder(Order order) {
        // 写入主库
        orderMapper.insert(order);
    }
    
    /**
     * 查询刚创建的订单(强制主库)
     */
    @DataSource("master")  // 强制路由到主库
    public Order getOrderById(Long orderId) {
        return orderMapper.selectById(orderId);
    }
    
    /**
     * 查询历史订单列表(可以走从库)
     */
    @DataSource("slave")  // 路由到从库
    public List<Order> listOrders(Long userId) {
        return orderMapper.selectByUserId(userId);
    }
}

自定义注解

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSource {
    String value() default "slave";  // 默认从库
}

AOP切面

@Aspect
@Component
public class DataSourceAspect {
    
    @Around("@annotation(dataSource)")
    public Object around(ProceedingJoinPoint point, DataSource dataSource) throws Throwable {
        // 设置数据源
        DynamicDataSourceHolder.setDataSource(dataSource.value());
        
        try {
            return point.proceed();
        } finally {
            // 清除数据源
            DynamicDataSourceHolder.clearDataSource();
        }
    }
}

优点

  • ✅ 100%数据一致
  • ✅ 实现简单

缺点

  • ❌ 主库压力大
  • ❌ 读写分离效果打折扣

适用场景:核心业务(订单、支付)


方案2️⃣:延迟补偿(简单粗暴)

思路:写入后等待一小段时间,再查询从库。

代码实现

@Service
public class OrderService {
    
    @Transactional
    public Order createOrderAndQuery(Order order) {
        // 1. 写入主库
        orderMapper.insert(order);
        
        // 2. 等待100ms(等主从同步)
        Thread.sleep(100);
        
        // 3. 查询从库
        return orderMapper.selectById(order.getId());
    }
}

优点

  • ✅ 实现超简单
  • ✅ 大部分场景有效

缺点

  • ❌ 不够优雅
  • ❌ 延迟不确定(100ms可能不够)
  • ❌ 白白浪费100ms

适用场景:临时方案,不推荐生产使用


方案3️⃣:二次读取(推荐)

思路:先查从库,查不到再查主库。

代码实现

@Service
public class OrderService {
    
    /**
     * 智能查询(先从库,查不到再主库)
     */
    public Order getOrderById(Long orderId) {
        // 1. 先查从库
        Order order = orderMapper.selectByIdFromSlave(orderId);
        
        // 2. 从库查不到,再查主库
        if (order == null) {
            order = orderMapper.selectByIdFromMaster(orderId);
        }
        
        return order;
    }
}

优化版(缓存标记)

@Service
public class OrderService {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    @Transactional
    public void createOrder(Order order) {
        // 1. 写入主库
        orderMapper.insert(order);
        
        // 2. 在Redis中标记"刚写入"(1秒过期)
        String key = "just_created:order:" + order.getId();
        redisTemplate.opsForValue().set(key, "1", 1, TimeUnit.SECONDS);
    }
    
    public Order getOrderById(Long orderId) {
        String key = "just_created:order:" + orderId;
        
        // 1. 检查Redis标记
        if (redisTemplate.hasKey(key)) {
            // 刚写入的,强制走主库
            return orderMapper.selectByIdFromMaster(orderId);
        }
        
        // 2. 否则走从库
        return orderMapper.selectByIdFromSlave(orderId);
    }
}

优点

  • ✅ 大部分请求走从库(性能好)
  • ✅ 数据一致性好
  • ✅ 自适应(自动判断)

缺点

  • ⚠️ 需要Redis
  • ⚠️ 代码稍复杂

适用场景:⭐⭐⭐⭐⭐ 推荐!


方案4️⃣:中间件路由(ShardingSphere)

思路:用数据库中间件自动路由。

ShardingSphere配置

# application.yml
spring:
  shardingsphere:
    datasource:
      names: master,slave1,slave2
      master:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://192.168.1.10:3306/db
        username: root
        password: 123456
      slave1:
        jdbc-url: jdbc:mysql://192.168.1.11:3306/db
      slave2:
        jdbc-url: jdbc:mysql://192.168.1.12:3306/db
    
    rules:
      readwrite-splitting:
        data-sources:
          myds:
            type: Static
            props:
              write-data-source-name: master
              read-data-source-names: slave1,slave2
              load-balancer-name: round_robin
        
        load-balancers:
          round_robin:
            type: ROUND_ROBIN  # 轮询

代码

@Service
public class OrderService {
    
    @Autowired
    private OrderMapper orderMapper;
    
    // 写操作自动路由到master
    @Transactional
    public void createOrder(Order order) {
        orderMapper.insert(order);
    }
    
    // 读操作自动路由到slave
    public Order getOrderById(Long orderId) {
        return orderMapper.selectById(orderId);
    }
    
    // 强制主库(在同一个事务中)
    @Transactional
    public Order getOrderByIdFromMaster(Long orderId) {
        return orderMapper.selectById(orderId);  // 事务中自动走主库
    }
}

优点

  • ✅ 代码无侵入
  • ✅ 配置化管理
  • ✅ 自动负载均衡

缺点

  • ⚠️ 引入中间件,架构复杂
  • ⚠️ 学习成本高

适用场景:大型项目,统一管理读写分离


方案5️⃣:分布式ID(避免依赖自增ID)

问题场景

// 主库插入数据,返回自增ID
orderMapper.insert(order);
Long orderId = order.getId();  // 自增ID = 100001

// 立即查询从库
Order result = orderMapper.selectById(orderId);  // 查不到(主从延迟)

解决方案:用分布式ID,不依赖自增ID

@Service
public class OrderService {
    
    @Autowired
    private SnowflakeIdGenerator idGenerator;
    
    @Transactional
    public void createOrder(Order order) {
        // 1. 提前生成ID(不依赖数据库自增)
        Long orderId = idGenerator.nextId();
        order.setId(orderId);
        
        // 2. 插入数据
        orderMapper.insert(order);
        
        // 3. 立即返回ID给前端
        return orderId;
    }
    
    // 前端可以直接用这个ID查询(不依赖主从同步)
    public Order getOrderById(Long orderId) {
        // 可以放心走从库,因为ID是提前生成的
        return orderMapper.selectById(orderId);
    }
}

雪花算法实现

@Component
public class SnowflakeIdGenerator {
    
    private final long workerId;
    private final long datacenterId;
    private long sequence = 0L;
    private long lastTimestamp = -1L;
    
    public synchronized long nextId() {
        long timestamp = System.currentTimeMillis();
        
        if (timestamp < lastTimestamp) {
            throw new RuntimeException("时钟回拨");
        }
        
        if (timestamp == lastTimestamp) {
            sequence = (sequence + 1) & 4095;
            if (sequence == 0) {
                timestamp = tilNextMillis(lastTimestamp);
            }
        } else {
            sequence = 0L;
        }
        
        lastTimestamp = timestamp;
        
        return ((timestamp - 1288834974657L) << 22)
                | (datacenterId << 17)
                | (workerId << 12)
                | sequence;
    }
    
    private long tilNextMillis(long lastTimestamp) {
        long timestamp = System.currentTimeMillis();
        while (timestamp <= lastTimestamp) {
            timestamp = System.currentTimeMillis();
        }
        return timestamp;
    }
}

优点

  • ✅ 彻底解决依赖自增ID的问题
  • ✅ 性能好(本地生成,不依赖数据库)

缺点

  • ⚠️ 需要引入分布式ID方案

方案6️⃣:最终一致性(接受短暂不一致)

思路:业务上容忍短时间内的数据不一致。

场景

用户发帖:
1. 写入主库
2. 立即查询"我的帖子列表"(从库)
3. 可能看不到刚发的帖子(1-2秒后才能看到)

方案:前端提示"发布成功,审核中..."
实际:不是审核,是主从延迟,但用户不知道

代码

@Service
public class PostService {
    
    @Transactional
    public Result createPost(Post post) {
        // 写入主库
        postMapper.insert(post);
        
        // 返回"审核中"(其实是等主从同步)
        return Result.success("发布成功,审核中...");
    }
    
    // 列表查询走从库(容忍短暂看不到)
    public List<Post> listMyPosts(Long userId) {
        return postMapper.selectByUserId(userId);
    }
}

优点

  • ✅ 性能最好(读全走从库)
  • ✅ 架构简单

缺点

  • ❌ 用户体验稍差
  • ❌ 不适合强一致性业务

适用场景:社交、内容类产品(帖子、评论、点赞)


6种方案对比

方案一致性性能复杂度适用场景
强制主库⭐⭐⭐⭐⭐⭐⭐核心业务(支付、订单)
延迟补偿⭐⭐⭐⭐⭐⭐临时方案
二次读取⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐ 推荐
中间件路由⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐大型项目
分布式ID⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐配合其他方案
最终一致性⭐⭐⭐⭐⭐⭐⭐非核心业务

🎯 读写分离架构设计

南北绿豆:"最后,来看看完整的读写分离架构。"

架构1️⃣:一主一从(最简单)

        【应用服务器】
             ↓
      写 ↙      ↘ 读
         ↓          ↓
    MySQL主库  ←  MySQL从库
      (写)    同步   (读)

优点:架构简单
缺点:从库单点,挂了就没法读


架构2️⃣:一主多从(常用)

        【应用服务器】
             ↓
      写 ↙      ↘ 读(负载均衡)
         ↓       ↙    ↘
    MySQL主库  ←  MySQL从库1
      (写)   同步↘   (读)
                  ↘
                   MySQL从库2
                      (读)

优点:读能力水平扩展
缺点:写能力受限于主库


架构3️⃣:双主(互为主从)

    MySQL-A  ←→  MySQL-B
      (主)   双向   (主)
             同步

优点:写能力翻倍
缺点:容易冲突(两边同时写同一行)


架构4️⃣:级联复制(减轻主库压力)

    MySQL主库
        ↓ 同步
    MySQL从库1(中继)
      ↙    ↘ 同步
从库2      从库3
(读)        (读)

优点:主库只需同步给1个从库,减轻网络压力
缺点:延迟增加(二级从库延迟更大)


🎓 面试高频题

题目1:主从复制的原理是什么?

答案

主从复制依赖binlog,有3个关键线程:

  1. Dump线程(主库):读取binlog,发送给从库
  2. IO线程(从库):接收binlog,写入relay log
  3. SQL线程(从库):读取relay log,执行SQL

流程:主库写binlog → Dump线程发送 → 从库IO线程接收 → 写入relay log → SQL线程执行 → 从库数据更新


题目2:binlog有哪几种格式?区别是什么?

答案

格式记录内容优点缺点
STATEMENT原始SQLbinlog小某些函数(NOW、UUID)会导致主从不一致
ROW每行数据的变化一致性强binlog大,批量操作慢
MIXED自动选择平衡判断可能不准

推荐ROW格式(MySQL 8.0默认)


题目3:如何解决主从延迟问题?

答案

原因:网络传输、从库配置低、大事务、锁等待

解决方案

  1. 强制走主库(核心业务)
  2. 二次读取(先从库,查不到再主库)
  3. 半同步复制(等从库确认)
  4. 并行复制(多个SQL线程)
  5. 分布式ID(不依赖自增ID)
  6. 最终一致性(容忍短暂不一致)

题目4:读写分离后,如何保证数据一致性?

答案

方案原理适用场景
强制主库写入后立即读取,走主库订单、支付
二次读取从库查不到,再查主库推荐方案
Redis标记刚写入的数据标记"走主库"高并发场景
最终一致性容忍1-2秒延迟帖子、评论

题目5:异步复制、半同步复制、同步复制的区别?

答案

模式主库等待从库确认?性能数据安全
异步复制不等可能丢数据
半同步复制等至少1个从库接收binlog更安全
全同步复制等所有从库执行完成最安全(MySQL不支持)

🎉 结束语

晚上10点,三人终于把主从复制和读写分离讲透了。

哈吉米:"原来主从延迟是必然的,异步复制性能好但有延迟,半同步复制更安全但性能差一些。"

南北绿豆:"对!核心业务用强制主库二次读取,非核心业务用最终一致性。"

阿西噶阿西:"还有,binlog格式推荐用ROW,虽然体积大但数据一致性强。"

哈吉米:"我明天就改成二次读取+Redis标记的方案!"

南北绿豆:"对,这个方案最实用。下周咱们聊聊分库分表?"

哈吉米:"好!我想知道单表多大需要分表!"


主从复制记忆口诀

binlog记录主库变化,三个线程协同工作
Dump发送IO接收,SQL线程执行更新
ROW格式保一致,异步复制性能好
半同步更安全,主从延迟要优化
强制主库保一致,二次读取最实用


希望这篇文章能帮你彻底搞懂MySQL主从复制和读写分离!记住:性能和一致性是权衡,根据业务场景选择合适的方案

收藏+点赞,面试不慌张!💪