端云同步应用的数据一致性设计笔记:版本号、冲突合并与离线队列

举报
yd_227879331 发表于 2026/11/08 00:23:42 2026/11/08
【摘要】 以待办清单类应用为例,梳理增量同步、字段级冲突合并、软删除墓碑、服务端逻辑时钟与幂等重试的设计取舍,并给出一份可复用的验收清单。

背景

端云一体的待办清单、笔记类应用,演示时往往很顺:端侧改一条记录,调个云函数写库,另一台设备拉取就同步了。真正上线后出问题的地方几乎都集中在同一处——弱网、多端同时编辑、离线期间积累的一堆待提交操作。这篇把这类系统里几个必须提前定好的规则整理一遍。

1. 先定数据模型,别用本地自增 ID

记录的唯一标识应当在客户端生成(UUID / ULID),而不是等服务端返回自增主键。否则离线创建的记录在提交前无法被本地队列引用,重连后也很难做去重。每条记录建议至少带:id、updated_at、version、deleted(软删除标记),以及一个客户端操作序号 client_op_id。

CREATE TABLE todos (
  id          varchar(36) primary key,
  title       varchar(255),
  done        boolean,
  due_at      timestamp,
  version     bigint not null,
  deleted     boolean default false,
  updated_at  timestamp not null
);

2. 增量同步而不是全量覆盖

常见的错误实现是每次同步把服务端全量列表拉下来覆盖本地。这在单端、低延迟下看不出问题,一旦有离线编辑,本地未提交的修改会被直接冲掉。更稳妥的做法是两段式:先按本地脏标记队列把待提交操作推上去,再带 since_version 拉增量。

POST /sync/push  { ops: [ {client_op_id, id, patch, base_version} ] }
GET  /sync/pull?since_version=12345  ->  { changes: [...], server_version: 12380 }

本地保存 server_version 作为水位线,下次从该值继续拉。push 与 pull 之间不要并发,按「先推后拉」串行执行,避免刚提交的变更又被当成服务端新变更合并一次。

3. 冲突处理:按字段合并,而不是整条覆盖

「最后写入胜出(LWW)」实现简单,但待办清单这类结构化记录里,标题、完成状态、截止日期常常是不同时刻分别修改的,整条覆盖会丢信息。更合适的是字段级合并:以 base_version 对应的快照作为共同祖先,比较云端当前值与本地修改值。

for field in record.fields:
    if local[field] == base[field]:       # 本地没改
        merged[field] = remote[field]     # 采用云端
    elif remote[field] == base[field]:    # 云端没改
        merged[field] = local[field]      # 采用本地
    else:                                 # 两边都改了
        merged[field] = resolve(field, local[field], remote[field])

resolve 可以按字段给策略:done 这类布尔状态适合「已完成优先」,due_at 取较新的一次修改,title 则保留本地并生成一条冲突提示让用户确认。CRDT 更适合长文本协作,对这种小结构体有点重。

4. 删除要用墓碑,别物理删除

如果直接用 DELETE 把行删掉,一台长期离线的设备重新上线时,本地还留着这条记录,拉取增量看不到任何关于它的信息,就会把它当成「本地新增」再推回服务端——删掉的东西自己复活了。正确做法是软删除:设置 deleted=true 并递增 version,让这条变更像普通更新一样参与增量同步;墓碑保留到确认所有端的水位线都超过它之后再清理。

5. 客户端时钟不可信

用客户端 updated_at 做冲突判定,遇到设备时间被手动改过、时区不一致或时间回拨,就会得出错误结论。版本号应当由服务端在一个单调递增序列里分配,客户端只保存和使用,不参与生成。若确实需要展示时间,服务端写库时另存一个 server_time,与冲突判定逻辑解耦。

6. 轮询、退避与幂等

定时轮询(例如 30 秒一次)实现成本低,但要注意三点:应用切到后台时暂停、回到前台立即补一次;连续失败按指数退避,避免弱网下把请求打满;每次操作带 client_op_id,服务端用唯一索引去重,网络超时重试才不会重复写入。

INSERT INTO sync_ops(client_op_id, result) VALUES (?, ?)
ON CONFLICT (client_op_id) DO NOTHING;  -- 重复提交直接返回首次结果

一批同步操作放在同一个事务里提交,避免出现「删除成功、新增失败」这类半截状态。

7. 一份最小验收清单

断网下连续新建、编辑、删除,恢复网络后顺序与结果正确;两台设备同时改同一条记录的不同字段,合并后两边信息都保留;一端删除后,旧端带着旧数据上线,记录不会复活;把设备时间调慢一小时,同步结果不变;请求超时后自动重试,服务端不产生重复记录;应用切后台再回前台,能立刻拿到期间其他端的变更。

小结

端云同步的复杂度不在接口怎么写,而在规则是否提前定清楚:ID 谁生成、版本谁分配、冲突按什么粒度合并、删除如何表达、重试如何去重。把这五条写进数据模型,后面换端侧框架或换云侧存储都不用推翻重来。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。