This commit is contained in:
2023-07-13 17:30:09 +08:00
parent 672f66b40e
commit 6ed1224410
373 changed files with 566 additions and 409 deletions

View File

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>qiaoba-common</artifactId>
<groupId>com.qiaoba</groupId>
<version>1.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>qiaoba-common-datasource</artifactId>
<description>通用-数据源模块</description>
<dependencies>
<!-- Druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
</dependency>
<!-- Mysql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- PgSql -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>
<!-- mybatis plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.qiaoba</groupId>
<artifactId>qiaoba-common-redis</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,16 @@
package com.qiaoba.common.database.annotation;
import java.lang.annotation.*;
/**
* 只查询一条数据
*
* @author ailanyin
* @version 1.0
* @since 2023/6/21 10:10
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SelectOneRow {
}

View File

@ -0,0 +1,179 @@
package com.qiaoba.common.database.config;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.io.IoUtil;
import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.pool.DruidPooledConnection;
import com.qiaoba.common.base.constant.TenantConstant;
import com.qiaoba.common.base.context.BaseContext;
import com.qiaoba.common.base.enums.BaseEnum;
import com.qiaoba.common.database.context.BackupDatasourceContext;
import com.qiaoba.common.database.context.DynamicDataSourceContext;
import com.qiaoba.common.database.context.PrimaryDatasourceContext;
import com.qiaoba.common.database.context.TenantDbTypeContext;
import com.qiaoba.common.database.entity.DynamicDataSource;
import com.qiaoba.common.database.monitor.NotOnlineDatasourceMonitor;
import com.qiaoba.common.database.properties.DataSourceProperties;
import com.qiaoba.common.database.service.DynamicDatasourceService;
import com.qiaoba.common.database.util.DatasourceUtil;
import com.qiaoba.common.database.util.JdbcUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.annotation.Resource;
import java.sql.SQLException;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
/**
* 多数据源配置
*
* @author ailanyin
* @version 1.0
* @since 2021/10/15 0015 下午 16:43
*/
@Slf4j
@Configuration
public class DynamicDataSourceConfig {
/**
* Bean 必须叫 dataSource
*/
@Resource
private DynamicDataSourceContext dataSource;
@Resource
private DataSourceProperties dataSourceProperties;
@Resource
private DynamicDatasourceService dynamicDatasourceService;
/**
* 数据源加载完成
*/
public static Boolean COMPLETE_LOAD_DATASOURCE = false;
/**
* 租户 ids
*/
public static List<String> TENANT_IDS = ListUtil.toList(TenantConstant.DEFAULT_TENANT_ID);
/**
* 把DynamicDataSourceContext 纳入容器管理其他地方使用DynamicDataSourceConfig 类可以直接从容器取对象并调用freshDataSource方法
*/
@PostConstruct
public void init() {
// 加载系统默认数据源
initDefault();
// 加载租户数据源
initTenant();
COMPLETE_LOAD_DATASOURCE = true;
}
public static DruidDataSource getPrimaryDataSource(String tenantId) {
return (DruidDataSource) PrimaryDatasourceContext.get(tenantId);
}
private void initDefault() {
List<DynamicDataSource> dataSources = dataSourceProperties.getDataSources();
for (int i = 0; i < dataSources.size(); i++) {
// 0索引作为主数据源
Object dataSource = DatasourceUtil.buildDataSource(TenantConstant.DEFAULT_TENANT_ID, dataSources.get(i));
if (Objects.isNull(dataSource)) {
// 默认的主数据源挂了
// 加入到错误数据源Map 等待重试
NotOnlineDatasourceMonitor.addErrorDatasource(TenantConstant.DEFAULT_TENANT_ID, dataSources.get(i));
dataSources.remove(i);
} else {
addPrimaryMap(TenantConstant.DEFAULT_TENANT_ID, dataSource);
dataSources.remove(i);
break;
}
}
if (CollUtil.isEmpty(PrimaryDatasourceContext.getAll())) {
log.error("主系统配置数据源全部无效, 请检查 yml 中相关配置");
}
// 其他数据源备用
addBackupMap(TenantConstant.DEFAULT_TENANT_ID, dataSources);
// 刷新数据源
this.dataSource.freshDataSource(PrimaryDatasourceContext.getAll());
}
private void initTenant() {
setDefaultSetting();
Map<String, List<DynamicDataSource>> tenantDatasource = dynamicDatasourceService.loadAllTenantDatasource();
for (String tenantId : tenantDatasource.keySet()) {
List<DynamicDataSource> dataSources = tenantDatasource.get(tenantId);
for (int i = 0; i < dataSources.size(); i++) {
DynamicDataSource dynamicDataSource = dataSources.get(i);
if (BaseEnum.YES.getCode().equals(dynamicDataSource.getIsPrimary())) {
Object dataSource = DatasourceUtil.buildDataSource(dataSources.get(i).getTenantId(), dataSources.get(i));
if (Objects.isNull(dataSource)) {
// 默认的主数据源挂了
// 加入到错误数据源Map 等待重试
NotOnlineDatasourceMonitor.addErrorDatasource(tenantId, dataSources.get(i));
// 在数据源集合中删除, 防止将错误的数据源加载到备用数据源中
dataSources.remove(i);
} else {
addPrimaryMap(tenantId, dataSource);
// 去除主要数据源,剩下皆为备用数据源
dataSources.remove(i);
break;
}
}
}
// 备用数据源
addBackupMap(tenantId, dataSources);
TENANT_IDS.add(tenantId);
}
BaseContext.clearAllHolder();
// 刷新数据源
dataSource.freshDataSource(PrimaryDatasourceContext.getAll());
}
/**
* 程序关闭后,要释放数据源连接池
*/
@PreDestroy
public void close() {
Set<Map.Entry<Object, Object>> entries = PrimaryDatasourceContext.getAll().entrySet();
for (Map.Entry<Object, Object> entry : entries) {
DruidDataSource dataSource = (DruidDataSource) entry.getValue();
IoUtil.close(dataSource);
}
}
private void addPrimaryMap(String tenantId, Object dataSource) {
if (Objects.nonNull(dataSource)) {
try {
PrimaryDatasourceContext.set(tenantId, dataSource);
// 将数据源的类型保存
DruidPooledConnection connection = ((DruidDataSource) dataSource).getConnection();
TenantDbTypeContext.set(tenantId, connection.getMetaData().getDatabaseProductName());
// 归还 connection
IoUtil.close(connection);
} catch (SQLException e) {
e.printStackTrace();
}
}
}
private void addBackupMap(String tenantId, List<DynamicDataSource> dataSources) {
if (CollUtil.isNotEmpty(dataSources)) {
BackupDatasourceContext.set(tenantId, dataSources);
}
}
public void setDefaultSetting() {
BaseContext.setDataSource(TenantConstant.DEFAULT_TENANT_ID);
BaseContext.setTenantId(TenantConstant.DEFAULT_TENANT_ID);
BaseContext.setDatabaseType(TenantDbTypeContext.getDefault());
}
}

