ABP之多租戶

「軟件多租戶」指的是一種軟件架構,一個軟件實例在一個服務器上運行,但爲多個租戶服務。租戶們對軟件實例有通用的訪問入口,可是每一個租戶都有特定的權限。web

在多租戶體系架構中,用程序旨在爲每一個租戶提供一個專用的實例共享,包括其數據*、配置、用戶管理、租戶我的功能和非功能屬性。數據庫

多租戶與多實例體系結構造成對比,在多實例體系結構中,獨立的軟件實例表明不一樣的租戶操做」(維基百科)。緩存

簡而言之,多租戶是一種用於建立SaaS(軟件即服務)應用程序的技術。服務器

 

數據庫和部署架構

有幾種不一樣的多租戶數據庫和部署方法:架構

多個部署-多個數據庫(Multiple Deployment - Multiple Database)

這實際上不是多租戶,可是若是咱們爲每一個客戶(租戶)運行一個應用程序實例,並使用一個獨立的數據庫,咱們能夠在一個服務器上爲多個租戶服務。咱們只須要確保應用程序的多個實例在同一個服務器環境中不會相互衝突。框架

對於已存在的但沒有被設計爲多租戶的應用程序也是可能的。建立這樣的應用程序可能更容易,由於不須要考慮多租戶,可是會有安裝,使用以及維護等各類問題。dom

單一部署-多個數據庫(Single Deployment - Multiple Database)

在這種方法中,咱們在服務器上運行應用程序的單個實例。咱們有一個主(主機)數據庫來存儲租戶元數據(好比租戶名稱和子域),每一個租戶有一個單獨的數據庫。一旦咱們肯定了當前的租戶(入從子域或者用戶登陸提交的form),咱們就能夠切換到該租戶的數據庫來執行操做。ide

在這種方法中,應用程序應該在某種程度上被設計爲多租戶,可是應用程序大部分能夠獨立於它。性能

 咱們爲每一個租戶建立和維護一個單獨的數據庫,包括數據庫遷移。若是咱們有許多擁有專用數據庫的客戶,那麼在應用程序更新期間遷移數據庫模式可能須要很長時間。因爲每一個租戶都有一個單獨的數據庫,所以能夠將其數據庫與其餘租戶分開備份。若是租戶須要,咱們還能夠將租戶數據庫移動到更強大的服務器。大數據

單一部署-單個數據庫(Single Deployment - Single Database)

這是最理想的多租戶體系結構:咱們只將應用程序的一個實例和一個數據庫部署到一個服務器上。咱們在每一個表(對於RDBMS)中都有一個TenantId(或相似的)字段,用於將租戶的數據與其餘數據隔離開來。

這種類型的應用程序易於安裝和維護,但建立起來比較困難。這是由於咱們必須防止租戶讀取或寫入其餘租戶數據。咱們能夠爲每一個數據庫讀取(選擇)操做添加一個TenantId過濾器。咱們也能夠在每次寫的時候檢查看看這個實體是否與當前租戶相關。這既乏味又容易出錯。然而,ASP.NET Boilerplate幫助咱們在這裏使用自動數據過濾。

若是咱們有許多具備大數據集的租戶,這種方法可能存在性能問題。咱們可使用表分區或其餘數據庫特性來克服這個問題。

單一部署-混合數據庫(Single Deployment - Hybrid Databases)

一般,咱們可能但願將租戶存儲在單個數據庫中,但可能但願爲所需的租戶建立單獨的數據庫。例如,咱們能夠將具備大數據的租戶存儲在本身的數據庫中,但將全部其餘租戶存儲在一個數據庫中。

多個部署——單個/多個/混合數據庫(Multiple Deployment - Single/Multiple/Hybrid Database)

最後,爲了得到更好的應用程序性能、高可用性和/或可伸縮性,咱們可能但願將應用程序部署到多個服務器(好比web farm)。這與數據庫方法無關。

ASP.NET Boilerplate中的多租戶

ASP.NET Boilerplate可用於上面描述的全部場景。

啓用多租戶

框架默認是禁用多租戶的,咱們能夠在模塊的預初始(PreInitialize)方法中啓用它,以下圖所示:

Configuration.MultiTenancy.IsEnabled = true; 

注意:在ASP.NET Core 和 ASP.NET MVC 5.x 啓動模板中都支持多租戶。

主機vs租戶(Host vs Tenant)

咱們定義了在多租戶系統中使用的兩個術語:

租戶:客戶擁有本身的用戶、角色、權限、設置……並使用與其餘租戶徹底隔離的應用程序。多租戶應用程序將有一個或多個租戶。若是這是一個CRM應用程序,不一樣的租戶都有本身的賬戶、聯繫人、產品和訂單。因此當咱們說「租戶用戶」時,咱們指的是租戶擁有的用戶。

主機:主機是單例的(只有一個主機)。主機負責建立和管理租戶。「主機用戶」處於更高級別,獨立於全部租戶,能夠控制它們。

會話(Session)

ASP.NET Boilerplate定義了IAbpSession接口來獲取當前用戶和租戶id,此接口用於多租戶在默認狀況下獲取當前租戶的id。所以,它能夠根據當前租戶的id過濾數據。規則以下:

  • 若是UserId 和 TenantId都是null,則當前用戶不會登陸到系統。咱們沒法知道它是主機用戶仍是租戶用戶。在這種狀況下,用戶沒法訪問受權內容。
  • 若是UserId不是null,而TenantId是null,則當前用戶是主機用戶。
  • 若是UserId不是null,而TenantId也不是null,則當前用戶是租戶用戶。
  • 若是UserId爲null,而TenantId不是null,這意味着咱們知道當前的租戶,可是當前的請求沒有被受權(用戶沒有登陸)。

