第十五章:多租户架构
一、多租户概述
1.1 什么是多租户
多租户(Multi-Tenancy)是一种软件架构模式,在同一个应用实例中为多个租户(客户/组织)提供服务,每个租户的数据相互隔离。多租户架构是 SaaS(Software as a Service)平台的核心技术之一。
1.2 多租户架构类型
| 架构类型 | 说明 | 隔离级别 | 成本 | 复杂度 |
|---|---|---|---|---|
| 独立数据库 | 每个租户一个独立数据库 | 最高 | 高 | 中 |
| 共享数据库独立Schema | 共享数据库,每个租户独立Schema | 高 | 中 | 高 |
| 共享数据库共享Schema | 通过 TenantId 字段区分数据 | 低 | 低 | 低 |
| 混合模式 | 大客户独立库,小客户共享库 | 可调 | 灵活 | 高 |
1.3 Furion 多租户支持
Furion 提供了灵活的多租户解决方案,支持多种租户识别策略和数据隔离方案:
var builder = WebApplication.CreateBuilder(args);
// 注册多租户服务
builder.Services.AddMultiTenant();
var app = builder.Build();
app.UseMultiTenant();
app.Run();
二、租户识别策略
2.1 Header 识别
通过请求头携带租户标识:
/// <summary>
/// 基于 Header 的租户识别策略
/// </summary>
public class HeaderTenantResolver : ITenantResolver
{
private const string TenantHeaderKey = "X-Tenant-Id";
public Task<string> ResolveAsync(HttpContext httpContext)
{
var tenantId = httpContext.Request.Headers[TenantHeaderKey].FirstOrDefault();
if (string.IsNullOrEmpty(tenantId))
{
throw new TenantNotFoundException("请求头中缺少租户标识");
}
return Task.FromResult(tenantId);
}
}
客户端请求示例:
GET /api/users HTTP/1.1
Host: api.example.com
X-Tenant-Id: tenant_001
Authorization: Bearer eyJhbGciOi...
2.2 Route 路由识别
通过 URL 路由中的租户参数识别:
/// <summary>
/// 基于路由的租户识别策略
/// </summary>
public class RouteTenantResolver : ITenantResolver
{
public Task<string> ResolveAsync(HttpContext httpContext)
{
var tenantId = httpContext.Request.RouteValues["tenantId"]?.ToString();
if (string.IsNullOrEmpty(tenantId))
{
throw new TenantNotFoundException("路由中缺少租户标识");
}
return Task.FromResult(tenantId);
}
}
路由配置:
app.MapControllerRoute(
name: "tenant",
pattern: "{tenantId}/{controller}/{action=Index}/{id?}");
// 请求示例:GET /tenant_001/api/users
2.3 Domain 域名识别
通过子域名识别租户:
/// <summary>
/// 基于域名的租户识别策略
/// </summary>
public class DomainTenantResolver : ITenantResolver
{
private readonly ITenantStore _tenantStore;
public DomainTenantResolver(ITenantStore tenantStore)
{
_tenantStore = tenantStore;
}
public async Task<string> ResolveAsync(HttpContext httpContext)
{
var host = httpContext.Request.Host.Host;
// 从子域名提取租户标识
// 例如:tenant001.example.com -> tenant001
var subdomain = host.Split('.').First();
var tenant = await _tenantStore.FindByDomainAsync(subdomain);
if (tenant == null)
{
throw new TenantNotFoundException($"未找到域名 {host} 对应的租户");
}
return tenant.TenantId;
}
}
// 请求示例:
// GET https://company-a.example.com/api/users
// GET https://company-b.example.com/api/users
2.4 Query 查询参数识别
通过 URL 查询参数识别:
/// <summary>
/// 基于查询参数的租户识别策略
/// </summary>
public class QueryTenantResolver : ITenantResolver
{
public Task<string> ResolveAsync(HttpContext httpContext)
{
var tenantId = httpContext.Request.Query["tenant"].FirstOrDefault();
if (string.IsNullOrEmpty(tenantId))
{
throw new TenantNotFoundException("查询参数中缺少租户标识");
}
return Task.FromResult(tenantId);
}
}
// 请求示例:GET /api/users?tenant=tenant_001
2.5 组合识别策略
/// <summary>
/// 组合租户识别策略(按优先级尝试多种方式)
/// </summary>
public class CompositeTenantResolver : ITenantResolver
{
public Task<string> ResolveAsync(HttpContext httpContext)
{
// 优先级1:Header
var tenantId = httpContext.Request.Headers["X-Tenant-Id"].FirstOrDefault();
// 优先级2:Route
if (string.IsNullOrEmpty(tenantId))
{
tenantId = httpContext.Request.RouteValues["tenantId"]?.ToString();
}
// 优先级3:Query
if (string.IsNullOrEmpty(tenantId))
{
tenantId = httpContext.Request.Query["tenant"].FirstOrDefault();
}
// 优先级4:Claims
if (string.IsNullOrEmpty(tenantId))
{
tenantId = httpContext.User?.FindFirst("tenant_id")?.Value;
}
if (string.IsNullOrEmpty(tenantId))
{
throw new TenantNotFoundException("无法识别租户");
}
return Task.FromResult(tenantId);
}
}
2.6 识别策略对比
| 策略 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| Header | 灵活、不影响URL | 需前端配合 | API 接口 |
| Route | 直观、RESTful | URL 变长 | 网站/API |
| Domain | 专业、品牌感 | 需配置DNS | SaaS 平台 |
| Query | 简单 | 不够优雅 | 调试/简单场景 |
| JWT Claims | 安全 | 依赖认证 | 认证系统集成 |
三、数据库隔离方案
3.1 独立数据库方案
每个租户使用独立的数据库,数据完全隔离:
/// <summary>
/// 租户数据库上下文
/// </summary>
public class TenantDbContext : DbContext
{
private readonly ITenantContext _tenantContext;
public TenantDbContext(
DbContextOptions<TenantDbContext> options,
ITenantContext tenantContext)
: base(options)
{
_tenantContext = tenantContext;
}
public DbSet<User> Users { get; set; }
public DbSet<Order> Orders { get; set; }
public DbSet<Product> Products { get; set; }
}
/// <summary>
/// 租户数据库连接字符串提供器
/// </summary>
public class TenantDbConnectionProvider : IDbConnectionProvider
{
private readonly ITenantStore _tenantStore;
private readonly ITenantContext _tenantContext;
public TenantDbConnectionProvider(
ITenantStore tenantStore,
ITenantContext tenantContext)
{
_tenantStore = tenantStore;
_tenantContext = tenantContext;
}
public async Task<string> GetConnectionStringAsync()
{
var tenantId = _tenantContext.TenantId;
var tenant = await _tenantStore.GetByIdAsync(tenantId);
if (tenant == null)
{
throw new TenantNotFoundException($"租户 {tenantId} 不存在");
}
return tenant.ConnectionString;
}
}
注册独立数据库方案:
builder.Services.AddDbContext<TenantDbContext>((serviceProvider, options) =>
{
var tenantContext = serviceProvider.GetRequiredService<ITenantContext>();
var tenantStore = serviceProvider.GetRequiredService<ITenantStore>();
var tenant = tenantStore.GetById(tenantContext.TenantId);
options.UseSqlServer(tenant.ConnectionString);
});
3.2 共享数据库方案(行级隔离)
所有租户共享同一个数据库,通过 TenantId 字段区分数据:
/// <summary>
/// 多租户基础实体
/// </summary>
public abstract class TenantEntity
{
public long Id { get; set; }
/// <summary>
/// 租户Id(用于数据隔离)
/// </summary>
public string TenantId { get; set; }
public DateTime CreatedTime { get; set; }
public DateTime? UpdatedTime { get; set; }
}
/// <summary>
/// 用户实体(继承租户基类)
/// </summary>
public class User : TenantEntity
{
public string UserName { get; set; }
public string Email { get; set; }
public string RealName { get; set; }
public int Status { get; set; }
}
/// <summary>
/// 订单实体
/// </summary>
public class Order : TenantEntity
{
public string OrderNo { get; set; }
public decimal TotalAmount { get; set; }
public int Status { get; set; }
}
3.3 全局查询过滤器
使用 EF Core 的全局查询过滤器自动添加租户条件:
/// <summary>
/// 多租户 DbContext
/// </summary>
public class MultiTenantDbContext : DbContext
{
private readonly ITenantContext _tenantContext;
public MultiTenantDbContext(
DbContextOptions<MultiTenantDbContext> options,
ITenantContext tenantContext)
: base(options)
{
_tenantContext = tenantContext;
}
public DbSet<User> Users { get; set; }
public DbSet<Order> Orders { get; set; }
public DbSet<Product> Products { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// 为所有实现 TenantEntity 的实体添加全局查询过滤器
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
{
if (typeof(TenantEntity).IsAssignableFrom(entityType.ClrType))
{
modelBuilder.Entity(entityType.ClrType)
.AddQueryFilter<TenantEntity>(e => e.TenantId == _tenantContext.TenantId);
}
}
}
/// <summary>
/// 重写 SaveChanges,自动设置 TenantId
/// </summary>
public override int SaveChanges()
{
SetTenantId();
return base.SaveChanges();
}
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
SetTenantId();
return await base.SaveChangesAsync(cancellationToken);
}
private void SetTenantId()
{
var entries = ChangeTracker.Entries<TenantEntity>()
.Where(e => e.State == EntityState.Added);
foreach (var entry in entries)
{
entry.Entity.TenantId = _tenantContext.TenantId;
}
}
}
四、Schema 隔离
4.1 Schema 隔离实现
每个租户使用不同的 Schema(如 PostgreSQL/SQL Server 中的 Schema):
/// <summary>
/// 基于 Schema 的多租户 DbContext
/// </summary>
public class SchemaMultiTenantDbContext : DbContext
{
private readonly ITenantContext _tenantContext;
public SchemaMultiTenantDbContext(
DbContextOptions<SchemaMultiTenantDbContext> options,
ITenantContext tenantContext)
: base(options)
{
_tenantContext = tenantContext;
}
public DbSet<User> Users { get; set; }
public DbSet<Order> Orders { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
var schema = $"tenant_{_tenantContext.TenantId}";
// 设置所有表的 Schema
modelBuilder.HasDefaultSchema(schema);
// 或者单独设置
modelBuilder.Entity<User>().ToTable("Users", schema);
modelBuilder.Entity<Order>().ToTable("Orders", schema);
}
}
4.2 Schema 自动创建
/// <summary>
/// 租户 Schema 管理服务
/// </summary>
public class TenantSchemaService
{
private readonly IServiceScopeFactory _scopeFactory;
private readonly ILogger<TenantSchemaService> _logger;
public TenantSchemaService(
IServiceScopeFactory scopeFactory,
ILogger<TenantSchemaService> logger)
{
_scopeFactory = scopeFactory;
_logger = logger;
}
/// <summary>
/// 为新租户创建 Schema
/// </summary>
public async Task CreateTenantSchemaAsync(string tenantId)
{
using var scope = _scopeFactory.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<SchemaMultiTenantDbContext>();
var schema = $"tenant_{tenantId}";
// 创建 Schema
await dbContext.Database.ExecuteSqlRawAsync($"CREATE SCHEMA IF NOT EXISTS \"{schema}\"");
// 应用迁移
await dbContext.Database.MigrateAsync();
_logger.LogInformation("租户 {TenantId} 的 Schema 创建完成", tenantId);
}
/// <summary>
/// 删除租户 Schema
/// </summary>
public async Task DropTenantSchemaAsync(string tenantId)
{
using var scope = _scopeFactory.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<SchemaMultiTenantDbContext>();
var schema = $"tenant_{tenantId}";
await dbContext.Database.ExecuteSqlRawAsync($"DROP SCHEMA IF EXISTS \"{schema}\" CASCADE");
_logger.LogInformation("租户 {TenantId} 的 Schema 已删除", tenantId);
}
}
五、租户上下文
5.1 ITenantContext 接口
/// <summary>
/// 租户上下文接口
/// </summary>
public interface ITenantContext
{
/// <summary>
/// 当前租户Id
/// </summary>
string TenantId { get; }
/// <summary>
/// 当前租户信息
/// </summary>
TenantInfo TenantInfo { get; }
/// <summary>
/// 是否已识别租户
/// </summary>
bool IsResolved { get; }
}
/// <summary>
/// 租户信息
/// </summary>
public class TenantInfo
{
public string TenantId { get; set; }
public string TenantName { get; set; }
public string ConnectionString { get; set; }
public string Schema { get; set; }
public TenantPlan Plan { get; set; }
public bool IsActive { get; set; }
public DateTime ExpireTime { get; set; }
public Dictionary<string, string> ExtraProperties { get; set; }
}
public enum TenantPlan
{
Free, // 免费版
Basic, // 基础版
Pro, // 专业版
Enterprise // 企业版
}
5.2 租户上下文实现
/// <summary>
/// 租户上下文实现
/// </summary>
public class TenantContext : ITenantContext
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly ITenantStore _tenantStore;
private TenantInfo _tenantInfo;
public TenantContext(
IHttpContextAccessor httpContextAccessor,
ITenantStore tenantStore)
{
_httpContextAccessor = httpContextAccessor;
_tenantStore = tenantStore;
}
public string TenantId
{
get
{
var httpContext = _httpContextAccessor.HttpContext;
return httpContext?.Items["TenantId"]?.ToString();
}
}
public TenantInfo TenantInfo
{
get
{
if (_tenantInfo == null && !string.IsNullOrEmpty(TenantId))
{
_tenantInfo = _tenantStore.GetById(TenantId);
}
return _tenantInfo;
}
}
public bool IsResolved => !string.IsNullOrEmpty(TenantId);
}
5.3 租户中间件
/// <summary>
/// 租户识别中间件
/// </summary>
public class TenantMiddleware
{
private readonly RequestDelegate _next;
public TenantMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context, ITenantResolver tenantResolver, ITenantStore tenantStore)
{
try
{
// 解析租户Id
var tenantId = await tenantResolver.ResolveAsync(context);
// 验证租户是否存在且有效
var tenant = await tenantStore.GetByIdAsync(tenantId);
if (tenant == null)
{
context.Response.StatusCode = 404;
await context.Response.WriteAsJsonAsync(new { Message = $"租户 {tenantId} 不存在" });
return;
}
if (!tenant.IsActive)
{
context.Response.StatusCode = 403;
await context.Response.WriteAsJsonAsync(new { Message = "租户已被禁用" });
return;
}
if (tenant.ExpireTime < DateTime.Now)
{
context.Response.StatusCode = 403;
await context.Response.WriteAsJsonAsync(new { Message = "租户已过期" });
return;
}
// 将租户信息存入 HttpContext
context.Items["TenantId"] = tenantId;
context.Items["TenantInfo"] = tenant;
}
catch (TenantNotFoundException)
{
context.Response.StatusCode = 400;
await context.Response.WriteAsJsonAsync(new { Message = "无法识别租户" });
return;
}
await _next(context);
}
}
// 注册中间件
// app.UseMiddleware<TenantMiddleware>();
六、租户配置管理
6.1 租户存储接口
/// <summary>
/// 租户存储接口
/// </summary>
public interface ITenantStore
{
Task<TenantInfo> GetByIdAsync(string tenantId);
TenantInfo GetById(string tenantId);
Task<TenantInfo> FindByDomainAsync(string domain);
Task<List<TenantInfo>> GetAllAsync();
Task CreateAsync(TenantInfo tenant);
Task UpdateAsync(TenantInfo tenant);
Task DeleteAsync(string tenantId);
}
6.2 数据库租户存储
/// <summary>
/// 基于数据库的租户存储
/// </summary>
public class DbTenantStore : ITenantStore
{
private readonly MasterDbContext _masterDb;
private readonly IMemoryCache _cache;
public DbTenantStore(MasterDbContext masterDb, IMemoryCache cache)
{
_masterDb = masterDb;
_cache = cache;
}
public async Task<TenantInfo> GetByIdAsync(string tenantId)
{
return await _cache.GetOrCreateAsync($"tenant:{tenantId}", async entry =>
{
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10);
var tenant = await _masterDb.Tenants
.FirstOrDefaultAsync(t => t.TenantId == tenantId);
return tenant?.ToTenantInfo();
});
}
public TenantInfo GetById(string tenantId)
{
return _cache.GetOrCreate($"tenant:{tenantId}", entry =>
{
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10);
var tenant = _masterDb.Tenants
.FirstOrDefault(t => t.TenantId == tenantId);
return tenant?.ToTenantInfo();
});
}
public async Task<TenantInfo> FindByDomainAsync(string domain)
{
var tenant = await _masterDb.Tenants
.FirstOrDefaultAsync(t => t.Domain == domain);
return tenant?.ToTenantInfo();
}
public async Task<List<TenantInfo>> GetAllAsync()
{
var tenants = await _masterDb.Tenants.ToListAsync();
return tenants.Select(t => t.ToTenantInfo()).ToList();
}
public async Task CreateAsync(TenantInfo tenant)
{
_masterDb.Tenants.Add(new TenantEntity
{
TenantId = tenant.TenantId,
TenantName = tenant.TenantName,
ConnectionString = tenant.ConnectionString,
IsActive = true,
Plan = tenant.Plan,
ExpireTime = tenant.ExpireTime
});
await _masterDb.SaveChangesAsync();
}
public async Task UpdateAsync(TenantInfo tenant)
{
var entity = await _masterDb.Tenants.FindAsync(tenant.TenantId);
if (entity != null)
{
entity.TenantName = tenant.TenantName;
entity.IsActive = tenant.IsActive;
entity.Plan = tenant.Plan;
entity.ExpireTime = tenant.ExpireTime;
await _masterDb.SaveChangesAsync();
// 清除缓存
_cache.Remove($"tenant:{tenant.TenantId}");
}
}
public async Task DeleteAsync(string tenantId)
{
var entity = await _masterDb.Tenants.FindAsync(tenantId);
if (entity != null)
{
_masterDb.Tenants.Remove(entity);
await _masterDb.SaveChangesAsync();
_cache.Remove($"tenant:{tenantId}");
}
}
}
6.3 租户管理 API
/// <summary>
/// 租户管理接口(仅平台管理员可用)
/// </summary>
[ApiDescriptionSettings("租户管理")]
public class TenantAppService : IDynamicApiController
{
private readonly ITenantStore _tenantStore;
private readonly TenantSchemaService _schemaService;
public TenantAppService(
ITenantStore tenantStore,
TenantSchemaService schemaService)
{
_tenantStore = tenantStore;
_schemaService = schemaService;
}
/// <summary>
/// 获取所有租户
/// </summary>
public async Task<List<TenantInfo>> GetAllTenants()
{
return await _tenantStore.GetAllAsync();
}
/// <summary>
/// 创建租户
/// </summary>
public async Task CreateTenant(CreateTenantInput input)
{
var tenant = new TenantInfo
{
TenantId = Guid.NewGuid().ToString("N"),
TenantName = input.TenantName,
Plan = input.Plan,
IsActive = true,
ExpireTime = DateTime.Now.AddYears(1),
ConnectionString = GenerateConnectionString(input)
};
// 创建租户记录
await _tenantStore.CreateAsync(tenant);
// 创建租户数据库/Schema
await _schemaService.CreateTenantSchemaAsync(tenant.TenantId);
}
/// <summary>
/// 禁用租户
/// </summary>
public async Task DisableTenant(string tenantId)
{
var tenant = await _tenantStore.GetByIdAsync(tenantId);
if (tenant != null)
{
tenant.IsActive = false;
await _tenantStore.UpdateAsync(tenant);
}
}
private string GenerateConnectionString(CreateTenantInput input)
{
return $"Server=localhost;Database=tenant_{input.TenantName};Trusted_Connection=True;";
}
}
七、租户数据迁移
7.1 租户数据库初始化
/// <summary>
/// 租户数据库迁移服务
/// </summary>
public class TenantMigrationService
{
private readonly ITenantStore _tenantStore;
private readonly IServiceScopeFactory _scopeFactory;
private readonly ILogger<TenantMigrationService> _logger;
public TenantMigrationService(
ITenantStore tenantStore,
IServiceScopeFactory scopeFactory,
ILogger<TenantMigrationService> logger)
{
_tenantStore = tenantStore;
_scopeFactory = scopeFactory;
_logger = logger;
}
/// <summary>
/// 迁移所有租户数据库
/// </summary>
public async Task MigrateAllTenantsAsync()
{
var tenants = await _tenantStore.GetAllAsync();
foreach (var tenant in tenants.Where(t => t.IsActive))
{
try
{
await MigrateTenantAsync(tenant.TenantId);
_logger.LogInformation("租户 {TenantId} 数据库迁移成功", tenant.TenantId);
}
catch (Exception ex)
{
_logger.LogError(ex, "租户 {TenantId} 数据库迁移失败", tenant.TenantId);
}
}
}
/// <summary>
/// 迁移指定租户数据库
/// </summary>
public async Task MigrateTenantAsync(string tenantId)
{
using var scope = _scopeFactory.CreateScope();
// 设置租户上下文
var httpContext = scope.ServiceProvider.GetRequiredService<IHttpContextAccessor>().HttpContext;
if (httpContext != null)
{
httpContext.Items["TenantId"] = tenantId;
}
var dbContext = scope.ServiceProvider.GetRequiredService<TenantDbContext>();
await dbContext.Database.MigrateAsync();
}
/// <summary>
/// 为新租户初始化种子数据
/// </summary>
public async Task SeedTenantDataAsync(string tenantId)
{
using var scope = _scopeFactory.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<TenantDbContext>();
// 创建默认管理员
dbContext.Users.Add(new User
{
UserName = "admin",
RealName = "管理员",
Email = "admin@tenant.com",
Status = 1,
TenantId = tenantId,
CreatedTime = DateTime.Now
});
// 创建默认角色
// ...
await dbContext.SaveChangesAsync();
}
}
7.2 应用启动时自动迁移
// Program.cs
var app = builder.Build();
// 应用启动时迁移所有租户数据库
using (var scope = app.Services.CreateScope())
{
var migrationService = scope.ServiceProvider.GetRequiredService<TenantMigrationService>();
await migrationService.MigrateAllTenantsAsync();
}
app.Run();
八、多租户与权限结合
8.1 租户级别的权限控制
/// <summary>
/// 租户权限检查过滤器
/// </summary>
public class TenantPermissionFilter : IAsyncAuthorizationFilter
{
private readonly ITenantContext _tenantContext;
public TenantPermissionFilter(ITenantContext tenantContext)
{
_tenantContext = tenantContext;
}
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
var tenantInfo = _tenantContext.TenantInfo;
if (tenantInfo == null)
{
context.Result = new UnauthorizedResult();
return;
}
// 检查租户套餐是否允许访问当前功能
var endpoint = context.HttpContext.GetEndpoint();
var requiredPlan = endpoint?.Metadata.GetMetadata<RequirePlanAttribute>();
if (requiredPlan != null && tenantInfo.Plan < requiredPlan.MinPlan)
{
context.Result = new ObjectResult(new
{
Message = $"当前套餐不支持此功能,请升级到 {requiredPlan.MinPlan} 以上"
})
{
StatusCode = 403
};
}
await Task.CompletedTask;
}
}
/// <summary>
/// 套餐要求特性
/// </summary>
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
public class RequirePlanAttribute : Attribute
{
public TenantPlan MinPlan { get; }
public RequirePlanAttribute(TenantPlan minPlan)
{
MinPlan = minPlan;
}
}
// 使用示例
[ApiDescriptionSettings("高级功能")]
public class AdvancedAppService : IDynamicApiController
{
/// <summary>
/// 数据分析(仅专业版以上可用)
/// </summary>
[RequirePlan(TenantPlan.Pro)]
public async Task<AnalyticsResult> GetAnalytics()
{
// 仅 Pro 和 Enterprise 可访问
return await Task.FromResult(new AnalyticsResult());
}
/// <summary>
/// 自定义报表(仅企业版可用)
/// </summary>
[RequirePlan(TenantPlan.Enterprise)]
public async Task<ReportResult> GetCustomReport()
{
// 仅 Enterprise 可访问
return await Task.FromResult(new ReportResult());
}
}
8.2 数据权限与租户结合
/// <summary>
/// 租户数据权限服务
/// </summary>
public class TenantDataPermissionService
{
private readonly ITenantContext _tenantContext;
private readonly IRepository<User> _userRepository;
public TenantDataPermissionService(
ITenantContext tenantContext,
IRepository<User> userRepository)
{
_tenantContext = tenantContext;
_userRepository = userRepository;
}
/// <summary>
/// 获取当前租户的用户列表
/// 全局过滤器会自动添加 TenantId 条件
/// </summary>
public async Task<List<User>> GetTenantUsers()
{
// 由于全局过滤器,这里只会返回当前租户的数据
return await _userRepository.AsQueryable().ToListAsync();
}
/// <summary>
/// 跨租户查询(仅平台管理员使用)
/// </summary>
public async Task<List<User>> GetAllTenantsUsers()
{
// 忽略全局过滤器
return await _userRepository.AsQueryable()
.IgnoreQueryFilters()
.ToListAsync();
}
}
九、SaaS 平台实践
9.1 完整的多租户架构示例
/// <summary>
/// SaaS 平台启动配置
/// </summary>
public static class SaaSStartup
{
public static void ConfigureMultiTenant(this WebApplicationBuilder builder)
{
// 1. 注册租户服务
builder.Services.AddScoped<ITenantResolver, CompositeTenantResolver>();
builder.Services.AddScoped<ITenantContext, TenantContext>();
builder.Services.AddSingleton<ITenantStore, DbTenantStore>();
// 2. 注册主数据库(管理租户信息)
builder.Services.AddDbContext<MasterDbContext>(options =>
{
options.UseSqlServer(builder.Configuration.GetConnectionString("Master"));
});
// 3. 注册租户数据库
builder.Services.AddDbContext<TenantDbContext>((sp, options) =>
{
var tenantContext = sp.GetRequiredService<ITenantContext>();
if (tenantContext.IsResolved)
{
var connStr = tenantContext.TenantInfo?.ConnectionString;
if (!string.IsNullOrEmpty(connStr))
{
options.UseSqlServer(connStr);
}
}
});
// 4. 注册迁移服务
builder.Services.AddScoped<TenantMigrationService>();
builder.Services.AddScoped<TenantSchemaService>();
}
public static void UseMultiTenant(this WebApplication app)
{
app.UseMiddleware<TenantMiddleware>();
}
}
9.2 租户资源配额管理
/// <summary>
/// 租户资源配额
/// </summary>
public class TenantQuota
{
public string TenantId { get; set; }
public int MaxUsers { get; set; } // 最大用户数
public long MaxStorageMb { get; set; } // 最大存储空间(MB)
public int MaxApiCallsPerDay { get; set; } // 每日最大API调用次数
}
/// <summary>
/// 配额检查服务
/// </summary>
public class QuotaCheckService
{
private readonly ITenantContext _tenantContext;
private readonly IRepository<User> _userRepository;
private readonly IMemoryCache _cache;
public QuotaCheckService(
ITenantContext tenantContext,
IRepository<User> userRepository,
IMemoryCache cache)
{
_tenantContext = tenantContext;
_userRepository = userRepository;
_cache = cache;
}
/// <summary>
/// 检查用户数量配额
/// </summary>
public async Task<bool> CheckUserQuotaAsync()
{
var quota = GetQuotaByPlan(_tenantContext.TenantInfo.Plan);
var currentUserCount = await _userRepository.CountAsync();
return currentUserCount < quota.MaxUsers;
}
/// <summary>
/// 检查API调用频率
/// </summary>
public bool CheckApiRateLimit()
{
var quota = GetQuotaByPlan(_tenantContext.TenantInfo.Plan);
var cacheKey = $"api:count:{_tenantContext.TenantId}:{DateTime.Today:yyyyMMdd}";
var currentCount = _cache.GetOrCreate(cacheKey, entry =>
{
entry.AbsoluteExpiration = DateTime.Today.AddDays(1);
return 0;
});
if (currentCount >= quota.MaxApiCallsPerDay)
{
return false;
}
_cache.Set(cacheKey, currentCount + 1);
return true;
}
private TenantQuota GetQuotaByPlan(TenantPlan plan)
{
return plan switch
{
TenantPlan.Free => new TenantQuota
{ MaxUsers = 5, MaxStorageMb = 100, MaxApiCallsPerDay = 1000 },
TenantPlan.Basic => new TenantQuota
{ MaxUsers = 50, MaxStorageMb = 1024, MaxApiCallsPerDay = 10000 },
TenantPlan.Pro => new TenantQuota
{ MaxUsers = 500, MaxStorageMb = 10240, MaxApiCallsPerDay = 100000 },
TenantPlan.Enterprise => new TenantQuota
{ MaxUsers = int.MaxValue, MaxStorageMb = 102400, MaxApiCallsPerDay = int.MaxValue },
_ => new TenantQuota
{ MaxUsers = 5, MaxStorageMb = 100, MaxApiCallsPerDay = 1000 }
};
}
}
十、性能考虑
10.1 多租户性能优化策略
| 策略 | 说明 | 适用场景 |
|---|---|---|
| 租户信息缓存 | 缓存租户配置,减少数据库查询 | 所有方案 |
| 连接池管理 | 合理配置数据库连接池大小 | 独立数据库方案 |
| 查询优化 | TenantId 列添加索引 | 行级隔离方案 |
| 读写分离 | 大租户使用读写分离 | 高并发场景 |
| 分库分表 | 超大租户数据分片 | 海量数据场景 |
| CDN 加速 | 静态资源使用 CDN | 所有方案 |
10.2 数据库索引优化
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// 为 TenantId 创建索引
modelBuilder.Entity<User>()
.HasIndex(u => u.TenantId)
.HasDatabaseName("IX_Users_TenantId");
// 复合索引
modelBuilder.Entity<User>()
.HasIndex(u => new { u.TenantId, u.Status })
.HasDatabaseName("IX_Users_TenantId_Status");
modelBuilder.Entity<Order>()
.HasIndex(o => new { o.TenantId, o.CreatedTime })
.HasDatabaseName("IX_Orders_TenantId_CreatedTime");
}
10.3 连接管理优化
/// <summary>
/// 租户数据库连接池管理
/// </summary>
public class TenantConnectionManager
{
private readonly ConcurrentDictionary<string, DbContextOptions<TenantDbContext>> _optionsCache = new();
public DbContextOptions<TenantDbContext> GetOrCreateOptions(string tenantId, string connectionString)
{
return _optionsCache.GetOrAdd(tenantId, _ =>
{
var optionsBuilder = new DbContextOptionsBuilder<TenantDbContext>();
optionsBuilder.UseSqlServer(connectionString, sqlOptions =>
{
sqlOptions.EnableRetryOnFailure(3); // 自动重试
sqlOptions.CommandTimeout(30); // 命令超时
});
return optionsBuilder.Options;
});
}
/// <summary>
/// 移除租户连接配置(用于租户删除或配置变更时)
/// </summary>
public void RemoveOptions(string tenantId)
{
_optionsCache.TryRemove(tenantId, out _);
}
}
10.4 方案选择建议
| 因素 | 独立数据库 | Schema 隔离 | 行级隔离 |
|---|---|---|---|
| 租户数量 < 100 | ✅ 推荐 | ✅ 可选 | ✅ 可选 |
| 租户数量 100-1000 | ⚠️ 成本高 | ✅ 推荐 | ✅ 推荐 |
| 租户数量 > 1000 | ❌ 不建议 | ⚠️ 管理复杂 | ✅ 推荐 |
| 数据安全性要求极高 | ✅ 推荐 | ✅ 可选 | ⚠️ 需额外措施 |
| 需要独立备份 | ✅ 推荐 | ⚠️ 较复杂 | ❌ 困难 |
| 运维成本敏感 | ❌ 高 | ⚠️ 中 | ✅ 低 |
多租户架构的选择应根据业务规模、安全要求、运维能力和成本预算综合考虑。Furion 框架提供了灵活的多租户支持,可以根据实际需求选择最适合的方案。在实际项目中,建议从行级隔离方案开始,随着业务增长和需求变化逐步升级到更高级别的隔离方案。