增量拉取以 updated_at > cursor 为过滤条件,这个条件有三个被普遍低估的失效场景:其一,源端可见延迟——业务已变更,源端对外视图晚数小时才可见,数据落在了游标已走过的区间;其二,窗口边界切割——updated_at 恰等于游标值的记录可能被相邻两轮同时跳过;其三,失败后游标错误推进。三者共同的表象是:任务日志零报错,报表与源端逐渐失联。
规范一:双窗口分工。 每轮同步执行两个窗口:游标窗口 [cursor, now) 抓新数据;回扫窗口 [cursor - overlap, cursor) 复查刚走过的区间,专收迟到修改。两窗口共用落库逻辑。
规范二:窗口宽度定量。 overlap = 源端最大可见延迟(实测值)+ 调度间隔 × 容错轮数。示例:可见延迟 2 小时、调度间隔 30 分钟、容错 2 轮,则 overlap = 3 小时。配置项必须注明实测依据,更换数据源时重新标定。
规范三:upsert 幂等落库。 回扫必然造成重复拉取,落库必须按业务号 upsert,并以版本号(updated_at)比较丢弃迟到的旧值。验收标准:同一窗口重复拉取三次,落库行数不变;旧版本不得覆盖新版本。
规范四:游标推进时机。 游标推进只允许发生在全部窗口全部落库成功之后。中途失败则游标不动,下一轮自然重拉。验收标准:人为制造拉取中途失败,游标不前进,重拉后数据完整。
参考实现:
from datetime import datetime, timedelta POLL_INTERVAL = timedelta(minutes=30) OVERLAP = timedelta(hours=3) # 可见延迟 2h + 容错 1h,换源须重新标定 def pull_orders(cursor: datetime, now: datetime): windows = [ (cursor, now), # 游标窗口:向前 (cursor - OVERLAP, cursor) # 回扫窗口:向后 ] max_seen = cursor for start, end in windows: page_token = None while True: resp = api.list_orders(updated_after=start, updated_before=end, page_token=page_token) for row in resp.rows: upsert_order(row.biz_id, row) max_seen = max(max_seen, row.updated_at) page_token = resp.next_token if not page_token: break save_cursor(max_seen) # 全部落库成功后推进 def upsert_order(biz_id: str, row): existing = db.get(biz_id) if existing and row.updated_at <= existing.updated_at: return # 迟到的旧版本,丢弃 db.save(biz_id, row)
成本说明:overlap 3 小时、30 分钟一轮,意味着每条数据平均被拉 6 次。该代价与漏改引发的对账成本不在一个量级;大表场景可将回扫范围限定于带状态字段的数据类型。
汇总验收清单:①历史订单状态回改后两个同步周期内对齐;②重复拉取不产生重复行;③中途失败游标不推进;④overlap 配置带实测依据注释。增量同步的完整性,取决于「改过的还能不能追上」。