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)使用不同的方法,找到与给定的顺序当前请求相关的租户:
- 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.
- 如果用户已经登录,然后tenantid从目前的声明。声明的名字是http://www.aspnetboilerplate.com/identity/claims/tenantid应该包含一个整型值。如果在声明中没有找到,那么用户被假定为主机用户。
- 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):
- 如果用户没有登录,然后尝试解决tenantid从租户解决提供者。有3个预定义的租户提供者并按给定的顺序运行(第一个成功的解析器获胜): 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解决。
- HttpHeaderTenantResolveContributer: Tries to resolve TenantId from "Abp.TenantId" header value, if present (This is a constant defined in Abp.MultiTenancy.MultiTenancyConsts.TenantIdResolveKey).
- HttpCookieTenantResolveContributer: Tries to resolve TenantId from "Abp.TenantId" cookie value, if present (uses the same constant explained above).
-
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运行你的代码。