在实际项目中,一个系统常常需要连接多个不同类型的数据库,以适应不同的业务需求:
- 核心业务数据存储在 MySQL
- 海量日志和时序数据存储在 TDengine
- 复杂报表数据存储在 PostgreSQL
- 部分历史遗留数据存储在 Oracle
面对这种架构,我们通常并不需要在运行时动态“切换”数据源,而是希望:不同的业务模块固定绑定并使用不同的数据源。这种模式,我们称之为 多数据源并存。请注意,它的核心目标并非实现读写分离或主从切换,而是让多个数据源在同一应用中“各司其职”。
一、什么是「多数据源并存」?
其核心思想非常简单:不进行运行时切换,而是同时配置多个独立的 DataSource,每个数据源都明确绑定自己的 Mapper、SqlSessionFactory 和事务管理器。
在代码中的典型表现是:
@Resource(name = "mysqlDataSource")
private DataSource mysqlDataSource;
@Resource(name = "tdengineDataSource")
private DataSource tdengineDataSource;
这种模式的核心特点包括:
- 同时存在多个
DataSource Bean
- 每个
DataSource 拥有独立的配置
- 每个
DataSource 对应专属的 Mapper / Dao / Service 层
- 不依赖
AbstractRoutingDataSource 进行路由
- 无需使用
ThreadLocal 来保存上下文
- 数据源之间互不干扰,各自独立工作
- 在使用方式上,与单数据源完全一致
二、适合使用多数据源并存的场景
这种方案非常适合以下情况:
✅ 数据库类型异构:
- MySQL + TDengine
- MySQL + Oracle
- MySQL + MongoDB
✅ 数据职责分离:
- 业务主库(MySQL)
- 日志/监控库(如 Elasticsearch)
- 时序数据库(如 TDengine、InfluxDB)
- 分析/报表库(如 PostgreSQL)
✅ 不存在运行时切换依赖:
- 不需要根据请求动态切换主从
- 不需要实现读写分离的路由
- 不需要按租户隔离数据源
典型场景示例:设计一个无人机轨迹管理系统,时序数据每天可达百万级别。若全部存入 MySQL,数据库压力会急剧增大。此时,引入专门的时序数据库(如 TDengine)成为必然选择。
系统将同时使用两个数据库:MySQL 存储业务元数据,TDengine 存储轨迹时序数据。目标就是让它们和谐共存:
- MySQL 拥有自己的一套 Mapper
- TDengine 拥有自己的一套 Mapper
- 两者的配置完全隔离,互不干扰
- 在业务代码中使用方式高度统一
三、配置多个数据源(以Druid为例)
以下是一个典型的 application.yml 配置,同时定义了 MySQL 和 TDengine 数据源。
spring:
# 配置数据源
datasource:
# 1. MySQL 数据源(存业务元数据)
mysql:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.112.58:3306/uav-safety?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowMultiQueries=true&rewriteBatchedStatements=true
username: root
password: root
type: com.alibaba.druid.pool.DruidDataSource
# 连接池配置
initialSize: 10
min-idle: 5
max-active: 20
max-wait: 60000
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 300000
validation-query: SELECT 1
testWhileIdle: true
pool-prepared-statements: true
max-pool-prepared-statement-per-connection-size: 20
# 配置监控统计拦截的filters
filters: stat,wall,slf4j
# 2. TDengine 数据源(时序库)
tdengine:
driver-class-name: com.taosdata.jdbc.rs.RestfulDriver
url: jdbc:TAOS-RS://192.168.112.58:6041/uav_safety?useSSL=false
username: root
password: taosdata
type: com.alibaba.druid.pool.DruidDataSource
# 连接池配置
initialSize: 7
minIdle: 5
maxActive: 20
maxWait: 60000
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 300000
validationQuery: SELECT 1
testWhileIdle: true
pool-prepared-statements: true
max-pool-prepared-statement-per-connection-size: 50
# 配置监控统计拦截的filters
filters: stat
四、创建 MySQL 数据源配置类
@Configuration
@MapperScan(
basePackages = "com.za.uav.mapper.mysql",
sqlSessionFactoryRef = "mysqlSqlSessionFactory")
public class MysqlDataSourceConfig {
@Bean(name = "mysqlDataSource")
@ConfigurationProperties(prefix = "spring.datasource.mysql")
public DataSource mysqlDataSource() {
return new com.alibaba.druid.pool.DruidDataSource();
}
@Bean(name = "mysqlSqlSessionFactory")
public SqlSessionFactory mysqlSqlSessionFactory(
@Qualifier("mysqlDataSource") DataSource dataSource
) throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
return bean.getObject();
}
@Bean(name = "mysqlTransactionManager")
public DataTransactionManager mysqlTransactionManager(
@Qualifier("mysqlDataSource") DataSource dataSource
) {
return new DataSourceTransactionManager(dataSource);
}
}
关键点:
@MapperScan 指定扫描的包路径为 com.za.uav.mapper.mysql,所有 MySQL 相关的 Mapper 接口必须位于此包或其子包下。
@ConfigurationProperties 会自动将 application.yml 中 spring.datasource.mysql 前缀的配置注入到 DruidDataSource 中。
五、创建 TDengine 数据源配置类
@Configuration
@MapperScan(
basePackages = "com.za.uav.mapper.tdengine",
sqlSessionFactoryRef = "tdengineSqlSessionFactory")
public class TdengineDataSourceConfig {
@Bean(name = "tdengineDataSource")
@ConfigurationProperties(prefix = "spring.datasource.tdengine")
public DataSource tdengineDataSource() {
return new com.alibaba.druid.pool.DruidDataSource();
}
@Bean(name = "tdengineSqlSessionFactory")
public SqlSessionFactory tdengineSqlSessionFactory(
@Qualifier("tdengineDataSource") DataSource dataSource
) throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
return bean.getObject();
}
}
核心原则:通过不同的包路径和 Bean 名称进行严格区分。
完成以上配置,多数据源并存的基础架构就已搭建完毕。
六、事务管理器的重点事项
一个重要警示:在本例中,我们没有为 TDengine 配置事务管理器 Bean,因为它可能不支持或不需要传统的事务。但对于 MySQL 这类需要事务支持的数据源,事务管理器是必需的。
在单数据源项目中,通常只有一个全局事务管理器,使用 @Transactional 注解非常方便。但在多数据源并存的环境中,如果存在多个事务管理器,使用时必须显式指定使用哪一个。
例如,操作 MySQL 时:
@Transactional(transactionManager = "mysqlTransactionManager", rollbackFor = Exception.class)
public void saveMysql() {
mysqlMapper.insert(data);
}
建议:将此作为项目规范。只要在 Service 层使用 @Transactional 注解,就必须明确指定 transactionManager 属性,否则应视为一个潜在 Bug。
便捷方案:自定义事务注解
为了避免在代码中频繁书写冗长的事务管理器名称,可以自定义具有业务语义的注解。
- 为 MySQL 事务定义注解:
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Transactional(
transactionManager = "mysqlTransactionManager",
rollbackFor = Exception.class)
@Documented
public @interface MysqlTx {
}
- 为 TDengine 事务定义注解(如果支持):
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Transactional(
transactionManager = "tdengineTransactionManager",
rollbackFor = Exception.class)
@Documented
public @interface TdTx {
}
这样,我们就为标准的 Spring 事务注解创建了语义更清晰的“别名”。
跨数据源的事务问题
如果一个业务方法需要同时操作两个数据库,该如何处理?
public void saveAll() {
mysqlMapper.insert(...); // 操作MySQL
tdMapper.insert(...); // 操作TDengine
}
由于一个 @Transactional 注解只能绑定一个事务管理器,因此如果发生异常,只能保证其中一个数据源的操作回滚。例如,即使绑定了 mysqlTransactionManager,TDengine 的操作也不在该事务范围内,不会回滚。
常见的解决方案是业务补偿:
@Transactional(transactionManager = “mysqlTransactionManager”)
public void process() {
mysqlMapper.insert(order);
try {
tdMapper.insert(log); // TDengine操作
} catch (Exception e) {
// 执行补偿逻辑:删除MySQL中刚插入的数据,或记录异常单据
mysqlMapper.deleteById(order.getId());
throw e; // 重新抛出异常,触发MySQL回滚(如果需要)
}
}
七、关于 @MapperScan 的要点
在多数据源配置中,不再推荐使用 @Mapper 注解在接口上声明 Bean。更可靠的方式是在每个数据源配置类中使用 @MapperScan 注解,并明确指定 basePackages 和 sqlSessionFactoryRef。
同时,Mapper 接口文件和对应的 XML 映射文件也应按数据源进行物理隔离存放,例如:
src/main/java/com/xxx/mapper/mysql/ 存放 MySQL 的 Mapper 接口
src/main/java/com/xxx/mapper/tdengine/ 存放 TDengine 的 Mapper 接口
src/main/resources/mapper/mysql/ 存放 MySQL 的 XML 文件
src/main/resources/mapper/tdengine/ 存放 TDengine 的 XML 文件
如果项目中使用了 MyBatis-Plus,注意在创建 SqlSessionFactory 时需要使用 MybatisSqlSessionFactoryBean,并显式设置 Mapper XML 文件的位置:
@Bean(name = “mysqlSqlSessionFactory”)
public SqlSessionFactory mysqlSqlSessionFactory(@Qualifier(“mysqlDataSource”) DataSource dataSource) throws Exception {
MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean();
bean.setDataSource(dataSource);
bean.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources(“classpath:mapper/mysql/*.xml”));
return bean.getObject();
}
八、与「动态数据源」的核心区别
| 特性 |
多数据源并存 |
动态数据源 (AbstractRoutingDataSource) |
| 本质 |
多个独立的 DataSource Bean 同时存在 |
对外只有一个 DataSource Bean,内部根据规则路由 |
| 配置 |
每个数据源独立配置,界限清晰 |
集中配置多个数据源,由一个路由数据源管理 |
| 使用方式 |
通过不同的 Bean 名称注入,代码明确指定 |
通过 AOP + ThreadLocal 在运行时动态切换,对业务代码透明 |
| 事务管理 |
每个数据源有独立的事务管理器,需显式指定 |
事务管理复杂,需自定义支持多数据源的事务管理器 |
| 适用场景 |
数据源职责固定,无需运行时切换 (如:业务库+日志库) |
需要运行时动态切换 (如:读写分离、多租户、分库分表) |
| 核心难点 |
配置稍繁琐,需注意包和Bean的隔离 |
上下文传递、事务一致性、调试困难 |
九、在业务层中的使用
配置完成后,在业务层中的使用非常简单直观:
@Service
public class UserService {
@Resource
private UserMapper userMapper; // 自动注入 mysql Mapper
@Resource
private TdLogMapper tdLogMapper; // 自动注入 tdengine Mapper
@MysqlTx
public void saveUser(User user) {
userMapper.insert(user);
}
@TdTx
public void saveLog(Log log) {
tdLogMapper.insert(log);
}
}
对于业务开发人员而言,几乎感知不到底层连接了多个数据库,这就是多数据源并存模式带来的便利性。
常见问题与排查
-
Druid 监控面板不显示 TDengine 的 SQL 统计
这可能是因为在 filters 配置中包含了 wall(SQL防火墙)。Druid 的 WallFilter 主要针对 MySQL 等数据库的语法设计,可能不兼容 TDengine 的 SQL 方言,导致空指针异常。解决方案:在 TDengine 数据源的配置中,仅保留 stat(统计)过滤器,移除 wall。
filters: stat # 移除了 wall
-
抛出 Invalid bound statement (not found) 异常
这是一个 MyBatis 的经典错误,表示 Mapper 接口方法与 XML 中的 SQL 语句 id 无法绑定。在多数据源环境下,请按顺序排查:
- 检查包扫描:确认 Mapper 接口是否位于对应数据源配置类中
@MapperScan 指定的包路径下。
- 清理并重新编译:执行
mvn clean compile 或 IDE 的 Rebuild Project,确保编译后的 class 文件包含最新代码。
- 检查 XML 文件位置与命名空间:确认 XML 文件是否放在正确目录,且
<mapper> 标签的 namespace 属性与对应的 Mapper 接口全限定名完全一致。
- 检查 MyBatis-Plus 配置:如果使用 MyBatis-Plus,确保使用了
MybatisSqlSessionFactoryBean 并正确设置了 mapperLocations(如第七点所述)。
总结
许多开发者在初次接触多数据源需求时,脑海里冒出的第一个问题往往是:“我该如何动态切换数据源?” 于是立刻去寻找基于 AbstractRoutingDataSource、ThreadLocal 和 AOP 的动态数据源方案。
然而,在真实的项目架构设计中,我们更应该首先问自己:“这个系统真的需要在运行时切换数据源吗?还是仅仅需要多个数据源各司其职、并行工作?”
动态数据源解决的核心问题是 “如何根据规则在运行时无缝切换”,适用于读写分离、多租户等场景。
多数据源并存解决的核心问题是 “如何让多个数据源明确分工、稳定共存”,适用于异构数据库、职责分离的场景。
这两种方案各有优劣,适用于不同的业务场景。在构建复杂系统时,提升稳定性和可维护性的关键,往往不在于使用多么精巧复杂的技术,而在于为每个组件划定清晰的职责边界。深刻理解每种方案的原理、配置要点和适用场景,才能在实际项目中做出恰当的技术选型,避免给自己埋下难以排查的“坑”。深入学习 Java 生态下的 Spring 框架和各类 数据库/中间件 的特性,是驾驭这类架构的基础。