1. Spring Boot多数据源连接与切换实战指南
在企业级应用开发中,经常需要同时访问多个数据库。比如电商系统要同时操作订单库和用户库,或者报表系统需要从多个业务系统抽取数据。传统的单数据源配置显然无法满足这类需求。本文将基于Spring Boot 2.7.x版本,手把手教你实现多数据源的配置、连接和动态切换。
注意:本文默认读者已掌握Spring Boot基础开发知识,熟悉Spring Data JPA或MyBatis等持久层框架的基本使用。
1.1 为什么需要多数据源?
多数据源场景在实际开发中非常普遍,主要包括以下几种情况:
- 业务拆分需求:用户数据和订单数据分别存储在不同数据库实例
- 读写分离:写操作走主库,读操作走从库
- 多租户系统:每个租户有独立的数据存储
- 异构数据源:同时连接MySQL、Oracle等不同类型的数据库
传统单数据源方案通过application.yml配置一个DataSource,Spring Boot会自动配置事务管理和JdbcTemplate等组件。但在多数据源场景下,我们需要手动管理多个DataSource及其相关组件。
2. 多数据源核心实现方案
2.1 基础架构设计
实现多数据源的核心思路是:
- 配置多个DataSource Bean
- 为每个DataSource配置独立的TransactionManager
- 实现动态数据源路由机制
- 通过AOP或注解实现数据源切换
java复制// 伪代码示例
@Configuration
public class DataSourceConfig {
@Bean
@Primary
public DataSource masterDataSource() {
// 主数据源配置
}
@Bean
public DataSource slaveDataSource() {
// 从数据源配置
}
}
2.2 数据源配置详解
以MySQL+HikariCP连接池为例,下面是典型的多数据源配置:
yaml复制# application.yml
spring:
datasource:
master:
jdbc-url: jdbc:mysql://localhost:3306/master_db
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
pool-name: MasterHikariCP
maximum-pool-size: 20
slave:
jdbc-url: jdbc:mysql://localhost:3306/slave_db
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
pool-name: SlaveHikariCP
maximum-pool-size: 15
提示:HikariCP是Spring Boot 2.x默认的连接池,性能优于传统的Tomcat JDBC和DBCP2。
2.3 动态数据源路由实现
核心是继承AbstractRoutingDataSource实现动态路由:
java复制public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSourceType();
}
}
public class DataSourceContextHolder {
private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
public static void setDataSourceType(String dataSourceType) {
contextHolder.set(dataSourceType);
}
public static String getDataSourceType() {
return contextHolder.get();
}
public static void clearDataSourceType() {
contextHolder.remove();
}
}
3. 完整实现步骤
3.1 配置多个数据源
java复制@Configuration
@MapperScan(basePackages = "com.example.mapper")
public class DataSourceConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.master")
public DataSource masterDataSource() {
return DataSourceBuilder.create().type(HikariDataSource.class).build();
}
@Bean
@ConfigurationProperties(prefix = "spring.datasource.slave")
public DataSource slaveDataSource() {
return DataSourceBuilder.create().type(HikariDataSource.class).build();
}
@Bean
@Primary
public DataSource dynamicDataSource(DataSource masterDataSource, DataSource slaveDataSource) {
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("master", masterDataSource);
targetDataSources.put("slave", slaveDataSource);
DynamicDataSource dynamicDataSource = new DynamicDataSource();
dynamicDataSource.setTargetDataSources(targetDataSources);
dynamicDataSource.setDefaultTargetDataSource(masterDataSource);
return dynamicDataSource;
}
@Bean
public PlatformTransactionManager transactionManager(DataSource dynamicDataSource) {
return new DataSourceTransactionManager(dynamicDataSource);
}
}
3.2 实现数据源切换注解
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 beforeSwitchDataSource(JoinPoint point, DataSource dataSource) {
String dsName = dataSource.value();
DataSourceContextHolder.setDataSourceType(dsName);
}
@After("@annotation(dataSource)")
public void afterSwitchDataSource(JoinPoint point, DataSource dataSource) {
DataSourceContextHolder.clearDataSourceType();
}
}
3.3 使用示例
java复制@Service
public class UserService {
@Autowired
private UserMapper userMapper;
// 默认使用主数据源
public User getUserById(Long id) {
return userMapper.selectById(id);
}
// 指定使用从数据源
@DataSource("slave")
public List<User> getAllUsers() {
return userMapper.selectAll();
}
}
4. 高级应用与优化
4.1 读写分离实现
结合Spring AOP实现自动读写分离:
java复制@Aspect
@Component
public class ReadWriteDataSourceAspect {
@Before("execution(* com.example.mapper.*.select*(..)) || " +
"execution(* com.example.mapper.*.get*(..)) || " +
"execution(* com.example.mapper.*.find*(..)) || " +
"execution(* com.example.mapper.*.query*(..))")
public void setReadDataSourceType() {
DataSourceContextHolder.setDataSourceType("slave");
}
@Before("execution(* com.example.mapper.*.insert*(..)) || " +
"execution(* com.example.mapper.*.update*(..)) || " +
"execution(* com.example.mapper.*.delete*(..)) || " +
"execution(* com.example.mapper.*.save*(..))")
public void setWriteDataSourceType() {
DataSourceContextHolder.setDataSourceType("master");
}
@AfterReturning("execution(* com.example.mapper.*.*(..))")
public void clearDataSourceType() {
DataSourceContextHolder.clearDataSourceType();
}
}
4.2 多数据源事务管理
多数据源环境下的事务管理需要特别注意:
- 每个数据源需要独立的PlatformTransactionManager
- 使用JTA实现分布式事务(如Atomikos)
- 或者采用最终一致性方案替代强一致性
java复制@Configuration
public class TransactionConfig {
@Bean
public PlatformTransactionManager masterTransactionManager(@Qualifier("masterDataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
@Bean
public PlatformTransactionManager slaveTransactionManager(@Qualifier("slaveDataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
5. 常见问题与解决方案
5.1 数据源切换不生效
现象:添加了@DataSource注解但数据源没有切换
排查步骤:
- 检查AOP是否生效,确保切面类被Spring管理
- 检查注解是否加在实现类而非接口上
- 确认方法调用是通过Spring代理而非直接调用
5.2 事务与数据源切换冲突
现象:在事务方法中切换数据源无效
原因:Spring事务的传播行为会在方法开始时确定数据源
解决方案:
- 将数据源切换放在事务方法外层
- 使用编程式事务管理
- 调整事务传播行为为REQUIRES_NEW
5.3 连接池配置优化
针对高并发场景的连接池参数建议:
| 参数 | 主库建议值 | 从库建议值 | 说明 |
|---|---|---|---|
| maximumPoolSize | 20-50 | 30-100 | 根据服务器CPU核心数调整 |
| minimumIdle | 5-10 | 10-20 | 保持适量的空闲连接 |
| idleTimeout | 600000 | 300000 | 从库可以设置更短的超时 |
| maxLifetime | 1800000 | 1800000 | 连接最大存活时间 |
| connectionTimeout | 30000 | 30000 | 连接超时时间 |
5.4 MyBatis多数据源配置
如果使用MyBatis,需要为每个数据源配置独立的SqlSessionFactory:
java复制@Configuration
public class MyBatisConfig {
@Bean
@Primary
public SqlSessionFactory masterSqlSessionFactory(@Qualifier("masterDataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
bean.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources("classpath:mapper/master/*.xml"));
return bean.getObject();
}
@Bean
public SqlSessionFactory slaveSqlSessionFactory(@Qualifier("slaveDataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
bean.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources("classpath:mapper/slave/*.xml"));
return bean.getObject();
}
}
6. 性能优化建议
- 连接池监控:集成Micrometer监控HikariCP指标
- 数据源健康检查:实现HealthIndicator监控各数据源状态
- 故障转移:实现从库故障自动切换到主库
- 负载均衡:多个从库时实现读请求的负载均衡
java复制@Component
public class DataSourceHealthIndicator implements HealthIndicator {
@Autowired
@Qualifier("masterDataSource")
private DataSource masterDataSource;
@Autowired
@Qualifier("slaveDataSource")
private DataSource slaveDataSource;
@Override
public Health health() {
Map<String, Object> details = new HashMap<>();
details.put("master", checkDataSource(masterDataSource));
details.put("slave", checkDataSource(slaveDataSource));
return Health.up().withDetails(details).build();
}
private String checkDataSource(DataSource dataSource) {
try (Connection conn = dataSource.getConnection()) {
return "OK";
} catch (Exception e) {
return "DOWN: " + e.getMessage();
}
}
}
在实际项目中,我遇到过从库延迟导致数据不一致的问题。解决方案是在配置从库数据源时添加连接参数:
yaml复制spring:
datasource:
slave:
jdbc-url: jdbc:mysql://slave:3306/db?useUnicode=true&characterEncoding=utf8&autoReconnect=true&failOverReadOnly=false&roundRobinLoadBalance=true
这样可以实现多个从库的轮询负载均衡,同时设置failOverReadOnly=false确保从库不可用时不会自动切换到主库,避免意外的写操作发到从库。
