# 多租户架构实现指南 ## 一、架构设计概述 资金服务平台采用**Feign 动态路由**方案实现多租户隔离,支持两种部署模式: ### 1.1 多租户部署模式对比 | 特性 | 一库多租户(逻辑隔离) | 一库一租户(物理隔离) | |------|---------------------|---------------------| | **数据隔离** | tenant_id 字段区分 | 独立数据库 | | **资源占用** | 低(共享服务实例) | 中(可独立部署) | | **扩展性** | 一般 | 优秀(单租户可独立扩容) | | **安全性** | 中 | 高 | | **运维成本** | 低 | 中 | | **适用场景** | 中小租户/SaaS | 大客户/私有化/金融政务 | ### 1.2 核心能力 ✅ **租户上下文传递**:通过 `TenantContextHolder` 在调用链中传递租户 ID ✅ **Feign 租户感知**:`FeignChainInterceptor` 自动透传租户信息 ✅ **租户负载均衡**:`TenantAwareLoadBalancer` 根据租户路由到对应实例 ✅ **Nacos 服务注册**:服务实例带租户标签注册 --- ## 二、已实现的核心组件 ### 2.1 租户上下文管理 **文件位置**:`fund-common/src/main/java/com/fundplatform/common/context/TenantContextHolder.java` ```java // 设置当前线程的租户 ID TenantContextHolder.setTenantId(1L); // 获取当前线程的租户 ID Long tenantId = TenantContextHolder.getTenantId(); // 清理(必须在 finally 中执行) TenantContextHolder.clear(); ``` **使用场景**: - Gateway 过滤器解析 Token 后设置 - Feign 拦截器从请求头提取并设置 - Service 层业务逻辑使用 ### 2.2 Feign 租户信息透传 **文件位置**:`fund-common/src/main/java/com/fundplatform/common/feign/FeignChainInterceptor.java` **功能**: - 自动将 `X-Tenant-Id` 传递到下游服务 - 同时传递用户信息(`X-Uid`、`X-Uname`) - 传递 TraceId 用于链路追踪 **示例**: ```java @FeignClient(name = "fund-proj") public interface ProjectFeignClient { @GetMapping("/api/v1/proj/project/{id}") Result getById(@PathVariable Long id); } // 调用时自动携带租户信息 projectFeignClient.getById(1L); // ↓ 实际请求头包含:X-Tenant-Id: 1 ``` ### 2.3 租户路由配置属性 **文件位置**:`fund-common/src/main/java/com/fundplatform/common/config/TenantRoutingProperties.java` **配置项**: ```yaml tenant: routing: enabled: true # 是否启用租户路由 tenant-header: X-Tenant-Id # 租户 ID 请求头 tenant-group-header: X-Tenant-Group # 租户组请求头 group-separator: TENANT_ # 租户组分隔符 default-tenant-id: "1" # 默认租户 ID 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 database: url: jdbc:mysql://localhost:3306/tenant_001_db username: tenant_001 password: xxx ``` ### 2.4 租户感知负载均衡器 **文件位置**:`fund-common/src/main/java/com/fundplatform/common/loadbalancer/TenantAwareLoadBalancer.java` **工作原理**: ``` 1. 从请求头获取 X-Tenant-Id ↓ 2. 构建租户组名称:TENANT_{tenantId} ↓ 3. 过滤 Nacos 服务实例 - 优先选择租户专属实例(metadata.tenant-group = TENANT_XXX) - 回退到共享实例(无 tenant-group 标签) ↓ 4. 轮询选择一个实例发起调用 ``` **服务实例元数据**: ```yaml spring: cloud: nacos: discovery: metadata: tenant-id: "1" tenant-group: TENANT_1 ``` --- ## 三、使用方式 ### 3.1 一库多租户模式(推荐 SaaS) **特点**:所有租户共享服务实例,通过 tenant_id 区分数据 **配置步骤**: #### 1. 服务提供方配置 ```yaml # application.yml spring: cloud: nacos: discovery: server-addr: localhost:8848 username: nacos password: nacos # 不需要特殊配置,所有租户共用实例 ``` ```java // Service 层设置租户 ID 默认值 @Service public class ProjectServiceImpl implements ProjectService { @Autowired private ProjectMapper projectMapper; @Override public void saveProject(Project project) { // 设置默认租户 ID(如果未提供) if (project.getTenantId() == null) { project.setTenantId(1L); // 或从 TenantContextHolder 获取 } projectMapper.insert(project); } @Override public List listProjects() { // 自动添加租户过滤条件 Long tenantId = TenantContextHolder.getTenantId(); return projectMapper.selectList(new LambdaQueryWrapper() .eq(Project::getTenantId, tenantId)); } } ``` #### 2. 服务消费方配置 ```yaml # application.yml tenant: routing: enabled: false # 一库多租户模式不需要启用租户路由 ``` ### 3.2 一库一租户模式(推荐私有化) **特点**:每个租户有独立的服务实例和数据库 **配置步骤**: #### 1. 启用租户路由 ```yaml # application.yml(Gateway 或调用方服务) tenant: routing: enabled: true group-separator: TENANT_ default-tenant-id: "1" shared-services: - fund-gateway - fund-report tenant-configs: tenant_001: tenant-id: tenant_001 services: fund-sys: service-name: fund-sys port: 8100 replicas: 2 database: url: jdbc:mysql://db-tenant001.com:3306/tenant_001_db username: t001 password: xxx ``` #### 2. 服务实例带租户标签注册 ```yaml # application.yml(租户专属服务实例) 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 server: port: 8100 ``` #### 3. 启动多个租户实例 ```bash # 租户 001 的系统服务 java -jar fund-sys.jar \ --spring.application.name=fund-sys \ --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 # 租户 002 的系统服务 java -jar fund-sys.jar \ --spring.application.name=fund-sys \ --spring.cloud.nacos.discovery.metadata.tenant-id=tenant_002 \ --spring.cloud.nacos.discovery.metadata.tenant-group=TENANT_TENANT_002 \ --server.port=8101 \ --spring.datasource.url=jdbc:mysql://db-tenant002.com:3306/tenant_002_db ``` --- ## 四、Gateway 租户路由配置 ### 4.1 Gateway 全局过滤器 ```java @Component public class TenantGatewayFilter implements GlobalFilter, Ordered { @Autowired private JwtTokenProvider tokenProvider; @Override public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request = exchange.getRequest(); // 1. 从 Token 中提取租户 ID String token = getToken(request); Claims claims = tokenProvider.parseToken(token); String tenantId = claims.get("tenantId", String.class); // 2. 将租户 ID 写入请求头 ServerHttpRequest mutatedRequest = request.mutate() .header("X-Tenant-Id", tenantId) .header("X-Tenant-Group", "TENANT_" + tenantId.toUpperCase()) .build(); // 3. 继续执行 return chain.filter(exchange.mutate().request(mutatedRequest).build()); } @Override public int getOrder() { return Ordered.HIGHEST_PRECEDENCE + 1; // 在 JWT 认证之后 } } ``` ### 4.2 Gateway 动态路由配置 ```yaml spring: cloud: gateway: routes: # 系统服务路由(租户感知) - id: fund-sys uri: lb://fund-sys # 使用负载均衡 predicates: - Path=/sys/** filters: - StripPrefix=1 # 租户路由过滤器会自动添加 X-Tenant-Id 和 X-Tenant-Group 头 # 报表服务路由(共享服务) - id: fund-report uri: lb://fund-report predicates: - Path=/report/** filters: - StripPrefix=1 ``` --- ## 五、最佳实践 ### 5.1 租户 ID 设置规范 ```java // ✅ 正确:在 Service 层设置默认值 @Service public class UserServiceImpl implements UserService { @Override public void saveUser(User user) { if (user.getTenantId() == null) { // 从上下文获取,或设置默认值 user.setTenantId(TenantContextHolder.getTenantId()); } userMapper.insert(user); } } // ❌ 错误:依赖前端传递 @Override public void saveUser(User user) { // 如果前端没传 tenantId,会报 NOT NULL 错误 userMapper.insert(user); } ``` ### 5.2 租户数据隔离 ```java // MyBatis Plus 自动注入租户条件 @Bean public ISqlInjector sqlInjector() { return new LogicSqlInjector(); } // 或使用拦截器 @Intercepts({ @Select(type = Executor.class, method = "query", args = {...}) }) public class TenantInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { // 自动添加 WHERE tenant_id = ? OR tenant_id = 0 Long tenantId = TenantContextHolder.getTenantId(); // ... 修改 SQL return invocation.proceed(); } } ``` ### 5.3 日志记录租户信息 ```yaml # logback-spring.xml %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} [TraceId:%X{traceId}, TenantId:%X{tenantId}] - %msg%n ``` 输出示例: ``` 2026-02-18 10:30:00 [main] INFO c.f.s.service.UserService [TraceId:abc123, TenantId:1] - User login: admin ``` --- ## 六、监控与运维 ### 6.1 Nacos 控制台查看租户实例 访问:http://localhost:8048/ 路径:**服务管理 → 服务列表 → fund-sys → 实例列表** 可以看到: - 实例 IP 和端口 - 元数据(tenant-id, tenant-group) - 健康状态 ### 6.2 查看租户调用链 通过 SkyWalking 或 ELK 查看: ``` GET /api/v1/proj/project/1 Headers: X-Tenant-Id: 1 X-Trace-Id: abc123 日志输出: [TraceId:abc123, TenantId:1] Gateway → fund-sys → fund-proj ``` --- ## 七、常见问题 ### Q1: 什么时候需要启用租户路由? **答**: - **一库多租户**:不需要启用(`tenant.routing.enabled=false`) - **一库一租户**:需要启用(`tenant.routing.enabled=true`) ### Q2: 如何判断服务是共享还是租户专属? **答**:通过 `tenant.routing.shared-services` 配置区分: - 列表中的服务:所有租户共用 - 不在列表中的服务:可以是租户专属 ### Q3: 租户专属实例如何水平扩展? **答**:启动多个相同配置的实例即可: ```bash # 租户 001 的 fund-sys 服务,启动 3 个实例 for i in {8100..8102}; do 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=$i & done ``` --- ## 八、下一步计划 ### 已完成 ✅ - [x] TenantContextHolder(租户上下文) - [x] FeignChainInterceptor(Feign 租户信息透传) - [x] TenantRoutingProperties(租户路由配置) - [x] TenantAwareLoadBalancer(租户感知负载均衡器) ### 待实现 🚧 - [ ] Gateway 租户全局过滤器 - [ ] MyBatis Plus 租户自动注入 - [ ] 租户服务实例管理界面 - [ ] 租户级别的监控指标 --- **文档版本**: v1.0 **最后更新**: 2026-02-18 **维护者**: 架构团队