#DevOps

读副本与读写分离

读多写少?加读副本把读分摊到多个库,主库只管写。本文讲清读副本的架构(主从复制)、读写分离的实现(应用层路由/代理层)、复制延迟的一致性陷阱、以及什么时候该上读副本(读量是瓶颈时)什么时候不该。

本文要点

  • 主题聚焦:DevOps
  • 适合人群:独立开发者、全栈工程师、AI 应用开发者
  • 核心价值:快速理解背景、实践路径、常见误区和工程建议
作者 @admin·发布于 2026年4月5日·更新于 2026年7月23日

读多写少,是绝大多数互联网应用的典型负载特征。内容社区、电商商品页、新闻门户,读写比例常常是 10:1 甚至 100:1。主库不仅要扛写,还要扛海量读,迟早会被读压力压垮——CPU 跑满、连接数耗尽、慢查询挤占写资源。这时候需要的不是换更贵的机器(纵向扩展总有上限),而是把读分摊出去——加读副本(Read Replica)。这是数据库水平扩展读能力最基础、最成熟的手段。

一、为什么需要读副本

主库单点抗全部读写,典型的崩盘路径是这样的:读 QPS 涨上来 → 连接池打满 → CPU 飙高 → 慢查询拖垮整体 → 写请求也开始超时 → 雪崩。

很多人第一反应是"加 CPU、加内存、上更贵的实例",这条路叫纵向扩展(Scale Up),问题是有物理和经济上限,一台机器再强也是天花板。另一条路是水平扩展(Scale Out):把读流量分摊到多台机器上,主库只管写。读副本就是这条路的基本盘。

主库专注写(以及强一致读),副本分担读,读压力被多台机器均摊。简单粗暴但有效——这也是 MySQL、PostgreSQL 在生产环境标配的能力。

二、主从复制的架构

读副本的核心机制是主从复制(老叫法 Master-Slave,现在文档多叫 Primary-Replica):

text
          ┌──────────────────┐
   写 ──> │   主库 Primary    │
          └─────────┬────────┘
                    │ 复制(异步 / 半同步 / 同步)
         ┌──────────┴──────────┐
         ▼                     ▼
   ┌──────────┐          ┌──────────┐
   │ 副本 1   │          │ 副本 2   │  <── 读
   └──────────┘          └──────────┘

主库执行写操作(INSERT/UPDATE/DELETE),把变更写入 binlog(MySQL)或 WAL(PostgreSQL),副本拉取日志回放,保持数据一致。

复制方式按一致性强度分三种:

  • 异步复制(Async):主库写完即返回,不等副本确认。性能最好,但主库挂了可能丢数据,副本有延迟。MySQL/PostgreSQL 默认方式。
  • 半同步(Semi-sync):主库等至少一个副本收到日志(不是回放完)才返回。折中方案,MySQL 有半同步插件。
  • 全同步(Sync):主库等副本回放完才返回。数据最安全,但写延迟显著增加,生产少见。

绝大多数互联网场景选异步复制——用一致性换性能,再用业务手段弥补那点延迟。

三、读写分离的实现

有了主从,下一步是让应用的读走向副本、写走向主库。两种主流实现:

1. 应用层路由

在 ORM 或连接层判断 SQL 类型自动分发:SELECT 走副本,INSERT/UPDATE/DELETE 走主库。主流 ORM(Prisma、TypeORM、Django ORM、MyCat 之上层)都支持配置一主多从,框架内置路由。

ts
// 伪代码:DataSource 配置
{
  master:  { url: 'mysql://primary...'  },
  replicas: [
    { url: 'mysql://replica1...' },
    { url: 'mysql://replica2...' },
  ],
}
// SELECT 自动命中副本(轮询/随机),写自动命中主库

优点:可控、透明、可嵌业务规则(比如"写入后 5 秒内强制读主")。缺点:每种语言/框架都要配置一遍,跨库事务、多数据源场景复杂。

2. 代理层路由

用中间代理屏蔽主从细节,应用只看到一个逻辑库,代理按 SQL 自动分发。

  • MySQL 阵营:ProxySQL、MySQL Router、MaxScale(路由能力强)
  • PostgreSQL 阵营:Pgpool-II(连接池 + 路由)、HAProxy(纯负载均衡,不懂 SQL);PgBouncer 主要是连接池,不擅长按语句路由
text
   应用 ──> [ 代理层 ProxySQL / Pgpool ] ──> 主库(写)
                     │
                     ├──> 副本1(读)
                     └──> 副本2(读,负载均衡)

优点:应用无感知,扩容、故障切换对业务透明。缺点:多一跳延迟,代理本身要做高可用,运维成本不低。

实战中两者常组合:代理层做基础路由和高可用,应用层在关键路径上做精细控制(比如写后强一致读)。

四、复制延迟的一致性陷阱

异步复制带来一个经典坑:复制延迟(Replication Lag)。

主库写完的瞬间,副本可能还没回放完。如果用户写完立刻读(发完评论刷新页面),而读走向副本,就读到旧数据——"我刚发的评论怎么没了?"。这种写后读不一致是读副本最常见的一致性陷阱。延迟通常毫秒级,但压力大时可能到秒级。

