Skip to content

第十六章:多租戶架構 (Multi-Tenancy)

16.1 引言:SaaS 的基石

多租戶 (Multi-Tenancy) 是 SaaS (Software as a Service) 應用程式的核心架構模式。它允許單一應用程式實例服務多個客戶 (租戶),同時確保每個租戶的資料完全隔離。

1. 為什麼需要多租戶?

  • 成本效益:共享基礎設施,降低每個客戶的成本。
  • 運維簡化:只需維護一套程式碼與部署。
  • 快速擴展:新增客戶不需要重新部署。

2. 挑戰

  • 資料隔離:絕對不能讓租戶 A 看到租戶 B 的資料。
  • 效能隔離:租戶 A 的大量請求不應影響租戶 B。
  • 客製化:不同租戶可能需要不同的功能或配置。

16.2 租戶隔離策略

ABP 支援三種主流的隔離策略。

1. 共享資料庫 + 共享 Schema (Shared Database)

所有租戶的資料存在同一個資料庫的同一個 Schema 中,透過 TenantId 欄位區分。

  • 優點:最簡單、成本最低。
  • 缺點:隔離性最差、單點故障、難以符合某些合規要求 (如 GDPR)。
  • 適用:初創 SaaS、租戶數量少 (<100)。
csharp
public class Book : FullAuditedAggregateRoot<Guid>, IMultiTenant
{
    public Guid? TenantId { get; set; } // ABP 自動管理
    public string Name { get; set; }
}

2. 共享資料庫 + 獨立 Schema (Shared Database, Separate Schemas)

所有租戶共享一個資料庫伺服器,但每個租戶有自己的 Schema。

  • 優點:較好的隔離性、備份與還原更靈活。
  • 缺點:並非所有資料庫都良好支援 (PostgreSQL 支援較好)。
  • 適用:中型 SaaS (100-1000 租戶)。

3. 獨立資料庫 (Database per Tenant)

每個租戶有自己的資料庫 (甚至可以在不同的伺服器上)。

  • 優點:最高隔離性、符合嚴格的合規要求、可針對大客戶單獨優化。
  • 缺點:運維複雜度高、成本高。
  • 適用:企業級 SaaS、高價值客戶。

16.3 啟用多租戶

1. 配置 Module

在 Domain 層的 Module 中啟用。

csharp
public override void ConfigureServices(ServiceConfigurationContext context)
{
    Configure<AbpMultiTenancyOptions>(options =>
    {
        options.IsEnabled = true;
    });
}

2. 租戶解析 (Tenant Resolvers)

ABP 需要知道當前請求屬於哪個租戶。預設支援多種解析方式:

  • Domain Resolver: tenant1.myapp.com, tenant2.myapp.com
  • Header Resolver: HTTP Header __tenantX-Tenant-Id
  • Cookie Resolver: Cookie 中的 __tenant
  • Query String: ?__tenant=tenant1

您可以自訂解析優先順序:

csharp
Configure<AbpTenantResolveOptions>(options =>
{
    options.TenantResolvers.Clear();
    options.TenantResolvers.Add(new DomainTenantResolveContributor());
    options.TenantResolvers.Add(new HeaderTenantResolveContributor());
});

16.4 實作租戶隔離

1. 自動資料過濾

當實體實作 IMultiTenant 介面時,ABP 會自動在查詢時加上 TenantId 過濾。

csharp
// 查詢時,ABP 自動加上 WHERE TenantId = @CurrentTenantId
var books = await _bookRepository.GetListAsync();

2. 手動切換租戶 (用於後台任務或測試)

csharp
using (_currentTenant.Change(tenantId))
{
    // 在這個 scope 內,所有操作都屬於指定的租戶
    var books = await _bookRepository.GetListAsync();
}

3. 存取宿主資料 (Host Data)

某些資料是跨租戶共享的 (如系統設定)。這些實體不應實作 IMultiTenant

csharp
public class SystemSetting : Entity<Guid>
{
    // 沒有 TenantId,所有租戶共享
    public string Key { get; set; }
    public string Value { get; set; }
}

16.5 獨立資料庫模式

1. 配置租戶連線字串

