<<ABP文档 - 框架>> 1.5 多租户

文档目录

本节内容:

  • 什么是多租户

    • 多部署 - 多数据库
    • 单部署 - 多数据库
    • 单部署 - 单数据库
    • 单部署 - 混数据库
    • 多部署 - 单/多/混 数据库
  • ABP中的多租户
    • 启用多租户
    • 宿主与租户
    • 会话
    • 数据过滤
      • IMustHaveTenant 接口
      • IMayHaveTenant 接口
      • 补充提醒
    • 在宿主与租户间切换

什么是多租户

维基百科:“软件多租户是一个软件架构,软件只有一个实例运行在服务器,并服务于多个租户。一个租户包含一组用户,他们拥有指定权限,共同访问一个软件实例。一个多租户架构,应用程序为每个租户提供一个专属于他们的数据、配置、用户管理、租户特有的功能和属性。多租户架构而多实例框架抽象而成,多实例架构是把每个实例看成一个租户。“

多租户通常用来创建Saas(软件作为服务)应用(云计算)。多租户有多种架构:

多部署 - 多数据库

这种实际上不算多租户,但是,如果我们为每个客户(租户)运行应用的一个实例,并使用一个独立的数据库,那么我们就可以在一台服务器上为多个租户服务,我们只要确保应用的多个实例不要在一个服务器的环境下互相冲突就行。

为一个不是为多租户设计,但已经在运行的应用,提供了可能性。这种方式虽然使得创建一个不考虑多租户的应用相对容易,但在安装、使用和维护方面有些问题。

单部署 - 多数据库

用这种方式,我们在一个服务器上运行应用的单一实例,我们有一个主(宿主)数据库存储租户元数据(像租户名和子域),并为每个租户维护一个隔离的数据库。我们一旦识别当前租户(例如:从子域或从一个用户登录窗体),就切换到该租户的数据库里执行操作。

用这种方式,我们应该在设计应用时,在某些层面上设计成多租户,但应用的大部分还是不依赖于多租户。

我们应该为每个租户创建并维护一个隔离的数据库,包括数据迁移。如果我们有多租户就需要维护它们专有的数据库,在应用更新时,可能就需要花很长的时间进行数据库结构迁移。由于我们有租户的隔离的数据库,所以我们可以单独地备份各自的数据库,同样在租户要求下,我们也可以移动租户数据库到一个更强大的服务器。

单部署 - 单数据库

这是最纯粹的多租户架构:我们只在一台服务器上部署应用的单个实例和单个数据库。我们在每个表(关系型数据库)里用一个TenantId(租户Id或类似的)字段来区分隔离每个租户数据。

这种方式易于安装和维护,但难于创建这种应用,因为我们必须防止一个租户读或写其它租户数据。我们得为每次的数据库读取(select)操作添加TenantId来过滤。同样,我们也在每次写数据库时进行检查当前实体是否与当前租户相关,这就是乏味和易犯错的地方。但ABP自动使用数据过滤技术帮我们解决这些问题。

这种方式在有很多租户和大数据量的情况下,可能带来性能问题,我们可以使用表分区或其它数据库特性来解决这个问题。

单部署 - 混数据库

我们可能想存储租户数据到一个数据库,但想为有需要的租户创建单独的数据库。例如,我们可以存储租户的大数据到各自的数据库,但其它的都保存到另一数据库。

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

最后,我们可能想部署我们的应用到多个服务器(像分布式服务器集群)获得更好地性能、实用性和扩展性。这是一种依赖于数据库的方式。

ABP中的多租户

ABP可用于上述所描述的场景。

启用多租户

默认情况多租户是禁用的,我们可以在我们模块的PreInitialize(预初始化)里启用它,如下:

Configuration.MultiTenancy.IsEnabled = true; 

宿主与租户

首先我们要在多租户系统里定义两个术语:

  • Tenant(租户):一个客户,拥有多个用户、角色、许可、设置等,并要单独地使用这个应用。一个多租户应用可能有多个租户,每个租户有它自己的帐户、联系人、产品及其它。所以当我们说一个“Tenant user(租户用户)”,表示一个租户下的一个用户。
  • Host(宿主):宿主是单例的(就一个宿主),这个宿主负责创建和管理租户,所以“Host user(宿主用户)”拥有更高级别,不依赖于租户,并能控制租户。

会话(Session)

ABP定义了IAbpSession接口,用来获取user(用户)和tenant ids(租户Id)。该接口在多租户系统中默认情况下获取当前租户Id,因此它能基于租户Id过滤数据。有如下规则:

  • 如果用户Id和租户Id都为null,当前用户尚未登录到系统,所以我们不知道它是宿主用户还是租户用户。这种情况下,用户不能访问需要授权的内容。
  • 如果用户Id不为null,租户Id为null,我们就可以知道当前用户为宿主用户。
  • 如果用户id不为null,租户Id也不为null,我们就可以知道当前用户为租户用户。

查看会话文档获取更多相关信息。

数据过滤

在多租户单数据库方式里,我们必须添加一个TenantId(租户Id)过滤,从数据库中只获取当前租户的实体。当你的实体实现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(可空的)。如下所示,一个实体实现IMayHaveTenant:

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

    public string RoleName { get; set; }

    //...other properties
}

我们可以使用两样的role类来存储宿主角色和租户角色,在这种情况下,靠TenantId属性来区分是宿主实体还是租户实体。如果为null表示这是一个宿主实体,否则它就是一个租户实体,它的值就是租户Id。

补充提醒:

IMayHaveTenant没有IMustHaveTenant那么通用。例如:一个Product(产品)类不能是IMayHaveTenant,因为它跟应用功能切实相关的,而与租户的管理无关。所以在使用IMayHaveTenant接口时要格外小心,毕竟维护共享于宿主与租户的代码比较难。

当你定义一个实体类型为IMustHaveTenant或IMayHaveTenant后,在创建一个新实体时应该特意支设置TenantId(尽管ABP会尝试把当前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确保我们工作于给定的租户的数据,获取方式依数据库而定:

  • 如果给定的租户有特定的数据库,它切换到这个数据库,从中获取产品。
  • 如果给定的租户没有特定的数据库(例如:单数据库方式),它自动添加TenantId过滤到查询里,只获取给定租户的产品。

如果我们不使用SetTenantId,如前面所说,将从会话中获取TenantId。这里有些提醒和最佳实践:

  • 使用SetTenantId(null)可切换到宿主。
  • 如果没有特殊情况,要像示例那样,在using块里使用SetTenantId,因为它会在块后面自动还原TenantId的值,并且调用GetProducts方法的代码也会像调用前那样工作。
  • 如果有需要,你可以块里嵌套使用SetTenantId
  • 由于_unitOfWorkManger.Current仅在同一工作单元内可用,所以确保你的代码是运行在同一个工作单元内。
时间: 2024-10-12 07:25:16

<<ABP文档 - 框架>> 1.5 多租户的相关文章

&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