1. Spring Boot多数据源连接与切换实战指南
在复杂的企业级应用开发中,单一数据源往往无法满足业务需求。我最近在金融行业项目中就遇到了需要同时连接Oracle、MySQL和PostgreSQL三种数据库的场景。Spring Boot的自动配置特性虽然简化了单数据源配置,但官方文档对多数据源的支持却着墨不多。本文将分享我通过AbstractRoutingDataSource实现动态数据源切换的完整方案,包含生产环境中验证过的配置细节和性能优化技巧。
2. 多数据源核心设计解析
2.1 典型应用场景分析
在我参与过的电商系统中,订单数据存放在MySQL集群,用户画像数据存储在MongoDB,而日志数据则写入Elasticsearch。这种异构数据存储架构需要应用层具备动态切换数据源的能力。多数据源配置主要解决以下问题:
- 读写分离:主库写从库读(如MySQL主从架构)
- 分库分表:按业务垂直拆分(用户库、订单库)
- 多租户架构:每个租户独立数据库
- 异构数据源:混合使用关系型和NoSQL数据库
2.2 技术选型对比
Spring生态中实现多数据源主要有三种方案:
- 多个JdbcTemplate:简单但无法实现方法级别切换
- MyBatis多SqlSessionFactory:配置复杂且与ORM框架强耦合
- AbstractRoutingDataSource(推荐方案):
- 基于AOP实现透明切换
- 与持久层框架解耦
- 支持运行时动态增减数据源
提示:在微服务架构下,如果数据源数量超过5个,建议考虑引入数据库中间件(如ShardingSphere)而非应用层实现。
3. 完整实现步骤
3.1 基础环境配置
首先在pom.xml中添加必要依赖(Spring Boot 2.7.x示例):
xml复制<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>4.0.3</version>
</dependency>
3.2 多数据源配置类
创建主从两个数据源的配置类(以MySQL为例):
java复制@Configuration
public class DataSourceConfig {
@Bean
@ConfigurationProperties("spring.datasource.master")
public DataSource masterDataSource() {
return DataSourceBuilder.create().type(HikariDataSource.class).build();
}
@Bean
@ConfigurationProperties("spring.datasource.slave")
public DataSource slaveDataSource() {
return DataSourceBuilder.create().type(HikariDataSource.class).build();
}
}
对应的application.yml配置:
yaml复制spring:
datasource:
master:
jdbc-url: jdbc:mysql://master-host:3306/db
username: root
password: master-pwd
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
maximum-pool-size: 20
slave:
jdbc-url: jdbc:mysql://slave-host:3306/db
username: readuser
password: read-pwd
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
maximum-pool-size: 30
3.3 动态数据源路由实现
核心路由逻辑通过继承AbstractRoutingDataSource实现:
java复制public class DynamicDataSource extends AbstractRoutingDataSource {
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
public static void setDataSourceKey(String key) {
CONTEXT_HOLDER.set(key);
}
public static void clearDataSourceKey() {
CONTEXT_HOLDER.remove();
}
@Override
protected Object determineCurrentLookupKey() {
return CONTEXT_HOLDER.get();
}
@Bean
public DataSource dynamicDataSource(
@Qualifier("masterDataSource") DataSource master,
@Qualifier("slaveDataSource") DataSource slave) {
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("master", master);
targetDataSources.put("slave", slave);
DynamicDataSource routing = new DynamicDataSource();
routing.setDefaultTargetDataSource(master);
routing.setTargetDataSources(targetDataSources);
routing.afterPropertiesSet();
return routing;
}
}
3.4 AOP切面实现自动切换
通过自定义注解实现方法级别的数据源切换:
java复制@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
String value() default "master";
}
@Aspect
@Component
@Order(-1) // 确保在事务注解前执行
public class DataSourceAspect {
@Before("@annotation(dataSource)")
public void beforeSwitch(JoinPoint point, DataSource dataSource) {
DynamicDataSource.setDataSourceKey(dataSource.value());
}
@After("@annotation(dataSource)")
public void afterSwitch(JoinPoint point, DataSource dataSource) {
DynamicDataSource.clearDataSourceKey();
}
}
使用示例:
java复制@Service
public class UserService {
@DataSource("master")
public void createUser(User user) {
// 写入主库
}
@DataSource("slave")
public User getUser(Long id) {
// 从从库读取
}
}
4. 生产环境优化实践
4.1 连接池参数调优
根据实际压测结果调整HikariCP参数:
yaml复制spring:
datasource:
master:
hikari:
minimum-idle: 5
maximum-pool-size: 20
idle-timeout: 30000
max-lifetime: 1800000
connection-timeout: 30000
connection-test-query: SELECT 1
4.2 事务管理增强
多数据源环境下需要特别处理事务传播:
java复制@Configuration
@EnableTransactionManagement
public class TransactionConfig {
@Bean
public PlatformTransactionManager transactionManager(
@Qualifier("dynamicDataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
重要:@Transactional注解会锁定初始数据源,如需在事务内切换数据源,需要手动管理事务边界。
4.3 健康检查集成
暴露数据源健康状态到Actuator端点:
java复制@Configuration
public class HealthConfig {
@Bean
public DataSourceHealthIndicator masterHealthIndicator(
@Qualifier("masterDataSource") DataSource dataSource) {
return new DataSourceHealthIndicator(dataSource, "SELECT 1");
}
@Bean
public DataSourceHealthIndicator slaveHealthIndicator(
@Qualifier("slaveDataSource") DataSource dataSource) {
return new DataSourceHealthIndicator(dataSource, "SELECT 1");
}
}
5. 常见问题排查指南
5.1 连接泄漏问题
现象:应用运行一段时间后出现连接池耗尽。解决方案:
- 确保所有@DataSource注解方法都有对应的切面清理
- 添加连接泄漏检测:
yaml复制spring:
datasource:
hikari:
leak-detection-threshold: 5000 # 5秒未关闭连接视为泄漏
5.2 事务失效场景
当遇到事务不生效时,检查:
- 是否在同类内部方法调用(需通过AOP代理调用)
- 是否在切换数据源后开启事务(应先@DataSource再@Transactional)
5.3 性能优化建议
- 为只读从库配置更大的连接池
- 对高频访问方法使用本地缓存减少数据源切换
- 监控各数据源连接池使用情况:
java复制HikariDataSource master = (HikariDataSource)masterDataSource();
log.info("Master pool active: {}", master.getHikariPoolMXBean().getActiveConnections());
6. 高级扩展方案
6.1 动态注册数据源
运行时新增数据源实现:
java复制public void addDataSource(String key, DataSource dataSource) {
DynamicDataSource ds = (DynamicDataSource)dynamicDataSource;
Map<Object, Object> target = new HashMap<>(ds.getTargetDataSources());
target.put(key, dataSource);
ds.setTargetDataSources(target);
ds.afterPropertiesSet();
}
6.2 基于分库算法的自动路由
实现ShardingSphere风格的算法路由:
java复制@Override
protected Object determineCurrentLookupKey() {
Long userId = UserContext.getCurrentUserId();
return userId % 2 == 0 ? "ds0" : "ds1";
}
6.3 多租户隔离方案
结合Spring Security实现租户隔离:
java复制@PreAuthorize("hasTenantAccess(#tenantId)")
@DataSource("#tenantId")
public List<Data> getTenantData(String tenantId) {
// 自动路由到对应租户数据库
}
在实际项目中,我建议根据具体业务场景选择合适的技术方案。对于简单的读写分离,本文介绍的基础实现已经足够;如果是复杂的多租户SaaS系统,可能需要结合Spring Cloud等分布式方案进行扩展。
