第14章 完整对账:证明内容相等而非指标相近
从行数开始,但不要停在那里
源表和投影都显示两千行,看起来令人放心。实际上源表可能有 id 一到两千,投影却缺少 id 二、多出一个已删除对象,行数仍然相同。再加上字段错位、价格单位和 null 语义,单纯数量比较远远不足。行数适合作为快速探针,不能作为最终正确性证明。
本书从源库按 id 排序读取全部业务字段,从投影读取相同世代里未删除的 document,并按 id 排序。实际通过条件是两个完整列表直接相等。随后才把规范 JSON 序列化后的 SHA256 写入报告,便于跨运行核对。摘要帮助压缩证据,却不替代程序真正执行的内容比较。
比较前先统一语义
整数价格、整数 active 与 revision 在两端保持相同类型。新增 note 后,源库旧行的 null 必须在投影中表现为同样键和值。若一边没有键,另一边键为 null,当前契约会判为不同。这种严格性有助于发现 schema 迁移遗漏,也要求我们先定义什么叫等价。
真实生产可能存在时区、精度、排序、字符归一化和默认值问题。对账程序应共享业务契约,而不是共享消费者的全部实现。如果两者复用同一个错误转换函数,可能一起把错误当正确。实验源侧直接读取数据库行,投影侧直接读取文档,没有用消费事件重新构造源侧预期,从而减少这种同源错误。
六次比较覆盖不同阶段
初始快照加增量比较验证接入衔接;重复回放比较验证幂等;崩溃重启比较验证重复恢复;停机追平比较验证积压;schema 演进比较验证新增字段与默认值迁移;全量重建比较验证新世代。每次都保存单独报告,使失败可以定位到某一阶段,而不是只在最后发现所有结果都不对。
对账也会检查删除后的结果,因为源库不存在的对象不会出现在源列表,而投影查询固定排除 deleted。若消费者没有删除标记,或者查询忘记过滤,列表长度或具体 id 都会不一致。这个检查覆盖业务可见性,但不会检查内部删除文档是否已经从磁盘擦除,那是不同目标。
对账时的时间参照
两个数据库之间没有共同读事务。如果源库在读取过程中继续变化,而投影在另一个时间被读取,结果差异可能来自时间边界,而不是永久错误。实验为有限批次停止写入后进行比较,因此参照稳定。生产持续系统需要固定水位、快照、变更窗口或允许重试的差异分类,不能每次比较都把短暂延迟当成数据损坏。
一种做法是按对象业务版本判断是否已经可比较:只核对源版本不再变化并且投影已经达到相应版本的对象。另一种是建立一致快照与日志截止边界。这些方法需要来源支持,不能简单取两个系统各自的当前时间。时钟同步有帮助,但不能把独立提交变成原子提交。
大规模对账如何扩展
数据量大时可以按稳定主键范围分段,记录每段行数、键集合摘要和字段摘要,先定位差异段,再逐行钻取。分段必须防止范围遗漏和重叠,不能只比较随机样本。哈希摘要还应包含字段名称和明确的编码规则,避免拼接歧义;例如两个字符串字段直接连接会让不同元组产生相同字节串。
本书使用结构化 JSON、固定排序与明确字段,因此可重复生成摘要。SHA256 相同仍是基于哈希抗碰撞假设的压缩证据,实际实验还比较原始列表。生产是否保留逐行明细,要结合敏感数据和证据体积决定,可以保留差异键与脱敏字段,而不是把整库内容上传到公开 CI。
发现差异之后不要立即补丁
先保留源快照、投影状态、最后交付位置与相关消息,再判断差异类型。少量对象落后可能是延迟,永久缺失可能是位点跳过,旧值覆盖可能是键路由或顺序错误。直接在投影上手工修复虽然能让一次比较通过,却可能被之后重放再次覆盖,也可能掩盖系统性错误。
修复应当对应原因:消费者逻辑有误则发布修正并重建,个别消息失败则按保留的输入安全重试,源数据错误则在权威源修复。对账是定位与验收工具,不应变成绕过同步协议的第二写入入口。
差异报告要避免泄露源数据
公开CI适合合成数据,不适合把真实客户行直接上传artifact。生产对账可以记录差异主键、字段名、版本和受控摘要,把敏感原值留在受权限保护的系统里。摘要也可能受到低熵字段枚举推测,不能自动当成脱敏。证据设计需要同时满足定位与最小暴露,两者都应在上线前确定,而不是出故障后把整库导出到临时公共位置。
本章练习
构造两份行数相同但内容不同的数据,说明数量和最大 id 为什么都会通过。再写一个分段对账方案,明确边界、规范编码、差异明细和持续写入期间的重试条件。