基于Django 3.0与MariaDB的独立数据库多租户实现方案咨询
解决方案:Django 3.0 + MariaDB 独立数据库多租户实现
我完全理解你的需求——因为数据驻留法规要求必须采用独立数据库,而django-tenant-schemas仅支持PostgreSQL的schema模式,确实没法满足你的场景。好在Django原生的数据库路由系统可以帮我们实现目标,下面是具体的实现思路和可落地的步骤:
一、核心方案:自定义数据库路由(Database Router)
Django允许通过自定义路由类控制每个模型的数据库读写逻辑,这是实现独立数据库多租户的基础。
1. 配置基础数据库与租户模板
首先在settings.py中配置存储租户元数据的主数据库,同时定义租户数据库的模板(后续动态添加租户数据库时复用):
# settings.py DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'tenant_metadata', 'USER': 'db_user', 'PASSWORD': 'db_pass', 'HOST': 'localhost', 'PORT': '3306', 'OPTIONS': { 'init_command': "SET sql_mode='STRICT_TRANS_TABLES'", 'charset': 'utf8mb4', }, } } # 租户数据库配置模板,创建新租户时直接复用 TENANT_DB_TEMPLATE = { 'ENGINE': 'django.db.backends.mysql', 'USER': 'db_user', 'PASSWORD': 'db_pass', 'HOST': 'localhost', # 若需跨地区部署,可根据租户region动态调整 'PORT': '3306', 'OPTIONS': { 'init_command': "SET sql_mode='STRICT_TRANS_TABLES'", 'charset': 'utf8mb4', }, }
主数据库tenant_metadata专门存储租户的核心信息:租户ID、数据库名称、所属地区(用于匹配数据驻留要求)等。
2. 编写租户识别中间件
需要一个中间件来识别当前请求对应的租户,比如通过请求头、子域名或URL参数获取标识,再将租户信息绑定到request对象上:
# middleware.py from django.http import HttpResponseForbidden from .models import Tenant from django.conf import settings from django.db import connections class TenantMiddleware: def __init__(self, get_response): self.get_response = get_response def __call__(self, request): # 示例:通过请求头X-Tenant-ID识别租户,可根据业务改为子域名/URL参数 tenant_id = request.headers.get('X-Tenant-ID') if not tenant_id: return HttpResponseForbidden("Missing tenant identifier") try: request.tenant = Tenant.objects.get(id=tenant_id) except Tenant.DoesNotExist: return HttpResponseForbidden("Invalid tenant") # 动态将租户数据库添加到DATABASES(避免重复添加) if request.tenant.db_name not in settings.DATABASES: settings.DATABASES[request.tenant.db_name] = { **settings.TENANT_DB_TEMPLATE, 'NAME': request.tenant.db_name } # 清除已缓存的连接,强制重新加载 connections._connections.clear() response = self.get_response(request) return response
记得将这个中间件添加到settings.MIDDLEWARE列表中。
3. 实现数据库路由类
编写路由类,根据当前请求的租户信息,将模型的读写操作路由到对应租户数据库:
# routers.py class TenantDatabaseRouter: def db_for_read(self, model, **hints): # 租户元数据模型固定在主数据库 if model._meta.app_label == 'tenant_management': return 'default' # 其他模型根据当前请求的租户选择数据库 request = hints.get('request') or getattr(self, '_current_request', None) if request and hasattr(request, 'tenant'): return request.tenant.db_name return 'default' def db_for_write(self, model, **hints): return self.db_for_read(model, **hints) def allow_relation(self, obj1, obj2, **hints): # 仅允许同一租户内的模型建立关联 return obj1._state.db == obj2._state.db def allow_migrate(self, db, app_label, model_name=None, **hints): # 租户元数据仅在主数据库迁移 if app_label == 'tenant_management': return db == 'default' # 业务模型仅在租户数据库迁移(需手动指定数据库运行migrate) return db != 'default'
最后在settings.py中配置这个路由:
# settings.py DATABASE_ROUTERS = ['your_project.routers.TenantDatabaseRouter']
二、租户数据库的自动化管理
1. 创建租户时自动初始化数据库
当新增租户时,自动创建对应的MariaDB数据库并执行迁移:
# models.py from django.db import models from django.db import connections from django.core.management import call_command class Tenant(models.Model): id = models.CharField(max_length=50, primary_key=True) name = models.CharField(max_length=100) db_name = models.CharField(max_length=100, unique=True) region = models.CharField(max_length=50) # 标记租户所属地区,匹配数据驻留要求 def save(self, *args, **kwargs): is_new = self._state.adding super().save(*args, **kwargs) if is_new: # 创建租户数据库 with connections['default'].cursor() as cursor: cursor.execute(f"CREATE DATABASE {self.db_name} CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;") # 执行迁移到租户数据库 call_command('migrate', database=self.db_name)
注意:需要确保数据库用户拥有创建数据库的权限。
2. 迁移的批量处理
- 主数据库仅迁移租户元数据相关的App
- 若需批量更新所有租户数据库的结构,可以编写自定义管理命令,遍历所有租户并执行
migrate命令
三、关键优化与注意事项
- 连接池管理:MariaDB连接数有限,建议使用
django-db-connection-pool这类工具复用连接,避免租户过多导致连接耗尽 - 缓存隔离:使用Django缓存时,需为每个租户的缓存键添加租户ID前缀,防止数据混淆
- Admin适配:默认Django Admin使用
default数据库,若要管理租户业务数据,需自定义Admin类,根据当前租户切换数据库 - 测试隔离:编写测试用例时,需为每个测试创建临时租户数据库,测试完成后销毁,避免数据污染
- 备份策略:因采用独立数据库,需为每个租户单独配置备份,符合数据驻留法规要求
四、第三方包辅助方案
如果不想完全从零搭建,可以基于django-multitenant扩展(需修改部分代码适配多数据库模式),或使用django-db-router简化路由逻辑,但核心思路仍离不开自定义数据库路由。
内容的提问来源于stack exchange,提问作者Manish
相关产品推荐
相关产品推荐

