docs: 架构文档补充Feign动态路由方案详细设计,对比DynamicDataSource方案优劣
This commit is contained in:
parent
8029ac31da
commit
2088742543
@ -427,17 +427,547 @@ CREATE TABLE sys_tenant (
|
|||||||
└───────────┘ └───────────┘ └───────────┘
|
└───────────┘ └───────────┘ └───────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2.5 两种模式对比
|
### 2.5 一库一租户路由方案对比
|
||||||
|
|
||||||
| 特性 | 一库多租户 | 一库一租户 |
|
#### 2.5.1 方案对比:DynamicDataSource vs Feign 动态路由
|
||||||
|------|-----------|-----------|
|
|
||||||
| **数据隔离** | 逻辑隔离(tenant_id) | 物理隔离(独立数据库) |
|
| 对比维度 | DynamicDataSource | Feign 动态路由 |
|
||||||
| **部署成本** | 低(共享资源) | 高(独立资源) |
|
|----------|-------------------|----------------|
|
||||||
| **运维复杂度** | 低 | 高 |
|
| **架构层级** | 数据层 | 服务层 |
|
||||||
| **扩展性** | 垂直扩展为主 | 水平扩展 |
|
| **隔离级别** | 数据源级别 | 服务实例级别 |
|
||||||
| **安全性** | 中 | 高 |
|
| **资源占用** | 高(连接池 × N租户) | 中(服务实例 × N租户) |
|
||||||
| **适用场景** | 中小客户、SaaS | 大客户、私有化部署 |
|
| **扩展性** | 差(单服务多数据源) | 优(独立扩容) |
|
||||||
| **数据迁移** | 复杂(需筛选数据) | 简单(整库迁移) |
|
| **故障隔离** | 差(单点故障影响多租户) | 优(租户间互不影响) |
|
||||||
|
| **跨租户查询** | 困难 | 可通过聚合服务实现 |
|
||||||
|
| **服务治理** | 简单 | 需要完善的服务发现 |
|
||||||
|
| **部署复杂度** | 低 | 中 |
|
||||||
|
| **私有化部署** | 困难 | 简单(整体迁移) |
|
||||||
|
| **运维成本** | 中 | 高 |
|
||||||
|
|
||||||
|
#### 2.5.2 推荐方案:Feign 动态路由
|
||||||
|
|
||||||
|
**推荐理由:**
|
||||||
|
1. **真正的租户隔离**:每个租户拥有独立的服务实例和数据库
|
||||||
|
2. **弹性扩容**:单个租户可独立水平扩展
|
||||||
|
3. **故障隔离**:单个租户故障不影响其他租户
|
||||||
|
4. **私有化友好**:便于大客户私有化部署
|
||||||
|
5. **符合云原生**:与 Kubernetes 等容器平台配合良好
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.6 Feign 动态路由详细设计
|
||||||
|
|
||||||
|
#### 2.6.1 架构设计
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Feign 动态路由架构 │
|
||||||
|
│ │
|
||||||
|
│ ┌─────────┐ │
|
||||||
|
│ │ 客户端 │ Header: X-Tenant-Id: tenant_001 │
|
||||||
|
│ └────┬────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Gateway (网关) │ │
|
||||||
|
│ │ ┌─────────────────────────────────────────────────────────────┐ │ │
|
||||||
|
│ │ │ TenantRoutingFilter │ │ │
|
||||||
|
│ │ │ • 解析 X-Tenant-Id │ │ │
|
||||||
|
│ │ │ • 根据租户ID路由到对应服务组 │ │ │
|
||||||
|
│ │ │ • 添加 X-Tenant-Route 标记 │ │ │
|
||||||
|
│ │ └─────────────────────────────────────────────────────────────┘ │ │
|
||||||
|
│ └────────────────────────────────┬───────────────────────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ┌───────────────────────────┼───────────────────────────┐ │
|
||||||
|
│ │ │ │ │
|
||||||
|
│ ▼ ▼ ▼ │
|
||||||
|
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Nacos 服务注册中心 │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ 服务分组设计: │ │
|
||||||
|
│ │ • fund-sys:DEFAULT (共享服务组) │ │
|
||||||
|
│ │ • fund-sys:TENANT_001 (租户001专属) │ │
|
||||||
|
│ │ • fund-sys:TENANT_002 (租户002专属) │ │
|
||||||
|
│ │ • fund-cust:TENANT_001 │ │
|
||||||
|
│ │ • ... │ │
|
||||||
|
│ └─────────────────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ┌───────────────────────────┼───────────────────────────┐ │
|
||||||
|
│ │ │ │ │
|
||||||
|
│ ▼ ▼ ▼ │
|
||||||
|
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
||||||
|
│ │ 租户001服务组 │ │ 租户002服务组 │ │ 租户003服务组 │ │
|
||||||
|
│ │ │ │ │ │ │ │
|
||||||
|
│ │ ┌──────────┐ │ │ ┌──────────┐ │ │ ┌──────────┐ │ │
|
||||||
|
│ │ │fund-sys │ │ │ │fund-sys │ │ │ │fund-sys │ │ │
|
||||||
|
│ │ │:8100 │ │ │ │:8101 │ │ │ │:8102 │ │ │
|
||||||
|
│ │ └──────────┘ │ │ └──────────┘ │ │ └──────────┘ │ │
|
||||||
|
│ │ ┌──────────┐ │ │ ┌──────────┐ │ │ ┌──────────┐ │ │
|
||||||
|
│ │ │fund-cust │ │ │ │fund-cust │ │ │ │fund-cust │ │ │
|
||||||
|
│ │ │:8110 │ │ │ │:8111 │ │ │ │:8112 │ │ │
|
||||||
|
│ │ └──────────┘ │ │ └──────────┘ │ │ └──────────┘ │ │
|
||||||
|
│ │ ┌──────────┐ │ │ ┌──────────┐ │ │ ┌──────────┐ │ │
|
||||||
|
│ │ │fund-proj │ │ │ │fund-proj │ │ │ │fund-proj │ │ │
|
||||||
|
│ │ │:8120 │ │ │ │:8121 │ │ │ │:8122 │ │ │
|
||||||
|
│ │ └──────────┘ │ │ └──────────┘ │ │ └──────────┘ │ │
|
||||||
|
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
|
||||||
|
│ │ │ │ │
|
||||||
|
│ └─────────────────────────┼─────────────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ┌───────────────────────────┼───────────────────────────┐ │
|
||||||
|
│ │ │ │ │
|
||||||
|
│ ▼ ▼ ▼ │
|
||||||
|
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
|
||||||
|
│ │tenant_001│ │tenant_002│ │tenant_003│ │
|
||||||
|
│ │ _db │ │ _db │ │ _db │ │
|
||||||
|
│ └──────────┘ └──────────┘ └──────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2.6.2 核心组件实现
|
||||||
|
|
||||||
|
**1. 租户路由元数据管理**
|
||||||
|
|
||||||
|
```java
|
||||||
|
/**
|
||||||
|
* 租户路由元数据
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Component
|
||||||
|
@ConfigurationProperties(prefix = "tenant.routing")
|
||||||
|
public class TenantRoutingProperties {
|
||||||
|
|
||||||
|
/** 是否启用租户路由 */
|
||||||
|
private boolean enabled = true;
|
||||||
|
|
||||||
|
/** 租户ID请求头 */
|
||||||
|
private String tenantHeader = "X-Tenant-Id";
|
||||||
|
|
||||||
|
/** 服务组分隔符 */
|
||||||
|
private String groupSeparator = "TENANT_";
|
||||||
|
|
||||||
|
/** 共享服务列表(不区分租户) */
|
||||||
|
private List<String> sharedServices = Arrays.asList("fund-gateway", "fund-report");
|
||||||
|
|
||||||
|
/** 租户服务配置 */
|
||||||
|
private Map<String, TenantServiceConfig> tenantConfigs = new HashMap<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 租户服务配置
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class TenantServiceConfig {
|
||||||
|
/** 租户ID */
|
||||||
|
private String tenantId;
|
||||||
|
|
||||||
|
/** 服务实例列表 */
|
||||||
|
private Map<String, ServiceInstanceConfig> services = new HashMap<>();
|
||||||
|
|
||||||
|
/** 数据库配置 */
|
||||||
|
private DatabaseConfig database;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 服务实例配置
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class ServiceInstanceConfig {
|
||||||
|
/** 服务名 */
|
||||||
|
private String serviceName;
|
||||||
|
|
||||||
|
/** 端口号 */
|
||||||
|
private int port;
|
||||||
|
|
||||||
|
/** 实例数 */
|
||||||
|
private int replicas = 1;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. Gateway 租户路由过滤器**
|
||||||
|
|
||||||
|
```java
|
||||||
|
/**
|
||||||
|
* Gateway 租户路由过滤器
|
||||||
|
* 根据 X-Tenant-Id 路由到对应租户的服务组
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
@Order(-50)
|
||||||
|
public class TenantRoutingGatewayFilter implements GlobalFilter {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private TenantRoutingProperties routingProperties;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private NacosServiceManager nacosServiceManager;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
|
||||||
|
ServerHttpRequest request = exchange.getRequest();
|
||||||
|
String tenantId = request.getHeaders().getFirst(routingProperties.getTenantHeader());
|
||||||
|
|
||||||
|
// 未指定租户或共享服务,使用默认路由
|
||||||
|
if (StringUtils.isEmpty(tenantId) || isSharedService(exchange)) {
|
||||||
|
return chain.filter(exchange);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建租户服务组名称
|
||||||
|
String tenantGroup = buildTenantGroup(tenantId);
|
||||||
|
|
||||||
|
// 将租户路由信息存入 Exchange 属性
|
||||||
|
exchange.getAttributes().put("tenantId", tenantId);
|
||||||
|
exchange.getAttributes().put("tenantGroup", tenantGroup);
|
||||||
|
|
||||||
|
// 添加到请求头,传递给下游服务
|
||||||
|
ServerHttpRequest mutatedRequest = request.mutate()
|
||||||
|
.header("X-Tenant-Group", tenantGroup)
|
||||||
|
.header("X-Tenant-Id", tenantId)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
log.info("[TenantRouting] Route request to tenant group: {}, URI: {}",
|
||||||
|
tenantGroup, request.getURI());
|
||||||
|
|
||||||
|
return chain.filter(exchange.mutate().request(mutatedRequest).build());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建租户服务组名称
|
||||||
|
*/
|
||||||
|
private String buildTenantGroup(String tenantId) {
|
||||||
|
return routingProperties.getGroupSeparator() + tenantId.toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断是否为共享服务
|
||||||
|
*/
|
||||||
|
private boolean isSharedService(ServerWebExchange exchange) {
|
||||||
|
String path = exchange.getRequest().getURI().getPath();
|
||||||
|
return routingProperties.getSharedServices().stream()
|
||||||
|
.anyMatch(path::contains);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. 自定义负载均衡器(按租户路由)**
|
||||||
|
|
||||||
|
```java
|
||||||
|
/**
|
||||||
|
* 租户感知的负载均衡器
|
||||||
|
*/
|
||||||
|
public class TenantAwareLoadBalancer implements ReactorServiceInstanceLoadBalancer {
|
||||||
|
|
||||||
|
private final String serviceId;
|
||||||
|
private final ObjectProvider<ServiceInstanceListSupplier> supplierProvider;
|
||||||
|
|
||||||
|
public TenantAwareLoadBalancer(String serviceId,
|
||||||
|
ObjectProvider<ServiceInstanceListSupplier> supplierProvider) {
|
||||||
|
this.serviceId = serviceId;
|
||||||
|
this.supplierProvider = supplierProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<Response<ServiceInstance>> choose(Request request) {
|
||||||
|
// 从请求上下文获取租户ID
|
||||||
|
String tenantId = getTenantIdFromRequest(request);
|
||||||
|
String tenantGroup = buildTenantGroup(tenantId);
|
||||||
|
|
||||||
|
ServiceInstanceListSupplier supplier = supplierProvider.getIfAvailable();
|
||||||
|
|
||||||
|
return supplier.get().next()
|
||||||
|
.map(instances -> filterByTenantGroup(instances, tenantGroup))
|
||||||
|
.map(this::getInstanceResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据租户组过滤服务实例
|
||||||
|
*/
|
||||||
|
private List<ServiceInstance> filterByTenantGroup(
|
||||||
|
List<ServiceInstance> instances, String tenantGroup) {
|
||||||
|
|
||||||
|
// 优先选择租户专属实例
|
||||||
|
List<ServiceInstance> tenantInstances = instances.stream()
|
||||||
|
.filter(inst -> tenantGroup.equals(inst.getMetadata().get("tenant-group")))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
if (!tenantInstances.isEmpty()) {
|
||||||
|
return tenantInstances;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 回退到默认实例
|
||||||
|
return instances.stream()
|
||||||
|
.filter(inst -> !inst.getMetadata().containsKey("tenant-group"))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances) {
|
||||||
|
if (instances.isEmpty()) {
|
||||||
|
return new EmptyResponse();
|
||||||
|
}
|
||||||
|
// 轮询选择
|
||||||
|
int index = ThreadLocalRandom.current().nextInt(instances.size());
|
||||||
|
return new DefaultResponse(instances.get(index));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 租户负载均衡配置
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
public class TenantLoadBalancerConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public ReactorLoadBalancer<ServiceInstance> tenantAwareLoadBalancer(
|
||||||
|
Environment environment,
|
||||||
|
LoadBalancerClientFactory loadBalancerClientFactory) {
|
||||||
|
|
||||||
|
String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
|
||||||
|
|
||||||
|
return new TenantAwareLoadBalancer(name,
|
||||||
|
loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**4. Feign 租户路由拦截器**
|
||||||
|
|
||||||
|
```java
|
||||||
|
/**
|
||||||
|
* Feign 租户路由拦截器
|
||||||
|
* 在服务间调用时保持租户上下文
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class FeignTenantRoutingInterceptor implements RequestInterceptor {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private TenantRoutingProperties routingProperties;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void apply(RequestTemplate template) {
|
||||||
|
// 从当前线程上下文获取租户ID
|
||||||
|
String tenantId = TenantContextHolder.getTenantId();
|
||||||
|
|
||||||
|
if (StringUtils.isNotEmpty(tenantId)) {
|
||||||
|
// 添加租户ID到请求头
|
||||||
|
template.header(routingProperties.getTenantHeader(), tenantId);
|
||||||
|
|
||||||
|
// 添加租户组到请求头(用于目标服务的路由)
|
||||||
|
String tenantGroup = buildTenantGroup(tenantId);
|
||||||
|
template.header("X-Tenant-Group", tenantGroup);
|
||||||
|
|
||||||
|
log.debug("[FeignTenantRouting] Add tenant header: {}, group: {}",
|
||||||
|
tenantId, tenantGroup);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildTenantGroup(String tenantId) {
|
||||||
|
return routingProperties.getGroupSeparator() + tenantId.toUpperCase();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**5. Nacos 服务注册(带租户标记)**
|
||||||
|
|
||||||
|
```java
|
||||||
|
/**
|
||||||
|
* Nacos 租户服务注册配置
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
public class NacosTenantRegistrationConfig {
|
||||||
|
|
||||||
|
@Value("${tenant.id:}")
|
||||||
|
private String tenantId;
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public NacosRegistrationCustomizer tenantNacosRegistrationCustomizer() {
|
||||||
|
return registration -> {
|
||||||
|
if (StringUtils.isNotEmpty(tenantId)) {
|
||||||
|
// 添加租户标记到服务元数据
|
||||||
|
registration.getMetadata().put("tenant-id", tenantId);
|
||||||
|
registration.getMetadata().put("tenant-group", "TENANT_" + tenantId.toUpperCase());
|
||||||
|
|
||||||
|
// 修改服务分组
|
||||||
|
registration.setGroup("TENANT_" + tenantId.toUpperCase());
|
||||||
|
|
||||||
|
log.info("[NacosRegistration] Register service with tenant: {}", tenantId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**6. 租户上下文管理**
|
||||||
|
|
||||||
|
```java
|
||||||
|
/**
|
||||||
|
* 租户上下文持有者
|
||||||
|
*/
|
||||||
|
public class TenantContextHolder {
|
||||||
|
|
||||||
|
private static final ThreadLocal<String> CURRENT_TENANT = new ThreadLocal<>();
|
||||||
|
private static final ThreadLocal<String> CURRENT_TENANT_GROUP = new ThreadLocal<>();
|
||||||
|
|
||||||
|
public static void setTenantId(String tenantId) {
|
||||||
|
CURRENT_TENANT.set(tenantId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getTenantId() {
|
||||||
|
return CURRENT_TENANT.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setTenantGroup(String tenantGroup) {
|
||||||
|
CURRENT_TENANT_GROUP.set(tenantGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getTenantGroup() {
|
||||||
|
return CURRENT_TENANT_GROUP.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void clear() {
|
||||||
|
CURRENT_TENANT.remove();
|
||||||
|
CURRENT_TENANT_GROUP.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 租户上下文过滤器
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class TenantContextFilter implements Filter {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void doFilter(ServletRequest request, ServletResponse response,
|
||||||
|
FilterChain chain) throws IOException, ServletException {
|
||||||
|
HttpServletRequest httpRequest = (HttpServletRequest) request;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 从请求头获取租户信息
|
||||||
|
String tenantId = httpRequest.getHeader("X-Tenant-Id");
|
||||||
|
String tenantGroup = httpRequest.getHeader("X-Tenant-Group");
|
||||||
|
|
||||||
|
if (StringUtils.isNotEmpty(tenantId)) {
|
||||||
|
TenantContextHolder.setTenantId(tenantId);
|
||||||
|
TenantContextHolder.setTenantGroup(tenantGroup);
|
||||||
|
|
||||||
|
// 设置到 MDC 用于日志
|
||||||
|
MDC.put("tenantId", tenantId);
|
||||||
|
}
|
||||||
|
|
||||||
|
chain.doFilter(request, response);
|
||||||
|
} finally {
|
||||||
|
TenantContextHolder.clear();
|
||||||
|
MDC.remove("tenantId");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2.6.3 配置示例
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# application-tenant.yml
|
||||||
|
spring:
|
||||||
|
cloud:
|
||||||
|
nacos:
|
||||||
|
discovery:
|
||||||
|
# 动态服务组,由代码设置
|
||||||
|
group: ${TENANT_GROUP:DEFAULT}
|
||||||
|
metadata:
|
||||||
|
tenant-id: ${TENANT_ID:}
|
||||||
|
tenant-group: ${TENANT_GROUP:DEFAULT}
|
||||||
|
loadbalancer:
|
||||||
|
configurations: tenant-aware
|
||||||
|
|
||||||
|
tenant:
|
||||||
|
routing:
|
||||||
|
enabled: true
|
||||||
|
tenant-header: X-Tenant-Id
|
||||||
|
group-separator: TENANT_
|
||||||
|
# 共享服务(所有租户共用)
|
||||||
|
shared-services:
|
||||||
|
- fund-gateway
|
||||||
|
- fund-report
|
||||||
|
- fund-file
|
||||||
|
# 租户配置
|
||||||
|
tenant-configs:
|
||||||
|
tenant_001:
|
||||||
|
tenant-id: tenant_001
|
||||||
|
services:
|
||||||
|
fund-sys:
|
||||||
|
service-name: fund-sys
|
||||||
|
port: 8100
|
||||||
|
replicas: 2
|
||||||
|
fund-cust:
|
||||||
|
service-name: fund-cust
|
||||||
|
port: 8110
|
||||||
|
replicas: 1
|
||||||
|
database:
|
||||||
|
url: jdbc:mysql://localhost:3306/tenant_001_db
|
||||||
|
username: tenant_001
|
||||||
|
password: xxx
|
||||||
|
tenant_002:
|
||||||
|
tenant-id: tenant_002
|
||||||
|
services:
|
||||||
|
fund-sys:
|
||||||
|
service-name: fund-sys
|
||||||
|
port: 8101
|
||||||
|
replicas: 1
|
||||||
|
fund-cust:
|
||||||
|
service-name: fund-cust
|
||||||
|
port: 8111
|
||||||
|
replicas: 1
|
||||||
|
database:
|
||||||
|
url: jdbc:mysql://localhost:3306/tenant_002_db
|
||||||
|
username: tenant_002
|
||||||
|
password: xxx
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2.6.4 部署架构
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Kubernetes 部署示例 │
|
||||||
|
│ │
|
||||||
|
│ apiVersion: apps/v1 │
|
||||||
|
│ kind: Deployment │
|
||||||
|
│ metadata: │
|
||||||
|
│ name: fund-sys-tenant-001 │
|
||||||
|
│ labels: │
|
||||||
|
│ app: fund-sys │
|
||||||
|
│ tenant: tenant-001 │
|
||||||
|
│ spec: │
|
||||||
|
│ replicas: 2 │
|
||||||
|
│ selector: │
|
||||||
|
│ matchLabels: │
|
||||||
|
│ app: fund-sys │
|
||||||
|
│ tenant: tenant-001 │
|
||||||
|
│ template: │
|
||||||
|
│ metadata: │
|
||||||
|
│ labels: │
|
||||||
|
│ app: fund-sys │
|
||||||
|
│ tenant: tenant-001 │
|
||||||
|
│ spec: │
|
||||||
|
│ containers: │
|
||||||
|
│ - name: fund-sys │
|
||||||
|
│ image: fundplatform/fund-sys:1.0.0 │
|
||||||
|
│ env: │
|
||||||
|
│ - name: TENANT_ID │
|
||||||
|
│ value: "tenant_001" │
|
||||||
|
│ - name: TENANT_GROUP │
|
||||||
|
│ value: "TENANT_001" │
|
||||||
|
│ - name: DB_URL │
|
||||||
|
│ value: "jdbc:mysql://mysql:3306/tenant_001_db" │
|
||||||
|
└─────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2.6.5 两种方案最终对比
|
||||||
|
|
||||||
|
| 场景 | 推荐方案 | 原因 |
|
||||||
|
|------|----------|------|
|
||||||
|
| 中小租户 (< 100) | DynamicDataSource | 资源利用率高,运维简单 |
|
||||||
|
| 大客户/私有化 | **Feign 动态路由** | 物理隔离,独立扩容 |
|
||||||
|
| 金融/政务行业 | **Feign 动态路由** | 安全合规要求高 |
|
||||||
|
| 快速原型/MVP | DynamicDataSource | 快速上线,成本低 |
|
||||||
|
| 长期运营 SaaS | **Feign 动态路由** | 可持续演进,租户自治 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user