端云同步不是简单调接口:用版本号、幂等键和冲突合并把数据做可靠
很多端云应用在演示阶段只需要把本地数据 POST 到服务端,看起来就完成了同步。真正上线后,网络重试、离线编辑、多端同时修改和接口超时都会出现。若没有明确的数据协议,“最后一次请求覆盖数据库”很快就会造成重复记录或用户内容丢失。
本文以笔记类应用为例,讨论一套可以直接落地的最小同步设计:本地先写、失败可重试、服务端幂等、版本可检查、冲突可解释。
一、先定义三条不变量
- 本地成功优先。用户点击保存后,内容必须先进入本地持久化存储。即使此刻断网,页面刷新或应用重启也不能丢。
- 请求可以重复。同一次保存被重发两次、三次,服务端最终也只能落一条业务记录。
- 版本必须显式。客户端不能只相信时间戳,因为设备时钟可能漂移;服务端应维护单调递增的记录版本。
这三条看似简单,却决定了系统在网络抖动时是否可恢复。
二、Outbox:把“写本地”和“传服务端”拆开
推荐把业务写入和同步任务放进同一个本地事务:先更新笔记表,再写一条 outbox 记录。后台同步器只读取 outbox,成功后标记完成。这样 UI 不必等待网络,失败也能在同一队列中重试。
type OutboxItem = {
opId: string; // 由客户端生成,全局唯一
entityId: string;
baseVersion: number;
payload: NotePayload;
status: "pending" | "sending" | "done";
retryCount: number;
};
opId 不是数据库主键的替代品,而是“这次操作”的幂等键。每次用户保存生成新值;网络重试必须复用原值,不能重新生成。
三、服务端幂等:先约束,再处理业务
服务端应在事务内先插入幂等记录。如果唯一索引冲突,说明请求已处理过,直接返回此前结果,不要再次修改业务表。
BEGIN;
INSERT INTO sync_op(op_id, user_id, result_version)
VALUES (:opId, :userId, :nextVersion); -- op_id 唯一
UPDATE note
SET content = :content,
version = version + 1,
updated_at = NOW()
WHERE id = :entityId
AND user_id = :userId
AND version = :baseVersion;
COMMIT;
同时检查受影响行数。若更新为零,通常是版本已变化,应返回 409 Conflict,并附带服务端当前版本和摘要。客户端据此决定拉取最新数据、保留冲突副本,或提示用户选择。
四、冲突处理要可解释
对短文本,最简单的策略不是静默覆盖,而是保留两份:服务端版本作为主记录,客户端版本另存为“冲突副本”。对结构化数据,可以按字段做三方合并,但界面必须明确标出自动合并和需要人工确认的部分。
版本号比 updatedAt 更可靠,因为时间戳既可能受设备时钟影响,也可能在同一毫秒内产生并列值。服务端版本由数据库原子递增,能稳定表达“我是基于哪一版修改的”。
五、客户端同步循环要有退避与上限
for (let attempt = 0; attempt < 5; attempt++) {
const item = await nextPendingOutbox();
try {
const res = await push(item); // 每次重试都复用 item.opId
await markDone(item.opId, res.version);
} catch (error) {
await markRetry(item.opId, attempt);
await sleep(2 ** attempt * 500 + Math.random() * 250);
}
}
重试不能无限循环,也不能固定间隔打满服务端。建议使用指数退避、随机抖动和最大次数;持续失败的记录进入可见的待处理状态,避免用户误以为已经同步。
六、上线前至少验证这些异常场景
- 服务端已提交但响应丢失,客户端重试后不能产生重复记录。
- 两台设备基于同一版本编辑,后提交的一方收到冲突信息而不是静默覆盖。
- 同一
opId并发到达时,服务端只执行一次业务修改。 - 长时间离线后恢复网络,待同步队列能按顺序完成并保留失败项。
- 设备时间被调快或调慢,版本判断仍然正确。
七、结语
端云同步的核心不是某个 POST 接口,而是一份双方都遵守的数据协议:本地先落盘保证用户体验,幂等键保证重试安全,版本号保证并发可检测,冲突副本保证信息不被静默吞掉。把这四件事做好,再复杂的同步场景也会从“偶发丢数据”变成可以定位、可以恢复的工程问题。
- 点赞
- 收藏
- 关注作者
评论(0)