SQL优化 2

读写分离、主从同步延迟问题及解决方案

什么是“读写分离”?

定义:

读写分离 是一种数据库架构优化策略,核心思想是将数据库的“写操作”(INSERT/UPDATE/DELETE)和“读操作”(SELECT)分别路由到不同的数据库实例上,以提升系统整体的并发处理能力和可用性。

目的:

  • 减轻主库压力
  • 提高读并发能力
  • 增强系统扩展性

架构示意图:

1
2
3
4
5
6
        应用层
     ┌─────┴─────┐
     ▼           ▼
   主库 (Master)   从库 (Slave 1, Slave 2, ...)
  (负责写)        (负责读)

写操作 → 主库
读操作 → 从库(可多个,负载均衡)


什么是“主从同步”?

定义:

主从同步(Replication)是指主库的数据变更(binlog)被自动复制到一个或多个从库,从而保持从库数据与主库“最终一致”。

同步流程(以 MySQL 为例):

  1. 主库执行写操作,记录到 binlog(二进制日志)
  2. 从库的 I/O 线程连接主库,请求 binlog,写入本地 relay log(中继日志)
  3. 从库的 SQL 线程读取 relay log,重放 SQL 语句,更新本地数据

注意:这个过程是异步的(默认),也就是说,主库提交事务后,不会等待从库执行完毕。


主从同步延迟问题(Replication Lag)

什么是“延迟”?

主库写入数据后,从库未能立即同步到最新状态,导致从库读取到的是“旧数据”。

举例:用户刚下单成功(写入主库),立即刷新页面查看订单(读从库),却看不到订单 —— 这就是典型的“主从延迟导致数据不一致”。


延迟产生的原因

原因说明
1. 从库性能瓶颈CPU、磁盘 IO、内存不足,SQL 线程执行慢
2. 大事务/长事务一个事务包含大量操作,从库需逐条重放,耗时久
3. 主库写入压力大binlog 产生速度 > 从库消费速度,积压 relay log
4. 网络延迟主从之间网络带宽不足或延迟高
5. 从库单线程重放(MySQL 5.6 之前)SQL 线程单线程执行,无法并行(MySQL 5.7+ 支持并行复制)
6. DDL 操作或无主键表ALTER TABLE、无主键 UPDATE/DELETE 导致全表扫描,拖慢同步

解决方案(应对主从延迟)


优化从库性能

  • 升级硬件(SSD、更多 CPU、更大内存)
  • 优化从库参数(如 innodb_flush_log_at_trx_commit=2, sync_binlog=0 —— 牺牲部分一致性换性能)
  • 关闭从库的 binlog(如果从库不作为其他从库的主库)

启用并行复制(MySQL 5.7+ / MariaDB 10.0+)

MySQL 5.7 开始支持 基于事务组提交的并行复制(logical clock),可大幅提升重放速度。

1
2
3
4
5
6
7
-- 查看当前并行复制配置
SHOW VARIABLES LIKE 'slave_parallel_workers';
SHOW VARIABLES LIKE 'slave_parallel_type';

-- 设置并行复制(示例)
SET GLOBAL slave_parallel_workers = 4;
SET GLOBAL slave_parallel_type = 'LOGICAL_CLOCK';

注意:表必须有主键或唯一索引,否则并行复制可能退化为串行。


业务层“强制读主”策略

对“写后立即读”的场景,强制走主库,避免读到旧数据。

实现方式:

  • 代码标记:在写操作后,设置一个“读主标记”,在读操作时检查该标记
  • 中间件路由:如 ShardingSphere、MyCat、ProxySQL,支持 hint 或注解路由
  • 短时读主:写入后 1~2 秒内读主,之后读从(需结合业务容忍度)

示例(伪代码):

1
2
3
4
5
6
# 写入订单
order.save()  # 写主库
set_read_from_master_for_next_2_seconds()  # 设置2秒内读主

# 读取订单(此时强制读主)
order = Order.get(order_id)  # 路由到主库

监控 + 自动降级

  • 监控主从延迟(SHOW SLAVE STATUSSeconds_Behind_Master
  • 当延迟 > 阈值(如 5s),自动将读请求切回主库
  • 延迟恢复后,再切回从库

可结合 Prometheus + AlertManager + 自动化脚本实现。


使用“半同步复制”(Semi-Sync Replication)

MySQL 支持 半同步复制:主库 commit 前,至少等待一个从库 ack 接收到 binlog。

1
2
3
4
5
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_slave_enabled = 1;

优点:降低“数据丢失”风险,一定程度减少“读不到最新数据”
缺点:写入性能下降(需等待网络往返),不能完全消除延迟(从库执行仍需时间)


采用“最终一致性” + 业务容忍设计

  • 不是所有场景都需要“强一致”
  • 比如:用户发评论 → 刷新页面可能看不到 → 提示“内容稍后显示”
  • 或者前端轮询、WebSocket 推送更新

适合:非核心路径、展示型数据、异步操作


引入缓存层(如 Redis)缓解读压力

  • 写主库后,同时更新/删除缓存
  • 读请求优先读缓存,缓存无则读从库
  • 可避免大量读请求直接压到数据库

注意缓存与数据库一致性问题(Cache Aside Pattern)


实际案例:电商“下单后查订单”

问题:

用户下单 → 页面跳转“我的订单” → 从库未同步 → 显示“空”

解决方案组合:

  1. 写后强制读主 2 秒(覆盖 99% 场景)
  2. 前端 loading + 重试机制(优雅降级)
  3. 监控延迟,自动切读主(兜底)
  4. 订单数据写入 Redis 缓存,读请求优先读缓存

如何监控主从延迟?

MySQL 命令:

1
SHOW SLAVE STATUS\G

重点关注:

  • Seconds_Behind_Master:从库落后主库的秒数(不完全准确,仅作参考)
  • Slave_IO_Running / Slave_SQL_Running:是否正常运行
  • Last_IO_Error / Last_SQL_Error:错误信息

更精准方式:

  • 在主库写入带时间戳的“心跳表”,从库读取计算真实延迟
  • 使用 pt-heartbeat(Percona 工具)精准测量延迟

总结:主从延迟应对策略速查表

场景推荐方案
写后立即读强制读主(2 秒内)
高并发读、允许延迟读从库 + 缓存
要求数据强一致半同步复制 + 读主
延迟过大自动保护监控 + 自动切主
性能瓶颈并行复制 + 硬件升级 + SQL 优化
架构演进考虑分库分表、NewSQL(TiDB)、读写分离中间件

版权声明:本文为原创,依据 CC BY-NC-SA 4.0 许可证进行授权,转载请附上出处链接及本声明。