1.5 多租户

多租户

  • 什么是多租户?

    • 数据库和部署架构

      • 多部署-多数据库
      • 单部署-多数据库
      • 单部署-单数据库
      • 单部署-混合数据库
      • 多部署-单/多/混合数据库
  • ABP的多租户
    • 启用多租户
    • 租主和租户
    • 会话
    • 决定当前租户
      • 租户仓库
    • 数据过滤器
      • IMustHaveTenant Interface
      • IMayHaveTenant Interface
      • 附加注意事项
    • 切换租主与租户

什么是多租户?

  “软件多租户技术指的是一种软件架构,这种架构可以使用软件的单实例运行并为多个租户提供服务。租户是通过软件实例的特定权限共享通用访问的一组用户。使用多租户架构,软件应用为每个租户提供实例的专用共享,包括实例的数据、配置、用户管理、租户的私有功能和非功能属性。多租户与多实例架构形成对比,将软件实例的行为根据不同的租户分割开来。”(维基百科)

  简单来说,多租户是一种创建SaaS(软件即服务)应用的技术。

数据库和部署架构

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

多个部署,多个数据库

  这种实际上不是多租户的。但是,如果我们为每个客户使用分离的数据库运行应用的一个实例,可以在一个单独的服务器上为多租户提供服务。我们只要保证在同样的服务器环境下,应用的多个实例之间不会相互冲突。

  这种情况同样适用于已经存在并没有设计为多租户的应用。创建这种对多租户没有感知的应用是容易的。但是使用这种方法需要安装、使用和维护众多的问题。

单个部署,多个数据库

  在这种方法里,在服务器上运行应用的单独实例。使用一个主数据库存储租户的元数据(如租户名称和子域),每个租户使用隔离的数据库。一旦我们呢识别出当前的租户(例如从一个子域或登录表单),然后就切换到当前租户的数据库执行操作。

  在这种方法里,应用程序需要在一定程度上设计成多租户的。但是应用的大部分仍然与多租户是不相干的。

  我们需要为每个租户创建和维护一个分离的数据库,包括数据库迁移。如果我们有许多专用数据库的客户,在应用升级时将会花费我们很长的时间迁移数据库模式。因为我们为租户分离了数据库,我们可以备份租户的数据库而不受其他租户的影响。如果租户需要的话,我们也可以把租户的数据库引动到一个更强劲的服务器上。

单个部署,单个数据库

  这种是最真实的多租户架构:我们只需要部署在一个单独服务器上部署应用的一个实例。我们在每个表里都有一个TenantId(或者相似的)字段用来区分租户间的数据。

  这种是最易安装和维护的。但是这种应用很难创建。因为,我们必须禁止一个租户读写另一个租户的数据。我们可以天剑TenantId字段为每个数据库的读操作。如果这个实体和当前租户相关,我们可以每次写都检查。这样是冗长乏味且容易犯错误的。ABP使用自动的数据过滤来帮助我们实现。

  如果有许多租户且数据庞大的话会造成性能问题。我们可以使用表分区或其他数据库特征克服这个问题。

单个部署,混合数据库

  我们想正常的在单独数据库中存储租户数据,但是也想为希望使用单独数据库的租户创建单独数据库。例如,我们把有大数据的租户存储到他们自己的数据库中,其他的租户存储在单独的一个数据库中。

多个部署,单个/多个/混合数据库

  最后,我们想讲应用部署在多个服务器(如web farms)上以获得更好的性能、高可用和可扩展性。这是独立于数据库方式的。

ABP的多租户

  ABP可以以上面描述的任何场景方式工作

设置多租户可用

  多租户默认是不可用的。我们可以在模块的PreInitialize方法按如下的方式使其可用:

Configuration.MultiTenancy.IsEnabled = true; 

租主和租户

  首先,需要定义在多租户系统中的两个术语:

  • 租户:拥有自己的用户,角色,权限,设置......完全独立于其他租户使用应用程序。多租户应用有一个或多个租户。如果是一个CRM应用,不同的租户拥有他们自己的账户,联系人,产品和订单。所以当我们说一个“租户用户”的时候,指的是租户拥有的一个用户。
  • 租主:租主是单例的(there is a single host)。租主负责创建和管理租户。所以,一个“租主用户”是高等级的和独立于其他所有的租户并且可以控制他们。