View File

@ -0,0 +1,66 @@
package com.qiaoba.common.database.config;
import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.net.NetUtil;
import com.baomidou.mybatisplus.core.incrementer.DefaultIdentifierGenerator;
import com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
import com.qiaoba.common.base.context.BaseContext;
import com.qiaoba.common.database.interceptor.SchemaInterceptor;
import com.qiaoba.common.database.interceptor.SelectOneRowInterceptor;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.StringValue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.List;
/**
* MybatisPlusConfig
*
* @author ailanyin
* @version 1.0
* @since 2023/5/8 11:18
*/
@Configuration
public class MybatisPlusConfig {
/**
* 不需要拼接租户ID的表 租户表/租户数据源表
*/
private static final List<String> IGNORE_TABLES = ListUtil.toList("sys_tenant", "sys_tenant_datasource", "generator_table", "generator_table_column");
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new SchemaInterceptor());
interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler() {
@Override
public Expression getTenantId() {
return new StringValue(BaseContext.getTenantId());
}
// 不拼接租户ID
@Override
public boolean ignoreTable(String tableName) {
return IGNORE_TABLES.contains(tableName);
}
}));
interceptor.addInnerInterceptor(new SelectOneRowInterceptor());
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return interceptor;
}
/**
* 使用网卡信息绑定雪花生成器
* 防止集群雪花ID重复
*/
@Bean
public IdentifierGenerator idGenerator() {
return new DefaultIdentifierGenerator(NetUtil.getLocalhost());
}
}

View File

@ -0,0 +1,97 @@
package com.qiaoba.common.database.context;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.ListUtil;
import com.qiaoba.common.database.entity.DynamicDataSource;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 备用(未在使用)数据源
*
* @author ailanyin
* @version 1.0
* @since 2023/6/28 10:36
*/
public class BackupDatasourceContext {
/**
* 备用数据源
*/
private static Map<String, List<DynamicDataSource>> BACKUP_DATASOURCE_MAP = new ConcurrentHashMap<>();
/**
* 获取租户备用数据源
*
* @param tenantId 租户ID
* @return 数据源集合
*/
public static List<DynamicDataSource> get(String tenantId) {
return BACKUP_DATASOURCE_MAP.get(tenantId);
}
/**
* 设置租户备用数据源
*
* @param tenantId 租户ID
* @param list 数据源集合
*/
public static void set(String tenantId, List<DynamicDataSource> list) {
BACKUP_DATASOURCE_MAP.put(tenantId, list);
}
/**
* 添加租户备用数据源
*
* @param tenantId 租户ID
* @param dataSource 数据源
*/
public static void addBackupMap(String tenantId, DynamicDataSource dataSource) {
List<DynamicDataSource> dataSourceList = get(tenantId);
if (CollUtil.isEmpty(dataSourceList)) {
set(tenantId, ListUtil.toList(dataSource));
} else {
dataSourceList.add(dataSource);
}
}
/**
* 删除租户备用数据源
*
* @param tenantId 租户ID
* @param dataSourceId 数据源ID
*/
public static void delBackupMap(String tenantId, String dataSourceId) {
List<DynamicDataSource> dataSourceList = get(tenantId);
if (CollUtil.isEmpty(dataSourceList)) {
for (DynamicDataSource dynamicDataSource : dataSourceList) {
if (dataSourceId.equals(dynamicDataSource.getDatasourceId())) {
dataSourceList.remove(dynamicDataSource);
break;
}
}
}
}
/**
* 修改租户备用数据源
*
* @param tenantId 租户ID
* @param dataSource 数据源
*/
public static void updateBackupMap(String tenantId, DynamicDataSource dataSource) {
List<DynamicDataSource> dataSourceList = get(tenantId);
if (CollUtil.isEmpty(dataSourceList)) {
for (DynamicDataSource dynamicDataSource : dataSourceList) {
if (dataSource.getDatasourceId().equals(dynamicDataSource.getDatasourceId())) {
dataSourceList.remove(dynamicDataSource);
dataSourceList.add(dataSource);
break;
}
}
}
}
}

View File

