fundplatform/doc/多租户配置示例.md
zhangjf 10eca3fb35 feat: 实现多租户架构完整能力
## 新增功能

### 1. 多租户核心组件
- TenantRoutingProperties: 租户路由配置属性
- TenantAwareLoadBalancer: 租户感知负载均衡器
- TenantLineHandlerImpl: MyBatis Plus 租户插件
- TenantIgnoreHelper: 忽略租户过滤工具类
- NacosMetadataConfig: Nacos 元数据自动注册

### 2. Gateway 租户过滤器
- TenantGatewayFilter: 从 JWT 提取租户信息写入请求头
- 透传 X-Tenant-Id、X-Tenant-Group、X-User-Id、X-Username

### 3. 支持的部署模式
- 一库多租户(SaaS 模式): 通过 tenant_id 字段隔离
- 一库一租户(私有化): 独立服务实例和数据库
- 混合模式: VIP 租户专属实例 + 普通租户共享实例

### 4. Nacos 3.0 适配
- 所有业务模块添加 username/password 认证配置
- 服务实例自动注册租户标签

## 问题修复
- #8: FeignClient 硬编码 URL 导致 Nacos 服务发现失效
- #9: Nacos 3.0 客户端缺少 username/password 认证配置
- fund-exp expenseType 字段类型从 Integer 改为 Long

## 测试
- TenantAwareLoadBalancerTest: 负载均衡器单元测试
- 混合模式集成测试脚本
2026-02-19 18:10:16 +08:00

9.0 KiB
Raw Permalink Blame History

多租户配置示例

场景一一库多租户SaaS 模式)

Gateway 配置

# fund-gateway/src/main/resources/application.yml
server:
  port: 8000

spring:
  application:
    name: fund-gateway
  
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
        username: nacos
        password: nacos
    
    gateway:
      routes:
        - id: fund-sys
          uri: lb://fund-sys
          predicates:
            - Path=/sys/**
          filters:
            - StripPrefix=1
        
        - id: fund-cust
          uri: lb://fund-cust
          predicates:
            - Path=/cust/**
          filters:
            - StripPrefix=1

# 不需要启用租户路由,所有租户共享实例
tenant:
  routing:
    enabled: false

业务服务配置(所有租户共用)

# fund-sys/src/main/resources/application.yml
server:
  port: 8100

spring:
  application:
    name: fund-sys
  
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
        username: nacos
        password: nacos
        # 不需要特殊元数据配置
  
  datasource:
    url: jdbc:mysql://localhost:3306/fund_sys?useUnicode=true&characterEncoding=utf8
    username: root
    password: ${DB_PASSWORD}
    hikari:
      maximum-pool-size: 20

mybatis-plus:
  mapper-locations: classpath*:/mapper/**/*.xml
  configuration:
    map-underscore-to-camel-case: true

# 不启用租户路由
tenant:
  routing:
    enabled: false

Service 层租户 ID 处理

@Service
public class UserServiceImpl implements UserService {
    
    @Autowired
    private UserMapper userMapper;
    
    @Override
    public void saveUser(User user) {
        // 设置默认租户 ID如果未提供
        if (user.getTenantId() == null) {
            Long tenantId = TenantContextHolder.getTenantId();
            if (tenantId == null) {
                tenantId = 1L; // 默认租户
            }
            user.setTenantId(tenantId);
        }
        
        userMapper.insert(user);
    }
    
    @Override
    public List<User> listUsers() {
        // 自动添加租户过滤条件
        Long tenantId = TenantContextHolder.getTenantId();
        return userMapper.selectList(new LambdaQueryWrapper<User>()
            .eq(User::getTenantId, tenantId));
    }
}

场景二:一库一租户(私有化部署)

Gateway 配置(支持租户路由)

# fund-gateway/src/main/resources/application.yml
server:
  port: 8000

