SpringBoot3多数据源深度实践:从选型到避坑,一文搞定

在后端开发中,随着业务复杂度的提升,单一数据源早已无法满足实际需求——电商系统的订单库与用户库拆分、数据归档场景的业务库与历史库联动、高并发场景的读写分离,甚至是多类型数据库(MySQL+ClickHouse)的整合,都离不开多数据源的支持。SpringBoot3作为目前主流的后端开发框架,基于Jakarta EE 9规范进行了重大升级,其多数据源的实现方式与SpringBoot2.x存在关键差异,本文将从核心原理、主流方案、实操落地到避坑优化,带你完整掌握SpringBoot3多数据源的应用,助力开发者高效应对复杂业务场景。

一、SpringBoot3多数据源核心差异(与2.x对比)

很多开发者升级SpringBoot3后,会遇到多数据源配置启动失败(如“url attribute is not specified”)的问题,核心原因是SpringBoot3的底层规范迁移,与2.x存在两大关键差异,也是后续配置的核心前提:

  1. 依赖包命名空间迁移:SpringBoot3基于Jakarta EE 9规范,将原有javax.*包迁移为jakarta.*包,例如javax.sql.DataSource迁移为jakarta.sql.DataSource。这导致SpringBoot2.x中常用的dynamic-datasource-spring-boot-starter(4.x版本)无法兼容,需使用专门适配SpringBoot3的starter依赖。

  2. JDK版本要求提升:SpringBoot3最低要求JDK17+(SpringBoot3.1支持JDK20,3.2支持JDK21),而SpringBoot2.x最低支持JDK8,这也导致部分旧版数据源相关依赖(如低版本Druid)需要同步升级适配。

核心提醒:SpringBoot3多数据源配置的核心是“适配Jakarta规范+选择正确的依赖版本”,避免因版本不兼容导致配置失效。

二、多数据源核心概念与应用场景

2.1 核心概念

多数据源:指一个SpringBoot应用中,同时配置并操作两个及以上的数据源(可同为关系型数据库,如MySQL+Oracle;也可异构,如MySQL+Redis+ClickHouse)。其核心是数据源隔离动态切换,确保不同业务逻辑能精准操作对应数据源,同时避免数据源冲突。

2.2 核心应用场景

多数据源并非“炫技”,而是为了解决单一数据源无法覆盖的业务痛点,核心应用场景分为4类:

  • 业务拆分:大型系统按业务模块拆分数据库(如电商的订单库、用户库、商品库),降低单库压力,提升系统扩展性和维护性。

  • 读写分离:主库负责写入操作(新增/修改/删除),从库负责查询操作,通过负载均衡分散压力,解决高并发下的查询性能瓶颈。

  • 数据归档/同步:历史数据归档场景中,需同时操作“业务库(源库)”和“历史库(目标库)”,实现数据迁移与同步。

  • 多类型数据源整合:系统需同时连接关系型数据库(MySQL)、非关系型数据库(Redis)、数据仓库(ClickHouse)等,实现数据联动(如用户数据查询+统计分析)。

三、SpringBoot3多数据源3种主流实现方案(实操落地)

SpringBoot3生态下,多数据源有3种主流实现方案,需根据业务复杂度、数据源数量、是否需要动态切换选型。以下将结合实操,详细讲解每种方案的原理、步骤、优缺点,技术栈统一采用“SpringBoot3.2 + MyBatis-Plus + MySQL + Druid连接池”,确保代码可直接复用。

方案1:多DataSource Bean分包实现(固定数据源,无动态切换)

3.1.1 核心原理

为每个数据源配置独立的DataSourceSqlSessionFactoryMapperScannerConfigurer,通过包路径区分数据源——不同数据源的Mapper接口放在不同包下,SpringBoot自动扫描对应包,关联对应的数据源,实现数据源隔离。

3.1.2 实操步骤
步骤1:引入核心依赖(pom.xml)

注意:Druid需使用适配SpringBoot3的版本,避免依赖冲突。

