⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 blog.csdn.net/weixin_46739493 「xxxzsh」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

在实际项目中,我们常常会遇到这样的场景:

  • 业务数据在 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,没几天就炸了。那我们现在就要引入时序数据库。

那我们现在已经引入了两个数据库:

数据源 用途 名称
MySQL 业务核心数据 mysqlDataSource
TDengine 时序/日志/设备轨迹 tdengineDataSource

我们要做到:

  • MySQL 有自己的一套 Mapper
  • TDengine 有自己的一套 Mapper
  • 配置互不干扰
  • 使用方式极其统一

三、配置两个数据源

我这里是用Druid

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 DataSourceTransactionManager mysqlTransactionManager(
@Qualifier("mysqlDataSource") DataSource dataSource
) {
return new DataSourceTransactionManager(dataSource);
}
}

注意点:

  • @MapperScan 扫描的是: com.za.uav.mapper.mysql
  • 所有属于 MySQL 的 mapper 都必须放进这个包
  • ConfigurationProperties配置好之后,它会自己读取Druid的配置

五、 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了。

但是这个问题很严重,所以专门拉一个出来写一下。

我们一般单数据源的时候,事务管理器只会有一个。所以我们事务用这个 @Transactional 就贼方便。

但如果你的系统中有多个事务管理器,在使用的时候必须显式指定事务管理器

比如MySQL的:

@Transactional(transactionManager = "mysqlTransactionManager", rollbackFor = Exception.class)
public void saveMysql() {
mysqlMapper.insert(data);
}

TDengine的:

@Transactional(transactionManager = "tdengineTransactionManager", rollbackFor = Exception.class)
public void saveTd() {
tdMapper.insert(data);
}

这个一定要有个肌肉记忆。事务可开不得玩笑。

如果你是项目负责人,可以立一条项目规则:只要是 Service 里写 @Transactional,不写 transactionManager = xxx 一律算 BUG。

便捷方式

因为很麻烦,所以其实可以自定义注解,比如我们现在可以分成:

@MysqlTx
public void saveUser(){...}

@TdTx
public void saveRecord(){...}

那我们要做的就是——包一层自己的注解进去:

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Transactional(
transactionManager = "mysqlTransactionManager",
rollbackFor = Exception.class
)
@Documented
public @interface MysqlTx {
}

也就是说:

✅ 我们只是给 @Transactional 起了一个「有语义的别名」

TDengine同理。就很方便了。

如果一个方法要操作两个数据库怎么办?

public void saveAll(){
mysqlMapper.insert(...);
tdMapper.insert(...);
}

我们业务经常要操作两个库,那这时候就有问题了。因为@Transactional 一次只能绑定一个事务管理器,所以如果异常了,只会保证:

  • MySQL 可回滚
  • TDengine 不在这个事务里,不回滚。

反过来也是一样。

那一般这种情况我们常用的就是业务补偿了:

@Transactional(transactionManager = "mysqlTransactionManager")
public void process() {

mysqlMapper.insert(order);

try {
tdMapper.insert(log);
} catch (Exception e) {
// 自行补偿(删 MySQL 或记录异常单)
mysqlMapper.deleteById(order.getId());
throw e;
}
}

七、MapperScan

这个跟事务差不多重要。@Mapper 注解就不要用了:

注册Bean的方式 换成MapperScan:

不然有时候它会不知道是哪个数据源的,会出错。

Mapper和xml都要要这样隔离开来放:

八、和「动态数据源」的核心区别对比

对比项 多数据源并存 动态数据源(切换)
DataSource 数量 多个 只有 1 个对外
是否切换 不切换 通过 ThreadLocal 切换
适合场景 不同业务不同库 读写分离 / 多租户
是否推荐 强烈推荐 只在必要时推荐

九、使用姿势

@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);
}
}

你会发现:

维度 MySQL TDengine
注入方式 @Resource @Resource
调用方式 一样 一样
感觉 一模一样 一模一样

业务代码根本无感知数据源差异 ,这就是并存方式最爽的地方。

一些常见问题

1,druidSQL监控不生效

我个人反正习惯用Druid的面板。

其中有个SQL统计,我们发现TD的不会被统计进来。

这时候要看看配置一下:

但记得像 wall 啥的就别配了,会空指针。

这是因为:wall 是为 SQL 注入检测设计的,但数据库语法只适配 MySQL 等关系型数据库,不兼容 TDengine 的 SQL 方言。

2,Invalid bound statement (not found):

检查一下这个。记得要被扫描到。看看class文件里面,没有的话就mvn clean一下再build一遍。

然后就是 检查 Mapper 接口方法名和 XML id 是否一致。

然后还有一种是MyBatisPlus的,记得要用 MybatisSqlSessionFactoryBean。

@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、ThreadLocal、AOP……

但在真实项目里,你更应该先问的其实是:

到底需不需要切换?还是各司其职就够了?

所以这两种方案解决的核心问题是完全不同的:

动态数据源解决的是「怎么切换」

  • 本质: 对外只有一个 DataSource
  • 内部: 通过 ThreadLocal + 路由规则自动切换
  • 常见场景: 读写分离、多租户、分库分表
  • 核心难点: 上下文传递准确性、事务一致性、调试困难

多数据源并存解决的是「怎么各司其职」

  • 本质:多个 DataSource 同时存在

  • 每个数据源拥有自己独立的

    • Mapper 扫描
    • SqlSessionFactory
    • TransactionManager
  • 通过 Bean 名称区分,不做切换,只做分工

不同场景下都有每个解决方案的优劣,在真实系统中,提高系统稳定性的,往往不是复杂技巧,而是清晰边界。主要是要知道各个方案的注意点,在项目中才不会给自己埋雷。

文章目录
  1. 1. 一、什么是「多数据源并存」?
  2. 2. 二、适合使用多数据源并存的场景
  3. 3. 三、配置两个数据源
  4. 4. 四、创建 MySQL 数据源配置
  5. 5. 五、 TDengine 数据源配置类
  6. 6. 六、事务管理器
    1. 6.0.1. 便捷方式
    2. 6.0.2. 如果一个方法要操作两个数据库怎么办?
  • 7. 七、MapperScan
  • 8. 八、和「动态数据源」的核心区别对比
  • 9. 九、使用姿势
  • 10. 一些常见问题
    1. 10.0.1. 1,druidSQL监控不生效
    2. 10.0.2. 2,Invalid bound statement (not found):
  • 11. 总结一下
    1. 11.0.0.1. 怎么切换?
    2. 11.0.0.2. 到底需不需要切换?还是各司其职就够了?