人人都会AI编程

19.5 读写分离方案:中间件代理、应用层路由

更新时间:2026-07-10

主从架构搭好之后,写入走主库,查询走从库——这个想法很自然,但实现起来并没有“一行配置就搞定”那么简单。真正的读写分离方案需要解决两个核心问题:由谁来把请求路由到不同的库? 以及 主从延迟下,写后立刻读怎么保证数据一致?

业内对读写分离通常有两种落地方式:中间件代理应用层路由。它们各有优劣,选哪个取决于你的团队规模、技术栈和架构复杂度。

19.5.1 中间件代理模式

中间件代理模式的核心思路是:在应用服务器与数据库之间部署一个独立的代理层(Proxy),由它来承接所有数据库请求,解析 SQL,自动将写请求转发给主库、读请求转发给从库。应用本身只连接代理,无需关心后端数据库的拓扑。

典型组件

  • ProxySQL:高性能 MySQL 代理,支持读写分离、查询缓存、SQL 防火墙、连接池复用,规则灵活且支持在线重载配置,是目前开源方案中的首选。
  • MaxScale:MariaDB 推出的数据库代理,与 MySQL 兼容,功能丰富但用户群体相对小众。
  • ShardingSphere-Proxy:Apache 顶级项目,不仅能做分库分表,也原生支持读写分离,配置方便,适合已有 ShardingSphere 体系的环境。
  • MyCat:老牌中间件,历史项目使用较多,但社区活跃度有所下降。

工作原理

以 ProxySQL 为例,它的路由规则机制非常清晰:

  1. 定义后端服务器组:通常 hostgroup 0 为主库组,hostgroup 1 为从库组。
  2. 编写查询规则(mysql_query_rules):通过匹配 SQL 的正则模式或 digest,将 SELECT...FOR UPDATE 等需要强一致性的读也路由到主库,普通 SELECT 路由到从库。
  3. 应用只需连接 ProxySQL 的单一地址,ProxySQL 自动分发请求,同时会周期性地检测后端数据库的健康状态,自动剔除故障节点。
  4. 它还内置了连接池,可以显著减少应用频繁建立和关闭 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 直接嵌入应用,通过配置文件定义读写分离规则,对代码侵入性很低,同时还能实现分库分表。
  • 手工维护:直接建立两个独立的数据库连接池,在具体业务方法中显式调用 masterJdbcTemplateslaveJdbcTemplate。这种方式最原始,但也是最灵活、最直接——问题是容易散落在代码各处,不好统一管控。

工作原理(以 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

配置完毕后,普通的 JdbcTemplateDataSource 操作会自动路由:写操作到 master,读操作在 slave0slave1 之间轮询。对于“写后立即读”这类需要强制走主库的场景,可以通过 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 扩展性的最基础手段,正确地选用方案并处理好延迟问题,能让你的数据库层在性能与一致性之间找到合适的平衡。结合下一节高可用架构,你将具备构建一个稳定、可扩展的数据库服务体系的全部基础。