问题
什么是数据库的主从延迟,如何解决?
答案
核心概念
主从延迟是指主库执行的写操作在从库上重放完成的时间差。表现为:主库已经提交的事务,在从库上还未执行完成,导致从库数据落后于主库。
延迟时间 = 从库当前时间戳 - 主库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;
面试答题总结
主从延迟是指从库数据落后于主库的时间差,主要原因是主库并发写入快、从库单线程回放慢,或硬件、网络、锁冲突等问题。解决方案包括:
- 并行复制:提升从库回放速度
- 半同步复制:保证数据可靠性
- 硬件升级:SSD、高配从库
- 应用优化:读写分离策略调整、缓存、避免大事务
- 业务容忍:区分强一致和最终一致场景
监控通过Seconds_Behind_Master指标,结合业务特点选择合适的优化策略。