会话

  ABP定义了IabpSession接口获取当前用户和租户的ids。在多租户系统中,默认使用这个接口来获取当前租户的id。因此,它可以基于当前租户的id来过滤数据。我们有以下规则:

  • 如果UserId和TenantId都是null,当前用户没有登录到系统。所以,我们不知道当前用户是一个租主用户还是一个租户用户。在这种情况下,用户不能方位授权内容。
  • 如果UserId不为null,TenantId为null,然后我们可以知道当前用户是一个租主用户。
  • 如果UserId不为null,TenantId也不为null,我们可以知道当前用户是一个租户用户。
  • 如果UserId为null,TenantId不为null,意味着我们可以知道当前为租户,但当前请求没有授权(用户没有登录)。参见下一部分了解如何决定当前租户。

  参见session documention章节了解更多关于session的信息。

决定当前租户

  既然所有的租户用户都使用同样的应用,我们应该有区分当前请求是哪个租户的方法。默认的会话实现按照下面给定的顺序使用不同的方法查找和当前请求相关的租户:

  1. 如果用户已经登录则从当前的声明中获取TenantId。声明名称为http://www.aspnetboilerplate.com/identity/claims/tenantId 应该会包含一个整型值。如果没在声明中找到则假定当前用户是主人用户。
  2. 如果用户没有登录,ABP会尝试从租户解决贡献者中解决TenantId的问题你。有三个预定义的租户贡献者,以规定的顺序运行(第一个成功的解决者将会胜利):
    1. DomainTenantResolveContributer:尝试从URL中获取租户名称,通常是从域名或子域名。可以再模块的PreInitialize方法中配置域名形式(如 Configuration.Modules.AbpWebCommon().MultiTenancy.DomainFormat = "{0}.mydomain.com")。如果域名形式是“{0}.mydomain.com”且请求的当前主机是acme.mydomain.com,name租户名称就是“acme”。然后下一步就是根据租户名查询ItenantStore找到TenantId。如果找到了租户,就作为当前的TenantId。
    2. 2.      HttpHeaderTenantResolveContributer:尝试从”Abp.TenantId”数据头中解决,如果存在的话(这是定义在Abp.MultiTenancy.MultiTenancyConsts.TenantIdResolveKey中的一个常数)。
    3. HttpCookieTenantResolveContributer:尝试从”Abp.TenantId“cookie值中解决,如果存在的话(使用上面解释的相同的常量)。

  如果以上所有的尝试都没有解决TenantId,当前的请求者会被认为一个主人。租户解决者是可以扩展的。可以向 Configuration.MultiTenancy.Resolvers 集合中添加解决者,也可以从中移除解决者。

  最后一个关于解决者的事情是:为了提升性能,在相同的请求中解决的租户id是被缓存的。所以,解决这在请求中只会被执行一次(且只有当前用户没有登录的时候)。

租户仓库

  DomainTenantResolveContributer使用ItenantStore通过租户名称查找租户idItenantStore默认实现是NullTenantStore,它不包含任何的租户,查询的时候返回null。可以重新实现这个接口以便从任何数据源查询租户。Module Zero实现方式是从他的租户管理中获取租户。

数据过滤器

  对于多租户单数据库的方法,必须添加一个TenantId过滤器,这样从数据库里提取数据的时候只获取当前租户的实体。当实体实现了IMustHaveTenant和IMayHaveTenant两个接口中的任意一个接口时,ABP会自动过滤。

IMustHaveTenant 接口

  这个接口使用定义的TenantId属性区分不同租户的实体。实现IMustHaveTenant接口的实例如下:

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

    public string Name { get; set; }

    //...other properties
}

  这样,ABP知道这是一个租户特定的实体,自动和其他租户的实体隔离开。

IMayHaveTenant 接口

  我们可能需要在租户和主人之间共享一个实体类型。所以,一个实体可能属于一个租户或主人。ImayHaveTenant接口也定义了TenantId(和ImustHaveTenant相似),但是子在这种情况下它是nullable。实现了此接口的示例如下:
