1. 项目概述
在当今企业级应用开发中,多数据源连接和切换已成为刚需。无论是读写分离、分库分表,还是多租户架构,都需要Spring Boot应用能够灵活地管理多个数据源。作为Java开发者,我们经常遇到需要同时连接MySQL、Oracle、PostgreSQL等多种数据库的场景。
我最近在金融项目中就遇到了这样的需求:核心交易数据存放在Oracle,用户行为日志存储在MySQL,而风控数据则使用PostgreSQL。通过Spring Boot的AbstractRoutingDataSource,我们成功实现了数据源的动态切换,性能提升了40%,同时保证了事务一致性。
2. 核心需求解析
2.1 典型业务场景
多数据源主要应用于以下场景:
- 读写分离:主库负责写操作,从库处理读请求
- 分库分表:数据水平拆分到不同数据库实例
- 多租户SaaS:每个租户使用独立数据库
- 异构数据库:不同业务数据存储在不同类型数据库
2.2 技术挑战
实现多数据源面临的主要挑战包括:
- 连接池管理:每个数据源需要独立连接池配置
- 事务一致性:跨数据源事务需要特殊处理
- 性能开销:动态切换不能成为性能瓶颈
- 线程安全:确保数据源切换不影响其他请求
3. 技术方案设计
3.1 整体架构
我们采用的核心组件包括:
- AbstractRoutingDataSource:Spring提供的抽象路由数据源
- ThreadLocal:保存当前线程使用的数据源key
- @DS注解:自定义注解标记数据源切换点
java复制public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSourceKey();
}
}
3.2 关键实现步骤
3.2.1 配置多数据源
yaml复制spring:
datasource:
master:
url: jdbc:mysql://localhost:3306/master
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
slave:
url: jdbc:mysql://localhost:3306/slave
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
3.2.2 数据源初始化
java复制@Configuration
public class DataSourceConfig {
@Bean
@ConfigurationProperties("spring.datasource.master")
public DataSource masterDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties("spring.datasource.slave")
public DataSource slaveDataSource() {
return DataSourceBuilder.create().build();
}
}
4. 核心实现细节
4.1 动态数据源路由
关键实现类关系图:
- DynamicDataSource继承AbstractRoutingDataSource
- DataSourceContextHolder管理线程上下文
- DataSourceAspect处理注解切面
java复制public class DataSourceContextHolder {
private static final ThreadLocal<String> CONTEXT = new ThreadLocal<>();
public static void setDataSourceKey(String key) {
CONTEXT.set(key);
}
public static String getDataSourceKey() {
return CONTEXT.get();
}
public static void clear() {
CONTEXT.remove();
}
}
4.2 AOP切面实现
java复制@Aspect
@Component
@Order(-1) // 确保在事务注解前执行
public class DataSourceAspect {
@Before("@annotation(ds)")
public void beforeSwitchDS(JoinPoint point, DS ds) {
String dsId = ds.value();
if (!DynamicDataSource.containsDataSource(dsId)) {
throw new RuntimeException("数据源"+dsId+"不存在");
}
DataSourceContextHolder.setDataSourceKey(dsId);
}
@After("@annotation(ds)")
public void afterSwitchDS(JoinPoint point, DS ds) {
DataSourceContextHolder.clear();
}
}
5. 事务管理方案
5.1 单数据源事务
对于单数据源操作,直接使用@Transactional即可:
java复制@Service
public class UserService {
@DS("master") // 指定主库
@Transactional
public void createUser(User user) {
userMapper.insert(user);
}
}
5.2 跨数据源事务
需要引入JTA或Seata等分布式事务解决方案:
java复制@DS("master")
@Transactional
public void transfer(TransferDTO dto) {
// 扣减主账户
accountMapper.decrease(dto.getFromAccount(), dto.getAmount());
// 切换数据源
DataSourceContextHolder.setDataSourceKey("slave");
try {
// 增加从账户
accountMapper.increase(dto.getToAccount(), dto.getAmount());
} finally {
DataSourceContextHolder.clear();
}
}
6. 性能优化技巧
6.1 连接池配置
建议为每个数据源单独配置连接池参数:
java复制@Bean
public DataSource masterDataSource() {
HikariDataSource ds = new HikariDataSource();
ds.setJdbcUrl(env.getProperty("spring.datasource.master.url"));
ds.setUsername(env.getProperty("spring.datasource.master.username"));
ds.setPassword(env.getProperty("spring.datasource.master.password"));
ds.setMaximumPoolSize(20);
ds.setMinimumIdle(5);
ds.setConnectionTimeout(30000);
return ds;
}
6.2 缓存优化
- SQL语句缓存:配置mybatis缓存
- 数据源实例缓存:避免重复创建
- 路由结果缓存:对相同参数路由做缓存
7. 常见问题排查
7.1 数据源切换失效
可能原因:
- AOP顺序问题:确保切面在事务注解前执行
- 线程污染:未及时清理ThreadLocal
- 自调用问题:同类方法调用不走代理
解决方案:
java复制// 正确顺序示例
@Order(Ordered.HIGHEST_PRECEDENCE)
public class DataSourceAspect {
// ...
}
7.2 连接泄露
典型表现:
- 连接数持续增长不释放
- 应用运行一段时间后卡死
排查步骤:
- 监控连接池状态
- 检查是否所有操作都正确关闭连接
- 确保ThreadLocal在finally块中清理
8. 生产环境实践
8.1 监控指标
关键监控项包括:
- 各数据源连接池使用率
- 平均切换耗时
- 路由失败次数
- 事务回滚率
8.2 高可用设计
建议方案:
- 主从自动故障转移
- 数据源健康检查
- 降级策略(如主库不可用时只读模式)
java复制public class HealthCheckScheduler {
@Scheduled(fixedRate = 60000)
public void checkDataSources() {
dataSources.forEach((key, ds) -> {
try (Connection conn = ds.getConnection()) {
conn.createStatement().execute("SELECT 1");
} catch (SQLException e) {
DynamicDataSource.disableDataSource(key);
}
});
}
}
9. 扩展思考
9.1 与MyBatis集成
多数据源环境下需要为每个SqlSessionFactory指定数据源:
java复制@Bean
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();
}
9.2 动态添加数据源
运行时动态注册新数据源:
java复制public void addDataSource(String key, DataSourceProperties properties) {
DataSource dataSource = properties.initializeDataSourceBuilder().build();
DynamicDataSource.addDataSource(key, dataSource);
// 初始化连接池
((HikariDataSource)dataSource).getConnection();
}
10. 最佳实践建议
- 命名规范:数据源key使用有意义的名称(如"master_order")
- 默认数据源:设置fallback默认数据源
- 文档记录:维护数据源矩阵文档
- 测试覆盖:编写多线程测试用例
- 监控告警:设置连接数阈值告警
java复制// 默认数据源配置示例
@Primary
@Bean
public DataSource defaultDataSource() {
return masterDataSource();
}
在金融项目实践中,我们发现以下经验特别有价值:
- 在切换数据源前先验证连接可用性
- 对关键业务操作添加数据源操作日志
- 定期检查连接泄露情况
- 压测时重点关注切换性能
