`Django`的`多租户`架构:`Schema`模式和`Shared`模式的`实现`。

Django 多租户架构:Schema 模式与 Shared 模式实现

各位同学,大家好!今天我们来深入探讨一下 Django 多租户架构的两种主要实现模式:Schema 模式和 Shared 模式。多租户架构允许单个应用程序实例为多个独立的用户(租户)提供服务,每个租户拥有自己的数据和配置,但共享底层的基础设施。选择哪种模式取决于应用程序的需求、性能考虑、数据隔离级别以及维护成本。

多租户架构概述

在深入讨论 Schema 模式和 Shared 模式之前,我们先简要回顾一下多租户架构的核心概念。

租户 (Tenant): 指应用程序的一个独立用户或客户。每个租户应该感觉自己在使用一个独立的应用程序实例。

数据隔离: 多租户架构的关键在于确保租户之间的数据隔离,防止未经授权的数据访问和篡改。

资源共享: 多个租户共享底层的计算、存储和网络资源,从而降低成本并提高资源利用率。

Schema 模式 (Database Per Tenant)

Schema 模式为每个租户创建一个独立的数据库 schema(或数据库),这是最彻底的数据隔离方法。每个租户的数据存储在自己的 schema 中,与其他租户的数据完全分离。

优点:

  • 最高级别的数据隔离: 租户数据完全隔离,消除了数据泄露的风险。
  • 数据安全: 即使应用程序存在漏洞,一个租户的数据也不会影响其他租户。
  • 易于备份和恢复: 每个租户的数据可以独立备份和恢复。
  • 易于迁移: 租户可以轻松地迁移到独立的服务器或数据库实例。
  • 自定义 schema: 每个租户的 schema 可以根据其特定需求进行自定义。

缺点:

  • 更高的资源消耗: 每个租户都需要独立的数据库 schema,增加了数据库服务器的资源消耗。
  • 更复杂的管理: 需要管理多个数据库 schema,增加了管理复杂性。
  • 更复杂的数据共享: 租户之间共享数据比较困难。
  • 初始化开销大: 创建新租户的开销比较大,因为需要创建新的 schema。

实现:

在 Django 中实现 Schema 模式,通常使用 django-tenants 库。该库提供了一套中间件、模型和管理命令,简化了 Schema 模式的实现。

1. 安装 django-tenants:

pip install django-tenants

2. 配置 settings.py:

# settings.py
INSTALLED_APPS = [
    'django_tenants',  # 必须放在第一位
    'django.contrib.contenttypes',
    # ... 其他应用程序
    'customers', # 存放租户模型的应用
    'your_app', # 你的主应用
]

MIDDLEWARE = [
    'django_tenants.middleware.main.TenantMainMiddleware',
    # ... 其他中间件
]

SHARED_APPS = (
    'django.contrib.contenttypes',
    'django.contrib.auth',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'customers', # 租户应用
)

TENANT_APPS = (
    'your_app',
    # ... 其他租户特定的应用程序
)

DATABASE_ROUTERS = (
    'django_tenants.routers.TenantSyncRouter',
)

TENANT_MODEL = "customers.Client" # 租户模型

PUBLIC_SCHEMA_NAME = 'public' # 公共schema的名字

DATABASE_URL = "postgres://user:password@host:port/database" # 设置数据库连接字符串

DATABASES = {
    'default': dj_database_url.parse(DATABASE_URL),
}

3. 创建租户模型 (customers/models.py):

from django.db import models
from django_tenants.models import TenantMixin, DomainMixin

class Client(TenantMixin):
    name = models.CharField(max_length=100)
    created_on = models.DateField(auto_now_add=True)

    auto_create_schema = True # 自动创建 schema

    def __str__(self):
        return self.name

class Domain(DomainMixin):
    pass

4. 创建租户应用:

创建一个名为 customers 的 Django 应用,用于存放租户模型。

5. 创建主应用:

创建一个名为 your_app 的 Django 应用,用于存放租户特定的模型和视图。

6. 迁移数据库:

python manage.py migrate_schemas --shared

这个命令会创建公共 schema (public) 和所有共享应用程序的表。

7. 创建租户:

from customers.models import Client, Domain

# 创建租户
tenant = Client(name='My Tenant')
tenant.save()

# 创建域名
domain = Domain(domain='mytenant.example.com', tenant=tenant)
domain.save()

示例代码:模型 (your_app/models.py):

from django.db import models
from django_tenants.utils import tenant_context

class Product(models.Model):
    name = models.CharField(max_length=100)
    price = models.DecimalField(max_digits=10, decimal_places=2)

    def __str__(self):
        return self.name

    def save(self, *args, **kwargs):
        current_tenant = Client.objects.get(schema_name=connection.schema_name)
        with tenant_context(current_tenant):
            super().save(*args, **kwargs) # 确保在正确的schema中保存

示例代码:视图 (your_app/views.py):

from django.shortcuts import render
from .models import Product
from django_tenants.utils import tenant_context
from customers.models import Client
from django.db import connection

def product_list(request):
    current_tenant = Client.objects.get(schema_name=connection.schema_name)

    with tenant_context(current_tenant):
        products = Product.objects.all()
        return render(request, 'product_list.html', {'products': products})

注意事项:

  • 确保 django_tenants 应用在 INSTALLED_APPS 中位于最前面。
  • 使用 TenantSyncRouter 确保共享应用程序的表只在公共 schema 中创建。
  • 使用 tenant_context 上下文管理器在租户特定的 schema 中执行数据库操作。

Shared 模式 (Shared Database, Shared Schema)

