Skip to content
返回维护与 LTS

数据库迁移

用版本化、可逆的迁移无停机演进 schema。

为什么

数据库 schema 变更是软件中最险操作 — 它改变无法重新部署消除的持久状态。坏迁移锁表 10 分钟、损坏数据或使应用无法启动。有纪律的方式是版本化、可逆迁移配 expand-contract 模式:先扩展(加新列/表、双写),再迁移回填,再收缩(停写旧、删旧)。每步独立可部署可逆,故步 3 出问题无需从备份恢复 — 只不做步 3。这是无维护窗口改 1TB 表 schema 的方式。

何时使用

schema 必须变时。绝不手跑原始 ALTER TABLE — 总通过版本化迁移工具。

如何做

1. 生成迁移:npx knex migrate:make add_users_display_name(或 prisma migrate dev)
2. 写 UP 迁移(扩展):ALTER TABLE users ADD COLUMN display_name TEXT
3. 写 DOWN 迁移(可逆):ALTER TABLE users DROP COLUMN display_name
4. 部署双写代码(同时写旧和新)— 旧代码在新 schema 上仍工作
5. 回填:批量从现有数据填 display_name(UPDATE ... WHERE display_name IS NULL LIMIT 1000)
6. 部署从新列读且停写旧的代码
7. 收缩:最终迁移删旧列(监控期确认无读取后)
8. 每步各自部署 + 迁移;绝不一步合并扩展 + 收缩

示例

# Knex 迁移:加 display_name(扩展)
exports.up = function(knex) {
  return knex.schema.alterTable('users', t => {
    t.text('display_name').nullable();  // 扩展:加列
  });
};
exports.down = function(knex) {
  return knex.schema.alterTable('users', t => {
    t.dropColumn('display_name');        // 可逆
  });
};

# 回填脚本(批量、幂等、可续)
async function backfill() {
  let done = 0;
  while (true) {
    const rows = await db.raw(`
      UPDATE users SET display_name = name
      WHERE display_name IS NULL
      RETURNING id LIMIT 1000`);
    if (rows.length === 0) break;
    done += rows.length;
    await sleep(100);  // 节流避免锁竞争
  }
}

# 收缩迁移(监控确认双读已去后)
exports.up = (knex) => knex.schema.alterTable('users', t => t.dropColumn('name'));

常见问题

  • ⚠一步合并扩展和收缩 — 若收缩(删列)失败无法回滚因应用已停写。
  • ⚠不可逆迁移(DROP COLUMN 无备份)— down 迁移是 no-op,坏部署意味数据丢。
  • ⚠一个巨型 UPDATE 回填 — 锁表数分钟;1000 行分块带 sleep。

最佳实践

  • ✓总用 expand-contract:加新、双写、回填、切读、删旧 — 每步各自部署。
  • ✓每迁移可逆(写真 DOWN)并在 staging 测 UP 然后 DOWN 然后 UP。
  • ✓小幂等分块带节流回填 — 绝不单 UPDATE 锁全表。