读副本与读写分离
读多写少?加读副本把读分摊到多个库,主库只管写。本文讲清读副本的架构(主从复制)、读写分离的实现(应用层路由/代理层)、复制延迟的一致性陷阱、以及什么时候该上读副本(读量是瓶颈时)什么时候不该。
本文要点
- 主题聚焦:DevOps
- 适合人群:独立开发者、全栈工程师、AI 应用开发者
- 核心价值:快速理解背景、实践路径、常见误区和工程建议
读多写少,是绝大多数互联网应用的典型负载特征。内容社区、电商商品页、新闻门户,读写比例常常是 10:1 甚至 100:1。主库不仅要扛写,还要扛海量读,迟早会被读压力压垮——CPU 跑满、连接数耗尽、慢查询挤占写资源。这时候需要的不是换更贵的机器(纵向扩展总有上限),而是把读分摊出去——加读副本(Read Replica)。这是数据库水平扩展读能力最基础、最成熟的手段。
一、为什么需要读副本
主库单点抗全部读写,典型的崩盘路径是这样的:读 QPS 涨上来 → 连接池打满 → CPU 飙高 → 慢查询拖垮整体 → 写请求也开始超时 → 雪崩。
很多人第一反应是"加 CPU、加内存、上更贵的实例",这条路叫纵向扩展(Scale Up),问题是有物理和经济上限,一台机器再强也是天花板。另一条路是水平扩展(Scale Out):把读流量分摊到多台机器上,主库只管写。读副本就是这条路的基本盘。
主库专注写(以及强一致读),副本分担读,读压力被多台机器均摊。简单粗暴但有效——这也是 MySQL、PostgreSQL 在生产环境标配的能力。
二、主从复制的架构
读副本的核心机制是主从复制(老叫法 Master-Slave,现在文档多叫 Primary-Replica):
┌──────────────────┐
写 ──> │ 主库 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 之上层)都支持配置一主多从,框架内置路由。
// 伪代码: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 主要是连接池,不擅长按语句路由
应用 ──> [ 代理层 ProxySQL / Pgpool ] ──> 主库(写)
│
├──> 副本1(读)
└──> 副本2(读,负载均衡)
优点:应用无感知,扩容、故障切换对业务透明。缺点:多一跳延迟,代理本身要做高可用,运维成本不低。
实战中两者常组合:代理层做基础路由和高可用,应用层在关键路径上做精细控制(比如写后强一致读)。
四、复制延迟的一致性陷阱
异步复制带来一个经典坑:复制延迟(Replication Lag)。
主库写完的瞬间,副本可能还没回放完。如果用户写完立刻读(发完评论刷新页面),而读走向副本,就读到旧数据——"我刚发的评论怎么没了?"。这种写后读不一致是读副本最常见的一致性陷阱。延迟通常毫秒级,但压力大时可能到秒级。
对策有四种,实战中按场景选:
- 写后强一致读走主库:关键路径(用户自己的写后读)在写入后一段时间(5-30 秒)内强制走主库,用 session 标记或 sticky session 实现。最常用。
- 关键路径永不读副本:用户主页、订单详情、账户余额这类强一致读,永远走主库;排行榜、推荐、报表这类容忍最终一致的,走副本。
- 等待复制追上:读前检查复制位点,等副本追上再读。MySQL 的
WAIT_FOR_EXECUTED_GTID_SET、PostgreSQL 的pg_wait_for_replication_lsn。准确但牺牲延迟,高并发下慎用。 - 业务容忍:很多场景用户感知不到(列表页晚几秒看到新内容),接受最终一致即可。
别追求全局强一致,而要识别出哪几条路径需要强一致,把那些读钉死在主库,其余的让副本安心干活。
五、副本数量:不是越多越好
新手常见误区:既然副本能分摊读,加它十几个岂不是读性能无敌?错。副本数量有明显边际效应:
- 每个副本都增加主库的复制负担:主库要给每个副本推 binlog/WAL,副本越多主库网络/CPU 开销越大。
- 运维成本线性增加:每加一台副本就多一台机器要监控、备份、打补丁、参与故障切换。
- 延迟管理更难:副本越多,任何一台拖后腿都要处理,出延迟问题的概率上升。
经验法则:够分摊读压力就行。先加一两个,盯主库 CPU、连接数、副本延迟指标,真到瓶颈再加。2-5 个副本能覆盖绝大多数场景。盲目堆副本是花更多钱买更复杂的运维,不是买性能。
六、什么时候该上读副本(以及什么时候不该)
该上的信号
- 读量是真瓶颈:主库 CPU 长期 > 70%、连接数打满、慢查询堆积,且瓶颈主要是 SELECT。
- 读可容忍最终一致:列表页、搜索、推荐、报表、分析这类延迟几秒无所谓。
- 基础优化已做完:索引、查询、表结构都优化到位,确实到了水平扩展的节点。
不该上的情况
- 写为主:写密集型场景读副本帮不上忙(副本不能分担写)。该考虑的是分库分表(Sharding)。
- 强一致要求极高:金融账户、库存扣减、支付,任何延迟导致的不一致都不能接受。
- 量没到瓶颈:很多时候加个索引、改个查询就解决了,别动辄上副本——架构复杂度是债,能不借就不借。
决策顺序:先优化(索引/查询/Schema),再上缓存,再读副本,最后分片。 读副本是优化手段用尽后的水平扩展,不是第一根救命稻草。
七、读副本 vs 缓存
读副本和缓存(Redis/Memcached)都是分摊读压力的手段,但定位完全不同,常被混淆:
| 维度 | 读副本 | 缓存(Redis) |
|---|---|---|
| 覆盖范围 | 全量读(任意查询) | 热点读(特定 key) |
| 延迟 | 毫秒级(等同主库) | 微秒级(纯内存) |
| 数据新鲜度 | 准实时(复制延迟) | 可能过期(靠失效策略) |
| 成本 | 高(整套数据库实例) | 低(内存即可) |
| 复杂度 | 复制 + 路由 | 失效 + 穿透/雪崩防护 |
实战中两者常组合:
读请求 ──> 缓存层(Redis,挡热点读) ──命中──> 直接返回
│ 未命中
▼
读副本(扛全量读) ──> 回填缓存
缓存挡在前面吃掉 80%-95% 的热点读,剩下的长尾读走副本。这样副本数量可以更少,整体更经济。缓存是第一道防线,副本是第二道。 别用副本去做缓存的活——副本能扛全量但贵且重;也别只用缓存不备副本——缓存一旦大面积失效(雪崩),没有副本兜底就直接打穿到主库。
八、跨区域副本:就近读与灾备
单区域副本解决了读压力,但跨地域用户访问仍有物理延迟——亚洲用户访问美东主库,光速就决定了上百毫秒的 RTT。
跨区域副本把数据复制到不同地理区域,服务两个目的:
- 就近读(Geo-local Reads):用户附近区域部署副本,读延迟大幅降低。亚太用户读亚太副本,美东用户读美东副本。
- 异地灾备(Disaster Recovery):主区域整个挂了(机房断电、光缆挖断),跨区域副本可提升为主,保证业务连续性。
┌──────────────────┐
│ 主区域(美东) │ 写入
│ Primary │
└────────┬─────────┘
│ 跨区域复制(通常异步,延迟更大)
┌────────┴────────┐
▼ ▼
┌──────────┐ ┌──────────┐
│ 亚太副本 │ │ 欧洲副本 │ 就近读 + 灾备
└──────────┘ └──────────┘
注意:跨区域复制延迟通常更大(几十到几百毫秒,取决于距离),一致性陷阱更明显。写后强一致读必须回主区域,业务设计要充分考虑——比如用户写完内容后,首页刷新走主区域,即便自己在亚太;列表页可读就近副本,容忍几秒延迟。
小结
- 读副本解决的是读压力瓶颈,通过主从复制把读分摊到多台机器,主库专注写与强一致读。
- 读写分离两层实现:应用层路由(ORM/连接层按语句分发)与代理层路由(ProxySQL/Pgpool 屏蔽细节),实战中常组合使用。
- 异步复制有延迟,写后读不一致是最常见的坑——识别关键路径,把强一致读钉死在主库。
- 副本数量够用就行,不是越多越好;跨区域副本兼顾就近读与异地灾备,但延迟与一致性要重新评估。
- 决策顺序始终是:先优化(索引/查询/Schema),再缓存,再读副本,最后分片。
金句:读副本不是银弹,而是水平扩展读能力的工具——用在对的场景(读多写少、可容忍最终一致)是利器,用错的场景(写为主、强一致要求高、量没到瓶颈)是负担。
Related
相关推荐
文章
Docker 部署 Next.js 社区项目的注意事项
围绕 DevOps、Next.js 的技术趋势、工程实践和独立开发视角。
文章
OpenTelemetry 如何帮助你观察 AI 应用?
围绕 DevOps、AI 的技术趋势、工程实践和独立开发视角。
文章
长运行工具调用:Operation、Poll 与 Cancel 的终态协议
初始请求超时后,工具可能仍在服务端运行;发出取消也不等于副作用已停止。本文为单次长运行工具调用建立 operation 身份、轮询水位、取消竞态和唯一终态接受点。
文章
Agent 记忆删除传播:从用户遗忘请求到可验证清除
删除主表一行不会清掉向量、摘要、缓存和恢复后的备份。本文用删除状态机、代际墓碑、派生血缘与端到端 canary 验证真正遗忘。