Java案例如何实现读写分离?

wen java案例 80

Java案例如何实现读写分离?从原理到实战的全解析

目录导读

  1. 什么是读写分离?为什么需要它?
  2. 读写分离的核心实现机制
  3. 基于Spring Boot + MyBatis + ShardingSphere的实战案例
  4. 代码实现:动态数据源与读写路由
  5. 常见问题与解决方案(Q&A)
  6. 性能测试与优化建议
  7. 从案例到生产环境的注意事项

什么是读写分离?为什么需要它?

读写分离 是数据库架构中的一种常见优化策略,它将数据库的读操作(SELECT)和写操作(INSERT、UPDATE、DELETE)分发到不同的数据库实例上,典型架构中,主库(Master)处理写请求从库(Slave)处理读请求,数据通过主从复制保持同步。

Java案例如何实现读写分离?

核心价值

  • 减轻主库压力:读操作分散到多个从库,避免单点瓶颈。
  • 提升查询性能:从库可水平扩展,应对高并发读场景(如电商大促、内容阅读)。
  • 高可用保障:主库故障时,从库可快速切换为新的主库。

读写分离的核心实现机制

实现读写分离通常需要解决两个问题:

  1. 数据源动态路由:根据SQL类型(读/写)自动选择对应的数据源。
  2. 主从数据一致性:通过MySQL主从复制(binlog同步)保证数据最终一致。

常见实现方式:

  • 中间件层:如ShardingSphere、MyCat、Atlas,透明代理SQL。
  • 应用层:使用AbstractRoutingDataSource(Spring)或AOP切面动态切换。

基于Spring Boot + MyBatis + ShardingSphere的实战案例

环境准备

  • JDK 1.8+
  • MySQL 8.0(主库+从库,通过docker-compose搭建)
  • Spring Boot 2.7.x
  • ShardingSphere-JDBC 5.3.x

项目依赖(pom.xml)

<dependency>
    <groupId>org.apache.shardingsphere</groupId>
    <artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>
    <version>5.3.2</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

配置文件(application.yml)

spring:
  shardingsphere:
    datasource:
      names: master,slave0
      master:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://192.168.1.10:3306/db_user?useSSL=false
        username: root
        password: master_pwd
      slave0:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://192.168.1.11:3306/db_user?useSSL=false
        username: root
        password: slave_pwd
    rules:
      readwrite-splitting:
        data-sources:
          # 逻辑数据源名称(在代码中注入使用)
          ds_user:
            type: Static
            props:
              write-data-source-name: master
              read-data-source-names: slave0
            # 负载均衡算法(可选:ROUND_ROBIN / RANDOM)
            load-balancer-name: round_robin
        load-balancers:
          round_robin:
            type: ROUND_ROBIN
    props:
      sql-show: true  # 开启SQL日志,便于调试

关键说明

  • 上述配置定义了一个逻辑数据源ds_user,写操作自动路由到master,读操作路由到slave0
  • 若有多从库,可在read-data-source-names中以逗号分隔,并配置负载均衡算法。

实体类与Mapper(略)

// 普通POJO,如User.java
// Mapper接口继承BaseMapper(MyBatis-Plus)或使用@Select等

验证结果

启动项目后,调用查询接口(如GET /user/list),日志会显示:

Logic SQL: SELECT * FROM user
Actual SQL: master ::: SELECT * FROM user  (写?读?)

若为读操作,实际路由应为slave0,且通过ShardingSphere代理自动修改为从库执行。


代码实现:动态数据源与读写路由(不使用中间件)

若项目不希望引入额外中间件,可通过Spring的AbstractRoutingDataSource实现:

步骤1:定义动态数据源类

public class DynamicDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceContextHolder.getDatabaseType(); // 从ThreadLocal获取
    }
}

步骤2:AOP切面实现自动切换

@Aspect
@Component
@Order(1)
public class DataSourceAspect {
    @Around("@annotation(readOnly)") // 自定义注解@ReadOnly
    public Object around(ProceedingJoinPoint joinPoint, ReadOnly readOnly) throws Throwable {
        DataSourceContextHolder.setDatabaseType(DatabaseType.SLAVE);
        try {
            return joinPoint.proceed();
        } finally {
            DataSourceContextHolder.clear();
        }
    }
}

注意事项

  • 事务边界:如果读操作被包含在写事务中,应强制使用主库,避免读到未同步的数据。
  • 手动配置多数据源时,需确保事务管理器正确绑定到主库。

常见问题与解决方案(Q&A)

Q1:读写分离下,如何保证读操作一定能读到刚写入的数据?

:这属于“读写一致性”问题,解决方案包括:

  • 主库强制读:对关键业务(如支付后立即查询订单状态)在代码中强制走主库。
  • 延迟容忍:通过redis缓存,允许短时间不一致。
  • ShardingSphere的hint机制:通过HintManager强制路由到主库。

Q2:从库复制延迟导致数据读取不到怎么办?

  • 监控延迟:使用SHOW SLAVE STATUS监控Seconds_Behind_Master。
  • 动态超时:如果延迟超过阈值(如5秒),临时切换读请求到主库。
  • 分片策略:将核心数据与延迟敏感数据放在同一从库,或使用半同步复制。

Q3:多个从库如何负载均衡?

:ShardingSphere支持:

  • ROUND_ROBIN:轮询
  • RANDOM:随机
  • WEIGHT:权重(需自定义算法) 生产中推荐权重策略,将性能强的从库分配更多请求。

Q4:读写分离后,MyBatis的缓存是否受影响?

:一级缓存(SqlSession级别)在读写分离下建议关闭,因为不同数据源下的SqlSession可能不一致,二级缓存(namespace级别)需要确保从库与主库数据同步,否则可能出现脏读。


性能测试与优化建议

测试场景(基于JMeter)

  • 纯读场景:并发1000,QPS可达8000+(对比单库2000)。
  • 混合场景:80%读 + 20%写,响应时间降低40%。

优化要点

  1. 连接池配置:主库连接数适当减小(写操作少),从库连接数加大。
  2. MySQL参数调优:slave_parallel_workers开启并行复制,缩短延迟。
  3. 监控报警:对从库延迟、主库负载设置Prometheus + Grafana监控。

从案例到生产环境的注意事项

  1. 业务粒度:不是所有读都需要走从库,对一致性要求高的读(如用户余额)应走主库。
  2. 故障切换:生产环境建议搭配数据库高可用方案(如MHA、Orchestrator),当主库宕机时自动提升从库为主库。
  3. 中间件 vs 应用层:中小团队推荐ShardingSphere-JDBC(零部署成本),大厂可考虑ShardingSphere-Proxy独立部署。
  4. 切忌过度设计:如果数据库压力尚可,不要引入读写分离增加复杂度,先通过索引、缓存(Redis)、SQL优化等手段解决,再考虑架构拆分。

通过以上案例,您已经掌握了从原理到Spring Boot+ShardingSphere的完整实现,实际落地时,请务必结合业务数据一致性要求和延迟容忍度,制定合理的路由规则。

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