问题

什么是数据库的主从延迟,如何解决?

答案

核心概念

主从延迟是指主库执行的写操作在从库上重放完成的时间差。表现为:主库已经提交的事务,在从库上还未执行完成,导致从库数据落后于主库。

延迟时间 = 从库当前时间戳 - 主库binlog中记录的事务时间戳

产生原因

1. 从库性能不足

  • 硬件差异:从库配置低于主库(CPU、内存、磁盘IO)
  • 资源竞争:从库同时承担大量查询请求,影响SQL线程执行
  • 磁盘IO慢:机械硬盘远慢于SSD

2. 主库写入压力过大

  • 高并发写入:主库多线程并发写入,从库单线程(或并行度不足)重放
  • 大事务:主库执行大批量更新,从库需要较长时间重放
  • DDL操作:ALTER TABLE等操作耗时长,阻塞后续事务

3. 网络延迟

  • 带宽不足:binlog传输慢
  • 跨地域复制:网络延迟高(如主库在北京,从库在上海)

4. 锁冲突

  • 从库SQL线程执行时遇到锁等待
  • 从库上有长事务未提交,阻塞复制线程

监控方法

1. 查看延迟时间

SHOW SLAVE STATUS\G

-- 关键指标
Seconds_Behind_Master: 5  -- 延迟秒数,NULL表示复制中断
Slave_SQL_Running: Yes
Slave_IO_Running: Yes

2. 查看位点信息

SHOW SLAVE STATUS\G

Master_Log_File: mysql-bin.000003
Read_Master_Log_Pos: 154
Relay_Log_File: relay-bin.000002
Relay_Log_Pos: 367
Exec_Master_Log_Pos: 154  -- 从库已执行到主库binlog的位置

3. 监控告警

-- 设置监控阈值
SELECT
  IF(Seconds_Behind_Master > 10, 'ALERT', 'OK') AS status,
  Seconds_Behind_Master
FROM information_schema.SLAVE_STATUS;

解决方案

1. 架构层面优化

使用并行复制

# MySQL 5.7+
slave-parallel-type=LOGICAL_CLOCK
slave-parallel-workers=8  -- 根据CPU核心数调整

# MySQL 8.0+
binlog_transaction_dependency_tracking=WRITESET
slave-parallel-workers=16

升级为半同步复制

# 主库配置
rpl_semi_sync_master_enabled=1
rpl_semi_sync_master_timeout=1000  -- 超时1秒降级为异步

# 从库配置
rpl_semi_sync_slave_enabled=1

半同步保证至少一个从库接收到binlog后主库才返回,减少数据不一致风险。

2. 硬件层面优化

  • 升级从库硬件:使用SSD、增加内存、更强CPU
  • 网络优化:使用高速专线、减少跨地域复制
  • 独立部署:从库专用于复制,查询流量分流到其他只读实例

3. 配置层面优化

主库优化组提交

# 增加组提交批次,提高从库并行度
binlog_group_commit_sync_delay=100  -- 延迟100微秒
binlog_group_commit_sync_no_delay_count=10  -- 或凑够10个事务

从库优化参数

# 跳过部分安全检查(仅从库)
sync_binlog=0  -- 从库不需要频繁刷盘
innodb_flush_log_at_trx_commit=2  -- 降低刷盘频率

关闭从库binlog

# 如果从库不作为其他实例的主库
log-bin=OFF  -- 节省写入开销

4. 应用层面优化

读写分离策略调整

// 强一致性读:直接读主库
@Transactional(readOnly = false)
public User getUserById(Long id) {
    return masterDataSource.queryUser(id);
}

// 最终一致性读:读从库
@Transactional(readOnly = true)
public List<User> listUsers() {
    return slaveDataSource.queryUsers();
}

// 延迟敏感读:先读从库,判断延迟后决定
public User getUserWithCheck(Long id) {
    if (checkSlaveDelay() < 1000) {  // 延迟小于1秒
        return slaveDataSource.queryUser(id);
    }
    return masterDataSource.queryUser(id);  // 降级读主库
}

使用缓存

@Cacheable(value = "users", key = "#id")
public User getUserById(Long id) {
    return slaveDataSource.queryUser(id);
}

避免大事务

// 不好的做法:大批量更新
UPDATE users SET status = 1 WHERE create_time < '2024-01-01';  -- 影响百万行

// 优化:分批处理
for (int i = 0; i < totalBatches; i++) {
    updateUsersBatch(i * 1000, 1000);  -- 每次1000行
    Thread.sleep(10);  -- 间隔执行减少主从压力
}

5. 业务层面容忍

适用场景识别

  • 可容忍延迟:商品浏览、评论列表、统计报表
  • 不可容忍延迟:用户刚注册后立即登录、支付后查订单状态

双写策略

// 写入主库 + 缓存
public void createOrder(Order order) {
    masterDataSource.insert(order);
    redis.set("order:" + order.getId(), order, 10);  // 缓存10秒
}

// 读取时优先从缓存
public Order getOrder(Long id) {
    Order order = redis.get("order:" + id);
    if (order != null) return order;
    return slaveDataSource.query(id);  // 缓存过期后读从库
}

极端情况处理

1. 延迟过大临时方案

-- 停止从库SQL线程
STOP SLAVE SQL_THREAD;

-- 跳过当前出问题的事务(谨慎使用)
SET GLOBAL SQL_SLAVE_SKIP_COUNTER = 1;

-- 重启SQL线程
START SLAVE SQL_THREAD;

2. 重建从库

# 主库导出
mysqldump --master-data=2 --single-transaction -A > backup.sql

# 从库导入
mysql < backup.sql

# 重新配置复制
CHANGE MASTER TO MASTER_LOG_FILE='xxx', MASTER_LOG_POS=xxx;
START SLAVE;

面试答题总结

主从延迟是指从库数据落后于主库的时间差,主要原因是主库并发写入快、从库单线程回放慢,或硬件、网络、锁冲突等问题。解决方案包括:

  1. 并行复制:提升从库回放速度
  2. 半同步复制:保证数据可靠性
  3. 硬件升级:SSD、高配从库
  4. 应用优化:读写分离策略调整、缓存、避免大事务
  5. 业务容忍:区分强一致和最终一致场景

监控通过Seconds_Behind_Master指标,结合业务特点选择合适的优化策略。