<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>电商 on 方糖猫Blog</title>
    <link>https://ftmao.com/blog/tags/%E7%94%B5%E5%95%86/</link>
    <description>AI 视频、AI Agent 与工程实践笔记</description>
    <generator>Hugo</generator>
    <language>zh-CN</language>
    <lastBuildDate>Wed, 26 Oct 2022 09:30:00 +0800</lastBuildDate><atom:link href="https://ftmao.com/blog/tags/%E7%94%B5%E5%95%86/index.xml" rel="self" type="application/rss+xml" /><item>
      <title>Spark 从入门到精通：架构原理、RDD/DataFrame、调优踩坑与电商实战全指南</title>
      <link>https://ftmao.com/blog/posts/spark-beginner-to-mastery/</link>
      <pubDate>Wed, 26 Oct 2022 09:30:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/spark-beginner-to-mastery/</guid>
      <description>&lt;h1 id=&#34;spark-从入门到精通架构原理rdddataframe调优踩坑与电商实战全指南&#34;&gt;Spark 从入门到精通：架构原理、RDD/DataFrame、调优踩坑与电商实战全指南&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;目标读者：写过一两年后端（Java/Scala/Python 皆可）、第一次要碰大数据的同学；或者用 PySpark 写过一些脚本、但一遇到性能问题就抓瞎的同学。
