程序与技术分享:ABP之多租户

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 程序与技术分享:ABP之多租户

“软件多租户”指的是一种软件架构,一个软件实例在一个服务器上运行,但为多个租户服务。租户们对软件实例有通用的访问入口,但是每个租户都有特定的权限。


在多租户体系架构中,用程序旨在为每个租户提供一个专用的实例共享,包括其数据*、配置、用户管理、租户个人功能和非功能属性。


多租户与多实例体系结构形成对比,在多实例体系结构中,独立的软件实例代表不同的租户操作”(维基百科)。


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


数据库和部署架构


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


多个部署-多个数据库(Multiple Deployment - Multiple Database)


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


对于已存在的但没有被设计为多租户的应用程序也是可能的。创建这样的应用程序可能更容易,因为不需要考虑多租户,但是会有安装,使用以及维护等各种问题。


单一部署-多个数据库(Single Deployment - Multiple Database)


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


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


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


单一部署-单个数据库(Single Deployment - Single Database)


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


这种类型的应用程序易于安装和维护,但创建起来比较困难。这是因为我们必须防止租户读取或写入其他租户数据。我们可以为每个数据库读取(选择)操作添加一个TenantId过滤器。我们也可以在每次写的时候检查看看这个实体是否与当前租户相关。这既乏味又容易出错。然而,ASP.NET Boilerplate帮助我们在这里使用自动数据过滤。


如果我们有许多具有大数据集的租户,这种方法可能存在性能问题。我们可以使用表分区或其他数据库特性来克服这个问题。


单一部署-混合数据库(Single Deployment - Hybrid Databases)


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


多个部署——单个/多个/混合数据库(Multiple Deployment - Single/Multiple/Hybrid Database)


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


ASP.NET Boilerplate中的多租户


ASP.NET Boilerplate可用于上面描述的所有场景。


启用多租户


框架默认是禁用多租户的,我们可以在模块的预初始(PreInitialize)方法中启用它,如下图所示:


Configuration.MultiTenancy.IsEnabled = true;


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


主机vs租户(Host vs Tenant)


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


租户:客户拥有自己的用户、角色、权限、设置……并使用与其他租户完全隔离的应用程序。多租户应用程序将有一个或多个租户。如果这是一个CRM应用程序,不同的租户都有自己的帐户、联系人、产品和订单。所以当我们说“租户用户”时,我们指的是租户拥有的用户。


主机:主机是单例的(只有一个主机)。主机负责创建和管理租户。“主机用户”处于更高级别,独立于所有租户,可以控制它们。


会话(Session)


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


如果UserId 和 TenantId都是null,则当前用户不会登录到系统。我们无法知道它是主机用户还是租户用户。在这种情况下,用户无法访问授权内容。


如果UserId不是null,而TenantId是null,则当前用户是主机用户。


如果UserId不是null,而TenantId也不是null,则当前用户是租户用户。


如果UserId为null,而TenantId不是null,这意味着我们知道当前的租户,但是当前的请求没有被授权(用户没有登录)。


确定当前租户


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


如果用户已经登录,它会从当前的claims获得TenantId。Claim名称是 一般是一个整数值),如果在claims中没有找到,则认为是主机用户。


如果用户尚未登录,那么它将尝试从租户解析贡献者解析TenantId。有3个预定义的租户贡献者,并且按照给定的顺序运行(第一个成功的解析器'wins'):


DomainTenantResolveContributer:尝试从url解析租赁名称,通常是从域或子域解析。您可以在模块的预初始化(PreInitialize)方法中配置域格式(像Configuration.Modules.AbpWebCommon().MultiTenancy.DomainFormat = "{0}.mydomain.com";)。如果域格式是“{0}.mydomain.com”,比如请求的当前主机是acme.mydomain.com,则租户名称解析为“acme”。下一步是查询ITenantStore以根据给定的租户名称找到TenantId。如果找到了租户,则将其解析为当前的TenantId。


