“软件多租户”指的是一种软件架构,一个软件实例在一个服务器上运行,但为多个租户服务。租户们对软件实例有通用的访问入口,可是每一个租户都有特定的权限。web
在多租户体系架构中,用程序旨在为每一个租户提供一个专用的实例共享,包括其数据*、配置、用户管理、租户我的功能和非功能属性。数据库
多租户与多实例体系结构造成对比,在多实例体系结构中,独立的软件实例表明不一样的租户操做”(维基百科)。缓存
简而言之,多租户是一种用于建立SaaS(软件即服务)应用程序的技术。服务器
有几种不一样的多租户数据库和部署方法:架构
这实际上不是多租户,可是若是咱们为每一个客户(租户)运行一个应用程序实例,并使用一个独立的数据库,咱们能够在一个服务器上为多个租户服务。咱们只须要确保应用程序的多个实例在同一个服务器环境中不会相互冲突。框架
对于已存在的但没有被设计为多租户的应用程序也是可能的。建立这样的应用程序可能更容易,由于不须要考虑多租户,可是会有安装,使用以及维护等各类问题。dom
在这种方法中,咱们在服务器上运行应用程序的单个实例。咱们有一个主(主机)数据库来存储租户元数据(好比租户名称和子域),每一个租户有一个单独的数据库。一旦咱们肯定了当前的租户(入从子域或者用户登陆提交的form),咱们就能够切换到该租户的数据库来执行操做。ide
在这种方法中,应用程序应该在某种程度上被设计为多租户,可是应用程序大部分能够独立于它。性能
咱们为每一个租户建立和维护一个单独的数据库,包括数据库迁移。若是咱们有许多拥有专用数据库的客户,那么在应用程序更新期间迁移数据库模式可能须要很长时间。因为每一个租户都有一个单独的数据库,所以能够将其数据库与其余租户分开备份。若是租户须要,咱们还能够将租户数据库移动到更强大的服务器。大数据
这是最理想的多租户体系结构:咱们只将应用程序的一个实例和一个数据库部署到一个服务器上。咱们在每一个表(对于RDBMS)中都有一个TenantId(或相似的)字段,用于将租户的数据与其余数据隔离开来。
这种类型的应用程序易于安装和维护,但建立起来比较困难。这是由于咱们必须防止租户读取或写入其余租户数据。咱们能够为每一个数据库读取(选择)操做添加一个TenantId过滤器。咱们也能够在每次写的时候检查看看这个实体是否与当前租户相关。这既乏味又容易出错。然而,ASP.NET Boilerplate帮助咱们在这里使用自动数据过滤。
若是咱们有许多具备大数据集的租户,这种方法可能存在性能问题。咱们可使用表分区或其余数据库特性来克服这个问题。
一般,咱们可能但愿将租户存储在单个数据库中,但可能但愿为所需的租户建立单独的数据库。例如,咱们能够将具备大数据的租户存储在本身的数据库中,但将全部其余租户存储在一个数据库中。
最后,为了得到更好的应用程序性能、高可用性和/或可伸缩性,咱们可能但愿将应用程序部署到多个服务器(好比web farm)。这与数据库方法无关。
ASP.NET Boilerplate可用于上面描述的全部场景。
框架默认是禁用多租户的,咱们能够在模块的预初始(PreInitialize)方法中启用它,以下图所示:
Configuration.MultiTenancy.IsEnabled = true;
注意:在ASP.NET Core 和 ASP.NET MVC 5.x 启动模板中都支持多租户。
咱们定义了在多租户系统中使用的两个术语:
租户:客户拥有本身的用户、角色、权限、设置……并使用与其余租户彻底隔离的应用程序。多租户应用程序将有一个或多个租户。若是这是一个CRM应用程序,不一样的租户都有本身的账户、联系人、产品和订单。因此当咱们说“租户用户”时,咱们指的是租户拥有的用户。
主机:主机是单例的(只有一个主机)。主机负责建立和管理租户。“主机用户”处于更高级别,独立于全部租户,能够控制它们。
ASP.NET Boilerplate定义了IAbpSession接口来获取当前用户和租户id,此接口用于多租户在默认状况下获取当前租户的id。所以,它能够根据当前租户的id过滤数据。规则以下:
因为全部租户都使用相同的应用程序,咱们应该有一种方法来区分当前请求的租户。默认会话实现(ClaimsAbpSession)使用不一样的方法查找与当前请求相关的租户,顺序以下:
若是这些尝试都不能解析TenantId,那么当前请求者被认为是主机。租户解析器是可扩展的。您能够向Configuration.MultiTenancy.Resolvers collection中添加解析器,或删除现有的解析器。
出于性能缘由,在同一请求期间缓存已解析的租户id。解析器在请求中执行一次,且仅在当前用户还没有登陆时执行。
DomainTenantResolveContributer使用ITenantStore根据租户名称查找租户id。ITenantStore的默认实现是NullTenantStore,它不包含任何租户,对于查询返回null。您能够实现并替换它来查询来自任何数据源的租户。
对于多租户单数据库方法,咱们必须添加一个TenantId过滤器,以便在从数据库检索实体时只获取当前租户的实体。若是你的实体实现IMustHaveTenant 和 IMayHaveTenant任何一个接口,ASP.NET Boilerplate会自动的帮你作这个。
这个接口经过定义TenantId属性来区分不一样租户的实体。实现IMustHaveTenant的示例实体:
public class Product : Entity, IMustHaveTenant { public int TenantId { get; set; } public string Name { get; set; } //...other properties }
这样,ASP.NET Boilerplate知道这是一个特定租户的实体,并自动将租户的实体与其余租户隔离开来。
咱们可能须要在主机和租户之间共享一个实体类型。所以,实体可能由租户或主机拥有。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接口,由于维护主机和租户共享的代码比较困难。
当您将实体类型定义为IMustHaveTenant或IMayHaveTenant时,老是在建立新实体时设置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确保咱们正在处理给定租户的数据,独立于数据库体系结构:
若是咱们不使用SetTenantId,它会从会话中获取tenantId。这里有一些指导方针和最佳实践: