Django 多租户
什么是多租户?
多租户(Multi-tenancy)是一种软件架构模式,它允许软件应用实例同时服务于多个不同的客户或组织(称为“租户”),而这些租户之间共享计算资源和服务,
但数据和应用配置是逻辑上隔离的。在多租户架构中,每个租户都拥有自己独立的用户和数据,但所有这些租户都运行在同一个应用实例上,而不是每个租户都部署一个
独立的应用实例。
多租户的特性?
多租户架构的主要优势在于:
- 成本效益:由于多个租户共享相同的计算资源和服务,因此可以显著降低每个租户的成本。相比为每个租户部署独立的应用实例,多租户架构能够更有效地利用硬件和软件资源。
- 易于管理:管理员可以集中管理所有租户的应用实例,包括更新、维护和监控等任务。这样可以减少重复工作,提高管理效率。
- 快速部署:新租户可以快速加入系统,因为他们可以使用已经存在的应用实例,而无需从头开始部署。
- 可扩展性:多租户架构支持按需扩展,可以根据租户数量的增长动态地调整资源分配。
然而,多租户也带来了一些挑战:
- 数据隔离:必须确保不同租户之间的数据完全隔离,以防止数据泄露和混淆。这通常通过数据库架构设计(如使用不同的数据库模式或表前缀)和访问控制机制来实现。
- 性能调优:在多租户环境中,需要确保所有租户都能获得足够的性能资源,而不会因为某个租户的负载过高而影响其他租户。
- 定制化需求:不同租户可能有不同的业务需求和应用定制需求,如何在满足这些需求的同时保持系统的统一性和可维护性是一个挑战。
实现多租户的方法有很多,包括但不限于:
- 数据库级别的多租户: 每个租户使用独立的数据库实例或者是数据库模式。
- 架构级别的多租户: 通过应用层的逻辑来区分不同租户的数据和配置,但所有租户共享相同的数据库实例。
- 虚拟化技术: 为每个租户提供独立的运行环境
django-tenants
1. 安装
pip install django-tenants==3.5.0
在这里我使用的是django-tenants
来作为多租户的框架,搭配pgsql
从数据库层面来实现多租户功能
文档地址: https://django-tenants.readthedocs.io/en/latest/install.html
2. 描述
django-tenants
是一个强大的开源应用,专为基于 Django 的网站提供多租户功能。以下是对 django-tenants
的详细解析:
- 多租户支持: 利用
PostgreSQL
的schemas
功能,将每个租户的数据隔离到各自的schema
中,避免了数据冲突。 - 通过自动识别请求的主机名或子域名,它能够实时切换到对应的租户模式下,确保所有数据查询都在正确的
schema
内进行。 - 易于集成
- 几乎无需大量修改现有代码即可实现多租户支持。
- 只需更改数据库引擎设置、添加中间件和路由器,即可开始使用。
- 高性能:
- 采用共享数据库连接,减少资源消耗,提高效率。
- 视图路由功能允许根据域名自动分配不同的视图,允许在同一 URL 结构下为不同租户设置个性化界面。
- 高度可定制化:
- 租户模型可以自定义,如包含额外属性或关联信息。
- 易于配置,提供清晰的文档和示例项目,帮助开发者快速上手。
3. 应用场景
1. Saas
应用:
对于提供多种服务并需要分离用户数据的服务提供商来说,django-tenants 可以帮助轻松地搭建和扩展平台。
2. 企业级平台:
如果正在构建一个支持多部门或子公司的企业内部系统,django-tenants 能够实现数据隔离,保护公司信息安全。
4. 注意事项
安装 django-tenants
可能需要对 Django
项目的中间件和数据库路由进行一些调整。
由于 django-tenants
主要依赖于 PostgreSQL
的 schemas
功能,因此建议使用 PostgreSQL
作为数据库。
在使用 django-tenants
时,可能会与项目中安装的其他模块发生冲突,特别是在数据库路由和中间件方面。
5. 模块核心功能详解
1. 租户和域模型
租户模型(Tenant Model)
租户模型代表了多租户系统中的每一个独立租户。在 django-tenants
中,通常会有一个自定义的租户模型,该模型继承自 TenantMixin
(或类似的Mixin),
并定义了租户特有的字段。例如:
from django_tenants.models import TenantMixin
from django.db import models
class Tenant(TenantMixin):
name = models.CharField(max_length=100)
paid_until = models.DateField(null=True, blank=True)
on_trial = models.BooleanField(default=True)
# 可以添加其他自定义字段
# ...
在这个例子中,Tenant
模型定义了租户的名称、付费截止日期和是否在试用期等字段。TenantMixin
提供了必要的多租户支持功能,如自动生成的 schema_name
(用于在 PostgreSQL
中创建对应的 schema
)和 domain_url
(用于将租户与特定的域名或子域名关联起来)。
域模型(Domain Model)
在 django-tenants
的上下文中,域模型通常不是直接由开发者自定义的,而是由库内部处理或与租户模型紧密关联的。然而,为了理解多租户架构中的域名处理,
我们可以考虑一个与租户模型相关联的“域”概念。
在 django-tenants
中,每个租户通常都与一个或多个域名(或子域名)相关联。这些域名用于在请求处理过程中识别租户,并相应地切换数据库 schema
。
虽然 django-tenants
没有直接提供一个显式的“域模型”类供开发者继承,但它通过中间件和数据库路由机制来实现这一功能。
import uuid
from django_tenants.models import DomainMixin
class Domain(DomainMixin):
id = models.CharField(max_length=255, primary_key=True, default=uuid.uuid4, help_text="Id", verbose_name="Id")
bucket = models.CharField(max_length=255, null=True, blank=True, verbose_name="租户存储桶")
在上述代码中,Domain
模型中定义了租户的存储桶等字段,DomainMinxin
自动自动去关联上租户表.
当请求到达 Django
应用时,django-tenants
的中间件会解析请求的域名(或子域名),并根据预定义的规则(如域名映射)来识别租户。一旦识别出租户,
中间件就会将数据库连接切换到该租户对应的 schema
,并确保后续的数据库操作都在正确的租户上下文中执行。
配置租户和共享应用程序
在Django
中配置多租户架构时,租户(Tenant)和共享应用程序(Shared Applications)之间的关系是核心考虑点之一。租户代表了系统中的独立用户或组织,
它们拥有自己的数据集和可能的特定配置,而共享应用程序则是所有租户共同使用的功能或服务。
数据隔离:每个租户拥有自己独立的数据集,这些数据被存储在数据库的不同schema中(在django-tenants的上下文中)。这意味着每个租户的数据都是相互隔离的,
一个租户无法访问或修改另一个租户的数据。
共享逻辑:尽管数据是隔离的,但许多逻辑和功能可能是所有租户共享的。这些共享的功能或应用程序(如用户认证、日志记录、通知服务等)可以部署为Django应用程序,
并由所有租户共同使用。
配置灵活性:虽然共享应用程序的逻辑对所有租户都是相同的,但某些配置(如模板、静态文件、特定于租户的设置等)可能需要根据租户的不同而有所变化。
这可以通过在租户模型中存储这些配置信息,并在运行时根据当前活动的租户来动态加载这些配置来实现。
SHARED_APPS = (
'django_tenants',
'customers',
'django.contrib.contenttypes',
'django.contrib.auth',
'django.contrib.sessions',
'django.contrib.sites',
'django.contrib.messages',
'django.contrib.admin',
)
TENANT_APPS = (
'myapp.hotels',
'myapp.houses',
)
INSTALLED_APPS = list(SHARED_APPS) + [app for app in TENANT_APPS if app not in SHARED_APPS]
上述代码示例中,SHARED_APPS
区域就属于所有租户共享的app
, 在此配置的app所产生的数据是所有租户都可见的,可操作的。
TENANT_APPS
就是每个租户下的独享 app
, 也就说明在TENANT_APPS
中定义的数据库模型产生的数据都是当前租户独享的,你访问不到其他租户的数据,
其他租户也访问不到你的数据。
中间件支持
TenantMainMiddleware
1. 工作原理:
当 Django 接收到一个 HTTP 请求时,它会按照 MIDDLEWARE 列表中定义的顺序依次通过各个中间件。TenantMainMiddleware 通常被配置在列表的较前面,
以便尽早识别并设置租户信息。
工作流程:
- 解析租户标识
中间件首先会尝试从请求中提取租户标识符。这通常是通过请求的路径(URL)、HTTP 头部、子域名或其他方式来实现的。具体方法取决于你的配置和应用的架构。 - 切换数据库
schema
一旦确定了当前活动的租户,中间件会调用 django-tenants 的内部机制来切换数据库连接,使其指向该租户的特定 schema。这一步确保了接下来的数据库操作
(如查询、更新、删除等)都会在该租户的 schema 中执行,从而实现数据的隔离。 - 处理请求: 完成了租户识别和数据库 schema 切换后,中间件会将请求传递给下一个中间件或视图,继续正常的请求处理流程
2. 核心方法详解
- no_tenant_found
SHOW_PUBLIC_IF_NO_TENANT_FOUND
: 默认情况下,SHOW_PUBLIC_IF_NO_TENANT_FOUND
的值为False
,即如果没有找到匹配的租户,
django-tenants
不会显示公共租户的数据。相反,它会返回一个租户未找到的错误或者执行其他配置的操作。
如果需要再没有获取到租户的情况下,展示公共资源,可在settings中设置其参数为True - get_tenant
获取当前租户的对象。在使用该方法时需要确保在settings
中配置租户模型和域名模型 - get_tenant_domain_model
获取当前租户的域名数据库对象。在使用该方法时需要确保在settings
中配置租户模型和域名模型 - hostname_from_request
从主机中获取当前请求的域名。
只有通过域名来判断当前是哪个租户,才能去切换对应的数据库schemas
。 - connection.set_schema_to_public()
是切换当前模式到public
中, 切换到共享模式。 - connection.set_tenants
切换到指定租户的模式中。
通过上述核心几个方法,便可以自如的配置中间件了。
以下是源码, 用作解析
def process_request(self, request):
# Connection needs first to be at the public schema, as this is where
# the tenant metadata is stored.
# 视图处理请求之前,先将数据库模式切换到公共模式中,对应的是pgsql中的 public模式
connection.set_schema_to_public()
try:
# 获取当前请求的域名。结果可能是 dev-1.kjyxgs.com
hostname = self.hostname_from_request(request)
except DisallowedHost:
from django.http import HttpResponseNotFound
return HttpResponseNotFound()
# 获取域名表中的域名数据
domain_model = get_tenant_domain_model()
try:
# 通过当前请求的域名去租户表中查询是否有当前租户
tenant = self.get_tenant(domain_model, hostname)
except domain_model.DoesNotExist:
# 如果不存在当前租户,这里就可以返回异常了 "当前租户不存在" 或者是自己的一些其他个性化需求。。
# 根据settings中的配置来决定是否在租户不存在的情况下让其访问共享数据。
self.no_tenant_found(request, hostname)
return
tenant.domain_url = hostname
request.tenant = tenant
# 校验通过后
# 切换schemas为当前租户
connection.set_tenant(request.tenant)
self.setup_url_routing(request)
6. Django配置
1. settings
# 配置pgsql
DATABASES = {
'default': {
'ENGINE': 'django_tenants.postgresql_backend',
'NAME': 'your_database_name',
'USER': 'your_database_user',
'PASSWORD': 'your_database_password',
'HOST': 'localhost',
'PORT': '5432',
}
}
# 配置中间件
MIDDLEWARE = [
# ... 其他中间件 ...
'django_tenants.middleware.main.TenantMainMiddleware',
# ... 其他中间件 ...
]
# 确保了租户数据的隔离和共享数据的正确路由
DATABASE_ROUTERS = (
'django_tenants.routers.TenantSyncRouter',
)
# 配置共享 APP
SHARED_APPS = (
'django_tenants', # mandatory
'customers', # you must list the app where your tenant model resides in
'django.contrib.contenttypes',
# everything below here is optional
'django.contrib.auth',
'django.contrib.sessions',
'django.contrib.sites',
'django.contrib.messages',
'django.contrib.admin',
)
# 配置租户独享APP
TENANT_APPS = (
# your tenant-specific apps
'myapp.hotels',
'myapp.houses',
)
INSTALLED_APPS = list(SHARED_APPS) + [app for app in TENANT_APPS if app not in SHARED_APPS]
TENANT_MODEL = "customers.Client" # 配置自定义的租户模型
TENANT_DOMAIN_MODEL = "customers.Domain" # 配置自定义的域名模型
SHOW_PUBLIC_IF_NO_TENANT_FOUND = True
2. 迁移命令
python3 manage.py makemigrations
python3 manage.py migrate # 默认迁移所有的模式数据。包括 public
python3 manage.py migrate_schemas --shared # 迁移指定租户的模型