主从架构搭好之后,写入走主库,查询走从库——这个想法很自然,但实现起来并没有“一行配置就搞定”那么简单。真正的读写分离方案需要解决两个核心问题:由谁来把请求路由到不同的库? 以及 主从延迟下,写后立刻读怎么保证数据一致?
业内对读写分离通常有两种落地方式:中间件代理和应用层路由。它们各有优劣,选哪个取决于你的团队规模、技术栈和架构复杂度。
19.5.1 中间件代理模式
中间件代理模式的核心思路是:在应用服务器与数据库之间部署一个独立的代理层(Proxy),由它来承接所有数据库请求,解析 SQL,自动将写请求转发给主库、读请求转发给从库。应用本身只连接代理,无需关心后端数据库的拓扑。
典型组件
- ProxySQL:高性能 MySQL 代理,支持读写分离、查询缓存、SQL 防火墙、连接池复用,规则灵活且支持在线重载配置,是目前开源方案中的首选。
- MaxScale:MariaDB 推出的数据库代理,与 MySQL 兼容,功能丰富但用户群体相对小众。
- ShardingSphere-Proxy:Apache 顶级项目,不仅能做分库分表,也原生支持读写分离,配置方便,适合已有 ShardingSphere 体系的环境。
- MyCat:老牌中间件,历史项目使用较多,但社区活跃度有所下降。
工作原理
以 ProxySQL 为例,它的路由规则机制非常清晰:
- 定义后端服务器组:通常
hostgroup 0为主库组,hostgroup 1为从库组。 - 编写查询规则(
mysql_query_rules):通过匹配 SQL 的正则模式或 digest,将SELECT...FOR UPDATE等需要强一致性的读也路由到主库,普通SELECT路由到从库。 - 应用只需连接 ProxySQL 的单一地址,ProxySQL 自动分发请求,同时会周期性地检测后端数据库的健康状态,自动剔除故障节点。
- 它还内置了连接池,可以显著减少应用频繁建立和关闭 MySQL 连接的开销。
优点
- 对应用完全透明:不论你的后端是单库、多从库还是正在做扩容,应用代码一行都不需要改。
- 集中管理路由策略:读写规则的变更只需在代理上操作,对多语言、多微服务项目特别友好。
- 增值功能丰富:查询缓存、SQL 防火墙、连接聚合、慢日志统计等都可以在代理层统一实现,减轻数据库压力。
- 高可用保障:可以通过多 Proxy 实例加虚拟 IP,或者使用云厂商内置的 Proxy 实现自身高可用。
缺点
- 多一层网络跳转:增加 0.5–1ms 的延迟,虽然不大,但对极致性能敏感的场景需要考虑。
- 运维复杂度上升:你需要额外维护 Proxy 集群的部署、升级和监控。
- SQL 解析能力有限:非常复杂或非标准的 SQL 可能被错误路由,需要关注规则匹配的精确性。
- 学习与配置成本:每个中间件都有自己的一套规则体系,团队需要投入时间理解。
配置要点(以 ProxySQL 为例)
一个典型的最小化读写分离配置如下:
-- 添加后端主库和从库
INSERT INTO mysql_servers(hostgroup_id, hostname, port) VALUES (0,'192.168.1.11',3306); -- 主库
INSERT INTO mysql_servers(hostgroup_id, hostname, port) VALUES (1,'192.168.1.12',3306); -- 从库1
INSERT INTO mysql_servers(hostgroup_id, hostname, port) VALUES (1,'192.168.1.13',3306); -- 从库2
-- 创建读写分离规则
INSERT INTO mysql_query_rules(rule_id,active,match_pattern,destination_hostgroup,apply) VALUES
(1,1,'^SELECT.*FOR UPDATE$',0,1), -- 带锁的读强制走主库
(2,1,'^SELECT',1,1); -- 普通SELECT走从库
-- 加载规则并持久化
LOAD MYSQL SERVERS TO RUNTIME; SAVE MYSQL SERVERS TO DISK;
LOAD MYSQL QUERY RULES TO RUNTIME; SAVE MYSQL QUERY RULES TO DISK;
此外,ProxySQL 支持通过 mysql 命令行管理,甚至可以为特定用户或数据库设置更精细的分发逻辑,灵活性很高。
19.5.2 应用层路由模式
应用层路由方案则截然相反:不在中间加任何服务,而是在应用程序代码内部自己管理多数据源,根据业务方法或 SQL 类型决定发往主库还是从库。
典型实现方式
- 框架多数据源抽象:例如 Spring Framework 提供了
AbstractRoutingDataSource,你可以在同一套 Dao 层定义多个数据源(master/slave),并根据当前线程上下文动态选择。 - ShardingSphere-JDBC:作为 SDK 直接嵌入应用,通过配置文件定义读写分离规则,对代码侵入性很低,同时还能实现分库分表。
- 手工维护:直接建立两个独立的数据库连接池,在具体业务方法中显式调用
masterJdbcTemplate或slaveJdbcTemplate。这种方式最原始,但也是最灵活、最直接——问题是容易散落在代码各处,不好统一管控。
工作原理(以 ShardingSphere-JDBC 为例)
在 Spring Boot 项目中引入 shardingsphere-jdbc-core 后,只需在 application.yml 中定义主从数据源和规则:
spring:
shardingsphere:
datasource:
names: master,slave0,slave1
master:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://192.168.1.11:3306/db
username: root
password: xxx
slave0:
...
slave1:
...
rules:
readwrite-splitting:
data-sources:
myds:
type: Static
props:
write-data-source-name: master
read-data-source-names: slave0,slave1
load-balancer-name: round_robin
load-balancers:
round_robin:
type: ROUND_ROBIN
props:
sql-show: true
配置完毕后,普通的 JdbcTemplate 或 DataSource 操作会自动路由:写操作到 master,读操作在 slave0 和 slave1 之间轮询。对于“写后立即读”这类需要强制走主库的场景,可以通过 HintManager 临时指定数据源:
HintManager.getInstance().setWriteRouteOnly();
// 执行查询...
HintManager.close();
优点
- 零额外中间件:部署架构更简单,少一层网络开销,延迟更低。
- 完全自治:开发团队完全掌控路由逻辑,不需要和 DBA 或运维协调中间件配置。
- 易于定制特殊逻辑:某些复杂的只读查询可能希望直接从主库获取最新数据,在代码里加个判断即可。
缺点
- 与代码耦合:如果有很多微服务,每个服务都需要配置和管理多数据源,维护成本高。
- 多语言支持差:Java 有成熟方案,但如果公司同时还有 Python、Go 服务,每个语言群需要自己实现路由逻辑。
- 升级与问题修复不得不发版:配置固化在项目中,变更需要重新走发布流程。
- 一致性处理需要人工干预:开发人员必须清楚哪些场景必须走主库,很容易遗漏。
19.5.3 方案对比与选型建议
| 维度 | 中间件代理 | 应用层路由 |
|------|-----------|-----------|
| 应用侵入性 | 无,完全透明 | 需要集成 SDK 或管理多数据源 |
| 延迟 | 多一跳(通常 0.5-1ms) | 无额外跳转 |
| 运维复杂度 | 需维护 Proxy 集群 | 无需额外服务 |
| 路由灵活性 | 基于 SQL 模式,较统一 | 可在代码级别精细控制 |
| 多语言支持 | 优秀,所有语言复用 | 各语言需自行实现 |
| 扩展功能 | 连接池、缓存、防火墙等 | 需依赖其他组件 |
选型推荐
- 小团队或单体应用:直接用应用层路由(如 ShardingSphere-JDBC)就足够了,部署省事,性能也好。
- 微服务多语言架构:中间件代理更合适,避免每个语言都造轮子。
- 已有强大 DBA/运维团队:ProxySQL 这类专业中间件能统一管控所有数据库流量,方便做监控、限流和审计。
- 追求极致性能:应用层路由少一跳网络开销,但差异通常可以忽略,不必过早优化。
两种方案并不互斥,很多公司内部是混合存在的:核心应用使用应用层路由保证最小延迟,而内部管理平台或非核心服务则通过代理入口,降低代码维护成本。
19.5.4 主从延迟下的读写一致性问题
无论用哪种方案,一个无法回避的现实是:主从复制存在延迟。刚在主库写入的数据,可能还没同步到从库,如果此时立即在从库执行读查询,就会读到旧数据或空结果。比如用户刚下了单,立即跳转到订单详情页,结果 404,这就是典型的主从延迟导致的体验问题。
处理这种问题有两个层面的解决思路:
1. 容忍延迟,业务上补偿
很多业务场景其实可以接受短暂的不一致。比如用户修改个人资料后刷新页面,延迟几百毫秒是可以接受的。对于这类场景,不需要特殊处理,但要确保从库延迟在受控范围内(通常建议 1 秒以内)。
2. 强制读主库(强制走主)
对于写操作后必须立即读取最新数据的场景,需要在代码或框架中指定“这条读请求必须走主库”。实现方式包括:
- 在应用层路由方案中,通过 ThreadLocal 标记或 AOP 切面,在执行完写操作的同一个线程内,短时间内所有的读都走主库。
- 在中间件方案中,利用 Hint 机制,如 ProxySQL 可以通过 SQL 注释
/ master /前缀来标记某条语句走主库。 - 更简单的办法是,在业务方法中将“写入”和“立即读取”放在同一个事务里。因为事务内的读默认走主库(可重复读级别下),自然就解决了问题。但注意事务粒度过大会影响并发性能,要谨慎使用。
无论哪种方案,核心原则是:将强一致性的读操作收敛到可控的少数场景,其余大量查询仍然享受从库扩展的红利。
读写分离是 MySQL 扩展性的最基础手段,正确地选用方案并处理好延迟问题,能让你的数据库层在性能与一致性之间找到合适的平衡。结合下一节高可用架构,你将具备构建一个稳定、可扩展的数据库服务体系的全部基础。