spring:
  application:
    name: fund-gateway
  
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
        username: nacos
        password: nacos
    
    gateway:
      routes:
        - id: fund-sys
          uri: lb://fund-sys
          predicates:
            - Path=/sys/**
          filters:
            - StripPrefix=1
        
        # 报表服务是共享的
        - id: fund-report
          uri: lb://fund-report
          predicates:
            - Path=/report/**
          filters:
            - StripPrefix=1

# 启用租户路由
tenant:
  routing:
    enabled: true
    tenant-header: X-Tenant-Id
    tenant-group-header: X-Tenant-Group
    group-separator: TENANT_
    default-tenant-id: "1"
    
    # 共享服务列表(所有租户共用)
    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://db-tenant001.com:3306/tenant_001_db
          username: t001
          password: ${DB_PASSWORD_TENANT001}
      
      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://db-tenant002.com:3306/tenant_002_db
          username: t002
          password: ${DB_PASSWORD_TENANT002}

租户专属服务实例配置

# fund-sys 租户 001 实例配置
# fund-sys-tenant001-instance1.yml
server:
  port: 8100

spring:
  application:
    name: fund-sys
  
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
        username: nacos
        password: nacos
        metadata:
          tenant-id: tenant_001
          tenant-group: TENANT_TENANT_001
  
  datasource:
    url: jdbc:mysql://db-tenant001.com:3306/tenant_001_db
    username: t001
    password: ${DB_PASSWORD_TENANT001}
    driver-class-name: com.mysql.cj.jdbc.Driver

tenant:
  routing:
    enabled: true
# fund-sys 租户 002 实例配置
# fund-sys-tenant002-instance1.yml
server:
  port: 8101

spring:
  application:
    name: fund-sys
  
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
        username: nacos
        password: nacos
        metadata:
          tenant-id: tenant_002
          tenant-group: TENANT_TENANT_002
  
  datasource:
    url: jdbc:mysql://db-tenant002.com:3306/tenant_002_db
    username: t002
    password: ${DB_PASSWORD_TENANT002}
    driver-class-name: com.mysql.cj.jdbc.Driver

tenant:
  routing:
    enabled: true

启动脚本示例

#!/bin/bash

# 启动租户 001 的系统服务2 个实例)
echo "Starting fund-sys for tenant_001 (instance 1)..."
nohup java -jar fund-sys.jar \
  --spring.cloud.nacos.discovery.metadata.tenant-id=tenant_001 \
  --spring.cloud.nacos.discovery.metadata.tenant-group=TENANT_TENANT_001 \
  --server.port=8100 \
  --spring.datasource.url=jdbc:mysql://db-tenant001.com:3306/tenant_001_db \
  > logs/fund-sys-tenant001-instance1.log 2>&1 &

echo "Starting fund-sys for tenant_001 (instance 2)..."
nohup java -jar fund-sys.jar \
  --spring.cloud.nacos.discovery.metadata.tenant-id=tenant_001 \
  --spring.cloud.nacos.discovery.metadata.tenant-group=TENANT_TENANT_001 \
  --server.port=8101 \
  --spring.datasource.url=jdbc:mysql://db-tenant001.com:3306/tenant_001_db \
  > logs/fund-sys-tenant001-instance2.log 2>&1 &

# 启动租户 002 的系统服务1 个实例)
echo "Starting fund-sys for tenant_002 (instance 1)..."
nohup java -jar fund-sys.jar \
  --spring.cloud.nacos.discovery.metadata.tenant-id=tenant_002 \
  --spring.cloud.nacos.discovery.metadata.tenant-group=TENANT_TENANT_002 \
  --server.port=8102 \
  --spring.datasource.url=jdbc:mysql://db-tenant002.com:3306/tenant_002_db \
  > logs/fund-sys-tenant002-instance1.log 2>&1 &

# 启动客户服务(共享服务,所有租户共用)
echo "Starting fund-cust (shared service)..."
nohup java -jar fund-cust.jar \
  --server.port=8110 \
  --spring.datasource.url=jdbc:mysql://localhost:3306/fund_cust \
  > logs/fund-cust-shared.log 2>&1 &

echo "All services started!"

场景三:混合模式(部分租户独立部署)

# Gateway 配置
tenant:
  routing:
    enabled: true
    default-tenant-id: "1"
    shared-services:
      - fund-gateway
      - fund-report
      - fund-file
    
    # 只有大客户有独立部署
    tenant-configs:
      # VIP 客户:独立部署
      vip_customer:
        tenant-id: vip_customer
        services:
          fund-sys:
            service-name: fund-sys
            port: 8200
            replicas: 3
          fund-cust:
            service-name: fund-cust
            port: 8210
            replicas: 2
        database:
          url: jdbc:mysql://vip-db.com:3306/vip_db
          username: vip
          password: xxx
      
      # 中小客户:共享部署(不在此配置,使用默认配置)

环境变量配置(生产环境推荐)

# .env 文件
# Nacos 配置
NACOS_SERVER_ADDR=nacos.prod.com:8848
NACOS_USERNAME=nacos
NACOS_PASSWORD=${NACOS_PROD_PASSWORD}

# 数据库配置
DB_HOST_PROD=mysql.prod.com
DB_PORT_PROD=3306
DB_USERNAME_PROD=root
DB_PASSWORD_PROD=${DB_PROD_PASSWORD}

# 租户配置
TENANT_ROUTING_ENABLED=true
TENANT_DEFAULT_ID=1

# 租户 001 配置
TENANT_001_ID=tenant_001
TENANT_001_DB_HOST=db-tenant001.com
TENANT_001_DB_USERNAME=t001
TENANT_001_DB_PASSWORD=${TENANT001_PASSWORD}

# 租户 002 配置
TENANT_002_ID=tenant_002
TENANT_002_DB_HOST=db-tenant002.com
TENANT_002_DB_USERNAME=t002
TENANT_002_DB_PASSWORD=${TENANT002_PASSWORD}
# application-prod.yml
spring:
  cloud:
    nacos:
      discovery:
        server-addr: ${NACOS_SERVER_ADDR}
        username: ${NACOS_USERNAME}
        password: ${NACOS_PASSWORD}

tenant:
  routing:
    enabled: ${TENANT_ROUTING_ENABLED:false}
    default-tenant-id: ${TENANT_DEFAULT_ID:1}
    tenant-configs:
      tenant_001:
        tenant-id: ${TENANT_001_ID}
        database:
          url: jdbc:mysql://${TENANT_001_DB_HOST}:${DB_PORT_PROD}/tenant_001_db
          username: ${TENANT_001_DB_USERNAME}
          password: ${TENANT_001_DB_PASSWORD}

文档版本: v1.0
最后更新: 2026-02-18