Shared 模式使用单个数据库和 schema 来存储所有租户的数据。通过在每个表中添加一个 tenant_id 列来区分不同租户的数据。

优点:

  • 更低的资源消耗: 所有租户共享相同的数据库和 schema,降低了数据库服务器的资源消耗。
  • 更简单的管理: 只需要管理一个数据库和 schema,降低了管理复杂性。
  • 更方便的数据共享: 租户之间共享数据比较容易。
  • 初始化开销小: 创建新租户的开销比较小,不需要创建新的 schema。

缺点:

  • 较低级别的数据隔离: 租户数据存储在同一个表中,存在数据泄露的风险。
  • 数据安全风险: 如果应用程序存在漏洞,一个租户的数据可能会影响其他租户。
  • 备份和恢复复杂: 需要备份和恢复整个数据库,无法独立备份和恢复单个租户的数据。
  • 迁移复杂: 租户迁移到独立的服务器或数据库实例比较困难。
  • 难以自定义 schema: 所有租户必须使用相同的 schema。
  • 查询性能影响: 由于所有租户的数据都存储在同一个表中,查询性能可能会受到影响。

实现:

在 Django 中实现 Shared 模式,需要手动添加 tenant_id 列到每个模型,并使用中间件或装饰器来过滤查询结果,确保每个租户只能访问自己的数据。

1. 添加 tenant_id 列到模型:

from django.db import models
from django.db.models import Q

class TenantAwareModel(models.Model):
    tenant_id = models.IntegerField()

    class Meta:
        abstract = True

    def save(self, *args, **kwargs):
        # 在保存前设置 tenant_id
        if not self.tenant_id:
            self.tenant_id = get_current_tenant_id() # 需要实现 get_current_tenant_id()
        super().save(*args, **kwargs)

    @classmethod
    def get_tenant_aware_queryset(cls, tenant_id):
        return cls.objects.filter(tenant_id=tenant_id)

class Product(TenantAwareModel):
    name = models.CharField(max_length=100)
    price = models.DecimalField(max_digits=10, decimal_places=2)

    def __str__(self):
        return self.name

2. 创建中间件或装饰器来过滤查询结果:

from django.utils.deprecation import MiddlewareMixin
from threading import local

_thread_locals = local()

def get_current_tenant_id():
    return getattr(_thread_locals, 'tenant_id', None)

class TenantMiddleware(MiddlewareMixin):
    def process_request(self, request):
        # 从请求中获取租户 ID (例如,从 URL、header 或 session 中)
        tenant_id = self.get_tenant_id(request)
        _thread_locals.tenant_id = tenant_id

    def get_tenant_id(self, request):
        #  TODO: 实现从请求中获取租户 ID 的逻辑
        #  例如:从 URL 中获取
        #  tenant_id = request.resolver_match.kwargs.get('tenant_id')
        #  或者从 header 中获取
        #  tenant_id = request.META.get('HTTP_X_TENANT_ID')
        # 这里只是一个示例,需要根据你的应用具体情况实现
        return 1 # 默认返回1,需要替换为实际的逻辑

3. 配置 settings.py:

MIDDLEWARE = [
    'your_app.middleware.TenantMiddleware',
    # ... 其他中间件
]

4. 修改视图,使用 TenantAwareModel:

from django.shortcuts import render
from .models import Product, get_current_tenant_id

def product_list(request):
    tenant_id = get_current_tenant_id()
    products = Product.get_tenant_aware_queryset(tenant_id)
    return render(request, 'product_list.html', {'products': products})

注意事项:

  • get_current_tenant_id() 函数必须根据你的应用程序的实际情况实现,从请求中获取租户 ID。
  • 使用 TenantAwareModel 作为所有租户特定模型的基类,确保每个模型都包含 tenant_id 列。
  • 使用中间件或装饰器来过滤查询结果,确保每个租户只能访问自己的数据。

模式对比

为了更清晰地了解两种模式的差异,我们可以将它们总结在一个表格中:

特性 Schema 模式 (Database Per Tenant) Shared 模式 (Shared Database, Shared Schema)
数据隔离 最高 较低
数据安全 最高 较低
资源消耗 较高 较低
管理复杂性 较高 较低
数据共享 复杂 简单
备份和恢复 简单 复杂
迁移 简单 复杂
自定义 schema 支持 不支持
查询性能 较好 可能较差
初始化开销

选择合适的模式

选择哪种模式取决于应用程序的需求。

  • Schema 模式 适用于对数据隔离和安全性要求非常高的应用程序,例如金融、医疗等行业。
  • Shared 模式 适用于对数据隔离要求不高,但对资源消耗和管理复杂性有较高要求的应用程序,例如 SaaS 应用、博客平台等。

在实际应用中,也可以将两种模式结合使用。例如,可以使用 Schema 模式来存储敏感数据,使用 Shared 模式来存储非敏感数据。

其它考虑事项

  • 数据库选择: Schema 模式更适合支持 schema 的数据库,例如 PostgreSQL。Shared 模式则对数据库的类型没有特别的限制。
  • 扩展性: Schema 模式的扩展性更好,可以轻松地将租户迁移到独立的服务器或数据库实例。
  • 成本: Shared 模式的初始成本较低,但随着租户数量的增加,可能需要升级数据库服务器。Schema 模式的初始成本较高,但可以随着租户数量的增加,逐步增加数据库服务器的数量。

总结

今天我们深入探讨了 Django 多租户架构的两种主要实现模式:Schema 模式和 Shared 模式,它们在数据隔离、资源消耗和管理复杂性等方面各有优缺点。希望通过这次讲座,大家能够更好地理解这两种模式,并根据自己的应用程序的需求选择合适的模式。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注