本文目录导读:

- 基于数据库中间件(推荐,生产环境最常用)
- 基于 ORM / 数据源框架(编程层面,开发友好)
- 基于数据库驱动层面(高级客户端)
- 负载均衡器(网络层,不推荐纯靠它做读写分离)
- 必须警惕的“数据延迟”问题(核心难点)
- 总结建议
在数据库层面实现读写分离,核心思想是将查询(SELECT)请求发送到从库,将增删改(INSERT/UPDATE/DELETE)请求发送到主库,并保证主从数据最终一致。
这主要在应用层、中间件层或数据库驱动层实现,而不是在数据库内核本身(MySQL/Oracle 本身不内置“读写分离路由器”)。
以下是几种主流的实现方式,按“对代码侵入性从低到高”排列:
基于数据库中间件(推荐,生产环境最常用)
这是目前大型互联网公司最主流的方案,应用程序只连接中间件,中间件负责解析 SQL 并自动路由。
-
典型产品:
- MySQL: Mycat, ShardingSphere-Proxy, ProxySQL, Vitess
- PostgreSQL: Pgpool-II, Citus
- 通用: MaxScale (MariaDB), Atalas(360开源)
-
工作流程:
- App 连接中间件(如 Mycat)。
- App 发送 SQL,
SELECT * FROM user或UPDATE user SET name=‘xx’。 - 中间件解析 SQL 语句,判断是读还是写。
- 读语句 -> 随机或轮询发送给 从库。
- 写语句 -> 强制发送给 主库。
- 主从数据通过 binlog 同步。
-
优点:
- 对应用透明,应用不需要改任何代码,就像连一个普通数据库一样。
- 容易做负载均衡、健康检查、主从切换。
-
缺点:
- 需要多维护一套中间件系统,增加架构复杂度和运维成本。
- 多一层网络跳转,增加毫秒级延迟。
基于 ORM / 数据源框架(编程层面,开发友好)
在 Java、Python 等应用代码中,通过框架配置多个数据源(DataSource),并在运行期间根据方法或注解动态切换。
-
典型实现:
- Spring + MyBatis(Java): 使用
AbstractRoutingDataSource+ AOP(面向切面编程)。 - Django(Python): 使用
DATABASE_ROUTERS配置。 - Ruby on Rails: 原生支持
establish_connection或octopus插件。
- Spring + MyBatis(Java): 使用
-
示例(Spring 实现思路):
- 配置数据源: 在 Spring 中配置两个
DataSource,一个指向主库,一个指向从库。 - 定义注解: 定义一个
@ReadOnly注解。 - AOP 拦截: 写一个切面,拦截 Service 层方法。
- 如果方法上有
@ReadOnly或者方法名以get/find/select开头 -> 动态切换到从库 DataSource。 - 其他(默认)-> 使用主库 DataSource。
- 如果方法上有
- 事务处理: 需要特别注意:如果事务方法内有写操作再跟读操作,读操作也应该在主库执行,否则会读到旧数据(因为从库同步有延迟),所以通常保证同一事务内所有操作在同一个主库连接中。
- 配置数据源: 在 Spring 中配置两个
-
优点:
- 不需要额外部署中间件,成本低。
- 架构简单,适合中小型项目。
-
缺点:
- 对代码有一定侵入(需要加注解或按命名规范)。
- 应用线程必须感知多数据源状态,处理不当容易导致连接泄漏。
- 强依赖语言框架(如换语言或框架需重写)。
基于数据库驱动层面(高级客户端)
某些数据库的高级客户端库支持内部自动读写分离,MySQL 的 Connector/J (JDBC) 或 PHP 的 mysqlnd。
-
MySQL Connector/J (8.0+) 示例(MySQL Replication Connection): 在 JDBC 连接 URL 中配置:
jdbc:mysql:replication://master:3306,slave1:3306,slave2:3306/test驱动会自动判断:autoCommit=true且是SELECT语句 -> 路由到从库。- 非 SELECT 或处于事务中 -> 路由到主库。
-
优点: 几乎零配置,驱动级别实现。
-
缺点:
- 功能相对简单,不能做复杂的负载均衡策略(如权重、延迟感知)。
- 只支持特定数据库的特定驱动版本。
负载均衡器(网络层,不推荐纯靠它做读写分离)
可以使用 HAProxy、LVS 或 Nginx 的 Stream 模块将流量分发到主从库。
- 做法: 将读写分离的规则写在 L4/L7 负载均衡器中。
- 问题: 负载均衡器是“四层”设备,不认识 SQL 语句,它不是将 SELECT 发往从库,而是随机分发。
- 唯一可行的变体: 端口分离。
- 主库监听 3306 端口,从库监听 3307 端口。
- 应用程序配置两个端口:写连接走 3306,读连接走 3307,负载均衡器只做转发。
- 严格来说这不算是“数据库层面”,而是网络层面的端口分流。
必须警惕的“数据延迟”问题(核心难点)
无论采用哪种方式,读写分离的最大坑是主从同步延迟。
场景: 用户注册后跳转,页面需要立即显示用户信息。
- 主库:
INSERT INTO user VALUES (‘xxx’,...)-> 成功。 - 从库: 正在同步 binlog,延迟 100ms。
- 应用: 查询
SELECT * FROM user WHERE name='xxx'。 - 结果: 路由到了从库,查不到。
解决方案:
- 强制读主库:
- 在需要实时一致性的请求参数上打标记(如:
req.setAttribute(‘forceMaster’, true))。 - AOP 或中间件检测到该标记,强制路由到主库读。
- 在需要实时一致性的请求参数上打标记(如:
- 业务容忍:
“刚刚创建的数据不显示”对业务无影响(如日志、评论审核)。
- 中间件延迟感知:
- 如 ProxySQL 的
mysql_query_rules可以检测从库的Seconds_Behind_Master,如果超过阈值(如 5秒),自动降级,不再路由 SELECT 到该从库。
- 如 ProxySQL 的
总结建议
| 你的项目规模 | 推荐方案 | 原因 |
|---|---|---|
| 中小项目 (< 500 QPS) | ORM 框架(AbstractRoutingDataSource) | 最简单,无需额外运维组件,开发成本低。 |
| 中型项目 (500-5000 QPS) | 数据库中间件(如 ProxySQL, Mycat) | 需要动态扩容、灰度发布、健康检查、性能更好,对应用透明。 |
| 大型/云原生项目 | 云数据库 + 数据库代理(如 RDS Proxy) | 云厂商自带的代理有自动读写分离、连接池、故障转移,运维成本几乎为零。 |
一句话总结:
- 代码层面用框架做(开发时期);
- 架构层面用中间件做(运维时期);
- 关键原则:写操作必须走主库;读操作默认走从库,但强一致性读必须走主库。