怎样做读写分离?

访客 性能优化 2

本文目录导读:

  1. 核心逻辑:如何判断“读”和“写”?
  2. 方案一:使用中间件(推荐,对应用透明)
  3. 方案二:在应用层代码中实现(灵活,但侵入性强)
  4. 方案三:ORM 框架或数据库驱动自带功能
  5. 必须解决的四个核心问题
  6. 推荐路线

读写分离是一种常见的数据库优化架构,旨在将查询(读)操作和写入(更新/插入/删除)操作分开到不同的数据库服务器上,以提升系统吞吐量和响应速度。

通常由一主多从的架构实现:主库(Master)负责写从库(Slave)负责读,数据从主库实时或准实时同步到从库。

下面是实现读写分离的完整步骤、技术方案和注意事项。


核心逻辑:如何判断“读”和“写”?

在代码层面,你需要将 SQL 请求动态路由:

  • 写操作(INSERT、UPDATE、DELETE、DDL):强制路由到主库。
  • 读操作(SELECT):默认路由到从库(可以在从库之间做负载均衡)。

使用中间件(推荐,对应用透明)

这是目前最主流、最成熟的做法,应用层不需要修改代码,只需连接中间件,由中间件负责转发请求。

  • 代表产品
    • ShardingSphere-Proxy:Apache 顶级项目,功能强大,不仅支持读写分离,还支持分库分表。
    • MyCat:老牌中间件,配置相对简单。
    • ProxySQL:性能极高,专为 MySQL 设计,支持在线修改配置。
  • 工作模式
    1. 应用层配置数据源指向 Proxy(jdbc:mysql://proxy_ip:3306/db)。
    2. Proxy 内部配置主库和从库的真实地址及同步关系。
    3. Proxy 解析 SQL 语句,判断是读还是写,然后路由到对应的真实数据库。
  • 优点:对开发人员零侵入,迁移成本低,运维统一管理。
  • 缺点:增加了一层网络跳转,会引入少量延迟;中间件本身需要高可用部署。

配置示例(ShardingSphere-Proxy)

# config-sharding.yaml
rules:
  - !READWRITE_SPLITTING
    dataSources:
      readwrite_ds:
        writeDataSourceName: write_ds
        readDataSourceNames:
          - read_ds_0
          - read_ds_1
        loadBalancerName: round_robin
    loadBalancers:
      round_robin:
        type: ROUND_ROBIN

在应用层代码中实现(灵活,但侵入性强)

通过访问数据库的数据源(DataSource)层,手动或通过框架注解切换数据源。

  • 核心思路:创建两个(或多个)数据库连接池,然后通过拦截器或注解动态选择使用哪个连接池。
  • 实现工具
    • Spring 中的 AbstractRoutingDataSource:最常用的 Java 实现方式。
    • TDDL:淘宝开源,Java 框架。
  • 实现步骤(以 Java Spring + MyBatis 为例)
    1. 配置多数据源:配置 masterDataSourceslaveDataSource
    2. 编写路由类:继承 AbstractRoutingDataSource,实现 determineCurrentLookupKey() 方法,该方法的返回值决定了当前线程使用哪个数据源。
    3. 使用 AOP 或注解标记
      • 强制写:默认走主库(@Master 或直接使用 Transactional 注解时强制走主库,因为事务中的一致性要求)。
      • 强制读:使用 @ReadOnly@Slave 注解标记 Service/Controller 方法,AOP 在方法执行前将线程上下文(ThreadLocal)设置为“从库”。
      • 自动判断:也可以在 DAO 层拦截,解析 SQL 前缀(select/update/delete)来自动判断。

简单伪代码(路由类)

public class ReadWriteRoutingDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        // 从 ThreadLocal 中获取当前操作类型:'master' 或 'slave'
        return DbContextHolder.getDbType();
    }
}
  • 优点:灵活,可以针对不同接口做精细控制(用户详情必须读主库保证实时)。
  • 缺点:与框架深度绑定,代码侵入性强,如果有多套系统都需要改。

ORM 框架或数据库驱动自带功能

少数数据库或驱动支持原生读写分离。

  • MySQL Connector/J:MySQL JDBC 驱动支持 replicationConnection,通过 jdbc:mysql:replication://master,slave1,slave2/db 的连接方式,让驱动自动分发读写。
    • 问题:功能较弱,故障转移、负载均衡策略不够完善,现在单独使用较少,多与中间件配合。
  • PHP 的 MySQLiPDO:需要手动封装,不推荐。

必须解决的四个核心问题

架构设计得再好,不考虑下面这些问题也会掉坑:

主从延迟(最头疼的问题)

从库数据同步存在毫秒到秒级别的延迟,如果刚写入一条数据,立刻去从库查询,可能会查不到。

  • 解决方案
    • 强制读主:对实时性要求极高的业务(如:刚创建的订单、支付结果、用户注册),查询时强制路由到主库。
    • 延迟容忍:对不关键的数据(如:历史文章、日志、排行榜),允许短时间的延迟。
    • 中间件策略:某些中间件(如 ShardingSphere)支持 hint基于 SQL 的强制路由
    • 监控报警:实时监控 Seconds_Behind_Master,超过阈值自动告警或切流量。

事务一致性

在一个事务中,如果既有读又有写,那么读操作必须也在主库上执行,否则,事务内的查询可能看不到未提交的修改。

  • 方案事务内的所有请求都走主库,在 Spring 中,只要 @Transactional 生效,数据源通常会被绑定为写库。

连接池与故障转移

  • 当某个从库宕机时,应用不应该报错。
  • 方案:使用成熟的连接池(HikariCP、Druid)并配置健康检查;使用中间件(如 ProxySQL)自带健康检测和自动剔除/恢复。

数据不均衡

如果业务是极端写多读少,或者读压力极大,读写分离后还需要考虑:

  • 写库压力:主库可能仍是瓶颈,需要分库分表。
  • 读库压力:如果从库也扛不住,需要增加从库数量,或引入缓存(Redis)来兜底。

推荐路线

  1. 小规模 / 入门项目:使用 Spring 的 AbstractRoutingDataSource + AOP 注解,简单快速实现。
  2. 中大规模 / 生产环境:特别推荐 ShardingSphere-ProxyProxySQL

    原因:解耦应用、配置灵活、支持平滑扩容、自带负载均衡和故障转移、方便未来做分库分表。

  3. 云原生 / 微服务:很多时候云数据库(如 AWS Aurora、阿里云 RDS)本身已经内置了读写分离和负载均衡,可以直接连接代理地址,无需自己搭建中间件。

一句话建议能上中间件就别在代码里写死,能用云服务就别自己搭建。

标签: 读写分离 数据库架构

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