如何在离线端本地解决冲突并标记为“resolved”避免再次上传?

解读

国内移动端、IoT 场景常要求“离线优先”,即设备断网时仍能读写,待网络恢复再同步。CouchDB 的多主复制机制会在两端同时修改同一文档时产生冲突版本(conflict revision)。如果离线端不做任何处理,同步时这些冲突仍会被带到服务器,导致用户反复看到“冲突待解”提示,体验差且浪费流量。面试官想确认候选人是否理解 CouchDB 的冲突模型本地冲突解决流程以及如何写回“胜出”版本并删除冲突分支,从而真正做到“本地解决、不再上传”。

知识点

  1. 冲突产生条件:同一文档在两个数据库实例分别被更新,且更新基于同一父版本。
  2. 冲突存储方式:CouchDB 保留冲突分支(conflict revs),但只把其中一个设为winning rev;其余分支可通过 ?conflicts=true_conflicts 数组获取。
  3. 本地解决步骤
    a. 读取文档并带上 ?conflicts=true 拿到所有冲突版本;
    b. 在本地应用业务规则(时间戳、向量时钟、用户手动选择等)决定胜出内容
    c. 把胜出内容写入新修订(new_edits=true),同时删除所有冲突分支(PUT 时带上 _rev_deleted_conflicts 或直接删除旧 rev);
    d. 确保本地数据库**压缩(compact)**后,冲突分支物理消失;
  4. PouchDB 适配:离线端若用 PouchDB,可监听 change 事件的 doc._conflicts,调用 db.remove(doc._id, conflictRev) 删除非胜出分支,再 db.put(winningDoc)
  5. 同步策略:本地解决后,下次 replicate.to 只上传胜出版本,冲突分支已不存在,因此服务器不会再收到冲突,实现“resolved 不再上传”。

答案

在离线端(如 PouchDB)按以下四步操作即可本地解决冲突并标记为 resolved,避免再次上传:

  1. 拉取冲突
    db.get(id, {conflicts: true, revs: true}) 得到 docdoc._conflicts 数组。
  2. 业务裁决
    遍历 _conflicts 中每个 rev,取回具体内容,按业务规则选出唯一胜出数据。
  3. 写回胜出并删除分支
    a. 构造胜出文档,保留 _id 与最新 _rev(winning),新增 _revisions 字段指明历史链;
    b. 依次调用 db.remove(doc._id, rev) 删除所有非胜出冲突分支;
    c. 立即 db.put(winningDoc) 把胜出内容写为新的 winning revision。
  4. 本地压缩
    执行 db.compact() 物理清除已删除的冲突分支,确保后续 replicate.to(remoteDB) 只上传干净版本,服务器端不再出现冲突记录。

完成以上步骤后,该文档在本地已无冲突分支,同步时 CouchDB 只见到单一 revision 链,自然不会再触发冲突,也就实现了“resolved 且不再上传”。

拓展思考

  1. 向量时钟 vs 时间戳:在分布式场景下,本地时间不可靠,可引入向量时钟业务优先级字段作为裁决依据,避免“后写者全赢”带来的数据丢失。
  2. 冲突解决策略下沉:把裁决逻辑封装成可插拔函数并同步到服务器,保证移动端与后端使用同一套规则,防止“本地解决完,服务器再解决一次”导致版本分叉。
  3. 附件冲突:若文档含 _attachments,冲突时附件也会多版本,需在删除冲突分支前把胜出附件 stub 显式复制到新 revision,否则会出现“附件丢失”现象。
  4. 权限与审计:国内金融、医疗项目要求留痕,可在胜出文档中新增 _conflict_resolved_by_conflict_resolved_time 字段,既满足合规,又方便后续追溯。
  5. 自动化测试:用 pouchdb-server 在本地模拟断网、双写、再同步的完整流程,通过断言 db.get(id,{conflicts:true})._conflicts === undefined 验证“零冲突上传”,形成持续集成用例,防止回归。