Ruoyi-vue-plus-5.x第四篇数据库技术与多数据源: 4.1 多数据源配置管理
·
👋 大家好,我是 阿问学长!专注于分享优质开源项目解析、毕业设计项目指导支持、幼小初高的教辅资料推荐等,欢迎关注交流!🚀
多数据源配置管理
前言
在现代企业级应用中,多数据源已成为常见需求。RuoYi-Vue-Plus框架基于Dynamic-Datasource实现了强大的多数据源管理功能,支持主从分离、读写分离、多租户数据隔离等场景。本文将详细介绍Dynamic-Datasource的集成配置、多数据源动态切换、数据源路由策略以及异构数据库支持等内容。
Dynamic-Datasource集成
基础配置
/**
* 多数据源配置
*/
@Configuration
@EnableConfigurationProperties(DynamicDataSourceProperties.class)
public class DynamicDataSourceConfig {
@Autowired
private DynamicDataSourceProperties properties;
/**
* 数据源配置
*/
@Bean
@ConfigurationProperties("spring.datasource.dynamic")
public DynamicDataSourceProperties dynamicDataSourceProperties() {
return new DynamicDataSourceProperties();
}
/**
* 动态数据源
*/
@Bean
@Primary
public DataSource dynamicDataSource() {
DynamicRoutingDataSource dataSource = new DynamicRoutingDataSource();
// 设置默认数据源
dataSource.setPrimary(properties.getPrimary());
// 设置严格模式
dataSource.setStrict(properties.getStrict());
// 设置数据源
Map<String, DataSourceProperty> datasourceMap = properties.getDatasource();
Map<Object, Object> targetDataSources = new HashMap<>();
for (Map.Entry<String, DataSourceProperty> entry : datasourceMap.entrySet()) {
String key = entry.getKey();
DataSourceProperty property = entry.getValue();
DataSource ds = createDataSource(property);
targetDataSources.put(key, ds);
}
dataSource.setTargetDataSources(targetDataSources);
dataSource.setDefaultTargetDataSource(targetDataSources.get(properties.getPrimary()));
return dataSource;
}
/**
* 创建数据源
*/
private DataSource createDataSource(DataSourceProperty property) {
HikariConfig config = new HikariConfig();
// 基础配置
config.setJdbcUrl(property.getUrl());
config.setUsername(property.getUsername());
config.setPassword(property.getPassword());
config.setDriverClassName(property.getDriverClassName());
// 连接池配置
config.setMaximumPoolSize(property.getHikari().getMaximumPoolSize());
config.setMinimumIdle(property.getHikari().getMinimumIdle());
config.setConnectionTimeout(property.getHikari().getConnectionTimeout());
config.setIdleTimeout(property.getHikari().getIdleTimeout());
config.setMaxLifetime(property.getHikari().getMaxLifetime());
config.setValidationTimeout(property.getHikari().getValidationTimeout());
// 连接测试
config.setConnectionTestQuery(property.getHikari().getConnectionTestQuery());
// 连接池名称
config.setPoolName(property.getPoolName());
return new HikariDataSource(config);
}
/**
* 数据源切换处理器
*/
@Bean
public DynamicDataSourceAnnotationAdvisor dynamicDatasourceAnnotationAdvisor() {
DynamicDataSourceAnnotationInterceptor interceptor = new DynamicDataSourceAnnotationInterceptor();
DynamicDataSourceAnnotationAdvisor advisor = new DynamicDataSourceAnnotationAdvisor(interceptor);
advisor.setOrder(Ordered.HIGHEST_PRECEDENCE);
return advisor;
}
/**
* 数据源健康检查
*/
@Bean
public DataSourceHealthIndicator dataSourceHealthIndicator() {
return new DataSourceHealthIndicator();
}
}
配置文件
spring:
datasource:
dynamic:
# 设置默认的数据源或者数据源组,默认值即为 master
primary: master
# 严格模式,默认false. 设置为true后在未匹配到指定数据源时候会抛出异常
strict: false
# 数据源配置
datasource:
# 主数据源
master:
url: jdbc:mysql://localhost:3306/ry-vue-plus?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: root
password: password
driver-class-name: com.mysql.cj.jdbc.Driver
# 连接池配置
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
validation-timeout: 5000
connection-test-query: SELECT 1
# 从数据源
slave:
url: jdbc:mysql://localhost:3307/ry-vue-plus?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: root
password: password
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
maximum-pool-size: 15
minimum-idle: 3
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
validation-timeout: 5000
connection-test-query: SELECT 1
# Oracle数据源
oracle:
url: jdbc:oracle:thin:@localhost:1521:orcl
username: ruoyi
password: password
driver-class-name: oracle.jdbc.OracleDriver
hikari:
maximum-pool-size: 10
minimum-idle: 2
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
validation-timeout: 5000
connection-test-query: SELECT 1 FROM DUAL
# PostgreSQL数据源
postgres:
url: jdbc:postgresql://localhost:5432/ruoyi
username: postgres
password: password
driver-class-name: org.postgresql.Driver
hikari:
maximum-pool-size: 10
minimum-idle: 2
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
validation-timeout: 5000
connection-test-query: SELECT 1
# 租户数据源示例
tenant1:
url: jdbc:mysql://localhost:3306/tenant1?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: tenant1
password: password
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
maximum-pool-size: 10
minimum-idle: 2
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
validation-timeout: 5000
connection-test-query: SELECT 1
多数据源动态切换
注解方式切换
/**
* 用户服务 - 演示多数据源切换
*/
@Service
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements ISysUserService {
/**
* 查询用户列表 - 使用从库
*/
@DS("slave")
public List<SysUserVo> selectUserList(SysUserBo user) {
LambdaQueryWrapper<SysUser> wrapper = buildQueryWrapper(user);
List<SysUser> userList = baseMapper.selectList(wrapper);
return BeanUtil.copyToList(userList, SysUserVo.class);
}
/**
* 新增用户 - 使用主库
*/
@DS("master")
@Transactional(rollbackFor = Exception.class)
public Boolean insertUser(SysUserBo user) {
SysUser sysUser = BeanUtil.toBean(user, SysUser.class);
// 新增用户信息
int rows = baseMapper.insert(sysUser);
user.setUserId(sysUser.getUserId());
// 新增用户岗位关联
insertUserPost(user);
// 新增用户与角色管理
insertUserRole(user);
return rows > 0;
}
/**
* 从Oracle数据源查询用户统计信息
*/
@DS("oracle")
public UserStatisticsVo getUserStatisticsFromOracle() {
// 这里可以调用Oracle特有的统计函数
return baseMapper.selectUserStatisticsFromOracle();
}
/**
* 从PostgreSQL数据源查询用户分析数据
*/
@DS("postgres")
public List<UserAnalysisVo> getUserAnalysisFromPostgres(AnalysisQuery query) {
return baseMapper.selectUserAnalysisFromPostgres(query);
}
/**
* 租户数据查询
*/
@DS("#tenantId")
public List<SysUserVo> selectTenantUsers(String tenantId, SysUserBo user) {
LambdaQueryWrapper<SysUser> wrapper = buildQueryWrapper(user);
List<SysUser> userList = baseMapper.selectList(wrapper);
return BeanUtil.copyToList(userList, SysUserVo.class);
}
/**
* 动态数据源查询
*/
@DS("#dataSourceName")
public List<SysUserVo> selectUsersFromDynamicSource(String dataSourceName, SysUserBo user) {
LambdaQueryWrapper<SysUser> wrapper = buildQueryWrapper(user);
List<SysUser> userList = baseMapper.selectList(wrapper);
return BeanUtil.copyToList(userList, SysUserVo.class);
}
}
编程方式切换
/**
* 数据源切换工具类
*/
@Component
public class DataSourceSwitcher {
/**
* 切换到指定数据源执行操作
*/
public <T> T executeWithDataSource(String dataSourceName, Supplier<T> supplier) {
String originalDataSource = DynamicDataSourceContextHolder.peek();
try {
DynamicDataSourceContextHolder.push(dataSourceName);
return supplier.get();
} finally {
DynamicDataSourceContextHolder.poll();
if (originalDataSource != null) {
DynamicDataSourceContextHolder.push(originalDataSource);
}
}
}
/**
* 切换到指定数据源执行操作(无返回值)
*/
public void executeWithDataSource(String dataSourceName, Runnable runnable) {
executeWithDataSource(dataSourceName, () -> {
runnable.run();
return null;
});
}
/**
* 在主库执行操作
*/
public <T> T executeOnMaster(Supplier<T> supplier) {
return executeWithDataSource("master", supplier);
}
/**
* 在从库执行操作
*/
public <T> T executeOnSlave(Supplier<T> supplier) {
return executeWithDataSource("slave", supplier);
}
/**
* 在指定租户数据源执行操作
*/
public <T> T executeOnTenant(String tenantId, Supplier<T> supplier) {
return executeWithDataSource("tenant_" + tenantId, supplier);
}
}
/**
* 使用编程方式切换数据源的服务示例
*/
@Service
public class DataMigrationService {
@Autowired
private DataSourceSwitcher dataSourceSwitcher;
@Autowired
private SysUserMapper userMapper;
/**
* 数据迁移示例
*/
@Transactional(rollbackFor = Exception.class)
public void migrateUserData(String sourceDataSource, String targetDataSource) {
// 从源数据源查询数据
List<SysUser> users = dataSourceSwitcher.executeWithDataSource(sourceDataSource, () -> {
return userMapper.selectList(new LambdaQueryWrapper<>());
});
log.info("从数据源 {} 查询到 {} 条用户数据", sourceDataSource, users.size());
// 写入目标数据源
dataSourceSwitcher.executeWithDataSource(targetDataSource, () -> {
for (SysUser user : users) {
// 重置ID,避免主键冲突
user.setUserId(null);
user.setCreateTime(new Date());
userMapper.insert(user);
}
return null;
});
log.info("成功迁移 {} 条用户数据到数据源 {}", users.size(), targetDataSource);
}
/**
* 跨数据源数据同步
*/
public void syncUserData(Long userId) {
// 从主库获取最新数据
SysUser masterUser = dataSourceSwitcher.executeOnMaster(() -> {
return userMapper.selectById(userId);
});
if (masterUser != null) {
// 同步到从库
dataSourceSwitcher.executeOnSlave(() -> {
SysUser existingUser = userMapper.selectById(userId);
if (existingUser != null) {
// 更新
userMapper.updateById(masterUser);
} else {
// 插入
userMapper.insert(masterUser);
}
return null;
});
log.info("用户数据同步完成: userId={}", userId);
}
}
/**
* 多数据源数据汇总
*/
public UserSummaryVo getUserSummary(Long userId) {
UserSummaryVo summary = new UserSummaryVo();
// 从主库获取基本信息
SysUser basicInfo = dataSourceSwitcher.executeOnMaster(() -> {
return userMapper.selectById(userId);
});
summary.setBasicInfo(basicInfo);
// 从Oracle获取统计信息
UserStatisticsVo statistics = dataSourceSwitcher.executeWithDataSource("oracle", () -> {
return userMapper.selectUserStatistics(userId);
});
summary.setStatistics(statistics);
// 从PostgreSQL获取分析数据
List<UserAnalysisVo> analysis = dataSourceSwitcher.executeWithDataSource("postgres", () -> {
return userMapper.selectUserAnalysis(userId);
});
summary.setAnalysis(analysis);
return summary;
}
}
数据源路由策略
读写分离策略
/**
* 读写分离数据源路由策略
*/
@Component
public class ReadWriteSplitStrategy implements DynamicDataSourceStrategy {
private static final Logger log = LoggerFactory.getLogger(ReadWriteSplitStrategy.class);
/**
* 确定数据源
*/
@Override
public String determineDataSource(String[] dataSourceNames) {
// 获取当前事务状态
boolean isReadOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly();
// 检查是否在事务中
boolean isInTransaction = TransactionSynchronizationManager.isActualTransactionActive();
String selectedDataSource;
if (isInTransaction && !isReadOnly) {
// 写事务,使用主库
selectedDataSource = selectMasterDataSource(dataSourceNames);
} else {
// 读操作或只读事务,使用从库
selectedDataSource = selectSlaveDataSource(dataSourceNames);
}
log.debug("数据源路由: isReadOnly={}, isInTransaction={}, selected={}",
isReadOnly, isInTransaction, selectedDataSource);
return selectedDataSource;
}
/**
* 选择主库
*/
private String selectMasterDataSource(String[] dataSourceNames) {
for (String dataSourceName : dataSourceNames) {
if (dataSourceName.contains("master") || dataSourceName.contains("write")) {
return dataSourceName;
}
}
return dataSourceNames[0]; // 默认返回第一个
}
/**
* 选择从库
*/
private String selectSlaveDataSource(String[] dataSourceNames) {
List<String> slaveDataSources = new ArrayList<>();
for (String dataSourceName : dataSourceNames) {
if (dataSourceName.contains("slave") || dataSourceName.contains("read")) {
slaveDataSources.add(dataSourceName);
}
}
if (slaveDataSources.isEmpty()) {
// 没有从库,使用主库
return selectMasterDataSource(dataSourceNames);
}
// 负载均衡选择从库
return loadBalanceSelect(slaveDataSources);
}
/**
* 负载均衡选择数据源
*/
private String loadBalanceSelect(List<String> dataSources) {
if (dataSources.size() == 1) {
return dataSources.get(0);
}
// 简单的轮询策略
int index = (int) (System.currentTimeMillis() % dataSources.size());
return dataSources.get(index);
}
}
租户路由策略
/**
* 租户数据源路由策略
*/
@Component
public class TenantDataSourceStrategy implements DynamicDataSourceStrategy {
@Autowired
private TenantService tenantService;
@Override
public String determineDataSource(String[] dataSourceNames) {
// 获取当前租户ID
String tenantId = TenantContextHolder.getTenantId();
if (StringUtils.isBlank(tenantId)) {
log.warn("租户ID为空,使用默认数据源");
return "master";
}
// 获取租户配置
TenantConfig tenantConfig = tenantService.getTenantConfig(tenantId);
if (tenantConfig == null) {
log.warn("租户配置不存在: tenantId={}", tenantId);
return "master";
}
// 根据租户配置选择数据源
String dataSourceName = tenantConfig.getDataSourceName();
// 验证数据源是否存在
if (isDataSourceExists(dataSourceName)) {
log.debug("租户数据源路由: tenantId={}, dataSource={}", tenantId, dataSourceName);
return dataSourceName;
} else {
log.warn("租户数据源不存在: tenantId={}, dataSource={}", tenantId, dataSourceName);
return "master";
}
}
/**
* 检查数据源是否存在
*/
private boolean isDataSourceExists(String dataSourceName) {
DynamicRoutingDataSource dynamicDataSource =
(DynamicRoutingDataSource) SpringUtils.getBean("dynamicDataSource");
return dynamicDataSource.getDataSources().containsKey(dataSourceName);
}
}
地域路由策略
/**
* 地域数据源路由策略
*/
@Component
public class RegionDataSourceStrategy implements DynamicDataSourceStrategy {
@Override
public String determineDataSource(String[] dataSourceNames) {
// 获取用户地域信息
String region = getCurrentUserRegion();
if (StringUtils.isBlank(region)) {
return "master"; // 默认数据源
}
// 根据地域选择数据源
String targetDataSource = "region_" + region.toLowerCase();
// 检查目标数据源是否存在
for (String dataSourceName : dataSourceNames) {
if (dataSourceName.equals(targetDataSource)) {
log.debug("地域数据源路由: region={}, dataSource={}", region, targetDataSource);
return targetDataSource;
}
}
log.debug("地域数据源不存在,使用默认数据源: region={}", region);
return "master";
}
/**
* 获取当前用户地域
*/
private String getCurrentUserRegion() {
try {
// 从用户信息中获取地域
if (StpUtil.isLogin()) {
Long userId = StpUtil.getLoginIdAsLong();
SysUser user = userService.selectUserById(userId);
return user != null ? user.getRegion() : null;
}
} catch (Exception e) {
log.debug("获取用户地域信息失败", e);
}
// 从请求头获取地域信息
HttpServletRequest request = ServletUtils.getRequest();
if (request != null) {
return request.getHeader("X-User-Region");
}
return null;
}
}
异构数据库支持
数据库适配器
/**
* 数据库适配器接口
*/
public interface DatabaseAdapter {
/**
* 获取数据库类型
*/
DatabaseType getDatabaseType();
/**
* 获取分页SQL
*/
String getPageSql(String originalSql, long offset, long limit);
/**
* 获取当前时间函数
*/
String getCurrentTimeFunction();
/**
* 获取字符串连接函数
*/
String getConcatFunction(String... columns);
/**
* 转换数据类型
*/
String convertDataType(String genericType);
/**
* 获取序列下一个值(用于主键生成)
*/
String getNextSequenceValue(String sequenceName);
}
/**
* MySQL数据库适配器
*/
@Component
public class MySQLAdapter implements DatabaseAdapter {
@Override
public DatabaseType getDatabaseType() {
return DatabaseType.MYSQL;
}
@Override
public String getPageSql(String originalSql, long offset, long limit) {
return originalSql + " LIMIT " + offset + ", " + limit;
}
@Override
public String getCurrentTimeFunction() {
return "NOW()";
}
@Override
public String getConcatFunction(String... columns) {
return "CONCAT(" + String.join(", ", columns) + ")";
}
@Override
public String convertDataType(String genericType) {
switch (genericType.toUpperCase()) {
case "STRING":
return "VARCHAR(255)";
case "TEXT":
return "TEXT";
case "INTEGER":
return "INT";
case "LONG":
return "BIGINT";
case "DECIMAL":
return "DECIMAL(10,2)";
case "DATETIME":
return "DATETIME";
case "BOOLEAN":
return "TINYINT(1)";
default:
return "VARCHAR(255)";
}
}
@Override
public String getNextSequenceValue(String sequenceName) {
// MySQL使用AUTO_INCREMENT,不需要序列
return null;
}
}
/**
* Oracle数据库适配器
*/
@Component
public class OracleAdapter implements DatabaseAdapter {
@Override
public DatabaseType getDatabaseType() {
return DatabaseType.ORACLE;
}
@Override
public String getPageSql(String originalSql, long offset, long limit) {
return "SELECT * FROM (SELECT ROWNUM rn, t.* FROM (" + originalSql + ") t WHERE ROWNUM <= " +
(offset + limit) + ") WHERE rn > " + offset;
}
@Override
public String getCurrentTimeFunction() {
return "SYSDATE";
}
@Override
public String getConcatFunction(String... columns) {
return String.join(" || ", columns);
}
@Override
public String convertDataType(String genericType) {
switch (genericType.toUpperCase()) {
case "STRING":
return "VARCHAR2(255)";
case "TEXT":
return "CLOB";
case "INTEGER":
return "NUMBER(10)";
case "LONG":
return "NUMBER(19)";
case "DECIMAL":
return "NUMBER(10,2)";
case "DATETIME":
return "DATE";
case "BOOLEAN":
return "NUMBER(1)";
default:
return "VARCHAR2(255)";
}
}
@Override
public String getNextSequenceValue(String sequenceName) {
return sequenceName + ".NEXTVAL";
}
}
/**
* PostgreSQL数据库适配器
*/
@Component
public class PostgreSQLAdapter implements DatabaseAdapter {
@Override
public DatabaseType getDatabaseType() {
return DatabaseType.POSTGRESQL;
}
@Override
public String getPageSql(String originalSql, long offset, long limit) {
return originalSql + " LIMIT " + limit + " OFFSET " + offset;
}
@Override
public String getCurrentTimeFunction() {
return "NOW()";
}
@Override
public String getConcatFunction(String... columns) {
return String.join(" || ", columns);
}
@Override
public String convertDataType(String genericType) {
switch (genericType.toUpperCase()) {
case "STRING":
return "VARCHAR(255)";
case "TEXT":
return "TEXT";
case "INTEGER":
return "INTEGER";
case "LONG":
return "BIGINT";
case "DECIMAL":
return "DECIMAL(10,2)";
case "DATETIME":
return "TIMESTAMP";
case "BOOLEAN":
return "BOOLEAN";
default:
return "VARCHAR(255)";
}
}
@Override
public String getNextSequenceValue(String sequenceName) {
return "NEXTVAL('" + sequenceName + "')";
}
}
数据库适配器工厂
/**
* 数据库适配器工厂
*/
@Component
public class DatabaseAdapterFactory {
private final Map<DatabaseType, DatabaseAdapter> adapters = new HashMap<>();
@Autowired
public DatabaseAdapterFactory(List<DatabaseAdapter> adapterList) {
for (DatabaseAdapter adapter : adapterList) {
adapters.put(adapter.getDatabaseType(), adapter);
}
}
/**
* 根据数据源名称获取适配器
*/
public DatabaseAdapter getAdapter(String dataSourceName) {
DatabaseType dbType = getDatabaseType(dataSourceName);
DatabaseAdapter adapter = adapters.get(dbType);
if (adapter == null) {
throw new ServiceException("不支持的数据库类型: " + dbType);
}
return adapter;
}
/**
* 根据当前数据源获取适配器
*/
public DatabaseAdapter getCurrentAdapter() {
String currentDataSource = DynamicDataSourceContextHolder.peek();
return getAdapter(currentDataSource != null ? currentDataSource : "master");
}
/**
* 获取数据库类型
*/
private DatabaseType getDatabaseType(String dataSourceName) {
try {
DynamicRoutingDataSource dynamicDataSource =
(DynamicRoutingDataSource) SpringUtils.getBean("dynamicDataSource");
DataSource dataSource = dynamicDataSource.getDataSource(dataSourceName);
try (Connection connection = dataSource.getConnection()) {
String databaseProductName = connection.getMetaData().getDatabaseProductName().toLowerCase();
if (databaseProductName.contains("mysql")) {
return DatabaseType.MYSQL;
} else if (databaseProductName.contains("oracle")) {
return DatabaseType.ORACLE;
} else if (databaseProductName.contains("postgresql")) {
return DatabaseType.POSTGRESQL;
} else if (databaseProductName.contains("sql server")) {
return DatabaseType.SQLSERVER;
} else {
return DatabaseType.UNKNOWN;
}
}
} catch (Exception e) {
log.error("获取数据库类型失败: dataSource={}", dataSourceName, e);
return DatabaseType.UNKNOWN;
}
}
}
/**
* 数据库类型枚举
*/
public enum DatabaseType {
MYSQL,
ORACLE,
POSTGRESQL,
SQLSERVER,
UNKNOWN
}
总结
本文详细介绍了多数据源配置管理,包括:
- Dynamic-Datasource集成:基础配置、连接池配置、健康检查
- 多数据源动态切换:注解方式和编程方式的数据源切换
- 数据源路由策略:读写分离、租户路由、地域路由等策略
- 异构数据库支持:数据库适配器模式,支持MySQL、Oracle、PostgreSQL等
这些功能为企业级应用提供了强大的多数据源管理能力,满足各种复杂的业务场景需求。
在下一篇文章中,我们将探讨数据库连接与事务管理。
参考资料
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐


所有评论(0)