2025年4月20日·阅读约1分钟

带检查点的增量数据同步:安全对齐各系统

带检查点的增量数据同步通过使用游标、哈希和恢复令牌帮助你保持系统一致,从而在无需重导入的情况下安全恢复。

带检查点的增量数据同步:安全对齐各系统

为什么全量重导入总是出问题

全量重导入看上去简单可靠:删掉、重载、完成。但实际上它们往往是造成同步变慢、账单增加和数据混乱的最容易的方式之一。

第一个问题是时间和成本。每次运行都拉取整个数据集意味着你会反复重新下载相同的记录。如果你每晚同步 500,000 名客户,即使只有 200 条记录发生了变化,你也要为计算、API 调用和数据库写入付费。

第二个问题是正确性。全量重导入经常产生重复(因为匹配规则不完美),或者用导出的旧数据覆盖了较新的编辑。许多团队还会看到总数随时间漂移,因为“删掉并重载”在中途默默失败。

典型症状包括:

  • 运行后系统之间的记录计数不一致
  • 记录以细微差别出现两次(电子邮件大小写、电话格式)
  • 最近更新的字段回滚到旧值
  • 同步有时“完成”但漏掉一段数据
  • 每次导入窗口后支持工单激增

检查点只是一个小的保存标记,表示“我处理到这里了”。下次你从该标记继续,而不是重新开始。这个标记可以是时间戳、记录 ID、版本号,或 API 返回的令牌。

如果你的真实目标是让两个系统随时间保持一致,带检查点的增量数据同步通常是更合适的选择。当数据频繁变化、导出量大、API 有速率限制,或需要在崩溃后安全恢复(例如你在像 AppMaster 这样的平台上为内部工具构建作业时),它尤为有用。

在选择方法之前先定义同步目标

带检查点的增量同步只有在你清楚“正确”是什么时才好用。如果跳过这步直接去用游标或哈希,通常会因为规则没有写下来而在 Later 重建同步。

先把系统命名并决定谁拥有真相。例如:你的 CRM 可能是客户姓名和电话号码的可信来源,而计费工具是订阅状态的真相。如果两个系统都可以编辑同一字段,那么就不存在单一真相,你必须为冲突做计划。

接着定义“对齐”是什么意思。你需要始终精确匹配,还是接受几分钟内最终一致?精确匹配通常意味着更严格的排序、对检查点的更强保证,以及更小心的删除处理。最终一致性通常成本更低,并且更能容忍临时故障。

决定同步方向。单向同步更简单:系统 A 给系统 B 提供数据。双向同步更难,因为每次更新都可能成为冲突点,而且必须避免双方互相“修正”导致无休止的循环。

构建前需要回答的问题

把简单规则写下来并让团队达成一致:

  • 每个字段(或每种对象类型)的真相归属是哪一方?
  • 可接受的延迟是多少(秒、分钟、小时)?
  • 这是单向还是双向,哪些事件向哪个方向流动?
  • 删除如何处理(硬删除、软删除、墓碑)?
  • 当双方都修改同一条记录时如何处理?

一个实际的冲突规则集可以很简单,比如“订阅字段以计费为准,联系人字段以 CRM 为准,其他字段以最新更新时间为准”。如果你在像 AppMaster 这样的工具中构建集成,就把这些规则写到 Business Process 逻辑里,这样它们是可见且可测试的,而不是藏在某个人记忆里。

游标、哈希和恢复令牌:构建模块

带检查点的增量同步通常依赖下面三种可以安全存储并重用的“位置”之一。合适的选择依赖于源系统能保证什么,以及你需要抵抗哪类故障。

游标检查点是最简单的。你保存“我最后处理到的东西”,比如最后的 ID、最后的 updated_at 时间戳或序列号。下一次运行时请求该点之后的记录。当源能保证一致排序且 ID 或时间戳可靠递增时,这种方法工作良好。但当更新延迟到达、时钟不同步,或记录可以被插入到“过去”(例如回填数据)时,它就会失效。

哈希在游标不足以判断变化时很有用。你可以对每条记录进行哈希(基于你关心的字段),仅在哈希变化时同步。或者对一个批次做哈希以快速发现漂移,然后再深入查找。逐记录哈希精确但增加存储和计算成本;批量哈希便宜但不容易定位到底哪条记录变了。

恢复令牌是源方发出的不透明值,通常用于分页或事件流。你不解析它们,只存储并传回以继续。令牌在 API 复杂时很有用,但可能会过期、在保留窗口后失效,或在不同环境下行为不同。

该用什么,以及可能出的问题

  • 游标:快速且简单,但要注意乱序更新。
  • 逐记录哈希:精确检测变化,但成本更高。
  • 批量哈希:廉价的漂移信号,但不够具体。
  • 恢复令牌:分页最安全,但可能过期或一次性使用。
  • 混合(游标 + 哈希):当 updated_at 不完全可信时很常见。

