如何确保幂等?

解读

在国内 CouchDB 面试中,面试官问“如何确保幂等”并不是想听“加唯一索引”这种通用答案,而是要确认候选人是否真正理解 CouchDB 的多主复制、离线优先、HTTP 语义与 MVCC 机制带来的特殊挑战。
一句话:在 CouchDB 里,幂等=同一份业务操作,无论重试多少次,最终文档状态、版本链、冲突标记与业务副作用都只出现一次

知识点

  1. MVCC 与修订号(_rev)
    每次写操作生成新的 _rev,旧版本仍保留,重试时必须携带上一次 _rev,否则 CouchDB 视为“新写”而非“重试”。

  2. 业务主键(_id)设计
    业务唯一键作为 _id,天然保证“同一业务实体只对应一份文档”,避免重复插入。

  3. 幂等写策略

    • PUT 带 _id+_rev:更新时把上一次读到的 _rev 放在请求头 If-Match 或 JSON 体内,失败返回 409,客户端捕获后重读再重试。
    • UPSERT 模板:先 HEAD 检查存在性,存在则 PUT 更新,不存在则 PUT 新建,全程用同一 _id。
    • 幂等字段:在文档内放一个业务幂等令牌(如订单号、消息 ID),更新前用 Mango 查询令牌是否存在,存在则直接返回成功,不再写盘。
  4. 批量与事务补偿
    _bulk_docs 接口支持 all_or_nothing=false(默认),单条失败不影响其他;若业务要求“全量成功”,客户端需自己实现补偿回滚二阶段提交语义。

  5. 复制与冲突
    多主场景下,同一文档在不同节点被并发修改会产生冲突。幂等必须保证冲突解决逻辑也是幂等的

    • 采用确定性冲突函数(如 last-write-wins 时间戳取最大,或业务字段 merge 规则),确保任意节点按同一规则收敛到同一值。
    • 把冲突函数放在design doc 的 validate_doc_update客户端 SDK 的 retry 策略里,避免人工干预。
  6. 离线重放
    移动端 PouchDB 先本地写,再同步到 CouchDB。

    • 本地用同一 _id+_rev 写,同步时 CouchDB 自动去重。
    • 若网络抖动导致重复同步,CouchDB 的复制检查点(_local 文档) 会跳过已处理序列,保证幂等。

答案

“在 CouchDB 中确保幂等,我通常分四层:

  1. 文档层:用业务唯一键做 _id,保证同一实体只有一份文档;
  2. 版本层:每次更新必须携带最新 _rev,利用 409 冲突重试机制,确保重试写不会生成多版本;
  3. 业务层:在文档内加幂等令牌字段,写前先读,存在即返回,避免重复落盘;
  4. 复制层:自定义确定性冲突解决函数,让任意节点对同一冲突得出同一结果,最终数据收敛一致。
    这样,无论网络重试、离线重放还是多主并发,都能保证‘一次操作、一次副作用’。”

拓展思考

  1. 如果业务不能改 _id,只能用 UUID 做 _id,如何仍然保证幂等?
    答案:在 design doc 里建唯一索引(_id 不能建,但可以用 Mango 索引+业务键),写前先用 selector 查询业务键是否存在,存在就更新,不存在再插入;查询与写入放在同一次 bulk 请求里,利用单节点事务快照规避竞态。

  2. 金融场景要求精确一次记账,CouchDB 没有多文档事务,如何落地?
    采用事件溯源+幂等事件表:所有写操作变成“事件文档”,_id=“账户+业务+幂等键”,金额字段用“指令值”而非“余额”。消费端按确定性顺序重放事件,重复事件因 _id 唯一被自动去重,最终余额一致。

  3. 国内监管要求数据不可物理删除,如何与幂等结合?
    不删除文档,而是打逻辑删除标记(status=deleted),冲突解决函数里把删除视为一种“状态”,确保重试删除不会把已删文档复活,同时保留完整审计链。