找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

517

积分

1

好友

62

主题
发表于 昨天 21:01 | 查看: 2| 回复: 0

在实际项目中,一个系统常常需要连接多个不同类型的数据库,以适应不同的业务需求:

  • 核心业务数据存储在 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.ymlspring.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。

便捷方案:自定义事务注解

为了避免在代码中频繁书写冗长的事务管理器名称,可以自定义具有业务语义的注解。

  1. 为 MySQL 事务定义注解:
    @Target({ElementType.METHOD, ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Transactional(
    transactionManager = "mysqlTransactionManager", 
    rollbackFor = Exception.class)
    @Documented
    public @interface MysqlTx {
    }
  2. 为 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 注解,并明确指定 basePackagessqlSessionFactoryRef

同时,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);
    }
}

对于业务开发人员而言,几乎感知不到底层连接了多个数据库,这就是多数据源并存模式带来的便利性。

常见问题与排查

  1. Druid 监控面板不显示 TDengine 的 SQL 统计 这可能是因为在 filters 配置中包含了 wall(SQL防火墙)。Druid 的 WallFilter 主要针对 MySQL 等数据库的语法设计,可能不兼容 TDengine 的 SQL 方言,导致空指针异常。解决方案:在 TDengine 数据源的配置中,仅保留 stat(统计)过滤器,移除 wall

    filters: stat # 移除了 wall
  2. 抛出 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 框架和各类 数据库/中间件 的特性,是驾驭这类架构的基础。




上一篇:《深入理解计算机系统》深度解析:中文技术书籍的平替挑战与思考
下一篇:Transformer架构深入解析:从注意力机制到LLM工作原理
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2025-12-8 23:07 , Processed in 1.084008 second(s), 44 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表