refactor: 简化路由逻辑,直接使用 tenantId 匹配实例
问题:tenantGroup 是 tenantId 的简单转换,造成冗余 tenantGroup = "TENANT_" + tenantId.toUpperCase() 解决方案: 1. 直接使用 tenantId 匹配实例 - 移除 tenantGroup 概念 - 负载均衡器直接匹配 metadata.tenant-id 2. 简化配置 - JWT 只需 tenantId 一个字段 - 实例元数据只有 tenant-id 3. 前端简化 - 请求头只需 X-Tenant-Id - 不再需要 X-Tenant-Group 路由规则: 共享实例: metadata.tenant-id = "" (空) VIP实例: metadata.tenant-id = "VIP_001" 匹配逻辑: 找到匹配实例 → VIP专属,找不到 → 共享实例
This commit is contained in:
parent
e52e2ba801
commit
5a2154c1a1
@ -184,8 +184,8 @@ services:
|
||||
REDIS_HOST: redis
|
||||
REDIS_PORT: 6379
|
||||
JAVA_OPTS: -Xms256m -Xmx512m
|
||||
# 租户组 - 共享实例(空值,所有租户可用)
|
||||
TENANT_GROUP: ""
|
||||
# 租户 ID - 共享实例(空值,所有租户可用)
|
||||
TENANT_ID: ""
|
||||
ports:
|
||||
- "8100:8100"
|
||||
depends_on:
|
||||
@ -227,8 +227,8 @@ services:
|
||||
REDIS_HOST: redis
|
||||
REDIS_PORT: 6379
|
||||
JAVA_OPTS: -Xms256m -Xmx512m
|
||||
# 租户组 - VIP_001 专属实例
|
||||
TENANT_GROUP: "TENANT_VIP_001"
|
||||
# 租户 ID - VIP_001 专属实例
|
||||
TENANT_ID: "VIP_001"
|
||||
ports:
|
||||
- "8101:8101"
|
||||
depends_on:
|
||||
@ -270,8 +270,8 @@ services:
|
||||
# REDIS_HOST: redis
|
||||
# REDIS_PORT: 6379
|
||||
# JAVA_OPTS: -Xms256m -Xmx512m
|
||||
# # 租户组 - VIP_002 专属实例
|
||||
# TENANT_GROUP: "TENANT_VIP_002"
|
||||
# # 租户 ID - VIP_002 专属实例
|
||||
# TENANT_ID: "VIP_002"
|
||||
# ports:
|
||||
# - "8102:8102"
|
||||
# depends_on:
|
||||
|
||||
@ -9,17 +9,17 @@ import java.util.List;
|
||||
/**
|
||||
* 多租户路由配置属性
|
||||
*
|
||||
* <p>租户路由基于 Nacos 服务实例的 metadata.tenant-group 元数据进行匹配</p>
|
||||
* <p>租户路由基于 Nacos 服务实例的 metadata.tenant-id 元数据进行匹配</p>
|
||||
*
|
||||
* <h3>工作原理:</h3>
|
||||
* <pre>
|
||||
* 1. 服务实例注册到 Nacos 时,在 metadata 中声明 tenant-group
|
||||
* - 共享实例: tenant-group 为空或不存在
|
||||
* - VIP 实例: tenant-group = "TENANT_VIP_001"
|
||||
* 1. 服务实例注册到 Nacos 时,在 metadata 中声明 tenant-id
|
||||
* - 共享实例: tenant-id 为空或不存在
|
||||
* - VIP 实例: tenant-id = "VIP_001"
|
||||
*
|
||||
* 2. Gateway 从请求中提取租户组,负载均衡器匹配实例元数据
|
||||
* - 请求 tenantGroup = "TENANT_VIP_001" → 路由到匹配的 VIP 实例
|
||||
* - 无匹配实例 → 回退到共享实例
|
||||
* 2. 负载均衡器根据请求中的 tenantId 匹配实例
|
||||
* - 找到匹配的 tenant-id → VIP 专属实例
|
||||
* - 找不到 → 回退到共享实例
|
||||
* </pre>
|
||||
*
|
||||
* <h3>配置示例:</h3>
|
||||
@ -28,11 +28,7 @@ import java.util.List;
|
||||
* routing:
|
||||
* enabled: true
|
||||
* tenant-header: X-Tenant-Id
|
||||
* default-tenant-id: "1"
|
||||
* fallback-to-shared: true
|
||||
* shared-services:
|
||||
* - fund-gateway
|
||||
* - fund-report
|
||||
* </pre>
|
||||
*/
|
||||
@Component
|
||||
@ -45,12 +41,6 @@ public class TenantRoutingProperties {
|
||||
/** 租户 ID 请求头 */
|
||||
private String tenantHeader = "X-Tenant-Id";
|
||||
|
||||
/** 租户组请求头 */
|
||||
private String tenantGroupHeader = "X-Tenant-Group";
|
||||
|
||||
/** 服务组分隔符 */
|
||||
private String groupSeparator = "TENANT_";
|
||||
|
||||
/** 默认租户 ID(当未指定时使用) */
|
||||
private String defaultTenantId = "1";
|
||||
|
||||
@ -80,22 +70,6 @@ public class TenantRoutingProperties {
|
||||
this.tenantHeader = tenantHeader;
|
||||
}
|
||||
|
||||
public String getTenantGroupHeader() {
|
||||
return tenantGroupHeader;
|
||||
}
|
||||
|
||||
public void setTenantGroupHeader(String tenantGroupHeader) {
|
||||
this.tenantGroupHeader = tenantGroupHeader;
|
||||
}
|
||||
|
||||
public String getGroupSeparator() {
|
||||
return groupSeparator;
|
||||
}
|
||||
|
||||
public void setGroupSeparator(String groupSeparator) {
|
||||
this.groupSeparator = groupSeparator;
|
||||
}
|
||||
|
||||
public String getDefaultTenantId() {
|
||||
return defaultTenantId;
|
||||
}
|
||||
@ -120,24 +94,8 @@ public class TenantRoutingProperties {
|
||||
this.fallbackToShared = fallbackToShared;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建租户组名称
|
||||
*
|
||||
* @param tenantId 租户 ID
|
||||
* @return 租户组名称,如 "TENANT_VIP_001"
|
||||
*/
|
||||
public String buildTenantGroup(String tenantId) {
|
||||
if (tenantId == null || tenantId.isEmpty()) {
|
||||
return "DEFAULT";
|
||||
}
|
||||
return getGroupSeparator() + tenantId.toUpperCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为共享服务
|
||||
*
|
||||
* @param serviceName 服务名
|
||||
* @return 是否为共享服务
|
||||
*/
|
||||
public boolean isSharedService(String serviceName) {
|
||||
return sharedServices != null && sharedServices.contains(serviceName);
|
||||
|
||||
@ -24,17 +24,21 @@ import java.util.stream.Collectors;
|
||||
*
|
||||
* <p>根据租户 ID 进行服务实例路由,支持:</p>
|
||||
* <ul>
|
||||
* <li>租户专属实例优先</li>
|
||||
* <li>共享实例回退</li>
|
||||
* <li>轮询负载均衡</li>
|
||||
* <li>混合模式(VIP 租户专属 + 普通租户共享)</li>
|
||||
* <li>租户专属实例优先(metadata.tenant-id 匹配)</li>
|
||||
* <li>共享实例回退(metadata.tenant-id 为空)</li>
|
||||
* <li>随机负载均衡</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h3>使用场景</h3>
|
||||
* <h3>路由规则:</h3>
|
||||
* <pre>
|
||||
* 混合模式部署:
|
||||
* - VIP 客户:独立部署服务实例(带 tenant-group 标签)
|
||||
* - 普通客户:共享服务实例(无 tenant-group 标签)
|
||||
* 1. 查找 metadata.tenant-id == 请求.tenantId 的实例 → VIP 专属实例
|
||||
* 2. 找不到 → 回退到共享实例(metadata.tenant-id 为空)
|
||||
* </pre>
|
||||
*
|
||||
* <h3>实例配置:</h3>
|
||||
* <pre>
|
||||
* 共享实例: metadata.tenant-id = "" (空)
|
||||
* VIP 实例: metadata.tenant-id = "VIP_001"
|
||||
* </pre>
|
||||
*/
|
||||
public class TenantAwareLoadBalancer implements ReactorServiceInstanceLoadBalancer {
|
||||
@ -61,9 +65,8 @@ public class TenantAwareLoadBalancer implements ReactorServiceInstanceLoadBalanc
|
||||
return getDefaultResponse();
|
||||
}
|
||||
|
||||
// 从请求上下文获取租户信息
|
||||
// 从请求上下文获取租户 ID
|
||||
String tenantId = getTenantIdFromRequest(request);
|
||||
String tenantGroup = buildTenantGroup(tenantId);
|
||||
|
||||
if (supplierProvider == null) {
|
||||
logger.warn("[TenantLB] ServiceInstanceListSupplier 未提供");
|
||||
@ -77,7 +80,7 @@ public class TenantAwareLoadBalancer implements ReactorServiceInstanceLoadBalanc
|
||||
}
|
||||
|
||||
return supplier.get().next()
|
||||
.map(instances -> filterByTenantGroup(instances, tenantGroup))
|
||||
.map(instances -> filterByTenantId(instances, tenantId))
|
||||
.map(this::getInstanceResponse);
|
||||
}
|
||||
|
||||
@ -123,34 +126,20 @@ public class TenantAwareLoadBalancer implements ReactorServiceInstanceLoadBalanc
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建租户组名称
|
||||
*/
|
||||
String buildTenantGroup(String tenantId) {
|
||||
if (routingProperties != null) {
|
||||
return routingProperties.buildTenantGroup(tenantId);
|
||||
}
|
||||
// 默认逻辑
|
||||
if (tenantId == null || tenantId.isEmpty()) {
|
||||
return "DEFAULT";
|
||||
}
|
||||
return "TENANT_" + tenantId.toUpperCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据租户组过滤服务实例
|
||||
* 根据租户 ID 过滤服务实例
|
||||
*
|
||||
* <p>路由策略:</p>
|
||||
* <ol>
|
||||
* <li>优先选择租户专属实例(metadata.tenant-group 匹配)</li>
|
||||
* <li>回退到共享实例(无 tenant-group 标签)</li>
|
||||
* <li>优先选择租户专属实例(metadata.tenant-id 匹配)</li>
|
||||
* <li>回退到共享实例(metadata.tenant-id 为空或不存在)</li>
|
||||
* </ol>
|
||||
*/
|
||||
List<ServiceInstance> filterByTenantGroup(List<ServiceInstance> instances, String tenantGroup) {
|
||||
List<ServiceInstance> filterByTenantId(List<ServiceInstance> instances, String tenantId) {
|
||||
if (instances == null || instances.isEmpty()) {
|
||||
return instances;
|
||||
}
|
||||
|
||||
logger.debug("[TenantLB] 租户组:{},候选实例数:{}", tenantGroup, instances.size());
|
||||
logger.debug("[TenantLB] 租户 ID:{},候选实例数:{}", tenantId, instances.size());
|
||||
|
||||
// 检查是否为共享服务(不需要租户路由)
|
||||
if (routingProperties != null && routingProperties.isSharedService(serviceId)) {
|
||||
@ -159,22 +148,24 @@ public class TenantAwareLoadBalancer implements ReactorServiceInstanceLoadBalanc
|
||||
}
|
||||
|
||||
// 优先选择租户专属实例
|
||||
List<ServiceInstance> tenantInstances = instances.stream()
|
||||
.filter(inst -> {
|
||||
Map<String, String> metadata = inst.getMetadata();
|
||||
if (metadata == null) return false;
|
||||
String instanceGroup = metadata.get("tenant-group");
|
||||
boolean match = tenantGroup.equals(instanceGroup);
|
||||
if (match) {
|
||||
logger.debug("[TenantLB] 匹配租户实例:{}:{}", inst.getHost(), inst.getPort());
|
||||
}
|
||||
return match;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (!tenantInstances.isEmpty()) {
|
||||
logger.info("[TenantLB] 找到 {} 个租户专属实例,租户组:{}", tenantInstances.size(), tenantGroup);
|
||||
return tenantInstances;
|
||||
if (tenantId != null && !tenantId.isEmpty()) {
|
||||
List<ServiceInstance> tenantInstances = instances.stream()
|
||||
.filter(inst -> {
|
||||
Map<String, String> metadata = inst.getMetadata();
|
||||
if (metadata == null) return false;
|
||||
String instanceTenantId = metadata.get("tenant-id");
|
||||
boolean match = tenantId.equals(instanceTenantId);
|
||||
if (match) {
|
||||
logger.debug("[TenantLB] 匹配租户专属实例:{}:{}", inst.getHost(), inst.getPort());
|
||||
}
|
||||
return match;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (!tenantInstances.isEmpty()) {
|
||||
logger.info("[TenantLB] 找到 {} 个租户专属实例,租户 ID:{}", tenantInstances.size(), tenantId);
|
||||
return tenantInstances;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否启用回退到共享实例
|
||||
@ -188,11 +179,11 @@ public class TenantAwareLoadBalancer implements ReactorServiceInstanceLoadBalanc
|
||||
return List.of();
|
||||
}
|
||||
|
||||
// 回退到共享实例(无 tenant-group 标签)
|
||||
// 回退到共享实例(metadata.tenant-id 为空或不存在)
|
||||
List<ServiceInstance> sharedInstances = instances.stream()
|
||||
.filter(inst -> {
|
||||
Map<String, String> metadata = inst.getMetadata();
|
||||
return metadata == null || !metadata.containsKey("tenant-group");
|
||||
return metadata == null || metadata.get("tenant-id") == null || metadata.get("tenant-id").isEmpty();
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
|
||||
@ -209,10 +200,9 @@ public class TenantAwareLoadBalancer implements ReactorServiceInstanceLoadBalanc
|
||||
return new EmptyResponse();
|
||||
}
|
||||
|
||||
// 随机选择
|
||||
int index = ThreadLocalRandom.current().nextInt(instances.size());
|
||||
ServiceInstance chosen = instances.get(index);
|
||||
logger.info("[TenantLB] 选择实例:{}:{} (index={})", chosen.getHost(), chosen.getPort(), index);
|
||||
logger.info("[TenantLB] 选择实例:{}:{}", chosen.getHost(), chosen.getPort());
|
||||
return new DefaultResponse(chosen);
|
||||
}
|
||||
|
||||
|
||||
@ -13,21 +13,21 @@ import java.util.Map;
|
||||
/**
|
||||
* Nacos 服务注册元数据配置
|
||||
*
|
||||
* <p>服务启动时自动注册租户组标签到 Nacos,支持租户感知的负载均衡</p>
|
||||
* <p>服务启动时自动注册租户 ID 到 Nacos,支持租户感知的负载均衡</p>
|
||||
*
|
||||
* <h3>租户组配置规则:</h3>
|
||||
* <h3>租户 ID 配置规则:</h3>
|
||||
* <pre>
|
||||
* 共享实例: tenant-group 为空(或不配置)→ 所有租户都可使用
|
||||
* VIP实例: tenant-group = "TENANT_VIP_001" → 仅该租户组可用
|
||||
* 共享实例: tenant-id 为空(或不配置)→ 所有租户都可使用
|
||||
* VIP实例: tenant-id = "VIP_001" → 仅该租户可用
|
||||
* </pre>
|
||||
*
|
||||
* <h3>配置方式:</h3>
|
||||
* <pre>
|
||||
* # 方式1: 环境变量(Docker/K8s 推荐)
|
||||
* TENANT_GROUP=TENANT_VIP_001
|
||||
* TENANT_ID=VIP_001
|
||||
*
|
||||
* # 方式2: 配置文件
|
||||
* spring.cloud.nacos.discovery.metadata.tenant-group=TENANT_VIP_001
|
||||
* spring.cloud.nacos.discovery.metadata.tenant-id=VIP_001
|
||||
* </pre>
|
||||
*/
|
||||
@Configuration
|
||||
@ -40,11 +40,11 @@ public class NacosMetadataConfig {
|
||||
private String applicationName;
|
||||
|
||||
/**
|
||||
* 租户组,优先级:环境变量 > 配置文件
|
||||
* 租户 ID,优先级:环境变量 > 配置文件
|
||||
* 为空表示共享实例,供所有租户使用
|
||||
*/
|
||||
@Value("${TENANT_GROUP:${spring.cloud.nacos.discovery.metadata.tenant-group:}}")
|
||||
private String tenantGroup;
|
||||
@Value("${TENANT_ID:${spring.cloud.nacos.discovery.metadata.tenant-id:}}")
|
||||
private String tenantId;
|
||||
|
||||
private final Registration registration;
|
||||
|
||||
@ -57,21 +57,21 @@ public class NacosMetadataConfig {
|
||||
if (registration != null && registration.getMetadata() != null) {
|
||||
Map<String, String> metadata = registration.getMetadata();
|
||||
|
||||
// 添加租户组元数据
|
||||
if (tenantGroup != null && !tenantGroup.isEmpty()) {
|
||||
metadata.put("tenant-group", tenantGroup);
|
||||
logger.info("[Nacos] {} 注册为 VIP 专属实例,租户组:{}", applicationName, tenantGroup);
|
||||
// 添加租户 ID 元数据
|
||||
if (tenantId != null && !tenantId.isEmpty()) {
|
||||
metadata.put("tenant-id", tenantId);
|
||||
logger.info("[Nacos] {} 注册为 VIP 专属实例,租户 ID:{}", applicationName, tenantId);
|
||||
} else {
|
||||
logger.info("[Nacos] {} 注册为共享实例,供所有租户使用", applicationName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public String getTenantGroup() {
|
||||
return tenantGroup;
|
||||
public String getTenantId() {
|
||||
return tenantId;
|
||||
}
|
||||
|
||||
public boolean isVipInstance() {
|
||||
return tenantGroup != null && !tenantGroup.isEmpty();
|
||||
return tenantId != null && !tenantId.isEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,13 +4,10 @@ import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.cloud.client.DefaultServiceInstance;
|
||||
import org.springframework.cloud.client.ServiceInstance;
|
||||
import org.springframework.cloud.client.loadbalancer.Request;
|
||||
import org.springframework.cloud.client.loadbalancer.RequestData;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* 租户负载均衡器测试
|
||||
@ -25,73 +22,70 @@ class TenantAwareLoadBalancerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBuildTenantGroup() {
|
||||
// 测试租户组名称构建
|
||||
String group1 = invokeBuildTenantGroup(loadBalancer, "1");
|
||||
assertEquals("TENANT_1", group1);
|
||||
|
||||
String group2 = invokeBuildTenantGroup(loadBalancer, "tenant_001");
|
||||
assertEquals("TENANT_TENANT_001", group2);
|
||||
|
||||
String group3 = invokeBuildTenantGroup(loadBalancer, null);
|
||||
assertEquals("DEFAULT", group3);
|
||||
|
||||
String group4 = invokeBuildTenantGroup(loadBalancer, "");
|
||||
assertEquals("DEFAULT", group4);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFilterByTenantGroup() {
|
||||
void testFilterByTenantId() {
|
||||
// 创建测试实例
|
||||
List<ServiceInstance> instances = Arrays.asList(
|
||||
createInstance("fund-sys", "192.168.1.1", 8100, Map.of("tenant-group", "TENANT_1")),
|
||||
createInstance("fund-sys", "192.168.1.2", 8101, Map.of("tenant-group", "TENANT_1")),
|
||||
createInstance("fund-sys", "192.168.1.3", 8102, Map.of("tenant-group", "TENANT_2")),
|
||||
createInstance("fund-sys", "192.168.1.1", 8100, Map.of("tenant-id", "VIP_001")),
|
||||
createInstance("fund-sys", "192.168.1.2", 8101, Map.of("tenant-id", "VIP_001")),
|
||||
createInstance("fund-sys", "192.168.1.3", 8102, Map.of("tenant-id", "VIP_002")),
|
||||
createInstance("fund-sys", "192.168.1.4", 8103, Collections.emptyMap()) // 共享实例
|
||||
);
|
||||
|
||||
// 测试租户 1 过滤
|
||||
List<ServiceInstance> tenant1Instances = invokeFilterByTenantGroup(loadBalancer, instances, "TENANT_1");
|
||||
assertEquals(2, tenant1Instances.size());
|
||||
assertTrue(tenant1Instances.stream().allMatch(i -> "TENANT_1".equals(i.getMetadata().get("tenant-group"))));
|
||||
// 测试 VIP_001 过滤
|
||||
List<ServiceInstance> vip1Instances = invokeFilterByTenantId(loadBalancer, instances, "VIP_001");
|
||||
assertEquals(2, vip1Instances.size());
|
||||
assertTrue(vip1Instances.stream().allMatch(i -> "VIP_001".equals(i.getMetadata().get("tenant-id"))));
|
||||
|
||||
// 测试租户 2 过滤
|
||||
List<ServiceInstance> tenant2Instances = invokeFilterByTenantGroup(loadBalancer, instances, "TENANT_2");
|
||||
assertEquals(1, tenant2Instances.size());
|
||||
// 测试 VIP_002 过滤
|
||||
List<ServiceInstance> vip2Instances = invokeFilterByTenantId(loadBalancer, instances, "VIP_002");
|
||||
assertEquals(1, vip2Instances.size());
|
||||
|
||||
// 测试未知租户(回退到共享实例)
|
||||
List<ServiceInstance> unknownTenantInstances = invokeFilterByTenantGroup(loadBalancer, instances, "TENANT_UNKNOWN");
|
||||
assertEquals(1, unknownTenantInstances.size());
|
||||
assertFalse(unknownTenantInstances.get(0).getMetadata().containsKey("tenant-group"));
|
||||
List<ServiceInstance> unknownInstances = invokeFilterByTenantId(loadBalancer, instances, "VIP_999");
|
||||
assertEquals(1, unknownInstances.size());
|
||||
assertFalse(unknownInstances.get(0).getMetadata().containsKey("tenant-id"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMixedMode() {
|
||||
// 混合模式:VIP 客户有专属实例,普通客户使用共享实例
|
||||
List<ServiceInstance> instances = Arrays.asList(
|
||||
// VIP 租户 001 的专属实例
|
||||
createInstance("fund-sys", "192.168.1.1", 8100, Map.of("tenant-group", "TENANT_VIP_001")),
|
||||
createInstance("fund-sys", "192.168.1.2", 8101, Map.of("tenant-group", "TENANT_VIP_001")),
|
||||
// VIP 租户 002 的专属实例
|
||||
createInstance("fund-sys", "192.168.1.3", 8102, Map.of("tenant-group", "TENANT_VIP_002")),
|
||||
// VIP_001 的专属实例
|
||||
createInstance("fund-sys", "192.168.1.1", 8100, Map.of("tenant-id", "VIP_001")),
|
||||
createInstance("fund-sys", "192.168.1.2", 8101, Map.of("tenant-id", "VIP_001")),
|
||||
// VIP_002 的专属实例
|
||||
createInstance("fund-sys", "192.168.1.3", 8102, Map.of("tenant-id", "VIP_002")),
|
||||
// 共享实例(普通租户使用)
|
||||
createInstance("fund-sys", "192.168.1.10", 8110, Collections.emptyMap()),
|
||||
createInstance("fund-sys", "192.168.1.11", 8111, Collections.emptyMap())
|
||||
);
|
||||
|
||||
// VIP 租户 001 应该路由到专属实例
|
||||
List<ServiceInstance> vip1Instances = invokeFilterByTenantGroup(loadBalancer, instances, "TENANT_VIP_001");
|
||||
// VIP_001 应该路由到专属实例
|
||||
List<ServiceInstance> vip1Instances = invokeFilterByTenantId(loadBalancer, instances, "VIP_001");
|
||||
assertEquals(2, vip1Instances.size());
|
||||
|
||||
// VIP 租户 002 应该路由到专属实例
|
||||
List<ServiceInstance> vip2Instances = invokeFilterByTenantGroup(loadBalancer, instances, "TENANT_VIP_002");
|
||||
// VIP_002 应该路由到专属实例
|
||||
List<ServiceInstance> vip2Instances = invokeFilterByTenantId(loadBalancer, instances, "VIP_002");
|
||||
assertEquals(1, vip2Instances.size());
|
||||
|
||||
// 普通租户应该路由到共享实例
|
||||
List<ServiceInstance> normalInstances = invokeFilterByTenantGroup(loadBalancer, instances, "TENANT_NORMAL");
|
||||
List<ServiceInstance> normalInstances = invokeFilterByTenantId(loadBalancer, instances, "NORMAL_001");
|
||||
assertEquals(2, normalInstances.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNullTenantId() {
|
||||
// 测试空租户 ID(应该回退到共享实例)
|
||||
List<ServiceInstance> instances = Arrays.asList(
|
||||
createInstance("fund-sys", "192.168.1.1", 8100, Map.of("tenant-id", "VIP_001")),
|
||||
createInstance("fund-sys", "192.168.1.2", 8101, Collections.emptyMap())
|
||||
);
|
||||
|
||||
List<ServiceInstance> result = invokeFilterByTenantId(loadBalancer, instances, null);
|
||||
assertEquals(1, result.size());
|
||||
assertFalse(result.get(0).getMetadata().containsKey("tenant-id"));
|
||||
}
|
||||
|
||||
// 辅助方法:创建服务实例
|
||||
private ServiceInstance createInstance(String serviceId, String host, int port, Map<String, String> metadata) {
|
||||
return new DefaultServiceInstance(
|
||||
@ -104,24 +98,13 @@ class TenantAwareLoadBalancerTest {
|
||||
);
|
||||
}
|
||||
|
||||
// 辅助方法:调用私有方法 buildTenantGroup
|
||||
private String invokeBuildTenantGroup(TenantAwareLoadBalancer lb, String tenantId) {
|
||||
try {
|
||||
var method = TenantAwareLoadBalancer.class.getDeclaredMethod("buildTenantGroup", String.class);
|
||||
method.setAccessible(true);
|
||||
return (String) method.invoke(lb, tenantId);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
// 辅助方法:调用私有方法 filterByTenantGroup
|
||||
// 辅助方法:调用私有方法 filterByTenantId
|
||||
@SuppressWarnings("unchecked")
|
||||
private List<ServiceInstance> invokeFilterByTenantGroup(TenantAwareLoadBalancer lb, List<ServiceInstance> instances, String tenantGroup) {
|
||||
private List<ServiceInstance> invokeFilterByTenantId(TenantAwareLoadBalancer lb, List<ServiceInstance> instances, String tenantId) {
|
||||
try {
|
||||
var method = TenantAwareLoadBalancer.class.getDeclaredMethod("filterByTenantGroup", List.class, String.class);
|
||||
var method = TenantAwareLoadBalancer.class.getDeclaredMethod("filterByTenantId", List.class, String.class);
|
||||
method.setAccessible(true);
|
||||
return (List<ServiceInstance>) method.invoke(lb, instances, tenantGroup);
|
||||
return (List<ServiceInstance>) method.invoke(lb, instances, tenantId);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
@ -1,4 +1,4 @@
|
||||
-------------------------------------------------------------------------------
|
||||
Test set: com.fundplatform.common.loadbalancer.TenantAwareLoadBalancerTest
|
||||
-------------------------------------------------------------------------------
|
||||
Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.310 s -- in com.fundplatform.common.loadbalancer.TenantAwareLoadBalancerTest
|
||||
Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.206 s -- in com.fundplatform.common.loadbalancer.TenantAwareLoadBalancerTest
|
||||
|
||||
Binary file not shown.
@ -14,9 +14,9 @@ spring:
|
||||
username: nacos
|
||||
password: nacos
|
||||
# 租户路由元数据
|
||||
# tenant-group: 空值=共享实例,有值=VIP专属实例
|
||||
# tenant-id: 空值=共享实例,有值=VIP专属实例
|
||||
metadata:
|
||||
tenant-group: ${TENANT_GROUP:}
|
||||
tenant-id: ${TENANT_ID:}
|
||||
|
||||
datasource:
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
|
||||
@ -14,9 +14,9 @@ spring:
|
||||
username: nacos
|
||||
password: nacos
|
||||
# 租户路由元数据
|
||||
# tenant-group: 空值=共享实例,有值=VIP专属实例
|
||||
# tenant-id: 空值=共享实例,有值=VIP专属实例
|
||||
metadata:
|
||||
tenant-group: ${TENANT_GROUP:}
|
||||
tenant-id: ${TENANT_ID:}
|
||||
|
||||
datasource:
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
|
||||
@ -1138,3 +1138,39 @@ java.lang.NullPointerException: Cannot invoke "com.zaxxer.hikari.HikariPoolMXBea
|
||||
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
|
||||
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
|
||||
at java.base/java.lang.Thread.run(Thread.java:1583)
|
||||
2026-02-19 21:26:06.988 [scheduling-1] [] INFO com.fundplatform.sys.config.HikariMonitorConfig - === HikariCP 连接池状态 ===
|
||||
2026-02-19 21:26:06.988 [scheduling-1] [] INFO com.fundplatform.sys.config.HikariMonitorConfig - 连接池名称: FundSysHikariPool
|
||||
2026-02-19 21:26:06.989 [scheduling-1] [] ERROR o.s.s.support.TaskUtils$LoggingErrorHandler - Unexpected error occurred in scheduled task
|
||||
java.lang.NullPointerException: Cannot invoke "com.zaxxer.hikari.HikariPoolMXBean.getActiveConnections()" because the return value of "com.zaxxer.hikari.HikariDataSource.getHikariPoolMXBean()" is null
|
||||
at com.fundplatform.sys.config.HikariMonitorConfig.monitorHikariPool(HikariMonitorConfig.java:38)
|
||||
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
|
||||
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
|
||||
at org.springframework.scheduling.support.ScheduledMethodRunnable.runInternal(ScheduledMethodRunnable.java:130)
|
||||
at org.springframework.scheduling.support.ScheduledMethodRunnable.lambda$run$2(ScheduledMethodRunnable.java:124)
|
||||
at io.micrometer.observation.Observation.observe(Observation.java:499)
|
||||
at org.springframework.scheduling.support.ScheduledMethodRunnable.run(ScheduledMethodRunnable.java:124)
|
||||
at org.springframework.scheduling.support.DelegatingErrorHandlingRunnable.run(DelegatingErrorHandlingRunnable.java:54)
|
||||
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:572)
|
||||
at java.base/java.util.concurrent.FutureTask.runAndReset(FutureTask.java:358)
|
||||
at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:305)
|
||||
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
|
||||
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
|
||||
at java.base/java.lang.Thread.run(Thread.java:1583)
|
||||
2026-02-19 21:31:06.988 [scheduling-1] [] INFO com.fundplatform.sys.config.HikariMonitorConfig - === HikariCP 连接池状态 ===
|
||||
2026-02-19 21:31:06.988 [scheduling-1] [] INFO com.fundplatform.sys.config.HikariMonitorConfig - 连接池名称: FundSysHikariPool
|
||||
2026-02-19 21:31:06.988 [scheduling-1] [] ERROR o.s.s.support.TaskUtils$LoggingErrorHandler - Unexpected error occurred in scheduled task
|
||||
java.lang.NullPointerException: Cannot invoke "com.zaxxer.hikari.HikariPoolMXBean.getActiveConnections()" because the return value of "com.zaxxer.hikari.HikariDataSource.getHikariPoolMXBean()" is null
|
||||
at com.fundplatform.sys.config.HikariMonitorConfig.monitorHikariPool(HikariMonitorConfig.java:38)
|
||||
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
|
||||
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
|
||||
at org.springframework.scheduling.support.ScheduledMethodRunnable.runInternal(ScheduledMethodRunnable.java:130)
|
||||
at org.springframework.scheduling.support.ScheduledMethodRunnable.lambda$run$2(ScheduledMethodRunnable.java:124)
|
||||
at io.micrometer.observation.Observation.observe(Observation.java:499)
|
||||
at org.springframework.scheduling.support.ScheduledMethodRunnable.run(ScheduledMethodRunnable.java:124)
|
||||
at org.springframework.scheduling.support.DelegatingErrorHandlingRunnable.run(DelegatingErrorHandlingRunnable.java:54)
|
||||
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:572)
|
||||
at java.base/java.util.concurrent.FutureTask.runAndReset(FutureTask.java:358)
|
||||
at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:305)
|
||||
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
|
||||
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
|
||||
at java.base/java.lang.Thread.run(Thread.java:1583)
|
||||
|
||||
@ -1120,3 +1120,21 @@ java.lang.NullPointerException: Cannot invoke "com.zaxxer.hikari.HikariPoolMXBea
|
||||
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
|
||||
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
|
||||
at java.base/java.lang.Thread.run(Thread.java:1583)
|
||||
2026-02-19 21:29:07.541 [scheduling-1] [] INFO com.fundplatform.sys.config.HikariMonitorConfig - === HikariCP 连接池状态 ===
|
||||
2026-02-19 21:29:07.541 [scheduling-1] [] INFO com.fundplatform.sys.config.HikariMonitorConfig - 连接池名称: FundSysHikariPool
|
||||
2026-02-19 21:29:07.541 [scheduling-1] [] ERROR o.s.s.support.TaskUtils$LoggingErrorHandler - Unexpected error occurred in scheduled task
|
||||
java.lang.NullPointerException: Cannot invoke "com.zaxxer.hikari.HikariPoolMXBean.getActiveConnections()" because the return value of "com.zaxxer.hikari.HikariDataSource.getHikariPoolMXBean()" is null
|
||||
at com.fundplatform.sys.config.HikariMonitorConfig.monitorHikariPool(HikariMonitorConfig.java:38)
|
||||
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
|
||||
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
|
||||
at org.springframework.scheduling.support.ScheduledMethodRunnable.runInternal(ScheduledMethodRunnable.java:130)
|
||||
at org.springframework.scheduling.support.ScheduledMethodRunnable.lambda$run$2(ScheduledMethodRunnable.java:124)
|
||||
at io.micrometer.observation.Observation.observe(Observation.java:499)
|
||||
at org.springframework.scheduling.support.ScheduledMethodRunnable.run(ScheduledMethodRunnable.java:124)
|
||||
at org.springframework.scheduling.support.DelegatingErrorHandlingRunnable.run(DelegatingErrorHandlingRunnable.java:54)
|
||||
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:572)
|
||||
at java.base/java.util.concurrent.FutureTask.runAndReset(FutureTask.java:358)
|
||||
at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:305)
|
||||
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
|
||||
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
|
||||
at java.base/java.lang.Thread.run(Thread.java:1583)
|
||||
|
||||
@ -1518,3 +1518,51 @@ java.lang.NullPointerException: Cannot invoke "com.zaxxer.hikari.HikariPoolMXBea
|
||||
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
|
||||
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
|
||||
at java.base/java.lang.Thread.run(Thread.java:1583)
|
||||
2026-02-19 21:26:06.989 [scheduling-1] [] ERROR o.s.s.support.TaskUtils$LoggingErrorHandler - Unexpected error occurred in scheduled task
|
||||
java.lang.NullPointerException: Cannot invoke "com.zaxxer.hikari.HikariPoolMXBean.getActiveConnections()" because the return value of "com.zaxxer.hikari.HikariDataSource.getHikariPoolMXBean()" is null
|
||||
at com.fundplatform.sys.config.HikariMonitorConfig.monitorHikariPool(HikariMonitorConfig.java:38)
|
||||
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
|
||||
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
|
||||
at org.springframework.scheduling.support.ScheduledMethodRunnable.runInternal(ScheduledMethodRunnable.java:130)
|
||||
at org.springframework.scheduling.support.ScheduledMethodRunnable.lambda$run$2(ScheduledMethodRunnable.java:124)
|
||||
at io.micrometer.observation.Observation.observe(Observation.java:499)
|
||||
at org.springframework.scheduling.support.ScheduledMethodRunnable.run(ScheduledMethodRunnable.java:124)
|
||||
at org.springframework.scheduling.support.DelegatingErrorHandlingRunnable.run(DelegatingErrorHandlingRunnable.java:54)
|
||||
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:572)
|
||||
at java.base/java.util.concurrent.FutureTask.runAndReset(FutureTask.java:358)
|
||||
at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:305)
|
||||
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
|
||||
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
|
||||
at java.base/java.lang.Thread.run(Thread.java:1583)
|
||||
2026-02-19 21:29:07.541 [scheduling-1] [] ERROR o.s.s.support.TaskUtils$LoggingErrorHandler - Unexpected error occurred in scheduled task
|
||||
java.lang.NullPointerException: Cannot invoke "com.zaxxer.hikari.HikariPoolMXBean.getActiveConnections()" because the return value of "com.zaxxer.hikari.HikariDataSource.getHikariPoolMXBean()" is null
|
||||
at com.fundplatform.sys.config.HikariMonitorConfig.monitorHikariPool(HikariMonitorConfig.java:38)
|
||||
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
|
||||
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
|
||||
at org.springframework.scheduling.support.ScheduledMethodRunnable.runInternal(ScheduledMethodRunnable.java:130)
|
||||
at org.springframework.scheduling.support.ScheduledMethodRunnable.lambda$run$2(ScheduledMethodRunnable.java:124)
|
||||
at io.micrometer.observation.Observation.observe(Observation.java:499)
|
||||
at org.springframework.scheduling.support.ScheduledMethodRunnable.run(ScheduledMethodRunnable.java:124)
|
||||
at org.springframework.scheduling.support.DelegatingErrorHandlingRunnable.run(DelegatingErrorHandlingRunnable.java:54)
|
||||
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:572)
|
||||
at java.base/java.util.concurrent.FutureTask.runAndReset(FutureTask.java:358)
|
||||
at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:305)
|
||||
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
|
||||
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
|
||||
at java.base/java.lang.Thread.run(Thread.java:1583)
|
||||
2026-02-19 21:31:06.988 [scheduling-1] [] ERROR o.s.s.support.TaskUtils$LoggingErrorHandler - Unexpected error occurred in scheduled task
|
||||
java.lang.NullPointerException: Cannot invoke "com.zaxxer.hikari.HikariPoolMXBean.getActiveConnections()" because the return value of "com.zaxxer.hikari.HikariDataSource.getHikariPoolMXBean()" is null
|
||||
at com.fundplatform.sys.config.HikariMonitorConfig.monitorHikariPool(HikariMonitorConfig.java:38)
|
||||
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
|
||||
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
|
||||
at org.springframework.scheduling.support.ScheduledMethodRunnable.runInternal(ScheduledMethodRunnable.java:130)
|
||||
at org.springframework.scheduling.support.ScheduledMethodRunnable.lambda$run$2(ScheduledMethodRunnable.java:124)
|
||||
at io.micrometer.observation.Observation.observe(Observation.java:499)
|
||||
at org.springframework.scheduling.support.ScheduledMethodRunnable.run(ScheduledMethodRunnable.java:124)
|
||||
at org.springframework.scheduling.support.DelegatingErrorHandlingRunnable.run(DelegatingErrorHandlingRunnable.java:54)
|
||||
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:572)
|
||||
at java.base/java.util.concurrent.FutureTask.runAndReset(FutureTask.java:358)
|
||||
at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:305)
|
||||
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
|
||||
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
|
||||
at java.base/java.lang.Thread.run(Thread.java:1583)
|
||||
|
||||
@ -334,3 +334,9 @@
|
||||
2026-02-19 21:21:06.988 [scheduling-1] [] INFO com.fundplatform.sys.config.HikariMonitorConfig - 连接池名称: FundSysHikariPool
|
||||
2026-02-19 21:24:07.541 [scheduling-1] [] INFO com.fundplatform.sys.config.HikariMonitorConfig - === HikariCP 连接池状态 ===
|
||||
2026-02-19 21:24:07.541 [scheduling-1] [] INFO com.fundplatform.sys.config.HikariMonitorConfig - 连接池名称: FundSysHikariPool
|
||||
2026-02-19 21:26:06.988 [scheduling-1] [] INFO com.fundplatform.sys.config.HikariMonitorConfig - === HikariCP 连接池状态 ===
|
||||
2026-02-19 21:26:06.988 [scheduling-1] [] INFO com.fundplatform.sys.config.HikariMonitorConfig - 连接池名称: FundSysHikariPool
|
||||
2026-02-19 21:29:07.541 [scheduling-1] [] INFO com.fundplatform.sys.config.HikariMonitorConfig - === HikariCP 连接池状态 ===
|
||||
2026-02-19 21:29:07.541 [scheduling-1] [] INFO com.fundplatform.sys.config.HikariMonitorConfig - 连接池名称: FundSysHikariPool
|
||||
2026-02-19 21:31:06.988 [scheduling-1] [] INFO com.fundplatform.sys.config.HikariMonitorConfig - === HikariCP 连接池状态 ===
|
||||
2026-02-19 21:31:06.988 [scheduling-1] [] INFO com.fundplatform.sys.config.HikariMonitorConfig - 连接池名称: FundSysHikariPool
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user