ABP框架系列之三十四:(Multi-Tenancy-多租戶)


What Is Multi Tenancy?

"Software Multitenancy refers to a software architecture in which a single instance of a software runs on a server and serves multiple tenants. A tenant is a group of users who share a common access with specific privileges to the software instance. With a multitenant architecture, a software application is designed to provide every tenant a dedicated share of the instance including its data, configuration, user management, tenant individual functionality and non-functional properties. Multitenancy contrasts with multi-instance architectures, where separate software instances operate on behalf of different tenants" (Wikipedia)

軟件多租戶是指一種軟件體系結構中的一個軟件的一個實例運行在服務器和多租戶服務。租戶是一組用戶,它們共享對軟件實例的特定權限的公共訪問。一個多租戶架構,一個應用軟件的設計是為了提供每個租戶專用的共享實例包括數據、配置、用戶管理、承租人個人的功能和非功能屬性。多實例多租戶架構的對比,在單獨的軟件實例操作對不同租戶代表

In brief, multi-tenancy is a technique that is used to create SaaS (Software as-a Service) applications.

總之,多租戶是一個用於創建SaaS(軟件為技術服務)的應用。

Database & Deployment Architectures(數據庫部署架構

There are some different multi-tenant database & deployment approaches:

有一些不同的多租戶數據庫和部署方法:

Multiple Deployment - Multiple Database(多重部署,多數據庫

This is not multi tenancy actually. But, if we run one instance of the application for each customer (tenant) with a seperated database, we can serve to multiple tenants in a single server. We just make sure that multiple instance of the application don't conflict with each other in same server environment.

This can be possible also for an existing application which is not designed as multitenant. It's easier to create such an application since the application has not aware of multitenancy. But there are setup, utilization and maintenance problems in this approach.

這不是多租戶實際上。但是,如果我們為一個單獨的數據庫為每個客戶(租戶)運行一個應用程序實例,那么我們可以在一個服務器上為多個租戶服務。我們只是確保應用程序的多個實例在同一服務器環境中不沖突。

這對於存在的不是設計成多租戶的應用時可行的,它更容易創建這樣的一個應用程序從應用沒有意識到多租戶。但是這種方法有設置、使用和維護問題。

Single Deployment - Multiple Database(單部署,多數據庫

ln this approach, we may run a single instance of the application in a server. We have a master (host) database to store tenant metadata (like tenant name and subdomain) and a seperated database for each tenant. Once we identify the current tenant (for example; from subdomain or from a user login form), then we can switch to that tenant's database to perform operations.

In this approach, application should be designed as multi-tenant in some level. But most of the application can remain independed from multi-tenancy.

We should create and maintain a seperated database for each tenant, this includes database migrations. If we have many customers with dedicated databases, it may take long time to migrate database schema in an application update. Since we have seperated database for a tenant, we can backup it's database seperately from other tenants. Also, we can move the tenant database to a stronger server if that tenant needs it.

在這種方法中,我們可以在服務器中運行應用程序的單個實例。我們有一個主(主機)數據庫來存儲租戶元數據(如租戶名稱和子域),每個租戶都有一個分離的數據庫。一旦我們識別當前的租戶(例如,從子域或用戶登錄表單),我們就可以切換到租戶的數據庫來執行操作。

在這種方法中,應用程序應該在一定級別上設計為多租戶。但是,大多數應用程序可以保持獨立的多租戶。

我們應該為每個租戶創建並維護一個獨立的數據庫,這包括數據庫遷移。如果我們有許多專用數據庫的客戶,在應用程序更新中遷移數據庫模式可能需要很長時間。既然我們已經分離數據庫的租客,我們可以備份是數據庫分開其他租戶。此外,如果租戶需要它,我們可以將租戶數據庫遷移到一個更強大的服務器上。

Single Deployment - Single Database(單部署,單數據庫

This is the most real multi-tenancy architecture: We only deploy single instance of the application with a single database into a single server. We have a TenantId (or similar) field in each table (for a RDBMS) which is used to isolate a tenant's data from others.

This is easy to setup and maintain. But harder to create such an application. Because, we must prevent a Tenant to read or write other tenant data. We may add TenantId filter for each database read (select) operation. Also, we may check it every write, if this entity is related to the current tenant. This is tedious and error-prone. But ASP.NET Boilerplate helps us here by using automatic data filtering.

This approach may have performance problems if we have many tenants with huge data. We may use table partitioning or other database features to overcome this problem.

這是最真實的多租戶體系結構:我們只使用單個數據庫將應用程序的單個實例部署到單個服務器中。我們有一個tenantid(或類似)的每個表中的字段(一個RDBMS)是用來從其他租戶的數據隔離。

這是易於安裝和維護。但更難創建這樣的應用程序。因為,我們必須阻止租戶讀取或寫入其他租戶數據。我們可以把每個數據庫的讀操作tenantid濾波器(選擇)。而且,如果這個實體與當前租戶相關,我們可以檢查它。這是繁瑣和容易出錯的。但ASP.NET的模板有助於我們這里采用自動數據濾波。

如果我們有大量數據巨大的租戶,這種方法可能會有性能問題。我們可以使用表分區或其他數據庫特性來克服這個問題。

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

We may want to store tenants in single databases normally, but want to create seperated databases for desired tenants. For example, we can store tenants with big data in their own databases, but store all other tenants in a single database.

我們可能希望將租戶存儲在單個數據庫中,但是要為想要的租戶創建分離的數據庫。例如,我們可以在自己的數據庫中存儲大數據的租戶,但將所有其他租戶存儲在一個數據庫中。

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

Finally, we may want to deploy our application to more than one server (like web farms) for a better application performance, high availability, and/or scalability. This is independent from the database approach.

最后,我們可能希望將應用程序部署到多個服務器(如Web服務器)上,以獲得更好的應用程序性能、高可用性和/或可伸縮性。這與數據庫方法無關。

Multi-Tenancy in ASP.NET Boilerplate(ABP框架中的多租戶

ASP.NET Boilerplate can work with all scenarios described above.

ASP.NET boilerplate 能工作上面描述的所有細節。

Enabling Multi Tenancy

Multi-tenancy is disabled by default. We can enable it in PreInitialize of our module as shown below:

默認情況下禁用多租戶。我們可以在我們的啟動模塊如下圖所示,使用它:

Configuration.MultiTenancy.IsEnabled = true; 

Host vs Tenant(主機與租戶

First, we should define two terms used in a multi-tenant system:

  • Tenant: A customer which have it's own users, roles, permissions, settings... and uses the application completely isolated from other tenants. A multi-tenant application will have one or more tenants. If this is a CRM application, different tenants have also thier own accounts, contacts, products and orders. So, when we say a 'tenant user', we mean a user owned by a tenant.
  • Host: Host is singleton (there is a single host). The Host is responsible to create and manage tenants. So, a 'host user' is higher level and independent from all tenants and can control they.
  • 首先,我們應該定義多租戶系統中使用的兩個術語:

    租戶:擁有自己的用戶、角色、權限、設置的客戶…並使用與其他租戶完全隔離的應用程序。多租戶應用程序將有一個或多個租戶。如果這是一個CRM應用程序,不同的租戶也有自己的帳戶、聯系人、產品和訂單。所以,當我們說“租戶用戶”時,我們指的是租戶擁有的用戶。
    主機:主機是單例(有一個主機)。主機負責創建和管理租戶。因此,“主機用戶”級別較高,獨立於所有租戶,並且可以控制它們。

Session

ASP.NET Boilerplate defines IAbpSession interface to obtain current user and tenant ids. This interface is used in multi-tenancy to get current tenant's id by default. Thus, it can filter data based on current tenant's id. We can say these rules:

ABP框架定義了IAbpSession接口獲得當前用戶和租戶的ID,這個接口在多租戶中默認獲得當前租戶的ID。因此,它能過濾數據根據當前的租戶ID。我們可以說下面這些規則:

  • If both of UserId and TenantId is null, then current user is not logged in to the system. So, we can not know if it's a host user or tenant user. In this case, user can not access to authorized content.
  • If UserId is not null and TenantId is null, then we can know that current user is a host user.
  • If UserId is not null and also TenantId is not null, we can know that current user is a tenant user.
  • If UserId is null but TenantId is not null, that means we can know the current tenant, but current request is not authorized (user did not login). See the next section to understand how current tenant is determined.
  • 如果兩userid和TenantId是無效的,那么當前用戶沒有登錄到系統中。因此,我們不能知道它是主機用戶還是租戶用戶。在這種情況下,用戶無法訪問授權內容。
    如果用戶名不為空,TenantId是空的,那么我們可以知道當前用戶是主機用戶。
    如果用戶名不為空,TenantId也不為空,我們可以知道當前用戶是租戶。
    如果用戶名為空但TenantId不為空,這意味着我們可以知道目前的租戶,但當前的請求沒有被授權(用戶沒有登錄)。請參見下一節了解當前租戶是如何確定的。

See session documentation for more information on the session.

Determining Current Tenant(確定當前的用戶

Since all tenant users use the same application, we should have a way of distinguishing the tenant of the current request. Default session implementation (ClaimsAbpSession) uses different approaches to find the tenant related to the current request with the given order:

由於所有租戶用戶都使用相同的應用程序,所以我們應該有一種方法來區分當前請求的租戶。默認會話實現(claimsabpsession)使用不同的方法,找到與給定的順序當前請求相關的租戶:

    1. If user has logged in, then gets TenantId from current claims. Claim name is http://www.aspnetboilerplate.com/identity/claims/tenantId and should contain an integer value. If it's not found in claims then the user is assumed as a host user.
    2. 如果用戶已經登錄,然后tenantid從目前的聲明。聲明的名字是http://www.aspnetboilerplate.com/identity/claims/tenantid應該包含一個整型值。如果在聲明中沒有找到,那么用戶被假定為主機用戶。
    3. If user has not logged in, then it tries to resolve TenantId from tenant resolve contributors. There are 3 pre-defined tenant contributors and runs in given order (first success resolver wins):
    4. 如果用戶沒有登錄,然后嘗試解決tenantid從租戶解決提供者。有3個預定義的租戶提供者並按給定的順序運行(第一個成功的解析器獲勝):
    5. DomainTenantResolveContributer: Tries to resolve tenancy name from url, generally from domain or subdomain. You can configure domain format in PreInitialize method of your module (like Configuration.Modules.AbpWebCommon().MultiTenancy.DomainFormat = "{0}.mydomain.com";). If domain format is "{0}.mydomain.com" and current host of the request is 

acme.mydomain.com

    , then tenancy name is resolved as "acme". Then next step is to query ITenantStore to find the TenantId by given tenancy name. If a tenant is found, then it's resolved as the current TenantId.
      DomainTenantResolveContributer:試圖從URL解決租賃的名稱,一般從域或子域。您可以配置域格式分發你的模塊的方法(如配置模塊。abpwebcommon()。multitenancy.domainformat =“{ 0 }。MyDomain .com”;)。如果域的格式是“{ 0 }。MyDomain .com”和請求的當前主機是acme.mydomain.com,然后租賃是解決“acme”。然后下一步就是要查詢itenantstore由租賃的名字找到tenantid。如果租戶被發現,那么它的當前tenantid解決。
    1. HttpHeaderTenantResolveContributer: Tries to resolve TenantId from "Abp.TenantId" header value, if present (This is a constant defined in Abp.MultiTenancy.MultiTenancyConsts.TenantIdResolveKey).
    2. HttpCookieTenantResolveContributer: Tries to resolve TenantId from "Abp.TenantId" cookie value, if present (uses the same constant explained above).
    3. HttpHeaderTenantResolveContributer:試圖解決TenantId從“ABP。tenantid”標頭值,如果存在的話(這是一個恆定的定義在ABP。租戶。multitenancyconsts。TenantIdResolveKey)。
      HttpCookieTenantResolveContributer:試圖解決TenantId從“ABP。tenantid”cookie的值,如果存在(使用相同的解釋以上)。

If none of these attemtps can resolve a TenantId, then current requester is considered as the host. Tenant resolvers are extensible. You can add resolvers to Configuration.MultiTenancy.Resolvers collection, or remove an existing resolver.

One last thing on resolvers is that; resolved tenant id is cached during the same request for performance reasons. So, resolvers are executed once in a request (and only if current user has not already logged in).

如果這些attemtps無人能解決TenantId,那么當前請求是主機。它是可擴展的租戶。您可以添加旋轉變壓器configuration.multitenancy.resolvers收集,或刪除一個現有的解析器。

最后一件事是,在解析器;解決租戶ID緩存性能的原因相同的請求時。所以,解析器在請求執行一次(如果當前用戶尚未登錄)。

Tenant Store(租戶存儲

DomainTenantResolveContributer uses ITenantStore to find tenant id by tenancy name. Default implementation of ITenantStore is NullTenantStore which does not contain any tenant and returns null for queries. You can implement and replace it to query tenants from any data source. Module zero properly implements it to get from it's tenant manager. So, if you are using module zero, don't care about the tenant store.

domaintenantresolvecontributer使用itenantstore通過租賃的名字找到租戶ID。對itenantstore默認實現nulltenantstore其中不含任何租戶和返回查詢。您可以實現並將它替換為任何數據源的查詢租戶。模塊0正確地從租戶管理器中獲取它。所以,如果您使用的是零模塊,請不要關心租戶存儲。

Data Filters

For multi tenant single database approach, we must add a TenantId filter to get only current tenant's entities while retrieving entities from database. ASP.NET Boilerplate automatically does it when you implement one of two interfaces for your entity: IMustHaveTenant and IMayHaveTenant.

多租戶單數據庫的方法,我們必須添加一個tenantid濾波器只得到當前租戶的實體而從數據庫檢索實體。ASP.NET樣板自動它當你實現一個接口的實體:imusthavetenant和imayhavetenant。

IMustHaveTenant Interface

This interface is used to distinguish entities of different tenants by defining TenantId property. An example entitiy that implements IMustHaveTenant:

這個接口是用來定義tenantid屬性區分不同租戶的實體。例實體實現imusthavetenant:

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

    public string Name { get; set; }

    //...other properties
}

Thus, ASP.NET Boilerplate knows that this is a tenant-specific entity and automatically isolates entities of a tenant from other tenants.

因此,ASP.NET樣板知道這是租戶的具體實體自動分離的其他租戶承租單位。

IMayHaveTenant interface

We may need to share an entity type between host and tenants. So, an entity may be owned by a tenant or the host. IMayHaveTenant interface also defines TenantId (similar to IMustHaveTenant), but nullable in this case. An example entitiy that implements IMayHaveTenant:

我們可能需要在主機和租戶之間共享一個實體類型。因此,一個實體可以由租戶或主機擁有。imayhavetenant接口還定義了tenantid(類似imusthavetenant),但在這種情況下可空。例實體實現imayhavetenant:

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

    public string RoleName { get; set; }

    //...other properties
}

We may use same Role class to store Host roles and Tenant roles. In this case, TenantId property says if this is an host entity or tenant entitiy. A null value means this is a host entity, a non-null value means this entity owned by a tenant which's Id is the TenantId.

我們可以使用相同的角色類來存儲主機角色和租戶角色。在這種情況下,tenantid物業說,如果這是一個主持單位或承租單元。空值意味着這是一個主機,一個非空的值意味着這個實體的租客的身份擁有的是tenantid。

Additional Notes(附加說明

IMayHaveTenant is not common as IMustHaveTenant. For example, a Product class can not be IMayHaveTenant since a Product is related to actual application functionality, not related to managing tenants. So, use IMayHaveTenant interface carefully since it's harder to maintain a code shared by host and tenants.

imayhavetenant是不常見的imusthavetenant。例如,一個產品不能imayhavetenant由於產品實際應用功能相關,管理租戶不相關。因此,使用imayhavetenant接口仔細因為很難保持代碼的主機和租戶共享。

When you define an entity type as IMustHaveTenant or IMayHaveTenant, always set TenantId when you create a new entity (While ASP.NET Boilerplate tries to set it from current TenantId, it may not be possible in some cases, especially for IMayHaveTenant entities). Most of times, this will be the only point you deal with TenantId properties. You don't need to explicitly write TenantId filter in Where conditions while writing LINQ, since it will be automatically filtered.

當你定義一個實體類型imusthavetenant或imayhavetenant,總是把TenantId當你創建一個新的實體(而ASP.NET樣板試圖將它從目前的TenantId,不可能在某些情況下,特別是imayhavetenant實體)。大多數時候,這將是唯一的一點你處理tenantid性質。你不需要明確地寫tenantid濾波器在條件而寫LINQ,因為它會自動過濾。

Switching Between Host and Tenants(在主機和租戶之間切換

While working on a multitenant application database, we should know the current tenant. By default, it's obtained from IAbpSession (as described before). We can change this behaviour and switch to other tenant's database. Example:

在一個多租戶應用數據庫,我們應該知道目前的租戶。By default, it's obtained from IAbpSession (as described before). 我們可以改變這種行為並切換到其他租戶的數據庫。例子:

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 ensures that we are working on given tenant data, independent from database architecture:

settenantid確保我們正在給租戶數據,獨立於數據庫的體系結構:

  • If given tenant has a dedicated database, it switches to that database and gets products from it.
  • If given tenant has not a dedicated database (single database approach, for example), it adds automatic TenantId filter to query get only that tenant's products.
  • 如果給定的租戶擁有專用數據庫,它將切換到該數據庫並從中獲取產品。
    如果承租人沒有專門的數據庫(例如單數據庫的方法,增加了自動過濾),tenantid查詢只有承租人的產品。

If we don't use SetTenantId, it gets tenantId from session, as said before. There are some notes and best practices here:

  • Use SetTenantId(null) to switch to the host.
  • Use SetTenantId within a using block as in the example if there is not a special case. Thus, it automatically restore tenantId at the end of the using block and the code calls GetProducts method works as before.
  • You can use SetTenantId in nested blocks if it's needed.
  • Since _unitOfWorkManager.Current only available in a unit of work, be sure that your code runs in a UOW.
  • 如果我們不使用settenantid,會話就tenantid,就像之前說的。這里有一些注釋和最佳實踐:

    使用settenantid(null)切換到主機。
    使用settenantid用塊為例內如果沒有特殊情況。因此,它會自動恢復tenantid在using塊端和代碼調用getProducts方法之前。
    你可以使用settenantid嵌套塊,如果需要的話。
    因為在一個工作單元只能_unitofworkmanager.current,確保在UOW運行你的代碼。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM