<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>MongoDB on 方糖猫Blog</title>
    <link>https://ftmao.com/blog/tags/mongodb/</link>
    <description>AI 视频、AI Agent 与工程实践笔记</description>
    <generator>Hugo</generator>
    <language>zh-CN</language>
    <lastBuildDate>Tue, 26 Oct 2021 10:30:00 +0800</lastBuildDate><atom:link href="https://ftmao.com/blog/tags/mongodb/index.xml" rel="self" type="application/rss+xml" /><item>
      <title>MongoDB 从入门到精通：存储引擎、建模、索引、副本集、分片与事务实战</title>
      <link>https://ftmao.com/blog/posts/mongodb-beginner-to-mastery/</link>
      <pubDate>Tue, 26 Oct 2021 10:30:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/mongodb-beginner-to-mastery/</guid>
      <description>&lt;h1 id=&#34;mongodb-从入门到精通存储引擎建模索引副本集分片与事务实战&#34;&gt;MongoDB 从入门到精通：存储引擎、建模、索引、副本集、分片与事务实战&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;目标读者：用过 MySQL 之类的关系型数据库，现在要接 MongoDB 的同学——可能是接手存量系统，可能是新业务选型，也可能是想给自己的技术栈补上 NoSQL 这一块。&lt;br&gt;
