当需要回滚单个文档时,如何快速定位并 PUT 历史版本?

解读

国内生产环境常把 CouchDB 当“离线优先”的中央节点,手机端、小程序、IoT 网关随时可能上传脏数据。面试官问“回滚单个文档”,不是让你把整个库还原,而是在不停服、不影响其它文档的前提下,把某一条记录瞬间恢复到指定历史版本。考点集中在三点:

  1. 如何秒级定位目标版本(版本号、时间、业务键);
  2. 如何拿到完整历史文档(不是 diff);
  3. 如何原子写入并解决并发冲突,确保回滚可重入。

答不上来就直接掉分,答得太重(比如备份还原、全量复制)会被判“思路不清”。

知识点

  • MVCC 机制:CouchDB 把每次更新存成新 revision,旧 revision 物理仍在文件里,直到数据库压缩。
  • _rev 树:每个 doc 维护一棵 revision tree,叶子节点才是“可见”版本;回滚本质是把某个非叶子节点重新变回叶子
  • GET /db/doc?revs=true&open_revs=[...]:可一次性拉回指定 revision 的完整内容,无需遍历 _changes
  • PUT 带 new_edits=false:把历史 revision 直接插回 tree,不生成新 rev,实现“真回滚”而非“新增覆盖”。
  • 国内常用封装:阿里云函数计算、腾讯云 SCF 里常把上述两步包成 HTTP 接口,供小程序前端一键“撤销”。

答案

分三步走,全部用官方 REST 接口,不依赖第三方插件,线上 200 ms 内完成:

  1. 定位版本号
    若已知业务时间戳,用 GET /db/_changes?doc_ids=["订单666"]&include_docs=true&since=0 过滤出该 doc 的所有事件,按本地时间戳倒序找到目标 rev(形如 3-abc)。
    若已知 rev,直接跳过本步。

  2. 拉取历史文档
    GET /db/订单666?rev=3-abc&revs=true
    返回体里 _revisions.ids 可验证 tree 路径,确保该 rev 未被压缩(国内默认 revs_limit=1000,7 天内必在)。

  3. 原子回滚
    把第 2 步的 JSON 原样 PUT,但加请求头

    PUT /db/订单666?new_edits=false  
    Content-Type: application/json  
    {  
      "_id": "订单666",  
      "_rev": "3-abc",  
      ... // 其余字段保持历史值  
    }  
    

    由于 new_edits=false,CouchDB 直接把 3-abc 重新标为最新叶子,不产生 4-def,实现“秒级回滚”。
    返回 201 即成功;若返回 409,说明并发又写了新 rev,可重试或抛给业务层人工二次确认。

一句话总结:用 new_edits=false 把旧 rev 重新插回 tree,是官方支持且国内云厂商默认可用的“单文档回滚”黄金命令。

拓展思考

  • 压缩窗口:国内磁盘紧张常把 revs_limit 调到 100 以下,过期的 rev 会被物理删除,此时只能走备份通道(如 couchdb-backup 工具),面试官会追问“备份回滚”方案,需准备 POST /db/_bulk_docsall_or_nothing=true 的脚本。
  • 权限隔离:生产库多租户模式下,回滚接口必须走子库管理员密钥,防止普通用户伪造历史版本;可在 nginx 层加 X-Auth-CouchDB-Roles 二次校验。
  • 审计留痕:回滚后立刻写一条 {type:"rollback", from:"5-xyz", to:"3-abc", operator:"张三"} 到同库审计文档,国内金融客户过等保三级时必须可追踪