## 新增功能 ### 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: 负载均衡器单元测试 - 混合模式集成测试脚本
12 KiB
多租户架构实现指南
一、架构设计概述
资金服务平台采用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
// 设置当前线程的租户 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 用于链路追踪
示例:
@FeignClient(name = "fund-proj")
public interface ProjectFeignClient {
@GetMapping("/api/v1/proj/project/{id}")
Result<Project> getById(@PathVariable Long id);
}
// 调用时自动携带租户信息
projectFeignClient.getById(1L);
// ↓ 实际请求头包含:X-Tenant-Id: 1
2.3 租户路由配置属性
文件位置:fund-common/src/main/java/com/fundplatform/common/config/TenantRoutingProperties.java
配置项:
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. 轮询选择一个实例发起调用
服务实例元数据:
spring:
cloud:
nacos:
discovery:
metadata:
tenant-id: "1"
tenant-group: TENANT_1
三、使用方式
3.1 一库多租户模式(推荐 SaaS)
特点:所有租户共享服务实例,通过 tenant_id 区分数据
配置步骤:
1. 服务提供方配置
# application.yml
spring:
cloud:
nacos:
discovery:
server-addr: localhost:8848
username: nacos
password: nacos
# 不需要特殊配置,所有租户共用实例
// 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<Project> listProjects() {
// 自动添加租户过滤条件
Long tenantId = TenantContextHolder.getTenantId();
return projectMapper.selectList(new LambdaQueryWrapper<Project>()
.eq(Project::getTenantId, tenantId));
}
}
2. 服务消费方配置
# application.yml
tenant:
routing:
enabled: false # 一库多租户模式不需要启用租户路由
3.2 一库一租户模式(推荐私有化)
特点:每个租户有独立的服务实例和数据库
配置步骤:
1. 启用租户路由
# 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. 服务实例带租户标签注册
# 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. 启动多个租户实例
# 租户 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 全局过滤器
@Component
public class TenantGatewayFilter implements GlobalFilter, Ordered {
@Autowired
private JwtTokenProvider tokenProvider;
@Override
public Mono<Void> 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 动态路由配置
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 设置规范
// ✅ 正确:在 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 租户数据隔离
// 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 日志记录租户信息
# logback-spring.xml
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36}
[TraceId:%X{traceId}, TenantId:%X{tenantId}] - %msg%n</pattern>
输出示例:
2026-02-18 10:30:00 [main] INFO c.f.s.service.UserService
[TraceId:abc123, TenantId:1] - User login: admin
六、监控与运维
6.1 Nacos 控制台查看租户实例
路径:服务管理 → 服务列表 → 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: 租户专属实例如何水平扩展?
答:启动多个相同配置的实例即可:
# 租户 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
八、下一步计划
已完成 ✅
- TenantContextHolder(租户上下文)
- FeignChainInterceptor(Feign 租户信息透传)
- TenantRoutingProperties(租户路由配置)
- TenantAwareLoadBalancer(租户感知负载均衡器)
待实现 🚧
- Gateway 租户全局过滤器
- MyBatis Plus 租户自动注入
- 租户服务实例管理界面
- 租户级别的监控指标
文档版本: v1.0
最后更新: 2026-02-18
维护者: 架构团队