Spring Boot「多数据源并存」的设计思路,它与动态数据源又有什么区别?

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

  • 业务数据在 MySQL

  • 日志 / 时序数据在 TDengine

  • 报表数据在 PostgreSQL

  • 部分历史数据在 Oracle

这时,你并不希望它们"互相切换",而是:

不同的业务 → 使用不同的数据库

不同的模块 → 绑定不同的数据源

这种模式,我们称之为:

多数据源并存

注意,不是:读写分离、动态切换、主从切换

动态数据源可以看看这篇:

Spring Boot 动态数据源:AbstractRoutingDataSource_abstractroutingdatasource 只有配置两个数据源-CSDN博客https://blog.csdn.net/weixin_46739493/article/details/149207123?spm=1001.2014.3001.5501


一、什么是「多数据源并存」?

核心思想只有一句话:

我不切换数据源,我只是同时拥有多个 DataSource,每一个都明确对应自己的 Mapper / SqlSessionFactory / 事务管理器

在代码里表现为:

java 复制代码
@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,

java 复制代码
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 数据源配置

java 复制代码
@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 数据源配置类

java 复制代码
@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的:

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

TDengine的:

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

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

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

6.2 便捷方式

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

java 复制代码
@MysqlTx
public void saveUser(){...}

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

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

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

也就是说:

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

TDengine同理。就很方便了。

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

java 复制代码
public void saveAll(){
    mysqlMapper.insert(...);
    tdMapper.insert(...);
}

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

✅ MySQL 可回滚

❌ TDengine 不在这个事务里,不回滚。

反过来也是一样。

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

java 复制代码
@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 切换
适合场景 不同业务不同库 读写分离 / 多租户
是否推荐 强烈推荐 只在必要时推荐

九、使用姿势

java 复制代码
@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。

java 复制代码
@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 名称区分,不做切换,只做分工

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

相关推荐
Godson_beginner38 分钟前
Sa-Token (java权限认证框架)
java·开发语言
W***533141 分钟前
SpringBoot Maven 项目 pom 中的 plugin 插件用法整理
spring boot·后端·maven
o***592743 分钟前
spring注入static属性
java·后端·spring
风象南1 小时前
Spring Boot实现HTTPS双向认证
java·spring boot·后端
是一个Bug1 小时前
Spring Boot 的全局异常处理器
spring boot·后端·python
嘻哈baby1 小时前
Prometheus + Grafana 监控系统搭建实战:从零到生产可用
后端
5***84641 小时前
【SpringBoot3】Spring Boot 3.0 集成 Mybatis Plus
spring boot·后端·mybatis
Java水解1 小时前
MySQL - 一文理清存储引擎:InnoDB vs MyISAM 核心差异
后端
青春不流名1 小时前
Java List初始化的例子
java·windows·list