<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>神经蛙</title>
  <icon>http://hexo.sjava.cn/img/pic.png</icon>
  <subtitle>分享科技与生活</subtitle>
  <link href="http://hexo.sjava.cn/rss.xml" rel="self"/>
  
  <link href="http://hexo.sjava.cn/"/>
  <updated>2026-09-21T02:10:00.000Z</updated>
  <id>http://hexo.sjava.cn/</id>
  
  <author>
    <name>神经蛙</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>Spark 核心原理与实战：RDD、DAG 调度、Shuffle 与内存管理</title>
    <link href="http://hexo.sjava.cn/posts/64218.html"/>
    <id>http://hexo.sjava.cn/posts/64218.html</id>
    <published>2026-09-21T02:10:00.000Z</published>
    <updated>2026-09-21T02:10:00.000Z</updated>
    
    <content type="html"><![CDATA[<div class="tip warning"><p>很多团队写批处理任务的第一反应是 MapReduce，但真正跑起来才发现：一个简单的 WordCount 要拆成 Map 和 Reduce 两个阶段，中间结果全部落盘 HDFS，稍复杂的迭代算法要串起十几个 MR 作业，每一个都在读写磁盘。Spark 用 <strong>DAG 执行引擎 + 基于内存的计算</strong> 把这个问题彻底重做了一遍。本文从原理到实战，把 Spark 最容易被问、最容易踩坑的部分一次讲透。</p></div><h1 id="一、为什么需要-Spark：MapReduce-的三大痛点"><a href="#一、为什么需要-Spark：MapReduce-的三大痛点" class="headerlink" title="一、为什么需要 Spark：MapReduce 的三大痛点"></a>一、为什么需要 Spark：MapReduce 的三大痛点</h1><p>MapReduce 是第一代批处理引擎，但它天生”腿短”：</p><div class="table-container"><table><thead><tr><th>痛点</th><th>MapReduce 的表现</th><th>Spark 的解法</th></tr></thead><tbody><tr><td>中间结果落盘</td><td>每个 MR 作业的输出写 HDFS，下游再读一遍</td><td>中间结果保留在内存，必要时才溢写磁盘</td></tr><tr><td>表达能力弱</td><td>只有 Map / Reduce 两个算子，Join、排序要手写</td><td>80+ 算子（<code>flatMap</code>、<code>reduceByKey</code>、<code>join</code>…），代码量降一个量级</td></tr><tr><td>延迟高</td><td>JVM 冷启动 + 磁盘 IO，分钟级延迟</td><td>DAG 调度 + 线程级任务模型，秒级~亚秒级</td></tr></tbody></table></div><div class="note orange anzhiyufont anzhiyu-icon-dengpao disabled"><p>一句话总结：<strong>MapReduce 把计算当”流水线上的搬运工”，Spark 把计算当”可以反复使用的工作台”</strong>。数据一旦加载进集群，后续十几步转换都在内存工作台上进行，只在必要时才落盘。</p></div><p>Spark 生态覆盖了统一的计算场景：<strong>Spark Core</strong>（RDD 内核）、<strong>Spark SQL</strong>（结构化数据 + Catalyst 优化器）、<strong>Spark Streaming / Structured Streaming</strong>（流计算）、<strong>MLlib</strong>（机器学习）、<strong>GraphX</strong>（图计算）。部署上支持 Local、Standalone、YARN、Kubernetes 四种模式，生产以 YARN / K8s 为主。</p><h1 id="二、RDD：Spark-的基石"><a href="#二、RDD：Spark-的基石" class="headerlink" title="二、RDD：Spark 的基石"></a>二、RDD：Spark 的基石</h1><h2 id="2-1-RDD-是什么"><a href="#2-1-RDD-是什么" class="headerlink" title="2.1 RDD 是什么"></a>2.1 RDD 是什么</h2><p>RDD（Resilient Distributed Dataset，弹性分布式数据集）是一个<strong>不可变、可分区、可并行计算的元素集合</strong>。”弹性”体现在三层：</p><ul><li><strong>存储弹性</strong>：内存不够自动落盘，磁盘不够自动重算；</li><li><strong>计算弹性</strong>：失败自动通过血缘（Lineage）重算恢复；</li><li><strong>分区弹性</strong>：分区数随时可以重新划分（<code>repartition</code> / <code>coalesce</code>）。</li></ul><p>源码里 RDD 的注释写明了五大特性，面试高频，务必能背出主干：</p><div class="table-container"><table><thead><tr><th>特性</th><th>含义</th><th>一句话理解</th></tr></thead><tbody><tr><td>A list of partitions</td><td>一组分区列表</td><td>并行度的基本单位</td></tr><tr><td>A function for computing each split</td><td>每个分区一个计算函数</td><td>作用在分区而非全量数据上</td></tr><tr><td>A list of dependencies on other RDDs</td><td>依赖（血缘）列表</td><td>容错重算的依据</td></tr><tr><td>Optionally, a Partitioner</td><td>可选分区器（K-V RDD）</td><td>Hash / Range，决定数据分布</td></tr><tr><td>Optionally, preferred locations</td><td>可选优先位置</td><td>移动计算不移动数据</td></tr></tbody></table></div><h2 id="2-2-算子体系：Transformation-与-Action"><a href="#2-2-算子体系：Transformation-与-Action" class="headerlink" title="2.2 算子体系：Transformation 与 Action"></a>2.2 算子体系：Transformation 与 Action</h2><p>RDD 算子分两类，<strong>这个区分决定了作业什么时候真正执行</strong>：</p><figure class="highlight scala"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">val</span> lines = sc.textFile(<span class="string">&quot;hdfs://data/logs&quot;</span>)            <span class="comment">// Transformation：不执行</span></span><br><span class="line"><span class="keyword">val</span> errors = lines.filter(_.contains(<span class="string">&quot;ERROR&quot;</span>))          <span class="comment">// Transformation：不执行</span></span><br><span class="line"><span class="keyword">val</span> wc = errors.flatMap(_.split(<span class="string">&quot; &quot;</span>))</span><br><span class="line">  .map((_, <span class="number">1</span>))</span><br><span class="line">  .reduceByKey(_ + _)                                   <span class="comment">// Transformation：仍不执行</span></span><br><span class="line">wc.collect()                                            <span class="comment">// Action：触发真正计算</span></span><br></pre></td></tr></table></figure><div class="table-container"><table><thead><tr><th>类别</th><th>触发时机</th><th>代表算子</th><th>返回值</th></tr></thead><tbody><tr><td>Transformation</td><td>惰性，只记录血缘</td><td><code>map</code> <code>filter</code> <code>flatMap</code> <code>reduceByKey</code> <code>join</code> <code>distinct</code></td><td>新 RDD</td></tr><tr><td>Action</td><td>立即触发 Job</td><td><code>collect</code> <code>count</code> <code>take</code> <code>first</code> <code>saveAsTextFile</code> <code>foreach</code></td><td>非 RDD（值/落盘）</td></tr></tbody></table></div><div class="note orange anzhiyufont anzhiyu-icon-dengpao disabled"><p><strong>每个 Action 生成一个 Job</strong>。代码里写了三个 <code>collect</code>，就是三个独立的 Job，前面的血缘会被重复计算——这就是为什么 “一个应用一个 Action” 是常见优化原则，或者用 <code>persist()</code> 把公共中间结果缓存起来。</p></div><h1 id="三、宽窄依赖与-DAG：Stage-划分的分界线"><a href="#三、宽窄依赖与-DAG：Stage-划分的分界线" class="headerlink" title="三、宽窄依赖与 DAG：Stage 划分的分界线"></a>三、宽窄依赖与 DAG：Stage 划分的分界线</h1><h2 id="3-1-宽依赖-vs-窄依赖"><a href="#3-1-宽依赖-vs-窄依赖" class="headerlink" title="3.1 宽依赖 vs 窄依赖"></a>3.1 宽依赖 vs 窄依赖</h2><div class="table-container"><table><thead><tr><th>依赖类型</th><th>定义</th><th>典型算子</th><th>特点</th></tr></thead><tbody><tr><td>窄依赖</td><td>父 RDD 的每个分区<strong>最多</strong>被子 RDD 的一个分区使用</td><td><code>map</code> <code>filter</code> <code>union</code> <code>mapPartitions</code></td><td>分区可流水线（pipeline）执行，一个分区的失败只需重算父分区</td></tr><tr><td>宽依赖</td><td>子 RDD 的分区依赖父 RDD 的<strong>多个</strong>分区</td><td><code>groupByKey</code> <code>reduceByKey</code> <code>join</code>（非同分区）</td><td>必须等父 Stage 全部就绪，伴随 Shuffle</td></tr></tbody></table></div><p><strong>DAG 划分 Stage 的规则：从后往前，遇到宽依赖就切一刀。</strong> 每个切出来的阶段是一个 Stage，Stage 内部尽量流水线执行，Shuffle 是两个 Stage 之间的数据交换边界。</p><h2 id="3-2-一个例子看懂-Stage-划分"><a href="#3-2-一个例子看懂-Stage-划分" class="headerlink" title="3.2 一个例子看懂 Stage 划分"></a>3.2 一个例子看懂 Stage 划分</h2><figure class="highlight scala"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">sc.textFile(<span class="string">&quot;data.txt&quot;</span>)          <span class="comment">// Stage 0 开始</span></span><br><span class="line">  .flatMap(_.split(<span class="string">&quot; &quot;</span>))         <span class="comment">// 窄</span></span><br><span class="line">  .map((_, <span class="number">1</span>))                   <span class="comment">// 窄</span></span><br><span class="line">  .reduceByKey(_ + _)            <span class="comment">// 宽依赖 → 在此切分 Stage</span></span><br><span class="line">  .map(kv =&gt; (kv._2, kv._1))     <span class="comment">// Stage 1</span></span><br><span class="line">  .sortByKey(ascending = <span class="literal">false</span>)  <span class="comment">// 宽依赖 → 切分</span></span><br><span class="line">  .collect()                     <span class="comment">// Stage 2（Action 触发 Job）</span></span><br></pre></td></tr></table></figure><p>整条链被切成 3 个 Stage：前两个窄操作被”打包”进同一个 Stage 流水线执行，<code>reduceByKey</code> 和 <code>sortByKey</code> 各自切开一个新 Stage。</p><h1 id="四、任务调度：从-Job-到-Task-的完整链路"><a href="#四、任务调度：从-Job-到-Task-的完整链路" class="headerlink" title="四、任务调度：从 Job 到 Task 的完整链路"></a>四、任务调度：从 Job 到 Task 的完整链路</h1><p>一个 Action 触发后，调度链路如下：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">Application</span><br><span class="line"> └── Job（每个 Action 一个）</span><br><span class="line">      └── Stage（DAGScheduler 按宽依赖切分）</span><br><span class="line">           └── TaskSet（Stage 内所有分区的任务集合）</span><br><span class="line">                └── Task（TaskScheduler 分发到 Executor 执行）</span><br></pre></td></tr></table></figure><p>关键角色：</p><ul><li><strong>DAGScheduler</strong>：负责把血缘 DAG 按 Stage 划分、维护 Stage 依赖、提交”没有父依赖”的 Stage；同时处理 ShuffleMapStage 与 ResultStage 的区分。</li><li><strong>TaskScheduler</strong>：把 TaskSet 里的 Task 按本地性级别（<code>PROCESS_LOCAL → NODE_LOCAL → ANY</code>）分发给 Executor，支持<strong>推测执行</strong>（Speculative Execution）：某个 Task 明显慢于同 Stage 均值时，另起一个备份 Task，谁先完成用谁的结果。</li><li><strong>Executor</strong>：以<strong>线程池</strong>方式执行 Task（MapReduce 是进程级，启动开销大），这是 Spark 秒级延迟的另一个原因。</li></ul><h1 id="五、Shuffle：性能问题的-80-都在这"><a href="#五、Shuffle：性能问题的-80-都在这" class="headerlink" title="五、Shuffle：性能问题的 80% 都在这"></a>五、Shuffle：性能问题的 80% 都在这</h1><p>Shuffle 是宽依赖的数据交换过程——按 Key 把数据重新分发到不同的下游分区。它是 Spark 里最昂贵、也最值得调优的环节。</p><h2 id="5-1-HashShuffle：为什么被淘汰"><a href="#5-1-HashShuffle：为什么被淘汰" class="headerlink" title="5.1 HashShuffle：为什么被淘汰"></a>5.1 HashShuffle：为什么被淘汰</h2><p>未优化的 HashShuffle 中，每个 Map Task 为每个 Reduce Task 生成一个文件：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">小文件数 = Map Task 数 × Reduce Task 数</span><br><span class="line">1000 map × 1000 reduce = 100 万个小文件</span><br></pre></td></tr></table></figure><p>文件句柄耗尽、磁盘随机 IO 打爆、GC 频繁。优化后的 Consolidate 机制把同一 Executor 上 Core 的输出合并为 <code>CPU 核数 × Reduce Task 数</code>，但依然治标不治本。</p><h2 id="5-2-SortShuffle：现在的默认机制（1-2-）"><a href="#5-2-SortShuffle：现在的默认机制（1-2-）" class="headerlink" title="5.2 SortShuffle：现在的默认机制（1.2+）"></a>5.2 SortShuffle：现在的默认机制（1.2+）</h2><p>SortShuffle 的核心思路：<strong>每个 Map Task 只输出一个按分区有序的数据文件 + 一个索引文件</strong>。</p><ol><li>数据写入内存排序结构（<code>PartitionedAppendOnlyMap</code>），超过阈值后溢写成临时文件；</li><li>最终把所有溢写文件<strong>归并</strong>成一个数据文件，按 Reduce 分区有序排列；</li><li>Reduce Task 根据索引文件偏移量拉取属于自己的数据段。</li></ol><p><strong>Bypass 机制</strong>是 SortShuffle 的”轻量通道”：当 Reduce Task 数 ≤ <code>spark.shuffle.sort.bypassMergeThreshold</code>（默认 200）<strong>且</strong>算子不需要 map 端聚合（如 <code>groupBy</code>，而非 <code>reduceByKey</code>）时，跳过排序，直接按分区写文件再归并——用 HashShuffle 的写法拿到单文件的好处。</p><h2 id="5-3-Reduce-端拉取与关键参数"><a href="#5-3-Reduce-端拉取与关键参数" class="headerlink" title="5.3 Reduce 端拉取与关键参数"></a>5.3 Reduce 端拉取与关键参数</h2><p>Reduce 端通过 HTTP 拉取数据，相关参数直接影响稳定性：</p><div class="table-container"><table><thead><tr><th>参数</th><th>默认值</th><th>说明</th></tr></thead><tbody><tr><td><code>spark.reducer.maxSizeInFlight</code></td><td>48m</td><td>单次拉取缓冲区，太小网络往返多</td></tr><tr><td><code>spark.shuffle.io.maxRetries</code></td><td>3</td><td>拉取失败重试次数，生产建议 5+</td></tr><tr><td><code>spark.shuffle.io.retryWait</code></td><td>5s</td><td>重试间隔，生产建议 15s+（<code>maxRetries × retryWait</code> 要能扛住 FullGC）</td></tr><tr><td><code>spark.sql.shuffle.partitions</code></td><td>200</td><td>SQL 场景 Reduce 并行度，按数据量调整</td></tr></tbody></table></div><h1 id="六、内存管理：统一内存管理模型"><a href="#六、内存管理：统一内存管理模型" class="headerlink" title="六、内存管理：统一内存管理模型"></a>六、内存管理：统一内存管理模型</h1><p>Spark 1.6+ 采用 <strong>UnifiedMemoryManager（统一内存管理）</strong>，Executor 堆内空间划分如下：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">Executor 堆内内存</span><br><span class="line">├── Reserved Memory：固定 300MB</span><br><span class="line">├── User Memory：(1 - spark.memory.fraction) × 堆，存用户数据结构、RDD 依赖等</span><br><span class="line">└── Unified Memory（spark.memory.fraction = 0.6）</span><br><span class="line">     ├── Execution Memory：Shuffle / Join / Sort / 聚合 —— 不被抢占</span><br><span class="line">     └── Storage Memory（storageFraction = 0.5）：缓存 RDD / 广播变量 —— 可被驱逐落盘</span><br></pre></td></tr></table></figure><div class="note orange anzhiyufont anzhiyu-icon-dengpao disabled"><p><strong>动态借用的不对称规则</strong>是面试重点：两块内存空闲时可以互相借用；但 Execution 占用的部分<strong>绝不会被归还</strong>（中途释放会导致数据结构失效），Storage 借用的部分在 Execution 需要时会被驱逐（缓存块溢写磁盘或丢弃）。所以 Executor OOM 大多发生在 Execution 侧——Shuffle 聚合是内存大户。</p></div><p>堆外内存通过 <code>spark.memory.offHeap.enabled</code> 开启，用 <code>spark.executor.memoryOverhead</code> 预留堆外空间（YARN/K8s 下默认为堆的 10%），容器总内存 = 堆 + 堆外 + overhead，配小了会被 YARN 直接 kill。</p><h1 id="七、持久化与容错：Lineage-与-Checkpoint"><a href="#七、持久化与容错：Lineage-与-Checkpoint" class="headerlink" title="七、持久化与容错：Lineage 与 Checkpoint"></a>七、持久化与容错：Lineage 与 Checkpoint</h1><h2 id="7-1-缓存：persist-cache"><a href="#7-1-缓存：persist-cache" class="headerlink" title="7.1 缓存：persist / cache"></a>7.1 缓存：persist / cache</h2><div class="table-container"><table><thead><tr><th>级别</th><th>空间</th><th>说明</th></tr></thead><tbody><tr><td><code>MEMORY_ONLY</code>（cache 默认）</td><td>内存</td><td>内存不够则部分分区不缓存，用时重算</td></tr><tr><td><code>MEMORY_AND_DISK</code></td><td>内存+磁盘</td><td>内存放不下的溢写磁盘，最常用</td></tr><tr><td><code>MEMORY_ONLY_SER</code></td><td>内存</td><td>序列化存储，省空间但耗 CPU</td></tr><tr><td><code>DISK_ONLY</code></td><td>磁盘</td><td>完全落盘</td></tr></tbody></table></div><p>缓存本身<strong>不切断血缘</strong>——缓存分区丢失时，仍然沿血缘重算。</p><h2 id="7-2-Checkpoint：斩断血缘"><a href="#7-2-Checkpoint：斩断血缘" class="headerlink" title="7.2 Checkpoint：斩断血缘"></a>7.2 Checkpoint：斩断血缘</h2><p>血缘太长时（迭代算法、长链路转换），单分区失败的重算代价会指数级膨胀。<code>checkpoint</code> 把 RDD <strong>完整写入高可靠存储（HDFS）并清空血缘</strong>，失败后直接从 checkpoint 恢复：</p><figure class="highlight scala"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">sc.setCheckpointDir(<span class="string">&quot;hdfs://checkpoints&quot;</span>)</span><br><span class="line"><span class="keyword">val</span> result = longLineageRdd.persist()</span><br><span class="line">result.checkpoint()          <span class="comment">// 必须在 Action 之前设置</span></span><br><span class="line">result.count()               <span class="comment">// 触发时顺带写入 checkpoint</span></span><br></pre></td></tr></table></figure><p>实践口诀：<strong>先 <code>persist</code> 再 <code>checkpoint</code></strong>——否则 checkpoint 会把血缘完整重算一遍，等于白算两次。</p><h1 id="八、数据倾斜：定位与治理套路"><a href="#八、数据倾斜：定位与治理套路" class="headerlink" title="八、数据倾斜：定位与治理套路"></a>八、数据倾斜：定位与治理套路</h1><h2 id="8-1-现象与定位"><a href="#8-1-现象与定位" class="headerlink" title="8.1 现象与定位"></a>8.1 现象与定位</h2><ul><li><strong>现象</strong>：绝大多数 Task 几秒跑完，个别 Task 卡几十分钟甚至 OOM；</li><li><strong>定位</strong>：Web UI 的 Stage 页看 Task 的 Shuffle Read Size 分布是否长尾；<code>df.groupBy(key).count().orderBy(&quot;count&quot;, ascending=False)</code> 检查热点 Key。</li></ul><h2 id="8-2-治理方案（按优先级）"><a href="#8-2-治理方案（按优先级）" class="headerlink" title="8.2 治理方案（按优先级）"></a>8.2 治理方案（按优先级）</h2><p><strong>方案一：过滤或单独处理异常 Key。</strong> 如果倾斜来自 <code>null</code>、<code>&quot;&quot;</code> 等脏数据，先过滤；如果是个别真实大 Key，把它们捞出来单独跑，再和主流程结果合并。</p><p><strong>方案二：广播 Join（小表广播）。</strong> 大小表 Join 时避免 Shuffle：</p><figure class="highlight scala"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> org.apache.spark.sql.functions.broadcast</span><br><span class="line"><span class="keyword">val</span> joined = bigDF.join(broadcast(smallDF), <span class="string">&quot;user_id&quot;</span>)   <span class="comment">// 小表全量广播到各 Executor</span></span><br></pre></td></tr></table></figure><p>阈值由 <code>spark.sql.autoBroadcastJoinThreshold</code>（默认 10MB）控制，超过时需手动 <code>broadcast()</code> 或调大阈值（注意广播本身占内存）。</p><p><strong>方案三：加盐两阶段聚合。</strong> 对 <code>reduceByKey</code> 类聚合的倾斜 Key 加随机前缀，先局部聚合打散热点，再去前缀做全局聚合：</p><figure class="highlight scala"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 第一阶段：key 加 0~N 随机前缀，局部聚合</span></span><br><span class="line"><span class="keyword">val</span> partial = rdd.map &#123; <span class="keyword">case</span> (k, v) =&gt; ((<span class="type">Random</span>.nextInt(<span class="number">10</span>) + <span class="string">&quot;_&quot;</span> + k), v) &#125;</span><br><span class="line">  .reduceByKey(_ + _)</span><br><span class="line"><span class="comment">// 第二阶段：去掉前缀，全局聚合</span></span><br><span class="line"><span class="keyword">val</span> global = partial.map &#123; <span class="keyword">case</span> (pk, v) =&gt; (pk.split(<span class="string">&quot;_&quot;</span>)(<span class="number">1</span>), v) &#125;</span><br><span class="line">  .reduceByKey(_ + _)</span><br></pre></td></tr></table></figure><p><strong>方案四：提高 Shuffle 并行度。</strong> 调大 <code>spark.sql.shuffle.partitions</code>，让热点 Key 的数据被切得更碎，缓解（而非根治）倾斜。</p><h1 id="九、横向对比：Spark-vs-MapReduce-vs-Flink"><a href="#九、横向对比：Spark-vs-MapReduce-vs-Flink" class="headerlink" title="九、横向对比：Spark vs MapReduce vs Flink"></a>九、横向对比：Spark vs MapReduce vs Flink</h1><div class="table-container"><table><thead><tr><th>维度</th><th>MapReduce</th><th>Spark</th><th>Flink</th></tr></thead><tbody><tr><td>计算模型</td><td>批处理</td><td>批处理为主（微批流）</td><td>流处理为主（流批一体）</td></tr><tr><td>中间结果</td><td>落盘 HDFS</td><td>内存优先</td><td>基于流，数据持续流动</td></tr><tr><td>延迟</td><td>分钟级</td><td>秒级~亚秒级</td><td>毫秒级</td></tr><tr><td>容错</td><td>任务级重跑</td><td>Lineage 重算 + Checkpoint</td><td>分布式快照（Chandy-Lamport）</td></tr><tr><td>适用场景</td><td>超大规模离线归档</td><td>离线数仓、ETL、机器学习</td><td>实时数仓、CEP、风控</td></tr></tbody></table></div><p>选型经验：<strong>离线批处理选 Spark，实时流处理选 Flink，两者不是替代关系</strong>。Kafka + Spark/Flink 是流式架构最常见的组合（Kafka 负责缓冲与解耦，计算引擎负责处理）。</p><h1 id="十、生产避坑清单"><a href="#十、生产避坑清单" class="headerlink" title="十、生产避坑清单"></a>十、生产避坑清单</h1><ol><li><strong>并行度别拍脑袋</strong>：理想状态是 Task 数 = 总 Core 数的 2~3 倍；<code>spark.sql.shuffle.partitions=200</code> 对小数据是浪费、对大数据是瓶颈，按单分区 128MB~256MB 反推。</li><li><strong>别用 <code>collect()</code> 拉全量数据</strong>：Driver OOM 的头号来源，调试用 <code>take(n)</code> / <code>limit(n)</code>，落盘用 <code>write</code>。</li><li><strong>小文件问题</strong>：频繁写 Hive/对象存储会产生海量小文件，写入前 <code>coalesce(n)</code> 合并分区（保序减少数据移动用 <code>coalesce</code>，允许全量重分布用 <code>repartition</code>）。</li><li><strong>Kryo 序列化</strong>：<code>spark.serializer=org.apache.spark.serializer.KryoSerializer</code>，自定义类注册 <code>registerKryoClasses</code>，比 Java 序列化快 10 倍、体积小一半。</li><li><strong>缓存不是越多越好</strong>：<code>MEMORY_ONLY</code> 缓存被频繁淘汰重算时反而更慢；缓存前先评估重用次数与分区大小，用 <code>unpersist()</code> 及时释放。</li><li><strong>GC 调优顺序</strong>：先调内存分配（fraction / overhead），再看 GC 日志选收集器；<code>spark.executor.cores=4~5</code> 能减少 HDFS 客户端并发争抢。</li><li><strong>广播变量只读</strong>：Executor 端修改广播变量不会同步回 Driver，也看不到——它本质是只读快照。</li><li><strong>Spill 不等于故障</strong>：Shuffle 溢写是正常兜底机制；持续大量 Spill 才说明内存给少了或倾斜了，结合 Web UI 的 Spill (memory/disk) 指标判断。</li></ol>]]></content>
    
    
      
      
    <summary type="html">&lt;div class=&quot;tip warning&quot;&gt;&lt;p&gt;很多团队写批处理任务的第一反应是 MapReduce，但真正跑起来才发现：一个简单的 WordCount 要拆成 Map 和 Reduce 两个阶段，中间结果全部落盘 HDFS，稍复杂的迭代算法要串起十几个 MR 作业，每一</summary>
      
    
    
    
    <category term="大数据" scheme="http://hexo.sjava.cn/categories/%E5%A4%A7%E6%95%B0%E6%8D%AE/"/>
    
    
    <category term="性能优化" scheme="http://hexo.sjava.cn/tags/%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/"/>
    
    <category term="Spark" scheme="http://hexo.sjava.cn/tags/Spark/"/>
    
    <category term="大数据" scheme="http://hexo.sjava.cn/tags/%E5%A4%A7%E6%95%B0%E6%8D%AE/"/>
    
    <category term="分布式计算" scheme="http://hexo.sjava.cn/tags/%E5%88%86%E5%B8%83%E5%BC%8F%E8%AE%A1%E7%AE%97/"/>
    
  </entry>
  
  <entry>
    <title>Java I/O 与 NIO 实战：BIO、NIO、AIO 与零拷贝深度解析</title>
    <link href="http://hexo.sjava.cn/posts/27394.html"/>
    <id>http://hexo.sjava.cn/posts/27394.html</id>
    <published>2026-09-18T02:00:00.000Z</published>
    <updated>2026-09-18T02:00:00.000Z</updated>
    
    <content type="html"><![CDATA[<div class="tip warning"><p>面试里问完并发、集合，下一个必考的就是 I/O 与网络编程。本文不重复线程池、《Java 并发编程核心》里的 volatile/CAS/AQS，也不展开集合源码，而是聚焦一个独立主题：<strong>Java 到底怎么读写数据、怎么用少量线程扛住海量连接</strong>。读完你能回答：BIO 为什么扛不住 C10K？Selector 凭什么一个线程管几万个连接？零拷贝究竟”零”掉了什么？</p></div><h1 id="一、为什么-I-O-模型是-Java-工程师的分水岭"><a href="#一、为什么-I-O-模型是-Java-工程师的分水岭" class="headerlink" title="一、为什么 I/O 模型是 Java 工程师的分水岭"></a>一、为什么 I/O 模型是 Java 工程师的分水岭</h1><p>写业务代码时，I/O 往往被框架封装得无影无踪（Spring MVC 一个 <code>@RequestMapping</code> 就完事）。但一旦要写中间件、网关、RPC 框架、消息队列客户端，或者排查”线程一路涨到几万、GC 频繁、CPU 却不高”的线上问题，I/O 模型的理解深度就直接决定了你的定位能力。</p><p>I/O 的本质只有一句话：<strong>数据在「用户空间」与「内核空间」之间的搬运</strong>。所有模型差异，都是”这段搬运由谁触发、要不要阻塞线程、要不要来回切换”的组合。</p><h1 id="二、先把概念厘清：阻塞-非阻塞、同步-异步"><a href="#二、先把概念厘清：阻塞-非阻塞、同步-异步" class="headerlink" title="二、先把概念厘清：阻塞/非阻塞、同步/异步"></a>二、先把概念厘清：阻塞/非阻塞、同步/异步</h1><p>这两组词最容易被混用，必须彻底分开看，它们描述的是<strong>两个正交维度</strong>：</p><ul><li><strong>阻塞 vs 非阻塞</strong>：描述”应用程序发起系统调用后，线程会不会被挂起”。阻塞调用会交出 CPU 让出线程；非阻塞调用立刻返回，即使数据还没准备好。</li><li><strong>同步 vs 异步</strong>：描述”数据从内核拷贝到用户空间这一步，由谁来完成、结果如何通知”。</li></ul><div class="table-container"><table><thead><tr><th style="text-align:left">组合</th><th style="text-align:left">含义</th><th style="text-align:left">代表</th></tr></thead><tbody><tr><td style="text-align:left">同步阻塞</td><td style="text-align:left">调用后线程挂起，直到数据拷贝完成</td><td style="text-align:left">传统 BIO</td></tr><tr><td style="text-align:left">同步非阻塞</td><td style="text-align:left">调用立刻返回，需轮询；就绪后仍由自己拷贝</td><td style="text-align:left">NIO 非阻塞模式</td></tr><tr><td style="text-align:left">异步非阻塞</td><td style="text-align:left">调用后立即返回，内核完成拷贝后回调通知</td><td style="text-align:left">AIO</td></tr></tbody></table></div><div class="note blue anzhiyufont anzhiyu-icon-lightbulb disabled"><p>一句话记忆：<strong>“阻塞/非阻塞”看线程等不等，”同步/异步”看拷贝谁来做</strong>。常见误区是把 NIO 当成”异步”，其实标准 NIO 是<strong>同步非阻塞</strong>——就绪后读数据仍是用户线程去读；真正异步的是 AIO。</p></div><p>UNIX 网络编程里经典的五种 I/O 模型：</p><ol><li><strong>阻塞 I/O</strong>：最传统，读/写/accept 全程阻塞。</li><li><strong>非阻塞 I/O</strong>：轮询，CPU 空转严重。</li><li><strong>I/O 多路复用</strong>：select/poll/epoll，一个线程监听多个 fd（NIO 的 Selector）。</li><li><strong>信号驱动 I/O</strong>：内核用信号通知就绪，实际使用少。</li><li><strong>异步 I/O</strong>：内核完成全部工作后通知（AIO，Linux 上实现有争议）。</li></ol><h1 id="三、BIO：传统阻塞式-I-O"><a href="#三、BIO：传统阻塞式-I-O" class="headerlink" title="三、BIO：传统阻塞式 I/O"></a>三、BIO：传统阻塞式 I/O</h1><h2 id="3-1-字节流与字符流体系"><a href="#3-1-字节流与字符流体系" class="headerlink" title="3.1 字节流与字符流体系"></a>3.1 字节流与字符流体系</h2><p>Java 传统 I/O（java.io）以两个维度组织：<strong>输入/输出</strong> × <strong>字节/字符</strong>。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">InputStream（字节输入）      OutputStream（字节输出）</span><br><span class="line"> ├ FileInputStream           ├ FileOutputStream</span><br><span class="line"> ├ ByteArrayInputStream      ├ ByteArrayOutputStream</span><br><span class="line"> ├ BufferedInputStream       ├ BufferedOutputStream</span><br><span class="line"> └ FilterInputStream         └ FilterOutputStream</span><br><span class="line">Reader（字符输入）           Writer（字符输出）</span><br><span class="line"> ├ FileReader                ├ FileWriter</span><br><span class="line"> ├ InputStreamReader         ├ OutputStreamWriter</span><br><span class="line"> └ BufferedReader            └ BufferedWriter</span><br></pre></td></tr></table></figure><p><strong>字节流</strong>以 8 位为单位，处理一切二进制数据（图片、音频、网络包）；<strong>字符流</strong>以 <code>char</code>（UTF-16 码元）为单位，内部靠 <code>StreamDecoder</code>/<code>StreamEncoder</code> 做编码转换。<strong>读写文本优先用字符流，避免手动处理编码导致中文乱码。</strong></p><h2 id="3-2-装饰器模式：缓冲流的真正价值"><a href="#3-2-装饰器模式：缓冲流的真正价值" class="headerlink" title="3.2 装饰器模式：缓冲流的真正价值"></a>3.2 装饰器模式：缓冲流的真正价值</h2><p>java.io 是<strong>装饰器模式</strong>的教科书实现：节点流负责”连到数据源”，处理流负责”增强能力”。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 不加缓冲：每次 read() 都可能触发一次系统调用</span></span><br><span class="line"><span class="keyword">try</span> (<span class="type">InputStream</span> <span class="variable">in</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">FileInputStream</span>(<span class="string">&quot;big.txt&quot;</span>)) &#123;</span><br><span class="line">    <span class="type">int</span> b;</span><br><span class="line">    <span class="keyword">while</span> ((b = in.read()) != -<span class="number">1</span>) &#123; <span class="comment">/* 逐字节读，慢 */</span> &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 加缓冲：一次性读 8KB 到内存，后续命中缓冲区</span></span><br><span class="line"><span class="keyword">try</span> (<span class="type">BufferedReader</span> <span class="variable">br</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">BufferedReader</span>(</span><br><span class="line">        <span class="keyword">new</span> <span class="title class_">InputStreamReader</span>(<span class="keyword">new</span> <span class="title class_">FileInputStream</span>(<span class="string">&quot;big.txt&quot;</span>), StandardCharsets.UTF_8))) &#123;</span><br><span class="line">    String line;</span><br><span class="line">    <span class="keyword">while</span> ((line = br.readLine()) != <span class="literal">null</span>) &#123;</span><br><span class="line">        System.out.println(line);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><div class="note green anzhiyufont anzhiyu-icon-chenggong disabled"><p><code>BufferedInputStream</code> 默认 8KB 缓冲区把”每次一字节一次系统调用”变成”每 8KB 一次”，吞吐量能提升一到两个数量级。<code>try-with-resources</code> 保证 <code>close()</code> 一定执行，且按<strong>后开先关</strong>的顺序释放。</p></div><h2 id="3-3-BIO-的致命伤：一连接一线程"><a href="#3-3-BIO-的致命伤：一连接一线程" class="headerlink" title="3.3 BIO 的致命伤：一连接一线程"></a>3.3 BIO 的致命伤：一连接一线程</h2><p>传统服务端写法：<code>ServerSocket.accept()</code> 会阻塞，每来一个连接就 <code>new Thread</code> 处理。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">ServerSocket</span> <span class="variable">serverSocket</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ServerSocket</span>(<span class="number">8080</span>);</span><br><span class="line"><span class="keyword">while</span> (<span class="literal">true</span>) &#123;</span><br><span class="line">    <span class="type">Socket</span> <span class="variable">socket</span> <span class="operator">=</span> serverSocket.accept();      <span class="comment">// 阻塞，直到有连接</span></span><br><span class="line">    <span class="keyword">new</span> <span class="title class_">Thread</span>(() -&gt; handle(socket)).start();   <span class="comment">// 一连接一线程</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>问题很直接：<strong>线程是稀缺资源</strong>。每个线程默认占用约 1MB 栈空间，且线程上下文切换有成本。1000 个连接就要 1000 个线程，1 万个就是 C10K 问题——线程创建/切换直接压垮系统。</p><div class="tip danger"><p>BIO 的线程数 ≈ 连接数。连接数一上来，内存与上下文切换双双爆炸。这正是 NIO 多路复用要解决的核心命题：<strong>用少量线程管理海量连接</strong>。</p></div><h1 id="四、NIO-三件套之一：Buffer（缓冲区）"><a href="#四、NIO-三件套之一：Buffer（缓冲区）" class="headerlink" title="四、NIO 三件套之一：Buffer（缓冲区）"></a>四、NIO 三件套之一：Buffer（缓冲区）</h1><p>NIO（java.nio）与流最大的区别是<strong>面向缓冲区</strong>：数据不再逐字节流动，而是先读进 <code>Buffer</code> 再处理。</p><p><code>Buffer</code> 通过四个指针控制读写：</p><div class="table-container"><table><thead><tr><th style="text-align:left">属性</th><th style="text-align:left">含义</th></tr></thead><tbody><tr><td style="text-align:left"><code>capacity</code></td><td style="text-align:left">容量，创建后不可变</td></tr><tr><td style="text-align:left"><code>limit</code></td><td style="text-align:left">可读/可写的边界</td></tr><tr><td style="text-align:left"><code>position</code></td><td style="text-align:left">下一个要读/写的位置</td></tr><tr><td style="text-align:left"><code>mark</code></td><td style="text-align:left">临时记录位置，便于 <code>reset()</code> 回退</td></tr></tbody></table></div><p><strong>读模式与写模式切换靠 <code>flip()</code></strong>，这是最容易踩坑的地方：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">ByteBuffer</span> <span class="variable">buf</span> <span class="operator">=</span> ByteBuffer.allocate(<span class="number">1024</span>);</span><br><span class="line">buf.put(<span class="string">&quot;hello&quot;</span>.getBytes());   <span class="comment">// 写模式：position=5, limit=1024</span></span><br><span class="line">buf.flip();                    <span class="comment">// 切换到读模式：limit=5, position=0</span></span><br><span class="line"><span class="keyword">while</span> (buf.hasRemaining()) &#123;</span><br><span class="line">    System.out.print((<span class="type">char</span>) buf.get());</span><br><span class="line">&#125;</span><br><span class="line">buf.clear();                   <span class="comment">// 清空，回到写模式：position=0, limit=capacity</span></span><br></pre></td></tr></table></figure><p>三个高频方法的区别：</p><ul><li><code>flip()</code>：<code>limit = position; position = 0</code>，写→读。</li><li><code>clear()</code>：<code>position = 0; limit = capacity</code>，丢弃全部数据。</li><li><code>compact()</code>：把未读完的数据挪到头部，再切回写模式（<strong>部分消费场景用</strong>）。</li></ul><div class="note orange anzhiyufont anzhiyu-icon-dengpao disabled"><p><code>ByteBuffer.allocate()</code> 分配在 <strong>JVM 堆</strong>上，<code>allocateDirect()</code> 分配在<strong>堆外内存（直接缓冲区）</strong>。堆外缓冲区不走 GC、<code>FileChannel</code>/<code>SocketChannel</code> 的读写可直接交给内核，适合大流量场景；代价是分配/回收昂贵，且不受堆内存限制，滥用会导致物理内存吃紧。</p></div><h1 id="五、NIO-三件套之二：Channel（通道）"><a href="#五、NIO-三件套之二：Channel（通道）" class="headerlink" title="五、NIO 三件套之二：Channel（通道）"></a>五、NIO 三件套之二：Channel（通道）</h1><p>Channel 与流的根本区别：</p><ul><li><strong>双向</strong>：一个 Channel 既可读也可写（流是单向的）。</li><li><strong>面向缓冲区</strong>：读写必须通过 Buffer。</li><li><strong>可非阻塞</strong>：可注册到 Selector 实现多路复用。</li></ul><p>常用实现：</p><div class="table-container"><table><thead><tr><th style="text-align:left">Channel</th><th style="text-align:left">用途</th></tr></thead><tbody><tr><td style="text-align:left"><code>FileChannel</code></td><td style="text-align:left">文件读写，支持 <code>transferTo</code>/<code>transferFrom</code>（零拷贝核心）</td></tr><tr><td style="text-align:left"><code>SocketChannel</code></td><td style="text-align:left">客户端 TCP 通道</td></tr><tr><td style="text-align:left"><code>ServerSocketChannel</code></td><td style="text-align:left">服务端监听通道</td></tr><tr><td style="text-align:left"><code>DatagramChannel</code></td><td style="text-align:left">UDP 通道</td></tr></tbody></table></div><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">try</span> (<span class="type">FileChannel</span> <span class="variable">in</span> <span class="operator">=</span> FileChannel.open(Paths.get(<span class="string">&quot;a.dat&quot;</span>), StandardOpenOption.READ);</span><br><span class="line">     <span class="type">FileChannel</span> <span class="variable">out</span> <span class="operator">=</span> FileChannel.open(Paths.get(<span class="string">&quot;b.dat&quot;</span>),</span><br><span class="line">             StandardOpenOption.CREATE, StandardOpenOption.WRITE)) &#123;</span><br><span class="line">    <span class="type">long</span> <span class="variable">size</span> <span class="operator">=</span> in.size();</span><br><span class="line">    <span class="type">long</span> <span class="variable">pos</span> <span class="operator">=</span> <span class="number">0</span>;</span><br><span class="line">    <span class="keyword">while</span> (pos &lt; size) &#123;</span><br><span class="line">        pos += in.transferTo(pos, size - pos, out);   <span class="comment">// 零拷贝搬运</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h1 id="六、NIO-三件套之三：Selector（多路复用器）"><a href="#六、NIO-三件套之三：Selector（多路复用器）" class="headerlink" title="六、NIO 三件套之三：Selector（多路复用器）"></a>六、NIO 三件套之三：Selector（多路复用器）</h1><p>Selector 是 NIO 的灵魂：<strong>一个线程通过一个 Selector 监听多个 Channel 的就绪事件</strong>，从而用极少的线程支撑海量连接。</p><p><code>SelectionKey</code> 的四种事件：</p><div class="table-container"><table><thead><tr><th style="text-align:left">事件常量</th><th style="text-align:left">值</th><th style="text-align:left">触发时机</th></tr></thead><tbody><tr><td style="text-align:left"><code>OP_ACCEPT</code></td><td style="text-align:left">16</td><td style="text-align:left">服务端有新连接</td></tr><tr><td style="text-align:left"><code>OP_CONNECT</code></td><td style="text-align:left">8</td><td style="text-align:left">客户端连接完成</td></tr><tr><td style="text-align:left"><code>OP_READ</code></td><td style="text-align:left">1</td><td style="text-align:left">可读</td></tr><tr><td style="text-align:left"><code>OP_WRITE</code></td><td style="text-align:left">4</td><td style="text-align:left">可写（发送缓冲区有空位）</td></tr></tbody></table></div><p>一个可运行的 NIO Echo 服务端骨架：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">ServerSocketChannel</span> <span class="variable">ssc</span> <span class="operator">=</span> ServerSocketChannel.open();</span><br><span class="line">ssc.bind(<span class="keyword">new</span> <span class="title class_">InetSocketAddress</span>(<span class="number">8080</span>));</span><br><span class="line">ssc.configureBlocking(<span class="literal">false</span>);                 <span class="comment">// 必须非阻塞才能注册</span></span><br><span class="line"><span class="type">Selector</span> <span class="variable">selector</span> <span class="operator">=</span> Selector.open();</span><br><span class="line">ssc.register(selector, SelectionKey.OP_ACCEPT);</span><br><span class="line"></span><br><span class="line"><span class="type">ByteBuffer</span> <span class="variable">buf</span> <span class="operator">=</span> ByteBuffer.allocate(<span class="number">1024</span>);</span><br><span class="line"><span class="keyword">while</span> (<span class="literal">true</span>) &#123;</span><br><span class="line">    selector.select();                        <span class="comment">// 阻塞，直到有通道就绪</span></span><br><span class="line">    Iterator&lt;SelectionKey&gt; it = selector.selectedKeys().iterator();</span><br><span class="line">    <span class="keyword">while</span> (it.hasNext()) &#123;</span><br><span class="line">        <span class="type">SelectionKey</span> <span class="variable">key</span> <span class="operator">=</span> it.next();</span><br><span class="line">        it.remove();                          <span class="comment">// 必须手动移除，否则重复处理</span></span><br><span class="line">        <span class="keyword">if</span> (key.isAcceptable()) &#123;</span><br><span class="line">            <span class="type">SocketChannel</span> <span class="variable">sc</span> <span class="operator">=</span> ssc.accept();</span><br><span class="line">            sc.configureBlocking(<span class="literal">false</span>);</span><br><span class="line">            sc.register(selector, SelectionKey.OP_READ);</span><br><span class="line">        &#125; <span class="keyword">else</span> <span class="keyword">if</span> (key.isReadable()) &#123;</span><br><span class="line">            <span class="type">SocketChannel</span> <span class="variable">sc</span> <span class="operator">=</span> (SocketChannel) key.channel();</span><br><span class="line">            buf.clear();</span><br><span class="line">            <span class="type">int</span> <span class="variable">n</span> <span class="operator">=</span> sc.read(buf);</span><br><span class="line">            <span class="keyword">if</span> (n == -<span class="number">1</span>) &#123; sc.close(); <span class="keyword">continue</span>; &#125;</span><br><span class="line">            buf.flip();</span><br><span class="line">            sc.write(buf);                    <span class="comment">// Echo 回去</span></span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><div class="tip warning"><p>两个经典陷阱：① <code>selectedKeys()</code> 处理完<strong>必须 <code>it.remove()</code></strong>，否则已处理的事件会被反复触发；② 注册 Channel 前<strong>必须 <code>configureBlocking(false)</code></strong>，否则抛 <code>IllegalBlockingModeException</code>。此外 JDK 早期版本在 Linux 上有 <strong>epoll 空轮询 bug</strong>（selector.select() 无就绪却立即返回，导致 CPU 100%），Netty 通过”统计空轮询次数、超阈值时重建 Selector”来规避。</p></div><h1 id="七、AIO：真正的异步-I-O"><a href="#七、AIO：真正的异步-I-O" class="headerlink" title="七、AIO：真正的异步 I/O"></a>七、AIO：真正的异步 I/O</h1><p>AIO（NIO.2，JDK 7 引入）才是真正的异步：发起读写后立即返回，<strong>内核完成数据拷贝后回调通知</strong>。</p><p>两种使用风格：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 风格一：Future —— 主动轮询结果</span></span><br><span class="line"><span class="type">AsynchronousSocketChannel</span> <span class="variable">ch</span> <span class="operator">=</span> AsynchronousSocketChannel.open();</span><br><span class="line">Future&lt;Integer&gt; f = ch.read(ByteBuffer.allocate(<span class="number">1024</span>));</span><br><span class="line"><span class="type">int</span> <span class="variable">n</span> <span class="operator">=</span> f.get();   <span class="comment">// 阻塞在这里取结果</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 风格二：CompletionHandler —— 回调，真正的异步</span></span><br><span class="line">ch.read(buffer, <span class="literal">null</span>, <span class="keyword">new</span> <span class="title class_">CompletionHandler</span>&lt;Integer, Void&gt;() &#123;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">completed</span><span class="params">(Integer result, Void attachment)</span> &#123;</span><br><span class="line">        buffer.flip();</span><br><span class="line">        <span class="comment">// 处理数据……</span></span><br><span class="line">    &#125;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">failed</span><span class="params">(Throwable exc, Void attachment)</span> &#123;</span><br><span class="line">        exc.printStackTrace();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><div class="note blue anzhiyufont anzhiyu-icon-bullhorn simple"><p>AIO 在国内为什么没火起来？<strong>Linux 原生 AIO（io_uring 之前）对网络 I/O 支持不完善</strong>，JDK 的 AIO 在 Linux 上本质是<strong>用 epoll 加线程池模拟</strong>，并没有比 NIO 更快，反而 API 更别扭。Netty 也因此早早放弃了 AIO，专注打磨 NIO 多路复用。</p></div><h1 id="八、零拷贝：把上下文切换砍到最低"><a href="#八、零拷贝：把上下文切换砍到最低" class="headerlink" title="八、零拷贝：把上下文切换砍到最低"></a>八、零拷贝：把上下文切换砍到最低</h1><p>“零拷贝”（Zero-Copy）不是真的不拷贝，而是<strong>消除 CPU 参与的数据拷贝与不必要的上下文切换</strong>。以经典的”把文件从磁盘发给网卡”为例。</p><p><strong>传统 read + write：4 次上下文切换 + 4 次拷贝</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">磁盘 →[DMA]→ 内核缓冲区 →[CPU]→ 用户缓冲区 →[CPU]→ Socket缓冲区 →[DMA]→ 网卡</span><br><span class="line">      读(切换)                        读(切换)       写(切换)      写(切换)</span><br></pre></td></tr></table></figure><p><strong>mmap + write：3 次拷贝</strong>，用户与内核共享缓冲区。</p><p><strong>sendfile / transferTo：2 次拷贝</strong>，数据不经过用户空间。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// FileChannel.transferTo 底层即 sendfile（Linux 上还可用 sendfile + DMA gather 进一步优化）</span></span><br><span class="line">in.transferTo(position, count, out);</span><br></pre></td></tr></table></figure><p>三种方式对比：</p><div class="table-container"><table><thead><tr><th style="text-align:left">方式</th><th style="text-align:center">上下文切换</th><th style="text-align:center">数据拷贝</th><th style="text-align:center">数据经用户空间</th></tr></thead><tbody><tr><td style="text-align:left">传统 read+write</td><td style="text-align:center">4</td><td style="text-align:center">4</td><td style="text-align:center">是</td></tr><tr><td style="text-align:left">mmap+write</td><td style="text-align:center">4</td><td style="text-align:center">3</td><td style="text-align:center">是（共享）</td></tr><tr><td style="text-align:left">sendfile / transferTo</td><td style="text-align:center">2</td><td style="text-align:center">2</td><td style="text-align:center">否</td></tr></tbody></table></div><div class="note orange anzhiyufont anzhiyu-icon-dengpao disabled"><p>Kafka、RocketMQ、Netty 的高吞吐都建立在零拷贝之上（Kafka 的 <code>FileRecords.writeTo</code> 用的就是 <code>transferTo</code>）。但注意：<strong>transferTo 单次传输有上限（约 2GB）</strong>，大文件必须循环调用；且它无法在传输途中修改数据，需要加密时零拷贝会失效。</p></div><h1 id="九、BIO-NIO-AIO-选型对比"><a href="#九、BIO-NIO-AIO-选型对比" class="headerlink" title="九、BIO / NIO / AIO 选型对比"></a>九、BIO / NIO / AIO 选型对比</h1><div class="table-container"><table><thead><tr><th style="text-align:left">维度</th><th style="text-align:left">BIO</th><th style="text-align:left">NIO</th><th style="text-align:left">AIO</th></tr></thead><tbody><tr><td style="text-align:left">模型</td><td style="text-align:left">同步阻塞</td><td style="text-align:left">同步非阻塞（多路复用）</td><td style="text-align:left">异步非阻塞</td></tr><tr><td style="text-align:left">编程复杂度</td><td style="text-align:left">低</td><td style="text-align:left">高（要自己管 Buffer/Selector）</td><td style="text-align:left">中（回调风格）</td></tr><tr><td style="text-align:left">连接数</td><td style="text-align:left">低（一连接一线程）</td><td style="text-align:left">高（少量线程管海量连接）</td><td style="text-align:left">高</td></tr><tr><td style="text-align:left">适用场景</td><td style="text-align:left">连接少、并发低</td><td style="text-align:left">高并发、长连接</td><td style="text-align:left">连接多且 IO 密集（当前生态偏弱）</td></tr><tr><td style="text-align:left">典型代表</td><td style="text-align:left">InputStream / 早期 Tomcat</td><td style="text-align:left">Netty、Nginx、Redis</td><td style="text-align:left">使用极少</td></tr></tbody></table></div><p>实际生产建议：<strong>高并发网络编程直接上 Netty（NIO + Reactor 模型）</strong>，不必纠结原生 NIO 的繁琐 API。</p><h1 id="十、生产避坑清单"><a href="#十、生产避坑清单" class="headerlink" title="十、生产避坑清单"></a>十、生产避坑清单</h1><ol><li><strong>文本读写用字符流</strong>，并显式指定 <code>StandardCharsets.UTF_8</code>，不要依赖平台默认编码。</li><li><strong><code>try-with-resources</code> 关闭资源</strong>，流/通道不关是句柄泄漏的头号来源。</li><li><strong>Buffer 别忘了 <code>flip()</code></strong>：”写完直接读”是新手第一个 bug，读到的永远是 0 字节。</li><li><strong><code>selectedKeys()</code> 处理完要 <code>remove()</code></strong>，否则事件被重复消费。</li><li><strong>注册 Selector 前必须 <code>configureBlocking(false)</code></strong>。</li><li><strong>堆外内存要节制</strong>：<code>allocateDirect</code> 不受 GC 管理，用 <code>Cleaner</code>/显式释放，警惕 <code>OutOfMemoryError: Direct buffer memory</code>。</li><li><strong>零拷贝注意单次 2GB 上限</strong>，大文件循环 <code>transferTo</code>；需加密/改写时不能用 sendfile。</li><li><strong>不要自己手写 NIO 服务端上生产</strong>，Reactor 模型、半包粘包、空轮询、写缓冲区水位等问题坑极深，用 Netty。</li></ol><div class="tip success"><p>一句话收尾：<strong>BIO 是”一个连接一个服务员”，NIO 是”一个服务员用叫号机管理全部窗口”，AIO 是”顾客自己办完再叫服务员”，零拷贝则是”让数据抄近道不打扰 CPU”</strong>。理解了这个类比，I/O 模型就再也不会混乱。</p></div>]]></content>
    
    
      
      
    <summary type="html">&lt;div class=&quot;tip warning&quot;&gt;&lt;p&gt;面试里问完并发、集合，下一个必考的就是 I/O 与网络编程。本文不重复线程池、《Java 并发编程核心》里的 volatile/CAS/AQS，也不展开集合源码，而是聚焦一个独立主题：&lt;strong&gt;Java 到底怎么读写数</summary>
      
    
    
    
    <category term="Java开发" scheme="http://hexo.sjava.cn/categories/Java%E5%BC%80%E5%8F%91/"/>
    
    
    <category term="Java" scheme="http://hexo.sjava.cn/tags/Java/"/>
    
    <category term="IO" scheme="http://hexo.sjava.cn/tags/IO/"/>
    
    <category term="NIO" scheme="http://hexo.sjava.cn/tags/NIO/"/>
    
    <category term="网络编程" scheme="http://hexo.sjava.cn/tags/%E7%BD%91%E7%BB%9C%E7%BC%96%E7%A8%8B/"/>
    
    <category term="零拷贝" scheme="http://hexo.sjava.cn/tags/%E9%9B%B6%E6%8B%B7%E8%B4%9D/"/>
    
  </entry>
  
  <entry>
    <title>Java 集合框架核心：ArrayList、HashMap 源码解析与选型避坑</title>
    <link href="http://hexo.sjava.cn/posts/39587.html"/>
    <id>http://hexo.sjava.cn/posts/39587.html</id>
    <published>2026-09-16T02:00:00.000Z</published>
    <updated>2026-09-16T02:00:00.000Z</updated>
    
    <content type="html"><![CDATA[<div class="tip warning"><p>集合是 Java 面试和日常开发出场率最高的知识点，没有之一。本文不聊并发容器（ConcurrentHashMap、CopyOnWriteArrayList 的细节已在《Java 并发编程核心》一文中拆解过），而是聚焦单线程场景下的集合骨架：ArrayList 为什么扩容是 1.5 倍？HashMap 为什么容量必须是 2 的幂？链表为什么树化阈值是 8？LinkedHashMap 如何三行代码实现 LRU？把这些问题一次讲透。</p></div><h1 id="一、集合框架全景图"><a href="#一、集合框架全景图" class="headerlink" title="一、集合框架全景图"></a>一、集合框架全景图</h1><p>Java 集合框架以两个顶层接口为根：<code>Collection</code>（单元素）和 <code>Map</code>（键值对）。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">Iterable</span><br><span class="line"> └── Collection</span><br><span class="line">      ├── List（有序、可重复）：ArrayList / LinkedList / Vector</span><br><span class="line">      ├── Set（不可重复）：HashSet / LinkedHashSet / TreeSet</span><br><span class="line">      └── Queue（队列）：ArrayDeque / PriorityQueue / LinkedList</span><br><span class="line">Map（独立体系）</span><br><span class="line"> ├── HashMap → LinkedHashMap（有序）</span><br><span class="line"> ├── TreeMap（排序）</span><br><span class="line"> └── Hashtable（遗留，勿用）</span><br></pre></td></tr></table></figure><div class="note orange anzhiyufont anzhiyu-icon-dengpao disabled"><p>记忆骨架：<strong>List 管顺序，Set 管唯一，Map 管映射，Queue 管出入</strong>。所有非并发集合的线程安全问题都不靠容器自己解决，需要外部同步。</p></div><h1 id="二、ArrayList：动态数组的扩容艺术"><a href="#二、ArrayList：动态数组的扩容艺术" class="headerlink" title="二、ArrayList：动态数组的扩容艺术"></a>二、ArrayList：动态数组的扩容艺术</h1><h2 id="2-1-底层结构与关键字段"><a href="#2-1-底层结构与关键字段" class="headerlink" title="2.1 底层结构与关键字段"></a>2.1 底层结构与关键字段</h2><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">ArrayList</span>&lt;E&gt; <span class="keyword">extends</span> <span class="title class_">AbstractList</span>&lt;E&gt;</span><br><span class="line">        <span class="keyword">implements</span> <span class="title class_">List</span>&lt;E&gt;, RandomAccess, Cloneable, java.io.Serializable &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">int</span> <span class="variable">DEFAULT_CAPACITY</span> <span class="operator">=</span> <span class="number">10</span>;  <span class="comment">// 默认容量</span></span><br><span class="line">    <span class="keyword">transient</span> Object[] elementData;                  <span class="comment">// 真正存数据的数组</span></span><br><span class="line">    <span class="keyword">private</span> <span class="type">int</span> size;                                <span class="comment">// 实际元素个数（≠ 数组长度）</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>注意 <code>size</code> 和 <code>elementData.length</code> 是两回事：<code>size</code> 是逻辑长度，数组长度是物理容量。无参构造时数组初始是<strong>空数组</strong>，第一次 <code>add</code> 才真正分配容量 10——这是懒加载设计。</p><h2 id="2-2-扩容机制：为什么是-1-5-倍"><a href="#2-2-扩容机制：为什么是-1-5-倍" class="headerlink" title="2.2 扩容机制：为什么是 1.5 倍"></a>2.2 扩容机制：为什么是 1.5 倍</h2><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title function_">grow</span><span class="params">(<span class="type">int</span> minCapacity)</span> &#123;</span><br><span class="line">    <span class="type">int</span> <span class="variable">oldCapacity</span> <span class="operator">=</span> elementData.length;</span><br><span class="line">    <span class="type">int</span> <span class="variable">newCapacity</span> <span class="operator">=</span> oldCapacity + (oldCapacity &gt;&gt; <span class="number">1</span>);  <span class="comment">// 1.5 倍</span></span><br><span class="line">    <span class="keyword">if</span> (newCapacity - minCapacity &lt; <span class="number">0</span>)</span><br><span class="line">        newCapacity = minCapacity;</span><br><span class="line">    <span class="keyword">if</span> (newCapacity - MAX_ARRAY_SIZE &gt; <span class="number">0</span>)</span><br><span class="line">        newCapacity = hugeCapacity(minCapacity);</span><br><span class="line">    elementData = Arrays.copyOf(elementData, newCapacity);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>扩容流程：<code>add</code> → 判断容量够不够 → 不够则 <code>grow</code> → <strong>新容量 = 旧容量 × 1.5</strong> → <code>Arrays.copyOf</code> 整体拷贝到新数组。</p><div class="table-container"><table><thead><tr><th>扩容策略</th><th>典型代表</th><th>思路</th><th>代价</th></tr></thead><tbody><tr><td>1.5 倍</td><td>ArrayList</td><td>温和增长，空间利用率高</td><td>扩容次数比 2 倍多</td></tr><tr><td>2 倍</td><td>HashMap</td><td>便于位运算取模与高低位拆分</td><td>空间浪费略多</td></tr></tbody></table></div><p>1.5 倍是空间与拷贝次数的折中：扩太慢（如 1.1 倍）拷贝频繁，扩太快（如 2 倍）浪费内存。<code>oldCapacity &gt;&gt; 1</code> 用位移代替乘法，也是源码的小巧思。</p><h2 id="2-3-modCount-与-fail-fast"><a href="#2-3-modCount-与-fail-fast" class="headerlink" title="2.3 modCount 与 fail-fast"></a>2.3 modCount 与 fail-fast</h2><p>ArrayList 内部维护一个 <code>modCount</code>（继承自 AbstractList），<strong>任何结构化修改（add/remove）都会自增</strong>。迭代器创建时会保存 <code>expectedModCount</code>，每次 <code>next()</code> 前校验：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">final</span> <span class="keyword">void</span> <span class="title function_">checkForComodification</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (modCount != expectedModCount)</span><br><span class="line">        <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">ConcurrentModificationException</span>();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><div class="note blue anzhiyufont anzhiyu-icon-circle-info disabled"><p><strong>遍历时删除元素的正确姿势</strong>：用迭代器自身的 <code>iterator.remove()</code>（会同步 expectedModCount），或 JDK 8+ 的 <code>list.removeIf(pred)</code>，或倒序 for 循环。直接在 foreach 里 <code>list.remove()</code> 必然抛 <code>ConcurrentModificationException</code>（或更糟——不抛但数据错乱）。</p></div><h1 id="三、LinkedList：双向链表的真实定位"><a href="#三、LinkedList：双向链表的真实定位" class="headerlink" title="三、LinkedList：双向链表的真实定位"></a>三、LinkedList：双向链表的真实定位</h1><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">class</span> <span class="title class_">Node</span>&lt;E&gt; &#123;</span><br><span class="line">    E item;</span><br><span class="line">    Node&lt;E&gt; next;</span><br><span class="line">    Node&lt;E&gt; prev;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>LinkedList 是<strong>双向链表</strong>，实现了 <code>List</code> 和 <code>Deque</code> 双接口。但一个经典误区是”随机插入用 LinkedList 更快”——实际上：</p><div class="table-container"><table><thead><tr><th>操作</th><th>ArrayList</th><th>LinkedList</th></tr></thead><tbody><tr><td>尾部插入</td><td>均摊 O(1)（可能触发扩容）</td><td>O(1)</td></tr><tr><td>中间插入（已定位下标）</td><td>O(n) 挪元素</td><td><strong>O(n) 定位 + O(1) 插入</strong></td></tr><tr><td>随机访问 get(i)</td><td>O(1)</td><td>O(n)，且会判断 i 在前半还是后半选择从头/尾遍历</td></tr><tr><td>内存布局</td><td>连续，CPU 缓存友好</td><td>节点离散，每节点多 2 个指针开销</td></tr></tbody></table></div><p>中间插入时 LinkedList 先要 <code>node(i)</code> 走链定位，同样是 O(n)，且 CPU 缓存对连续数组极不友好。<strong>绝大多数场景 ArrayList 都优于 LinkedList</strong>；需要双端队列时用 <code>ArrayDeque</code> 而不是 LinkedList。</p><h1 id="四、HashMap：源码的重头戏"><a href="#四、HashMap：源码的重头戏" class="headerlink" title="四、HashMap：源码的重头戏"></a>四、HashMap：源码的重头戏</h1><h2 id="4-1-put-全流程"><a href="#4-1-put-全流程" class="headerlink" title="4.1 put 全流程"></a>4.1 put 全流程</h2><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> V <span class="title function_">put</span><span class="params">(K key, V value)</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> putVal(hash(key), key, value, <span class="literal">false</span>, <span class="literal">true</span>);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">static</span> <span class="keyword">final</span> <span class="type">int</span> <span class="title function_">hash</span><span class="params">(Object key)</span> &#123;</span><br><span class="line">    <span class="type">int</span> h;</span><br><span class="line">    <span class="comment">// 扰动：高 16 位异或低 16 位</span></span><br><span class="line">    <span class="keyword">return</span> (key == <span class="literal">null</span>) ? <span class="number">0</span> : (h = key.hashCode()) ^ (h &gt;&gt;&gt; <span class="number">16</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>put 一次的完整旅程：</p><ol><li>计算 <code>hash(key)</code>（扰动函数）；</li><li>若数组为空，先 <code>resize()</code> 初始化；</li><li><code>(n - 1) &amp; hash</code> 定位桶下标——<strong>等价于 hash % n，但要求 n 是 2 的幂</strong>，位运算比取模快一个量级；</li><li>桶为空 → 直接新建节点；</li><li>桶非空 → 首节点 key 相同则覆盖；是 TreeNode 走红黑树插入；否则遍历链表尾插，长度达 8 且数组容量 ≥ 64 时树化；</li><li><code>++size &gt; threshold</code> → <code>resize()</code> 扩容。</li></ol><p><strong>扰动函数的意义</strong>：直接用 hashCode 低位做下标，若 hashCode 只在高位有差异（如某些对象地址分布），桶会大量碰撞。<code>h ^ (h &gt;&gt;&gt; 16)</code> 把高 16 位”混合”进低位，让散列更均匀——这是用一次异或换更低碰撞率的划算买卖。</p><h2 id="4-2-为什么容量必须是-2-的幂"><a href="#4-2-为什么容量必须是-2-的幂" class="headerlink" title="4.2 为什么容量必须是 2 的幂"></a>4.2 为什么容量必须是 2 的幂</h2><p><code>length - 1</code> 的二进制形如 <code>0000 1111</code>，与 hash 相与后下标均匀落在 <code>[0, n-1]</code>。若长度不是 2 的幂，<code>n-1</code> 的二进制出现 0 位，某些下标<strong>永远取不到</strong>，散列性直接劣化。这也是为什么 <code>tableSizeFor</code> 会把任意初始容量向上取整到最近的 2 的幂：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">static</span> <span class="keyword">final</span> <span class="type">int</span> <span class="title function_">tableSizeFor</span><span class="params">(<span class="type">int</span> cap)</span> &#123;</span><br><span class="line">    <span class="type">int</span> <span class="variable">n</span> <span class="operator">=</span> -<span class="number">1</span> &gt;&gt;&gt; Integer.numberOfLeadingZeros(cap - <span class="number">1</span>);</span><br><span class="line">    <span class="keyword">return</span> (n &lt; <span class="number">0</span>) ? <span class="number">1</span> : (n &gt;= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + <span class="number">1</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>指定 <code>new HashMap&lt;&gt;(17)</code>，实际容量会是 32。</p><h2 id="4-3-树化：为什么阈值是-8-和-6"><a href="#4-3-树化：为什么阈值是-8-和-6" class="headerlink" title="4.3 树化：为什么阈值是 8 和 6"></a>4.3 树化：为什么阈值是 8 和 6</h2><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">static</span> <span class="keyword">final</span> <span class="type">int</span> <span class="variable">TREEIFY_THRESHOLD</span> <span class="operator">=</span> <span class="number">8</span>;     <span class="comment">// 链表 → 红黑树</span></span><br><span class="line"><span class="keyword">static</span> <span class="keyword">final</span> <span class="type">int</span> <span class="variable">UNTREEIFY_THRESHOLD</span> <span class="operator">=</span> <span class="number">6</span>;   <span class="comment">// 红黑树 → 链表</span></span><br><span class="line"><span class="keyword">static</span> <span class="keyword">final</span> <span class="type">int</span> <span class="variable">MIN_TREEIFY_CAPACITY</span> <span class="operator">=</span> <span class="number">64</span>; <span class="comment">// 树化前提：数组容量达标</span></span><br></pre></td></tr></table></figure><p>两个反直觉的点：</p><ul><li><strong>树化还需要数组容量 ≥ 64</strong>。容量小时碰撞多是因为”桶太少”而不是”散列差”，此时优先扩容而不是树化。</li><li>阈值 8 来源于<strong>泊松分布</strong>：在理想散列下，单桶链表长度达到 8 的概率约为千万分之六（<code>0.00000006</code>）。正常使用几乎触发不了树化，树化只是应对散列恶意攻击（哈希碰撞 DoS）的兜底。</li><li>退化阈值 6 而不是 7：留出缓冲带，防止节点数在 7、8 之间抖动导致链表/树反复转换。</li></ul><h2 id="4-4-扩容：JDK-8-的高低位拆分"><a href="#4-4-扩容：JDK-8-的高低位拆分" class="headerlink" title="4.4 扩容：JDK 8 的高低位拆分"></a>4.4 扩容：JDK 8 的高低位拆分</h2><p>扩容把数组翻倍，节点要么留在原下标 <code>i</code>，要么去 <code>i + oldCap</code>。<strong>判断依据只需看 hash 在 oldCap 对应位上是 0 还是 1</strong>：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> ((e.hash &amp; oldCap) == <span class="number">0</span>) &#123;</span><br><span class="line">    <span class="comment">// 低位链：留在原位置</span></span><br><span class="line">    <span class="keyword">if</span> (loTail == <span class="literal">null</span>) loHead = e; <span class="keyword">else</span> loTail.next = e;</span><br><span class="line">    loTail = e;</span><br><span class="line">&#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    <span class="comment">// 高位链：移动到 原下标 + oldCap</span></span><br><span class="line">    <span class="keyword">if</span> (hiTail == <span class="literal">null</span>) hiHead = e; <span class="keyword">else</span> hiTail.next = e;</span><br><span class="line">    hiTail = e;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>JDK 8 不需要重新计算每个元素的 hash，一次与运算完成分流，且链表保持原相对顺序——这是 JDK 8 相对 JDK 7 最大的性能改进之一。</p><h2 id="4-5-JDK-7-vs-JDK-8-关键差异"><a href="#4-5-JDK-7-vs-JDK-8-关键差异" class="headerlink" title="4.5 JDK 7 vs JDK 8 关键差异"></a>4.5 JDK 7 vs JDK 8 关键差异</h2><div class="table-container"><table><thead><tr><th>维度</th><th>JDK 7</th><th>JDK 8</th></tr></thead><tbody><tr><td>结构</td><td>数组 + 链表</td><td>数组 + 链表 + 红黑树</td></tr><tr><td>插入方式</td><td>头插法</td><td>尾插法</td></tr><tr><td>扩容后顺序</td><td>链表逆序</td><td>保持原序</td></tr><tr><td>并发扩容风险</td><td>可能形成环形链表，get 时 CPU 100%</td><td>环形链问题消除，但仍非线程安全</td></tr><tr><td>hash 计算</td><td>4 次扰动</td><td>1 次扰动（h ^ h&gt;&gt;&gt;16）</td></tr></tbody></table></div><div class="note red anzhiyufont anzhiyu-icon-triangle-exclamation disabled"><p>JDK 8 修复了头插法环形链表，但 <strong>HashMap 从来就不是线程安全的</strong>：并发 put 仍会丢数据（两线程同时定位到同一空桶互相覆盖）。并发场景请直接使用 ConcurrentHashMap——其分段锁/CAS+synchronized 的实现已在《Java 并发编程核心》详解，此处不重复。</p></div><h1 id="五、LinkedHashMap：三行代码的-LRU"><a href="#五、LinkedHashMap：三行代码的-LRU" class="headerlink" title="五、LinkedHashMap：三行代码的 LRU"></a>五、LinkedHashMap：三行代码的 LRU</h1><p>LinkedHashMap 在 HashMap 的 Node 上加了 <code>before/after</code> 双指针，维护一条贯穿所有节点的双向链表：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">static</span> <span class="keyword">class</span> <span class="title class_">Entry</span>&lt;K,V&gt; <span class="keyword">extends</span> <span class="title class_">HashMap</span>.Node&lt;K,V&gt; &#123;</span><br><span class="line">    Entry&lt;K,V&gt; before, after;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li><code>accessOrder = false</code>（默认）：按插入顺序迭代；</li><li><code>accessOrder = true</code>：按<strong>访问顺序</strong>迭代，get 一次就把节点挪到链表尾。</li></ul><p>配合 <code>removeEldestEntry</code>，一个 20 行的 LRU 缓存就出来了：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">LruCache</span>&lt;K, V&gt; <span class="keyword">extends</span> <span class="title class_">LinkedHashMap</span>&lt;K, V&gt; &#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> <span class="type">int</span> maxEntries;</span><br><span class="line"></span><br><span class="line">    LruCache(<span class="type">int</span> maxEntries) &#123;</span><br><span class="line">        <span class="built_in">super</span>(<span class="number">16</span>, <span class="number">0.75f</span>, <span class="literal">true</span>);          <span class="comment">// accessOrder = true 是灵魂</span></span><br><span class="line">        <span class="built_in">this</span>.maxEntries = maxEntries;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">protected</span> <span class="type">boolean</span> <span class="title function_">removeEldestEntry</span><span class="params">(Map.Entry&lt;K, V&gt; eldest)</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> size() &gt; maxEntries;      <span class="comment">// 超容量淘汰最久未访问的队头</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><div class="note orange anzhiyufont anzhiyu-icon-dengpao disabled"><p>面试常问的”手写 LRU”，标准答案就是这个类。注意 <code>accessOrder = true</code> 时 get/put 都是写操作，多线程使用同样要外部加锁。</p></div><h1 id="六、HashSet-与-TreeMap-的底层真相"><a href="#六、HashSet-与-TreeMap-的底层真相" class="headerlink" title="六、HashSet 与 TreeMap 的底层真相"></a>六、HashSet 与 TreeMap 的底层真相</h1><p><strong>HashSet 就是包装了 HashMap</strong>：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">HashSet</span>&lt;E&gt; ... &#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">transient</span> HashMap&lt;E,Object&gt; map;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">Object</span> <span class="variable">PRESENT</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Object</span>();  <span class="comment">// 共享的 value 占位符</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="type">boolean</span> <span class="title function_">add</span><span class="params">(E e)</span> &#123; <span class="keyword">return</span> map.put(e, PRESENT) == <span class="literal">null</span>; &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>所有元素作为 key 存入，value 用同一个哑对象填充。因此 <code>LinkedHashSet</code> = LinkedHashMap 的 key 视图，<code>TreeSet</code> = TreeMap 的 key 视图——一套代码三个容器。</p><p>TreeMap 底层是<strong>红黑树</strong>，按 key 排序，提供 <code>firstKey/ceilingKey/floorKey/subMap</code> 等范围 API。使用它要求 key 可排序：实现 <code>Comparable</code> 或构造时传入 <code>Comparator</code>，否则直接 <code>ClassCastException</code>。</p><h1 id="七、集合选型速查表"><a href="#七、集合选型速查表" class="headerlink" title="七、集合选型速查表"></a>七、集合选型速查表</h1><div class="table-container"><table><thead><tr><th>需求场景</th><th>推荐</th><th>理由</th></tr></thead><tbody><tr><td>随机访问、尾部增删</td><td>ArrayList</td><td>下标 O(1)，缓存友好</td></tr><tr><td>频繁头部增删 / 双端队列</td><td>ArrayDeque</td><td>比 LinkedList 更省内存更快</td></tr><tr><td>去重、无序</td><td>HashSet</td><td>HashMap 的 key 视图</td></tr><tr><td>去重、保持插入顺序</td><td>LinkedHashSet</td><td>多维护一条链表</td></tr><tr><td>排序、范围查询</td><td>TreeMap / TreeSet</td><td>红黑树，O(log n)</td></tr><tr><td>键值缓存 + 淘汰策略</td><td>LinkedHashMap(accessOrder=true)</td><td>天然 LRU</td></tr><tr><td>键值映射（默认选择）</td><td>HashMap</td><td>综合性能最优</td></tr><tr><td>并发场景</td><td>ConcurrentHashMap / CopyOnWriteArrayList</td><td>见并发篇，勿用 Hashtable</td></tr></tbody></table></div><h1 id="八、高频避坑清单"><a href="#八、高频避坑清单" class="headerlink" title="八、高频避坑清单"></a>八、高频避坑清单</h1><details class="folding-tag" ><summary> 点开查看 8 条实战避坑 </summary>              <div class='content'>              <ol><li><strong>预估容量</strong>：<code>new ArrayList&lt;&gt;(1000)</code>、<code>new HashMap&lt;&gt;(1024)</code> 能避免反复扩容拷贝；HashMap 注意传入的是初始容量，内部会自动取 2 的幂，想装 1024 条不扩容应传 2048（阈值 = 容量 × 0.75）。</li><li><strong>foreach 中增删</strong>：会抛 ConcurrentModificationException，用 <code>removeIf</code> 或迭代器 <code>remove</code>。</li><li><strong>自定义对象做 key</strong>：必须同时重写 <code>equals</code> 和 <code>hashCode</code>，只重写一个会导致”存进去取不出来”。</li><li><strong>Arrays.asList 的坑</strong>：返回的是定长视图，add/remove 会抛 UnsupportedOperationException；转换后需要可变列表用 <code>new ArrayList&lt;&gt;(Arrays.asList(...))</code>。</li><li><strong>subList 是视图</strong>：<code>list.subList(0,5)</code> 与原列表共享数据，结构性修改原列表后 subList 直接失效。</li><li><strong>负载因子不要乱调</strong>：默认 0.75 是时间与空间的黄金平衡点，调大省内存但碰撞增多、查找变慢。</li><li><strong>key 用包装类优先</strong>：Integer、String 等 hashCode 质量高且不可变；可变对象做 key 后改字段，永远取不回 value。</li><li><strong>Collections.unmodifiableList 只是视图</strong>：底层列表变了它跟着变，真正不可变集合用 JDK 9+ 的 <code>List.of / Map.of</code>。</li></ol>              </div>            </details><hr><p><strong>参考阅读</strong>：并发容器（ConcurrentHashMap、CopyOnWriteArrayList、BlockingQueue）的原理与 JDK7/8 差异，见本站《Java 并发编程核心：volatile、synchronized、CAS 与 AQS 原理解析》；线程池的参数与执行流程见《Java 线程池详解》。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;div class=&quot;tip warning&quot;&gt;&lt;p&gt;集合是 Java 面试和日常开发出场率最高的知识点，没有之一。本文不聊并发容器（ConcurrentHashMap、CopyOnWriteArrayList 的细节已在《Java 并发编程核心》一文中拆解过），而是聚焦单线程</summary>
      
    
    
    
    <category term="Java开发" scheme="http://hexo.sjava.cn/categories/Java%E5%BC%80%E5%8F%91/"/>
    
    
    <category term="Java" scheme="http://hexo.sjava.cn/tags/Java/"/>
    
    <category term="集合框架" scheme="http://hexo.sjava.cn/tags/%E9%9B%86%E5%90%88%E6%A1%86%E6%9E%B6/"/>
    
    <category term="HashMap" scheme="http://hexo.sjava.cn/tags/HashMap/"/>
    
    <category term="ArrayList" scheme="http://hexo.sjava.cn/tags/ArrayList/"/>
    
    <category term="源码" scheme="http://hexo.sjava.cn/tags/%E6%BA%90%E7%A0%81/"/>
    
  </entry>
  
  <entry>
    <title>Vue 3 响应式原理与 Composition API 实战：Proxy、依赖收集与性能优化</title>
    <link href="http://hexo.sjava.cn/posts/52831.html"/>
    <id>http://hexo.sjava.cn/posts/52831.html</id>
    <published>2026-09-15T14:06:00.000Z</published>
    <updated>2026-09-15T14:06:00.000Z</updated>
    
    <content type="html"><![CDATA[<div class="tip warning"><p>Vue 2 用 <code>Object.defineProperty</code> 做响应式，遇到”新增属性不更新””数组索引改了没反应”只能靠 <code>$set</code> 打补丁。Vue 3 把底层换成了 <code>Proxy</code>，从根上解决了这些坑。本文把响应式的”任督二脉”——依赖收集与派发更新——彻底拆开，再看 Composition API 怎么把逻辑组织得更干净。</p></div><h1 id="一、为什么-Vue-2-的响应式不够用"><a href="#一、为什么-Vue-2-的响应式不够用" class="headerlink" title="一、为什么 Vue 2 的响应式不够用"></a>一、为什么 Vue 2 的响应式不够用</h1><p>Vue 2 在初始化时递归遍历 <code>data</code>，用 <code>Object.defineProperty</code> 给每个属性加 <code>getter/setter</code>。这带来三个绕不开的硬伤：</p><div class="table-container"><table><thead><tr><th>痛点</th><th>现象</th><th>Vue 2 的妥协方案</th></tr></thead><tbody><tr><td>新增属性无响应</td><td><code>this.obj.newKey = 1</code> 视图不更新</td><td>必须用 <code>this.$set(obj, &#39;newKey&#39;, 1)</code></td></tr><tr><td>删除属性无响应</td><td><code>delete this.obj.key</code> 视图不更新</td><td>必须用 <code>this.$delete(obj, &#39;key&#39;)</code></td></tr><tr><td>数组索引/length 监听不到</td><td><code>arr[0] = x</code> / <code>arr.length = 0</code> 不触发</td><td>重写数组 7 个方法（push/pop/…）打补丁</td></tr></tbody></table></div><div class="note info flat"><p><code>Object.defineProperty</code> 只能劫持<strong>已经存在的属性</strong>，无法感知”属性被新增或删除”，这是语言层面决定的死穴。Vue 3 的 <code>Proxy</code> 能拦截 <code>get/set/deleteProperty/has/ownKeys</code> 等十余种操作，从根本上覆盖这些场景。</p></div><h1 id="二、Vue-3-响应式核心：Proxy-Reflect"><a href="#二、Vue-3-响应式核心：Proxy-Reflect" class="headerlink" title="二、Vue 3 响应式核心：Proxy + Reflect"></a>二、Vue 3 响应式核心：Proxy + Reflect</h1><p><code>reactive()</code> 的本质，是给目标对象包一层 <code>Proxy</code>，在 <code>get</code> 时收集依赖、在 <code>set</code> 时触发更新。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">reactive</span>(<span class="params">target</span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">Proxy</span>(target, &#123;</span><br><span class="line">    <span class="title function_">get</span>(<span class="params">obj, key, receiver</span>) &#123;</span><br><span class="line">      <span class="comment">// 依赖收集：谁在读这个属性，就记住谁</span></span><br><span class="line">      <span class="title function_">track</span>(obj, key)</span><br><span class="line">      <span class="keyword">const</span> result = <span class="title class_">Reflect</span>.<span class="title function_">get</span>(obj, key, receiver)</span><br><span class="line">      <span class="comment">// 嵌套对象懒代理，只有被访问才继续 reactive</span></span><br><span class="line">      <span class="keyword">return</span> <span class="keyword">typeof</span> result === <span class="string">&#x27;object&#x27;</span> &amp;&amp; result !== <span class="literal">null</span></span><br><span class="line">        ? <span class="title function_">reactive</span>(result)</span><br><span class="line">        : result</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="title function_">set</span>(<span class="params">obj, key, value, receiver</span>) &#123;</span><br><span class="line">      <span class="keyword">const</span> oldVal = obj[key]</span><br><span class="line">      <span class="keyword">const</span> result = <span class="title class_">Reflect</span>.<span class="title function_">set</span>(obj, key, value, receiver)</span><br><span class="line">      <span class="keyword">if</span> (oldVal !== value) &#123;</span><br><span class="line">        <span class="comment">// 派发更新：属性变了，通知所有依赖它的副作用重跑</span></span><br><span class="line">        <span class="title function_">trigger</span>(obj, key)</span><br><span class="line">      &#125;</span><br><span class="line">      <span class="keyword">return</span> result</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="title function_">deleteProperty</span>(<span class="params">obj, key</span>) &#123;</span><br><span class="line">      <span class="keyword">const</span> had = key <span class="keyword">in</span> obj</span><br><span class="line">      <span class="keyword">const</span> result = <span class="title class_">Reflect</span>.<span class="title function_">deleteProperty</span>(obj, key)</span><br><span class="line">      <span class="keyword">if</span> (had) <span class="title function_">trigger</span>(obj, key) <span class="comment">// 删除属性也能触发更新</span></span><br><span class="line">      <span class="keyword">return</span> result</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>Reflect</code> 的作用有两个：一是保证 <code>this</code> 指向正确（配合 <code>receiver</code> 处理继承链上的 getter），二是返回 boolean 表示操作是否成功。</p><div class="tip "><p>为什么必须配合 <code>receiver</code>？当对象存在继承、且父类 getter 里访问了 <code>this</code> 时，没有 <code>receiver</code> 会让 <code>this</code> 指向原始对象而非 Proxy，导致依赖收集遗漏。这是手写响应式最容易踩的坑。</p></div><h1 id="三、依赖收集：effect-track-trigger"><a href="#三、依赖收集：effect-track-trigger" class="headerlink" title="三、依赖收集：effect / track / trigger"></a>三、依赖收集：effect / track / trigger</h1><p>Vue 3 用一个三级数据结构记录”谁依赖了谁”：</p><div class="table-container"><table><thead><tr><th>结构</th><th>类型</th><th>作用</th></tr></thead><tbody><tr><td><code>targetMap</code></td><td><code>WeakMap</code></td><td>key 是响应式对象，value 是它的依赖表</td></tr><tr><td><code>depsMap</code></td><td><code>Map</code></td><td>key 是属性名，value 是该属性的依赖集合</td></tr><tr><td><code>dep</code></td><td><code>Set</code></td><td>存放所有依赖该属性的副作用函数（effect）</td></tr></tbody></table></div><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> activeEffect = <span class="literal">null</span> <span class="comment">// 当前正在执行的副作用</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">effect</span>(<span class="params">fn</span>) &#123;</span><br><span class="line">  activeEffect = fn</span><br><span class="line">  <span class="title function_">fn</span>() <span class="comment">// 执行时会触发 get → track 收集 activeEffect</span></span><br><span class="line">  activeEffect = <span class="literal">null</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">track</span>(<span class="params">target, key</span>) &#123;</span><br><span class="line">  <span class="keyword">if</span> (!activeEffect) <span class="keyword">return</span></span><br><span class="line">  <span class="keyword">const</span> depsMap = targetMap.<span class="title function_">get</span>(target) ?? targetMap.<span class="title function_">set</span>(target, <span class="keyword">new</span> <span class="title class_">Map</span>()).<span class="title function_">get</span>(target)</span><br><span class="line">  <span class="keyword">const</span> dep = depsMap.<span class="title function_">get</span>(key) ?? depsMap.<span class="title function_">set</span>(key, <span class="keyword">new</span> <span class="title class_">Set</span>()).<span class="title function_">get</span>(key)</span><br><span class="line">  dep.<span class="title function_">add</span>(activeEffect)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">trigger</span>(<span class="params">target, key</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> dep = targetMap.<span class="title function_">get</span>(target)?.<span class="title function_">get</span>(key)</span><br><span class="line">  dep?.<span class="title function_">forEach</span>(<span class="function"><span class="params">eff</span> =&gt;</span> <span class="title function_">eff</span>()) <span class="comment">// 重新执行所有副作用</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><div class="note success flat"><p><code>WeakMap</code> 用对象作 key 且不影响垃圾回收——当响应式对象不再被引用时，它的依赖表会随之被回收，避免内存泄漏。这是 Vue 3 对比手写实现的一个关键细节。</p></div><h1 id="四、ref-vs-reactive：到底用哪个"><a href="#四、ref-vs-reactive：到底用哪个" class="headerlink" title="四、ref vs reactive：到底用哪个"></a>四、ref vs reactive：到底用哪个</h1><p><code>reactive</code> 只能代理<strong>对象</strong>，基本类型（number/string/boolean）没法直接 <code>Proxy</code>。于是 Vue 3 引入了 <code>ref</code>：用一个带 <code>.value</code> 的对象把基本类型包起来。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">RefImpl</span> &#123;</span><br><span class="line">  <span class="title function_">constructor</span>(<span class="params">value</span>) &#123;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">__v_isRef</span> = <span class="literal">true</span></span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">_value</span> = value</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">get</span> <span class="title function_">value</span>() &#123; <span class="title function_">track</span>(<span class="variable language_">this</span>, <span class="string">&#x27;value&#x27;</span>); <span class="keyword">return</span> <span class="variable language_">this</span>.<span class="property">_value</span> &#125;</span><br><span class="line">  <span class="keyword">set</span> <span class="title function_">value</span>(<span class="params">newVal</span>) &#123; <span class="variable language_">this</span>.<span class="property">_value</span> = newVal; <span class="title function_">trigger</span>(<span class="variable language_">this</span>, <span class="string">&#x27;value&#x27;</span>) &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><div class="table-container"><table><thead><tr><th>维度</th><th><code>ref</code></th><th><code>reactive</code></th></tr></thead><tbody><tr><td>适用类型</td><td>基本类型 + 对象</td><td>仅对象/数组</td></tr><tr><td>访问方式</td><td>脚本里 <code>.value</code>，模板里自动解包</td><td>直接访问</td></tr><tr><td>重新赋值</td><td><code>count.value = 2</code> 安全</td><td><code>state = newObj</code> 会丢失响应性</td></tr><tr><td>解构</td><td>直接解构丢失响应，需 <code>toRefs</code></td><td><code>toRefs(state)</code> 保持</td></tr></tbody></table></div><div class="tip warning"><p><strong>解构会丢失响应性</strong>。直接 <code>const { name } = reactiveObj</code> 拿到的只是那一刻的值；要用 <code>const { name } = toRefs(reactiveObj)</code>，<code>toRefs</code> 会把每个属性包成 <code>ref</code>，从而继续追踪。</p></div><p>另外还有几个变体：</p><ul><li><code>shallowRef</code> / <code>shallowReactive</code>：只做浅层响应式，深层变化不触发，适合大对象性能优化。</li><li><code>readonly</code>：深层只读代理，常用于把状态暴露给子组件或外部，防止被意外修改。</li></ul><h1 id="五、computed：带缓存的派生状态"><a href="#五、computed：带缓存的派生状态" class="headerlink" title="五、computed：带缓存的派生状态"></a>五、computed：带缓存的派生状态</h1><p><code>computed</code> 不是简单包一层 <code>effect</code>，它的核心是两个标志：<strong><code>lazy</code>（默认不立即执行）</strong> 和 <strong><code>dirty</code>（脏检查）</strong>。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">computed</span>(<span class="params">getter</span>) &#123;</span><br><span class="line">  <span class="keyword">let</span> value, dirty = <span class="literal">true</span></span><br><span class="line">  <span class="keyword">const</span> runner = <span class="title function_">effect</span>(getter, &#123; <span class="attr">lazy</span>: <span class="literal">true</span> &#125;)</span><br><span class="line">  <span class="keyword">return</span> &#123;</span><br><span class="line">    <span class="keyword">get</span> <span class="title function_">value</span>() &#123;</span><br><span class="line">      <span class="keyword">if</span> (dirty) &#123; value = <span class="title function_">runner</span>(); dirty = <span class="literal">false</span> &#125; <span class="comment">// 依赖没变就返回缓存</span></span><br><span class="line">      <span class="title function_">track</span>(<span class="variable language_">this</span>, <span class="string">&#x27;value&#x27;</span>)</span><br><span class="line">      <span class="keyword">return</span> value</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="keyword">set</span> <span class="title function_">value</span>(<span class="params">newVal</span>) &#123; <span class="comment">/* 有 setter 时支持写回 */</span> &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>只有依赖的响应式数据变化、<code>dirty</code> 被 <code>trigger</code> 置回 <code>true</code> 时，下次读取才会重新计算。这避免了模板里重复调用带来的无谓开销。</p><h1 id="六、watch-与-watchEffect-的差异"><a href="#六、watch-与-watchEffect-的差异" class="headerlink" title="六、watch 与 watchEffect 的差异"></a>六、watch 与 watchEffect 的差异</h1><p>两者都基于 <code>effect</code>，但触发时机和写法不同：</p><div class="table-container"><table><thead><tr><th>对比项</th><th><code>watch</code></th><th><code>watchEffect</code></th></tr></thead><tbody><tr><td>依赖来源</td><td>显式指定（第一个参数）</td><td>函数体内用到谁就追踪谁</td></tr><tr><td>触发时机</td><td>默认 <code>pre</code>（组件更新前），可配 <code>flush</code></td><td>同上，但首次立即执行</td></tr><tr><td>拿到旧值</td><td>有 <code>oldVal</code> / <code>newVal</code></td><td>无旧值</td></tr><tr><td>典型场景</td><td>监听某个具体状态做异步/副作用</td><td>依赖收集式地自动追踪</td></tr></tbody></table></div><div class="note info flat"><p><code>watch</code> 的 <code>flush: &#39;post&#39;</code> 能保证回调在 DOM 更新后执行，适合”数据变了再操作 DOM”；<code>flush: &#39;sync&#39;</code> 则同步触发，谨慎使用，容易引发性能问题。</p></div><h1 id="七、Composition-API-实战"><a href="#七、Composition-API-实战" class="headerlink" title="七、Composition API 实战"></a>七、Composition API 实战</h1><p>Composition API 把”按逻辑关注点”组织代码变成可能，而不是 Options API 的”按选项类型分散”。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; ref, onMounted, computed &#125; <span class="keyword">from</span> <span class="string">&#x27;vue&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">function</span> <span class="title function_">useCounter</span>(<span class="params">initial = <span class="number">0</span></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> count = <span class="title function_">ref</span>(initial)</span><br><span class="line">  <span class="keyword">const</span> double = <span class="title function_">computed</span>(<span class="function">() =&gt;</span> count.<span class="property">value</span> * <span class="number">2</span>)</span><br><span class="line">  <span class="keyword">const</span> <span class="title function_">increment</span> = (<span class="params"></span>) =&gt; count.<span class="property">value</span>++</span><br><span class="line">  <span class="title function_">onMounted</span>(<span class="function">() =&gt;</span> <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&#x27;counter mounted:&#x27;</span>, count.<span class="property">value</span>))</span><br><span class="line">  <span class="keyword">return</span> &#123; count, double, increment &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 组件中使用</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> &#123;</span><br><span class="line">  <span class="title function_">setup</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="keyword">const</span> &#123; count, double, increment &#125; = <span class="title function_">useCounter</span>(<span class="number">10</span>)</span><br><span class="line">    <span class="keyword">return</span> &#123; count, double, increment &#125; <span class="comment">// 模板直接可用</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>把状态、计算、方法、生命周期收敛进 <code>useCounter</code> 这样的<strong>组合函数（composable）</strong>，多个组件之间可以零成本复用，逻辑内聚、可读性远胜 Options API 的 <code>data/methods/computed/mounted</code> 四分五裂。</p><h1 id="八、编译期-运行时的双重性能优化"><a href="#八、编译期-运行时的双重性能优化" class="headerlink" title="八、编译期 + 运行时的双重性能优化"></a>八、编译期 + 运行时的双重性能优化</h1><p>Vue 3 快，不只是因为 Proxy，更因为<strong>编译期</strong>帮你做了大量优化：</p><div class="table-container"><table><thead><tr><th>优化手段</th><th>说明</th></tr></thead><tbody><tr><td>静态提升（hoistStatic）</td><td>不变的节点/属性提升到 <code>render</code> 外，只创建一次</td></tr><tr><td><code>PatchFlag</code></td><td>给动态节点打标记，diff 时只比对有变化的维度（class/text/props）</td></tr><tr><td><code>Block Tree</code></td><td>以结构稳定的”块”为单位收集动态节点，跳过静态子树</td></tr><tr><td>缓存事件处理函数</td><td><code>@click=&quot;foo&quot;</code> 编译为缓存引用，避免每次渲染新建函数</td></tr><tr><td><code>v-memo</code></td><td>手动声明依赖数组，数组不变则跳过该子树更新</td></tr></tbody></table></div><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">&lt;!-- v-memo：列表项成本较高时，只有 id/selected 变才更新 --&gt;</span><br><span class="line">&lt;div v-for=&quot;item in list&quot; :key=&quot;item.id&quot; v-memo=&quot;[item.id, item.selected]&quot;&gt;</span><br><span class="line">  &#123;&#123; item.label &#125;&#125;</span><br><span class="line">&lt;/div&gt;</span><br></pre></td></tr></table></figure><h1 id="九、生产避坑清单"><a href="#九、生产避坑清单" class="headerlink" title="九、生产避坑清单"></a>九、生产避坑清单</h1><ol><li><strong>大对象用 <code>shallowRef</code></strong>：深层不常变、只在顶层替换时，避免深层递归代理带来的初始化开销。</li><li><strong>不要在 <code>setup</code> 顶层写同步重逻辑</strong>：耗时操作放进 <code>onMounted</code> 或 <code>watch</code> 回调。</li><li><strong><code>reactive</code> 不要整体重新赋值</strong>：需要替换请用 <code>ref</code> 包对象，或 <code>Object.assign(state, newObj)</code>。</li><li><strong>解构必用 <code>toRefs</code></strong>：传给子组件的 props/state 解构后仍是响应式的。</li><li><strong><code>computed</code> 里不要有副作用</strong>：它可能被多次求值或缓存跳过，副作用应放在 <code>watch</code>。</li><li><strong><code>watch</code> 监听对象用深层 <code>&#123; deep: true &#125;</code></strong>，但深层监听成本高，尽量监听具体路径 <code>() =&gt; state.a.b</code>。</li><li><strong><code>v-for</code> 务必带稳定 <code>key</code></strong>，且避免用 <code>index</code> 当 key 造成错位复用。</li></ol><div class="tip "><p>一句话总结：Vue 3 响应式 = <code>Proxy</code> 拦截读写 + <code>track/trigger</code> 三级依赖表；组织逻辑用 <code>Composition API</code> 的组合函数；性能靠编译期 <code>PatchFlag</code>/<code>Block</code> 与运行期 <code>v-memo</code> 双管齐下。理解了这条主线，绝大多数”为什么不更新””为什么这么慢”的问题都能定位到根因。</p></div>]]></content>
    
    
      
      
    <summary type="html">&lt;div class=&quot;tip warning&quot;&gt;&lt;p&gt;Vue 2 用 &lt;code&gt;Object.defineProperty&lt;/code&gt; 做响应式，遇到”新增属性不更新””数组索引改了没反应”只能靠 &lt;code&gt;$set&lt;/code&gt; 打补丁。Vue 3 把底层换成了 &lt;co</summary>
      
    
    
    
    <category term="前端开发" scheme="http://hexo.sjava.cn/categories/%E5%89%8D%E7%AB%AF%E5%BC%80%E5%8F%91/"/>
    
    
    <category term="性能优化" scheme="http://hexo.sjava.cn/tags/%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/"/>
    
    <category term="前端" scheme="http://hexo.sjava.cn/tags/%E5%89%8D%E7%AB%AF/"/>
    
    <category term="Vue3" scheme="http://hexo.sjava.cn/tags/Vue3/"/>
    
    <category term="响应式" scheme="http://hexo.sjava.cn/tags/%E5%93%8D%E5%BA%94%E5%BC%8F/"/>
    
    <category term="Composition API" scheme="http://hexo.sjava.cn/tags/Composition-API/"/>
    
  </entry>
  
  <entry>
    <title>Kafka 核心原理与实战：分区、副本、消费组与精确一次语义</title>
    <link href="http://hexo.sjava.cn/posts/91327.html"/>
    <id>http://hexo.sjava.cn/posts/91327.html</id>
    <published>2026-09-14T02:08:00.000Z</published>
    <updated>2026-09-14T02:08:00.000Z</updated>
    
    <content type="html"><![CDATA[<div class="tip warning"><p>很多系统一开始用”直接调接口”把服务串起来，随着流量和依赖变多，耦合、峰值、故障扩散会接踵而至。Kafka 不是单纯的”消息中间件”，它更像一套<strong>分布式的提交日志系统</strong>。本文从原理到实战，把 Kafka 最容易被问、最容易踩坑的部分一次讲透。</p></div><h1 id="一、为什么需要-Kafka：消息队列解决了什么"><a href="#一、为什么需要-Kafka：消息队列解决了什么" class="headerlink" title="一、为什么需要 Kafka：消息队列解决了什么"></a>一、为什么需要 Kafka：消息队列解决了什么</h1><p>在一个典型后端系统里，订单服务可能要同时通知库存、积分、风控、物流。如果全部用同步 RPC 调用，任何一个下游抖动都会拖垮下单链路。消息队列的价值可以用一张表概括：</p><div class="table-container"><table><thead><tr><th>痛点</th><th>同步直连</th><th>引入消息队列（Kafka）</th></tr></thead><tbody><tr><td>耦合</td><td>A 直接依赖 B/C/D，改一个要动一片</td><td>只依赖 Topic，下游可随时增减</td></tr><tr><td>峰值</td><td>流量洪峰直接打垮数据库</td><td>broker 缓冲，消费者按能力消费（削峰填谷）</td></tr><tr><td>失败扩散</td><td>库存超时导致下单失败</td><td>消息持久化，下游恢复后继续消费</td></tr><tr><td>能力扩展</td><td>新增消费者要改调用方</td><td>直接加 Consumer 即可扩容</td></tr></tbody></table></div><div class="note orange anzhiyufont anzhiyu-icon-dengpao disabled"><p>Kafka 的”本领”不止异步解耦：它还天然支持<strong>流处理</strong>（Kafka Streams / Flink 接入）、<strong>事件溯源</strong>、<strong>日志聚合</strong>和<strong>多副本容灾</strong>。当你需要的不是”发个通知”，而是”一份数据被多个系统以不同速度、不同语义反复消费”时，Kafka 的优势才真正显现。</p></div><h1 id="二、核心概念速览"><a href="#二、核心概念速览" class="headerlink" title="二、核心概念速览"></a>二、核心概念速览</h1><p>理解 Kafka 的第一步是把名词对齐，否则后面全是黑话：</p><div class="table-container"><table><thead><tr><th>概念</th><th>含义</th><th>类比</th></tr></thead><tbody><tr><td><strong>Topic</strong></td><td>一类消息的逻辑集合（如 <code>order_created</code>）</td><td>数据库里的一张表</td></tr><tr><td><strong>Partition</strong></td><td>Topic 的物理分片，是并行与有序的基本单位</td><td>表的”分库分表”</td></tr><tr><td><strong>Offset</strong></td><td>消息在分区内的唯一递增序号</td><td>表里的自增主键</td></tr><tr><td><strong>Broker</strong></td><td>一台 Kafka 服务器节点</td><td>一个 MySQL 实例</td></tr><tr><td><strong>Producer</strong></td><td>生产者，往 Topic 发消息</td><td>INSERT 写入方</td></tr><tr><td><strong>Consumer</strong></td><td>消费者，从 Topic 拉消息</td><td>SELECT 读取方</td></tr><tr><td><strong>Consumer Group</strong></td><td>一组消费者协同消费一个 Topic</td><td>一个”消费集群”</td></tr><tr><td><strong>Segment</strong></td><td>分区在磁盘上的物理文件（.log + .index）</td><td>按大小切分的日志文件</td></tr><tr><td><strong>Leader / Follower</strong></td><td>分区的读写主副本与同步从副本</td><td>主从架构</td></tr></tbody></table></div><blockquote><p>新版本（≥ 2.8 起陆续，3.x 默认）用 <strong>KRaft</strong> 取代了 ZooKeeper 来管元数据与选主，运维复杂度显著下降，但”分区 / 副本 / 消费组”这套模型完全不变。</p></blockquote><h1 id="三、分区：并行度与有序性的平衡点"><a href="#三、分区：并行度与有序性的平衡点" class="headerlink" title="三、分区：并行度与有序性的平衡点"></a>三、分区：并行度与有序性的平衡点</h1><p><strong>一个 Topic 可以有多个 Partition，消息按规则分散到不同分区。</strong> 这是 Kafka 水平扩展的根本：</p><ul><li><strong>并行度上限 = 分区数</strong>。同一个 Consumer Group 里，一个分区同一时刻只被一个消费者实例持有，所以分区数决定了该 Group 的最大消费并发。</li><li><strong>分区内有序，跨分区不保证有序</strong>。如果业务要求”同一订单的状态变更必须按发生顺序消费”，就必须用 <code>订单ID</code> 作为分区 Key，让同订单的消息落到同一分区。</li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 指定 key，保证相同 key 始终进入同一分区（分区内严格有序）</span></span><br><span class="line">ProducerRecord&lt;String, String&gt; record =</span><br><span class="line">        <span class="keyword">new</span> <span class="title class_">ProducerRecord</span>&lt;&gt;(<span class="string">&quot;order_events&quot;</span>, orderId, eventJson);</span><br><span class="line">producer.send(record);</span><br></pre></td></tr></table></figure><p>分区数怎么定？经验公式：</p><div class="table-container"><table><thead><tr><th>维度</th><th>建议</th></tr></thead><tbody><tr><td>与消费者数的关系</td><td>分区数 ≥ 消费者实例数，否则多余消费者闲置</td></tr><tr><td>单分区吞吐</td><td>普通机器单分区生产约 10MB/s、消费约 20MB/s 量级</td></tr><tr><td>未来扩容</td><td>分区数只增不减（减少要重建），按”预期峰值 × 2~3”预留</td></tr></tbody></table></div><div class="note orange anzhiyufont anzhiyu-icon-dengpao disabled"><p>误区：把分区数开到 1000 不一定更快。分区过多会带来更频繁的 Leader 选举、文件句柄与内存开销，Broker 元数据压力陡增。<strong>先测再扩</strong>，别拍脑袋。</p></div><h1 id="四、副本与高可用：ISR-机制"><a href="#四、副本与高可用：ISR-机制" class="headerlink" title="四、副本与高可用：ISR 机制"></a>四、副本与高可用：ISR 机制</h1><p>每个 Partition 有一个 <strong>Leader</strong>（负责读写）和若干 <strong>Follower</strong>（异步/同步拉取）。关键点在于 <strong>ISR（In-Sync Replicas，同步副本集合）</strong>：</p><ul><li>Follower 只要能在 <code>replica.lag.time.max.ms</code> 内追上 Leader，就留在 ISR 中；</li><li>只有 ISR 里的副本才有资格被选举为新 Leader；</li><li>生产者设置 <code>acks=all</code> 时，消息要被 <strong>ISR 中全部副本</strong> 写入才算成功。</li></ul><figure class="highlight properties"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># server.properties 关键副本配置</span></span><br><span class="line"><span class="attr">default.replication.factor</span>=<span class="string">3        # 每个分区 3 副本</span></span><br><span class="line"><span class="attr">min.insync.replicas</span>=<span class="string">2               # ISR 至少 2 个才算写入成功</span></span><br><span class="line"><span class="attr">unclean.leader.election.enable</span>=<span class="string">false # 禁止非 ISR 副本&quot;抢位&quot;当 Leader</span></span><br></pre></td></tr></table></figure><div class="table-container"><table><thead><tr><th>acks 取值</th><th>含义</th><th>可靠性</th><th>吞吐</th></tr></thead><tbody><tr><td><code>0</code></td><td>发了就不管</td><td>最低（可能丢）</td><td>最高</td></tr><tr><td><code>1</code></td><td>Leader 写入即返回</td><td>中（Leader 宕机可能丢）</td><td>中</td></tr><tr><td><code>all</code>(-1)</td><td>ISR 全部写入</td><td>最高（配合 min.insync.replicas）</td><td>较低</td></tr></tbody></table></div><blockquote><p>⚠️ <code>unclean.leader.election.enable=true</code> 看似提高了可用性，但会让一个<strong>落后很多的副本</strong>成为 Leader，造成<strong>已提交消息丢失</strong>。生产环境务必设为 <code>false</code>。</p></blockquote><h1 id="五、高吞吐的秘密：不是”快”，是”不绕路”"><a href="#五、高吞吐的秘密：不是”快”，是”不绕路”" class="headerlink" title="五、高吞吐的秘密：不是”快”，是”不绕路”"></a>五、高吞吐的秘密：不是”快”，是”不绕路”</h1><p>Kafka 能扛住百万级 QPS，靠的是几个工程取舍：</p><ol><li><strong>顺序写磁盘</strong>：消息只追加（append-only）到分区日志尾部，避开了随机写寻道；磁盘顺序写的吞吐甚至高于随机内存写。</li><li><strong>零拷贝（Zero-Copy）</strong>：消费者读取时，数据从磁盘页缓存经 <code>sendfile</code> 直接拷贝到网卡，<strong>少了内核态→用户态的来回拷贝</strong>。</li><li><strong>页缓存（Page Cache）</strong>：Kafka 不强依赖堆内缓存，而是把读写都交给 OS 页缓存，重启后缓存依然热。</li><li><strong>批量 + 压缩</strong>：Producer 攒一批（<code>linger.ms</code> / <code>batch.size</code>）再发，并支持 snappy / gzip / lz4 / zstd 压缩，网络与磁盘开销大幅下降。</li></ol><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">传统路径: 磁盘 → 内核缓冲 → 用户缓冲 → 内核 socket 缓冲 → 网卡   (多次拷贝)</span><br><span class="line">零拷贝:   磁盘 → 内核页缓存 ───────────────→ 网卡                (sendfile)</span><br></pre></td></tr></table></figure><h1 id="六、生产者：可靠性与发送语义"><a href="#六、生产者：可靠性与发送语义" class="headerlink" title="六、生产者：可靠性与发送语义"></a>六、生产者：可靠性与发送语义</h1><p>要”不丢消息”，生产者侧三件套是 <code>acks</code> + <code>retries</code> + <code>enable.idempotence</code>：</p><figure class="highlight properties"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># producer 可靠性配置</span></span><br><span class="line"><span class="attr">acks</span>=<span class="string">all</span></span><br><span class="line"><span class="attr">retries</span>=<span class="string">2147483647</span></span><br><span class="line"><span class="attr">enable.idempotence</span>=<span class="string">true        # 开启幂等：PID + 序列号，broker 去重</span></span><br><span class="line"><span class="attr">max.in.flight.requests.per.connection</span>=<span class="string">5</span></span><br></pre></td></tr></table></figure><ul><li><strong>幂等生产者</strong>：为每个 Producer 分配 PID，每条消息带序列号，Broker 端对 <code>&lt;PID, 分区, 序列号&gt;</code> 去重，避免网络重试导致的<strong>重复写入</strong>。</li><li><strong>事务</strong>：设置 <code>transactional.id</code> 后，可让”消费→处理→生产”在多个分区间<strong>原子提交</strong>，实现端到端不重复。</li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">producer.initTransactions();</span><br><span class="line">producer.beginTransaction();</span><br><span class="line"><span class="keyword">try</span> &#123;</span><br><span class="line">    producer.send(<span class="keyword">new</span> <span class="title class_">ProducerRecord</span>&lt;&gt;(<span class="string">&quot;topic_out&quot;</span>, key, processed));</span><br><span class="line">    producer.commitTransaction();   <span class="comment">// 要么都成功，要么都回滚</span></span><br><span class="line">&#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">    producer.abortTransaction();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h1 id="七、消费者：位移提交与再均衡"><a href="#七、消费者：位移提交与再均衡" class="headerlink" title="七、消费者：位移提交与再均衡"></a>七、消费者：位移提交与再均衡</h1><p>Kafka 是 <strong>Pull 模型</strong>：消费者主动从分区拉取，自己控制节奏和位移（offset）。位移提交方式直接决定”会不会丢 / 会不会重复”：</p><div class="table-container"><table><thead><tr><th>提交方式</th><th>配置</th><th>风险</th></tr></thead><tbody><tr><td>自动提交</td><td><code>enable.auto.commit=true</code></td><td>拉到就提交，处理前崩溃→<strong>消息丢失</strong></td></tr><tr><td>手动同步</td><td><code>consumer.commitSync()</code></td><td>处理完再提交，安全但阻塞</td></tr><tr><td>手动异步</td><td><code>consumer.commitAsync()</code></td><td>不阻塞，但失败不会重试（可回调补救）</td></tr></tbody></table></div><div class="tip warning"><p><strong>再均衡（Rebalance）</strong> 是 Kafka 消费里最容易被忽视的”停顿源”：当消费者加入/退出、分区数变化、心跳超时时，Group 会重新分配分区，期间<strong>所有消费者暂停消费</strong>（Stop-The-World）。务必把 <code>session.timeout.ms</code>、<code>heartbeat.interval.ms</code>、<code>max.poll.interval.ms</code> 配合理，避免因为单次 <code>poll</code> 处理太久被踢出。新版的 <strong>Cooperative Rebalancing</strong> 支持增量再均衡，能显著减少停顿。</p></div><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">while</span> (<span class="literal">true</span>) &#123;</span><br><span class="line">    ConsumerRecords&lt;String, String&gt; records = consumer.poll(Duration.ofMillis(<span class="number">100</span>));</span><br><span class="line">    <span class="keyword">for</span> (ConsumerRecord&lt;String, String&gt; r : records) &#123;</span><br><span class="line">        process(r);                       <span class="comment">// 先处理业务</span></span><br><span class="line">    &#125;</span><br><span class="line">    consumer.commitSync();                <span class="comment">// 处理完再同步提交，避免丢失</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h1 id="八、顺序性与精确一次语义（EOS）"><a href="#八、顺序性与精确一次语义（EOS）" class="headerlink" title="八、顺序性与精确一次语义（EOS）"></a>八、顺序性与精确一次语义（EOS）</h1><p>消息系统的投递语义通常分三档：</p><div class="table-container"><table><thead><tr><th>语义</th><th>含义</th><th>实现代价</th></tr></thead><tbody><tr><td>At-Most-Once</td><td>最多一次，可能丢</td><td>最低</td></tr><tr><td>At-Least-Once</td><td>至少一次，可能重复</td><td>中（Kafka 默认可达）</td></tr><tr><td>Exactly-Once</td><td>精确一次，不丢不重</td><td>最高（EOS）</td></tr></tbody></table></div><p>Kafka 的 <strong>EOS（Exactly-Once Semantics）</strong> 由三层拼起来：</p><ol><li><strong>分区内有序</strong> —— 同一 Key 落到同一分区，天然有序；</li><li><strong>幂等生产者</strong> —— 去重，避免重试产生重复；</li><li><strong>事务 + 事务性消费转换</strong> —— 消费者用 <code>isolation.level=read_committed</code> 只读取已提交事务，处理后再事务性地写回，形成”读已提交→处理→写”的原子闭环。</li></ol><p>在流处理场景（Kafka Streams / Flink 接 Kafka），这套机制能保障”端到端精确一次”，是实时数仓、计费对账等对准确性敏感业务的基础。</p><h1 id="九、Kafka-与-RabbitMQ-RocketMQ-怎么选"><a href="#九、Kafka-与-RabbitMQ-RocketMQ-怎么选" class="headerlink" title="九、Kafka 与 RabbitMQ / RocketMQ 怎么选"></a>九、Kafka 与 RabbitMQ / RocketMQ 怎么选</h1><div class="table-container"><table><thead><tr><th>维度</th><th>Kafka</th><th>RabbitMQ</th><th>RocketMQ</th></tr></thead><tbody><tr><td>设计定位</td><td>高吞吐分布式日志</td><td>低延迟路由（AMQP）</td><td>高可靠业务消息（阿里系）</td></tr><tr><td>吞吐</td><td>极高（十万~百万 QPS）</td><td>中（万级）</td><td>高（十万级）</td></tr><tr><td>消费模型</td><td>Pull + 消费组</td><td>Push + 队列</td><td>Pull + 消费组</td></tr><tr><td>顺序性</td><td>分区内严格有序</td><td>队列内有序</td><td>队列/分区有序</td></tr><tr><td>事务</td><td>支持（EOS）</td><td>不支持</td><td>支持</td></tr><tr><td>典型场景</td><td>日志、埋点、流处理、削峰</td><td>任务分发、RPC 解耦</td><td>订单、交易等核心业务</td></tr></tbody></table></div><p>一句话：<strong>要吞吐和流处理选 Kafka，要灵活路由和极低延迟选 RabbitMQ，要强事务与阿里生态选 RocketMQ。</strong></p><h1 id="十、生产避坑清单"><a href="#十、生产避坑清单" class="headerlink" title="十、生产避坑清单"></a>十、生产避坑清单</h1><div class="table-container"><table><thead><tr><th>坑</th><th>现象</th><th>正确姿势</th></tr></thead><tbody><tr><td>分区数过少</td><td>消费者扩不上去，吞吐卡死</td><td>按峰值 ×2~3 预留，支持后续增分区</td></tr><tr><td>acks=1 又单副本</td><td>Leader 宕机丢消息</td><td>副本 ≥3，<code>min.insync.replicas=2</code></td></tr><tr><td>自动提交 + 慢处理</td><td>消息”丢了”</td><td>处理完再手动 <code>commitSync</code></td></tr><tr><td>消费逻辑耗时过长</td><td>被踢出触发频繁 Rebalance</td><td>调大 <code>max.poll.interval.ms</code>，处理异步化</td></tr><tr><td>key 设计随意</td><td>跨分区乱序</td><td>用业务主键（如订单ID）做 Key</td></tr><tr><td>消息体过大</td><td>吞吐骤降、GC 压力</td><td>大对象走对象存储，消息只存引用</td></tr><tr><td>不监控 Lag</td><td>消费跟不上无人知</td><td>盯 <code>consumer lag</code>，设告警阈值</td></tr></tbody></table></div><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>Kafka 的威力来自一套自洽的设计：<strong>分区提供并行与有序，ISR 副本兼顾高可用与不丢，顺序写+零拷贝+页缓存成就高吞吐，幂等与事务补齐精确一次语义</strong>。它既不是”最快的队列”，也不是”最易用的 MQ”，而是<strong>为”数据被多系统反复、可靠、有序地消费”而生</strong>的基础设施。掌握本文的分区、副本、生产与消费语义、EOS 四条主线，足以应对绝大多数后端场景下的 Kafka 设计与排障。</p><blockquote><p>下一篇可以沿着”Kafka 集群部署与监控（KRaft + Cruise Control + Kafka Eagle）”或”Flink 接 Kafka 实现端到端 Exactly-Once”继续深入——把消息系统的”存”和流计算的”算”打通，才算真正用好它。</p></blockquote>]]></content>
    
    
      
      
    <summary type="html">&lt;div class=&quot;tip warning&quot;&gt;&lt;p&gt;很多系统一开始用”直接调接口”把服务串起来，随着流量和依赖变多，耦合、峰值、故障扩散会接踵而至。Kafka 不是单纯的”消息中间件”，它更像一套&lt;strong&gt;分布式的提交日志系统&lt;/strong&gt;。本文从原理到实战，把 K</summary>
      
    
    
    
    <category term="大数据" scheme="http://hexo.sjava.cn/categories/%E5%A4%A7%E6%95%B0%E6%8D%AE/"/>
    
    
    <category term="分布式" scheme="http://hexo.sjava.cn/tags/%E5%88%86%E5%B8%83%E5%BC%8F/"/>
    
    <category term="大数据" scheme="http://hexo.sjava.cn/tags/%E5%A4%A7%E6%95%B0%E6%8D%AE/"/>
    
    <category term="Kafka" scheme="http://hexo.sjava.cn/tags/Kafka/"/>
    
    <category term="消息队列" scheme="http://hexo.sjava.cn/tags/%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97/"/>
    
    <category term="流处理" scheme="http://hexo.sjava.cn/tags/%E6%B5%81%E5%A4%84%E7%90%86/"/>
    
  </entry>
  
  <entry>
    <title>Java 并发编程核心：volatile、synchronized、CAS 与 AQS 原理解析</title>
    <link href="http://hexo.sjava.cn/posts/48217.html"/>
    <id>http://hexo.sjava.cn/posts/48217.html</id>
    <published>2026-09-12T08:00:00.000Z</published>
    <updated>2026-09-12T08:00:00.000Z</updated>
    
    <content type="html"><![CDATA[<div class="tip warning"><p>线程池解决了”怎么管理线程”的问题，但线程池本身并不保证线程安全——它只是把任务交给线程执行。真正保证多线程正确协作的，是 volatile、synchronized、CAS 和 AQS 这套并发原语。本文从 JMM 内存模型讲起，把 Java 并发的”内功心法”彻底拆开。</p></div><h1 id="一、JMM-内存模型：并发问题的根源"><a href="#一、JMM-内存模型：并发问题的根源" class="headerlink" title="一、JMM 内存模型：并发问题的根源"></a>一、JMM 内存模型：并发问题的根源</h1><p>Java 多线程程序中出现的各种诡异 Bug——值没变、结果不对、指令乱序——归根结底都源于 <strong>JMM（Java Memory Model）</strong>。JMM 不是真实存在的硬件结构，而是一组规范，定义了线程如何通过主内存交互。</p><h2 id="1-1-三大特性：可见性、原子性、有序性"><a href="#1-1-三大特性：可见性、原子性、有序性" class="headerlink" title="1.1 三大特性：可见性、原子性、有序性"></a>1.1 三大特性：可见性、原子性、有序性</h2><div class="table-container"><table><thead><tr><th>特性</th><th>含义</th><th>违反时的现象</th><th>保证手段</th></tr></thead><tbody><tr><td><strong>可见性</strong></td><td>一个线程修改了共享变量，其他线程能立即看到最新值</td><td>线程 A 改了 flag=true，线程 B 还在读到 false</td><td>volatile / synchronized / final</td></tr><tr><td><strong>原子性</strong></td><td>一个操作不可分割，要么全部完成，要么全不做</td><td>i++ 看起来是一行，实际是 读-改-写 三步</td><td>synchronized / Lock / Atomic</td></tr><tr><td><strong>有序性</strong></td><td>程序按照代码顺序执行（在本线程内观察）</td><td>指令重排导致对象未初始化就被引用</td><td>volatile / happens-before</td></tr></tbody></table></div><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 经典的可见性问题示例</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">VisibilityDemo</span> &#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="type">boolean</span> <span class="variable">flag</span> <span class="operator">=</span> <span class="literal">false</span>;  <span class="comment">// 没有 volatile！</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> Exception &#123;</span><br><span class="line">        <span class="keyword">new</span> <span class="title class_">Thread</span>(() -&gt; &#123;</span><br><span class="line">            <span class="keyword">while</span> (!flag) &#123; &#125;  <span class="comment">// 可能永远看不到 flag 变为 true</span></span><br><span class="line">            System.out.println(<span class="string">&quot;Thread B sees flag = true&quot;</span>);</span><br><span class="line">        &#125;).start();</span><br><span class="line"></span><br><span class="line">        Thread.sleep(<span class="number">100</span>);</span><br><span class="line">        flag = <span class="literal">true</span>;  <span class="comment">// 主线程修改了，但子线程可能永远看不到</span></span><br><span class="line">        System.out.println(<span class="string">&quot;Main set flag = true&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><blockquote><p>上述代码在 <code>-server</code> 模式下极大概率会死循环——因为 JIT 编译器可能将 <code>while(!flag)</code> 优化为无限循环，不再从主内存重新读取 flag。</p></blockquote><h2 id="1-2-happens-before-规则"><a href="#1-2-happens-before-规则" class="headerlink" title="1.2 happens-before 规则"></a>1.2 happens-before 规则</h2><p>JMM 通过 <strong>happens-before</strong> 原则来界定操作之间的可见性顺序。如果 A happens-before B，那么 A 的结果对 B 可见：</p><div class="table-container"><table><thead><tr><th>规则</th><th>说明</th></tr></thead><tbody><tr><td>程序顺序规则</td><td>同一线程中，前面的操作 happens-before 后面的操作</td></tr><tr><td>监视器锁规则</td><td>unlock 操作 happens-before 后续对同一把锁的 lock 操作</td></tr><tr><td>volatile 变量规则</td><td>写操作 happens-before 后续对同一变量的读操作</td></tr><tr><td>线程启动规则</td><td>Thread.start() happens-before 该线程的每一个操作</td></tr><tr><td>线程终止规则</td><td>线程中的所有操作 happens-before 其他线程检测到该线程结束</td></tr><tr><td>线程中断规则</td><td>interrupt() 调用 happens-before 被中断线程检测到中断事件</td></tr><tr><td>对象终结规则</td><td>构造函数返回 happens-before finalize() 方法</td></tr><tr><td>传递性</td><td>若 A hb B 且 B hb C，则 A hb C</td></tr></tbody></table></div><h2 id="1-3-指令重排序与内存屏障"><a href="#1-3-指令重排序与内存屏障" class="headerlink" title="1.3 指令重排序与内存屏障"></a>1.3 指令重排序与内存屏障</h2><p>编译器和 CPU 为了提升性能，会对指令进行<strong>重排序</strong>，分为三种：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">int</span> <span class="variable">a</span> <span class="operator">=</span> <span class="number">1</span>;   <span class="comment">// ①</span></span><br><span class="line"><span class="type">int</span> <span class="variable">b</span> <span class="operator">=</span> <span class="number">2</span>;   <span class="comment">// ②</span></span><br><span class="line"><span class="type">int</span> <span class="variable">c</span> <span class="operator">=</span> a + b; <span class="comment">// ③</span></span><br><span class="line"><span class="comment">// ①和②可以互换顺序，但③必须在①②之后 —— 这就是重排序的边界</span></span><br></pre></td></tr></table></figure><p><strong>内存屏障（Memory Barrier）</strong> 是阻止重排序的”硬指令”，JVM 插入四种屏障：</p><div class="table-container"><table><thead><tr><th>屏障类型</th><th>作用</th></tr></thead><tbody><tr><td>LoadLoad</td><td>确保 Load1 数据装载先于 Load2 及后续装载</td></tr><tr><td>StoreStore</td><td>确保 Store1 数据刷新先于 Store2 及后续存储</td></tr><tr><td>LoadStore</td><td>确保 Load1 数据装载先于 Store2 及后续存储</td></tr><tr><td>StoreLoad</td><td>确保 Store1 数据刷新先于 Load2 及后续装载（开销最大）</td></tr></tbody></table></div><hr><h1 id="二、volatile-关键字深度解析"><a href="#二、volatile-关键字深度解析" class="headerlink" title="二、volatile 关键字深度解析"></a>二、volatile 关键字深度解析</h1><h2 id="2-1-volatile-能保证什么（不能保证什么）"><a href="#2-1-volatile-能保证什么（不能保证什么）" class="headerlink" title="2.1 volatile 能保证什么（不能保证什么）"></a>2.1 volatile 能保证什么（不能保证什么）</h2><div class="note info flat"><p><strong>一句话总结</strong>：volatile 保证<strong>可见性</strong>和<strong>有序性</strong>（禁止指令重排），但不保证<strong>原子性</strong>。</p></div><div class="table-container"><table><thead><tr><th>能力</th><th>是否保证</th><th>说明</th></tr></thead><tbody><tr><td>可见性</td><td>✅</td><td>写操作强制刷回主存，读操作强制从主存读取</td></tr><tr><td>有序性</td><td>✅</td><td>插入内存屏障，禁止特定类型的指令重排</td></tr><tr><td>原子性</td><td>❌</td><td><code>volatile int i; i++</code> 仍然不是原子操作</td></tr></tbody></table></div><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// volatile 不保证原子性的经典反例</span></span><br><span class="line"><span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">volatile</span> <span class="type">int</span> <span class="variable">count</span> <span class="operator">=</span> <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> Exception &#123;</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> <span class="variable">i</span> <span class="operator">=</span> <span class="number">0</span>; i &lt; <span class="number">10</span>; i++) &#123;</span><br><span class="line">        <span class="keyword">new</span> <span class="title class_">Thread</span>(() -&gt; &#123;</span><br><span class="line">            <span class="keyword">for</span> (<span class="type">int</span> <span class="variable">j</span> <span class="operator">=</span> <span class="number">0</span>; j &lt; <span class="number">10000</span>; j++) count++;</span><br><span class="line">        &#125;).start();</span><br><span class="line">    &#125;</span><br><span class="line">    Thread.sleep(<span class="number">1000</span>);</span><br><span class="line">    System.out.println(<span class="string">&quot;count = &quot;</span> + count);  <span class="comment">// 几乎一定 &lt; 100000</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><blockquote><p>即使加了 volatile，<code>count++</code> 仍然是 读-改-写 三步操作，多线程交叉执行就会丢数据。要保证原子性必须用 synchronized 或 Atomic 类。</p></blockquote><h2 id="2-2-volatile-实现原理"><a href="#2-2-volatile-实现原理" class="headerlink" title="2.2 volatile 实现原理"></a>2.2 volatile 实现原理</h2><p>volatile 的底层依赖两套机制协同工作：</p><p><strong>（1）内存语义层面 —— 内存屏障</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">volatile</span> <span class="type">boolean</span> v;</span><br><span class="line">v = <span class="literal">true</span>;   <span class="comment">// 写操作：插入 StoreStore + StoreLoad 屏障</span></span><br><span class="line"><span class="type">boolean</span> <span class="variable">b</span> <span class="operator">=</span> v; <span class="comment">// 读操作：插入 LoadLoad + LoadStore 屏障</span></span><br></pre></td></tr></table></figure><p><strong>（2）硬件层面 —— MESI 缓存一致性协议</strong></p><p>现代 CPU 使用 MESI 协议维护缓存行状态：</p><div class="table-container"><table><thead><tr><th>状态</th><th>含义</th></tr></thead><tbody><tr><td>M（Modified）</td><td>已修改，与主存不一致，仅在本缓存有效</td></tr><tr><td>E（Exclusive）</td><td>独占，与主存一致，仅在本缓存有效</td></tr><tr><td>S（Shared）</td><td>共享，与主存一致，多个缓存都有副本</td></tr><tr><td>I（Invalid）</td><td>无效，缓存行已失效</td></tr></tbody></table></div><p>当 CPU 修改一个 volatile 变量时，会触发 <strong>Bus Locking</strong> 或 <strong>MESI Invalidate</strong>，使其他 CPU 中对应的缓存行失效，从而保证可见性。</p><h2 id="2-3-volatile-典型使用场景"><a href="#2-3-volatile-典型使用场景" class="headerlink" title="2.3 volatile 典型使用场景"></a>2.3 volatile 典型使用场景</h2><h3 id="场景一：状态标志位"><a href="#场景一：状态标志位" class="headerlink" title="场景一：状态标志位"></a>场景一：状态标志位</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 最经典的 volatile 用法 —— 停止标志</span></span><br><span class="line"><span class="keyword">private</span> <span class="keyword">volatile</span> <span class="type">boolean</span> <span class="variable">running</span> <span class="operator">=</span> <span class="literal">true</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">shutdown</span><span class="params">()</span> &#123; running = <span class="literal">false</span>; &#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">doWork</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="keyword">while</span> (running) &#123;</span><br><span class="line">        <span class="comment">// 处理业务逻辑...</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="场景二：DCL-双重检查锁定（单例模式）"><a href="#场景二：DCL-双重检查锁定（单例模式）" class="headerlink" title="场景二：DCL 双重检查锁定（单例模式）"></a>场景二：DCL 双重检查锁定（单例模式）</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Singleton</span> &#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">volatile</span> Singleton instance;  <span class="comment">// 必须加 volatile！</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> Singleton <span class="title function_">getInstance</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">if</span> (instance == <span class="literal">null</span>) &#123;                    <span class="comment">// 第一次检查（无锁）</span></span><br><span class="line">            <span class="keyword">synchronized</span> (Singleton.class) &#123;</span><br><span class="line">                <span class="keyword">if</span> (instance == <span class="literal">null</span>) &#123;            <span class="comment">// 第二次检查（有锁）</span></span><br><span class="line">                    instance = <span class="keyword">new</span> <span class="title class_">Singleton</span>();    <span class="comment">// ⚠️ 这里不是原子操作！</span></span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> instance;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><div class="tip danger"><p>为什么 DCL 中 <code>instance</code> 必须 volatile？因为 <code>new Singleton()</code> 分三步：</p><ol><li>分配内存空间</li><li>初始化对象</li><li>将引用指向内存地址</li></ol><p>步骤 2 和 3 可能被重排序为 1→3→2。如果线程 A 执行完 3 但还没执行 2，此时线程 B 进入第一次检查发现 <code>instance != null</code>，直接返回了一个<strong>未初始化完成</strong>的对象！volatile 的禁止重排恰好解决这个问题。</p></div><h3 id="场景三：volatile-数组-引用的局限性"><a href="#场景三：volatile-数组-引用的局限性" class="headerlink" title="场景三：volatile 数组/引用的局限性"></a>场景三：volatile 数组/引用的局限性</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">volatile</span> <span class="type">int</span>[] arr = <span class="keyword">new</span> <span class="title class_">int</span>[<span class="number">10</span>];</span><br><span class="line">arr[<span class="number">0</span>] = <span class="number">1</span>;  <span class="comment">// ❌ 这不是 volatile 写！只保证了 arr 引用的可见性</span></span><br><span class="line">              <span class="comment">//    arr[0] 的修改对其他线程不一定可见</span></span><br></pre></td></tr></table></figure><blockquote><p>volatile 只保证<strong>引用本身</strong>的可见性，不保证数组元素或对象内部字段的可见性。需要细粒度控制时请用 Atomic*Array 或加锁。</p></blockquote><hr><h1 id="三、synchronized-锁机制与锁升级"><a href="#三、synchronized-锁机制与锁升级" class="headerlink" title="三、synchronized 锁机制与锁升级"></a>三、synchronized 锁机制与锁升级</h1><h2 id="3-1-synchronized-的三种用法"><a href="#3-1-synchronized-的三种用法" class="headerlink" title="3.1 synchronized 的三种用法"></a>3.1 synchronized 的三种用法</h2><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 1. 修饰实例方法 —— 锁的是当前实例对象（this）</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">synchronized</span> <span class="keyword">void</span> <span class="title function_">method</span><span class="params">()</span> &#123; ... &#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 2. 修饰静态方法 —— 锁的是当前类的 Class 对象</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">synchronized</span> <span class="keyword">void</span> <span class="title function_">staticMethod</span><span class="params">()</span> &#123; ... &#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 3. 修饰代码块 —— 锁的是指定对象</span></span><br><span class="line"><span class="keyword">synchronized</span> (lockObject) &#123; ... &#125;</span><br></pre></td></tr></table></figure><h2 id="3-2-对象头与-Mark-Word"><a href="#3-2-对象头与-Mark-Word" class="headerlink" title="3.2 对象头与 Mark Word"></a>3.2 对象头与 Mark Word</h2><p>synchronized 的锁信息存储在<strong>对象头（Object Header）</strong>的 <strong>Mark Word</strong> 中。在 64 位 JVM 中，Mark Word 的结构如下：</p><div class="table-container"><table><thead><tr><th>锁状态</th><th>25 bit</th><th>31 bit</th><th>1 bit</th><th>4 bit</th><th>1 bit</th><th>2 bit</th></tr></thead><tbody><tr><td>无锁</td><td>hashcode</td><td>age</td><td>0</td><td>01</td><td>unused</td><td>无锁(01)</td></tr><tr><td>偏向锁</td><td>thread ID</td><td>epoch</td><td>1</td><td>01</td><td>unused</td><td>偏向(01)</td></tr><tr><td>轻量级锁</td><td>指向栈中 Lock Record 的指针</td><td>—</td><td>00</td><td>轻量锁(00)</td></tr><tr><td>重量级锁</td><td>指向互斥量（Monitor）的指针</td><td>—</td><td>10</td><td>重量锁(10)</td></tr><tr><td>GC 标记</td><td>空</td><td>—</td><td>11</td><td>GC(11)</td></tr></tbody></table></div><h2 id="3-3-锁升级过程"><a href="#3-3-锁升级过程" class="headerlink" title="3.3 锁升级过程"></a>3.3 锁升级过程</h2><p>JDK 6 之后，synchronized 引入了<strong>偏向锁</strong>和<strong>轻量级锁</strong>，大多数情况下不需要直接进入重量级锁（操作系统层面的互斥锁）。升级路径如下：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">无锁 ──(第一次获取)──▶ 偏向锁 ──(竞争出现)──▶ 轻量级锁 ──(自旋失败)──▶ 重量级锁</span><br><span class="line">  │                      │                     │                       │</span><br><span class="line">  │  CAS 设置 ThreadID   │  CAS 替换 Mark Word  │  自旋+CAS             │  OS mutex</span><br><span class="line">  │  无竞争时零开销      │  少量竞争           │  短时间竞争           │  上下文切换</span><br></pre></td></tr></table></figure><h3 id="偏向锁（Biased-Locking）"><a href="#偏向锁（Biased-Locking）" class="headerlink" title="偏向锁（Biased Locking）"></a>偏向锁（Biased Locking）</h3><p>当一个线程反复进入同一个同步块时，JVM 认为这个锁”偏向”该线程：</p><ul><li>通过 CAS 将 Mark Word 中的 ThreadID 设为当前线程 ID</li><li>后续该线程进入/退出同步块<strong>无需任何 CAS 操作</strong></li><li>只有当其他线程尝试获取这把锁时，才撤销偏向锁</li></ul><blockquote><p><strong>生产建议</strong>：如果锁竞争确实激烈（如 Web 服务器的请求处理），偏向锁反而增加撤销开销。JDK 15 默认关闭偏向锁（<code>-XX:-UseBiasedLocking</code>），JDK 18 正式移除。</p></blockquote><h3 id="轻量级锁（Lightweight-Locking）"><a href="#轻量级锁（Lightweight-Locking）" class="headerlink" title="轻量级锁（Lightweight Locking）"></a>轻量级锁（Lightweight Locking）</h3><p>偏向锁撤销后，或两个线程交替获取锁时，升级为轻量级锁：</p><ul><li>在当前线程的栈帧中创建 <strong>Lock Record</strong>（锁记录）</li><li>将对象的 Mark Word CAS 替换为指向 Lock Record 的指针</li><li>解锁时再将 Lock Record 中的旧 Mark Word CAS 回去</li><li>如果 CAS 失败（说明有竞争），<strong>自旋</strong>重试若干次</li></ul><h3 id="重量级锁（Heavyweight-Locking）"><a href="#重量级锁（Heavyweight-Locking）" class="headerlink" title="重量级锁（Heavyweight Locking）"></a>重量级锁（Heavyweight Locking）</h3><p>自旋超过阈值（默认 10 次，可通过 <code>-XX:PreBlockSpin</code> 调整）后，升级为重量级锁：</p><ul><li>向操作系统申请<strong>互斥量（mutex）</strong></li><li>未获取到锁的线程<strong>阻塞</strong>（BLOCKED 状态），涉及用户态/内核态切换</li><li>被唤醒的线程需要<strong>重新竞争</strong>锁（非公平）</li></ul><h2 id="3-4-锁消除与锁粗化（JIT-编译器优化）"><a href="#3-4-锁消除与锁粗化（JIT-编译器优化）" class="headerlink" title="3.4 锁消除与锁粗化（JIT 编译器优化）"></a>3.4 锁消除与锁粗化（JIT 编译器优化）</h2><p>JIT 编译器在运行时还会对 synchronized 做两种智能优化：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 锁消除：锁的对象是局部变量，不可能被外部线程访问</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">lockElimination</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="type">StringBuffer</span> <span class="variable">sb</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">StringBuffer</span>();  <span class="comment">// StringBuffer 方法都是 synchronized 的</span></span><br><span class="line">    sb.append(<span class="string">&quot;a&quot;</span>).append(<span class="string">&quot;b&quot;</span>);            <span class="comment">// JIT 发现 sb 是局部的 → 直接去掉所有锁</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 锁粗化：循环体内频繁加锁解锁 → 合并为一次</span></span><br><span class="line"><span class="keyword">for</span> (<span class="type">int</span> <span class="variable">i</span> <span class="operator">=</span> <span class="number">0</span>; i &lt; <span class="number">1000</span>; i++) &#123;</span><br><span class="line">    <span class="keyword">synchronized</span> (lock) &#123; ... &#125;  <span class="comment">// JIT 可能合并为 synchronized(lock) &#123; for(...) &#123;...&#125; &#125;</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="3-5-synchronized-vs-volatile-对比"><a href="#3-5-synchronized-vs-volatile-对比" class="headerlink" title="3.5 synchronized vs volatile 对比"></a>3.5 synchronized vs volatile 对比</h2><div class="table-container"><table><thead><tr><th>维度</th><th>synchronized</th><th>volatile</th></tr></thead><tbody><tr><td>可见性</td><td>✅</td><td>✅</td></tr><tr><td>原子性</td><td>✅（复合操作也保证）</td><td>❌</td></tr><tr><td>有序性</td><td>✅（互斥即有序）</td><td>✅（内存屏障）</td></tr><tr><td>阻塞</td><td>会阻塞（重量级锁时）</td><td>不会阻塞</td></tr><tr><td>性能开销</td><td>较高（尤其重量级锁）</td><td>很低</td></tr><tr><td>适用场景</td><td>复合原子操作、临界区保护</td><td>状态标志、单次读/写</td></tr></tbody></table></div><hr><h1 id="四、CAS-与原子操作"><a href="#四、CAS-与原子操作" class="headerlink" title="四、CAS 与原子操作"></a>四、CAS 与原子操作</h1><h2 id="4-1-什么是-CAS（Compare-And-Swap）"><a href="#4-1-什么是-CAS（Compare-And-Swap）" class="headerlink" title="4.1 什么是 CAS（Compare And Swap）"></a>4.1 什么是 CAS（Compare And Swap）</h2><p>CAS 是一条 <strong>CPU 原子指令</strong>，其语义等价于以下伪代码：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// CAS(V, Expected, NewValue)</span></span><br><span class="line"><span class="comment">// 如果内存值 V == 期望值 Expected，则将 V 更新为 NewValue，否则什么都不做</span></span><br><span class="line"><span class="type">boolean</span> <span class="title function_">CAS</span><span class="params">(V, Expected, NewValue)</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (V == Expected) &#123;</span><br><span class="line">        V = NewValue;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">true</span>;   <span class="comment">// 成功</span></span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">false</span>;      <span class="comment">// 失败，V 已被其他线程修改</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>CAS 是<strong>乐观锁</strong>的核心思想：不先加锁，而是假设没有冲突，更新时再检验。冲突了就重试。</p><h2 id="4-2-CAS-底层实现"><a href="#4-2-CAS-底层实现" class="headerlink" title="4.2 CAS 底层实现"></a>4.2 CAS 底层实现</h2><p>Java 中 CAS 通过 <code>sun.misc.Unsafe</code> 类调用本地方法，最终映射为 x86 的 <code>cmpxchg</code> 指令：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// AtomicInteger.getAndIncrement() 的核心调用：</span></span><br><span class="line"><span class="comment">// unsafe.compareAndSwapInt(this, valueOffset, expect, update)</span></span><br><span class="line"><span class="comment">//</span></span><br><span class="line"><span class="comment">// 最终汇编：</span></span><br><span class="line"><span class="comment">// lock cmpxchg [addr], reg   ← lock 前缀保证多核原子性</span></span><br></pre></td></tr></table></figure><div class="table-container"><table><thead><tr><th>平台</th><th>CAS 指令</th></tr></thead><tbody><tr><td>x86/x64</td><td><code>lock cmpxchg</code></td></tr><tr><td>ARM</td><td><code>LDREX / STREX</code></td></tr><tr><td>RISC-V</td><td><code>LR / SC</code></td></tr></tbody></table></div><h2 id="4-3-ABA-问题及解决方案"><a href="#4-3-ABA-问题及解决方案" class="headerlink" title="4.3 ABA 问题及解决方案"></a>4.3 ABA 问题及解决方案</h2><p>CAS 检查的是”值有没有变”，但如果值从 A → B → A 变回来，CAS 会误以为没变过：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">线程1 读取值 A（准备 CAS 为 C）</span><br><span class="line">线程2 把 A 改成 B</span><br><span class="line">线程3 又把 B 改回 A</span><br><span class="line">线程1 执行 CAS(A, C) → 成功！但实际上中间已经变了两次</span><br></pre></td></tr></table></figure><p><strong>解决方案：加版本号</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// AtomicStampedReference —— 带版本号的引用</span></span><br><span class="line">AtomicStampedReference&lt;String&gt; asr = <span class="keyword">new</span> <span class="title class_">AtomicStampedReference</span>&lt;&gt;(<span class="string">&quot;A&quot;</span>, <span class="number">0</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">// CAS 时同时比较值和版本号</span></span><br><span class="line"><span class="type">int</span>[] stampHolder = <span class="keyword">new</span> <span class="title class_">int</span>[<span class="number">1</span>];</span><br><span class="line"><span class="type">String</span> <span class="variable">ref</span> <span class="operator">=</span> asr.get(stampHolder);       <span class="comment">// ref=&quot;A&quot;, stamp=0</span></span><br><span class="line"><span class="type">boolean</span> <span class="variable">ok</span> <span class="operator">=</span> asr.compareAndSet(ref, <span class="string">&quot;C&quot;</span>, stampHolder[<span class="number">0</span>], stampHolder[<span class="number">0</span>] + <span class="number">1</span>);</span><br><span class="line"><span class="comment">//                              期望引用  新引用  期望版本  新版本</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// AtomicMarkableReference —— 带布尔标记的引用（简化版，只关心&quot;变没变过&quot;）</span></span><br><span class="line">AtomicMarkableReference&lt;String&gt; amr = <span class="keyword">new</span> <span class="title class_">AtomicMarkableReference</span>&lt;&gt;(<span class="string">&quot;A&quot;</span>, <span class="literal">false</span>);</span><br><span class="line">amr.compareAndSet(<span class="string">&quot;A&quot;</span>, <span class="string">&quot;B&quot;</span>, <span class="literal">false</span>, <span class="literal">true</span>);  <span class="comment">// 同时比较引用和标记</span></span><br></pre></td></tr></table></figure><h2 id="4-4-CAS-自旋与性能问题"><a href="#4-4-CAS-自旋与性能问题" class="headerlink" title="4.4 CAS 自旋与性能问题"></a>4.4 CAS 自旋与性能问题</h2><p>CAS 失败后会<strong>自旋重试</strong>，在高竞争下可能导致：</p><div class="table-container"><table><thead><tr><th>问题</th><th>现象</th><th>解决方案</th></tr></thead><tbody><tr><td><strong>CPU 空转</strong></td><td>大量线程自旋消耗 CPU</td><td>LongAdder（分段 CAS）、限制自旋次数</td></tr><tr><td><strong>ABA 问题</strong></td><td>值被改回原样导致误判</td><td>AtomicStampedReference</td></tr><tr><td><strong>只能保证单个变量</strong></td><td>无法对多个变量做原子 CAS</td><td>用 synchronized 或封装成对象</td></tr></tbody></table></div><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// LongAdder —— 高并发下的计数器替代方案（JDK 8+）</span></span><br><span class="line"><span class="type">LongAdder</span> <span class="variable">counter</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">LongAdder</span>();</span><br><span class="line">counter.increment();  <span class="comment">// 内部分散到多个 Cell，减少 CAS 竞争</span></span><br><span class="line"><span class="type">long</span> <span class="variable">sum</span> <span class="operator">=</span> counter.sum();  <span class="comment">// 汇总所有 Cell</span></span><br></pre></td></tr></table></figure><hr><h1 id="五、Atomic-原子类体系"><a href="#五、Atomic-原子类体系" class="headerlink" title="五、Atomic 原子类体系"></a>五、Atomic 原子类体系</h1><p>JUC 提供了一套基于 CAS 实现的原子类，位于 <code>java.util.concurrent.atomic</code> 包下。</p><h2 id="5-1-体系总览"><a href="#5-1-体系总览" class="headerlink" title="5.1 体系总览"></a>5.1 体系总览</h2><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line">java.util.concurrent.atomic</span><br><span class="line">├── 基本类型</span><br><span class="line">│   ├── AtomicInteger</span><br><span class="line">│   ├── AtomicLong</span><br><span class="line">│   └── AtomicBoolean</span><br><span class="line">├── 数组类型</span><br><span class="line">│   ├── AtomicIntegerArray</span><br><span class="line">│   ├── AtomicLongArray</span><br><span class="line">│   └── AtomicReferenceArray&lt;E&gt;</span><br><span class="line">├── 引用类型</span><br><span class="line">│   ├── AtomicReference&lt;V&gt;</span><br><span class="line">│   ├── AtomicStampedReference&lt;V&gt;</span><br><span class="line">│   └── AtomicMarkableReference&lt;V&gt;</span><br><span class="line">├── 字段更新器</span><br><span class="line">│   ├── AtomicIntegerFieldUpdater&lt;T&gt;</span><br><span class="line">│   ├── AtomicLongFieldUpdater&lt;T&gt;</span><br><span class="line">│   └── AtomicReferenceFieldUpdater&lt;T,V&gt;</span><br><span class="line">└── 累加器（JDK 8+）</span><br><span class="line">    ├── LongAdder</span><br><span class="line">    ├── DoubleAdder</span><br><span class="line">    ├── LongAccumulator</span><br><span class="line">    └── DoubleAccumulator</span><br></pre></td></tr></table></figure><h2 id="5-2-AtomicInteger-核心源码解析"><a href="#5-2-AtomicInteger-核心源码解析" class="headerlink" title="5.2 AtomicInteger 核心源码解析"></a>5.2 AtomicInteger 核心源码解析</h2><p>以最常用的 <code>getAndIncrement()</code> （即 <code>i++</code> 的原子版）为例：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">final</span> <span class="type">int</span> <span class="title function_">getAndIncrement</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> getAndAdd(<span class="number">1</span>);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">final</span> <span class="type">int</span> <span class="title function_">getAndAdd</span><span class="params">(<span class="type">int</span> delta)</span> &#123;</span><br><span class="line">    <span class="comment">// U = sun.misc.Unsafe 实例</span></span><br><span class="line">    <span class="comment">// valueOffset = 字段 value 在对象内存中的偏移量</span></span><br><span class="line">    <span class="keyword">return</span> U.getAndAddInt(<span class="built_in">this</span>, VALUE, delta);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Unsafe.getAndAddInt —— CAS 自旋循环</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">final</span> <span class="type">int</span> <span class="title function_">getAndAddInt</span><span class="params">(Object o, <span class="type">long</span> offset, <span class="type">int</span> delta)</span> &#123;</span><br><span class="line">    <span class="type">int</span> prev;</span><br><span class="line">    <span class="type">int</span> next;</span><br><span class="line">    <span class="keyword">do</span> &#123;</span><br><span class="line">        prev = getIntVolatile(o, offset);  <span class="comment">// 读取当前值（volatile 读）</span></span><br><span class="line">        next = prev + delta;               <span class="comment">// 计算新值</span></span><br><span class="line">    &#125; <span class="keyword">while</span> (!compareAndSwapInt(o, offset, prev, next));  <span class="comment">// CAS 更新，失败则重试</span></span><br><span class="line">    <span class="keyword">return</span> prev;  <span class="comment">// 返回旧值</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><blockquote><p>这个 <code>do-while</code> 循环就是<strong>自旋锁</strong>的本质——不断尝试直到成功。在低竞争时几乎无开销；高竞争时会浪费 CPU 周期。</p></blockquote><h2 id="5-3-字段更新器：给现有类”打补丁”"><a href="#5-3-字段更新器：给现有类”打补丁”" class="headerlink" title="5.3 字段更新器：给现有类”打补丁”"></a>5.3 字段更新器：给现有类”打补丁”</h2><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 场景：想对一个第三方类的某个 volatile 字段做原子操作，</span></span><br><span class="line"><span class="comment">// 但不能修改它的源码</span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">Student</span> &#123;</span><br><span class="line">    <span class="keyword">volatile</span> <span class="type">int</span> score;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">AtomicIntegerFieldUpdater&lt;Student&gt; updater =</span><br><span class="line">    AtomicIntegerFieldUpdater.newUpdater(Student.class, <span class="string">&quot;score&quot;</span>);</span><br><span class="line"></span><br><span class="line"><span class="type">Student</span> <span class="variable">s</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Student</span>();</span><br><span class="line">updater.set(s, <span class="number">80</span>);          <span class="comment">// 相当于 s.score = 80（原子写）</span></span><br><span class="line"><span class="type">int</span> <span class="variable">old</span> <span class="operator">=</span> updater.getAndIncrement(s);  <span class="comment">// 原子的 s.score++</span></span><br></pre></td></tr></table></figure><div class="note warning flat"><p>字段更新器要求目标字段必须是 <strong>volatile</strong> 的，且反射能访问到。它本质上是对指定偏移量做 CAS 操作，是一种”侵入性更低”的原子操作方式。</p></div><hr><h1 id="六、AQS（AbstractQueuedSynchronizer）框架"><a href="#六、AQS（AbstractQueuedSynchronizer）框架" class="headerlink" title="六、AQS（AbstractQueuedSynchronizer）框架"></a>六、AQS（AbstractQueuedSynchronizer）框架</h1><h2 id="6-1-AQS-设计思想"><a href="#6-1-AQS-设计思想" class="headerlink" title="6.1 AQS 设计思想"></a>6.1 AQS 设计思想</h2><p>AQS 是 JUC 并发包的<strong>基石</strong>——ReentrantLock、CountDownLatch、Semaphore、ReentrantReadWriteLock 全部基于 AQS 构建。</p><p>Doug Lea（JUC 作者）的设计哲学：<strong>将同步状态的管理与线程的排队机制分离</strong>。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">AQS 核心架构：</span><br><span class="line">┌─────────────────────────────────────┐</span><br><span class="line">│         AbstractQueuedSynchronizer   │</span><br><span class="line">│                                      │</span><br><span class="line">│  ┌──────────┐    ┌───────────────┐  │</span><br><span class="line">│  │  state   │    │  CLH 队列      │  │</span><br><span class="line">│  │ (int,    │    │  (FIFO 双向链表)│  │</span><br><span class="line">│  │  volatile)│    │  Node:thread  │  │</span><br><span class="line">│  └──────────┘    │  + waitStatus │  │</span><br><span class="line">│                   └───────────────┘  │</span><br><span class="line">│                                      │</span><br><span class="line">│  tryAcquire() / tryRelease()  ← 子类实现  │</span><br><span class="line">│  tryAcquireShared() / tryReleaseShared() │</span><br><span class="line">└─────────────────────────────────────┘</span><br></pre></td></tr></table></figure><h2 id="6-2-核心数据结构"><a href="#6-2-核心数据结构" class="headerlink" title="6.2 核心数据结构"></a>6.2 核心数据结构</h2><h3 id="state-——-同步状态"><a href="#state-——-同步状态" class="headerlink" title="state —— 同步状态"></a>state —— 同步状态</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// AQS 内部就是一个 volatile int</span></span><br><span class="line"><span class="keyword">private</span> <span class="keyword">volatile</span> <span class="type">int</span> state;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 提供三个方法操作（都是 CAS 原子操作）</span></span><br><span class="line"><span class="keyword">protected</span> <span class="keyword">final</span> <span class="type">int</span> <span class="title function_">getState</span><span class="params">()</span> &#123; <span class="keyword">return</span> state; &#125;</span><br><span class="line"><span class="keyword">protected</span> <span class="keyword">final</span> <span class="keyword">void</span> <span class="title function_">setState</span><span class="params">(<span class="type">int</span> newState)</span> &#123; state = newNode; &#125;</span><br><span class="line"><span class="keyword">protected</span> <span class="keyword">final</span> <span class="type">boolean</span> <span class="title function_">compareAndSetState</span><span class="params">(<span class="type">int</span> expect, <span class="type">int</span> update)</span> &#123;</span><br><span class="line">    <span class="comment">// 底层调用 Unsafe.compareAndSwapInt</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>不同同步器对 state 的含义不同：</p><div class="table-container"><table><thead><tr><th>同步器</th><th>state 含义</th></tr></thead><tbody><tr><td>ReentrantLock</td><td>0=未锁，&gt;0=重入次数</td></tr><tr><td>CountDownLatch</td><td>剩余计数</td></tr><tr><td>Semaphore</td><td>可用许可数</td></tr><tr><td>ReentrantReadWriteLock</td><td>高16位=读锁持有数，低16位=写锁重入数</td></tr></tbody></table></div><h3 id="CLH-队列-——-等待线程队列"><a href="#CLH-队列-——-等待线程队列" class="headerlink" title="CLH 队列 —— 等待线程队列"></a>CLH 队列 —— 等待线程队列</h3><p>AQS 内部维护一个 <strong>FIFO 双向链表</strong>（CLH 变体），每个节点是一个 Node：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">static</span> <span class="keyword">final</span> <span class="keyword">class</span> <span class="title class_">Node</span> &#123;</span><br><span class="line">    <span class="keyword">volatile</span> Node prev;       <span class="comment">// 前驱节点</span></span><br><span class="line">    <span class="keyword">volatile</span> Node next;       <span class="comment">// 后继节点</span></span><br><span class="line">    <span class="keyword">volatile</span> Thread thread;   <span class="comment">// 等待的线程</span></span><br><span class="line">    <span class="type">int</span> waitStatus;           <span class="comment">// 等待状态</span></span><br><span class="line">    <span class="comment">// waitStatus 取值：</span></span><br><span class="line">    <span class="comment">//   0: 初始状态</span></span><br><span class="line">    <span class="comment">//   SIGNAL(-1): 后继节点需要被唤醒</span></span><br><span class="line">    <span class="comment">//   CANCELLED(1): 节点已取消</span></span><br><span class="line">    <span class="comment">//   CONDITION(-2): 节点在 Condition 队列中</span></span><br><span class="line">    <span class="comment">//   PROPAGATE(-3): 共享模式下传播释放信号</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="6-3-独占模式-vs-共享模式"><a href="#6-3-独占模式-vs-共享模式" class="headerlink" title="6.3 独占模式 vs 共享模式"></a>6.3 独占模式 vs 共享模式</h2><p>AQS 支持两种资源获取模式：</p><div class="table-container"><table><thead><tr><th>模式</th><th>方法</th><th>特点</th><th>代表实现</th></tr></thead><tbody><tr><td><strong>独占（Exclusive）</strong></td><td>acquire/release</td><td>同一时刻只有一个线程持有</td><td>ReentrantLock</td></tr><tr><td><strong>共享（Shared）</strong></td><td>acquireShared/releaseShared</td><td>多个线程可同时持有</td><td>CountDownLatch, Semaphore</td></tr></tbody></table></div><h2 id="6-4-AQS-获取锁的完整流程（独占模式）"><a href="#6-4-AQS-获取锁的完整流程（独占模式）" class="headerlink" title="6.4 AQS 获取锁的完整流程（独占模式）"></a>6.4 AQS 获取锁的完整流程（独占模式）</h2><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line">acquire(arg)</span><br><span class="line">  │</span><br><span class="line">  ├─ tryAcquire(arg) ──成功──▶ 直接返回（无需入队）</span><br><span class="line">  │                  ──失败──▶ addWaiter(Node.EXCLUSIVE)  // 尾插法加入队列</span><br><span class="line">  │                               │</span><br><span class="line">  │                               ▼</span><br><span class="line">  │                        acquireQueued(node, arg)</span><br><span class="line">  │                          │</span><br><span class="line">  │                          ├─ 前驱是 head → 再 tryAcquire 一次（抢锁机会）</span><br><span class="line">  │                          │     └─ 成功 → 设置自己为 head，返回</span><br><span class="line">  │                          │     └─ 失败 → shouldParkAfterFailedAcquire()</span><br><span class="line">  │                          │              └─ 检查前驱 waitStatus</span><br><span class="line">  │                          │                 └─ SIGNAL → park（挂起，等待 unpark）</span><br><span class="line">  │                          │                 ── CANCELLED → 跳过前驱，重试</span><br><span class="line">  │                          │</span><br><span class="line">  │                          └─ 被唤醒后 → 再次循环 tryAcquire</span><br><span class="line">  │</span><br><span class="line">  └─ 如果中断 → selfInterrupt() 补上中断标记</span><br></pre></td></tr></table></figure><h2 id="6-5-ReentrantLock-如何基于-AQS-实现"><a href="#6-5-ReentrantLock-如何基于-AQS-实现" class="headerlink" title="6.5 ReentrantLock 如何基于 AQS 实现"></a>6.5 ReentrantLock 如何基于 AQS 实现</h2><p>ReentrantLock 内部有两个 AQS 子类：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// NonfairSync —— 非公平锁</span></span><br><span class="line"><span class="keyword">static</span> <span class="keyword">final</span> <span class="keyword">class</span> <span class="title class_">NonfairSync</span> <span class="keyword">extends</span> <span class="title class_">Sync</span> &#123;</span><br><span class="line">    <span class="keyword">final</span> <span class="keyword">void</span> <span class="title function_">lock</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">if</span> (compareAndSetState(<span class="number">0</span>, <span class="number">1</span>))  <span class="comment">// 先直接 CAS 抢锁！</span></span><br><span class="line">            setExclusiveOwnerThread(Thread.currentThread());</span><br><span class="line">        <span class="keyword">else</span></span><br><span class="line">            acquire(<span class="number">1</span>);  <span class="comment">// 抢不到再走 AQS 流程</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">protected</span> <span class="keyword">final</span> <span class="type">boolean</span> <span class="title function_">tryAcquire</span><span class="params">(<span class="type">int</span> acquires)</span> &#123;</span><br><span class="line">        <span class="comment">// 非公平：不管队列里有没有等待的线程，直接抢</span></span><br><span class="line">        ...</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// FairSync —— 公平锁</span></span><br><span class="line"><span class="keyword">static</span> <span class="keyword">final</span> <span class="keyword">class</span> <span class="title class_">FairSync</span> <span class="keyword">extends</span> <span class="title class_">Sync</span> &#123;</span><br><span class="line">    <span class="keyword">protected</span> <span class="keyword">final</span> <span class="type">boolean</span> <span class="title function_">tryAcquire</span><span class="params">(<span class="type">int</span> acquires)</span> &#123;</span><br><span class="line">        <span class="comment">// 公平：检查是否有前驱节点，有则放弃抢锁</span></span><br><span class="line">        <span class="keyword">final</span> <span class="type">Thread</span> <span class="variable">current</span> <span class="operator">=</span> Thread.currentThread();</span><br><span class="line">        <span class="type">int</span> <span class="variable">c</span> <span class="operator">=</span> getState();</span><br><span class="line">        <span class="keyword">if</span> (c == <span class="number">0</span>) &#123;</span><br><span class="line">            <span class="keyword">if</span> (!hasQueuedPredecessors() &amp;&amp;   <span class="comment">// ★ 关键：看队列里有没有人</span></span><br><span class="line">                compareAndSetState(<span class="number">0</span>, acquires)) &#123;</span><br><span class="line">                setExclusiveOwnerThread(current);</span><br><span class="line">                <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        ...</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><div class="tip info"><p><strong>非公平锁 vs 公平锁的性能差异</strong>：非公平锁允许”插队”（新来的线程可以直接 CAS 抢锁，不用排队），减少了线程上下文切换，吞吐量更高。公平锁严格按照 FIFO 顺序获取锁，避免了饥饿，但吞吐量低 10%~20%。<strong>默认是非公平锁</strong>——因为大多数场景更看重吞吐量而非绝对公平。</p></div><h3 id="可重入实现"><a href="#可重入实现" class="headerlink" title="可重入实现"></a>可重入实现</h3><p>ReentrantLock 的”可重入”体现在 <code>tryAcquire</code> 中判断当前线程是否已经是锁的持有者：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 简化的可重入逻辑</span></span><br><span class="line"><span class="keyword">final</span> <span class="type">boolean</span> <span class="title function_">nonfairTryAcquire</span><span class="params">(<span class="type">int</span> acquires)</span> &#123;</span><br><span class="line">    <span class="keyword">final</span> <span class="type">Thread</span> <span class="variable">current</span> <span class="operator">=</span> Thread.currentThread();</span><br><span class="line">    <span class="type">int</span> <span class="variable">c</span> <span class="operator">=</span> getState();</span><br><span class="line">    <span class="keyword">if</span> (c == <span class="number">0</span>) &#123;</span><br><span class="line">        <span class="keyword">if</span> (compareAndSetState(<span class="number">0</span>, acquires)) &#123; ... <span class="keyword">return</span> <span class="literal">true</span>; &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">else</span> <span class="keyword">if</span> (current == getExclusiveOwnerThread()) &#123;  <span class="comment">// 已经是持有者 → 重入</span></span><br><span class="line">        <span class="type">int</span> <span class="variable">nextc</span> <span class="operator">=</span> c + acquires;</span><br><span class="line">        <span class="keyword">if</span> (nextc &lt; <span class="number">0</span>) <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">Error</span>(<span class="string">&quot;Maximum lock count exceeded&quot;</span>);</span><br><span class="line">        setState(nextc);  <span class="comment">// 直接设置 state，无需 CAS（因为只有持有者才能到这里）</span></span><br><span class="line">        <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="6-6-AQS-Condition（条件变量）"><a href="#6-6-AQS-Condition（条件变量）" class="headerlink" title="6.6 AQS Condition（条件变量）"></a>6.6 AQS Condition（条件变量）</h2><p>Condition 是 AQS 提供的<strong>条件等待/通知</strong>机制，替代了 Object.wait()/notify()：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">ReentrantLock</span> <span class="variable">lock</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ReentrantLock</span>();</span><br><span class="line"><span class="type">Condition</span> <span class="variable">notFull</span> <span class="operator">=</span> lock.newCondition();   <span class="comment">// 队列不满条件</span></span><br><span class="line"><span class="type">Condition</span> <span class="variable">notEmpty</span> <span class="operator">=</span> lock.newCondition();  <span class="comment">// 队列不空条件</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 生产者</span></span><br><span class="line">lock.lock();</span><br><span class="line"><span class="keyword">try</span> &#123;</span><br><span class="line">    <span class="keyword">while</span> (count == bufferSize) notFull.await();  <span class="comment">// 队列满 → 等待</span></span><br><span class="line">    enqueue(item);</span><br><span class="line">    notEmpty.signal();                            <span class="comment">// 通知消费者</span></span><br><span class="line">&#125; <span class="keyword">finally</span> &#123; lock.unlock(); &#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 消费者</span></span><br><span class="line">lock.lock();</span><br><span class="line"><span class="keyword">try</span> &#123;</span><br><span class="line">    <span class="keyword">while</span> (count == <span class="number">0</span>) notEmpty.await();  <span class="comment">// 队列空 → 等待</span></span><br><span class="line">    item = dequeue();</span><br><span class="line">    notFull.signal();                        <span class="comment">// 通知生产者</span></span><br><span class="line">&#125; <span class="keyword">finally</span> &#123; lock.unlock(); &#125;</span><br></pre></td></tr></table></figure><blockquote><p>Condition 的优势：一个 Lock 可以创建<strong>多个</strong> Condition，实现<strong>精准唤醒</strong>（只唤醒某一类等待线程），而 Object.notify() 是随机唤醒一个。</p></blockquote><hr><h1 id="七、JUC-并发工具类"><a href="#七、JUC-并发工具类" class="headerlink" title="七、JUC 并发工具类"></a>七、JUC 并发工具类</h1><h2 id="7-1-CountDownLatch-——-倒计时门闩"><a href="#7-1-CountDownLatch-——-倒计时门闩" class="headerlink" title="7.1 CountDownLatch —— 倒计时门闩"></a>7.1 CountDownLatch —— 倒计时门闩</h2><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 场景：主线程等待 N 个子线程全部完成后汇总</span></span><br><span class="line"><span class="type">CountDownLatch</span> <span class="variable">latch</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">CountDownLatch</span>(<span class="number">3</span>);  <span class="comment">// 初始计数 = 3</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> (<span class="type">int</span> <span class="variable">i</span> <span class="operator">=</span> <span class="number">0</span>; i &lt; <span class="number">3</span>; i++) &#123;</span><br><span class="line">    <span class="keyword">new</span> <span class="title class_">Thread</span>(() -&gt; &#123;</span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            <span class="comment">// 执行任务...</span></span><br><span class="line">        &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">            latch.countDown();  <span class="comment">// 计数 -1</span></span><br><span class="line">        &#125;</span><br><span class="line">    &#125;).start();</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">latch.await();  <span class="comment">// 阻塞等待，直到计数归零</span></span><br><span class="line">System.out.println(<span class="string">&quot;所有任务完成！&quot;</span>);</span><br></pre></td></tr></table></figure><p><strong>原理</strong>：内部基于 AQS 共享模式，<code>countDown()</code> 就是 <code>releaseShared(1)</code>（state - 1），<code>await()</code> 就是 <code>acquireSharedInterruptibly(1)</code>（state != 0 则阻塞）。</p><h2 id="7-2-Semaphore-——-信号量"><a href="#7-2-Semaphore-——-信号量" class="headerlink" title="7.2 Semaphore —— 信号量"></a>7.2 Semaphore —— 信号量</h2><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 场景：限流 —— 控制同时访问某资源的线程数不超过 N</span></span><br><span class="line"><span class="type">Semaphore</span> <span class="variable">semaphore</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Semaphore</span>(<span class="number">5</span>);  <span class="comment">// 5 个许可</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> (<span class="type">int</span> <span class="variable">i</span> <span class="operator">=</span> <span class="number">0</span>; i &lt; <span class="number">20</span>; i++) &#123;</span><br><span class="line">    <span class="keyword">new</span> <span class="title class_">Thread</span>(() -&gt; &#123;</span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            semaphore.acquire();   <span class="comment">// 获取许可（阻塞式）</span></span><br><span class="line">            <span class="comment">// 访问受限资源...</span></span><br><span class="line">        &#125; <span class="keyword">catch</span> (InterruptedException e) &#123;</span><br><span class="line">            Thread.currentThread().interrupt();</span><br><span class="line">        &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">            semaphore.release();  <span class="comment">// 归还许可</span></span><br><span class="line">        &#125;</span><br><span class="line">    &#125;).start();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>原理</strong>：同样基于 AQS 共享模式，state 表示剩余许可数。<code>acquire()</code> 就是 <code>tryAcquireShared</code>（state &gt; 0 则 CAS 减 1），<code>release()</code> 就是 <code>releaseShared</code>（state + 1 并唤醒等待线程）。</p><h2 id="7-3-CyclicBarrier-——-循环栅栏"><a href="#7-3-CyclicBarrier-——-循环栅栏" class="headerlink" title="7.3 CyclicBarrier —— 循环栅栏"></a>7.3 CyclicBarrier —— 循环栅栏</h2><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 场景：N 个线程互相等待，全部到达后一起继续（可重复使用）</span></span><br><span class="line"><span class="type">CyclicBarrier</span> <span class="variable">barrier</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">CyclicBarrier</span>(<span class="number">3</span>, () -&gt; &#123;</span><br><span class="line">    System.out.println(<span class="string">&quot;所有选手到达终点，开始颁奖！&quot;</span>);</span><br><span class="line">&#125;);</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> (<span class="type">int</span> <span class="variable">i</span> <span class="operator">=</span> <span class="number">0</span>; i &lt; <span class="number">3</span>; i++) &#123;</span><br><span class="line">    <span class="keyword">final</span> <span class="type">int</span> <span class="variable">no</span> <span class="operator">=</span> i + <span class="number">1</span>;</span><br><span class="line">    <span class="keyword">new</span> <span class="title class_">Thread</span>(() -&gt; &#123;</span><br><span class="line">        System.out.println(<span class="string">&quot;选手 &quot;</span> + no + <span class="string">&quot; 出发...&quot;</span>);</span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            Thread.sleep((<span class="type">long</span>)(Math.random() * <span class="number">3000</span>));</span><br><span class="line">            System.out.println(<span class="string">&quot;选手 &quot;</span> + no + <span class="string">&quot; 到达终点&quot;</span>);</span><br><span class="line">            barrier.await();  <span class="comment">// 等待其他线程</span></span><br><span class="line">            System.out.println(<span class="string">&quot;选手 &quot;</span> + no + <span class="string">&quot; 继续下一轮比赛&quot;</span>);</span><br><span class="line">        &#125; <span class="keyword">catch</span> (Exception e) &#123; e.printStackTrace(); &#125;</span><br><span class="line">    &#125;).start();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>与 CountDownLatch 的关键区别</strong>：CyclicBarrier <strong>可以复用</strong>（<code>reset()</code> 后重新计数），且支持 <strong>barrierAction</strong>（所有线程到达后执行的回调）。底层用的是 <code>ReentrantLock + Condition</code>，不是 AQS。</p><h2 id="7-4-三大工具类对比"><a href="#7-4-三大工具类对比" class="headerlink" title="7.4 三大工具类对比"></a>7.4 三大工具类对比</h2><div class="table-container"><table><thead><tr><th>维度</th><th>CountDownLatch</th><th>Semaphore</th><th>CyclicBarrier</th></tr></thead><tbody><tr><td>计数方向</td><td>递减（归零后不可重置）</td><td>可增可减</td><td>递增（到达后自动重置）</td></tr><tr><td>是否可复用</td><td>❌ 一次性</td><td>✅</td><td>✅ 循环使用</td></tr><tr><td>底层实现</td><td>AQS 共享模式</td><td>AQS 共享模式</td><td>ReentrantLock + Condition</td></tr><tr><td>等待方</td><td>一个/多个线程等待</td><td>获取许可的线程</td><td>所有参与线程互相等待</td></tr><tr><td>典型场景</td><td>等待 N 个任务完成</td><td>限流、资源池</td><td>多阶段并行计算</td></tr></tbody></table></div><hr><h1 id="八、并发容器"><a href="#八、并发容器" class="headerlink" title="八、并发容器"></a>八、并发容器</h1><h2 id="8-1-ConcurrentHashMap（JDK-7-vs-JDK-8）"><a href="#8-1-ConcurrentHashMap（JDK-7-vs-JDK-8）" class="headerlink" title="8.1 ConcurrentHashMap（JDK 7 vs JDK 8）"></a>8.1 ConcurrentHashMap（JDK 7 vs JDK 8）</h2><p>这是面试最高频的并发容器，JDK 7 和 JDK 8 的实现完全不同：</p><div class="table-container"><table><thead><tr><th>维度</th><th>JDK 7</th><th>JDK 8</th></tr></thead><tbody><tr><td><strong>数据结构</strong></td><td>Segment + HashEntry（分段锁）</td><td>Node 数组 + CAS + synchronized</td></tr><tr><td><strong>锁粒度</strong></td><td>每个 Segment 一把锁（默认 16 段）</td><td>每个桶头节点一把锁（更细）</td></tr><tr><td><strong>锁实现</strong></td><td>ReentrantLock</td><td>synchronized + CAS</td></tr><tr><td><strong>size 计算</strong></td><td>累加各 Segment 的 count（不加锁可能不准）</td><td>baseCount + CounterCell[] 分散计数</td></tr><tr><td><strong>扩容</strong></td><td>Segment 独立扩容</td><td>多线程协助扩容（transfer）</td></tr></tbody></table></div><h3 id="JDK-8-ConcurrentHashMap-核心源码"><a href="#JDK-8-ConcurrentHashMap-核心源码" class="headerlink" title="JDK 8 ConcurrentHashMap 核心源码"></a>JDK 8 ConcurrentHashMap 核心源码</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// put 操作简化流程</span></span><br><span class="line"><span class="keyword">final</span> V <span class="title function_">putVal</span><span class="params">(K key, V value, <span class="type">boolean</span> onlyIfAbsent)</span> &#123;</span><br><span class="line">    <span class="comment">// ... hash 计算 ...</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> (Node&lt;K,V&gt;[] tab = table;;) &#123;</span><br><span class="line">        Node&lt;K,V&gt; f; <span class="type">int</span> n, i, fh;</span><br><span class="line">        <span class="keyword">if</span> (tab == <span class="literal">null</span> || (n = tab.length) == <span class="number">0</span>)</span><br><span class="line">            tab = initTable();                    <span class="comment">// ① 表空 → 初始化</span></span><br><span class="line">        <span class="keyword">else</span> <span class="keyword">if</span> ((f = tabAt(tab, i = (n - <span class="number">1</span>) &amp; hash)) == <span class="literal">null</span>) &#123;</span><br><span class="line">            <span class="keyword">if</span> (casTabAt(tab, i, <span class="literal">null</span>, <span class="keyword">new</span> <span class="title class_">Node</span>(...)))  <span class="comment">// ② 桶空 → CAS 插入</span></span><br><span class="line">                <span class="keyword">break</span>;                                   <span class="comment">//    无锁成功</span></span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">else</span> <span class="keyword">if</span> ((fh = f.hash) == MOVED)</span><br><span class="line">            tab = helpTransfer(tab, f);              <span class="comment">// ③ 正在扩容 → 协助迁移</span></span><br><span class="line">        <span class="keyword">else</span> &#123;</span><br><span class="line">            <span class="keyword">synchronized</span> (f) &#123;                       <span class="comment">// ④ 桶非空 → 加锁（锁住头节点）</span></span><br><span class="line">                <span class="comment">// 链表遍历 or 红黑树操作...</span></span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// ... addCount() → 可能触发扩容 ...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><blockquote><p>JDK 8 的设计非常精妙：<strong>大部分情况用无锁 CAS，只在哈希冲突时才降级为 synchronized 锁住单个桶</strong>。而且 synchronized 锁的是桶的头节点（Node 对象），不是整个 Segment，粒度比 JDK 7 更细。</p></blockquote><h2 id="8-2-CopyOnWriteArrayList"><a href="#8-2-CopyOnWriteArrayList" class="headerlink" title="8.2 CopyOnWriteArrayList"></a>8.2 CopyOnWriteArrayList</h2><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 写时复制列表 —— 读完全无锁，写时复制整个底层数组</span></span><br><span class="line">CopyOnWriteArrayList&lt;String&gt; list = <span class="keyword">new</span> <span class="title class_">CopyOnWriteArrayList</span>&lt;&gt;();</span><br><span class="line"></span><br><span class="line"><span class="comment">// 读操作：直接读数组，无锁（适合读多写少）</span></span><br><span class="line"><span class="type">String</span> <span class="variable">s</span> <span class="operator">=</span> list.get(<span class="number">0</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 写操作：复制新数组，修改后替换引用</span></span><br><span class="line">list.add(<span class="string">&quot;hello&quot;</span>);  <span class="comment">// 内部：Arrays.copyOf() → 新数组添加 → 替换</span></span><br></pre></td></tr></table></figure><div class="table-container"><table><thead><tr><th>优点</th><th>缺点</th></tr></thead><tbody><tr><td>读操作性能极高（无锁）</td><td>写操作需要复制整个数组，O(n) 开销</td></tr><tr><td>读写完全不互斥</td><td>数据一致性最终一致（不是强一致）</td></tr><tr><td>适合配置列表、事件监听器等读多写少场景</td><td>不适合写频繁的场景</td></tr></tbody></table></div><h2 id="8-3-BlockingQueue-家族"><a href="#8-3-BlockingQueue-家族" class="headerlink" title="8.3 BlockingQueue 家族"></a>8.3 BlockingQueue 家族</h2><div class="table-container"><table><thead><tr><th>实现类</th><th>数据结构</th><th>有界？</th><th>特点</th></tr></thead><tbody><tr><td>ArrayBlockingQueue</td><td>数组</td><td>✅ 有界</td><td>有界容量，必须指定大小</td></tr><tr><td>LinkedBlockingQueue</td><td>链表</td><td>可选有界</td><td>默认 Integer.MAX_VALUE（近似无界）</td></tr><tr><td>PriorityBlockingQueue</td><td>堆</td><td>无界</td><td>按优先级出队</td></tr><tr><td>SynchronousQueue</td><td>无缓冲</td><td>容量为 0</td><td>直接传递，不存储元素</td></tr><tr><td>DelayQueue</td><td>堆</td><td>无界</td><td>元素只有到期后才能取走</td></tr><tr><td>LinkedTransferQueue</td><td>链表</td><td>无界</td><td>transfer() 方法可直接交给消费者</td></tr></tbody></table></div><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 生产者-消费者经典模式</span></span><br><span class="line">BlockingQueue&lt;Task&gt; queue = <span class="keyword">new</span> <span class="title class_">ArrayBlockingQueue</span>&lt;&gt;(<span class="number">128</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 生产者</span></span><br><span class="line">queue.put(task);     <span class="comment">// 队列满时阻塞</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 消费者</span></span><br><span class="line"><span class="type">Task</span> <span class="variable">task</span> <span class="operator">=</span> queue.take();  <span class="comment">// 队列空时阻塞</span></span><br></pre></td></tr></table></figure><h2 id="8-4-并发容器选择指南"><a href="#8-4-并发容器选择指南" class="headerlink" title="8.4 并发容器选择指南"></a>8.4 并发容器选择指南</h2><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">需要 Map？</span><br><span class="line">  ├── 读多写少 → ConcurrentHashMap（首选）</span><br><span class="line">  ├── 要求强一致 → Collections.synchronizedMap()（性能差但简单）</span><br><span class="line">  └── 需要排序 → ConcurrentSkipListMap（跳表，有序）</span><br><span class="line"></span><br><span class="line">需要 List？</span><br><span class="line">  ├── 读多写少 → CopyOnWriteArrayList</span><br><span class="line">  └── 读写均衡 → Collections.synchronizedList() 或手动加锁</span><br><span class="line"></span><br><span class="line">需要 Queue？</span><br><span class="line">  ├── 有界队列 → ArrayBlockingQueue</span><br><span class="line">  ├── 无界队列 → LinkedBlockingQueue</span><br><span class="line">  ├── 直接传递 → SynchronousQueue</span><br><span class="line">  └── 延迟任务 → DelayQueue</span><br></pre></td></tr></table></figure><hr><h1 id="九、实战避坑清单"><a href="#九、实战避坑清单" class="headerlink" title="九、实战避坑清单"></a>九、实战避坑清单</h1><div class="note danger flat"><p>以下每一条都是线上血泪教训的总结：</p></div><div class="table-container"><table><thead><tr><th>#</th><th>坑点</th><th>正确做法</th></tr></thead><tbody><tr><td>1</td><td>用 <code>synchronized</code> 包裹耗时 IO 操作</td><td>缩小同步范围，或用读写锁分离</td></tr><tr><td>2</td><td>在 <code>synchronized</code> 块中调用外部方法（可能死锁）</td><td>同步块内只操作自己的对象</td></tr><tr><td>3</td><td><code>volatile</code> 当锁用（以为能保证 i++ 原子性）</td><td>复合操作用 Atomic 或 synchronized</td></tr><tr><td>4</td><td>忘记在 finally 中 <code>unlock()</code></td><td>始终用 <code>try-finally</code> 或 <code>try-with-resources</code></td></tr><tr><td>5</td><td>CountDownLatch 记得调用 <code>countDown()</code></td><td>放在 finally 块中，防止异常导致死等</td></tr><tr><td>6</td><td>创建线程池用 <code>Executors</code> 工厂方法</td><td>手动创建 ThreadPoolExecutor，指定合理参数</td></tr><tr><td>7</td><td>共享可变对象作为 HashMap 的 Key</td><td>Key 必须是不可变的（immutable）</td></tr><tr><td>8</td><td>在构造函数中启动新线程（this 引用逃逸）</td><td>不要在构造函数中暴露 this 给其他线程</td></tr><tr><td>9</td><td>忽略中断异常（catch 后吞掉）</td><td>至少恢复中断状态：<code>Thread.currentThread().interrupt()</code></td></tr><tr><td>10</td><td>用 <code>wait()/notify()</code> 而不用 Condition</td><td>Condition 支持多条件队列、精确唤醒，功能更强</td></tr></tbody></table></div><hr><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>本文从 JMM 内存模型出发，梳理了 Java 并发编程的完整知识链路：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">JMM（可见性/原子性/有序性）</span><br><span class="line">  ↓</span><br><span class="line">volatile（内存屏障 + MESI）←→ synchronized（锁升级：偏向→轻量→重量）</span><br><span class="line">  ↓</span><br><span class="line">CAS（CPU 原子指令）+ ABA 问题</span><br><span class="line">  ↓</span><br><span class="line">Atomic 原子类（基于 CAS + 自旋）</span><br><span class="line">  ↓</span><br><span class="line">AQS 框架（state + CLH 队列 + 独占/共享模式）</span><br><span class="line">  ↓</span><br><span class="line">  ├── ReentrantLock（可重入 + 公平/非公平 + Condition）</span><br><span class="line">  ├── CountDownLatch（一次性倒计时）</span><br><span class="line">  ├── Semaphore（信号量限流）</span><br><span class="line">  └── CyclicBarrier（循环栅栏）</span><br><span class="line">  ↓</span><br><span class="line">并发容器（ConcurrentHashMap / CopyOnWriteArrayList / BlockingQueue）</span><br></pre></td></tr></table></figure><p>掌握这套体系，你就拥有了分析和解决绝大多数 Java 并发问题的能力。下一步建议结合 <strong>JConsole / VisualVM / Arthas</strong> 观察线程状态，在实践中加深理解。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;div class=&quot;tip warning&quot;&gt;&lt;p&gt;线程池解决了”怎么管理线程”的问题，但线程池本身并不保证线程安全——它只是把任务交给线程执行。真正保证多线程正确协作的，是 volatile、synchronized、CAS 和 AQS 这套并发原语。本文从 JMM 内存模</summary>
      
    
    
    
    <category term="Java开发" scheme="http://hexo.sjava.cn/categories/Java%E5%BC%80%E5%8F%91/"/>
    
    
    <category term="Java" scheme="http://hexo.sjava.cn/tags/Java/"/>
    
    <category term="并发编程" scheme="http://hexo.sjava.cn/tags/%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B/"/>
    
    <category term="多线程" scheme="http://hexo.sjava.cn/tags/%E5%A4%9A%E7%BA%BF%E7%A8%8B/"/>
    
    <category term="AQS" scheme="http://hexo.sjava.cn/tags/AQS/"/>
    
    <category term="CAS" scheme="http://hexo.sjava.cn/tags/CAS/"/>
    
  </entry>
  
  <entry>
    <title>MySQL 安全与权限管理实战：账号权限模型、角色、SSL 与审计</title>
    <link href="http://hexo.sjava.cn/posts/73051.html"/>
    <id>http://hexo.sjava.cn/posts/73051.html</id>
    <published>2026-09-12T07:11:00.000Z</published>
    <updated>2026-09-12T07:11:00.000Z</updated>
    
    <content type="html"><![CDATA[<div class="tip warning"><p>索引、事务、复制、分库、备份解决的是“能不能跑、跑得快不快、丢不丢数据”的问题；而安全解决的是“谁能进来、进来能干什么、干了什么能不能被追溯”的问题。MySQL 的安全事故，80% 不是被黑客攻破，而是账号权限过大、密码太弱、连接不加密、操作没审计导致的。本文聚焦权限、加密与审计这三根支柱。</p></div><h1 id="一、安全设计的三个原则"><a href="#一、安全设计的三个原则" class="headerlink" title="一、安全设计的三个原则"></a>一、安全设计的三个原则</h1><p>在动手改权限之前，先统一认识：</p><ol><li><strong>最小权限原则</strong>：一个账号只拥有完成工作所需的最小权限集合，能只读就不给写，能只访问一个库就不给所有库。</li><li><strong>纵深防御原则</strong>：安全不能只靠某一层，权限 + 密码 + 网络 + 应用层 + 审计要同时生效。</li><li><strong>可追溯原则</strong>：关键操作必须留痕，知道谁在什么时间做了什么，出了问题能回溯。</li></ol><p>这三条会贯穿后面的所有配置。</p><h1 id="二、MySQL-账号与权限模型"><a href="#二、MySQL-账号与权限模型" class="headerlink" title="二、MySQL 账号与权限模型"></a>二、MySQL 账号与权限模型</h1><h2 id="2-1-user-host-的含义"><a href="#2-1-user-host-的含义" class="headerlink" title="2.1 user@host 的含义"></a>2.1 user@host 的含义</h2><p>MySQL 的账号不是单纯的用户名，而是 <strong>用户名 + 允许登录的主机</strong> 的组合。</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 三个完全不同的账号</span></span><br><span class="line"><span class="string">&#x27;app_user&#x27;</span>@<span class="string">&#x27;10.0.0.%&#x27;</span></span><br><span class="line"><span class="string">&#x27;app_user&#x27;</span>@<span class="string">&#x27;app-server-01&#x27;</span></span><br><span class="line"><span class="string">&#x27;app_user&#x27;</span>@<span class="string">&#x27;%&#x27;</span></span><br></pre></td></tr></table></figure><div class="table-container"><table><thead><tr><th>主机表达式</th><th>含义</th></tr></thead><tbody><tr><td><code>&#39;user&#39;@&#39;localhost&#39;</code></td><td>只允许本机 socket/tcp 登录</td></tr><tr><td><code>&#39;user&#39;@&#39;127.0.0.1&#39;</code></td><td>只允许本机 TCP 登录</td></tr><tr><td><code>&#39;user&#39;@&#39;10.0.0.10&#39;</code></td><td>只允许指定 IP</td></tr><tr><td><code>&#39;user&#39;@&#39;10.0.0.%&#39;</code></td><td>允许 10.0.0.0/24 网段</td></tr><tr><td><code>&#39;user&#39;@&#39;%&#39;</code></td><td>允许任意主机</td></tr><tr><td><code>&#39;user&#39;@&#39;%.example.com&#39;</code></td><td>允许 example.com 子域名</td></tr></tbody></table></div><div class="note warning flat"><p><code>&#39;user&#39;@&#39;%&#39;</code> 是生产中最危险的账号形态。即使内网，也应该按业务网段、主机名或 IP 精确控制。运维跳板机、应用服务器、备份服务器应该分别使用不同账号。</p></div><h2 id="2-2-账号创建与基础授权"><a href="#2-2-账号创建与基础授权" class="headerlink" title="2.2 账号创建与基础授权"></a>2.2 账号创建与基础授权</h2><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 创建只允许从 10.0.0.% 登录的只读账号</span></span><br><span class="line"><span class="keyword">CREATE</span> <span class="keyword">USER</span> <span class="string">&#x27;app_read&#x27;</span>@<span class="string">&#x27;10.0.0.%&#x27;</span></span><br><span class="line">  IDENTIFIED <span class="keyword">BY</span> <span class="string">&#x27;ComplexP@ssw0rd!2026&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 授予对 order 库的 SELECT 权限</span></span><br><span class="line"><span class="keyword">GRANT</span> <span class="keyword">SELECT</span> <span class="keyword">ON</span> order.<span class="operator">*</span> <span class="keyword">TO</span> <span class="string">&#x27;app_read&#x27;</span>@<span class="string">&#x27;10.0.0.%&#x27;</span>;</span><br><span class="line"></span><br><span class="line">FLUSH PRIVILEGES;</span><br></pre></td></tr></table></figure><p>常用授权粒度：</p><div class="table-container"><table><thead><tr><th>粒度</th><th>示例</th><th>适用场景</th></tr></thead><tbody><tr><td>全局</td><td><code>GRANT SELECT ON *.*</code></td><td>监控、审计账号</td></tr><tr><td>数据库</td><td><code>GRANT ALL ON order.*</code></td><td>业务应用账号</td></tr><tr><td>表级</td><td><code>GRANT SELECT, INSERT ON order.user</code></td><td>受限 ETL 账号</td></tr><tr><td>列级</td><td><code>GRANT SELECT(id, name) ON order.user</code></td><td>敏感字段脱敏</td></tr><tr><td>存储过程</td><td><code>GRANT EXECUTE ON PROCEDURE db.proc</code></td><td>只允许调用指定接口</td></tr></tbody></table></div><h2 id="2-3-权限回收"><a href="#2-3-权限回收" class="headerlink" title="2.3 权限回收"></a>2.3 权限回收</h2><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 回收某个权限</span></span><br><span class="line"><span class="keyword">REVOKE</span> <span class="keyword">DELETE</span> <span class="keyword">ON</span> order.<span class="operator">*</span> <span class="keyword">FROM</span> <span class="string">&#x27;app_read&#x27;</span>@<span class="string">&#x27;10.0.0.%&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 删除账号（连同权限）</span></span><br><span class="line"><span class="keyword">DROP</span> <span class="keyword">USER</span> <span class="string">&#x27;app_read&#x27;</span>@<span class="string">&#x27;10.0.0.%&#x27;</span>;</span><br></pre></td></tr></table></figure><div class="note info flat"><p><code>FLUSH PRIVILEGES</code> 在 MySQL 5.7/8.0 中通常不是必须的：CREATE/GRANT/REVOKE 已经会自动刷新内存中的权限表。但在手动改 <code>mysql.user</code> 表时才需要它。</p></div><h1 id="三、生产常用账号模板"><a href="#三、生产常用账号模板" class="headerlink" title="三、生产常用账号模板"></a>三、生产常用账号模板</h1><p>一个典型的生产环境，至少应该把账号拆开。</p><h2 id="3-1-应用只读账号"><a href="#3-1-应用只读账号" class="headerlink" title="3.1 应用只读账号"></a>3.1 应用只读账号</h2><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">CREATE</span> <span class="keyword">USER</span> <span class="string">&#x27;order_app_ro&#x27;</span>@<span class="string">&#x27;10.0.10.%&#x27;</span></span><br><span class="line">  IDENTIFIED <span class="keyword">BY</span> <span class="string">&#x27;ro-密码&#x27;</span>;</span><br><span class="line"><span class="keyword">GRANT</span> <span class="keyword">SELECT</span> <span class="keyword">ON</span> order.<span class="operator">*</span> <span class="keyword">TO</span> <span class="string">&#x27;order_app_ro&#x27;</span>@<span class="string">&#x27;10.0.10.%&#x27;</span>;</span><br></pre></td></tr></table></figure><p>只读账号不能写，也不能访问其他业务库，即使应用被 SQL 注入，也能把损失控制在只读范围内。</p><h2 id="3-2-应用读写账号"><a href="#3-2-应用读写账号" class="headerlink" title="3.2 应用读写账号"></a>3.2 应用读写账号</h2><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">CREATE</span> <span class="keyword">USER</span> <span class="string">&#x27;order_app_rw&#x27;</span>@<span class="string">&#x27;10.0.10.%&#x27;</span></span><br><span class="line">  IDENTIFIED <span class="keyword">BY</span> <span class="string">&#x27;rw-密码&#x27;</span>;</span><br><span class="line"><span class="keyword">GRANT</span> <span class="keyword">SELECT</span>, <span class="keyword">INSERT</span>, <span class="keyword">UPDATE</span>, <span class="keyword">DELETE</span> <span class="keyword">ON</span> order.<span class="operator">*</span></span><br><span class="line">  <span class="keyword">TO</span> <span class="string">&#x27;order_app_rw&#x27;</span>@<span class="string">&#x27;10.0.10.%&#x27;</span>;</span><br></pre></td></tr></table></figure><p>不授予 <code>DROP</code>、<code>ALTER</code>、<code>CREATE</code> 等 DDL 权限，应用不应该在线上做结构变更。</p><h2 id="3-3-备份专用账号"><a href="#3-3-备份专用账号" class="headerlink" title="3.3 备份专用账号"></a>3.3 备份专用账号</h2><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">CREATE</span> <span class="keyword">USER</span> <span class="string">&#x27;backup_user&#x27;</span>@<span class="string">&#x27;10.0.20.%&#x27;</span></span><br><span class="line">  IDENTIFIED <span class="keyword">BY</span> <span class="string">&#x27;backup-密码&#x27;</span>;</span><br><span class="line"><span class="keyword">GRANT</span> <span class="keyword">SELECT</span>, RELOAD, LOCK TABLES, REPLICATION CLIENT, <span class="keyword">SHOW</span> <span class="keyword">VIEW</span></span><br><span class="line">  <span class="keyword">ON</span> <span class="operator">*</span>.<span class="operator">*</span> <span class="keyword">TO</span> <span class="string">&#x27;backup_user&#x27;</span>@<span class="string">&#x27;10.0.20.%&#x27;</span>;</span><br></pre></td></tr></table></figure><p>权限说明：</p><div class="table-container"><table><thead><tr><th>权限</th><th>作用</th></tr></thead><tbody><tr><td><code>SELECT</code></td><td>读取数据</td></tr><tr><td><code>RELOAD</code></td><td>执行 <code>FLUSH TABLES</code></td></tr><tr><td><code>LOCK TABLES</code></td><td>一致性锁定（mysqldump）</td></tr><tr><td><code>REPLICATION CLIENT</code></td><td>查看 binlog 位点</td></tr><tr><td><code>SHOW VIEW</code></td><td>导出视图定义</td></tr></tbody></table></div><h2 id="3-4-监控与审计账号"><a href="#3-4-监控与审计账号" class="headerlink" title="3.4 监控与审计账号"></a>3.4 监控与审计账号</h2><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">CREATE</span> <span class="keyword">USER</span> <span class="string">&#x27;monitor&#x27;</span>@<span class="string">&#x27;10.0.30.%&#x27;</span></span><br><span class="line">  IDENTIFIED <span class="keyword">BY</span> <span class="string">&#x27;monitor-密码&#x27;</span>;</span><br><span class="line"><span class="keyword">GRANT</span> <span class="keyword">SELECT</span> <span class="keyword">ON</span> performance_schema.<span class="operator">*</span> <span class="keyword">TO</span> <span class="string">&#x27;monitor&#x27;</span>@<span class="string">&#x27;10.0.30.%&#x27;</span>;</span><br><span class="line"><span class="keyword">GRANT</span> <span class="keyword">SELECT</span> <span class="keyword">ON</span> sys.<span class="operator">*</span> <span class="keyword">TO</span> <span class="string">&#x27;monitor&#x27;</span>@<span class="string">&#x27;10.0.30.%&#x27;</span>;</span><br><span class="line"><span class="keyword">GRANT</span> REPLICATION CLIENT <span class="keyword">ON</span> <span class="operator">*</span>.<span class="operator">*</span> <span class="keyword">TO</span> <span class="string">&#x27;monitor&#x27;</span>@<span class="string">&#x27;10.0.30.%&#x27;</span>;</span><br></pre></td></tr></table></figure><p>监控账号不需要写权限，也不需要访问业务数据表。</p><h2 id="3-5-DBA-管理账号"><a href="#3-5-DBA-管理账号" class="headerlink" title="3.5 DBA 管理账号"></a>3.5 DBA 管理账号</h2><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">CREATE</span> <span class="keyword">USER</span> <span class="string">&#x27;dba_admin&#x27;</span>@<span class="string">&#x27;10.0.0.10&#x27;</span></span><br><span class="line">  IDENTIFIED <span class="keyword">BY</span> <span class="string">&#x27;dba-强密码&#x27;</span>;</span><br><span class="line"><span class="keyword">GRANT</span> <span class="keyword">ALL</span> PRIVILEGES <span class="keyword">ON</span> <span class="operator">*</span>.<span class="operator">*</span> <span class="keyword">TO</span> <span class="string">&#x27;dba_admin&#x27;</span>@<span class="string">&#x27;10.0.0.10&#x27;</span></span><br><span class="line">  <span class="keyword">WITH</span> <span class="keyword">GRANT</span> OPTION;</span><br></pre></td></tr></table></figure><div class="note warning flat"><p>DBA 账号应该：从固定堡垒机/跳板机 IP 登录；启用审计；禁用 <code>%</code> 主机；尽量使用 <code>-ssl-mode=REQUIRED</code> 连接。不要把 root 当 DBA 日常账号用。</p></div><h1 id="四、MySQL-8-0-角色（Role）"><a href="#四、MySQL-8-0-角色（Role）" class="headerlink" title="四、MySQL 8.0 角色（Role）"></a>四、MySQL 8.0 角色（Role）</h1><p>角色让权限管理从“每人一份权限清单”变成“按岗位分组授权”。</p><h2 id="4-1-创建并激活角色"><a href="#4-1-创建并激活角色" class="headerlink" title="4.1 创建并激活角色"></a>4.1 创建并激活角色</h2><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 创建角色</span></span><br><span class="line"><span class="keyword">CREATE</span> ROLE <span class="string">&#x27;app_read_role&#x27;</span>, <span class="string">&#x27;app_write_role&#x27;</span>, <span class="string">&#x27;backup_role&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 给角色授权</span></span><br><span class="line"><span class="keyword">GRANT</span> <span class="keyword">SELECT</span> <span class="keyword">ON</span> order.<span class="operator">*</span> <span class="keyword">TO</span> <span class="string">&#x27;app_read_role&#x27;</span>;</span><br><span class="line"><span class="keyword">GRANT</span> <span class="keyword">SELECT</span>, <span class="keyword">INSERT</span>, <span class="keyword">UPDATE</span>, <span class="keyword">DELETE</span> <span class="keyword">ON</span> order.<span class="operator">*</span> <span class="keyword">TO</span> <span class="string">&#x27;app_write_role&#x27;</span>;</span><br><span class="line"><span class="keyword">GRANT</span> <span class="keyword">SELECT</span>, RELOAD, LOCK TABLES <span class="keyword">ON</span> <span class="operator">*</span>.<span class="operator">*</span> <span class="keyword">TO</span> <span class="string">&#x27;backup_role&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 把角色授予用户</span></span><br><span class="line"><span class="keyword">GRANT</span> <span class="string">&#x27;app_read_role&#x27;</span> <span class="keyword">TO</span> <span class="string">&#x27;order_app_ro&#x27;</span>@<span class="string">&#x27;10.0.10.%&#x27;</span>;</span><br><span class="line"><span class="keyword">GRANT</span> <span class="string">&#x27;app_write_role&#x27;</span> <span class="keyword">TO</span> <span class="string">&#x27;order_app_rw&#x27;</span>@<span class="string">&#x27;10.0.10.%&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 设置用户默认激活角色（登录时自动生效）</span></span><br><span class="line"><span class="keyword">SET</span> <span class="keyword">DEFAULT</span> ROLE <span class="string">&#x27;app_read_role&#x27;</span> <span class="keyword">TO</span> <span class="string">&#x27;order_app_ro&#x27;</span>@<span class="string">&#x27;10.0.10.%&#x27;</span>;</span><br><span class="line"><span class="keyword">SET</span> <span class="keyword">DEFAULT</span> ROLE <span class="string">&#x27;app_write_role&#x27;</span> <span class="keyword">TO</span> <span class="string">&#x27;order_app_rw&#x27;</span>@<span class="string">&#x27;10.0.10.%&#x27;</span>;</span><br></pre></td></tr></table></figure><h2 id="4-2-激活角色与查看"><a href="#4-2-激活角色与查看" class="headerlink" title="4.2 激活角色与查看"></a>4.2 激活角色与查看</h2><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 当前会话激活所有已授权角色</span></span><br><span class="line"><span class="keyword">SET</span> ROLE <span class="keyword">ALL</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 查看当前用户的角色和权限</span></span><br><span class="line"><span class="keyword">SHOW</span> GRANTS;</span><br><span class="line"><span class="keyword">SHOW</span> GRANTS <span class="keyword">FOR</span> <span class="built_in">CURRENT_USER</span> <span class="keyword">USING</span> <span class="string">&#x27;app_read_role&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 查看角色成员</span></span><br><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> mysql.role_edges;</span><br></pre></td></tr></table></figure><div class="note green anzhiyufont anzhiyu-icon-chenggong disabled"><p>角色特别适合中台或 SaaS 场景：每个业务线创建一个库，给该库配一组读写/只读角色，新成员入职只授予角色，离职只回收角色，避免在 N 个库里反复改权限。</p></div><h1 id="五、密码策略"><a href="#五、密码策略" class="headerlink" title="五、密码策略"></a>五、密码策略</h1><p>弱密码是数据库被入侵最常见的原因之一。MySQL 提供 <code>validate_password</code> 插件强制密码复杂度。</p><h2 id="5-1-查看与启用"><a href="#5-1-查看与启用" class="headerlink" title="5.1 查看与启用"></a>5.1 查看与启用</h2><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- MySQL 8.0 默认已安装</span></span><br><span class="line"><span class="keyword">SHOW</span> VARIABLES <span class="keyword">LIKE</span> <span class="string">&#x27;validate_password%&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 关键参数</span></span><br><span class="line"><span class="operator">|</span> Variable_name                          <span class="operator">|</span> <span class="keyword">Value</span>  <span class="operator">|</span></span><br><span class="line"><span class="operator">|</span><span class="comment">----------------------------------------|--------|</span></span><br><span class="line"><span class="operator">|</span> validate_password.check_user_name      <span class="operator">|</span> <span class="keyword">ON</span>     <span class="operator">|</span></span><br><span class="line"><span class="operator">|</span> validate_password.length                 <span class="operator">|</span> <span class="number">8</span>      <span class="operator">|</span></span><br><span class="line"><span class="operator">|</span> validate_password.mixed_case_count       <span class="operator">|</span> <span class="number">1</span>      <span class="operator">|</span></span><br><span class="line"><span class="operator">|</span> validate_password.number_count           <span class="operator">|</span> <span class="number">1</span>      <span class="operator">|</span></span><br><span class="line"><span class="operator">|</span> validate_password.special_char_count     <span class="operator">|</span> <span class="number">1</span>      <span class="operator">|</span></span><br><span class="line"><span class="operator">|</span> validate_password.policy                 <span class="operator">|</span> MEDIUM <span class="operator">|</span></span><br></pre></td></tr></table></figure><h2 id="5-2-调整策略"><a href="#5-2-调整策略" class="headerlink" title="5.2 调整策略"></a>5.2 调整策略</h2><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">SET</span> <span class="keyword">GLOBAL</span> validate_password.policy <span class="operator">=</span> STRONG;</span><br><span class="line"><span class="keyword">SET</span> <span class="keyword">GLOBAL</span> validate_password.length <span class="operator">=</span> <span class="number">16</span>;</span><br><span class="line"><span class="keyword">SET</span> <span class="keyword">GLOBAL</span> validate_password.mixed_case_count <span class="operator">=</span> <span class="number">1</span>;</span><br><span class="line"><span class="keyword">SET</span> <span class="keyword">GLOBAL</span> validate_password.number_count <span class="operator">=</span> <span class="number">1</span>;</span><br><span class="line"><span class="keyword">SET</span> <span class="keyword">GLOBAL</span> validate_password.special_char_count <span class="operator">=</span> <span class="number">1</span>;</span><br></pre></td></tr></table></figure><p>要永久生效，写入 <code>my.cnf</code>：</p><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">[mysqld]</span></span><br><span class="line"><span class="attr">plugin-load-add</span> = validate_password.so</span><br><span class="line"><span class="attr">validate_password.policy</span> = STRONG</span><br><span class="line"><span class="attr">validate_password.length</span> = <span class="number">16</span></span><br><span class="line"><span class="attr">validate_password.mixed_case_count</span> = <span class="number">1</span></span><br><span class="line"><span class="attr">validate_password.number_count</span> = <span class="number">1</span></span><br><span class="line"><span class="attr">validate_password.special_char_count</span> = <span class="number">1</span></span><br></pre></td></tr></table></figure><h2 id="5-3-密码过期与历史"><a href="#5-3-密码过期与历史" class="headerlink" title="5.3 密码过期与历史"></a>5.3 密码过期与历史</h2><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 创建账号时设置 90 天必须改密码</span></span><br><span class="line"><span class="keyword">CREATE</span> <span class="keyword">USER</span> <span class="string">&#x27;app_user&#x27;</span>@<span class="string">&#x27;10.0.10.%&#x27;</span></span><br><span class="line">  IDENTIFIED <span class="keyword">BY</span> <span class="string">&#x27;TmpPass!2026&#x27;</span></span><br><span class="line">  PASSWORD EXPIRE <span class="type">INTERVAL</span> <span class="number">90</span> <span class="keyword">DAY</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 保留最近 6 次密码，禁止复用</span></span><br><span class="line"><span class="keyword">ALTER</span> <span class="keyword">USER</span> <span class="string">&#x27;app_user&#x27;</span>@<span class="string">&#x27;10.0.10.%&#x27;</span> PASSWORD HISTORY <span class="number">6</span>;</span><br></pre></td></tr></table></figure><div class="note warning flat"><p>密码过期策略要配合应用发布流程：如果应用账号密码过期而应用没更新配置，会直接连接失败。建议用 Vault、KMS 或配置中心做密码轮转，不要硬编码在配置文件中。</p></div><h2 id="5-4-认证插件选择"><a href="#5-4-认证插件选择" class="headerlink" title="5.4 认证插件选择"></a>5.4 认证插件选择</h2><p>MySQL 8.0 默认认证插件从 <code>mysql_native_password</code> 改为 <code>caching_sha2_password</code>，安全性更高，但老旧驱动可能不支持。</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 查看默认认证插件</span></span><br><span class="line"><span class="keyword">SHOW</span> VARIABLES <span class="keyword">LIKE</span> <span class="string">&#x27;default_authentication_plugin&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 为兼容老驱动，指定某用户使用原生密码</span></span><br><span class="line"><span class="keyword">CREATE</span> <span class="keyword">USER</span> <span class="string">&#x27;legacy_app&#x27;</span>@<span class="string">&#x27;10.0.10.%&#x27;</span></span><br><span class="line">  IDENTIFIED <span class="keyword">WITH</span> mysql_native_password <span class="keyword">BY</span> <span class="string">&#x27;密码&#x27;</span>;</span><br></pre></td></tr></table></figure><div class="table-container"><table><thead><tr><th>插件</th><th>安全性</th><th>兼容性</th></tr></thead><tbody><tr><td><code>caching_sha2_password</code></td><td>高</td><td>MySQL 8.0+ 驱动</td></tr><tr><td><code>mysql_native_password</code></td><td>中</td><td>兼容旧驱动</td></tr><tr><td><code>sha256_password</code></td><td>高</td><td>不推荐新项目</td></tr></tbody></table></div><div class="tip "><p>新项目优先用 <code>caching_sha2_password</code>；只有历史系统确实升级不了驱动时，才为特定账号降级为 <code>mysql_native_password</code>。</p></div><h1 id="六、SSL-TLS-加密连接"><a href="#六、SSL-TLS-加密连接" class="headerlink" title="六、SSL/TLS 加密连接"></a>六、SSL/TLS 加密连接</h1><p>默认情况下，客户端到 MySQL 的报文是明文的，在内网抓包就能读到 SQL 和结果。开启 SSL 是生产安全基线。</p><h2 id="6-1-检查-SSL-状态"><a href="#6-1-检查-SSL-状态" class="headerlink" title="6.1 检查 SSL 状态"></a>6.1 检查 SSL 状态</h2><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">SHOW</span> VARIABLES <span class="keyword">LIKE</span> <span class="string">&#x27;%ssl%&#x27;</span>;</span><br><span class="line"><span class="keyword">SHOW</span> STATUS <span class="keyword">LIKE</span> <span class="string">&#x27;Ssl_cipher&#x27;</span>;</span><br></pre></td></tr></table></figure><p>如果 <code>have_ssl</code> 为 <code>YES</code>，说明服务器已启用 SSL。</p><h2 id="6-2-服务端启用-SSL"><a href="#6-2-服务端启用-SSL" class="headerlink" title="6.2 服务端启用 SSL"></a>6.2 服务端启用 SSL</h2><p>MySQL 8.0 通常自带自签名证书，也可以替换为企业 CA 签发的证书。</p><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">[mysqld]</span></span><br><span class="line"><span class="attr">require_secure_transport</span> = <span class="literal">ON</span></span><br><span class="line"><span class="attr">ssl_ca</span>   = /etc/mysql/ssl/ca.pem</span><br><span class="line"><span class="attr">ssl_cert</span> = /etc/mysql/ssl/server-cert.pem</span><br><span class="line"><span class="attr">ssl_ssl_key</span> = /etc/mysql/ssl/server-key.pem</span><br></pre></td></tr></table></figure><p><code>require_secure_transport = ON</code> 表示所有连接必须使用 SSL 或 Unix socket，拒绝明文 TCP。</p><div class="note info flat"><p>自签名证书可以加密传输，但无法做服务端身份认证。高安全场景建议用内部 CA 签发，并在客户端配置 <code>ssl_ca</code> 校验服务器证书。</p></div><h2 id="6-3-客户端强制-SSL"><a href="#6-3-客户端强制-SSL" class="headerlink" title="6.3 客户端强制 SSL"></a>6.3 客户端强制 SSL</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 命令行</span></span><br><span class="line">mysql -u app_user -p -h db.example.com --ssl-mode=REQUIRED</span><br><span class="line"></span><br><span class="line"><span class="comment"># SSL 模式说明</span></span><br><span class="line">--ssl-mode=DISABLED     <span class="comment"># 明文</span></span><br><span class="line">--ssl-mode=PREFERRED    <span class="comment"># 优先 SSL，失败则明文（默认，不安全）</span></span><br><span class="line">--ssl-mode=REQUIRED     <span class="comment"># 必须 SSL，但不校验证书</span></span><br><span class="line">--ssl-mode=VERIFY_CA    <span class="comment"># 必须 SSL 且校验 CA</span></span><br><span class="line">--ssl-mode=VERIFY_IDENTITY <span class="comment"># 校验 CA + 主机名</span></span><br></pre></td></tr></table></figure><p>JDBC 连接示例：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">String</span> <span class="variable">url</span> <span class="operator">=</span> <span class="string">&quot;jdbc:mysql://db.example.com:3306/order?&quot;</span> +</span><br><span class="line">             <span class="string">&quot;useSSL=true&amp;requireSSL=true&amp;verifyServerCertificate=true&amp;&quot;</span> +</span><br><span class="line">             <span class="string">&quot;trustCertificateKeyStoreUrl=file:/path/to/truststore.jks&quot;</span>;</span><br></pre></td></tr></table></figure><h2 id="6-4-账号级别强制-SSL"><a href="#6-4-账号级别强制-SSL" class="headerlink" title="6.4 账号级别强制 SSL"></a>6.4 账号级别强制 SSL</h2><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 该账号必须从 SSL 连接</span></span><br><span class="line"><span class="keyword">ALTER</span> <span class="keyword">USER</span> <span class="string">&#x27;app_user&#x27;</span>@<span class="string">&#x27;10.0.10.%&#x27;</span> REQUIRE SSL;</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 要求指定证书 CN</span></span><br><span class="line"><span class="keyword">ALTER</span> <span class="keyword">USER</span> <span class="string">&#x27;app_user&#x27;</span>@<span class="string">&#x27;10.0.10.%&#x27;</span> REQUIRE X509;</span><br></pre></td></tr></table></figure><div class="table-container"><table><thead><tr><th>REQUIRE 类型</th><th>含义</th></tr></thead><tbody><tr><td><code>NONE</code></td><td>不强制</td></tr><tr><td><code>SSL</code></td><td>必须 SSL</td></tr><tr><td><code>X509</code></td><td>必须 SSL 且客户端提供有效 X509 证书</td></tr><tr><td><code>ISSUER &#39;...&#39;</code></td><td>必须指定 CA 签发</td></tr><tr><td><code>SUBJECT &#39;...&#39;</code></td><td>必须指定证书主题</td></tr><tr><td><code>CIPHER &#39;...&#39;</code></td><td>强制加密套件</td></tr></tbody></table></div><h1 id="七、连接层与网络加固"><a href="#七、连接层与网络加固" class="headerlink" title="七、连接层与网络加固"></a>七、连接层与网络加固</h1><h2 id="7-1-绑定监听地址"><a href="#7-1-绑定监听地址" class="headerlink" title="7.1 绑定监听地址"></a>7.1 绑定监听地址</h2><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">[mysqld]</span></span><br><span class="line"><span class="attr">bind-address</span> = <span class="number">10.0</span>.<span class="number">0.5</span></span><br><span class="line"><span class="comment"># 或者只监听本机</span></span><br><span class="line"><span class="comment"># bind-address = 127.0.0.1</span></span><br></pre></td></tr></table></figure><p>云服务器不要把 MySQL 端口暴露在公网。即使是内网，也应该放在独立的数据库子网，通过网络 ACL 限制来源。</p><h2 id="7-2-关闭危险功能"><a href="#7-2-关闭危险功能" class="headerlink" title="7.2 关闭危险功能"></a>7.2 关闭危险功能</h2><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">[mysqld]</span></span><br><span class="line"><span class="comment"># 禁止通过 LOAD DATA INFILE 读取服务器本地任意文件</span></span><br><span class="line"><span class="attr">local_infile</span> = <span class="number">0</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 关闭符号链接（防止目录遍历）</span></span><br><span class="line"><span class="attr">symbolic-links</span> = <span class="number">0</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 禁用危险函数（需要安全插件或 SQL 模式配合）</span></span><br></pre></td></tr></table></figure><h2 id="7-3-安全-SQL-模式"><a href="#7-3-安全-SQL-模式" class="headerlink" title="7.3 安全 SQL 模式"></a>7.3 安全 SQL 模式</h2><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">SET</span> <span class="keyword">GLOBAL</span> sql_mode <span class="operator">=</span> <span class="string">&#x27;STRICT_TRANS_TABLES,NO_ZERO_DATE,NO_ZERO_IN_DATE,ONLY_FULL_GROUP_BY,ERROR_FOR_DIVISION_BY_ZERO&#x27;</span>;</span><br></pre></td></tr></table></figure><p>另外，可以在个人会话开启安全更新模式，防止误删：</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">SET</span> sql_safe_updates <span class="operator">=</span> <span class="keyword">ON</span>;</span><br><span class="line"><span class="comment">-- 之后没有 WHERE 条件的 UPDATE/DELETE 会被拒绝</span></span><br></pre></td></tr></table></figure><h2 id="7-4-失败登录与连接限制"><a href="#7-4-失败登录与连接限制" class="headerlink" title="7.4 失败登录与连接限制"></a>7.4 失败登录与连接限制</h2><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">[mysqld]</span></span><br><span class="line"><span class="comment"># 失败 5 次锁定账号（需安装 connection_control 插件）</span></span><br><span class="line"><span class="attr">connection_control_min_connection_delay</span> = <span class="number">1000</span></span><br><span class="line"><span class="attr">connection_control_max_connection_delay</span> = <span class="number">60000</span></span><br><span class="line"><span class="attr">connection_control_failed_connections_threshold</span> = <span class="number">5</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 最大连接数</span></span><br><span class="line"><span class="attr">max_connections</span> = <span class="number">1000</span></span><br><span class="line"><span class="attr">max_user_connections</span> = <span class="number">100</span></span><br></pre></td></tr></table></figure><h1 id="八、审计：知道“谁做了什么”"><a href="#八、审计：知道“谁做了什么”" class="headerlink" title="八、审计：知道“谁做了什么”"></a>八、审计：知道“谁做了什么”</h1><h2 id="8-1-企业版审计插件（Enterprise-Audit）"><a href="#8-1-企业版审计插件（Enterprise-Audit）" class="headerlink" title="8.1 企业版审计插件（Enterprise Audit）"></a>8.1 企业版审计插件（Enterprise Audit）</h2><p>MySQL 企业版提供 <code>audit_log</code> 插件，开箱即用：</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 安装插件（企业版）</span></span><br><span class="line">INSTALL PLUGIN audit_log SONAME <span class="string">&#x27;audit_log.so&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 配置审计策略</span></span><br><span class="line"><span class="keyword">SET</span> <span class="keyword">GLOBAL</span> audit_log_policy <span class="operator">=</span> <span class="string">&#x27;ALL&#x27;</span>;  <span class="comment">-- ALL / LOGINS / QUERIES / NONE</span></span><br></pre></td></tr></table></figure><h2 id="8-2-社区版审计方案"><a href="#8-2-社区版审计方案" class="headerlink" title="8.2 社区版审计方案"></a>8.2 社区版审计方案</h2><p>社区版没有官方审计插件，常用替代方案：</p><div class="table-container"><table><thead><tr><th>方案</th><th>原理</th><th>优缺点</th></tr></thead><tbody><tr><td>通用日志 <code>general_log</code></td><td>记录所有 SQL</td><td>性能开销大，明文敏感信息</td></tr><tr><td>慢查询日志</td><td>只记录慢 SQL</td><td>无法覆盖全部操作</td></tr><tr><td><code>init_connect</code></td><td>连接时执行审计语句</td><td>只能记录连接，不记录 SQL</td></tr><tr><td>Percona Audit Log Plugin</td><td>第三方审计插件</td><td>功能接近企业版</td></tr><tr><td>数据库审计网关/旁路镜像</td><td>在网络层抓包解析</td><td>性能好，部署成本高</td></tr></tbody></table></div><h3 id="通用日志"><a href="#通用日志" class="headerlink" title="通用日志"></a>通用日志</h3><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">[mysqld]</span></span><br><span class="line"><span class="attr">general_log</span> = <span class="number">1</span></span><br><span class="line"><span class="attr">general_log_file</span> = /var/log/mysql/general.log</span><br></pre></td></tr></table></figure><div class="note warning flat"><p><code>general_log</code> 会记录完整 SQL 文本，包括可能的密码参数，开启前务必评估合规要求，并配置好日志轮转和访问权限（600）。</p></div><h3 id="init-connect-轻量方案"><a href="#init-connect-轻量方案" class="headerlink" title="init_connect 轻量方案"></a>init_connect 轻量方案</h3><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">[mysqld]</span></span><br><span class="line"><span class="attr">init_connect</span> = <span class="string">&quot;INSERT INTO audit.audit_connection(user,host,login_time,program_name) VALUES (CURRENT_USER(), CONNECTION_ID(), NOW(), @@program_name)&quot;</span></span><br></pre></td></tr></table></figure><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 创建审计连接表</span></span><br><span class="line"><span class="keyword">CREATE</span> DATABASE IF <span class="keyword">NOT</span> <span class="keyword">EXISTS</span> audit;</span><br><span class="line"><span class="keyword">CREATE</span> <span class="keyword">TABLE</span> audit.audit_connection (</span><br><span class="line">  id <span class="type">BIGINT</span> AUTO_INCREMENT <span class="keyword">PRIMARY</span> KEY,</span><br><span class="line">  <span class="keyword">user</span> <span class="type">VARCHAR</span>(<span class="number">128</span>),</span><br><span class="line">  host <span class="type">VARCHAR</span>(<span class="number">128</span>),</span><br><span class="line">  login_time DATETIME,</span><br><span class="line">  program_name <span class="type">VARCHAR</span>(<span class="number">128</span>)</span><br><span class="line">);</span><br></pre></td></tr></table></figure><p>这种方式只能记录“谁连上来了”，无法记录具体执行的 SQL，适合轻量合规场景。</p><h2 id="8-3-审计日志应该关注什么"><a href="#8-3-审计日志应该关注什么" class="headerlink" title="8.3 审计日志应该关注什么"></a>8.3 审计日志应该关注什么</h2><p>审计不是为了存所有 SQL，而是为了回答三个问题：</p><ol><li><strong>谁</strong>在访问数据库？（user + host + program_name + 时间）</li><li>有没有<strong>异常权限提升</strong>？（GRANT、CREATE USER 操作）</li><li>有没有<strong>危险操作</strong>？（DROP、TRUNCATE、DELETE 不带 WHERE、批量导出）</li></ol><p>建议把审计日志同步到 SIEM 或 ELK，设置告警规则。</p><h1 id="九、应用层防-SQL-注入"><a href="#九、应用层防-SQL-注入" class="headerlink" title="九、应用层防 SQL 注入"></a>九、应用层防 SQL 注入</h1><p>数据库权限再严，也拦不住拼接 SQL。防注入的核心是<strong>参数化查询</strong>。</p><h2 id="9-1-错误示例"><a href="#9-1-错误示例" class="headerlink" title="9.1 错误示例"></a>9.1 错误示例</h2><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 拼接用户输入，极度危险</span></span><br><span class="line"><span class="type">String</span> <span class="variable">sql</span> <span class="operator">=</span> <span class="string">&quot;SELECT * FROM user WHERE phone = &#x27;&quot;</span> + phone + <span class="string">&quot;&#x27;&quot;</span>;</span><br></pre></td></tr></table></figure><p>如果 <code>phone</code> 传入 <code>&#39; OR &#39;1&#39;=&#39;1</code>，整个条件被改写。</p><h2 id="9-2-正确示例"><a href="#9-2-正确示例" class="headerlink" title="9.2 正确示例"></a>9.2 正确示例</h2><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">String</span> <span class="variable">sql</span> <span class="operator">=</span> <span class="string">&quot;SELECT * FROM user WHERE phone = ?&quot;</span>;</span><br><span class="line"><span class="type">PreparedStatement</span> <span class="variable">ps</span> <span class="operator">=</span> conn.prepareStatement(sql);</span><br><span class="line">ps.setString(<span class="number">1</span>, phone);</span><br><span class="line"><span class="type">ResultSet</span> <span class="variable">rs</span> <span class="operator">=</span> ps.executeQuery();</span><br></pre></td></tr></table></figure><p>预编译语句把数据和 SQL 结构分离，任何输入都只被当作参数值处理。</p><h2 id="9-3-数据库层补充"><a href="#9-3-数据库层补充" class="headerlink" title="9.3 数据库层补充"></a>9.3 数据库层补充</h2><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 应用账号只授予最小权限，即使被注入，也无法 DROP/CREATE</span></span><br><span class="line"><span class="keyword">GRANT</span> <span class="keyword">SELECT</span>, <span class="keyword">INSERT</span>, <span class="keyword">UPDATE</span> <span class="keyword">ON</span> order.<span class="operator">*</span> <span class="keyword">TO</span> <span class="string">&#x27;app_user&#x27;</span>@<span class="string">&#x27;10.0.10.%&#x27;</span>;</span><br></pre></td></tr></table></figure><div class="note green anzhiyufont anzhiyu-icon-chenggong disabled"><p>权限是最后一道闸门，不是第一道防线。安全优先级：<strong>参数化查询 &gt; 输入校验 &gt; WAF &gt; 最小权限</strong>。</p></div><h1 id="十、账号生命周期管理"><a href="#十、账号生命周期管理" class="headerlink" title="十、账号生命周期管理"></a>十、账号生命周期管理</h1><h2 id="10-1-定期盘点账号"><a href="#10-1-定期盘点账号" class="headerlink" title="10.1 定期盘点账号"></a>10.1 定期盘点账号</h2><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 列出所有非系统账号</span></span><br><span class="line"><span class="keyword">SELECT</span> <span class="keyword">user</span>, host, plugin, password_expired, account_locked</span><br><span class="line"><span class="keyword">FROM</span> mysql.user</span><br><span class="line"><span class="keyword">WHERE</span> <span class="keyword">user</span> <span class="keyword">NOT</span> <span class="keyword">IN</span> (<span class="string">&#x27;mysql.infoschema&#x27;</span>, <span class="string">&#x27;mysql.session&#x27;</span>, <span class="string">&#x27;mysql.sys&#x27;</span>, <span class="string">&#x27;root&#x27;</span>);</span><br></pre></td></tr></table></figure><h2 id="10-2-锁定-解锁账号"><a href="#10-2-锁定-解锁账号" class="headerlink" title="10.2 锁定/解锁账号"></a>10.2 锁定/解锁账号</h2><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 离职员工账号锁定</span></span><br><span class="line"><span class="keyword">ALTER</span> <span class="keyword">USER</span> <span class="string">&#x27;old_dev&#x27;</span>@<span class="string">&#x27;10.0.10.%&#x27;</span> ACCOUNT LOCK;</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 重新启用</span></span><br><span class="line"><span class="keyword">ALTER</span> <span class="keyword">USER</span> <span class="string">&#x27;old_dev&#x27;</span>@<span class="string">&#x27;10.0.10.%&#x27;</span> ACCOUNT UNLOCK;</span><br></pre></td></tr></table></figure><h2 id="10-3-清理匿名账号和空密码"><a href="#10-3-清理匿名账号和空密码" class="headerlink" title="10.3 清理匿名账号和空密码"></a>10.3 清理匿名账号和空密码</h2><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 删除匿名账号</span></span><br><span class="line"><span class="keyword">DROP</span> <span class="keyword">USER</span> <span class="string">&#x27;&#x27;</span>@<span class="string">&#x27;localhost&#x27;</span>;</span><br><span class="line"><span class="keyword">DROP</span> <span class="keyword">USER</span> <span class="string">&#x27;&#x27;</span>@<span class="string">&#x27;%&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 给 root 设置强密码并限制 host</span></span><br><span class="line"><span class="keyword">ALTER</span> <span class="keyword">USER</span> <span class="string">&#x27;root&#x27;</span>@<span class="string">&#x27;localhost&#x27;</span> IDENTIFIED <span class="keyword">BY</span> <span class="string">&#x27;强密码&#x27;</span>;</span><br></pre></td></tr></table></figure><h2 id="10-4-密码轮换流程"><a href="#10-4-密码轮换流程" class="headerlink" title="10.4 密码轮换流程"></a>10.4 密码轮换流程</h2><ol><li>在 KMS/Vault 生成新密码。</li><li>更新 MySQL 账号密码。</li><li>更新应用配置中心或环境变量。</li><li>滚动重启应用实例。</li><li>旧密码保留观察一个版本周期后废弃。</li></ol><h1 id="十一、生产安全-checklist"><a href="#十一、生产安全-checklist" class="headerlink" title="十一、生产安全 checklist"></a>十一、生产安全 checklist</h1><ul><li>[ ] root 账号强密码，且 <code>host</code> 限定为 <code>localhost</code> 或堡垒机 IP。</li><li>[ ] 删除匿名账号、测试账号、空密码账号。</li><li>[ ] 应用账号按“只读 / 读写 / 备份 / 监控 / DBA”拆分。</li><li>[ ] 不使用 <code>&#39;user&#39;@&#39;%&#39;</code> 形态，按 IP 段或主机名精确授权。</li><li>[ ] 启用 <code>validate_password</code> 并设置 MEDIUM/STRONG 策略。</li><li>[ ] 开启 SSL，账号级别 <code>REQUIRE SSL</code>，禁用明文连接。</li><li>[ ] MySQL 不监听公网，数据库子网通过安全组限制来源。</li><li>[ ] 应用使用参数化查询（PreparedStatement）。</li><li>[ ] 按需开启审计：通用日志、Percona Audit 或网络层审计。</li><li>[ ] 定期 review <code>mysql.user</code>、<code>mysql.db</code>、<code>mysql.role_edges</code>。</li><li>[ ] 关键账号启用密码过期和登录失败锁定。</li><li>[ ] 备份账号最小权限，备份文件加密并异地保存。</li></ul><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>MySQL 安全不是装一个插件或改一个密码就能解决的。它的核心是：</p><ul><li><strong>权限最小化</strong>：谁能连、从哪连、能读还是能写，都要精确控制。</li><li><strong>传输加密</strong>：SSL/TLS 防止内网抓包，是生产基线。</li><li><strong>操作可审计</strong>：关键动作留痕，出了问题可追溯。</li><li><strong>应用层兜底</strong>：参数化查询 + 最小权限，把注入损失降到最低。</li></ul><p>把账号体系当成代码一样管理：用角色替代散落的权限，用 KMS 替代配置文件里的明文密码，用审计替代“出了问题再查日志”，才能把数据库从“人人皆可访问的宝库”变成“有门禁、有监控、有闸口的金库”。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;div class=&quot;tip warning&quot;&gt;&lt;p&gt;索引、事务、复制、分库、备份解决的是“能不能跑、跑得快不快、丢不丢数据”的问题；而安全解决的是“谁能进来、进来能干什么、干了什么能不能被追溯”的问题。MySQL 的安全事故，80% 不是被黑客攻破，而是账号权限过大、密码太弱</summary>
      
    
    
    
    <category term="经验分享" scheme="http://hexo.sjava.cn/categories/%E7%BB%8F%E9%AA%8C%E5%88%86%E4%BA%AB/"/>
    
    
    <category term="MySQL" scheme="http://hexo.sjava.cn/tags/MySQL/"/>
    
    <category term="数据库" scheme="http://hexo.sjava.cn/tags/%E6%95%B0%E6%8D%AE%E5%BA%93/"/>
    
    <category term="安全" scheme="http://hexo.sjava.cn/tags/%E5%AE%89%E5%85%A8/"/>
    
    <category term="权限管理" scheme="http://hexo.sjava.cn/tags/%E6%9D%83%E9%99%90%E7%AE%A1%E7%90%86/"/>
    
    <category term="SSL" scheme="http://hexo.sjava.cn/tags/SSL/"/>
    
  </entry>
  
  <entry>
    <title>MySQL 备份与恢复实战：mysqldump、XtraBackup 与 binlog 定点恢复</title>
    <link href="http://hexo.sjava.cn/posts/86729.html"/>
    <id>http://hexo.sjava.cn/posts/86729.html</id>
    <published>2026-09-11T07:55:00.000Z</published>
    <updated>2026-09-11T07:55:00.000Z</updated>
    
    <content type="html"><![CDATA[<div class="tip warning"><p>索引优化、事务锁、主从复制和分库分表能帮你“跑得快、扛得住”，但数据一旦误删、磁盘损坏或勒索软件加密，backup 才是最后一道防线。本文聚焦 MySQL 备份与恢复：从 mysqldump 到 XtraBackup，再到 binlog 定点恢复，把“能回滚”这件事讲透。</p></div><h1 id="一、备份是最后一道防线"><a href="#一、备份是最后一道防线" class="headerlink" title="一、备份是最后一道防线"></a>一、备份是最后一道防线</h1><p>很多团队把备份当成“有就行”，直到出事才发现：</p><ul><li>备份文件损坏，无法解压。</li><li>只备份了 schema，没备份数据。</li><li>恢复演练从没做过，真正恢复时耗时半天。</li><li>binlog 没开，误删后只能回档到昨晚的全量。</li></ul><p>所以备份的核心目标不只是“存一份数据”，而是<strong>在可接受的时间内，恢复到可接受的状态</strong>。</p><h1 id="二、备份分类与策略"><a href="#二、备份分类与策略" class="headerlink" title="二、备份分类与策略"></a>二、备份分类与策略</h1><h2 id="2-1-逻辑备份-vs-物理备份"><a href="#2-1-逻辑备份-vs-物理备份" class="headerlink" title="2.1 逻辑备份 vs 物理备份"></a>2.1 逻辑备份 vs 物理备份</h2><div class="table-container"><table><thead><tr><th>维度</th><th>逻辑备份（mysqldump）</th><th>物理备份（XtraBackup）</th></tr></thead><tbody><tr><td>形式</td><td>SQL 文本 / CSV</td><td>数据文件副本</td></tr><tr><td>速度</td><td>慢（逐行导出）</td><td>快（拷贝页 + redo）</td></tr><tr><td>体积</td><td>大，可压缩</td><td>接近原库，可压缩</td></tr><tr><td>恢复</td><td>逐条执行 SQL</td><td>直接替换数据文件</td></tr><tr><td>适用</td><td>小库、跨版本、部分表</td><td>大库、生产全量、热备</td></tr><tr><td>一致性</td><td><code>--single-transaction</code></td><td>InnoDB 热备机制</td></tr></tbody></table></div><h2 id="2-2-全量、增量与差异"><a href="#2-2-全量、增量与差异" class="headerlink" title="2.2 全量、增量与差异"></a>2.2 全量、增量与差异</h2><ul><li><strong>全量</strong>：每次备份完整数据。简单、恢复快，但占用空间大。</li><li><strong>增量</strong>：只备份自上次备份后变化的数据。XtraBackup 基于 LSN（Log Sequence Number）实现。</li><li><strong>差异</strong>：备份自上次全量以来的变化。介于两者之间，恢复时需要一次全量 + 一次差异。</li></ul><p>生产常见组合：<strong>周全量 + 日增量 + 实时 binlog</strong>。</p><h2 id="2-3-热备、温备与冷备"><a href="#2-3-热备、温备与冷备" class="headerlink" title="2.3 热备、温备与冷备"></a>2.3 热备、温备与冷备</h2><ul><li><strong>热备</strong>：应用不中断，InnoDB 可通过 XtraBackup 实现。</li><li><strong>温备</strong>：只读锁表，MyISAM 常用。</li><li><strong>冷备</strong>：停库复制数据文件，一般仅用于维护窗口。</li></ul><div class="note info flat"><p>XtraBackup 对 MyISAM 表备份时会短暂加锁，因此大量 MyISAM 的场景建议先迁到 InnoDB。</p></div><h1 id="三、mysqldump-逻辑备份"><a href="#三、mysqldump-逻辑备份" class="headerlink" title="三、mysqldump 逻辑备份"></a>三、mysqldump 逻辑备份</h1><h2 id="3-1-最常用命令"><a href="#3-1-最常用命令" class="headerlink" title="3.1 最常用命令"></a>3.1 最常用命令</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 全库一致性备份</span></span><br><span class="line">mysqldump -u root -p \</span><br><span class="line">  --single-transaction \</span><br><span class="line">  --master-data=2 \</span><br><span class="line">  --routines --triggers --events \</span><br><span class="line">  --all-databases &gt; full_backup_$(<span class="built_in">date</span> +%F).sql</span><br></pre></td></tr></table></figure><p>参数含义：</p><ul><li><code>--single-transaction</code>：在事务中导出，保证 InnoDB 一致性，不锁表。</li><li><code>--master-data=2</code>：记录 binlog 位点，注释形式写入备份文件，便于 PITR。</li><li><code>--routines --triggers --events</code>：导出存储过程、触发器、事件。</li><li><code>--all-databases</code>：全库。</li></ul><h2 id="3-2-单库-单表-只导结构"><a href="#3-2-单库-单表-只导结构" class="headerlink" title="3.2 单库 / 单表 / 只导结构"></a>3.2 单库 / 单表 / 只导结构</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 单库</span></span><br><span class="line">mysqldump -u root -p --single-transaction db_name &gt; db_name.sql</span><br><span class="line"></span><br><span class="line"><span class="comment"># 单表</span></span><br><span class="line">mysqldump -u root -p db_name table_name &gt; table_name.sql</span><br><span class="line"></span><br><span class="line"><span class="comment"># 只导结构</span></span><br><span class="line">mysqldump -u root -p --no-data db_name &gt; db_name_schema.sql</span><br><span class="line"></span><br><span class="line"><span class="comment"># 只导数据</span></span><br><span class="line">mysqldump -u root -p --no-create-info db_name &gt; db_name_data.sql</span><br></pre></td></tr></table></figure><h2 id="3-3-压缩与定时任务"><a href="#3-3-压缩与定时任务" class="headerlink" title="3.3 压缩与定时任务"></a>3.3 压缩与定时任务</h2><p>全量 SQL 通常很大，导出时直接压缩：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">mysqldump -u root -p --single-transaction --all-databases \</span><br><span class="line">  | gzip &gt; full_backup_$(<span class="built_in">date</span> +%F).sql.gz</span><br></pre></td></tr></table></figure><p>配合 crontab 每日执行：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">0 2 * * * /usr/local/bin/backup_mysql.sh &gt;&gt; /var/log/mysql_backup.log 2&gt;&amp;1</span><br></pre></td></tr></table></figure><p><code>backup_mysql.sh</code> 示例：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#!/bin/bash</span></span><br><span class="line">BACKUP_DIR=/data/backup/mysql</span><br><span class="line">DATE=$(<span class="built_in">date</span> +%F)</span><br><span class="line"><span class="built_in">mkdir</span> -p <span class="variable">$BACKUP_DIR</span></span><br><span class="line"></span><br><span class="line">mysqldump -u backup_user -p<span class="string">&#x27;password&#x27;</span> \</span><br><span class="line">  --single-transaction --master-data=2 \</span><br><span class="line">  --all-databases | gzip &gt; <span class="variable">$BACKUP_DIR</span>/full_<span class="variable">$DATE</span>.sql.gz</span><br><span class="line"></span><br><span class="line"><span class="comment"># 保留 7 天</span></span><br><span class="line">find <span class="variable">$BACKUP_DIR</span> -name <span class="string">&#x27;full_*.sql.gz&#x27;</span> -mtime +7 -delete</span><br></pre></td></tr></table></figure><div class="note warning flat"><p>密码写在命令行里会暴露在 <code>ps</code> 中。更安全的做法是把账号密码放进 <code>~/.my.cnf</code> 的 <code>[mysqldump]</code> 段，并设置 600 权限。</p></div><h2 id="3-4-mydumper：并行逻辑备份"><a href="#3-4-mydumper：并行逻辑备份" class="headerlink" title="3.4 mydumper：并行逻辑备份"></a>3.4 mydumper：并行逻辑备份</h2><p>当库很大但暂时不想用物理备份时，可以用 mydumper 多线程导出：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">mydumper -u root -p -B db_name -t 4 -o /backup/dir</span><br></pre></td></tr></table></figure><ul><li><code>-t 4</code>：4 个线程。</li><li>导出为多个文件，恢复用 <code>myloader</code>，可并发导入。</li></ul><h2 id="3-5-mysqldump-避坑"><a href="#3-5-mysqldump-避坑" class="headerlink" title="3.5 mysqldump 避坑"></a>3.5 mysqldump 避坑</h2><div class="table-container"><table><thead><tr><th>坑</th><th>说明</th></tr></thead><tbody><tr><td>大表导出导致从库延迟</td><td>长事务会阻塞 purge，主从场景注意选择低峰期</td></tr><tr><td><code>--single-transaction</code> 对 MyISAM 无效</td><td>MyISAM 会锁表，应改用 <code>--lock-all-tables</code></td></tr><tr><td>未压缩占满磁盘</td><td>大库务必备份时直接 gzip / pigz</td></tr><tr><td>没记录 binlog 位点</td><td>缺少位点无法做 PITR</td></tr><tr><td>权限不足</td><td>需 SELECT、RELOAD、LOCK TABLES、REPLICATION CLIENT</td></tr></tbody></table></div><h1 id="四、XtraBackup-物理备份"><a href="#四、XtraBackup-物理备份" class="headerlink" title="四、XtraBackup 物理备份"></a>四、XtraBackup 物理备份</h1><p>XtraBackup（Percona XtraBackup）是 MySQL 物理热备的首选，基于 InnoDB 崩溃恢复机制，备份期间不阻塞读写。</p><h2 id="4-1-全量备份"><a href="#4-1-全量备份" class="headerlink" title="4.1 全量备份"></a>4.1 全量备份</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 1. 备份</span></span><br><span class="line">xtrabackup --backup --target-dir=/backup/full/$(<span class="built_in">date</span> +%F) \</span><br><span class="line">  --user=root --password=<span class="string">&#x27;password&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 2. 准备（应用 redo log，使数据文件一致）</span></span><br><span class="line">xtrabackup --prepare --target-dir=/backup/full/2026-09-11</span><br><span class="line"></span><br><span class="line"><span class="comment"># 3. 恢复时停止 MySQL，清空数据目录后执行 copy-back</span></span><br><span class="line">xtrabackup --copy-back --target-dir=/backup/full/2026-09-11</span><br><span class="line"><span class="built_in">chown</span> -R mysql:mysql /var/lib/mysql</span><br></pre></td></tr></table></figure><h2 id="4-2-增量备份"><a href="#4-2-增量备份" class="headerlink" title="4.2 增量备份"></a>4.2 增量备份</h2><p>XtraBackup 增量基于 LSN：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 周日全量</span></span><br><span class="line">xtrabackup --backup --target-dir=/backup/full/2026-09-06</span><br><span class="line"></span><br><span class="line"><span class="comment"># 周一增量，基于周日全量</span></span><br><span class="line">xtrabackup --backup --target-dir=/backup/inc/2026-09-07 \</span><br><span class="line">  --incremental-basedir=/backup/full/2026-09-06</span><br></pre></td></tr></table></figure><p>恢复时，先把全量 prepare，再依次 apply 增量：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">xtrabackup --prepare --apply-log-only --target-dir=/backup/full/2026-09-06</span><br><span class="line">xtrabackup --prepare --apply-log-only --target-dir=/backup/full/2026-09-06 \</span><br><span class="line">  --incremental-dir=/backup/inc/2026-09-07</span><br><span class="line"><span class="comment"># 最后一天增量去掉 --apply-log-only，让它做完整恢复</span></span><br><span class="line">xtrabackup --prepare --target-dir=/backup/full/2026-09-06</span><br></pre></td></tr></table></figure><h2 id="4-3-流式压缩与远程备份"><a href="#4-3-流式压缩与远程备份" class="headerlink" title="4.3 流式压缩与远程备份"></a>4.3 流式压缩与远程备份</h2><p>大库备份直接写本地磁盘可能撑满，可用流式输出：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">xtrabackup --backup --stream=xbstream --compress \</span><br><span class="line">  --user=root --password=<span class="string">&#x27;password&#x27;</span> \</span><br><span class="line">  &gt; /backup/full.xbstream</span><br></pre></td></tr></table></figure><p>或直传远程：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">xtrabackup --backup --stream=xbstream \</span><br><span class="line">  | ssh backup-server <span class="string">&quot;cat &gt; /data/mysql_backup.xbstream&quot;</span></span><br></pre></td></tr></table></figure><h2 id="4-4-mysqldump-vs-XtraBackup"><a href="#4-4-mysqldump-vs-XtraBackup" class="headerlink" title="4.4 mysqldump vs XtraBackup"></a>4.4 mysqldump vs XtraBackup</h2><div class="table-container"><table><thead><tr><th>场景</th><th>推荐工具</th><th>理由</th></tr></thead><tbody><tr><td>&lt; 50GB，需跨版本/部分表</td><td>mysqldump</td><td>灵活、可读</td></tr><tr><td>&gt; 50GB，生产热备</td><td>XtraBackup</td><td>快、不锁表</td></tr><tr><td>需要增量/差异</td><td>XtraBackup</td><td>支持 LSN 增量</td></tr><tr><td>快速搭建从库</td><td>XtraBackup + <code>--slave-info</code></td><td>直接复制数据目录</td></tr><tr><td>仅做 Schema 备份</td><td><code>mysqldump --no-data</code></td><td>文本 diff 友好</td></tr></tbody></table></div><h1 id="五、binlog-定点恢复（PITR）"><a href="#五、binlog-定点恢复（PITR）" class="headerlink" title="五、binlog 定点恢复（PITR）"></a>五、binlog 定点恢复（PITR）</h1><p>binlog 在主从复制篇已经讲过，它是 MySQL 的逻辑变更日志。除了复制，binlog 另一个核心用途就是<strong>基于时间点的恢复（Point-in-Time Recovery, PITR）</strong>。</p><h2 id="5-1-前置条件"><a href="#5-1-前置条件" class="headerlink" title="5.1 前置条件"></a>5.1 前置条件</h2><p>要做 PITR，必须同时满足：</p><ol><li>已开启 binlog：<code>log_bin = ON</code>，建议 <code>binlog_format = ROW</code>。</li><li>有全量或增量备份作为基线。</li><li>备份记录了 binlog 位点（mysqldump 的 <code>--master-data=2</code> 或 XtraBackup 的 <code>xtrabackup_binlog_info</code>）。</li></ol><h2 id="5-2-找到目标位点"><a href="#5-2-找到目标位点" class="headerlink" title="5.2 找到目标位点"></a>5.2 找到目标位点</h2><p>误操作后，先别急着恢复。确定：</p><ul><li>误操作执行的大致时间。</li><li>对应的 binlog 文件。</li></ul><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">SHOW</span> <span class="type">BINARY</span> LOGS;</span><br><span class="line"><span class="keyword">SHOW</span> MASTER STATUS;</span><br></pre></td></tr></table></figure><h2 id="5-3-用-mysqlbinlog-恢复"><a href="#5-3-用-mysqlbinlog-恢复" class="headerlink" title="5.3 用 mysqlbinlog 恢复"></a>5.3 用 mysqlbinlog 恢复</h2><p>假设周日凌晨 02:00 的全量备份位点是 <code>mysql-bin.000015:156</code>，而周三 14:30 误删了一张表。目标是把数据恢复到 14:29:59。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">mysqlbinlog \</span><br><span class="line">  --start-position=156 \</span><br><span class="line">  --stop-datetime=<span class="string">&quot;2026-09-09 14:29:59&quot;</span> \</span><br><span class="line">  /var/lib/mysql/mysql-bin.000015 \</span><br><span class="line">  /var/lib/mysql/mysql-bin.000016 \</span><br><span class="line">  | mysql -u root -p</span><br></pre></td></tr></table></figure><p>参数说明：</p><ul><li><code>--start-position</code>：从备份记录的位点开始。</li><li><code>--stop-datetime</code> / <code>--stop-position</code>：恢复到误操作前一刻。</li><li>可指定多个 binlog 文件，按顺序回放。</li></ul><div class="note info flat"><p>ROW 格式下，<code>mysqlbinlog</code> 默认输出 base64 事件。若需人工审计，可加 <code>-v</code> 或 <code>--base64-output=DECODE-ROWS</code> 转成伪 SQL。</p></div><h2 id="5-4-误删库恢复实战流程"><a href="#5-4-误删库恢复实战流程" class="headerlink" title="5.4 误删库恢复实战流程"></a>5.4 误删库恢复实战流程</h2><ol><li><strong>保护现场</strong>：立即停止业务写入，避免 binlog 被覆盖。</li><li><strong>准备临时实例</strong>：用备份恢复一个独立实例，不要直接覆盖生产。</li><li><strong>回放 binlog</strong>：在临时实例上执行 <code>mysqlbinlog --stop-datetime</code> 恢复到误删前。</li><li><strong>导出需要的数据</strong>：用 mysqldump 导出被删的库/表。</li><li><strong>回灌生产</strong>：确认数据无误后，导入生产或做主从切换。</li></ol><div class="tip "><p>如果误操作是 <code>DROP TABLE</code>，PITR 能救回数据；但如果是 <code>TRUNCATE TABLE</code> 后又有新写入，目标表的数据需要单独导出再合并，不能简单回放。</p></div><h2 id="5-5-binlog-恢复的常见坑"><a href="#5-5-binlog-恢复的常见坑" class="headerlink" title="5.5 binlog 恢复的常见坑"></a>5.5 binlog 恢复的常见坑</h2><div class="table-container"><table><thead><tr><th>坑</th><th>避免方法</th></tr></thead><tbody><tr><td>恢复时越过误操作</td><td>精确计算 <code>--stop-datetime</code>，宁可少恢复几秒</td></tr><tr><td>多个 binlog 文件顺序错</td><td>按文件名序号依次回放</td></tr><tr><td>表结构变更后回放失败</td><td>确保目标实例 schema 与备份时一致</td></tr><tr><td>GTID 模式下重复执行</td><td>用 <code>--skip-gtids</code> 或 <code>--include-gtids</code> / <code>--exclude-gtids</code> 精确控制</td></tr></tbody></table></div><div class="note info flat"><p>GTID 模式下，<code>mysqlbinlog</code> 默认带 <code>SET @@SESSION.GTID_NEXT</code>。若只想恢复部分事件，可用 <code>--exclude-gtids</code> 跳过误操作事务。</p></div><h1 id="六、生产备份架构"><a href="#六、生产备份架构" class="headerlink" title="六、生产备份架构"></a>六、生产备份架构</h1><h2 id="6-1-3-2-1-原则"><a href="#6-1-3-2-1-原则" class="headerlink" title="6.1 3-2-1 原则"></a>6.1 3-2-1 原则</h2><ul><li><strong>3</strong> 份数据副本：生产数据 + 2 份备份。</li><li><strong>2</strong> 种不同介质：本地磁盘 + 对象存储 / 磁带。</li><li><strong>1</strong> 份异地：跨可用区或跨地域。</li></ul><h2 id="6-2-延迟从库"><a href="#6-2-延迟从库" class="headerlink" title="6.2 延迟从库"></a>6.2 延迟从库</h2><p>延迟从库是防御误删的神器：</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">CHANGE MASTER <span class="keyword">TO</span> MASTER_DELAY <span class="operator">=</span> <span class="number">3600</span>;</span><br></pre></td></tr></table></figure><p>设置 1 小时延迟后，主库误删表时，从库还有 1 小时前的数据，可直接在从库上导出恢复，省去全量 + binlog 回放的漫长等待。</p><h2 id="6-3-备份校验与演练"><a href="#6-3-备份校验与演练" class="headerlink" title="6.3 备份校验与演练"></a>6.3 备份校验与演练</h2><p>备份不校验等于没备：</p><ul><li><strong>定期恢复演练</strong>：每周/每月用备份在临时环境做一次全量恢复。</li><li><strong>校验文件完整性</strong>：对比 md5、检查压缩包能否解压。</li><li><strong>校验数据一致性</strong>：恢复后跑几条关键查询，对比记录数、checksum。</li><li><strong>监控备份任务</strong>：cron 失败告警、备份文件大小异常告警。</li></ul><h2 id="6-4-云-RDS-自动备份"><a href="#6-4-云-RDS-自动备份" class="headerlink" title="6.4 云 RDS 自动备份"></a>6.4 云 RDS 自动备份</h2><p>如果使用阿里云 RDS、AWS RDS 等托管服务，通常已提供：</p><ul><li>自动全量快照。</li><li>自动 binlog 备份。</li><li>按时间点恢复控制台。</li></ul><p>但即便如此，也建议<strong>额外做逻辑导出</strong>，防止云账号异常、区域故障或误操作控制台删除实例。</p><h1 id="七、避坑清单"><a href="#七、避坑清单" class="headerlink" title="七、避坑清单"></a>七、避坑清单</h1><ul><li>[ ] 不要只备份数据文件，忽略 binlog。</li><li>[ ] 不要只开 binlog，却不做全量基线。</li><li>[ ] 不要把备份和数据库放在同一块磁盘。</li><li>[ ] 不要备份到生产服务器本地就不管，至少同步到对象存储。</li><li>[ ] 不要高估备份文件，恢复演练要定期开展。</li><li>[ ] 不要给备份脚本 root 密码明文，使用专用备份账号和 <code>.my.cnf</code>。</li><li>[ ] 不要忽视权限，备份账号需最小权限原则。</li><li>[ ] 不要在恢复时直接覆盖生产，先在临时实例验证。</li></ul><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>MySQL 备份没有银弹，但有标准答案：</p><ul><li><strong>小库 / 跨版本 / 部分表</strong>：mysqldump。</li><li><strong>大库 / 生产热备 / 快速恢复</strong>：XtraBackup。</li><li><strong>误删回滚 / 定点恢复</strong>：全量备份 + binlog PITR。</li><li><strong>防误删兜底</strong>：延迟从库 + 异地备份。</li><li><strong>备份有效性的唯一证明</strong>：定期恢复演练。</li></ul><p>把备份当作一项持续运营的工作，而不是一次性的脚本任务，才能在真正出事时把“删库跑路”变成“有惊无险”。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;div class=&quot;tip warning&quot;&gt;&lt;p&gt;索引优化、事务锁、主从复制和分库分表能帮你“跑得快、扛得住”，但数据一旦误删、磁盘损坏或勒索软件加密，backup 才是最后一道防线。本文聚焦 MySQL 备份与恢复：从 mysqldump 到 XtraBackup，再到 </summary>
      
    
    
    
    <category term="经验分享" scheme="http://hexo.sjava.cn/categories/%E7%BB%8F%E9%AA%8C%E5%88%86%E4%BA%AB/"/>
    
    
    <category term="MySQL" scheme="http://hexo.sjava.cn/tags/MySQL/"/>
    
    <category term="数据库" scheme="http://hexo.sjava.cn/tags/%E6%95%B0%E6%8D%AE%E5%BA%93/"/>
    
    <category term="备份恢复" scheme="http://hexo.sjava.cn/tags/%E5%A4%87%E4%BB%BD%E6%81%A2%E5%A4%8D/"/>
    
    <category term="XtraBackup" scheme="http://hexo.sjava.cn/tags/XtraBackup/"/>
    
    <category term="PITR" scheme="http://hexo.sjava.cn/tags/PITR/"/>
    
  </entry>
  
  <entry>
    <title>MySQL 分库分表实战：垂直拆分、水平分片与 ShardingSphere 落地</title>
    <link href="http://hexo.sjava.cn/posts/81563.html"/>
    <id>http://hexo.sjava.cn/posts/81563.html</id>
    <published>2026-09-10T08:45:00.000Z</published>
    <updated>2026-09-10T08:45:00.000Z</updated>
    
    <content type="html"><![CDATA[<div class="tip warning"><p>主从复制和读写分离能扛住读流量，却扛不住写流量无限增长。当单库写性能、磁盘容量或连接数触及天花板时，分库分表就成了必经之路。本文从拆分策略讲到 ShardingSphere 落地，帮你把“拆”这件事拆明白。</p></div><h1 id="一、单库撑不住时，先别急着分"><a href="#一、单库撑不住时，先别急着分" class="headerlink" title="一、单库撑不住时，先别急着分"></a>一、单库撑不住时，先别急着分</h1><p>分库分表是把双刃剑：它解决了容量与性能问题，却引入了复杂度。在决定拆分之前，先确认是否已经把单库优化到极致。</p><h2 id="1-1-什么指标说明该拆了"><a href="#1-1-什么指标说明该拆了" class="headerlink" title="1.1 什么指标说明该拆了"></a>1.1 什么指标说明该拆了</h2><div class="table-container"><table><thead><tr><th>瓶颈类型</th><th>常见阈值</th><th>说明</th></tr></thead><tbody><tr><td>单表数据量</td><td>5000 万 ~ 1 亿行</td><td>InnoDB B+Tree 层级变深，范围查询变慢</td></tr><tr><td>单库数据量</td><td>500 GB ~ 1 TB</td><td>备份、恢复、迁移成本陡增</td></tr><tr><td>单库连接数</td><td>80% max_connections</td><td>应用池化再好也会被打满</td></tr><tr><td>单库写 TPS</td><td>CPU / IO 长期 80%+</td><td>索引维护、redo log、锁竞争放大</td></tr></tbody></table></div><h2 id="1-2-拆分前的三板斧"><a href="#1-2-拆分前的三板斧" class="headerlink" title="1.2 拆分前的三板斧"></a>1.2 拆分前的三板斧</h2><ol><li><strong>SQL 与索引优化</strong>：慢查询、索引失效、大事务往往是真正元凶，详见 MySQL 索引优化专题。</li><li><strong>读写分离</strong>：把读流量拆到从库，主库只负责写，成本比分片低得多。</li><li><strong>缓存与归档</strong>：热数据放 Redis，冷数据归档，能大幅推迟拆分。</li></ol><div class="note orange anzhiyufont anzhiyu-icon-dengpao disabled"><p>只有当“写”成为真正的瓶颈，或者单表/单库容量无法再扩展时，才值得引入分库分表。</p></div><h1 id="二、垂直拆分：按业务拆库、冷热拆表"><a href="#二、垂直拆分：按业务拆库、冷热拆表" class="headerlink" title="二、垂直拆分：按业务拆库、冷热拆表"></a>二、垂直拆分：按业务拆库、冷热拆表</h1><h2 id="2-1-垂直分库"><a href="#2-1-垂直分库" class="headerlink" title="2.1 垂直分库"></a>2.1 垂直分库</h2><p>把不同业务模块的表放到独立的数据库实例中。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">订单库：order、order_item、order_log</span><br><span class="line">用户库：user、user_profile、user_address</span><br><span class="line">商品库：product、category、inventory</span><br></pre></td></tr></table></figure><p><strong>优点</strong>：</p><ul><li>各业务库独立扩容，互不拖累。</li><li>降低单库连接数压力。</li></ul><p><strong>缺点</strong>：</p><ul><li>跨库 JOIN 需要应用层聚合，或引入分布式查询中间件。</li><li>分布式事务复杂度上升。</li></ul><h2 id="2-2-垂直分表"><a href="#2-2-垂直分表" class="headerlink" title="2.2 垂直分表"></a>2.2 垂直分表</h2><p>把宽表中冷热不均的字段拆出去。</p><div class="table-container"><table><thead><tr><th>原表 user</th><th>拆分后</th></tr></thead><tbody><tr><td>id, nickname, phone, avatar, last_login</td><td>热表 user_basic</td></tr><tr><td>id, signature, address, tag_json, extend</td><td>冷表 user_extra</td></tr></tbody></table></div><p>热表常驻内存缓存，冷表偶尔查询，能显著减少单表宽度与索引体积。</p><h1 id="三、水平拆分：把同一张表的数据拆到多库"><a href="#三、水平拆分：把同一张表的数据拆到多库" class="headerlink" title="三、水平拆分：把同一张表的数据拆到多库"></a>三、水平拆分：把同一张表的数据拆到多库</h1><h2 id="3-1-什么时候必须水平分"><a href="#3-1-什么时候必须水平分" class="headerlink" title="3.1 什么时候必须水平分"></a>3.1 什么时候必须水平分</h2><p>垂直拆分只能解决业务隔离问题，无法解决单表数据量过大。当一张表行数过亿、写入量持续增长，就需要水平拆分。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">db_0: order_0, order_2, order_4</span><br><span class="line">db_1: order_1, order_3, order_5</span><br></pre></td></tr></table></figure><h2 id="3-2-分片键（Sharding-Key）是灵魂"><a href="#3-2-分片键（Sharding-Key）是灵魂" class="headerlink" title="3.2 分片键（Sharding Key）是灵魂"></a>3.2 分片键（Sharding Key）是灵魂</h2><p>分片键决定了数据落在哪个库/表。选错分片键，后患无穷。</p><div class="table-container"><table><thead><tr><th>维度</th><th>建议</th></tr></thead><tbody><tr><td>访问频率</td><td>高频查询条件尽量命中同一分片</td></tr><tr><td>数据均匀</td><td>避免热点，例如按 user_id 比按 date 更均匀</td></tr><tr><td>业务语义</td><td>订单按 user_id，日志按时间，地理按 region_code</td></tr><tr><td>不可变</td><td>分片键尽量选不会修改的字段，否则数据迁移很痛苦</td></tr></tbody></table></div><div class="tip danger"><p>分片键一旦选定，后期迁移成本极高。上线前务必用真实数据模拟分布，检查是否有倾斜。</p></div><h1 id="四、分片算法：怎么把数据路由到某个片"><a href="#四、分片算法：怎么把数据路由到某个片" class="headerlink" title="四、分片算法：怎么把数据路由到某个片"></a>四、分片算法：怎么把数据路由到某个片</h1><h2 id="4-1-取模分片"><a href="#4-1-取模分片" class="headerlink" title="4.1 取模分片"></a>4.1 取模分片</h2><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">shardingValue % dbCount  <span class="comment">// 路由到库</span></span><br><span class="line">tableValue % tableCount  <span class="comment">// 路由到表</span></span><br></pre></td></tr></table></figure><p><strong>优点</strong>：数据分布最均匀。<br><strong>缺点</strong>：扩容时需要迁移大量数据（re-sharding）。</p><h2 id="4-2-范围分片"><a href="#4-2-范围分片" class="headerlink" title="4.2 范围分片"></a>4.2 范围分片</h2><p>按时间或 ID 范围切分：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">order_202401, order_202402, order_202403...</span><br></pre></td></tr></table></figure><p><strong>优点</strong>：扩容只需新增分片，无需迁移历史数据。<br><strong>缺点</strong>：新分片容易成为写热点，查询旧数据需跨片。</p><h2 id="4-3-一致性哈希"><a href="#4-3-一致性哈希" class="headerlink" title="4.3 一致性哈希"></a>4.3 一致性哈希</h2><p>在取模基础上引入虚拟节点，扩容时只影响相邻节点，迁移量降到最低。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">虚拟节点 -&gt; 物理节点</span><br><span class="line">hash(user_id) -&gt; 虚拟节点环 -&gt; 物理库</span><br></pre></td></tr></table></figure><p>适合节点数会动态变化的场景，但实现复杂度高于取模。</p><h2 id="4-4-基因法解决“按非分片键查询”"><a href="#4-4-基因法解决“按非分片键查询”" class="headerlink" title="4.4 基因法解决“按非分片键查询”"></a>4.4 基因法解决“按非分片键查询”</h2><p>订单表按 user_id 分片，但业务又常按 order_id 查询。可以在 order_id 中“嵌入” user_id 的基因片段：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">order_id = 时间戳 + 基因(user_id) + 序列号</span><br><span class="line">gene = user_id % 16  // 把分片信息写进 order_id</span><br></pre></td></tr></table></figure><p>由 order_id 反解分片基因，即可直接路由到对应分片。</p><h1 id="五、分布式-ID：分片后不能用自增了"><a href="#五、分布式-ID：分片后不能用自增了" class="headerlink" title="五、分布式 ID：分片后不能用自增了"></a>五、分布式 ID：分片后不能用自增了</h1><h2 id="5-1-自增主键的问题"><a href="#5-1-自增主键的问题" class="headerlink" title="5.1 自增主键的问题"></a>5.1 自增主键的问题</h2><p>多库各自自增会出现重复主键，合并查询时直接冲突。必须引入全局 ID 生成。</p><h2 id="5-2-雪花算法"><a href="#5-2-雪花算法" class="headerlink" title="5.2 雪花算法"></a>5.2 雪花算法</h2><p>Twitter Snowflake 的核心结构：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">0 | 41 bit 时间戳 | 10 bit 机器 ID | 12 bit 序列号</span><br></pre></td></tr></table></figure><p><strong>优点</strong>：趋势递增、毫秒级生成百万级 ID。<br><strong>缺点</strong>：依赖机器时钟，时钟回拨会生成重复 ID。</p><p>生产改进：</p><ul><li>用 NTP + 闰秒处理避免回拨。</li><li>改写为号段模式，批量缓存 ID 减少 RPC。</li></ul><h2 id="5-3-号段模式（Leaf）"><a href="#5-3-号段模式（Leaf）" class="headerlink" title="5.3 号段模式（Leaf）"></a>5.3 号段模式（Leaf）</h2><p>美团 Leaf 的号段模式：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">DB 表记录 biz_tag、max_id、step。</span><br><span class="line">应用每次从 DB 取一段 ID 区间，本地用完再取。</span><br></pre></td></tr></table></figure><p><strong>优点</strong>：DB 压力小，生成速度快，可水平扩展。<br><strong>缺点</strong>：号段用完后有短暂抖动，需要监控预警。</p><h1 id="六、ShardingSphere-落地实战"><a href="#六、ShardingSphere-落地实战" class="headerlink" title="六、ShardingSphere 落地实战"></a>六、ShardingSphere 落地实战</h1><p>Apache ShardingSphere 提供了两种接入方式：</p><div class="table-container"><table><thead><tr><th>模式</th><th>接入方式</th><th>优点</th><th>缺点</th></tr></thead><tbody><tr><td>ShardingSphere-JDBC</td><td>Java 依赖</td><td>性能高、无额外网络跳转</td><td>对业务代码有侵入</td></tr><tr><td>ShardingSphere-Proxy</td><td>独立代理</td><td>多语言通用、可集中管理</td><td>多一跳网络，运维成本高</td></tr></tbody></table></div><h2 id="6-1-分库分表配置示例（ShardingSphere-JDBC-YAML）"><a href="#6-1-分库分表配置示例（ShardingSphere-JDBC-YAML）" class="headerlink" title="6.1 分库分表配置示例（ShardingSphere-JDBC YAML）"></a>6.1 分库分表配置示例（ShardingSphere-JDBC YAML）</h2><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">dataSources:</span></span><br><span class="line">  <span class="attr">ds_0:</span> <span class="type">!!com.zaxxer.hikari.HikariDataSource</span></span><br><span class="line">    <span class="attr">jdbcUrl:</span> <span class="string">jdbc:mysql://127.0.0.1:3306/db_0</span></span><br><span class="line">    <span class="attr">username:</span> <span class="string">root</span></span><br><span class="line">    <span class="attr">password:</span> <span class="string">root</span></span><br><span class="line">  <span class="attr">ds_1:</span> <span class="type">!!com.zaxxer.hikari.HikariDataSource</span></span><br><span class="line">    <span class="attr">jdbcUrl:</span> <span class="string">jdbc:mysql://127.0.0.1:3306/db_1</span></span><br><span class="line">    <span class="attr">username:</span> <span class="string">root</span></span><br><span class="line">    <span class="attr">password:</span> <span class="string">root</span></span><br><span class="line"></span><br><span class="line"><span class="attr">rules:</span></span><br><span class="line"><span class="bullet">-</span> <span class="type">!SHARDING</span></span><br><span class="line">  <span class="attr">tables:</span></span><br><span class="line">    <span class="attr">t_order:</span></span><br><span class="line">      <span class="attr">actualDataNodes:</span> <span class="string">ds_$&#123;0..1&#125;.t_order_$&#123;0..3&#125;</span></span><br><span class="line">      <span class="attr">tableStrategy:</span></span><br><span class="line">        <span class="attr">standard:</span></span><br><span class="line">          <span class="attr">shardingColumn:</span> <span class="string">user_id</span></span><br><span class="line">          <span class="attr">shardingAlgorithmName:</span> <span class="string">t_order_inline</span></span><br><span class="line">      <span class="attr">databaseStrategy:</span></span><br><span class="line">        <span class="attr">standard:</span></span><br><span class="line">          <span class="attr">shardingColumn:</span> <span class="string">user_id</span></span><br><span class="line">          <span class="attr">shardingAlgorithmName:</span> <span class="string">database_inline</span></span><br><span class="line">      <span class="attr">keyGenerateStrategy:</span></span><br><span class="line">        <span class="attr">column:</span> <span class="string">order_id</span></span><br><span class="line">        <span class="attr">keyGeneratorName:</span> <span class="string">snowflake</span></span><br><span class="line"></span><br><span class="line">  <span class="attr">shardingAlgorithms:</span></span><br><span class="line">    <span class="attr">database_inline:</span></span><br><span class="line">      <span class="attr">type:</span> <span class="string">INLINE</span></span><br><span class="line">      <span class="attr">props:</span></span><br><span class="line">        <span class="attr">algorithm-expression:</span> <span class="string">ds_$&#123;user_id</span> <span class="string">%</span> <span class="number">2</span><span class="string">&#125;</span></span><br><span class="line">    <span class="attr">t_order_inline:</span></span><br><span class="line">      <span class="attr">type:</span> <span class="string">INLINE</span></span><br><span class="line">      <span class="attr">props:</span></span><br><span class="line">        <span class="attr">algorithm-expression:</span> <span class="string">t_order_$&#123;user_id</span> <span class="string">%</span> <span class="number">4</span><span class="string">&#125;</span></span><br><span class="line"></span><br><span class="line">  <span class="attr">keyGenerators:</span></span><br><span class="line">    <span class="attr">snowflake:</span></span><br><span class="line">      <span class="attr">type:</span> <span class="string">SNOWFLAKE</span></span><br></pre></td></tr></table></figure><h2 id="6-2-绑定表与广播表"><a href="#6-2-绑定表与广播表" class="headerlink" title="6.2 绑定表与广播表"></a>6.2 绑定表与广播表</h2><ul><li><strong>绑定表</strong>：具有相同分片规则的关联表，例如 <code>t_order</code> 与 <code>t_order_item</code> 都按 <code>user_id</code> 分片，JOIN 可下推到单库执行。</li><li><strong>广播表</strong>：数据量小、变化少的表（如字典表）同步到每个分片，避免跨库 JOIN。</li></ul><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">bindingTables:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">t_order,t_order_item</span></span><br><span class="line"><span class="attr">broadcastTables:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">t_config</span></span><br></pre></td></tr></table></figure><h2 id="6-3-读写分离-分片组合配置"><a href="#6-3-读写分离-分片组合配置" class="headerlink" title="6.3 读写分离 + 分片组合配置"></a>6.3 读写分离 + 分片组合配置</h2><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">rules:</span></span><br><span class="line"><span class="bullet">-</span> <span class="type">!READWRITE_SPLITTING</span></span><br><span class="line">  <span class="attr">dataSources:</span></span><br><span class="line">    <span class="attr">ds_0:</span></span><br><span class="line">      <span class="attr">type:</span> <span class="string">Static</span></span><br><span class="line">      <span class="attr">props:</span></span><br><span class="line">        <span class="attr">write-data-source-name:</span> <span class="string">ds_0_master</span></span><br><span class="line">        <span class="attr">read-data-source-names:</span> <span class="string">ds_0_slave</span></span><br><span class="line">    <span class="attr">ds_1:</span></span><br><span class="line">      <span class="attr">type:</span> <span class="string">Static</span></span><br><span class="line">      <span class="attr">props:</span></span><br><span class="line">        <span class="attr">write-data-source-name:</span> <span class="string">ds_1_master</span></span><br><span class="line">        <span class="attr">read-data-source-names:</span> <span class="string">ds_1_slave</span></span><br><span class="line"></span><br><span class="line"><span class="bullet">-</span> <span class="type">!SHARDING</span></span><br><span class="line">  <span class="attr">tables:</span></span><br><span class="line">    <span class="attr">t_order:</span></span><br><span class="line">      <span class="attr">actualDataNodes:</span> <span class="string">ds_$&#123;0..1&#125;.t_order_$&#123;0..3&#125;</span></span><br></pre></td></tr></table></figure><p>这样写流量路由到主库，读流量自动走从库，同时再做分片。</p><h1 id="七、分库分表后的工程难题"><a href="#七、分库分表后的工程难题" class="headerlink" title="七、分库分表后的工程难题"></a>七、分库分表后的工程难题</h1><h2 id="7-1-跨片查询与分页"><a href="#7-1-跨片查询与分页" class="headerlink" title="7.1 跨片查询与分页"></a>7.1 跨片查询与分页</h2><p>按 user_id 分片后，<code>SELECT * FROM t_order ORDER BY create_time LIMIT 100</code> 需要：</p><ol><li>向所有分片发送同一条 SQL。</li><li>在 Proxy 或应用内存中归并排序。</li><li>取前 100 条。</li></ol><div class="tip danger"><p>深分页（OFFSET 很大）在分片环境下会被放大几十倍，性能灾难。应改用“上一页最大 ID + 时间戳”的游标分页。</p></div><h2 id="7-2-跨片-JOIN"><a href="#7-2-跨片-JOIN" class="headerlink" title="7.2 跨片 JOIN"></a>7.2 跨片 JOIN</h2><p>ShardingSphere 只能处理同一分片内的 JOIN。跨片 JOIN 需要：</p><ul><li>用绑定表把关联数据放在一起。</li><li>在应用层拆两次查询再组装。</li><li>引入宽表/ES/ClickHouse 做离线聚合。</li></ul><h2 id="7-3-分布式事务"><a href="#7-3-分布式事务" class="headerlink" title="7.3 分布式事务"></a>7.3 分布式事务</h2><p>分片后本地事务无法保证跨库一致性，常用方案：</p><div class="table-container"><table><thead><tr><th>方案</th><th>一致性</th><th>性能</th><th>适用场景</th></tr></thead><tbody><tr><td>XA 两阶段提交</td><td>强一致</td><td>低</td><td>金融核心交易</td></tr><tr><td>BASE 柔性事务（Seata TCC / Saga）</td><td>最终一致</td><td>高</td><td>电商、物流</td></tr><tr><td>业务层幂等 + 对账</td><td>最终一致</td><td>最高</td><td>允许短时不一致</td></tr></tbody></table></div><div class="note orange anzhiyufont anzhiyu-icon-dengpao disabled"><p>不要为了用分布式事务而用分布式事务。很多场景下，先把分片键选得足够好、让事务尽量落在一个分片内，才是更优解。</p></div><h2 id="7-4-扩容与数据迁移"><a href="#7-4-扩容与数据迁移" class="headerlink" title="7.4 扩容与数据迁移"></a>7.4 扩容与数据迁移</h2><p>取模分片扩容时，需要把旧数据按新规则重新分布。常见做法：</p><ol><li><strong>双倍扩容</strong>：4 片扩到 8 片，每个旧片拆成两个新片，迁移量可控。</li><li><strong>一致性哈希扩容</strong>：只迁移相邻节点的数据。</li><li><strong>双写 + 增量同步</strong>：新库追平旧库后切流，保证可用性。</li></ol><p>迁移期间一般采用：</p><ul><li>旧库继续写读。</li><li>新库双写增量数据。</li><li>全量比对一致后切读，再切写。</li></ul><h1 id="八、生产避坑清单"><a href="#八、生产避坑清单" class="headerlink" title="八、生产避坑清单"></a>八、生产避坑清单</h1><div class="table-container"><table><thead><tr><th>坑</th><th>后果</th><th>建议</th></tr></thead><tbody><tr><td>分片键选择只看均匀性</td><td>高频查询跨片</td><td>同时满足均匀 + 高频查询条件</td></tr><tr><td>全局自增 ID</td><td>主键冲突</td><td>使用雪花 / 号段 / UUID</td></tr><tr><td>深分页跨片归并</td><td>内存/CPU 爆炸</td><td>用游标分页，避免大 OFFSET</td></tr><tr><td>跨片 JOIN 在应用层硬拼</td><td>性能差、复杂度高</td><td>用绑定表、宽表或搜索引擎</td></tr><tr><td>忽略分片键不可变</td><td>数据需要迁移</td><td>选创建时间、用户 ID 等稳定字段</td></tr><tr><td>一次性拆太多片</td><td>运维复杂、资源浪费</td><td>先拆少，观察后再扩</td></tr><tr><td>没有灰度切换</td><td>全量故障</td><td>按用户维度灰度，先读再写</td></tr></tbody></table></div><h1 id="九、总结"><a href="#九、总结" class="headerlink" title="九、总结"></a>九、总结</h1><p>分库分表是 MySQL 横向扩展的最后一道防线，不是性能优化的银弹：</p><ul><li><strong>垂直拆分</strong>解决业务隔离与冷热分离，适合按模块拆库、按字段拆表。</li><li><strong>水平拆分</strong>解决单表容量与写瓶颈，核心是<strong>分片键</strong>和<strong>分片算法</strong>。</li><li><strong>取模</strong>均匀但扩容痛苦，<strong>范围</strong>扩容简单但易热点，<strong>一致性哈希</strong>在两者之间平衡。</li><li><strong>分布式 ID</strong>是自增主键的替代，雪花算法和号段模式最常用。</li><li><strong>ShardingSphere</strong> 能把分片、读写分离、分布式事务以配置化方式落地，降低侵入。</li><li>真正的难点不在“拆”，而在拆后的<strong>分页、JOIN、事务、迁移</strong>与<strong>运维</strong>。</li></ul><p>如果你已经完成了索引优化、主从复制和读写分离，分库分表就是下一步让 MySQL 继续承载增长的合理选择。前提是：把分片键想清楚，把灰度方案做扎实。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;div class=&quot;tip warning&quot;&gt;&lt;p&gt;主从复制和读写分离能扛住读流量，却扛不住写流量无限增长。当单库写性能、磁盘容量或连接数触及天花板时，分库分表就成了必经之路。本文从拆分策略讲到 ShardingSphere 落地，帮你把“拆”这件事拆明白。&lt;/p&gt;
&lt;/di</summary>
      
    
    
    
    <category term="经验分享" scheme="http://hexo.sjava.cn/categories/%E7%BB%8F%E9%AA%8C%E5%88%86%E4%BA%AB/"/>
    
    
    <category term="MySQL" scheme="http://hexo.sjava.cn/tags/MySQL/"/>
    
    <category term="数据库" scheme="http://hexo.sjava.cn/tags/%E6%95%B0%E6%8D%AE%E5%BA%93/"/>
    
    <category term="分库分表" scheme="http://hexo.sjava.cn/tags/%E5%88%86%E5%BA%93%E5%88%86%E8%A1%A8/"/>
    
    <category term="ShardingSphere" scheme="http://hexo.sjava.cn/tags/ShardingSphere/"/>
    
    <category term="分布式" scheme="http://hexo.sjava.cn/tags/%E5%88%86%E5%B8%83%E5%BC%8F/"/>
    
  </entry>
  
  <entry>
    <title>MySQL 主从复制与读写分离：binlog、GTID 与高可用实战</title>
    <link href="http://hexo.sjava.cn/posts/70928.html"/>
    <id>http://hexo.sjava.cn/posts/70928.html</id>
    <published>2026-09-09T09:45:00.000Z</published>
    <updated>2026-09-09T09:45:00.000Z</updated>
    
    <content type="html"><![CDATA[<div class="tip warning"><p>“线上读写都在一台 MySQL，CPU 飙到 90% 怎么办？””主库挂了，多久能切换？”主从复制和读写分离是 MySQL 横向扩展与容灾的必经之路。本文从复制原理到生产落地一次性讲透。</p></div><h1 id="一、为什么需要主从复制"><a href="#一、为什么需要主从复制" class="headerlink" title="一、为什么需要主从复制"></a>一、为什么需要主从复制</h1><p>单节点 MySQL 同时承担读和写时，随着流量增长，很快会碰到三大瓶颈：</p><div class="table-container"><table><thead><tr><th>瓶颈</th><th>表现</th><th>影响</th></tr></thead><tbody><tr><td><strong>读压力</strong></td><td>查询量过大，CPU/内存/IO 飙升</td><td>影响写入稳定性</td></tr><tr><td><strong>可用性</strong></td><td>单点故障导致整个业务不可用</td><td>RTO 无法保证</td></tr><tr><td><strong>备份</strong></td><td>在业务库上直接做全量备份</td><td>锁表、抖动、影响线上</td></tr></tbody></table></div><p>主从复制（Replication）把主库（Master）的数据实时同步到一个或多个从库（Slave），做到：</p><ul><li><strong>读扩展</strong>：把读流量分散到从库。</li><li><strong>高可用</strong>：主库故障时，从库可快速切换。</li><li><strong>离线备份</strong>：在从库上做备份、归档、报表，不打扰主库。</li></ul><h1 id="二、复制依赖：binlog-与-relay-log"><a href="#二、复制依赖：binlog-与-relay-log" class="headerlink" title="二、复制依赖：binlog 与 relay log"></a>二、复制依赖：binlog 与 relay log</h1><p>MySQL 复制本质上是<strong>基于 binlog 的逻辑日志复制</strong>。主库把变更记录到 binlog，从库读取并重放。</p><h2 id="2-1-binlog-的三种格式"><a href="#2-1-binlog-的三种格式" class="headerlink" title="2.1 binlog 的三种格式"></a>2.1 binlog 的三种格式</h2><div class="table-container"><table><thead><tr><th>格式</th><th>记录内容</th><th>优点</th><th>缺点</th><th>适用场景</th></tr></thead><tbody><tr><td><strong>STATEMENT</strong></td><td>原 SQL 语句</td><td>日志小</td><td>某些语句（如 UUID、触发器）会导致主从不一致</td><td>简单 OLAP、兼容旧版本</td></tr><tr><td><strong>ROW</strong></td><td>每行变更前后镜像</td><td>精确、安全</td><td>日志大、写密集时 IO 高</td><td><strong>生产默认推荐</strong></td></tr><tr><td><strong>MIXED</strong></td><td>默认 STATEMENT，不安全时切换 ROW</td><td>折中</td><td>行为不易预测</td><td>逐步迁移过渡</td></tr></tbody></table></div><p>生产环境推荐固定为 ROW：</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">SET</span> <span class="keyword">GLOBAL</span> binlog_format <span class="operator">=</span> <span class="string">&#x27;ROW&#x27;</span>;</span><br></pre></td></tr></table></figure><h2 id="2-2-relay-log-与线程协作"><a href="#2-2-relay-log-与线程协作" class="headerlink" title="2.2 relay log 与线程协作"></a>2.2 relay log 与线程协作</h2><p>从库接收 binlog 后不是直接执行，而是先写入 <strong>relay log（中继日志）</strong>，再由 SQL 线程读取重放。这样即使网络抖动，也不会阻塞主库。</p><h1 id="三、主从复制原理：三个线程的故事"><a href="#三、主从复制原理：三个线程的故事" class="headerlink" title="三、主从复制原理：三个线程的故事"></a>三、主从复制原理：三个线程的故事</h1><p>一条 DML 从主库写入到从库生效，主要经过三条线程：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">Master</span><br><span class="line">├── binlog dump thread：读取 binlog 发送给 Slave</span><br><span class="line">└── 写入 binlog</span><br><span class="line"></span><br><span class="line">Slave</span><br><span class="line">├── IO thread：连接 Master，接收 binlog，写入 relay log</span><br><span class="line">└── SQL thread：读取 relay log，重放到从库数据页</span><br></pre></td></tr></table></figure><h2 id="3-1-核心步骤"><a href="#3-1-核心步骤" class="headerlink" title="3.1 核心步骤"></a>3.1 核心步骤</h2><ol><li>主库事务提交后，把变更写入 binlog。</li><li>binlog dump thread 把 binlog event 推给从库 IO thread。</li><li>从库 IO thread 写入 relay log。</li><li>从库 SQL thread 按顺序读取 relay log，解析并执行。</li></ol><div class="note orange anzhiyufont anzhiyu-icon-dengpao disabled"><p>复制是<strong>异步串行</strong>的：SQL 线程只有一条，这是主从延迟的根本原因之一。</p></div><h1 id="四、复制模式：异步、半同步、全同步"><a href="#四、复制模式：异步、半同步、全同步" class="headerlink" title="四、复制模式：异步、半同步、全同步"></a>四、复制模式：异步、半同步、全同步</h1><div class="table-container"><table><thead><tr><th>模式</th><th>主库何时返回成功</th><th>数据一致性</th><th>性能</th><th>风险</th></tr></thead><tbody><tr><td><strong>异步复制</strong></td><td>写入本地 binlog 即返回</td><td>最弱</td><td>最高</td><td>主库宕机可能丢数据</td></tr><tr><td><strong>半同步复制</strong></td><td>至少一个从库收到并写入 relay log 后返回</td><td>中等</td><td>中等</td><td>网络抖动会拖慢主库</td></tr><tr><td><strong>组复制 / 全同步</strong></td><td>多数节点确认</td><td>最强</td><td>最低</td><td>需要 Group Replication</td></tr></tbody></table></div><h2 id="4-1-半同步复制配置"><a href="#4-1-半同步复制配置" class="headerlink" title="4.1 半同步复制配置"></a>4.1 半同步复制配置</h2><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 主库</span></span><br><span class="line"><span class="attr">plugin-load</span>=rpl_semi_sync_master=semisync_master.so</span><br><span class="line"><span class="attr">rpl_semi_sync_master_enabled</span>=<span class="number">1</span></span><br><span class="line"><span class="attr">rpl_semi_sync_master_timeout</span>=<span class="number">1000</span>  <span class="comment"># 毫秒，超时降级异步</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 从库</span></span><br><span class="line"><span class="attr">plugin-load</span>=rpl_semi_sync_slave=semisync_slave.so</span><br><span class="line"><span class="attr">rpl_semi_sync_slave_enabled</span>=<span class="number">1</span></span><br></pre></td></tr></table></figure><p>半同步复制不能保证从库 SQL 线程已经执行，只能保证 relay log 已落盘。如果业务对从库读到最新数据有强要求，需要配合读写分离策略。</p><h1 id="五、GTID-复制：让切换更省心"><a href="#五、GTID-复制：让切换更省心" class="headerlink" title="五、GTID 复制：让切换更省心"></a>五、GTID 复制：让切换更省心</h1><h2 id="5-1-什么是-GTID"><a href="#5-1-什么是-GTID" class="headerlink" title="5.1 什么是 GTID"></a>5.1 什么是 GTID</h2><p>GTID（Global Transaction Identifier）为每个已提交事务分配全局唯一 ID：<code>server_uuid:transaction_id</code>。主库和从库的 GTID 集合一致，表示数据一致。</p><h2 id="5-2-GTID-复制的优势"><a href="#5-2-GTID-复制的优势" class="headerlink" title="5.2 GTID 复制的优势"></a>5.2 GTID 复制的优势</h2><ul><li><strong>切换简单</strong>：无需手动找 binlog 文件名和位点。</li><li><strong>避免位点错误</strong>：主从切换后从库直接根据 GTID 集合自动定位。</li><li><strong>故障恢复可追溯</strong>：通过 <code>Executed_Gtid_Set</code> 判断哪些事务已执行。</li></ul><h2 id="5-3-GTID-基础配置"><a href="#5-3-GTID-基础配置" class="headerlink" title="5.3 GTID 基础配置"></a>5.3 GTID 基础配置</h2><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">[mysqld]</span></span><br><span class="line"><span class="attr">gtid_mode</span>=<span class="literal">ON</span></span><br><span class="line"><span class="attr">enforce_gtid_consistency</span>=<span class="literal">ON</span></span><br><span class="line"><span class="attr">log-bin</span>=mysql-bin</span><br><span class="line"><span class="attr">binlog_format</span>=ROW</span><br><span class="line"><span class="attr">log_slave_updates</span>=<span class="literal">ON</span></span><br></pre></td></tr></table></figure><p>启用 GTID 后，<code>change master to</code> 不再需要 <code>MASTER_LOG_FILE</code> 和 <code>MASTER_LOG_POS</code>：</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">CHANGE MASTER <span class="keyword">TO</span></span><br><span class="line">  MASTER_HOST<span class="operator">=</span><span class="string">&#x27;master_host&#x27;</span>,</span><br><span class="line">  MASTER_USER<span class="operator">=</span><span class="string">&#x27;repl&#x27;</span>,</span><br><span class="line">  MASTER_PASSWORD<span class="operator">=</span><span class="string">&#x27;repl_pass&#x27;</span>,</span><br><span class="line">  MASTER_AUTO_POSITION<span class="operator">=</span><span class="number">1</span>;</span><br></pre></td></tr></table></figure><h1 id="六、主从延迟：原因、监控与治理"><a href="#六、主从延迟：原因、监控与治理" class="headerlink" title="六、主从延迟：原因、监控与治理"></a>六、主从延迟：原因、监控与治理</h1><h2 id="6-1-延迟来源"><a href="#6-1-延迟来源" class="headerlink" title="6.1 延迟来源"></a>6.1 延迟来源</h2><div class="table-container"><table><thead><tr><th>层级</th><th>原因</th><th>应对</th></tr></thead><tbody><tr><td><strong>主库</strong></td><td>大事务、DDL、批量写入</td><td>拆小事务、业务低峰执行</td></tr><tr><td><strong>网络</strong></td><td>跨机房、带宽不足</td><td>就近部署、专线</td></tr><tr><td><strong>从库 SQL 线程</strong></td><td>单线程串行重放</td><td>开启并行复制</td></tr><tr><td><strong>从库硬件</strong></td><td>IO/CPU 弱于主库</td><td>从库配置不低于主库</td></tr></tbody></table></div><h2 id="6-2-延迟监控"><a href="#6-2-延迟监控" class="headerlink" title="6.2 延迟监控"></a>6.2 延迟监控</h2><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">SHOW</span> SLAVE STATUS\G</span><br><span class="line"><span class="comment">-- 关注 Seconds_Behind_Master</span></span><br></pre></td></tr></table></figure><p>更精确的判断：</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">SELECT</span> <span class="built_in">MAX</span>(@<span class="variable">@global</span>.gtid_executed) <span class="operator">-</span> <span class="built_in">MIN</span>(@<span class="variable">@global</span>.gtid_executed) <span class="keyword">FROM</span> ...;</span><br></pre></td></tr></table></figure><div class="note orange anzhiyufont anzhiyu-icon-dengpao disabled"><p><code>Seconds_Behind_Master</code> 为 0 不代表完全无延迟，只能作为趋势参考。</p></div><h2 id="6-3-并行复制"><a href="#6-3-并行复制" class="headerlink" title="6.3 并行复制"></a>6.3 并行复制</h2><p>MySQL 5.6 起支持按库并行，5.7 引入 <code>slave_parallel_type=LOGICAL_CLOCK</code> 可按事务组并行，显著提升重放速度：</p><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">slave_parallel_workers</span>=<span class="number">8</span></span><br><span class="line"><span class="attr">slave_parallel_type</span>=LOGICAL_CLOCK</span><br><span class="line"><span class="attr">slave_preserve_commit_order</span>=<span class="literal">ON</span>  <span class="comment"># 5.7.19+</span></span><br></pre></td></tr></table></figure><h1 id="七、读写分离：把读流量拆出去"><a href="#七、读写分离：把读流量拆出去" class="headerlink" title="七、读写分离：把读流量拆出去"></a>七、读写分离：把读流量拆出去</h1><h2 id="7-1-常见方案对比"><a href="#7-1-常见方案对比" class="headerlink" title="7.1 常见方案对比"></a>7.1 常见方案对比</h2><div class="table-container"><table><thead><tr><th>方案</th><th>位置</th><th>优点</th><th>缺点</th></tr></thead><tbody><tr><td><strong>应用层多数据源</strong></td><td>业务代码</td><td>最灵活</td><td>侵入代码</td></tr><tr><td><strong>MyCat</strong></td><td>中间件</td><td>成熟、插件多</td><td>社区活跃度下降</td></tr><tr><td><strong>ShardingSphere</strong></td><td>JDBC/Proxy 两层</td><td>生态好、文档全</td><td>复杂 SQL 可能有限制</td></tr><tr><td><strong>ProxySQL</strong></td><td>数据库代理</td><td>性能高、规则灵活</td><td>需要额外运维</td></tr></tbody></table></div><h2 id="7-2-读写分离的核心策略"><a href="#7-2-读写分离的核心策略" class="headerlink" title="7.2 读写分离的核心策略"></a>7.2 读写分离的核心策略</h2><ul><li><strong>强制走主库</strong>：写操作、事务内读、对实时性要求高的查询。</li><li><strong>允许走从库</strong>：纯读请求、报表、离线统计。</li><li><strong>延迟容忍</strong>：设置 <code>max_slave_lag</code>，超过阈值自动切回主库。</li></ul><h2 id="7-3-ProxySQL-简单示例"><a href="#7-3-ProxySQL-简单示例" class="headerlink" title="7.3 ProxySQL 简单示例"></a>7.3 ProxySQL 简单示例</h2><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 添加主库 hostgroup 10，从库 hostgroup 20</span></span><br><span class="line"><span class="keyword">INSERT</span> <span class="keyword">INTO</span> mysql_servers(hostgroup_id, hostname, port) <span class="keyword">VALUES</span> (<span class="number">10</span>,<span class="string">&#x27;192.168.1.10&#x27;</span>,<span class="number">3306</span>);</span><br><span class="line"><span class="keyword">INSERT</span> <span class="keyword">INTO</span> mysql_servers(hostgroup_id, hostname, port) <span class="keyword">VALUES</span> (<span class="number">20</span>,<span class="string">&#x27;192.168.1.11&#x27;</span>,<span class="number">3306</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 读写分离规则</span></span><br><span class="line"><span class="keyword">INSERT</span> <span class="keyword">INTO</span> mysql_query_rules (rule_id, active, match_pattern, destination_hostgroup, apply)</span><br><span class="line"><span class="keyword">VALUES</span> (<span class="number">1</span>, <span class="number">1</span>, <span class="string">&#x27;^SELECT.*FOR UPDATE&#x27;</span>, <span class="number">10</span>, <span class="number">1</span>);</span><br><span class="line"><span class="keyword">INSERT</span> <span class="keyword">INTO</span> mysql_query_rules (rule_id, active, match_pattern, destination_hostgroup, apply)</span><br><span class="line"><span class="keyword">VALUES</span> (<span class="number">2</span>, <span class="number">1</span>, <span class="string">&#x27;^SELECT&#x27;</span>, <span class="number">20</span>, <span class="number">1</span>);</span><br></pre></td></tr></table></figure><h1 id="八、高可用切换：MHA-与-Orchestrator"><a href="#八、高可用切换：MHA-与-Orchestrator" class="headerlink" title="八、高可用切换：MHA 与 Orchestrator"></a>八、高可用切换：MHA 与 Orchestrator</h1><h2 id="8-1-MHA（Master-High-Availability）"><a href="#8-1-MHA（Master-High-Availability）" class="headerlink" title="8.1 MHA（Master High Availability）"></a>8.1 MHA（Master High Availability）</h2><p>传统 MySQL 高可用方案，自动完成主库故障检测、新主库选举、VIP 漂移、其余从库重新指向。</p><p><strong>适用场景</strong>：5.7 及之前版本，配合 VIP 脚本。</p><h2 id="8-2-Orchestrator"><a href="#8-2-Orchestrator" class="headerlink" title="8.2 Orchestrator"></a>8.2 Orchestrator</h2><p>GitHub 开源的 MySQL 拓扑管理与可视化工具：</p><ul><li>自动探测复制拓扑。</li><li>Web UI 可视化主从链。</li><li>支持 hook 脚本，可集成 Consul/DNS 做服务发现。</li><li>可以进行 graceful 切换（planned promotion）和故障切换（emergency promotion）。</li></ul><h2 id="8-3-切换时的一致性问题"><a href="#8-3-切换时的一致性问题" class="headerlink" title="8.3 切换时的一致性问题"></a>8.3 切换时的一致性问题</h2><ul><li>异步复制下，主库宕机可能丢失未同步到从库的事务。</li><li>半同步复制 + GTID 可在切换后确认是否补齐数据。</li><li>核心指标：<strong>RPO</strong>（数据丢失量）和 <strong>RTO</strong>（恢复时间）。</li></ul><h1 id="九、生产避坑清单"><a href="#九、生产避坑清单" class="headerlink" title="九、生产避坑清单"></a>九、生产避坑清单</h1><div class="table-container"><table><thead><tr><th>坑</th><th>后果</th><th>建议</th></tr></thead><tbody><tr><td>从库配置低于主库</td><td>延迟越拉越大</td><td>从库 CPU/内存/磁盘对齐主库</td></tr><tr><td>写从库</td><td>主从不同步</td><td>从库设置 read_only=1</td></tr><tr><td>大事务同步</td><td>Seconds_Behind_Master 飙升</td><td>拆分批量写入</td></tr><tr><td>跨机房复制</td><td>网络抖动、延迟</td><td>专线或就近部署</td></tr><tr><td>无 GTID</td><td>切换时找位点痛苦</td><td>5.7+ 尽量启用 GTID</td></tr><tr><td>不监控复制延迟</td><td>读到旧数据引发业务 bug</td><td>监控 Seconds_Behind_Master + GTID gap</td></tr></tbody></table></div><h1 id="十、总结"><a href="#十、总结" class="headerlink" title="十、总结"></a>十、总结</h1><p>MySQL 主从复制是数据库扩展和容灾的基石：</p><ul><li>用 <strong>binlog + ROW 格式</strong> 保证复制安全。</li><li>用 <strong>半同步复制 + GTID</strong> 平衡一致性与性能。</li><li>用 <strong>并行复制 + 硬件对齐</strong> 降低延迟。</li><li>用 <strong>读写分离中间件</strong> 把读压力拆出去。</li><li>用 <strong>Orchestrator / MHA</strong> 实现故障自动切换。</li></ul><p>复制不是银弹，它解决的是<strong>读扩展和高可用</strong>，不是无限写扩展。当写流量单库扛不住时，就需要考虑分库分表、TiDB 等 NewSQL 方案。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;div class=&quot;tip warning&quot;&gt;&lt;p&gt;“线上读写都在一台 MySQL，CPU 飙到 90% 怎么办？””主库挂了，多久能切换？”主从复制和读写分离是 MySQL 横向扩展与容灾的必经之路。本文从复制原理到生产落地一次性讲透。&lt;/p&gt;
&lt;/div&gt;
&lt;h1 id=</summary>
      
    
    
    
    <category term="经验分享" scheme="http://hexo.sjava.cn/categories/%E7%BB%8F%E9%AA%8C%E5%88%86%E4%BA%AB/"/>
    
    
    <category term="MySQL" scheme="http://hexo.sjava.cn/tags/MySQL/"/>
    
    <category term="数据库" scheme="http://hexo.sjava.cn/tags/%E6%95%B0%E6%8D%AE%E5%BA%93/"/>
    
    <category term="主从复制" scheme="http://hexo.sjava.cn/tags/%E4%B8%BB%E4%BB%8E%E5%A4%8D%E5%88%B6/"/>
    
    <category term="读写分离" scheme="http://hexo.sjava.cn/tags/%E8%AF%BB%E5%86%99%E5%88%86%E7%A6%BB/"/>
    
    <category term="高可用" scheme="http://hexo.sjava.cn/tags/%E9%AB%98%E5%8F%AF%E7%94%A8/"/>
    
  </entry>
  
  <entry>
    <title>MySQL 事务与锁机制：MVCC、幻读与死锁深度解析</title>
    <link href="http://hexo.sjava.cn/posts/64120.html"/>
    <id>http://hexo.sjava.cn/posts/64120.html</id>
    <published>2026-09-08T12:06:00.000Z</published>
    <updated>2026-09-08T12:06:00.000Z</updated>
    
    <content type="html"><![CDATA[<div class="tip warning"><p>“我用的是 RR 隔离级别，为什么还是出现了幻读？””两个事务各更新一行，结果直接死锁了？”事务和锁是 MySQL 里最容易翻车、也最常被面试官追问的部分。本文从原理到实战一次讲透，建议收藏对照。</p></div><h1 id="一、为什么需要事务"><a href="#一、为什么需要事务" class="headerlink" title="一、为什么需要事务"></a>一、为什么需要事务</h1><p>没有事务时，一组操作要么全部成功、要么全部失败就无法保证。典型场景：账户 A 给账户 B 转账 100 元，需要执行「A 扣 100」「B 加 100」两条 SQL。如果在两条语句之间数据库宕机，就会出现 A 的钱扣了、B 的钱没加的<strong>数据不一致</strong>。</p><p>事务（Transaction）的作用，就是把多个操作打包成一个<strong>不可分割</strong>的整体，保证业务的正确性。</p><h1 id="二、事务的四大特性：ACID"><a href="#二、事务的四大特性：ACID" class="headerlink" title="二、事务的四大特性：ACID"></a>二、事务的四大特性：ACID</h1><div class="table-container"><table><thead><tr><th>特性</th><th>含义</th><th>InnoDB 如何实现</th></tr></thead><tbody><tr><td><strong>A</strong>tomicity 原子性</td><td>事务内的操作要么全做、要么全不做</td><td>undo log（回滚日志）</td></tr><tr><td><strong>C</strong>onsistency 一致性</td><td>事务前后数据都满足业务约束</td><td>由 A、I、D 共同保证</td></tr><tr><td><strong>I</strong>solation 隔离性</td><td>并发事务之间互不影响</td><td>锁 + MVCC</td></tr><tr><td><strong>D</strong>urability 持久性</td><td>提交后数据永久保存</td><td>redo log（重做日志）</td></tr></tbody></table></div><h2 id="2-1-原子性靠-undo-log"><a href="#2-1-原子性靠-undo-log" class="headerlink" title="2.1 原子性靠 undo log"></a>2.1 原子性靠 undo log</h2><p>每一条写操作（INSERT/UPDATE/DELETE）执行前，InnoDB 都会先把「反向操作」记录到 undo log。事务回滚时，按 undo log 反向执行即可恢复到事务前的状态。</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 逻辑示意：更新前先把旧值记入 undo log</span></span><br><span class="line"><span class="keyword">UPDATE</span> account <span class="keyword">SET</span> balance <span class="operator">=</span> balance <span class="operator">-</span> <span class="number">100</span> <span class="keyword">WHERE</span> id <span class="operator">=</span> <span class="string">&#x27;A&#x27;</span>;</span><br><span class="line"><span class="comment">-- 若失败回滚，InnoDB 根据 undo log 把 A 的 balance 还原</span></span><br></pre></td></tr></table></figure><h2 id="2-2-持久性靠-redo-log"><a href="#2-2-持久性靠-redo-log" class="headerlink" title="2.2 持久性靠 redo log"></a>2.2 持久性靠 redo log</h2><p>数据先写内存（Buffer Pool），再异步刷盘，这之间存在崩溃风险。redo log 采用 <strong>WAL（Write-Ahead Logging）</strong> 机制：事务提交时，先把修改记录到 redo log（顺序写、速度快），即使还没刷到数据页，崩溃后也能根据 redo log 重放恢复。</p><div class="note orange anzhiyufont anzhiyu-icon-dengpao disabled"><p>一句话记忆：undo log 负责「回滚」，redo log 负责「重做」，二者共同撑起 ACID。</p></div><h1 id="三、隔离级别与并发三兄弟"><a href="#三、隔离级别与并发三兄弟" class="headerlink" title="三、隔离级别与并发三兄弟"></a>三、隔离级别与并发三兄弟</h1><p>当多个事务并发执行，会出现三类经典问题：</p><div class="table-container"><table><thead><tr><th>问题</th><th>描述</th><th>例子</th></tr></thead><tbody><tr><td><strong>脏读</strong></td><td>读到另一个事务<strong>未提交</strong>的数据</td><td>事务 A 改了值还没提交，事务 B 读到了，A 万一回滚，B 读到就是脏数据</td></tr><tr><td><strong>不可重复读</strong></td><td>同一事务内<strong>两次读同一行，结果不同</strong>（侧重修改）</td><td>事务 A 读余额为 100，事务 B 改成了 200 并提交，A 再读变成 200</td></tr><tr><td><strong>幻读</strong></td><td>同一事务内<strong>两次范围查询，行数不同</strong>（侧重增删）</td><td>事务 A <code>SELECT WHERE age&gt;20</code> 得到 3 行，事务 B 插入 1 行并提交，A 再查得到 4 行</td></tr></tbody></table></div><p>SQL 标准定义了四种隔离级别：</p><div class="table-container"><table><thead><tr><th>隔离级别</th><th>脏读</th><th>不可重复读</th><th>幻读</th></tr></thead><tbody><tr><td>READ UNCOMMITTED（读未提交）</td><td>❌ 可能</td><td>❌ 可能</td><td>❌ 可能</td></tr><tr><td>READ COMMITTED（读已提交，RC）</td><td>✅ 解决</td><td>❌ 可能</td><td>❌ 可能</td></tr><tr><td>REPEATABLE READ（可重复读，RR）</td><td>✅ 解决</td><td>✅ 解决</td><td>⚠️ 基本解决（见第五节）</td></tr><tr><td>SERIALIZABLE（串行化）</td><td>✅ 解决</td><td>✅ 解决</td><td>✅ 解决</td></tr></tbody></table></div><blockquote><p><strong>重点</strong>：MySQL InnoDB 默认的隔离级别是 <strong>REPEATABLE READ</strong>，并且通过 MVCC + 临键锁在「大部分场景」下已经能避免幻读，这是它和标准 SQL 定义不同的地方。</p></blockquote><h1 id="四、MVCC-多版本并发控制"><a href="#四、MVCC-多版本并发控制" class="headerlink" title="四、MVCC 多版本并发控制"></a>四、MVCC 多版本并发控制</h1><p>MVCC（Multi-Version Concurrency Control）是 InnoDB 实现「读不加锁、读写不阻塞」的核心。它的本质是：<strong>为每行数据保存多个历史版本，读请求根据事务的「可见性规则」找到属于自己的那个版本</strong>。</p><h2 id="4-1-行记录的隐藏字段"><a href="#4-1-行记录的隐藏字段" class="headerlink" title="4.1 行记录的隐藏字段"></a>4.1 行记录的隐藏字段</h2><p>InnoDB 每行数据除了你定义的列，还有几个隐藏字段：</p><div class="table-container"><table><thead><tr><th>字段</th><th>作用</th></tr></thead><tbody><tr><td><code>DB_TRX_ID</code>（事务 ID）</td><td>最近一次修改该行的事务 ID</td></tr><tr><td><code>DB_ROLL_PTR</code>（回滚指针）</td><td>指向 undo log 中该行上一个版本的指针</td></tr><tr><td><code>DB_ROW_ID</code></td><td>隐式主键（无主键时由它充当）</td></tr></tbody></table></div><h2 id="4-2-undo-log-版本链"><a href="#4-2-undo-log-版本链" class="headerlink" title="4.2 undo log 版本链"></a>4.2 undo log 版本链</h2><p>每次更新一行，旧版本并不会立刻删除，而是被写入 undo log，并通过 <code>DB_ROLL_PTR</code> 串成一条<strong>版本链</strong>。链条头部是最新版本，尾部是最早版本。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">最新版本 (trx_id=20)</span><br><span class="line">   │ roll_ptr</span><br><span class="line">   ▼</span><br><span class="line">旧版本 (trx_id=15)</span><br><span class="line">   │ roll_ptr</span><br><span class="line">   ▼</span><br><span class="line">更旧版本 (trx_id=10)</span><br><span class="line">   │ roll_ptr</span><br><span class="line">   ▼</span><br><span class="line">   NULL（初始插入版本）</span><br></pre></td></tr></table></figure><h2 id="4-3-ReadView：判断哪个版本「对我可见」"><a href="#4-3-ReadView：判断哪个版本「对我可见」" class="headerlink" title="4.3 ReadView：判断哪个版本「对我可见」"></a>4.3 ReadView：判断哪个版本「对我可见」</h2><p>事务执行快照读时，InnoDB 会生成一个 <strong>ReadView</strong>，里面记录了：</p><ul><li><code>m_ids</code>：当前<strong>活跃（未提交）</strong>的事务 ID 列表</li><li><code>min_trx_id</code>：<code>m_ids</code> 中的最小值</li><li><code>max_trx_id</code>：下一个将要分配的事务 ID</li><li><code>creator_trx_id</code>：创建该 ReadView 的事务 ID</li></ul><p>对版本链上的每一版，可见性判断规则：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">版本的事务 ID = trx_id</span><br><span class="line">1. trx_id == creator_trx_id        → 可见（自己改的）</span><br><span class="line">2. trx_id &lt; min_trx_id            → 可见（在 ReadView 创建前已提交）</span><br><span class="line">3. trx_id &gt;= max_trx_id           → 不可见（ReadView 创建后才开启）</span><br><span class="line">4. min_trx_id &lt;= trx_id &lt; max_trx_id：</span><br><span class="line">      ├─ trx_id 在 m_ids 中  → 不可见（该事务还没提交）</span><br><span class="line">      └─ trx_id 不在 m_ids → 可见（已提交）</span><br><span class="line">不可见就顺着 roll_ptr 找上一个版本，直到找到可见版本或链尾</span><br></pre></td></tr></table></figure><h2 id="4-4-RC-与-RR-的关键差异：ReadView-生成时机"><a href="#4-4-RC-与-RR-的关键差异：ReadView-生成时机" class="headerlink" title="4.4 RC 与 RR 的关键差异：ReadView 生成时机"></a>4.4 RC 与 RR 的关键差异：ReadView 生成时机</h2><div class="table-container"><table><thead><tr><th>隔离级别</th><th>ReadView 生成时机</th><th>效果</th></tr></thead><tbody><tr><td>READ COMMITTED</td><td><strong>每次</strong> SELECT 都生成新 ReadView</td><td>能看到别的事务<strong>最新已提交</strong>的数据 → 不可重复读</td></tr><tr><td>REPEATABLE READ</td><td><strong>第一次</strong> SELECT 时生成，之后整个事务复用</td><td>始终看到同一份快照 → 可重复读</td></tr></tbody></table></div><div class="note green anzhiyufont anzhiyu-icon-chenggong disabled"><p>这就是为什么 RR 能实现「可重复读」：整个事务只认第一次读时生成的快照，别人的提交对它「不可见」。</p></div><h1 id="五、幻读：快照读与当前读"><a href="#五、幻读：快照读与当前读" class="headerlink" title="五、幻读：快照读与当前读"></a>五、幻读：快照读与当前读</h1><h2 id="5-1-快照读-vs-当前读"><a href="#5-1-快照读-vs-当前读" class="headerlink" title="5.1 快照读 vs 当前读"></a>5.1 快照读 vs 当前读</h2><ul><li><strong>快照读（Snapshot Read）</strong>：普通 <code>SELECT</code>，基于 MVCC 读历史版本，不加锁。</li><li><strong>当前读（Current Read）</strong>：<code>SELECT ... FOR UPDATE</code>、<code>SELECT ... LOCK IN SHARE MODE</code>、<code>INSERT/UPDATE/DELETE</code>，读的是<strong>最新已提交</strong>版本，并加锁。</li></ul><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> <span class="keyword">user</span> <span class="keyword">WHERE</span> age <span class="operator">&gt;</span> <span class="number">20</span>;          <span class="comment">-- 快照读，无锁</span></span><br><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> <span class="keyword">user</span> <span class="keyword">WHERE</span> age <span class="operator">&gt;</span> <span class="number">20</span> <span class="keyword">FOR</span> <span class="keyword">UPDATE</span>; <span class="comment">-- 当前读，加临键锁</span></span><br></pre></td></tr></table></figure><h2 id="5-2-间隙锁与临键锁如何解决幻读"><a href="#5-2-间隙锁与临键锁如何解决幻读" class="headerlink" title="5.2 间隙锁与临键锁如何解决幻读"></a>5.2 间隙锁与临键锁如何解决幻读</h2><p>光靠 MVCC 的快照读只能解决「自己事务内看到的内容稳定」，但无法阻止别的事务<strong>插入新行</strong>。InnoDB 在 RR 下通过<strong>锁</strong>来堵住这个口子：</p><div class="table-container"><table><thead><tr><th>锁类型</th><th>锁定范围</th><th>说明</th></tr></thead><tbody><tr><td>记录锁 Record Lock</td><td>锁住某一行</td><td>最基础的行锁</td></tr><tr><td>间隙锁 Gap Lock</td><td>锁住两条记录之间的「空隙」</td><td>防止在空隙中插入新记录</td></tr><tr><td>临键锁 Next-Key Lock</td><td>记录锁 + 间隙锁（左开右闭）</td><td>RR 下的默认行锁算法</td></tr></tbody></table></div><p>例如 <code>SELECT * FROM user WHERE age &gt; 20 FOR UPDATE</code>，InnoDB 会对 <code>(20, +∞)</code> 这个区间加临键锁，<strong>禁止其他事务往这段区间插入新行</strong>，从而杜绝幻读。</p><div class="tip warning"><p>注意：RR 下的「解决幻读」指的是<strong>当前读</strong>场景。如果你的事务全程只用普通 <code>SELECT</code>（快照读），确实不会幻读；但如果混合了 <code>UPDATE/INSERT</code> 或 <code>FOR UPDATE</code>，靠的正是临键锁在保护你。</p></div><h1 id="六、InnoDB-锁体系总览"><a href="#六、InnoDB-锁体系总览" class="headerlink" title="六、InnoDB 锁体系总览"></a>六、InnoDB 锁体系总览</h1><h2 id="6-1-行锁、表锁与意向锁"><a href="#6-1-行锁、表锁与意向锁" class="headerlink" title="6.1 行锁、表锁与意向锁"></a>6.1 行锁、表锁与意向锁</h2><div class="table-container"><table><thead><tr><th>锁</th><th>粒度</th><th>说明</th></tr></thead><tbody><tr><td>行锁</td><td>行</td><td>InnoDB 支持，MyISAM 不支持</td></tr><tr><td>表锁</td><td>表</td><td>整张表锁定</td></tr><tr><td>意向锁（IS/IX）</td><td>表</td><td><strong>意向</strong>标记，表级，用于快速判断能否加表锁，本身不阻塞行锁</td></tr></tbody></table></div><p>意向锁是「占位符」：事务准备给某几行加行锁前，先给整张表加一个意向锁，这样别的事务想加表锁时不用逐行扫描，直接看意向锁即可。</p><h2 id="6-2-锁的兼容矩阵"><a href="#6-2-锁的兼容矩阵" class="headerlink" title="6.2 锁的兼容矩阵"></a>6.2 锁的兼容矩阵</h2><div class="table-container"><table><thead><tr><th>已持有 \ 请求</th><th>IS</th><th>IX</th><th>S（表共享）</th><th>X（表排他）</th></tr></thead><tbody><tr><td>IS</td><td>✅</td><td>✅</td><td>✅</td><td>❌</td></tr><tr><td>IX</td><td>✅</td><td>✅</td><td>❌</td><td>❌</td></tr><tr><td>S</td><td>✅</td><td>❌</td><td>✅</td><td>❌</td></tr><tr><td>X</td><td>❌</td><td>❌</td><td>❌</td><td>❌</td></tr></tbody></table></div><h2 id="6-3-行锁加在哪？索引是关键"><a href="#6-3-行锁加在哪？索引是关键" class="headerlink" title="6.3 行锁加在哪？索引是关键"></a>6.3 行锁加在哪？索引是关键</h2><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 假设 id 是主键，name 有普通索引，age 无索引</span></span><br><span class="line"><span class="keyword">UPDATE</span> <span class="keyword">user</span> <span class="keyword">SET</span> age <span class="operator">=</span> <span class="number">30</span> <span class="keyword">WHERE</span> id <span class="operator">=</span> <span class="number">1</span>;        <span class="comment">-- 只锁 id=1 这一行 ✅</span></span><br><span class="line"><span class="keyword">UPDATE</span> <span class="keyword">user</span> <span class="keyword">SET</span> age <span class="operator">=</span> <span class="number">30</span> <span class="keyword">WHERE</span> name <span class="operator">=</span> <span class="string">&#x27;Tom&#x27;</span>;  <span class="comment">-- 锁 name=&#x27;Tom&#x27; 的行（走索引）✅</span></span><br><span class="line"><span class="keyword">UPDATE</span> <span class="keyword">user</span> <span class="keyword">SET</span> age <span class="operator">=</span> <span class="number">30</span> <span class="keyword">WHERE</span> age <span class="operator">=</span> <span class="number">30</span>;      <span class="comment">-- 全表扫描！退化为表锁 ❌ 危险</span></span><br></pre></td></tr></table></figure><div class="note orange anzhiyufont anzhiyu-icon-dengpao disabled"><p>生产铁律：UPDATE/DELETE 的 WHERE 条件<strong>必须走索引</strong>，否则行锁升级为表锁，直接拖垮整张表并发。这与「索引失效」一文中的结论一脉相承。</p></div><h1 id="七、死锁：成因、案例与排查"><a href="#七、死锁：成因、案例与排查" class="headerlink" title="七、死锁：成因、案例与排查"></a>七、死锁：成因、案例与排查</h1><h2 id="7-1-死锁的四个必要条件"><a href="#7-1-死锁的四个必要条件" class="headerlink" title="7.1 死锁的四个必要条件"></a>7.1 死锁的四个必要条件</h2><ol><li><strong>互斥</strong>：资源只能被一个事务占用</li><li><strong>持有并等待</strong>：持有锁的同时还在等别的锁</li><li><strong>不可剥夺</strong>：锁不能被强行抢占</li><li><strong>循环等待</strong>：A 等 B 的锁、B 等 A 的锁，形成环</li></ol><p>四个条件<strong>同时满足</strong>才会死锁，打破任意一个即可解除。</p><h2 id="7-2-经典死锁案例"><a href="#7-2-经典死锁案例" class="headerlink" title="7.2 经典死锁案例"></a>7.2 经典死锁案例</h2><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 事务 A</span></span><br><span class="line"><span class="keyword">BEGIN</span>;</span><br><span class="line"><span class="keyword">UPDATE</span> account <span class="keyword">SET</span> balance <span class="operator">=</span> balance <span class="operator">-</span> <span class="number">100</span> <span class="keyword">WHERE</span> id <span class="operator">=</span> <span class="string">&#x27;A&#x27;</span>;  <span class="comment">-- 持有 A 的锁</span></span><br><span class="line"><span class="keyword">UPDATE</span> account <span class="keyword">SET</span> balance <span class="operator">=</span> balance <span class="operator">+</span> <span class="number">100</span> <span class="keyword">WHERE</span> id <span class="operator">=</span> <span class="string">&#x27;B&#x27;</span>;  <span class="comment">-- 等待 B 的锁</span></span><br><span class="line"></span><br><span class="line"><span class="comment">-- 事务 B（几乎同时）</span></span><br><span class="line"><span class="keyword">BEGIN</span>;</span><br><span class="line"><span class="keyword">UPDATE</span> account <span class="keyword">SET</span> balance <span class="operator">=</span> balance <span class="operator">-</span> <span class="number">50</span> <span class="keyword">WHERE</span> id <span class="operator">=</span> <span class="string">&#x27;B&#x27;</span>;  <span class="comment">-- 持有 B 的锁</span></span><br><span class="line"><span class="keyword">UPDATE</span> account <span class="keyword">SET</span> balance <span class="operator">=</span> balance <span class="operator">+</span> <span class="number">50</span> <span class="keyword">WHERE</span> id <span class="operator">=</span> <span class="string">&#x27;A&#x27;</span>;  <span class="comment">-- 等待 A 的锁</span></span><br></pre></td></tr></table></figure><p>事务 A 持有 A 等 B，事务 B 持有 B 等 A → <strong>循环等待 → 死锁</strong>。InnoDB 默认会检测死锁，并<strong>回滚其中一个代价较小的事务</strong>，另一个得以继续执行（报错：<code>Deadlock found when trying to get lock</code>）。</p><h2 id="7-3-排查死锁"><a href="#7-3-排查死锁" class="headerlink" title="7.3 排查死锁"></a>7.3 排查死锁</h2><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">SHOW</span> ENGINE INNODB STATUS;   <span class="comment">-- 查看最近一次死锁的详细信息（LATEST DETECTED DEADLOCK 段）</span></span><br><span class="line"><span class="keyword">SHOW</span> PROCESSLIST;            <span class="comment">-- 查看当前所有连接和锁等待</span></span><br><span class="line"><span class="comment">-- 开启死锁日志（my.cnf）</span></span><br><span class="line"><span class="comment">-- innodb_print_all_deadlocks = 1</span></span><br></pre></td></tr></table></figure><h2 id="7-4-避免死锁的实战建议"><a href="#7-4-避免死锁的实战建议" class="headerlink" title="7.4 避免死锁的实战建议"></a>7.4 避免死锁的实战建议</h2><ul><li><strong>统一加锁顺序</strong>：所有事务都按相同顺序（如先 A 后 B）访问资源，避免交叉等待。</li><li><strong>缩短事务</strong>：事务越长，持有锁越久，冲突概率越高，尽量把大事务拆小。</li><li><strong>降低锁粒度</strong>：用 <code>SELECT ... FOR UPDATE</code> 时范围尽量小，避免无索引导致表锁。</li><li><strong>合理重试</strong>：捕获死锁异常后，业务层做有限次数的重试（被回滚的那个事务）。</li></ul><h1 id="八、生产避坑总结"><a href="#八、生产避坑总结" class="headerlink" title="八、生产避坑总结"></a>八、生产避坑总结</h1><div class="table-container"><table><thead><tr><th>场景</th><th>风险</th><th>正确做法</th></tr></thead><tbody><tr><td>大事务</td><td>长时间持锁、主从延迟</td><td>拆分事务、批量提交</td></tr><tr><td>无索引更新</td><td>行锁退化为表锁</td><td>确保 WHERE 走索引</td></tr><tr><td>混用快照读/当前读</td><td>误以为不会幻读</td><td>明确自己需要的是哪类读</td></tr><tr><td>并发更新同一行</td><td>锁等待、死锁</td><td>统一顺序、控制并发</td></tr><tr><td>RC 下统计</td><td>前后不一致</td><td>需要可重复读时用 RR</td></tr></tbody></table></div><div class="note green anzhiyufont anzhiyu-icon-chenggong disabled"><p>总结：事务靠 ACID 立身，隔离性由「MVCC + 锁」双管齐下；快照读靠 ReadView 看历史、当前读靠临键锁防插入；行锁的命门在索引；死锁靠「统一顺序 + 短事务」化解。理解这套逻辑，MySQL 并发难题基本都能迎刃而解。</p></div>]]></content>
    
    
      
      
    <summary type="html">&lt;div class=&quot;tip warning&quot;&gt;&lt;p&gt;“我用的是 RR 隔离级别，为什么还是出现了幻读？””两个事务各更新一行，结果直接死锁了？”事务和锁是 MySQL 里最容易翻车、也最常被面试官追问的部分。本文从原理到实战一次讲透，建议收藏对照。&lt;/p&gt;
&lt;/div&gt;
&lt;h</summary>
      
    
    
    
    <category term="经验分享" scheme="http://hexo.sjava.cn/categories/%E7%BB%8F%E9%AA%8C%E5%88%86%E4%BA%AB/"/>
    
    
    <category term="并发编程" scheme="http://hexo.sjava.cn/tags/%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B/"/>
    
    <category term="MySQL" scheme="http://hexo.sjava.cn/tags/MySQL/"/>
    
    <category term="数据库" scheme="http://hexo.sjava.cn/tags/%E6%95%B0%E6%8D%AE%E5%BA%93/"/>
    
    <category term="事务" scheme="http://hexo.sjava.cn/tags/%E4%BA%8B%E5%8A%A1/"/>
    
    <category term="锁机制" scheme="http://hexo.sjava.cn/tags/%E9%94%81%E6%9C%BA%E5%88%B6/"/>
    
  </entry>
  
  <entry>
    <title>MySQL 索引失效的 12 种场景与慢查询优化实战</title>
    <link href="http://hexo.sjava.cn/posts/59217.html"/>
    <id>http://hexo.sjava.cn/posts/59217.html</id>
    <published>2026-08-31T09:30:00.000Z</published>
    <updated>2026-08-31T09:30:00.000Z</updated>
    
    <content type="html"><![CDATA[<div class="tip warning"><p>“我明明建了索引，为什么还是全表扫描？”这是 MySQL 优化里最常遇到的问题。索引失效的原因很多，但归根结底就两大类：<strong>写法让索引没法用</strong>，或者<strong>优化器算完账觉得不用更快</strong>。本文把两类都拆开讲。</p></div><h1 id="一、先搞懂-InnoDB-的索引结构"><a href="#一、先搞懂-InnoDB-的索引结构" class="headerlink" title="一、先搞懂 InnoDB 的索引结构"></a>一、先搞懂 InnoDB 的索引结构</h1><p>不理解 B+Tree，就记不住那些失效规则，只能死记硬背。</p><h2 id="1-1-为什么是-B-Tree-而不是-BTree"><a href="#1-1-为什么是-B-Tree-而不是-BTree" class="headerlink" title="1.1 为什么是 B+Tree 而不是 BTree"></a>1.1 为什么是 B+Tree 而不是 BTree</h2><div class="table-container"><table><thead><tr><th>对比项</th><th>BTree</th><th>B+Tree</th></tr></thead><tbody><tr><td>数据存储位置</td><td>每个节点都存数据</td><td><strong>只有叶子节点存数据</strong></td></tr><tr><td>叶子节点连接</td><td>无</td><td>有双向链表相连</td></tr><tr><td>单点查询性能</td><td>可能更快（根附近就命中）</td><td>稳定，都要走到叶子</td></tr><tr><td>范围查询</td><td>需要中序遍历</td><td><strong>沿链表顺序扫描，极快</strong></td></tr><tr><td>非叶子节点容量</td><td>小（要存数据）</td><td>大（只存键值），<strong>树更矮</strong></td></tr></tbody></table></div><p>B+Tree 的优势集中在一句话上：<strong>非叶子节点不存数据，所以一个页能装下更多键值，树高更矮，磁盘 IO 次数更少</strong>。</p><p>粗略估算：InnoDB 默认页大小 16 KB，主键为 bigint（8 字节）+ 指针（6 字节）约 14 字节，一个非叶子页能放约 1170 个键值。</p><ul><li>树高 2 层：约 1170 × 16 ≈ 1.8 万行</li><li>树高 3 层：约 1170 × 1170 × 16 ≈ <strong>2190 万行</strong></li><li>树高 4 层：约 256 亿行</li></ul><p><strong>两千万数据只需 3 次磁盘 IO</strong>，这就是索引快的根本原因。</p><h2 id="1-2-聚簇索引与二级索引"><a href="#1-2-聚簇索引与二级索引" class="headerlink" title="1.2 聚簇索引与二级索引"></a>1.2 聚簇索引与二级索引</h2><p>InnoDB 的主键索引就是<strong>聚簇索引</strong> —— 叶子节点存的是<strong>整行数据</strong>。而二级索引（普通索引、唯一索引）的叶子节点存的只是<strong>主键值</strong>。</p><p>这带来一个关键概念：<strong>回表</strong>。</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- name 上有二级索引</span></span><br><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> <span class="keyword">user</span> <span class="keyword">WHERE</span> name <span class="operator">=</span> <span class="string">&#x27;张三&#x27;</span>;</span><br></pre></td></tr></table></figure><p>执行过程分两步：</p><ol><li>在 <code>idx_name</code> 这棵 B+Tree 上找到 <code>name=&#39;张三&#39;</code> 对应的<strong>主键 id</strong></li><li>拿着这个 id 回到聚簇索引再查一次，取出完整行数据</li></ol><p>第二步就是回表，多一次 B+Tree 查找。</p><h2 id="1-3-覆盖索引：避免回表"><a href="#1-3-覆盖索引：避免回表" class="headerlink" title="1.3 覆盖索引：避免回表"></a>1.3 覆盖索引：避免回表</h2><p>如果查询的字段全都在索引里，就不需要回表：</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 建立联合索引 (name, age)</span></span><br><span class="line"><span class="keyword">CREATE</span> INDEX idx_name_age <span class="keyword">ON</span> <span class="keyword">user</span>(name, age);</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 只查 name 和 age，索引里全都有，Extra 显示 Using index</span></span><br><span class="line"><span class="keyword">SELECT</span> name, age <span class="keyword">FROM</span> <span class="keyword">user</span> <span class="keyword">WHERE</span> name <span class="operator">=</span> <span class="string">&#x27;张三&#x27;</span>;</span><br></pre></td></tr></table></figure><div class="note green anzhiyufont anzhiyu-icon-chenggong disabled"><p>覆盖索引是最容易被忽略的优化手段。把 <code>SELECT *</code> 改成只查需要的字段，配合合理的联合索引，往往能让查询快一个数量级，而且这属于<strong>零成本的改动</strong>。</p></div><h1 id="二、12-种索引失效场景"><a href="#二、12-种索引失效场景" class="headerlink" title="二、12 种索引失效场景"></a>二、12 种索引失效场景</h1><p>以下示例基于这张表：</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">CREATE</span> <span class="keyword">TABLE</span> `<span class="keyword">user</span>` (</span><br><span class="line">  `id`       <span class="type">bigint</span> <span class="keyword">NOT</span> <span class="keyword">NULL</span> AUTO_INCREMENT,</span><br><span class="line">  `name`     <span class="type">varchar</span>(<span class="number">50</span>)  <span class="keyword">NOT</span> <span class="keyword">NULL</span>,</span><br><span class="line">  `phone`    <span class="type">varchar</span>(<span class="number">20</span>)  <span class="keyword">NOT</span> <span class="keyword">NULL</span>,</span><br><span class="line">  `age`      <span class="type">int</span>          <span class="keyword">NOT</span> <span class="keyword">NULL</span>,</span><br><span class="line">  `city`     <span class="type">varchar</span>(<span class="number">20</span>)  <span class="keyword">NOT</span> <span class="keyword">NULL</span>,</span><br><span class="line">  `status`   tinyint      <span class="keyword">NOT</span> <span class="keyword">NULL</span>,</span><br><span class="line">  `create_time` datetime  <span class="keyword">NOT</span> <span class="keyword">NULL</span>,</span><br><span class="line">  <span class="keyword">PRIMARY</span> KEY (`id`),</span><br><span class="line">  KEY `idx_name_age_city` (`name`, `age`, `city`),</span><br><span class="line">  KEY `idx_phone` (`phone`),</span><br><span class="line">  KEY `idx_create_time` (`create_time`)</span><br><span class="line">) ENGINE<span class="operator">=</span>InnoDB <span class="keyword">DEFAULT</span> CHARSET<span class="operator">=</span>utf8mb4;</span><br></pre></td></tr></table></figure><h2 id="场景-1：违反最左前缀法则"><a href="#场景-1：违反最左前缀法则" class="headerlink" title="场景 1：违反最左前缀法则"></a>场景 1：违反最左前缀法则</h2><p>联合索引 <code>(name, age, city)</code> 的排序逻辑是<strong>先按 name 排，name 相同再按 age 排，age 也相同才按 city 排</strong>。</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- ✅ 走索引，用到 name</span></span><br><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> <span class="keyword">user</span> <span class="keyword">WHERE</span> name <span class="operator">=</span> <span class="string">&#x27;张三&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">-- ✅ 走索引，用到 name + age</span></span><br><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> <span class="keyword">user</span> <span class="keyword">WHERE</span> name <span class="operator">=</span> <span class="string">&#x27;张三&#x27;</span> <span class="keyword">AND</span> age <span class="operator">=</span> <span class="number">28</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">-- ✅ 走索引，三个都用上</span></span><br><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> <span class="keyword">user</span> <span class="keyword">WHERE</span> name <span class="operator">=</span> <span class="string">&#x27;张三&#x27;</span> <span class="keyword">AND</span> age <span class="operator">=</span> <span class="number">28</span> <span class="keyword">AND</span> city <span class="operator">=</span> <span class="string">&#x27;杭州&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">-- ❌ 跳过 name 直接从 age 查，索引失效</span></span><br><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> <span class="keyword">user</span> <span class="keyword">WHERE</span> age <span class="operator">=</span> <span class="number">28</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">-- ❌ 跳过 age，name 之后的 city 用不上</span></span><br><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> <span class="keyword">user</span> <span class="keyword">WHERE</span> name <span class="operator">=</span> <span class="string">&#x27;张三&#x27;</span> <span class="keyword">AND</span> city <span class="operator">=</span> <span class="string">&#x27;杭州&#x27;</span>;</span><br></pre></td></tr></table></figure><div class="note blue anzhiyufont anzhiyu-icon-lightbulb disabled"><p>记忆方法：联合索引就像查字典，必须先知道第一个字母才能往后翻。不过 MySQL 8.0 引入了<strong>索引跳跃扫描（Index Skip Scan）</strong>，在首列区分度极低时优化器可能跳过它。但别依赖这个特性，建索引时还是按最左前缀来设计。</p></div><h2 id="场景-2：索引列上使用函数"><a href="#场景-2：索引列上使用函数" class="headerlink" title="场景 2：索引列上使用函数"></a>场景 2：索引列上使用函数</h2><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- ❌ 对索引列做函数运算，B+Tree 的有序性被破坏</span></span><br><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> <span class="keyword">user</span> <span class="keyword">WHERE</span> <span class="keyword">YEAR</span>(create_time) <span class="operator">=</span> <span class="number">2026</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">-- ✅ 改写成范围查询</span></span><br><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> <span class="keyword">user</span></span><br><span class="line"><span class="keyword">WHERE</span> create_time <span class="operator">&gt;=</span> <span class="string">&#x27;2026-01-01 00:00:00&#x27;</span></span><br><span class="line">  <span class="keyword">AND</span> create_time <span class="operator">&lt;</span>  <span class="string">&#x27;2027-01-01 00:00:00&#x27;</span>;</span><br></pre></td></tr></table></figure><p>其他常见写法：</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- ❌</span></span><br><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> <span class="keyword">user</span> <span class="keyword">WHERE</span> <span class="built_in">SUBSTRING</span>(phone, <span class="number">1</span>, <span class="number">3</span>) <span class="operator">=</span> <span class="string">&#x27;138&#x27;</span>;</span><br><span class="line"><span class="comment">-- ✅</span></span><br><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> <span class="keyword">user</span> <span class="keyword">WHERE</span> phone <span class="keyword">LIKE</span> <span class="string">&#x27;138%&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">-- ❌</span></span><br><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> <span class="keyword">user</span> <span class="keyword">WHERE</span> <span class="type">DATE</span>(create_time) <span class="operator">=</span> <span class="string">&#x27;2026-08-31&#x27;</span>;</span><br><span class="line"><span class="comment">-- ✅</span></span><br><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> <span class="keyword">user</span></span><br><span class="line"><span class="keyword">WHERE</span> create_time <span class="operator">&gt;=</span> <span class="string">&#x27;2026-08-31 00:00:00&#x27;</span></span><br><span class="line">  <span class="keyword">AND</span> create_time <span class="operator">&lt;</span>  <span class="string">&#x27;2026-09-01 00:00:00&#x27;</span>;</span><br></pre></td></tr></table></figure><h2 id="场景-3：索引列参与表达式计算"><a href="#场景-3：索引列参与表达式计算" class="headerlink" title="场景 3：索引列参与表达式计算"></a>场景 3：索引列参与表达式计算</h2><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- ❌ age 参与了运算</span></span><br><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> <span class="keyword">user</span> <span class="keyword">WHERE</span> age <span class="operator">+</span> <span class="number">1</span> <span class="operator">=</span> <span class="number">29</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">-- ✅ 把计算挪到右边</span></span><br><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> <span class="keyword">user</span> <span class="keyword">WHERE</span> age <span class="operator">=</span> <span class="number">29</span> <span class="operator">-</span> <span class="number">1</span>;</span><br></pre></td></tr></table></figure><h2 id="场景-4：隐式类型转换"><a href="#场景-4：隐式类型转换" class="headerlink" title="场景 4：隐式类型转换"></a>场景 4：隐式类型转换</h2><p>这是<strong>最隐蔽</strong>的一种，SQL 看着完全没问题，但索引就是不走。</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- phone 是 varchar，这里却用数字比较</span></span><br><span class="line"><span class="comment">-- ❌ MySQL 会对整列做 CAST 转换，等价于在索引列上加函数</span></span><br><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> <span class="keyword">user</span> <span class="keyword">WHERE</span> phone <span class="operator">=</span> <span class="number">13800138000</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">-- ✅ 类型保持一致</span></span><br><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> <span class="keyword">user</span> <span class="keyword">WHERE</span> phone <span class="operator">=</span> <span class="string">&#x27;13800138000&#x27;</span>;</span><br></pre></td></tr></table></figure><p>规则总结：</p><div class="table-container"><table><thead><tr><th>字段类型</th><th>传入类型</th><th>是否走索引</th></tr></thead><tbody><tr><td>varchar</td><td>字符串</td><td>✅ 走</td></tr><tr><td>varchar</td><td>数字</td><td>❌ 不走，字段被隐式转换</td></tr><tr><td>int</td><td>数字</td><td>✅ 走</td></tr><tr><td>int</td><td>字符串</td><td>✅ 走，转换发生在常量侧，不影响索引</td></tr></tbody></table></div><div class="tip warning"><p>注意最后一行的不对称性：字符串字段传数字会失效，数字字段传字符串却没问题。因为 MySQL 的转换规则是”把字符串转成数字”，数字字段传字符串时，转换的是常量而不是索引列。</p></div><h2 id="场景-5：LIKE-以-开头"><a href="#场景-5：LIKE-以-开头" class="headerlink" title="场景 5：LIKE 以 % 开头"></a>场景 5：LIKE 以 % 开头</h2><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- ✅ 前缀匹配，能用到索引的有序性</span></span><br><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> <span class="keyword">user</span> <span class="keyword">WHERE</span> name <span class="keyword">LIKE</span> <span class="string">&#x27;张%&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">-- ❌ 前缀不确定，无法从根节点开始定位</span></span><br><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> <span class="keyword">user</span> <span class="keyword">WHERE</span> name <span class="keyword">LIKE</span> <span class="string">&#x27;%三&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">-- ❌ 同上</span></span><br><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> <span class="keyword">user</span> <span class="keyword">WHERE</span> name <span class="keyword">LIKE</span> <span class="string">&#x27;%张%&#x27;</span>;</span><br></pre></td></tr></table></figure><p>如果业务<strong>必须</strong>支持前后模糊搜索，就不要硬扛索引了，用全文索引：</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 建全文索引</span></span><br><span class="line"><span class="keyword">ALTER</span> <span class="keyword">TABLE</span> <span class="keyword">user</span> <span class="keyword">ADD</span> FULLTEXT INDEX ft_name (name);</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 使用全文检索</span></span><br><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> <span class="keyword">user</span> <span class="keyword">WHERE</span> <span class="keyword">MATCH</span>(name) AGAINST(<span class="string">&#x27;张三&#x27;</span> <span class="keyword">IN</span> <span class="type">BOOLEAN</span> MODE);</span><br></pre></td></tr></table></figure><p>或者上 Elasticsearch，这才是模糊搜索的正解。</p><h2 id="场景-6：OR-连接了非索引列"><a href="#场景-6：OR-连接了非索引列" class="headerlink" title="场景 6：OR 连接了非索引列"></a>场景 6：OR 连接了非索引列</h2><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- status 没有索引，优化器只能全表扫（否则要扫两遍再合并）</span></span><br><span class="line"><span class="comment">-- ❌</span></span><br><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> <span class="keyword">user</span> <span class="keyword">WHERE</span> name <span class="operator">=</span> <span class="string">&#x27;张三&#x27;</span> <span class="keyword">OR</span> status <span class="operator">=</span> <span class="number">1</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">-- ✅ 方案一：给 status 也建索引</span></span><br><span class="line"><span class="keyword">ALTER</span> <span class="keyword">TABLE</span> <span class="keyword">user</span> <span class="keyword">ADD</span> INDEX idx_status (status);</span><br><span class="line"></span><br><span class="line"><span class="comment">-- ✅ 方案二：拆成两个查询用 UNION</span></span><br><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> <span class="keyword">user</span> <span class="keyword">WHERE</span> name <span class="operator">=</span> <span class="string">&#x27;张三&#x27;</span></span><br><span class="line"><span class="keyword">UNION</span> <span class="keyword">ALL</span></span><br><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> <span class="keyword">user</span> <span class="keyword">WHERE</span> status <span class="operator">=</span> <span class="number">1</span>;</span><br></pre></td></tr></table></figure><h2 id="场景-7：-、-lt-gt-、NOT-IN"><a href="#场景-7：-、-lt-gt-、NOT-IN" class="headerlink" title="场景 7：!= 、&lt;&gt;、NOT IN"></a>场景 7：!= 、&lt;&gt;、NOT IN</h2><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- ❌ 否定条件的匹配范围太广，优化器倾向全表</span></span><br><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> <span class="keyword">user</span> <span class="keyword">WHERE</span> status <span class="operator">!=</span> <span class="number">1</span>;</span><br><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> <span class="keyword">user</span> <span class="keyword">WHERE</span> age <span class="keyword">NOT</span> <span class="keyword">IN</span> (<span class="number">20</span>, <span class="number">30</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">-- ✅ 改写为正向范围，前提是能表达</span></span><br><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> <span class="keyword">user</span> <span class="keyword">WHERE</span> status <span class="keyword">IN</span> (<span class="number">0</span>, <span class="number">2</span>, <span class="number">3</span>);</span><br></pre></td></tr></table></figure><p>并非绝对失效 —— 如果 <code>status != 1</code> 命中的行数极少，优化器仍可能走索引。判断依据始终是 <strong>explain 的实际输出</strong>。</p><h2 id="场景-8：IS-NULL-IS-NOT-NULL"><a href="#场景-8：IS-NULL-IS-NOT-NULL" class="headerlink" title="场景 8：IS NULL / IS NOT NULL"></a>场景 8：IS NULL / IS NOT NULL</h2><p>是否能用索引，取决于<strong>该列的空值比例</strong>，这是个动态决策：</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 如果 name 列绝大部分是 NULL，这个查询命中极少，会走索引</span></span><br><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> <span class="keyword">user</span> <span class="keyword">WHERE</span> name <span class="keyword">IS</span> <span class="keyword">NULL</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">-- IS NOT NULL 命中比例高，通常全表扫</span></span><br><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> <span class="keyword">user</span> <span class="keyword">WHERE</span> name <span class="keyword">IS</span> <span class="keyword">NOT</span> <span class="keyword">NULL</span>;</span><br></pre></td></tr></table></figure><p>优化方向是<strong>让字段 NOT NULL 并给默认值</strong>，从根本上消除 NULL 判断：</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">ALTER</span> <span class="keyword">TABLE</span> <span class="keyword">user</span></span><br><span class="line">  MODIFY <span class="keyword">COLUMN</span> name <span class="type">varchar</span>(<span class="number">50</span>) <span class="keyword">NOT</span> <span class="keyword">NULL</span> <span class="keyword">DEFAULT</span> <span class="string">&#x27;&#x27;</span>;</span><br></pre></td></tr></table></figure><h2 id="场景-9：范围查询右侧的列失效"><a href="#场景-9：范围查询右侧的列失效" class="headerlink" title="场景 9：范围查询右侧的列失效"></a>场景 9：范围查询右侧的列失效</h2><p>联合索引中，<strong>某一列用了范围查询之后，它右边的所有列都无法再用于精确定位</strong>。</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 索引 (name, age, city)</span></span><br><span class="line"><span class="comment">-- age 用了范围，city 用不上，key_len 会暴露这一点</span></span><br><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> <span class="keyword">user</span> <span class="keyword">WHERE</span> name <span class="operator">=</span> <span class="string">&#x27;张三&#x27;</span> <span class="keyword">AND</span> age <span class="operator">&gt;</span> <span class="number">20</span> <span class="keyword">AND</span> city <span class="operator">=</span> <span class="string">&#x27;杭州&#x27;</span>;</span><br></pre></td></tr></table></figure><p>因为 <code>age &gt; 20</code> 匹配到的是一批值，这批值里 <code>city</code> 是<strong>无序</strong>的，无法继续用索引定位。</p><p><strong>优化方案：把范围列放到联合索引的最后一位</strong>。</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 调整索引顺序，让等值列在前</span></span><br><span class="line"><span class="keyword">CREATE</span> INDEX idx_name_city_age <span class="keyword">ON</span> <span class="keyword">user</span>(name, city, age);</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 这样 name 和 city 都能精确定位，age 做范围</span></span><br><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> <span class="keyword">user</span> <span class="keyword">WHERE</span> name <span class="operator">=</span> <span class="string">&#x27;张三&#x27;</span> <span class="keyword">AND</span> city <span class="operator">=</span> <span class="string">&#x27;杭州&#x27;</span> <span class="keyword">AND</span> age <span class="operator">&gt;</span> <span class="number">20</span>;</span><br></pre></td></tr></table></figure><h2 id="场景-10：JOIN-时字符集不一致"><a href="#场景-10：JOIN-时字符集不一致" class="headerlink" title="场景 10：JOIN 时字符集不一致"></a>场景 10：JOIN 时字符集不一致</h2><p>跨表关联时，如果两个关联字段的字符集或排序规则不同，MySQL 会对字段做转换，等同于加函数。</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- t1.phone 是 utf8mb4，t2.phone 是 utf8</span></span><br><span class="line"><span class="comment">-- ❌ 关联字段字符集不一致，索引失效</span></span><br><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> t1 <span class="keyword">JOIN</span> t2 <span class="keyword">ON</span> t1.phone <span class="operator">=</span> t2.phone;</span><br><span class="line"></span><br><span class="line"><span class="comment">-- ✅ 统一字符集</span></span><br><span class="line"><span class="keyword">ALTER</span> <span class="keyword">TABLE</span> t2 MODIFY phone <span class="type">varchar</span>(<span class="number">20</span>) <span class="type">CHARACTER</span> <span class="keyword">SET</span> utf8mb4 <span class="keyword">COLLATE</span> utf8mb4_general_ci;</span><br></pre></td></tr></table></figure><p>排查方法：</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">SHOW</span> <span class="keyword">FULL</span> COLUMNS <span class="keyword">FROM</span> t1 <span class="keyword">LIKE</span> <span class="string">&#x27;phone&#x27;</span>;</span><br><span class="line"><span class="keyword">SHOW</span> <span class="keyword">FULL</span> COLUMNS <span class="keyword">FROM</span> t2 <span class="keyword">LIKE</span> <span class="string">&#x27;phone&#x27;</span>;</span><br></pre></td></tr></table></figure><h2 id="场景-11：优化器主动放弃索引"><a href="#场景-11：优化器主动放弃索引" class="headerlink" title="场景 11：优化器主动放弃索引"></a>场景 11：优化器主动放弃索引</h2><p>有时候索引完全可用，但优化器算完账觉得<strong>全表更快</strong>，于是弃用。</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 假设 status=1 占了全表 90% 的行</span></span><br><span class="line"><span class="comment">-- 优化器判断：走索引要回表 90 万次，不如直接顺序扫全表</span></span><br><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> <span class="keyword">user</span> <span class="keyword">WHERE</span> status <span class="operator">=</span> <span class="number">1</span>;</span><br></pre></td></tr></table></figure><p>原因就是<strong>回表代价</strong>：二级索引取出主键后还要回聚簇索引取完整行，随机 IO。当命中比例超过约 20%~30%，随机 IO 的开销就超过顺序扫全表了。</p><p><strong>优化方案一：用覆盖索引消除回表</strong></p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">CREATE</span> INDEX idx_status_name <span class="keyword">ON</span> <span class="keyword">user</span>(status, name);</span><br><span class="line"><span class="comment">-- 只查索引内的字段，不回表，优化器就愿意走索引</span></span><br><span class="line"><span class="keyword">SELECT</span> name <span class="keyword">FROM</span> <span class="keyword">user</span> <span class="keyword">WHERE</span> status <span class="operator">=</span> <span class="number">1</span>;</span><br></pre></td></tr></table></figure><p><strong>优化方案二：强制走索引（谨慎使用）</strong></p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> <span class="keyword">user</span> FORCE INDEX (idx_status) <span class="keyword">WHERE</span> status <span class="operator">=</span> <span class="number">1</span>;</span><br></pre></td></tr></table></figure><div class="tip warning"><p><code>FORCE INDEX</code> 是把优化器的决策权抢过来，属于硬编码。数据分布一变，它可能从”优化”变成”劣化”。只在<strong>明确知道数据分布且长期稳定</strong>时使用，并且要加注释说明原因。</p></div><h2 id="场景-12：索引选择性太差"><a href="#场景-12：索引选择性太差" class="headerlink" title="场景 12：索引选择性太差"></a>场景 12：索引选择性太差</h2><p>在区分度极低的列上建索引，本身就没什么意义。</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 假设 gender 只有 男/女 两个值，10 万行里男女各 5 万</span></span><br><span class="line"><span class="comment">-- 这个索引几乎不会被引擎选中</span></span><br><span class="line"><span class="keyword">CREATE</span> INDEX idx_gender <span class="keyword">ON</span> <span class="keyword">user</span>(gender);</span><br></pre></td></tr></table></figure><p>用下面的公式计算<strong>索引选择性</strong>，越接近 1 越好：</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">SELECT</span></span><br><span class="line">  <span class="built_in">COUNT</span>(<span class="keyword">DISTINCT</span> gender) <span class="operator">/</span> <span class="built_in">COUNT</span>(<span class="operator">*</span>) <span class="keyword">AS</span> gender_selectivity,</span><br><span class="line">  <span class="built_in">COUNT</span>(<span class="keyword">DISTINCT</span> phone)  <span class="operator">/</span> <span class="built_in">COUNT</span>(<span class="operator">*</span>) <span class="keyword">AS</span> phone_selectivity,</span><br><span class="line">  <span class="built_in">COUNT</span>(<span class="keyword">DISTINCT</span> name)   <span class="operator">/</span> <span class="built_in">COUNT</span>(<span class="operator">*</span>) <span class="keyword">AS</span> name_selectivity</span><br><span class="line"><span class="keyword">FROM</span> <span class="keyword">user</span>;</span><br></pre></td></tr></table></figure><div class="table-container"><table><thead><tr><th>字段</th><th>选择性</th><th>建议</th></tr></thead><tbody><tr><td>id（主键）</td><td>1.0000</td><td>最优</td></tr><tr><td>phone</td><td>0.9980</td><td>适合建索引</td></tr><tr><td>name</td><td>0.8500</td><td>可以建</td></tr><tr><td>city</td><td>0.0200</td><td>单独建意义不大</td></tr><tr><td>gender</td><td>0.00002</td><td><strong>不要建</strong></td></tr></tbody></table></div><p>低选择性的列不是不能出现在索引里，而是<strong>应该作为联合索引的后缀列</strong>，配合高选择性列一起用，比如 <code>(city, name)</code>。</p><h1 id="三、explain-执行计划怎么看"><a href="#三、explain-执行计划怎么看" class="headerlink" title="三、explain 执行计划怎么看"></a>三、explain 执行计划怎么看</h1><h2 id="3-1-常用列解读"><a href="#3-1-常用列解读" class="headerlink" title="3.1 常用列解读"></a>3.1 常用列解读</h2><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">EXPLAIN <span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> <span class="keyword">user</span> <span class="keyword">WHERE</span> name <span class="operator">=</span> <span class="string">&#x27;张三&#x27;</span> <span class="keyword">AND</span> age <span class="operator">=</span> <span class="number">28</span>;</span><br></pre></td></tr></table></figure><div class="table-container"><table><thead><tr><th>列</th><th>含义</th><th>关注点</th></tr></thead><tbody><tr><td><code>type</code></td><td>访问类型</td><td><strong>最关键</strong>，见下表</td></tr><tr><td><code>key</code></td><td>实际使用的索引</td><td>为 NULL 表示未走索引</td></tr><tr><td><code>key_len</code></td><td>实际用到的索引字节数</td><td>判断联合索引用了几列</td></tr><tr><td><code>rows</code></td><td>预计扫描行数</td><td>越小越好</td></tr><tr><td><code>Extra</code></td><td>附加信息</td><td>看是否有 Using filesort / Using temporary</td></tr></tbody></table></div><h2 id="3-2-type-性能排序"><a href="#3-2-type-性能排序" class="headerlink" title="3.2 type 性能排序"></a>3.2 type 性能排序</h2><p>从好到坏：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">system &gt; const &gt; eq_ref &gt; ref &gt; range &gt; index &gt; ALL</span><br></pre></td></tr></table></figure><div class="table-container"><table><thead><tr><th>类型</th><th>含义</th><th>出现场景</th></tr></thead><tbody><tr><td><code>system</code></td><td>表只有一行</td><td>极少见</td></tr><tr><td><code>const</code></td><td>主键或唯一索引等值查询，最多一行</td><td><code>WHERE id = 1</code></td></tr><tr><td><code>eq_ref</code></td><td>唯一索引关联，每行只匹配一条</td><td>JOIN 主键</td></tr><tr><td><code>ref</code></td><td>普通索引等值查询</td><td><code>WHERE name = &#39;张三&#39;</code></td></tr><tr><td><code>range</code></td><td>索引范围扫描</td><td><code>WHERE age &gt; 20</code></td></tr><tr><td><code>index</code></td><td>全索引扫描</td><td>覆盖索引但无条件</td></tr><tr><td><code>ALL</code></td><td><strong>全表扫描</strong></td><td>需要优化</td></tr></tbody></table></div><p><strong>优化目标是至少达到 <code>range</code>，最好是 <code>ref</code> 或 <code>const</code>。看到 <code>ALL</code> 就要警惕。</strong></p><h2 id="3-3-Extra-常见提示"><a href="#3-3-Extra-常见提示" class="headerlink" title="3.3 Extra 常见提示"></a>3.3 Extra 常见提示</h2><div class="table-container"><table><thead><tr><th>提示</th><th>含义</th><th>严重性</th></tr></thead><tbody><tr><td><code>Using index</code></td><td>覆盖索引，未回表</td><td>🟢 优秀</td></tr><tr><td><code>Using where</code></td><td>在存储引擎返回后再次过滤</td><td>🟡 正常</td></tr><tr><td><code>Using index condition</code></td><td>索引下推（ICP），减少回表</td><td>🟢 良好</td></tr><tr><td><code>Using filesort</code></td><td>无法用索引排序，需额外排序</td><td>🔴 需优化</td></tr><tr><td><code>Using temporary</code></td><td>创建了临时表</td><td>🔴 需优化</td></tr><tr><td><code>Using join buffer</code></td><td>JOIN 未走索引</td><td>🔴 需优化</td></tr></tbody></table></div><h2 id="3-4-用-key-len-判断联合索引用了几列"><a href="#3-4-用-key-len-判断联合索引用了几列" class="headerlink" title="3.4 用 key_len 判断联合索引用了几列"></a>3.4 用 key_len 判断联合索引用了几列</h2><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">EXPLAIN <span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> <span class="keyword">user</span> <span class="keyword">WHERE</span> name <span class="operator">=</span> <span class="string">&#x27;张三&#x27;</span> <span class="keyword">AND</span> age <span class="operator">=</span> <span class="number">28</span>;</span><br></pre></td></tr></table></figure><p>假设 <code>name</code> 是 <code>varchar(50)</code> utf8mb4，则 <code>50 × 4 + 2 = 202</code> 字节；<code>age</code> 是 int 为 4 字节。</p><ul><li><code>key_len = 202</code> → 只用了 name</li><li><code>key_len = 206</code> → 用了 name + age</li><li><code>key_len = 288</code> → 用了 name + age + city</li></ul><p><code>key_len</code> 是判断”联合索引到底生效了几列”最直接的方法。</p><h1 id="四、慢查询定位"><a href="#四、慢查询定位" class="headerlink" title="四、慢查询定位"></a>四、慢查询定位</h1><h2 id="4-1-开启慢查询日志"><a href="#4-1-开启慢查询日志" class="headerlink" title="4.1 开启慢查询日志"></a>4.1 开启慢查询日志</h2><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 查看当前配置</span></span><br><span class="line"><span class="keyword">SHOW</span> VARIABLES <span class="keyword">LIKE</span> <span class="string">&#x27;slow_query%&#x27;</span>;</span><br><span class="line"><span class="keyword">SHOW</span> VARIABLES <span class="keyword">LIKE</span> <span class="string">&#x27;long_query_time&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 临时开启（重启失效）</span></span><br><span class="line"><span class="keyword">SET</span> <span class="keyword">GLOBAL</span> slow_query_log <span class="operator">=</span> <span class="string">&#x27;ON&#x27;</span>;</span><br><span class="line"><span class="keyword">SET</span> <span class="keyword">GLOBAL</span> long_query_time <span class="operator">=</span> <span class="number">1</span>;</span><br><span class="line"><span class="keyword">SET</span> <span class="keyword">GLOBAL</span> slow_query_log_file <span class="operator">=</span> <span class="string">&#x27;/var/log/mysql/slow.log&#x27;</span>;</span><br></pre></td></tr></table></figure><p>永久生效要改配置文件：</p><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">[mysqld]</span></span><br><span class="line"><span class="attr">slow_query_log</span> = <span class="number">1</span></span><br><span class="line"><span class="attr">slow_query_log_file</span> = /var/log/mysql/slow.log</span><br><span class="line"><span class="attr">long_query_time</span> = <span class="number">1</span></span><br><span class="line"><span class="attr">log_queries_not_using_indexes</span> = <span class="number">1</span></span><br></pre></td></tr></table></figure><h2 id="4-2-分析慢日志"><a href="#4-2-分析慢日志" class="headerlink" title="4.2 分析慢日志"></a>4.2 分析慢日志</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># MySQL 自带工具，按耗时排序取前 10</span></span><br><span class="line">mysqldumpslow -s t -t 10 /var/log/mysql/slow.log</span><br><span class="line"></span><br><span class="line"><span class="comment"># 按出现次数排序</span></span><br><span class="line">mysqldumpslow -s c -t 10 /var/log/mysql/slow.log</span><br></pre></td></tr></table></figure><p>生产环境更推荐 Percona Toolkit：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 生成完整分析报告</span></span><br><span class="line">pt-query-digest /var/log/mysql/slow.log &gt; slow_report.txt</span><br></pre></td></tr></table></figure><h2 id="4-3-实时抓问题-SQL"><a href="#4-3-实时抓问题-SQL" class="headerlink" title="4.3 实时抓问题 SQL"></a>4.3 实时抓问题 SQL</h2><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 查看当前正在执行的慢查询</span></span><br><span class="line"><span class="keyword">SELECT</span> id, <span class="keyword">user</span>, host, db, command, <span class="type">time</span>, state, <span class="keyword">LEFT</span>(info, <span class="number">100</span>) <span class="keyword">AS</span> sql_text</span><br><span class="line"><span class="keyword">FROM</span> information_schema.processlist</span><br><span class="line"><span class="keyword">WHERE</span> command <span class="operator">!=</span> <span class="string">&#x27;Sleep&#x27;</span> <span class="keyword">AND</span> <span class="type">time</span> <span class="operator">&gt;</span> <span class="number">2</span></span><br><span class="line"><span class="keyword">ORDER</span> <span class="keyword">BY</span> <span class="type">time</span> <span class="keyword">DESC</span>;</span><br></pre></td></tr></table></figure><h1 id="五、实战案例"><a href="#五、实战案例" class="headerlink" title="五、实战案例"></a>五、实战案例</h1><h2 id="案例一：千万级分页优化"><a href="#案例一：千万级分页优化" class="headerlink" title="案例一：千万级分页优化"></a>案例一：千万级分页优化</h2><p><strong>问题 SQL</strong>：</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 耗时 12 秒</span></span><br><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> orders <span class="keyword">ORDER</span> <span class="keyword">BY</span> create_time <span class="keyword">DESC</span> LIMIT <span class="number">1000000</span>, <span class="number">20</span>;</span><br></pre></td></tr></table></figure><p><strong>原因</strong>：MySQL 必须先扫描并丢弃前 100 万行，才能取到想要的 20 行。</p><p><strong>优化方案：延迟关联</strong>，先用覆盖索引定位主键，再回表取数据。</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 耗时 0.3 秒</span></span><br><span class="line"><span class="keyword">SELECT</span> o.<span class="operator">*</span> <span class="keyword">FROM</span> orders o</span><br><span class="line"><span class="keyword">INNER</span> <span class="keyword">JOIN</span> (</span><br><span class="line">    <span class="keyword">SELECT</span> id <span class="keyword">FROM</span> orders</span><br><span class="line">    <span class="keyword">ORDER</span> <span class="keyword">BY</span> create_time <span class="keyword">DESC</span></span><br><span class="line">    LIMIT <span class="number">1000000</span>, <span class="number">20</span></span><br><span class="line">) t <span class="keyword">ON</span> o.id <span class="operator">=</span> t.id;</span><br></pre></td></tr></table></figure><p><strong>更彻底的方案：游标分页</strong>，彻底告别深翻页。</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 已知上一页最后一条的 create_time 和 id</span></span><br><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> orders</span><br><span class="line"><span class="keyword">WHERE</span> create_time <span class="operator">&lt;</span> <span class="string">&#x27;2026-08-01 10:00:00&#x27;</span></span><br><span class="line">   <span class="keyword">OR</span> (create_time <span class="operator">=</span> <span class="string">&#x27;2026-08-01 10:00:00&#x27;</span> <span class="keyword">AND</span> id <span class="operator">&lt;</span> <span class="number">9527</span>)</span><br><span class="line"><span class="keyword">ORDER</span> <span class="keyword">BY</span> create_time <span class="keyword">DESC</span>, id <span class="keyword">DESC</span></span><br><span class="line">LIMIT <span class="number">20</span>;</span><br></pre></td></tr></table></figure><p>配合索引 <code>(create_time, id)</code>，无论翻到第几页都是毫秒级。</p><h2 id="案例二：隐式字符集转换导致全表扫描"><a href="#案例二：隐式字符集转换导致全表扫描" class="headerlink" title="案例二：隐式字符集转换导致全表扫描"></a>案例二：隐式字符集转换导致全表扫描</h2><p><strong>现象</strong>：一个 JOIN 查询突然从 50ms 变成 30 秒。</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">SELECT</span> u.name, o.amount</span><br><span class="line"><span class="keyword">FROM</span> <span class="keyword">user</span> u</span><br><span class="line"><span class="keyword">JOIN</span> orders o <span class="keyword">ON</span> u.phone <span class="operator">=</span> o.phone</span><br><span class="line"><span class="keyword">WHERE</span> u.city <span class="operator">=</span> <span class="string">&#x27;杭州&#x27;</span>;</span><br></pre></td></tr></table></figure><p>explain 显示 <code>orders</code> 表的 <code>type = ALL</code>。查字符集发现：</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- user.phone:   utf8mb4_general_ci</span></span><br><span class="line"><span class="comment">-- orders.phone: utf8_general_ci</span></span><br></pre></td></tr></table></figure><p><strong>修复</strong>：</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">ALTER</span> <span class="keyword">TABLE</span> orders</span><br><span class="line">  MODIFY phone <span class="type">varchar</span>(<span class="number">20</span>) <span class="type">CHARACTER</span> <span class="keyword">SET</span> utf8mb4 <span class="keyword">COLLATE</span> utf8mb4_general_ci;</span><br></pre></td></tr></table></figure><p>修复后 <code>type</code> 变为 <code>ref</code>，耗时回到 40ms。</p><h2 id="案例三：filesort-消除"><a href="#案例三：filesort-消除" class="headerlink" title="案例三：filesort 消除"></a>案例三：filesort 消除</h2><p><strong>问题 SQL</strong>：</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- Extra: Using where; Using filesort</span></span><br><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> orders <span class="keyword">WHERE</span> user_id <span class="operator">=</span> <span class="number">1001</span> <span class="keyword">ORDER</span> <span class="keyword">BY</span> create_time <span class="keyword">DESC</span> LIMIT <span class="number">10</span>;</span><br></pre></td></tr></table></figure><p><strong>原因</strong>：<code>idx_user_id</code> 只按 user_id 排序，同一 user_id 下的 create_time 是无序的，只能临时排序。</p><p><strong>修复</strong>：建立联合索引，让排序天然有序。</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 删除冗余的单列索引</span></span><br><span class="line"><span class="keyword">DROP</span> INDEX idx_user_id <span class="keyword">ON</span> orders;</span><br><span class="line"><span class="comment">-- 建立联合索引，等值列在前、排序列在后</span></span><br><span class="line"><span class="keyword">CREATE</span> INDEX idx_user_create <span class="keyword">ON</span> orders(user_id, create_time);</span><br></pre></td></tr></table></figure><p>修复后 Extra 变为 <code>Using index condition</code>，filesort 消失。</p><h1 id="六、索引设计最佳实践"><a href="#六、索引设计最佳实践" class="headerlink" title="六、索引设计最佳实践"></a>六、索引设计最佳实践</h1><ol><li><strong>优先使用自增主键</strong> —— 顺序写入避免页分裂，且主键长度越小，二级索引越省空间</li><li><strong>联合索引优于多个单列索引</strong> —— MySQL 通常只会选择其中一个索引，联合索引效率更高</li><li><strong>区分度高的列放前面</strong> —— 但等值查询列一律优先于范围查询列</li><li><strong>控制索引数量</strong> —— 每个索引都是一棵 B+Tree，写操作要同步维护，索引多了写入会变慢</li><li><strong>善用覆盖索引</strong> —— 把查询高频的字段纳入联合索引，消除回表</li><li><strong>避免冗余索引</strong> —— 有了 <code>(a, b, c)</code>，<code>(a)</code> 和 <code>(a, b)</code> 就是冗余的</li><li><strong>字段尽量 NOT NULL</strong> —— 省一个字节的 NULL 标记位，同时避免 NULL 判断导致的失效</li><li><strong>长字符串用前缀索引</strong> —— <code>CREATE INDEX idx ON t(email(20))</code>，但要权衡区分度</li><li><strong>定期清理无用索引</strong> —— 查询 <code>sys.schema_unused_indexes</code> 找出从没被用过的索引</li><li><strong>上线前必看 explain</strong> —— 数据量和线上不一致时，本地测试的执行计划没有参考价值</li></ol><div class="note green anzhiyufont anzhiyu-icon-chenggong disabled"><p>总结：索引失效看似规则繁多，其实都逃不出两条根因 —— <strong>写法破坏了 B+Tree 的有序性</strong>（函数、类型不匹配、% 开头），或者<strong>回表代价让优化器觉得不划算</strong>（低选择性、大范围命中）。排查时不要猜，一律 <code>EXPLAIN</code> 说话，重点盯 <code>type</code>、<code>key_len</code>、<code>rows</code>、<code>Extra</code> 这四列。</p></div>]]></content>
    
    
      
      
    <summary type="html">&lt;div class=&quot;tip warning&quot;&gt;&lt;p&gt;“我明明建了索引，为什么还是全表扫描？”这是 MySQL 优化里最常遇到的问题。索引失效的原因很多，但归根结底就两大类：&lt;strong&gt;写法让索引没法用&lt;/strong&gt;，或者&lt;strong&gt;优化器算完账觉得不用更快&lt;/st</summary>
      
    
    
    
    <category term="经验分享" scheme="http://hexo.sjava.cn/categories/%E7%BB%8F%E9%AA%8C%E5%88%86%E4%BA%AB/"/>
    
    
    <category term="MySQL" scheme="http://hexo.sjava.cn/tags/MySQL/"/>
    
    <category term="数据库" scheme="http://hexo.sjava.cn/tags/%E6%95%B0%E6%8D%AE%E5%BA%93/"/>
    
    <category term="性能优化" scheme="http://hexo.sjava.cn/tags/%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/"/>
    
  </entry>
  
</feed>