HttpHeaderTenantResolveContributer:尝试从“Abp.TenantId”标头值(如果存在)解析TenantId。这是在Abp.MultiTenancy.MultiTenancyConsts.TenantIdResolveKey中定义的常数。


HttpCookieTenantResolveContributer:尝试从“Abp.TenantId”Cookie值(如果存在)解析TenantId。这是在Abp.MultiTenancy.MultiTenancyConsts.TenantIdResolveKey中定义的常数。


如果这些尝试都不能解析TenantId,那么当前请求者被认为是主机。租户解析器是可扩展的。您可以向Configuration.MultiTenancy.Resolvers collection中添加解析器,或删除现有的解析器。


出于性能原因,在同一请求期间缓存已解析的租户id。解析器在请求中执行一次,且仅在当前用户尚未登录时执行。


租户存储


DomainTenantResolveContributer使用ITenantStore根据租户名称查找租户id。ITenantStore的默认实现是NullTenantStore,它不包含任何租户,对于查询返回null。您可以实现并替换它来查询来自任何数据源的租户。


数据过滤


对于多租户单数据库方法,我们必须添加一个TenantId过滤器,以便在从数据库检索实体时只获取当前租户的实体。如果你的实体实现IMustHaveTenant 和 IMayHaveTenant任何一个接口,ASP.NET Boilerplate会自动的帮你做这个。


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的示例实体:


?12345678public 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 //代码效果参考:http://www.jhylw.com.cn/174621129.html

class ProductService : ITransientDependency

{


private readonly IRepository


_productRepository;


private readonly IUnitOfWorkManager _unitOfWorkManager;


public ProductService(IRepository


productRepository, IUnitOfWorkManager unitOfWorkManager)


{


_productRepository = productRepository;


_unitOfWorkManager = unitOfWorkManager;


}


【UnitOfWork】


public virtual List


GetProducts(int tenantId)


{


using (_unitOfWorkManager.Current.SetTenantId(tenantId))


{


return _productRepository.GetAllList();


}


}


}


SetTenantId确保我们正在处理给定租户的数据,独立于数据库体系结构:


如果给定的租户有一个专用数据库,它将切换到该数据库并从中获取产品。


如果给定的租户没有专用的数据库(例如,单一数据库方法),它将添加自动TenantId过滤器,只查询该租户的产品。


如果我们不使用SetTenantId,它会从会话中获取tenantId。这里有一些指导方针和最佳实践:


使用SetTenantId(null)切换到主机。


如果没有特殊情况,在using块中使用SetTenantId(如示例中所示)。通过这种方式,它在using块的末尾自动恢复tenantId,调用GetProducts方法的代码与前面一样工作。


如果需要,可以在嵌套块中使用SetTenantId。


因为_unitOfWorkManager.Current只能在工作单元中可用,请确保您的代码在UOW中运行。

