怎样在数据库层面实现读写分离?

wen IT资讯 240

本文目录导读:

怎样在数据库层面实现读写分离?

  1. 基于数据库中间件(推荐,生产环境最常用)
  2. 基于 ORM / 数据源框架(编程层面,开发友好)
  3. 基于数据库驱动层面(高级客户端)
  4. 负载均衡器(网络层,不推荐纯靠它做读写分离)
  5. 必须警惕的“数据延迟”问题(核心难点)
  6. 总结建议

在数据库层面实现读写分离,核心思想是将查询(SELECT)请求发送到从库将增删改(INSERT/UPDATE/DELETE)请求发送到主库,并保证主从数据最终一致。

这主要在应用层中间件层数据库驱动层实现,而不是在数据库内核本身(MySQL/Oracle 本身不内置“读写分离路由器”)。

以下是几种主流的实现方式,按“对代码侵入性从低到高”排列:

基于数据库中间件(推荐,生产环境最常用)

这是目前大型互联网公司最主流的方案,应用程序只连接中间件,中间件负责解析 SQL 并自动路由。

  • 典型产品:

    • MySQL: Mycat, ShardingSphere-Proxy, ProxySQL, Vitess
    • PostgreSQL: Pgpool-II, Citus
    • 通用: MaxScale (MariaDB), Atalas(360开源)
  • 工作流程:

    1. App 连接中间件(如 Mycat)。
    2. App 发送 SQL,SELECT * FROM userUPDATE user SET name=‘xx’
    3. 中间件解析 SQL 语句,判断是读还是写。
    4. 读语句 -> 随机或轮询发送给 从库
    5. 写语句 -> 强制发送给 主库
    6. 主从数据通过 binlog 同步。
  • 优点:

    • 对应用透明,应用不需要改任何代码,就像连一个普通数据库一样。
    • 容易做负载均衡、健康检查、主从切换。
  • 缺点:

    • 需要多维护一套中间件系统,增加架构复杂度和运维成本。
    • 多一层网络跳转,增加毫秒级延迟。

基于 ORM / 数据源框架(编程层面,开发友好)

在 Java、Python 等应用代码中,通过框架配置多个数据源(DataSource),并在运行期间根据方法或注解动态切换。

  • 典型实现:

    • Spring + MyBatis(Java): 使用 AbstractRoutingDataSource + AOP(面向切面编程)。
    • Django(Python): 使用 DATABASE_ROUTERS 配置。
    • Ruby on Rails: 原生支持 establish_connectionoctopus 插件。
  • 示例(Spring 实现思路):

    1. 配置数据源: 在 Spring 中配置两个 DataSource,一个指向主库,一个指向从库。
    2. 定义注解: 定义一个 @ReadOnly 注解。
    3. AOP 拦截: 写一个切面,拦截 Service 层方法。
      • 如果方法上有 @ReadOnly 或者方法名以 get/find/select 开头 -> 动态切换到从库 DataSource。
      • 其他(默认)-> 使用主库 DataSource。
    4. 事务处理: 需要特别注意:如果事务方法内有写操作再跟读操作,读操作也应该在主库执行,否则会读到旧数据(因为从库同步有延迟),所以通常保证同一事务内所有操作在同一个主库连接中。
  • 优点:

    • 不需要额外部署中间件,成本低。
    • 架构简单,适合中小型项目。
  • 缺点:

    • 对代码有一定侵入(需要加注解或按命名规范)。
    • 应用线程必须感知多数据源状态,处理不当容易导致连接泄漏。
    • 强依赖语言框架(如换语言或框架需重写)。

基于数据库驱动层面(高级客户端)

某些数据库的高级客户端库支持内部自动读写分离,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 或处于事务中 -> 路由到主库。
  • 优点: 几乎零配置,驱动级别实现。

  • 缺点:

    • 功能相对简单,不能做复杂的负载均衡策略(如权重、延迟感知)。
    • 只支持特定数据库的特定驱动版本。

负载均衡器(网络层,不推荐纯靠它做读写分离)

可以使用 HAProxyLVSNginx 的 Stream 模块将流量分发到主从库。

  • 做法: 将读写分离的规则写在 L4/L7 负载均衡器中。
  • 问题: 负载均衡器是“四层”设备,不认识 SQL 语句,它不是将 SELECT 发往从库,而是随机分发。
  • 唯一可行的变体: 端口分离
    • 主库监听 3306 端口,从库监听 3307 端口。
    • 应用程序配置两个端口:写连接走 3306,读连接走 3307,负载均衡器只做转发。
  • 严格来说这不算是“数据库层面”,而是网络层面的端口分流。

必须警惕的“数据延迟”问题(核心难点)

无论采用哪种方式,读写分离的最大坑是主从同步延迟

场景: 用户注册后跳转,页面需要立即显示用户信息。

  1. 主库: INSERT INTO user VALUES (‘xxx’,...) -> 成功。
  2. 从库: 正在同步 binlog,延迟 100ms。
  3. 应用: 查询 SELECT * FROM user WHERE name='xxx'
  4. 结果: 路由到了从库,查不到

解决方案:

  1. 强制读主库:
    • 在需要实时一致性的请求参数上打标记(如:req.setAttribute(‘forceMaster’, true))。
    • AOP 或中间件检测到该标记,强制路由到主库读。
  2. 业务容忍:

    “刚刚创建的数据不显示”对业务无影响(如日志、评论审核)。

  3. 中间件延迟感知:
    • 如 ProxySQL 的 mysql_query_rules 可以检测从库的 Seconds_Behind_Master,如果超过阈值(如 5秒),自动降级,不再路由 SELECT 到该从库。

总结建议

你的项目规模 推荐方案 原因
中小项目 (< 500 QPS) ORM 框架(AbstractRoutingDataSource) 最简单,无需额外运维组件,开发成本低。
中型项目 (500-5000 QPS) 数据库中间件(如 ProxySQL, Mycat) 需要动态扩容、灰度发布、健康检查、性能更好,对应用透明。
大型/云原生项目 云数据库 + 数据库代理(如 RDS Proxy) 云厂商自带的代理有自动读写分离、连接池、故障转移,运维成本几乎为零。

一句话总结:

  • 代码层面用框架做(开发时期);
  • 架构层面用中间件做(运维时期);
  • 关键原则:写操作必须走主库;读操作默认走从库,但强一致性读必须走主库

抱歉,评论功能暂时关闭!