33

我正在寻找其他尝试使用数据库级隔离构建多租户 Django 应用程序的工作代码和想法。

更新/解决方案:我在一个新的开源项目中解决了这个问题:见django-db-multitenant

目标

我的目标是根据请求主机名或请求路径(例如,foo.example.com/将 Django 连接设置为使用数据库foo,并bar.example.com/使用数据库bar),在请求进入单个应用程序服务器(如 gunicorn 的 WSGI 前端)时多路复用请求。

先例

我知道 Django 中有一些现有的多租户解决方案:

  1. django-tenant-schemas:这与我想要的非常接近:您以最高优先级安装它的中间件,然后它向SET search_path数据库发送命令。不幸的是,它是 Postgres 特有的,我被 MySQL 困住了。
  2. django-simple-multitenant:这里的策略是为所有模型添加一个“租户”外键,并调整所有应用程序业务逻辑以关闭它。基本上每一行都被索引(id, tenant_id)而不是索引(id)。我已经尝试过但不喜欢这种方法,原因有很多:它使应用程序更加复杂,可能导致难以发现的错误,并且它不提供数据库级别的隔离。
  3. 每个租户一个 {app server, django settings file with appropriate db}。又名穷人的多租户(实际上是富人的,考虑到它所涉及的资源)。我不想为每个租户启动一个新的应用服务器,为了可扩展性,我希望任何应用服务器都能够为任何客户端分派请求。

想法

到目前为止,我最好的想法是做类似的事情django-tenant-schemas:在第一个中间件中,抓取django.db.connection和摆弄数据库选择而不是模式。我还没有完全考虑到这在池/持久连接方面意味着什么

我追求的另一个死胡同是特定于租户的表前缀:除了我需要它们是动态的之外,即使是全局表前缀在 Django 中也不容易实现(请参阅拒绝票 5000等)。

最后,Django多数据库支持允许您定义多个命名数据库,并根据实例类型和读/写模式在它们之间多路复用。没有帮助,因为没有设施可以根据每个请求选择数据库。

问题

有没有人管理过类似的事情?如果是这样,您是如何实施的?

4

3 回答 3

16

我已经做了最接近第 1 点的类似操作,但没有使用中间件来设置默认连接,而是使用了 Django 数据库路由器。如果每个请求需要,这允许应用程序逻辑使用多个数据库。为每个查询选择合适的数据库取决于应用程序逻辑,这是这种方法的一大缺点。

使用此设置,所有数据库都列在 中settings.DATABASES,包括可能在客户之间共享的数据库。每个客户特定的模型都放置在具有特定应用标签的 Django 应用中。

例如。下面的类定义了一个存在于所有客户数据库中的模型。

class MyModel(Model):
    ....
    class Meta:
        app_label = 'customer_records'
        managed = False

将数据库路由器放置在settings.DATABASE_ROUTERS链中以通过 路由数据库请求app_label,如下所示(不是完整示例):

class AppLabelRouter(object):
    def get_customer_db(self, model):
        # Route models belonging to 'myapp' to the 'shared_db' database, irrespective
        # of customer.
        if model._meta.app_label == 'myapp':
            return 'shared_db'
        if model._meta.app_label == 'customer_records':
            customer_db = thread_local_data.current_customer_db()
            if customer_db is not None:
                return customer_db

            raise Exception("No customer database selected")
        return None

    def db_for_read(self, model, **hints):
        return self.get_customer_db(model, **hints)

    def db_for_write(self, model, **hints):
        return self.get_customer_db(model, **hints)

这个路由器的特殊部分是thread_local_data.current_customer_db()调用。在使用路由器之前,调用者/应用程序必须已经在thread_local_data. 为此,可以使用 Python 上下文管理器来推送/弹出当前客户数据库。

配置完所有这些后,应用程序代码看起来像这样,UseCustomerDatabase上下文管理器将当前客户数据库名称推送/弹出到其中thread_local_data,以便thread_local_data.current_customer_db()在最终命中路由器时返回正确的数据库名称:

class MyView(DetailView):
    def get_object(self):
        db_name = determine_customer_db_to_use(self.request) 
        with UseCustomerDatabase(db_name):
            return MyModel.object.get(pk=1)

这已经是一个相当复杂的设置了。它有效,但我将尝试总结我认为的优点和缺点:

优点

  • 数据库选择灵活。它允许在单个查询中使用多个数据库,客户特定数据库和共享数据库都可以在请求中使用。
  • 数据库选择是明确的(不确定这是优点还是缺点)。如果您尝试运行查询客户数据库但应用程序没有选择一个,则会发生异常,指示编程错误。
  • 使用数据库路由器允许不同的数据库存在于不同的主机上,而不是依赖于USE db;猜测所有数据库都可以通过单个连接访问的语句。

缺点

  • 设置起来很复杂,并且需要很多层才能使其正常运行。
  • 线程本地数据的需要和使用是模糊的。
  • 视图中充斥着数据库选择代码。这可以使用基于类的视图进行抽象,以根据请求参数以与中间件选择默认数据库相同的方式自动选择数据库。
  • 选择数据库的上下文管理器必须以这样一种方式包装查询集,即在评估查询时上下文管理器仍然处于活动状态。

建议

如果您想要灵活的数据库访问,我建议使用 Django 的数据库路由器。使用中间件或视图 Mixin,它会根据请求参数自动设置用于连接的默认数据库。您可能不得不求助于线程本地数据来存储要使用的默认数据库,以便当路由器被击中时,它知道要路由到哪个数据库。这允许 Django 使用其现有的持久连接到数据库(如果需要,可以驻留在不同的主机上),并根据请求中设置的路由选择要使用的数据库。

这种方法还有一个优点,即如果需要,可以通过使用该QuerySet using()函数选择默认数据库以外的数据库来覆盖查询的数据库。

于 2013-06-06T04:50:28.527 回答
4

作为记录,我选择实现我的第一个想法的变体:USE <dbname>在早期请求中间件中发出 a。我也以同样的方式设置了 CACHE 前缀。

我在一个小型生产站点上使用它,根据请求主机从 Redis 数据库中查找租户名称。到目前为止,我对结果很满意。

我在这里把它变成了一个(希望是可重复使用的)github项目:https ://github.com/mik3y/django-db-multitenant

于 2013-07-11T17:58:52.460 回答
2

您可以创建自己的简单中间件,从您的子域或其他任何内容中确定数据库名称,然后针对每个请求在数据库游标上执行USE语句。查看 django-tenants-schema 代码,这基本上就是它正在做的事情。它是 psycopg2 的子类,并发出等同于 USE 的 postgres,“set search_path XXX”。您也可以创建一个模型来管理和创建您的租户,但是您将重写大部分 django-tenants-schema。

在 MySQL 中切换模式(数据库名称)不应该有性能或资源损失。它只是为连接设置一个会话参数。

于 2013-05-31T01:02:51.093 回答