@ -0,0 +1,42 @@
package com.qiaoba.common.database.context;
import com.qiaoba.common.base.constant.TenantConstant;
import com.qiaoba.common.base.context.BaseContext;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import java.util.Map;
/**
* 动态数据源上下文对象
*
* @author ailanyin
* @version 1.0
* @since 2021/10/15 0015 下午 16:43
*/
public class DynamicDataSourceContext extends AbstractRoutingDataSource {
/**
* 设置默认数据源、全部数据源,及刷新
*/
public void freshDataSource(Map<Object, Object> targetDataSources) {
//默认数据源 (determineCurrentLookupKey 如果找不到就使用默认数据源)
super.setDefaultTargetDataSource(targetDataSources.get(TenantConstant.DEFAULT_TENANT_ID));
//设置全部数据源
super.setTargetDataSources(targetDataSources);
//刷新(即把targetDataSources刷到resolvedDataSources中去resolvedDataSources才是我们真正存放数据源的map)
super.afterPropertiesSet();
}
@Override
protected Object determineCurrentLookupKey() {
//获取当前指定的数据源
return BaseContext.getDataSource();
}
@Override
public void afterPropertiesSet() {
}
}

View File

@ -0,0 +1,63 @@
package com.qiaoba.common.database.context;
import com.qiaoba.common.base.constant.TenantConstant;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 主要(正在使用)数据源
*
* @author ailanyin
* @version 1.0
* @since 2023/6/28 10:36
*/
public class PrimaryDatasourceContext {
/**
* 主要数据源
*/
private static Map<Object, Object> PRIMARY_DATASOURCE_MAP = new ConcurrentHashMap<>();
public static Map<Object, Object> getAll() {
return PRIMARY_DATASOURCE_MAP;
}
/**
* 获取租户主要数据源
*
* @param tenantId 租户ID
* @return 数据源
*/
public static Object get(String tenantId) {
return PRIMARY_DATASOURCE_MAP.get(tenantId);
}
/**
* 获取默认(主)主要数据源
*
* @return 数据源
*/
public static Object getDefault() {
return PRIMARY_DATASOURCE_MAP.get(TenantConstant.DEFAULT_TENANT_ID);
}
/**
* 设置租户主要数据源
*
* @param tenantId 租户ID
* @param datasource 数据源
*/
public static void set(String tenantId, Object datasource) {
PRIMARY_DATASOURCE_MAP.put(tenantId, datasource);
}
/**
* 删除
*
* @param tenantId 租户ID
*/
public static void remove(String tenantId) {
PRIMARY_DATASOURCE_MAP.remove(tenantId);
}
}

View File

@ -0,0 +1,59 @@
package com.qiaoba.common.database.context;
import com.qiaoba.common.base.constant.TenantConstant;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 租户数据源类型
*
* @author ailanyin
* @version 1.0
* @since 2023/6/28 10:25
*/
public class TenantDbTypeContext {
/**
* 租户数据源类型
*/
private static Map<String, String> TENANT_DATASOURCE_TYPE_MAP = new ConcurrentHashMap<>();
/**
* 获取租户数据源类型
*
* @param tenantId 租户ID
* @return 数据源类型
*/
public static String get(String tenantId) {
return TENANT_DATASOURCE_TYPE_MAP.get(tenantId);
}
/**
* 设置租户数据源类型
*
* @param tenantId 租户ID
* @param dbType 数据源类型
*/
public static void set(String tenantId, String dbType) {
TENANT_DATASOURCE_TYPE_MAP.put(tenantId, dbType);
}
/**
* 获取默认(主)数据源类型
*
* @return 数据源类型
*/
public static String getDefault() {
return TENANT_DATASOURCE_TYPE_MAP.get(TenantConstant.DEFAULT_TENANT_ID);
}
/**
* 设置默认(主)数据源类型
*
* @param dbType 数据源类型
*/
public static void set(String dbType) {
set(TenantConstant.DEFAULT_TENANT_ID, dbType);
}
}

View File

@ -0,0 +1,70 @@
package com.qiaoba.common.database.entity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 动态数据源实体
*
* @author ailanyin
* @version 1.0
* @since 2023-06-13 20:24:31
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class DynamicDataSource {
/**
* 数据源ID
*/
private String datasourceId;
/**
* 主要数据源
*/
private String isPrimary;
/**
* 数据库-url
*/
private String url;
/**
* 数据库-username
*/
private String username;
/**
* 数据库-password
*/
private String password;
/**
* 数据库-驱动
*/
private String driver;
/**
* 连接池-初始化大小
*/
private Integer initialSize;
/**
* 连接池-最小空闲线程数
*/
private Integer minIdle;
/**
* 连接池-最大连接池数量
*/
private Integer maxActive;
/**
* 租户ID
*/
private String tenantId;
}

View File

@ -0,0 +1,50 @@
package com.qiaoba.common.database.entity;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.Data;
import java.io.Serializable;
/**
* 分页查询实体类
*
* @author ailanyin
* @version 1.0
* @since 2023-04-23 20:33:43
*/
@Data
public class PageQuery implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 分页大小
*/
private Integer pageSize;
/**
* 当前页数
*/
private Integer pageNum;
/**
* 当前记录起始索引 默认值
*/
public static final int DEFAULT_PAGE_NUM = 1;
/**
* 每页显示记录数 默认值 默认查全部
*/
public static final int DEFAULT_PAGE_SIZE = Integer.MAX_VALUE;
public <T> Page<T> build() {
Integer pageNum = ObjectUtil.defaultIfNull(getPageNum(), DEFAULT_PAGE_NUM);
Integer pageSize = ObjectUtil.defaultIfNull(getPageSize(), DEFAULT_PAGE_SIZE);
if (pageNum <= 0) {
pageNum = DEFAULT_PAGE_NUM;
}
return new Page<>(pageNum, pageSize);
}
}