对策有四种,实战中按场景选:

  1. 写后强一致读走主库:关键路径(用户自己的写后读)在写入后一段时间(5-30 秒)内强制走主库,用 session 标记或 sticky session 实现。最常用。
  2. 关键路径永不读副本:用户主页、订单详情、账户余额这类强一致读,永远走主库;排行榜、推荐、报表这类容忍最终一致的,走副本。
  3. 等待复制追上:读前检查复制位点,等副本追上再读。MySQL 的 WAIT_FOR_EXECUTED_GTID_SET、PostgreSQL 的 pg_wait_for_replication_lsn。准确但牺牲延迟,高并发下慎用。
  4. 业务容忍:很多场景用户感知不到(列表页晚几秒看到新内容),接受最终一致即可。

别追求全局强一致,而要识别出哪几条路径需要强一致,把那些读钉死在主库,其余的让副本安心干活。

五、副本数量:不是越多越好

新手常见误区:既然副本能分摊读,加它十几个岂不是读性能无敌?错。副本数量有明显边际效应:

  • 每个副本都增加主库的复制负担:主库要给每个副本推 binlog/WAL,副本越多主库网络/CPU 开销越大。
  • 运维成本线性增加:每加一台副本就多一台机器要监控、备份、打补丁、参与故障切换。
  • 延迟管理更难:副本越多,任何一台拖后腿都要处理,出延迟问题的概率上升。

经验法则:够分摊读压力就行。先加一两个,盯主库 CPU、连接数、副本延迟指标,真到瓶颈再加。2-5 个副本能覆盖绝大多数场景。盲目堆副本是花更多钱买更复杂的运维,不是买性能。

六、什么时候该上读副本(以及什么时候不该)

该上的信号

  • 读量是真瓶颈:主库 CPU 长期 > 70%、连接数打满、慢查询堆积,且瓶颈主要是 SELECT。
  • 读可容忍最终一致:列表页、搜索、推荐、报表、分析这类延迟几秒无所谓。
  • 基础优化已做完:索引、查询、表结构都优化到位,确实到了水平扩展的节点。

不该上的情况

  • 写为主:写密集型场景读副本帮不上忙(副本不能分担写)。该考虑的是分库分表(Sharding)。
  • 强一致要求极高:金融账户、库存扣减、支付,任何延迟导致的不一致都不能接受。
  • 量没到瓶颈:很多时候加个索引、改个查询就解决了,别动辄上副本——架构复杂度是债,能不借就不借。

决策顺序:先优化(索引/查询/Schema),再上缓存,再读副本,最后分片。 读副本是优化手段用尽后的水平扩展,不是第一根救命稻草。

七、读副本 vs 缓存

读副本和缓存(Redis/Memcached)都是分摊读压力的手段,但定位完全不同,常被混淆:

维度读副本缓存(Redis)
覆盖范围全量读(任意查询)热点读(特定 key)
延迟毫秒级(等同主库)微秒级(纯内存)
数据新鲜度准实时(复制延迟)可能过期(靠失效策略)
成本高(整套数据库实例)低(内存即可)
复杂度复制 + 路由失效 + 穿透/雪崩防护

实战中两者常组合:

text
   读请求 ──> 缓存层(Redis,挡热点读) ──命中──> 直接返回
                  │ 未命中
                  ▼
             读副本(扛全量读) ──> 回填缓存

缓存挡在前面吃掉 80%-95% 的热点读,剩下的长尾读走副本。这样副本数量可以更少,整体更经济。缓存是第一道防线,副本是第二道。 别用副本去做缓存的活——副本能扛全量但贵且重;也别只用缓存不备副本——缓存一旦大面积失效(雪崩),没有副本兜底就直接打穿到主库。

八、跨区域副本:就近读与灾备

单区域副本解决了读压力,但跨地域用户访问仍有物理延迟——亚洲用户访问美东主库,光速就决定了上百毫秒的 RTT。

跨区域副本把数据复制到不同地理区域,服务两个目的:

  1. 就近读(Geo-local Reads):用户附近区域部署副本,读延迟大幅降低。亚太用户读亚太副本,美东用户读美东副本。
  2. 异地灾备(Disaster Recovery):主区域整个挂了(机房断电、光缆挖断),跨区域副本可提升为主,保证业务连续性。
text
        ┌──────────────────┐
        │  主区域(美东)    │  写入
        │    Primary       │
        └────────┬─────────┘
                 │ 跨区域复制(通常异步,延迟更大)
        ┌────────┴────────┐
        ▼                 ▼
  ┌──────────┐      ┌──────────┐
  │ 亚太副本  │      │ 欧洲副本  │  就近读 + 灾备
  └──────────┘      └──────────┘

注意:跨区域复制延迟通常更大(几十到几百毫秒,取决于距离),一致性陷阱更明显。写后强一致读必须回主区域,业务设计要充分考虑——比如用户写完内容后,首页刷新走主区域,即便自己在亚太;列表页可读就近副本,容忍几秒延迟。

小结

  • 读副本解决的是读压力瓶颈,通过主从复制把读分摊到多台机器,主库专注写与强一致读。
  • 读写分离两层实现:应用层路由(ORM/连接层按语句分发)与代理层路由(ProxySQL/Pgpool 屏蔽细节),实战中常组合使用。
  • 异步复制有延迟,写后读不一致是最常见的坑——识别关键路径,把强一致读钉死在主库。
  • 副本数量够用就行,不是越多越好;跨区域副本兼顾就近读与异地灾备,但延迟与一致性要重新评估。
  • 决策顺序始终是:先优化(索引/查询/Schema),再缓存,再读副本,最后分片。

金句:读副本不是银弹,而是水平扩展读能力的工具——用在对的场景(读多写少、可容忍最终一致)是利器,用错的场景(写为主、强一致要求高、量没到瓶颈)是负担。

Related

相关推荐