第12章 Schema 演进:字段出现不等于语义已经迁移
一个可空列带来的两种状态
源表最初没有 note,后续真实执行 ALTER TABLE 增加 VARCHAR 可空列。数据库中旧行立即具有该列的默认 null 语义,但没有因此为每一行产生一次普通 UPDATE。连接器会学习新的表结构,后续行事件能够携带 note;已经存在于查询库中的旧文档却仍然可能完全没有这个键。
缺少字段与字段值为 null 在许多查询语言中并不相同。即使业务暂时把它们视为等价,完整对账也需要统一表示。因此本实验把 schema 迁移拆成明确动作:先新增源列,再对 live 投影已有文档显式补入 note:null,然后消费后续包含新字段的更新。这个回填由我们的投影迁移 SQL 完成,不是 Debezium 自动更新全部历史行。
真实演练怎样进行
控制程序执行源库 ALTER,再更新 id 一,把 note 写为可识别字符串并递增 revision。然后对 live 世代执行 JSONB 文档归一化,最后启动消费者读取真实 binlog 更新。检查要求收到更新操作,查询 id 一的 note 等于预期字符串,并将所有源行与投影行完整比较。
随后重建连接器创建新快照,新世代直接读取含 note 的完整表结构。新世代与 live 世代比较相等,证明显式迁移后的在线文档和重新生成的文档具有同样语义。这个组合验收比只检查新增字段能被 JSON 解析更强,也能发现旧行缺少默认键的问题。
为什么完整替换有助于理解
消费者从 after 构造当前完整业务文档,已知基础字段固定读取,note 存在时加入。它没有直接把任意新字段全部暴露给查询层。这样字段变更仍需经过代码评审,避免源表突然增加敏感列就自动进入可搜索副本。代价是每次新增查询字段都需要更新投影契约与兼容逻辑。
对已知字段完整替换,可以避免字段被源端清空后投影仍保留旧值。若采用局部 patch,需要区分“没有提供字段”和“明确设置 null”,还要定义删除字段的行为。不同查询引擎对 null 和缺失的索引策略也不同,替换存储后必须重新验证,不能只比较 Python 字典输出。
兼容窗口比一条 DDL 更长
生产上线通常需要旧消费者和新消费者短暂共存。安全的加字段流程可以先让读取端容忍缺失和 null,再增加源字段,部署写入与投影支持,完成历史回填,最后启用依赖新字段的功能。每一步都应当能够回滚或暂停,不让新功能先于数据准备完成。
删除字段、修改类型和重命名通常比新增可空列更危险。重命名可能在事件上表现为旧字段消失、新字段出现,消费者若仍强制读取旧名会失败。类型变化可能改变 converter 的编码和精度。当前实验只验证新增可空字符串,不声称已经覆盖任意 DDL 自动迁移。
结构历史也是恢复数据
连接器需要正确表结构才能解释日志。清空 schema 历史后尝试从旧 binlog 位置继续,可能让解码缺少上下文。连接器内部历史 topic 应当按官方要求维护,而不是把它当成可以随意压缩和删除的普通业务 topic。我们给在线和重建连接器分别配置历史 topic,避免互相覆盖。
消费者自己的文档版本也值得记录。当前代码规模小,通过发布版本和固定字段契约追踪;更复杂系统可以在投影元数据中保存 schema version,使读取层明确处理旧版本。版本字段不能仅仅用于展示,还应参与迁移判断和验收,避免一批文档永久停留在旧结构。
回填必须有独立验收
手工回填能修复结构差异,也可能覆盖刚刚到达的新值。实验在受控有限阶段先完成源更新、再统一补 null、后消费新消息,因此最终新值不会被回填覆盖。持续生产流中不能直接照搬这个顺序,需要使用只补缺失字段的条件、分批迁移和并发版本保护,或者选择新世代重建。
本书的迁移 SQL只用于当前实验时间线。它证明新增列、显式默认值迁移和后续真实事件能够共同收敛,不是通用在线无锁迁移框架。部署前必须根据实际并发写入和查询行为补充测试。
本章练习
将 note 改为必填字段,给出源库、旧消费者、新消费者、历史文档和读取功能的上线顺序。再设计一种只为缺失键补 null 的迁移条件,解释它为何比无条件覆盖更适合持续消费场景。