🚀 ABP vNext 多租戶開發實戰指南
🛠? 環境:.NET 8.0 + ABP vNext 8.1.5 (C# 11, EF Core 8)
📚 目錄
- 🚀 ABP vNext 多租戶開發實戰指南
- 🏠 一、什么是多租戶?
- 📦 二、ABP 多租戶的核心機制
- 🚀 三、快速上手:啟用多租戶支持
- 🔄 四、ICurrentTenant 用法詳解
- 🗄? 五、數據庫策略與配置建議
- 🐞 六、常見問題與解決方案
- 1. 🚫 無租戶上下文
- 2. 🧩 緩存污染
- 3. 🔗 鏈路追蹤
- 4. 🔄 多數據庫遷移批量處理
- ???🩹 七、健康檢查
- 🔗 八、流程圖:ABP 多租戶請求流程
- 🔚 九、總結
🏠 一、什么是多租戶?
多租戶 (Multi-Tenancy) 是一種軟件架構模式,使一個應用程序可為多個租戶服務,同時隔離各自數據。
常見的三種隔離方式:
隔離模型 | 說明 |
---|---|
🏢 單庫共享 | 所有租戶使用同一套表,通過 TenantId 區分 |
🗃? 單庫分表 | 每個租戶獨立一套表結構 |
🏛? 多數據庫 | 每個租戶單獨數據庫實例,隔離最強 |
📦 二、ABP 多租戶的核心機制
- 🧩 Tenant 實體:核心領域模型。
- 🔄 ICurrentTenant 接口:獲取/切換當前租戶上下文。
- 🛠? ITenantResolveContributor:自定義解析器,支持子域名、Header 等。
- 🔒 IDataFilter:自動為查詢加上 TenantId 過濾。
- 📦 模塊依賴:
[DependsOn(typeof(AbpTenantManagementDomainModule),typeof(AbpTenantManagementApplicationModule)
)]
public class MyAppModule : AbpModule { }
// IDataFilter 自動過濾 TenantId
var list = await _repository.Where(e => e.IsActive).ToListAsync();
💡? 小貼士:核心機制不只在數據層,還體現在中間件和租戶上下文控制。
🚀 三、快速上手:啟用多租戶支持
// Program.cs
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Volo.Abp;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Modularity;var builder = WebApplication.CreateBuilder(args);// 1. 啟用 ABP 與多租戶
builder.Services.AddAbp<MyAppModule>(options =>
{// ? 如需替換默認解析器,可在此處注入 CustomTenantResolver// options.Services.Replace(ServiceDescriptor.Singleton<ITenantResolveContributor, CustomTenantResolver>());
});// 2. 構建并初始化
var app = builder.Build();
await app.InitializeAsync();// 3. 安全中間件
app.UseHttpsRedirection();
app.UseHsts();// 4. 路由與多租戶
app.UseRouting();
app.UseMultiTenancy(); // 🛡? 必須在身份驗證/授權之前
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();// 5. 運行
await app.RunAsync();
// appsettings.json
{"MultiTenancy": {"IsEnabled": true},"ConnectionStrings": {"TenantDb": "Server=.;Database=Tenant_{TENANT_ID};User Id={{USER}};Password={{PASSWORD}};"},"AllowedTenants": ["tenant1-id","tenant2-id"]
}
// 自定義租戶解析器
using System.Text.RegularExpressions;
using Volo.Abp.MultiTenancy;
using Volo.Abp;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Configuration;public class CustomTenantResolver : ITenantResolveContributor
{public string Name => "CustomHeader";private readonly IDistributedCache _cache;private readonly IConfiguration _configuration;public CustomTenantResolver(IDistributedCache cache, IConfiguration configuration){_cache = cache;_configuration = configuration;}public async Task<string> ResolveAsync(ITenantResolveContext context){var header = context.HttpContext.Request.Headers["Tenant"];if (string.IsNullOrEmpty(header) || !IsValidTenant(header))throw new UserFriendlyException("? 無效租戶");return header;}private bool IsValidTenant(string header){// 簡單格式校驗if (header.Length > 36 || !Regex.IsMatch(header, @"^[0-9A-Za-z\-]+$"))return false;// 從緩存或配置讀取白名單var whitelist = _cache.GetOrCreate("TenantWhitelist", entry =>{entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);return _configuration.GetSection("AllowedTenants").Get<List<string>>();});return whitelist.Contains(header);}
}
// 在模塊中注冊解析器
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Modularity;[DependsOn(typeof(AbpTenantManagementDomainModule))]
public class MyAppModule : AbpModule
{public override void ConfigureServices(ServiceConfigurationContext context){context.Services.Replace(ServiceDescriptor.Singleton<ITenantResolveContributor, CustomTenantResolver>());}
}
💡? 小貼士:推薦結合 Header + 子域名解析,適配多端生產場景。
🔄 四、ICurrentTenant 用法詳解
using Volo.Abp.DependencyInjection;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Uow;public class MyService : ITransientDependency
{private readonly ICurrentTenant _currentTenant;public MyService(ICurrentTenant currentTenant) => _currentTenant = currentTenant;[UnitOfWork]public async Task DoSomethingAsync(){var tid = _currentTenant.Id;using (_currentTenant.Change(tid)){// 在特定租戶上下文中執行邏輯}}
}
💡? 小貼士:Id == null
表示主機環境;可配合 IUnitOfWork
在后臺任務中切換上下文。
🗄? 五、數據庫策略與配置建議
模式 | 說明 | 示例代碼 |
---|---|---|
🔖 單庫共享 | 通過 TenantId 分類 | context.Set<T>().Where(e => e.TenantId == _currentTenant.Id).ToListAsync(); |
🔑 多數據庫 | 每租戶動態連接切換 | services.Replace(ServiceDescriptor.Singleton<IConnectionStringResolver, MyConnResolver>()); |
// 自定義 ConnectionStringResolver
using Volo.Abp.DependencyInjection;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Data;
using Microsoft.Extensions.Configuration;public class MyConnResolver : DefaultConnectionStringResolver, ITransientDependency
{private readonly ICurrentTenant _currentTenant;private readonly IConfiguration _configuration;public MyConnResolver(IConfiguration configuration, ICurrentTenant currentTenant): base(configuration)=> (_configuration, _currentTenant) = (configuration, currentTenant);public override string Resolve(string name = null){var template = _configuration["ConnectionStrings:TenantDb"];var id = _currentTenant.Id?.ToString() ?? "Host";return template.Replace("{TENANT_ID}", id);}
}
💡? 小貼士:可將租戶列表緩存到 IDistributedCache
或內存中,避免頻繁訪問數據庫。
🐞 六、常見問題與解決方案
1. 🚫 無租戶上下文
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using Volo.Abp.DependencyInjection;
using Volo.Abp.MultiTenancy;public class TenantBackgroundService : BackgroundService, ITransientDependency
{private readonly ICurrentTenant _currentTenant;private readonly ITenantManager _tenantManager;private readonly IMyBusinessService _myBusinessService;public TenantBackgroundService(ICurrentTenant currentTenant,ITenantManager tenantManager,IMyBusinessService myBusinessService){_currentTenant = currentTenant;_tenantManager = tenantManager;_myBusinessService = myBusinessService;}protected override async Task ExecuteAsync(CancellationToken stoppingToken){var tenants = await _tenantManager.GetListAsync(stoppingToken);foreach (var tenant in tenants){if (stoppingToken.IsCancellationRequested)break;await using (_currentTenant.Change(tenant.Id)){await _myBusinessService.ProcessTenantDataAsync(stoppingToken);}}}
}
2. 🧩 緩存污染
var key = $"product:{_currentTenant.Id}:{id}";
3. 🔗 鏈路追蹤
builder.Services.AddOpenTelemetryTracing(b =>b.AddAspNetCoreInstrumentation().AddSqlClientInstrumentation().AddJaegerExporter()
);
4. 🔄 多數據庫遷移批量處理
var tenants = await tenantManager.GetListAsync();
foreach (var tenant in tenants)
{using (_currentTenant.Change(tenant.Id)){await databaseMigrationService.MigrateAsync();}
}
💡? 小貼士:建議將遷移操作作為獨立運維命令或 CI/CD 作業執行,避免在應用啟動時觸發。
???🩹 七、健康檢查
// 在 Program.cs 中
builder.Services.AddHealthChecks().AddDbContextCheck<MyDbContext>("TenantDb").AddUrlGroup(new Uri("https://your-app/health"), name: "AppEndpoint");app.MapHealthChecks("/health");
💡? 小貼士:結合 Prometheus/Grafana 定時監控,提前設置告警閾值。
🔗 八、流程圖:ABP 多租戶請求流程
🔚 九、總結
💡? 小貼士:
- 🏁 項目初期即明確隔離模型,避免后期大改架構。
- ? 上線前務必在主機和各租戶環境進行全鏈路測試,確保無遺漏。
- ?? 結合緩存、健康檢查與鏈路追蹤,可大幅提升多租戶系統性能與可觀察性。