1. 为什么数据库变更难?因为它既“有状态”,又“有历史”
代码发布失败,你可以回滚镜像;但数据变更失败,你面对的是三件事: 已经写进去的数据、正在运行的旧代码、以及所有历史数据。 所以数据库交付的核心能力不是“会写迁移脚本”,而是“会设计兼容窗口”。
- 不可逆:删字段、改语义、重写历史数据。
- 长尾:数据量越大、分布越极端,越容易出现“只在生产出现”的边界样本。
- 耦合:应用版本、配置版本、数据版本必须对齐(上一章的 bundle)。
关键心法:你无法消除风险,但你可以把风险分散到多个小步骤,并让每一步都可判定、可止损。
2. Expand/Contract:给回滚留出窗口
这套模型你在上一章见过,这里我们把它落到数据库细节上:
- Expand:新增字段/表/索引、加默认值、双写、回填(仍兼容旧版本)。
- Switch:逐步把读写路径切到新结构(通常配合开关与金丝雀)。
- Contract:确认稳定后删除旧字段/旧表/旧代码路径(不可逆,尽量放到最后)。
图 1:Expand→Switch→Contract 的分阶段迁移。把不可逆动作推迟到最后,让回滚窗口在前期尽可能长。
3. 双写与回填:让历史数据追上新结构
迁移常见的“中间态”是:新字段存在,但历史数据还没填;新代码开始写新字段,但老代码还在写旧字段。 这时你需要把写路径与读路径都设计成可渐进切换。
3.1 写路径:单写 → 双写 → 单写(新)
- 单写(旧):系统原始状态。
- 双写:新旧字段/表同时写入(必须考虑幂等、失败重试与一致性)。
- 单写(新):确认稳定后停止旧写入。
3.2 读路径:旧读 → 回退读(fallback)→ 新读
- 回退读:优先读新字段;缺失则回退读旧字段(过渡期常用)。
- 一致性风险:双写与回填期间,读到“半迁移”数据很正常,要靠逻辑兜底。
图 2:双写 + 回填 + 回退读。过渡期允许中间态,但必须可观测(缺失率、分歧率)且可止损(开关切回)。
4. 零停机 DDL:别让迁移把生产锁死
DDL(建索引、改字段类型、加约束)最常见的事故是:一条迁移在生产上拿到了大锁,业务请求全部排队,系统看起来“像宕机”。 所以零停机 DDL 的目标是:避免长时间排他锁,把变更拆成“小块、可暂停、可恢复”。
- 小批量:分段处理,避免一次性扫描全表。
- 限流:迁移任务要让位于线上流量。
- 可暂停:任何长任务都必须可中断与续跑。
- 观察指标:锁等待、复制延迟、CPU/IO、慢查询。
重要提醒:不同数据库(Postgres/MySQL)与不同版本对“在线 DDL”的支持差异很大。你的策略必须与引擎能力对齐:先做“风险评估”,再做“执行计划”。
图 3:零停机变更控制面。迁移像后台作业:拆块、限流、可暂停、强观测;任何异常都能止损。
5. 最小落地清单(让 DB 迁移成为可交付能力)
- 默认向后兼容:任何发布都要允许新旧版本短暂共存。
- 迁移分阶段:Expand → Switch → Contract,破坏性动作最后做。
- 回填可控:小批量、限流、checkpoint、可暂停/续跑。
- 强观测与止损:锁等待、复制延迟、慢查询、错误率必须可见;有 kill switch。
- 把它放进 bundle:每次发布记录 schema stage,并把回滚/前滚策略写成运行手册。
你现在应该能回答:
1) 这次变更是 Expand 还是 Contract?有没有把不可逆动作推迟?
2) 写路径与读路径如何过渡?双写/回退读是否有开关与观测?
3) 线上 DDL 会不会拿大锁?如果会,拆块与限流方案是什么?
1) 这次变更是 Expand 还是 Contract?有没有把不可逆动作推迟?
2) 写路径与读路径如何过渡?双写/回退读是否有开关与观测?
3) 线上 DDL 会不会拿大锁?如果会,拆块与限流方案是什么?