This commit is contained in:
2023-06-14 17:38:18 +08:00
parent 5511fd8fb0
commit 239201c811
16 changed files with 260 additions and 28 deletions

View File

@ -1,6 +1,7 @@
package com.qiaoba.common.database.config;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.ListUtil;
import com.alibaba.druid.pool.DruidDataSource;
import com.qiaoba.common.base.constants.TenantConstant;
import com.qiaoba.common.base.context.BaseContext;
@ -9,13 +10,13 @@ import com.qiaoba.common.database.constants.DynamicDatasourceConstant;
import com.qiaoba.common.database.entity.DynamicDataSource;
import com.qiaoba.common.database.properties.DataSourceProperties;
import com.qiaoba.common.database.service.DynamicDatasourceService;
import com.qiaoba.common.database.utils.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;
@ -43,6 +44,11 @@ public class DynamicDataSourceConfig {
@Resource
private DynamicDatasourceService dynamicDatasourceService;
/**
* 数据源加载完成
*/
public static Boolean COMPLETE_LOAD_DATASOURCE = false;
/**
* 主要数据源
*/
@ -53,6 +59,11 @@ public class DynamicDataSourceConfig {
*/
public static Map<String, List<DynamicDataSource>> BACKUP_DATASOURCE_MAP = new ConcurrentHashMap<>();
/**
* 租户 ids
*/
public static List<String> TENANT_IDS = ListUtil.toList(TenantConstant.DEFAULT_TENANT_ID);
/**
* 把DynamicDataSourceContext 纳入容器管理其他地方使用DynamicDataSourceConfig 类可以直接从容器取对象并调用freshDataSource方法
*/
@ -62,6 +73,7 @@ public class DynamicDataSourceConfig {
initDefault();
// 加载租户数据源
initTenant();
COMPLETE_LOAD_DATASOURCE = true;
}
public static DruidDataSource getPrimaryDataSource(String tenantId) {
@ -90,14 +102,17 @@ public class DynamicDataSourceConfig {
addPrimaryMap(DynamicDatasourceConstant.MASTER_PREFIX + tenantId, buildDataSource(dataSources.get(i)));
// 去除主要数据源,剩下皆为备用数据源
dataSources.remove(i);
break;
}
}
// 备用数据源
addBackupMap(tenantId, dataSources);
TENANT_IDS.add(tenantId);
}
BaseContext.clearAllHolder();
// 刷新数据源
dataSource.freshDataSource(PRIMARY_DATASOURCE_MAP);
}
public void deleteTenantDataSource(String tenantId) {
@ -105,7 +120,18 @@ public class DynamicDataSourceConfig {
dataSource.freshDataSource(PRIMARY_DATASOURCE_MAP);
}
private static Object buildDataSource(DynamicDataSource dynamicDataSource) {
public void changePrimaryDatasource(String tenantId, Object datasource) {
PRIMARY_DATASOURCE_MAP.put(DynamicDatasourceConstant.MASTER_PREFIX + tenantId, datasource);
dataSource.freshDataSource(PRIMARY_DATASOURCE_MAP);
}
public static Object buildDataSource(DynamicDataSource dynamicDataSource) {
log.debug("正在创建数据源DataSource, 租户: {}", dynamicDataSource.getTenantId());
boolean connect = JdbcUtil.checkConnect(dynamicDataSource.getDriver(), dynamicDataSource.getUrl(), dynamicDataSource.getUsername(), dynamicDataSource.getPassword());
if (!connect) {
log.error("租户: {} 数据源连接失败, Url: {}", dynamicDataSource.getTenantId(), dynamicDataSource.getUrl());
return null;
}
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUrl(dynamicDataSource.getUrl());
dataSource.setUsername(dynamicDataSource.getUsername());
@ -114,10 +140,11 @@ public class DynamicDataSourceConfig {
dataSource.setInitialSize(dynamicDataSource.getInitialSize());
dataSource.setMinIdle(dynamicDataSource.getMinIdle());
dataSource.setMaxActive(dynamicDataSource.getMaxActive());
dataSource.setKeepAlive(false);
try {
dataSource.init();
return dataSource;
} catch (SQLException e) {
} catch (Exception e) {
dataSource.close();
return null;
}
@ -131,7 +158,9 @@ public class DynamicDataSourceConfig {
Set<Map.Entry<Object, Object>> entries = PRIMARY_DATASOURCE_MAP.entrySet();
for (Map.Entry<Object, Object> entry : entries) {
DruidDataSource dataSource = (DruidDataSource) entry.getValue();
dataSource.close();
if (!dataSource.isClosed()) {
dataSource.close();
}
}
}

View File

@ -19,9 +19,9 @@ import lombok.NoArgsConstructor;
public class DynamicDataSource {
/**
* 租户Code
* 数据源ID
*/
private String tenantId;
private String datasourceId;
/**
* 主要数据源
@ -63,4 +63,8 @@ public class DynamicDataSource {
*/
private Integer maxActive;
/**
* 租户ID
*/
private String tenantId;
}

View File

@ -1,5 +1,6 @@
package com.qiaoba.common.database.interceptors;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
import com.qiaoba.common.base.context.BaseContext;
@ -8,6 +9,7 @@ import org.springframework.beans.factory.annotation.Value;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Objects;
/**
@ -28,10 +30,14 @@ public class SchemaInterceptor implements InnerInterceptor {
if (Objects.nonNull(BaseContext.isSchemaMode()) && BaseContext.isSchemaMode()) {
// eg: use qiaoba-1;
String sql = StrUtil.format("use `{}-{}`;", baseDatabase, BaseContext.getTenantId());
Statement statement = null;
try {
conn.createStatement().execute(sql);
statement = conn.createStatement();
statement.execute(sql);
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
IoUtil.close(statement);
}
}
InnerInterceptor.super.beforePrepare(sh, conn, transactionTimeout);

View File

@ -0,0 +1,137 @@
package com.qiaoba.common.database.monitor;
import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.util.JdbcUtils;
import com.qiaoba.common.base.constants.TenantConstant;
import com.qiaoba.common.database.config.DynamicDataSourceConfig;
import com.qiaoba.common.database.constants.DynamicDatasourceConstant;
import com.qiaoba.common.database.entity.DynamicDataSource;
import com.qiaoba.common.database.service.DynamicDatasourceService;
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.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.*;
/**
* 监控-数据源连接监控
*
* @author ailanyin
* @version 1.0
* @since 2023/6/14 14:48
*/
@Component
@Slf4j
public class DatasourceConnectionMonitor {
private static final String CHECK_SQL = "SELECT 1";
@Resource
private DynamicDataSourceConfig dynamicDataSourceConfig;
@Resource
private DynamicDatasourceService dynamicDatasourceService;
//@PostConstruct
public void init() {
new Timer().schedule(new TimerTask() {
@Override
public void run() {
// 项目加载数据源还未完成
if (!DynamicDataSourceConfig.COMPLETE_LOAD_DATASOURCE) {
return;
}
log.debug("开始运行数据源监控线程, 时间: {}", new Date());
for (String tenantId : DynamicDataSourceConfig.TENANT_IDS) {
Object primary = DynamicDataSourceConfig.PRIMARY_DATASOURCE_MAP.get(DynamicDatasourceConstant.MASTER_PREFIX + tenantId);
if (Objects.isNull(primary)) {
// 说明初始化主要数据源的时候出错
log.error("租户[{}]-目前主数据源异常, 开始切换备用数据源", tenantId);
// 切换备用数据源
changePrimary(tenantId);
} else {
DruidDataSource dataSource = (DruidDataSource) primary;
try {
Connection connection = dataSource.getConnection();
if (check(connection)) {
// 说明数据源正常
log.debug("租户[{}]-目前主数据源正常, 无需切换数据源", tenantId);
continue;
}
log.error("租户[{}]-目前主数据源异常, 开始切换备用数据源", tenantId);
// 关闭原有异常数据源
connection.close();
dataSource.close();
// 主数据源异常 切换备用数据源
changePrimary(tenantId);
} catch (SQLException e) {
e.printStackTrace();
}
}
}
log.debug("结束运行数据源监控线程, 时间: {}", new Date());
}
}, 0, 1000);
}
private Boolean check(Connection conn) {
Statement stmt = null;
ResultSet rs = null;
try {
stmt = conn.createStatement();
stmt.setQueryTimeout(1);
rs = stmt.executeQuery(CHECK_SQL);
return true;
} catch (Exception e) {
return false;
} finally {
JdbcUtils.close(rs);
JdbcUtils.close(stmt);
}
}
/**
* 切换主数据源
*
* @param tenantId 租户ID
*/
private void changePrimary(String tenantId) {
// 备用数据源
List<DynamicDataSource> dataSources = DynamicDataSourceConfig.BACKUP_DATASOURCE_MAP.get(tenantId);
if (Objects.isNull(dataSources)) {
log.error("租户:[{}]切换备用数据源失败, 原因: 没有备用数据源", tenantId);
}
Integer backupIndex = null;
for (int i = 0; i < dataSources.size(); i++) {
Object dynamicDataSource = DynamicDataSourceConfig.buildDataSource(dataSources.get(i));
// 不是空,说明备用数据源有用
if (Objects.nonNull(dynamicDataSource)) {
dynamicDataSourceConfig.changePrimaryDatasource(tenantId, dynamicDataSource);
backupIndex = i;
break;
}
}
if (Objects.nonNull(backupIndex)) {
// 切换成功
DynamicDataSource dynamicDataSource = dataSources.get(backupIndex);
// 更改数据库中该数据源为主要数据源
if (!TenantConstant.DEFAULT_TENANT_ID.equals(dynamicDataSource.getTenantId())) {
dynamicDatasourceService.changePrimaryDatasource(dynamicDataSource.getTenantId(), dynamicDataSource.getDatasourceId());
}
// 备用数据源集合删除该数据源
dataSources.remove((int) backupIndex);
log.info("租户:[{}]切换备用数据源成功, 现主数据ID: {}", tenantId, dynamicDataSource.getDatasourceId());
} else {
log.error("租户:[{}]切换备用数据源失败, 原因: 备用数据源均无效", tenantId);
}
}
}

View File

@ -20,4 +20,12 @@ 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);
}

View File

@ -1,5 +1,6 @@
package com.qiaoba.common.database.utils;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.StrUtil;
import com.qiaoba.common.base.exceptions.ServiceException;
@ -39,13 +40,7 @@ public class JdbcUtil {
} catch (Exception e) {
return false;
} finally {
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
IoUtil.close(conn);
}
}

View File

@ -1,3 +1,4 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.qiaoba.common.database.factories.DynamicDataSourceFactory,\
com.qiaoba.common.database.monitor.DatasourceConnectionMonitor,\
com.qiaoba.common.database.config.MybatisPlusConfig