数据库表结构变更如何设计才能兼容灰度发布?
简化版
表结构变更要遵循向前兼容和分阶段发布:先加可空字段或新表,代码双写或兼容读,再回填数据,确认稳定后切读,最后清理旧字段。不要在一次发布里同时删除字段、改语义和上线新代码。
详细版
灰度发布期间,新旧代码可能同时运行。如果数据库变更只适配新代码,旧代码可能报错;如果直接删除旧字段,新代码没全量上线前也可能失败。
安全流程通常是 expand-migrate-contract:先扩展 schema,不破坏旧代码;再迁移和回填数据;最后在所有代码都切换后收缩旧结构。
典型例子是把 full_name 拆成 first_name 和 last_name。不能直接删 full_name,要先加新字段、双写、回填、切读,再删除。
完整版教学
一、为什么表结构变更会影响发布
线上发布不是瞬间完成的。灰度、滚动重启、多实例部署意味着一段时间内新旧代码共存。数据库却通常是共享的,一次 DDL 会影响所有版本代码。
如果新代码需要字段 nickname,但字段还没加,就会报错;如果先删除旧字段 name,旧代码还在读它,也会报错。
所以 schema 变更的核心是兼容窗口,而不是单纯执行 DDL。
二、expand-migrate-contract 是什么
这个模式分三步:扩展、迁移、收缩。
Expand:新增结构,保持旧逻辑可用
Migrate:双写、回填、切读
Contract:确认无旧依赖后删除旧结构
它的本质是把高风险的一次性大改,拆成多个可回滚的小步骤。
三、字段拆分例子怎么做
假设要把 full_name 拆成 first_name 和 last_name。
第一步加新字段,允许为空;第二步新代码写入时同时写旧字段和新字段;第三步后台回填历史数据;第四步读逻辑切到新字段,并保留旧字段兜底;第五步确认没有旧代码后删除旧字段。
| 阶段 | 数据库 | 代码 |
|---|---|---|
| 1 | 加新字段 | 旧代码不受影响 |
| 2 | 新旧字段共存 | 双写 |
| 3 | 回填历史 | 兼容读 |
| 4 | 保留旧字段 | 切新读 |
| 5 | 删除旧字段 | 清理旧逻辑 |
四、回填数据要可控
历史数据回填不能一次性 update 全表。大表更新会带来锁、redo/binlog、主从延迟和业务抖动。
可以按主键范围分批,例如每批 1000 行,低峰执行,记录进度,失败后可从断点继续。
UPDATE user_profile
SET first_name = ..., last_name = ...
WHERE id BETWEEN ? AND ?
AND first_name IS NULL;
条件里加 first_name IS NULL 是为了让回填幂等,重复执行也不会覆盖新写入。
五、删除字段是最后一步
很多事故来自“字段没人用了吧,删掉”。实际上报表、脚本、异步任务、老版本服务可能仍在依赖旧字段。
删除前要看代码搜索、SQL 日志、慢查询、BI 报表、数据同步任务和回滚计划。最好先下线读写,再观察一段时间,再真正删除。
数据库清理应该像拆桥:确认没有车、设置绕行、观察流量,再拆。
六、常见误区与追问
- 误区:DDL 执行成功就代表变更完成。 数据回填、代码切换、旧依赖清理才是完整变更。
- 误区:灰度发布只需要代码兼容。 数据库是共享资源,schema 也必须兼容新旧代码。
- 误区:字段删除可以和新代码一起发。 删除要放最后,确认没有旧依赖后再做。
- 追问:大表回填如何避免影响线上? 分批、限速、断点续跑、低峰执行,并监控主从延迟。
- 追问:如何支持回滚? 保留旧字段和双写一段时间,回滚代码仍能读到旧结构。
七、加强记忆
记忆钩子:表结构变更像修路,先拓宽路,再引流,再拆旧路;不能半夜把旧桥炸了再通知司机改道。
回答这题抓住“新旧代码共存”这个根因,再讲 expand-migrate-contract、双写、回填、切读、清理。它是数据库设计和发布工程的交叉考点。