写这篇文章时（2022 年 10 月），Spark 3.3 刚发布，DataFrame 早已是绝对主流，Flink 在实时方向上攻城略地。但离线批处理、数仓 ETL、特征工程这些重活，Spark 依然是绕不开的那座山。&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;一 为什么是 Spark&amp;lt;br/&amp;gt;从单机到分布式&amp;#34;] --&amp;gt; B[&amp;#34;二 架构与原理&amp;lt;br/&amp;gt;Driver Executor DAG&amp;#34;]
    B --&amp;gt; C[&amp;#34;三 RDD 与 DataFrame&amp;lt;br/&amp;gt;两代 API 的分水岭&amp;#34;]
    C --&amp;gt; D[&amp;#34;四 电商实战&amp;lt;br/&amp;gt;宽表 漏斗 特征&amp;#34;]
    D --&amp;gt; E[&amp;#34;五 调优与踩坑&amp;lt;br/&amp;gt;倾斜 OOM 小文件&amp;#34;]
    E --&amp;gt; F[&amp;#34;六 AI 时代&amp;lt;br/&amp;gt;少写代码 多写提示词&amp;#34;]
    style F fill:#e8f4e8&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;为什么是这个顺序？第一篇解决「&lt;strong&gt;为什么要用它&lt;/strong&gt;」；第二篇解决「&lt;strong&gt;它在集群里怎么跑&lt;/strong&gt;」——不理解这个，写出来的代码就是玄学；第三篇解决「&lt;strong&gt;代码怎么写&lt;/strong&gt;」；第四篇用真实业务把知识串起来；第五篇是血泪史，生产环境和本地 Demo 是两个物种；第六篇聊怎么让 AI 替你干活。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;第一篇-为什么是-spark一台机器装不下的时代&#34;&gt;第一篇 为什么是 Spark：一台机器装不下的时代&lt;/h2&gt;
&lt;h3 id=&#34;11-一个电商场景把问题引出来&#34;&gt;1.1 一个电商场景把问题引出来&lt;/h3&gt;
&lt;p&gt;假设你在一家中型电商公司做后端。双十一刚过，老板要看一份报表：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;昨天所有下单用户，按城市分组，统计成交额、下单次数、客单价，再看这批用户里有多少是 30 天内的新客。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;订单表 8 亿行，用户表 4000 万行，行为日志（埋点）一天 20 亿条。MySQL 里跑这个 SQL？&lt;code&gt;GROUP BY&lt;/code&gt; 一个 8 亿行的表，跑一晚上 MySQL 先把你杀了——不，是 DBA 先把你杀了。&lt;/p&gt;
&lt;p&gt;你想到的办法是「把数据导出来，写个 Python 脚本跑」。8 亿行订单数据，CSV 导出大概 400GB。你的开发机内存 32GB。&lt;strong&gt;装不下，这就是大数据的全部问题。&lt;/strong&gt;&lt;/p&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 TB
    subgraph 思路一[&amp;#34;思路一：垂直扩展（Stack Overflow 式的天真）&amp;#34;]
        A1[&amp;#34;换个 1TB 内存的机器&amp;#34;] --&amp;gt; A2[&amp;#34;贵得离谱&amp;lt;br/&amp;gt;且有物理上限&amp;#34;]
    end
    subgraph 思路二[&amp;#34;思路二：水平扩展（唯一出路）&amp;#34;]
        B1[&amp;#34;把 400GB 切成 1000 份&amp;#34;] --&amp;gt; B2[&amp;#34;1000 台机器&amp;lt;br/&amp;gt;每台处理 400MB&amp;#34;]
        B2 --&amp;gt; B3[&amp;#34;各自算局部结果&amp;lt;br/&amp;gt;最后合并&amp;#34;]
    end&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;思路二就是分布式计算。但自己写 1000 台机器的协调代码？分发任务、处理机器挂掉、合并结果、处理网络超时……你会写出第二个 Hadoop。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Spark 就是替你把这些脏活全包了&lt;/strong&gt;：你只写「对这批数据做什么变换」，它负责切分、调度、容错、合并。&lt;/p&gt;
&lt;h3 id=&#34;12-spark-在生态里的位置&#34;&gt;1.2 Spark 在生态里的位置&lt;/h3&gt;
&lt;p&gt;新人最容易被一堆名词搞晕：Hadoop、HDFS、YARN、Hive、Spark、Flink。一张图说清楚：&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 TB
    subgraph 存储层
        HDFS[&amp;#34;HDFS&amp;lt;br/&amp;gt;分布式文件系统&amp;lt;br/&amp;gt;（把文件切块存到多台机器）&amp;#34;]
        S3[&amp;#34;S3 / OSS&amp;lt;br/&amp;gt;对象存储&amp;#34;]
    end
    subgraph 资源层
        YARN[&amp;#34;YARN / K8s&amp;lt;br/&amp;gt;集群资源调度&amp;lt;br/&amp;gt;（给任务分配 CPU 和内存）&amp;#34;]
    end
    subgraph 计算层
        MR[&amp;#34;MapReduce&amp;lt;br/&amp;gt;老一辈 磁盘IO 慢&amp;#34;]
        SPARK[&amp;#34;Spark&amp;lt;br/&amp;gt;内存计算 快&amp;lt;br/&amp;gt;本篇主角&amp;#34;]
        FLINK[&amp;#34;Flink&amp;lt;br/&amp;gt;流处理专精&amp;#34;]
    end
    subgraph 应用层
        HIVE[&amp;#34;Hive/Spark SQL&amp;lt;br/&amp;gt;SQL 写报表&amp;#34;]
        DS[&amp;#34;数据科学&amp;lt;br/&amp;gt;MLlib 特征工程&amp;#34;]
    end
    HDFS --&amp;gt; YARN
    S3 --&amp;gt; YARN
    YARN --&amp;gt; MR
    YARN --&amp;gt; SPARK
    YARN --&amp;gt; FLINK
    SPARK --&amp;gt; HIVE
    SPARK --&amp;gt; DS
    style SPARK fill:#ffd699&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;几个关键认知：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Spark 不是存储系统&lt;/strong&gt;，它没有自己的存储，数据放在 HDFS、Hive 表、S3 甚至 MySQL 里，它读进来算完再写回去。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Spark 也不是资源调度器&lt;/strong&gt;，它跑在 YARN 或 Kubernetes 上，向它们「申请」机器。&lt;/li&gt;
&lt;li&gt;Spark 相对 MapReduce 的快，核心就一句话：&lt;strong&gt;中间结果放内存，不落地磁盘&lt;/strong&gt;。MapReduce 每步都要写磁盘，迭代式计算（比如机器学习跑 50 轮迭代）就慢到不可用。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;踩坑预告&lt;/strong&gt;：很多人学 Spark 的第一步是去装 Hadoop 全家桶，装了一周还没跑起来 Hello World。其实本地学习完全不需要——Spark 有 &lt;code&gt;local&lt;/code&gt; 模式， pip 装个 &lt;code&gt;pyspark&lt;/code&gt; 就能跑。下一篇的 Demo 全是单机可运行的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;第二篇-架构与原理代码提交之后到底发生了什么&#34;&gt;第二篇 架构与原理：代码提交之后到底发生了什么&lt;/h2&gt;
&lt;p&gt;这是最重要的一篇。Spark 调优 80% 靠对这套架构的直觉，剩下 20% 才是参数。&lt;/p&gt;
&lt;h3 id=&#34;21-一次-spark-任务的全貌&#34;&gt;2.1 一次 Spark 任务的全貌&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 TB
    CODE[&amp;#34;你的代码&amp;lt;br/&amp;gt;spark.read... .groupBy... .write...&amp;#34;] --&amp;gt; DRV
    subgraph Cluster[&amp;#34;集群（YARN / K8s）&amp;#34;]
        subgraph DRV[&amp;#34;Driver（司机/总指挥）&amp;#34;]
            DAG[&amp;#34;DAG Scheduler&amp;lt;br/&amp;gt;把代码翻译成执行计划&amp;lt;br/&amp;gt;切成若干 Stage&amp;#34;]
            TS[&amp;#34;Task Scheduler&amp;lt;br/&amp;gt;把 Stage 拆成 Task&amp;lt;br/&amp;gt;派发给 Executor&amp;#34;]
        end
        subgraph EXE[&amp;#34;Executor × N（干活的工人）&amp;#34;]
            T1[&amp;#34;Task 1&amp;lt;br/&amp;gt;处理 Partition 1&amp;#34;]
            T2[&amp;#34;Task 2&amp;lt;br/&amp;gt;处理 Partition 2&amp;#34;]
            T3[&amp;#34;Task 3&amp;lt;br/&amp;gt;处理 Partition 3&amp;#34;]
            CACHE[&amp;#34;内存/磁盘&amp;lt;br/&amp;gt;缓存中间数据&amp;#34;]
        end
    end
    TS --&amp;gt; T1
    TS --&amp;gt; T2
    TS --&amp;gt; T3
    T1 -.shuffle 阶段互相交换数据.-&amp;gt; T2&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;关键角色就三个：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Driver&lt;/strong&gt;：你的 &lt;code&gt;main&lt;/code&gt; 函数（或 PySpark 脚本）运行的地方。它不干活，它&lt;strong&gt;发号施令&lt;/strong&gt;——分析你的代码、生成执行计划、把任务派发出去、回收结果。Driver 挂了，整个任务就完了。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Executor&lt;/strong&gt;：集群里真正干活的 JVM 进程，每个 Executor 有若干核，每个核同时跑一个 Task。数据是**分区（Partition）**存储的，一个 Task 处理一个 Partition。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Shuffle&lt;/strong&gt;：当操作需要跨机器汇总数据（&lt;code&gt;groupBy&lt;/code&gt;、&lt;code&gt;join&lt;/code&gt;、&lt;code&gt;distinct&lt;/code&gt;），数据就要在网络上重新分发——这叫 Shuffle。&lt;strong&gt;Shuffle 是 Spark 任务慢的第一大原因&lt;/strong&gt;，后文反复出现。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;22-懒执行lazy-evaluation为什么你的代码没报错却在运行时炸了&#34;&gt;2.2 懒执行（Lazy Evaluation）：为什么你的代码&amp;quot;没报错&amp;quot;却在运行时炸了&lt;/h3&gt;
&lt;p&gt;新手最常踩的认知坑：&lt;strong&gt;Spark 代码写下去并不会立刻执行&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;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-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;df &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; spark&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;read&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;csv(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;orders.csv&amp;#34;&lt;/span&gt;)          &lt;span style=&#34;color:#75715e&#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;df2 &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; df&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;filter(df&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;amount &lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;100&lt;/span&gt;)           &lt;span style=&#34;color:#75715e&#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;df3 &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; df2&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;groupBy(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;city&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;sum(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;amount&amp;#34;&lt;/span&gt;)    &lt;span style=&#34;color:#75715e&#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;df3&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;write&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;save(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;output&amp;#34;&lt;/span&gt;)                   &lt;span style=&#34;color:#75715e&#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;strong&gt;Transformation（变换）&lt;/strong&gt;，只是往计划里加节点；&lt;code&gt;write&lt;/code&gt;、&lt;code&gt;count&lt;/code&gt;、&lt;code&gt;collect&lt;/code&gt;、&lt;code&gt;show&lt;/code&gt; 这些叫 &lt;strong&gt;Action（行动）&lt;/strong&gt;，碰到 Action，Driver 才把整条链路翻译成 DAG（有向无环图），切成 Stage，真正派发执行。&lt;/p&gt;
&lt;p&gt;这个设计的好处是 &lt;strong&gt;优化&lt;/strong&gt;：Spark 的 Catalyst 优化器能看到整条链路，比如发现你过滤条件写晚了，它可以自动把 &lt;code&gt;filter&lt;/code&gt; 下推到读数据时就做（Predicate Pushdown），少读一半数据。&lt;/p&gt;
&lt;p&gt;但坑也随之而来：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;踩坑故事 1：错误行号的谎言&lt;/strong&gt;
刚转 Spark 时我排查一个任务失败，日志说第 87 行 &lt;code&gt;df.write.save()&lt;/code&gt; 报错 &amp;ldquo;文件不存在&amp;rdquo;。我盯着 87 行看了半小时——write 语法没问题啊！后来才明白：&lt;strong&gt;真正的错误发生在第 12 行的 &lt;code&gt;spark.read&lt;/code&gt;（路径写错了），但因为是懒执行，直到 87 行触发 Action 才爆出来&lt;/strong&gt;。看 Spark 报错，永远要从日志里的执行计划往上找 Transformation 的源头，而不是看报错行号。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&#34;23-stage-是怎么切出来的宽依赖与窄依赖&#34;&gt;2.3 Stage 是怎么切出来的：宽依赖与窄依赖&lt;/h3&gt;
&lt;p&gt;理解 Shuffle 就理解了 Stage 的切分：&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 Stage1[&amp;#34;Stage 1（窄依赖，流水线执行）&amp;#34;]
        P1[&amp;#34;Partition 1&amp;lt;br/&amp;gt;读文件&amp;#34;] --&amp;gt; F1[&amp;#34;filter&amp;lt;br/&amp;gt;过滤&amp;#34;]
        P2[&amp;#34;Partition 2&amp;lt;br/&amp;gt;读文件&amp;#34;] --&amp;gt; F2[&amp;#34;filter&amp;lt;br/&amp;gt;过滤&amp;#34;]
    end
    subgraph SHUFFLE[&amp;#34;⚡ Shuffle（跨网络搬数据）&amp;#34;]
        direction TB
        S[&amp;#34;按 city 重新分区&amp;lt;br/&amp;gt;同 city 的数据&amp;lt;br/&amp;gt;汇聚到同一 Partition&amp;#34;]
    end
    subgraph Stage2[&amp;#34;Stage 2&amp;#34;]
        G1[&amp;#34;Partition A&amp;lt;br/&amp;gt;北京的订单&amp;lt;br/&amp;gt;sum(amount)&amp;#34;]
        G2[&amp;#34;Partition B&amp;lt;br/&amp;gt;上海的订单&amp;lt;br/&amp;gt;sum(amount)&amp;#34;]
    end
    F1 --&amp;gt; SHUFFLE
    F2 --&amp;gt; SHUFFLE
    SHUFFLE --&amp;gt; G1
    SHUFFLE --&amp;gt; G2
    style SHUFFLE fill:#ffcccc&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;窄依赖&lt;/strong&gt;（filter、map）：子 Partition 只依赖父的一个 Partition，可以像流水线一样在同一台机器一口气算完，不分 Stage。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;宽依赖&lt;/strong&gt;（groupBy、join、distinct）：子 Partition 需要父的&lt;strong&gt;所有&lt;/strong&gt; Partition 的数据，必须 Shuffle，Stage 在这里断开。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;一个 Stage 内的失败重试只重跑这个 Stage&lt;/strong&gt;。所以 Spark 的容错单位是 Stage——这也是为什么 Spark 会把 Shuffle 的中间结果落盘（对，嘴上说自己内存计算，Shuffle 还是要写磁盘的），挂了能从这里恢复，而不是从头再来。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;第三篇-rdd-与-dataframe两代-api-的分水岭&#34;&gt;第三篇 RDD 与 DataFrame：两代 API 的分水岭&lt;/h2&gt;
&lt;h3 id=&#34;31-rdd能看清每个零件但要自己造发动机&#34;&gt;3.1 RDD：能看清每个零件，但要自己造发动机&lt;/h3&gt;
&lt;p&gt;RDD（弹性分布式数据集）是 Spark 1.x 的核心抽象。用它写上面的需求：&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-scala&#34; data-lang=&#34;scala&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// RDD 风格：你告诉 Spark 每一步&amp;#34;怎么做&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;orders
&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;.&lt;/span&gt;filter&lt;span style=&#34;color:#f92672&#34;&gt;(&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;_&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;split&lt;span style=&#34;color:#f92672&#34;&gt;(&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;,&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;)(&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;5&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;).&lt;/span&gt;toDouble &lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;100&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;)&lt;/span&gt;     &lt;span style=&#34;color:#75715e&#34;&gt;// 自己解析 CSV，自己转类型
&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;.&lt;/span&gt;map&lt;span style=&#34;color:#f92672&#34;&gt;(&lt;/span&gt;line &lt;span style=&#34;color:#66d9ef&#34;&gt;=&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;(&lt;/span&gt;line&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;split&lt;span style=&#34;color:#f92672&#34;&gt;(&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;,&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;)(&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;3&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;),&lt;/span&gt;           &lt;span style=&#34;color:#75715e&#34;&gt;// 自己取城市字段做 key
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                line&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;split&lt;span style=&#34;color:#f92672&#34;&gt;(&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;,&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;)(&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;5&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;).&lt;/span&gt;toDouble&lt;span style=&#34;color:#f92672&#34;&gt;))&lt;/span&gt; &lt;span style=&#34;color:#75715e&#34;&gt;// 自己取金额做 value
&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;.&lt;/span&gt;reduceByKey&lt;span style=&#34;color:#f92672&#34;&gt;(&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;_&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;_&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;)&lt;/span&gt;                          &lt;span style=&#34;color:#75715e&#34;&gt;// 按 key 聚合
&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;.&lt;/span&gt;saveAsTextFile&lt;span style=&#34;color:#f92672&#34;&gt;(&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;output&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#f92672&#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;RDD 的问题是：&lt;strong&gt;Spark 看不懂你在干什么&lt;/strong&gt;。&lt;code&gt;filter(_.split(&amp;quot;,&amp;quot;)(5).toDouble &amp;gt; 100)&lt;/code&gt; 对优化器来说就是一个黑盒 lambda，它不知道你在过滤哪列，没法做谓词下推，没法选更好的 join 策略。而且 JVM 里每一行都是一个 Java 对象，序列化开销大、GC 压力巨大。&lt;/p&gt;
&lt;h3 id=&#34;32-dataframe你只说要什么优化器替你想怎么做&#34;&gt;3.2 DataFrame：你只说&amp;quot;要什么&amp;quot;，优化器替你想&amp;quot;怎么做&amp;quot;&lt;/h3&gt;
&lt;p&gt;DataFrame（Spark 1.3 引入，2.0 后与 Dataset 合并）给数据加了&lt;strong&gt;Schema（表结构）&lt;/strong&gt;，从此 Spark 能看懂你的意图：&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-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# DataFrame 风格：声明式&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;(spark&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;read
&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;.&lt;/span&gt;schema(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;order_id STRING, city STRING, amount DOUBLE&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;.&lt;/span&gt;csv(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;orders.csv&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;.&lt;/span&gt;filter(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;amount &amp;gt; 100&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;.&lt;/span&gt;groupBy(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;city&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;.&lt;/span&gt;sum(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;amount&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;.&lt;/span&gt;write&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;parquet(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;output&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;两者差距有多大？同样的逻辑，DataFrame 通常比 RDD &lt;strong&gt;快 2~10 倍&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 TB
    subgraph RDD路径[&amp;#34;RDD：信任程序员&amp;#34;]
        R1[&amp;#34;你的 lambda&amp;#34;] --&amp;gt; R2[&amp;#34;逐行 Java 对象&amp;lt;br/&amp;gt;序列化/GC 开销大&amp;lt;br/&amp;gt;优化器爱莫能助&amp;#34;]
    end
    subgraph DF路径[&amp;#34;DataFrame：信任优化器&amp;#34;]
        D1[&amp;#34;声明式表达式&amp;#34;] --&amp;gt; D2[&amp;#34;Catalyst 优化器&amp;lt;br/&amp;gt;逻辑计划优化&amp;lt;br/&amp;gt;谓词下推/列裁剪&amp;#34;]
        D2 --&amp;gt; D3[&amp;#34;Tungsten 执行引擎&amp;lt;br/&amp;gt;二进制内存布局&amp;lt;br/&amp;gt;代码生成 绕开 GC&amp;#34;]
    end
    style DF路径 fill:#e8f4e8&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;一句话结论（2022 年视角）&lt;/strong&gt;：新业务&lt;strong&gt;永远用 DataFrame/Spark SQL 写&lt;/strong&gt;，RDD 只在两种情况下出现——操作的是非结构化数据（比如解析一堆不规则日志），或者需要逐分区精细控制（&lt;code&gt;mapPartitions&lt;/code&gt; 里建数据库连接池这种）。面试让你手写 RDD wordcount 是考察理解，不是让你生产上这么写。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&#34;33-本地就能跑的完整-demo&#34;&gt;3.3 本地就能跑的完整 Demo&lt;/h3&gt;
&lt;p&gt;别急着上集群。装个 Python 环境就能跑：&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-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;pip install pyspark&lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt;3.3.0&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;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-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# demo01_basic.py&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;from&lt;/span&gt; pyspark.sql &lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; SparkSession
&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;from&lt;/span&gt; pyspark.sql &lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; functions &lt;span style=&#34;color:#66d9ef&#34;&gt;as&lt;/span&gt; F
&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;spark &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; (SparkSession&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;builder
&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;.&lt;/span&gt;appName(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;demo01&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;.&lt;/span&gt;master(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;local[*]&amp;#34;&lt;/span&gt;)      &lt;span style=&#34;color:#75715e&#34;&gt;# 本地模式，用满 CPU 核&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;.&lt;/span&gt;getOrCreate())
&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;# 造数据：100 万行模拟订单&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:#f92672&#34;&gt;=&lt;/span&gt; (spark&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;range(&lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;1_000_000&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;.&lt;/span&gt;select(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;              F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;concat(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;lit(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;ORD&amp;#34;&lt;/span&gt;), F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;id&amp;#34;&lt;/span&gt;))&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;alias(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;order_id&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;              (F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;id&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;%&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;50000&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;alias(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;user_id&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;              F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;element_at(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                  F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;array([F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;lit(c) &lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; c &lt;span style=&#34;color:#f92672&#34;&gt;in&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;#34;北京&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;上海&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;广州&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;深圳&amp;#34;&lt;/span&gt;, &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;                  (F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;id&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;%&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;5&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;cast(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;int&amp;#34;&lt;/span&gt;))&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;alias(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;city&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;              (F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;rand() &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;2000&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;alias(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;amount&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;              F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;date_add(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;lit(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;2022-10-25&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;-&lt;/span&gt;(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;id&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;%&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;30&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;cast(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;int&amp;#34;&lt;/span&gt;))&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;alias(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;order_date&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;result &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; (orders
&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;.&lt;/span&gt;filter(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;amount&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;100&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;.&lt;/span&gt;groupBy(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;city&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;.&lt;/span&gt;agg(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;count(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;*&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;alias(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;order_cnt&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;               F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;round(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;sum(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;amount&amp;#34;&lt;/span&gt;), &lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;alias(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;gmv&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;               F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;round(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;avg(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;amount&amp;#34;&lt;/span&gt;), &lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;alias(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;avg_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:#f92672&#34;&gt;.&lt;/span&gt;orderBy(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;desc(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;gmv&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;result&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;show()          &lt;span style=&#34;color:#75715e&#34;&gt;# Action：真正触发执行&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;result&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;write&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;mode(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;overwrite&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;parquet(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;output/city_gmv&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;spark&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;stop()&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;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;python demo01_basic.py&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&gt;+----+---------+-----------+---------+
|city|order_cnt|        gmv|avg_price|
+----+---------+-----------+---------+
|  深圳|   190231|...        |  1050.12|
|  北京|   190087|...        |  1049.87|
...
+----+---------+-----------+---------+&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;这个 Demo 麻雀虽小，五脏俱全：&lt;code&gt;spark.range&lt;/code&gt; 造数、&lt;code&gt;element_at&lt;/code&gt; 数组取值、&lt;code&gt;rand&lt;/code&gt; 随机数、聚合多指标、写 Parquet。本地玩熟了，语法迁移到集群上&lt;strong&gt;一行都不用改&lt;/strong&gt;——这就是 Spark 的优雅之处。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;踩坑故事 2：Windows 上的 winutils.exe&lt;/strong&gt;
我在 Windows 笔记本上第一次跑 PySpark，直接 &lt;code&gt;winutils.exe&lt;/code&gt; 缺失报错 + 一堆权限异常。解决办法是下载对应 Hadoop 版本的 &lt;code&gt;winutils.exe&lt;/code&gt; 放到 &lt;code&gt;%HADOOP_HOME%\bin&lt;/code&gt; 并设置环境变量。后来学乖了：&lt;strong&gt;学习期直接 WSL2 或者 Docker 起个 Linux 环境&lt;/strong&gt;，少生一肚子气。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;第四篇-电商实战把知识串成业务&#34;&gt;第四篇 电商实战：把知识串成业务&lt;/h2&gt;
&lt;p&gt;这一篇用三个电商真实场景，把前面的知识点全部用起来。这三个场景也是面试里最高频的&amp;quot;你做过什么项目&amp;quot;素材。&lt;/p&gt;
&lt;h3 id=&#34;41-场景一订单宽表每日-etl&#34;&gt;4.1 场景一：订单宽表（每日 ETL）&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;业务&lt;/strong&gt;：订单数据分散在三张表——订单主表（order_id、user_id、amount、pay_time）、订单明细表（order_id、sku_id、quantity）、商品维表（sku_id、category、brand）。数据仓库要求每天凌晨产出一张&amp;quot;订单明细宽表&amp;quot;，下游 BI 直接查它。&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-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# demo02_dwd_wide_table.py&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:#f92672&#34;&gt;=&lt;/span&gt; spark&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;table(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;ods.orders&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;filter(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;dt&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;2022-10-25&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;items  &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; spark&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;table(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;ods.order_items&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;filter(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;dt&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;2022-10-25&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;skus   &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; spark&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;table(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;dim.sku&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;filter(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;dt&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;2022-10-25&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;wide &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; (items
&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;.&lt;/span&gt;join(orders, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;order_id&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;.&lt;/span&gt;join(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;broadcast(skus), &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;sku_id&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 style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;select(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;order_id&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;user_id&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;sku_id&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;category&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;brand&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;&amp;#34;quantity&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;amount&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;pay_time&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;.&lt;/span&gt;withColumn(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;dt&amp;#34;&lt;/span&gt;, F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;lit(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;2022-10-25&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;(wide&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;write
&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;.&lt;/span&gt;mode(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;overwrite&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;.&lt;/span&gt;partitionBy(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;dt&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;.&lt;/span&gt;parquet(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;warehouse/dwd/order_detail_wide&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;strong&gt;知识点：Broadcast Join&lt;/strong&gt;。普通 join 两张表都要 Shuffle；但如果一张表很小（商品维表几十万行，几十 MB），Spark 可以把它&lt;strong&gt;整个复制到每个 Executor 的内存里&lt;/strong&gt;，大表原地不动就能完成 join——Shuffle 直接消失，速度提升一个数量级。&lt;/p&gt;
&lt;p&gt;判断标准：小表 &amp;lt; &lt;code&gt;spark.sql.autoBroadcastJoinThreshold&lt;/code&gt;（默认 10MB，生产常调到 100MB~500MB）。写 &lt;code&gt;F.broadcast(skus)&lt;/code&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 普通Join[&amp;#34;普通 Sort-Merge Join（两张表都 Shuffle）&amp;#34;]
        A1[&amp;#34;大表 Partition&amp;#34;] -.网络搬家.-&amp;gt; J1[&amp;#34;Join 节点&amp;#34;]
        A2[&amp;#34;维表 Partition&amp;#34;] -.网络搬家.-&amp;gt; J1
    end
    subgraph 广播Join[&amp;#34;Broadcast Join（只有小表复制）&amp;#34;]
        B1[&amp;#34;大表 Partition 1&amp;lt;br/&amp;gt;原地不动&amp;#34;] --&amp;gt; C1[&amp;#34;本地 Join&amp;lt;br/&amp;gt;查内存里的小表&amp;#34;]
        B2[&amp;#34;大表 Partition 2&amp;lt;br/&amp;gt;原地不动&amp;#34;] --&amp;gt; C2[&amp;#34;本地 Join&amp;lt;br/&amp;gt;查内存里的小表&amp;#34;]
        SM[&amp;#34;小表整表复制&amp;lt;br/&amp;gt;→ 每个 Executor&amp;#34;] --&amp;gt; C1
        SM --&amp;gt; C2
    end
    style 广播Join fill:#e8f4e8&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h3 id=&#34;42-场景二用户行为漏斗转化率分析&#34;&gt;4.2 场景二：用户行为漏斗（转化率分析）&lt;/h3&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;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-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# demo03_funnel.py&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;events &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; spark&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;table(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;ods.user_events&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;filter(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;dt&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;2022-10-25&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:#75715e&#34;&gt;# events 字段：user_id, event_type(view/click/cart/order/pay), ts&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;first_ts &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; (events
&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;.&lt;/span&gt;groupBy(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;user_id&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;.&lt;/span&gt;agg(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;min(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;when(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;event_type&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;view&amp;#34;&lt;/span&gt;,  F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;ts&amp;#34;&lt;/span&gt;)))&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;alias(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;t_view&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                 F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;min(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;when(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;event_type&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;click&amp;#34;&lt;/span&gt;, F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;ts&amp;#34;&lt;/span&gt;)))&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;alias(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;t_click&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                 F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;min(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;when(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;event_type&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;cart&amp;#34;&lt;/span&gt;,  F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;ts&amp;#34;&lt;/span&gt;)))&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;alias(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;t_cart&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                 F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;min(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;when(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;event_type&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;order&amp;#34;&lt;/span&gt;, F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;ts&amp;#34;&lt;/span&gt;)))&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;alias(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;t_order&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                 F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;min(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;when(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;event_type&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;pay&amp;#34;&lt;/span&gt;,   F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;ts&amp;#34;&lt;/span&gt;)))&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;alias(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;t_pay&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;funnel &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; first_ts&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;agg(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;count(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;t_view&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;alias(&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;    F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;count(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;when(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;t_click&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;t_view&amp;#34;&lt;/span&gt;), &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;))&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;alias(&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;    F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;count(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;when(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;t_cart&amp;#34;&lt;/span&gt;)  &lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;t_click&amp;#34;&lt;/span&gt;), &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;))&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;alias(&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;    F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;count(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;when(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;t_order&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;t_cart&amp;#34;&lt;/span&gt;),  &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;))&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;alias(&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;    F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;count(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;when(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;t_pay&amp;#34;&lt;/span&gt;)   &lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;t_order&amp;#34;&lt;/span&gt;), &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;))&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;alias(&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;funnel&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;show()&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;知识点：&lt;code&gt;groupBy&lt;/code&gt; + 条件聚合（&lt;code&gt;F.when&lt;/code&gt; inside &lt;code&gt;agg&lt;/code&gt;）是漏斗分析的标准套路&lt;/strong&gt;——一行 SQL 出五步漏斗，不用写五次 count 再 join。这也是经典面试题。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;踩坑故事 3：漏斗口径的战争&lt;/strong&gt;
我第一版漏斗上线后，运营说数据和他们对不上。查了半天，发现分歧在口径：我写的是&amp;quot;严格顺序漏斗&amp;quot;（必须 view 之后才 click 才算），运营期望的是&amp;quot;存在性漏斗&amp;quot;（只要当天两个行为都发生过就算）。&lt;strong&gt;大数据开发一半的返工不是技术问题，是口径没对齐&lt;/strong&gt;。现在的习惯：开工前把每一步的定义写成一行白话发群里，确认了再写代码。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&#34;43-场景三推荐特征工程用户画像&#34;&gt;4.3 场景三：推荐特征工程（用户画像）&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;业务&lt;/strong&gt;：算法团队要用户特征——每个用户近 30 天的购买次数、客单价、最偏爱类目、最近一次购买距今天数（RFM 模型的雏形）。&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-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# demo04_user_features.py&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;from&lt;/span&gt; pyspark.sql.window &lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; Window
&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;orders_30d &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; spark&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;table(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;dwd.order_detail_wide&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;filter(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;dt&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;between(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;2022-09-26&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;2022-10-25&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;agg_feat &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; (orders_30d
&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;.&lt;/span&gt;groupBy(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;user_id&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;.&lt;/span&gt;agg(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;countDistinct(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;order_id&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;alias(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;order_cnt_30d&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                 F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;round(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;avg(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;amount&amp;#34;&lt;/span&gt;), &lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;alias(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;avg_amount_30d&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                 F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;max(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;pay_time&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;alias(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;last_pay_time&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;w &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; Window&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;partitionBy(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;user_id&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;orderBy(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;desc(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;cat_amount&amp;#34;&lt;/span&gt;))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;top_cat &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; (orders_30d
&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;.&lt;/span&gt;groupBy(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;user_id&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;category&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;.&lt;/span&gt;agg(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;sum(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;amount&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;alias(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;cat_amount&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;.&lt;/span&gt;withColumn(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;rn&amp;#34;&lt;/span&gt;, F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;row_number()&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;over(w))
&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;.&lt;/span&gt;filter(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;rn&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#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:#f92672&#34;&gt;.&lt;/span&gt;select(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;user_id&amp;#34;&lt;/span&gt;, F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;category&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;alias(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;top_category&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;features &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; (agg_feat
&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;.&lt;/span&gt;join(top_cat, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;user_id&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;.&lt;/span&gt;withColumn(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;days_since_last_pay&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                        F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;datediff(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;lit(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;2022-10-26&amp;#34;&lt;/span&gt;), F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;last_pay_time&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;features&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;write&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;mode(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;overwrite&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;parquet(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;features/user_rfm/dt=2022-10-25&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;strong&gt;知识点：窗口函数&lt;/strong&gt;（&lt;code&gt;Window.partitionBy(...).orderBy(...)&lt;/code&gt; + &lt;code&gt;row_number&lt;/code&gt;）是&amp;quot;每组取 TopN&amp;quot;的标准解法，Hive SQL 里也是同一句，大数据面试必考。&lt;/p&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
    subgraph 数据源
        DB[&amp;#34;业务 MySQL&amp;lt;br/&amp;gt;订单/明细/商品&amp;#34;]
        LOG[&amp;#34;埋点日志&amp;lt;br/&amp;gt;曝光/点击/加购&amp;#34;]
    end
    DB --&amp;gt;|Sqoop/DataX 同步| ODS
    LOG --&amp;gt;|Flume/直接落盘| ODS
    subgraph 数仓[&amp;#34;数仓分层（Hive on Spark）&amp;#34;]
        ODS[&amp;#34;ODS 贴源层&amp;lt;br/&amp;gt;原样搬运&amp;#34;] --&amp;gt; DWD[&amp;#34;DWD 明细层&amp;lt;br/&amp;gt;清洗 关联 宽表&amp;lt;br/&amp;gt;★ 场景一&amp;#34;]
        DWD --&amp;gt; DWS[&amp;#34;DWS 汇总层&amp;lt;br/&amp;gt;按主题聚合&amp;lt;br/&amp;gt;★ 场景二漏斗&amp;#34;]
        DWS --&amp;gt; ADS[&amp;#34;ADS 应用层&amp;lt;br/&amp;gt;直接服务业务&amp;lt;br/&amp;gt;★ 场景三特征&amp;#34;]
    end
    ADS --&amp;gt; BI[&amp;#34;BI 报表&amp;#34;]
    ADS --&amp;gt; ALG[&amp;#34;推荐算法&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;hr&gt;
&lt;h2 id=&#34;第五篇-调优与踩坑生产环境才是真的战场&#34;&gt;第五篇 调优与踩坑：生产环境才是真的战场&lt;/h2&gt;
&lt;p&gt;本地 Demo 岁月静好，上了生产环境才知道什么叫残酷。下面三个坑，每一个我都在凌晨两点处理过。&lt;/p&gt;
&lt;h3 id=&#34;51-天坑一数据倾斜一个-task-跑三小时其他三分钟&#34;&gt;5.1 天坑一：数据倾斜（一个 Task 跑三小时，其他三分钟）&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：任务 99% 的 Stage 秒过，最后一个 Stage 的 200 个 Task 里，199 个 5 分钟跑完，&lt;strong&gt;1 个跑了 3 小时还在爬&lt;/strong&gt;。Spark UI 上看，那个 Task 的 Shuffle Read 是别人的几百倍。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：某个 key 的数据量碾压其他 key。电商里最典型的就是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;大促时某个爆款商品（iPhone 发售当天）的订单明细占全站 30%；&lt;/li&gt;
&lt;li&gt;维表 join 时空值/默认值 key——脏数据的 &lt;code&gt;user_id = null&lt;/code&gt; 或 &lt;code&gt;&#39;-1&#39;&lt;/code&gt; 有上千万条，全挤到一个 Partition。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;排查&lt;/strong&gt;：Spark UI → Stages → 看 Task 的 Summary Metrics，Max 时间远大于 Median（中位数），就是倾斜。&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;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-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# 招式1：过滤异常 key（脏数据导致的倾斜，90% 的情况）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;df &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; df&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;filter(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;user_id&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;isNotNull() &lt;span style=&#34;color:#f92672&#34;&gt;&amp;amp;&lt;/span&gt; (F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;user_id&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;-1&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;# 招式2：热点 key 加盐打散（真热点，比如爆款商品）&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;# 给大表的热点 key 加随机后缀 0~9，小表对应 key 复制 10 份&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;salted &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; big&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;withColumn(
&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;#34;join_key&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;when(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;sku_id&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;爆款SKU&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;           F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;concat(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;sku_id&amp;#34;&lt;/span&gt;), F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;lit(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;_&amp;#34;&lt;/span&gt;), (F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;rand()&lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;10&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;cast(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;int&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;.&lt;/span&gt;otherwise(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;sku_id&amp;#34;&lt;/span&gt;)))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;small_exploded &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; small&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;withColumn(
&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;#34;join_key&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;when(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;sku_id&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;爆款SKU&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;           F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;concat(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;sku_id&amp;#34;&lt;/span&gt;), F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;lit(&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;                    F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;explode(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;array([F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;lit(i) &lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; i &lt;span style=&#34;color:#f92672&#34;&gt;in&lt;/span&gt; range(&lt;span style=&#34;color:#ae81ff&#34;&gt;10&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;.&lt;/span&gt;otherwise(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;sku_id&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;# 招式3：开启 AQE 自动处理（Spark 3.x 的救星）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;spark&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;conf&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;set(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;spark.sql.adaptive.enabled&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;true&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;spark&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;conf&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;set(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;spark.sql.adaptive.skewJoin.enabled&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;true&amp;#34;&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;踩坑故事 4：AQE 救了我一个通宵&lt;/strong&gt;
2022 年上半年迁移 Spark 3.x 后开了 AQE（Adaptive Query Execution），它能&lt;strong&gt;在运行时&lt;/strong&gt;发现倾斜的 Partition 并自动拆分。以前要手工调参数 &lt;code&gt;spark.sql.shuffle.partitions&lt;/code&gt; 反复试的任务，AQE 直接自己搞定了。所以第一建议永远是：&lt;strong&gt;先升级到 Spark 3.x 并开启 AQE，再用脑子去调&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&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 TB
    subgraph 倾斜前[&amp;#34;倾斜：一个 Partition 吞掉 40% 数据&amp;#34;]
        K1[&amp;#34;Partition 1: 50MB&amp;#34;] 
        K2[&amp;#34;Partition 2: 8GB 😱&amp;#34;] 
        K3[&amp;#34;Partition 3: 50MB&amp;#34;]
    end
    subgraph 倾斜后[&amp;#34;AQE 拆分后：化整为零&amp;#34;]
        L1[&amp;#34;Partition 1: 50MB&amp;#34;]
        L2a[&amp;#34;Partition 2a: 200MB&amp;#34;]
        L2b[&amp;#34;Partition 2b: 200MB&amp;#34;]
        L2c[&amp;#34;Partition 2c: 200MB&amp;lt;br/&amp;gt;...共拆 40 份&amp;#34;]
        L3[&amp;#34;Partition 3: 50MB&amp;#34;]
    end
    倾斜前 --&amp;gt;|&amp;#34;AQE 运行时检测拆分&amp;#34;| 倾斜后
    style K2 fill:#ffcccc&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h3 id=&#34;52-天坑二oom内存不够但到底谁不够&#34;&gt;5.2 天坑二：OOM（内存不够，但到底谁不够）&lt;/h3&gt;
&lt;p&gt;Spark 的 OOM 分两种，报错长得像，病因完全不同：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;报错关键字&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;code&gt;ExecutorLostFailure&lt;/code&gt; / 容器被 kill / &lt;code&gt;java.lang.OutOfMemoryError&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;Executor 内存不够&lt;/strong&gt;：数据太多、缓存太大&lt;/td&gt;
					&lt;td&gt;加 &lt;code&gt;executor-memory&lt;/code&gt;、增加分区数摊薄数据、减少 &lt;code&gt;cache&lt;/code&gt; 滥用&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Driver 端 OOM&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;Driver 内存不够&lt;/strong&gt;：几乎永远是 &lt;code&gt;collect()&lt;/code&gt; / &lt;code&gt;toPandas()&lt;/code&gt; 把全量数据拉回 Driver&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;不要用 collect 收大数据！&lt;/strong&gt; 用 &lt;code&gt;take(100)&lt;/code&gt; 抽样看，用 &lt;code&gt;write&lt;/code&gt; 落盘&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;踩坑故事 5：一个 collect 拖垮了整个集群调度&lt;/strong&gt;
同事为了&amp;quot;快速验证&amp;quot;，在生产任务里写了 &lt;code&gt;df.collect()&lt;/code&gt; 然后 len() 一下——那张表 2 亿行。Driver 内存从 4G 一路撑爆，任务挂掉不说，还因为是调度系统的重要任务，报警电话打到了总监那里。&lt;strong&gt;从此组里 Code Review 第一条红线：搜索代码里有没有 &lt;code&gt;collect()&lt;/code&gt;、&lt;code&gt;toPandas()&lt;/code&gt;，有就必须说明数据量上限&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&#34;53-天坑三小文件灾难hdfs-的慢性病&#34;&gt;5.3 天坑三：小文件灾难（HDFS 的慢性病）&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：任务很快跑完，但写出的结果目录里是 &lt;strong&gt;20000 个 100KB 的小文件&lt;/strong&gt;。然后：Hive 查这张表慢得离谱，NameNode 内存报警，下游任务读它要启动几万个 Task。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：200 个分区 × 每次写 100 个文件 = 每天积累，几个月后就是几百万小文件。HDFS 的 NameNode 把每个文件的元数据放内存，小文件多是集群的头号慢性病。&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;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-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# 写出前合并分区（注意：coalesce 不 Shuffle，repartition 会 Shuffle）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;df&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;coalesce(&lt;span style=&#34;color:#ae81ff&#34;&gt;20&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;write&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;parquet(&lt;span style=&#34;color:#f92672&#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;df&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;repartition(&lt;span style=&#34;color:#ae81ff&#34;&gt;20&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;dt&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;write&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;parquet(&lt;span style=&#34;color:#f92672&#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 style=&#34;color:#75715e&#34;&gt;# 根治：开启 AQE 的 Shuffle 分区自动合并，从源头减少小文件&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;spark&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;conf&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;set(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;spark.sql.adaptive.coalescePartitions.enabled&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;true&amp;#34;&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;54-一张排障决策图&#34;&gt;5.4 一张排障决策图&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;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    Q1{&amp;#34;任务挂了还是慢？&amp;#34;}
    Q1 --&amp;gt;|挂了| Q2{&amp;#34;Driver 日志里&amp;lt;br/&amp;gt;OOM 关键字？&amp;#34;}
    Q2 --&amp;gt;|Executor 端| F1[&amp;#34;加 executor-memory&amp;lt;br/&amp;gt;或增大分区数&amp;#34;]
    Q2 --&amp;gt;|Driver 端| F2[&amp;#34;查 collect/toPandas&amp;lt;br/&amp;gt;换成 write 或 take&amp;#34;]
    Q2 --&amp;gt;|文件不存在等| F3[&amp;#34;懒执行报错错位&amp;lt;br/&amp;gt;从执行计划往上找源头&amp;#34;]
    Q1 --&amp;gt;|慢| Q3{&amp;#34;Spark UI 看 Stage：&amp;lt;br/&amp;gt;个别 Task 特别慢？&amp;#34;}
    Q3 --&amp;gt;|是 Max远大于Median| F4[&amp;#34;数据倾斜&amp;lt;br/&amp;gt;过滤脏key/加盐/AQE&amp;#34;]
    Q3 --&amp;gt;|否 全都慢| Q4{&amp;#34;Shuffle Read/Write&amp;lt;br/&amp;gt;数据量巨大？&amp;#34;}
    Q4 --&amp;gt;|是| F5[&amp;#34;能广播的小表广播&amp;lt;br/&amp;gt;列裁剪 过滤下推&amp;lt;br/&amp;gt;提前聚合减少数据量&amp;#34;]
    Q4 --&amp;gt;|否| F6[&amp;#34;查上游：小文件太多？&amp;lt;br/&amp;gt;查集群：资源不够排队？&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;hr&gt;
&lt;h2 id=&#34;第六篇-ai-时代少写代码完成大数据开发&#34;&gt;第六篇 AI 时代：少写代码，完成大数据开发&lt;/h2&gt;
&lt;p&gt;时间快进到 AI 编程工具普及之后。回头看，Spark 开发是&lt;strong&gt;最适合被 AI 改造&lt;/strong&gt;的领域之一，因为它的代码模式极其规整：读、变换、聚合、写，套路就那么多。&lt;/p&gt;
&lt;h3 id=&#34;61-ai-能替你做什么不能替你做什么&#34;&gt;6.1 AI 能替你做什么，不能替你做什么&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 LR
    subgraph AI擅长[&amp;#34;AI 做得比人好&amp;#34;]
        A1[&amp;#34;自然语言 → PySpark/SQL 代码&amp;lt;br/&amp;gt;按用户30天行为算RFM特征 一句话出码&amp;#34;]
        A2[&amp;#34;样板代码生成&amp;lt;br/&amp;gt;Schema 定义 造数脚本 单元测试&amp;#34;]
        A3[&amp;#34;报错诊断&amp;lt;br/&amp;gt;贴堆栈 直接定位到懒执行错位/倾斜&amp;#34;]
        A4[&amp;#34;口径文档化&amp;lt;br/&amp;gt;把代码翻译成业务能看懂的白话&amp;#34;]
    end
    subgraph AI不擅长[&amp;#34;人必须自己把关&amp;#34;]
        B1[&amp;#34;业务口径&amp;lt;br/&amp;gt;漏斗是严格顺序还是存在性&amp;lt;br/&amp;gt;AI 不知道你老板要什么&amp;#34;]
        B2[&amp;#34;性能直觉&amp;lt;br/&amp;gt;哪里该广播 哪里会倾斜&amp;lt;br/&amp;gt;要看真实数据分布&amp;#34;]
        B3[&amp;#34;生产决策&amp;lt;br/&amp;gt;资源参数 重跑策略 数据回补&amp;#34;]
    end&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;：写「用户漏斗分析」= 查文档 30 分钟 + 写代码 40 分钟 + 调试 30 分钟。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;现在&lt;/strong&gt;：给 AI 描述清楚——表名、字段、漏斗定义（严格按时间顺序）、输出格式——10 秒出代码，我花 20 分钟 review 口径和边界条件。&lt;strong&gt;总耗时砍到三分之一&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但注意：&lt;strong&gt;省下来的时间全部应该砸在口径确认和数据校验上&lt;/strong&gt;。AI 写的代码语法几乎不会错，但它不知道你们的 &lt;code&gt;user_id = &#39;-1&#39;&lt;/code&gt; 是脏数据、不知道运营要的是存在性漏斗。这正是第五篇那些坑的价值——&lt;strong&gt;AI 时代，踩坑经验不是贬值了，而是成了你最贵的资产&lt;/strong&gt;，因为代码人人都能生成，能判断代码对不对的人变稀缺了。&lt;/p&gt;
&lt;h3 id=&#34;62-具体怎么干三个提效姿势&#34;&gt;6.2 具体怎么干：三个提效姿势&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;姿势一：需求描述模板化。&lt;/strong&gt; 给 AI 下指令时固定四段式：&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&gt;背景：表 ods.user_events，字段 user_id/event_type/ts，按天分区 dt
任务：计算 2022-10-25 的转化漏斗 曝光→点击→加购→下单→支付
口径：严格顺序漏斗，后一步的最早时间必须大于前一步
输出：一行五列，列名用中文&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;第四篇场景三的完整代码，用这样的提示词一次就能生成得八九不离十。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;姿势二：报错即提示词。&lt;/strong&gt; Spark 报错堆栈又长又唬人，直接整段贴给 AI，附上你的 Transformation 代码，它对「懒执行错位」「ClassNotFound（依赖没打包）」「分区越界」这类经典错误的诊断准确率很高。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;姿势三：让 AI 写测试和校验脚本。&lt;/strong&gt; 大数据开发最缺的就是测试——数据对不对以前靠人肉抽查。现在让 AI 顺手生成校验 SQL（行数对比、NULL 比例、金额总和与上游对账），质量反而比手工时代更好了。&lt;/p&gt;
&lt;h3 id=&#34;63-冷静的一句话&#34;&gt;6.3 冷静的一句话&lt;/h3&gt;
&lt;p&gt;AI 不会让你&amp;quot;少学 Spark&amp;quot;，它让&lt;strong&gt;学习曲线的重心变了&lt;/strong&gt;：以前背 API 占 60% 的时间，现在这部分 AI 补上了；剩下 40%——架构直觉、业务口径、排障经验——变成了 100% 的核心竞争力。&lt;strong&gt;入门变容易了，精通的标准反而更高了。&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;思考与感悟&#34;&gt;思考与感悟&lt;/h2&gt;
&lt;p&gt;写完这篇文章，有几句话想单独说：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. 学 Spark 的本质是学&amp;quot;分布式的世界观&amp;quot;。&lt;/strong&gt; 单机时代的直觉——数据随手可得、循环遍历随意、内存无限——全都要扔掉。你要习惯一种新的思维方式：数据是流动的、分区是平行的、Shuffle 是昂贵的、失败是必然的（所以容错设计是一等公民）。这套世界观建立起来之后，学 Flink、学 Kafka、学 ClickHouse 都会很快，因为它们要解决的是同一族问题。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. 调优经验 = 疼痛的记忆。&lt;/strong&gt; 没有人看文档能学会处理数据倾斜，都是被凌晨两点的报警电话教会的。所以别怕踩坑，踩了记下来——我这篇第五篇的每个坑，当年都值一个通宵。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. 离业务近一点，离技术远一点。&lt;/strong&gt; 做了几年大数据，最大的感悟是：把 ETL 跑得快不算本事，&lt;strong&gt;知道这张表给谁用、口径对不对、错了谁会亏钱&lt;/strong&gt;才是本事。电商场景里，GMV 算错 1% 可能就是一个部门的年终奖。技术只是手段，数据是业务的镜像。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. AI 时代最值得押注的能力是&amp;quot;判断力&amp;quot;。&lt;/strong&gt; 生成代码会越来越便宜，判断&amp;quot;这段代码对不对、这个口径该不该这么定、这个架构扛不扛得住双十一&amp;quot;会越来越贵。把 AI 当成一个能力极强但没有常识的实习生：活让它干，锅你来背，所以你必须看得懂它干的活。&lt;/p&gt;
&lt;p&gt;与君共勉。&lt;/p&gt;
</description>
    </item>
    <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>
    <item>
      <title>starmall：一个微信小程序电商系统的全栈实战——预约、支付与订单架构解析</title>
      <link>https://ftmao.com/blog/posts/starmall-wechat-miniprogram-ecommerce-fullstack/</link>
      <pubDate>Tue, 20 Jul 2021 10:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/starmall-wechat-miniprogram-ecommerce-fullstack/</guid>
      <description>&lt;h1 id=&#34;starmall一个微信小程序电商系统的全栈实战预约支付与订单架构解析&#34;&gt;starmall：一个微信小程序电商系统的全栈实战——预约、支付与订单架构解析&lt;/h1&gt;
&lt;p&gt;这是我 2021 年做的一套完整电商系统 &lt;strong&gt;starmall&lt;/strong&gt;（六爻星辰）：微信小程序端 + Java 后端 + PC 管理后台，支持商品购买、老师咨询时段预约、择吉日查询、在线咨询等业务。整理开源出来，写篇文章做一次完整的技术复盘。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;前端（uni-app 小程序）：&lt;a href=&#34;https://gitee.com/soulnew/starmall&#34;&gt;https://gitee.com/soulnew/starmall&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;后端（Java + 管理端）：&lt;a href=&#34;https://gitee.com/soulnew/crm_server&#34;&gt;https://gitee.com/soulnew/crm_server&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;技术栈：Spring Boot 2.2 + MyBatis-Plus + MySQL + Redis / uni-app + uView / Vue 2 + Element UI（管理端）&lt;/li&gt;
&lt;li&gt;说明：后端是在开源商城 &lt;strong&gt;CRMEB Java 版&lt;/strong&gt; 基础上二次开发的，分销/秒杀/拼团/砍价等营销能力来自底座，预约系统、择吉日、名片、咨询聊天等是自研部分&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;一整体架构&#34;&gt;一、整体架构&lt;/h2&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;graph TD
    MP[&amp;#34;微信小程序端&amp;lt;br/&amp;gt;starmall uni-app + uView&amp;#34;] --&amp;gt;|&amp;#34;https://ftmao.com/api/front/**&amp;#34;| API[&amp;#34;SpringBoot 单体后端&amp;lt;br/&amp;gt;62 个 Mapper / 23 个前台 Controller&amp;#34;]
    H5[&amp;#34;H5 / App&amp;lt;br/&amp;gt;uni-app 多端编译&amp;#34;] --&amp;gt; API
    ADMIN[&amp;#34;PC 管理后台&amp;lt;br/&amp;gt;Vue 2 + Element UI + ECharts&amp;#34;] --&amp;gt;|&amp;#34;后台接口&amp;#34;| API
    API --&amp;gt; MYSQL[&amp;#34;MySQL 5.7&amp;lt;br/&amp;gt;62 张表 Druid 连接池&amp;#34;]
    API --&amp;gt; REDIS[&amp;#34;Redis&amp;lt;br/&amp;gt;token / 订单缓存 / 任务队列 / 配置缓存&amp;#34;]
    API --&amp;gt; WX[&amp;#34;微信开放平台&amp;lt;br/&amp;gt;登录 / 支付 V2 / 订阅消息&amp;#34;]
    API --&amp;gt; QN[&amp;#34;七牛云 OSS&amp;lt;br/&amp;gt;图片上传&amp;#34;]
    API --&amp;gt; QY[&amp;#34;企业微信机器人&amp;lt;br/&amp;gt;部署结果通知&amp;#34;]
    MP --&amp;gt;|&amp;#34;wss://... websocket&amp;#34;| CHAT[&amp;#34;咨询聊天服务&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;代码规模：后端约 850 个 Java 文件、12.6 万行；小程序前端业务代码约 1.2 万行；数据库初始化 SQL 全量在仓库里，共 62 张表。后端按业务分包：&lt;code&gt;front&lt;/code&gt;（小程序接口）、&lt;code&gt;order/payment&lt;/code&gt;、&lt;code&gt;marketing&lt;/code&gt;、&lt;code&gt;seckill&lt;/code&gt;、&lt;code&gt;task&lt;/code&gt;（定时任务）、&lt;code&gt;wechat&lt;/code&gt;，以及自研的 &lt;code&gt;appoint&lt;/code&gt;（预约）、&lt;code&gt;lunar&lt;/code&gt;（黄历）等模块。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;二功能介绍&#34;&gt;二、功能介绍&lt;/h2&gt;
&lt;h3 id=&#34;21-商品购买全流程&#34;&gt;2.1 商品购买全流程&lt;/h3&gt;
&lt;p&gt;标准的商城闭环：商品详情（多规格 SKU）→ 立即购买/加购物车 → 确认订单（优惠券、积分抵扣、运费计算）→ 创建订单 → 微信支付 → 轮询支付结果 → 收货评价 → 退款/售后。&lt;strong&gt;支付是真实对接的微信支付 V2&lt;/strong&gt;——后端手工实现 XML 报文 + MD5 签名调统一下单（&lt;code&gt;WeChatPayServiceImpl&lt;/code&gt;，746 行），前端弹起 &lt;code&gt;wx.requestPayment&lt;/code&gt;，不是 mock。&lt;/p&gt;
&lt;p&gt;此外还支持：余额支付、运费模板（满额包邮/指定城市包邮/按件数重量体积三种首续费）、物流查询、自提核销码、再次下单。营销玩法（秒杀、砍价、拼团、优惠券）来自 CRMEB 底座，一应俱全。&lt;/p&gt;
&lt;h3 id=&#34;22-老师咨询时段预约自研核心&#34;&gt;2.2 老师咨询时段预约（自研核心）&lt;/h3&gt;
&lt;p&gt;这是这个项目区别于普通商城的地方——卖的不只是实物商品，还有&amp;quot;老师的时间&amp;quot;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;商品分类为&amp;quot;老师类&amp;quot;时，商品详情页出现一个&lt;strong&gt;时段选择器&lt;/strong&gt;（每天 6 个时段：9/11/13/15/17/19 点）；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;teacher_plan&lt;/code&gt; 表按「日期 × 时段」预生成号源，被约走的时段前端置灰；&lt;/li&gt;
&lt;li&gt;下单时携带 &lt;code&gt;teacherPlanId&lt;/code&gt;，&lt;strong&gt;创建订单的同一个事务里&lt;/strong&gt;把该时段标记为已约——号源占用和订单创建要么一起成功要么一起回滚，不会出现&amp;quot;钱付了时段没了&amp;quot;；&lt;/li&gt;
&lt;li&gt;号源由定时任务 &lt;code&gt;TeacherPlanTask&lt;/code&gt; 每 8 小时自动向前补齐未来 8 天，并清理过期的空闲号源。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;23-其他自研功能&#34;&gt;2.3 其他自研功能&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;择吉日查询&lt;/strong&gt;：4.7 万行黄历数据入库（&lt;code&gt;lunar&lt;/code&gt; 系列表），按&amp;quot;宜&amp;quot;事反查吉日，比如查&amp;quot;最近适合搬家的日子&amp;quot;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;在线咨询&lt;/strong&gt;：基于 WebSocket 长连接的实时聊天，支持文字/语音/图片消息（详见第三章「长连接聊天」）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;老师名片&lt;/strong&gt;、文章资讯、签到积分、充值、小程序订阅消息提醒；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;管理后台&lt;/strong&gt;：商品/订单/营销/分销/财务/短信/按钮级权限/数据统计大屏（ECharts）。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;三后端能力全景&#34;&gt;三、后端能力全景&lt;/h2&gt;
&lt;p&gt;后端虽然是 SpringBoot 单体，但能力面相当完整：前台 23 个 Controller、62 个 MyBatis Mapper，按业务分包组织：&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;graph TD
    subgraph M1[&amp;#34;业务模块&amp;#34;]
        A1[&amp;#34;front 前台接口&amp;lt;br/&amp;gt;23 个 Controller&amp;#34;]
        A2[&amp;#34;order / payment&amp;lt;br/&amp;gt;订单与支付&amp;#34;]
        A3[&amp;#34;marketing / seckill / bargain / combination&amp;lt;br/&amp;gt;优惠券 秒杀 砍价 拼团&amp;#34;]
        A4[&amp;#34;appoint / lunar / businessCard&amp;lt;br/&amp;gt;预约 择吉日 名片（自研）&amp;#34;]
        A5[&amp;#34;finance / statistics / export&amp;lt;br/&amp;gt;充值提现 数据统计 Excel 导出&amp;#34;]
        A6[&amp;#34;wechat / wechatMessage / sms&amp;lt;br/&amp;gt;微信生态与短信&amp;#34;]
        A7[&amp;#34;task&amp;lt;br/&amp;gt;定时任务体系&amp;#34;]
    end
    subgraph M2[&amp;#34;公共层&amp;#34;]
        B1[&amp;#34;拦截器链&amp;lt;br/&amp;gt;前台 Token / 后台 Token+权限 / Swagger&amp;#34;]
        B2[&amp;#34;工具层 RedisUtil / WxPayUtil / CrmebUtil&amp;#34;]
        B3[&amp;#34;ThreadLocal 上下文传递当前用户&amp;#34;]
    end
    A1 --&amp;gt; B1
    A2 --&amp;gt; B2
    B1 --&amp;gt; B3&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;wechatMessage&lt;/code&gt; 模块支持文字/语音/图片/图文四种回复类型，关注、取关、扫码等事件都有对应处理；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;模板消息与订阅消息&lt;/strong&gt;：小程序订阅消息、公众号模板消息的增删查与群发，且发送走异步任务（&lt;code&gt;AsyncWeChatProgramTempMessage&lt;/code&gt; 等），不阻塞主流程；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;渠道二维码与素材管理&lt;/strong&gt;：带参数的推广二维码、微信素材的上传管理；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据导出与统计&lt;/strong&gt;：POI 实现的 Excel 导出、交易/商品/用户多维统计接口（配合管理端 ECharts 大屏）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;短信与物流&lt;/strong&gt;：短信验证码/通知、快递物流轨迹查询；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;接口文档&lt;/strong&gt;：Swagger 在线文档 + 仓库内置 admin/app/公共三份离线接口文档。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;31-长连接聊天websocket架构&#34;&gt;3.1 长连接聊天（WebSocket）架构&lt;/h3&gt;
&lt;p&gt;在线咨询是这个系统的&amp;quot;第二前台&amp;quot;，采用的是 &lt;strong&gt;WebSocket 长连接 + HTTP 接口&lt;/strong&gt;的组合方案。部署上两个服务跑在同一台服务器，由 nginx 按路径前缀分流：&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;graph TD
    MP[&amp;#34;小程序聊天页&amp;lt;br/&amp;gt;pages/chat&amp;#34;] --&amp;gt;|&amp;#34;wss://.../websocket/userId/sessionId&amp;#34;| NGX[&amp;#34;nginx 按路径分流&amp;#34;]
    MP --&amp;gt;|&amp;#34;HTTP /chat/session/**  /chat/msg/**&amp;#34;| NGX
    MP --&amp;gt;|&amp;#34;HTTP /api/front/**&amp;#34;| NGX
    NGX --&amp;gt; CHAT[&amp;#34;聊天服务&amp;lt;br/&amp;gt;WebSocket 长连接 + 会话/消息接口&amp;#34;]
    NGX --&amp;gt; MALL[&amp;#34;商城后端 crm_server&amp;lt;br/&amp;gt;SpringBoot 18080&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;：WebSocket 握手 URL 直接携带 &lt;code&gt;userId/sessionId&lt;/code&gt;，连接建立的同时就完成了身份识别与会话路由，不需要额外的登录帧；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实时走长连接，持久化走 HTTP&lt;/strong&gt;：收发消息走 WebSocket（消息体为 JSON）；会话绑定（&lt;code&gt;session/bindSession&lt;/code&gt;）、会话列表、删除会话、历史消息拉取（&lt;code&gt;msg/msgList&lt;/code&gt;）都走 &lt;code&gt;/chat/&lt;/code&gt; 前缀的 HTTP 接口——长连接管&amp;quot;实时推送&amp;quot;，HTTP 管&amp;quot;可查询、可回溯&amp;quot;，职责分得很干净；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;统一的消息类型协议&lt;/strong&gt;：&lt;code&gt;contentType&lt;/code&gt; 1=文字、2=语音（带 &lt;code&gt;contentDuration&lt;/code&gt; 时长）、3=图片（先经 &lt;code&gt;user/upload/image&lt;/code&gt; 上传拿到 URL，再把 URL 作为消息内容发出）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;语音消息的移动端交互&lt;/strong&gt;：按住说话（&lt;code&gt;touchstart&lt;/code&gt;/&lt;code&gt;touchend&lt;/code&gt; 控制录音起止）、上滑取消（&lt;code&gt;touchmove&lt;/code&gt;/&lt;code&gt;touchcancel&lt;/code&gt;），体验和微信原生聊天对齐；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;发送体验乐观化&lt;/strong&gt;：消息发出后先本地 push 进消息列表立即上屏，&lt;code&gt;send&lt;/code&gt; 的 fail 回调再 toast 兜底提示。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;需要如实说明：聊天服务端的代码没有随这两个仓库开源（它部署在同一台服务器上，由 nginx 分流），仓库里能看到的生产实现是小程序端；&lt;code&gt;crm_server&lt;/code&gt; 里的 &lt;code&gt;com/im/SocketServer.java&lt;/code&gt; 只是当时练手的原生 TCP Socket demo（12345 端口），并非线上实现。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;四技术亮点&#34;&gt;四、技术亮点&lt;/h2&gt;
&lt;h3 id=&#34;41-下单幂等两段式--唯一键兜底&#34;&gt;4.1 下单幂等：两段式 + 唯一键兜底&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;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;graph TD
    A[&amp;#34;order/confirm 确认订单页&amp;#34;] --&amp;gt; B[&amp;#34;订单数据序列化存 Redis&amp;lt;br/&amp;gt;key = md5 当前时间&amp;lt;br/&amp;gt;有效期 60 分钟&amp;#34;]
    B --&amp;gt; C[&amp;#34;order/create/orderKey 创建订单&amp;#34;]
    C --&amp;gt; D{&amp;#34;第一步:查库判重&amp;lt;br/&amp;gt;StoreOrder.unique = orderKey&amp;#34;}
    D --&amp;gt;|&amp;#34;已存在&amp;#34;| E[&amp;#34;直接返回已有订单&amp;lt;br/&amp;gt;不重复创建&amp;#34;]
    D --&amp;gt;|&amp;#34;不存在&amp;#34;| F{&amp;#34;第二步:校验 Redis 缓存&amp;lt;br/&amp;gt;是否过期/被用过&amp;#34;}
    F --&amp;gt;|&amp;#34;过期&amp;#34;| G[&amp;#34;拒绝 提示重新确认&amp;#34;]
    F --&amp;gt;|&amp;#34;有效&amp;#34;| H[&amp;#34;事务内创建订单&amp;lt;br/&amp;gt;成功后删除缓存&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;&lt;code&gt;unique&lt;/code&gt; 字段是最后的兜底——即使 Redis 挂了、缓存丢了，数据库唯一约束也保证同一个 orderKey 不会建出两张单。&lt;/p&gt;
&lt;h3 id=&#34;42-原子扣库存防超卖&#34;&gt;4.2 原子扣库存防超卖&lt;/h3&gt;
&lt;p&gt;库存扣减没有用&amp;quot;先查后改&amp;quot;，而是一条 SQL 完成：&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;// setSql + 条件改写，数据库层面原子完成&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;stock &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; stock &lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt; n   且要求   stock &lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt; n &lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;=&lt;/span&gt; 0&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;数据库单行更新的原子性天然挡住并发超卖，不需要分布式锁。秒杀/砍价/拼团场景更复杂：同一事务内要扣&lt;strong&gt;活动表 + 活动 SKU 表 + 商品表 + 商品 SKU 表&lt;/strong&gt;四份库存，任何一步失败整体回滚。&lt;/p&gt;
&lt;h3 id=&#34;43-编程式事务包住整个建单过程&#34;&gt;4.3 编程式事务包住整个建单过程&lt;/h3&gt;
&lt;p&gt;创建订单是个多写操作：扣库存、占预约时段、写订单主表、写订单详情、核销优惠券、写订单日志。代码用 &lt;code&gt;TransactionTemplate&lt;/code&gt; 编程式事务（而不是 &lt;code&gt;@Transactional&lt;/code&gt; 注解）把这一串操作包起来——好处是事务边界精确可控，不会把整个方法（包括不需要事务的远程调用）都圈进去。订单核心逻辑集中在 &lt;code&gt;OrderUtils.java&lt;/code&gt;（1500+ 行）和 &lt;code&gt;OrderServiceImpl.java&lt;/code&gt;（近 1000 行）。&lt;/p&gt;
&lt;h3 id=&#34;44-支付回调防重--异步化&#34;&gt;4.4 支付回调：防重 + 异步化&lt;/h3&gt;
&lt;p&gt;微信支付的回调处理（&lt;code&gt;CallbackServiceImpl.weChat&lt;/code&gt;，448 行）有几个关键点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;解析 XML、校验 &lt;code&gt;return_code&lt;/code&gt;/&lt;code&gt;result_code&lt;/code&gt;，按 &lt;code&gt;attach&lt;/code&gt; 里携带的 userId + type 区分是订单支付还是充值；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;已支付直接返回 SUCCESS&lt;/strong&gt;——微信会多次重推回调，幂等是回调接口的生死线；&lt;/li&gt;
&lt;li&gt;事务内完成 &lt;code&gt;updatePaid&lt;/code&gt;（置订单已支付）+ 扣积分 + 拼团状态处理；&lt;/li&gt;
&lt;li&gt;后续的&amp;quot;慢动作&amp;quot;——发短信、推订阅消息、分销结算——&lt;strong&gt;不放在回调里同步做&lt;/strong&gt;，而是 &lt;code&gt;lPush&lt;/code&gt; 进 Redis 队列异步执行，回调接口尽快给微信返回，避免超时重推。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&#34;45-没有-mqredis-list--定时任务的务实替代&#34;&gt;4.5 没有 MQ：Redis List + 定时任务的务实替代&lt;/h3&gt;
&lt;p&gt;项目没有引入 RabbitMQ/RocketMQ，而是用 &lt;strong&gt;Redis List 当队列 + Spring &lt;code&gt;@Scheduled&lt;/code&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;graph LR
    A[&amp;#34;下单成功&amp;lt;br/&amp;gt;lPush ORDER_AUTO_CANCEL_KEY&amp;#34;] --&amp;gt; Q[&amp;#34;Redis List 队列&amp;#34;]
    P[&amp;#34;支付成功回调&amp;lt;br/&amp;gt;lPush 后续动作&amp;#34;] --&amp;gt; Q
    Q --&amp;gt; T1[&amp;#34;OrderAutoCancelTask&amp;lt;br/&amp;gt;每分钟消费&amp;lt;br/&amp;gt;超时未支付自动取消 + 回滚库存&amp;#34;]
    Q --&amp;gt; T2[&amp;#34;自动收货 / 自动好评&amp;#34;]
    Q --&amp;gt; T3[&amp;#34;优惠券过期 / 砍价截止&amp;lt;br/&amp;gt;佣金解冻&amp;#34;]
    Q --&amp;gt; T4[&amp;#34;TeacherPlanTask&amp;lt;br/&amp;gt;每8小时补齐号源&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;&lt;code&gt;task/order/&lt;/code&gt; 下共有 8 个定时任务。对这个体量的系统，省掉一个 MQ 的部署运维成本，换来的是完全够用的可靠性——这是小项目技术选型上我很认可的一个取舍。&lt;/p&gt;
&lt;h3 id=&#34;46-鉴权链路uuid-token--redis--threadlocal&#34;&gt;4.6 鉴权链路：UUID Token + Redis + ThreadLocal&lt;/h3&gt;
&lt;p&gt;没有用 JWT，而是更传统的方案：登录后签发 UUID 存 Redis（按模块区分 key 前缀），拦截器 &lt;code&gt;FrontTokenInterceptor&lt;/code&gt; → &lt;code&gt;CheckFrontToken&lt;/code&gt; → &lt;code&gt;TokenManagerImpl&lt;/code&gt; 逐级校验，通过后把当前用户放进 &lt;code&gt;ThreadLocal&lt;/code&gt; 供全链路取用。管理端是 Token + 权限双拦截器。好处是&lt;strong&gt;服务端可随时踢人下线&lt;/strong&gt;（删 Redis key 即可），这是纯 JWT 做不到的。&lt;/p&gt;
&lt;h3 id=&#34;47-其他值得提的实现&#34;&gt;4.7 其他值得提的实现&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;订单快照&lt;/strong&gt;：下单时把购物车内容序列化成 JSON 存进 &lt;code&gt;StoreOrderInfo.info&lt;/code&gt;，之后商品改价改名都不影响历史订单的展示；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;运费计算引擎&lt;/strong&gt;：&lt;code&gt;getOrderPriceGroup&lt;/code&gt; 支持满额包邮、指定城市包邮（件数 + 金额双条件）、按件数/重量/体积三种首续计费模式；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;XSS 转义&lt;/strong&gt;：用户输入统一 &lt;code&gt;escapeHtml4&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;部署脚本&lt;/strong&gt;：&lt;code&gt;start.sh&lt;/code&gt; nohup 启动 + 轮询日志判断启动成功 + 企业微信机器人推送部署结果——单机部署也搞出了仪式感。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;五复盘与遗憾&#34;&gt;五、复盘与遗憾&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;底座与自研的边界&lt;/strong&gt;：秒杀/拼团/砍价/分销这些&amp;quot;重&amp;quot;能力直接复用 CRMEB 底座，把精力集中在业务差异化（预约、择吉日、聊天）上——对个人开发者来说这是正确的杠杆；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;单体的局限&lt;/strong&gt;：所有东西在一个 SpringBoot 应用里，定时任务、支付、前台接口互相影响，量大了必然要拆；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;原子扣库存的返回值没检查&lt;/strong&gt;：&lt;code&gt;setSql&lt;/code&gt; 扣减失败（库存不足）时影响的行数为 0，但代码没有校验这个返回值，极端情况下会漏判超卖——这是现在回头看想补的一个洞；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;聊天服务端未开源&lt;/strong&gt;：如第三章所述，长连接聊天的服务端实现没有随仓库开源，小程序端和接口契约是完整的，算是开源的一点遗憾。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;六写在最后&#34;&gt;六、写在最后&lt;/h2&gt;
&lt;p&gt;这个项目让我完整走了一遍&amp;quot;真实收钱&amp;quot;的系统该有的样子：幂等、事务、防超卖、回调防重、异步补偿——每一个点背后都是电商系统趟过的坑。代码在 &lt;a href=&#34;https://gitee.com/soulnew/starmall&#34;&gt;https://gitee.com/soulnew/starmall&lt;/a&gt; 和 &lt;a href=&#34;https://gitee.com/soulnew/crm_server&#34;&gt;https://gitee.com/soulnew/crm_server&lt;/a&gt;，建库 SQL、接口文档都在仓库里，希望能给做小程序商城的朋友一个可参照的完整样本。&lt;/p&gt;
</description>
    </item>
    
  </channel>
</rss>
