架构--数据库层面--读写分离

之前的文章,对架构层面的知识体系,有一个全面一点的介绍

从nginx做集群

到gateway做集群

到拆分微服务,给jar包做集群

在到缓存做集群

给中间件做集群

给存储系统做集群

在引入k8s

在每个架构领域节点,实现三高

从而实现整体的三高

包括,高性能,高可用,高并发

就是响应快,不会挂,一次能接收大量的数据

这里我们这个文章,介绍一下存储领域,mysql架构层面,实现三高的思路

我们这里讲一下解决这三个问题的,出的解决方案

我们在引入两个概念,读写分离和分库分表

首先说一下,为什么需要读写分离

我们知道单机的mysql性能有瓶颈

于是,我们就可以横向的拓展mysql

增加mysql的主机

又因为,从业务领域来看,查询是mysql的主要业务,

我们限定了一些机器只做查询

进而增加了mysql的高可用,高性能,和高并发

因为增加了机器,从整体上,肯定实现了这个效果

当然,选用中间件,缓存,在不同的场景下,更合适

我们如何实现读写分离:

你提到的"配置好了"可能是指两个 MySQL 的主从复制已经设置完成。这是实现读写分离的数据基础,确保写入主库的数据能同步到从库。

在此基础上,Spring Boot 应用层需要做的,是将"读"和"写"两类 SQL 请求,路由到不同的数据库。这在 Java 生态里有几种成熟的实现方式。

⚙️ 方案一:Spring 原生方案(AbstractRoutingDataSource + AOP)

这是最经典、侵入性较低的方式,通过在应用层动态决定使用哪个数据源。

1. 添加依赖

确保 pom.xml 中有 Spring Boot Starter JDBC 和 MyBatis 相关依赖。

2. 配置数据源 (application.yml)

分别配置主库(写)和从库(读)的连接信息。

yaml 复制代码
spring:
  datasource:
    master:
      driver-class-name: com.mysql.cj.jdbc.Driver
      jdbc-url: jdbc:mysql://<你的写库DockerIP>:3306/<数据库名>?useSSL=false&serverTimezone=UTC
      username: root
      password: <你的写库密码>
    slave:
      driver-class-name: com.mysql.cj.jdbc.Driver
      jdbc-url: jdbc:mysql://<你的读库DockerIP>:3306/<数据库名>?useSSL=false&serverTimezone=UTC
      username: root
      password: <你的读库密码>

注意:如果两个 MySQL 在 Docker 的不同容器中,Spring Boot 应用要通过容器所在宿主机的 IP 和映射端口来访问。

3. 创建数据源上下文 (DataSourceContextHolder)

使用 ThreadLocal 保存当前线程的数据源标识。

java 复制代码
public class DataSourceContextHolder {
    private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();

    public static void setDataSourceType(String dataSourceType) {
        CONTEXT_HOLDER.set(dataSourceType);
    }

    public static String getDataSourceType() {
        return CONTEXT_HOLDER.get();
    }

    public static void clearDataSourceType() {
        CONTEXT_HOLDER.remove();
    }
}

4. 创建动态数据源 (DynamicDataSource)

继承 AbstractRoutingDataSource,实现数据源的动态路由。

java 复制代码
public class DynamicDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceContextHolder.getDataSourceType();
    }
}

5. 配置动态数据源 (DataSourceConfig)

将主从数据源注入到 DynamicDataSource 中,并设为 MyBatis 的主数据源。

java 复制代码
@Configuration
public class DataSourceConfig {

    @Bean(name = "masterDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.master")
    public DataSource masterDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean(name = "slaveDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.slave")
    public DataSource slaveDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean
    @Primary
    public DataSource dynamicDataSource() {
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put("master", masterDataSource());
        targetDataSources.put("slave", slaveDataSource());
        dynamicDataSource.setDefaultTargetDataSource(masterDataSource()); // 默认主库
        dynamicDataSource.setTargetDataSources(targetDataSources);
        return dynamicDataSource;
    }
}

6. 使用 AOP 实现自动切换

通过 AOP 拦截 Mapper 层方法,根据方法名自动切换数据源。

java 复制代码
@Aspect
@Component
public class DataSourceAspect {

    @Before("execution(* com.yourpackage.mapper..*.*(..))")
    public void beforeMethod(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        if (methodName.startsWith("select") || methodName.startsWith("get") 
                || methodName.startsWith("find") || methodName.startsWith("query")) {
            DataSourceContextHolder.setDataSourceType("slave");
        } else {
            DataSourceContextHolder.setDataSourceType("master");
        }
    }

    @After("execution(* com.yourpackage.mapper..*.*(..))")
    public void afterMethod() {
        DataSourceContextHolder.clearDataSourceType();
    }
}

7. (可选) 强制读主库

若某些场景(如刚写完立即查询)必须读主库,可自定义 @Master 注解,并在 AOP 中优先判断。

🚀 方案二:使用专业中间件(ShardingSphere-JDBC)

如果未来有分库分表需求,ShardingSphere-JDBC 是更专业的选择。它是一个轻量级 Java 框架,在应用层透明地提供读写分离等功能。

配置示例 (application.yml):

yaml 复制代码
spring:
  shardingsphere:
    datasource:
      names: master,slave
      master:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://<写库IP>:3306/<数据库名>?useSSL=false
        username: root
        password: <写库密码>
      slave:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://<读库IP>:3306/<数据库名>?useSSL=false
        username: root
        password: <读库密码>
    rules:
      readwrite-splitting:
        data-sources:
          myds:
            type: Static
            props:
              write-data-source-name: master
              read-data-source-names: slave
    props:
      sql-show: true

🏛️ 方案三:使用数据库中间件(如 MyCat, ProxySQL)

这种方式将读写分离逻辑从应用剥离,由独立的中间件服务负责 SQL 路由。应用只连接中间件,由中间件将写 SQL 转发到主库,读 SQL 负载均衡到从库。此方案对应用完全透明,但需要额外部署和维护中间件。

⚠️ 重要注意事项

  • 主从同步延迟 :这是读写分离的经典问题。从库数据同步有延迟,可能导致刚写入的数据读不到。解决方案包括:1) 使用 @Master 注解让关键读操作强制走主库;2) 对于非实时性要求高的查询,可以接受短暂延迟。
  • 事务管理 :在 @Transactional 标注的方法内,应全程使用主库,避免读写分离导致事务内数据不一致。可在事务拦截器中设置数据源为主库。
  • 故障转移:生产环境需考虑从库宕机的情况,应有机制将读请求降级到主库。
  • 数据源配置 :在方案一的 DataSourceConfig 中,@Primary 注解必须加在 dynamicDataSource() 上,确保 MyBatis 使用它作为默认数据源。

💎 总结与建议

  • 快速入门/学习 :推荐方案一 (AbstractRoutingDataSource + AOP),能让你深入理解原理,实现也直接。
  • 生产环境/复杂需求 :推荐方案二 (ShardingSphere-JDBC),它功能强大且对代码侵入小,能更好地应对未来扩展。
  • 大规模/运维主导 :可考虑方案三 (独立中间件),实现应用与数据库的解耦。

另外要强调一下,springboot直接连mycat就可以了,

在mycat的配置文件里做配置,实现主从分离