<!-- SpringBoot核心依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- MyBatis-Plus(简化CRUD) -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.5.1</version>
</dependency>
<!-- MySQL驱动 -->
<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <scope>runtime</scope>
</dependency>
<!-- Druid连接池(适配SpringBoot3) -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-3-starter</artifactId>
    <version>1.2.20</version>
</dependency>
<!-- Lombok(简化实体类) -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>
<!-- 测试依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
步骤2:配置多数据源信息(application.yml)

配置两个数据源:业务库(business)和历史库(history),指定连接信息、连接池参数。


spring:
  datasource:
    # 业务库(主数据源)
    business:
      url: jdbc:mysql://localhost:3306/ecommerce_business?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
      username: root
      password: 123456
      driver-class-name: com.mysql.cj.jdbc.Driver
    # 历史库(从数据源)
    history:
      url: jdbc:mysql://localhost:3306/ecommerce_history?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
      username: root
      password: 123456
      driver-class-name: com.mysql.cj.jdbc.Driver
    # Druid连接池配置
    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 FROM DUAL
      test-while-idle: true
      test-on-borrow: false
      test-on-return: false

# MyBatis-Plus配置(全局)
mybatis-plus:
  configuration:
    map-underscore-to-camel-case: true # 下划线转驼峰
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 日志打印(便于调试)
  global-config:
    db-config:
      id-type: auto # 主键自增
步骤3:配置数据源核心配置类

分别为两个数据源创建配置类,指定包扫描路径、SqlSessionFactory等,注意主数据源需添加@Primary注解(避免SpringBoot自动配置冲突)。

① 业务库配置类(DataSourceBusinessConfig.java)


package com.example.multipledatasource.config;

import com.baomidou.mybatisplus.core.SqlSessionFactory;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
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 jakarta.sql.DataSource;

@Configuration
// 扫描业务库的Mapper接口(包路径区分)
@MapperScan(basePackages = "com.example.multipledatasource.mapper.business",
           sqlSessionFactoryRef = "businessSqlSessionFactory")
public class DataSourceBusinessConfig {

    // 配置业务库DataSource
    @Primary // 主数据源,必须添加,避免冲突
    @Bean(name = "businessDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.business") // 对应yml中的前缀
    public DataSource businessDataSource() {
        return DataSourceBuilder.create().build();
    }

    // 配置业务库SqlSessionFactory
    @Primary
    @Bean(name = "businessSqlSessionFactory")
    public SqlSessionFactory businessSqlSessionFactory(@Qualifier("businessDataSource") DataSource dataSource) throws Exception {
        MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean();
        bean.setDataSource(dataSource);
        // 配置Mapper.xml路径(若有)
        bean.setMapperLocations(new PathMatchingResourcePatternResolver()
                .getResources("classpath:mapper/business/*.xml"));
        return bean.getObject();
    }

    // 配置业务库SqlSessionTemplate
    @Primary
    @Bean(name = "businessSqlSessionTemplate")
    public SqlSessionTemplate businessSqlSessionTemplate(@Qualifier("businessSqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory);
    }
}

② 历史库配置类(DataSourceHistoryConfig.java)


package com.example.multipledatasource.config;

import com.baomidou.mybatisplus.core.SqlSessionFactory;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;

import jakarta.sql.DataSource;

@Configuration
// 扫描历史库的Mapper接口(包路径区分)
@MapperScan(basePackages = "com.example.multipledatasource.mapper.history",
           sqlSessionFactoryRef = "historySqlSessionFactory")
public class DataSourceHistoryConfig {

    // 配置历史库DataSource
    @Bean(name = "historyDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.history")
    public DataSource historyDataSource() {
        return DataSourceBuilder.create().build();
    }

    // 配置历史库SqlSessionFactory
    @Bean(name = "historySqlSessionFactory")
    public SqlSessionFactory historySqlSessionFactory(@Qualifier("historyDataSource") DataSource dataSource) throws Exception {
        MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean();
        bean.setDataSource(dataSource);
        bean.setMapperLocations(new PathMatchingResourcePatternResolver()
                .getResources("classpath:mapper/history/*.xml"));
        return bean.getObject();
    }

