如何生成 DAG?
解读
国内面试官问“CouchDB 里如何生成 DAG”并不是想听你背概念,而是考察你对** CouchDB 多主复制冲突模型**的底层理解:
- 每一次写操作都会产生一条** 修订历史链**;
- 当出现并发写,链会分叉,形成** 有向无环图(DAG)**;
- 系统必须能** 自动合并或提示冲突,并保证最终一致。
回答时要能把“DAG 的生成时机、数据结构、合并策略、对业务的影响”一口气讲清楚,才能体现 分布式数据库内核级**能力。
知识点
- MVCC + 修订树(Rev-Tree):CouchDB 用
{_id, _rev}对文档做多版本控制,_rev是“代数+UUID”形式,形如3-abc123,代数表示深度,UUID 表示分支。 - 分叉(Branch):两个客户端同时基于
2-xyz写文档,各自生成3-uvw与3-def,服务器保留两条路径,DAG 即刻出现。 - Rev-Tree 存储:B+Tree 叶子节点保存
{rev, status, children[]},children 指针让历史天然形成 DAG。 - 合并策略:
– ** 自动合并**:若叶子节点代数相同且内容无字段冲突,CouchDB 会选代数大的作为获胜版本(deterministic winner)。
– ** 冲突版本留痕**:失败分支被标记为conflict:true,仍可通过?conflicts=true拉回,应用层负责三向合并。 - 压缩(Compaction):定期把旧修订、已删除分支从 Rev-Tree 剪掉,既节省空间又防止 DAG 无限膨胀。
- 对国内业务的启示:移动办公、IoT 场景下网络常抖动,DAG 冲突可达 5%~10%,必须** 预置冲突解决微服务**,否则用户会看到数据“回退”。
答案
“在 CouchDB 中,DAG 并不是用户手动创建的,而是** 并发写操作**触发的必然结果。具体流程如下:
- 客户端 A、B 同时拿到同一文档的
_rev:2-abc; - A 本地修改后 PUT,服务器生成
_rev:3-uvw; - B 稍后也 PUT,服务器发现其父修订
2-abc已非叶子,于是把 B 的版本挂成兄弟节点,生成_rev:3-def; - 此时文档的修订树出现两条从
2-abc出发的边,** 有向无环图正式生成**; - 查询时,CouchDB 通过确定性算法选出
3-def为获胜版本返回,另一分支留作冲突; - 若业务需要,可通过
GET /db/doc?conflicts=true拉回所有叶子,由应用层做三向合并,再写回新的_rev:4-merged,从而** 把 DAG 收敛成单链**。
整个过程对开发者透明,但理解 DAG 的生成与收敛机制,是设计** 离线优先**应用和冲突解决策略的前提。”
拓展思考
- ** 国内私有云多活架构**:在两地三中心部署 CouchDB 集群,中心间 RTT 80 ms,写冲突概率升高;可** 按业务域切库**+** 写入路由层加时钟漂移检测**,把冲突率压到 1% 以下。
- ** 与 Git 的区别**:Git 的 DAG 由用户显式提交产生,CouchDB 的 DAG 由** 自动版本控制产生,因此冲突解决必须 零人工介入**;可借鉴 Git 的三向合并算法,在网关层预置
merge()函数。 - ** 性能陷阱**:Rev-Tree 过深会拖慢读路径;国内某电商曾出现单文档 700+ 分支,读 QPS 跌 40%。解决方式是** 定期强制压缩**+** 业务层限制离线写次数**,把树深度控制在 50 以内。