View File

@ -0,0 +1,34 @@
package com.qiaoba.common.database.entity;
import cn.hutool.http.HttpStatus;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.qiaoba.common.base.entity.BasePage;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
/**
* Mybatis-plus 分页封装对象
*
* @author ailanyin
* @version 1.0
* @since 2023-04-23 15:37:43
*/
@Data
@NoArgsConstructor
@EqualsAndHashCode(callSuper = false)
@SuppressWarnings("unchecked")
public class TableDataInfo<T> extends BasePage {
private static final long serialVersionUID = 1L;
public static <T> TableDataInfo<T> build(IPage<T> page) {
TableDataInfo<T> rspData = new TableDataInfo<>();
rspData.setCode(HttpStatus.HTTP_OK);
rspData.setMsg("查询成功");
rspData.setRows(page.getRecords());
rspData.setTotal(page.getTotal());
return rspData;
}
}

View File

@ -0,0 +1,46 @@
package com.qiaoba.common.database.factory;
import com.qiaoba.common.database.config.DynamicDataSourceConfig;
import com.qiaoba.common.database.context.DynamicDataSourceContext;
import com.qiaoba.common.database.properties.DataSourceProperties;
import com.qiaoba.common.database.properties.TenantSchema;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
/**
* 动态数据源工厂
*
* @author ailanyin
* @version 1.0
* @since 2021/10/15 0015 下午 16:43
*/
@Configuration
public class DynamicDataSourceFactory {
@Value("${spring.application.name}")
private String schemaPrefix;
@PostConstruct
public void setSchemaPrefix() {
TenantSchema.setSchemaPrefix(schemaPrefix);
}
@Bean
public DynamicDataSourceContext dataSource() {
return new DynamicDataSourceContext();
}
@Bean
public DynamicDataSourceConfig dynamicDataSourceConfig() {
return new DynamicDataSourceConfig();
}
@Bean
public DataSourceProperties defaultDataSourceProperties() {
return new DataSourceProperties();
}
}

View File

@ -0,0 +1,39 @@
package com.qiaoba.common.database.interceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
import com.qiaoba.common.base.context.BaseContext;
import com.qiaoba.common.database.context.TenantDbTypeContext;
import com.qiaoba.common.database.properties.TenantSchema;
import com.qiaoba.common.database.util.DbUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.statement.StatementHandler;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Objects;
/**
* SchemaInterceptor
*
* @author ailanyin
* @version 1.0
* @since 2023-04-25 22:48:43
*/
@Slf4j
public class SchemaInterceptor implements InnerInterceptor {
@Override
public void beforePrepare(StatementHandler sh, Connection conn, Integer transactionTimeout) {
// SCHEMA 模式
if (Objects.nonNull(BaseContext.isSchemaMode()) && BaseContext.isSchemaMode()) {
try {
DbUtil.setSchema(TenantDbTypeContext.getDefault(), conn, TenantSchema.getSchema(BaseContext.getTenantId()));
} catch (SQLException e) {
log.error("切换SCHEMA失败, 原因: {}", e.getMessage());
}
}
}
}

View File

@ -0,0 +1,53 @@
package com.qiaoba.common.database.interceptor;
import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.pagination.DialectFactory;
import com.baomidou.mybatisplus.extension.plugins.pagination.DialectModel;
import com.baomidou.mybatisplus.extension.plugins.pagination.dialects.IDialect;
import com.baomidou.mybatisplus.extension.toolkit.JdbcUtils;
import com.qiaoba.common.base.util.AnnotationUtil;
import com.qiaoba.common.database.annotation.SelectOneRow;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import java.sql.SQLException;
import java.util.List;
import java.util.Map;
/**
* 查询一条
* 因为Oracle 不支持 limit 1 写法, 故调用MP的分页方言处理
*
* @author ailanyin
* @version 1.0
* @since 2023/6/21 8:53
*/
public class SelectOneRowInterceptor implements InnerInterceptor {
@Override
public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
if (AnnotationUtil.hasAnnotation(ms.getId(), SelectOneRow.class)) {
String buildSql = boundSql.getSql();
IDialect dialect = DialectFactory.getDialect(JdbcUtils.getDbType(executor));
DialectModel model = dialect.buildPaginationSql(buildSql, 0, 1);
replaceSql(ms, boundSql, model);
}
}
private void replaceSql(MappedStatement ms, BoundSql boundSql, DialectModel model) {
PluginUtils.MPBoundSql mpBoundSql = PluginUtils.mpBoundSql(boundSql);
final Configuration configuration = ms.getConfiguration();
List<ParameterMapping> mappings = mpBoundSql.parameterMappings();
Map<String, Object> additionalParameter = mpBoundSql.additionalParameters();
model.consumers(mappings, configuration, additionalParameter);
mpBoundSql.sql(model.getDialectSql());
mpBoundSql.parameterMappings(mappings);
}
}

View File