    // 配置历史库SqlSessionTemplate
    @Bean(name = "historySqlSessionTemplate")
    public SqlSessionTemplate historySqlSessionTemplate(@Qualifier("historySqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory);
    }
}
步骤4:编写测试代码

分别创建两个数据源的Mapper、Service,测试数据源是否正常生效。

① 业务库Mapper(BusinessUserMapper.java)


package com.example.multipledatasource.mapper.business;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.multipledatasource.entity.business.BusinessUser;
import org.springframework.stereotype.Repository;

@Repository
public interface BusinessUserMapper extends BaseMapper<BusinessUser> {
}

② 历史库Mapper(HistoryUserMapper.java)

package com.example.multipledatasource.mapper.history;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.multipledatasource.entity.history.HistoryUser;
import org.springframework.stereotype.Repository;

@Repository
public interface HistoryUserMapper extends BaseMapper<HistoryUser> {
}

③ 测试类(MultiDataSourceApplicationTests.java)


package com.example.multipledatasource;

import com.example.multipledatasource.entity.business.BusinessUser;
import com.example.multipledatasource.entity.history.HistoryUser;
import com.example.multipledatasource.mapper.business.BusinessUserMapper;
import com.example.multipledatasource.mapper.history.HistoryUserMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class MultiDataSourceApplicationTests {

    @Autowired
    private BusinessUserMapper businessUserMapper;

    @Autowired
    private HistoryUserMapper historyUserMapper;

    @Test
    void testMultiDataSource() {
        // 操作业务库
        BusinessUser businessUser = new BusinessUser();
        businessUser.setUsername("business_test");
        businessUserMapper.insert(businessUser);

        // 操作历史库
        HistoryUser historyUser = new HistoryUser();
        historyUser.setUsername("history_test");
        historyUserMapper.insert(historyUser);
    }
}
3.1.3 优缺点与适用场景
  • 优点:实现简单,无额外依赖;数据源隔离性好;支持不同ORM框架;调试方便。

  • 缺点:配置冗余,新增数据源需重复编写配置类;无法动态切换数据源;跨数据源事务处理复杂。

  • 适用场景:数据源数量固定(2-3个)、无需动态切换的场景(如固定的业务库与历史库拆分)。

方案2:AbstractRoutingDataSource自定义动态切换(灵活切换,无额外依赖)

3.2.1 核心原理

继承Spring提供的AbstractRoutingDataSource类,重写determineCurrentLookupKey()方法,通过ThreadLocal存储当前数据源标识,结合AOP切面拦截自定义注解,实现数据源的动态切换——在方法上添加注解,即可指定该方法使用的数据源,灵活适配多场景需求。

3.2.2 实操步骤
步骤1:引入依赖(同方案1,无需新增依赖)
步骤2:配置多数据源信息(application.yml)

spring:
  datasource:
    dynamic:
      primary: master # 默认数据源标识
    # 主数据源(业务库)
    master:
      url: jdbc:mysql://localhost:3306/ecommerce_business?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
      username: root
      password: 123456
      driver-class-name: com.mysql.cj.jdbc.Driver
    # 从数据源(历史库)
    slave:
      url: jdbc:mysql://localhost:3306/ecommerce_history?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
      username: root
      password: 123456
      driver-class-name: com.mysql.cj.jdbc.Driver
    # Druid连接池配置
    druid:
      initial-size: 5
      min-idle: 5
      max-active: 20
      max-wait: 60000

mybatis-plus:
  configuration:
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  global-config:
    db-config:
      id-type: auto
步骤3:创建数据源上下文(存储当前数据源标识)

package com.example.multipledatasource.context;

/**
 * 数据源上下文,用于存储当前线程使用的数据源标识(ThreadLocal保证线程安全)
 */
public class DataSourceContextHolder {

    // ThreadLocal存储数据源标识,避免多线程冲突
    private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();

