如何写回聚合结果到 CouchDB?
解读
国内面试官问“写回聚合结果”并不是想听 Map/Reduce 的入门概念,而是考察候选人是否真正在生产环境里做过离线汇总、增量统计、移动端同步这类场景。CouchDB 本身没有“UPDATE … GROUP BY”语法,也没有内置的物化视图自动回写机制,因此必须自己设计幂等、可重入、并发安全的写回方案。能否把聚合结果安全地落回同库或分库,并保证后续同步不冲突,是区分“会用”与“实战”的关键分水岭。
知识点
- Map/Reduce 视图仅产生索引,不落地文档;要持久化必须显式写回。
- _bulk_docs 批量接口支持
new_edits=false模式,可绕过版本冲突,实现幂等覆盖。 - 设计文档(_design/*)里可放 update 函数,在 HTTP PUT 一个聚合结果时由服务器端脚本完成“读-改-写”,避免客户端并发竞争。
- *_local/ 文档**不参与复制,可用来存“上次聚合游标”实现增量统计,降低重复计算。
- 多主复制场景下,同一聚合键可能出现版本分支,需用业务层合并策略(max、sum、last-write-wins)并在写回时把
_rev置为最新。 - 国内阿里云、腾讯云托管 CouchDB 均关闭
reduce_limit = false,但默认单次查询仍限 200 条分组,需分页或增量聚合。 - Fauxton 控制台不能直接回写,必须通过代码或脚本,面试时要强调“自动化脚本 + CI 定时任务”而不是手工点按钮。
答案
线上环境推荐“增量聚合 + 幂等写回”两步走:
第一步,用 Mango 或视图按时间戳过滤出增量文档,在业务服务内存里做 reduce,得到聚合值。
第二步,调用 _bulk_docs 把结果写回专用聚合库(或同一库下的 agg_* 前缀文档),请求体里带上 new_edits=false 与固定 _id,保证重复跑任务不会生成新版本;若需覆盖旧值,则先 HEAD 取最新 _rev,再带 _rev 写回。
如果并发量高,可在设计文档里写一个 update 函数:
function(doc, req) {
if (!doc) {
doc = {_id: req.id, count: 0, sum: 0};
}
var delta = JSON.parse(req.body);
doc.count += delta.count;
doc.sum += delta.sum;
return [doc, 'updated'];
}
客户端只需 PUT /db/agg_key 把增量差值发过去,服务器端完成原子累加,避免“读-改-写”竞争。
最后,用 _local/lastAggSeq 记录已处理过的 update_seq,下次从该序号继续扫描,实现断点续传与小时级离线窗口统计。
拓展思考
- 如果聚合结果需要按租户分库,可让租户 DB 名带
agg_前缀,利用 CouchDB 的多库复制过滤器把结果同步到中心 BI 库,再二次汇总。 - 对移动断网场景,可在 PouchDB 端先本地 reduce,再把结果文档同步到云端;此时要用业务时间戳 + 设备 ID 作为
_id的一部分,防止不同终端写回同一 key 造成冲突。 - 国内金融类项目要求审计溯源,可在写回时同时生成一条
_id=audit/{UUID}的审计文档,记录来源update_seq、计算脚本版本、操作人,方便监管核查。