相关文章
|
6月前
|
开发框架 移动开发 JavaScript
SpringCloud微服务实战——搭建企业级开发框架(四十七):【移动开发】整合uni-app搭建移动端快速开发框架-添加Axios并实现登录功能
在uni-app中,使用axios实现网络请求和登录功能涉及以下几个关键步骤: 1. **安装axios和axios-auth-refresh**: 在项目的`package.json`中添加axios和axios-auth-refresh依赖,可以通过HBuilderX的终端窗口运行`yarn add axios axios-auth-refresh`命令来安装。 2. **配置自定义常量**: 创建`project.config.js`文件,配置全局常量,如API基础URL、TenantId、APP_CLIENT_ID和APP_CLIENT_SECRET等。
254 60
|
4月前
|
前端开发 开发者 C#
WPF开发者必读:MVVM模式实战,轻松实现现代桌面应用架构,让你的代码更上一层楼!
【8月更文挑战第31天】在WPF应用程序开发中,MVVM(Model-View-ViewModel)模式通过分离应用程序的逻辑和界面,提高了代码的可维护性和可扩展性。本文介绍了MVVM模式的三个核心组件:Model(数据模型)、View(用户界面)和ViewModel(处理数据绑定和逻辑),并通过示例代码展示了如何在WPF项目中实现MVVM模式。通过这种方式,开发者可以构建更加高效和可扩展的桌面应用程序。
221 0
|
4月前
|
缓存 前端开发 JavaScript
Angular邂逅PWA:一场关于如何利用现代Web技术栈中的明星框架与渐进式理念,共同编织出具备原生应用般丝滑体验、离线访问及桌面集成能力的未来Web应用的探索之旅
【8月更文挑战第31天】本文详细介绍如何利用Angular将传统Web应用升级为渐进式Web应用(PWA),克服后者在网络依赖、设备集成及通知功能上的局限。通过具体命令行操作与代码示例,指导读者从新建Angular项目到配置`manifest.json`和服务工作进程,最终实现离线访问、主屏添加及推送通知等功能,显著提升用户体验。适合各水平开发者学习实践。
46 0
|
5月前
|
开发框架 前端开发 JavaScript
ABP开发框架前后端开发系列---(16)ABP框架升级最新版本的经验总结
ABP开发框架前后端开发系列---(16)ABP框架升级最新版本的经验总结
|
7月前
|
开发框架 移动开发 JavaScript
SpringCloud微服务实战——搭建企业级开发框架(四十六):【移动开发】整合uni-app搭建移动端快速开发框架-环境搭建
正如优秀的软件设计一样,uni-app把一些移动端常用的功能做成了独立的服务或者插件,我们在使用的时候只需要选择使用即可。但是在使用这些服务或者插件时一定要区分其提供的各种服务和插件的使用场景,例如其提供的【uni-starter快速开发项目模版】几乎集成了移动端所需的所有基础功能,使用非常方便,但是其许可协议只允许对接其uniCloud的JS开发服务端,不允许对接自己的php、java等其他后台系统。
323 61
|
7月前
|
移动开发 JavaScript 小程序
uniapp为什么能支持多端开发?uniapp底层是怎么做的?
uniapp为什么能支持多端开发?uniapp底层是怎么做的?
254 0
|
开发框架 监控 BI
NetCore多租户开源项目,快速后台开发企业框架,赚钱就靠她了
NetCore多租户开源项目,快速后台开发企业框架,赚钱就靠她了
187 0
|
缓存 开发框架 前端开发
SpringCloud微服务实战——搭建企业级开发框架(四十一):扩展JustAuth+SpringSecurity+Vue实现多租户系统微信扫码、钉钉扫码等第三方登录
  如果我们自己的系统需要调用第三方登录,那么我们就需要实现单点登录客户端,然后跟需要对接的平台调试登录SDK。JustAuth是第三方授权登录的工具类库,对接了国外内数十家第三方登录的SDK,我们在需要实现第三方登录时,只需要集成JustAuth工具包,然后配置即可实现第三方登录,省去了需要对接不同SDK的麻烦。   JustAuth官方提供了多种入门指南,集成使用非常方便。但是如果要贴合我们自有开发框架的业务需求,还是需要进行整合优化。下面根据我们的系统需求,从两方面进行整合:一是支持多租户功能,二是和自有系统的用户进行匹配。
4417 56
SpringCloud微服务实战——搭建企业级开发框架(四十一):扩展JustAuth+SpringSecurity+Vue实现多租户系统微信扫码、钉钉扫码等第三方登录
|
开发框架 数据安全/隐私保护 微服务
SpringCloud微服务实战——搭建企业级开发框架(二十一):基于RBAC模型的系统权限设计
RBAC(基于角色的权限控制)模型的核心是在用户和权限之间引入了角色的概念。取消了用户和权限的直接关联,改为通过用户关联角色、角色关联权限的方法来间接地赋予用户权限,从而达到用户和权限解耦的目的,RBAC介绍原文链接。 RABC的好处
448 55
SpringCloud微服务实战——搭建企业级开发框架(二十一):基于RBAC模型的系统权限设计
|
开发框架 JSON 前端开发
浅入ABP(2):添加基础集成服务
浅入ABP(2):添加基础集成服务
680 0
浅入ABP(2):添加基础集成服务