Chapter 07

自测题库

前六章建立了从文档模型到分片选型的完整链条。这一章不讲新东西,只逼着把它取出来用——题目分三层梯度,最后六道跨章场景题是真正检验"换没换对脑子"的地方。

怎么用这一章

  • 所有答案集中在文末一个折叠块里。先把答案写在纸上或编辑器里,再展开对照——直接看答案等于把题目当又读了一遍,零收益。
  • 三层梯度:概念层考词汇与定义,原理层考机制(为什么这么设计),应用判别层给场景逼你在多章方案间做选择。
  • 卡在原理层或判别层是正常的、也是有用的——那说明题目正好压在还没真正内化的地方。

▸题目的难度梯度

三层不是按章节分,而是按认知动作分:能复述 ≠ 懂机制 ≠ 会在真实场景里选对。判别层最少,却最能暴露问题。

难度递增 应用判别层 跨 01–06 场景 · 综合压轴 原理层 机制 why · 02 / 04 / 05 概念层 词汇与定义 · 01 / 03
图 7题目的三层梯度。注意:金字塔越往上越窄、题目越少,但单题信息量越大——能背概念层却答不出判别层,恰恰是"读得顺但没换对模型"的典型信号。

L1概念层(对应 01 / 03)

  1. BSON 相比文本 JSON,多了哪一类机制性能力?这对引擎读取嵌套字段有什么用?→ 01 §1.1
  2. _id 的三条硬规则是什么?ObjectId 的 12 字节由哪三段构成?→ 01 §1.2
  3. 16MB 文档上限"是信号不是限制"——它具体在提醒检查什么?→ 01 §1.3
  4. 一个数组字段建 multikey 索引、100 万文档、平均数组长 20,索引项大致多少?这对写入意味着什么?→ 03 §3.1
  5. ESR 规则里 E / S / R 各代表什么?为什么 Equality 必须放最前?→ 03 §3.2
  6. Bucket 设计模式解决的是哪一类问题?举一个适用场景。→ 03 §3.4

L2原理层(对应 02 / 04 / 05)

  1. $lookup 为什么是嵌套循环而不是 hash join?foreignField 无索引时复杂度是多少?→ 02 §2.3
  2. 多计划竞速(multi-planner)和 plan cache 怎么协作?什么时候会 replan?→ 02 §2.4
  3. WiredTiger 为什么没有 VACUUM?旧文档版本最终去了哪里?→ 04 §4.2
  4. cache 满到阈值后会发生什么?为什么把它叫"延迟悬崖"而不是"逐渐变慢"?→ 04 §4.3
  5. checkpoint 和 journal 各自负责持久性的哪一段?崩溃恢复怎么用它们?→ 04 §4.4
  6. oplog 条目为什么必须幂等?举一个被改写成幂等形式的操作。→ 05 §5.1
  7. 副本集复制协议和 Raft 的两个关键差异是什么?→ 05 §5.2
  8. w:majority 和 readConcern: majority 各自保证什么?组合起来排除了哪种故障?→ 05 §5.3

L3应用判别层(跨章场景 · 最难)

每道题给一个真实场景,要求在多章的方案之间做选择并说出判据。这一层是这份教程的综合压轴——不是考"会不会用某个 API",而是考"换没换对模型"。

  1. 建模 × 查询:电商系统里"商品"和"库存",库存被多个端高频独立改写,而商品详情页要同时显示商品信息和当前库存。库存该嵌入商品文档还是引用?说出判据,并指出嵌入会带来哪种写入代价。(01 建模 + 03 嵌入决策 + 02 $lookup 代价)
  2. 查询 × 索引:一条聚合很慢,explain 显示它对 $lookup 的 foreignField 做全集合扫描,且末尾有一个超 100MB 的 $sort。瓶颈在哪两处?分别怎么修?(02 $lookup/执行 + 03 索引/ESR)
  3. 分片 × 访问模式:多租户 SaaS,写入按租户高度不均(少数大租户占大部分写),偶尔要跨租户出 BI 报表。shard key 选什么?跨租户报表该怎么处理?(06 shard key + 01 访问模式 + 02 scatter-gather)
  4. 一致性 × 读写关注:用户改完个人资料立刻跳转详情页,必须看到刚改的值;而全站用户列表页允许看到稍旧的数据。两处分别怎么配 read preference / readConcern / 会话?(05 读写关注 + 因果一致性)
  5. 建模 × 存储:IoT 设备每秒一条约 200 字节的读数。直接把读数 push 进设备文档的数组里,约 22 小时就撞 16MB。正确的文档粒度是什么?背后是哪个设计模式、为什么也对 cache 友好?(01 16MB + 03 Bucket + 04 cache)
  6. 选型判别:什么时候 PostgreSQL 的 JSONB 比 MongoDB 更合适?反过来,哪三个特征出现时 MongoDB 才真正胜过带 JSONB 的 Postgres?(06 选型 + 01 访问模式主线)
