java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > SpringBoot MySQL读写分离

SpringBoot实现MySQL读写分离的生产级实战指南

作者:hdsoft_huge

还在为SpringBoot多数据源冲突、事务失效、线程串库而头疼,本文从底层自动配置原理出发,手把手带你实现生产级动态数据源读写分离架构,基于AOP和AbstractRoutingDataSource,零侵入代码,彻底解决线程安全与事务兼容问题,并覆盖全部高频踩坑点,可直接投产

本文独家核心收获

一、前言:为什么生产环境必须做读写分离?

在高并发、大数据量互联网架构中,单库单数据源架构是性能最大瓶颈。所有增删改查、统计报表、分页查询全部挤压同一数据库,极易出现:数据库CPU打满、写入阻塞、查询超时、事务堆积、线上雪崩等严重问题。

因此,MySQL主从 + 读写分离 是企业后端第一道性能优化刚需方案,也是中高级开发、架构师必备核心能力。

1.1 传统单库架构三大致命痛点

1.2 生产标准架构:主从分离设计

1.3 市面教程普遍存在的问题

网上多数据源教程只能跑Demo,无法上生产

本文优势:全套生产级闭环方案,经过线上验证、线程安全、事务兼容、可直接部署投产。

二、核心原理深度剖析:SpringBoot数据源底层机制

2.1 SpringBoot默认单数据源自动配置流程

SpringBoot 通过 DataSourceAutoConfiguration 实现零配置数据源,核心流程:

  1. 自动扫描Classpath数据库驱动包
  2. 读取 spring.datasource配置
  3. 自动创建 DataSourceSqlSessionFactoryTransactionManager
  4. 单例Bean注入全局,所有SQL共用一个数据源

本质问题:SpringBoot默认是单数据源强绑定设计,原生不支持多数据源动态切换。

2.2 多数据源引入的三大核心冲突

1. Bean歧义注入冲突

多个DataSource同时注册到Spring容器,自动装配无法识别优先数据源,直接启动报错。

2. 事务管理器绑定固化

Spring事务AOP优先于数据源AOP,一旦开启事务,数据源永久固定,无法动态切换。

3. 无原生路由机制

框架无法自动区分读写SQL,需要手动实现动态路由分发逻辑。

2.3 主流读写分离方案生产选型对比

方案类型代表技术适用场景优势劣势
应用层AOP自研本文动态数据源中小项目、纯读写分离、轻量架构零中间件、无侵入、性能高、可控性强无复杂分片能力,需自研代码
客户端中间件Sharding-JDBC读写分离+分库分表、海量数据功能全面、生态成熟有学习成本、版本兼容问题多
代理层中间件MyCat、ProxySQL大规模集群、多库复杂分片统一代理、业务无感知部署复杂、有性能损耗、运维成本高

本文选型结论:绝大多数企业业务系统,AOP动态数据源方案性价比最高,轻量稳定、零运维、零入侵。

三、生产级环境依赖与配置

3.1 完整Maven依赖(可直接复制投产)

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.15</version>
        <relativePath/>
    </parent>
    <dependencies>
        <!-- Web核心 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- JDBC数据库核心 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <!-- AOP动态切换核心依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <!-- MyBatis -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.3.0</version>
        </dependency>
        <!-- MySQL8驱动 -->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>
        <!-- 生产级Druid连接池 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.2.16</version>
        </dependency>
        <!-- 测试 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>

3.2 生产级YAML多数据源配置(主从分离+连接池优化)

