Spring Boot多数据源配置全解析:从静态到动态,实战指南
在Java开发中,单数据源早已无法满足复杂业务需求——主从读写分离、多租户数据隔离、分库分表……这些场景都需要多数据源支持。本文将介绍Spring Boot中多数据源的静态配置和动态切换实战,附完整代码和避坑指南!
在Java开发中,单数据源早已无法满足复杂业务需求——主从读写分离、多租户数据隔离、分库分表……这些场景都需要多数据源支持。今天咱们就来聊聊Spring Boot中多数据源的静态配置和动态切换实战,附完整代码和避坑指南!
一、为什么需要多数据源?
先明确场景,才能选对方案!
- 读写分离:主库扛写操作(增删改),从库分担读压力(查询),提升系统吞吐量。
- 多租户隔离:SaaS系统中,不同租户数据存独立数据库,避免数据泄露。
- 分库分表:数据量暴增时,按业务或时间拆分到不同库/表,降低单库压力。
- 异构数据库访问:同时连MySQL(关系型)、Redis(缓存)、MongoDB(文档型),满足多样化需求。
二、静态多数据源:固定数据源配置
适合数据源数量明确、无需动态切换的场景(比如固定的主从库)。
1. 配置文件定义多数据源
在application.yml
里直接声明两个数据源(主库primary
和从库secondary
),Spring Boot会自动加载这些配置。
spring:
datasource:
# 主数据源(写操作)
primary:
url: jdbc:mysql://localhost:3306/db_primary?useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
hikari: # 连接池配置(可选)
maximum-pool-size: 10 # 最大连接数
connection-timeout: 30000 # 连接超时时间(ms)
# 从数据源(读操作)
secondary:
url: jdbc:mysql://localhost:3307/db_secondary?useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
maximum-pool-size: 8
2. 手动创建数据源Bean
通过@Configuration
和@Bean
显式创建两个DataSource
,并用@Primary
标记主数据源(避免Spring注入歧义)。
@Configuration
public class DataSourceConfig {
// 主数据源(必须@Primary)
@Primary
@Bean("primaryDataSource")
@ConfigurationProperties(prefix = "spring.datasource.primary") // 绑定yaml中primary前缀的配置
public DataSource primaryDataSource() {
return DataSourceBuilder.create().build(); // 直接用Builder创建
}
// 从数据源
@Bean("secondaryDataSource")
@ConfigurationProperties(prefix = "spring.datasource.secondary")
public DataSource secondaryDataSource() {
return DataSourceBuilder.create().build();
}
}
3. 配置MyBatis的SqlSessionFactory
如果用MyBatis,每个数据源需要独立的SqlSessionFactory
,并通过@MapperScan
指定Mapper接口的扫描路径。
主库配置类:
@Configuration
@MapperScan(basePackages = "com.example.mapper.primary", sqlSessionFactoryRef = "primarySqlSessionFactory")
public class PrimaryMyBatisConfig {
@Primary
@Bean("primarySqlSessionFactory")
public SqlSessionFactory primarySqlSessionFactory(@Qualifier("primaryDataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
sessionFactory.setDataSource(dataSource); // 绑定主数据源
// 可选:指定Mapper XML文件位置(如果用了xml)
sessionFactory.setMapperLocations(
new PathMatchingResourcePatternResolver().getResources("classpath:mapper/primary/*.xml")
);
// 可选:设置MyBatis全局配置(如驼峰命名)
sessionFactory.setConfiguration(mybatisConfig());
return sessionFactory.getObject();
}
// 可选:MyBatis核心配置(如mapUnderscoreToCamelCase)
@Bean
public Configuration mybatisConfig() {
Configuration configuration = new Configuration();
configuration.setMapUnderscoreToCamelCase(true); // 驼峰命名自动映射
return configuration;
}
}
从库配置类(类似主库,修改扫描路径和数据源引用即可):
@Configuration
@MapperScan(basePackages = "com.example.mapper.secondary", sqlSessionFactoryRef = "secondarySqlSessionFactory")
public class SecondaryMyBatisConfig {
@Bean("secondarySqlSessionFactory")
public SqlSessionFactory secondarySqlSessionFactory(@Qualifier("secondaryDataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
sessionFactory.setDataSource(dataSource);
sessionFactory.setMapperLocations(
new PathMatchingResourcePatternResolver().getResources("classpath:mapper/secondary/*.xml")
);
return sessionFactory.getObject();
}
}
4. 配置事务管理器
每个数据源需要独立的事务管理器,否则事务会混乱!
@Configuration
public class TransactionManagerConfig {
// 主库事务管理器
@Primary
@Bean("primaryTransactionManager")
public PlatformTransactionManager primaryTransactionManager(@Qualifier("primaryDataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
// 从库事务管理器
@Bean("secondaryTransactionManager")
public PlatformTransactionManager secondaryTransactionManager(@Qualifier("secondaryDataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
5. 在Service中指定数据源
通过@Qualifier
注入对应的SqlSessionTemplate
(或Mapper),或用@Transactional
指定事务管理器。
@Service
public class UserService {
// 主库Mapper(自动注入primarySqlSessionFactory对应的Bean)
@Autowired
private UserPrimaryMapper userPrimaryMapper;
// 从库Mapper
@Autowired
private UserSecondaryMapper userSecondaryMapper;
// 操作主库(写)
public User insertUser(User user) {
userPrimaryMapper.insert(user); // 插入主库
return user;
}
// 操作从库(读)
public User selectFromSecondary(Long id) {
return userSecondaryMapper.selectById(id); // 查询从库
}
}
注意:如果用
@Transactional
,必须指定transactionManager
属性!@Transactional(transactionManager = "primaryTransactionManager") // 主库事务 public void updateUser(User user) { userPrimaryMapper.updateById(user); }
三、动态多数据源:运行时灵活切换
适合需要根据业务逻辑动态选择数据源的场景(比如多租户SaaS系统,根据租户ID切换数据库)。
1. 核心原理:AbstractRoutingDataSource
Spring提供的AbstractRoutingDataSource
是抽象类,通过重写determineCurrentLookupKey()
方法动态选择数据源。它内部维护了一个targetDataSources
映射(数据源key → 具体DataSource),运行时根据当前线程的lookupKey
选择数据源。
2. 实现步骤
步骤1:用ThreadLocal保存当前数据源key
线程隔离是关键!用ThreadLocal
存储当前线程的数据源key,避免多线程干扰。
public class DataSourceContextHolder {
// ThreadLocal保存当前数据源key(如"primary"/"tenant_123")
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
// 设置key
public static void setDataSourceKey(String key) {
CONTEXT_HOLDER.set(key);
}
// 获取key
public static String getDataSourceKey() {
return CONTEXT_HOLDER.get();
}
// 清理key(防止内存泄漏!)
public static void clear() {
CONTEXT_HOLDER.remove();
}
}
步骤2:自定义动态数据源
继承AbstractRoutingDataSource
,重写determineCurrentLookupKey()
,从ThreadLocal
获取当前key。
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
// 从ThreadLocal获取当前数据源key
return DataSourceContextHolder.getDataSourceKey();
}
}
步骤3:配置多数据源与动态路由
在配置类中注册所有数据源,并将它们注入DynamicRoutingDataSource
的targetDataSources
映射中。
@Configuration
public class DynamicDataSourceConfig {
@Bean("dynamicRoutingDataSource")
public DynamicRoutingDataSource dynamicRoutingDataSource(
@Qualifier("primaryDataSource") DataSource primaryDataSource,
@Qualifier("secondaryDataSource") DataSource secondaryDataSource,
// 如果有其他数据源(如租户库),继续注入...
@Qualifier("tenant1DataSource") DataSource tenant1DataSource) {
DynamicRoutingDataSource routingDataSource = new DynamicRoutingDataSource();
// 设置默认数据源(未匹配key时使用)
routingDataSource.setDefaultTargetDataSource(primaryDataSource);
// 注册所有数据源(key是数据源标识,value是具体的DataSource)
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("primary", primaryDataSource);
targetDataSources.put("secondary", secondaryDataSource);
targetDataSources.put("tenant_123", tenant1DataSource); // 示例租户库
routingDataSource.setTargetDataSources(targetDataSources);
return routingDataSource;
}
}
步骤4:用AOP自动切换数据源
定义切面,在方法执行前根据注解切换数据源,执行后清理ThreadLocal
。
第一步:定义注解
@Target(ElementType.METHOD) // 作用在方法上
@Retention(RetentionPolicy.RUNTIME) // 运行时保留
public @interface TargetDataSource {
String value(); // 数据源key(如"primary"/"tenant_123")
}
第二步:编写切面
@Aspect
@Component
public class DataSourceAspect {
// 切点:标注了@TargetDataSource的方法
@Pointcut("@annotation(com.example.annotation.TargetDataSource)")
public void dataSourcePointcut() {}
// 方法执行前切换数据源
@Before("dataSourcePointcut()")
public void before(JoinPoint joinPoint) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
TargetDataSource annotation = method.getAnnotation(TargetDataSource.class);
if (annotation != null) {
String dataSourceKey = annotation.value();
DataSourceContextHolder.setDataSourceKey(dataSourceKey); // 设置当前数据源key
}
}
// 方法执行后清理ThreadLocal
@After("dataSourcePointcut()")
public void after() {
DataSourceContextHolder.clear(); // 防止线程复用导致key残留
}
}
步骤5:在Service中使用
在需要切换数据源的方法上添加@TargetDataSource
注解,指定key即可。
@Service
public class TenantService {
@Autowired
private TenantMapper tenantMapper; // 该Mapper绑定动态数据源
// 操作租户123的数据库
@TargetDataSource("tenant_123")
public Tenant getTenantInfo(Long tenantId) {
return tenantMapper.selectById(tenantId);
}
// 操作默认主库
public User addUser(User user) {
return userMapper.insert(user); // 未标注注解,使用默认数据源(primary)
}
}
四、避坑指南:常见问题与解决
1. 事务失效(最常见!)
现象:跨数据源操作时,@Transactional
不生效,数据没提交。
原因:
- 未为
@Transactional
指定transactionManager
(默认只关联一个数据源的事务管理器)。 - 动态数据源切换在事务开启之后(事务开启时数据源已确定,后续切换无效)。
解决:
- 单数据源事务:显式指定
transactionManager
(如@Transactional(transactionManager = "primaryTransactionManager")
)。 - 跨数据源事务:必须用分布式事务(如Seata),本地事务无法保证原子性。
2. 线程安全问题(内存泄漏)
现象:线程复用时,数据源key错误(比如A线程设置了tenant_123
,B线程复用该线程后仍用这个key)。
原因:ThreadLocal
未清理,导致线程池中的线程复用旧数据。
解决:
- 在切面的
@After
方法中调用DataSourceContextHolder.clear()
,确保每次方法执行后清理。 - 避免在
@Async
异步方法中使用动态数据源(除非手动传递ThreadLocal
)。
3. MyBatis Mapper扫描冲突
现象:启动时报错No qualifying bean of type 'xxxMapper' available
。
原因:多个SqlSessionFactory
的Mapper包路径重叠,Spring无法确定注入哪个。
解决:
- 在
@MapperScan
中明确指定sqlSessionFactoryRef
(如sqlSessionFactoryRef = "primarySqlSessionFactory"
)。 - 确保不同数据源的Mapper接口放在不同包路径(如
com.example.mapper.primary
和com.example.mapper.secondary
)。
4. 性能问题(连接池耗尽)
现象:高并发下报Cannot get a connection from the pool
。
原因:动态数据源切换频繁,或连接池参数配置不合理(如最大连接数太小)。
解决:
- 调整连接池参数(如Hikari的
maximum-pool-size
设为CPU核心数*2+1
)。 - 减少不必要的动态切换(比如高频操作固定使用主库)。
五、总结
Spring Boot多数据源配置的核心是:
- 静态多数据源:适合数据源固定的场景,通过
@Primary
和@Qualifier
区分。 - 动态多数据源:适合运行时切换的场景,依赖
AbstractRoutingDataSource
和ThreadLocal
,配合AOP自动切换。
实际开发中,读写分离推荐静态配置(逻辑简单,性能稳定),多租户/分库分表推荐动态配置(灵活扩展)。无论哪种方案,都要注意事务边界和线程安全,避免踩坑!
如果觉得有用,记得点赞收藏,评论区留言讨论你的多数据源场景~ 😊

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐
所有评论(0)