Django 5.0 默认 migrate 不支持零停机,因其为同步阻塞操作,会锁表且要求新旧代码兼容同一表结构;添加非空字段等变更会导致老版本应用报错,必须分三阶段手动实施兼容性迁移。

零停机数据库迁移在 Django 5.0 中无法靠单次 makemigrations + migrate 自动实现,必须手动拆解、分阶段执行,并配合应用层兼容逻辑。
为什么 Django 的默认 migrate 不支持零停机?
Django 的 migrate 命令本质是同步阻塞操作:它会锁表(尤其在 MySQL/PostgreSQL 的某些 DDL 场景下),且要求新旧代码同时能读写同一套表结构。一旦你添加非空字段、删除字段或修改字段类型,老版本应用立即报错(如 django.core.exceptions.FieldError 或数据库层面的列不存在错误)。
典型失败场景包括:
- 上线前运行
python manage.py migrate,但旧进程仍在用老模型访问数据库 → 查询失败或写入丢失 - 新增
CharField(null=False, blank=False)并设默认值 → 迁移需为存量行填充数据,期间表被锁,API 超时 - 重命名字段后,旧代码仍调用
old_field_name→AttributeError
分三阶段实施:兼容性迁移(Django 5.0 推荐模式)
Django 5.0 没有内置“在线迁移”开关,但提供了更清晰的迁移依赖控制和 SeparateDatabaseAndState 支持,适合人工编排三阶段流程。核心是让数据库结构变更与代码变更解耦:
立即学习“Python免费学习笔记(深入)”;
-
第一阶段(部署前):只加兼容字段(如
new_column),不删旧字段,所有模型方法同时读写新旧字段;运行python manage.py makemigrations --empty myapp手动编写迁移,用RunPython做渐进式数据填充(避免大表锁) -
第二阶段(灰度发布):部署新代码(开始写
new_column,读优先用new_column但 fallback 到old_column);此时双写并存,数据库无结构风险 -
第三阶段(确认后):再建一次迁移,删
old_column;仅在此阶段运行migrate,且确保所有实例已升级到第二阶段代码
示例迁移片段(阶段一):
class Migration(migrations.Migration):
dependencies = [("myapp", "0001_initial")]
<pre class="brush:php;toolbar:false;">operations = [
migrations.AddField(
model_name="user",
name="email_normalized",
field=models.CharField(max_length=254, null=True, blank=True),
),
# 避免全表 UPDATE,用分批脚本替代 RunPython
migrations.RunPython(
lambda apps, schema_editor: None, # 占位,实际用 management command 分批更新
reverse_code=lambda apps, schema_editor: None,
elidable=True,
),
]
关键配置与避坑点(Django 5.0 特有)
Django 5.0 对迁移的原子性和数据库支持做了微调,以下设置直接影响零停机可行性:
- PostgreSQL 用户必须开启
atomic_migration = True(默认),否则单个迁移可能部分成功;MySQL 用户需确认innodb_lock_wait_timeout足够长,避免大迁移被 kill - 禁用
manage.py migrate --fake除调试外的任何场景 —— 它跳过真实 DDL,但后续迁移依赖会断裂 - 不要在迁移中调用
Model.objects.all()加载全量数据,改用iterator(chunk_size=1000)或原生 SQL 分页 - Django 5.0 的
AlterField在 PostgreSQL 上支持CONCURRENTLY(需手写SeparateDatabaseAndState),但 MySQL 仍无原生并发 DDL 支持,必须接受短时锁表
生产环境必须做的验证动作
即使流程跑通,漏掉下面任意一项都可能导致凌晨告警:
- 在 staging 环境用生产级数据量测试迁移耗时,记录
migrate命令输出中的 “Applying …” 行实际执行秒数 - 检查所有 Celery worker、ASGI server 实例是否完成滚动重启 —— 旧进程可能缓存了旧模型类,导致
FieldDoesNotExist - 对涉及的 API 接口做双写一致性校验(例如对比
old_column和new_column的值是否恒等) - 备份迁移前的数据库快照,并验证恢复流程(
pg_dump/mysqldump导出后能否psql成功导入)
最常被忽略的是:Django 迁移本身不管理应用进程生命周期。数据库结构就绪 ≠ 服务就绪。你得盯着进程、日志、监控指标,而不是只盯着 migrate 命令的返回码。