@ -0,0 +1,110 @@
package com.qiaoba.common.database.monitor;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.ListUtil;
import com.baomidou.lock.LockInfo;
import com.baomidou.lock.LockTemplate;
import com.qiaoba.common.database.config.DynamicDataSourceConfig;
import com.qiaoba.common.database.context.BackupDatasourceContext;
import com.qiaoba.common.database.entity.DynamicDataSource;
import com.qiaoba.common.database.util.JdbcUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* 不在线的数据源监控
* 尝试连接,如果连接成功,加入到备用数据源
*
* @author ailanyin
* @version 1.0
* @since 2023/6/26 10:46
*/
@Component
@Slf4j
public class NotOnlineDatasourceMonitor {
/**
* 错误数据源(连接中断)
* <p>
* key: 租户ID
* value: 错误数据源 list
*/
public static Map<String, List<DynamicDataSource>> ERROR_DATASOURCE_MAP = new ConcurrentHashMap<>();
private static final String LOCK_KEY = "lock4j:notOnlineDatasourceMonitor";
@Resource
private LockTemplate lockTemplate;
@PostConstruct
public void init() {
// 10s 运行一次
new Timer().schedule(new TimerTask() {
@Override
public void run() {
// 项目启动时加载数据源还未完成
if (!DynamicDataSourceConfig.COMPLETE_LOAD_DATASOURCE) {
return;
}
// 没有暂时失联的数据源, 直接结束
if (CollUtil.isEmpty(ERROR_DATASOURCE_MAP)) {
return;
}
// expire = -1 锁自动续期, 防止数据源过多或异常等待, 超过默认锁 30s
final LockInfo lockInfo = lockTemplate.lock(LOCK_KEY, -1, 1000);
//申请锁失败 说明集群中其他设备正在执行监控
if (null == lockInfo) {
return;
}
//申请锁成功
try {
log.trace("开始-[错误数据源重试]-线程, 时间: {}", new Date());
tryConnect();
log.trace("结束-[错误数据源重试]-线程, 时间: {}", new Date());
} finally {
// 释放锁
lockTemplate.releaseLock(lockInfo);
}
}
}, 0, 10 * 1000);
}
private void tryConnect() {
Set<String> tenantIds = ERROR_DATASOURCE_MAP.keySet();
for (String tenantId : tenantIds) {
List<DynamicDataSource> errorDatasourceList = ERROR_DATASOURCE_MAP.get(tenantId);
for (int i = 0; i < errorDatasourceList.size(); i++) {
DynamicDataSource errorDatasource = errorDatasourceList.get(i);
// 说明连接成功
boolean check = JdbcUtil.checkConnect(errorDatasource.getDriver(), errorDatasource.getUrl(), errorDatasource.getUsername(), errorDatasource.getPassword());
if (check) {
log.info("数据源重连成功, Url: {}", errorDatasource.getUrl());
// 从errorMap中删除
errorDatasourceList.remove(errorDatasource);
if (CollUtil.isEmpty(errorDatasourceList)) {
ERROR_DATASOURCE_MAP.remove(tenantId);
}
// 加入到备用Map中
BackupDatasourceContext.addBackupMap(tenantId, errorDatasource);
}
}
}
}
public static void addErrorDatasource(String tenantId, DynamicDataSource dataSource) {
List<DynamicDataSource> errorDataSourceList = NotOnlineDatasourceMonitor.ERROR_DATASOURCE_MAP.get(tenantId);
if (CollUtil.isEmpty(errorDataSourceList)) {
NotOnlineDatasourceMonitor.ERROR_DATASOURCE_MAP.put(tenantId, ListUtil.toList(dataSource));
} else {
errorDataSourceList.add(dataSource);
}
}
}

View File