肯定當前租戶

因爲全部租戶都使用相同的應用程序,咱們應該有一種方法來區分當前請求的租戶。默認會話實現(ClaimsAbpSession)使用不一樣的方法查找與當前請求相關的租戶,順序以下:

  1. 若是用戶已經登陸,它會從當前的claims得到TenantId。Claim名稱是 http://www.aspnetboilerplate.com/identity/claims/tenantId(tenantId通常是一個整數值),若是在claims中沒有找到,則認爲是主機用戶。
  2. 若是用戶還沒有登陸,那麼它將嘗試從租戶解析貢獻者解析TenantId。有3個預約義的租戶貢獻者,而且按照給定的順序運行(第一個成功的解析器'wins'): 
    1. DomainTenantResolveContributer:嘗試從url解析租賃名稱,一般是從域或子域解析。您能夠在模塊的預初始化(PreInitialize)方法中配置域格式(像Configuration.Modules.AbpWebCommon().MultiTenancy.DomainFormat = "{0}.mydomain.com";)。若是域格式是「{0}.mydomain.com」,好比請求的當前主機是acme.mydomain.com,則租戶名稱解析爲「acme」。下一步是查詢ITenantStore以根據給定的租戶名稱找到TenantId。若是找到了租戶,則將其解析爲當前的TenantId。
    2. HttpHeaderTenantResolveContributer:嘗試從「Abp.TenantId」標頭值(若是存在)解析TenantId。這是在Abp.MultiTenancy.MultiTenancyConsts.TenantIdResolveKey中定義的常數。
    3. HttpCookieTenantResolveContributer:嘗試從「Abp.TenantId」Cookie值(若是存在)解析TenantId。這是在Abp.MultiTenancy.MultiTenancyConsts.TenantIdResolveKey中定義的常數。

若是這些嘗試都不能解析TenantId,那麼當前請求者被認爲是主機。租戶解析器是可擴展的。您能夠向Configuration.MultiTenancy.Resolvers collection中添加解析器,或刪除現有的解析器。

出於性能緣由,在同一請求期間緩存已解析的租戶id。解析器在請求中執行一次,且僅在當前用戶還沒有登陸時執行。

租戶存儲

DomainTenantResolveContributer使用ITenantStore根據租戶名稱查找租戶id。ITenantStore的默認實現是NullTenantStore,它不包含任何租戶,對於查詢返回null。您能夠實現並替換它來查詢來自任何數據源的租戶。

數據過濾

對於多租戶單數據庫方法,咱們必須添加一個TenantId過濾器,以便在從數據庫檢索實體時只獲取當前租戶的實體。若是你的實體實現IMustHaveTenant IMayHaveTenant任何一個接口,ASP.NET Boilerplate會自動的幫你作這個。

IMustHaveTenant

這個接口經過定義TenantId屬性來區分不一樣租戶的實體。實現IMustHaveTenant的示例實體:

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

    public string Name { get; set; }

    //...other properties
}

這樣,ASP.NET Boilerplate知道這是一個特定租戶的實體,並自動將租戶的實體與其餘租戶隔離開來。

IMayHaveTenant

咱們可能須要在主機和租戶之間共享一個實體類型。所以,實體可能由租戶或主機擁有。IMayHaveTenant接口也定義了TenantId(相似於IMustHaveTenant),但在本例中它是可空的。實現IMayHaveTenant的示例實體:

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

    public string RoleName { get; set; }

    //...other properties
}

咱們可使用相同的角色類來存儲主機角色和租戶角色。在本例中,TenantId屬性表示這是主機實體仍是租戶實體。空值表示這是一個主機實體,非空值表示該實體由租戶全部,其中Id是TenantId。

還有,

IMayHaveTenant並不像IMustHaveTenant那樣經常使用。例如,產品類不能是IMayHaveTenant,由於產品與實際應用程序功能相關,而與管理租戶無關。因此要當心使用IMayHaveTenant接口,由於維護主機和租戶共享的代碼比較困難。

當您將實體類型定義爲IMustHaveTenantIMayHaveTenant時,老是在建立新實體時設置TenantId(ASP.NET Boilerplate 試圖從當前TenantId設置它,在某些狀況下可能不可能,特別是對於IMayHaveTenant實體)。大多數狀況下,這是處理TenantId屬性的唯一一點。在編寫LINQ時,不須要顯式地編寫TenantId過濾器,由於它是自動過濾的。

在主機和租戶之間切換

在處理多租戶應用程序數據庫時,咱們能夠得到當前的租戶。默認狀況下,它是從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,它會從會話中獲取tenantId。這裏有一些指導方針和最佳實踐:

  • 使用SetTenantId(null)切換到主機。
  • 若是沒有特殊狀況,在using塊中使用SetTenantId(如示例中所示)。經過這種方式,它在using塊的末尾自動恢復tenantId,調用GetProducts方法的代碼與前面同樣工做。
  • 若是須要,能夠在嵌套塊中使用SetTenantId。
  • 由於_unitOfWorkManager.Current只能在工做單元中可用,請確保您的代碼在UOW中運行。
相關文章
相關標籤/搜索