端云同步应用的数据一致性设计笔记:版本号、冲突合并与离线队列
背景
端云一体的待办清单、笔记类应用,演示时往往很顺:端侧改一条记录,调个云函数写库,另一台设备拉取就同步了。真正上线后出问题的地方几乎都集中在同一处——弱网、多端同时编辑、离线期间积累的一堆待提交操作。这篇把这类系统里几个必须提前定好的规则整理一遍。
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 谁生成、版本谁分配、冲突按什么粒度合并、删除如何表达、重试如何去重。把这五条写进数据模型,后面换端侧框架或换云侧存储都不用推翻重来。
- 点赞
- 收藏
- 关注作者
评论(0)