add
This commit is contained in:
43
qiaoba-common/qiaoba-common-datasource/pom.xml
Normal file
43
qiaoba-common/qiaoba-common-datasource/pom.xml
Normal 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>
|
@ -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 {
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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() {
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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": []
|
||||
}
|
@ -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
|
Reference in New Issue
Block a user