@ -0,0 +1,222 @@
package com.qiaoba.common.database.monitor;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.io.IoUtil;
import com.alibaba.druid.pool.DruidDataSource;
import com.baomidou.lock.LockInfo;
import com.baomidou.lock.LockTemplate;
import com.qiaoba.common.base.constant.TenantConstant;
import com.qiaoba.common.base.enums.DataBaseEnum;
import com.qiaoba.common.database.config.DynamicDataSourceConfig;
import com.qiaoba.common.database.context.BackupDatasourceContext;
import com.qiaoba.common.database.context.PrimaryDatasourceContext;
import com.qiaoba.common.database.entity.DynamicDataSource;
import com.qiaoba.common.database.service.DynamicDatasourceService;
import com.qiaoba.common.database.util.DatasourceUtil;
import com.qiaoba.common.database.util.JdbcUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* 在线的数据源监控
* 尝试连接, 如果连接不成功, 替换可用数据源, 将失败数据源加入到错误数据源, 等待重试
*
* @author ailanyin
* @version 1.0
* @since 2023/6/26 10:46
*/
@Component
@Slf4j
public class OnlineDatasourceMonitor {
private static final String LOCK_KEY = "lock4j:onlineDatasourceMonitor";
private static Map<String, String> WAIT_UPDATE_DATASOURCE_STATUS = new ConcurrentHashMap<>();
private static Map<String, List<String>> WAIT_ADD_ERROR_MAP = new ConcurrentHashMap<>();
@Resource
private LockTemplate lockTemplate;
@Resource
private DynamicDatasourceService dynamicDatasourceService;
@PostConstruct
public void init() {
// 1s 运行一次
new Timer().schedule(new TimerTask() {
@Override
public void run() {
// 项目启动时加载数据源还未完成
if (!DynamicDataSourceConfig.COMPLETE_LOAD_DATASOURCE) {
return;
}
// expire = -1 锁自动续期, 防止数据源过多或异常等待, 超过默认锁 30s
final LockInfo lockInfo = lockTemplate.lock(LOCK_KEY, -1, 1000);
//申请锁失败 说明集群中其他设备正在执行监控
if (null == lockInfo) {
return;
}
//申请锁成功
try {
log.trace("开始运行数据源监控线程, 时间: {}", new Date());
tryConnect();
log.trace("结束运行数据源监控线程, 时间: {}", new Date());
} finally {
// 释放锁
lockTemplate.releaseLock(lockInfo);
}
}
}, 0, 1000);
}
private void tryConnect() {
for (String tenantId : DynamicDataSourceConfig.TENANT_IDS) {
Object primary = PrimaryDatasourceContext.get(tenantId);
if (Objects.isNull(primary)) {
// 说明初始化主要数据源的时候出错
log.error("租户[{}]-目前主数据源异常, 开始切换备用数据源", tenantId);
// 切换备用数据源为主数据源
backToPrimary(tenantId);
continue;
}
DruidDataSource dataSource = (DruidDataSource) primary;
Connection connection = null;
try {
connection = dataSource.getConnection();
if (JdbcUtil.checkConnect(connection)) {
// 说明数据源正常
log.trace("租户[{}]-目前主数据源正常, 无需切换数据源", tenantId);
// 系统默认主数据源 处理任务
if (TenantConstant.DEFAULT_TENANT_ID.equals(tenantId)) {
handleUpdateDatasourceStatus();
handleErrorDatasource();
}
continue;
}
log.error("租户[{}]-目前主数据源异常, 开始切换备用数据源", tenantId);
// 主数据源异常 切换备用数据源
if (!backToPrimary(tenantId)) {
// 备用切换失败
// 关闭原有异常数据源
IoUtil.close(dataSource);
// 在数据源Map中删除
PrimaryDatasourceContext.remove(tenantId);
}
// 将原有异常数据源加入到错误数据源Map, 等待重试
addErrorDatasource(tenantId, dataSource);
} catch (SQLException e) {
} finally {
IoUtil.close(connection);
}
}
}
/**
* 切换备用数据源为主数据源
*
* @param tenantId 租户ID
*/
private Boolean backToPrimary(String tenantId) {
// 备用数据源
List<DynamicDataSource> dataSources = BackupDatasourceContext.get(tenantId);
if (CollUtil.isEmpty(dataSources)) {
log.error("租户:[{}]切换备用数据源失败, 原因: 没有备用数据源", tenantId);
return false;
}
Integer backupIndex = null;
for (int i = 0; i < dataSources.size(); i++) {
Object dynamicDataSource = DatasourceUtil.buildDataSource(tenantId, dataSources.get(i));
// 不是空,说明备用数据源有用
if (Objects.nonNull(dynamicDataSource)) {
DatasourceUtil.changePrimaryDatasource(tenantId, dynamicDataSource);
backupIndex = i;
break;
}
}
if (Objects.nonNull(backupIndex)) {
// 切换成功
DynamicDataSource dynamicDataSource = dataSources.get(backupIndex);
// 更改数据库中该数据源为主要数据源
if (Objects.nonNull(dynamicDataSource.getTenantId()) && !TenantConstant.DEFAULT_TENANT_ID.equals(dynamicDataSource.getTenantId())) {
// 添加到待处理任务中
WAIT_UPDATE_DATASOURCE_STATUS.put(dynamicDataSource.getTenantId(), dynamicDataSource.getDatasourceId());
}
// 备用数据源集合删除该数据源
dataSources.remove((int) backupIndex);
log.info("租户:[{}]切换备用数据源成功, 现主数据ID: {}", tenantId, dynamicDataSource.getDatasourceId());
return true;
} else {
log.error("租户:[{}]切换备用数据源失败, 原因: 备用数据源均无效", tenantId);
return false;
}
}
private void handleUpdateDatasourceStatus() {
Set<String> keys = WAIT_UPDATE_DATASOURCE_STATUS.keySet();
for (String key : keys) {
try {
log.info("开始更新数据库中租户数据源状态, 租户ID: {}", key);
// 防止更新过程中主数据挂了
dynamicDatasourceService.changePrimaryDatasource(key, WAIT_UPDATE_DATASOURCE_STATUS.get(key));
// 处理完成 删除任务
WAIT_UPDATE_DATASOURCE_STATUS.remove(key);
log.info("更新数据库中租户数据源状态完成, 租户ID: {}", key);
} catch (Exception e) {
log.error("更新数据库中租户数据源状态未完成, 租户ID: {}, 失败原因: {}", key, e.getMessage());
}
}
}
private void handleErrorDatasource() {
Set<String> tenantIds = WAIT_ADD_ERROR_MAP.keySet();
for (String tenantId : tenantIds) {
List<String> ipList = WAIT_ADD_ERROR_MAP.get(tenantId);
for (String ip : ipList) {
NotOnlineDatasourceMonitor.addErrorDatasource(tenantId, dynamicDatasourceService.selectByIp(tenantId, ip));
}
WAIT_ADD_ERROR_MAP.remove(tenantId);
}
}
private void addErrorDatasource(String tenantId, DruidDataSource dataSource) {
// 主系统
if (TenantConstant.DEFAULT_TENANT_ID.equals(tenantId)) {
DynamicDataSource dynamicDataSource = new DynamicDataSource();
dynamicDataSource.setPassword(dataSource.getPassword());
dynamicDataSource.setUsername(dataSource.getUsername());
dynamicDataSource.setUrl(dataSource.getUrl());
dynamicDataSource.setDriver(dataSource.getDriverClassName());
dynamicDataSource.setTenantId(tenantId);
dynamicDataSource.setInitialSize(dataSource.getInitialSize());
dynamicDataSource.setMaxActive(dataSource.getMaxActive());
dynamicDataSource.setMinIdle(dataSource.getMinIdle());
NotOnlineDatasourceMonitor.addErrorDatasource(tenantId, dynamicDataSource);
return;
}
// 普通租户
String ip = DataBaseEnum.getIp(dataSource.getUrl(), dataSource.getDriverClassName());
List<String> errorIpList = WAIT_ADD_ERROR_MAP.get(tenantId);
if (CollUtil.isEmpty(errorIpList)) {
WAIT_ADD_ERROR_MAP.put(tenantId, CollUtil.toList(ip));
} else {
errorIpList.add(ip);
}
}
}

