在后端开发中,随着业务复杂度的提升,单一数据源早已无法满足实际需求——电商系统的订单库与用户库拆分、数据归档场景的业务库与历史库联动、高并发场景的读写分离,甚至是多类型数据库(MySQL+ClickHouse)的整合,都离不开多数据源的支持。SpringBoot3作为目前主流的后端开发框架,基于Jakarta EE 9规范进行了重大升级,其多数据源的实现方式与SpringBoot2.x存在关键差异,本文将从核心原理、主流方案、实操落地到避坑优化,带你完整掌握SpringBoot3多数据源的应用,助力开发者高效应对复杂业务场景。
一、SpringBoot3多数据源核心差异(与2.x对比)
很多开发者升级SpringBoot3后,会遇到多数据源配置启动失败(如“url attribute is not specified”)的问题,核心原因是SpringBoot3的底层规范迁移,与2.x存在两大关键差异,也是后续配置的核心前提:
-
依赖包命名空间迁移:SpringBoot3基于Jakarta EE 9规范,将原有
javax.*包迁移为jakarta.*包,例如javax.sql.DataSource迁移为jakarta.sql.DataSource。这导致SpringBoot2.x中常用的dynamic-datasource-spring-boot-starter(4.x版本)无法兼容,需使用专门适配SpringBoot3的starter依赖。 -
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 核心原理
为每个数据源配置独立的DataSource、SqlSessionFactory、MapperScannerConfigurer,通过包路径区分数据源——不同数据源的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多数据源的各类应用场景,提升系统的扩展性与性能。
899

被折叠的 条评论
为什么被折叠?



