如何在离线端本地解决冲突并标记为“resolved”避免再次上传?
解读
国内移动端、IoT 场景常要求“离线优先”,即设备断网时仍能读写,待网络恢复再同步。CouchDB 的多主复制机制会在两端同时修改同一文档时产生冲突版本(conflict revision)。如果离线端不做任何处理,同步时这些冲突仍会被带到服务器,导致用户反复看到“冲突待解”提示,体验差且浪费流量。面试官想确认候选人是否理解 CouchDB 的冲突模型、本地冲突解决流程以及如何写回“胜出”版本并删除冲突分支,从而真正做到“本地解决、不再上传”。
知识点
- 冲突产生条件:同一文档在两个数据库实例分别被更新,且更新基于同一父版本。
- 冲突存储方式:CouchDB 保留冲突分支(conflict revs),但只把其中一个设为winning rev;其余分支可通过
?conflicts=true或_conflicts数组获取。 - 本地解决步骤:
a. 读取文档并带上?conflicts=true拿到所有冲突版本;
b. 在本地应用业务规则(时间戳、向量时钟、用户手动选择等)决定胜出内容;
c. 把胜出内容写入新修订(new_edits=true),同时删除所有冲突分支(PUT 时带上_rev与_deleted_conflicts或直接删除旧 rev);
d. 确保本地数据库**压缩(compact)**后,冲突分支物理消失; - PouchDB 适配:离线端若用 PouchDB,可监听
change事件的doc._conflicts,调用db.remove(doc._id, conflictRev)删除非胜出分支,再db.put(winningDoc); - 同步策略:本地解决后,下次
replicate.to只上传胜出版本,冲突分支已不存在,因此服务器不会再收到冲突,实现“resolved 不再上传”。
答案
在离线端(如 PouchDB)按以下四步操作即可本地解决冲突并标记为 resolved,避免再次上传:
- 拉取冲突:
db.get(id, {conflicts: true, revs: true})得到doc与doc._conflicts数组。 - 业务裁决:
遍历_conflicts中每个rev,取回具体内容,按业务规则选出唯一胜出数据。 - 写回胜出并删除分支:
a. 构造胜出文档,保留_id与最新_rev(winning),新增_revisions字段指明历史链;
b. 依次调用db.remove(doc._id, rev)删除所有非胜出冲突分支;
c. 立即db.put(winningDoc)把胜出内容写为新的 winning revision。 - 本地压缩:
执行db.compact()物理清除已删除的冲突分支,确保后续replicate.to(remoteDB)只上传干净版本,服务器端不再出现冲突记录。
完成以上步骤后,该文档在本地已无冲突分支,同步时 CouchDB 只见到单一 revision 链,自然不会再触发冲突,也就实现了“resolved 且不再上传”。
拓展思考
- 向量时钟 vs 时间戳:在分布式场景下,本地时间不可靠,可引入向量时钟或业务优先级字段作为裁决依据,避免“后写者全赢”带来的数据丢失。
- 冲突解决策略下沉:把裁决逻辑封装成可插拔函数并同步到服务器,保证移动端与后端使用同一套规则,防止“本地解决完,服务器再解决一次”导致版本分叉。
- 附件冲突:若文档含
_attachments,冲突时附件也会多版本,需在删除冲突分支前把胜出附件 stub 显式复制到新 revision,否则会出现“附件丢失”现象。 - 权限与审计:国内金融、医疗项目要求留痕,可在胜出文档中新增
_conflict_resolved_by与_conflict_resolved_time字段,既满足合规,又方便后续追溯。 - 自动化测试:用
pouchdb-server在本地模拟断网、双写、再同步的完整流程,通过断言db.get(id,{conflicts:true})._conflicts === undefined验证“零冲突上传”,形成持续集成用例,防止回归。