如何确保幂等?
解读
在国内 CouchDB 面试中,面试官问“如何确保幂等”并不是想听“加唯一索引”这种通用答案,而是要确认候选人是否真正理解 CouchDB 的多主复制、离线优先、HTTP 语义与 MVCC 机制带来的特殊挑战。
一句话:在 CouchDB 里,幂等=同一份业务操作,无论重试多少次,最终文档状态、版本链、冲突标记与业务副作用都只出现一次。
知识点
-
MVCC 与修订号(_rev)
每次写操作生成新的 _rev,旧版本仍保留,重试时必须携带上一次 _rev,否则 CouchDB 视为“新写”而非“重试”。 -
业务主键(_id)设计
用业务唯一键作为 _id,天然保证“同一业务实体只对应一份文档”,避免重复插入。 -
幂等写策略
- PUT 带 _id+_rev:更新时把上一次读到的 _rev 放在请求头 If-Match 或 JSON 体内,失败返回 409,客户端捕获后重读再重试。
- UPSERT 模板:先 HEAD 检查存在性,存在则 PUT 更新,不存在则 PUT 新建,全程用同一 _id。
- 幂等字段:在文档内放一个业务幂等令牌(如订单号、消息 ID),更新前用 Mango 查询令牌是否存在,存在则直接返回成功,不再写盘。
-
批量与事务补偿
_bulk_docs 接口支持 all_or_nothing=false(默认),单条失败不影响其他;若业务要求“全量成功”,客户端需自己实现补偿回滚或二阶段提交语义。 -
复制与冲突
多主场景下,同一文档在不同节点被并发修改会产生冲突。幂等必须保证冲突解决逻辑也是幂等的:- 采用确定性冲突函数(如 last-write-wins 时间戳取最大,或业务字段 merge 规则),确保任意节点按同一规则收敛到同一值。
- 把冲突函数放在design doc 的 validate_doc_update 或客户端 SDK 的 retry 策略里,避免人工干预。
-
离线重放
移动端 PouchDB 先本地写,再同步到 CouchDB。- 本地用同一 _id+_rev 写,同步时 CouchDB 自动去重。
- 若网络抖动导致重复同步,CouchDB 的复制检查点(_local 文档) 会跳过已处理序列,保证幂等。
答案
“在 CouchDB 中确保幂等,我通常分四层:
- 文档层:用业务唯一键做 _id,保证同一实体只有一份文档;
- 版本层:每次更新必须携带最新 _rev,利用 409 冲突重试机制,确保重试写不会生成多版本;
- 业务层:在文档内加幂等令牌字段,写前先读,存在即返回,避免重复落盘;
- 复制层:自定义确定性冲突解决函数,让任意节点对同一冲突得出同一结果,最终数据收敛一致。
这样,无论网络重试、离线重放还是多主并发,都能保证‘一次操作、一次副作用’。”
拓展思考
-
如果业务不能改 _id,只能用 UUID 做 _id,如何仍然保证幂等?
答案:在 design doc 里建唯一索引(_id 不能建,但可以用 Mango 索引+业务键),写前先用 selector 查询业务键是否存在,存在就更新,不存在再插入;查询与写入放在同一次 bulk 请求里,利用单节点事务快照规避竞态。 -
金融场景要求精确一次记账,CouchDB 没有多文档事务,如何落地?
采用事件溯源+幂等事件表:所有写操作变成“事件文档”,_id=“账户+业务+幂等键”,金额字段用“指令值”而非“余额”。消费端按确定性顺序重放事件,重复事件因 _id 唯一被自动去重,最终余额一致。 -
国内监管要求数据不可物理删除,如何与幂等结合?
不删除文档,而是打逻辑删除标记(status=deleted),冲突解决函数里把删除视为一种“状态”,确保重试删除不会把已删文档复活,同时保留完整审计链。