Coexisting Multiple DataSources in Spring Boot: No Switching Needed
This guide explains how to configure Spring Boot applications to use multiple coexisting DataSource beans—such as MySQL and TDengine—without runtime switching, covering bean definitions, MyBatis mapper scanning, transaction manager setup, custom @Transactional annotations, and common pitfalls, enabling clean separation of business, log, and time‑series data.
Concept of Multiple DataSource Coexistence
In a Spring‑Boot + MyBatis project you can define several DataSource beans, each bound to its own SqlSessionFactory, Mapper package and DataSourceTransactionManager. No AbstractRoutingDataSource, no ThreadLocal routing, and each datasource works independently.
I don’t switch data sources; I simply have several DataSource beans, each clearly linked to its own Mapper/SqlSessionFactory/TransactionManager.
Typical injection:
@Resource(name = "mysqlDataSource")
private DataSource mysqlDataSource;
@Resource(name = "tdengineDataSource")
private DataSource tdengineDataSource;Multiple DataSource beans exist simultaneously
Each bean has its own configuration
Each bean has its own Mapper/Dao/Service layer
No routing logic, no ThreadLocal switching
Isolation between datasources
Usage pattern is identical for all
When to Use
Different database types (e.g., MySQL + TDengine, MySQL + Oracle, MySQL + MongoDB)
Different responsibilities (business DB, log DB, time‑series DB, analytics DB)
No need for read/write separation, master‑slave, or multi‑tenant switching
Example: a UAV trajectory system stores core business data in MySQL and massive time‑series data in TDengine.
Configuration Steps
1. Define DataSource Properties (application.yml)
spring:
datasource:
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
validation-query: SELECT 1
test-while-idle: true
pool-prepared-statements: true
max-pool-prepared-statement-per-connection-size: 20
filters: stat,wall,slf4j
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
min-idle: 5
max-active: 20
max-wait: 60000
validation-query: SELECT 1
test-while-idle: true
pool-prepared-statements: true
max-pool-prepared-statement-per-connection-size: 50
filters: stat2. MySQL DataSource Configuration
@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 DataSourceTransactionManager mysqlTransactionManager(@Qualifier("mysqlDataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}3. TDengine DataSource Configuration
@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();
}
}4. Transaction Management
Each datasource gets its own transaction manager. You can use the full annotation or create semantic aliases:
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Transactional(transactionManager = "mysqlTransactionManager", rollbackFor = Exception.class)
@Documented
public @interface MysqlTx {}
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Transactional(transactionManager = "tdengineTransactionManager", rollbackFor = Exception.class)
@Documented
public @interface TdTx {}Now service methods can be annotated with @MysqlTx or @TdTx instead of the verbose @Transactional declaration.
5. Operations Involving Both Databases
A single @Transactional can bind only one transaction manager. When a method writes to both MySQL and TDengine you must handle compensation manually:
@Transactional(transactionManager = "mysqlTransactionManager")
public void process() {
mysqlMapper.insert(order);
try {
tdMapper.insert(log);
} catch (Exception e) {
// compensate: delete MySQL record or record the failure
mysqlMapper.deleteById(order.getId());
throw e;
}
}6. MapperScan Registration
Do not place @Mapper on individual interfaces. Register mappers per datasource with @MapperScan to avoid ambiguity.
7. Comparison with Dynamic DataSource
Multiple DataSource Coexistence keeps several datasource beans alive, no runtime switching, and is suited for distinct business modules. Dynamic DataSource exposes a single bean and switches via ThreadLocal, typically used for read/write separation or multi‑tenant scenarios. The former is strongly recommended when each module has its own dedicated database.
8. Usage Example
@Service
public class UserService {
@Resource
private UserMapper userMapper;
@Resource
private TdLogMapper tdLogMapper;
@MysqlTx
public void saveUser(User user) {
userMapper.insert(user);
}
@TdTx
public void saveLog(Log log) {
tdLogMapper.insert(log);
}
}Business code injects the appropriate mapper with @Resource and calls methods in the same way; the underlying datasource is transparent.
Common Pitfalls
Druid monitoring for TDengine : TDengine’s SQL dialect is not compatible with the wall filter. Remove wall from the filter list to avoid NPE.
"Invalid bound statement (not found)" : Ensure the mapper interface is scanned, the XML file is present, and the method name matches the id in the XML.
MyBatis‑Plus integration : Use MybatisSqlSessionFactoryBean instead of the plain SqlSessionFactoryBean when configuring MyBatis‑Plus.
Conclusion
Multiple DataSource coexistence solves the problem of letting each database perform its own responsibilities without any runtime switching logic. Dynamic DataSource solves the problem of switching between databases when needed. Choose the approach that matches your architectural requirements, keep bean names distinct, and avoid unnecessary routing to maintain a clean and stable backend.
Selected Java Interview Questions
A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
