Django多数据库读写分离的实质是通过自定义DatabaseRouter类,重写db_for_read和db_for_write方法实现查询/写入的数据库路由;它非开箱即用,需手动配置别名、注册路由器,并严格约束主从库权限与事务行为。

什么是Django多数据库读写分离的实质
Django本身不自动区分读/写操作,所谓“读写分离”是靠手动指定数据库别名 + 重写db_for_read和db_for_write方法实现的路由逻辑。它不是开箱即用的功能,而是基于DatabaseRouter类的一套约定。
- 路由器只影响
QuerySet执行时的数据库选择,不影响save()、delete()等模型方法的默认行为(除非显式传using=...) -
SELECT语句走db_for_read,INSERT/UPDATE/DELETE走db_for_write,事务内所有操作都强制使用同一库 - 主从延迟下,刚写入就立刻读可能查不到——Django不帮你做读主库兜底,得自己加
.using('default')
如何编写一个最小可用的读写分离路由器
核心是继承DatabaseRouter并实现两个方法。注意:Django 4.2+要求四个方法都存在(哪怕空实现),否则启动报错AttributeError: 'YourRouter' object has no attribute 'allow_relation'。
class ReadWriteRouter:
def db_for_read(self, model, **hints):
return 'replica'
<pre class="brush:python;toolbar:false;">def db_for_write(self, model, **hints):
return 'default'
def allow_relation(self, obj1, obj2, **hints):
return True
def allow_migrate(self, db, app_label, model_name=None, **hints):
return None
-
'default'必须是主库(支持写),'replica'必须是只读从库(不能写,否则IntegrityError或同步中断) -
allow_relation返回True表示允许跨库关联查询(如select_related),但实际会失败——Django不支持跨库JOIN,这里只是绕过校验 -
allow_migrate返回None表示迁移只在default上运行(避免在从库建表)
为什么get()或filter()有时还是走主库
这不是路由器失效,而是Django在以下场景会忽略路由规则:
- 模型方法里显式写了
using='default'(比如自定义Manager中调用了self.get_queryset().using('default').filter(...)) - 使用了
transaction.atomic():事务内所有查询强制走同一个库,且该库由第一个SQL决定(通常是写操作触发的db_for_write) - 查询涉及
ForeignKey反向关系且未预加载:Django会为每个外键对象单独发SELECT,但这些查询仍走db_for_read——除非你在select_related里指定了using
常见误判点:
- 认为
Model.objects.using('replica').all()是“读写分离”,其实这只是硬编码,和路由器无关 - 在
shell里测试时没清缓存,反复执行QuerySet可能复用前一次连接,看不出切换效果
生产环境必须检查的三件事
- 确保从库用户权限仅为
SELECT:避免意外写入导致主从不一致,MySQL报错ERROR 1290 (HY000): The MySQL server is running with the --read-only option不算异常,是预期行为
- 在
settings.py中配置CONN_MAX_AGE = 0:长连接容易卡在旧从库上,尤其K8s滚动更新后DNS没刷新,连接仍指向已销毁的Pod
- 所有
bulk_create、update、delete操作必须确认是否在事务中——如果事务里先filter().first()再save(),整个事务会落在replica上,导致OperationalError: attempt to write a readonly database
SELECT:避免意外写入导致主从不一致,MySQL报错ERROR 1290 (HY000): The MySQL server is running with the --read-only option不算异常,是预期行为settings.py中配置CONN_MAX_AGE = 0:长连接容易卡在旧从库上,尤其K8s滚动更新后DNS没刷新,连接仍指向已销毁的Podbulk_create、update、delete操作必须确认是否在事务中——如果事务里先filter().first()再save(),整个事务会落在replica上,导致OperationalError: attempt to write a readonly database
复杂点在于:读写分离不是开关,而是一组约束条件的组合。漏掉任意一环(比如忘记覆盖allow_migrate,或从库没关autocommit),都会让系统在某个边界场景下静默失败。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











