SpringBoot多数据源实战:从架构设计到生产避坑的深度指南
最近在重构一个老项目的报表模块,需要同时从业务库和日志库拉取数据。本以为在SpringBoot里配个多数据源是分分钟的事,结果一脚踩进了配置的深坑。从连接池莫名变成HikariCP,到事务在多个数据源间乱窜,再到Mybatis-Plus的拦截器失效,这一路折腾下来,发现网上很多“抄来抄去”的教程,要么配置层级不对,要么关键细节缺失,真正能直接用在生产环境的少之又少。
如果你也正在为SpringBoot应用连接多个数据库而头疼,特别是想用好Druid连接池的监控和防护能力,那么这篇文章或许能帮你省下不少排查时间。我会从一个真实的微服务场景出发,拆解多数据源配置的核心逻辑,分享那些官方文档里不会写的“坑”,并附上可直接复用的完整代码。无论你是要对接异构数据库(MySQL + PostgreSQL),还是做读写分离、分库分表的前期技术预研,这里的内容都能给你一个扎实的起点。
1. 多数据源架构:不只是多个DataSource那么简单
在单数据源应用中,SpringBoot的自动配置为我们包办了一切:它自动创建DataSource、SqlSessionFactory、事务管理器,我们只需要在application.yml里填上数据库地址就行。但当我们声明第二个DataSource时,这种“魔法”就失效了。SpringBoot的自动配置默认是单数据源导向的,它会因为找到多个DataSource Bean而困惑,不知道应该把SqlSessionFactory和事务管理器绑定到哪一个上。
所以,实现多数据源的核心,实质上是手动接管原本由自动配置完成的那些Bean的创建过程,并为每个数据源明确地建立一套独立的、完整的Mybatis运行环境。这套环境主要包括三个核心组件:
- DataSource: 数据库连接的来源,通常包含连接池配置。
- SqlSessionFactory: Mybatis的核心,负责创建
SqlSession,它需要绑定到特定的DataSource,并加载对应的Mapper接口和XML文件。 - TransactionManager: 事务管理器,确保数据库操作在事务边界内执行,它也必须与特定的
DataSource关联。
注意:很多人误以为配好几个
DataSourceBean就完事了,结果运行时发现事务不生效、Mapper找不到,根本原因就是后两者没有正确配置和关联。
在多数据源场景下,我们通常需要指定一个主数据源(Primary)。这个“主”并非指业务上最重要,而是技术上的默认选择。当某些框架或场景(例如一些健康检查端点、没有明确指定数据源的简单查询)需要注入一个DataSource时,Spring会优先选择带有@Primary注解的那个。因此,合理设置@Primary是避免NoUniqueBeanDefinitionException(找到多个Bean)错误的关键。
为了更清晰地理解这三个组件与数据源的关系,我们可以看下面这个对比表格:
| 组件 | 单数据源 (SpringBoot自动配置) | 多数据源 (手动配置) | 关键区别 |
|---|---|---|---|
| DataSource | 自动从spring.datasource读取配置创建1个。 | 需手动创建多个Bean,分别读取不同的配置前缀。 | 从“一个”变为“多个”,需要明确命名和区分。 |
| SqlSessionFactory | 自动创建1个,绑定到唯一的DataSource。 | 需手动创建多个,每个通过setDataSource()绑定到特定的DataSource。 | 需要建立明确的绑定关系,并指定不同的Mapper扫描路径。 |
| TransactionManager | 自动创建1个,基于唯一的DataSource。 | 需手动创建多个,每个在构造时传入对应的DataSource。 | 必须确保事务管理器与操作使用的SqlSessionFactory使用的是同一个DataSource。 |
| 配置复杂度 | 极低,开箱即用。 | 高,需要完整定义多套环境。 | 从“零配置”到“全手动配置”。 |
理解了这层关系,我们再来看具体的技术选型。本次我们选择 Mybatis-Plus + Druid 的组合。Mybatis-Plus在Mybatis基础上提供了强大的CRUD增强和插件机制,而Druid则是经过阿里大规模生产验证的连接池,以其强大的监控、防御SQL注入、统计等功能著称。将它们与多数据源结合,既能提升开发效率,又能保障应用稳定性。
2. 项目依赖与配置:奠定正确的基础
一切从pom.xml开始。依赖的版本选择不当,往往是后续一系列诡异问题的根源。对于SpringBoot 2.7.x 或 3.x 版本,我推荐以下依赖配置,这能确保各组件间的良好兼容性。
<dependencies>
<!-- SpringBoot Web 基础 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Mybatis-Plus 启动器 (已包含Mybatis和JDBC) -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version> <!-- 请检查最新版本 -->
</dependency>
<!-- Druid 连接池启动器 (关键!) -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.18</version> <!-- 请检查最新版本 -->
</dependency>
<!-- 数据库驱动 (以MySQL为例) -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- 其他可能需要的依赖,如Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
这里有两个极易踩坑的点:
- 不要单独引入
spring-boot-starter-jdbc:mybatis-plus-boot-starter已经包含了它。重复引入可能导致依赖冲突或配置加载顺序问题。 - 务必使用
druid-spring-boot-starter,而不是旧的druid。这个starter提供了与SpringBoot配置属性(spring.datasource.druid.*)的无缝集成,简化了监控等功能的开启。
接下来是重头戏:application.yml配置。网上很多错误配置都源于对配置层级的误解。Druid starter期望的配置结构是特定的。
spring:
datasource:
# 1. 指定数据源类型为DruidDataSource
type: com.alibaba.druid.pool.DruidDataSource
druid:
# 2. Druid连接池的通用配置 (对所有数据源生效)
initial-size: 5
min-idle: 5
max-active: 20
max-wait: 60000
validation-query: SELECT 1
test-while-idle: true
test-on-borrow: false
test-on-return: false
# 开启监控统计等功能(按需)
stat-view-servlet:
enabled: true
login-username: admin
login-password: admin
filter:
stat:
enabled: true
log-slow-sql: true
slow-sql-millis: 2000
# 3. 多数据源定义 (关键!)
# 这里定义的`datasource1`和`datasource2`是自定义名称,对应后续Bean的prefix
datasource1:
url: jdbc:mysql://localhost:3306/db_primary?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
datasource2:
url: jdbc:mysql://localhost:3307/db_log?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
配置解读与避坑:
spring.datasource.type:这个必须指定为Druid的类。如果不指定,SpringBoot可能会根据classpath下的连接池jar包自动选择(通常是HikariCP),导致Druid配置不生效。spring.datasource.druid.*:这个层级的配置是Druid连接池的全局默认属性。比如initial-size: 5,意味着你通过Druid创建的每个数据源连接池,初始连接数默认都是5,除非你在数据源专属配置里覆盖它。spring.datasource.druid.datasource1:这是我们自定义的数据源配置块。名字datasource1可以任意取,但后续Java配置中的@ConfigurationProperties(prefix = "spring.datasource.druid.datasource1")必须与此保持一致。这里配置的是该数据源独有的连接信息(URL、用户名等)。- 关于
jdbc-url:网上有些教程会写jdbc-url。对于Druid starter,使用标准的url属性即可。jdbc-url是HikariCP等连接池的写法,在Druid的特定配置语境下可能不识别。
3. 核心配置类详解:手动构建多套运行时环境
配置写好了,现在需要写代码把它们“激活”。我们将创建三个配置类。
第一步:创建多个DataSource Bean
这个类的唯一职责,就是根据YAML配置,生成多个DruidDataSource实例。
import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
@Configuration
public class DataSourceConfig {
/**
* 主数据源 (业务库)
* Bean名称为 "primaryDataSource"
* 配置前缀绑定到 spring.datasource.druid.datasource1
*/
@Bean(name = "primaryDataSource")
@ConfigurationProperties(prefix = "spring.datasource.druid.datasource1")
public DataSource primaryDataSource() {
// 关键!必须返回 new DruidDataSource()
// 如果使用 DataSourceBuilder.create().build(),得到的将是默认的HikariCP数据源
return new DruidDataSource();
}
/**
* 第二数据源 (日志库)
* Bean名称为 "secondaryDataSource"
* 配置前缀绑定到 spring.datasource.druid.datasource2
*/
@Bean(name = "secondaryDataSource")
@ConfigurationProperties(prefix = "spring.datasource.druid.datasource2")
public DataSource secondaryDataSource() {
return new DruidDataSource();
}
}
这里有个巨坑:如果你看到代码里写的是return DataSourceBuilder.create().build();,请立刻警惕。DataSourceBuilder是SpringBoot提供的一个便捷工具,但它无法感知spring.datasource.type并将其应用于Druid。它默认会创建一个HikariDataSource(如果HikariCP在classpath下)。这就是为什么很多人配置了半天,监控里看到的连接池却是Hikari。正确的做法是直接new DruidDataSource(),@ConfigurationProperties注解会自动将prefix指定的配置属性(url, username等)注入到这个新创建的对象中。
第二步:配置主数据源的Mybatis环境
我们将主数据源(业务库)标记为@Primary,这意味着当其他地方需要注入一个DataSource但未指定名称时,Spring会使用这个。
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
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 javax.sql.DataSource;
@Configuration
// 指定扫描主数据源对应的Mapper接口包,并绑定到名为`primarySqlSessionFactory`的工厂
@MapperScan(
basePackages = "com.yourpackage.mapper.primary",
sqlSessionFactoryRef = "primarySqlSessionFactory"
)
public class PrimaryDataSourceConfig {
/**
* 创建SqlSessionFactory,并绑定到主数据源。
* 使用MybatisSqlSessionFactoryBean而不是原生的SqlSessionFactoryBean,以支持Mybatis-Plus特性。
*/
@Bean(name = "primarySqlSessionFactory")
@Primary // 标记为主要的SqlSessionFactory
public SqlSessionFactory primarySqlSessionFactory(@Qualifier("primaryDataSource") DataSource dataSource) throws Exception {
MybatisSqlSessionFactoryBean sessionFactoryBean = new MybatisSqlSessionFactoryBean();
// 绑定数据源
sessionFactoryBean.setDataSource(dataSource);
// 设置Mapper XML文件位置 (路径根据实际项目调整)
sessionFactoryBean.setMapperLocations(
new PathMatchingResourcePatternResolver()
.getResources("classpath:mapper/primary/*.xml")
);
// 配置Mybatis-Plus插件,例如分页插件 (这是关键,否则MP的分页等功能可能失效)
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
sessionFactoryBean.setPlugins(interceptor);
return sessionFactoryBean.getObject();
}
/**
* 创建SqlSessionTemplate,它是SqlSession的线程安全实现。
*/
@Bean(name = "primarySqlSessionTemplate")
@Primary
public SqlSessionTemplate primarySqlSessionTemplate(@Qualifier("primarySqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
/**
* 创建事务管理器,并绑定到主数据源。
* 事务注解@Transactional默认会使用这个管理器。
*/
@Bean(name = "primaryTransactionManager")
@Primary
public DataSourceTransactionManager primaryTransactionManager(@Qualifier("primaryDataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
关键点解析:
@MapperScan:这个注解告诉Mybatis去哪里找Mapper接口。basePackages最好按数据源进行物理目录隔离(如mapper.primary和mapper.secondary),避免混淆。sqlSessionFactoryRef必须指向本配置类中创建的SqlSessionFactoryBean的名称。MybatisSqlSessionFactoryBean:使用Mybatis-Plus时必须用这个类来创建工厂,否则Mybatis-Plus的全局配置和插件(如分页、乐观锁)不会生效。- 插件配置:必须在
SqlSessionFactoryBean中手动添加MybatisPlusInterceptor。在多数据源配置下,Mybatis-Plus的自动配置可能会失效,这是一个常见的“坑”。分页、动态表名等插件都需要在这里显式添加。 @Primary:在SqlSessionFactory、SqlSessionTemplate和TransactionManager上都标记@Primary,确保它们成为默认选择。
第三步:配置从数据源的Mybatis环境
第二个数据源(日志库)的配置与主数据源几乎一样,核心区别在于不能使用@Primary注解,并且所有Bean的名称、扫描的包路径、XML文件路径都必须不同。
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import javax.sql.DataSource;
@Configuration
// 扫描从数据源对应的Mapper接口包
@MapperScan(
basePackages = "com.yourpackage.mapper.secondary",
sqlSessionFactoryRef = "secondarySqlSessionFactory"
)
public class SecondaryDataSourceConfig {
@Bean(name = "secondarySqlSessionFactory")
public SqlSessionFactory secondarySqlSessionFactory(@Qualifier("secondaryDataSource") DataSource dataSource) throws Exception {
MybatisSqlSessionFactoryBean sessionFactoryBean = new MybatisSqlSessionFactoryBean();
sessionFactoryBean.setDataSource(dataSource);
// 指向从数据源专用的XML目录
sessionFactoryBean.setMapperLocations(
new PathMatchingResourcePatternResolver()
.getResources("classpath:mapper/secondary/*.xml")
);
// 如果从数据源也需要Mybatis-Plus插件,同样需要在这里配置
// MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
// sessionFactoryBean.setPlugins(interceptor);
return sessionFactoryBean.getObject();
}
@Bean(name = "secondarySqlSessionTemplate")
public SqlSessionTemplate secondarySqlSessionTemplate(@Qualifier("secondarySqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
@Bean(name = "secondaryTransactionManager")
public DataSourceTransactionManager secondaryTransactionManager(@Qualifier("secondaryDataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
至此,两套独立的Mybatis运行环境就搭建完毕了。它们拥有各自的数据源、Mapper接口包、XML文件目录和事务管理器,互不干扰。
4. 使用、事务控制与生产级避坑指南
配置完成后,如何在Service层使用呢?关键在于使用@Qualifier注解来指定你要注入哪个数据源对应的组件。但更常见的做法是,直接让Mapper接口归属于对应的@MapperScan包下,这样Mybatis会自动将其与正确的SqlSessionFactory关联。
Mapper与Service层示例:
// PrimaryUserMapper.java - 位于 com.yourpackage.mapper.primary 包下
// 它会自动被 PrimaryDataSourceConfig 中的 @MapperScan 扫描,并使用 primarySqlSessionFactory
@Mapper
public interface PrimaryUserMapper extends BaseMapper<User> {
// 自定义查询...
}
// SecondaryLogMapper.java - 位于 com.yourpackage.mapper.secondary 包下
// 它会自动被 SecondaryDataSourceConfig 中的 @MapperScan 扫描,并使用 secondarySqlSessionFactory
@Mapper
public interface SecondaryLogMapper extends BaseMapper<Log> {
// 自定义查询...
}
@Service
public class BusinessService {
@Autowired
private PrimaryUserMapper primaryUserMapper; // 自动使用主数据源
@Autowired
private SecondaryLogMapper secondaryLogMapper; // 自动使用从数据源
public void doBusiness() {
User user = primaryUserMapper.selectById(1L);
Log log = new Log();
log.setAction("query");
secondaryLogMapper.insert(log);
// 此时操作了两个数据库
}
}
多数据源下的事务控制:
这是另一个难点。Spring的@Transactional注解默认使用@Primary标记的事务管理器(即我们配置的primaryTransactionManager)。这意味着,如果一个Service方法上加了@Transactional,那么该方法内所有数据库操作(即使是通过secondaryLogMapper)都将在主数据源的事务中执行。这显然不符合预期,并且对从数据源的操作可能无法回滚。
解决方案是使用Spring的事务管理器路由或编程式事务。一个相对简单的实践是:将涉及不同数据源写操作的方法拆分开,或者使用@Transactional注解的transactionManager属性显式指定。
@Service
public class MixedService {
@Transactional(transactionManager = "primaryTransactionManager")
public void updatePrimary() {
// 操作 primaryUserMapper
}
@Transactional(transactionManager = "secondaryTransactionManager")
public void updateSecondary() {
// 操作 secondaryLogMapper
}
// 这个方法没有@Transactional,或者只标记了默认的,则只适合查询,或单个数据源的写操作。
public void updateBoth() {
updatePrimary(); // 在独立事务中执行
updateSecondary(); // 在另一个独立事务中执行
// 注意:这两个事务是独立的,无法做到原子性。如果需要分布式事务,需引入Seata等方案。
}
}
生产环境避坑清单:
- Druid监控未生效:检查
druid-spring-boot-starter依赖,并确保YAML中stat-view-servlet.enabled: true。访问/druid/index.html查看。 - Mybatis-Plus分页失效:检查是否在
SqlSessionFactoryBean中手动添加了MybatisPlusInterceptor和PaginationInnerInterceptor。多数据源下自动配置可能不生效。 - Mapper绑定异常(Invalid bound statement):99%的原因是
@MapperScan的basePackages路径写错,或者XML文件路径setMapperLocations没配置对。确保接口和XML在正确的、被扫描的目录下。 - 循环依赖:如果配置类之间或与Service有复杂的
@Autowired依赖,可能启动报循环依赖错误。尽量使用构造器注入或@Lazy注解。 - 性能问题:Druid的默认配置可能不适合高并发场景。需要根据实际监控数据调整
max-active(最大连接数)、max-wait(获取连接超时时间)等参数。max-active设置过高可能导致数据库连接耗尽,设置过低则应用排队等待。 - 动态数据源切换:本文是静态配置多数据源。如果业务需要根据请求参数、分片键等动态切换数据源(如分库分表),则需要引入
AbstractRoutingDataSource和线程上下文(如ThreadLocal)来实现,复杂度会更高。
配置多数据源就像搭积木,每一块都必须严丝合缝。最让我印象深刻的一次排查是,所有配置看起来都正确,但Druid监控里就是看不到连接池信息。最后发现是另一位同事在某个不起眼的测试配置类里,用@Bean定义了一个未指定类型的DataSource,Spring容器里存在多个同类型Bean,导致自动注入混乱。所以,在多数据源项目中,保持配置的清晰和唯一性至关重要。建议将不同数据源的配置类、Mapper包、Service层进行严格的模块化隔离,从物理结构上避免混淆。
971



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