    // 设置数据源标识
    public static void setDataSourceKey(String dataSourceKey) {
        CONTEXT_HOLDER.set(dataSourceKey);
    }

    // 获取数据源标识
    public static String getDataSourceKey() {
        // 若未设置,返回默认数据源标识
        return CONTEXT_HOLDER.get() == null ? "master" : CONTEXT_HOLDER.get();
    }

    // 清除数据源标识(避免内存泄漏)
    public static void clearDataSourceKey() {
        CONTEXT_HOLDER.remove();
    }
}
步骤4:自定义动态数据源(继承AbstractRoutingDataSource)

package com.example.multipledatasource.config;

import com.example.multipledatasource.context.DataSourceContextHolder;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

import jakarta.sql.DataSource;
import java.util.Map;

/**
 * 自定义动态数据源,路由到指定数据源
 */
public class DynamicDataSource extends AbstractRoutingDataSource {

    /**
     * 重写方法,返回当前数据源标识(从ThreadLocal中获取)
     */
    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceContextHolder.getDataSourceKey();
    }

    /**
     * 初始化数据源(可选,用于动态添加数据源)
     */
    @Override
    public void setTargetDataSources(Map<Object, Object> targetDataSources) {
        super.setTargetDataSources(targetDataSources);
        super.afterPropertiesSet(); // 刷新数据源缓存
    }
}
步骤5:配置数据源Bean(整合所有数据源)

package com.example.multipledatasource.config;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

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

@Configuration
public class DynamicDataSourceConfig {

    // 配置主数据源(master)
    @Bean(name = "masterDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.master")
    public DataSource masterDataSource() {
        return DataSourceBuilder.create().build();
    }

    // 配置从数据源(slave)
    @Bean(name = "slaveDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.slave")
    public DataSource slaveDataSource() {
        return DataSourceBuilder.create().build();
    }

    // 配置动态数据源(整合所有数据源)
    @Primary
    @Bean(name = "dynamicDataSource")
    public DataSource dynamicDataSource() {
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        // 存储所有数据源(key:数据源标识,value:数据源对象)
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put("master", masterDataSource());
        targetDataSources.put("slave", slaveDataSource());
        // 设置所有目标数据源
        dynamicDataSource.setTargetDataSources(targetDataSources);
        // 设置默认数据源(master)
        dynamicDataSource.setDefaultTargetDataSource(masterDataSource());
        return dynamicDataSource;
    }
}
步骤6:自定义注解(用于指定数据源)

package com.example.multipledatasource.annotation;

import java.lang.annotation.*;

/**
 * 自定义数据源注解,用于指定方法使用的数据源
 */
@Target({ElementType.METHOD, ElementType.TYPE}) // 可用于方法、类
@Retention(RetentionPolicy.RUNTIME) // 运行时生效
@Documented // 生成文档
public @interface DataSource {
    // 数据源标识,默认master
    String value() default "master";
}
步骤7:编写AOP切面(拦截注解,切换数据源)

package com.example.multipledatasource.aspect;

import com.example.multipledatasource.annotation.DataSource;
import com.example.multipledatasource.context.DataSourceContextHolder;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.aspectj.lang.annotation.Aspect;

import java.lang.reflect.Method;

/**
 * 数据源切换切面,拦截@DataSource注解,设置对应的数据源标识
 */
@Aspect
@Component
public class DataSourceAspect {

    // 切入点:拦截所有带有@DataSource注解的方法或类
    @Pointcut("@annotation(com.example.multipledatasource.annotation.DataSource) || @within(com.example.multipledatasource.annotation.DataSource)")
    public void dataSourcePointCut() {}

    // 前置通知:在方法执行前,设置数据源标识
    @Before("dataSourcePointCut()")
    public void beforeSwitchDataSource(JoinPoint joinPoint) {
        // 获取当前方法上的@DataSource注解
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        DataSource dataSource = method.getAnnotation(DataSource.class);

        // 若方法上无注解,检查类上的注解
        if (dataSource == null) {
            dataSource = joinPoint.getTarget().getClass().getAnnotation(DataSource.class);
        }

        // 设置数据源标识(若注解为null,使用默认值master)
        if (dataSource != null) {
            DataSourceContextHolder.setDataSourceKey(dataSource.value());
        }
    }

