跨服务数据同步规格:一致性设计与取舍

跨服务数据同步规格:一致性设计与取舍
Spec Coding 编辑部 · Spec-First 工程实践内容

我见过的每一份失败的跨服务同步规格都长得一个样:它把 happy path 写完就算交差。真正出事的地方,永远是作者没想到要写下来的那些——顺序、迟到事件、部分失败,以及上线第二天需要回填时到底要怎么办。

发布于 2026-03-01 · ✓ 已更新 2026-04-29 · 阅读约 6 分钟 · 作者:Spec Coding 编辑部 · 审校:编辑与事实核查政策

内容整理说明

复查日期:2026-04-29。本文作为定向参考保留,主要指导已并入 API 契约 Hub。它不再出现在 sitemap、RSS 和站内搜索推荐中。

第一个决策:push、pull,还是 log

规格必须先选定一种同步模型,其他所有问题的答案几乎都依赖于这个选择。我会强制作者在以下三种之间选一种:

把这几种混着用就是踩坑的起点。"我们既发事件,又暴露 GET 接口,还有一条 CDC 流"——这往往意味着你有三个互相不一致的半成品实现。规格里要挑一条主路径,任何次要路径都必须明确标成"仅限 fallback"。

顺序:所有人都跳过的那一段

"事件按顺序到达"这句承诺,几乎没人真正做得到。规格必须明确到底给什么级别的顺序保证:

不管规格最后怎么选,都必须写明目标端收到一条 version 比当前更旧的事件时要做什么。我的默认答案:记日志,丢掉。规格里要把这一点写死,否则评审者会默认"用最新的覆盖"——这是个 bug 量产工厂。

事件载荷:thin vs. fat

两种模式都合法。规格要挑一种,并写清楚为什么:

我倾向于在有严格延迟 SLA、或者源端是那种动不动就崩的遗留系统时选 fat events;在 PII 或数据最小化是强约束时选 thin events。不管选哪种,把理由写进规格——下一个要改这块的人会感激你让他知道 why。

双向写入时的冲突解决

如果目标端只读,这一节可以跳过。如果两边都能写同一个字段,规格就必须回答:谁说了算?

这一节的检验方式:让评审者描述——Alice 在 10:00:00 于 Service A 更新了客户邮箱,Bob 在 10:00:01 于 Service B 更新了同一个字段,两条事件在途中交叉,最后会发生什么?如果光看规格答不上来,这份规格就还没写完。

回填方案是契约的一部分

第一天上线。第七天有人发现,同步开始之前的 5 万条记录在目标端完全缺失。规格里本来就应该回答:我们怎么补?

对账:不性感但要命的那一块

任何长时间运行的同步都会 drift。一定会。规格必须定义一个对账 job:

能捞到真实故障的验收标准

- 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 响应。否则数据已经落表但用户仍看到旧状态,问题会被误判成前端缓存。

关键词:cross-service sync · event ordering · backfill plan · reconciliation job · change data capture

编辑说明与免责声明

本文用于软件工程教学与实践参考,不构成法律、税务或投资建议。示例场景用于解释规格方法,不对应真实客户数据。