端云协同 API 设计:离线同步、弱网重试与版本兼容
用本地事务与 outbox 保住离线记录,以业务操作 ID 去重;设计逐项确认、游标失效、删除传播、冲突策略和内容包原子切换,并验证断电与重连。
更新与核对 · 2026-10-11
客户端离线记录了十次操作,重连时上报成功,但确认响应丢了。客户端重发后,云端应仍然只有十条记录。端云同步的目标是:本地操作不丢,重传不重复,冲突有规则,用户知道数据同步到了哪里。 这些约束同时适用于移动 App、桌面客户端与边缘设备。
先分数据的权威来源
离线优先让 UI 读取本地数据,并不意味着所有数据都由设备决定。操作事件可在本地生成;内容由云端版本管理;配额权益、支付结果由服务端权威确认,离线不能随意合并出更多余额。
用户操作
→ 同一本地事务:更新可见状态 + 写入 outbox
→ 后台提交带 op_id 的操作
→ 服务端去重并确认
→ 客户端只移除已确认条目
Outbox 是持久出站队列。操作与入队必须同事务,否则断电会留下“界面已改但从未同步”的状态。Android 离线优先指南[1]
图 1 · 传输允许重复,业务应用负责去重;下行数据与游标同事务提交。
上行:逐项确认和原子去重
操作 ID 在创建时生成,所有重试复用;服务端按用户或设备范围加 op_id 唯一约束,并绑定请求摘要。同 ID 不同内容是错误,不能静默返回成功。
{
"acked": ["op-1", "op-2"],
"retryable": [{ "id": "op-3", "reason": "temporary_unavailable" }],
"rejected": [{ "id": "op-4", "reason": "invalid_content_revision" }]
}
永久拒绝进入可恢复的错误队列,用户可查看或修正;临时错误退避,不删除。去重记录与业务写入原子提交,不能先查询再写入。跨服务写入需 outbox、唯一业务键或补偿与对账。
幂等窗口要覆盖预期离线周期。若只保留一天,设备一月后重传可能再次执行;长期业务 ID 或快照确认机制更重要。不要假定自定义幂等头让所有上游自动去重。Stripe 幂等实现范例[2]
冲突:不能把两个累计值直接相加
设备 A 和 B 都看到累计处理条数 100,A 增至 110,B 增至 120,相加成 230 显然不对。可以上报有唯一 ID 的操作事件或增量,去重后聚合;也可在适合的数据类型上使用明确设计的 CRDT。
| 数据 | 可选策略 | 必须防范 |
|---|---|---|
| 操作、审计事件 | 唯一事件追加并去重 | 重传、多设备重复记录 |
| 昵称与设置 | 服务端版本号加条件更新 | 并发覆盖用户意图 |
| 协作文档 | 领域合并或 CRDT | 实现与数据模型不匹配 |
| 权益、金额 | 服务端权威事务 | 离线合并产生重复权益 |
Last-write-wins 适合可容忍覆盖的数据,但可能丢更新。设备物理时钟不可信;逻辑或混合逻辑时钟帮助排序,并不自动解决冲突或真实性。重要更新使用 revision、If-Match 或业务规则检测冲突。
下行:游标、快照与删除一起设计
增量返回 changes 和不透明的 next_cursor。客户端在同一事务中应用变化并推进游标;崩溃后重复拉取,也应按对象版本幂等应用。
page = pull(local_cursor)
transaction:
apply_upserts_and_deletes(page.changes)
save_cursor(page.next_cursor)
服务端为游标定义有效期与保留边界。游标过期返回明确状态,客户端获取完整快照,再接续增量。快照与后续增量需要共同的稳定版本或水位,不能在下载中漏掉并发更新。
删除用可传播的墓碑或删除事件;保留期短于离线周期时,需要快照重建来识别消失数据。客户端待上报旧记录不得让已删除对象复活。授权改变也应刷新本地可见数据,并定义离线访问政策。
API 兼容:新增字段也可能破坏客户端
旧客户端未必忽略未知字段或枚举值,字段加法也需契约测试。固定旧版请求、响应和真实客户端解析器,验证新增字段、未知枚举、空值与错误体。
服务端按设备型号与版本下发能力,网关做协议适配,减少业务里散落版本判断。版本弃用依据存量、支持承诺和安全风险制定,不承诺永远支持所有旧版;确需停用时给升级与离线数据导出路径。
内容分发与多端权限
内容包使用版本 manifest、分块下载、校验和与签名。哈希检测损坏,签名验证来源,二者不等价。下载到暂存区,全部验证后原子切换版本;校验失败或断电保留旧版。差分失败可回退完整包,回滚也要避免加载已撤销的不安全版本。
统计报告可由云端预聚合,展示统计截止时间。管理端与普通客户端的写入能力按角色授权,变更同样经过版本检查和同步协议。聚合结果也要应用租户和对象权限,不能因报告来自缓存就跳过授权。
重试、状态与验证
指数退避加抖动、请求优先级、分块续传和电量约束一起考虑。后台同步可跨多次联网继续,单次联网尝试仍有截止时间;“持续最终同步”不等于一个 HTTP 请求无限等待。
界面区分本地已保存、待同步、已同步、需处理冲突。用户主动刷新不清空队列或生成新操作 ID。
至少演练:本地提交后断电、服务端成功但 ack 丢失、批次部分拒绝、下行应用中崩溃、游标过期、多设备并发、离线后删除、包校验失败、最老客户端重连。检查数据不丢不重、权限正确和状态可见,而非只看接口返回 200。
落地检查
- 本地写入与 outbox 同事务,业务 ID 随重试保持稳定。
- 服务端原子去重,逐项 ack,明确永久失败恢复路径。
- 增量、删除、快照和游标水位成套设计。
- 按数据类型处理冲突,累计值不直接相加。
- 老客户端契约测试,内容包验签后原子切换。
- 用断电、丢 ack 和长离线演练验证不丢不重。
参考资料
正文中的编号可跳转至对应来源。工程报告、官方文档与研究论文分别标注;支撑范围说明该资料能证明什么,以及不能推导什么。
- [1]官方文档Android — Build an offline-first app
支撑范围: 本地优先的数据层、同步与持久队列设计;本文跨端方案为工程归纳。
- [2]官方文档Stripe — Idempotent requests
支撑范围: Stripe 幂等键的具体契约;不能假设所有外部服务具有相同行为。
- [3]官方文档Firebase — Offline data
支撑范围: Firestore 离线缓存与同步语义;不是所有数据库的通用冲突规则。
- [4]官方文档AWS — Retry with backoff
支撑范围: 重试与退避模式的用途和限制;不能替代业务幂等。