    // 后置通知:方法执行完毕,清除数据源标识(避免内存泄漏)
    @After("dataSourcePointCut()")
    public void afterSwitchDataSource() {
        DataSourceContextHolder.clearDataSourceKey();
    }
}
步骤8:测试动态切换

package com.example.multipledatasource.service;

import com.example.multipledatasource.annotation.DataSource;
import com.example.multipledatasource.entity.business.BusinessUser;
import com.example.multipledatasource.entity.history.HistoryUser;
import com.example.multipledatasource.mapper.business.BusinessUserMapper;
import com.example.multipledatasource.mapper.history.HistoryUserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class UserService {

    @Autowired
    private BusinessUserMapper businessUserMapper;

    @Autowired
    private HistoryUserMapper historyUserMapper;

    // 使用默认数据源(master,业务库)
    public void addBusinessUser(BusinessUser user) {
        businessUserMapper.insert(user);
    }

    // 使用slave数据源(历史库)
    @DataSource("slave")
    public void addHistoryUser(HistoryUser user) {
        historyUserMapper.insert(user);
    }

    // 测试同一方法切换数据源(实际开发中不推荐,需注意事务)
    public void testSwitchDataSource() {
        // 使用master数据源
        BusinessUser businessUser = new BusinessUser();
        businessUser.setUsername("master_test");
        businessUserMapper.insert(businessUser);

        // 手动切换到slave数据源
        DataSourceContextHolder.setDataSourceKey("slave");
        HistoryUser historyUser = new HistoryUser();
        historyUser.setUsername("slave_test");
        historyUserMapper.insert(historyUser);

        // 清除标识(避免影响后续方法)
        DataSourceContextHolder.clearDataSourceKey();
    }
}
3.2.3 优缺点与适用场景
  • 优点:配置简洁,支持动态新增数据源;切换灵活,可通过注解快速指定数据源;无额外依赖,轻量化;适配大多数业务场景。

  • 缺点:需自定义切面与上下文管理,编码量略多;跨数据源事务需额外处理;多线程环境下需注意线程安全(ThreadLocal已保证)。

  • 适用场景:大多数多数据源场景(读写分离、数据归档、动态业务库),数据源数量不固定、需要灵活切换的场景。

方案3:dynamic-datasource-spring-boot3-starter(主流首选,高效便捷)

3.3.1 核心原理

由MyBatis-Plus团队开发的开源框架,专门适配SpringBoot3,底层封装了AbstractRoutingDataSource与AOP,简化了多数据源配置——通过配置文件指定数据源信息,使用框架提供的@DS注解即可实现数据源动态切换,支持读写分离、主从切换、动态新增数据源等高级功能,是目前SpringBoot3多数据源的主流首选方案。

3.3.2 实操步骤
步骤1:引入核心依赖(关键:使用适配SpringBoot3的starter)
<!-- 其他依赖同方案1,新增动态数据源starter -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>dynamic-datasource-spring-boot3-starter</artifactId>
    <version>4.5.0</version> <!-- 适配SpringBoot3,JDK17+ -->
</dependency>

注意:SpringBoot2.x使用dynamic-datasource-spring-boot-starter,SpringBoot3.x必须使用dynamic-datasource-spring-boot3-starter,否则会出现版本兼容错误。

步骤2:配置多数据源信息(application.yml)

框架提供了简洁的配置方式,支持多数据源分组、读写分离等。

spring:
  datasource:
    dynamic:
      primary: master # 默认数据源(必填)
      strict: false # 非严格模式:找不到数据源时,使用默认数据源;true则抛出异常
      datasource:
        # 主数据源(业务库)
        master:
          url: jdbc:mysql://localhost:3306/ecommerce_business?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
          username: root
          password: 123456
          driver-class-name: com.mysql.cj.jdbc.Driver
        # 从数据源1(历史库)
        slave1:
          url: jdbc:mysql://localhost:3306/ecommerce_history?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
          username: root
          password: 123456
          driver-class-name: com.mysql.cj.jdbc.Driver
        # 从数据源2(统计库,可新增多个)
        slave2:
          url: jdbc:mysql://localhost:3306/ecommerce_stat?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
          username: root
          password: 123456
          driver-class-name: com.mysql.cj.jdbc.Driver
      # Druid连接池配置(框架自动集成,无需额外配置Bean)
      druid:
        initial-size: 5
        min-idle: 5
        max-active: 20
        max-wait: 60000
        validation-query: SELECT 1 FROM DUAL

# MyBatis-Plus配置(无需额外配置SqlSessionFactory)
mybatis-plus:
  configuration:
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  global-config:
    db-config:
      id-type: auto
步骤3:使用@DS注解切换数据源

框架提供@DS注解,可用于类、方法上,优先级:方法注解 > 类注解,无需自定义切面和上下文。


package com.example.multipledatasource.service;

import com.baomidou.dynamic.datasource.annotation.DS;
import com.example.multipledatasource.entity.business.BusinessUser;
import com.example.multipledatasource.entity.history.HistoryUser;
import com.example.multipledatasource.mapper.business.BusinessUserMapper;
import com.example.multipledatasource.mapper.history.HistoryUserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
@DS("master") // 类上注解,默认使用master数据源
public class DynamicUserService {