View File

@ -0,0 +1,26 @@
package com.qiaoba.common.database.properties;
import com.qiaoba.common.database.entity.DynamicDataSource;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* DataSourceProperties
*
* @author ailanyin
* @version 1.0
* @since 2022-09-22 04:20:28
*/
@Component
@ConfigurationProperties(prefix = "qiaoba")
@Data
@EnableConfigurationProperties
public class DataSourceProperties {
private List<DynamicDataSource> dataSources;
}

View File

@ -0,0 +1,35 @@
package com.qiaoba.common.database.properties;
import com.qiaoba.common.base.constant.BaseConstant;
import org.springframework.stereotype.Component;
/**
* 租户Schema
*
* @author ailanyin
* @version 1.0
* @since 2023/6/29 8:53
*/
@Component
public class TenantSchema {
private static String schemaPrefix;
public static void setSchemaPrefix(String name) {
schemaPrefix = name;
}
/**
* 租户schema生成逻辑
* schemaPrefix + '-' + tenantId
* eg: schema = abc-2
*
* @param tenantId 租户ID
* @return schema
*/
public static String getSchema(String tenantId) {
return schemaPrefix + BaseConstant.HYPHEN_JOIN_STR + tenantId;
}
}

View File

@ -0,0 +1,40 @@
package com.qiaoba.common.database.service;
import com.qiaoba.common.database.entity.DynamicDataSource;
import java.util.List;
import java.util.Map;
/**
* 动态数据源接口
*
* @author ailanyin
* @version 1.0
* @since 2023/6/9 13:15
*/
public interface DynamicDatasourceService {
/**
* 加载所有租户的数据源
*
* @return 数据源集合 key: tenantId | value: list
*/
Map<String, List<DynamicDataSource>> loadAllTenantDatasource();
/**
* 改变主数据源
*
* @param tenantId 租户ID
* @param datasourceId 数据源ID
*/
void changePrimaryDatasource(String tenantId, String datasourceId);
/**
* 通过IP查询
*
* @param tenantId tenantId
* @param ip ip
* @return obj
*/
DynamicDataSource selectByIp(String tenantId, String ip);
}

View File

@ -0,0 +1,82 @@
package com.qiaoba.common.database.util;
import cn.hutool.core.io.IoUtil;
import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.pool.DruidPooledConnection;
import com.qiaoba.common.database.context.DynamicDataSourceContext;
import com.qiaoba.common.database.context.PrimaryDatasourceContext;
import com.qiaoba.common.database.context.TenantDbTypeContext;
import com.qiaoba.common.database.entity.DynamicDataSource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.sql.SQLException;
/**
* DatasourceUtil
*
* @author ailanyin
* @version 1.0
* @since 2023/7/13 0013 下午 16:20
*/
@Component
@Slf4j
public class DatasourceUtil {
@Resource
private DynamicDataSourceContext dataSource;
private static DynamicDataSourceContext dynamicDataSourceContext;
@PostConstruct
public void init() {
dynamicDataSourceContext = dataSource;
}
public static void changePrimaryDatasource(String tenantId, Object datasource) {
PrimaryDatasourceContext.set(tenantId, datasource);
// 将数据源的类型保存
DruidPooledConnection connection = null;
try {
connection = ((DruidDataSource) datasource).getConnection();
TenantDbTypeContext.set(tenantId, connection.getMetaData().getDatabaseProductName());
} catch (SQLException e) {
e.printStackTrace();
} finally {
// 归还 connection
IoUtil.close(connection);
}
dynamicDataSourceContext.freshDataSource(PrimaryDatasourceContext.getAll());
}
public static Object buildDataSource(String tenantId, DynamicDataSource dynamicDataSource) {
log.debug("正在创建数据源DataSource, 租户: {}", tenantId);
boolean connect = JdbcUtil.checkConnect(dynamicDataSource.getDriver(), dynamicDataSource.getUrl(), dynamicDataSource.getUsername(), dynamicDataSource.getPassword());
if (!connect) {
log.error("租户: {} 数据源连接失败, Url: {}", tenantId, dynamicDataSource.getUrl());
return null;
}
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUrl(dynamicDataSource.getUrl());
dataSource.setUsername(dynamicDataSource.getUsername());
dataSource.setPassword(dynamicDataSource.getPassword());
dataSource.setDriverClassName(dynamicDataSource.getDriver());
dataSource.setInitialSize(dynamicDataSource.getInitialSize());
dataSource.setMinIdle(dynamicDataSource.getMinIdle());
dataSource.setMaxActive(dynamicDataSource.getMaxActive());
try {
dataSource.addFilters("stat");
// wall 防火墙 切勿开启, 开启后 导入SQL 会失败
// dataSource.addFilters("wall")
// 初始化数据源
dataSource.init();
return dataSource;
} catch (Exception e) {
IoUtil.close(dataSource);
return null;
}
}
}

View File

