数据库模式演进要让新旧应用版本在过渡期兼容
说明扩展与收缩、双读写、回填和约束验证如何降低在线模式变更风险,并保持回滚能力。
数据库模式演进要让新旧应用版本在过渡期兼容
在线系统升级时,旧实例、新实例、后台任务和数据库迁移可能同时运行。直接重命名字段、删除列或改变约束,会让其中某个版本立即失败。安全演进把破坏性变化拆成扩展、迁移和收缩阶段。
扩展阶段只增加兼容能力
先新增可空列、表、索引或兼容视图,让旧应用继续使用原结构。新应用可以开始写入新旧表示,或通过适配层读取任一版本。此阶段不能要求所有实例已经升级。
新增带默认值和非空约束的列可能重写表或长时间验证,具体行为依数据库版本而异。应在与生产规模相近的数据上测量锁、日志和复制影响。
迁移脚本必须幂等或有明确执行记录,失败后能判断执行到哪里。Flyway 等工具保证版本顺序,不自动保证 SQL 对在线流量无阻塞。
双写和双读是有限过渡
应用双写新旧字段能保持数据同步,但任一步失败都可能造成分歧。应在同一事务内完成可共库写入,跨系统则需要发件箱、重试和对账。
双读通常优先新值、缺失时回退旧值,便于逐步回填。回退次数应监测,达到零并经过稳定窗口后才能收缩。长期保留双逻辑会使每次修改都承担两套语义。
数据库触发器可集中兼容写入,但隐藏行为会增加调试难度。无论采用应用还是数据库实现,都应明确权威字段和过渡结束条件。
回填要控制批次和并发
历史行需要从旧结构转换到新结构。一次大事务会持有锁、产生大量 WAL、扩大复制延迟并使失败回滚昂贵。分批按稳定键更新,记录游标,并在批次间限速更可控。
回填必须能与在线写入并存。可使用条件更新避免覆盖新值,并对更新期间新增记录有明确捕获机制。转换逻辑要版本化,抽样与全量统计检查应分开。
完成后比较行数、空值、分布和业务聚合,不能只看脚本退出码。异常记录需要隔离和处置规则,不能因少量错误永久阻塞迁移又无人知晓。
索引和约束需要分阶段建立
大表创建索引可能阻塞写入,数据库常提供并发创建方式,但失败后会留下无效对象,需要清理与重试。新查询发布前,应确认索引可用并检查执行计划。
非空、唯一和外键约束可以先在应用写入路径执行,再清理历史数据,最后由数据库验证。数据库约束才是并发条件下最终防线,应用检查不能永久替代。
验证约束也会消耗 I/O。应选择低峰窗口、设置超时并监测复制。分区表的索引和约束行为需依据 PostgreSQL 当前版本文档验证。
事务隔离影响迁移观察
回填与在线事务同时运行时,不同隔离级别看到的数据快照不同。长事务会阻碍清理并使表膨胀,也可能在最后提交时遭遇冲突。
PostgreSQL 文档说明各隔离级别允许的现象。迁移程序应选择满足算法的最弱充分级别,并为序列化失败、死锁和锁超时设计重试,而不是无限等待。
读取旧值后再写新值的回填需要防止覆盖并发更新,可使用版本条件、行锁或原子表达式。具体策略要通过并发测试证明。
收缩只能在所有依赖退出后进行
旧应用、报表、导出任务和临时脚本都可能依赖旧列。收缩前应从查询日志、代码搜索、数据目录和责任人确认依赖,并保留观察期。
先停止旧写入,再停止旧读取,最后删除结构。字段重命名通常用“新增新名、同步、迁移、删除旧名”实现,而不是直接执行 rename。
删除后回滚旧应用将不可行,因此发布回滚窗口必须在收缩前结束。高风险删除应单独发布,并有完整备份和恢复演练。
每阶段都要有前进和停止条件
计划应写明兼容矩阵、指标、负责人、预计持续时间和退出门禁。发现数据分歧时,通常先暂停推进并修复,而不是立即回滚数据库结构,因为旧结构仍被保留。
模式、应用和消息契约是一个整体。数据库兼容但事件已经删除字段,旧消费者仍会失败。架构决策记录应覆盖全部接口。
安全演进的核心是让不可逆操作最后发生。扩展与回填提供证据,收缩只在证据充分后执行,这比追求单次“零停机 SQL”更可靠。
发布前后的核对
执行迁移前应估算受影响行数、锁类型、日志增长、复制延迟和回填时长,并在接近生产规模的数据上演练。迁移脚本成功结束并不代表业务数据已经正确,仍需比较行数、空值比例、唯一性、关键聚合和新旧读取结果。长时间回填应分批提交并记录进度,避免一次事务扩大锁与恢复成本。
发布完成后,要等所有旧应用实例退出并确认没有旧格式写入,再进入收缩阶段。删除旧列或约束之前应保留可审计的观察窗口;若回滚需要恢复已删除数据,单靠应用版本回退并不足够。
资料依据
- PostgreSQL Global Development Group:Transaction Isolation,https://www.postgresql.org/docs/current/transaction-iso.html
- PostgreSQL Global Development Group:Table Partitioning,https://www.postgresql.org/docs/current/ddl-partitioning.html