spring:
  # 关键:关闭默认数据源自动配置,彻底解决Bean冲突
  autoconfigure:
    exclude:
      - com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure
      - org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
  datasource:
    # 主库(写库)配置
    master:
      url: jdbc:mysql://127.0.0.1:3306/db_master?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&allowMultiQueries=true&rewriteBatchedStatements=true
      username: root
      password: 123456
      driver-class-name: com.mysql.cj.jdbc.Driver
      druid:
        initial-size: 5
        min-idle: 5
        max-active: 20
        max-wait: 60000
        time-between-eviction-runs-millis: 60000
        min-evictable-idle-time-millis: 300000
        validation-query: SELECT 1
        test-while-idle: true
        test-on-borrow: false
        test-on-return: false
        pool-prepared-statements: true
        filters: stat,wall,slf4j
    # 从库(读库)配置
    slave:
      url: jdbc:mysql://127.0.0.1:3306/db_slave?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&allowMultiQueries=true&rewriteBatchedStatements=true
      username: root
      password: 123456
      driver-class-name: com.mysql.cj.jdbc.Driver
      druid:
        initial-size: 10
        min-idle: 10
        max-active: 50
        max-wait: 30000
        time-between-eviction-runs-millis: 60000
        min-evictable-idle-time-millis: 300000
        validation-query: SELECT 1
        test-while-idle: true
        test-on-borrow: false
# MyBatis生产配置
mybatis:
  mapper-locations: classpath:mapper/**/*.xml
  type-aliases-package: com.example.entity
  configuration:
    map-underscore-to-camel-case: true
    cache-enabled: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

核心配置要点

  1. 必须关闭默认自动配置,否则启动直接Bean冲突
  2. 从库连接数大于主库,适配海量查询场景
  3. 开启批量SQL优化、心跳检测、防SQL注入过滤器

四、核心架构手写实现(生产级、零BUG、线程安全)

4.1 数据源枚举:统一路由标识

类型安全管理所有数据源,杜绝字符串硬编码

public enum DataSourceType {
    /**
     * 主库:写操作、事务、强一致性查询
     */
    MASTER,
    /**
     * 从库:纯查询、报表、统计
     */
    SLAVE
}

4.2 数据源上下文持有者(ThreadLocal线程隔离)

解决多线程串库、线程池复用残留、内存泄漏三大生产难题

public class DataSourceContextHolder {

    private static final ThreadLocal<DataSourceType> CONTEXT = new ThreadLocal<>();

    /**
     * 设置当前线程数据源
     */
    public static void set(DataSourceType type) {
        CONTEXT.set(type);
    }

    /**
     * 获取当前数据源,默认主库(保证写安全)
     */
    public static DataSourceType get() {
        return CONTEXT.get() == null ? DataSourceType.MASTER : CONTEXT.get();
    }

    /**
     * 清空上下文,必须finally执行
     */
    public static void clear() {
        CONTEXT.remove();
    }
}

4.3 动态路由数据源(Spring核心扩展类)

基于 AbstractRoutingDataSource 实现动态路由,Spring官方标准扩展方式

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

public class DynamicDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        // 从线程上下文获取当前数据源key
        return DataSourceContextHolder.get().name();
    }
}

4.4 AOP切面:全自动读写路由(业务零侵入)

通过 Mapper 方法名自动判断读写,无需手动注解

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class DataSourceAspect {

    // 拦截所有mapper层方法
    @Pointcut("execution(* com.example.mapper.*.*(..))")
    public void mapperPoint(){}

    @Around("mapperPoint()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        String methodName = point.getSignature().getName();
        try {
            // 查询类方法走从库
            if (methodName.startsWith("select") || methodName.startsWith("get")
                    || methodName.startsWith("list") || methodName.startsWith("count")
                    || methodName.startsWith("query") || methodName.startsWith("find")) {
                DataSourceContextHolder.set(DataSourceType.SLAVE);
            } else {
                // 增删改默认主库
                DataSourceContextHolder.set(DataSourceType.MASTER);
            }
            return point.proceed();
        } finally {
            // 强制清除,杜绝线程池串库、内存泄漏
            DataSourceContextHolder.clear();
        }
    }
}

4.5 多数据源核心配置类(完整Bean注册+事务兼容)

import com.alibaba.druid.pool.DruidDataSource;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

@Configuration
@EnableTransactionManagement
@MapperScan("com.example.mapper")
public class DataSourceConfig {

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.master")
    public DataSource masterDataSource() {
        return new DruidDataSource();
    }

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.slave")
    public DataSource slaveDataSource() {
        return new DruidDataSource();
    }