本文不浪费篇幅在「什么是文档」这种名词解释上，直接从真正影响系统生死的东西讲起：存储引擎、一致性、高可用、性能。&lt;br&gt;
写这篇的契机：2021 年我所在团队的电商中台把部分业务从 MySQL 迁到 MongoDB，整个过程脱了一层皮。这篇是我希望迁移前有人能拍在桌上的笔记。&lt;br&gt;
文内版本基线：MongoDB 5.0（2021 年 7 月发布）、官方 Java 同步驱动 4.3、JDK 11。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;全文八篇，按真实的成长路径编排：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    A[&amp;#34;一 选型认知&amp;lt;br/&amp;gt;Mongo 适合什么&amp;#34;] --&amp;gt; B[&amp;#34;二 存储引擎&amp;lt;br/&amp;gt;WiredTiger 底牌&amp;#34;]
    B --&amp;gt; C[&amp;#34;三 数据建模&amp;lt;br/&amp;gt;核心战场&amp;#34;]
    C --&amp;gt; D[&amp;#34;四 索引与 explain&amp;lt;br/&amp;gt;调优基本功&amp;#34;]
    D --&amp;gt; E[&amp;#34;五 聚合管道&amp;lt;br/&amp;gt;服务端计算&amp;#34;]
    E --&amp;gt; F[&amp;#34;六 副本集&amp;lt;br/&amp;gt;高可用与读写语义&amp;#34;]
    F --&amp;gt; G[&amp;#34;七 事务与分片&amp;lt;br/&amp;gt;规模化与一致性&amp;#34;]
    G --&amp;gt; H[&amp;#34;八 运维与 AI 时代&amp;lt;br/&amp;gt;监控备份 少写代码&amp;#34;]
    style H fill:#e8f4e8&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;学习 MongoDB 要始终围绕三个问题：&lt;strong&gt;数据安不安全、查询快不快、流量扛不扛得住&lt;/strong&gt;。这篇的每一节，都是对这三问的某一次正面回答。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;第一篇-选型认知什么时候该用-mongodb什么时候别逞强&#34;&gt;第一篇 选型认知：什么时候该用 MongoDB，什么时候别逞强&lt;/h2&gt;
&lt;p&gt;第一课不是怎么用，是&lt;strong&gt;什么时候用&lt;/strong&gt;。用错场景的 MongoDB，比不用更可怕。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    subgraph 适合[&amp;#34;MongoDB 的主场&amp;#34;]
        P[&amp;#34;商品中心&amp;lt;br/&amp;gt;类目属性千差万别&amp;lt;br/&amp;gt;文档模型天然契合&amp;#34;]
        C[&amp;#34;购物车/会话&amp;lt;br/&amp;gt;读多写多 结构灵活&amp;#34;]
        L[&amp;#34;行为日志/埋点&amp;lt;br/&amp;gt;写入量大 schema 常变&amp;lt;br/&amp;gt;TTL 索引自动过期&amp;#34;]
        CM[&amp;#34;CMS/配置中心&amp;lt;br/&amp;gt;页面配置 JSON 化&amp;#34;]
    end
    subgraph 谨慎[&amp;#34;需要精心设计的边界&amp;#34;]
        O[&amp;#34;订单/库存&amp;lt;br/&amp;gt;可用 但要事务+对账设计&amp;#34;]
        M[&amp;#34;消息/Feed 流&amp;lt;br/&amp;gt;可用 注意分片键设计&amp;#34;]
    end
    subgraph 别上[&amp;#34;别硬上 Mongo&amp;#34;]
        F[&amp;#34;账务/清结算&amp;lt;br/&amp;gt;强事务 强审计&amp;lt;br/&amp;gt;交给关系型&amp;#34;]
        S[&amp;#34;复杂搜索&amp;lt;br/&amp;gt;交给 Elasticsearch&amp;#34;]
        J[&amp;#34;重度多表关联分析&amp;lt;br/&amp;gt;交给 OLAP/数仓&amp;#34;]
    end
    style 适合 fill:#e8f4e8
    style 别上 fill:#fde8e8&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;一句话原则：&lt;strong&gt;MongoDB 适合「单实体自成一体、结构多变、读多」的在线业务；不适合「钱」「搜索」「重度关联分析」。&lt;/strong&gt; 判断依据不是性能 benchmark，而是数据访问模式——先把系统里 80% 的查询列出来，看它们能不能被「单集合 + 少量 $lookup」覆盖。覆盖不了，选型本身就有问题。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;思考与感悟之一&lt;/strong&gt;：数据库选型和框架选型完全不同。框架选错了可以重构，数据库选错了是「数据搬家」级别的灾难——停机迁移、双写对账、灰度切流，每一步都在钢丝上走。所以对「不做什么」的清单，要比「做什么」的清单更长。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;第二篇-wiredtiger理解存储引擎才配谈性能&#34;&gt;第二篇 WiredTiger：理解存储引擎，才配谈性能&lt;/h2&gt;
&lt;p&gt;很多人用 MongoDB 三年，不知道底下是 WiredTiger。这就像开了一辈子车不懂发动机——平时没事，出事时只能干瞪眼。这一篇先把「数据到底怎么存的」讲透，再回答那个面试必问的问题：&lt;strong&gt;MongoDB 为什么快，为什么扛得住大量读写。&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&#34;21-底层数据模型bson磁盘上到底存了什么&#34;&gt;2.1 底层数据模型：BSON，磁盘上到底存了什么&lt;/h3&gt;
&lt;p&gt;你在应用层操作的是「文档」（可以理解成 JSON），但磁盘上存的是 &lt;strong&gt;BSON（Binary JSON）&lt;/strong&gt;——一种二进制、自描述、长度前缀的编码格式：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    subgraph BSON[&amp;#34;一个 BSON 文档的字节布局&amp;#34;]
        L[&amp;#34;int32 总长度&amp;lt;br/&amp;gt;开头 4 字节&amp;#34;] --&amp;gt; E1[&amp;#34;元素1&amp;lt;br/&amp;gt;类型字节 + 字段名 + 值&amp;#34;]
        E1 --&amp;gt; E2[&amp;#34;元素2&amp;lt;br/&amp;gt;类型字节 + 字段名 + 值&amp;#34;]
        E2 --&amp;gt; EN[&amp;#34;...&amp;#34;]
        EN --&amp;gt; Z[&amp;#34;0x00 结束符&amp;#34;]
    end
    subgraph TYPES[&amp;#34;BSON 比 JSON 多出的类型&amp;#34;]
        T1[&amp;#34;ObjectId / Date / 正则&amp;#34;]
        T2[&amp;#34;int32 / int64 / double&amp;#34;]
        T3[&amp;#34;Decimal128 高精度十进制&amp;#34;]
        T4[&amp;#34;BinData 二进制 / 数组 / 内嵌文档&amp;#34;]
    end
    BSON -.-&amp;gt; TYPES&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;BSON 的设计直接回答了两个性能问题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;长度前缀 + 类型字节 → 遍历时可以「跳过」字段。&lt;/strong&gt; 查询只取 &lt;code&gt;name&lt;/code&gt; 字段时，引擎不需要解析整个文档，读到字段长度直接指针后移——这就是为什么投影（projection）能省时间。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;类型丰富 → 不需要序列化转换。&lt;/strong&gt; JSON 只有 string/number/boolean，日期只能存字符串再解析；BSON 原生支持 Date、ObjectId、Decimal128（钱的精确计算就靠它）、二进制，省去了应用层和存储层之间大量的类型转换开销。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;更关键的是&lt;strong&gt;文档在磁盘上是连续存放的&lt;/strong&gt;：一个商品文档（含内嵌的 SKU 数组、图片列表）在磁盘上是&lt;strong&gt;一整块连续的字节&lt;/strong&gt;。这和关系型数据库形成鲜明对比——&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    subgraph MYSQL[&amp;#34;MySQL 读一个完整商品&amp;#34;]
        A1[&amp;#34;product 表的 B+ 树页&amp;#34;] --&amp;gt; J[&amp;#34;JOIN&amp;#34;]
        A2[&amp;#34;sku 表的 B+ 树页&amp;lt;br/&amp;gt;磁盘另一处&amp;#34;] --&amp;gt; J
        A3[&amp;#34;image 表的 B+ 树页&amp;lt;br/&amp;gt;磁盘又一处&amp;#34;] --&amp;gt; J
        J --&amp;gt; R[&amp;#34;多次随机 I/O&amp;lt;br/&amp;gt;才能拼出一个商品&amp;#34;]
    end
    subgraph MONGO[&amp;#34;MongoDB 读一个完整商品&amp;#34;]
        B[&amp;#34;一次磁盘寻道&amp;lt;br/&amp;gt;读出连续的一整块 BSON&amp;#34;]
    end
    style MONGO fill:#e8f4e8&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;这就是 MongoDB 设计哲学里最重要的词：&lt;strong&gt;数据局部性（data locality）&lt;/strong&gt;。「一起读的数据，就存在一起」。嵌入建模之所以是 Mongo 的灵魂，底层原因就在这里——它不是风格偏好，是磁盘 I/O 决定的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;追问：文档是定长连续存放的，那修改、变大之后怎么办？磁盘会不连续吗？&lt;/strong&gt; 这要分两个时代说。MMAPv1 时代（3.0 之前）文档存在定长槽位里，变大就得整体搬家（document move），旧槽位变空洞，磁盘碎片是当年最臭名昭著的问题。WiredTiger 时代换了个思路：&lt;strong&gt;存储单位是 B 树的页而不是文档&lt;/strong&gt;，文档是叶页里的一条 record；修改&lt;strong&gt;从不在原地写&lt;/strong&gt;——checkpoint 时用 copy-on-write 把改后的整页写到磁盘&lt;strong&gt;新位置&lt;/strong&gt;，旧块进空闲列表复用；文档长大导致页放不下就触发&lt;strong&gt;页分裂&lt;/strong&gt;，超大文档走 overflow 页。所以「连续」的准确含义是「逻辑上一条 record、一次 I/O 读取」，物理位置本来就会搬。代价有两个：长期高频更新会积累碎片（用 &lt;code&gt;db.collection.compact()&lt;/code&gt; 定期回收），以及&lt;strong&gt;频繁长大的文档会引发频繁页分裂&lt;/strong&gt;——这是第三篇「增长边界」判断的底层原因，时序类数据的标准解法是按时间桶预分配文档的 bucket 模式，让单文档尺寸保持稳定。&lt;/p&gt;
&lt;h3 id=&#34;22-为什么快读快与写快的底层原因&#34;&gt;2.2 为什么快：读快与写快的底层原因&lt;/h3&gt;
&lt;p&gt;「MongoDB 快」不能当口号喊，要能拆成具体机制。先写后读。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;写入为什么快——一条写请求的完整路径：&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    APP[&amp;#34;应用&amp;lt;br/&amp;gt;Java 驱动&amp;#34;] --&amp;gt; M[&amp;#34;mongod&amp;lt;br/&amp;gt;内存操作&amp;#34;]
    M --&amp;gt; C[&amp;#34;WiredTiger cache&amp;lt;br/&amp;gt;修改页标记为 dirty&amp;lt;br/&amp;gt;写操作到此即完成&amp;#34;]
    M --&amp;gt; J[&amp;#34;journal 日志&amp;lt;br/&amp;gt;顺序追加写&amp;lt;br/&amp;gt;磁盘顺序 I/O 极快&amp;#34;]
    C -.-&amp;gt;|&amp;#34;checkpoint&amp;lt;br/&amp;gt;默认每 60 秒&amp;lt;br/&amp;gt;批量刷盘&amp;#34;| D[&amp;#34;数据文件&amp;lt;br/&amp;gt;压缩后的 B 树页&amp;#34;]
    style C fill:#e8f4e8&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;四个叠加的机制：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;写内存即返回&lt;/strong&gt;。写操作修改的是 cache 里的页，标记 dirty 就完事，数据文件由 checkpoint 线程&lt;strong&gt;每 60 秒批量刷一次&lt;/strong&gt;——随机写被攒成了顺序批量写。这是「写快」的第一来源。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;journal 用顺序写保证持久性&lt;/strong&gt;。怕宕机丢内存里的数据？journal（WAL 日志）是&lt;strong&gt;顺序追加写&lt;/strong&gt;，磁盘的顺序写比随机写快几个数量级。&lt;code&gt;j: true&lt;/code&gt; 的写确认就是等 journal 落盘，而不是等数据文件落盘。&lt;/p&gt;
&lt;p&gt;这里必须正面回答一个所有人都会问的问题：&lt;strong&gt;「写入内存就返回，服务器挂了怎么办？」&lt;/strong&gt; 不会变成脏数据——cache 里丢掉的只是 dirty 页的内存副本，每次修改都已先追加进 journal；mongod 重启时以最后一次 checkpoint 为基线&lt;strong&gt;重放 journal&lt;/strong&gt;，把丢失的写补回数据文件。真正的风险窗口在确认时机：默认 &lt;code&gt;w:1&lt;/code&gt; 下 journal 每 ~100ms 才批量提交，断电最多丢「最近约 100ms 且未同步到从节点」的写（第六篇丢的 23 笔订单就是这么没的）；&lt;code&gt;j: true&lt;/code&gt; 等日志落盘，单节点宕机不丢；&lt;code&gt;w: majority&lt;/code&gt; 等多数派确认，机器物理损毁也不丢。&lt;strong&gt;丢不丢数据不是 MongoDB 决定的，是你选 writeConcern 档位时决定的&lt;/strong&gt;——这套「内存脏页异步刷盘 + 日志重放」和 MySQL InnoDB 的 redo log 是同一套思想。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;文档级并发&lt;/strong&gt;。更新只锁单个文档，100 个线程写 100 个不同文档完全不互斥。对比早期 MySQL MyISAM 的表锁、乃至行锁时代的间隙锁开销，文档锁的冲突概率天然更低。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;没有跨行约束的包袱&lt;/strong&gt;。无外键检查、无触发器、单文档原子性不需要 undo log（改一个文档就是改一整块，要么全改要么不改），写入路径短。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;读取为什么快——四道防线：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;局部性&lt;/strong&gt;：如上，嵌入建模让「读一个完整实体」= 一次磁盘寻道，没有 JOIN 的多次随机 I/O。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缓存&lt;/strong&gt;：热数据常驻 WiredTiger cache（默认一半内存），命中内存的读在微秒级。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;B 树索引&lt;/strong&gt;：索引项 O(log n) 定位，千万级数据也就 4 层左右，加覆盖索引（第四篇）连文档都不用读。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;副本集读扩展&lt;/strong&gt;：&lt;code&gt;readPreference: secondary&lt;/code&gt; 把展示类读流量分摊到从节点，读吞吐随节点数线性扩展。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;大量读 + 大量写同时扛住的原因&lt;/strong&gt;：读写分离（从节点扛读）+ MVCC（读不阻塞写、写不阻塞读）+ 写内存化。这三件事同时成立，才撑得起电商大促「几十万读 QPS 的商品页 + 几万写 QPS 的库存扣减」同框的场景。还不够？分片把写入水平切到多个副本集，写吞吐同样线性扩展（第七篇）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;快的代价（必须说清楚，否则就是忽悠）&lt;/strong&gt;：写内存即返回意味着默认 &lt;code&gt;w:1&lt;/code&gt; 下宕机可能丢最后一小段数据（第六篇的丢单事故就是这么来的）；热数据依赖内存，内存小于工作集时性能断崖；没有 JOIN 意味着关联查询要自己扛。&lt;strong&gt;MongoDB 的快是「用设计取舍换来的快」，不是魔法。&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&#34;23-wiredtiger-你必须知道的四件事&#34;&gt;2.3 WiredTiger 你必须知道的四件事&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    W[&amp;#34;WiredTiger 存储引擎&amp;lt;br/&amp;gt;MongoDB 3.2+ 默认&amp;#34;] --&amp;gt; A[&amp;#34;文档级并发控制&amp;lt;br/&amp;gt;多线程写同一集合不互斥&amp;lt;br/&amp;gt;只有同一文档冲突才重试&amp;#34;]
    W --&amp;gt; B[&amp;#34;压缩存储&amp;lt;br/&amp;gt;snappy 默认&amp;lt;br/&amp;gt;磁盘占用约为数据量的 30%-70%&amp;#34;]
    W --&amp;gt; C[&amp;#34;缓存机制&amp;lt;br/&amp;gt;cacheSizeGB 默认 = 50% × (内存 - 1GB)&amp;lt;br/&amp;gt;热数据全在内存里&amp;#34;]
    W --&amp;gt; D[&amp;#34;MVCC 多版本&amp;lt;br/&amp;gt;读不阻塞写 写不阻塞读&amp;lt;br/&amp;gt;snapshot 隔离&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;要记的结论：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;文档级锁&lt;/strong&gt;：并发写同一个集合是免费的，但&lt;strong&gt;并发改同一个文档会串行重试&lt;/strong&gt;。这直接决定建模——秒杀库存如果把 100 个 SKU 的库存都嵌在一个商品文档里，大促时这个文档就是全站最热的锁热点。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缓存即命&lt;/strong&gt;：WiredTiger cache 默认吃掉一半内存。生产上「Mongo 内存打满」往往不是泄漏，是 cache 在正常工作——看 &lt;code&gt;wiredTiger.cache&lt;/code&gt; 指标而不是看进程 RSS 下结论。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;压缩是把双刃剑&lt;/strong&gt;：省磁盘，费 CPU。写密集型日志库可换 zlib 更高压缩比，CPU 紧张时别乱调。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MVCC 的代价&lt;/strong&gt;：老版本数据要清理，长时间不提交的事务会压住清理线程，导致 cache 中 dirty data 堆积、写入被拖慢。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;踩坑故事 #1（cache 被大事务压垮）&lt;/strong&gt;：我们有个定时任务一次性更新 200 万条商品，跑的时候整个库延迟飙升。原因：巨型事务导致 WiredTiger 无法清理旧版本，cache 中 dirty bytes 超过阈值后应用线程被迫参与淘汰（eviction），写入集体变慢。解法：&lt;strong&gt;批量操作必须拆批&lt;/strong&gt;（每批 1000 条 + 短暂让出窗口），生产环境永远不开「不设限的大批量更新」。这条后来成了团队《MongoDB 使用红线》第一条。&lt;/p&gt;
&lt;h3 id=&#34;24-本地环境直接上副本集别玩单机&#34;&gt;2.4 本地环境：直接上副本集，别玩单机&lt;/h3&gt;
&lt;p&gt;单节点 MongoDB 不支持事务，也不具备生产形态。本地练手就用 Docker Compose 起一套三节点副本集：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# docker-compose.yml&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;version&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;3.8&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;services&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;mongo1&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;image&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;mongo:5.0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;command&lt;/span&gt;: [&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;--replSet&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;rs0&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;--bind_ip_all&amp;#34;&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;ports&lt;/span&gt;: [&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;27017:27017&amp;#34;&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;mongo2&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;image&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;mongo:5.0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;command&lt;/span&gt;: [&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;--replSet&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;rs0&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;--bind_ip_all&amp;#34;&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;mongo3&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;image&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;mongo:5.0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;command&lt;/span&gt;: [&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;--replSet&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;rs0&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;--bind_ip_all&amp;#34;&lt;/span&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# 初始化副本集（执行一次）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;mongosh --eval &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;rs.initiate({
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;  _id: &amp;#34;rs0&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;  members: [
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;    { _id: 0, host: &amp;#34;mongo1:27017&amp;#34; },
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;    { _id: 1, host: &amp;#34;mongo2:27017&amp;#34; },
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;    { _id: 2, host: &amp;#34;mongo3:27017&amp;#34; }
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;  ]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;})&amp;#39;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;hr&gt;
&lt;h2 id=&#34;第三篇-数据建模核心战场没有之一&#34;&gt;第三篇 数据建模：核心战场，没有之一&lt;/h2&gt;
&lt;p&gt;MongoDB 精通与否，就看建模。SQL 建模有范式护体，Mongo 建模全靠经验与取舍——这也是 AI 最帮不上忙的部分（第八篇详谈）。&lt;/p&gt;
&lt;h3 id=&#34;31-唯一重要的问题嵌入还是引用&#34;&gt;3.1 唯一重要的问题：嵌入还是引用&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    Q[&amp;#34;一份数据，嵌入父文档&amp;lt;br/&amp;gt;还是独立成集合？&amp;#34;] --&amp;gt; A{&amp;#34;三个判断&amp;#34;}
    A --&amp;gt; B[&amp;#34;读写边界&amp;lt;br/&amp;gt;总是一起读写吗？&amp;#34;]
    A --&amp;gt; C[&amp;#34;增长边界&amp;lt;br/&amp;gt;数量有上限吗？&amp;#34;]
    A --&amp;gt; D[&amp;#34;并发边界&amp;lt;br/&amp;gt;会被高并发独立更新吗？&amp;#34;]
    B --&amp;gt;|&amp;#34;是，且&amp;#34;| E[&amp;#34;嵌入&amp;lt;br/&amp;gt;如：订单内嵌 items 快照&amp;#34;]
    C --&amp;gt;|&amp;#34;否，无限增长&amp;#34;| R[&amp;#34;引用&amp;lt;br/&amp;gt;如：商品引用评论&amp;#34;]
    D --&amp;gt;|&amp;#34;是，热点&amp;#34;| R2[&amp;#34;引用&amp;lt;br/&amp;gt;如：SKU 库存独立成文档&amp;lt;br/&amp;gt;避免单文档成锁热点&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;三个判断，每个都对应一个坑：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;读写边界&lt;/strong&gt;：订单的 &lt;code&gt;items&lt;/code&gt; 嵌入（下单时一次写入、之后整体读取）；但「收货地址」独立成集合——地址在下单、物流、售后多个流程被独立读写，嵌在用户文档里每次都要加载整个用户。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;增长边界&lt;/strong&gt;：单文档 16MB 硬上限。商品的「问大家」嵌进商品文档，爆款商品一年撑爆，连夜迁移。教训：&lt;strong&gt;文档不是仓库，是货架。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;并发边界&lt;/strong&gt;：文档级锁决定了——&lt;strong&gt;凡是会被高并发写的数据，从父文档里拆出去&lt;/strong&gt;。秒杀库存必须一个 SKU 一个文档，绝不能嵌在商品里。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;32-电商核心建模订单快照与库存拆分&#34;&gt;3.2 电商核心建模：订单快照与库存拆分&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;erDiagram
    USER ||--o{ ORDER : &amp;#34;引用&amp;#34;
    PRODUCT ||--o{ SKU_STOCK : &amp;#34;拆出 独立高并发写&amp;#34;
    ORDER ||--|{ ORDER_ITEM : &amp;#34;内嵌 商品快照&amp;#34;
    PRODUCT ||--o{ COMMENT : &amp;#34;引用 无界增长&amp;#34;
    PRODUCT {
        ObjectId _id
        string name
        string categoryId
        array attrs &amp;#34;类目属性 灵活嵌入&amp;#34;
        string detailUrl &amp;#34;富文本放对象存储&amp;#34;
    }
    SKU_STOCK {
        ObjectId _id
        ObjectId productId
        string spec
        int stock &amp;#34;独立文档 原子扣减&amp;#34;
        int version &amp;#34;乐观锁&amp;#34;
    }
    ORDER {
        ObjectId _id
        ObjectId userId
        array items &amp;#34;名称/单价/图片快照&amp;#34;
        long totalAmount &amp;#34;单位: 分&amp;#34;
        string status
        date createdAt
    }&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;两个关键设计，每个都值一次线上事故的钱：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;订单必须存商品快照&lt;/strong&gt;。下单那一刻把商品名、成交单价、主图冗余进 &lt;code&gt;items&lt;/code&gt;。商品会改名改价下架，三个月后客服处理售后，看的必须是「成交时」的样子。快照冗余不是脏数据，是业务真相。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;金额用「分」，long 存&lt;/strong&gt;。浮点的 &lt;code&gt;199.00&lt;/code&gt; 元在 IEEE 754 世界里是 &lt;code&gt;198.999...&lt;/code&gt;，所有和钱有关的字段一律整数存分。这不是 Mongo 的问题，但 Mongo 的自由度会让人更容易犯懒。&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;思考与感悟之二&lt;/strong&gt;：SQL 建模回答「数据之间是什么关系」，Mongo 建模回答「数据以什么节奏被读写」。前者是静态的学问，后者是动态的学问——必须预测流量形状（谁是热点、谁会暴涨、谁一起出现）才能建好模。这就是为什么 Mongo 建模更依赖业务理解，也更难被自动化。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;第四篇-索引与-explain调优基本功没有捷径&#34;&gt;第四篇 索引与 explain：调优基本功，没有捷径&lt;/h2&gt;
&lt;h3 id=&#34;41-explain-是唯一标准答案&#34;&gt;4.1 explain 是唯一标准答案&lt;/h3&gt;
&lt;p&gt;任何「这条查询为什么慢」的讨论，不贴 explain 输出就是耍流氓。Java 里看执行计划：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 在 Java 驱动里用 explain 需要走 Document 命令（或先在 mongosh 里验证）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// mongosh 中的写法：&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;db.&lt;span style=&#34;color:#a6e22e&#34;&gt;products&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;find&lt;/span&gt;({ categoryId: cid, status: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;on&amp;#34;&lt;/span&gt; })
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  .&lt;span style=&#34;color:#a6e22e&#34;&gt;sort&lt;/span&gt;({ price: &lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt;1 })
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  .&lt;span style=&#34;color:#a6e22e&#34;&gt;explain&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;executionStats&amp;#34;&lt;/span&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;输出只盯四处：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    A[&amp;#34;winningPlan.stage&amp;#34;] --&amp;gt; B{&amp;#34;COLLSCAN?&amp;#34;}
    B --&amp;gt;|&amp;#34;是&amp;#34;| C[&amp;#34;全表扫描&amp;lt;br/&amp;gt;立刻建索引&amp;#34;]
    B --&amp;gt;|&amp;#34;IXSCAN&amp;#34;| D[&amp;#34;看三个数字&amp;#34;]
    D --&amp;gt; E[&amp;#34;nReturned&amp;lt;br/&amp;gt;返回多少条&amp;#34;]
    D --&amp;gt; F[&amp;#34;totalDocsExamined&amp;lt;br/&amp;gt;读了多少文档&amp;#34;]
    D --&amp;gt; G[&amp;#34;totalKeysExamined&amp;lt;br/&amp;gt;扫了多少索引键&amp;#34;]
    E --&amp;gt; H{&amp;#34;理想: 三者接近&amp;lt;br/&amp;gt;危险: Examined 远大于 Returned&amp;#34;}&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h3 id=&#34;42-esr-法则与-java-建索引&#34;&gt;4.2 ESR 法则与 Java 建索引&lt;/h3&gt;
&lt;p&gt;复合索引字段顺序：&lt;strong&gt;Equality（等值）→ Sort（排序）→ Range（范围）&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;商品列表页查询：&lt;code&gt;{ categoryId: 等值, status: 等值 }&lt;/code&gt; + 按价格倒序，用 Java 驱动建索引：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 依赖: org.mongodb:mongodb-driver-sync:4.3.x&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; com.mongodb.client.MongoClient;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; com.mongodb.client.MongoClients;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; com.mongodb.client.MongoCollection;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; com.mongodb.client.model.Indexes;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; org.bson.Document;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;IndexDemo&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;void&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;main&lt;/span&gt;(String&lt;span style=&#34;color:#f92672&#34;&gt;[]&lt;/span&gt; args) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;try&lt;/span&gt; (MongoClient client &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; MongoClients.&lt;span style=&#34;color:#a6e22e&#34;&gt;create&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;mongodb://localhost:27017/?replicaSet=rs0&amp;#34;&lt;/span&gt;)) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            MongoCollection&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt;Document&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; products &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; client.&lt;span style=&#34;color:#a6e22e&#34;&gt;getDatabase&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;shop&amp;#34;&lt;/span&gt;).&lt;span style=&#34;color:#a6e22e&#34;&gt;getCollection&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;products&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#75715e&#34;&gt;// ESR：categoryId/status 等值(E) 在前，price 排序(S) 在后&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            products.&lt;span style=&#34;color:#a6e22e&#34;&gt;createIndex&lt;/span&gt;(Indexes.&lt;span style=&#34;color:#a6e22e&#34;&gt;compoundIndex&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                    Indexes.&lt;span style=&#34;color:#a6e22e&#34;&gt;ascending&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;categoryId&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                    Indexes.&lt;span style=&#34;color:#a6e22e&#34;&gt;ascending&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;status&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                    Indexes.&lt;span style=&#34;color:#a6e22e&#34;&gt;descending&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;price&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            ));
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#75715e&#34;&gt;// 部分索引：只给在售商品建，索引体积砍半&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            products.&lt;span style=&#34;color:#a6e22e&#34;&gt;createIndex&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                    Indexes.&lt;span style=&#34;color:#a6e22e&#34;&gt;ascending&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;price&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                    &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; com.&lt;span style=&#34;color:#a6e22e&#34;&gt;mongodb&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;client&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;model&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;IndexOptions&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                            .&lt;span style=&#34;color:#a6e22e&#34;&gt;partialFilterExpression&lt;/span&gt;(com.&lt;span style=&#34;color:#a6e22e&#34;&gt;mongodb&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;client&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;model&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Filters&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;eq&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;status&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;on&amp;#34;&lt;/span&gt;))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            );
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#75715e&#34;&gt;// TTL 索引：埋点日志 7 天自动删除，省掉清理脚本&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            client.&lt;span style=&#34;color:#a6e22e&#34;&gt;getDatabase&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;shop&amp;#34;&lt;/span&gt;).&lt;span style=&#34;color:#a6e22e&#34;&gt;getCollection&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;access_logs&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                    .&lt;span style=&#34;color:#a6e22e&#34;&gt;createIndex&lt;/span&gt;(Indexes.&lt;span style=&#34;color:#a6e22e&#34;&gt;ascending&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;createdAt&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                            &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; com.&lt;span style=&#34;color:#a6e22e&#34;&gt;mongodb&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;client&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;model&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;IndexOptions&lt;/span&gt;().&lt;span style=&#34;color:#a6e22e&#34;&gt;expireAfter&lt;/span&gt;(7L, java.&lt;span style=&#34;color:#a6e22e&#34;&gt;util&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;concurrent&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;TimeUnit&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;DAYS&lt;/span&gt;));
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;注意：加了「价格区间筛选」后 &lt;code&gt;price&lt;/code&gt; 就从 Sort 变成 Range，一个索引满足不了两个需求——&lt;strong&gt;索引设计永远是针对具体查询集合的权衡，没有万能索引&lt;/strong&gt;。每个索引都拖慢写入，写多读少的日志库尤其敏感。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;踩坑故事 #2（32MB 排序内存墙）&lt;/strong&gt;：运营后台「全量商品按上架时间倒序」接口，商品过 10 万后直接报 &lt;code&gt;Sort exceeded memory limit of 33554432 bytes&lt;/code&gt;。Mongo 内存排序上限 32MB，超了就炸。解法不是调参数，是建索引让排序走索引（索引天然有序）。&lt;strong&gt;上线红线第二条：凡带 sort 的查询，必须 explain 验证走索引排序。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;踩坑故事 #3（正则索引进退两难）&lt;/strong&gt;：模糊搜索 &lt;code&gt;find({ name: /卫衣/ })&lt;/code&gt;，非前缀正则无法走索引，数据量上来后 P99 三秒起步。前缀匹配 &lt;code&gt;/^卫衣/&lt;/code&gt; 可以走索引，但中文搜索很少是前缀形态。2021 年的正经答案：&lt;strong&gt;Mongo 管数据，ES 管搜索&lt;/strong&gt;，用 oplog/canal 同步，别指望一个库包打天下。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;第五篇-聚合管道把计算下推给服务端&#34;&gt;第五篇 聚合管道：把计算下推给服务端&lt;/h2&gt;
&lt;p&gt;核心思维：「能在库里算完的，绝不拉回应用层」。拉 10 万条数据到 JVM 里 for 循环求和，是最常见的性能自杀。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    A[&amp;#34;$match&amp;lt;br/&amp;gt;筛: 本月已支付&amp;#34;] --&amp;gt; B[&amp;#34;$unwind&amp;lt;br/&amp;gt;拆: items 展开&amp;#34;]
    B --&amp;gt; C[&amp;#34;$group&amp;lt;br/&amp;gt;按商品汇总&amp;#34;]
    C --&amp;gt; D[&amp;#34;$sort&amp;lt;br/&amp;gt;销量倒序&amp;#34;]
    D --&amp;gt; E[&amp;#34;$limit&amp;lt;br/&amp;gt;Top 10&amp;#34;]
    E --&amp;gt; F[&amp;#34;热销榜&amp;lt;br/&amp;gt;一次出结果&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;实战 Demo：本月热销商品 Top 10（Java 驱动写法）：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// demo01-top-sales：本月热销榜&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; com.mongodb.client.MongoClient;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; com.mongodb.client.MongoClients;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; com.mongodb.client.MongoCollection;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; com.mongodb.client.model.Accumulators;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; com.mongodb.client.model.Aggregates;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; com.mongodb.client.model.Filters;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; com.mongodb.client.model.Sorts;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; org.bson.Document;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; org.bson.conversions.Bson;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; java.util.ArrayList;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; java.util.Arrays;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; java.util.Date;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; java.util.List;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;TopSalesDemo&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;void&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;main&lt;/span&gt;(String&lt;span style=&#34;color:#f92672&#34;&gt;[]&lt;/span&gt; args) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;try&lt;/span&gt; (MongoClient client &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; MongoClients.&lt;span style=&#34;color:#a6e22e&#34;&gt;create&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;mongodb://localhost:27017/?replicaSet=rs0&amp;#34;&lt;/span&gt;)) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            MongoCollection&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt;Document&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; orders &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; client.&lt;span style=&#34;color:#a6e22e&#34;&gt;getDatabase&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;shop&amp;#34;&lt;/span&gt;).&lt;span style=&#34;color:#a6e22e&#34;&gt;getCollection&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;orders&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            Date monthStart &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; Date(); &lt;span style=&#34;color:#75715e&#34;&gt;// 实际取本月 1 号 0 点，此处从简&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            Date monthEnd &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; Date();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            List&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt;Bson&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; pipeline &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; Arrays.&lt;span style=&#34;color:#a6e22e&#34;&gt;asList&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                    &lt;span style=&#34;color:#75715e&#34;&gt;// 第一道永远是 $match：能用索引，砍掉 99% 的数据&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                    Aggregates.&lt;span style=&#34;color:#a6e22e&#34;&gt;match&lt;/span&gt;(Filters.&lt;span style=&#34;color:#a6e22e&#34;&gt;and&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                            Filters.&lt;span style=&#34;color:#a6e22e&#34;&gt;eq&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;status&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;paid&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                            Filters.&lt;span style=&#34;color:#a6e22e&#34;&gt;gte&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;createdAt&amp;#34;&lt;/span&gt;, monthStart),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                            Filters.&lt;span style=&#34;color:#a6e22e&#34;&gt;lt&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;createdAt&amp;#34;&lt;/span&gt;, monthEnd))),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                    Aggregates.&lt;span style=&#34;color:#a6e22e&#34;&gt;unwind&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;$items&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                    Aggregates.&lt;span style=&#34;color:#a6e22e&#34;&gt;group&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;$items.productId&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                            Accumulators.&lt;span style=&#34;color:#a6e22e&#34;&gt;first&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;productName&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;$items.productName&amp;#34;&lt;/span&gt;), &lt;span style=&#34;color:#75715e&#34;&gt;// 快照红利：不用 $lookup&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                            Accumulators.&lt;span style=&#34;color:#a6e22e&#34;&gt;sum&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;totalQty&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;$items.qty&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                            Accumulators.&lt;span style=&#34;color:#a6e22e&#34;&gt;sum&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;totalAmount&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                                    &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; Document(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;$multiply&amp;#34;&lt;/span&gt;, Arrays.&lt;span style=&#34;color:#a6e22e&#34;&gt;asList&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;$items.qty&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;$items.price&amp;#34;&lt;/span&gt;)))),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                    Aggregates.&lt;span style=&#34;color:#a6e22e&#34;&gt;sort&lt;/span&gt;(Sorts.&lt;span style=&#34;color:#a6e22e&#34;&gt;descending&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;totalQty&amp;#34;&lt;/span&gt;)),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                    Aggregates.&lt;span style=&#34;color:#a6e22e&#34;&gt;limit&lt;/span&gt;(10)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            );
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            List&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt;Document&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; hotList &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; ArrayList&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&amp;gt;&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            orders.&lt;span style=&#34;color:#a6e22e&#34;&gt;aggregate&lt;/span&gt;(pipeline)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                    .&lt;span style=&#34;color:#a6e22e&#34;&gt;allowDiskUse&lt;/span&gt;(&lt;span style=&#34;color:#66d9ef&#34;&gt;true&lt;/span&gt;)   &lt;span style=&#34;color:#75715e&#34;&gt;// 中间结果超 100MB 允许落盘，报表查询记得开&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                    .&lt;span style=&#34;color:#a6e22e&#34;&gt;into&lt;/span&gt;(hotList);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            hotList.&lt;span style=&#34;color:#a6e22e&#34;&gt;forEach&lt;/span&gt;(doc &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; System.&lt;span style=&#34;color:#a6e22e&#34;&gt;out&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;println&lt;/span&gt;(doc.&lt;span style=&#34;color:#a6e22e&#34;&gt;toJson&lt;/span&gt;()));
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;三个要点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;$match&lt;/code&gt; 必须放第一&lt;/strong&gt;，放 &lt;code&gt;$unwind&lt;/code&gt; 后面等于先把 500 万订单拆成 3000 万条再筛——有人这么写过，直接把一个节点的内存打爆。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;allowDiskUse(true)&lt;/code&gt;&lt;/strong&gt;：聚合中间结果超 100MB 默认报错，报表类查询记得开。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;能不用 &lt;code&gt;$lookup&lt;/code&gt; 就不用&lt;/strong&gt;。它是应急手段，性能和多表 join 一样是噩梦。需要大量 &lt;code&gt;$lookup&lt;/code&gt; 才能完成的查询，说明建模（第三篇）出了问题——回头改模型，别在查询层硬扛。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 id=&#34;第六篇-副本集高可用与读写语义的分水岭&#34;&gt;第六篇 副本集：高可用与读写语义的分水岭&lt;/h2&gt;
&lt;p&gt;单节点 MongoDB 是玩具，副本集才是起点。这篇是「会用 Mongo」和「敢把核心业务放 Mongo」之间的分水岭。&lt;/p&gt;
&lt;h3 id=&#34;61-架构与选主&#34;&gt;6.1 架构与选主&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    subgraph RS[&amp;#34;副本集 rs0（3 节点是最小生产配置）&amp;#34;]
        P[&amp;#34;Primary&amp;lt;br/&amp;gt;唯一可写&amp;#34;]
        S1[&amp;#34;Secondary&amp;lt;br/&amp;gt;异步复制 oplog&amp;#34;]
        S2[&amp;#34;Secondary&amp;lt;br/&amp;gt;异步复制 oplog&amp;#34;]
        P -- &amp;#34;oplog 同步&amp;#34; --&amp;gt; S1
        P -- &amp;#34;oplog 同步&amp;#34; --&amp;gt; S2
    end
    P -.-&amp;gt;|&amp;#34;心跳超时 10s&amp;lt;br/&amp;gt;自动选主&amp;#34;| S1
    APP[&amp;#34;应用（驱动）&amp;#34;] --&amp;gt;|&amp;#34;自动感知拓扑变化&amp;lt;br/&amp;gt;故障转移对代码透明&amp;#34;| RS&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;必须理解的机制：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;oplog&lt;/strong&gt;：主节点的操作日志，从节点靠它追赶。oplog 是有界集合，从节点宕机太久会追不上，只能全量同步（initial sync）——大库的全量同步以小时计，这就是「从节点宕机要当事故处理」的原因。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;自动选主&lt;/strong&gt;：主挂了，10 秒左右选出新主。&lt;strong&gt;驱动自动重连新主，应用无感&lt;/strong&gt;——前提是连接串写对了（多节点 + &lt;code&gt;replicaSet=rs0&lt;/code&gt;），而不是只写了一个 IP。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;连接池&lt;/strong&gt;：&lt;code&gt;MongoClient&lt;/code&gt; 是线程安全的重对象，&lt;strong&gt;全应用一个单例&lt;/strong&gt;，连接池大小用 &lt;code&gt;maxPoolSize&lt;/code&gt; 调。每请求 new 一个 &lt;code&gt;MongoClient&lt;/code&gt; 等于每请求建一批 TCP 连接，压测时会把数据库连接数打爆——这和「每请求连一次 MySQL」是同一种自杀。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;62-读写语义writeconcern-与-readconcern&#34;&gt;6.2 读写语义：writeConcern 与 readConcern&lt;/h3&gt;
&lt;p&gt;这部分决定你能不能睡着觉——数据到底「写没写进去」？&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// demo02-write-concern：不同档位，不同的安全与延迟权衡&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; com.mongodb.WriteConcern;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; com.mongodb.client.MongoCollection;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; org.bson.Document;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;MongoCollection&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt;Document&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; orders &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; client.&lt;span style=&#34;color:#a6e22e&#34;&gt;getDatabase&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;shop&amp;#34;&lt;/span&gt;).&lt;span style=&#34;color:#a6e22e&#34;&gt;getCollection&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;orders&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 快但可能丢：主节点内存确认就返回（埋点日志可用）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;orders.&lt;span style=&#34;color:#a6e22e&#34;&gt;withWriteConcern&lt;/span&gt;(WriteConcern.&lt;span style=&#34;color:#a6e22e&#34;&gt;W1&lt;/span&gt;).&lt;span style=&#34;color:#a6e22e&#34;&gt;insertOne&lt;/span&gt;(logDoc);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 安全：多数派落盘 + journal 落盘才返回（订单必须这档）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;orders.&lt;span style=&#34;color:#a6e22e&#34;&gt;withWriteConcern&lt;/span&gt;(WriteConcern.&lt;span style=&#34;color:#a6e22e&#34;&gt;MAJORITY&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;withJournal&lt;/span&gt;(&lt;span style=&#34;color:#66d9ef&#34;&gt;true&lt;/span&gt;)).&lt;span style=&#34;color:#a6e22e&#34;&gt;insertOne&lt;/span&gt;(orderDoc);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;电商业务对号入座：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;业务&lt;/th&gt;
					&lt;th&gt;writeConcern&lt;/th&gt;
					&lt;th&gt;理由&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;订单创建&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;majority&lt;/code&gt; + journal&lt;/td&gt;
					&lt;td&gt;主从切换时绝不能丢单&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;库存扣减&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;majority&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;丢扣减记录不可忍&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;行为埋点&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;w: 1&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;丢几条日志无所谓，别拖累写入&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;购物车&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;majority&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;用户加购后刷新没了，投诉就来了&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;code&gt;readConcern&lt;/code&gt; 同理：读「已提交到多数派」的数据用 &lt;code&gt;ReadConcern.MAJORITY&lt;/code&gt;，避免读到主从切换后被回滚的数据。&lt;strong&gt;钱相关的读，一律 majority；展示类的读，local 即可。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;踩坑故事 #4（丢单惊魂夜）&lt;/strong&gt;：上线初期订单写入用默认 &lt;code&gt;w: 1&lt;/code&gt;。某天主节点宕机选主，新主上没有最后 8 秒的 23 笔订单——用户付了钱（支付回调已记录），订单却不存在，客服电话被打爆。复盘结论：&lt;strong&gt;支付回调链路和订单链路的 writeConcern 必须对齐 majority&lt;/strong&gt;，宁慢勿丢。这 8 秒人工补单补了一整夜。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;思考与感悟之三&lt;/strong&gt;：分布式系统里「默认配置」几乎都是「性能优先、安全靠边」的，MongoDB 的 &lt;code&gt;w:1&lt;/code&gt;、MySQL 的异步复制、Kafka 的 &lt;code&gt;acks=1&lt;/code&gt; 皆如此。成熟的标志之一：&lt;strong&gt;每接一个中间件，先把默认一致性档位改成业务需要的档位，再开始写业务代码。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;第七篇-事务与分片规模化之后的一致性难题&#34;&gt;第七篇 事务与分片：规模化之后的一致性难题&lt;/h2&gt;
&lt;h3 id=&#34;71-能不用事务就不用原子操作的威力&#34;&gt;7.1 能不用事务就不用：原子操作的威力&lt;/h3&gt;
&lt;p&gt;MongoDB 单文档操作天然原子。电商防超卖，一条条件更新就够，根本不需要事务：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// demo03-deduct-stock：条件原子扣减，1000 并发抢 1 件也只有 1 个成功&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; com.mongodb.client.MongoCollection;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; com.mongodb.client.model.Filters;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; com.mongodb.client.model.Updates;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; com.mongodb.client.result.UpdateResult;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; org.bson.Document;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; org.bson.types.ObjectId;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;DeductStockDemo&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;void&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;deduct&lt;/span&gt;(MongoCollection&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt;Document&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; skuStocks, ObjectId skuId, &lt;span style=&#34;color:#66d9ef&#34;&gt;int&lt;/span&gt; qty) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        UpdateResult r &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; skuStocks.&lt;span style=&#34;color:#a6e22e&#34;&gt;updateOne&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#75715e&#34;&gt;// 库存够不够，放进查询条件 —— 判断和修改合并成一次原子操作&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                Filters.&lt;span style=&#34;color:#a6e22e&#34;&gt;and&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                        Filters.&lt;span style=&#34;color:#a6e22e&#34;&gt;eq&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;_id&amp;#34;&lt;/span&gt;, skuId),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                        Filters.&lt;span style=&#34;color:#a6e22e&#34;&gt;gte&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;stock&amp;#34;&lt;/span&gt;, qty)),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                Updates.&lt;span style=&#34;color:#a6e22e&#34;&gt;combine&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                        Updates.&lt;span style=&#34;color:#a6e22e&#34;&gt;inc&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;stock&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt;qty),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                        Updates.&lt;span style=&#34;color:#a6e22e&#34;&gt;inc&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;sold&amp;#34;&lt;/span&gt;, qty),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                        Updates.&lt;span style=&#34;color:#a6e22e&#34;&gt;currentDate&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;updatedAt&amp;#34;&lt;/span&gt;))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        );
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; (r.&lt;span style=&#34;color:#a6e22e&#34;&gt;getModifiedCount&lt;/span&gt;() &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; 0) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;throw&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; IllegalStateException(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;库存不足&amp;#34;&lt;/span&gt;); &lt;span style=&#34;color:#75715e&#34;&gt;// 没抢到，上层返回售罄&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;精髓：判断（&lt;code&gt;stock &amp;gt;= qty&lt;/code&gt;）和修改（&lt;code&gt;$inc&lt;/code&gt;）合并成&lt;strong&gt;一次原子操作&lt;/strong&gt;，不存在「查出来够、扣的时候不够」的竞态。这比任何分布式锁都干净。&lt;/p&gt;
&lt;h3 id=&#34;72-真正需要事务的场景&#34;&gt;7.2 真正需要事务的场景&lt;/h3&gt;
&lt;p&gt;跨集合的一致性——下单要同时「扣库存 + 建订单 + 核销券」，三个动作三个集合，这时上多文档事务（4.0+，需副本集）：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// demo04-place-order-tx：多文档事务下单&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; com.mongodb.ClientSessionOptions;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; com.mongodb.ReadConcern;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; com.mongodb.TransactionOptions;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; com.mongodb.WriteConcern;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; com.mongodb.client.ClientSession;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; com.mongodb.client.MongoClient;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; org.bson.Document;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;PlaceOrderTxDemo&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;void&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;placeOrder&lt;/span&gt;(MongoClient client, Document orderDoc, ObjectId skuId, &lt;span style=&#34;color:#66d9ef&#34;&gt;int&lt;/span&gt; qty, ObjectId couponId) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        TransactionOptions txnOptions &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; TransactionOptions.&lt;span style=&#34;color:#a6e22e&#34;&gt;builder&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                .&lt;span style=&#34;color:#a6e22e&#34;&gt;readConcern&lt;/span&gt;(ReadConcern.&lt;span style=&#34;color:#a6e22e&#34;&gt;SNAPSHOT&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                .&lt;span style=&#34;color:#a6e22e&#34;&gt;writeConcern&lt;/span&gt;(WriteConcern.&lt;span style=&#34;color:#a6e22e&#34;&gt;MAJORITY&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                .&lt;span style=&#34;color:#a6e22e&#34;&gt;build&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;try&lt;/span&gt; (ClientSession session &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; client.&lt;span style=&#34;color:#a6e22e&#34;&gt;startSession&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                ClientSessionOptions.&lt;span style=&#34;color:#a6e22e&#34;&gt;builder&lt;/span&gt;().&lt;span style=&#34;color:#a6e22e&#34;&gt;defaultTransactionOptions&lt;/span&gt;(txnOptions).&lt;span style=&#34;color:#a6e22e&#34;&gt;build&lt;/span&gt;())) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            session.&lt;span style=&#34;color:#a6e22e&#34;&gt;withTransaction&lt;/span&gt;(() &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#66d9ef&#34;&gt;var&lt;/span&gt; db &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; client.&lt;span style=&#34;color:#a6e22e&#34;&gt;getDatabase&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;shop&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#75715e&#34;&gt;// 1. 条件原子扣库存&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#66d9ef&#34;&gt;var&lt;/span&gt; r &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; db.&lt;span style=&#34;color:#a6e22e&#34;&gt;getCollection&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;sku_stocks&amp;#34;&lt;/span&gt;).&lt;span style=&#34;color:#a6e22e&#34;&gt;updateOne&lt;/span&gt;(session,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                        com.&lt;span style=&#34;color:#a6e22e&#34;&gt;mongodb&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;client&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;model&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Filters&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;and&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                                com.&lt;span style=&#34;color:#a6e22e&#34;&gt;mongodb&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;client&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;model&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Filters&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;eq&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;_id&amp;#34;&lt;/span&gt;, skuId),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                                com.&lt;span style=&#34;color:#a6e22e&#34;&gt;mongodb&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;client&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;model&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Filters&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;gte&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;stock&amp;#34;&lt;/span&gt;, qty)),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                        com.&lt;span style=&#34;color:#a6e22e&#34;&gt;mongodb&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;client&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;model&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Updates&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;inc&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;stock&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt;qty));
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; (r.&lt;span style=&#34;color:#a6e22e&#34;&gt;getModifiedCount&lt;/span&gt;() &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; 0) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                    &lt;span style=&#34;color:#66d9ef&#34;&gt;throw&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; IllegalStateException(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;库存不足，触发回滚&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#75715e&#34;&gt;// 2. 插入订单（含商品快照）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                db.&lt;span style=&#34;color:#a6e22e&#34;&gt;getCollection&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;orders&amp;#34;&lt;/span&gt;).&lt;span style=&#34;color:#a6e22e&#34;&gt;insertOne&lt;/span&gt;(session, orderDoc);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#75715e&#34;&gt;// 3. 核销优惠券&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                db.&lt;span style=&#34;color:#a6e22e&#34;&gt;getCollection&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;coupons&amp;#34;&lt;/span&gt;).&lt;span style=&#34;color:#a6e22e&#34;&gt;updateOne&lt;/span&gt;(session,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                        com.&lt;span style=&#34;color:#a6e22e&#34;&gt;mongodb&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;client&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;model&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Filters&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;and&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                                com.&lt;span style=&#34;color:#a6e22e&#34;&gt;mongodb&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;client&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;model&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Filters&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;eq&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;_id&amp;#34;&lt;/span&gt;, couponId),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                                com.&lt;span style=&#34;color:#a6e22e&#34;&gt;mongodb&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;client&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;model&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Filters&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;eq&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;used&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#66d9ef&#34;&gt;false&lt;/span&gt;)),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                        com.&lt;span style=&#34;color:#a6e22e&#34;&gt;mongodb&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;client&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;model&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Updates&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;set&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;used&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#66d9ef&#34;&gt;true&lt;/span&gt;));
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;null&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            });
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#75715e&#34;&gt;// 任何一步抛异常，前面所有操作自动回滚&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;sequenceDiagram
    participant A as 下单服务
    participant M as MongoDB 副本集
    A-&amp;gt;&amp;gt;M: withTransaction 开启
    A-&amp;gt;&amp;gt;M: 1. 条件原子扣库存
    alt 库存不足
        M--&amp;gt;&amp;gt;A: modifiedCount = 0
        A-&amp;gt;&amp;gt;M: 自动 abortTransaction&amp;lt;br/&amp;gt;三步全部回滚
        A--&amp;gt;&amp;gt;A: 返回售罄
    else 成功
        A-&amp;gt;&amp;gt;M: 2. 插入订单(含快照)
        A-&amp;gt;&amp;gt;M: 3. 核销优惠券
        A-&amp;gt;&amp;gt;M: commit (w:majority)
        M--&amp;gt;&amp;gt;A: 提交成功
    end&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;踩坑故事 #5（事务的 QPS 税）&lt;/strong&gt;：压测发现全事务下单链路 QPS 比「原子扣减 + 消息队列异步落单」低约 40%，且长事务会压住 WiredTiger 的旧版本清理（第二篇的坑）。大促链路的最终方案：&lt;strong&gt;核心扣减用单文档原子操作（快），订单与券的一致性用 MQ + 定时对账任务兜底（最终一致）&lt;/strong&gt;。事务留给后台管理类低频强一致操作。教训：&lt;strong&gt;事务是保险丝，不是日常用电。&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&#34;73-分片单副本集扛不住之后&#34;&gt;7.3 分片：单副本集扛不住之后&lt;/h3&gt;
&lt;p&gt;数据量单副本集放不下（经验值：单集合过 2TB 或写入 QPS 过万再考虑），才上分片：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    APP[&amp;#34;应用&amp;#34;] --&amp;gt; MONGOS[&amp;#34;mongos 路由层&amp;#34;]
    MONGOS --&amp;gt; S1[&amp;#34;Shard 1&amp;lt;br/&amp;gt;副本集&amp;#34;]
    MONGOS --&amp;gt; S2[&amp;#34;Shard 2&amp;lt;br/&amp;gt;副本集&amp;#34;]
    MONGOS --&amp;gt; S3[&amp;#34;Shard 3&amp;lt;br/&amp;gt;副本集&amp;#34;]
    MONGOS -.-&amp;gt; CFG[&amp;#34;Config Server&amp;lt;br/&amp;gt;元数据副本集&amp;#34;]
    S1 -.-&amp;gt;|&amp;#34;均衡器&amp;lt;br/&amp;gt;chunk 迁移&amp;#34;| S2&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;分片键（shard key）是 Mongo 最「一选定终身」的决策，选错无法修改（5.0 之前）。三条铁律：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;高基数&lt;/strong&gt;：用 &lt;code&gt;status&lt;/code&gt;（只有几个枚举值）做分片键，所有数据挤在少数 chunk 里，分了个寂寞。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;避免单调递增&lt;/strong&gt;：用 &lt;code&gt;createdAt&lt;/code&gt; 或自增 ObjectId 做分片键，所有新写入都打到&lt;strong&gt;最后一个分片&lt;/strong&gt;——热点分片被写爆，其他分片围观。日志类业务要么 hash 分片键，要么接受这个形态。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;匹配查询模式&lt;/strong&gt;：订单按 &lt;code&gt;userId&lt;/code&gt; hash 分片，「查我的订单」路由到单分片，飞快；「全平台订单报表」变成广播查询（scatter-gather），慢——所以报表本来就不该打在分片集群上，该走数仓。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;踩坑故事 #6（jumbo chunk 卡死均衡）&lt;/strong&gt;：早期一张集合按 &lt;code&gt;categoryId&lt;/code&gt; 分片，头部类目一个 chunk 涨到 500MB 还无法拆分（jumbo chunk），均衡器彻底停摆，分片间数据倾斜到 3:1。最后导出数据重建集合换 hash 分片键才解决，停机 4 小时。&lt;strong&gt;分片键评审必须过架构组，这是红线第三条。&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;第八篇-运维底线与-ai-时代把系统做稳把代码写少&#34;&gt;第八篇 运维底线与 AI 时代：把系统做稳，把代码写少&lt;/h2&gt;
&lt;h3 id=&#34;81-生产运维的最低纲领&#34;&gt;8.1 生产运维的最低纲领&lt;/h3&gt;
&lt;p&gt;不展开讲 DBA 手册，只列必须亲自盯的五件事：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    M[&amp;#34;监控&amp;lt;br/&amp;gt;slow query / cache 压力 / oplog 窗口 / 复制延迟&amp;#34;] --&amp;gt; A[&amp;#34;告警&amp;lt;br/&amp;gt;复制延迟 &amp;gt; 30s 即报警&amp;lt;br/&amp;gt;别等追不上才发现&amp;#34;]
    B[&amp;#34;备份&amp;lt;br/&amp;gt; mongodump 逻辑备份&amp;lt;br/&amp;gt;+ 快照物理备份&amp;lt;br/&amp;gt;恢复演练每季度一次&amp;#34;] --&amp;gt; A
    C[&amp;#34;慢查询&amp;lt;br/&amp;gt;profiler 只开采样&amp;lt;br/&amp;gt;每周过 top 慢查询&amp;#34;] --&amp;gt; A
    D[&amp;#34;容量&amp;lt;br/&amp;gt;磁盘水位 70% 预警&amp;lt;br/&amp;gt;oplog 大小按写入峰值调&amp;#34;] --&amp;gt; A
    E[&amp;#34;权限&amp;lt;br/&amp;gt;最小化授权 禁止裸奔&amp;lt;br/&amp;gt;生产禁 dropDatabase&amp;#34;] --&amp;gt; A&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;踩坑故事 #7（恢复演练救了我们一命）&lt;/strong&gt;：团队坚持每季度做一次「备份恢复演练」，被吐槽形式主义。直到有人误删集合（&lt;code&gt;drop()&lt;/code&gt; 当 &lt;code&gt;deleteMany()&lt;/code&gt; 用了），靠备份 40 分钟恢复，业务无损。&lt;strong&gt;没演练过的备份等于没有备份&lt;/strong&gt;——这句话值一个 P0 事故。&lt;/p&gt;
&lt;h3 id=&#34;82-ai-时代如何少写代码把系统做稳&#34;&gt;8.2 AI 时代：如何少写代码把系统做稳&lt;/h3&gt;
&lt;p&gt;写这篇博客的 2021 年，GitHub Copilot 刚发布。站在工程视角，做几个判断，留给未来验证：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;AI 会吃掉「模式化代码」，吃不掉「模式化决策」。&lt;/strong&gt; CRUD、Schema 定义、聚合管道骨架、连接池配置——这些高度模式化的代码 AI 生成得又快又准（你甚至可以现在就试试让 Copilot 补全上面的 &lt;code&gt;DeductStockDemo&lt;/code&gt;，完成度惊人）。但「嵌入还是引用」「分片键选什么」「writeConcern 定哪档」——这些&lt;strong&gt;依赖业务流量预测的决策&lt;/strong&gt;，AI 给不出答案。核心竞争力从「会写」转向「会判断」。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;「少写代码」的正确姿势是把决策做在数据层。&lt;/strong&gt; 用快照冗余消灭 &lt;code&gt;$lookup&lt;/code&gt;，用 TTL 索引消灭清理脚本，用原子扣减消灭分布式锁，用条件更新消灭事务——本篇通篇的「少写代码」讲的都是这个：&lt;strong&gt;好的建模让代码自然变少&lt;/strong&gt;。AI 是放大器，放大的既可以是好设计，也可以是坏设计。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;一个人撑起一个 App 的门槛在崩塌。&lt;/strong&gt; MongoDB Atlas（托管副本集/分片）+ Serverless 函数 + AI 生成 CRUD 层，一个小电商的服务端几天就能搭起来。2021 年这还只是苗头，我确信它会成常态。但反过来：&lt;strong&gt;门槛越低，线上事故越便宜&lt;/strong&gt;——不懂 writeConcern 的人也能上线了，丢单的事故只会更多。AI 时代的工程教育，重点应该从「怎么实现」转向「哪里会炸」。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AI 生成代码的第一评审项是资源语义。&lt;/strong&gt; 连接谁关、事务多长、批量多大、一致性哪档——AI 生成的代码功能往往是对的，资源语义常常是错的（比如给你 &lt;code&gt;w: 1&lt;/code&gt; 的默认写入）。Code Review 的重心会整体迁移到这四个问题上。&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;思考与感悟之四（收尾）&lt;/strong&gt;：工程的本质，是「替业务把不确定性挡在系统之外」。MongoDB 给了你自由度，也把范式的保护伞收走了——锁粒度、一致性档位、分片拓扑，每一个都要自己选。AI 会帮你把代码写得越来越快，但「选什么」的责任永远在人身上。这既是这份工作的辛苦之处，也是它的护城河。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;附录demo-与检查清单&#34;&gt;附录：Demo 与检查清单&lt;/h2&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;Demo&lt;/th&gt;
					&lt;th&gt;类/片段&lt;/th&gt;
					&lt;th&gt;覆盖知识点&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;建索引&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;IndexDemo&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;ESR 复合索引、部分索引、TTL 索引&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;热销榜聚合&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;TopSalesDemo&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;$match&lt;/code&gt; 前置、&lt;code&gt;$unwind&lt;/code&gt;/&lt;code&gt;$group&lt;/code&gt;、&lt;code&gt;allowDiskUse&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;写入档位&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;demo02-write-concern&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;w:1&lt;/code&gt; / &lt;code&gt;majority&lt;/code&gt; / journal 的取舍&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;原子扣库存&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;DeductStockDemo&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;条件原子更新防超卖&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;事务下单&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;PlaceOrderTxDemo&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;多文档事务、回滚、事务读写关注级别&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;MongoDB 生产红线（本文沉淀，可直接抄进团队规范）：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;生产环境永远不开不设限的大批量更新，批量操作必拆批 + 让出窗口。&lt;/li&gt;
&lt;li&gt;凡带 sort 的查询，必须 explain 验证走索引排序。&lt;/li&gt;
&lt;li&gt;分片键评审必须过架构组；能用副本集解决的绝不上分片。&lt;/li&gt;
&lt;li&gt;钱相关的读写一律 &lt;code&gt;majority&lt;/code&gt;；默认配置永远不可信。&lt;/li&gt;
&lt;li&gt;备份必须每季度做恢复演练，没演练过的备份等于没有备份。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;文中代码基于 MongoDB 5.0 + 官方 Java 同步驱动 4.3 实测可编译运行。如果你正站在「要不要接 MongoDB」的门口，建议按八篇顺序在本地副本集上把每个坑亲手复现一遍——看别人踩坑是故事，自己踩一遍才是经验。&lt;/p&gt;
</description>
    </item>
    
  </channel>
</rss>
