编写一个 Map 函数输出每个用户最近订单时间,要求键为 userId、值为 timestamp。
解读
国内一线互联网公司在面试 CouchDB 岗位时,通常把“写 Map 函数”作为手写代码+现场讲解的必考环节。本题表面看只是“取最大值”,但面试官真正想验证的是:
- 你是否理解 CouchDB 视图只能升序输出、一次扫描的限制;
- 你是否会用 emit(key, value) 的“二次排序”技巧把“最近”转换成“最大时间戳”;
- 你是否知道 rereduce 阶段可能拿到的值是上一轮 reduce 的结果,而不是原始时间戳;
- 你是否能在 5 分钟内写出可编译、可解释、无语法错误的代码,并主动说出“国内项目常用 UTC+8 时间戳、Long 型毫秒”这类工程细节。
如果仅仅写一条 emit(doc.userId, doc.timestamp) 而不解释 reduce 怎么拿最大值,基本会被判为“只用过 MongoDB,没真正踩过 CouchDB 的坑”。
知识点
- Map 函数签名:
function (doc) {},一旦声明即对全库逐行执行,禁止访问外部变量,禁止做阻塞 I/O。 - emit(key, value) 的语义:同一 key 的多条 value 在视图输出时会按 emit 顺序排列;利用这一点可以把“最大时间”问题转化为“取最后一条”。
- 视图只能升序:CouchDB 默认按 Unicode 码点升序排列;若想“倒序取最新”,要么在 reduce 里比较,要么在查询端用
?descending=true。 - reduce 可选,但本题必须写:因为同一 userId 可能有多条订单;reduce 阶段需要返回“最大值”,且必须处理 rereduce=true 的场景。
- 国内时间戳规范:线上环境统一用 13 位 Long 型毫秒时间戳(UTC+8),避免前端 Date 对象精度丢失。
- 性能陷阱:Map 里禁止
Date.parse等耗时操作;如果源数据是 ISO 字符串,最好在写入库前就转成 Long,Map 只做透传。
答案
以下代码可直接粘进 Fauxton 的“自定义视图”编辑器,通过 Build Index 后查询 /_design/orders/_view/latest_order?group=true 即可拿到每个 userId 的最新时间戳。
// map.js
function (doc) {
if (doc.type === 'order' && doc.userId && typeof doc.timestamp === 'number') {
// 把 timestamp 放在 key 的第二位,利用 CouchDB 的“key 有序”特性
emit([doc.userId, doc.timestamp], null);
}
}
// reduce.js
function (keys, values, rereduce) {
// 由于 map 阶段 emit 的 value 是 null,真正有用的是 keys[i][0][1]
var max = 0;
for (var i = 0; i < keys.length; i++) {
var ts = rereduce ? values[i] : keys[i][0][1];
if (ts > max) max = ts;
}
return max;
}
使用姿势
GET /db/_design/orders/_view/latest_order?group=true&group_level=1
返回片段示例:
{"key":["user123"],"value":1719993600000}
前端再按 new Date(value) 即可拿到北京时间。
拓展思考
- 如果订单量过亿,全量 Build 视图会触发大量磁盘写,国内大厂会采用 预聚合写时更新 策略:在订单服务落库时就往“用户最新订单时间”单独写一条汇总 doc,彻底绕过视图。
- 多租户隔离场景,key 可改为
[tenantId, userId, timestamp],利用 group_level=2 实现“租户+用户”两级汇总。 - 离线同步冲突:CouchDB 多主复制时同一 userId 可能在不同节点生成订单,reduce 拿到的最大值可能不是业务语义上的“最新”;解决方法是引入 向量时钟 或 业务层版本号,在 Map 里一起 emit,再在 reduce 里做冲突裁决。
- 国内合规要求:订单数据属“重要数据”,视图落地后同样要走 数据分级加密;Map/Reduce 代码必须进 Git 仓库做 代码审计,防止留后门把 userId 发到外部。