编写一个 Map 函数输出每个用户最近订单时间,要求键为 userId、值为 timestamp。

解读

国内一线互联网公司在面试 CouchDB 岗位时,通常把“写 Map 函数”作为手写代码+现场讲解的必考环节。本题表面看只是“取最大值”,但面试官真正想验证的是:

  1. 你是否理解 CouchDB 视图只能升序输出、一次扫描的限制;
  2. 你是否会用 emit(key, value) 的“二次排序”技巧把“最近”转换成“最大时间戳”;
  3. 你是否知道 rereduce 阶段可能拿到的值是上一轮 reduce 的结果,而不是原始时间戳;
  4. 你是否能在 5 分钟内写出可编译、可解释、无语法错误的代码,并主动说出“国内项目常用 UTC+8 时间戳、Long 型毫秒”这类工程细节。

如果仅仅写一条 emit(doc.userId, doc.timestamp) 而不解释 reduce 怎么拿最大值,基本会被判为“只用过 MongoDB,没真正踩过 CouchDB 的坑”。

知识点

  1. Map 函数签名function (doc) {},一旦声明即对全库逐行执行,禁止访问外部变量,禁止做阻塞 I/O。
  2. emit(key, value) 的语义:同一 key 的多条 value 在视图输出时会按 emit 顺序排列;利用这一点可以把“最大时间”问题转化为“取最后一条”。
  3. 视图只能升序:CouchDB 默认按 Unicode 码点升序排列;若想“倒序取最新”,要么在 reduce 里比较,要么在查询端用 ?descending=true
  4. reduce 可选,但本题必须写:因为同一 userId 可能有多条订单;reduce 阶段需要返回“最大值”,且必须处理 rereduce=true 的场景。
  5. 国内时间戳规范:线上环境统一用 13 位 Long 型毫秒时间戳(UTC+8),避免前端 Date 对象精度丢失。
  6. 性能陷阱: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) 即可拿到北京时间。

拓展思考

  1. 如果订单量过亿,全量 Build 视图会触发大量磁盘写,国内大厂会采用 预聚合写时更新 策略:在订单服务落库时就往“用户最新订单时间”单独写一条汇总 doc,彻底绕过视图。
  2. 多租户隔离场景,key 可改为 [tenantId, userId, timestamp],利用 group_level=2 实现“租户+用户”两级汇总。
  3. 离线同步冲突:CouchDB 多主复制时同一 userId 可能在不同节点生成订单,reduce 拿到的最大值可能不是业务语义上的“最新”;解决方法是引入 向量时钟业务层版本号,在 Map 里一起 emit,再在 reduce 里做冲突裁决。
  4. 国内合规要求:订单数据属“重要数据”,视图落地后同样要走 数据分级加密;Map/Reduce 代码必须进 Git 仓库做 代码审计,防止留后门把 userId 发到外部。