How to Co‑exist Multiple DataSources in Spring Without Switching?
This guide explains the concept of multi‑DataSource coexistence, when it is appropriate, step‑by‑step configuration for MySQL and TDengine, transaction manager handling, custom annotations, mapper scanning, differences from dynamic routing, common pitfalls, and best‑practice usage in a Spring‑MyBatis backend.
What Is “Multi‑DataSource Coexistence”?
The core idea is to keep several DataSource beans in the same application, each bound to its own Mapper / SqlSessionFactory / TransactionManager, without using AbstractRoutingDataSource or ThreadLocal switching.
@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 maps to a dedicated Mapper/DAO/Service
No AbstractRoutingDataSource involved
No ThreadLocal switching
Isolation between data sources
Each works independently
Usage syntax is identical to a single‑source setup
When to Use It
Different database types (e.g., MySQL + TDengine, MySQL + Oracle, MySQL + MongoDB)
Distinct responsibilities (business DB, log DB, time‑series DB, analytics DB)
No need for read/write splitting, master‑slave switching, or tenant isolation
Example: a UAV‑tracking system stores core business data in MySQL and massive time‑series logs in TDengine; both databases are required simultaneously.
Configuration Steps
1. Define Two DataSources (using Druid)
spring:
datasource:
# MySQL data source (business data)
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 data source (time‑series / log)
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 Class
@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);
}
}Key points : @MapperScan must point to the MySQL mapper package; @ConfigurationProperties automatically binds the Druid settings.
3. TDengine DataSource Configuration Class
@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();
}
}Key points : Separate package names and bean names keep the two data sources isolated.
4. Transaction Managers
When multiple transaction managers exist, you must specify which one to use:
@Transactional(transactionManager = "mysqlTransactionManager", rollbackFor = Exception.class)
public void saveMysql() {
mysqlMapper.insert(data);
}
@Transactional(transactionManager = "tdengineTransactionManager", rollbackFor = Exception.class)
public void saveTd() {
tdMapper.insert(data);
}If a method touches both databases, a single @Transactional can only bind one manager, so the other database will not roll back on failure.
5. Custom Annotations for Simpler Transaction Declaration
@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 {}Usage example:
@MysqlTx
public void saveUser(User user) {
userMapper.insert(user);
}
@TdTx
public void saveLog(Log log) {
tdLogMapper.insert(log);
}6. Handling a Method That Needs Both Databases
public void saveAll() {
mysqlMapper.insert(...);
tdMapper.insert(...);
}Because @Transactional can bind only one manager, you typically use a compensation pattern: commit the MySQL part, then try the TDengine part inside a try/catch and roll back manually if needed.
@Transactional(transactionManager = "mysqlTransactionManager")
public void process() {
mysqlMapper.insert(order);
try {
tdMapper.insert(log);
} catch (Exception e) {
// manual compensation, e.g., delete the MySQL record
mysqlMapper.deleteById(order.getId());
throw e;
}
}7. MapperScan vs. @Mapper
Do not use @Mapper on individual interfaces; instead register the whole package with @MapperScan so that each data source knows which mappers belong to it.
Similarly, keep mapper XML files in separate directories (e.g., mapper/mysql vs. mapper/tdengine).
8. Comparison with Dynamic DataSource (Routing)
DataSource count : multiple vs. single external
Switching : none vs. ThreadLocal‑based routing
Typical scenarios : different business databases vs. read/write splitting, multi‑tenant
Recommendation : coexistence is strongly recommended when responsibilities are fixed; dynamic routing only when switching is truly required
9. Practical 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);
}
}The service code does not need to know which database is used; the underlying configuration handles the separation.
Common Issues
Druid monitoring not showing TDengine queries : ensure the filters setting does not include wall, which is MySQL‑specific and can cause NPEs.
"Invalid bound statement (not found)" : verify that the mapper interface and its XML file are scanned by the correct @MapperScan and that the method name matches the XML id.
MyBatis‑Plus SqlSessionFactory : when using MyBatis‑Plus, replace SqlSessionFactoryBean with MybatisSqlSessionFactoryBean and set the mapper locations accordingly.
Conclusion
Multi‑DataSource coexistence solves the "how to let each component work with its own dedicated database" problem, while dynamic routing solves the "how to switch" problem. Choosing the right approach depends on whether you need runtime switching or simply clear separation of responsibilities. Proper bean naming, package isolation, and explicit transaction manager selection prevent hidden bugs and keep the system stable.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Code Ape Tech Column
Former Ant Group P8 engineer, pure technologist, sharing full‑stack Java, job interview and career advice through a column. Site: java-family.cn
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.
