MySQL主从延迟还有什么处理方法?—— 6种你不知道的解决方案

摘要:从一次"秒杀活动主从延迟5秒导致超卖"的严重故障出发,深度剖析主从延迟的6种高级解决方案。通过并行复制的配置优化、MTS多线程复制的原理图解、以及半同步复制的性能权衡,揭秘如何将主从延迟从5秒降到50ms。配合时序图展示复制流程,给出不同业务场景下的最佳选型和配置参数。


💥 翻车现场

双十一当天,中午12点。

告警:
🚨 秒杀活动出现超卖!
🚨 库存100件,卖出了127件!
🚨 用户投诉:显示有货,下单后提示缺货

哈吉米(崩溃):"怎么又超卖了?我明明做了库存检查啊!"

紧急查看代码:

// 秒杀接口(读写分离架构)
public boolean seckill(Long userId, Long productId) {
    // 1. 查询库存(走从库)
    Stock stock = stockMapper.selectByProductId(productId);
    if (stock.getNum() <= 0) {
        return false;
    }
    
    // 2. 扣减库存(写主库)
    stockMapper.decreaseStock(productId);
    
    // 3. 创建订单
    orderMapper.insert(order);
    
    return true;
}

哈吉米:"代码没问题啊……"

紧急查看主从延迟:

-- 从库执行
SHOW SLAVE STATUS\G

Seconds_Behind_Master: 5  ← 延迟5秒!

问题分析

时间线:
T1: 主库库存=100
T2: 用户A扣减库存(主库库存=99),但从库还是100(延迟5秒)
T3: 用户B查询库存(从库库存=100),检查通过 ✅
T4: 用户B扣减库存(主库库存=98)
T5: 用户C查询库存(从库库存=100),检查通过 ✅
T6: 用户C扣减库存(主库库存=97)
...

结果:100个库存,因为主从延迟,多卖了27件!

哈吉米:"卧槽,主从延迟导致的超卖!"

晚上,南北绿豆和阿西噶阿西来了。

南北绿豆:"主从延迟在高并发场景下会放大问题!必须优化!"
阿西噶阿西:"我给你讲6种高级解决方案。"


🚀 方案1:并行复制(MTS多线程复制)

问题:SQL线程是单线程

主库:
1秒写入10万条SQL

从库:
SQL线程是单线程,1秒只能执行5万条

结果:延迟越来越大

解决:开启并行复制

MySQL 5.6+支持并行复制

-- 从库执行

-- 查看当前配置
SHOW VARIABLES LIKE 'slave_parallel%';

+------------------------+----------+
| Variable_name          | Value    |
+------------------------+----------+
| slave_parallel_workers | 0        |0表示单线程
| slave_parallel_type    | DATABASE |
+------------------------+----------+

-- 开启并行复制(4个SQL线程)
STOP SLAVE SQL_THREAD;
SET GLOBAL slave_parallel_type = 'LOGICAL_CLOCK';  -- MySQL 5.7+
SET GLOBAL slave_parallel_workers = 4;             -- 4个线程
START SLAVE SQL_THREAD;

并行复制的类型

类型并行粒度适用场景
DATABASE不同数据库并行多个数据库
LOGICAL_CLOCK同时提交的事务并行⭐⭐⭐⭐⭐ 推荐

并行复制流程图

sequenceDiagram
    participant Master as 主库
    participant IOThread as 从库IO线程
    participant SQL1 as 从库SQL线程1
    participant SQL2 as 从库SQL线程2
    participant SQL3 as 从库SQL线程3
    participant SQL4 as 从库SQL线程4

    Master->>IOThread: binlog(10万条SQL)
    IOThread->>IOThread: 写relay log
    
    par 并行执行
        IOThread->>SQL1: 分配事务1
        IOThread->>SQL2: 分配事务2
        IOThread->>SQL3: 分配事务3
        IOThread->>SQL4: 分配事务4
        
        SQL1->>SQL1: 执行SQL
        SQL2->>SQL2: 执行SQL
        SQL3->>SQL3: 执行SQL
        SQL4->>SQL4: 执行SQL
    end
    
    Note over SQL1,SQL4: 4个线程并行<br/>吞吐量提升4倍

性能对比

配置吞吐量延迟
单线程5万条/秒5秒
4线程并行20万条/秒0.5秒

性能提升:10倍


🚀 方案2:半同步复制 + 组提交

半同步复制配置

-- 主库安装插件
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;
SET GLOBAL rpl_semi_sync_master_timeout = 1000;  -- 超时1秒降级为异步

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

组提交优化