    @Autowired
    private BusinessUserMapper businessUserMapper;

    @Autowired
    private HistoryUserMapper historyUserMapper;

    // 使用类上的默认数据源(master)
    public void addBusinessUser(BusinessUser user) {
        businessUserMapper.insert(user);
    }

    // 方法注解,切换到slave1数据源(历史库)
    @DS("slave1")
    public void addHistoryUser(HistoryUser user) {
        historyUserMapper.insert(user);
    }

    // 切换到slave2数据源(统计库)
    @DS("slave2")
    public void addStatUser(StatUser user) {
        statUserMapper.insert(user);
    }

    // 读写分离示例(主库写,从库读)
    public void testReadWriteSplit() {
        // 写操作:使用master数据源(默认)
        BusinessUser user = new BusinessUser();
        user.setUsername("read_write_test");
        businessUserMapper.insert(user);

        // 读操作:切换到slave1数据源
        @DS("slave1")
        BusinessUser queryUser = businessUserMapper.selectById(user.getId());
    }
}
步骤4:高级功能(动态新增数据源)

框架支持动态新增数据源,无需重启服务,适用于多租户、动态业务库场景。


package com.example.multipledatasource.service;

import com.baomidou.dynamic.datasource.DynamicRoutingDataSource;
import com.baomidou.dynamic.datasource.creator.DataSourceCreator;
import com.baomidou.dynamic.datasource.model.DataSourceProperty;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import jakarta.sql.DataSource;
import java.util.Map;

@Service
public class DynamicDataSourceService {

    @Autowired
    private DynamicRoutingDataSource dynamicRoutingDataSource;

    @Autowired
    private DataSourceCreator dataSourceCreator;

    /**
     * 动态新增数据源
     * @param dataSourceKey 数据源标识
     * @param url 数据库地址
     * @param username 用户名
     * @param password 密码
     */
    public void addDataSource(String dataSourceKey, String url, String username, String password) {
        // 1. 构建数据源配置
        DataSourceProperty property = new DataSourceProperty();
        property.setUrl(url);
        property.setUsername(username);
        property.setPassword(password);
        property.setDriverClassName("com.mysql.cj.jdbc.Driver");

        // 2. 创建数据源
        DataSource dataSource = dataSourceCreator.createDataSource(property);

        // 3. 新增到动态数据源中
        dynamicRoutingDataSource.addDataSource(dataSourceKey, dataSource);
    }

    /**
     * 删除数据源
     * @param dataSourceKey 数据源标识
     */
    public void removeDataSource(String dataSourceKey) {
        dynamicRoutingDataSource.removeDataSource(dataSourceKey);
    }