如果你在 AppMaster 中构建同步,这些检查点通常保存在一个小的“sync state”表里,每次运行都能在不猜测的情况下恢复。

设计你的检查点存储

检查点存储是让带检查点的增量同步可靠的小部件。如果它难以读、容易被覆盖,或没有绑定到特定作业,你的同步看起来正常,直到某次失败后你只能靠猜测修复。

首先,选好检查点的存放位置。数据库表通常最安全,因为它支持事务、审计和简单查询。如果你已经在使用并且它支持原子更新,键值存储也可行。配置文件只适用于单用户、低风险的同步,因为它难以加锁且容易丢失。

要存什么(以及为什么)

检查点不仅仅是游标。保存足够的上下文以便调试、恢复和检测漂移:

  • 作业标识:作业名、租户或账号 ID、对象类型(例如 customers)
  • 进度:游标值或恢复令牌,加上游标类型(时间、ID、token)
  • 健康信号:上次运行时间、状态、读取和写入的记录数
  • 安全信息:最后一次成功的游标(而不是最后一次尝试的游标),以及最近失败的短错误信息

如果你使用变更检测哈希,也要存哈希方法的版本号。否则以后你更改哈希算法时可能会误把所有东西都认为“已变更”。

版本控制与多个同步作业

当数据模型变化时,要为检查点做版本管理。最简单的方法是添加一个 schema_version 字段,并为新版本创建新行,而不是修改旧数据。保留旧行一段时间以便回滚。

对于多个同步作业,要对所有项目做命名空间隔离。一个好的键是 (tenant_id, integration_id, object_name, job_version)。这能避免两个作业共享同一游标并安静地跳过数据的经典 Bug。

具体例子:如果你把同步做成 AppMaster 内部工具,把检查点存到 PostgreSQL,每个租户和对象一行,并只在批次提交成功后更新它。

逐步实现增量同步循环

在用户之前收到通知
当检查点卡住时,通过邮件、短信或 Telegram 在用户之前收到通知。
设置告警

带检查点的增量同步在循环简单且可预测时效果最好。目标很简单:以稳定的顺序读取变化,安全写入,然后只有在确认写入完成后才推进检查点。

一个你能信赖的简单循环

首先,选一个对于同一条记录永远不变的排序方式。时间戳可以用,但要包含一个破局器(比如 ID)以防两次更新时间相同导致顺序混淆。

然后按下面流程运行:

  • 决定你的游标(例如:last_updated + id)和页面大小。
  • 获取比存储的检查点更新的下一页记录。
  • 对每条记录在目标端执行 upsert(不存在则创建,存在则更新),并捕获失败。
  • 提交成功的写入,然后持久化从最后处理记录得到的新检查点。
  • 重复。如果页面为空,休眠一会儿再试。

把检查点更新与抓取分开。如果过早保存检查点,崩溃可能会悄悄跳过数据。

无重复的回退与重试

假设调用会失败。当抓取或写入失败时,用短退避重试(例如:1s、2s、5s)并设置最大重试次数。通过使用 upsert 并使写入幂等来保证重试安全(相同输入产生相同结果)。

一个小而实用的例子:如果你每分钟同步客户更新,你可以每次抓取 200 条变化,上载它们,然后只有在写入成功后才把最后一个客户的 (updated_at, id) 存为新游标。

如果你在 AppMaster 中实现,可以在 Data Designer 用一个简单表建模检查点,并在 Business Process 中以受控流程完成抓取、上载和检查点更新。

让恢复安全:幂等性与原子检查点

如果你的同步可以恢复,它会在最糟糕的时刻恢复:超时、崩溃或部分部署之后。目标很简单:重跑同一批次不应产生重复或丢失更新。

幂等性是安全网。通常通过可重复而不改变最终结果的写入方式获得。实践中通常使用 upsert 而不是 insert:用稳定键(如 customer_id)写入记录,当记录已存在时更新它。

一个好的“写键”是你在重试时能够信任的东西。常见选项是来自源系统的自然 ID,或第一次看到记录时生成的合成键。为其添加唯一约束,这样数据库即使在两个 worker 竞争时也能强制规则。

原子检查点同样重要。如果你在数据提交之前推进了检查点,崩溃会导致你永久跳过记录。把检查点更新当作与写入相同的工作单元处理。

这是一个简单模式:

  • 读取自上次检查点以来的变化(游标或令牌)。
  • 使用去重键对每条记录执行 upsert。
  • 提交事务。
  • 只有在提交成功后才持久化新检查点。

