前几天有个做电商的朋友跟我吐槽,说他们的Django项目上线半年后,用户量蹭蹭往上涨,结果数据库扛不住了——读请求太多,主库CPU经常飙到90%以上,页面加载慢得像蜗牛。他问我怎么办。我说,兄弟,你该上读写分离了。其实Django本身对多数据库的支持相当成熟,但很多人要么不知道,要么知道了觉得麻烦就拖着。结果呢?用户流失,老板拍桌子。今天我就跟你聊聊,怎么在Django项目里优雅地配置多个数据库,让读写分离这件事变得像喝水一样简单。

先别急着撸代码,咱们得搞清楚一个核心问题:什么时候该用多数据库配置。不是所有项目都需要,别为了炫技而炫技。当你发现主库的读压力明显大于写压力,比如一个电商网站,用户浏览商品、查看订单的频率远高于下单、支付;或者你的业务天然就有数据隔离需求,比如一个SaaS平台,不同租户的数据要存到不同数据库里;再或者你想把日志、历史数据这种冷数据放到廉价存储上——这时候,多数据库就是你的救星。Django的settings.py里,DATABASES配置项可以定义多个数据库连接,每个库用不同的别名,比如default、readdb、logdb。默认情况下,所有操作都走default,但你可以在代码里指定用哪个库。听起来很简单对吧?但坑往往藏在细节里。
配置多数据库的第一步,就是在settings.py里把数据库连接写清楚。假设你有一个主库用于写入,两个从库用于读取。代码大概长这样:
'ENGINE': 'django.db.backends.mysql',
'USER': 'writeuser',
'PASSWORD': 'writepass',
'HOST': 'masterhost',
'ENGINE': 'django.db.backends.mysql',
'PASSWORD': 'readpass',
'HOST': 'slavehost1',
'ENGINE': 'django.db.backends.mysql',
'PASSWORD': 'readpass',
'HOST': 'slavehost2',
这里要注意,用户名和密码最好分开,主库用写权限账号,从库用只读账号,安全第一。配置完这些,数据库层面就准备好了。但光有配置还不行,Django默认所有查询都走default,你得告诉它什么时候该用哪个库。
这时候就该Django的数据库路由(Database Router)上场了。路由是个Python类,里面定义几个方法,用来决定某个模型的操作该走哪个数据库。比如你想让所有读操作随机分配到两个从库,写操作只走主库。可以写个简单的路由:
class PrimaryReplicaRouter:
def dbforread(self, model, hints):
return random.choice(['readdb1', 'readdb2'])
def dbforwrite(self, model, hints):
def allowrelation(self, obj1, obj2, hints):
dblist = ('default', 'readdb1', 'readdb2')
if obj1.state.db in dblist and obj2.state.db in dblist:
def allowmigrate(self, db, applabel, modelname=None, hints):
return db == 'default'
然后在settings.py里注册它:。这样,每次你调用或者,Django都会自动根据路由规则选择数据库。是不是很优雅?但别高兴太早,有几个坑你得留神。
第一个坑:事务和读写分离不兼容。如果你在写操作的事务里接着做读操作,读请求可能被路由到从库,而从库数据还没同步过来,读到的是旧数据。解决方案是:在事务里强制读主库。你可以用指定数据库,或者更聪明点,在路由的方法里加个判断:如果当前请求有活跃事务,就返回主库。比如这样:
from django.db import transaction
def dbforread(self, model, hints):
if transaction.getconnection().inatomicblock:
return random.choice(['readdb1', 'readdb2'])
第二个坑:数据同步延迟。主从复制不是实时的,MySQL默认的异步复制可能有几毫秒甚至几秒的延迟。如果你的业务对实时性要求很高,比如用户刚下完单,紧接着查订单状态,这时候路由到从库可能会看到旧数据。一个折中办法是:给用户一个“刷新”按钮,或者把刚写的数据缓存到Redis里。更激进的做法是:在写操作后,把该用户的读请求强制指向主库一段时间。Django的方法可以临时覆盖路由,比如。
第三个坑:跨数据库关联查询。如果你的模型之间有关联,比如User和Order,但User在default库,Order在readdb1,Django默认不允许这种跨库关联,会抛出异常。路由的方法可以控制这一点,但我的建议是:尽量别做跨库关联。把关联的数据放到同一个库里,或者用API调用代替ORM关联。如果非要跨,可以用和时指定数据库,或者干脆手动写SQL。
除了读写分离,多数据库还有一个常见场景:分库分表。比如你的SaaS产品,每个租户的数据完全独立,可以给每个租户分配一个独立的数据库。这时候路由就不能硬编码库名了,得动态计算。你可以根据租户ID,在请求中间件里把当前租户的数据库连接信息注入到settings里,或者定义一个路由类,从请求的session或token里提取租户ID,然后返回对应的数据库别名。代码大概这样:
def dbforread(self, model, hints):
tenantid = hints.get('tenantid') or getcurrenttenantid()
return f'tenant{tenantid}'
def dbforwrite(self, model, **hints):
tenantid = hints.get('tenantid') or getcurrenttenantid()
return f'tenant{tenantid}'
然后每个租户的数据库连接,提前在DATABASES里定义好,或者动态加载。这种方法的好处是数据隔离性好,坏处是数据库数量多了之后,管理成本上升,比如迁移脚本要跑N次。Django的参数可以指定迁移目标库,比如。你可以写个脚本循环所有租户库跑迁移。
说到迁移,多数据库配置里还有个容易忽略的点:数据一致性。主从复制模式下,写操作只发生在主库,从库只读,数据同步靠数据库自己搞定。但如果你在代码里直接对从库执行写操作,比如调用了,那数据就乱套了。所以路由里的必须严格返回主库,同时你可以在模型层加个保护:重写模型的方法,如果参数不是default,就抛出异常。或者用Django的信号(signal)在里检查数据库。
另外,别忘了Django的缓存机制。很多时候,读写分离的瓶颈不在数据库,而在ORM的查询效率。比如你频繁查同一个用户的资料,每次都从从库读,即使从库负载不高,网络开销也大。这时候你应该用缓存,比如,然后读的时候先查缓存,缓存没有再去数据库。这样既能减轻数据库压力,又能降低延迟。Django的缓存框架支持多种后端,Memcached、Redis都行,配置起来也很简单。
还有个小技巧:利用Django的的方法,可以在代码里灵活切换数据库。比如有个报表功能,需要查历史数据,你可以这样写:。这样即使路由规则写死了,也能临时覆盖。但要注意,别滥用这种写法,否则代码里到处是,维护起来头疼。
说说性能监控。配置了多数据库不代表万事大吉,你得知道每个库的负载情况。Django的可以显示每个SQL查询走了哪个数据库,开发环境下很有用。生产环境可以用把数据库查询指标暴露给Prometheus,再配合Grafana做可视化。举个例子,你发现readdb1的查询延迟比readdb2高很多,可能说明负载不均衡,需要调整路由策略,或者给readdb1加硬件资源。
回到开头那个电商朋友的问题。我帮他配了三个从库,一个主库,路由里用了权重随机算法——根据每个从库的CPU和内存动态调整概率。同时,在用户下单后的一分钟内,该用户的读请求强制走主库,避免读到旧数据。迁移脚本只跑在主库上,从库通过MySQL的主从复制自动同步。上线后,主库CPU从90%降到了30%,页面加载时间从3秒降到0.5秒。他后来跟我说:“早知道Django多数据库这么简单,我早就该搞了。”
所以,别被“多个数据库”这个词吓到。Django把最复杂的路由逻辑封装好了,你要做的就是定义规则、处理边界情况。关键是理解你的业务场景——是读多写少,还是数据隔离,还是冷热分离?想清楚了,配置起来就是几行代码的事。剩下的,就是让Django替你把活干了。


