数据库迁移的零停机方案:Expand-Contract 模式与生产环境实战
数据库迁移的零停机方案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 变更管理工具如Flyway、Liquibase、prisma migrate、drizzle-kit可以管理 schema 变更的版本支持回滚并且能和 CI/CD 集成。它们的核心价值不是「帮你写 SQL」而是「帮你管理变更的顺序和状态」确保开发、预发和生产环境的 schema 一致。数据迁移工具对于大表的数据迁移工具如gh-ostGitHub 开源、pt-online-schema-changePercona Toolkit可以在不锁表的情况下做表结构变更和大批量数据迁移。它们的原理是创建一个新表逐步迁移数据最后做原子切换——整个过程对线上业务几乎无感知。对于使用 ORM 的团队迁移通常分两层schema migration用 ORM 的迁移工具管理表结构变更和data migration用脚本或者专门的数据迁移任务处理数据内容的迁移。这两层迁移应该分开管理schema 迁移通常和代码发布绑定data 迁移通常独立运行因为它的执行时间更长失败处理也更复杂。最后数据库迁移的「演练」是非常重要的。在生产环境执行迁移之前务必在预发环境或者一个生产数据快照的环境里完整跑一遍迁移流程包括扩展、迁移、校验和收缩的所有步骤。只有演练过的迁移才应该在生产环境执行。五、总结数据库迁移的零停机方案核心在于把「不兼容的变更」拆成「多个兼容的过渡步骤」。Expand-Contract 模式通过扩展新旧共存、迁移数据搬迁、收缩清理旧格式三个阶段让应用代码和数据库 schema 在整个迁移过程中始终保持兼容。生产环境中迁移脚本的幂等性、可暂停性和可校验性是降低迁移风险的三大工程保障。工具可以辅助但不替代严谨的流程备份、演练、灰度、回滚方案每一项都不可省略。