乱序更新和晚到数据是另一个常见陷阱。记录可能在 10:01 更新但比 10:02 的记录更晚到达,或 API 在重试时可能发出较旧的更改。通过存储源系统的 last_modified 并应用“最后写入生效”规则来保护自己:只有当传入记录比已有记录更新时才覆盖。

如果需要更强的保护,保留一个小的重叠窗口(例如重读最近几分钟的变化),并依赖幂等 upsert 忽略重复。这会增加一些工作量,但会让恢复变得枯燥无味——这正是你想要的。

在 AppMaster 中,相同的思路可以在 Business Process 流程中清晰映射:先做 upsert 逻辑并提交,然后把游标或恢复令牌作为最后一步存储。

破坏增量同步的常见错误

调度可靠的同步运行
运行处理重试和退避且不会重复写入的定时同步循环。
设置作业

大部分同步 Bug 并不在代码。它们来自一些看起来安全但在真实数据面前会露馅的假设。如果你想让带检查点的增量同步保持可靠,尽早注意这些陷阱。

常见失败点

一个常见错误是过度信任 updated_at。一些系统在回填、时区修正、批量编辑或读修复时会重写时间戳。如果你的游标只是时间戳,你可能会错过记录(时间戳回退)或重处理大量范围(时间戳跳跃向前)。

另一个陷阱是假设 ID 是连续或严格递增的。导入、分片、UUID 和已删除行都会破坏这个假设。如果你用“最后见到的 ID”作为检查点,间隙和乱序写入会让记录被遗漏。

最致命的错误是基于部分成功推进检查点。例如,你抓取 1,000 条记录,写入了 700 条然后崩溃,但仍保存了抓取得到的“下一个游标”。恢复时,其余 300 条永远不会被重试。

删除也容易被忽略。源端可能是软删除(标记)、硬删除(行被移除)或“下线”(状态变更)。如果你只 upsert 活跃记录,目标端会慢慢漂移。

最后,模式变更会使旧哈希失效。如果你的变更检测哈希基于一组字段,添加或重命名字段会让“无变化”看起来像“已变更”(或反之),除非你对哈希逻辑做版本化。

更安全的默认做法包括:

  • 尽量使用单调递增的游标(事件 ID、日志位置)而不是原始时间戳。
  • 把检查点写入作为数据写入成功边界的一部分。
  • 明确跟踪删除(墓碑、状态转换或定期对账)。
  • 版本化你的哈希输入并保持旧版本可读。
  • 如果源端能重排更新,加入一个小的重叠窗口(重读最后 N 项)。

如果你在 AppMaster 中实现,请在 Data Designer 中把检查点建为独立表,并在单次 Business Process 运行内把“写入数据 + 写入检查点”步骤放一起,这样重试不会跳过工作。

监控与漂移检测(但不要太嘈杂)

将规则变为工作流
在一个清晰的 Business Process 中实现抓取、上载和检查点更新。
构建工作流

对带检查点的增量同步来说,好的监控不是“更多日志”,而是可信赖的几项关键数字。如果你能回答“我们处理了什么、用了多长时间、下次从哪里恢复?”,大多数问题能在几分钟内排查清楚。

每次同步执行时写一条简洁的一致运行记录。保持格式统一以便比较运行并发现趋势。

要记录的内容示例:

  • 起始游标(或恢复令牌)与结束游标
  • 抓取记录数、写入记录数、跳过记录数
  • 运行时长与每条记录(或每页)的平均时间
  • 错误计数及最常见错误原因
  • 检查点写入状态(成功/失败)

漂移检测是下一层:它告诉你两边“都在工作”但慢慢分歧了。仅凭总数容易误导,所以把轻量级的总量检查与小样本抽查结合起来。例如每天比较一次两个系统中的活跃客户总数,然后随机抽 20 个客户 ID,核对几个关键字段(status、updated_at、email)。如果总数不一致但样本匹配,说明你可能漏掉了删除或过滤。如果样本不一致,说明你的变更检测哈希或字段映射可能有问题。

告警应当少而可操作。一个简单规则:只在需要人立即介入时才告警。

示例告警条件:

  • 游标卡住(结束游标 N 次运行不动)
  • 错误率上升(例如一个小时内从 1% -> 5%)
  • 运行变慢(时长超过正常上限)
  • 积压增长(新变化到达速度超过同步速度)
  • 漂移确认(两次检查总数不一致)

故障后可在不人工清理的情况下重跑,方法是从最后一次已提交的检查点恢复,而不是从最后“见到”的记录恢复。如果你使用小重叠窗口(重读最后一页),使写入幂等:用稳定 ID upsert,并且只有在写入成功后才推进检查点。在 AppMaster 中,团队常把这些检查做成 Business Process 流程,并通过邮件/SMS 或 Telegram 模块发送告警,让失败可见而不需盯着仪表盘。

上线前的快速检查清单

