1、代码能回滚,数据不能

一次发布:新版本把 user.name 拆成了 first_name + last_name,代码写得漂漂亮亮。上线后发现问题,回滚——新版本已经把 1000 条数据写成了新格式,旧代码读不懂了。服务彻底瘫痪。

应用代码可以回滚,数据库变更往往"覆水难收"。 这使得数据库演进(Database Evolution)成为发布中最危险、最需要谨慎的一环。本课专门讲它。

2、为什么数据库变更这么难

代码数据库
无状态、可随时替换有状态,数据是命根子
回滚 = 换回旧版本回滚 = 可能丢数据/结构不兼容
一份代码一个版本新旧代码可能同时运行(滚动/灰度)
出错影响一次部署出错影响所有历史与未来数据

核心矛盾:滚动发布/灰度期间,新旧两版代码会同时访问同一个库。 所以数据库变更必须向后兼容:

数据库的当前结构,要能同时满足"旧代码"和"新代码"。

3、迁移(Migration):数据库的版本控制

迁移把数据库的每次结构变更,记录成一个有序、可执行、可追溯的脚本:

migrations/
  001_create_tasks.sql
  002_add_due_date.sql
  003_add_index_on_status.sql
  004_split_user_name.sql        ← 本次发布

每个迁移分 up(正向) 和 down(回滚):

-- 002_add_due_date.up.sql
ALTER TABLE tasks ADD COLUMN due_date DATE;

-- 002_add_due_date.down.sql
ALTER TABLE tasks DROP COLUMN due_date;

工具:Flyway、Liquibase(Java 生态)、Alembic(Python)、Rails Migrations、Prisma Migrate、golang-migrate。

迁移三原则:

  1. 只进不退(大部分情况):生产上 down 讲起来容易做起来难(数据已丢)。很多团队只写 up,靠向后兼容来保证可回滚。
  2. 幂等/可重复:脚本重复执行不出错(或用版本号表跳过)。
  3. 纳入版本控制:迁移脚本和代码一起提交、一起评审。

4、向后兼容的"扩展-收缩"模式

处理破坏性变更(改名、拆表、改类型)的黄金模式:扩展 → 迁移 → 收缩(Expand-Contract / Parallel Change)。

以"user.name 拆成 first_name + last_name"为例:

阶段 0(现状):表有 name 字段,代码用 name

阶段 1 · 扩展(Expand)
  ① 数据库:新增 first_name、last_name 两列(可空,不删 name)
  ② 代码:写的时候同时写 name 和 first/last(双写);
          读的时候优先读 first/last,回退读 name
  → 新旧代码都能工作 ✅

阶段 2 · 迁移历史数据
  跑后台任务,把存量数据从 name 拆填到 first/last

阶段 3 · 收缩(Contract)
  ① 代码:确认全部改用 first/last 后,停止写 name
  ② 数据库:确认无人再用 name 后,删除 name 列

要点:

  • 每一步都向后兼容,任意时刻都可以回滚到上一步。
  • 删字段放到最后(且通常隔一个发布周期),确认无人使用再删。
  • 加字段优先可空(或带默认值),避免锁表 & 老代码插入失败。
口诀:先加后删、先双写后切换、先兼容后清理。

5、危险操作清单(生产上要格外小心)

操作风险安全做法
加非空列(无默认)老代码插入失败、锁表先加可空列,再回填,再设非空
改列类型隐式转换、精度丢失、锁表加新列 → 回填 → 切换 → 删旧列
重命名列/表老代码找不到同"扩展-收缩"
大表加索引长时间锁表CREATE INDEX CONCURRENTLY(PG)等在线方式
删除列/表不可逆、旧代码崩确认无人用、备份、延后
大批量更新锁表、主从延迟分批、限速、错峰
直接改数据无审计、易错用脚本 + 事务 + 记录

大表 DDL 的教训:一条 ALTER TABLE 可能锁表几分钟,线上直接雪崩。先在小数据量环境测时机,或用在线 DDL 工具(pt-online-schema-change、gh-ost)。

6、数据迁移 ≠ 结构迁移

除了改结构,还有搬数据:

  • 同库数据清洗:如统一手机号格式(分批 + 可重入 + 记录)。
  • 跨库/跨系统迁移:如从 MySQL 迁到 PostgreSQL、老系统迁到新系统。

跨系统迁移的关键:

① 双写:新老系统同时写,保证两边一致
② 校验:比对两边数据,找出差异
③ 灰度:逐步把读流量切到新系统
④ 切换:读全部切新,老系统停写
⑤ 观察:确认无误后,下线老系统
迁移的可回滚性:要么能切回,要么保留原数据只读不动,作为兜底。

7、迁移与发布如何配合

发布前:
  □ 迁移脚本已评审、已在预发演练
  □ 变更向后兼容(旧代码能跑新库)
  □ 有备份(重要!)
  □ 大表操作已评估锁表影响

发布中:
  □ 先跑迁移(兼容版),再发新代码
  □ 观察数据库指标(连接、慢查询、主从延迟)

发布后:
  □ 确认新老代码都正常
  □ 数据校验(数量、抽样)
  □ 收缩步骤(删旧列)**下一个**发布再做

顺序:数据库先兼容 → 代码后切换 → 清理放最后。

8、常见误区

误区问题正解
迁移能 up 就能 downdown 常丢数据、难执行靠向后兼容保证可回滚
直接改列名/类型旧代码崩、锁表扩展-收缩四步走
发布时跑大表 DDL锁表、雪崩在线 DDL / 错峰 / 分批
删字段立刻删无法回滚延后一个周期,确认无人用
不做备份出事无法恢复变更前必备份
迁移脚本不入库环境间不一致脚本进版本控制,纳入评审

9、动手实验 🎯

任务:为贯穿项目「团队任务清单」做一次向后兼容的数据库演进。

  1. 搭迁移工具:选一个(Flyway/Alembic/原生脚本),把现有表结构变成第一个迁移脚本 001。
  2. 做一次扩展-收缩:把 tasks.title 拆成 title + 备注,或给任务加一个 priority 字段——严格走四步(扩展 → 回填 → 切换 → 收缩)。
  3. 验证兼容性:在"只加列"阶段,用旧代码插入一条数据,确认成功。
  4. 写 up 和 down:为 002 迁移写回滚脚本,并在测试库演练一次回滚。
  5. 分批更新:写一个分批回填脚本(每批 100 条),模拟大数据量场景。
  6. 演练一次"发布中迁移":备份 → 跑迁移 → 部署新代码 → 校验数据 → 规划下一周期的收缩。
  7. 反思:如果这次迁移在发布后 10 分钟必须回滚,你的数据能安全回退吗?为什么?

下一课:系统退役与迁移。

10、今天你带走的三句话

  1. 代码可回滚,数据难回滚;数据库变更必须向后兼容,让新旧代码共存。
  2. 扩展 → 迁移 → 收缩:先加后删、先双写后切换、先把删字段拖到最后。
  3. 结构变更与数据搬运都走"可回滚路径":备份、分批、双写、灰度、校验。

下一课:系统退役与迁移。

标签: 软件工程, 项目管理, 开发流程

添加新评论