1. 项目概述:为什么多数据源配置是后端开发的硬核技能
在前后端分离的架构下,后端服务作为数据中枢,其灵活性和健壮性直接决定了整个应用的边界。我接手过不少从单体应用向微服务或复杂业务系统演进的项目,一个高频出现的痛点就是:业务数据散落在多个不同的数据库中。可能是历史遗留的SQL Server库需要与新开发的MySQL模块共存,也可能是核心交易数据在A库,而日志、报表数据在B库。这时候,如果还在代码里写死一个数据库连接,或者用各种“土法炼钢”的切换方式,不仅代码难以维护,性能和数据一致性更是噩梦。
若依框架(RuoYi)作为一个基于Spring Boot的快速开发平台,其前后端分离版本提供了优雅的解决方案骨架。它内置了多数据源的支持,但官方文档往往点到为止,真正要在生产环境稳稳落地,里面有不少门道。这个教程,我就结合自己趟过的坑,把从原理到配置,再到避坑的完整流程拆解清楚。无论你是需要接入多个同构数据库(如多个MySQL实例),还是异构数据库(如MySQL + PostgreSQL),都能从这里找到可复用的路径。核心就一句话:让不同的Service方法,像调用本地函数一样,透明地操作各自背后的数据源,且保证事务的边界清晰。
2. 核心设计思路:抽象与动态路由的艺术
多数据源的本质不是一个技术炫技,而是一个设计问题。其核心思路是:在应用启动时,创建多个数据源(DataSource)对象;在执行SQL操作前,通过一个拦截机制,动态地将当前操作路由到正确的数据源上。若依框架的实现,正是基于这一经典模式。
2.1 技术栈选型与底层原理
若依前后端分离版默认集成了MyBatis作为ORM框架,并依赖Spring Boot的自动配置。实现多数据源,社区主流有两种做法:
- 基于
AbstractRoutingDataSource的动态数据源:这是Spring框架提供的一个抽象类,允许我们自定义一个数据源,在每次获取数据库连接时,根据某个“键”(Lookup Key)动态返回真实的数据源。这是若依框架采用的方式,也是本教程的核心。它的好处是与Spring事务管理(@Transactional)集成度好,对业务代码侵入小。 - 多套独立的MyBatis SqlSessionTemplate:为每个数据源配置一套独立的
SqlSessionFactory和SqlSessionTemplate,在DAO层或Service层手动注入和使用对应的Template。这种方式更直接,但需要手动管理事务传播,代码侵入性强,不适合在若依这种约定大于配置的框架中大规模使用。
为什么若依选择第一种?因为它更符合Spring的“习惯”。通过一个注解(如@DataSource)来标记数据源,在AOP切面中设置当前线程的Lookup Key,AbstractRoutingDataSource在获取连接时就能自动切换。业务开发者几乎无感知。
2.2 若依多数据源模块结构解析
在若依的源码中,多数据源相关代码主要位于ruoyi-common模块下的datasource包内。理解这个结构,是后续一切配置和调试的基础:
DynamicDataSource类:继承自AbstractRoutingDataSource。这是“路由器”本身。你需要重写它的determineCurrentLookupKey()方法,告诉它当前应该返回哪个数据源的名字(如“master”, “slave”)。DataSourceType枚举类:定义数据源名称的常量。例如MASTER、SLAVE。这是为了避免在代码中硬编码字符串,提升可维护性。DynamicDataSourceContextHolder类:这是一个关键的上下文持有者。它使用ThreadLocal来保存当前线程对应的数据源键值。DynamicDataSource的determineCurrentLookupKey()方法就是从这里获取值的。ThreadLocal保证了线程间的隔离,这是实现正确路由的基石。DataSourceAspect切面类:这是“调度员”。它拦截被@DataSource注解标记的方法,在方法执行前,将注解指定的数据源名称设置到DynamicDataSourceContextHolder中;在方法执行后(包括异常情况),清理上下文,避免内存泄漏和上下文污染。
这个“注解 -> 切面 -> 上下文持有者 -> 动态数据源”的链条,就是若依多数据源的核心工作原理。我们的配置,就是为这个链条提供“燃料”——即真实的数据源Bean。
3. 详细配置步骤:从零到一的实战
假设我们的场景是:主业务数据在master数据源(MySQL),一些运营报表数据在slave数据源(另一个MySQL实例,也可以是其他数据库)。
3.1 依赖确认与数据源配置
首先,确保你的pom.xml中已经包含了数据库驱动和连接池依赖。若依默认使用HikariCP,这是目前性能最好的连接池之一。
<!-- MySQL 驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <!-- 若依框架已经集成了HikariCP,通过spring-boot-starter-jdbc引入 -->接下来是重头戏:在application.yml(或application-druid.yml,若依默认使用Druid配置) 中配置多个数据源。这里有个关键点:Spring Boot默认的spring.datasource配置只会自动创建一个数据源Bean。我们要禁用这个自动配置,改为手动定义多个。
# application.yml spring: # 1. 首先,关闭默认的数据源自动配置(重要!) autoconfigure: exclude: - com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure # 如果不用Druid,也需要排除对应的自动配置类 # 2. 定义多个数据源的连接信息,前缀可以自定义 datasource: druid: master: url: jdbc:mysql://localhost:3306/ry_master?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver slave: url: jdbc:mysql://192.168.1.100:3306/ry_slave?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 username: report_user password: report_password driver-class-name: com.mysql.cj.jdbc.Driver # 以下是Druid连接池的通用配置(可选但建议) type: com.alibaba.druid.pool.DruidDataSource initialSize: 5 minIdle: 5 maxActive: 20 maxWait: 60000 # ... 其他Druid监控、过滤器配置注意:这里以Druid为例。如果你使用HikariCP,配置前缀是
spring.datasource.hikari,并且属性名有所不同(如maximum-pool-size对应maxActive)。务必保持连接池配置与你的实际依赖一致。
3.2 核心Java配置类编写
配置完YAML,我们需要在Java代码中将这些配置“实例化”为Spring Bean。通常会在com.ruoyi.framework.config包下创建一个DataSourceConfig类。
package com.ruoyi.framework.config; import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import javax.sql.DataSource; import java.util.HashMap; import java.util.Map; @Configuration public class DataSourceConfig { /** * 主数据源 (默认数据源) * @ConfigurationProperties 注解将 spring.datasource.druid.master 下的属性绑定到DataSource */ @Bean(name = "masterDataSource") @ConfigurationProperties(prefix = "spring.datasource.druid.master") @Primary // 标记为默认数据源,当找不到指定数据源时使用 public DataSource masterDataSource() { return DruidDataSourceBuilder.create().build(); } /** * 从数据源 */ @Bean(name = "slaveDataSource") @ConfigurationProperties(prefix = "spring.datasource.druid.slave") public DataSource slaveDataSource() { return DruidDataSourceBuilder.create().build(); } /** * 动态数据源核心配置 * 将多个数据源注入到 DynamicDataSource 中 */ @Bean(name = "dynamicDataSource") public DataSource dynamicDataSource() { DynamicDataSource dynamicDataSource = new DynamicDataSource(); // 配置默认数据源(必须) dynamicDataSource.setDefaultTargetDataSource(masterDataSource()); // 配置多数据源映射 Map<Object, Object> dataSourceMap = new HashMap<>(); dataSourceMap.put(DataSourceType.MASTER.name(), masterDataSource()); dataSourceMap.put(DataSourceType.SLAVE.name(), slaveDataSource()); dynamicDataSource.setTargetDataSources(dataSourceMap); return dynamicDataSource; } }这个配置类做了三件事:
- 创建了名为
masterDataSource和slaveDataSource的两个具体数据源Bean。 - 将
masterDataSource标记为@Primary,这是Spring的“保底”机制。当系统无法确定用哪个数据源时(例如某些框架内部初始化),就会使用这个默认的。 - 创建了关键的
dynamicDataSourceBean。它把前面两个具体数据源包装成一个Map,并设置了默认数据源。这样,DynamicDataSource类就能根据Key从这个Map里拿到真正的连接。
3.3 MyBatis与事务管理器配置
数据源准备好了,接下来要告诉MyBatis和Spring事务管理器使用我们刚刚创建的动态数据源。
package com.ruoyi.framework.config; import org.mybatis.spring.annotation.MapperScan; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; import javax.sql.DataSource; @Configuration @EnableTransactionManagement // 启用注解式事务管理 @MapperScan(basePackages = "com.ruoyi.**.mapper") // MyBatis mapper扫描路径 public class MyBatisConfig { /** * 配置事务管理器,注意这里注入的是 dynamicDataSource */ @Bean public PlatformTransactionManager transactionManager(DataSource dynamicDataSource) { return new DataSourceTransactionManager(dynamicDataSource); } // 如果你的MyBatis配置需要指定SqlSessionFactory,也需要注入dynamicDataSource // @Bean // public SqlSessionFactory sqlSessionFactory(DataSource dynamicDataSource) throws Exception { // SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean(); // sessionFactory.setDataSource(dynamicDataSource); // // ... 其他配置,如typeAliasesPackage, mapperLocations // return sessionFactory.getObject(); // } }这里有一个至关重要的细节:PlatformTransactionManager的Bean注入的是dynamicDataSource,而不是任何一个具体的数据源。因为Spring的事务管理是基于一个DataSource的。如果我们注入masterDataSource,那么所有的事务(包括标记为@DataSource(“slave”)的方法)都会在master库上开启和提交,这会导致严重的数据错乱。让事务管理器绑定到动态数据源上,它才能感知到路由变化,并在正确的物理连接上管理事务。
4. 业务层使用与注解驱动开发
配置完成后,在业务代码中使用就非常优雅了。
4.1 在Service层方法上使用@DataSource注解
假设有一个查询报表的服务:
package com.ruoyi.system.service.impl; import com.ruoyi.common.annotation.DataSource; import com.ruoyi.common.enums.DataSourceType; import com.ruoyi.system.domain.ReportData; import com.ruoyi.system.mapper.ReportMapper; import com.ruoyi.system.service.IReportService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; @Service public class ReportServiceImpl implements IReportService { @Autowired private ReportMapper reportMapper; /** * 此方法使用从库数据源 */ @Override @DataSource(DataSourceType.SLAVE) // 关键注解 public List<ReportData> selectReportList(ReportData reportData) { // 这个方法内部的所有数据库操作,都会自动路由到 slave 数据源 return reportMapper.selectReportList(reportData); } /** * 此方法使用主库数据源(默认,可不加注解,或显式指定@DataSource(DataSourceType.MASTER)) */ @Override // @DataSource(DataSourceType.MASTER) // 可加可不加 public void updateMasterData(SomeData data) { // 这里的操作会在 master 数据源上执行 someMapper.update(data); } }对应的Mapper接口和XML文件无需任何特殊改动,就像平时写单数据源一样。MyBatis的SqlSession会从dynamicDataSource获取连接,而后者已经通过切面设置好了正确的路由键。
4.2 注解生效范围与继承性
@DataSource注解是基于Spring AOP实现的,这意味着它只对Spring代理对象的方法调用生效。以下情况需要特别注意:
- 同类方法调用失效:在同一个Service类中,一个未加注解的方法A直接调用加了
@DataSource注解的方法B,注解是不会生效的。因为A调用B是this.B(),绕过了Spring的代理。解决方法是将方法B抽取到另一个Service中,或者使用AopContext.currentProxy()来获取当前代理对象再调用(不推荐,侵入性强)。 - 事务注解的协同:
@DataSource和@Transactional可以同时使用。执行顺序是:先进入@DataSource切面设置数据源键,再进入@Transactional切面开启事务。务必确保这两个注解修饰的是同一个方法,并且事务管理器绑定的是dynamicDataSource。 - 无注解默认行为:如果一个方法没有标注
@DataSource,那么DynamicDataSourceContextHolder中就没有值,DynamicDataSource会返回其设置的默认数据源(即我们在配置中设置的setDefaultTargetDataSource,通常是master)。
5. 生产环境进阶配置与深度避坑指南
把多数据源跑起来只是第一步,要让它稳定可靠地服务于生产环境,还需要考虑更多。
5.1 连接池参数精细化调优
不同业务的数据源,其访问模式可能天差地别。主库可能写多读少,连接要求高可用、低延迟;从库或报表库可能主要用于复杂查询,连接占用时间长。切忌使用同一套连接池参数。
spring: datasource: druid: master: url: ... # 主库连接池:快速响应,连接复用率高 initialSize: 10 minIdle: 10 maxActive: 50 # 根据应用服务器线程数和QPS估算 maxWait: 1000 # 等1秒拿不到连接就抛异常,避免雪崩 validationQuery: SELECT 1 testWhileIdle: true timeBetweenEvictionRunsMillis: 60000 slave: url: ... # 报表库连接池:允许长时间占用,但总量控制 initialSize: 5 minIdle: 5 maxActive: 20 # 防止复杂查询拖垮从库 maxWait: 5000 # 查询可以多等一会儿 validationQuery: SELECT 1 testWhileIdle: true timeBetweenEvictionRunsMillis: 120000 # 空闲检查可以慢一些5.2 多数据源下的分布式事务难题
这是多数据源架构中最棘手的问题之一。Spring的@Transactional在单数据源下是本地事务,但在多数据源下,它无法保证跨数据源的原子性。例如:
@Transactional public void transfer() { // 操作master数据源 masterAccountMapper.deductMoney(); // 操作slave数据源(或另一个master) slaveRecordMapper.addRecord(); // 如果这里抛出异常,master上的扣款会回滚,但slave上的记录不会! }解决方案取决于你的业务容忍度:
- 最终一致性(推荐):接受短时间的不一致,通过消息队列、定时任务或日志补偿机制,在事后达成一致。这是互联网高并发场景下的主流选择。
- Seata等分布式事务框架:引入中间件,通过AT、TCC等模式实现强一致性。这会带来一定的性能损耗和架构复杂度,适用于金融、交易等核心场景。若依框架本身不集成Seata,需要自行引入和配置。
- 尽量避免跨库事务:从业务设计上规避,将相关操作收敛到同一个数据源中。
5.3 监控与健康检查
多个数据源意味着更多的监控点。除了应用本身的监控,每个数据库实例、每个连接池的健康状态都需要关注。
- Druid监控:如果使用Druid,其内置的监控页面
/druid非常强大。你需要为每个DataSourceBean配置StatFilter和WallFilter,并在监控页上查看各自的SQL执行情况、连接池状态。 - Spring Boot Actuator:通过
/actuator/health端点,可以集成数据库健康检查。你需要自定义一个HealthIndicator,遍历所有数据源,执行validationQuery来检查连通性。 - 关键指标告警:对每个数据源的
activeCount(活跃连接数)、waitThreadCount(等待线程数)设置告警。如果某个数据源的等待线程数持续过高,很可能遇到了慢查询或连接泄漏。
5.4 常见问题排查实录
报错:”No qualifying bean of type ‘javax.sql.DataSource’ available”
- 原因:Spring容器中存在多个
DataSource类型的Bean,而某些自动配置类(如JPA、MyBatis AutoConfiguration)期望只有一个。或者你的@MapperScan、EntityManager等注入错了数据源。 - 解决:检查是否排除了数据源自动配置(
spring.autoconfigure.exclude)。确保@MapperScan、SqlSessionFactoryBean、TransactionManager等Bean明确注入了dynamicDataSource。
- 原因:Spring容器中存在多个
报错:”Could not open JDBC Connection for transaction”
- 原因:事务管理器使用了错误的数据源。典型症状是,在标记了
@DataSource(“slave”)的方法上开启事务,但连接却尝试从master获取,而master的配置可能不对。 - 解决:百分百确认你的
PlatformTransactionManagerBean定义中,DataSource参数注入的是dynamicDataSource。
- 原因:事务管理器使用了错误的数据源。典型症状是,在标记了
数据源切换偶尔失效,全部跑到默认库
- 原因:线程上下文污染。
DynamicDataSourceContextHolder使用ThreadLocal,如果在切面中设置后没有及时清理,当线程被线程池复用时,残留的数据源键会导致后续无关操作路由错误。或者,在异步任务(@Async)中,上下文没有正确传递。 - 解决:确保
DataSourceAspect切面在finally块中清理了ThreadLocal。对于异步场景,需要自定义任务装饰器,在任务执行前传递并设置数据源键。
- 原因:线程上下文污染。
性能问题:在循环中频繁切换数据源
- 场景:在一个方法里循环调用多个不同
@DataSource注解的Service方法。 - 分析:每次切换数据源都有微小开销(AOP拦截、ThreadLocal操作)。虽然不大,但在高频循环中会累积。
- 建议:重构业务逻辑,尽量将访问同一数据源的操作批量执行。或者,在更高层的方法上标注数据源,在内部通过
DataSourceAspect的子切面控制更细粒度的切换(较复杂)。
- 场景:在一个方法里循环调用多个不同
多数据源与MyBatis-Plus共用时的冲突
- 现象:引入了MyBatis-Plus后,多数据源配置不生效。
- 原因:MyBatis-Plus有自己的自动配置和
SqlSessionFactoryBean,它会使用@Primary的数据源,而忽略我们的动态数据源。 - 解决:需要手动配置MyBatis-Plus的
MybatisSqlSessionFactoryBean,并将其数据源设置为我们的dynamicDataSource。同时,可能需要排除MyBatis-Plus的部分自动配置。