public class Role : Entity, IMayHaveTenant
{
    public int? TenantId { get; set; }

    public string RoleName { get; set; }

    //...other properties
}
  我们可能会使用相同的角色类存储主人角色和租户角色。在这种情况下,TenantId属性会判定当前实体是主人实体还是租户实体。Null意味着是租主实体,non-null值意味着这个实体属于租户,当前值即为TenantId。

附加说明

  ImayHaveTenant并不如ImustHaveTenant通用。例如,产品类不能继承ImayHaveTenant,因为产品与实际应用功能关联,不予管理租户关联。所以,小心使用ImayHaveTenant接口,因为很难维护被主人和租户共享的代码。

  当定义ImustHaveTenant或ImayHaveTenant的实体类型时,当创建一个新实体时(当ABP尝试从当前TenantId设置它时,在某些情况下是不可能的,尤其是对于ImayHaveTenant实体)最好总是设置TenantId。大多数的时候,这是处理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方法确保我们在给定的租户数据上工作,独立于数据库架构:

  • 如果给定的租户有一个专有的数据库,它会切换到这个数据库并且从中获取产品。
  • 如果给定的租户没有专有的数据库(单数据库方式,例如),它会自动添加TenantId过滤器以只查询那个租户的产品。

  如果我们不使用SetTenantId,它会从会话中获取tenantid,如之前所说。这有一些建议和最佳实践:

  • 使用SetTenantId(null)切换到主人。
  • 如果不是在特殊情况下,在using块中使用SetTenantId。这样,ABP会自动在using块结束时恢复tenantid,GetProducts方法可以和以前一样工作。
  • 如果需要的话可以在嵌套块中使用SetTenantId。
  • 既然_unitOfWorkManager.Current只能在工作单元中使用,确保代码在UOW中运行。

返回主目录

时间: 2024-11-03 21:50:24

1.5 多租户的相关文章

&lt;&lt;ABP文档 - 框架&gt;&gt; 1.5 多租户

文档目录 本节内容: 什么是多租户 多部署 - 多数据库 单部署 - 多数据库 单部署 - 单数据库 单部署 - 混数据库 多部署 - 单/多/混 数据库 ABP中的多租户 启用多租户 宿主与租户 会话 数据过滤 IMustHaveTenant 接口 IMayHaveTenant 接口 补充提醒 在宿主与租户间切换 什么是多租户 维基百科:“软件多租户是一个软件架构,软件只有一个实例运行在服务器,并服务于多个租户.一个租户包含一组用户,他们拥有指定权限,共同访问一个软件实例.一个多租户架构,应用

&lt;&lt;ABP文档 - 框架&gt;&gt; 1.4 启动配置

