我正在寻找使用数据库级隔离构建多租户Django应用程序的其他人的工作代码和想法。
更新/解决方案:我在一个新的开源项目中解决了这个问题:参见django-db-multitenant
目标
我的目标是基于请求主机名或请求路径(例如,foo.example.com/
设置Django连接使用数据库foo
, bar.example.com/
使用数据库bar
),当它们进入单个应用服务器(WSGI前端如gunicorn)时,多路请求。
先例
我知道Django中有一些现有的多租户解决方案:
- django-tenant-schemas:这非常接近我想要的:您以最高优先级安装它的中间件,并且它向db发送
SET search_path
命令。不幸的是,它是Postgres特定的,我被MySQL卡住了。 - django-simple-multitenant:这里的策略是为所有模型添加一个"租户"外键,并调整所有应用程序业务逻辑以关闭该外键。基本上,每一行都由
(id, tenant_id)
而不是(id)
索引。我尝试过这种方法,但不喜欢这种方法,原因有很多:它使应用程序更复杂,它可能导致难以发现的bug,并且它没有提供数据库级别的隔离。 - 每个租户一个{应用服务器,django设置文件和适当的db}。又名穷人的多租户(实际上是富人的,考虑到它所涉及的资源)。我不想为每个租户启动一个新的应用服务器,为了可扩展性,我希望任何应用服务器都能够为任何客户端调度请求。
想法/h3>到目前为止,我最好的想法是做一些类似django-tenant-schemas
的事情:在第一个中间件中,抓取django.db.connection
并摆弄数据库选择,而不是模式。我还没有完全考虑过这在池化/持久连接方面意味着什么
我追求的另一个死胡同是特定于租户的表前缀:先不说我需要它们是动态的,即使是全局表前缀在Django中也不容易实现(参见拒绝票5000等)。
最后,Django多数据库支持允许你定义多个命名数据库,并根据实例类型和读/写模式在它们之间进行交互。没有帮助,因为没有基于每个请求选择db的工具。
问题
有人做过类似的事情吗?如果有,你是如何实现它的?
我已经做了类似的事情,最接近第一点,但不是使用中间件来设置默认连接,而是使用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
中设置当前客户db。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;
语句猜测所有数据库都可以通过单个连接访问。
缺点/strong>
- 设置起来很复杂,要使其正常工作需要相当多的层。
- 线程本地数据的需求和使用是模糊的。
- 视图充斥着数据库选择代码。这可以使用基于类的视图来抽象,以与中间件选择默认数据库相同的方式根据请求参数自动选择数据库。
- 选择数据库的上下文管理器必须以这样一种方式包装在查询集周围,即在计算查询时上下文管理器仍然是活动的。
建议strong>
如果你想要灵活的数据库访问,我建议使用Django的数据库路由器。使用中间件或视图Mixin,它们会根据请求参数自动为连接设置一个默认数据库。您可能不得不求助于线程本地数据来存储要使用的默认数据库,以便在遇到路由器时,它知道要路由到哪个数据库。这允许Django使用它现有的持久连接到一个数据库(如果需要,它可能驻留在不同的主机上),并根据请求中设置的路由选择要使用的数据库。
这种方法还有一个优点,即如果需要,可以通过使用QuerySet using()
函数选择默认数据库以外的数据库来覆盖查询的数据库。
为了记录,我选择实现我的第一个想法的变体:在早期请求中间件中发出USE <dbname>
。我也以同样的方式设置CACHE前缀。
我在一个小型生产站点上使用它,根据请求主机从Redis数据库查找租户名称。到目前为止,我对结果很满意。
我已经把它变成了一个(希望可恢复)github项目在这里:https://github.com/mik3y/django-db-multitenant
你可以创建一个自己的简单的中间件,从你的子域或任何东西确定数据库名称,然后为每个请求在数据库游标上执行一个USE语句。看看django-tenants-schema代码,这就是它所做的事情。它是psycopg2的子类,并发出相当于USE的postgres,"set search_path XXX"。您也可以创建一个模型来管理和创建您的租户,但是这样您就需要重写django-tenants-schema的大部分内容。
在MySQL中切换模式(db名称)应该没有性能或资源损失。它只是为连接设置一个会话参数。