在租戶建立時,儲存其專屬的連線字串。

csharp
public async Task CreateTenantAsync(string name, string connectionString)
{
    var tenant = new Tenant(GuidGenerator.Create(), name);

    // 設定租戶專屬連線字串
    tenant.SetConnectionString(connectionString);

    await _tenantRepository.InsertAsync(tenant);
}

2. 動態連線字串解析

ABP 會自動根據當前租戶載入對應的連線字串。

3. 資料庫遷移

每個租戶的資料庫都需要執行 Migration。

csharp
public class TenantDatabaseMigrationService : ITransientDependency
{
    private readonly ITenantRepository _tenantRepository;
    private readonly IDbContextProvider<BookStoreDbContext> _dbContextProvider;

    public async Task MigrateAllTenantsAsync()
    {
        var tenants = await _tenantRepository.GetListAsync();

        foreach (var tenant in tenants)
        {
            using (_currentTenant.Change(tenant.Id))
            {
                var dbContext = await _dbContextProvider.GetDbContextAsync();
                await dbContext.Database.MigrateAsync();
            }
        }
    }
}

16.6 Feature Management (功能管理)

Feature Management 允許您為不同租戶啟用或停用特定功能,這是實現 SaaS 定價層級 (Pricing Tiers) 的關鍵。

1. 定義 Features

csharp
public class BookStoreFeatureDefinitionProvider : FeatureDefinitionProvider
{
    public override void Define(IFeatureDefinitionContext context)
    {
        var group = context.AddGroup("BookStore");

        group.AddFeature(
            "BookStore.MaxBookCount",
            defaultValue: "100",
            displayName: L("Feature:MaxBookCount"),
            valueType: new FreeTextStringValueType(new NumericValueValidator(0, 1000000))
        );

        group.AddFeature(
            "BookStore.AdvancedReporting",
            defaultValue: "false",
            displayName: L("Feature:AdvancedReporting"),
            valueType: new ToggleStringValueType()
        );
    }
}

2. 檢查 Feature

csharp
public class BookAppService : ApplicationService
{
    public async Task CreateAsync(CreateBookDto input)
    {
        // 檢查租戶是否有權限建立更多書籍
        var maxCount = await FeatureChecker.GetAsync<int>("BookStore.MaxBookCount");
        var currentCount = await _bookRepository.CountAsync();

        if (currentCount >= maxCount)
        {
            throw new BusinessException("BookStore:MaxBookCountExceeded");
        }

        // ...
    }

    [RequiresFeature("BookStore.AdvancedReporting")]
    public async Task<byte[]> GenerateAdvancedReportAsync()
    {
        // 只有啟用此 Feature 的租戶才能呼叫
    }
}

3. 設定租戶 Feature 值

csharp
await FeatureManager.SetForTenantAsync(
    tenantId,
    "BookStore.MaxBookCount",
    "500" // 升級為 Premium 方案,可建立 500 本書
);

16.7 實戰練習

練習 1:建立多租戶應用

  1. 啟用多租戶功能。
  2. 建立兩個租戶:TenantATenantB
  3. 分別以兩個租戶的身分建立書籍,驗證資料隔離。

練習 2:實作 Feature-based 定價

  1. 定義三個 Features:MaxUsers, AdvancedReporting, APIAccess
  2. 建立三個定價方案:
    • Basic: MaxUsers=10, 其他關閉。
    • Pro: MaxUsers=100, AdvancedReporting=開啟。
    • Enterprise: 全部開啟。
  3. 為不同租戶設定不同方案,並測試功能限制。

練習 3:獨立資料庫遷移

  1. 為一個租戶配置獨立的資料庫連線字串。
  2. 實作自動遷移腳本,在租戶建立時自動建立並初始化資料庫。

16.8 總結

多租戶架構是構建 SaaS 應用的核心。

  • 隔離策略 的選擇影響整個系統的架構。
  • Feature Management 讓您能靈活地為不同客戶提供差異化服務。
  • 自動化 (租戶建立、資料庫遷移) 是運維的關鍵。

在下一章,我們將探討 測試策略 (Testing Strategy),學習如何為多租戶應用撰寫可靠的測試。


參考資源

Released under the MIT License.