给出一个“rereduce=true”分支的代码示例并解释其输入输出差异。
解读
国内一线互联网公司(阿里、腾讯、字节、美团等)在招聘 CouchDB 内核/运维/云原生方向 时,常把 reduce/rereduce 语义 作为“手写视图”环节的必考题。
面试官真正想看的是:
- 你是否理解 “rereduce=true” 时 CouchDB 传进来的值已经是上一轮 reduce 的聚合结果;
- 能否写出 幂等且可累加 的代码;
- 是否知道 输入结构差异(第一次 reduce 收到的是 emit 的原始 value,rereduce 收到的是上一轮 reduce 的输出)。
答不出“输入差异”会直接被判定为“只写过 Map 没写过 Reduce”,面试基本结束。
知识点
- reduce 阶段两次调用:
- 第一次:rereduce=false,values[] 里是 map 函数 emit 的原始值;
- 第二次及以后:rereduce=true,values[] 里是上一轮 reduce 返回的聚合对象。
- 输入长度:rereduce=false 时 values 长度等于当前分片内相同 key 的 emit 次数;rereduce=true 时 values 长度等于需要合并的上游分片数(默认 1~8)。
- 输出约束:必须返回 与 rereduce=false 时相同结构 的 JSON,否则后续再 rereduce 会类型错乱。
- 国内实战:移动端日志上报、离线订单同步等场景,都用 rereduce 做增量聚合 来降低网络传输。
答案
以下示例计算每个品类的 销售总额 & 订单笔数,兼容两级 rereduce,可直接粘进 CouchDB 2.3/3.x 的 design doc:
// map 函数
function (doc) {
if (doc.type === 'order' && doc.amount != null) {
emit(doc.category, {sum: doc.amount, count: 1});
}
}
// reduce 函数
function (keys, values, rereduce) {
var acc = {sum: 0, count: 0};
for (var i = 0; i < values.length; i++) {
acc.sum += values[i].sum;
acc.count += values[i].count;
}
return acc;
}
输入输出差异验证(用 curl 本地 5984 端口,国内开发机常用):
-
rereduce=false(CouchDB 第一次 reduce)
请求:POST /orders/_design/sales/_view/byCategory?group_level=1输入 values 示例:
[{sum:12.5,count:1}, {sum:30,count:1}, {sum:7.5,count:1}]输出:
{"rows":[ {"key":"手机配件","value":{"sum":50.0,"count":3}} ]} -
rereduce=true(CouchDB 把多个分片结果再次合并)
输入 values 变成上一轮 reduce 的输出:[{sum:50,count:3}, {sum:200,count:10}]输出:
{"sum":250,"count":13}
关键差异一句话总结:
rereduce=false 时 values 是“原始订单粒度的值”;rereduce=true 时 values 是“已经聚合过的中间结果”,但代码结构必须完全一致,否则 CouchDB 会抛 reduce_overflow_error。
拓展思考
- 国内大数据量优化:当单个品类日均千万条时,reduce 输出过大会触发
reduce_overflow_error,此时应:- 在 reduce 里 只保留预估值(HyperLogLog、分位数摘要),或
- 把
group_level调小,让 rereduce 层数变多,单层输入变少。
- 与 Cloudant 兼容:IBM Cloudant 国内节点(香港、上海)对 rereduce 有 512 KiB 返回值限制,务必在代码里做
JSON.stringify(result).length < 500*1024的防御。 - 面试加分点:主动提到 “二次 reduce 幂等性” 和 “分片迁移后触发 rereduce” 的坑,能让面试官直接给出“基础扎实,可进下一轮”的评价。