多租户

什么是多租户?

“ 软件 多租户 指的是软件 架构 ,其中 单个实例 软件的服务器上运行,并提供 * 多个租户 * 。租户是一组谁分享特定权限的软件实例的共同接入用户。随着多租户架构,软件应用程序被设计成提供每个租户 * 实例的专用共享包括其数据 * ,配置,用户管理,承租人个别功能和非功能特性。多租户与多实例的架构,其中,独立的软件实例操作的对比代表不同的租户 “(维基百科

简而言之,多租户是一种用于创建SaaS (软件即服务)应用程序的技术。

数据库和部署架构

有一些不同的多租户数据库和部署方法:

多部署 - 多个数据库

实际上不是多方租用,但如果我们为每个客户(租户)运行一个 应用程序实例并使用单独的数据库,我们可以在一台服务器上多个租户提供服务。我们只需要确保应用程序的多个实例在同一服务器环境中不会 相互冲突

对于未设计为多租户现有应用程序这也是可能的由于应用程序不了解多租户,因此创建此类应用程序更容易。但是,这种方法存在设置,利用和维护问题。

单一部署 - 多个数据库

在这种方法中,我们在服务器上运行应用程序单个实例我们有一个(主机)数据库来存储租户元数据(如租户名称和子域)以及每个租户单独数据库一旦我们识别出当前的租户(例如,来自子域或用户登录表单),我们就可以切换到该租户的数据库来执行操作。

在这种方法中,应用程序应该在某种程度上被设计为多租户,但是大多数应用程序可以保持独立于它。

我们为每个租户创建和维护一个单独的数据库,包括数据库迁移如果我们有许多具有专用数据库的客户,则在应用程序更新期间迁移数据库模式可能需要很长时间。由于每个租户都有一个单独的数据库,因此我们可以将其数据库与其他租户分开备份如果租户需要,我们还可以 租户数据库移动到更强大的服务器。

单一部署 - 单个数据库

这是最理想的多租户架构:我们只将单个数据库单个应用程序实例部署 到 单个服务器上我们在每个表(对于RDBMS)中都有一个TenantId(或类似)字段,用于将租户的数据与其他人隔离。

这种类型的应用程序易于设置和维护,但更难创建。这是因为我们必须阻止租户阅读或写入其他租户数据。我们可以为每个数据库读取(选择)操作添加一个TenantId过滤器我们也可以在每次写作时检查它,看看这个实体是否与当前租户有关这很乏味且容易出错。但是,ASP.NET Boilerplate通过使用自动数据过滤来帮助我们

如果我们有许多拥有大量数据集的租户,这种方法可能会出现性能问题。我们可以使用表分区或其他数据库功能来解决此问题。

单一部署 - 混合数据库

我们可能希望将租户正常存储在单个数据库中,但可能希望为所需租户创建单独的数据库。例如,我们可以将拥有大数据的租户存储在他们自己的数据库中,但将所有其他租户存储在一个数据库中。

多部署 - 单/多/混合数据库

最后,我们可能希望将我们的应用程序部署到多个服务器(如Web场),以获得更好的应用程序性能,高可用性和/或可伸缩性。这与数据库方法无关。

ASP.NET Boilerplate中的多租户

ASP.NET Boilerplate可以与上述所有场景一起使用。

启用多租户

默认情况下,对于Framework级别禁用多租户。我们可以在我们模块的PreInitialize方法中启用它,如下所示:

Configuration.MultiTenancy.IsEnabled = true

注意: ASP.NET Core和ASP.NET MVC 5.x启动模板中都启用了多租户。

主持人与租户

我们定义了多租户系统中使用的两个术语:

会议

ASP.NET Boilerplate定义了IAbpSession接口以获取当前 用户租户 ID。此接口在多租户中用于默认获取当前租户的ID。因此,它可以根据当前租户的id过滤数据。以下是规则:

有关更多信息,请参阅会话文档

确定当前租户

由于所有租户用户使用相同的应用程序,我们应该有一种区分当前请求的租户的方法。默认会话实现(ClaimsAbpSession)使用不同的方法以此给定顺序查找与当前请求相关的租户:

  1. 如果用户已登录,则从当前声明中获取TenantId。声明名称为http://www.aspnetboilerplate.com/identity/claims/tenantId ,应包含整数值。如果在索赔中找不到,则假定用户是主机用户。
  2. 如果用户尚未登录,则会尝试从租户解析参与者解析TenantId 有3个预定义的租户贡献者,并按给定的顺序运行(第一个成功的解析器'胜利'):
    1. DomainTenantResolveContributer:尝试从网址解析租户名称,通常来自域或子域。您可以在模块的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(如果存在)。这使用了上面解释的相同常数。

如果这些尝试都不能解析TenantId,则当前请求者被视为主机。租户解析器是可扩展的。您可以将解析器添加到Configuration.MultiTenancy.Resolvers集合,或删除现有的解析程序。

解析器的最后一件事:由于性能原因,在同一请求期间缓存已解析的租户ID。解析器在请求中执行一次,并且仅在当前用户尚未登录时执行。

租户商店

DomainTenantResolveContributer使用ITenantStore以找到租户名租户ID。ITenantStore的默认实现NullTenantStore,它不包含任何租户并为查询返回null。您可以实现并替换它以从任何数据源查询租户。模块零通过从租户管理器获取它来正确实现它因此,如果您使用的是Module Zero,则无需担心租户商店。

数据过滤器

对于多租户单一数据库方法,我们必须添加 TenantId过滤器,以便在从数据库中检索实体时仅获取当前租户的实体当您为实体实现两个接口之一时,ASP.NET Boilerplate会自动执行此操作:IMustHaveTenantIMayHaveTenant

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
}

我们可以使用相同的Role类来存储Host角色和Tenant角色。在这种情况下,TenantId属性表示这是主机实体还是租户权利。一个值意味着这是一个主机实体,一个 非空值意味着该实体由拥有租户其中ID是TenantId

补充笔记

IMayHaveTenant并不像IMustHaveTenant那样常见。例如,Product类不能是IMayHaveTenant,因为Product与实际应用程序功能相关,而与管理租户无关。因此,请谨慎使用IMayHaveTenant接口,因为维护主机和租户共享的代码更加困难。

当您将实体类型定义为IMustHaveTenant或IMayHaveTenant时,请 始终在创建新实体时设置TenantId(虽然ASP.NET Boilerplate尝试从当前TenantId设置它,但在某些情况下可能无法实现,特别是对于IMayHaveTenant实体)。大多数情况下,这将是您处理TenantId属性的唯一方面。在编写LINQ时,您不需要在Where条件中显式写入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 这里有一些指导方针和最佳做法:

nidie.com.cn - 用心与你沟通