数据库迁移的零停机方案:Expand-Contract 模式与生产环境实战
2026/7/7 10:09:04 网站建设 项目流程

数据库迁移的零停机方案:Expand-Contract 模式与生产环境实战

一、数据库迁移最危险的时刻,不是数据迁移本身,而是应用代码和数据库 schema 不兼容的那段时间窗口

「零停机迁移」这个词,听起来像是一个可以一键完成的操作——把旧数据库的数据同步到新数据库,切换流量,完成。但实际生产环境中的数据库迁移,远比这个复杂。如果你在迁移过程中需要修改表结构(加字段、删字段、改字段类型、拆表、合表),「切换流量」就不再是一个瞬间完成的操作,而是一段时间窗口——在这段时间里,有的应用实例已经用了新 schema,有的还在用旧 schema,数据库需要同时兼容两种格式。

Expand-Contract 模式(扩展-收缩模式)就是为解决这个问题而生的。它的核心思路是:把一次「不兼容的数据库变更」拆成三个可兼容的过渡阶段——扩展阶段(新旧格式共存)、迁移阶段(数据从旧格式迁移到新格式)、收缩阶段(删除旧格式)。在每一个阶段,应用代码和数据库 schema 都是兼容的,不需要在切换时刻停机。

但这个模式有一个重要前提:你需要能控制应用代码的发布和数据库 schema 的变更顺序。如果你用的是托管数据库服务,或者你的团队没有 DBA,这个模式的执行会更难——不是技术上不可行,而是需要在流程上保证「先发布兼容代码,再做 schema 变更,最后清理旧格式」的顺序。

二、Expand-Contract 模式的三个阶段:以「为用户表增加独立手机号字段」为例

flowchart TD A[阶段1: Expand 扩展] --> B[新旧格式共存] B --> C[阶段2: Migrate 迁移] C --> D[数据双写/回填] D --> E[阶段3: Contract 收缩] E --> F[删除旧格式] A --> G[1. 新字段允许 NULL] A --> H[2. 应用代码同时写新旧字段] A --> I[3. 应用代码读新字段, 回退到旧字段] C --> J[异步迁移已有数据] J --> K[校验数据一致性] E --> L[1. 停止写旧字段] E --> M[2. 删除旧字段]

用一个具体例子来说明。假设你的users表里,用户手机号存在profileJSON 字段里(profile->'phone'),现在你要把它拆成一个独立的phone字段,方便做索引查询和格式校验。

阶段一:扩展。第一步,用ALTER TABLE users ADD COLUMN phone VARCHAR(20)添加新字段,允许 NULL。第二步,修改应用代码,在写入用户资料时,同时写profile->'phone'phone字段(双写)。读取时,优先读phone字段,如果为 NULL,回退到profile->'phone'(先读新、回退旧)。这个阶段的代码发布后,新注册用户和更新的用户会有phone字段,老用户仍然是 NULL。

阶段二:迁移。用脚本把已有用户的手机号从profile字段迁移到phone字段。迁移时可以分批进行,避免对数据库造成太大压力。迁移完成后,做数据校验:确保每条记录的phone字段和profile->'phone'一致(或者phone不为 NULL 时一致)。这个阶段完成后,所有用户的phone字段都有了有效值。

阶段三:收缩。第一步,修改应用代码,停止写profile->'phone',只写phone字段。第二步,确认没有代码再依赖profile->'phone'作为手机号的权威来源后,可以删除这个 JSON 路径,或者至少停止维护它。如果确定不再需要,可以用ALTER TABLE删除phone字段——但这一步要谨慎,最好先观察一段时间。

三、生产环境的关键细节:迁移脚本的幂等性、可暂停性与数据校验

数据库迁移脚本和生产应用代码有一个本质区别:应用代码出问题了可以重新发布,迁移脚本出问题了,数据可能已经部分变更,恢复成本很高。生产环境的迁移脚本,必须满足三个条件:幂等性(可以重复执行而不产生副作用)、可暂停性(可以随时中断和恢复)、以及可校验性(可以验证迁移结果的正确性)。

幂等性的实现,通常通过在脚本里先检查目标状态再决定是否执行。比如「给所有用户迁移手机号」的脚本,不应该盲目执行UPDATE,而应该先WHERE phone IS NULL只更新还没迁移的记录。这样即使脚本中途失败,重新运行时不会重复迁移已经处理过的记录。

可暂停性的实现,通常通过在迁移脚本里加入「批次处理 + 进度记录」。不要用一个UPDATE语句更新所有记录——如果表里有几百万行,这个语句会锁表很久,而且中途不能暂停。正确的做法是用主键范围或者创建时间分批更新,每处理一批就记录进度(如写入一个迁移状态表),中断后可以从上次进度继续。

数据校验是迁移脚本里最容易被省略、但出了问题最致命的环节。校验可以分为两种:结构校验(新字段是否有值、格式是否正确)和内容校验(新字段的值和旧字段的值是否一致)。对于关键数据的迁移,建议在迁移完成后、收缩阶段之前,做一次全量校验,确保新旧数据的一致性。

-- 数据校验示例:检查 phone 字段和 profile->phone 的一致性 SELECT id, phone, profile->>'phone' as old_phone FROM users WHERE phone IS NOT NULL AND phone != profile->>'phone' LIMIT 100;

如果这个查询返回了记录,说明迁移有数据不一致的问题,需要先排查再继续。

四、工具与平台:降低迁移风险的工程手段

对于大型迁移,手写 SQL 脚本和迁移程序容易出错。工程上应该使用专门的数据库迁移工具或者平台来降低风险。

Schema 变更管理:工具如FlywayLiquibaseprisma migratedrizzle-kit可以管理 schema 变更的版本,支持回滚,并且能和 CI/CD 集成。它们的核心价值不是「帮你写 SQL」,而是「帮你管理变更的顺序和状态」,确保开发、预发和生产环境的 schema 一致。

数据迁移工具:对于大表的数据迁移,工具如gh-ost(GitHub 开源)、pt-online-schema-change(Percona Toolkit)可以在不锁表的情况下做表结构变更和大批量数据迁移。它们的原理是创建一个新表,逐步迁移数据,最后做原子切换——整个过程对线上业务几乎无感知。

对于使用 ORM 的团队,迁移通常分两层:schema migration(用 ORM 的迁移工具管理表结构变更)和data migration(用脚本或者专门的数据迁移任务处理数据内容的迁移)。这两层迁移应该分开管理:schema 迁移通常和代码发布绑定,data 迁移通常独立运行,因为它的执行时间更长,失败处理也更复杂。

最后,数据库迁移的「演练」是非常重要的。在生产环境执行迁移之前,务必在预发环境或者一个生产数据快照的环境里完整跑一遍迁移流程,包括扩展、迁移、校验和收缩的所有步骤。只有演练过的迁移,才应该在生产环境执行。

五、总结

数据库迁移的零停机方案,核心在于把「不兼容的变更」拆成「多个兼容的过渡步骤」。Expand-Contract 模式通过扩展(新旧共存)、迁移(数据搬迁)、收缩(清理旧格式)三个阶段,让应用代码和数据库 schema 在整个迁移过程中始终保持兼容。生产环境中,迁移脚本的幂等性、可暂停性和可校验性,是降低迁移风险的三大工程保障。工具可以辅助但不替代严谨的流程:备份、演练、灰度、回滚方案,每一项都不可省略。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询