多租户架构设计
大约 10 分钟约 2953 字
多租户架构设计
简介
多租户(Multi-Tenancy)是 SaaS 应用的核心架构模式,允许多个客户(租户)共享同一套应用实例,同时保证数据隔离和定制化。ASP.NET Core 支持基于域名、请求头、路由参数等多种租户解析策略,配合 EF Core 的全局查询过滤器和数据库隔离方案,可以构建灵活的多租户系统。
多租户架构模式
隔离级别(从低到高):
1. 共享数据库 + 共享 Schema(最简单)
| tenants | products | orders |
| tenant001 | 产品A | 订单1 |
| tenant002 | 产品B | 订单2 |
每行有 TenantId 字段,通过 EF Core 全局过滤隔离
2. 共享数据库 + 独立 Schema(中等)
| tenant001.products | tenant002.products |
| 产品A | 产品B |
每个租户独立 Schema,通过 Schema 切换隔离
3. 独立数据库(最隔离)
| db_tenant001 | db_tenant002 |
| products | products |
每个租户独立数据库,通过连接字符串切换
选择指南:
- 100 租户以下 → 共享数据库 + 全局过滤
- 100-1000 租户 → 共享数据库 + 独立 Schema
- 1000+ 租户或合规要求高 → 独立数据库特点
租户解析
租户模型
/// <summary>
/// 租户模型
/// </summary>
public class Tenant
{
public string Id { get; set; } = ""; // 租户唯一标识
public string Name { get; set; } = ""; // 租户名称
public string Domain { get; set; } = ""; // 绑定域名
public string ConnectionString { get; set; } = ""; // 数据库连接串
public string Theme { get; set; } = "default"; // 主题
public TenantPlan Plan { get; set; } = TenantPlan.Basic; // 套餐
public bool IsActive { get; set; } = true; // 是否激活
public DateTimeOffset CreatedAt { get; set; } // 创建时间
public Dictionary<string, string> Settings { get; set; } = new(); // 自定义配置
}
public enum TenantPlan
{
Free = 0,
Basic = 1,
Premium = 2,
Enterprise = 3
}租户解析器
// ============================================
// 租户解析接口
// ============================================
public interface ITenantResolver
{
Task<string?> ResolveTenantIdAsync(HttpContext context);
}
// ============================================
// 方式 A:基于域名解析
// ============================================
public class DomainTenantResolver : ITenantResolver
{
private readonly ITenantStore _store;
private readonly ILogger<DomainTenantResolver> _logger;
public DomainTenantResolver(ITenantStore store, ILogger<DomainTenantResolver> logger)
{
_store = store;
_logger = logger;
}
public async Task<string?> ResolveTenantIdAsync(HttpContext context)
{
var host = context.Request.Host.Host;
_logger.LogDebug("解析租户域名: {Host}", host);
var tenant = await _store.GetByDomainAsync(host);
if (tenant != null && tenant.IsActive)
{
_logger.LogInformation("域名解析租户: Host={Host}, TenantId={TenantId}", host, tenant.Id);
return tenant.Id;
}
_logger.LogWarning("域名未匹配到租户: {Host}", host);
return null;
}
}
// ============================================
// 方式 B:基于请求头解析
// ============================================
public class HeaderTenantResolver : ITenantResolver
{
public Task<string?> ResolveTenantIdAsync(HttpContext context)
{
var tenantId = context.Request.Headers["X-Tenant-Id"].FirstOrDefault();
return Task.FromResult(tenantId);
}
}
// ============================================
// 方式 C:基于路由参数解析
// ============================================
public class RouteTenantResolver : ITenantResolver
{
public Task<string?> ResolveTenantIdAsync(HttpContext context)
{
var tenantId = context.Request.RouteValues["tenantId"]?.ToString();
return Task.FromResult(tenantId);
}
}
// ============================================
// 方式 D:基于 JWT Claims 解析
// ============================================
public class ClaimsTenantResolver : ITenantResolver
{
public Task<string?> ResolveTenantIdAsync(HttpContext context)
{
if (context.User.Identity?.IsAuthenticated == true)
{
var tenantId = context.User.FindFirst("tenant_id")?.Value;
return Task.FromResult(tenantId);
}
return Task.FromResult<string?>(null);
}
}
// ============================================
// 组合解析器 — 按优先级尝试多种方式
// ============================================
public class CompositeTenantResolver : ITenantResolver
{
private readonly ITenantResolver[] _resolvers;
public CompositeTenantResolver(params ITenantResolver[] resolvers)
{
_resolvers = resolvers;
}
public async Task<string?> ResolveTenantIdAsync(HttpContext context)
{
foreach (var resolver in _resolvers)
{
var tenantId = await resolver.ResolveTenantIdAsync(context);
if (!string.IsNullOrEmpty(tenantId))
return tenantId;
}
return null;
}
}租户中间件
自动注入租户上下文
// ============================================
// 租户上下文
// ============================================
public interface ITenantContext
{
string TenantId { get; }
Tenant Tenant { get; }
bool IsResolved { get; }
}
public class TenantContext : ITenantContext
{
public string TenantId { get; }
public Tenant Tenant { get; }
public bool IsResolved { get; }
public TenantContext(string tenantId, Tenant tenant)
{
TenantId = tenantId;
Tenant = tenant;
IsResolved = true;
}
}
// ============================================
// 租户上下文持有者 — Scoped 级别
// ============================================
public class TenantContextHolder
{
public ITenantContext? Current { get; set; }
}
// ============================================
// 租户中间件
// ============================================
public class TenantMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<TenantMiddleware> _logger;
public TenantMiddleware(RequestDelegate next, ILogger<TenantMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(
HttpContext context,
ITenantResolver resolver,
ITenantStore store,
TenantContextHolder holder)
{
var tenantId = await resolver.ResolveTenantIdAsync(context);
if (!string.IsNullOrEmpty(tenantId))
{
var tenant = await store.GetByIdAsync(tenantId);
if (tenant != null && tenant.IsActive)
{
var tenantContext = new TenantContext(tenantId, tenant);
context.Features.Set<ITenantContext>(tenantContext);
holder.Current = tenantContext;
// 设置租户请求头供后续使用
context.Items["TenantId"] = tenantId;
context.Items["TenantPlan"] = tenant.Plan.ToString();
_logger.LogDebug("租户已解析: TenantId={TenantId}, Plan={Plan}",
tenantId, tenant.Plan);
}
else
{
_logger.LogWarning("租户未找到或未激活: TenantId={TenantId}", tenantId);
context.Response.StatusCode = StatusCodes.Status403Forbidden;
await context.Response.WriteAsJsonAsync(new
{
error = "tenant_not_found",
message = "租户不存在或未激活"
});
return;
}
}
await _next(context);
}
}
// ============================================
// 扩展方法
// ============================================
public static class TenantMiddlewareExtensions
{
public static IApplicationBuilder UseMultiTenancy(this IApplicationBuilder app)
{
return app.UseMiddleware<TenantMiddleware>();
}
}
// 注册
builder.Services.AddScoped<TenantContextHolder>();
builder.Services.AddScoped<ITenantContext>(sp =>
{
var holder = sp.GetRequiredService<TenantContextHolder>();
if (holder.Current == null)
throw new InvalidOperationException("租户上下文未解析");
return holder.Current;
});
app.UseMultiTenancy();租户存储
// ============================================
// 租户存储接口
// ============================================
public interface ITenantStore
{
Task<Tenant?> GetByIdAsync(string tenantId);
Task<Tenant?> GetByDomainAsync(string domain);
Task<IReadOnlyList<Tenant>> GetAllAsync();
}
// ============================================
// 数据库存储实现
// ============================================
public class DbTenantStore : ITenantStore
{
private readonly AppDbContext _context;
private readonly IMemoryCache _cache;
public DbTenantStore(AppDbContext context, IMemoryCache cache)
{
_context = context;
_cache = cache;
}
public async Task<Tenant?> GetByIdAsync(string tenantId)
{
var cacheKey = $"tenant:{tenantId}";
if (_cache.TryGetValue(cacheKey, out Tenant? cached))
return cached;
var tenant = await _context.Tenants.FindAsync(tenantId);
if (tenant != null)
_cache.Set(cacheKey, tenant, TimeSpan.FromMinutes(10));
return tenant;
}
public async Task<Tenant?> GetByDomainAsync(string domain)
{
var cacheKey = $"tenant:domain:{domain}";
if (_cache.TryGetValue(cacheKey, out Tenant? cached))
return cached;
var tenant = await _context.Tenants
.FirstOrDefaultAsync(t => t.Domain == domain);
if (tenant != null)
_cache.Set(cacheKey, tenant, TimeSpan.FromMinutes(10));
return tenant;
}
public async Task<IReadOnlyList<Tenant>> GetAllAsync()
{
return await _context.Tenants
.Where(t => t.IsActive)
.ToListAsync();
}
}数据隔离
共享数据库 + 全局过滤
// ============================================
// EF Core 全局查询过滤器 — 共享数据库隔离
// ============================================
public class TenantDbContext : DbContext
{
private readonly string _tenantId;
public TenantDbContext(DbContextOptions options, ITenantContext tenantContext)
: base(options)
{
_tenantId = tenantContext.TenantId;
}
public DbSet<Product> Products => Set<Product>();
public DbSet<Order> Orders => Set<Order>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// 全局查询过滤 — 自动添加 TenantId 条件
modelBuilder.Entity<Product>().HasQueryFilter(p => p.TenantId == _tenantId);
modelBuilder.Entity<Order>().HasQueryFilter(o => o.TenantId == _tenantId);
// 配置复合索引(TenantId + 主键)
modelBuilder.Entity<Product>()
.HasIndex(p => new { p.TenantId, p.Id })
.IsUnique();
modelBuilder.Entity<Order>()
.HasIndex(o => new { o.TenantId, o.Id })
.IsUnique();
}
public override int SaveChanges()
{
SetTenantId();
return base.SaveChanges();
}
public override Task<int> SaveChangesAsync(CancellationToken ct = default)
{
SetTenantId();
return base.SaveChangesAsync(ct);
}
/// <summary>
/// 自动为新实体设置 TenantId
/// </summary>
private void SetTenantId()
{
foreach (var entry in ChangeTracker.Entries<ITenantEntity>()
.Where(e => e.State == EntityState.Added))
{
entry.Property("TenantId").CurrentValue = _tenantId;
}
}
}
// 实体接口
public interface ITenantEntity
{
string TenantId { get; set; }
}
public class Product : ITenantEntity
{
public int Id { get; set; }
public string TenantId { get; set; } = "";
public string Name { get; set; } = "";
public decimal Price { get; set; }
public bool IsActive { get; set; }
}
public class Order : ITenantEntity
{
public long Id { get; set; }
public string TenantId { get; set; } = "";
public decimal Amount { get; set; }
public string Status { get; set; } = "";
public DateTimeOffset CreatedAt { get; set; }
}忽略全局过滤(管理员场景)
/// <summary>
/// 管理员跨租户查询 — 临时禁用全局过滤
/// </summary>
public class AdminService
{
private readonly TenantDbContext _context;
public AdminService(TenantDbContext context)
{
_context = context;
}
public async Task<List<Product>> GetAllProductsAsync(string tenantId)
{
// 临时忽略全局过滤,按指定租户查询
return await _context.Products
.IgnoreQueryFilters()
.Where(p => p.TenantId == tenantId)
.ToListAsync();
}
}独立数据库隔离
// ============================================
// 每个租户独立数据库
// ============================================
public class TenantDbContextFactory
{
private readonly ITenantContext _tenantContext;
private readonly IConfiguration _config;
private readonly ILogger<TenantDbContextFactory> _logger;
public TenantDbContextFactory(
ITenantContext tenantContext,
IConfiguration config,
ILogger<TenantDbContextFactory> logger)
{
_tenantContext = tenantContext;
_config = config;
_logger = logger;
}
public TenantDbContext CreateDbContext()
{
var connectionString = _tenantContext.Tenant.ConnectionString;
if (string.IsNullOrEmpty(connectionString))
{
throw new InvalidOperationException(
$"租户 {_tenantContext.TenantId} 未配置数据库连接串");
}
_logger.LogDebug("为租户创建 DbContext: TenantId={TenantId}", _tenantContext.TenantId);
var optionsBuilder = new DbContextOptionsBuilder<TenantDbContext>();
optionsBuilder.UseSqlServer(connectionString);
return new TenantDbContext(optionsBuilder.Options, _tenantContext);
}
}
// 注册 — 每次请求动态创建
builder.Services.AddScoped<TenantDbContext>(sp =>
{
var factory = sp.GetRequiredService<TenantDbContextFactory>();
return factory.CreateDbContext();
});租户配置与功能开关
// ============================================
// 租户功能开关
// ============================================
public interface IFeatureService
{
bool IsEnabled(string featureName);
T GetSetting<T>(string key, T defaultValue = default!);
}
public class TenantFeatureService : IFeatureService
{
private readonly ITenantContext _tenantContext;
public TenantFeatureService(ITenantContext tenantContext)
{
_tenantContext = tenantContext;
}
public bool IsEnabled(string featureName)
{
var settingKey = $"features:{featureName}";
if (_tenantContext.Tenant.Settings.TryGetValue(settingKey, out var value))
{
return bool.TryParse(value, out var enabled) && enabled;
}
// 默认按套餐决定
return _tenantContext.Tenant.Plan switch
{
TenantPlan.Free => false,
TenantPlan.Basic => IsBasicFeature(featureName),
TenantPlan.Premium => true,
TenantPlan.Enterprise => true,
_ => false
};
}
public T GetSetting<T>(string key, T defaultValue = default!)
{
if (_tenantContext.Tenant.Settings.TryGetValue(key, out var value))
{
try { return (T)Convert.ChangeType(value, typeof(T)); }
catch { return defaultValue; }
}
return defaultValue;
}
private static bool IsBasicFeature(string feature) => feature switch
{
"export" => true,
"api_access" => true,
"custom_branding" => false,
"webhook" => false,
_ => false
};
}
// 在业务代码中使用
app.MapGet("/api/export", (IFeatureService features) =>
{
if (!features.IsEnabled("export"))
{
return Results.Forbid("当前套餐不支持导出功能");
}
return Results.Ok("导出数据...");
});租户级缓存隔离
// ============================================
// 按租户隔离的缓存服务
// ============================================
public class TenantCacheService
{
private readonly IDistributedCache _cache;
private readonly ITenantContext _tenantContext;
public TenantCacheService(IDistributedCache cache, ITenantContext tenantContext)
{
_cache = cache;
_tenantContext = tenantContext;
}
public async Task<T?> GetAsync<T>(string key)
{
var tenantKey = $"tenant:{_tenantContext.TenantId}:{key}";
var json = await _cache.GetStringAsync(tenantKey);
return json == null ? default : JsonSerializer.Deserialize<T>(json);
}
public async Task SetAsync<T>(string key, T value, TimeSpan? expiration = null)
{
var tenantKey = $"tenant:{_tenantContext.TenantId}:{key}";
var json = JsonSerializer.Serialize(value);
await _cache.SetStringAsync(tenantKey, json, new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = expiration ?? TimeSpan.FromMinutes(30)
});
}
public async Task RemoveAsync(string key)
{
var tenantKey = $"tenant:{_tenantContext.TenantId}:{key}";
await _cache.RemoveAsync(tenantKey);
}
}优点
缺点
总结
多租户方案选择:中小规模用共享数据库 + EF Core 全局过滤,大规模用独立数据库。租户解析推荐域名或请求头方式。核心实现:中间件自动解析租户 -> 注入租户上下文 -> EF Core 全局过滤自动隔离。安全关键:确保全局过滤覆盖所有查询路径,防止数据越权。功能开关和缓存隔离是生产环境必备的治理能力。
关键知识点
- 先分清这个主题位于请求链路、后台任务链路还是基础设施链路。
- 服务端主题通常不只关心功能正确,还关心稳定性、性能和可观测性。
- 任何框架能力都要结合配置、生命周期、异常传播和外部依赖一起看。
项目落地视角
- 画清请求进入、业务执行、外部调用、日志记录和错误返回的完整路径。
- 为关键链路补齐超时、重试、熔断、追踪和结构化日志。
- 把配置与敏感信息分离,并明确不同环境的差异来源。
常见误区
- 只会堆中间件或组件,不知道它们在链路中的执行顺序。
- 忽略生命周期和线程池、连接池等运行时资源约束。
- 没有监控和测试就对性能或可靠性下结论。
- 全局过滤器未覆盖所有查询路径,导致跨租户数据泄露。
进阶路线
- 继续向运行时行为、可观测性、发布治理和微服务协同深入。
- 把主题和数据库、缓存、消息队列、认证授权联动起来理解。
- 沉淀团队级模板,包括统一异常处理、配置约定和基础设施封装。
适用场景
- 当你准备把《多租户架构设计》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合 API 服务、后台任务、实时通信、认证授权和微服务协作场景。
- 当需求开始涉及稳定性、性能、可观测性和发布流程时,这类主题会成为基础设施能力。
落地建议
- 先定义请求链路与失败路径,再决定中间件、过滤器、服务边界和依赖方式。
- 为关键链路补日志、指标、追踪、超时与重试策略。
- 环境配置与敏感信息分离,避免把生产参数写死在代码或镜像里。
排错清单
- 先确认问题发生在路由、模型绑定、中间件、业务层还是基础设施层。
- 检查 DI 生命周期、配置来源、序列化规则和认证上下文。
- 查看线程池、连接池、缓存命中率和外部依赖超时。
- 检查全局过滤器是否正确应用,是否有跨租户查询。
复盘问题
- 如果把《多租户架构设计》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《多租户架构设计》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《多租户架构设计》最大的收益和代价分别是什么?