亲手画一张图

合上教程,在纸上或 Excalidraw 里默画 MongoDB 的概念地图——只画 8 个节点(文档模型 / 查询·聚合 / 索引 / schema 设计 / WiredTiger / 副本集 / 分片 / 选型)和它们之间的依赖箭头。画完回到起点页图 0 对照:你有没有画出"索引"和"schema 设计"之间那条"同一个决策"的边?如果没画出来,回 03 章重读——那条边正是这份教程要你换上的那块模型。

第二张(可选):默画图 5.2 的写入时间线——标出 w:1 在哪个时刻返回成功、哪个时刻起才不可回滚。中间那段缝隙能不能讲清,决定了你有没有真正区分"持久"和"可见"。

▸答案

先把上面三层都做完,再展开。给自己的判别层答案打分时,标准不是"措辞对不对",而是"判据指对没指对、有没有落回访问模式"。

展开全部答案(先做完再点)

概念层

  1. 多了带类型与长度前缀的二进制字段编码,以及 JSON 没有的类型(ObjectId / Decimal128 / Date / Binary / 区分 32/64 位整数)。长度前缀让引擎能不解析整个文档就跳过字段、直接定位到一个嵌套路径。
  2. 规则:强制存在、不可变、自带唯一索引。ObjectId = 4 字节时间戳 + 5 字节随机值 + 3 字节自增计数器,三段都能在客户端本地生成,无需访问数据库取序号。
  3. 提醒检查:是不是把一个无界增长的数组(评论、事件、历史)嵌进了文档。逼近上限几乎总意味着访问模式选错了粒度,应改成分桶或引用。
  4. 约 100 万 × 20 = 2000 万个索引项(multikey 给数组每个元素各存一项)。意味着每次改这个数组都要维护多个索引项,数组越大写入越慢——所以数组要有界。
  5. E = Equality(等值)、S = Sort(排序)、R = Range(范围)。Equality 放最前是因为它把 B-tree 扫描缩到一段连续区间,后面的 Sort 和 Range 只在这一小段上工作;放后面则失去这个收窄效果。
  6. 解决高频小数据无界增长的问题——把按时间/批次产生的小数据分桶成多条有界文档,而不是无限嵌进一个数组。典型场景:IoT 读数、日志、按小时聚合的指标。

原理层

  1. 因为 $lookup 对左侧每个输入文档都去右集合查一次,没有关系型优化器的 hash/merge join 策略。foreignField 无索引时,每个左文档触发一次右集合全扫描,复杂度 O(N×M)。修法:给 foreignField 建索引、先 $match 缩小左侧、或干脆嵌入。
  2. 第一次见到某查询形状,multi-planner 并行试跑多个候选计划一小段,选"产出最多/做最少工作"的胜者存进 plan cache;后续相同形状复用缓存。当数据分布变化或缓存计划表现退化时触发 replan。
  3. 因为旧版本不就地覆盖、也不靠后台清理:eviction 做页重整(reconciliation)时,最新值进数据文件、旧版本进磁盘上的 history store。回收发生在 eviction/checkpoint 流程里,不需要 Postgres 那样的 VACUUM 扫描死元组。
  4. 越过 eviction 阈值后,应用线程被"征召"同步参与 eviction 才能继续推进自己的操作。这是一道悬崖而非缓坡——working set 一旦超过 cache,p99 延迟陡升,因为前台线程突然要替引擎打工,而不是后台默默降速。
  5. checkpoint(约 60s)落一份一致的磁盘快照;journal/WAL 覆盖两次 checkpoint 之间的窗口。崩溃恢复 = 加载最后一个完整 checkpoint,再重放它之后的 journal。
  6. 因为 secondary 可能重复重放同一批 oplog,幂等保证重放不出错。例:$inc: {n:1} 被改写成绝对值 $set: {n: 实际新值};一条改多文档的更新被拆成逐文档条目。
  7. 任选两个:(1) 副本集日志是幂等操作,不是状态机命令;(2) 复制是 secondary 主动 pull,不是 Raft 的 leader push(AppendEntries);(3) commit point 靠 primary 轮询多数节点的 lastDurable 算出,不是逐条计票;(4) 多了 priority takeover 和 dry-run election。
  8. w:majority 保证写在多数节点 durable 后才返回(故不会因 failover 被回滚);readConcern: majority 保证读到的是 majority commit point 的快照(读到的数据不会被回滚)。组合起来排除了"读到一个稍后会被回滚的写"这种故障。

