1. 多数据源配置的必要性与场景分析
在真实的企业级应用开发中,单数据库架构往往无法满足复杂业务需求。以我参与过的一个电商平台项目为例,会员数据存放在MySQL集群,订单数据使用PostgreSQL,而风控数据则存储在Oracle中。这种异构数据库环境正是多数据源技术的典型应用场景。
RuoYi-Vue-Plus作为基于Spring Boot的快速开发框架,其多数据源功能本质上是通过动态数据源路由机制实现的。核心原理是继承AbstractRoutingDataSource类,通过线程上下文(ThreadLocal)保存当前数据源标识,在执行SQL操作前动态切换数据源。这种设计既保持了Spring事务管理的完整性,又实现了灵活的数据库访问。
关键提示:多数据源与分库分表有本质区别。前者针对不同业务库的异构访问,后者是单一业务库的水平拆分。配置时需明确业务边界,避免混用。
2. 基础环境准备与依赖配置
2.1 必要组件清单
在开始配置前,需确保项目中包含以下关键依赖(以Maven为例):
xml复制<!-- 多数据源核心依赖 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>3.5.2</version>
</dependency>
<!-- 数据库驱动(示例包含MySQL和PostgreSQL) -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.3.1</version>
</dependency>
2.2 配置文件结构设计
推荐采用YAML格式的配置文件,结构示例如下:
yaml复制spring:
datasource:
dynamic:
primary: master # 默认数据源
strict: true # 严格匹配数据源
datasource:
master:
url: jdbc:mysql://127.0.0.1:3306/ry-vue?useSSL=false
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
slave1:
url: jdbc:postgresql://localhost:5432/report_db
username: postgres
password: postgres
driver-class-name: org.postgresql.Driver
实际项目中我曾遇到一个坑:当使用SQL Server时,必须添加encrypt=false参数到连接字符串,否则会因默认加密配置导致连接超时。这类数据库特定的配置项需要特别注意。
3. 核心配置实现详解
3.1 数据源注解的使用规范
RuoYi-Vue-Plus基于@DS注解实现数据源切换,典型用法包括:
- 方法级注解(最常用):
java复制@DS("slave1")
public List<Report> getSalesReport(DateRange range) {
return reportMapper.selectByDate(range);
}
- 类级注解(统一指定):
java复制@DS("master")
@Repository
public class UserServiceImpl implements UserService {
// 所有方法默认使用master数据源
}
- 事务中的特殊处理:
java复制@Transactional
@DS("master")
public void updateOrder(Order order) {
// 事务方法必须指定数据源,否则可能路由失败
}
重要经验:在事务方法中,数据源切换必须在进入事务前完成。因此事务方法的@DS注解必须放在类或方法上,不能通过AOP动态注入。
3.2 多数据源事务管理
跨数据源的事务需要引入JTA解决方案。我们项目中使用Atomikos的配置示例:
java复制@Configuration
@ConditionalOnClass(PlatformTransactionManager.class)
public class TransactionConfig {
@Bean
public JtaTransactionManager transactionManager() {
UserTransactionManager tm = new UserTransactionManager();
UserTransaction ut = new UserTransactionImp();
return new JtaTransactionManager(ut, tm);
}
}
配合application.yml配置:
yaml复制spring:
jta:
atomikos:
datasource:
default-isolation-level: READ_COMMITTED
connectionfactory:
default-isolation-level: READ_COMMITTED
实测中发现分布式事务性能损耗明显,在非必要场景应尽量避免。我们的最佳实践是:
- 90%的场景通过业务设计避免跨库事务
- 8%的场景使用最终一致性方案
- 仅2%的强一致性需求才启用JTA
4. 动态数据源高级用法
4.1 基于业务规则的动态路由
通过继承AbstractRoutingDataSource实现自定义路由策略:
java复制public class TenantDataSourceRouter extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
// 从请求头获取租户ID
String tenantId = RequestContextHolder.getRequestAttributes()
.getHeader("X-Tenant-Id");
return StringUtils.isNotBlank(tenantId) ? tenantId : "master";
}
}
配置类中注册该数据源:
java复制@Bean
@Primary
public DataSource dataSource() {
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("master", masterDataSource());
targetDataSources.put("tenant1", tenant1DataSource());
// ...其他数据源
TenantDataSourceRouter router = new TenantDataSourceRouter();
router.setTargetDataSources(targetDataSources);
router.setDefaultTargetDataSource(masterDataSource());
return router;
}
4.2 读写分离实现方案
结合AbstractRoutingDataSource和Spring AOP实现自动读写分离:
java复制@Aspect
@Component
public class ReadWriteSplitAspect {
@Before("@annotation(org.springframework.transaction.annotation.Transactional)")
public void setWriteDataSource(JoinPoint jp) {
// 事务方法使用主库
DataSourceContextHolder.setDbType("master");
}
@Before("execution(* com..query*(..)) || execution(* com..find*(..))")
public void setReadDataSource(JoinPoint jp) {
// 查询方法使用从库
DataSourceContextHolder.setDbType("slave");
}
}
配合自定义注解实现更精细的控制:
java复制@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ReadOnly {
boolean value() default true;
}
5. 性能优化与问题排查
5.1 连接池配置建议
多数据源环境下,连接池配置尤为关键。我们的生产配置经验:
yaml复制spring:
datasource:
dynamic:
datasource:
master:
hikari:
maximum-pool-size: 20
minimum-idle: 5
idle-timeout: 30000
max-lifetime: 1800000
connection-timeout: 30000
slave1:
hikari:
maximum-pool-size: 15
# 从库可以配置较小连接数
常见连接泄露问题可通过以下SQL检测(MySQL示例):
sql复制SHOW PROCESSLIST;
-- 查找长时间空闲的连接
5.2 典型问题排查指南
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 数据源切换无效 | 1. 注解位置错误 2. 事务传播特性冲突 |
1. 检查@DS注解位置 2. 调整@Transactional的propagation属性 |
| 跨库事务失败 | 1. JTA配置错误 2. 数据库不支持XA |
1. 检查Atomikos日志 2. 改用本地事务+消息队列 |
| 连接池耗尽 | 1. 连接泄露 2. 配置不合理 |
1. 添加连接泄露检测 2. 调整maxPoolSize |
曾遇到一个棘手案例:在Kubernetes环境中,由于Pod频繁重启导致连接未正常关闭。最终通过添加以下配置解决:
yaml复制spring:
datasource:
hikari:
register-mbeans: true # 启用JMX监控
leak-detection-threshold: 60000 # 60秒泄露检测
6. 生产环境最佳实践
经过多个项目的实战检验,我们总结了以下黄金准则:
-
命名规范:
- 主库统一使用
master - 从库按功能命名:
report_slave、log_db - 租户数据源:
tenant_{id}
- 主库统一使用
-
监控指标:
java复制// 通过Micrometer暴露指标 @Bean public DataSourcePoolMetrics dataSourceMetrics(DataSource dataSource) { return new DataSourcePoolMetrics(dataSource, "app_datasource", Tags.empty()); } -
灾备方案:
java复制@DS("#header['x-db-fallback'] ?: 'master'") public CriticalData getCriticalData() { // 支持通过请求头动态降级 } -
测试策略:
- 单元测试:使用H2内存数据库模拟多数据源
- 集成测试:Testcontainers创建真实数据库实例
- 压力测试:重点关注连接池竞争情况
在最近的一个金融项目中,我们通过多数据源+分库分表组合方案,成功支撑了日均千万级的交易量。关键点在于:
- 核心交易走主库强一致
- 报表查询走从库+Elasticsearch
- 风控数据单独路由到Oracle集群
这种架构既保证了性能,又满足了不同业务的数据隔离需求。