-- 主库开启组提交(批量发送binlog)
SET GLOBAL binlog_group_commit_sync_delay = 1000;        -- 延迟1ms
SET GLOBAL binlog_group_commit_sync_no_delay_count = 10; -- 或攒够10个事务

原理

不用组提交:
事务1提交 → 发送binlog → 等待从库确认
事务2提交 → 发送binlog → 等待从库确认
...

用组提交:
事务1提交 → 
事务2提交 → 攒一批
事务3提交 → 
一起发送binlog → 一次等待从库确认

性能提升:减少网络往返次数

性能对比

配置TPS主从延迟
异步复制100002-5秒
半同步(无组提交)300050ms
半同步 + 组提交7000100ms

🚀 方案3:读写分离智能路由

原理:刚写入的数据强制走主库

@Service
public class SmartDataSourceService {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    /**
     * 写入数据时,记录"刚写入"
     */
    @Transactional
    public void createOrder(Order order) {
        // 1. 写主库
        orderMapper.insert(order);
        
        // 2. 在Redis标记"刚写入"(1秒过期)
        String key = "just_wrote:order:" + order.getId();
        redisTemplate.opsForValue().set(key, "1", 1, TimeUnit.SECONDS);
    }
    
    /**
     * 读取数据时,智能路由
     */
    public Order getOrderById(Long orderId) {
        String key = "just_wrote:order:" + orderId;
        
        // 检查是否刚写入
        if (redisTemplate.hasKey(key)) {
            // 刚写入的,强制走主库
            return orderMapper.selectByIdFromMaster(orderId);
        }
        
        // 否则走从库
        return orderMapper.selectByIdFromSlave(orderId);
    }
}

时序图

sequenceDiagram
    participant User as 用户
    participant App as 应用
    participant Redis
    participant Master as 主库
    participant Slave as 从库

    User->>App: 1. 创建订单
    App->>Master: 2. INSERT订单
    App->>Redis: 3. 标记"刚写入"(1秒)
    App->>User: 4. 返回成功
    
    User->>App: 5. 查询订单(1秒内)
    App->>Redis: 6. 检查是否刚写入?
    Redis->>App: 7. 是,有标记
    App->>Master: 8. 走主库查询 ✅
    Master->>User: 9. 返回订单
    
    Note over User,Slave: 1秒后
    User->>App: 10. 再次查询
    App->>Redis: 11. 检查是否刚写入?
    Redis->>App: 12. 否,标记过期了
    App->>Slave: 13. 走从库查询
    Slave->>User: 14. 返回订单

优点

  • ✅ 99%的查询走从库(性能好)
  • ✅ 刚写入的数据走主库(一致性好)
  • ✅ 自适应

🚀 方案4:分离核心业务和非核心业务

原理:核心业务强制主库,非核心业务可以从库

@Service
public class OrderService {
    
    /**
     * 核心业务:支付后查询(强制主库)
     */
    @DataSource("master")
    public Order getOrderAfterPayment(Long orderId) {
        return orderMapper.selectById(orderId);
    }
    
    /**
     * 非核心业务:订单列表(可以从库)
     */
    @DataSource("slave")
    public List<Order> listOrders(Long userId) {
        return orderMapper.selectByUserId(userId);
    }
}

业务分类

业务类型一致性要求推荐数据源
支付后查询主库
创建后查询主库
订单列表从库
历史订单从库
统计报表从库

🚀 方案5:延迟监控 + 自动降级

原理:主从延迟超过阈值,自动切换主库

@Component
public class ReplicationLagMonitor {
    
    @Autowired
    private DataSource slaveDataSource;
    
    private volatile boolean slaveLagging = false;
    
    /**
     * 定时检测主从延迟
     */
    @Scheduled(fixedDelay = 1000)  // 每秒检测
    public void checkReplicationLag() {
        try (Connection conn = slaveDataSource.getConnection()) {
            Statement stmt = conn.createStatement();
            ResultSet rs = stmt.executeQuery("SHOW SLAVE STATUS");
            
            if (rs.next()) {
                int secondsBehind = rs.getInt("Seconds_Behind_Master");
                
                // 延迟超过2秒,标记为延迟
                if (secondsBehind > 2) {
                    slaveLagging = true;
                    log.warn("主从延迟: {}秒,切换到主库", secondsBehind);
                } else {
                    slaveLagging = false;
                }
            }
        } catch (Exception e) {
            log.error("检测主从延迟失败", e);
            slaveLagging = true;  // 检测失败,保守起见走主库
        }
    }
    
    /**
     * 是否延迟
     */
    public boolean isSlaveLagging() {
        return slaveLagging;
    }
}

@Service
public class SmartOrderService {
    
