4. 从单机到主从复制:读写分离的引入,解决单点故障与读扩展问题

说实话,单机架构在早期确实够用。但业务量一上来,问题就暴露了。

我记得有一次线上告警,数据库CPU直接飙到95%。查了半天,发现是某个报表查询把整个库拖死了。所有读写请求都挤在同一台机器上,谁也跑不掉。更可怕的是——如果这台机器挂了,整个系统就瘫痪了。

这就是单机架构的两个致命伤:单点故障读扩展瓶颈

怎么破?主从复制 + 读写分离。

4.1 主从复制的核心原理

说白了,就是让一台数据库(主库)负责写,多台数据库(从库)负责读。主库把数据变更同步给从库。

我习惯用MySQL的binlog机制来理解这个过程:

主库操作流程:
1. 客户端发起写请求(INSERT/UPDATE/DELETE)
2. 主库执行SQL,写入binlog
3. 从库的I/O线程拉取binlog,写入relay log
4. 从库的SQL线程回放relay log,完成数据同步

嗯,这里要注意:主从复制是异步的。也就是说,主库写完就返回客户端了,不等待从库确认。这带来了性能优势,但也埋了个坑——数据延迟。

核心要点:

  • 主库:负责所有写操作,也处理部分读操作
  • 从库:只负责读操作,不处理写
  • 同步方式:异步复制(默认)、半同步复制、全同步复制

4.2 读写分离的架构设计

你想想看,读写分离本质上就是流量拆分。把写流量引到主库,读流量分散到从库。

我在项目中遇到过一种典型的实现方式——在应用层做路由:

// 伪代码示例:读写分离路由
public class DataSourceRouter {
    
    private DataSource master;   // 主库
    private List<DataSource> slaves; // 从库列表
    
    public Connection getConnection(boolean isWrite) {
        if (isWrite) {
            return master.getConnection();
        } else {
            // 从库负载均衡,我用的是轮询
            int index = atomicCounter.incrementAndGet() % slaves.size();
            return slaves.get(index).getConnection();
        }
    }
}

这段代码看起来简单,但实际落地时坑不少。我曾经踩过一个坑——事务内的读操作必须路由到主库。为什么?因为事务内的写还没提交,从库根本看不到。如果读操作走了从库,就会读到脏数据。

避坑指南:

我曾经在项目中遇到一个诡异bug:用户下单后查不到订单。排查了半天,发现是事务内的查询走了从库,而主库的binlog还没同步过去。解决方案很简单——事务内的所有SQL都强制走主库。

4.3 主从复制的三种模式

不同的业务场景,对数据一致性的要求不一样。我一般这样选型:

模式 原理 一致性 性能 适用场景
异步复制 主库写完就返回,不等待从库 最终一致 最高 读多写少,可接受秒级延迟
半同步复制 主库等待至少一个从库确认 较强一致 中等 金融交易、订单系统
全同步复制 主库等待所有从库确认 强一致 最低 极少使用,性能代价太大

我个人习惯:默认用异步复制,对一致性要求高的场景切半同步。全同步复制?说实话,我几乎没用过,性能损耗太大了。

4.4 解决单点故障:主库高可用

读写分离解决了读扩展问题,但主库还是单点。主库挂了怎么办?

常见的方案是主从切换。我经历过一次手动切换,那叫一个手忙脚乱:

  1. 检测到主库宕机
  2. 选择一个从库,提升为主库
  3. 修改应用配置,指向新主库
  4. 重启应用

整个过程至少几分钟,业务完全不可用。后来我引入了MHA(Master High Availability),实现了自动切换:

MHA自动切换流程:
1. 监控节点检测到主库不可用
2. 选举新主库(数据最完整的从库)
3. 其他从库重新指向新主库
4. 虚拟IP漂移到新主库
5. 应用无感知,秒级完成

注意:

自动切换不是万能的。我曾经遇到过脑裂问题——主库没完全挂,只是网络分区了。这时候两个节点都认为自己是主库,数据就乱套了。解决方案是引入仲裁节点租约机制

4.5 读扩展:从库的横向扩展

读压力大了怎么办?加从库啊。

我习惯用一主多从的架构。比如一个主库带三个从库,读流量均匀分散。但要注意:从库不是越多越好。

为什么?因为每个从库都要从主库拉取binlog。从库太多,主库的网络带宽和磁盘I/O会成为瓶颈。我见过一个极端案例——主库挂了50个从库,结果主库的网卡被打满了,正常业务都受影响。

我的经验是:一个主库带3-5个从库比较合理。如果还需要更多读能力,可以考虑引入缓存层(比如Redis)或者做数据库分片。

4.6 架构图:主从复制与读写分离

下面这张图是我自己画的,展示了主从复制+读写分离的完整架构:

应用服务 读写分离路由器 主库(Master) 从库1(Slave) 从库2(Slave) 从库3(Slave) binlog同步 图例: 写请求 读请求 binlog同步 说明:应用通过路由器将写请求发往主库,读请求分散到多个从库。主库通过binlog异步同步数据到从库。

4.7 实践中的几个关键问题

最后,分享几个我在实战中遇到的坑:

  • 数据延迟监控:我习惯在从库上监控seconds_behind_master指标。超过阈值就报警,把读流量切回主库。
  • 从库故障处理:某个从库挂了,自动从负载均衡列表中摘除。修复后再加回来。
  • 主从数据一致性校验:定期用pt-table-checksum工具检查主从数据是否一致。不一致就修复。

总结一下:

主从复制+读写分离,解决了单点故障和读扩展两个核心问题。但它不是银弹——数据延迟、主从切换、脑裂等问题都需要认真对待。下一层,我们会聊到更复杂的分布式架构。


公众号:蓝海资料掘金营,微信deep3321