应用判别层

  1. 引用。库存命中两条"倾向引用"的判据——高频独立写入、写入时机与商品信息不一致;嵌入会造成更新放大(改库存要重写整个商品大文档,并制造版本churn)。详情页同时要两者,可在读取时 $lookup(给 inventory 的 productId 建索引),或用 Computed/Subset 模式冗余一个"是否有货"的轻量标志到商品文档、精确库存再查。核心:按写入模式决定,不按"库存属于商品"这种逻辑归属决定。
  2. 两处瓶颈:(1) $lookup 的 foreignField 全扫描——给 foreignField 建索引,并把 $match 提到 $lookup 之前缩小左侧输入;(2) 超 100MB 的 $sort 内存溢出——用索引支撑的排序(按 ESR 把 sort 字段放进复合索引的正确位置),实在不行才开 allowDiskUse。两处分别对应 02 章的执行机制和 03 章的索引设计。
  3. shard key 用 compound,如 {tenantId, 某高基数字段}:tenantId 提供租户隔离与查询定向,第二字段把大租户内部的写入分散开,避免单调递增热点。纯 tenantId 会让大租户变成 jumbo chunk。跨租户 BI 报表是即席聚合的弱区,且会变成 scatter-gather——把分析数据 ETL 到列存/数仓单独跑,不要在 OLTP 分片库上硬跑全表聚合。一个系统两种负载分开放。
  4. 详情页要 read-your-writes:用因果一致性会话(causally consistent session)+ readConcern: majority,或直接读 primary,保证读到刚写的值。列表页容忍旧值:可读 secondary + readConcern: local 换吞吐。关键是按访问点分别选一致性级别,不是全站一刀切——这正是把一致性当连续谱而非布尔。
  5. 正确粒度是设备 + 时间窗(如每设备每小时一条文档,内含该小时的读数数组),即 Bucket 模式。它对 cache 友好的原因:WiredTiger 以整个文档为载入单位,有界的小桶文档不会像无界大文档那样把热数据挤出 cache;分桶把读写都限制在固定大小的文档上。
  6. JSONB 更合适当:JSON 只是整体关系模型里的少数字段、需要把 JSON 与强类型表 join、或 schema 迟早会硬化下来(GIN 索引能查 containment)。MongoDB 真正胜出需要三个特征同时出现:整个模型是文档 + 读写以单个文档为单位 + 需要原生水平写分片。只满足"有点 JSON"不构成换库理由。
进阶挑战 · 刚好够不着

把这份教程压成一页选型备忘

不看教程,写一页"MongoDB 选型 + 建模"备忘录给团队:包含 (1) 三个"该上 MongoDB"的信号、(2) 三个"别上"的信号、(3) 嵌入 vs 引用的四问、(4) shard key 的两条红线、(5) 一句话的持久性配置默认建议。写完和各章对照,缺哪条就回哪章。

提示(卡住再展开)

骨架:该上=schema 多变 + 以文档为读写单位 + 要水平写扩展;别上=重多实体事务 + 复杂即席 join/BI + 强关系约束。四问=一起读/无界/共享/写入时机。红线=不用单调递增键、不用低基数键。默认=w:majority 起步,按访问点调 readConcern。每一条都能在前六章找到出处。