必须在settings.py的databases中定义'default'(主库)和'replica'(从库)两个别名,engine与name须一致;自定义databaserouter实现db_for_read返回'replica'、db_for_write返回'default',并重写allow_relation和allow_migrate以防止跨库join和从库迁移。

如何在 Django 的 settings.py 中定义多个数据库(主库 + 从库)
读写分离的前提是 Django 能识别多个数据库,且明确区分主从角色。Django 本身不自动判断“读”或“写”,靠的是路由逻辑和手动指定(如 using 参数),所以第一步必须在 DATABASES 配置中声明至少两个数据库,并给它们起有意义的别名(比如 'default' 和 'replica')。
关键点:主库必须叫 'default' —— 这不是强制语法要求,但几乎所有第三方路由类(包括 Django 自带的 DatabaseRouter 示例)都默认把 'default' 当作写库;如果你改掉它,后续路由逻辑容易出错。
-
'default'配置为你的主数据库(支持读写),确保HOST指向主库地址 -
'replica'配置为只读从库,USER应使用权限受限的账号(仅SELECT) - 两个库的
ENGINE、NAME必须一致(否则表结构不同步会导致查询失败) - 建议显式设置
'ATOMIC_REQUESTS': False和'AUTOCOMMIT': True,避免事务意外跨库
为什么必须自定义 DatabaseRouter 类而不是靠 using 硬编码
硬编码 .using('replica') 确实能发读请求到从库,但会立刻带来维护灾难:每个 Model.objects.filter(...) 都得加一遍,漏一个就打到主库;更麻烦的是 select_related、prefetch_related、count()、exists() 这些隐式查询根本没法手动指定 —— 它们默认走 'default',结果全压在主库上。
自定义路由才是可控方案,核心在于重写四个方法:
-
db_for_read(model, **hints):返回'replica'(或轮询列表),但需排除某些敏感模型(如User的 session 表可能未同步) -
db_for_write(model, **hints):一律返回'default' -
allow_relation(obj1, obj2, **hints):若两对象跨库(比如主库User关联从库Log),必须返回False,否则ForeignKey查询报错 -
allow_migrate(db, app_label, model_name=None, **hints):迁移只允许发生在'default',从库不能跑migrate
注意:hints 里可能有 'instance'(用于判断是否刚 save 过),可据此让刚写入的对象后续读仍走主库(解决主从延迟问题)。
QuerySet 手动切库时为何 .using('replica').select_related() 仍可能查主库
这是因为 select_related 会生成 JOIN,而 Django 默认要求 JOIN 的所有表必须在同一个库中。如果你的 Author 在 'default',Book 在 'replica',即使你调用 Book.objects.using('replica').select_related('author'),Django 也会拒绝执行并抛出 DatabaseError: Cannot combine queries from differing databases。
解决方案只有两个:
- 确保关联模型物理上在同一库(即从库也同步
Author表)—— 这是最常见做法,依赖 MySQL 主从复制或逻辑复制工具 - 改用
prefetch_related(发两条独立查询),并在路由中确保两次查询都落到同一库(靠db_for_read一致性实现)
另外,.values()、.values_list() 后接 .using() 是安全的,但 .annotate() 若含子查询,子查询仍走默认库,需额外处理。
主从延迟导致刚写入数据查不到?transaction.atomic 不是万能解
很多人以为包个 transaction.atomic(using='default') 就能保证后续 .using('replica') 查到最新数据,其实不能 —— 事务隔离级别再高,也挡不住从库复制延迟。MySQL 半同步复制下延迟通常几十毫秒,异步复制可能达秒级。
- 最稳妥方式:对刚创建/更新的对象,显式用
.using('default')读一次(绕过路由) - 次选:在路由的
db_for_read中检查hints.get('instance'),若存在且刚保存过(比如有_state.adding == False),强制返回'default' - 避免用
time.sleep(0.1)等待 —— 不可靠且拖慢响应
真正难处理的是「写后立即列表页刷新」这类场景:用户发帖后跳转到帖子列表,此时从库还没同步新帖。这种必须结合前端乐观更新或服务端主动等待(如轮询主库),不能寄希望于数据库配置。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











