TT Lab
开始
学习 学习路径 课程

集成与部署

你在读的时候,源头还在变

在 TT Lab 中继续学习

一句话总结

把不断变化的源分页拉取时,如果用编号(offset)来确定位置,行就会悄悄消失,而用最后看到的值(cursor)来确定位置,就不会消失。这个值不能只是一个时刻,而必须是 (updated_at, id) 两栏。

为什么需要它

有这样一种同步:“每天把客户列表全部拉取一遍”。60 万条,每次 1000 条,分成 600 次拉取。然而在循环这 600 次的期间,源也在不断变化。

假设按 ORDER BY updated_at 排序,每次用 LIMIT 1000 OFFSET 10000 拉取。就在我们拉取第 11 页之前,已经翻过去的页中有 3 行被更新,updated_at 变成了当前时刻。在排序顺序中,这 3 行会跑到最后面。这样一来,后面的所有行都会向前移动三格。偏移量 10000 原本指向第 10000 行,现在却指向第 10003 行。中间的 3 行,没有被任何人读取就这样过去了。

这类事故有三个特点。不会报错。条数大致对得上。每次漏掉的行都不一样。所以无法重现,几个月之后会以“这位客户在我们这边没有啊”的投诉形式回来。

工作原理

修复的办法,是用值而不是编号来确定位置。记住最后读取的那一行的排序键,下次请求时索要“比这个值更大的部分”。通常称为键集分页(keyset pagination)或游标分页。

오프셋:  "10000번째부터 1000개"     ← 앞이 밀리면 가리키는 곳이 달라진다
커서:    "이 값보다 큰 것 1000개"   ← 앞이 밀려도 이 값보다 큰 것은 그대로다

这里出现了第二个陷阱。排序键并不唯一。updated_at 以秒为单位,所以具有相同值的行往往有多个。如果只用 updated_at 一个值作游标,就会出现二者之一的情况。

所以游标要增加栏位直到唯一为止。通常 (updated_at, id) 两栏就足够,比较也是两栏一起比。即 (updated_at, id) > (:last_at, :last_id)。

水位线就是把那个游标带到下一次运行。为了中断时不必从头开始拉取,水位线必须保存在磁盘上。而且,什么时候保存水位线很重要。如果在处理收到的行之前保存,中断时就会丢失那一页;如果在处理之后保存,中断时会重新拉取那一页。重新拉取比丢失要好——所以大多数同步都按至少一次(at-least-once)来设计,并让接收的一方具备幂等性。

而且在这种方式下,重复是正常的。我们已经走过的行如果被更新,那一行就会跑到游标之后,再次被抓到。那不是 bug,而是“期间发生了变化”这一事实的如实反映。如果副本一侧以 id 为键进行覆盖,结果就是正确的。

最后是对账(reconciliation)。不要以为拉完了就万事大吉,要数一数。查看源的条数和副本的条数,以及两边都有的行,其值是否一致。只是条数对得上,并不算对——少了一条又重复了一条,条数就不会变。

用时刻做键时,表示方法也必须约定。RFC 3339 定义了互联网上使用的日期和时刻表示法。如果打算按字符串比较,就必须让位数和时区表示法在所有行中都相同——2026-09-10T00:00:00Z 和 2026-09-10T00:00:00+00:00 是同一个时间点,但在字符串比较中却不同。翻页的方式本身,也有很多 API 是通过 RFC 8288 的 Link 头给出 next,所以在对接之前,先确认对方给出的是什么。

在现场相遇的样子

第一,“全部重新拉取不就行了吗”这句话经常出现。数据量小的时候,这话没错。只不过,全量重新采集在循环期间也会发生变化,所以快照时点不是一瞬间,而是覆盖了整个遍历的时间,这一点是一样的。

第二,有些源不会更新 updated_at。如果无论修改哪个字段,时刻都不变,游标方式就永远看不到那次变更。对接之前,一定要问“修改什么会让这个时刻发生变化”。删除也是如此——行消失之后,靠游标是无法得知的,所以必须另有通知删除的方法。

第三,时钟会倒退。如果源服务器有好几台,时钟略有不同,后写入的行的 updated_at 可能比先写入的行更小。这样它就位于游标已经走过的位置,永远抓不到。所以有许多实现会把水位线稍微往回退一点(例如退回几秒之前),并接受由此产生的重复。

第四,第一次运行最危险。在第一次拉取 60 万条的期间,源仍在不断变化。最好在安静的时段运行第一次,结束之后立刻再运行一次,追上这期间的变更。

下一项实验要做什么

启动一台不断变化的源服务器,对全部 60 行做一份快照。用偏移量方式遍历,同时在途中修改源,并用数字确认行被漏掉、同时又有重复的现象。然后换成 (updated_at, id) 游标,看到在同样的情况下遗漏变为 0。把水位线保存到文件中,让第二次运行只拉取有变化的部分,并让页的边界恰好切在拥有相同时刻的五行之间,看看游标能否经受住。最后接续被中断的同步,对照源和副本,确认连值也一致。