    /**
     * 获取所有数据源标识
     */
    public Map<String, DataSource> getAllDataSources() {
        return dynamicRoutingDataSource.getDataSources();
    }
}
3.3.3 优缺点与适用场景
  • 优点:配置极简,无需自定义切面和配置类;支持动态新增/删除数据源;支持读写分离、主从切换等高级功能;升级门槛低,API友好;社区活跃,问题易解决。

  • 缺点:依赖第三方框架;轻量场景略显重量级;复杂自定义需求(如特殊数据源切换逻辑)需二次开发。

  • 适用场景:大多数多数据源场景,尤其是数据源数量多、需要动态切换、读写分离、多租户的复杂场景(推荐首选)。

四、3种方案对比与选型建议

实现方案

核心原理

优点

缺点

适用场景

多DataSource Bean分包

包路径区分,独立配置DataSource、SqlSessionFactory

实现简单,无额外依赖,隔离性好

配置冗余,无法动态切换,跨事务复杂

数据源固定(2-3个)、无需动态切换

AbstractRoutingDataSource自定义

ThreadLocal+AOP+抽象类,动态路由数据源

灵活切换,无额外依赖,轻量化

编码量多,需自定义切面和上下文

数据源不固定、需要灵活切换,无第三方依赖诉求

dynamic-datasource-starter

封装AbstractRoutingDataSource,注解驱动切换

配置极简,支持动态增删、读写分离,API友好

依赖第三方框架,轻量场景略重

大多数场景(推荐),尤其是复杂多数据源、多租户

五、SpringBoot3多数据源注意事项与避坑指南

5.1 版本兼容坑(最常见)

  • 坑1:使用SpringBoot3时,引入dynamic-datasource-spring-boot-starter(适配2.x),导致启动失败。解决方案:替换为dynamic-datasource-spring-boot3-starter,版本4.0.0+。

  • 坑2:JDK版本低于17,启动SpringBoot3项目报错。解决方案:升级JDK至17+(推荐JDK17,兼容性最好)。

  • 坑3:Druid版本过低,不支持Jakarta规范。解决方案:使用druid-spring-boot-3-starter,版本1.2.16+。

5.2 事务管理坑

  • 坑1:同一事务中切换数据源,切换失效。原因:Spring事务是基于数据源的,事务启动后,数据源无法切换。解决方案:避免同一事务中切换数据源;若必须切换,使用分布式事务(如Seata)。

  • 坑2:跨数据源事务未处理,导致数据不一致。解决方案:简单场景使用@Transactional(propagation = Propagation.REQUIRES_NEW)(多事务隔离);复杂场景集成Seata实现分布式事务。

5.3 线程安全与内存泄漏

使用ThreadLocal存储数据源标识时,若未及时清除,会导致内存泄漏(尤其是线程池场景)。解决方案:在AOP后置通知中,调用clearDataSourceKey()清除标识(自定义方案需手动处理,dynamic-datasource框架已自动处理)。

5.4 其他注意事项

  • 数据源标识不可重复,否则会覆盖原有数据源。

  • 配置默认数据源(primary),避免找不到数据源报错。

  • 多数据源场景下,尽量避免使用SpringBoot自动配置的DataSourceAutoConfiguration,可通过@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)排除。

  • 异构数据源(如MySQL+ClickHouse),需引入对应数据库驱动,配置不同的DataSource类型。

六、总结

SpringBoot3多数据源的实现,核心是“适配Jakarta规范+选择合适的方案”。本文讲解的3种方案,各有优劣:多DataSource分包适合简单固定场景,AbstractRoutingDataSource适合无第三方依赖的灵活场景,dynamic-datasource-starter适合大多数复杂场景(推荐首选)。

实际开发中,需根据数据源数量、是否需要动态切换、业务复杂度选型,同时规避版本兼容、事务管理、线程安全等常见坑。掌握本文的实操步骤与避坑指南,即可轻松应对SpringBoot3多数据源的各类应用场景,提升系统的扩展性与性能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

what丶k

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值