    @Autowired
    private ReplicationLagMonitor monitor;
    
    public Order getOrderById(Long orderId) {
        // 如果主从延迟,走主库
        if (monitor.isSlaveLagging()) {
            return orderMapper.selectByIdFromMaster(orderId);
        }
        
        // 否则走从库
        return orderMapper.selectByIdFromSlave(orderId);
    }
}

优点

  • ✅ 自动切换
  • ✅ 保证一致性

🚀 方案6:业务补偿机制

原理:容忍短暂不一致,后续补偿

/**
 * 秒杀场景:先扣库存,后续异步校验
 */
@Service
public class SeckillServiceV2 {
    
    @Transactional
    public boolean seckill(Long userId, Long productId) {
        // 1. 强制走主库查询库存
        Stock stock = stockMapper.selectByIdFromMaster(productId);
        if (stock.getNum() <= 0) {
            return false;
        }
        
        // 2. 扣减库存(主库)
        int updated = stockMapper.decreaseStock(productId);
        if (updated == 0) {
            return false;
        }
        
        // 3. 创建订单
        Order order = new Order();
        order.setUserId(userId);
        order.setProductId(productId);
        order.setStatus(0);  // 待确认
        orderMapper.insert(order);
        
        // 4. 发送异步消息:延迟校验库存
        rabbitTemplate.convertAndSend("seckill.verify.queue", order.getId());
        
        return true;
    }
}

/**
 * 延迟校验(1秒后,主从已同步)
 */
@Component
public class SeckillVerifyConsumer {
    
    @RabbitListener(queues = "seckill.verify.queue")
    public void verifyOrder(Long orderId) {
        // 延迟1秒
        Thread.sleep(1000);
        
        // 校验库存(此时主从已同步)
        Order order = orderMapper.selectById(orderId);
        Stock stock = stockMapper.selectByProductId(order.getProductId());
        
        if (stock.getNum() < 0) {
            // 超卖了,取消订单
            orderMapper.updateStatus(orderId, -1);  // 已取消
            // 恢复库存
            stockMapper.increaseStock(order.getProductId(), 1);
            // 通知用户
            notifyUser(order.getUserId(), "抢购失败,库存不足");
        } else {
            // 确认订单
            orderMapper.updateStatus(orderId, 1);  // 已确认
        }
    }
}

优点

  • ✅ 性能好(异步校验)
  • ✅ 自动补偿

缺点

  • ⚠️ 用户体验稍差(可能先显示成功,后续取消)

📊 性能对比

测试场景:秒杀100个库存

方案主从延迟是否超卖TPS
异步复制 + 读从库5秒✅ 超卖27个10000
强制走主库-❌ 不超卖3000
半同步复制50ms❌ 不超卖7000
并行复制(4线程)0.5秒✅ 可能超卖3个9000
智能路由(Redis标记)1秒❌ 不超卖8000
业务补偿5秒(异步校验)❌ 不超卖9500

🎓 面试标准答案

题目:主从延迟有哪些解决方案?

答案

6种解决方案

  1. 强制走主库

    • 写入后立即读取,走主库
    • 适合:核心业务(支付、订单)
  2. 二次读取

    • 从库查不到,再查主库
    • 适合:大部分场景
  3. 并行复制

    • 开启多线程SQL线程
    • 延迟从5秒降到0.5秒
  4. 半同步复制

    • 等从库确认接收binlog
    • 延迟降到50ms
  5. 智能路由

    • Redis标记刚写入的数据
    • 刚写入的走主库,其他走从库
  6. 业务补偿

    • 容忍短暂不一致
    • 后续异步校验和补偿

选型建议

  • 金融支付:半同步 + 强制主库
  • 电商订单:智能路由 + 业务补偿
  • 社交内容:最终一致性

🎉 结束语

一周后,哈吉米把主从延迟优化完了。

哈吉米:"开启了并行复制,延迟从5秒降到0.5秒!"

南北绿豆:"对,4个SQL线程并行,吞吐量提升4倍。"

阿西噶阿西:"核心业务加了智能路由,刚写入的数据走主库,再也不会因为主从延迟导致超卖了。"

哈吉米:"还有半同步复制,虽然性能下降30%,但数据更安全。"

南北绿豆:"对,根据业务场景选方案,没有银弹!"


记忆口诀

并行复制多线程,吞吐量翻四倍强
半同步等从库认,延迟五十毫秒降
智能路由Redis标,刚写数据走主库
业务补偿异步校,容忍延迟后补偿
核心业务强主库,非核心可从库


希望这篇文章能帮你搞定主从延迟的各种场景!根据业务选方案,性能和一致性都能兼顾!💪