@ -0,0 +1,79 @@
package com.qiaoba.common.database.util;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.StrUtil;
import com.qiaoba.common.base.enums.DataBaseEnum;
import org.apache.ibatis.jdbc.ScriptRunner;
import org.postgresql.jdbc.PgConnection;
import org.springframework.core.io.ClassPathResource;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
/**
* @author ailanyin
* @version 1.0
* @since 2023/6/28 8:52
*/
public class DbUtil {
/**
* PgSQL 查询 schema 是否存在
*/
private static final String PGSQL_CHECK_SCHEMA_EXIST_SQL = "SELECT count(1) FROM information_schema.schemata WHERE schema_name = '{}'";
public static void setSchema(String dbType, Connection conn, String schema) throws SQLException {
if (DataBaseEnum.MY_SQL.getType().equals(dbType)) {
conn.setSchema(schema);
} else if (DataBaseEnum.POSTGRE_SQL.getType().equals(dbType)) {
conn.unwrap(PgConnection.class).setSchema(schema);
} else if (DataBaseEnum.SQL_SERVER.getType().equals(dbType)) {
}
}
public static Boolean checkSchema(String dbType, Connection conn, String schema) throws Exception {
if (DataBaseEnum.POSTGRE_SQL.getType().equals(dbType)) {
Statement statement = null;
try {
statement = conn.createStatement();
ResultSet resultSet = statement.executeQuery(StrUtil.format(PGSQL_CHECK_SCHEMA_EXIST_SQL, schema));
resultSet.next();
return resultSet.getInt(1) > 0;
} finally {
IoUtil.close(statement);
}
}
return false;
}
public static void runScript(Connection conn, String filePath) throws IOException {
ClassPathResource resource = new ClassPathResource(filePath);
File file = resource.getFile();
FileReader reader = new FileReader(file);
ScriptRunner scriptRunner = new ScriptRunner(conn);
scriptRunner.setSendFullScript(true);
scriptRunner.runScript(reader);
reader.close();
}
public static void runSql(Connection conn, String sql) throws SQLException {
Statement statement = null;
try {
statement = conn.createStatement();
statement.execute(sql);
} finally {
IoUtil.close(statement);
}
}
}

View File

@ -0,0 +1,85 @@
package com.qiaoba.common.database.util;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.StrUtil;
import com.qiaoba.common.base.enums.DataBaseEnum;
import com.qiaoba.common.base.exception.ServiceException;
import lombok.extern.slf4j.Slf4j;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
/**
* JdbcUtil
*
* @author ailanyin
* @version 1.0
* @since 2021/10/15 0015 下午 16:43
*/
@Slf4j
public class JdbcUtil {
/**
* 检查数据源是否可以连接
*
* @param driver 数据库驱动
* @param url url
* @param username 用户名
* @param password 密码
* @return true = 是
*/
public static boolean checkConnect(String driver, String url, String username, String password) {
Connection conn = null;
try {
Class.forName(driver);
//建立连接
conn = DriverManager.getConnection(url, username, password);
return true;
} catch (Exception e) {
log.debug(e.getMessage());
return false;
} finally {
IoUtil.close(conn);
}
}
/**
* 检查数据源连接可用性
*
* @param conn 数据源
* @return 结果
*/
public static boolean checkConnect(Connection conn) {
Statement stmt = null;
ResultSet rs = null;
try {
stmt = conn.createStatement();
// 允许 2s 延时
stmt.setQueryTimeout(2);
rs = stmt.executeQuery(DataBaseEnum.getCheckSql(conn.getMetaData().getDatabaseProductName()));
return true;
} catch (Exception e) {
return false;
} finally {
IoUtil.close(rs);
IoUtil.close(stmt);
}
}
public static Connection getConnection(String driver, String url, String username, String password) {
try {
Class.forName(driver);
return DriverManager.getConnection(url, username, password);
} catch (Exception e) {
throw new ServiceException(StrUtil.format("数据源连接失败,错误: {}", e.getMessage()));
}
}
public static Connection connection(String driver, String url, String username, String password) throws Exception {
Class.forName(driver);
return DriverManager.getConnection(url, username, password);
}
}

View File

@ -0,0 +1,46 @@
{
"groups": [],
"properties": [
{
"name": "qiaoba.dataSources",
"type": "java.util.List<com.qiaoba.common.database.entity.DynamicDataSource>",
"sourceType": "com.qiaoba.common.database.properties.DataSourceProperties"
},
{
"name": "qiaoba.datasources.driver",
"type": "java.lang.String",
"sourceType": "com.qiaoba.common.database.properties.DataSourceProperties"
},
{
"name": "qiaoba.datasources.url",
"type": "java.lang.String",
"sourceType": "com.qiaoba.common.database.properties.DataSourceProperties"
},
{
"name": "qiaoba.datasources.username",
"type": "java.lang.String",
"sourceType": "com.qiaoba.common.database.properties.DataSourceProperties"
},
{
"name": "qiaoba.datasources.password",
"type": "java.lang.String",
"sourceType": "com.qiaoba.common.database.properties.DataSourceProperties"
},
{
"name": "qiaoba.datasources.initial-size",
"type": "java.lang.Integer",
"sourceType": "com.qiaoba.common.database.properties.DataSourceProperties"
},
{
"name": "qiaoba.datasources.min-idle",
"type": "java.lang.Integer",
"sourceType": "com.qiaoba.common.database.properties.DataSourceProperties"
},
{
"name": "qiaoba.datasources.max-active",
"type": "java.lang.Integer",
"sourceType": "com.qiaoba.common.database.properties.DataSourceProperties"
}
],
"hints": []
}

View File

@ -0,0 +1,4 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.qiaoba.common.database.factory.DynamicDataSourceFactory,\
com.qiaoba.common.database.util.DatasourceUtil,\
com.qiaoba.common.database.config.MybatisPlusConfig