文档目录 本节内容: 配置ABP 替换内置服务 配置模块 为一个模块创建配置 ABP在启动时,提供基础框架和模型来配置和模块化. 配置ABP 在预初始化事件中进行配置,示例: public class SimpleTaskSystemModule : AbpModule { public override void PreInitialize() { //为你的应用添加语言 Configuration.Localization.Languages.Add(new LanguageInfo("en

&lt;&lt;ABP文档 - 框架&gt;&gt; 1.3 模块系统

文档目录 本节内容: 简介 模块定义 生命周期方法 PreInitialize(预初始化) Initialize(初始化) PostInitialize(提交初始化) Shutdown(关闭) 模块依赖 插件模块 Asp.net Core Asp.net Mvc,Web Api 插件中的控制器 附加程序集 自定义模块方法 模块配置 模块生命期 简介 ABP为创建模块及组织它们提供基础框架.一个模块可依赖于另一个模块.通常地,一个程序集做为一个模块.如果你的应用是多个程序集,建议为每个程序集定义一

&lt;&lt;ABP文档&gt;&gt; 通知系统

文档目录 本节内容: 简介 发送模式 通知类型 通知数据 通知重要性 关于通知持久化 订阅通知 发布通知 用户通知管理器 实时通知 客户端 通知存储 通知定义 简介 通知用来告知用户系统里特定的事件发生了,ABP提供一个发布/订阅,它基于实时通知基础框架. 发送模式 有两种方式可以发送通知给用户: 用户订阅一个特定的通知类型,然后我们发布一个此类型的通知,它会分发给所有订阅的用户,这就是发布/订阅模式. 我们可以直接发送一个通知给目标用户(users). 通知类型 有两种通知类型: 一般通知:任

&lt;&lt;ABP文档&gt;&gt; 审计日志

文档目录 本节内容: 简介 关于 IAuditingStore 配置 通过特性启用/禁用 注意 简介 维基百科:“一个审计追踪(也叫审计日志)是一个安全相关的时序记录.记录组.和/或记录源和目标,作为任何时候一个特殊操作带来影响的一序列活动的书面文件”. ABP提供一个基础框架来自动记录所有与应用的交互,它能记录有意的方法调用和调用者信息与参数. 基本上,保存的字段有:相关的租户id,调用者id,被调用的服务名(被调用方法的类名),被调用的方法名,执行参数(序列化成Json),执行时间,执行时长

&lt;&lt;ABP文档&gt;&gt; EntityFramework 集成

文档目录 本节内容: Nuget 包 DbContext 仓储 默认仓储 自定义仓储 特定的仓储基类 自定义仓储示例 仓储最佳实践 ABP可使用任何ORM框架,它已经内置了EntityFrame(以下简称EF),这个文档将解释如何在ABP里使用EF,我们假设你对EF已经有初步的了解. Nuget 包 在ABP里使用EF的Nuget包是Abp.EntityFramework,你应该把它加入到你的应用里,最好在你项目里单独建立一个EF程序集(dll),然后依赖该于这个包. DbContext 如你所

&lt;&lt;ABP文档&gt;&gt; 导航

文档目录 本节内容: 创建菜单 注册导航供应器 显示菜单 每个web应用都有一些菜单用来在页面/屏幕之间导航,ABP提供了一个通用的基础框架创建并显示菜单给用户. 创建菜单 一个应用可能由不同模块组成,每个模块可以有它自己的菜单项,为了定义菜单项,我们需要创建一个继承于NavigationProvider的类. 假设有一个如下所示的主菜单: Tasks Reports Administration User management Role management 这里,Administration

&lt;&lt;ABP文档&gt;&gt; 异常处理

文档目录 本节内容: 简介 启用错误处理 非AJAX请求 显示异常 UserFriendlyException Error 模型 AJAX 请求 异常事件 简介 这个方法针对Asp.net Mvc和Web Api,如果你对Asp.net Core感兴趣,请看Asp.net Core文档. 在一个Web应用里,异常通常在Mvc控制器的Action或Web Api 控制器的Action里被处理,当一个异常姓时,应用的用户会通过某种方式收到错误信息和错误的可能原因. 如果一个错误发生在平常的HTTP请

ABP文档 - 对象与对象之间的映射

文档目录 本节内容: 简介 IObjectMapper 接口 集成 AutoMapper 安装 创建映射 自动映射的特性 自定义映射 扩展方法 MapTo 单元测试 预定义的映射 LocalizableString -> string 注入 IMapper 简介 把一个对象映射到另一个相似的对象很常见,两个对象(类)具有相似或相同的属性,它们之间要互相映射,其实这项工作重复且无聊,考虑一个典型的应用服务方法,如下: public class UserAppService : Applicatio

&lt;&lt;ABP文档&gt;&gt; 嵌入的资源文件

文档目录 本节内容: 简介 创建嵌入的文件 暴露嵌入的文件 使用嵌入的文件 简介 一个web应用里,客户端包含javascript,css,xml等文件,这此文件被添加到一个web项目后,发布成独立的文件,有时,我们需要把一些这样的文件打包入一个程序集(一个类库项目,一个Dll文件),并且作为嵌入式资源文件部署在这个程序集里,ABP提供了一个基础架构,方便地处理这件事. 创建嵌入的文件 我们首先要创建一个资源文件并把它标记为嵌入式资源,任何程序集都可以包含嵌入式资源文件,假设我们有一个名为“Ab