外观
多租户(nebula-framework-tenant)
2026-04-01
Nebula 的多租户模块提供了行级隔离(FIELD)、Schema 隔离、数据库隔离三种策略,以及完整的租户上下文管理、数据源路由和切面工具。
绝大多数 SaaS 业务使用行级隔离即可,框架通过 MyBatis-Plus 拦截器自动追加tenant_id条件,业务代码无需手动处理。
目录
- 一、隔离策略说明
- 二、TenantContextHolder 上下文
- 三、行级隔离的自动处理
- 四、切换租户:@TenantSwitch
- 五、忽略租户:@TenantIgnore
- 六、功能开关:@FeatureEnabled
- 七、数据源路由(DATABASE 模式)
- 八、配置参考
一、隔离策略说明
| 隔离模式 | TenantIsolationMode | 说明 | 适用场景 |
|---|---|---|---|
| 行级隔离 | FIELD | 所有租户共用同一数据库和表,通过 tenant_id 字段区分 | 推荐,适合大多数 SaaS 场景 |
| Schema 隔离 | SCHEMA | 同一数据库实例,每个租户使用独立的 Schema | 数据需要物理隔离但不需要独立实例 |
| 数据库隔离 | DATABASE | 每个租户使用独立的数据库实例 | 高安全性要求或大客户专属部署 |
选择建议: 优先使用 FIELD(行级隔离),除非客户有明确的数据物理隔离要求。
二、TenantContextHolder 上下文
TenantContextHolder 是当前请求租户信息的存储与访问入口,基于 ThreadLocal 实现。
2.1 基本读取
// 获取当前租户信息(完整对象)
TenantInfo tenantInfo = TenantContextHolder.get();
// 获取当前租户 ID
Long tenantId = TenantContextHolder.getTenantId();
// 获取当前租户编码(如 "TENANT_001")
String tenantCode = TenantContextHolder.getTenantCode();
// 获取 Schema 名称(Schema 隔离模式使用)
String schemaName = TenantContextHolder.getSchemaName();
// 获取数据库路由 Key(DATABASE 隔离模式使用)
String dbKey = TenantContextHolder.getDatabaseKey();
// 是否有租户上下文
boolean hasTenant = TenantContextHolder.hasTenant();2.2 手动设置(少数场景)
// 设置租户上下文(框架通过 Filter 自动设置,业务代码一般不需要手动调用)
TenantContextHolder.set(tenantInfo);
// 清除(在 Filter 的 finally 块中调用,框架已处理)
TenantContextHolder.clear();2.3 切换租户执行代码
// 以指定租户身份执行代码块(无返回值)
TenantContextHolder.runAsTenant(tenantInfo, () -> {
// 此块内的所有 SQL 都会以指定租户身份执行
demoService.batchCreate(demos);
});
// 以指定租户身份执行代码块(有返回值)
List<DemoDO> result = TenantContextHolder.callAsTenant(tenantInfo, () -> {
return demoMapper.selectList(null);
});
// 忽略租户执行(查询所有租户数据)
TenantContextHolder.runWithoutTenant(() -> {
// 此块内的 SQL 不追加 tenant_id 过滤
List<TenantDO> tenants = tenantMapper.selectList(null);
});三、行级隔离的自动处理
启用 FIELD 模式后,框架通过 MyBatis-Plus 的 TenantLineInnerInterceptor 自动完成:
1. SQL 注入: 所有通过 BaseMapper / LambdaQueryWrapper 执行的 SQL,自动追加 AND tenant_id = {currentTenantId}
2. INSERT 填充: 新增记录时,MetaObjectHandlerImpl 自动将 tenantId 字段填充为当前租户 ID
HTTP 请求到来
│
▼
TenantContextFilter(框架自动注册)
├── 从 JWT Token 中解析租户信息
└── 写入 TenantContextHolder(ThreadLocal)
│
▼
业务代码执行
├── BaseMapper.selectList(wrapper)
│ └── 拦截器自动追加 AND tenant_id = xxx
└── BaseMapper.insert(entity)
└── MetaObjectHandler 自动填充 tenant_id
│
▼
Filter finally 块
└── TenantContextHolder.clear()(清除 ThreadLocal)四、切换租户:@TenantSwitch
某些特殊场景(如数据导出、定时任务、租户初始化)需要以特定租户身份执行逻辑,使用 @TenantSwitch 注解:
// 按租户 ID 切换
@TenantSwitch(tenantIdExpr = "#tenantId")
public void exportTenantData(Long tenantId) {
// 此方法内的所有 SQL 都以 tenantId 对应的租户身份执行
List<DemoDO> list = demoMapper.selectList(null);
// ...
}
// 按租户编码切换(框架内部会解析为租户 ID)
@TenantSwitch(tenantCodeExpr = "#tenantCode")
public void initTenantData(String tenantCode) {
// ...
}参数说明:
| 参数 | 说明 |
|---|---|
tenantIdExpr | SpEL 表达式,指定目标租户 ID |
tenantCodeExpr | SpEL 表达式,指定目标租户编码(与 tenantIdExpr 二选一,id 优先) |
注意:
@TenantSwitch基于 AOP 实现,需要确保调用方是通过 Spring 代理调用(不能在同类内直接调用被注解的方法)。
五、忽略租户:@TenantIgnore
某些查询需要跨租户访问数据(如后台全局查询、租户管理功能),使用 @TenantIgnore 跳过多租户过滤:
// 方法级:该方法内的 SQL 不追加 tenant_id 过滤
@TenantIgnore
public List<TenantDO> listAllTenants() {
return tenantMapper.selectList(null);
}
// 类级:该类所有方法都跳过多租户过滤
@TenantIgnore
@Service
public class TenantManagementService { ... }安全警告:
@TenantIgnore会查询所有租户数据,务必在后台管理接口或系统级操作中使用,避免在普通业务接口上误用导致数据泄露。
六、功能开关:@FeatureEnabled
@FeatureEnabled 用于检查当前租户是否开通了某项功能,通过 TenantFeatureRepository SPI 查询租户功能配置,未开通时抛出 FeatureNotEnabledException:
// 检查当前租户是否开通了"文件存储"功能
@FeatureEnabled("FILE_STORAGE")
public FileUploadResult upload(MultipartFile file) {
// ...
}
// 检查多个功能
@FeatureEnabled({"EXCEL_EXPORT", "FILE_STORAGE"})
public void exportWithFile() {
// ...
}
// 自定义未开通提示消息
@FeatureEnabled(value = "AI_ANALYSIS", message = "当前套餐不支持 AI 分析功能,请升级套餐")
public AnalysisResult analyze(Long dataId) {
// ...
}七、数据源路由(DATABASE 模式)
当隔离模式为 DATABASE 时,TenantRoutingDataSource 根据当前租户的数据库 Key 动态切换数据源。
7.1 动态注册数据源
@Autowired
private TenantRoutingDataSource routingDataSource;
// 注册新租户数据源
routingDataSource.registerDataSource("TENANT_001_DB", dataSource);
// 移除数据源
routingDataSource.removeDataSource("TENANT_001_DB");
// 查询已注册的所有 key
Set<String> keys = routingDataSource.getRegisteredKeys();7.2 数据源 SPI
业务服务实现 TenantDataSourceProvider 接口,提供动态数据源的加载逻辑:
@Component
public class MyTenantDataSourceProvider implements TenantDataSourceProvider {
@Override
public DataSource getDataSource(String databaseKey) {
// 根据 databaseKey 加载或创建数据源(如从配置中心读取数据库连接信息)
return buildDataSource(databaseKey);
}
}八、配置参考
nebula:
tenant:
enabled: true # 是否启用多租户(默认 true)
isolation-mode: FIELD # 隔离模式:FIELD / SCHEMA / DATABASE
ignore-tables: # 不需要租户过滤的表(如全局配置表)
- sys_region
- sys_dict_type