如何配置“retry”策略使“_bulk_docs”幂等?

解读

在国内互联网、金融、物联网等真实业务中,_bulk_docs 被大量用于离线端批量同步、日志归集、订单对账等场景。
由于移动网络抖动、网关超时、容器重启等原因,客户端经常触发“自动重试”。
如果重试策略配置不当,同一条批量请求可能被 CouchDB 执行多次,导致:

  1. 同一文档生成多个冲突版本(_conflicts 数组膨胀);
  2. 业务层计数、库存、金额被重复累加;
  3. 移动端同步 checkpoint 错乱,引发“幽灵数据”。

因此,面试官真正想考察的是:
在保留 CouchDB 原生多版本机制的前提下,如何通过客户端重试策略与文档设计,让 _bulk_docs 在业务语义上只生效一次”。

知识点

  1. _bulk_docs 的语义

    • 默认 all_or_nothing=false,即逐条写入;任何一条失败不影响其余文档。
    • 当携带 "new_edits": false 时,CouchDB 会绕过版本检查,直接存储指定修订,具备天然幂等性,但要求客户端必须提前拿到 正确的 _rev
  2. 幂等键(Idempotency-Key)模式

    • 在每条文档体内加入 biz_id + 请求批次号 组成的复合键,写入前用 _find_view 做存在性校验。
    • 利用 _design/update 函数 在服务器端做“upsert 去重”,把重复请求转空操作。
  3. 重试策略配置要点

    • 指数退避 + 最大重试次数(国内公有云环境建议 3~5 次,退避基数 500 ms)。
    • 只重试 5xx 与 429;对 4xx(除 429)直接失败,避免把业务错误放大。
    • 同一批次内文档顺序固定,防止并发场景下“部分成功”后重试顺序错乱。
  4. 移动/离线场景补充

    • 在 PouchDB 端开启 retry: true, live: true 同步时,自定义 filter 函数 把已确认写入的 biz_id 排除掉,实现“客户端幂等过滤”。
    • 对账阶段用 _changes?since=last_seq 拉取增量,按 biz_id 去重合并,确保最终一致。

答案

线上环境推荐“new_edits=false + 精确 _rev”与“幂等键 + 服务端 update 函数”双轨方案:

  1. 能提前拿到 _rev 的同步链路(如主主复制、边缘节点回源)

    • 客户端在本地维护 rev_map(id→rev)
    • 构造 _bulk_docs 时置 "new_edits": false,并填入准确 _rev
    • 重试策略:
      • 超时或 5xx/429 时按 2^n × 500 ms 退避,最多 3 次
      • 收到 201 响应后,立即更新本地 rev_map,后续重试使用新 _rev,保证整条链路幂等。
  2. 无法提前拿到 _rev 的业务写入(如 APP 端首次上报订单)

    • 每条文档携带 biz_id + 请求批次号 作为业务主键;
    • 设计 /_design/dedupupdate 函数
      function(doc, req) {
        var key = req.body.biz_id + req.body.batch_no;
        if (doc && doc.processed_keys && doc.processed_keys.indexOf(key) >= 0) {
          return [null, "ignored"];
        }
        if (!doc) doc = {_id: req.id};
        doc.processed_keys = doc.processed_keys || [];
        doc.processed_keys.push(key);
        // 继续合并业务字段
        return [doc, "updated"];
      }
      
    • 客户端重试策略:
      • 仍采用 指数退避 3 次
      • 每次重试保持 biz_id 与 batch_no 不变
      • 收到 201 后把该批次号标记为“已落库”,本地不再重发。

通过以上配置,_bulk_docs 在网络抖动、网关重发、容器重启等典型国内云场景下,业务层面只会生效一次,满足面试官对“幂等重试”的严苛要求。

拓展思考

  1. 跨地域多活场景
    若北京、上海双集群同时接受写入,new_edits=false 方案需配合 全局序列号服务(如基于 Redis Lua 的 INCR + 时间戳)预分配 _rev 号段,避免集群间 rev 碰撞导致冲突爆炸。

  2. 大数据量一次性迁移
    对历史库做 tb 级 _bulk_docs 导入时,可先把 target 集群设置 q=1、n=1,关闭 delayed_commits,批量写入完成后再 reshard 并调高副本数,既保证幂等又降低磁盘 IO 抖动

  3. 审计与排查
    在文档内增加 __retry_cnt 字段,每次重试时自增;结合 ELK 收集 429/502 日志,按 __retry_cnt 聚合可快速定位国内某运营商网络异常导致的重试风暴,实现 可观测幂等