多租户配置(Multi‑Tenancy)
本页系统性介绍 Ape‑Volo‑Admin 的多租户能力与落地方式:租户解析、两种隔离模式(按 Id 与按库)、ORM 过滤器、实体设计、动态路由到租户库,以及常见陷阱与最佳实践。
你将获得
- 如何开启多租户与选择隔离模式
- 租户 Id 从哪里来、如何贯穿请求链
- Id 隔离与库隔离各自的实体/查询/写入策略
- SqlSugar 层面的全局租户过滤与动态连接路由
- 常见边界与安全注意(越权、跨租户事务、缓存键设计等)
1. 开关与模式
配置对象(Options):
csharp
[OptionsSettings]
public class TenantOptions
{
public bool Enabled { get; set; }
public TenantType Type { get; set; } // Id 或 Db
}
public enum TenantType
{
[Display(Name = "Enum.Tenant.Id")]
Id = 1,
[Display(Name = "Enum.Tenant.Db")]
Db = 2
}JSON 配置示例:
json
{
"Tenant": {
"Enabled": true,
"Type": 2
}
}含义:
- Enabled:总开关。
- Type:隔离模式,1=按 Id(同库同表,按字段隔离),2=按库(不同租户不同库)。
2. 租户解析(TenantId 来源)
系统通过登录态解析当前用户上下文中的 TenantId(App.HttpUser.TenantId)。常见来源:
- 登录后 Token 中的租户声明(Claim)
- 管理端切换租户时写入当前上下文
提示:确保登录中间件正确填充 App.HttpUser,并在无租户或租户无权的情况下及时拦截。
3. 模式一:按 Id 隔离(Row‑Level)
实体实现 ITenantEntity,ORM 自动在查询阶段追加租户过滤;写入阶段自动回填 TenantId。
实体示例:
csharp
[SugarTable("test_order")]
public class TestOrder : BaseEntity, ITenantEntity
{
public int TenantId { get; set; }
}全局查询过滤器(来自 SqlSugar 扩展):
csharp
private static void ConfiguringTenantFilter(this SqlSugarScopeProvider db)
{
if (App.HttpUser.IsNotNull() && App.HttpUser.TenantId > 0)
{
db.QueryFilter.AddTableFilter<ITenantEntity>(it => it.TenantId == App.HttpUser.TenantId);
}
}写入时自动补全(AOP DataExecuting 阶段):
csharp
if (tenant != null && App.HttpUser.TenantId > 0)
{
if (tenant.TenantId == 0)
{
tenant.TenantId = App.HttpUser.TenantId;
}
}适用场景:
- 数据量较小或表结构一致性要求高;
- 单库维护方便、成本低;
- 跨租户统计可通过管理员角色在“关闭过滤器”的专用查询里完成(需严格鉴权)。
4. 模式二:按库隔离(Database‑Level)
实体添加标记特性 [MultiDbTenant],仓储在访问该实体时,根据当前 TenantId 动态切换到对应租户库。
实体示例:
csharp
[MultiDbTenant]
[SugarTable("test_order")]
public class TestOrder : BaseEntity
{
public int TenantId { get; set; }
}动态路由核心逻辑(示意):
csharp
var useMultiTenant = AppSettings.GetValue<bool>("Tenant", "Enabled");
if (useMultiTenant)
{
var multiDbTenantAttribute = typeof(TEntity).GetCustomAttribute<MultiDbTenantAttribute>();
if (multiDbTenantAttribute != null && httpUser.IsNotNull() && httpUser.TenantId > 0)
{
var tenants = sqlSugarScope.Queryable<Tenant>().WithCache(86400).ToList();
var tenant = tenants.FirstOrDefault(x => x.TenantId == httpUser.TenantId);
if (tenant != null)
{
var iTenant = sqlSugarScope.AsTenant();
if (!iTenant.IsAnyConnection(tenant.ConfigId))
{
var conn = tenant.ConnectionString;
if (tenant.DbType == DbType.Sqlite)
{
conn = "DataSource=" + Path.Combine(AppSettings.ContentRootPath, tenant.ConnectionString);
}
iTenant.AddConnection(TenantHelper.GetConnectionConfig(tenant.ConfigId, tenant.DbType.Value, conn));
}
SugarClient = iTenant.GetConnectionScope(tenant.ConfigId);
return;
}
}
}适用场景:
- 大租户/强隔离诉求(数据/性能/合规);
- 不同租户可使用不同 DB 类型或连接参数;
- 支持迁移、弹性扩容、分库分表策略。
5. 缓存与跨库注意
- 缓存键建议统一加租户前缀:如
tenant:{TenantId}:xxx,避免串租; - 队列/定时任务等后台服务同样需要“租户上下文”;
- 严禁跨租户事务;涉及跨租户统计,建议走离线数仓或只读报表库。
6. 与 SqlSugar AOP 及过滤器的协同
参考《DB 仓储(SqlSugar)》:
- 软删除过滤器 + 多租户过滤器 + 数据权限过滤器 组合;
- AOP 写入前补全(
CreateBy/UpdateBy/TenantId); - 慢 SQL 警告与 MiniProfiler 联动,便于定位某租户的慢查询。
7. 常见问题(FAQ)
- 为什么查出来是“别的租户”的数据?
- 实体是否实现了
ITenantEntity(Id 隔离场景)或添加了[MultiDbTenant](库隔离场景); App.HttpUser.TenantId是否为空或被错误覆盖;- 是否在管理接口中关闭/绕过了过滤器(请严格权限控制)。
- 库隔离下,租户配置如何维护?
- 通常在“租户表”存储租户的
ConfigId/DbType/ConnectionString,并可缓存(如WithCache(86400)); - 连接不存在时动态
AddConnection,并通过GetConnectionScope获取客户端。
- 多租户与日志库/操作异常日志如何配合?
- 建议日志写入独立库(
LogDataBase),与生产数据解耦; - 若需按租户检索日志,可在日志结构中添加
TenantId字段。
- 迁移与初始化(Code‑First/Seed)怎么做?
- Id 隔离:同库同表,按租户回填数据即可;
- 库隔离:按租户逐库执行迁移与种子,需配套自动化脚本或后台任务。
8. 最佳实践(建议)
- 统一“租户解析”入口,保证
TenantId在请求首个环节就生效; - 严格的鉴权/审计:管理员跨租户查询需显式开关,并完整记录操作日志;
- 缓存/消息/任务全面租户化:从键前缀、Topic 到任务分片均考虑租户维度;
- 对大租户优先选“库隔离”,为迁移与容量规划留足空间;
- 与 AOP、数据权限、软删除链路联测,防串租与“数据看不见”。
相关文档:

