如何用设计文档存储模型元数据?
解读
国内面试官问“如何用设计文档存储模型元数据”,并不是想听“把 JSON 写进去就行”这种表面答案,而是考察候选人是否真正理解 CouchDB 设计文档(_design 文档) 的体系化用法:
- 能否把字段约束、版本、权限、生命周期等业务元数据与视图、验证函数等系统元数据分门别类地放进设计文档;
- 能否利用 validate_doc_update、update_handler、show/list 等钩子,把元数据变成可执行约束,而不是静态注释;
- 能否在多租户、多版本、多环境(dev/test/prod)下,用 命名空间 + 版本号 + 时间戳 做隔离,并配合 replication filter 实现灰度;
- 能否解释清楚“设计文档也是文档”这一 CouchDB 核心哲学,从而推导出“元数据随数据一起走,不依赖外部系统”的离线优先优势。
一句话:面试官想看你是否能把设计文档当成分布式元数据注册中心来用,而不是简单当成“放视图的地方”。
知识点
- 设计文档本质:就是普通 JSON 文档,_id 以 _design/ 开头,自带 views、validate_doc_update、updates、shows、lists、rewrites、options 等保留字段。
- 元数据分层:
- 业务元数据:字段类型、是否必填、枚举值、默认值、敏感度等级、所属租户、生效时间、过期时间。
- 系统元数据:视图函数、索引定义、变更钩子、重写规则、权限掩码。
- 存储策略:
- 单设计文档多模型:用 models 子对象按模型名聚类,例如
models.user、models.order,避免 _design 爆炸。 - 版本化命名:
_id: "_design/v2.3.1_user",配合 replication filterfunction(doc, req){ return !doc._id.startsWith('_design/v2.3.0'); }实现灰度升级。 - 加密与脱敏:把敏感元数据放到 shows/encrypt 处理器里,运行时动态加解密,静态不落盘。
- 单设计文档多模型:用 models 子对象按模型名聚类,例如
- 校验落地:在 validate_doc_update 里读取同设计文档的
models[doc.type],用 JSON Schema 子集做字段校验,失败直接throw({forbidden: '字段 age 必须 >= 18'});返回 403。 - 性能陷阱:
- 设计文档过大(>1 MB)会拖慢 build_indices,需拆片或外置到 attachment;
- 视图函数里避免访问 models 子对象,防止 map 阶段频繁读取同一设计文档导致 couchjs 进程 CPU 飙高;
- 生产环境务必打开 options.partitioned = false 避免跨分区重建索引。
- 国内合规:若元数据含 个人信息字段规则,需在设计文档里附加 data_category=PII 标记,配合 国家出境评估办法 在 rewrites 层做地域路由,防止跨境同步。
答案
给出一个可直接落地的模板,分五步:
- 命名规则:
_id: "_design/v2.3.1_schema@tenant_a",版本号在前,租户在后,方便字典序排序与过滤。 - 文档骨架:
{ "_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)}; } }" } } - 灰度发布:
在 Fauxton 里先新建_design/v2.3.2_schema@tenant_a,把 replication filter 设为只同步新版本到 北京可用区,观察 24 h 无异常后,再全量推送。 - 离线场景:
移动端 PouchDB 先拉取设计文档,本地 db.get('_design/v2.3.1_schema@tenant_a') 拿到元数据,做客户端校验,保证弱网环境也能拦截非法数据,同步时再二次校验。 - 回滚策略:
若线上发现模型规则过严,可直接 DELETE _design/v2.3.2_schema@tenant_a,CouchDB 会回退到上一版本,无需停机,符合国内金融客户“变更窗口 < 5 min” 的硬性要求。
拓展思考
- 多主复制冲突:当两个数据中心同时更新同一设计文档,CouchDB 会生成冲突分支。可以在 validate_doc_update 里检测 doc._conflicts,强制保留 版本号更大 的分支,或把冲突信息写入 conflict_log 业务库,供运营人工合并。
- 元数据即代码:把设计文档纳入 GitOps 流水线,MR 阶段跑 couchdb-verify 工具做静态检查(语法、正则性能、敏感词),合并后自动 PUT 到测试 CouchDB,CD 阶段通过 ansible-couchdb 角色灰度到生产,实现“数据库 schema 像代码一样评审”。
- Serverless 场景:在 阿里云函数计算 里放一段 Node.js,监听 CouchDB _changes feed,一旦检测到设计文档版本号变更,自动刷新 API 网关的 JsonSchema 校验缓存,做到“模型变更 30 s 内生效”,满足国内电商大促期间快速改字段的极限需求。