更新文档时如果不提供最新 _rev 会发生什么?如何捕获并处理 409 Conflict?

解读

这是 CouchDB 面试里“必问”的并发控制题,考察候选人对 MVCC(多版本并发控制) 机制的理解深度。国内一线互联网与金融场景普遍用 CouchDB 做 离线优先移动同步 底座,若不能正确处理 _rev 与 409,轻则丢数据,重则造成 账不平用户重复提交。面试官希望听到:

  1. 为什么 CouchDB 强制带 _rev
  2. 409 产生的完整链路
  3. 生产级代码如何 无锁重试合并差异告警埋点
  4. 对业务吞吐量的影响与降级方案

知识点

  1. MVCC 与 _rev:CouchDB 把 _rev 作为 逻辑时钟,每次写生成新 UUID+版本号,老版本立即标记为 tombstone,保证 最终一致性 而非强一致。
  2. 无 _rev 写操作:PUT/POST 不带 _rev 被视为 新建文档,若库中已存在同名 _id,则直接返回 409 Conflict,不会自动覆盖。
  3. 409 响应体:返回 {error:"conflict", reason:"Document update conflict."},HTTP 状态码 409,并带 Etag 头给出当前最新 _rev。
  4. 重试策略
    • 立即重读:GET 最新文档 → 本地合并字段 → 带新 _rev 再 PUT,重试上限 3 次,指数退避 50 ms→100 ms→200 ms。
    • 三向合并:用 _revs_info_revs_diff 拿到公共祖先,业务层做 语义化合并(如累加库存、取最大值)。
    • 冲突分支持久化:把冲突写回同一文档的 _conflicts 数组,后台 冲突收割服务 定时处理,避免阻塞前端。
  5. 幂等设计:对支付、订单等场景,文档内嵌 幂等令牌(如订单号),重试时先比对令牌,防止重复扣款。
  6. 监控告警:在 阿里云 SLS腾讯云 CLS 中配置 409 比例 >1% 即告警,联动 钉钉 群机器人,10 分钟内定位热点 _id。

答案

“更新文档不带最新 _rev 时,CouchDB 会返回 409 Conflict,因为底层 MVCC 把 _rev 当作写令牌,保证 写时复制语义。生产代码我会按以下模板处理:

  1. 捕获 409 异常,解析响应体拿到 最新 _revEtag
  2. 立即发起一次 带 ?revs_info=true 的 GET,拿到完整版本链,判断是否存在 冲突分支
  3. 业务层做 三向合并:以公共祖先版本为基准,对可累加字段求和,对不可合并字段采用 last-write-wins 并打标 merged_by=system
  4. 把合并后的文档带上最新 _rev 再次 PUT,设置 重试计数器指数退避,最多 3 次仍冲突则写入 死信队列(Kafka topic: couchdb_conflict_dlq),由运营后台人工介入。
  5. 同时埋点 Prometheus:couchdb_conflict_total{db="order",method="update"},当 5 分钟内增长率 >10% 时,自动降级为 异步写,先写本地 SQLite,待网络恢复再 批量同步,保障 用户体验数据最终一致。”

拓展思考

  1. 多主复制场景:若两个数据中心同时离线写同一条订单,CouchDB 会把两份冲突都保留在 _conflicts。如何设计 冲突收割算法 才能避免 超卖
  2. 性能权衡:高频 counter 场景,每次 409 重试都会生成新版本,导致 tombstone 爆炸。是否考虑用 Redis+Lua 脚本热计数器,再定期 checkpoint 到 CouchDB?
  3. 合规审计:金融类项目要求 所有版本可回溯,如何利用 _revs_limit=1000compaction 策略,既满足 监管留痕 又控制 磁盘膨胀