1. 程式人生 > >ABP官方文件(五)【多租戶】

ABP官方文件(五)【多租戶】

1.5 ABP總體介紹 - 多租戶

1.5.1 什麼是多租戶

維基百科:“軟體多租戶是指一個軟體架構的例項軟體執行在一個伺服器上,但存在多個租戶。租戶是一組共享一個公共的使用者訪問特定許可權的軟體例項。多租戶架構,軟體應用程式旨在提供每個租戶專用的例項包括資料、配置、使用者管理、租戶個體功能和非功能屬性。多租戶與多例項架構,獨立的軟體例項代表不同的租戶”操作多租戶一般用來建立SaaS(軟體即服務)應用程式(雲端計算),下面有一些案例:

1.5.2 多個部署多個數據庫

這實際上並不是多租戶,如果為每個客戶(租戶)配置一個單獨的資料庫和應用程式的一個例項,即在單個伺服器中部署但提供給多個客戶(租戶)使用,我們需要確保應用程式的多個例項不會因為系統相同的配置環境而發生衝突。

這種已有的設計方式也不是真正為多租戶服務的,它的好處是更容易的建立,但是存在一些安裝、使用和維護的問題。

1.5.3 單個部署多個數據庫

使用這種方式,我們可以在伺服器上執行應用程式的一個例項。我們有一個主資料庫用來儲存租戶的資料(例如:租戶名稱以及子域名)以及每個租戶的單個數據庫。一旦我們識別出當前租戶(例如:從子域名或者使用者登入的資訊來判定),那麼我們可以切換到當前租戶的資料庫來執行操作。

以這種方式設計出來的應用程式,在某種程度上可以被看做多租戶。但是大多數的應用仍然依賴於多租戶。

我們應該為每個租戶建立和維護它們自己單獨的資料庫,這包括資料遷移。如果我們有很多的客戶以及與之相應的資料庫,在更新應用程式的時候,那會花費太多的時間在資料庫架構的遷移上。當然這樣做,我們為租戶分離出了資料庫,我們可以為每個租戶備份它們自己的資料庫。如果租戶需要的話,我們可以將租戶的資料庫遷移到更強大的伺服器上。

1.5.4 單個部署單個數據庫

這是真正的多租戶構架,我們只在伺服器上部署應用程式的單一例項且只有一個數據庫。在各表中使用TenantId來隔離其它租戶的資訊。

這樣的好處是易於安裝和維護,但建立這樣的一個應用程式比較困難。因為,需要防止租戶讀寫其它租戶的資訊。在使用者讀取資料時候可以新增TenantId過濾器過濾資料,同樣,系統會檢測使用者的寫入操作。這是很繁瑣的,而且也容易出錯。ABP可以幫助我們自動資料過濾。

如果我們有很多租戶並且資料量巨大,那麼這種實現方式將會導致一些效能問題。我們可以使用表分割槽或者資料庫的其它功能來克服這些問題。

1.5.5 單部署混合資料庫

通常我們可能想儲存租戶到一個單獨的資料庫中,但是也可能想為租戶建立分離的資料庫。例如:我們可以為那些資料量巨大的租戶建立單獨的資料庫,但是其它租戶仍使用同一個資料庫。

1.5.6 多部署-單/多/混合資料庫

最後,為了達到更好的效能,高可用性以及伸縮性;我們可能想要部署我們的應用到多個伺服器上。這些都是依賴於資料庫的方式。

1.5.7 ABP中的多租戶

ABP可以工作於所有上面所描述的場景。

1. 開啟多租戶

預設多租戶是被禁用的,我們需要在模組的 PreInitialize 方法中開啟它,如下所示:

Configuration.MultiTenancy.IsEnabled = true;

2. Host VS 租戶

