你的增量数据流漏掉迟提交的行:构建可信的游标
有一行迟到,是因为水位线不是提交边界。先走一遍故障,再定义覆盖、边界、保留期与安全恢复。
这是我在两个不同引擎里各见过一次的故障。一次导出在 10:00 运行,记录它见过的最大最后修改时间戳。一个在 09:58 开始的事务仍然打开,在 10:03 提交,却带着开始时刻的时间戳。下一次导出只请求比 10:00 更新的行,于是永远看不到它。传输量很漂亮,数据却是错的。
走一遍丢掉一行的导出
追踪事件顺序,而不是 SQL。应用在事务开始时写入行的修改时间,而不是提交时。导出器在事务仍打开时读取挂钟水位线。随后事务带着比已保存水位线更旧的修改时间提交。消费者的下一批从水位线之后开始,于是这一行落在游标后面,再也不会被选中。
没有任何地方记录错误,因为没有任何东西失败。这正是这类缺陷代价高昂的原因:增量流很快,行数看着合理,缺失的行则在几周后变成一张没人能复现的对账工单。
序列号有同样形状的缺陷,原因不同。单调 ID 告诉你值的分配顺序,而不是事务的提交顺序。拿到小号的事务可能在大号之后提交,所以基于分配顺序的游标同样会留洞。
数据流必须给出的五项承诺
选择传输方式之前,先向源系统要一份包含五个条款的契约。缺任何一条,你手里就是启发式而不是变更流。
- 覆盖:插入、更新和删除,每条都带应用它所需的稳定键。
- 边界:游标值意味着什么,一致的一批包含什么。
- 一致性:如何在不混用互不兼容源状态的前提下读取相关表。
- 保留期:断开连接的消费者可以从已保存位置恢复多久。
- 重新初始化:当该位置失效时,有受支持的完整快照。
受支持的机制可以满足全部五项。SQL Server 的变更跟踪文档就是一个具体的公开实例:它定义了最小有效版本,既记录变更也记录删除,并描述了一个横跨校验、版本捕获和相关变更查询的快照事务。它是向源系统提出要求的范例,而不是可以照抄到另一个引擎里的语法。
把检查点当作管道的一部分来设计
消费者保存的位置才是真正的状态机,所以要按状态机来写。游标只能通过已经应用该批并提交目标事务的工作流前进。
cursor = durable_checkpoint() # last fully applied position
batch = extract(cursor, upper_bound) # one consistent batch
apply(batch) # idempotent at the target
commit()
checkpoint(batch.upper_bound) # only now
如果投递中途失败,请对账同一批,而不是另造一个源位置。用更新的上界重新抽取,正是让部分应用变成永久空洞的路径。镜像规则在另一侧同样成立:目标端的幂等应用能防重复,但对抽取从未看见的变更无能为力。
删除需要记录,而不是推断
比较仍然存在的行,无法告诉消费者哪些键消失了。从不记录删除的源只留给你两个不完美的选项:保留足够长的键历史来做比对,或按计划执行显式的全量对账。两者都是有成本的设计选择,而只要没人负责,就一个都不存在。
保留期也是同一类决定。每条受支持的变更流都有窗口;把它写进契约,并在消费者检查点接近时告警。停摆两周后醒来的消费者,应该读到明确的“需要重新初始化”错误,而不是悄悄收到一批不完整的数据。
然后衡量增量是否值得
退回逐键加哈希比对有时是对的,也值得知道它的代价。你仍在传输每一行的信息,哈希给不了跨表的一致快照,哈希方案还带来表示与碰撞的问题。目标是更少字节加上可验证的边界;而悄悄丢掉已提交变更的更少字节,根本不是增量。
最后修改列只是线索。能扛过迟提交、删除和长时间停摆的游标,才是契约。