如何用设计文档存储模型元数据?

解读

国内面试官问“如何用设计文档存储模型元数据”,并不是想听“把 JSON 写进去就行”这种表面答案,而是考察候选人是否真正理解 CouchDB 设计文档(_design 文档) 的体系化用法:

  1. 能否把字段约束、版本、权限、生命周期等业务元数据与视图、验证函数等系统元数据分门别类地放进设计文档;
  2. 能否利用 validate_doc_updateupdate_handlershow/list 等钩子,把元数据变成可执行约束,而不是静态注释;
  3. 能否在多租户、多版本、多环境(dev/test/prod)下,用 命名空间 + 版本号 + 时间戳 做隔离,并配合 replication filter 实现灰度;
  4. 能否解释清楚“设计文档也是文档”这一 CouchDB 核心哲学,从而推导出“元数据随数据一起走,不依赖外部系统”的离线优先优势。

一句话:面试官想看你是否能把设计文档当成分布式元数据注册中心来用,而不是简单当成“放视图的地方”。

知识点

  1. 设计文档本质:就是普通 JSON 文档,_id 以 _design/ 开头,自带 views、validate_doc_update、updates、shows、lists、rewrites、options 等保留字段。
  2. 元数据分层:
    • 业务元数据:字段类型、是否必填、枚举值、默认值、敏感度等级、所属租户、生效时间、过期时间。
    • 系统元数据:视图函数、索引定义、变更钩子、重写规则、权限掩码。
  3. 存储策略:
    • 单设计文档多模型:用 models 子对象按模型名聚类,例如 models.usermodels.order,避免 _design 爆炸。
    • 版本化命名_id: "_design/v2.3.1_user",配合 replication filter function(doc, req){ return !doc._id.startsWith('_design/v2.3.0'); } 实现灰度升级。
    • 加密与脱敏:把敏感元数据放到 shows/encrypt 处理器里,运行时动态加解密,静态不落盘。
  4. 校验落地:在 validate_doc_update 里读取同设计文档的 models[doc.type],用 JSON Schema 子集做字段校验,失败直接 throw({forbidden: '字段 age 必须 >= 18'}); 返回 403。
  5. 性能陷阱:
    • 设计文档过大(>1 MB)会拖慢 build_indices,需拆片或外置到 attachment
    • 视图函数里避免访问 models 子对象,防止 map 阶段频繁读取同一设计文档导致 couchjs 进程 CPU 飙高
    • 生产环境务必打开 options.partitioned = false 避免跨分区重建索引。
  6. 国内合规:若元数据含 个人信息字段规则,需在设计文档里附加 data_category=PII 标记,配合 国家出境评估办法rewrites 层做地域路由,防止跨境同步。

答案

给出一个可直接落地的模板,分五步:

  1. 命名规则:
    _id: "_design/v2.3.1_schema@tenant_a"版本号在前,租户在后,方便字典序排序与过滤。
  2. 文档骨架:
    {
      "_id": "_design/v2.3.1_schema@tenant_a",
      "language": "javascript",
      "options": {"partitioned": false},
      "models": {
        "user": {
          "type": "object",
          "required": ["mobile", "idCard"],
          "properties": {
            "mobile": {"pattern": "^1[3-9]\\d{9}$", "敏感等级": "C3"},
            "idCard": {"pattern": "^\\d{17}[\\dX]$", "敏感等级": "C4"}
          },
          "lifecycle": {"createBy": "system", "ttl": 31536000}
        }
      },
      "validate_doc_update": "function(newDoc, oldDoc, userCtx, secObj) {
        if (!newDoc.type) return;
        var model = this.models[newDoc.type];
        if (!model) throw({forbidden: '未知模型'});
        for (var field in model.required) {
          if (!newDoc[model.required[field]]) {
            throw({forbidden: '字段 ' + model.required[field] + ' 缺失'});
          }
        }
      }",
      "shows": {
        "encrypt": "function(doc, req) {
          if (req.query.field && doc[req.query.field]) {
            return {body: require('crypto').aesEncrypt(doc[req.query.field], req.query.key)};
          }
        }"
      }
    }
    
  3. 灰度发布:
    Fauxton 里先新建 _design/v2.3.2_schema@tenant_a,把 replication filter 设为只同步新版本到 北京可用区,观察 24 h 无异常后,再全量推送。
  4. 离线场景:
    移动端 PouchDB 先拉取设计文档,本地 db.get('_design/v2.3.1_schema@tenant_a') 拿到元数据,做客户端校验,保证弱网环境也能拦截非法数据,同步时再二次校验。
  5. 回滚策略:
    若线上发现模型规则过严,可直接 DELETE _design/v2.3.2_schema@tenant_a,CouchDB 会回退到上一版本,无需停机,符合国内金融客户“变更窗口 < 5 min” 的硬性要求。

拓展思考

  1. 多主复制冲突:当两个数据中心同时更新同一设计文档,CouchDB 会生成冲突分支。可以在 validate_doc_update 里检测 doc._conflicts,强制保留 版本号更大 的分支,或把冲突信息写入 conflict_log 业务库,供运营人工合并。
  2. 元数据即代码:把设计文档纳入 GitOps 流水线,MR 阶段couchdb-verify 工具做静态检查(语法、正则性能、敏感词),合并后自动 PUT 到测试 CouchDB,CD 阶段通过 ansible-couchdb 角色灰度到生产,实现“数据库 schema 像代码一样评审”。
  3. Serverless 场景:在 阿里云函数计算 里放一段 Node.js,监听 CouchDB _changes feed,一旦检测到设计文档版本号变更,自动刷新 API 网关JsonSchema 校验缓存,做到“模型变更 30 s 内生效”,满足国内电商大促期间快速改字段的极限需求。