更新文档时如果不提供最新 _rev 会发生什么?如何捕获并处理 409 Conflict?
解读
这是 CouchDB 面试里“必问”的并发控制题,考察候选人对 MVCC(多版本并发控制) 机制的理解深度。国内一线互联网与金融场景普遍用 CouchDB 做 离线优先 或 移动同步 底座,若不能正确处理 _rev 与 409,轻则丢数据,重则造成 账不平 或 用户重复提交。面试官希望听到:
- 为什么 CouchDB 强制带 _rev
- 409 产生的完整链路
- 生产级代码如何 无锁重试、合并差异、告警埋点
- 对业务吞吐量的影响与降级方案
知识点
- MVCC 与 _rev:CouchDB 把 _rev 作为 逻辑时钟,每次写生成新 UUID+版本号,老版本立即标记为 tombstone,保证 最终一致性 而非强一致。
- 无 _rev 写操作:PUT/POST 不带 _rev 被视为 新建文档,若库中已存在同名 _id,则直接返回 409 Conflict,不会自动覆盖。
- 409 响应体:返回
{error:"conflict", reason:"Document update conflict."},HTTP 状态码 409,并带 Etag 头给出当前最新 _rev。 - 重试策略:
- 立即重读:GET 最新文档 → 本地合并字段 → 带新 _rev 再 PUT,重试上限 3 次,指数退避 50 ms→100 ms→200 ms。
- 三向合并:用
_revs_info或_revs_diff拿到公共祖先,业务层做 语义化合并(如累加库存、取最大值)。 - 冲突分支持久化:把冲突写回同一文档的
_conflicts数组,后台 冲突收割服务 定时处理,避免阻塞前端。
- 幂等设计:对支付、订单等场景,文档内嵌 幂等令牌(如订单号),重试时先比对令牌,防止重复扣款。
- 监控告警:在 阿里云 SLS 或 腾讯云 CLS 中配置 409 比例 >1% 即告警,联动 钉钉 群机器人,10 分钟内定位热点 _id。
答案
“更新文档不带最新 _rev 时,CouchDB 会返回 409 Conflict,因为底层 MVCC 把 _rev 当作写令牌,保证 写时复制语义。生产代码我会按以下模板处理:
- 捕获 409 异常,解析响应体拿到 最新 _rev 与 Etag。
- 立即发起一次 带 ?revs_info=true 的 GET,拿到完整版本链,判断是否存在 冲突分支。
- 业务层做 三向合并:以公共祖先版本为基准,对可累加字段求和,对不可合并字段采用 last-write-wins 并打标
merged_by=system。 - 把合并后的文档带上最新 _rev 再次 PUT,设置 重试计数器 与 指数退避,最多 3 次仍冲突则写入 死信队列(Kafka topic: couchdb_conflict_dlq),由运营后台人工介入。
- 同时埋点 Prometheus:
couchdb_conflict_total{db="order",method="update"},当 5 分钟内增长率 >10% 时,自动降级为 异步写,先写本地 SQLite,待网络恢复再 批量同步,保障 用户体验 与 数据最终一致。”
拓展思考
- 多主复制场景:若两个数据中心同时离线写同一条订单,CouchDB 会把两份冲突都保留在
_conflicts。如何设计 冲突收割算法 才能避免 超卖? - 性能权衡:高频 counter 场景,每次 409 重试都会生成新版本,导致 tombstone 爆炸。是否考虑用 Redis+Lua 脚本 做 热计数器,再定期 checkpoint 到 CouchDB?
- 合规审计:金融类项目要求 所有版本可回溯,如何利用
_revs_limit=1000与 compaction 策略,既满足 监管留痕 又控制 磁盘膨胀?