首先,我們先定義兩個多租戶系統中的術語:

  • 租戶:客戶有它自己的使用者,角色,許可權,設定…並使用應用程式與其他租戶完全隔離。多租戶應用程式將有一個或多個租戶。如果這是一個CRM應用程式,不同的租戶也他們自己的帳戶、聯絡人、產品和訂單。所以,當我們說一個租戶的使用者,我們的意思是使用者擁有的租戶。

  • Host: Host是單例的(只有唯一一個Host).Host負責建立和管理租戶。所以Host使用者獨立與租戶且可以控制租戶。

3. Session

ABP定義IAbpSession介面來獲取當前使用者和租戶id。這個介面中使用多租戶當前租戶的id。因此,它可以基於當前租戶的id過濾資料。

這裡有一些規則:

  • 如果兩個使用者id和TenantId是null,那麼當前使用者沒有登入到系統中。所以,我們不知道這是一個主機使用者或租戶的使用者。在這種情況下,使用者不能訪問授權的內容。

  • 使用者id(如果不為空,TenantId為空的,然後我們可以知道當前使用者是一個主機使用者。

  • 使用者id(如果不為空,TenantId也不為空,我們可以知道當前使用者是一個租戶的使用者。

有關更多的Session內容可檢視:Session

4. 當前租戶的斷定

由於所有的租戶使用者都是使用了相同的應用程式,我們應該有一種方式來區分當前請求的租戶。預設會話(ClaimsAbpSession)用給定的順序實現了使用不同的方式來找到當前請求相關的租戶:

  • 2. 如果使用者沒有登入,那麼它會嘗試從 tenant resolve contributors(暫翻譯為:租戶解析參與者) 中解析租戶ID。這裡有3種預定義的租戶參與者,並按照給定的順序執行(第一個解析成功的解析器獲勝):

    • 1. DomainTenantResolveContributer:嘗試從url中解析租戶名,通常來說是域名或者子域名。在模組的預初始化(PreInitialize)中可以配置域名格式(例如:Configuration.Modules.AbpWebCommon().MultiTenancy.DomainFormat = “{0}.mydomain.com”;)。如果域名的格式是 “{0}.mydomain.com”,並且當前請求的域名是:acme.mydomain.com,那麼租戶名被解析為 acme。那麼下一步就是通過 ITenantStore 用給定的租戶名來查詢租戶ID,如果租戶被發現,那麼該租戶ID就是當前租戶的ID。

    • 2. HttpHeaderTenantResolveContributer:如果存在 Abp.TenantId 請求頭(這個常量被定義在Abp.MultiTenancy.MultiTenancyConsts.TenantIdResolveKey),那麼嘗試從該請求頭中解析租戶ID。

    • 3. HttpCookieTenantResolveContributer:如果存在 Abp.TenantId 的cookie值(這個常量被定義在Abp.MultiTenancy.MultiTenancyConsts.TenantIdResolveKey),那麼就從該cookie中解析租戶ID。

如果上述方式都沒有解析得到租戶ID,那麼當前的請求會被考慮作為Host請求。租戶解析器是可擴充套件的。你可以新增解析器到集合:Configuration.MultiTenancy.Resolvers,或者移除某個存在的解析器。

關於解析租戶ID的最後一件事情是:為了效能優化,解析的租戶ID被快取在相同的請求中。所以,在同一個請求中解析僅被執行一次(當且僅當該使用者沒有登入)。

5. Tenant Store

DomainTenantResolveContributer 使用 ITenantStore 通過租戶名來查詢租戶ID。NullTenantStore 預設實現了 ITenantStore 介面,但是它不包含任何租戶,對於查詢僅僅返回null值。當你需要從資料來源中查詢時,你可以實現並替換它。在 Module ZeroTenant Manager 中已經實現了該擴充套件。所以,如果你使用了module zero,那麼你不需要關心tenant store。

6. 資料過濾

當我們從資料庫檢索實體,我們必須新增一個TenantId過濾當前的租戶實體。當你實現了介面:IMustHaveTenant或IMayHaveTenant中的一個時,ABP將自動完成資料過濾。

IMustHaveTenant Interface

這個介面通過TenantId屬性來區分不同的租戶的實體。示例:

public class Product : Entity, IMustHaveTenant
{
    public int TenantId { get; set; }

    public string Name { get; set; }

    //...其它屬性
}

因此,ABP能發現這是一個與租戶相關的實體,並自動隔離其它租戶的實體。

IMayHaveTenant interface

我們可能需要在Host和租戶之間共享實體型別。一個實體可能屬於租戶或Host,IMayHaveTenant介面還定義了TenantId(類似於IMustHaveTenant),但在這種情況下可以為空。示例如下:

public class Role : Entity, IMayHaveTenant
{
    public int? TenantId { get; set; }

    public string RoleName { get; set; }

    //...其它屬性
}

我們可以使用相同的角色類儲存主機角色和租戶的角色。在這種情況下,TenantId屬性會告訴我們這是一個Host實體還是一個租戶實體。null 值意味著這是一個 Host實體 ,一個 非空值 意味著這被一個租戶擁有,該租戶的Id是 TenantId

備註

IMayHaveTenant不像IMustHaveTenant一樣常用。比如,一個Product類可以不實現IMayHaveTenant介面,因為Product和實際的應用功能相關,和管理租戶不相干。因此,要小心使用IMayHaveTenant介面,因為它更難維護租戶和租主共享的程式碼。

當你定義一個實體型別實現了 IMustHaveTenant 或者 IMayHaveTenant 介面的時候;那麼在建立一個新實體的時候,你就需要設定 TenantId 的值,(ABP會嘗試把當前AbpSession的TenantId的值設定給它,但是在某些情況下這是不可能的,尤其是實現了IMayHaveTenant介面的實體)。在大多數時候,這是唯一一個地方你需要處理TenantI的地方,但是在其它對租戶資料過濾的時候,你不需要在寫Linq的where條件語句的時候明確指出TenantId,因為它會自動的實現過濾。

在Host和租戶之間的切換

當在多租戶應用資料庫上工作的時候,我們應該知道當前的租。預設獲取租戶ID的方式是從 IAbpSession 上獲取的。我們可以改變這個行為並且切換到其它租戶的資料庫上。例如:

public class ProductService : ITransientDependency
{
    private readonly IRepository<Product> _productRepository;
    private readonly IUnitOfWorkManager _unitOfWorkManager;

    public ProductService(IRepository<Product> productRepository, IUnitOfWorkManager unitOfWorkManager)
    {
        _productRepository = productRepository;
        _unitOfWorkManager = unitOfWorkManager;
    }

    [UnitOfWork]
    public virtual List<Product> GetProducts(int tenantId)
    {
        using (_unitOfWorkManager.Current.SetTenantId(tenantId))
        {
            return _productRepository.GetAllList();
        }
    }
}

SetTenantId 方法確保我們得到的資料是指定租戶的資料,這依賴於資料庫架構:

  • 如果給定的租戶有特定的資料庫,那麼切換到這個資料庫並且從該資料庫中取得產品資料

  • 如果給定的租戶沒有特定的資料庫(例如:單資料庫方式),它會自動的新增TenantId條件到查詢語句來過濾資料獲取指定的租戶的產品資料

如果我們沒有使用SetTenantId方法,它會從Session中取得租戶Id,如同之前所述。

這裡有一些關於最佳實踐的建議:

  • 使用 SetTenantId(null) 切換到Host

  • 如果沒有特別的原因,你應該像上面示例所展示的一樣,在using語句塊中使用SetTenantId方法。因為它會在using語句塊後且在 GetProducts 方法工作完成之前,自動的還原TenantId (也就是說using語句塊執行完後,TenantId是從Session中獲取的不會是來自於GetProducts的傳入引數)

  • 如果需要你可以巢狀使用SetTenantId方法

  • 因為 _unitOfWorkManager.Current 僅在工作單元中有效,請確保你的程式碼是在工作單元中執行