|
@@ -0,0 +1,141 @@
|
|
|
|
|
+package com.fs.framework.datasource;
|
|
|
|
|
+
|
|
|
|
|
+import com.alibaba.druid.pool.DruidDataSource;
|
|
|
|
|
+import com.fs.common.enums.DataSourceType;
|
|
|
|
|
+import com.fs.tenant.domain.TenantInfo;
|
|
|
|
|
+import com.fs.tenant.service.TenantInfoService;
|
|
|
|
|
+import org.slf4j.Logger;
|
|
|
|
|
+import org.slf4j.LoggerFactory;
|
|
|
|
|
+import org.springframework.stereotype.Component;
|
|
|
|
|
+
|
|
|
|
|
+import javax.annotation.Resource;
|
|
|
|
|
+import javax.sql.DataSource;
|
|
|
|
|
+import java.lang.reflect.Field;
|
|
|
|
|
+import java.util.Map;
|
|
|
|
|
+import java.util.concurrent.ConcurrentHashMap;
|
|
|
|
|
+
|
|
|
|
|
+@Component
|
|
|
|
|
+public class TenantDataSourceManager {
|
|
|
|
|
+
|
|
|
|
|
+ private static final Logger log = LoggerFactory.getLogger(TenantDataSourceManager.class);
|
|
|
|
|
+
|
|
|
|
|
+ @Resource
|
|
|
|
|
+ private DynamicDataSource dynamicDataSource;
|
|
|
|
|
+
|
|
|
|
|
+ @Resource
|
|
|
|
|
+ private TenantInfoService tenantInfoService;
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 租户数据源缓存
|
|
|
|
|
+ */
|
|
|
|
|
+ private static final Map<String, DataSource> TENANT_DS_CACHE = new ConcurrentHashMap<>();
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 切换到租户数据源(不存在则创建)
|
|
|
|
|
+ */
|
|
|
|
|
+ public void switchTenant(TenantInfo tenantInfo) {
|
|
|
|
|
+
|
|
|
|
|
+ // 用租户主键作为唯一标识
|
|
|
|
|
+ String tenantKey = buildTenantKey(tenantInfo.getId());
|
|
|
|
|
+
|
|
|
|
|
+ if (!TENANT_DS_CACHE.containsKey(tenantKey)) {
|
|
|
|
|
+ synchronized (this) {
|
|
|
|
|
+ if (!TENANT_DS_CACHE.containsKey(tenantKey)) {
|
|
|
|
|
+
|
|
|
|
|
+ DataSource tenantDs = createTenantDataSource(tenantInfo);
|
|
|
|
|
+ TENANT_DS_CACHE.put(tenantKey, tenantDs);
|
|
|
|
|
+
|
|
|
|
|
+ // 动态追加到已解析的数据源
|
|
|
|
|
+ Map<Object, DataSource> resolvedMap = getResolvedDataSources();
|
|
|
|
|
+ resolvedMap.put(tenantKey, tenantDs);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ThreadLocal 切库
|
|
|
|
|
+ DynamicDataSourceContextHolder.setDataSourceType(tenantKey);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private String buildTenantKey(Long tenantId) {
|
|
|
|
|
+ return "tenant:" + tenantId;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 清理 ThreadLocal
|
|
|
|
|
+ */
|
|
|
|
|
+ public void clear() {
|
|
|
|
|
+ DynamicDataSourceContextHolder.clearDataSourceType();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 根据租户ID确保数据源已注册并切换(用于 Filter/拦截器等非登录场景)
|
|
|
|
|
+ * 解决 JVM 重启后 TENANT_DS_CACHE 被清空,导致 resolvedDataSources 中找不到租户数据源的问题
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param tenantId 租户ID
|
|
|
|
|
+ */
|
|
|
|
|
+ public void ensureSwitchByTenantId(Long tenantId) {
|
|
|
|
|
+ String tenantKey = buildTenantKey(tenantId);
|
|
|
|
|
+
|
|
|
|
|
+ if (TENANT_DS_CACHE.containsKey(tenantKey)) {
|
|
|
|
|
+ DynamicDataSourceContextHolder.setDataSourceType(tenantKey);
|
|
|
|
|
+ log.debug("[TenantDS] 数据源已缓存,直接切换: {}", tenantKey);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
|
|
|
|
|
+ try {
|
|
|
|
|
+ TenantInfo tenantInfo = tenantInfoService.getById(tenantId);
|
|
|
|
|
+ if (tenantInfo == null) {
|
|
|
|
|
+ log.warn("[TenantDS] 租户ID={} 在主库中不存在,回退到主库", tenantId);
|
|
|
|
|
+ DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!tenantInfo.getStatus().equals(1)) {
|
|
|
|
|
+ log.warn("[TenantDS] 租户ID={} 已禁用,回退到主库", tenantId);
|
|
|
|
|
+ DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ switchTenant(tenantInfo);
|
|
|
|
|
+ log.info("[TenantDS] 动态注册并切换数据源: key={}, url={}", tenantKey, tenantInfo.getDbUrl());
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("[TenantDS] 动态注册租户数据源失败, tenantId={}, 回退到主库", tenantId, e);
|
|
|
|
|
+ DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 创建租户数据源(MySQL + Druid)
|
|
|
|
|
+ */
|
|
|
|
|
+ private DataSource createTenantDataSource(TenantInfo tenant) {
|
|
|
|
|
+
|
|
|
|
|
+ DruidDataSource ds = new DruidDataSource();
|
|
|
|
|
+ ds.setUrl(tenant.getDbUrl());
|
|
|
|
|
+ ds.setUsername(tenant.getDbAccount());
|
|
|
|
|
+ ds.setPassword(tenant.getDbPwd());
|
|
|
|
|
+
|
|
|
|
|
+ // 统一 MySQL
|
|
|
|
|
+ ds.setDriverClassName("com.mysql.cj.jdbc.Driver");
|
|
|
|
|
+
|
|
|
|
|
+ ds.setInitialSize(5);
|
|
|
|
|
+ ds.setMinIdle(10);
|
|
|
|
|
+ ds.setMaxActive(20);
|
|
|
|
|
+ ds.setMaxWait(60000);
|
|
|
|
|
+
|
|
|
|
|
+ return ds;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 反射获取 AbstractRoutingDataSource.resolvedDataSources
|
|
|
|
|
+ */
|
|
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
|
|
+ private Map<Object, DataSource> getResolvedDataSources() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ Field field = org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource
|
|
|
|
|
+ .class.getDeclaredField("resolvedDataSources");
|
|
|
|
|
+ field.setAccessible(true);
|
|
|
|
|
+ return (Map<Object, DataSource>) field.get(dynamicDataSource);
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ throw new IllegalStateException("获取 resolvedDataSources 失败", e);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|