TenantDsProcessor 接入动态数据源的加载

This commit is contained in:
YunaiV 2023-02-27 00:44:12 +08:00
parent 9be666e068
commit 1b563fecaa
14 changed files with 246 additions and 31 deletions

View File

@ -18,6 +18,9 @@ import cn.iocoder.yudao.framework.tenant.core.web.TenantContextWebFilter;
import cn.iocoder.yudao.framework.web.config.WebProperties;
import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler;
import cn.iocoder.yudao.module.system.api.tenant.TenantApi;
import com.baomidou.dynamic.datasource.DynamicRoutingDataSource;
import com.baomidou.dynamic.datasource.creator.DataSourceCreator;
import com.baomidou.dynamic.datasource.creator.DefaultDataSourceCreator;
import com.baomidou.dynamic.datasource.processor.DsProcessor;
import com.baomidou.dynamic.datasource.processor.DsSpelExpressionProcessor;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
@ -36,6 +39,7 @@ import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import javax.sql.DataSource;
import java.util.Objects;
@AutoConfiguration
@ -68,7 +72,12 @@ public class YudaoTenantAutoConfiguration {
}
@Bean
public DsProcessor dsProcessor() {
public DsProcessor dsProcessor(
// TenantFrameworkService tenantFrameworkService,
// DataSource dataSource,
// DefaultDataSourceCreator dataSourceCreator
) {
// TenantDsProcessor tenantDsProcessor = new TenantDsProcessor(tenantFrameworkService, dataSourceCreator);
TenantDsProcessor tenantDsProcessor = new TenantDsProcessor();
tenantDsProcessor.setNextProcessor(new DsSpelExpressionProcessor());
return tenantDsProcessor;

View File

@ -1,18 +1,52 @@
package cn.iocoder.yudao.framework.tenant.core.db.dynamic;
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
import cn.iocoder.yudao.framework.tenant.core.service.TenantFrameworkService;
import com.baomidou.dynamic.datasource.DynamicRoutingDataSource;
import com.baomidou.dynamic.datasource.creator.DefaultDataSourceCreator;
import com.baomidou.dynamic.datasource.processor.DsProcessor;
import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DataSourceProperty;
import jodd.util.CollectionUtil;
import lombok.RequiredArgsConstructor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.context.annotation.Lazy;
import javax.annotation.Resource;
import javax.sql.DataSource;
import java.util.Objects;
/**
* 基于 {@link TenantDS} 的数据源处理器
*
* 1. 如果有 @TenantDS 注解返回该租户的数据源
* 2. 如果该租户的数据源未创建则进行创建
*
* @author 芋道源码
*/
@RequiredArgsConstructor
public class TenantDsProcessor extends DsProcessor {
/**
* 用于获取租户数据源配置的 Service
*/
@Resource
@Lazy
private TenantFrameworkService tenantFrameworkService;
/**
* 动态数据源
*/
@Resource
@Lazy // 为什么添加 @Lazy 注解因为它和 DynamicRoutingDataSource 相互依赖导致无法初始化
private DynamicRoutingDataSource dynamicRoutingDataSource;
/**
* 用于创建租户数据源的 Creator
*/
@Resource
@Lazy
private DefaultDataSourceCreator dataSourceCreator;
@Override
public boolean matches(String key) {
return Objects.equals(key, TenantDS.KEY);
@ -20,12 +54,33 @@ public class TenantDsProcessor extends DsProcessor {
@Override
public String doDetermineDatasource(MethodInvocation invocation, String key) {
// 获得数据源配置
Long tenantId = TenantContextHolder.getRequiredTenantId();
// TODO 芋艿临时测试
if (tenantId != 1) {
tenantId = 2L;
DataSourceProperty dataSourceProperty = tenantFrameworkService.getDataSourceProperty(tenantId);
// 创建 or 创建数据源并返回数据源名字
return createDatasourceIfAbsent(dataSourceProperty);
}
private String createDatasourceIfAbsent(DataSourceProperty dataSourceProperty) {
// 1. 重点如果数据源不存在则进行创建
if (isDataSourceNotExist(dataSourceProperty)) {
// 问题一为什么要加锁因为如果多个线程同时执行到这里会导致多次创建数据源
// 问题二为什么要使用 poolName 加锁保证多个不同的 poolName 可以并发创建数据源
// 问题三为什么要使用 intern 方法因为intern 方法会返回一个字符串的常量池中的引用
// intern 的说明可见 https://www.cnblogs.com/xrq730/p/6662232.html 文章
synchronized (dataSourceProperty.getPoolName().intern()) {
if (isDataSourceNotExist(dataSourceProperty)) {
DataSource dataSource = dataSourceCreator.createDataSource(dataSourceProperty);
dynamicRoutingDataSource.addDataSource(dataSourceProperty.getPoolName(), dataSource);
}
}
}
return "tenant_" + tenantId + "_ds";
// 2. 返回数据源的名字
return dataSourceProperty.getPoolName();
}
private boolean isDataSourceNotExist(DataSourceProperty dataSourceProperty) {
return !dynamicRoutingDataSource.getDataSources().containsKey(dataSourceProperty.getPoolName());
}
}

View File

@ -1,5 +1,7 @@
package cn.iocoder.yudao.framework.tenant.core.service;
import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DataSourceProperty;
import java.util.List;
/**
@ -23,4 +25,12 @@ public interface TenantFrameworkService {
*/
void validTenant(Long id);
/**
* 获得租户对应的数据源配置
*
* @param id 租户编号
* @return 数据源配置
*/
DataSourceProperty getDataSourceProperty(Long id);
}

View File

@ -2,7 +2,10 @@ package cn.iocoder.yudao.framework.tenant.core.service;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.framework.common.util.cache.CacheUtils;
import cn.iocoder.yudao.module.infra.api.db.dto.DataSourceConfigRespDTO;
import cn.iocoder.yudao.module.system.api.tenant.TenantApi;
import cn.iocoder.yudao.module.system.api.tenant.dto.TenantDataSourceConfigRespDTO;
import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DataSourceProperty;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import lombok.RequiredArgsConstructor;
@ -56,6 +59,28 @@ public class TenantFrameworkServiceImpl implements TenantFrameworkService {
});
/**
* 针对 {@link #getDataSourceProperty(Long)} 的缓存
*/
private final LoadingCache<Long, DataSourceProperty> dataSourcePropertyCache = CacheUtils.buildAsyncReloadingCache(
Duration.ofMinutes(1L), // 过期时间 1 分钟
new CacheLoader<Long, DataSourceProperty>() {
@Override
public DataSourceProperty load(Long id) {
// 获得租户对应的数据源配置
TenantDataSourceConfigRespDTO dataSourceConfig = tenantApi.getTenantDataSourceConfig(id);
if (dataSourceConfig == null) {
return null;
}
// 转换成 dynamic-datasource 配置
return new DataSourceProperty()
.setPoolName(dataSourceConfig.getName()).setUrl(dataSourceConfig.getUrl())
.setUsername(dataSourceConfig.getUsername()).setPassword(dataSourceConfig.getPassword());
}
});
@Override
@SneakyThrows
public List<Long> getTenantIds() {
@ -70,4 +95,10 @@ public class TenantFrameworkServiceImpl implements TenantFrameworkService {
}
}
@Override
@SneakyThrows
public DataSourceProperty getDataSourceProperty(Long id) {
return dataSourcePropertyCache.get(id);
}
}

View File

@ -0,0 +1,20 @@
package cn.iocoder.yudao.module.infra.api.db;
import cn.iocoder.yudao.module.infra.api.db.dto.DataSourceConfigRespDTO;
/**
* 数据源配置 API 接口
*
* @author 芋道源码
*/
public interface DataSourceConfigServiceApi {
/**
* 获得数据源配置
*
* @param id 编号
* @return 数据源配置
*/
DataSourceConfigRespDTO getDataSourceConfig(Long id);
}

View File

@ -0,0 +1,35 @@
package cn.iocoder.yudao.module.infra.api.db.dto;
import lombok.Data;
/**
* 数据源配置 Response DTO
*
* @author 芋道源码
*/
@Data
public class DataSourceConfigRespDTO {
/**
* 主键编号
*/
private Long id;
/**
* 连接名
*/
private String name;
/**
* 数据源连接
*/
private String url;
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
}

View File

@ -0,0 +1,26 @@
package cn.iocoder.yudao.module.infra.api.db;
import cn.iocoder.yudao.module.infra.api.db.dto.DataSourceConfigRespDTO;
import cn.iocoder.yudao.module.infra.convert.db.DataSourceConfigConvert;
import cn.iocoder.yudao.module.infra.service.db.DataSourceConfigService;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
/**
* 数据源配置 API 实现类
*
* @author 芋道源码
*/
@Service
public class DataSourceConfigServiceApiImpl implements DataSourceConfigServiceApi {
@Resource
private DataSourceConfigService dataSourceConfigService;
@Override
public DataSourceConfigRespDTO getDataSourceConfig(Long id) {
return DataSourceConfigConvert.INSTANCE.convert02(dataSourceConfigService.getDataSourceConfig(id));
}
}

View File

@ -4,6 +4,7 @@ import java.util.*;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.infra.api.db.dto.DataSourceConfigRespDTO;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
import cn.iocoder.yudao.module.infra.controller.admin.db.vo.*;
@ -27,4 +28,6 @@ public interface DataSourceConfigConvert {
List<DataSourceConfigRespVO> convertList(List<DataSourceConfigDO> list);
DataSourceConfigRespDTO convert02(DataSourceConfigDO bean);
}

View File

@ -1,5 +1,7 @@
package cn.iocoder.yudao.module.system.api.tenant;
import cn.iocoder.yudao.module.system.api.tenant.dto.TenantDataSourceConfigRespDTO;
import java.util.List;
/**
@ -23,4 +25,12 @@ public interface TenantApi {
*/
void validateTenant(Long id);
/**
* 获得租户的数据源配置
*
* @param tenantId 租户编号
* @return 数据源配置
*/
TenantDataSourceConfigRespDTO getTenantDataSourceConfig(Long tenantId);
}

View File

@ -1,5 +1,9 @@
package cn.iocoder.yudao.module.system.api.tenant;
import cn.iocoder.yudao.module.infra.api.db.DataSourceConfigServiceApi;
import cn.iocoder.yudao.module.system.api.tenant.dto.TenantDataSourceConfigRespDTO;
import cn.iocoder.yudao.module.system.convert.tenant.TenantConvert;
import cn.iocoder.yudao.module.system.dal.dataobject.tenant.TenantDO;
import cn.iocoder.yudao.module.system.service.tenant.TenantService;
import org.springframework.stereotype.Service;
@ -17,6 +21,9 @@ public class TenantApiImpl implements TenantApi {
@Resource
private TenantService tenantService;
@Resource
private DataSourceConfigServiceApi dataSourceConfigServiceApi;
@Override
public List<Long> getTenantIdList() {
return tenantService.getTenantIdList();
@ -27,4 +34,16 @@ public class TenantApiImpl implements TenantApi {
tenantService.validTenant(id);
}
@Override
public TenantDataSourceConfigRespDTO getTenantDataSourceConfig(Long tenantId) {
// 获得租户信息
TenantDO tenant = tenantService.getTenant(tenantId);
if (tenant == null) {
return null;
}
// 获得租户的数据源配置
return TenantConvert.INSTANCE.convert(
dataSourceConfigServiceApi.getDataSourceConfig(tenant.getDatasourceConfigId()));
}
}

View File

@ -1,6 +1,8 @@
package cn.iocoder.yudao.module.system.convert.tenant;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.infra.api.db.dto.DataSourceConfigRespDTO;
import cn.iocoder.yudao.module.system.api.tenant.dto.TenantDataSourceConfigRespDTO;
import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant.TenantCreateReqVO;
import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant.TenantExcelVO;
import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant.TenantRespVO;
@ -42,4 +44,6 @@ public interface TenantConvert {
return reqVO;
}
TenantDataSourceConfigRespDTO convert(DataSourceConfigRespDTO bean);
}

View File

@ -79,4 +79,13 @@ public class TenantDO extends BaseDO {
*/
private Integer accountCount;
/**
* 数据源配置编号
*
* 多租户采用分库方案时通过该字段配置所在数据源
*
* 关联 DataSourceConfigDO id 字段
*/
private Long datasourceConfigId;
}

View File

@ -44,15 +44,11 @@ spring:
primary: master
datasource:
master:
name: ruoyi-vue-pro
url: jdbc:mysql://400-infra.server.iocoder.cn:3306/${spring.datasource.dynamic.datasource.master.name}?useSSL=false&serverTimezone=UTC&useSSL=false&allowPublicKeyRetrieval=true
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://400-infra.server.iocoder.cn:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=UTC&useSSL=false&allowPublicKeyRetrieval=true
username: root
password: 3WLiVUBEwTbvAfsh
slave: # 模拟从库,可根据自己需要修改 # 模拟从库,可根据自己需要修改
name: ruoyi-vue-pro
url: jdbc:mysql://400-infra.server.iocoder.cn:3306/${spring.datasource.dynamic.datasource.slave.name}?useSSL=false&serverTimezone=UTC&useSSL=false&allowPublicKeyRetrieval=true
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://400-infra.server.iocoder.cn:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=UTC&useSSL=false&allowPublicKeyRetrieval=true
username: root
password: 3WLiVUBEwTbvAfsh

View File

@ -44,37 +44,25 @@ spring:
primary: master
datasource:
master:
name: ruoyi-vue-pro-master
url: jdbc:mysql://127.0.0.1:3306/${spring.datasource.dynamic.datasource.master.name}?useSSL=false&serverTimezone=UTC&useSSL=false&allowPublicKeyRetrieval=true # MySQL Connector/J 8.X 连接的示例
# url: jdbc:mysql://127.0.0.1:3306/${spring.datasource.dynamic.datasource.master.name}?useSSL=false&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=CTT # MySQL Connector/J 5.X 连接的示例
# url: jdbc:postgresql://127.0.0.1:5432/${spring.datasource.dynamic.datasource.slave.name} # PostgreSQL 连接的示例
url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro-master?useSSL=false&serverTimezone=UTC&useSSL=false&allowPublicKeyRetrieval=true # MySQL Connector/J 8.X 连接的示例
# url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro-master?useSSL=false&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=CTT # MySQL Connector/J 5.X 连接的示例
# url: jdbc:postgresql://127.0.0.1:5432/ruoyi-vue-pro # PostgreSQL 连接的示例
# url: jdbc:oracle:thin:@127.0.0.1:1521:xe # Oracle 连接的示例
# url: jdbc:sqlserver://127.0.0.1:1433;DatabaseName=${spring.datasource.dynamic.datasource.master.name} # SQLServer 连接的示例
# url: jdbc:sqlserver://127.0.0.1:1433;DatabaseName=ruoyi-vue-pro-master # SQLServer 连接的示例
username: root
password: 123456
# username: sa
# password: JSm:g(*%lU4ZAkz06cd52KqT3)i1?H7W
slave: # 模拟从库,可根据自己需要修改
name: ruoyi-vue-pro
url: jdbc:mysql://127.0.0.1:3306/${spring.datasource.dynamic.datasource.slave.name}?useSSL=false&serverTimezone=UTC&useSSL=false&allowPublicKeyRetrieval=true # MySQL Connector/J 8.X 连接的示例
# url: jdbc:mysql://127.0.0.1:3306/${spring.datasource.dynamic.datasource.slave.name}?useSSL=false&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=CTT # MySQL Connector/J 5.X 连接的示例
# url: jdbc:postgresql://127.0.0.1:5432/${spring.datasource.dynamic.datasource.slave.name} # PostgreSQL 连接的示例
url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=UTC&useSSL=false&allowPublicKeyRetrieval=true # MySQL Connector/J 8.X 连接的示例
# url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=CTT # MySQL Connector/J 5.X 连接的示例
# url: jdbc:postgresql://127.0.0.1:5432/ruoyi-vue-pro # PostgreSQL 连接的示例
# url: jdbc:oracle:thin:@127.0.0.1:1521:xe # Oracle 连接的示例
# url: jdbc:sqlserver://127.0.0.1:1433;DatabaseName=${spring.datasource.dynamic.datasource.slave.name} # SQLServer 连接的示例
# url: jdbc:sqlserver://127.0.0.1:1433;DatabaseName=ruoyi-vue-pro # SQLServer 连接的示例
username: root
password: 123456
# username: sa
# password: JSm:g(*%lU4ZAkz06cd52KqT3)i1?H7W
tenant_1_ds:
name: tenant_1
url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro-tenant-a?useSSL=false&serverTimezone=UTC&useSSL=false&allowPublicKeyRetrieval=true
username: root
password: 123456
tenant_2_ds:
name: tenant_2
url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro-tenant-b?useSSL=false&serverTimezone=UTC&useSSL=false&allowPublicKeyRetrieval=true
username: root
password: 123456
# Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优
redis: