跨服务数据同步规格:一致性设计与取舍
我见过的每一份失败的跨服务同步规格都长得一个样:它把 happy path 写完就算交差。真正出事的地方,永远是作者没想到要写下来的那些——顺序、迟到事件、部分失败,以及上线第二天需要回填时到底要怎么办。
内容整理说明
复查日期:2026-04-29。本文作为定向参考保留,主要指导已并入 API 契约 Hub。它不再出现在 sitemap、RSS 和站内搜索推荐中。
第一个决策:push、pull,还是 log
规格必须先选定一种同步模型,其他所有问题的答案几乎都依赖于这个选择。我会强制作者在以下三种之间选一种:
- Event push(事件推送)。源端发出事件,目标端订阅。适合目标端需要数据新鲜度、且源端不想知道有谁在监听的场景。
- Target pull(目标拉取)。目标端轮询接口或查询 change feed。适合目标端要自己控制负载、可以容忍一定延迟的场景。
- Shared log(共享日志)。源端写入 Kafka / Kinesis 之类的系统,多个目标端各自独立回放。适合有三个以上消费者、或者需要 replay 的场景。
把这几种混着用就是踩坑的起点。"我们既发事件,又暴露 GET 接口,还有一条 CDC 流"——这往往意味着你有三个互相不一致的半成品实现。规格里要挑一条主路径,任何次要路径都必须明确标成"仅限 fallback"。
顺序:所有人都跳过的那一段
"事件按顺序到达"这句承诺,几乎没人真正做得到。规格必须明确到底给什么级别的顺序保证:
- Per-key ordering(单键有序)。同一个实体 ID 的所有事件按提交顺序到达。这是大多数场景真正需要的,也是分区日志能给你的。
- Global ordering(全局有序)。代价极高。除非真的非要不可,否则别承诺这个。
- No ordering(无序)。目标端必须有能力调和乱序事件,通常靠一个单调递增的
version或updated_at。
不管规格最后怎么选,都必须写明目标端收到一条 version 比当前更旧的事件时要做什么。我的默认答案:记日志,丢掉。规格里要把这一点写死,否则评审者会默认"用最新的覆盖"——这是个 bug 量产工厂。
事件载荷:thin vs. fat
两种模式都合法。规格要挑一种,并写清楚为什么:
- Thin events(瘦事件)。事件只说"实体 X 变了",目标端再通过 API 回取详情。队列便宜、读到的永远是最新数据,但同步能跑的前提是源端 API 可用。
- Fat events(胖事件)。事件里直接带上实体的完整快照。目标端可以直接 apply。源端挂了同步也能活,但你现在把 PII 塞进了事件总线,而且必须给载荷版本化。
我倾向于在有严格延迟 SLA、或者源端是那种动不动就崩的遗留系统时选 fat events;在 PII 或数据最小化是强约束时选 thin events。不管选哪种,把理由写进规格——下一个要改这块的人会感激你让他知道 why。
双向写入时的冲突解决
如果目标端只读,这一节可以跳过。如果两边都能写同一个字段,规格就必须回答:谁说了算?
- Last-writer-wins 加时间戳。简单,但遇到 clock skew 就崩。
- Source of truth(权威源)。一边是权威,另一边是可被覆盖的缓存。明明白白写下来。
- CRDT 或 merge 函数。如果你真的需要这个,你自己心里清楚。否则别碰。
- 拒绝并升级。冲突丢到人工处理队列。慢,但对资金类或合规类数据是安全的。
这一节的检验方式:让评审者描述——Alice 在 10:00:00 于 Service A 更新了客户邮箱,Bob 在 10:00:01 于 Service B 更新了同一个字段,两条事件在途中交叉,最后会发生什么?如果光看规格答不上来,这份规格就还没写完。
回填方案是契约的一部分
第一天上线。第七天有人发现,同步开始之前的 5 万条记录在目标端完全缺失。规格里本来就应该回答:我们怎么补?
- 源端有没有暴露历史 change feed?能追溯到哪一天?
- 有没有带分页和 cursor 语义的批量导出接口?
- 目标端怎么区分一条 backfill 事件和一条实时事件?(通常加一个
origin: backfill标记,免得遥测误触发重复写入告警。) - backfill 的限流是多少,才不至于把实时流量挤死?
- 怎么验证 backfill 真的成功了——行数比对、checksum 抽样、还是全量对账?
对账:不性感但要命的那一块
任何长时间运行的同步都会 drift。一定会。规格必须定义一个对账 job:
- 频率。每晚?每小时?按需?
- 范围。全表比对,还是抽样?
- 发现不一致时的动作。自动重同步修复,还是告警等人工决策?
- 成功指标。"差异率低于 0.01%"——给个真实数字。
能捞到真实故障的验收标准
- Given a source emits events for entity X When the target is offline for 30 minutes Then on recovery the target catches up within 5 minutes And no events in that window are permanently lost - Given two events for the same entity arrive out of order When the older event is processed after the newer one Then the target state reflects the newer event And the older event is logged as stale - Given the sync has been running for 24 hours When the reconciliation job runs Then fewer than 0.01% of rows show a diff And all diffs are auto-repaired or surfaced as alerts
评审时我看的那个信号
我用的质量信号是:规格有没有描述运维同学在第 30 天、系统出幺蛾子时看到的画面?如果它只描述了第一天的 happy path,那它就不是一份同步规格——它只是一张交接便签,迟早变成凌晨三点的 oncall 告警。
评审时看什么
这篇文章适合用在跨服务同步方案评审时。别从“原则”聊起,直接拿一条真实改动来对照,看看规格里还缺什么。
- 写清事件乱序时谁赢。
- 说明回填和实时同步如何区分。
- 给冲突解决一个明确规则,不要留给实现临场发挥。
- 上线后用对账任务证明两边没有漂移。
同步规格如果只写 day one happy path,就不够。评审重点应该放在 day thirty:数据不一致时谁能看见、谁来修。
例子:账户邮箱变更要写清 identity、billing、CRM 谁是源头;重复事件如何去重;漏消费时哪一个 reconciliation job 修复;用户在同步延迟期间会看到旧邮箱还是新邮箱。
落地例子
客户状态同步可以按完整生命周期写:billing 发出 status.changed 事件并带 idempotency key;CRM 记录最新 sequence number;客服后台在 reconciliation 完成前显示“数据可能滞后”。删除也要同样清楚:软删除、tombstone 事件、保留窗口,以及消费者确认后清理孤儿记录的任务。
这样写看起来细,但它解决的是事故里的常见问题:谁的数据可信、重复消息怎么处理、漏消息怎么补、用户看到不一致时由谁解释。
同步规格还要写“可观察性”。至少包括队列积压、重试次数、最后成功同步时间和 reconciliation 修复数量。没有这些信号,团队只能等用户报错才知道两个系统已经分叉。
同步规格要写清楚水位线
跨服务同步最怕“我已经发了”和“我没收到”同时成立。规格里要有水位线、重放范围、去重键和落后报警。否则排查时只能翻日志猜。
Sync contract: - source: billing.invoice_events - ordering key: account_id - cursor: monotonically increasing event_id - dedupe key: event_id + consumer_name - replay window: 30 days - lag alert: consumer cursor behind source by 10 minutes - backfill: writes through same handler, marks mode=backfill
边界:不是所有字段都该实时同步。报表字段可以批处理,权限和余额字段通常需要更强的时效保证。先按用户可见风险分级。
落后报警要能指向责任方
同步延迟告警不能只说“consumer lag high”。它要带 source topic、consumer name、cursor、最后成功 event_id、失败状态和 owner。这样值班人员知道是生产者停了、消费者挂了,还是某条坏数据卡住了队列。
同步测试至少要覆盖重复事件、乱序事件、坏数据、消费者重启和 backfill。每个测试都断言目标表状态、cursor、dedupe 记录、错误日志和回滚策略。只测 happy path 的同步基本等于没测。
如果目标服务暴露 API,还要说明同步状态如何对外可见:last_synced_at、source_event_id、sync_status、error_code 和 retry_after。测试既要看目标表,也要看 API 响应。否则数据已经落表但用户仍看到旧状态,问题会被误判成前端缓存。
继续阅读
编辑说明与免责声明
本文用于软件工程教学与实践参考,不构成法律、税务或投资建议。示例场景用于解释规格方法,不对应真实客户数据。
- 作者信息:Spec Coding 编辑部
- 编辑政策:编辑与事实核查政策
- 联系方式:联系页面