1. 多数据源场景的现实需求
在企业级应用开发中,我们经常会遇到需要同时操作多个数据库的场景。比如电商系统中,订单数据可能存储在MySQL集群,用户行为日志可能写入Elasticsearch,而风控数据则保存在Oracle数据库。这种多数据源并存的需求已经成为现代分布式系统的标配。
我去年参与的一个供应链管理系统就面临这样的挑战:需要同时连接ERP系统的SQL Server、WMS系统的MySQL和第三方物流平台的PostgreSQL。最初尝试用动态数据源切换方案,结果在高并发下频繁出现连接泄漏问题。后来改用多数据源并存方案才彻底解决。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 多数据源并存的核心实现方案
2.1 基础配置方式
最直接的方式是为每个数据源创建独立的配置类。以SpringBoot为例,我们需要:
java复制@Configuration
@MapperScan(basePackages = "com.example.mapper.db1", sqlSessionTemplateRef = "db1SqlSessionTemplate")
public class Db1DataSourceConfig {
@Bean(name = "db1DataSource")
@ConfigurationProperties(prefix = "spring.datasource.db1")
public DataSource db1DataSource() {
return DataSourceBuilder.create().build();
}
@Bean(name = "db1SqlSessionFactory")
public SqlSessionFactory db1SqlSessionFactory(@Qualifier("db1DataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
bean.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources("classpath:mapper/db1/*.xml"));
return bean.getObject();
}
// 同理配置其他Bean...
}
这种方案的优点是:
- 各数据源完全独立,互不干扰
- 配置直观,易于维护
- 支持不同的数据库类型和连接池配置
2.2 事务管理要点
多数据源环境下的事务管理需要特别注意。Spring的@Transactional默认只能管理一个数据源的事务。如果需要跨数据源事务,可以考虑:
- 使用JTA实现分布式事务(性能开销大)
- 采用最终一致性方案(如消息队列)
- 为每个数据源配置独立的事务管理器
java复制@Bean(name = "db1TransactionManager")
public DataSourceTransactionManager db1TransactionManager(@Qualifier("db1DataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
使用时需要明确指定事务管理器:
java复制@Transactional(transactionManager = "db1TransactionManager")
public void businessMethod() {
// ...
}
3. 动态数据源的实现原理
3.1 核心设计思路
动态数据源的核心在于运行时切换数据源。通过继承AbstractRoutingDataSource并重写determineCurrentLookupKey方法实现:
java复制public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSourceType();
}
}
配合ThreadLocal保存当前线程的数据源标识:
java复制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.2 实际应用中的坑点
在实际项目中,动态数据源最容易出现以下问题:
- 连接泄漏:切换数据源后未正确关闭连接
- 线程污染:异步任务中线程复用导致数据源错乱
- 事务失效:跨数据源事务处理不当
我曾遇到一个典型案例:在@Async方法中使用动态数据源,由于线程池复用导致后续任务使用了错误的数据源。解决方案是每次任务执行前后显式清理上下文:
java复制@Async
public void asyncTask() {
try {
DataSourceContextHolder.setDataSource("db1");
// 业务逻辑
} finally {
DataSourceContextHolder.clear();
}
}
4. 两种方案的对比选型
4.1 适用场景分析
| 维度 | 多数据源并存 | 动态数据源 |
|---|---|---|
| 数据源数量 | 适合固定少量(2-5个) | 适合大量动态数据源 |
| 数据库类型 | 支持异构数据库 | 通常要求同类型数据库 |
| 性能表现 | 更稳定,连接池独立管理 | 存在路由开销 |
| 事务管理 | 可配置独立事务管理器 | 跨数据源事务复杂 |
| 维护成本 | 配置明确,易于排查问题 | 需要处理线程安全问题 |
4.2 选型建议
根据我的项目经验:
- 当数据源数量固定且较少时,优先选择多数据源并存方案
- 当需要支持租户隔离或多环境动态切换时,考虑动态数据源
- 对性能要求苛刻的场景,多数据源方案通常更可靠
一个折中方案是混合使用:核心业务用固定多数据源,辅助功能使用动态数据源。例如在SAAS系统中:
- 用户主数据用固定数据源保证稳定性
- 租户特定数据用动态数据源实现隔离
5. 实战中的性能优化技巧
5.1 连接池配置要点
多数据源环境下,连接池配置尤为关键。建议:
- 不同业务数据源使用独立的连接池配置
- 根据业务特点调整参数:
- 查询密集型:增大maxActive
- 短事务型:减小maxIdle
- 监控连接使用情况
yaml复制spring:
datasource:
db1:
hikari:
maximum-pool-size: 20
minimum-idle: 5
idle-timeout: 30000
db2:
hikari:
maximum-pool-size: 10
minimum-idle: 2
5.2 MyBatis优化建议
- 为每个数据源配置独立的Mapper接口包和XML路径
- 使用@MapperScan的sqlSessionTemplateRef属性明确绑定
- 不同数据源可以使用不同的MyBatis配置
java复制@Bean(name = "db1SqlSessionFactory")
public SqlSessionFactory db1SqlSessionFactory(@Qualifier("db1DataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
// 自定义配置
org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
configuration.setMapUnderscoreToCamelCase(true);
configuration.setDefaultFetchSize(100);
bean.setConfiguration(configuration);
return bean.getObject();
}
6. 常见问题排查指南
6.1 典型错误场景
-
启动报错:
Failed to configure a DataSource: 'url' attribute is not specified- 原因:未正确配置多数据源时SpringBoot自动配置冲突
- 解决:在主类排除自动配置:
java复制@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
-
Mapper注入冲突:
- 现象:
No qualifying bean of type 'com.example.mapper.UserMapper' available - 原因:多个SqlSessionFactory扫描了相同的Mapper接口
- 解决:确保每个Mapper接口只被一个SqlSessionFactory扫描
- 现象:
6.2 监控与日志
建议为每个数据源配置独立的日志和监控:
- 使用Druid的监控页面
- 为不同数据源配置Logback的独立日志文件
- 集成Prometheus监控各数据源连接池状态
xml复制<!-- logback配置示例 -->
<appender name="DB1_LOGFILE" class="ch.qos.logback.core.FileAppender">
<file>logs/db1-sql.log</file>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<logger name="com.example.mapper.db1" level="DEBUG" additivity="false">
<appender-ref ref="DB1_LOGFILE"/>
</logger>
7. 高级应用场景
7.1 读写分离实现
结合多数据源与Spring AOP实现读写分离:
java复制@Aspect
@Component
public class DataSourceAspect {
@Before("execution(* com.example.mapper..*.select*(..)) || " +
"execution(* com.example.mapper..*.get*(..)) || " +
"execution(* com.example.mapper..*.find*(..))")
public void setReadDataSource() {
DataSourceContextHolder.setReadDataSource();
}
@Before("execution(* com.example.mapper..*.insert*(..)) || " +
"execution(* com.example.mapper..*.update*(..)) || " +
"execution(* com.example.mapper..*.delete*(..))")
public void setWriteDataSource() {
DataSourceContextHolder.setWriteDataSource();
}
}
7.2 多租户架构
在多租户系统中,可以结合两种方案:
- 固定数据源处理系统级数据
- 动态数据源按租户路由
java复制public class TenantDataSourceRouter {
public static String determineDataSource(String tenantId) {
// 根据租户ID返回对应的数据源key
if("tenant_a".equals(tenantId)) {
return "db_tenant_a";
}
// ...
}
}
在实际项目中,我建议采用"固定数据源+动态数据源"的混合模式。比如用户认证等核心功能使用固定数据源保证稳定性,而租户业务数据使用动态数据源实现灵活扩展。这种架构既保证了核心模块的可靠性,又兼顾了业务扩展的灵活性。