在把带检查点的增量同步投入生产前,做一遍快速检查,解决那些通常在最后引发惊喜的小细节。这些检查只需几分钟,却能避免几天的“为什么我们漏掉记录?”调试。

实用的上线前清单:

  • 确认你用于排序的字段(时间戳、序列、ID)确实稳定,并在源端有索引。如果它会在事后改变,游标会漂移。
  • 确认你的 upsert 键是唯一的,并且两个系统对它的处理一致(大小写敏感性、修剪、格式化)。如果一个系统存储为 "ABC" 而另一个存储为 "abc",你会得到重复。
  • 为每个作业和每个数据集单独存储检查点。一个“全局最后游标”听起来简单,但一旦你同步两个表、两个租户或两个过滤器就会出问题。
  • 如果源端是最终一致性的,加入一个小的重叠窗口。例如从 "last_updated = 10:00:00" 恢复时,从 09:59:30 开始并依赖幂等 upsert 忽略重复。
  • 规划轻量级对账:定期抽样(如 100 条随机记录)并比较关键字段以捕捉静默漂移。

快速现实测试:在运行中暂停同步并重启,验证结果是否一致。如果重启改变了计数或生成了额外行,在上线前修复它。

如果你在 AppMaster 中构建同步,请把每个集成流的检查点数据绑定到具体流程和数据集,而不是在无关自动化之间共享。

示例:在两个应用间同步客户记录

从一个干净的同步开始
先从一个干净的同步原型开始,然后将同一模式复用到对象与租户中。
开始项目

想象一个简单场景:CRM 是联系人数据的真相来源,你希望这些人在支持工具中存在(以便工单能映射到真实客户),或在客户门户中存在(以便用户登录查看账户)。

首次运行时做一次一次性导入。按稳定顺序拉取联系人,例如以 updated_at 加上 id 作为破局器。把每个批次写入目的地后,保存一个检查点,比如:last_updated_atlast_id。这个检查点就是今后每次运行的起点。

持续运行时只抓取比检查点更新的记录。更新很直接:如果目标已有 CRM 联系人则更新,否则创建。合并最棘手:CRM 常常合并重复并保留一个“胜出”联系。把这当作一次更新,同时把被淘汰的记录标记为不活跃(或映射到胜出者),避免在门户中出现两个对应同一人的用户。

删除通常不会出现在普通的“自上次更新以来”查询里,所以要为它们做计划。常见方式有源端的软删除标志、单独的“已删除联系人”流,或定期的轻量对账检查缺失 ID。

现在看失败场景:同步半途中崩溃。如果你只在最后存储检查点,会导致大量重处理。相反,为每个批次使用恢复令牌。

  • 启动一次运行并生成一个 run_id(你的恢复令牌)
  • 处理一个批次,写入目的地更改,然后原子性地保存与 run_id 绑定的检查点
  • 重启时检测该 run_id 的最后保存检查点并从那里继续

成功的样子很无聊:每日计数稳定,运行时间可预测,重跑相同窗口不会产生意外更改。

下一步:选定模式并减少返工

一旦第一个增量循环工作正常,避免返工的最快方法是把同步规则写下来。保持简短:哪些记录在范围内、冲突时哪些字段优先、每次运行后的“完成”标准是什么。

先从小处做起。选一个数据集(比如 customers)端到端运行:初次导入、增量更新、删除处理,以及在一次有意失败后恢复。现在修正假设要比后来添加五张表时容易得多。

有时完全重建仍然是正确的选择。当检查点状态损坏、标识符变更或模式变更破坏了你的变更检测(例如你用了哈希而字段含义改变)时,就做重建。重建时当作受控操作而不是紧急按钮。

一个安全的重导入方式:

  • 导入到影子表或平行数据集中,保持当前数据在线。
  • 验证计数并抽查样本,包括边缘情况(null、合并记录)。
  • 回填关系,然后在一次计划的切换中把读取者切到新数据集。
  • 在短暂回滚窗口后清理旧数据集。

如果你想在不写代码的情况下构建,AppMaster 可以把各部分放在一起:用 Data Designer 在 PostgreSQL 中建模数据,在 Business Process Editor 定义同步规则,运行计划作业来抓取、转换并 upsert 记录。因为 AppMaster 在需求变化时能重生成干净代码,它也能降低“我们需要再加一个字段”带来的风险。

在把更多数据集加入之前,把你的同步契约文档化,选定一种模式(游标、恢复令牌或哈希),并把一个同步做到完全可靠。然后把相同结构复制到下一个数据集。如果想快速试试,先在 AppMaster 中创建一个应用并运行一个小型定时同步作业。

容易上手
创造一些 惊人的东西

使用免费计划试用 AppMaster。
准备就绪后,您可以选择合适的订阅。

开始吧
带检查点的增量数据同步:安全对齐各系统 | AppMaster