    /**
     * 核心:动态数据源,@Primary解决Bean冲突
     */
    @Bean
    @Primary
    public DataSource dynamicDataSource() {
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        Map<Object, Object> dataSourceMap = new HashMap<>();

        dataSourceMap.put(DataSourceType.MASTER.name(), masterDataSource());
        dataSourceMap.put(DataSourceType.SLAVE.name(), slaveDataSource());

        // 默认主库
        dynamicDataSource.setDefaultTargetDataSource(masterDataSource());
        dynamicDataSource.setTargetDataSources(dataSourceMap);
        return dynamicDataSource;
    }

    /**
     * 绑定动态数据源的SqlSessionFactory
     */
    @Bean
    public SqlSessionFactoryBean sqlSessionFactory() throws Exception {
        SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
        factory.setDataSource(dynamicDataSource());
        factory.setMapperLocations(new PathMatchingResourcePatternResolver()
                .getResources("classpath:mapper/**/*.xml"));
        return factory;
    }

    /**
     * 事务管理器:绑定动态数据源,保证事务生效
     */
    @Bean
    public PlatformTransactionManager transactionManager() {
        return new DataSourceTransactionManager(dynamicDataSource());
    }
}

五、业务使用规范 & 生产调用示例

5.1 写操作(新增/修改/删除/事务)

自动走主库,无需手动干预,保证数据一致性

@Service
public class UserService {

    @Transactional(rollbackFor = Exception.class)
    public void addUser(User user){
        userMapper.insert(user);
    }

    @Transactional(rollbackFor = Exception.class)
    public void updateUser(User user){
        userMapper.updateById(user);
    }
}

5.2 读操作(查询、列表、统计)

自动走从库,分担主库压力

@Service
public class UserService {
    public List<User> list(){
        return userMapper.selectList(null);
    }

    public User getById(Long id){
        return userMapper.selectById(id);
    }
}

六、生产高频踩坑全集 & 终极解决方案

6.1 坑点1:事务中数据源切换失效(面试最高频)

根因:事务AOP优先级高于数据源AOP,事务开启瞬间数据源已固定。

生产规范所有带@Transactional方法强制走主库,查询逻辑抽离无事务方法。

6.2 坑点2:线程池复用导致串库、数据错乱

根因:ThreadLocal残留旧线程数据源标识。

解决方案:本文代码已自带 finally clear(),彻底杜绝该问题。

6.3 坑点3:启动报错Bean冲突

解决方案:必须排除自动配置 + 动态数据源添加 @Primary

6.4 坑点4:主从延迟导致刚写入查不到数据

解决方案:写后即时查询、强一致性查询强制走主库

七、大厂面试高阶问答(满分标准答案)

7.1 基础面试

Q:读写分离的核心作用?

A:读写流量拆分,分担主库查询压力,解决高并发查询导致的数据库CPU、IO打满,保障核心写入事务稳定。

Q:动态数据源核心原理?

A:继承Spring AbstractRoutingDataSource,结合ThreadLocal线程级存储、AOP切面拦截,实现运行时动态数据源路由。

7.2 进阶面试

Q:为什么事务内无法切换数据源?

A:Spring事务AOP优先级更高,事务开启时数据源已绑定,整个事务周期数据源不可变更。

Q:自研动态数据源和Sharding-JDBC怎么选型?

A:仅做读写分离、轻量架构选自研AOP方案;需要分库分表、分片规则、复杂路由选Sharding-JDBC。

八、全文总结

本文完整实现了企业生产级 SpringBoot 读写分离 + 动态多数据源架构,彻底解决全网Demo级代码的各类线上BUG:

读写分离是后端性能优化的核心基础能力,吃透本文可轻松搞定日常开发、性能优化、大厂面试高频考点。

以上就是SpringBoot实现MySQL读写分离的生产级实战指南的详细内容,更多关于SpringBoot MySQL读写分离的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文