<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>希尔的博客</title>
  
  <subtitle>兰之猗猗，扬扬其香。不采而佩，于兰何伤？</subtitle>
  <link href="/zshell/atom.xml" rel="self"/>
  
  <link href="http://zshell.cc/"/>
  <updated>2025-07-31T02:39:50.546Z</updated>
  <id>http://zshell.cc/</id>
  
  <author>
    <name>zshellzhang</name>
    
  </author>
  
  <generator uri="http://hexo.io/">Hexo</generator>
  
  <entry>
    <title>阿里工作总结之一: 轻量级流程引擎的一种实现思路</title>
    <link href="http://zshell.cc/2025/07/28/taobao-%E4%B8%80%E7%A7%8D%E8%BD%BB%E9%87%8F%E7%BA%A7%E6%B5%81%E7%A8%8B%E5%BC%95%E6%93%8E%E7%9A%84%E5%AE%9E%E7%8E%B0%E6%80%9D%E8%B7%AF/"/>
    <id>http://zshell.cc/2025/07/28/taobao-一种轻量级流程引擎的实现思路/</id>
    <published>2025-07-28T13:43:58.000Z</published>
    <updated>2025-07-31T02:39:50.546Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>本人在淘天集团移动中间件团队亲历过不少有意思的项目, 本文总结了其中之一: 轻量级流程引擎;<br>同其他已在编排领域深内耕多年的框架相比, 本流程引擎还显得十分稚嫩, 甚至有重复造轮之嫌, 然而与老前辈们相比它又确实有一点点自己的小个性, 也确实满足了特定场景下的业务需求, 该篇即总结一下本人在相关领域内的一些实践经历;<br>(注: 本文涉及的相关代码已做过脱敏及混淆, 不一定能实际执行)</p></blockquote><a id="more"></a><h1 id="背景-简介"><a href="#背景-简介" class="headerlink" title="背景 &amp; 简介"></a><strong>背景 &amp; 简介</strong></h1><p>淘系移动中间件的新版控制台, 在各种日常运维和配置发布场景中, 广泛使用了形态多变、需求各异的多种工作流程; 为了尽可能轻量化并降低对外部系统的依赖, 新控制台在最近几年的迭代与演进中, 沉淀出了一套轻量而不失丰富、多变但不显冗余的流程引擎, 并在多条产品线中落地生根, 包括但不限于:</p><ul><li>API 网关 (<a href="https://apachecon.com/acasia2021/zh/sessions/1013.html" target="_blank" rel="noopener">MTOP</a>) 的接口统一发布</li><li>API 网关的接口归属应用迁移</li><li>SSR (Server-Side-Render) 投放的流量接入</li><li>统一接入层 (<a href="https://tengine.taobao.org/" target="_blank" rel="noopener">tengine</a>) 流量防护 (前置限流等) 配置下发</li><li>统一接入层的金丝雀 (精细) 路由配置下发</li><li>面向大语言模型的 MCP Server 统一发布</li></ul><p>该平台使用的流程引擎 (以下简称本框架) 的能力发展主要经历了以下几个阶段:</p><ol><li><strong>早期孵化</strong>: 早在流量网关只能于旧控制台上运维的时代是没有流程的概念的, 所有关于 API 的操作都是零散、互相无关、没有统筹规划的; 感谢前同事在当时做了最初的尝试, 搭建起了本框架的内核雏形, 规范了 Task - Step 交互的 <a href="#%E8%AE%BE%E8%AE%A1%E5%93%B2%E5%AD%A6">基本原则</a>, 本人也是在这个阶段接手了本项目;</li><li><strong>状态管控</strong>: 当业务开始产生真实的诉求, 本框架顺势补齐了自身最大的缺陷: 状态一致性保证; 因为业务可能会随时取消一个发布任务, 当任务中的部分步骤已经执行成功时, 无任何补偿措施地取消该任务会导致资源整体状态不一致, 这是业务所不能容忍的, 于是本框架推出了 <a href="#%E4%BB%BB%E5%8A%A1%E5%8F%96%E6%B6%88%E7%9A%84%E8%A1%A5%E5%81%BF%E6%9C%BA%E5%88%B6">具备补偿机制</a> 的版本, 这意味着本框架实现了对流程生命周期的可控托管, 意味着本框架在生产环境基本可用;</li><li><strong>可定义化</strong>: 当新控制台计划将发布任务的概览以可视化透出到界面时, 本框架又遇到了任务定义职责不清的问题: 任务的定义被隐式混淆于任务实例的构建逻辑中, 这导致无法给用户提供一个全图视野的任务概览, 只能随任务的执行而逐步展开, 这让可视化的效果大打折扣; 本框架以此为契机将 <a href="#%E4%BB%BB%E5%8A%A1%E5%AE%9A%E4%B9%89%E7%9A%84%E6%BC%94%E8%BF%9B">任务定义 (Task Definition)</a> 显式独立出来, 任务定义和任务实例 (Task Instance) 的生命周期至此彻底切割;</li><li><strong>产品化尝试</strong>: 随着新控制台能力的不断丰富, 越来越多场景的工作流程亦试图接入进来, 此刻接入成本的问题开始凸显, 本框架于是迈出产品化尝试的第一步: 在任务定义的基础之上进一步实现了 <a href="#%E5%AE%9A%E4%B9%89%E7%9A%84%E9%85%8D%E7%BD%AE%E5%8C%96">可配置化</a>, 允许在 diamond 等平台独立维护任务定义, 降低流程的管理成本, 增强任务定义的动态性;</li></ol><p>时至今日, 本框架已形成了一个五脏俱全的小生态, 为上文提及的业务场景持续输出技术价值; 同其他已在编排领域深内耕多年的框架相比, 本框架还显得十分稚嫩, 甚至有重复造轮之嫌, 然而与老前辈们相比它又确实有一点点自己的小个性, 也确实满足了特定场景下的业务需求, 不过以当前的产品化水平, 只能支撑部门内部的需求, 如果想要更进一步, 还有很长的路要走;<br>&nbsp;<br>本文便以分享为目的, 介绍一下本框架的设计思想, 抛砖引玉;</p><h1 id="基本原理"><a href="#基本原理" class="headerlink" title="基本原理"></a><strong>基本原理</strong></h1><h2 id="设计哲学"><a href="#设计哲学" class="headerlink" title="设计哲学"></a><strong>设计哲学</strong></h2><p>本框架具备微内核、高可扩展的特性:</p><ul><li><p>内核层仅定义了 Task 和 Step 的极简模型:</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">TaskCtx   StepCtx</span><br><span class="line">1 ^          ^ 1</span><br><span class="line">|          |</span><br><span class="line">1 |          | 1</span><br><span class="line">Task ----&gt; Step</span><br><span class="line">    1    *</span><br></pre></td></tr></table></figure></li><li><p>在内核之上做丰富的能力扩展:</p><ul><li>编排能力扩展: SequentialStep (串行步骤)、CompositeStep (并行步骤);</li><li>有状态能力扩展: StatefulTask、StatefulStep (会激活存储模块);</li><li>可补偿能力扩展: CompensableTask、CompensableStep (允许取消任务时自动调度用户配置的补偿步骤);</li><li>自动调度能力扩展: AutoExeStep (会激活调度模块);</li><li>第三方异步流程扩展: 对接外部系统, 比如安全审核、changefree 等;</li></ul></li><li><p>扁平的继承结构, 优先使用组合的方式拼搭任务能力;</p></li></ul><h2 id="模型细节"><a href="#模型细节" class="headerlink" title="模型细节"></a><strong>模型细节</strong></h2><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://github.com/zshell-zhang/static-content/raw/master/cs/taobao/%E4%B8%80%E7%A7%8D%E8%BD%BB%E9%87%8F%E7%BA%A7%E6%B5%81%E7%A8%8B%E5%BC%95%E6%93%8E%E7%9A%84%E5%AE%9E%E7%8E%B0%E6%80%9D%E8%B7%AF/1.png" alt="核心模型" title="">                </div>                <div class="image-caption">核心模型</div>            </figure><p>这里有一个细节需要解释: 如上图, Task 和 TaskStep 被实际设计为 1:1 的关系, 这是为了更清晰地区分任务和步骤的职责边界, 例如:</p><ul><li>假设 Task 和 TaskStep 是 1:N 的关系:<ul><li>那么 task 就需要感知 step 的编排逻辑, 要负责去按照约定顺序调度多个 step;</li><li>当任务执行了一半后需要取消, task 还需要额外负责处理这 N 个 step 补偿的顺序关系, 亲自编排补偿步骤 $step^´$;</li></ul></li><li>而在 1:1 的关系下:<ul><li>task 只需要无脑去调用 step.execute(), 至于 step 如何去处理内部多个子步骤的调用顺序, 完全不用关心;</li><li>当任务执行了一半后需要取消, 一个 CompensableTask 只需要规范其步骤也必须是 CompensableTaskStep, 就可以将步骤的补偿逻辑屏蔽, 全权交给 CompensableTaskStep 自己去实现;</li></ul></li></ul><p>下文中我们将直接被 task 关联依赖的 step 称为: <strong>Task 直隶的统领型步骤</strong>;</p><h2 id="执行模式"><a href="#执行模式" class="headerlink" title="执行模式"></a><strong>执行模式</strong></h2><p>本着轻量化的原则, 该框架在任务调度之外不会使用额外资源维护各任务的状态, 并且为了简化调度逻辑, 当一个任务被派发到执行器, 一律从任务入口处重新执行, 任务 (及相关步骤) 是否需要加载状态的判断、以及实际的状态拉起动作 被延迟到其真正执行的那一刻, 这意味着:</p><ul><li>当一个任务 (及其步骤) 未配置任何 stateful 能力的扩展, 则该任务的每次调度都等于从头重新执行一遍, 即使之前的调度已经对该任务的部分步骤完成了执行;</li><li>当一个任务 (及其步骤) 配置了 stateful 能力扩展, 则任务及相关步骤的每次执行都会保存其最新状态, 在任务的下一次调度中, 已执行的步骤会加载恢复到最新状态, 并从最新状态继续执行, 如果步骤已经执行成功, 执行器会越过该步骤并按照编排顺序执行下一个步骤;</li></ul><p>无状态的执行模式有助于降低任务调度间隙的内存消耗, 由于大部分任务都是 IO 密集型的, 如果后期能进一步对执行的调用模型做一些反应式的改造, 本框架将能撑起很高的任务并发;</p><h2 id="作用域-约束关系"><a href="#作用域-约束关系" class="headerlink" title="作用域 &amp; 约束关系"></a><strong>作用域 &amp; 约束关系</strong></h2><ul><li>TaskStep 由 Task 生成, Task 的生命周期 &gt; TaskStep 的生命周期;</li><li>一个 TaskStep 只能属于一个 Task, 一个 Task 可以基于组合型的 TaskStep 派生出多个 TaskStep;</li><li>一个 Task 只有一个 TaskContext, 多个 TaskStep 可以共享同一个 TaskContext;</li><li>一个 TaskStep 只有一个 TaskStepContext, 不同 TaskStep 之间的 TaskStepContext 互相隔离, 不能共享;</li></ul><p>&nbsp;<br>由以上约束关系可以得知 TaskStep 间的通信方式如下:</p><ul><li>同一个 TaskStep, 在不同的调度批号 (schedulerx 任务) 中, 可以通过对应的 TaskStepContext 传递信息;</li><li>不同的 TaskStep, 可以通过其所在 Task 的 TaskContext 传递信息;</li></ul><h2 id="状态流转"><a href="#状态流转" class="headerlink" title="状态流转"></a><strong>状态流转</strong></h2><p>任务的状态取决于步骤的状态, 步骤的状态取决于步骤自身或其嵌套的子步骤的执行状态;</p><ul><li>任务的状态流转:<figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://github.com/zshell-zhang/static-content/raw/master/cs/taobao/%E4%B8%80%E7%A7%8D%E8%BD%BB%E9%87%8F%E7%BA%A7%E6%B5%81%E7%A8%8B%E5%BC%95%E6%93%8E%E7%9A%84%E5%AE%9E%E7%8E%B0%E6%80%9D%E8%B7%AF/2.png" alt="task status machine" title="">                </div>                <div class="image-caption">task status machine</div>            </figure></li><li>步骤的状态流转:<figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://github.com/zshell-zhang/static-content/raw/master/cs/taobao/%E4%B8%80%E7%A7%8D%E8%BD%BB%E9%87%8F%E7%BA%A7%E6%B5%81%E7%A8%8B%E5%BC%95%E6%93%8E%E7%9A%84%E5%AE%9E%E7%8E%B0%E6%80%9D%E8%B7%AF/3.png" alt="step status machine" title="">                </div>                <div class="image-caption">step status machine</div>            </figure></li></ul><h1 id="组合优先架构"><a href="#组合优先架构" class="headerlink" title="组合优先架构"></a><strong>组合优先架构</strong></h1><p>本框架以 “组合优先” 的理念为指导, 提供了 steteful、autoExecute、compensate (补偿) 等多种可以拼搭组合的能力;</p><h2 id="TaskStep-的组合能力"><a href="#TaskStep-的组合能力" class="headerlink" title="TaskStep 的组合能力"></a><strong>TaskStep 的组合能力</strong></h2><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://github.com/zshell-zhang/static-content/raw/master/cs/taobao/%E4%B8%80%E7%A7%8D%E8%BD%BB%E9%87%8F%E7%BA%A7%E6%B5%81%E7%A8%8B%E5%BC%95%E6%93%8E%E7%9A%84%E5%AE%9E%E7%8E%B0%E6%80%9D%E8%B7%AF/4.png" alt="TaskStep 的组合继承结构" title="">                </div>                <div class="image-caption">TaskStep 的组合继承结构</div>            </figure><p>以下是几种经典的能力:</p><ul><li><p><strong>StatefulTaskStep</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><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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="title">StatefulTaskStep</span><span class="params">(AbstractTaskStep delegatedTaskStep, TaskPersistence taskPersistence)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">super</span>(delegatedTaskStep);</span><br><span class="line">    <span class="keyword">this</span>.taskPersistence = taskPersistence;</span><br><span class="line">    <span class="comment">// 恢复步骤状态</span></span><br><span class="line">    recoverTaskStep();</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="function"><span class="keyword">public</span> TaskStepResult <span class="title">execute</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">        val taskStepResult = delegatedTaskStep.execute();</span><br><span class="line">        setStatus(taskStepResult.getStatus());</span><br><span class="line">        <span class="keyword">return</span> taskStepResult;</span><br><span class="line">    &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">        <span class="comment">// 保存步骤状态</span></span><br><span class="line">        saveTaskStep();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">saveTaskStep</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    TaskStepSnapshot taskStepSnapshot = TaskStepSnapshot.builder()</span><br><span class="line">        .taskId(taskId)</span><br><span class="line">        .name(delegatedTaskStep.getName())</span><br><span class="line">        .type(delegatedTaskStep.getType())</span><br><span class="line">        .......</span><br><span class="line">        .build();</span><br><span class="line">    taskPersistence.saveTaskStep(taskStepSnapshot);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">recoverTaskStep</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    val taskStepSnapshot = taskPersistence.recoverTaskStep(taskId, delegatedTaskStep.name);</span><br><span class="line">    val taskStepContext = taskStepSnapshot.getTaskStepContext();</span><br><span class="line">    delegatedTaskStep.saveStepContext(taskStepContext);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>  其本质是:</p><ul><li>在构造步骤实例时恢复被代理步骤上次执行后的上下文;</li><li>在被代理步骤执行完成后保存步骤最新的上下文;</li></ul><p>  <img src="https://github.com/zshell-zhang/static-content/raw/master/cs/taobao/%E4%B8%80%E7%A7%8D%E8%BD%BB%E9%87%8F%E7%BA%A7%E6%B5%81%E7%A8%8B%E5%BC%95%E6%93%8E%E7%9A%84%E5%AE%9E%E7%8E%B0%E6%80%9D%E8%B7%AF/5.png" alt="持久化的步骤上下文"></p></li><li><p><strong>AutoExecuteTaskStep</strong>: 自动调度能力增强<br>AutoExecuteTaskStep 实现自动调度的原理是借助 StatefulTaskStep 将 autoExecute 类型的 taskStep 上下文写入持久层, 然后由 AutoExecuteScheduler 扫描持久层追踪正在执行中的 autoExeTaskStep, 并执行相应 task;</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="function"><span class="keyword">public</span> <span class="title">AutoExecuteTaskStep</span><span class="params">(String name, </span></span></span><br><span class="line"><span class="function"><span class="params">                           AbstractTaskStep scheduledTaskStep,</span></span></span><br><span class="line"><span class="function"><span class="params">                           TaskPersistence taskPersistence)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">super</span>(name, StatefulTaskStep.wrap(<span class="keyword">new</span> DoAutoExecuteStep(</span><br><span class="line">              name, scheduledTaskStep), taskPersistence));</span><br><span class="line">&#125;</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="class"><span class="keyword">class</span> <span class="title">DoAutoExecuteStep</span> <span class="keyword">extends</span> <span class="title">DelegateTaskStep</span> </span>&#123;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> String <span class="title">getType</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="string">"autoExecute"</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>  可以看到, AutoExecuteTaskStep 自身就极致利用了组合代理的能力, 通过两个维度的简单代理实现自动调度的核心能力:</p><ul><li>第一个维度: 通过一个间接的 DoAutoExecuteStep 代理用户真实的业务步骤, 并标识自身的 type 为约定的 autoExecute, 以插桩的形式指引调度器识别出需要自动调度的 task;</li><li>第二个维度: 通过 StatefulTaskStep 代理 DoAutoExecuteStep, 以持久化 DoAutoExecuteStep 的状态, 从而允许调度器扫描持久层列举需要自动调度的 task;</li></ul><p>  <img src="https://github.com/zshell-zhang/static-content/raw/master/cs/taobao/%E4%B8%80%E7%A7%8D%E8%BD%BB%E9%87%8F%E7%BA%A7%E6%B5%81%E7%A8%8B%E5%BC%95%E6%93%8E%E7%9A%84%E5%AE%9E%E7%8E%B0%E6%80%9D%E8%B7%AF/6.png" alt="持久化的 type = autoExecute 的插桩步骤"></p></li></ul><p>&nbsp;<br><strong>能力的组合</strong></p><p>框架提供了便于拼搭 stateful 与 autoExecute 能力的包装工具:</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="function"><span class="keyword">public</span> <span class="keyword">static</span> AbstractTaskStep <span class="title">wrap</span><span class="params">(AbstractTaskStep taskStep,</span></span></span><br><span class="line"><span class="function"><span class="params">                                    TaskPersistence taskPersistence)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">new</span> StatefulTaskStep(taskStep, taskPersistence);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">static</span> AbstractTaskStep <span class="title">wrap</span><span class="params">(AbstractTaskStep taskStep,</span></span></span><br><span class="line"><span class="function"><span class="params">                                    TaskPersistence taskPersistence)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">new</span> AutoExecuteTaskStep(<span class="string">"AutoExecute:"</span> + taskStep.getName(),</span><br><span class="line">                taskStep, taskPersistence);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><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></pre></td><td class="code"><pre><span class="line"><span class="function">AbstractTaskStep <span class="title">wrap</span><span class="params">(AbstractTaskStep taskStep)</span> </span>&#123;</span><br><span class="line">    <span class="comment">// stateful</span></span><br><span class="line">    AbstractTaskStep taskStep = StatefulTaskStep.wrap(</span><br><span class="line">        taskStep, taskPersistence);</span><br><span class="line">    <span class="comment">// autoExecute</span></span><br><span class="line">    taskStep = AutoExecuteTaskStep.wrap(taskStep, taskPersistence);</span><br><span class="line">    <span class="keyword">return</span> taskStep;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>执行以上代码将会得到如下状态:</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://github.com/zshell-zhang/static-content/raw/master/cs/taobao/%E4%B8%80%E7%A7%8D%E8%BD%BB%E9%87%8F%E7%BA%A7%E6%B5%81%E7%A8%8B%E5%BC%95%E6%93%8E%E7%9A%84%E5%AE%9E%E7%8E%B0%E6%80%9D%E8%B7%AF/7.png" alt="stateful 和 autoExecute 能力组合的步骤上下文" title="">                </div>                <div class="image-caption">stateful 和 autoExecute 能力组合的步骤上下文</div>            </figure><p>当然下文会讲到, 用户并不需要如此亲自编写代码, 本框架提供了便捷的方式帮用户自动生成相关能力叠加;</p><p>&nbsp;<br><strong>补充: 高级能力</strong><br>除了 stateful、autoExecute 等常用能力外, 框架内还有其他高级能力使用了该组合机制, 比如与补偿相关的顺序执行可补偿步骤 (SequentialCompensableTaskStep) 和 并行执行可补偿步骤 (CompositeCompensableTaskStep), 这些补偿相关的步骤没有被设计为直接对用户开放, 而是通过更友好的方式被上层包装, 下文将会具体介绍;</p><h2 id="Task-的组合能力"><a href="#Task-的组合能力" class="headerlink" title="Task 的组合能力"></a><strong>Task 的组合能力</strong></h2><span id="OpsTask">与 TaskStep 类似, Task 也提供了诸如 stateful、compensate 等多种可以拼搭组合的任务能力; 不过与 TaskStep 略有不同的是:<ul><li>TaskStep 是直接面向具体业务细节的, 不同的业务场景差异很大, 灵活度太高, 无法沉淀出一套通用的步骤范式, 只能提供一些最基础的 stateful、autoExecute 等原子能力;</li><li>而 Task 不是面向具体业务细节的, 它是可以被更抽象的场景所概括描述的, 比如说内部系统的审批流、配置发布的流程等, 所以框架针对这类通用运维场景沉淀出了一个最佳实践OpsTask &#x2F; CompensableOpsTask;</li></ul><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://github.com/zshell-zhang/static-content/raw/master/cs/taobao/%E4%B8%80%E7%A7%8D%E8%BD%BB%E9%87%8F%E7%BA%A7%E6%B5%81%E7%A8%8B%E5%BC%95%E6%93%8E%E7%9A%84%E5%AE%9E%E7%8E%B0%E6%80%9D%E8%B7%AF/8.png" alt="" title="">                </div>                <div class="image-caption"></div>            </figure><ul><li>OpsTask 的组合链路: StatefulTask (AbstractTask);</li><li>CompensableOpsTask 的组合链路: StatefulTask (CompensableTask);</li></ul><p>OpsTask 维护了通用运维任务所需要的业务强类型数据结构 globalBusData 及对应的序列化&#x2F;反序列化方法 taskDataSerDeser, 在任务生命周期中的每一次调度, 都会在初始化时帮用户恢复最新的数据, 调度结束后再将更新后的数据状态持久化;<br>CompensableOpsTask 是 OpsTask 的可补偿版本, 除了 OpsTask 的基本能力外, CompensableOpsTask 还会帮助用户管理补偿编排逻辑;<br>OpsTask &#x2F; CompensableOpsTask 帮用户设定好了组合模式, 可以开箱即用, 用户直接继承使用即可, 当然如果用户有自定义需求, 也可以选择自己去组合 stateful 等原子能力;<br>根据目前的实际使用情况, 大部分业务的任务都选择了具有补偿能力的 CompensableOpsTask, 只有少数简单的没有数据一致性要求的场景选择了 OpsTask, 暂没有业务选择自定义 Task;</p><h1 id="任务取消的补偿机制"><a href="#任务取消的补偿机制" class="headerlink" title="任务取消的补偿机制"></a><strong>任务取消的补偿机制</strong></h1><p>由于任务中存在多个步骤, 在执行任意一个步骤时, 用户都可能取消, 此时当前步骤之前的步骤已经执行成功, 当前步骤之后的步骤均未执行; 要想让任务取消后保持任务执行前的状态, 就需要引入回退补偿机制: <a href="https://www.cs.cornell.edu/andru/cs711/2002fa/reading/sagas.pdf" target="_blank" rel="noopener">Hector &amp; Kenneth: Sagas (1987)</a></p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://github.com/zshell-zhang/static-content/raw/master/cs/taobao/%E4%B8%80%E7%A7%8D%E8%BD%BB%E9%87%8F%E7%BA%A7%E6%B5%81%E7%A8%8B%E5%BC%95%E6%93%8E%E7%9A%84%E5%AE%9E%E7%8E%B0%E6%80%9D%E8%B7%AF/9.png" alt="自适应补偿的简单示例" title="">                </div>                <div class="image-caption">自适应补偿的简单示例</div>            </figure><p>由用户定义自己的步骤 $(S_1, S_2, ….. S_n)$, 及对应的补偿步骤 $(S_1^´, S_2^´, ….. S_n^´)$, CompensableTask 负责根据当前状态自适应地生成补偿步骤, 比如:<br>某任务共有 $S_1、S_2、S_3$ 三个步骤, 用户执行完成了 $S_1、S_2$ 后取消任务, 则此时任务的步骤链路变成: $S_1 \Rightarrow S_2 \Rightarrow S_2^´ \Rightarrow S_1^´$;</p><p>&nbsp;<br>Task 和 TaskStep 分别在各自的维度实现了补偿能力:</p><h2 id="TaskStep-的补偿原理"><a href="#TaskStep-的补偿原理" class="headerlink" title="TaskStep 的补偿原理"></a><strong>TaskStep 的补偿原理</strong></h2><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://github.com/zshell-zhang/static-content/raw/master/cs/taobao/%E4%B8%80%E7%A7%8D%E8%BD%BB%E9%87%8F%E7%BA%A7%E6%B5%81%E7%A8%8B%E5%BC%95%E6%93%8E%E7%9A%84%E5%AE%9E%E7%8E%B0%E6%80%9D%E8%B7%AF/10.png" alt="补偿体系的继承结构" title="">                </div>                <div class="image-caption">补偿体系的继承结构</div>            </figure><h3 id="CompensateAwareTaskStep"><a href="#CompensateAwareTaskStep" class="headerlink" title="CompensateAwareTaskStep"></a><strong>CompensateAwareTaskStep</strong></h3><p>对补偿有感知能力的步骤, 着重点是步骤的性质分解, 表达的是对任务中的 (原子或复合) 步骤 能够识别自身已执行和待补偿 (子) 步骤的一种抽象;<br>顾名思义, 该接口在本框架的语境下就是能够识别出已被执行的 “逻辑步骤”, 以及能够推导出已执行步骤对应的 “补偿逻辑步骤”:</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">interface</span> <span class="title">CompensateAwareTaskStep</span> </span>&#123;</span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 当所在任务被取消时, 提取 originStep 已经被执行的 "逻辑步骤"</span></span><br><span class="line"><span class="comment">     * 如果 originTask 没有被执行, 应当返回 Optional.empty()</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="function">Optional&lt;AbstractTaskStep&gt; <span class="title">executedTaskStep</span><span class="params">()</span></span>;</span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 当所在任务被取消时, 基于 originStep 的状态生成对应的补偿 "逻辑步骤"</span></span><br><span class="line"><span class="comment">     * 如果 originTask 不需要被补偿, 应当返回 Optional.empty()</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="function">Optional&lt;AbstractTaskStep&gt; <span class="title">compensateTaskStep</span><span class="params">()</span></span>;</span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 原始的 taskStep, 本小节不讨论该方法</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="function">AbstractTaskStep <span class="title">originTaskStep</span><span class="params">()</span></span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>对于原子步骤、串行步骤、并行步骤这三种类型, 将分别围绕各自的编排特性完成对以上接口的实现:</p><ul><li><p>原子步骤的补偿版本: <code>AtomicCompensateAwareStep</code>:<br>感知补偿的原子步骤的逻辑最简单, 需要用户同时给定目标步骤 (normalStep) 和对应的补偿步骤 (compensateStep):</p><ul><li><strong>executedTaskStep 方法</strong>: 当 normalStep 已执行则返回 normalStep, 否则返回 Optional.empty();</li><li><strong>compensateTaskStep 方法</strong>: 当用户没有配置补偿步骤 (NoneTaskStep) 返回 Optional.empty(), 或者当 normalStep 的状态需要补偿, 则返回 compensateStep;  <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></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">AtomicCompensateAwareStep</span> <span class="keyword">implements</span> <span class="title">CompensateAwareTaskStep</span> </span>&#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> AbstractTaskStep normalStep;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> AbstractTaskStep compensateStep;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> Optional&lt;AbstractTaskStep&gt; <span class="title">executedTaskStep</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> normalStep.getStatus().isProcessedStatus()</span><br><span class="line">                ? Optional.of(normalStep)</span><br><span class="line">                : Optional.empty();</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> Optional&lt;AbstractTaskStep&gt; <span class="title">compensateTaskStep</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">if</span> (!TaskStepStatus.needCompensate(normalStep.getStatus())) &#123;</span><br><span class="line">            <span class="keyword">return</span> Optional.empty();</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">// 补偿步骤类型为占位步骤, 标识用户认为无需针对目标步骤补偿</span></span><br><span class="line">        <span class="keyword">if</span> (compensateStep <span class="keyword">instanceof</span> NoneTaskStep) &#123;</span><br><span class="line">            <span class="keyword">return</span> Optional.empty();</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> Optional.ofNullable(compensateStep);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></li></ul></li><li><p>串行步骤的补偿版本: <code>SequentialCompensableTaskStep</code>:<br>感知补偿的串行步骤的逻辑是在原子补偿步骤的基础上, 结合了自身串行编排的特征, 需要用户同时给定 n 组目标步骤和对应补偿步骤的配对 $(S_1, S_1^´), (S_2, S_2^´) ….. (S_n, S_n^´)$, SequentialCompensableTaskStep 基于这 n 组配对构建一个 List&lt; CompensateAwareTaskStep&gt; compensateAwareSteps:</p><ul><li><strong>executedTaskStep 方法</strong>: 对 compensateAwareSteps 中的元素遍历执行 executedTaskStep 方法获取一个 executedSteps 列表, 当列表不为空, 以此构建一个新的 SequentialTaskStep 返回, 否则返回 Optional.empty();</li><li><strong>compensateTaskStep 方法</strong>: 对 compensateAwareSteps 中的元素遍历执行 compensateTaskStep 方法获取一个 compensateSteps 列表, 当列表不为空, 对此做 reverse 构建一个反向的 SequentialTaskStep 返回, 否则返回 Optional.empty();  <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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">SequentialCompensableTaskStep</span> <span class="keyword">extends</span> <span class="title">DelegateTaskStep</span></span></span><br><span class="line"><span class="class">        <span class="keyword">implements</span> <span class="title">CompensableTaskStep</span>, <span class="title">CompensateAwareTaskStep</span> </span>&#123;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> Optional&lt;AbstractTaskStep&gt; <span class="title">executedTaskStep</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        List&lt;AbstractTaskStep&gt; executedSteps = Compensates.getExecutedSteps(compensateAwareSteps);</span><br><span class="line">        <span class="keyword">return</span> CollectionUtils.isNotEmpty(executedSteps)</span><br><span class="line">                ? Optional.of(buildCompensatingSequentialTaskStep(executedSteps))</span><br><span class="line">                : Optional.empty();</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> Optional&lt;AbstractTaskStep&gt; <span class="title">compensateTaskStep</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">final</span> List&lt;AbstractTaskStep&gt; compensateSteps = Compensates.getCompensateSteps(compensateAwareSteps);</span><br><span class="line">        <span class="keyword">final</span> List&lt;AbstractTaskStep&gt; compensateStepChain = Lists.reverse(compensateSteps);</span><br><span class="line">        <span class="keyword">return</span> CollectionUtils.isNotEmpty(compensateStepChain)</span><br><span class="line">                ? Optional.of(buildCompensatingSequentialTaskStep(compensateStepChain))</span><br><span class="line">                : Optional.empty();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></li></ul></li><li><p>并行步骤的补偿版本: <code>CompositeCompensableTaskStep</code>:<br>感知补偿的并行步骤的逻辑是在原子补偿步骤的基础上, 结合了自身并行编排的特征, 需要用户同时给定 n 组目标步骤和对应补偿步骤的配对 $(S_1, S_1^´), (S_2, S_2^´) ….. (S_n, S_n^´)$, CompositeCompensableTaskStep 基于这 n 组配对构建一个 List&lt; CompensateAwareTaskStep&gt; compensateAwareSteps:</p><ul><li>executedTaskStep 方法: 对 compensateAwareSteps 中的元素遍历执行 executedTaskStep 方法获取一个 executedSteps 列表, 当列表不为空, 以此构建一个新的 CompositeTaskStep 返回, 否则返回 Optional.empty();</li><li>compensateTaskStep 方法: 对 compensateAwareSteps 中的元素遍历执行 compensateTaskStep 方法获取一个 compensateSteps 列表, 当列表不为空, 以此构建一个新的 CompositeTaskStep 返回 (不需要 reverse, 因为是并行的, 不区分次序), 否则返回 Optional.empty();  <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="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">CompositeCompensableTaskStep</span> <span class="keyword">extends</span> <span class="title">DelegateTaskStep</span></span></span><br><span class="line"><span class="class">        <span class="keyword">implements</span> <span class="title">CompensableTaskStep</span>, <span class="title">CompensateAwareTaskStep</span> </span>&#123;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> Optional&lt;AbstractTaskStep&gt; <span class="title">executedTaskStep</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        List&lt;AbstractTaskStep&gt; executedSteps = Compensates.getExecutedSteps(compensateAwareSteps);</span><br><span class="line">        <span class="keyword">return</span> CollectionUtils.isNotEmpty(executedSteps)</span><br><span class="line">                ? Optional.of(<span class="keyword">new</span> CompositeTaskStep(name, taskContext, executedSteps))</span><br><span class="line">                : Optional.empty();</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> Optional&lt;AbstractTaskStep&gt; <span class="title">compensateTaskStep</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">final</span> List&lt;AbstractTaskStep&gt; compensateSteps = Compensates.getCompensateSteps(compensateAwareSteps);</span><br><span class="line">        <span class="keyword">return</span> CollectionUtils.isNotEmpty(compensateSteps)</span><br><span class="line">                ? Optional.of(<span class="keyword">new</span> CompositeTaskStep(name, taskContext, compensateSteps))</span><br><span class="line">                : Optional.empty();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></li></ul></li></ul><p>&nbsp;</p><h3 id="CompensableTaskStep"><a href="#CompensableTaskStep" class="headerlink" title="CompensableTaskStep"></a><strong>CompensableTaskStep</strong></h3><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">interface</span> <span class="title">CompensableTaskStep</span> <span class="keyword">extends</span> <span class="title">TaskStep</span> </span>&#123;</span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 基于原有 taskStep 的当前状态, 针对性地生成具备补偿原有 taskStep 能力的新的步骤链路</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="function">TaskStep <span class="title">generateCompensableTaskStep</span><span class="params">()</span></span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li><p>串行补偿链路整合的实现: <code>SequentialCompensableTaskStep</code>:</p><ul><li>对 compensateAwareSteps 中的元素遍历分别生成已执行和待补偿的两部分, 并拼接两者构建新的 SequentialTaskStep;  <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="class"><span class="keyword">class</span> <span class="title">SequentialCompensableTaskStep</span> <span class="keyword">extends</span> <span class="title">DelegateTaskStep</span></span></span><br><span class="line"><span class="class">        <span class="keyword">implements</span> <span class="title">CompensableTaskStep</span>, <span class="title">CompensateAwareTaskStep</span> </span>&#123;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> TaskStep <span class="title">generateCompensableTaskStep</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="comment">// 提取已执行过的普通步骤</span></span><br><span class="line">        <span class="keyword">final</span> List&lt;AbstractTaskStep&gt; executedSteps = Compensates.getExecutedSteps(compensateAwareSteps);</span><br><span class="line">        <span class="comment">// 根据原有步骤的执行状态生成适应性的补偿步骤</span></span><br><span class="line">        <span class="keyword">final</span> List&lt;AbstractTaskStep&gt; compensateSteps = Compensates.getCompensateSteps(compensateAwareSteps);</span><br><span class="line"></span><br><span class="line">        <span class="keyword">final</span> List&lt;AbstractTaskStep&gt; compensatingStepChain = Lists.newArrayList(executedSteps);</span><br><span class="line">        compensatingStepChain.addAll(Lists.reverse(compensateSteps));</span><br><span class="line">        <span class="keyword">return</span> buildCompensatingSequentialTaskStep(compensatingStepChain);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></li></ul></li><li><p>并行补偿链路整合的实现: <code>CompositeCompensableTaskStep</code>:</p><ul><li>并行补偿的链路整合没有做到如语义上的绝对并行, 而是出于实现复杂度的考虑, 降级为: 已执行和待补偿两部分 在整体上先后串行执行, 而各自在内部并行执行;</li><li>所以其实现是将 executedSteps 和 compensateSteps 两个 CompositeTaskStep 串为一个整体的 SequentialTaskStep;  <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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">CompositeCompensableTaskStep</span> <span class="keyword">extends</span> <span class="title">DelegateTaskStep</span></span></span><br><span class="line"><span class="class">        <span class="keyword">implements</span> <span class="title">CompensableTaskStep</span>, <span class="title">CompensateAwareTaskStep</span> </span>&#123;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> TaskStep <span class="title">generateCompensableTaskStep</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="comment">// 提取已执行过的普通步骤</span></span><br><span class="line">        <span class="keyword">final</span> List&lt;AbstractTaskStep&gt; executedSteps = Compensates.getExecutedSteps(compensateAwareSteps);</span><br><span class="line">        <span class="comment">// 根据原有步骤的执行状态生成适应性的补偿步骤</span></span><br><span class="line">        <span class="keyword">final</span> List&lt;AbstractTaskStep&gt; compensateSteps = Compensates.getCompensateSteps(compensateAwareSteps);</span><br><span class="line"></span><br><span class="line">        <span class="keyword">final</span> CompositeTaskStep executedCompositeStep = <span class="keyword">new</span> CompositeTaskStep(name, taskContext, executedSteps);</span><br><span class="line">        <span class="keyword">final</span> CompositeTaskStep compensateCompositeStep = <span class="keyword">new</span> CompositeTaskStep(COMPENSATE_STEP_DEFAULT_PREFIX + name, taskContext, compensateSteps);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 为了使用统一的接口 (CompensateAwareTaskStep) 以便于管理, composite compensable step 的补偿逻辑</span></span><br><span class="line">        <span class="comment">// 采用了由 executedStep + compensateStep 串联起的 sequentialTaskStep</span></span><br><span class="line">        <span class="keyword">final</span> List&lt;AbstractTaskStep&gt; compensatingStepChain = ImmutableList.of(executedCompositeStep,compensateCompositeStep);</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> CompensatingSequentialTaskStep(name, taskContext, compensatingStepChain);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></li></ul></li></ul><p>由以上描述可知, SequentialCompensableTaskStep 和 CompositeCompensableTaskStep 既是 CompensateAwareTaskStep 又是 CompensableTaskStep, 这意味着它们:</p><ul><li>既可以作为一个直接由 Task 直隶的统领型步骤, 从整体上去整合普通链路与补偿链路;</li><li>又可以作为一个局部的被嵌套的子步骤, 提供基本的步骤拆分能力, 由上层统领型步骤去统一调度;</li></ul><p>如此便可编织出一张允许任意递归灵活嵌套的步骤执行网络, 从而拥有强大的自适应补偿能力;<br><span id="AtomicCompensateAwareStep"><br>本框架目前只有复合型步骤实现了该接口, 为了简化实现, 原子步骤 AtomicCompensateAwareStep 未实现该接口, 因为原子步骤在语义上可以转化为只有一个子步骤的 SequentialTaskStep 或 CompositeTaskStep;</span></p><p>&nbsp;<br>最后还需要额外补充一下: SequentialCompensableTaskStep 和 CompositeCompensableTaskStep 除了实现了上述两个核心接口外, 还直接继承了 DelegateTaskStep, 它们代理的 TaskStep 逻辑如下:</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="function"><span class="keyword">public</span> <span class="keyword">static</span> SequentialCompensableTaskStep <span class="title">buildSequentialCompensableTaskStep</span><span class="params">(String name, TaskContext taskContext,</span></span></span><br><span class="line"><span class="function"><span class="params">                                                                               List&lt;Boolean&gt; autoExecuteConfigs,</span></span></span><br><span class="line"><span class="function"><span class="params">                                                                               List&lt;CompensateAwareTaskStep&gt; taskSteps)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">final</span> List&lt;AbstractTaskStep&gt; normalTaskSteps = taskSteps.stream()</span><br><span class="line">            .map(CompensateAwareTaskStep::originTaskStep)</span><br><span class="line">            .collect(Collectors.toList());</span><br><span class="line">    <span class="keyword">final</span> SequentialTaskStep sequentialTaskStep = <span class="keyword">new</span> SequentialTaskStep(name, taskContext,</span><br><span class="line">            normalTaskSteps, autoExecuteConfigs);</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">new</span> SequentialCompensableTaskStep(name, taskContext, sequentialTaskStep, taskSteps);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">private</span> <span class="title">SequentialCompensableTaskStep</span><span class="params">(String name, TaskContext taskContext,</span></span></span><br><span class="line"><span class="function"><span class="params">                                      SequentialTaskStep delegatedTaskStep,</span></span></span><br><span class="line"><span class="function"><span class="params">                                      List&lt;CompensateAwareTaskStep&gt; compensateAwareSteps)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">super</span>(delegatedTaskStep);</span><br><span class="line">    <span class="keyword">this</span>.name = name;</span><br><span class="line">    <span class="keyword">this</span>.taskContext = taskContext;</span><br><span class="line">    <span class="keyword">this</span>.compensateAwareSteps = compensateAwareSteps;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><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="function"><span class="keyword">public</span> <span class="keyword">static</span> CompositeCompensableTaskStep <span class="title">buildCompositeCompensableTaskStep</span><span class="params">(String name, TaskContext taskContext,</span></span></span><br><span class="line"><span class="function"><span class="params">                                                                             List&lt;CompensateAwareTaskStep&gt; taskSteps)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">final</span> List&lt;AbstractTaskStep&gt; normalTaskSteps = taskSteps.stream()</span><br><span class="line">            .map(CompensateAwareTaskStep::originTaskStep)</span><br><span class="line">            .collect(Collectors.toList());</span><br><span class="line">    <span class="keyword">final</span> CompositeTaskStep compositeTaskStep = <span class="keyword">new</span> CompositeTaskStep(name, taskContext, normalTaskSteps);</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">new</span> CompositeCompensableTaskStep(name, taskContext, compositeTaskStep, taskSteps);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">private</span> <span class="title">CompositeCompensableTaskStep</span><span class="params">(String name, TaskContext taskContext,</span></span></span><br><span class="line"><span class="function"><span class="params">                                     CompositeTaskStep delegatedTaskStep,</span></span></span><br><span class="line"><span class="function"><span class="params">                                     List&lt;CompensateAwareTaskStep&gt; compensateAwareSteps)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">super</span>(delegatedTaskStep);</span><br><span class="line">    <span class="keyword">this</span>.name = name;</span><br><span class="line">    <span class="keyword">this</span>.taskContext = taskContext;</span><br><span class="line">    <span class="keyword">this</span>.compensateAwareSteps = compensateAwareSteps;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>也即: 将 List<compensateawaretaskstep> 中每个 step 的原始部分执行逻辑剥离, 单独重组为逻辑上的 SequentialTaskStep 或 CompositeTaskStep, 并将其作为被代理的逻辑步骤, 这就使得 SequentialCompensableTaskStep 和 CompositeCompensableTaskStep 在没有取消时可以像正常步骤一样执行, 任务取消时可以优雅切换到补偿模式;</compensateawaretaskstep></p><h2 id="Task-的补偿原理"><a href="#Task-的补偿原理" class="headerlink" title="Task 的补偿原理"></a><strong>Task 的补偿原理</strong></h2><p>CompensableTask 是任务级补偿能力的核心: CompensableTask 只接受关联一个 CompensableTaskStep, 这就使得 CompensableTask 的补偿逻辑十分简洁:</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><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><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">abstract</span> <span class="class"><span class="keyword">class</span> <span class="title">CompensableTask</span> <span class="keyword">extends</span> <span class="title">AbstractTask</span> </span>&#123;</span><br><span class="line">    <span class="function"><span class="keyword">protected</span> TaskResult <span class="title">executeTask</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            <span class="keyword">if</span> (isCancelling()) &#123;</span><br><span class="line">                <span class="keyword">return</span> executeCompensatedStep();</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">if</span> (canExecute()) &#123;</span><br><span class="line">                <span class="keyword">return</span> executeNormalStep();</span><br><span class="line">            &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                <span class="keyword">return</span> <span class="keyword">new</span> TaskResult(getTaskId(), getStatus());</span><br><span class="line">            &#125;</span><br><span class="line">        &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">            <span class="keyword">if</span> (isCancelling()) &#123;</span><br><span class="line">                <span class="keyword">final</span> TaskStep compensableTaskStep = <span class="keyword">this</span>.compensableTaskStep.generateCompensableTaskStep();</span><br><span class="line">                compensableTaskStep.onFailure(e);</span><br><span class="line">            &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                compensableTaskStep.onFailure(e);</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">return</span> <span class="keyword">new</span> TaskResult(getTaskId(), TaskStatus.FAILURE, e, e.getMessage());</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">cancel</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="comment">// 幂等返回</span></span><br><span class="line">        <span class="keyword">if</span> (TaskStatus.CANCELED.equals(getStatus())</span><br><span class="line">                || TaskStatus.CANCELLING.equals(getStatus())) &#123;</span><br><span class="line">            <span class="keyword">return</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">// 终态不允许取消</span></span><br><span class="line">        <span class="keyword">if</span> (TaskStatus.SUCCESS.equals(getStatus())) &#123;</span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> UnsupportedOperationException(<span class="string">"已执行成功的任务无法取消"</span>);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            compensableTaskStep.onInterrupt();</span><br><span class="line">            getTaskContext().put(TASK_CANCEL_MARK, <span class="keyword">true</span>);</span><br><span class="line">            <span class="keyword">final</span> TaskResult compensateResult = executeCompensatedStep();</span><br><span class="line">            setStatus(compensateResult.getStatus());</span><br><span class="line">        &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">            setStatus(TaskStatus.FAILURE);</span><br><span class="line">            compensableTaskStep.onFailure(e);</span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> RuntimeException(<span class="string">"cancel task error, errorDetail: "</span> + e.getMessage(), e);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">private</span> TaskResult <span class="title">executeCompensatedStep</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">final</span> TaskStep compensableTaskStep = <span class="keyword">this</span>.compensableTaskStep.generateCompensableTaskStep();</span><br><span class="line">        <span class="keyword">final</span> TaskStepResult taskStepResult = compensableTaskStep.execute();</span><br><span class="line">        ......</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">private</span> TaskResult <span class="title">executeNormalStep</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">final</span> TaskStepResult taskStepResult = compensableTaskStep.execute();</span><br><span class="line">        ......</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>只需重写 AbstractTask 的两个方法: executeTask() 和 cancel();</li><li>对于 cancel() 方法:<ol><li>先 Interrupt 原步骤;</li><li>执行 compensableTaskStep.generateCompensableTaskStep().execute();</li></ol></li><li>对于 executeTask() 方法:<ul><li>如果当前未取消: 执行 compensableTaskStep.execute();</li><li>如果当前正在取消中: 执行 compensableTaskStep.generateCompensableTaskStep().execute();</li></ul></li></ul><p>除此外无需任何多余的逻辑, 便能实现任务级的补偿能力;</p><h2 id="复杂-case-举例"><a href="#复杂-case-举例" class="headerlink" title="复杂 case 举例"></a><strong>复杂 case 举例</strong></h2><p>图示说明:</p><ul><li><p>状态缩写:</p><ul><li><strong>P</strong> (PENDING)</li><li><strong>R</strong> (RUNNING)</li><li><strong>SK</strong> (SKIPPED)</li><li><strong>SU</strong> (SUCCESS)</li><li><strong>INT</strong> (INTERRUPTED)</li></ul></li><li><p>步骤标号: 用于区分步骤次序</p><ul><li>数字: 单一步骤 (或并行包装步骤) 的次序;</li><li>字母: 并行包装步骤内各个真正并行步骤的次序;</li><li>上标 <strong>´</strong> : 表示对应数字 (字母) 步骤的补偿步骤;</li></ul></li></ul><p>本节用具体示例表示几种复杂任务拓扑结构下, 当任务取消后的瞬时状态:</p><ul><li><strong>case 1:</strong><br>2 号步骤中的两个子步骤是并行的;<br>SKIP 状态的步骤在补偿阶段被自适应跳过;<figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://github.com/zshell-zhang/static-content/raw/master/cs/taobao/%E4%B8%80%E7%A7%8D%E8%BD%BB%E9%87%8F%E7%BA%A7%E6%B5%81%E7%A8%8B%E5%BC%95%E6%93%8E%E7%9A%84%E5%AE%9E%E7%8E%B0%E6%80%9D%E8%B7%AF/11.png" alt="取消后的步骤状态" title="">                </div>                <div class="image-caption">取消后的步骤状态</div>            </figure></li><li><strong>case 2:</strong><br>取消前 a、b、c 号步骤作为整体, 和 d 号步骤是并行的;<br>取消后两条并行链路继续并行补偿;<figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://github.com/zshell-zhang/static-content/raw/master/cs/taobao/%E4%B8%80%E7%A7%8D%E8%BD%BB%E9%87%8F%E7%BA%A7%E6%B5%81%E7%A8%8B%E5%BC%95%E6%93%8E%E7%9A%84%E5%AE%9E%E7%8E%B0%E6%80%9D%E8%B7%AF/12.png" alt="取消之前的步骤状态" title="">                </div>                <div class="image-caption">取消之前的步骤状态</div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://github.com/zshell-zhang/static-content/raw/master/cs/taobao/%E4%B8%80%E7%A7%8D%E8%BD%BB%E9%87%8F%E7%BA%A7%E6%B5%81%E7%A8%8B%E5%BC%95%E6%93%8E%E7%9A%84%E5%AE%9E%E7%8E%B0%E6%80%9D%E8%B7%AF/13.png" alt="取消后的步骤状态" title="">                </div>                <div class="image-caption">取消后的步骤状态</div>            </figure></li></ul><h1 id="任务定义的演进"><a href="#任务定义的演进" class="headerlink" title="任务定义的演进"></a><strong>任务定义的演进</strong></h1><p>截止到本节之前, 其实本框架最核心的能力已经介绍完毕; 但是很明显, 如果让用户直接去裸用上述接口, 还是有一定理解及操作成本的, 本框架需要给用户提供一种易理解易上手的友好型交互使用模式;</p><h2 id="定义层-Definition-Layer"><a href="#定义层-Definition-Layer" class="headerlink" title="定义层 (Definition Layer)"></a><strong>定义层 (Definition Layer)</strong></h2><p>本框架考虑了将复合型步骤 (串行编排、并行编排) 的概念对用户屏蔽, 让用户专注于自身的业务步骤定义以及业务步骤间的关系管理, 为此引入了一个概念: <em><strong>Stage (阶段)</strong></em>, 并将步骤定义下放到 Stage 内:</p><figure class="highlight kotlin"><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="class"><span class="keyword">class</span> <span class="title">Stage</span> </span>&#123;</span><br><span class="line">  <span class="keyword">val</span> name: String</span><br><span class="line">  <span class="keyword">val</span> order: Integer</span><br><span class="line">  <span class="keyword">val</span> steps: List&lt;Step&gt;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Step</span> </span>&#123;</span><br><span class="line">  <span class="comment">// type = ATOMIC 时有意义</span></span><br><span class="line">  <span class="keyword">val</span> configs: StepConfigs</span><br><span class="line">  <span class="comment">// type = NESTED 时有意义</span></span><br><span class="line">  <span class="keyword">val</span> stages: List&lt;Stage&gt;</span><br><span class="line">  <span class="keyword">val</span> type: ATOMIC | NESTED</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>其行为特征如下:</p><ul><li>同一个 stage 内的多个 step 并行执行;</li><li>不同 stage 按照 stage 的 order 从小打到依次执行;</li><li>stage 有两种类型的 step:<ul><li>原子步骤: 不能再继续分解;</li><li>嵌套步骤: 嵌套另一个 stage, 递归定义;</li></ul></li></ul><p>虽然从本质上讲, 一个 stage 等效于本框架中的 CompositeTaskStep, 多个 stages 按 order 排列等效于本框架中的 SequentialTaskStep, 但从语义上看, 对用户暴露 <strong>阶段</strong> 的概念更贴合人类的思维习惯, 用户可以基于 Stage 这个 middle layer 组织业务步骤之间的关系;</p><h3 id="Stage-的数据结构"><a href="#Stage-的数据结构" class="headerlink" title="Stage 的数据结构"></a><strong>Stage 的数据结构</strong></h3><p>虽然本框架可以构建任意嵌套层级的任务结构, 但若将 Stage 作为一个整体来观察, 无论处于嵌套结构中的哪个位置, 同一层级同一作用域内的多个 stage 一定是线性排列的; 另外还需考虑到 Stage 的表达能力要同时兼顾补偿机制, 具备同时编排用户的普通 stage 和对应的补偿 $stage^´$ 的能力;</p><p>以上意味着 Stage 的数据结构要满足:</p><ul><li>支持正反两个方向 (正向普通和逆向补偿) 的序列编排;</li><li>支持从任何一个正向的 stage 节点切换到补偿节点 $stage^´$ 并反向流转;  <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></pre></td><td class="code"><pre><span class="line">  [end] &lt;- stage1´ &lt;- stage2´ &lt;- stage3´</span><br><span class="line">             ^          ^          ^</span><br><span class="line">             |          |          |</span><br><span class="line">[start] -&gt; stage1  -&gt; stage2  -&gt; stage3 -&gt; [end]</span><br></pre></td></tr></table></figure></li></ul><p>本框架为此构造了一种 <em><strong>“mirrored double list”</strong></em> 结构, 释义如下:</p><ul><li><strong>mirrored</strong>: 普通 stage 与补偿 $stage^´$ 操作对象相同、操作内容相反, 构成轴对称镜像;</li><li><strong>double</strong>: 表示正向普通与逆向补偿两条任务推进的动线;</li></ul><p>实现逻辑如下:</p><figure class="highlight kotlin"><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">// 泛型参数 DepType 的含义在下一节解释</span></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Stage</span>&lt;<span class="type">DepType</span>&gt; </span>&#123;</span><br><span class="line">  <span class="keyword">val</span> steps: List&lt;Step&gt;</span><br><span class="line">  <span class="keyword">val</span> compensate: CompensateStage</span><br><span class="line">  <span class="keyword">var</span> next: Stage&lt;DepType&gt;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">CompensateStage</span>&lt;<span class="type">DepType</span>&gt;: <span class="type">Stage</span>&lt;<span class="type">DepType</span>&gt; </span>&#123;</span><br><span class="line">  CompensateStage(compensateSteps: List&lt;Step&gt;) &#123;</span><br><span class="line">    <span class="keyword">super</span>(compensateSteps, CompensateStage&lt;&gt;())</span><br><span class="line">  &#125;</span><br><span class="line">  CompensateStage() &#123;</span><br><span class="line">    <span class="keyword">super</span>(nil, nil)</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>其中的关键是两个指针 <em>next</em> 和 <em>compensate</em>:</p><ul><li>普通 $stage_n$ 的 <em>next</em> 指向下一个 $stage_{n+1}$, $stage_n$ 的 <em>compensate</em> 指向与自己对应的补偿 $stage_n^´$;</li><li>补偿 $stage_n^´$ 的 <em>next</em> 指向下一个补偿 $stage_{n-1}^´$, 补偿 $stage_n^´$ 的 <em>compensate</em> 指向空;</li></ul><p>&nbsp;<br>附: 本框架基于 <em><strong>linked list</strong></em> 去编织 stage 镜像双链:</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><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><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> &lt;DepType&gt; <span class="function">Stage&lt;DepType&gt; <span class="title">buildStage</span><span class="params">(List&lt;StageConfig&gt; configs)</span> </span>&#123;</span><br><span class="line">  Stage&lt;DepType&gt; currentStage = <span class="keyword">null</span>;</span><br><span class="line">  Stage&lt;DepType&gt; previousStage;</span><br><span class="line">  <span class="comment">// 最终要返回的目标: 步骤定义链的头指针</span></span><br><span class="line">  Stage&lt;DepType&gt; entranceStage = <span class="keyword">null</span>;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">for</span> (<span class="keyword">int</span> order = <span class="number">0</span>; order &lt; configs.size(); order++) &#123;</span><br><span class="line">    previousStage = currentStage;</span><br><span class="line">    <span class="keyword">final</span> StageConfig stageConfig = configs.get(order);</span><br><span class="line">    currentStage = doBuildStage(order, configs.size(), stageConfig);</span><br><span class="line">    <span class="keyword">if</span> (previousStage != <span class="keyword">null</span>) &#123;</span><br><span class="line">      previousStage.next(currentStage);</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">      entranceStage = currentStage;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> entranceStage;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">private</span> &lt;DepType&gt; <span class="function">Stage&lt;DepType&gt; <span class="title">doBuildStage</span><span class="params">(<span class="keyword">int</span> order, <span class="keyword">int</span> totalStageCount,</span></span></span><br><span class="line"><span class="function"><span class="params">                                              StageConfig stageConfig)</span> </span>&#123;</span><br><span class="line">  <span class="keyword">final</span> List&lt;Step&lt;DepType&gt;&gt; steps = Lists.newArrayListWithExpectedSize(</span><br><span class="line">    stageConfig.getSteps().size() * <span class="number">2</span>);</span><br><span class="line">  <span class="keyword">for</span> (StepConfig step : stageConfig.getSteps()) &#123;</span><br><span class="line">    <span class="keyword">final</span> Pair&lt;Step&lt;DepType&gt;, Step&lt;DepType&gt;&gt; normalAndCompensates =</span><br><span class="line">      step.getType().buildSteps(order, totalStageCount, stageConfig, step);</span><br><span class="line">    steps.add(normalAndCompensates.getLeft());</span><br><span class="line">    steps.add(normalAndCompensates.getRight());</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> Stage.buildCompensable(steps.toArray(<span class="keyword">new</span> Step[<span class="number">0</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">static</span> &lt;DepType&gt; <span class="function">Stage&lt;DepType&gt; <span class="title">buildCompensable</span><span class="params">(Step&lt;DepType&gt;... steps)</span> </span>&#123;</span><br><span class="line">  <span class="keyword">final</span> List&lt;Step&lt;DepType&gt;&gt; normals = Lists.newArrayList();</span><br><span class="line">  <span class="keyword">final</span> List&lt;Step&lt;DepType&gt;&gt; compensates = Lists.newArrayList();</span><br><span class="line">  <span class="keyword">for</span> (<span class="keyword">int</span> i = <span class="number">0</span>; i &lt; steps.length / <span class="number">2</span>; i++) &#123;</span><br><span class="line">    <span class="keyword">final</span> Step&lt;DepType&gt; normal = steps[<span class="number">2</span> * i + <span class="number">0</span>];</span><br><span class="line">    <span class="keyword">final</span> Step&lt;DepType&gt; compensate = steps[<span class="number">2</span> * i + <span class="number">1</span>];</span><br><span class="line">    normals.add(normal);</span><br><span class="line">    compensates.add(compensate);</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> <span class="keyword">new</span> Stage&lt;&gt;(normals, <span class="keyword">new</span> CompensateStage&lt;&gt;(compensates));</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="任务实例的解释执行"><a href="#任务实例的解释执行" class="headerlink" title="任务实例的解释执行"></a><strong>任务实例的解释执行</strong></h3><p>定义层作为一个中间桥接层, 向上对接用户的任务&#x2F;步骤定义, 向下负责将用户的定义 interpret 为具体的任务实例, 具体的原则如下:</p><ul><li>当一个 stage 没有配置对应的补偿 stage^´, 本框架会使用一个空实现的 NoneTaskStep 对其补偿逻辑占位;</li><li>从入口 stage 开始, 本框架会通过 stage 的 next 指针遍历, 依据 Stage 中的 Step 定义, 构建具体的 TaskStep 实例:<ul><li><p>本框架引入了一个 step builder 的概念, 表示如何驱动具体步骤的实例化;<br>  -泛型 DepType 表示构建步骤实例所依赖的具体资源, 用户可以定义一个总线性质的类去承载整个任务中所有步骤的依赖对象, 并由该任务的所有步骤统一依赖 (上文 <a href="#OpsTask">OpsTask</a> 即遵循此约定);</p></li><li><p>step builder 的引入有效降低了中间定义层和步骤实例构造之间的耦合, 为 “一次编写, 处处使用” 的步骤复用场景打下了基础 (例如 API 统一发布和 API 归属应用迁移 两种场景, 依托 step builder 灵活的适配能力, 实现了步骤的 100% 复用);</p></li><li><p>另外, step builder 也为 定义的配置化 创造了条件: 实例构建的具体细节被屏蔽, 取而代之的是, step builder 的签名允许以极简的形式转化为任务定义的文本配置;</p>  <figure class="highlight kotlin"><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="class"><span class="keyword">class</span> <span class="title">AtomicStep</span>&lt;<span class="type">DepType</span>&gt;: <span class="type">Step</span>&lt;<span class="type">DepType</span>&gt; </span>&#123;</span><br><span class="line">    <span class="keyword">val</span> taskStepBuilder: TaskStepBuilder&lt;DepType&gt;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">NestedStep</span>: <span class="type">Step</span>&lt;<span class="type">DepType</span>&gt; </span>&#123;</span><br><span class="line">    <span class="keyword">val</span> nestedStage: Stage&lt;DepType&gt;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">interface</span> <span class="title">TaskStepBuilder</span>&lt;<span class="type">DepType</span>&gt; </span>&#123;</span><br><span class="line">    <span class="function"><span class="keyword">fun</span> <span class="title">build</span><span class="params">(String name, TaskContext taskContext, DepType stepDependencies)</span></span>: TaskStep</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></li><li><p>(stage 内并行编排) 扫描 stage 分别生成 normal 和 compensate 步骤实例的过程:</p><ul><li>一个 stage 内如果只有一个 step 定义, 解释为步骤实例直接返回;</li><li>一个 stage 内如果有多个 step 定义, 将其分别解释后的步骤实例组合为一个 CompositeTaskStep;</li></ul></li></ul></li><li>(stage 间串行编排) 当扫描完一段完整的 stage 链, 因为这一步可能是 CompensableTask 初始化时直接调用的, 需要返回一个 CompensableTaskStep (当然也可能不是, 比如嵌套 stage 的初始化):<ul><li>如果只生成了一对 normal &#x2F; compensate 步骤, 既可以构造一个 CompositeCompensableTaskStep, 也可以构造一个 SequentialCompensableTaskStep (<a href="#AtomicCompensateAwareStep">参考此处说明</a>);</li><li>如果生成了多对 normal &#x2F; compensate 步骤, 按序传入, 构造一个 SequentialCompensableTaskStep;</li></ul></li></ul><h3 id="任务实例的可视化"><a href="#任务实例的可视化" class="headerlink" title="任务实例的可视化"></a><strong>任务实例的可视化</strong></h3><p>对任务定义的独立化, 还有一个有益的用途是解除任务实例的生命周期对任务定义的生命周期的绑架:</p><ul><li>本框架的 <a href="#%E6%89%A7%E8%A1%8C%E6%A8%A1%E5%BC%8F">执行模式</a> 决定了一个任务中的步骤只有被执行了才会记录其状态, 否则不会留下任何痕迹;</li><li>如果没有任务定义的独立化, 那么当查询一个任务实例的执行概要就只能列出该任务已经执行过的步骤, 无法列出未执行的步骤, 用户无法建立起对任务执行进度的全局观, 有损用户体验;</li></ul><p>任务定义的生命周期和任务实例的生命周期并不相同, 缺乏任务定义的视角、单纯基于任务实例执行现状去构造任务执行概要, 只能取得管中窥豹的效果; 而当任务定义被剥离出来后, 本框架可以基于任务定义的快照, 在此之上填充各步骤的状态概要, 从而得以更友好地向用户展示任务执行进度;</p><h2 id="定义的配置化"><a href="#定义的配置化" class="headerlink" title="定义的配置化"></a><strong>定义的配置化</strong></h2><p>即使上一小节引入了对用户更友好的 Stage 概念, 但这只是降低了用户对步骤编排的理解成本, 却并未降低用户的使用操作成本; 为此本框架在上一节的基础之上更进一步, 实现了任务定义的全面配置化, 比如以下任务定义 DSL:</p><figure class="highlight"><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><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br></pre></td><td class="code"><pre><span class="line">[</span><br><span class="line">    &#123;</span><br><span class="line">        <span class="attr">"stageName"</span>: <span class="string">"前置准备"</span>,</span><br><span class="line">        <span class="attr">"steps"</span>: [</span><br><span class="line">            &#123;</span><br><span class="line">                <span class="attr">"normal"</span>: &#123;</span><br><span class="line">                    <span class="attr">"name"</span>: <span class="string">"APIChangefreeRecord"</span>,</span><br><span class="line">                    <span class="attr">"taskStepBuilder"</span>: <span class="string">"APIPublishCfRecordStep$APIPublishCfRecordStepBuilder"</span>,</span><br><span class="line">                &#125;,</span><br><span class="line">                <span class="attr">"compensate"</span>: &#123;</span><br><span class="line">                    <span class="attr">"name"</span>: <span class="string">"APIChangefreeRecordCancel"</span>,</span><br><span class="line">                    <span class="attr">"taskStepBuilder"</span>: <span class="string">"APIPublishCfRecordCancelStep$APIPublishCfRecordCancelStepBuilder"</span>,</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;,</span><br><span class="line">            &#123;</span><br><span class="line">                <span class="attr">"normal"</span>: &#123;</span><br><span class="line">                    <span class="attr">"name"</span>: <span class="string">"SecurityAudit"</span>,</span><br><span class="line">                    <span class="attr">"taskStepBuilder"</span>: <span class="string">"SecuritySubmitStep$SecuritySubmitStepBuilder"</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">    &#123;</span><br><span class="line">        <span class="attr">"stageName"</span>: <span class="string">"API 发布"</span>,</span><br><span class="line">        <span class="attr">"steps"</span>: [</span><br><span class="line">            &#123;</span><br><span class="line">                // 嵌套 stage 定义</span><br><span class="line">                "stages": [</span><br><span class="line">                    &#123;</span><br><span class="line">                        <span class="attr">"stageName"</span>: <span class="string">"元信息安全生产灰度引流"</span>,</span><br><span class="line">                        <span class="attr">"steps"</span>: [</span><br><span class="line">                            &#123;</span><br><span class="line">                                <span class="attr">"normal"</span>: &#123;</span><br><span class="line">                                    <span class="attr">"name"</span>: <span class="string">"APIMetaGrayPublish"</span>,</span><br><span class="line">                                    <span class="attr">"taskStepBuilder"</span>: <span class="string">"MetaGrayPublishStep$MetaGrayPublishStepBuilder"</span></span><br><span class="line">                                &#125;,</span><br><span class="line">                                <span class="attr">"compensate"</span>: &#123;</span><br><span class="line">                                    <span class="attr">"name"</span>: <span class="string">"APIMetaGrayCompensate"</span>,</span><br><span class="line">                                    <span class="attr">"taskStepBuilder"</span>: <span class="string">"MetaGrayCompensateStep$MetaGrayCompensateStepBuilder"</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">                    &#123;</span><br><span class="line">                        <span class="attr">"stageName"</span>: <span class="string">"元信息正式发布"</span>,</span><br><span class="line">                        <span class="attr">"steps"</span>: [</span><br><span class="line">                            &#123;</span><br><span class="line">                                <span class="attr">"normal"</span>: &#123;</span><br><span class="line">                                    <span class="attr">"name"</span>: <span class="string">"APIMetaPublish"</span>,</span><br><span class="line">                                    <span class="attr">"taskStepBuilder"</span>: <span class="string">"MetaPublishStep$MetaPublishStepBuilder"</span></span><br><span class="line">                                &#125;,</span><br><span class="line">                                <span class="attr">"compensate"</span>: &#123;</span><br><span class="line">                                    <span class="attr">"name"</span>: <span class="string">"APIMetaCompensate"</span>,</span><br><span class="line">                                    <span class="attr">"taskStepBuilder"</span>: <span class="string">"MetaCompensateStep$MetaCompensateStepBuilder"</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">                ]</span><br><span class="line">            &#125;,</span><br><span class="line">            &#123;</span><br><span class="line">                <span class="attr">"normal"</span>: &#123;</span><br><span class="line">                    <span class="attr">"name"</span>: <span class="string">"APIRoutePublish"</span>,</span><br><span class="line">                    <span class="attr">"taskStepBuilder"</span>: <span class="string">"APIRoutePublishStep$APIRoutePublishStepBuilder"</span></span><br><span class="line">                &#125;,</span><br><span class="line">                <span class="attr">"compensate"</span>: &#123;</span><br><span class="line">                    <span class="attr">"name"</span>: <span class="string">"APIRouteCompensate"</span>,</span><br><span class="line">                    <span class="attr">"taskStepBuilder"</span>: <span class="string">"APIRouteReverseStep$APIRouteCompensateStepBuilder"</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">    &#123;</span><br><span class="line">        <span class="attr">"stageName"</span>: <span class="string">"后置处理"</span>,</span><br><span class="line">        <span class="attr">"steps"</span>: [</span><br><span class="line">            &#123;</span><br><span class="line">                <span class="attr">"normal"</span>: &#123;</span><br><span class="line">                    <span class="attr">"name"</span>: <span class="string">"APIBaselineRecord"</span>,</span><br><span class="line">                    <span class="attr">"taskStepBuilder"</span>: <span class="string">"BaseLineRecordStep$BaseLineRecordStepBuilder"</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">]</span><br></pre></td></tr></table></figure><p>上述配置本框架将会为之生成如下任务结构:</p><ul><li><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">APIChangefreeRecord -|     |- APIMetaGrayPublish --&gt; APIMetaPublish -|</span><br><span class="line">                     |----&gt;|                                         |----&gt; APIBaselineRecord</span><br><span class="line">SecurityAudit -------|     |------------ APIRoutePublish ------------|</span><br></pre></td></tr></table></figure></li><li><p>补偿流程 1:</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">APIChangefreeRecord -|</span><br><span class="line">                     |----&gt; APIChangefreeRecordstage´</span><br><span class="line">SecurityAudit -------|</span><br></pre></td></tr></table></figure></li><li><p>补偿流程 2:</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">APIChangefreeRecord -|     |- APIMetaGrayPublish --&gt; APIMetaPublish -|     |- APIMetaPublish´ --&gt; APIMetaGrayPublish´ -|</span><br><span class="line">                     |----&gt;|                                         |----&gt;|                                           |----&gt;    APIChangefreeRecordstage´</span><br><span class="line">SecurityAudit -------|     |------------ APIRoutePublish ------------|     |------------- APIRoutePublish´ ------------|</span><br></pre></td></tr></table></figure></li></ul><p>为了最大限度地迎合普通人的直观思维习惯, 本框架允许以自然结对的方式配置 normal 和 compensate 步骤定义, 即: 用户无需关注任务取消时补偿步骤应当如何编排, 只管按照正向流程的自然顺序依次定义即可, 本框架全权负责将 DSL 配置翻译为以 Stage 为核心的中间定义层, 再将中间定义层转译解释为真实的任务实例;</p><h1 id="产品化展望"><a href="#产品化展望" class="headerlink" title="产品化展望"></a><strong>产品化展望</strong></h1><p>截止到本节之前, 本框架目前已实现的所有能力均已介绍完毕; 然而在产品化方面, 本框架仅仅处于最初级的阶段, 以下是本框架接下来可以发展的几个方向;</p><h2 id="任务定义域"><a href="#任务定义域" class="headerlink" title="任务定义域"></a><strong>任务定义域</strong></h2><h3 id="版本化"><a href="#版本化" class="headerlink" title="版本化"></a><strong>版本化</strong></h3><p>每一次任务定义的修改都应该被赋予一个唯一的版本, 而每一个任务实例也都应绑定到唯一的任务定义版本, 并伴随实例的整个生命周期;</p><p>如果没有版本化, 当任务定义修改 (比如前置增加一个步骤), 已执行或正在执行中的实例会因未曾执行过该新步骤而出现不可预知的结果; 而绑定任务定义版本的实例, 每次调度都会通过既定版本的任务定义解释执行, 与其他版本隔离, 保证执行结果的确定性;</p><h3 id="低代码"><a href="#低代码" class="headerlink" title="低代码"></a><strong>低代码</strong></h3><p>本框架当前使用 diamond 维护任务定义的配置内容,<em><strong>「上 diamond 编写任务定义的 DSL」</strong></em>这种方式只能说在部门内部小规模使用勉强可以满足, 但要推广到外部就不够看了;<br>当前业内有影响力的流程引擎大多支持了 ISO <a href="https://www.bpmn.org/" target="_blank" rel="noopener">业务流程建模 BPMN 2.0</a> 标准, 本框架也同样应当拥抱该业界规范, 实现图形化表达和 DSL 定义之间的互相转换, 从而允许开发者直接在图形界面上通过拖拽组件的方式实现便捷的流程编排, 更进一步降低接入及运维成本;<br>由于本框架编排流程的底层数据结构是基于<strong>「横(串行)」</strong>和<strong>「竖(并行)」</strong>两种基本逻辑单元组合交织而成, 对于任何 “无冗余依赖型的有向无环图” 都可以通过这种逻辑方式表达 (反例: 令 $G&#x3D;(a,b,c | a \rightarrow b, b \rightarrow c, a \rightarrow c)$, 由于 c 对 b 有依赖, 因而 c 对 a 的依赖被 b 对 a 的依赖所阻塞, 导致 c 对 a 的依赖是冗余的);<br>当一个 DAG 的数据结构被本框架标准化后, 就可以很方便地接入补偿机制, 实现生命周期的自动托管, 这也算是本框架的一个小小优势吧;</p><h2 id="调度域"><a href="#调度域" class="headerlink" title="调度域"></a><strong>调度域</strong></h2><h3 id="分布式化"><a href="#分布式化" class="headerlink" title="分布式化"></a><strong>分布式化</strong></h3><p>本框架当前虽已经沉淀了诸多可复用的常见步骤类型及通用实践经验, 但它们的代码都在淘系中间件控制台的 git 仓库内, 目前只能跑在控制台本地环境, 这意味着想要复用就必须局限在控制台内开发新的任务流程, 这极大限制了本框架的推广;<br>诚然, 轻量级的特性使得本框架也可以走 jar 包分发的路线, 允许用户在自己的应用中自主集成 sdk, 但如此就形成一个个应用孤岛, 无法聚集起生态, 管控亦没有抓手, 反馈与改进难以闭环, 因此走分布式化执行的路线是一个非常值得考虑的方向;</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://github.com/zshell-zhang/static-content/raw/master/cs/taobao/%E4%B8%80%E7%A7%8D%E8%BD%BB%E9%87%8F%E7%BA%A7%E6%B5%81%E7%A8%8B%E5%BC%95%E6%93%8E%E7%9A%84%E5%AE%9E%E7%8E%B0%E6%80%9D%E8%B7%AF/14.png" alt="分布式调度模型" title="">                </div>                <div class="image-caption">分布式调度模型</div>            </figure><p>在这个领域内最值得参考的就是 <a href="https://www.aliyun.com/aliware/schedulerx" target="_blank" rel="noopener">SchedulerX2</a>, 事实上 SchedulerX2 最近也推出了自己的流程编排能力: 一个分布式调度领域的框架进军编排领域是一个积累到一定程度自然而然的事情, 同理一个编排领域的框架涉足分布式调度领域, 某种角度上看也属殊途同归;</p><h2 id="产品定价"><a href="#产品定价" class="headerlink" title="产品定价"></a><strong>产品定价</strong></h2><p>流程引擎的服务定价是一件比较复杂的事情, 因为服务对象的形态差异较大, 对资源的要求不尽相同, 很难为每一个类型的任务给出合适精确的定价; 但得益于 <a href="#%E6%89%A7%E8%A1%8C%E6%A8%A1%E5%BC%8F">无状态的执行模式</a>, 本框架尽可能抹平了异构任务之间的复杂度差异 (结合 <a href="#%E5%88%86%E5%B8%83%E5%BC%8F%E5%8C%96">分布式化调度</a> 效果更加), 从而有可能只基于任务特征及步骤特征构建一个极简的定价模型:</p><p>$$ P(type) &#x3D; \alpha \cdot \vec{\gamma_t} \cdot \vec{\lambda_t} + (1 - \alpha) \cdot \sum_{n&#x3D;1}^{cnt(type)} \vec{\gamma_{sn}} \cdot \vec{\lambda_{sn}} + \omega $$</p><p>其中:</p><ul><li>$P(type)$: 某类型任务的单价 (单位 元&#x2F;次);</li><li>$cnt(type)$: 指定类型的任务下步骤的编排数量;</li><li>$\alpha$: 任务分量的定价权重;</li><li>$\vec{\gamma_t}$ 与 $\vec{\lambda_t}$: 任务的特征向量及特征对应的定价因子;</li><li>$\vec{\gamma_{sn}}$ 与 $\vec{\lambda_{sn}}$: 任务中指定步骤的特征向量及特征对应的定价因子;</li><li>$\omega$: 损耗常量 (云&#x2F;网络 等资源的成本分摊);</li></ul><p>为简化计算, 我们只取最核心的特征分量, 令:</p><p>$$ \vec{\gamma_t} &#x3D; \begin{bmatrix} steteful \\ stateless \end{bmatrix}, \vec{\gamma_{sn}} &#x3D; \begin{bmatrix} stateful \\ stateless \\ autoExe \\ manualExe \end{bmatrix} $$</p><p>结合存储服务和调度服务分摊给本框架的费用, 就可以为每一种类型的任务分别算出各自的定价; 以上模型在保证本产品运营不亏损的前提下, 尽可能兼顾了公平;</p><h1 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a><strong>参考资料</strong></h1><ul><li><a href="https://www.cs.cornell.edu/andru/cs711/2002fa/reading/sagas.pdf" target="_blank" rel="noopener">Hector &amp; Kenneth: Sagas (1987)</a></li><li><a href="https://www.bpmn.org/" target="_blank" rel="noopener">ISO 标准: 业务流程建模 BPMN 2.0</a></li><li><a href="https://www.aliyun.com/aliware/schedulerx" target="_blank" rel="noopener">SchedulerX2</a></li></ul></span>]]></content>
    
    <summary type="html">
    
      &lt;blockquote&gt;
&lt;p&gt;本人在淘天集团移动中间件团队亲历过不少有意思的项目, 本文总结了其中之一: 轻量级流程引擎;&lt;br&gt;同其他已在编排领域深内耕多年的框架相比, 本流程引擎还显得十分稚嫩, 甚至有重复造轮之嫌, 然而与老前辈们相比它又确实有一点点自己的小个性, 也确实满足了特定场景下的业务需求, 该篇即总结一下本人在相关领域内的一些实践经历;&lt;br&gt;(注: 本文涉及的相关代码已做过脱敏及混淆, 不一定能实际执行)&lt;/p&gt;
&lt;/blockquote&gt;
    
    </summary>
    
      <category term="taobao" scheme="http://zshell.cc/categories/taobao/"/>
    
      <category term="work" scheme="http://zshell.cc/categories/taobao/work/"/>
    
    
      <category term="阿里工作总结" scheme="http://zshell.cc/tags/%E9%98%BF%E9%87%8C%E5%B7%A5%E4%BD%9C%E6%80%BB%E7%BB%93/"/>
    
      <category term="分布式事务" scheme="http://zshell.cc/tags/%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1/"/>
    
  </entry>
  
  <entry>
    <title>https 的原理学习</title>
    <link href="http://zshell.cc/2025/06/22/web-https--https%E7%9A%84%E5%8E%9F%E7%90%86%E5%AD%A6%E4%B9%A0/"/>
    <id>http://zshell.cc/2025/06/22/web-https--https的原理学习/</id>
    <published>2025-06-22T13:11:12.000Z</published>
    <updated>2025-07-28T13:35:01.537Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>https 的实现涉及了众多密码学算法的应用, 为了保证信息传输的绝对安全衍生出了众多概念, 环环相扣, 逻辑无比严密, 值得详细地梳理与记录;</p></blockquote><a id="more"></a><h1 id="密码学基础知识"><a href="#密码学基础知识" class="headerlink" title="密码学基础知识"></a><strong>密码学基础知识</strong></h1><h2 id="对称加密"><a href="#对称加密" class="headerlink" title="对称加密"></a><strong>对称加密</strong></h2><p>定义一个密钥 <code>secretKey</code>, 用其对明文进行加密, 同理用这把密钥也可以对密文进行解密, 也就是说加密和解密，可以用同一个密钥, 速度比较快;<br>常用的对称加密方法有:</p><ul><li>DES</li><li>3DES</li><li>AES</li></ul><h2 id="非对称加密"><a href="#非对称加密" class="headerlink" title="非对称加密"></a><strong>非对称加密</strong></h2><p>定义一对公钥 <code>publicKey</code> 和私钥 <code>privateKey</code>, 可以使用私钥加密、公钥解密, 同理也可以使用公钥加密、私钥解密, 速度非常慢;<br>常见非对称加密方式的有:</p><ul><li>RSA (最常用)</li><li>DSA</li></ul><p>非对称加密的用途:</p><ul><li>加密 &#x2F; 解密;</li><li>签名 &#x2F; 验签 (身份认证);</li></ul><h2 id="单向加密"><a href="#单向加密" class="headerlink" title="单向加密"></a><strong>单向加密</strong></h2><p>不可逆加密, 对明文的加密产生一个密文, 不能再通过密文解出对应的明文, 一般用于计算消息摘要;<br>常见的单向加密有:</p><ul><li>MD5</li><li>SHA (SHA-128、SHA-256 等)</li></ul><h2 id="秘钥交换协议"><a href="#秘钥交换协议" class="headerlink" title="秘钥交换协议"></a><strong>秘钥交换协议</strong></h2><ul><li>Diffie-Hellman:<ul><li>依赖于计算有限群的离散对数的困难性;</li><li>运算速度相对快;</li><li>支持前向保密;</li></ul></li><li>ECDHE: 基于椭圆曲线 ECC 的 Diffie-Hellman, 使用椭圆曲线上的点作为公钥和私钥<ul><li>依赖于计算椭圆曲线离散对数的困难性;    </li><li>运算速度比传统的 DH 更快, 安全性也更高;</li><li>支持前向保密;</li></ul></li><li>RSA: 除了用于非对称加密, 也可用于秘钥交换<ul><li>依赖于大数分解问题的困难性;</li><li>运算速度相对慢;</li><li>不支持前向保密;</li></ul></li></ul><h1 id="安全传输的原理"><a href="#安全传输的原理" class="headerlink" title="安全传输的原理"></a><strong>安全传输的原理</strong></h1><p>https 安全传输的过程用一句话概述为: 用对称加解密实现内容的安全传输, 用非对称加解密 (或专用秘钥交换协议) 实现对称加密秘钥的安全传输;<br>这么做主要是出于性能的考虑:</p><ul><li>由于目标报文内容巨大, 故使用速度快的对称加密;</li><li>而对称加密秘钥的传输内容简短, 故可以容忍使用速度慢的非对称加密 (或专用秘钥交换协议);</li></ul><p>&nbsp;<br>以下我将用 6 个最关键的问题, 基于自然语言描述安全传输的核心逻辑:</p><ul><li><em><strong>如何将通信内容安全地传输给对方?</strong></em><br>答: 用对称加密的方式进行通信;</li><li><em><strong>如何将对称加密的秘钥 secretKey 安全地传输给对方?</strong></em><br>  方案一: 使用专用的秘钥交换协议 (例如 ECDHE, 两端通过特定算法分别自主计算出相同的秘钥);<br>  方案二: 用非对称加密的方式:<ol><li>服务端将公钥 publicKey 明文传输给客户端;</li><li>客户端随机生成一个对称加密的秘钥 secretKey;</li><li>客户端用 publicKey 对 secretKey 加密, 并传给服务端;</li><li>服务端用 privateKey 解密出 secretKey;</li></ol></li><li><em><strong>服务端如何将真实的公钥 publicKey 传输给对方?</strong></em><br>答: 使用 CA 颁发的数字证书验证 publicKey 真伪:<ol><li>服务端将包含公钥 publicKey 的证书发送给客户端;</li><li>客户端验证证书的真伪;</li><li>客户端从证书中提取网站的公钥 publicKey;</li></ol></li><li><em><strong>客户端如何验证数字证书的真伪 (防篡改)?</strong></em><br>答: 客户端对数字证书验签:<ol><li>客户端用 CA 的公钥对证书中的签名解密得到摘要 D1;</li><li>客户端用证书中的摘要算法, 对证书内容计算出摘要 D2;</li><li>若 D1 &#x3D;&#x3D; D2 则验证通过;</li></ol></li><li><em><strong>如何确保 CA 的公钥是真实的?</strong></em><br>答: CA 为自己的公钥专门生成的证书已提前内置在操作系统中了, 无需从网络中获取;</li><li><em><strong>如何生成域名的数字证书?</strong></em><br>答: 网站提交指定材料, 向 CA 机构申请数字证书:<ol><li>CA 向证书中写入摘要算法、域名、网站的公钥等重要信息;</li><li>CA 根据证书中写入的摘要算法, 计算出证书的摘要;</li><li>CA 用自己的私钥对摘要进行加密, 计算出签名;</li><li>CA 生成一张数字证书, 颁发给指定域名;</li><li>网站的管理员, 把证书放在自己的服务器上;</li></ol></li></ul><p>&nbsp;<br>用一张图来形象概括上述 6 个问题提及的知识点:</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://github.com/zshell-zhang/static-content/raw/master/cs/web/https/https%20%E7%9A%84%E5%8E%9F%E7%90%86%E5%AD%A6%E4%B9%A0/1.png" alt="https 的简要过程" title="">                </div>                <div class="image-caption">https 的简要过程</div>            </figure><h2 id="数字证书"><a href="#数字证书" class="headerlink" title="数字证书"></a><strong>数字证书</strong></h2><p>数字证书 (Digital Certificate) 是一种用于验证网络实体 (如个人、设备、服务器等) 身份的电子凭证, 类似于现实生活中的身份证或护照; 它基于<strong>公钥基础设施 (PKI，Public Key Infrastructure)</strong> 技术, 确保通信双方的身份真实性 (确保客户端访问的是真实的网站而非钓鱼网站) 和数据传输的安全性;</p><h3 id="证书的内容"><a href="#证书的内容" class="headerlink" title="证书的内容"></a><strong>证书的内容</strong></h3><p>一个标准的数字证书通常包含以下信息:</p><ul><li>持有者信息: 名称、组织等;</li><li>颁发者信息: 证书颁发机构 (CA) 的名称;</li><li>有效期: 证书的生效和过期时间;</li><li>公钥 (Public Key): 用于加密数据或验证签名;</li><li>数字签名: 由 CA 对证书内容进行签名, 防止伪造;</li></ul><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://github.com/zshell-zhang/static-content/raw/master/cs/web/https/https%20%E7%9A%84%E5%8E%9F%E7%90%86%E5%AD%A6%E4%B9%A0/2.png" alt="zshell.cc 证书内容" title="">                </div>                <div class="image-caption">zshell.cc 证书内容</div>            </figure><h3 id="证书颁发机构"><a href="#证书颁发机构" class="headerlink" title="证书颁发机构"></a><strong>证书颁发机构</strong></h3><p>由上文可知, 由于窃听者 &#x2F; 中间人的存在, 我们无法信任公网传输的任何信息, 这就形成了一个无解的猜疑链:</p><ul><li>我怎么知道秘钥 secretKey 是否安全得传给对方?</li><li>我怎么知道对方传输的公钥 publicKey 是真实的?</li><li>如果要引入一个权威的公证人来证明 publicKey 的真实性, 我又如何知道这个公证人本身是没问题的?</li><li>……</li></ul><p>要想打破这个猜疑链, 就必须要在源头上建立起对公证人权威的信任, 于是现代主流的操作系统都内置了主流证书颁发机构 (Certificate Authority) 的公钥, 只要我们使用的是正版操作系统, 就可以放心使用 CA 的 publicKey 来验证服务端传输的数字证书的真伪;</p><h1 id="SSL-TLS-协议"><a href="#SSL-TLS-协议" class="headerlink" title="SSL &#x2F; TLS 协议"></a><strong>SSL &#x2F; TLS 协议</strong></h1><p>TLS (Transport Layer Security) 传输层安全性协议, 它的前身是 SSL (Secure Sockets Layer) 安全套接层, 是一个被应用程序用来在网络中安全的通讯协议, 它实现了将应用层的报文进行加密后再交由 TCP 进行传输的功能;<br>TLS 基于 TCP, 实现了两个应用进程之间的安全传输连接:</p><ul><li>完成身份鉴别 (鉴别服务端或客户端);</li><li>安全地共享一对协商密钥 (主密钥) 来进行对称加密;</li><li>保证报文的完整性, 防篡改;</li><li>预防恶意的重放攻击;</li></ul><h2 id="协议栈总览"><a href="#协议栈总览" class="headerlink" title="协议栈总览"></a><strong>协议栈总览</strong></h2><ul><li>TLS 握手协议: 用于加密、身份鉴别;</li><li>TLS 记录协议: 用于保证报文的完整性和防重放攻击;</li></ul><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://github.com/zshell-zhang/static-content/raw/master/cs/web/https/https%20%E7%9A%84%E5%8E%9F%E7%90%86%E5%AD%A6%E4%B9%A0/3.png" alt="TLS 1.3 协议栈" title="">                </div>                <div class="image-caption">TLS 1.3 协议栈</div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://github.com/zshell-zhang/static-content/raw/master/cs/web/https/https%20%E7%9A%84%E5%8E%9F%E7%90%86%E5%AD%A6%E4%B9%A0/4.png" alt="TLS 1.3 协议栈_English" title="">                </div>                <div class="image-caption">TLS 1.3 协议栈_English</div>            </figure><h3 id="TLS-握手"><a href="#TLS-握手" class="headerlink" title="TLS 握手"></a><strong>TLS 握手</strong></h3><p>TLS 握手协议用于实现在进行安全传输之前必要的身份鉴别和协商共享密钥:</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://github.com/zshell-zhang/static-content/raw/master/cs/web/https/https%20%E7%9A%84%E5%8E%9F%E7%90%86%E5%AD%A6%E4%B9%A0/5.png" alt="TLS 1.3 handshake protocol brief" title="">                </div>                <div class="image-caption">TLS 1.3 handshake protocol brief</div>            </figure><p>握手协议的信息交换是在没有加密的情况下进行的, 在这一协议中所收发的所有数据都可能被窃听者窃听 (当然此阶段不传输真实报文, 即使被窃听也不用担心);</p><p>&nbsp;</p><h2 id="密码套件-Cipher-Suites"><a href="#密码套件-Cipher-Suites" class="headerlink" title="密码套件 (Cipher Suites)"></a><strong>密码套件 (Cipher Suites)</strong></h2><p>由上文我们知道, https 的建立过程涉及到如下四个逻辑:</p><ul><li>密钥交换 (Key Exchange): 生成需要的密钥;</li><li>身份认证 (Authentication): 验证 Server 身份;</li><li>对称加密 (Symmetric Encryption): 为数据传输提供保密性;</li><li>摘要算法 (Hashing): 数据完整性校验;</li></ul><p>以上每个部分都可以使用不同的、可替换的算法实现, 如果要穷举所有的组合可能性, 其组合总数将会爆炸, 为了在 client - server 两端就以上四点快速达成一致协议, 就产生了密码套件的概念 (TLS 协议不允许用户对局部某个算法进行自定义, 只能协商选择一个密码套件);<br>在 <a href="https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml" target="_blank" rel="noopener">Internet Assigned Numbers Authority</a> 中详细枚举了所有的密码套件, 以下是几种常见的密码套件:</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://github.com/zshell-zhang/static-content/raw/master/cs/web/https/https%20%E7%9A%84%E5%8E%9F%E7%90%86%E5%AD%A6%E4%B9%A0/6.png" alt="密码套件举例" title="">                </div>                <div class="image-caption">密码套件举例</div>            </figure><p>其中关于秘钥交换协议的对比:</p><ul><li>RSA:<ul><li>RSA 的客户端 pre-master 需要通过网络传输发送给服务端, 一旦服务器的私钥泄露, 会话密钥就会被破解;</li><li>TLS 完成四次握手后, 才能进行应用数据传输;</li></ul></li><li>ECDHE (更加先进):<ul><li>秘钥是两端独立计算出来的, 不会在网络上传输, 不存在直接泄漏的可能;</li><li>客户端可以不用等服务端的最后一次 TLS 握手, 就提前发出加密的 HTTP 数据, 节省了一个 RTT;</li></ul></li></ul><p>现代主流的 TLS 1.3 已淘汰 RSA, 默认使用 ECDHE 作为秘钥交换协议;</p><p>&nbsp;</p><h2 id="TLS-握手协议详细过程"><a href="#TLS-握手协议详细过程" class="headerlink" title="TLS 握手协议详细过程"></a><strong>TLS 握手协议详细过程</strong></h2><ul><li><strong>pre-master secret</strong>: 客户端随机生成的一个 48 字节 (384 位) 的临时秘钥, 其格式为:<ul><li>前 2 字节为 TLS 版本号 (如 0x0303 代表 TLS 1.2);</li><li>后 46 字节为随机数据;</li></ul></li><li>pre-master 的安全传输:<ul><li>RSA 秘钥交换: 非前向保密;</li><li>ECDHE 秘钥交换: 前向保密;</li></ul></li><li><strong>master secret</strong>: 生成最终对称加密秘钥的 secret<ul><li>client &#x2F; server 互相明文交换 random 随机数;</li><li>通过伪随机函数 PRF 生成: <code>PRF(pre-master, &quot;master secret&quot;, client.random + server.random)</code></li></ul></li></ul><p>TLS 握手协议的详细过程如下:</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://github.com/zshell-zhang/static-content/raw/master/cs/web/https/https%20%E7%9A%84%E5%8E%9F%E7%90%86%E5%AD%A6%E4%B9%A0/7.png" alt="TLS 1.3 handshake protocol detail" title="">                </div>                <div class="image-caption">TLS 1.3 handshake protocol detail</div>            </figure><h3 id="多级秘钥-pre-master-master"><a href="#多级秘钥-pre-master-master" class="headerlink" title="多级秘钥: pre-master &#x2F; master"></a><strong>多级秘钥: pre-master &#x2F; master</strong></h3><p>由上图可见, TLS 的秘钥交换协议并非直接交换对称加密秘钥或相关 secret, 而是区分出了 pre-master &#x2F; master secret, 如此设计的原因如下:  </p><ol><li><p>提高安全性, 防止密钥单一依赖:</p><ul><li>master Secret 依赖更多随机因素:<ul><li>master secret 由 Pre-master secret + 客户端随机数 (ClientRandom) + 服务器随机数 (ServerRandom) 通过 PRF (伪随机函数) 计算得出;</li><li>如果直接传输 master secret，攻击者截获后就能直接解密通信。  </li><li>而通过 pre-master secret 计算 master secret, 增加了额外随机数 (ClientRandom 和 ServerRandom), 即使 pre-master 被破解, 仍需这两个随机数才能推导会话密钥, 提高安全性;</li></ul></li><li>防止重放攻击:  <ul><li>如果 master secret 直接传输，攻击者可能记录握手过程并重放，导致相同的 master secret 被多次使用;</li><li>但 master secret 是由握手阶段的随机数动态生成的，每次握手都会不同，即使相同的 pre-master secret 也无法恢复相同的会话密钥;</li></ul></li></ul></li><li><p>增强前向保密:</p><ul><li>pre-master secret 可以临时生成:<ul><li>在 TLS 1.2 及之前，如果使用 RSA 密钥交换，pre-master secret 可能被服务器私钥解密，导致历史通信被破解 (无 PFS);</li><li>但 ECDHE&#x2F;DHE 允许 pre-master secret 通过临时密钥计算得出，即使服务器私钥泄漏，也无法解密过去的 pre-master secret;</li><li>如果直接传输 master secret, 则无法实现这种前向保密机制;</li></ul></li><li>TLS 1.3 已淘汰 RSA 密钥交换, 默认使用 ECDHE, 确保 pre-master secret 具备 PFS;</li></ul></li><li><p>防止中间人篡改密钥:</p><ul><li>master secret 依赖于握手阶段的随机数：  <ul><li>如果攻击者试图篡改 ClientRandom 或 ServerRandom, 会导致双方计算出的 master secret 不同, 握手失败;</li><li>而如果直接传输 master secret, 攻击者可能篡改它导致密钥被控制;</li></ul></li></ul></li></ol><h1 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a><strong>参考资料</strong></h1><ul><li><a href="https://my.oschina.net/helloworldnet/blog/5587819" target="_blank" rel="noopener">通俗大白话，彻底弄懂 https 原理本质</a></li><li><a href="https://blog.csdn.net/xxdw1992/article/details/142628035" target="_blank" rel="noopener">TLS协议详解</a></li><li><a href="https://blog.csdn.net/Taki_UP/article/details/142050966" target="_blank" rel="noopener">SSL证书之密码套件总览</a></li><li><a href="https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml" target="_blank" rel="noopener">Internet Assigned Numbers Authority</a></li><li><a href="https://zhuanlan.zhihu.com/p/662286336" target="_blank" rel="noopener">HTTPS中的密钥交换协议</a></li><li><a href="https://blog.csdn.net/weixin_68074170/article/details/140618393" target="_blank" rel="noopener">密钥交换算法（RSA、ECDHE）</a></li></ul>]]></content>
    
    <summary type="html">
    
      &lt;blockquote&gt;
&lt;p&gt;https 的实现涉及了众多密码学算法的应用, 为了保证信息传输的绝对安全衍生出了众多概念, 环环相扣, 逻辑无比严密, 值得详细地梳理与记录;&lt;/p&gt;
&lt;/blockquote&gt;
    
    </summary>
    
      <category term="web" scheme="http://zshell.cc/categories/web/"/>
    
      <category term="https" scheme="http://zshell.cc/categories/web/https/"/>
    
    
      <category term="https" scheme="http://zshell.cc/tags/https/"/>
    
      <category term="信息安全" scheme="http://zshell.cc/tags/%E4%BF%A1%E6%81%AF%E5%AE%89%E5%85%A8/"/>
    
  </entry>
  
  <entry>
    <title>利用 cloudflare 托管个人域名</title>
    <link href="http://zshell.cc/2025/06/02/life-pc--%E5%88%A9%E7%94%A8cloudflare%E6%89%98%E7%AE%A1%E4%B8%AA%E4%BA%BA%E5%9F%9F%E5%90%8D/"/>
    <id>http://zshell.cc/2025/06/02/life-pc--利用cloudflare托管个人域名/</id>
    <published>2025-06-02T15:46:38.000Z</published>
    <updated>2025-07-28T13:35:01.516Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>我在阿里云万网购买的个人域名 <code>zshell.cc</code>, 原先 CNAME 到了 github pages 的域名, 由于地缘政治风险, 已无法通畅访问, 于是我将博客迁移到了国内的 <a href="https://atomgit.com/zshellzhang/zshell" target="_blank" rel="noopener">开放原子 atomgit pages</a>, 本文记录了我如何利用 cloudflare 将域名解析完美迁移到 atomgit 的过程;</p></blockquote><a id="more"></a><h2 id="国内域名托管商的限制"><a href="#国内域名托管商的限制" class="headerlink" title="国内域名托管商的限制"></a><strong>国内域名托管商的限制</strong></h2><p>我的需求如下:</p><ul><li>我想让用户使用 zshell.cc 域名访问我的博客, 同时不能重定向到 atomgit 给我分配的默认域名 zshellzhang.atomgit.net;</li><li>atomgit pages 首页限制了必须要带上默认的 path (值为注册在 atomgit 的仓库名, 我的是 zshell), 那么当我访问 zshell.cc, 需要帮我重定向到 <a href="zshell.cc/zshell">zshell.cc&#x2F;zshell</a>;</li></ul><p>对此需求, 普通的 CNAME 解析已经不够用了, 必须使用隐式 URI 解析, 另外还存在当匹配部分 path 时替换为指定 path 的场景, 这已经属于比较高级的需求了;<br>原先我的域名解析托管在阿里云万网, 然而万网对于配置 URI 解析的前提是要求域名必须在国内备案, 而备案又要求域名必须绑定阿里云的服务器 (且要求服务器套餐费用不得低于 100 元&#x2F;年);<br>很明显, 我是不可能在这种事情上花一分钱的, 于是只能另寻他路;</p><h2 id="域名解析的迁移"><a href="#域名解析的迁移" class="headerlink" title="域名解析的迁移"></a><strong>域名解析的迁移</strong></h2><p>首先登录 <a href="https://dc.console.aliyun.com/#/domain/details/dns-modify?saleId=S20182Q12PQ78564&domain=zshell.cc" target="_blank" rel="noopener">万网域名管理</a>, 修改 DNS 服务器, 用 cloudflare 代替万网来托管域名解析:</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://github.com/zshell-zhang/static-content/raw/master/life/pc/%E5%88%A9%E7%94%A8%20cloudflare%20%E6%89%98%E7%AE%A1%E4%B8%AA%E4%BA%BA%E5%9F%9F%E5%90%8D/1.png" alt="" title="">                </div>                <div class="image-caption"></div>            </figure><p>同时在 <a href="https://dash.cloudflare.com/e4e235d197ac3476205068b8079c323a/zshell.cc/dns/records" target="_blank" rel="noopener">cloudflare 控制台</a> 配置 CNAME 规则:</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://github.com/zshell-zhang/static-content/raw/master/life/pc/%E5%88%A9%E7%94%A8%20cloudflare%20%E6%89%98%E7%AE%A1%E4%B8%AA%E4%BA%BA%E5%9F%9F%E5%90%8D/2.png" alt="" title="">                </div>                <div class="image-caption"></div>            </figure><h2 id="定义-Worker-反向代理"><a href="#定义-Worker-反向代理" class="headerlink" title="定义 Worker 反向代理"></a><strong>定义 Worker 反向代理</strong></h2><p>编写反向代理逻辑 url-implict-redirect.js:</p><figure class="highlight javascript"><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></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * Welcome to Cloudflare Workers! This is your first worker.</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * - Run "npm run dev" in your terminal to start a development server</span></span><br><span class="line"><span class="comment"> * - Open a browser tab at http://localhost:8787/ to see your worker in action</span></span><br><span class="line"><span class="comment"> * - Run "npm run deploy" to publish your worker</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * Learn more at https://developers.cloudflare.com/workers/</span></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="keyword">async</span> fetch(request) &#123;</span><br><span class="line">    <span class="keyword">const</span> url = <span class="keyword">new</span> URL(request.url);</span><br><span class="line">    <span class="keyword">const</span> path = url.pathname;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 目标域名（实际目标）</span></span><br><span class="line">    <span class="keyword">const</span> targetDomain = <span class="string">"https://zshellzhang.atomgit.net"</span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 处理路径：</span></span><br><span class="line">    <span class="comment">//   - / → 代理到 /zshell</span></span><br><span class="line">    <span class="comment">//   - /about → 代理到 /zshell/about</span></span><br><span class="line">    <span class="comment">//   - /zshell → 代理到 /zshell</span></span><br><span class="line">    <span class="comment">//   - /zshell/about → 代理到 /zshell/about</span></span><br><span class="line">    <span class="keyword">const</span> targetPath = path.startsWith(<span class="string">"/zshell"</span>) ? path : <span class="string">`/zshell<span class="subst">$&#123;path&#125;</span>`</span>;</span><br><span class="line">    <span class="keyword">const</span> response = <span class="keyword">await</span> fetch(<span class="string">`<span class="subst">$&#123;targetDomain&#125;</span><span class="subst">$&#123;targetPath&#125;</span>`</span>, &#123;</span><br><span class="line">      headers: request.headers</span><br><span class="line">    &#125;);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 返回响应（保持原始URL）</span></span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">new</span> Response(response.body, &#123;</span><br><span class="line">      status: response.status,</span><br><span class="line">      headers: response.headers</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>部署并监控 Worker 的状态: <a href="https://dash.cloudflare.com/e4e235d197ac3476205068b8079c323a/workers/services/view/url-implicit-redirect/production/metrics" target="_blank" rel="noopener">url-implict-redirect</a>;</p><h2 id="将域名绑定-Worker"><a href="#将域名绑定-Worker" class="headerlink" title="将域名绑定 Worker"></a><strong>将域名绑定 Worker</strong></h2><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://github.com/zshell-zhang/static-content/raw/master/life/pc/%E5%88%A9%E7%94%A8%20cloudflare%20%E6%89%98%E7%AE%A1%E4%B8%AA%E4%BA%BA%E5%9F%9F%E5%90%8D/3.png" alt="" title="">                </div>                <div class="image-caption"></div>            </figure><p>效果如下:</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://github.com/zshell-zhang/static-content/raw/master/life/pc/%E5%88%A9%E7%94%A8%20cloudflare%20%E6%89%98%E7%AE%A1%E4%B8%AA%E4%BA%BA%E5%9F%9F%E5%90%8D/4.png" alt="" title="">                </div>                <div class="image-caption"></div>            </figure>]]></content>
    
    <summary type="html">
    
      &lt;blockquote&gt;
&lt;p&gt;我在阿里云万网购买的个人域名 &lt;code&gt;zshell.cc&lt;/code&gt;, 原先 CNAME 到了 github pages 的域名, 由于地缘政治风险, 已无法通畅访问, 于是我将博客迁移到了国内的 &lt;a href=&quot;https://atomgit.com/zshellzhang/zshell&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;开放原子 atomgit pages&lt;/a&gt;, 本文记录了我如何利用 cloudflare 将域名解析完美迁移到 atomgit 的过程;&lt;/p&gt;
&lt;/blockquote&gt;
    
    </summary>
    
      <category term="life" scheme="http://zshell.cc/categories/life/"/>
    
      <category term="pc" scheme="http://zshell.cc/categories/life/pc/"/>
    
    
      <category term="科学上网" scheme="http://zshell.cc/tags/%E7%A7%91%E5%AD%A6%E4%B8%8A%E7%BD%91/"/>
    
  </entry>
  
  <entry>
    <title>epoll 的原理详解</title>
    <link href="http://zshell.cc/2025/05/04/linux-base--epoll%E7%9A%84%E5%8E%9F%E7%90%86%E8%AF%A6%E8%A7%A3/"/>
    <id>http://zshell.cc/2025/05/04/linux-base--epoll的原理详解/</id>
    <published>2025-05-04T15:23:10.000Z</published>
    <updated>2025-07-28T13:35:01.520Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p> 2002 年 linux 2.5.44 发布了 epoll, epoll 是 linux 实现高并发网络 I&#x2F;O 处理的基础, 但凡存在网络 I&#x2F;O 的系统或框架 (比如 nginx、netty、kafka、redis 等) 都离不开 epoll;<br>因此学习 epoll 的原理对于应用开发者理解网络框架的运行原理有着巨大的帮助;</p></blockquote><a id="more"></a><h1 id="前置基础知识"><a href="#前置基础知识" class="headerlink" title="前置基础知识"></a><strong>前置基础知识</strong></h1><h2 id="DMA-与中断"><a href="#DMA-与中断" class="headerlink" title="DMA 与中断"></a><strong>DMA 与中断</strong></h2><p>当 NIC 网卡收到网络数据时的处理流程:</p><ul><li>网卡通过 Direct Memory Access 将数据写到内核内存 (Ring Buffer 和 sk_buff);</li><li>网卡向 CPU 发出中断请求 (Interrupt Request, IRQ);</li><li>为解决频繁 IRQ 导致大量上下文切换的开销, 内核采用了请求预聚合技术 (<strong>N-API</strong>, 即 New API):<ul><li>内核的 napi_schedule 函数专门快速响应 IRQ, 只记录必要信息, 并在合适的时机发出软中断 softirq;</li><li>内核的 net_rx_action 函数响应软中断, 从 Ring Buffer 中批量拉取收到的数据并处理协议栈, 填充 socket 再交付上层;<figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://github.com/zshell-zhang/static-content/raw/master/cs/linux/base/epoll%20%E7%9A%84%E5%8E%9F%E7%90%86%E8%AF%A6%E8%A7%A3/1.png" alt="net data transfer" title="">                </div>                <div class="image-caption">net data transfer</div>            </figure></li></ul></li></ul><h2 id="linux-一切皆文件"><a href="#linux-一切皆文件" class="headerlink" title="linux: 一切皆文件"></a><strong>linux: 一切皆文件</strong></h2><p>在 linux 系统中 “一切皆文件 (Everything is a file)” 是一个核心的设计理念,<br>linux 将所有资源 (无论是普通文件、目录、设备、网络套接字还是进程信息) 都抽象为文件的形式, 并以一个抽象层 (Virtual File System) 统一管理, 使得用户可以用统一的文件描述符处理各种资源:</p><ul><li>普通文件：如文本文件、图片文件等;</li><li>目录：目录本身也被视为一种特殊的文件;</li><li>设备：硬件设备（如硬盘、键盘、显示器）被抽象为设备文件;</li><li>管道和套接字：用于进程间通信的管道和网络通信的套接字也可以像文件一样操作;</li><li>系统信息：例如 &#x2F;proc 和 &#x2F;sys 文件系统中的内容，提供对系统状态和内核参数的访问;</li></ul><p><strong>当我们使用 epoll 时, 内核会创建一个 struct <code>eventpoll</code>, eventpoll 也是文件系统中的一种资源类型</strong>;</p><h2 id="文件系统的-wait-queue"><a href="#文件系统的-wait-queue" class="headerlink" title="文件系统的 wait queue"></a><strong>文件系统的 wait queue</strong></h2><p>在 Linux 文件系统中，等待队列是一种内核同步机制，用于管理因某些条件未满足而需要等待的进程; 它广泛应用于文件 I&#x2F;O 操作、设备驱动程序和网络通信中，尤其是在处理阻塞操作时:</p><ul><li>当读取文件时，如果文件的数据尚未准备好 (如管道或设备文件), 进程会被加入到等待队列中;</li><li>当写入文件时，如果缓冲区已满，进程也会被加入到等待队列中;</li></ul><p>当条件满足时 (如数据到达或缓冲区有空间可用) 内核会唤醒等待队列中的进程;</p><p><strong>当我们使用 epoll 时, 我们主要与两种文件类型打交道: eventpoll 和 socket, 他们分别有自己的等待队列 (wq), 存放不同类型的资源</strong>:</p><ul><li><strong>eventpoll 队列: 存放的是等待 IO 事件到来的进程</strong>;</li><li><strong>socket 队列: 存放的是 struct eventpoll</strong>;</li></ul><p>linux 内核中通常用 <code>wait_queue_head_t</code> 双向链表类型来定义等待队列;</p><h1 id="epoll-的数据结构"><a href="#epoll-的数据结构" class="headerlink" title="epoll 的数据结构"></a><strong>epoll 的数据结构</strong></h1><h2 id="eventpoll"><a href="#eventpoll" class="headerlink" title="eventpoll"></a><strong>eventpoll</strong></h2><p>eventpoll 是 epoll 核心数据结构, 用于管理一个 epoll 实例的所有相关信息:</p><figure class="highlight c"><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="class"><span class="keyword">struct</span> <span class="title">eventpoll</span> &#123;</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">rb_root</span> <span class="title">rbr</span>;</span>             <span class="comment">// 红黑树，存储所有注册的 epitem</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">list_head</span> <span class="title">rdllist</span>;</span>       <span class="comment">// 就绪链表，存储当前就绪的 epitem</span></span><br><span class="line">    <span class="keyword">wait_queue_head_t</span> wq;           <span class="comment">// 等待队列，用于阻塞调用 epoll_wait() 的进程</span></span><br><span class="line">    </span><br><span class="line">    <span class="keyword">spinlock_t</span> lock;                <span class="comment">// 保护 eventpoll 对象的自旋锁</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">mutex</span> <span class="title">mtx</span>;</span>               <span class="comment">// 互斥锁，用于保护某些关键操作</span></span><br><span class="line">    <span class="keyword">wait_queue_head_t</span> poll_wait;    <span class="comment">// 用于支持嵌套 epoll (epoll over epoll)</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">epitem</span> *<span class="title">ovflist</span>;</span>         <span class="comment">// 溢出链表，用于处理边缘触发模式下的特殊情况</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">user_struct</span> *<span class="title">user</span>;</span>       <span class="comment">// 用户信息，用于资源限制跟踪</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">file</span> *<span class="title">file</span>;</span>              <span class="comment">// 指向与该 epoll 实例关联的文件对象</span></span><br><span class="line">    <span class="keyword">int</span> visited;                    <span class="comment">// 用于标记是否访问过，避免循环嵌套</span></span><br><span class="line">    ...</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>eventpoll 结构体主要成员如下:</p><ul><li><strong>rbr</strong>: 用于记录用户注册的其所感兴趣的 socket 事件, 使用红黑树结构, 方便后续查找;</li><li><strong>rdllist</strong>: 当向 epoll 注册的 socket 事件发生后, 就绪队列会记录相关 socket 的读&#x2F;写事件并返回给用户进程;</li><li><strong>wq</strong>: 当用户进程调用 epoll_wait 进入休眠后, 被挂在该等待队列, 用于注册事件发生时回调唤醒该进程;</li></ul><h2 id="epitem"><a href="#epitem" class="headerlink" title="epitem"></a><strong>epitem</strong></h2><p>epitem 是 epoll 另一个核心数据结构, 承载了相关文件描述符 (通常为 socket 描述符) 及其事件订阅信息:</p><ul><li>ffd: 通过 ffd.fd 可以获取目标文件描述符;</li><li>event: 事件类型, 常用如下: EPOLLIN (socket 可读), EPOLLOUT (socket 可写)<figure class="highlight c"><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="class"><span class="keyword">struct</span> <span class="title">epitem</span> &#123;</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">epoll_filefd</span> <span class="title">ffd</span>;</span>  <span class="comment">// 文件描述符及其相关信息</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">epoll_event</span> <span class="title">event</span>;</span> <span class="comment">// 用户注册的事件信息</span></span><br><span class="line">    </span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">rb_node</span> <span class="title">rbn</span>;</span>       <span class="comment">// 红黑树节点, 用于插入到红黑树 (rbr) 中</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">list_head</span> <span class="title">rdllink</span>;</span> <span class="comment">// 就绪链表节点, 用于插入到就绪链表 (rdllist) 中</span></span><br><span class="line">    </span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">eventpoll</span> *<span class="title">ep</span>;</span>     <span class="comment">// 指向所属的 eventpoll 对象</span></span><br><span class="line">    ...</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure></li></ul><p>除此之外, epitem 还有两个关键的字段:</p><ul><li><strong>rbn</strong>: 作为一个 rb_node 类型 (linux 内核中通用的红黑树节点结构体), 它是 epitem 的字段, 同时也是 eventpoll rbr 红黑树的节点, eventpoll 通过内核中的一个通用宏定义 <code>container_of</code>, 可依据局部字段的地址计算出整个结构体的地址, 从而定位到 epitem;</li><li><strong>rdllink</strong>: 作为一个 list_head 类型 (linux 内核中通用的双向链表节点结构体), 与 rbn 类似, 它是 epitem 的字段, 同时也是 eventpoll rdllist 双向链表的节点, eventpoll 同样可以基于 <code>container_of</code> 根据 rdllink 字段定位到 epitem;</li></ul><p>可以看到, epoll 的数据结构非常紧凑, eventpoll 的 rbr 和 rdllist 都是基于对 epitem 的引用构建的, 以最大限度地提升性能和节约内存:</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://github.com/zshell-zhang/static-content/raw/master/cs/linux/base/epoll%20%E7%9A%84%E5%8E%9F%E7%90%86%E8%AF%A6%E8%A7%A3/2.png" alt="struct eventpoll" title="">                </div>                <div class="image-caption">struct eventpoll</div>            </figure><h2 id="epoll-数据结构的选型"><a href="#epoll-数据结构的选型" class="headerlink" title="epoll 数据结构的选型"></a><strong>epoll 数据结构的选型</strong></h2><p>有很多文章说, eventpoll 使用红黑树管理 socket 事件订阅只是一种实现方式, 也可以换成哈希表等其他高效的内存结构, 个人认为这是一个值得讨论的问题;<br>首先我们先梳理一下 epoll 面临的业务场景:</p><ul><li><strong>大量、高并发的 socket 请求</strong>: epoll 通常被服务端用于处理高并发的海量用户请求, 对低延迟要求很高;</li><li><strong>低活跃的单个 socket 对象</strong>: 虽然有海量的请求连接到服务器, 但是对每一个具体的 socket 对象, 却活跃度很低, 在其生命周期的大部分时间内都不会产生 IO 事件;</li><li><strong>朝生夕死的 socket 对象</strong>: socket 对象通常不会存活很久, 用户请求结束后会关闭连接;</li></ul><p>总结一下业务特点就是:</p><ul><li>存在 socket 的大量新增;</li><li>存在 socket 的大量删除;</li><li>对 socket 事件的响应处理必须要快;</li><li>虽然 socket 总量很大, 但是同一时刻发生 IO 事件的 socket 占总量比例不高;</li></ul><p>再来看下不同数据结构对以上业务特点的支持能力:</p><ul><li><strong>红黑树</strong>:<ul><li>优点:<ol><li>依托于链式结构无需大块的连续内存, 不管当前树结构有多么庞大, 创建新节点的成本都极低, 只要能找到一块可用内存就行;</li><li>删除节点的成本也相对可控, 通过有限的旋转和重新着色即可维护树的平衡;</li></ol></li><li>缺点:<ol><li>查询时间复杂度是 O(logn), 当节点数量巨大, 查询性能会受一定影响;</li></ol></li></ul></li><li><strong>哈希表</strong>:<ul><li>优点:<ol><li>当内存足够大时, 查询 socket 的时间复杂度为 O(1);</li></ol></li><li>缺点:<ol><li>需要申请一大片连续的内存用作文件描述符到 socket 的映射, 不够灵活;</li><li>当 socket 数量不断增长, hash 冲突的概率越大, 查询复杂度会劣化 (最差可劣化到 O(n));</li><li>如果需要扩容哈希表以减少 key 冲突, 需要申请一片新的连续内存, 并将旧的数据复制过去, 开销很大;</li><li>当请求流量进入低谷期时, 已扩容的哈希表如果不缩容, 会造成内存浪费; 如果缩容, 则又涉及数据复制, 增大开销;</li></ol></li></ul></li></ul><p>&nbsp;<br>综合考虑: 我们需要的理想数据结构是:</p><ul><li>增删 socket 订阅事件、维护索引的成本要足够低;</li><li>socket 事件订阅查询效率要尽可能高, 且要保持查询性能稳定, 不能因 socket 数量规模的急剧变化而产生明显劣化;</li></ul><p>因此 linux 内核选择了查询性能十分稳定、对内存使用效率更高、使用方式更灵活的红黑树作为 eventpoll 索引 socket 订阅事件的数据结构;</p><h2 id="补充-内核常用结构和宏定义"><a href="#补充-内核常用结构和宏定义" class="headerlink" title="补充: 内核常用结构和宏定义"></a><strong>补充: 内核常用结构和宏定义</strong></h2><ul><li><p><strong>rb_node</strong>: 内核通用红黑树节点</p><figure class="highlight c"><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="class"><span class="keyword">struct</span> <span class="title">rb_node</span> &#123;</span></span><br><span class="line">    <span class="keyword">unsigned</span> <span class="keyword">long</span> __rb_parent_color;  <span class="comment">// 父节点指针和颜色标志</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">rb_node</span> *<span class="title">rb_right</span>;</span>         <span class="comment">// 右子节点指针</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">rb_node</span> *<span class="title">rb_left</span>;</span>          <span class="comment">// 左子节点指针</span></span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure></li><li><p><strong>rb_root</strong>: 内核通用红黑树根</p><figure class="highlight c"><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="class"><span class="keyword">struct</span> <span class="title">rb_root</span> &#123;</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">rb_node</span> *<span class="title">rb_node</span>;</span>  <span class="comment">// 指向红黑树的根节点</span></span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure></li><li><p><strong>list_head</strong>: 内核通用双向链表节点</p><figure class="highlight c"><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="class"><span class="keyword">struct</span> <span class="title">list_head</span> &#123;</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">list_head</span> *<span class="title">next</span>;</span>  <span class="comment">// 指向下一个节点</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">list_head</span> *<span class="title">prev</span>;</span>  <span class="comment">// 指向前一个节点</span></span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure></li></ul><p><strong>container_of</strong> 宏定义:</p><figure class="highlight c"><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="comment"> * @param ptr：指向结构体中某个字段的指针 (如 rb_node 的地址)</span></span><br><span class="line"><span class="comment"> * @param type：结构体的类型 (如 struct epitem)</span></span><br><span class="line"><span class="comment"> * @param member：字段的名称 (如 rbn)</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="meta">#<span class="meta-keyword">define</span> container_of(ptr, type, member) (&#123;                      \</span></span><br><span class="line">    const typeof(((type *)0)-&gt;member) *__mptr = (ptr);          \</span><br><span class="line">    (type *)((<span class="keyword">char</span> *)__mptr - offsetof(type, member)); &#125;)</span><br></pre></td></tr></table></figure><h1 id="epoll-的执行逻辑"><a href="#epoll-的执行逻辑" class="headerlink" title="epoll 的执行逻辑"></a><strong>epoll 的执行逻辑</strong></h1><p>一般来说, 用户使用 epoll 的逻辑是:</p><ul><li>调用 epoll_create, 创建对象;</li><li>调用 epoll_ctl, 订阅感兴趣的 socket 事件;</li><li>调用 epoll_wait, 等待订阅事件的发生;</li></ul><h2 id="epoll-create"><a href="#epoll-create" class="headerlink" title="epoll_create"></a><strong>epoll_create</strong></h2><figure class="highlight c"><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="meta">#<span class="meta-keyword">include</span> <span class="meta-string">&lt;sys/epoll.h&gt;</span></span></span><br><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">epoll_create</span><span class="params">(<span class="keyword">int</span> size)</span></span>;</span><br></pre></td></tr></table></figure><p>进程调用 epoll_create 方法, 内核会创建出一个 struct <strong>eventpoll</strong>, 注册到文件列表中, 并将其文件描述符 epfd 返回给进程;</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://github.com/zshell-zhang/static-content/raw/master/cs/linux/base/epoll%20%E7%9A%84%E5%8E%9F%E7%90%86%E8%AF%A6%E8%A7%A3/3.png" alt="epoll_create" title="">                </div>                <div class="image-caption">epoll_create</div>            </figure><h2 id="epoll-ctl"><a href="#epoll-ctl" class="headerlink" title="epoll_ctl"></a><strong>epoll_ctl</strong></h2><figure class="highlight c"><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="meta">#<span class="meta-keyword">include</span> <span class="meta-string">&lt;sys/epoll.h&gt;</span></span></span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * @param epfd  eventpoll 的文件描述符</span></span><br><span class="line"><span class="comment"> * @param op    EPOLL_CTL_ADD / EPOLL_CTL_MOD / EPOLL_CTL_DEL</span></span><br><span class="line"><span class="comment"> * @param fd    需要操作的文件 (比如 socket)</span></span><br><span class="line"><span class="comment"> * @param event 感兴趣的 socket 事件类型</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">epoll_ctl</span><span class="params">(<span class="keyword">int</span> epfd, <span class="keyword">int</span> op, <span class="keyword">int</span> fd, struct epoll_event *event)</span></span>;</span><br></pre></td></tr></table></figure><p>进程调用 epoll_ctl, 内核主要做了三件事:</p><ol><li>在内存中创建一个 struct <strong>epitem</strong>;</li><li>插入目标 epitem 的 rbn 节点至 eventpoll 的 rbr 红黑树, 从而建立对 socket 事件订阅的高效查询;</li><li>将 struct eventpoll 插入到目标 socket 等待队列, 用于 socket 接收到数据时回调以唤醒调用 epoll 线程;</li></ol><p><img src="https://github.com/zshell-zhang/static-content/raw/master/cs/linux/base/epoll%20%E7%9A%84%E5%8E%9F%E7%90%86%E8%AF%A6%E8%A7%A3/4.png" alt="epoll_ctl_process"></p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://github.com/zshell-zhang/static-content/raw/master/cs/linux/base/epoll%20%E7%9A%84%E5%8E%9F%E7%90%86%E8%AF%A6%E8%A7%A3/5.png" alt="insert_into_socket_wq" title="">                </div>                <div class="image-caption">insert_into_socket_wq</div>            </figure><h2 id="epoll-wait"><a href="#epoll-wait" class="headerlink" title="epoll_wait"></a><strong>epoll_wait</strong></h2><figure class="highlight c"><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="meta">#<span class="meta-keyword">include</span> <span class="meta-string">&lt;sys/epoll.h&gt;</span></span></span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * @param epfd      eventpoll 的文件描述符</span></span><br><span class="line"><span class="comment"> * @param events    epoll 事件数组</span></span><br><span class="line"><span class="comment"> * @param maxevents 指定events 数组的大小, 即可以处理的最大事件数</span></span><br><span class="line"><span class="comment"> * @param timeout   若无目标事件发生, timeout 时间后返回 0</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">epoll_wait</span><span class="params">(<span class="keyword">int</span> epfd, struct epoll_event *events, <span class="keyword">int</span> maxevents, <span class="keyword">int</span> timeout)</span></span>;</span><br></pre></td></tr></table></figure><p>关于 timeout 的设置:</p><ul><li><strong>timeout &gt; 0</strong>: 如果在指定的时间内没有事件发生, epoll_wait 将阻塞最多 timeout 毫秒;<br>如果在此期间有事件发生则立即返回, 且返回值为触发事件的数量;</li><li><strong>timeout &#x3D;&#x3D; 0</strong>: 不阻塞而是立即返回, 这种模式通常用于非阻塞检查, 适合需要快速轮询的场景;</li><li><strong>timeout &#x3D;&#x3D; -1</strong>: 无限期阻塞, 直到至少有一个事件发生, 这是最常用的模式, 适合不需要定时功能的场景;<br>绝大多数 I&#x2F;O 多路复用场景, 尤其是长时间运行的服务端程序都应该是这个模式;</li></ul><p>&nbsp;<br>进程调用 epoll_wait 的过程:</p><ul><li>如果没有注册的 socket 事件触发, 则进程被挂到 struct eventpoll 的等待队列, 然后休眠: <figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://github.com/zshell-zhang/static-content/raw/master/cs/linux/base/epoll%20%E7%9A%84%E5%8E%9F%E7%90%86%E8%AF%A6%E8%A7%A3/6.png" alt="epoll_wait_sleep" title="">                </div>                <div class="image-caption">epoll_wait_sleep</div>            </figure></li><li>如果有注册的 socket 事件触发, 中断程序会唤醒 eventpoll 等待队列中的进程, 同时将目标 socket 挂到 eventpoll 的 rdllist 上并返回给目标进程:<figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://github.com/zshell-zhang/static-content/raw/master/cs/linux/base/epoll%20%E7%9A%84%E5%8E%9F%E7%90%86%E8%AF%A6%E8%A7%A3/7.png" alt="" title="">                </div>                <div class="image-caption"></div>            </figure></li></ul><h3 id="整体事件-数据流"><a href="#整体事件-数据流" class="headerlink" title="整体事件&#x2F;数据流"></a><strong>整体事件&#x2F;数据流</strong></h3><ul><li>事件流图:<ul><li><font color="red">红色数字</font>: 调用 epoll_wait 并被挂起的执行顺序;</li><li><font color="blue">蓝色字母</font>: socket 事件触发唤醒进程并返回结果的执行顺序;<figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://github.com/zshell-zhang/static-content/raw/master/cs/linux/base/epoll%20%E7%9A%84%E5%8E%9F%E7%90%86%E8%AF%A6%E8%A7%A3/8.png" alt="epoll_wait events flow" title="">                </div>                <div class="image-caption">epoll_wait events flow</div>            </figure>&nbsp;</li></ul></li><li>数据流图:<ul><li>有 socket 事件触发, 从 rbr 中查找是否有注册的 socket 事件与之匹配;</li><li>匹配到了 socket 事件, 则将 rbr 上对应的 epitem 选出, 组装到 rdlist 链中返回给应用进程;<figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://github.com/zshell-zhang/static-content/raw/master/cs/linux/base/epoll%20%E7%9A%84%E5%8E%9F%E7%90%86%E8%AF%A6%E8%A7%A3/9.png" alt="epoll_wait data flow" title="">                </div>                <div class="image-caption">epoll_wait data flow</div>            </figure></li></ul></li></ul><h1 id="epoll-的触发模式"><a href="#epoll-的触发模式" class="headerlink" title="epoll 的触发模式"></a><strong>epoll 的触发模式</strong></h1><h2 id="水平触发-LT"><a href="#水平触发-LT" class="headerlink" title="水平触发 (LT)"></a><strong>水平触发 (LT)</strong></h2><p><strong>Level Trigger</strong>, 适合需要确保所有数据都被读取的场景:<br>当调用 epoll_wait 没有全部读完 socket 缓冲区, 下一次调用 epoll_wait 依然能够检测到 socket 就绪事件, 直到 socket 缓冲区数据被全部读完为止;</p><h2 id="边缘触发-ET"><a href="#边缘触发-ET" class="headerlink" title="边缘触发 (ET)"></a><strong>边缘触发 (ET)</strong></h2><p><strong>Edge Trigger</strong>, 适合高效快速响应的场景:<br>当 socket 接收数据后, epoll rdlist 只会插入一次 socket 就绪事件, epoll_wait 检测到 socket 读事件后, 必须一次性把 socket 缓冲区数据全部读完, 否则数据可能丢失;</p><h1 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a><strong>参考资料</strong></h1><ul><li><a href="https://zhuanlan.zhihu.com/p/64746509" target="_blank" rel="noopener">如果这篇文章说不清epoll的本质，那就过来掐死我吧 (3)</a></li><li><a href="https://mp.weixin.qq.com/s/zObydvTaBc0tKzx4G8B3tw" target="_blank" rel="noopener">Linux epoll完全图解，彻底搞懂epoll机制</a></li><li><a href="https://mp.weixin.qq.com/s/fE6xROVb5c4PrePOFrAbeA" target="_blank" rel="noopener">别再纠结 select 和 poll 了！epoll 才是 I&#x2F;O 复用的顶流担当</a></li><li><a href="https://mp.weixin.qq.com/s/_G9KRzIl7B7cPWKiMsZzOA" target="_blank" rel="noopener">深入理解 epoll</a></li></ul>]]></content>
    
    <summary type="html">
    
      &lt;blockquote&gt;
&lt;p&gt; 2002 年 linux 2.5.44 发布了 epoll, epoll 是 linux 实现高并发网络 I&amp;#x2F;O 处理的基础, 但凡存在网络 I&amp;#x2F;O 的系统或框架 (比如 nginx、netty、kafka、redis 等) 都离不开 epoll;&lt;br&gt;因此学习 epoll 的原理对于应用开发者理解网络框架的运行原理有着巨大的帮助;&lt;/p&gt;
&lt;/blockquote&gt;
    
    </summary>
    
      <category term="linux" scheme="http://zshell.cc/categories/linux/"/>
    
      <category term="base" scheme="http://zshell.cc/categories/linux/base/"/>
    
    
      <category term="linux::io" scheme="http://zshell.cc/tags/linux-io/"/>
    
      <category term="epoll" scheme="http://zshell.cc/tags/epoll/"/>
    
      <category term="C10K" scheme="http://zshell.cc/tags/C10K/"/>
    
  </entry>
  
  <entry>
    <title>Raft 学习笔记</title>
    <link href="http://zshell.cc/2025/03/19/consensus-raft--Raft%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"/>
    <id>http://zshell.cc/2025/03/19/consensus-raft--Raft学习笔记/</id>
    <published>2025-03-19T05:10:28.000Z</published>
    <updated>2025-07-28T13:35:01.506Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>Raft (Resilience And Fault Tolerance) 于 2014 年由斯坦福大学的 Diego Ongaro 和 John Ousterhout 首次提出, 旨在用一种更简单易懂的算法来代替艰涩的 Paxos 算法;<br>本文将详细记录一下 Raft 的原理及各种细节;</p></blockquote><a id="more"></a><h1 id="基本概念"><a href="#基本概念" class="headerlink" title="基本概念"></a><strong>基本概念</strong></h1><h2 id="角色及状态机"><a href="#角色及状态机" class="headerlink" title="角色及状态机"></a><strong>角色及状态机</strong></h2><p>raft 有三种角色:</p><ul><li><strong>Leader</strong>: 负责发起心跳，响应客户端，创建日志，同步日志;</li><li><strong>Candidate</strong>: Leader 选举过程中的临时角色，由 Follower 转化而来，发起投票参与竞选;</li><li><strong>Follower</strong>: 接受 Leader 的心跳和日志同步数据，投票给 Candidate;</li></ul><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://github.com/zshell-zhang/static-content/raw/master/cs/consensus/raft/Raft%20%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/1.png" alt="raft 的角色状态机" title="">                </div>                <div class="image-caption">raft 的角色状态机</div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://github.com/zshell-zhang/static-content/raw/master/cs/consensus/raft/Raft%20%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/7.png" alt="raft 的角色状态机" title="">                </div>                <div class="image-caption">raft 的角色状态机</div>            </figure><h2 id="超时机制"><a href="#超时机制" class="headerlink" title="超时机制"></a><strong>超时机制</strong></h2><p>raft 有两种超时时间:</p><ul><li><strong>HeartBeat Timeout</strong>: 心跳超时, 用于 Leader 定期向 Follower 发送心跳, 维持自己的 Leader 地位; 心跳的作用是告诉 Follower: <em>我仍然是 Leader, 不要发起选举</em>;<ul><li>实现方式: Leader 会定期 (默认 50 ms) 向所有 Follower 发送心跳请求 (<code>AppendEntries</code> RPC，即使没有日志条目也会发送);</li></ul></li><li><strong>Election Timeout</strong>: 选举超时, 集群中的 Follower 节点在 electionTimeout 时间内没有收到 Leader 的心跳信息就会晋升为 Candidate, 触发新一轮的选主流程 (<code>RequestVote</code> RPC), 选举超时时间为随机值 150 ~ 300 ms;<ul><li><strong>Minimum Election Timeout</strong>: 最小选举超时, 为了防止 Follower 可能会在 Leader 仍然正常工作的情况下误判 Leader 失效从而发起不必要的选举, 通常至少要设为为 HeartBeat Timeout 的 3 ~ 5 倍, 默认 150 ms;</li></ul></li></ul><h1 id="流程详解"><a href="#流程详解" class="headerlink" title="流程详解"></a><strong>流程详解</strong></h1><h2 id="状态同步"><a href="#状态同步" class="headerlink" title="状态同步"></a><strong>状态同步</strong></h2><p>Leader 发起日志同步的 AppendEntries RPC 请求数据结构如下:</p><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></pre></td><td class="code"><pre><span class="line"><span class="attr">prevLogIndex:</span> <span class="string">long</span>  <span class="comment"># 上一个 entry 的 index</span></span><br><span class="line"><span class="attr">prevLogTerm:</span> <span class="string">long</span>   <span class="comment"># 上一个 entry 的 leader 任期</span></span><br><span class="line"><span class="attr">entries:</span></span><br><span class="line"><span class="attr">    - index:</span> <span class="string">long</span></span><br><span class="line"><span class="attr">    - term:</span> <span class="string">long</span></span><br><span class="line"><span class="attr">    - cmd:</span> <span class="string">Command</span></span><br><span class="line"><span class="attr">leaderId:</span> <span class="string">long</span>      <span class="comment"># 用于 Follower 识别发送 RPC 的 Leader</span></span><br><span class="line"><span class="attr">leaderCommit:</span> <span class="string">long</span>  <span class="comment"># leader 已提交的 index, 用于通知 Follower 可以提交到哪个日志 index</span></span><br></pre></td></tr></table></figure><h3 id="日志复制"><a href="#日志复制" class="headerlink" title="日志复制"></a><strong>日志复制</strong></h3><ul><li>当 Leader 收到客户端请求后, 会生成一个 entry, 包含 <em>&lt; index, term, cmd &gt;</em>, 在将这个 entry 添加到自己的日志末尾后, 会向所有的节点发送 AppendEntries RPC 广播该 entry, 要求其他 Followers 复制这条 entry;</li><li>如果 Follower 接受该 entry, 则会将其添加到自己的日志后面, 同时返回给 Leader 已成功接受;</li><li>如果 Leader 收到了多数 Follower 的成功响应, Leader 会将这个 entry 应用到自己的状态机中, 随后将该 entry 置为 committed, 向客户端返回结果, 并将日志被 committed 的结果广播到整个集群, 以让各个 Follower 也将其应用到状态机;</li></ul><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://github.com/zshell-zhang/static-content/raw/master/cs/consensus/raft/Raft%20%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/2.gif" alt="" title="">                </div>                <div class="image-caption"></div>            </figure><h3 id="状态快照"><a href="#状态快照" class="headerlink" title="状态快照"></a><strong>状态快照</strong></h3><h2 id="领导者选举"><a href="#领导者选举" class="headerlink" title="领导者选举"></a><strong>领导者选举</strong></h2><p>Candidate 发起选举的 RequestVote RPC 请求数据结构如下:</p><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">term:</span> <span class="string">long</span></span><br><span class="line"><span class="attr">candidateId:</span> <span class="string">long</span></span><br><span class="line"><span class="attr">lastLogIndex:</span> <span class="string">long</span></span><br><span class="line"><span class="attr">lastLogTerm:</span> <span class="string">long</span></span><br></pre></td></tr></table></figure><h3 id="随机-election-timeout"><a href="#随机-election-timeout" class="headerlink" title="随机 election timeout"></a><strong>随机 election timeout</strong></h3><p>每一个 Follower 会计算一个随机的 electionTimeout, 为了减少多个 Follower 同时成为 Candidate 的可能性, 从而提升 raft 选举的效率:</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://github.com/zshell-zhang/static-content/raw/master/cs/consensus/raft/Raft%20%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/3.gif" alt="" title="">                </div>                <div class="image-caption"></div>            </figure><h3 id="Leader-宕机重选"><a href="#Leader-宕机重选" class="headerlink" title="Leader 宕机重选"></a><strong>Leader 宕机重选</strong></h3><p>如下图所示, 当原 Leader B 宕机, A 率先触发 election timeout 变成 Candidate, 将任期 term 自增为 2, 并获得了 A 和 C 的多数派投票, 成功变为新的 Leader;<br>即使后续 B 重新上线, 当其检测到 A 的 term 大于自己时, 也会自动退变为 Follower;</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://github.com/zshell-zhang/static-content/raw/master/cs/consensus/raft/Raft%20%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/4.gif" alt="" title="">                </div>                <div class="image-caption"></div>            </figure><h3 id="选举资格限制"><a href="#选举资格限制" class="headerlink" title="选举资格限制"></a><strong>选举资格限制</strong></h3><p>如 <a href="#%E9%A2%86%E5%AF%BC%E8%80%85%E9%80%89%E4%B8%BE">数据结构</a> 所示, 每个 Candidate 发送 RequestVote RPC 时都会携带上一个 entry 的 <em>&lt; term, index &gt;</em> 信息, 当 Follower 收到投票信息时, 会比较自己与 Candidate 谁的日志更完整:</p><ul><li>如果两个日志的 term 不同: 认为 term 大的更完整;</li><li>如果两个日志的 term 相同: 认为 index 大的更完整;</li></ul><p>如果 Follower 发现自己的日志更加完整, 则拒绝投票给该 Candidate;<br>一般来说, 我们不保证旧 Leader 尚未 commit 的日志能顺利保留到下一任期中, 但要<strong>保证已经 commit 的日志不能丢失, 必须在下一任期中得到保留</strong>; 可惜, 仅仅依靠上述选举资格的限制规则并不能保证这一承诺;</p><h3 id="Leader-交接的安全保证"><a href="#Leader-交接的安全保证" class="headerlink" title="Leader 交接的安全保证"></a><strong>Leader 交接的安全保证</strong></h3><p>在上述选举资格限制规则下, 如果我们允许新选举出的 Leader 直接提交上一任期的 entry, 就可能触发漏洞, 我们用一个例子来详细描述该问题;<br>假设一个 Raft 集群有 3 个节点:</p><ol><li><strong>term &#x3D; 1</strong>: R1 是 Leader, 产生 entry <em>&lt; term&#x3D;1, index&#x3D;1 &gt;</em>, 尚未开始复制, 接着 R1 崩溃了;</li><li><strong>term &#x3D; 2</strong>: R3 当选新 Leader (由 R2、R3 投票产生), 产生 entry <em>&lt; term&#x3D;2, index&#x3D;1 &gt;</em>, 尚未开始复制, 也崩溃了;</li><li><strong>term &#x3D; 3</strong>: R1 恢复, 并重新当选新 Leader, 假设此刻 R1 复制了其 term &#x3D; 1 任期的 entry <em>&lt; term&#x3D;1, index&#x3D;1 &gt;</em> 到 R2, 从而完成了多数派的复制, 于是直接提交该 entry, 然后 R1 再一次崩溃;</li><li><strong>term &#x3D; 4</strong>: R3 恢复, 并再次当选新 Leader (因为它 RequestVote 请求携带的 lastLogTerm &#x3D; 2, 根据选举限制规则, R1 和 R2 均会认为 R3 比自己更完整), 假设此刻 R3 广播了其 term &#x3D; 2 任期的 entry <em>&lt; term&#x3D;2, index&#x3D;1 &gt;</em>, 则 R1 和 R2 之前已经 commit 的 entry <em>&lt; term&#x3D;1, index&#x3D;1 &gt;</em> 将会被强制覆盖 (在 <a href="#%E6%97%A5%E5%BF%97%E5%BA%8F%E5%88%97%E7%9A%84%E5%BC%BA%E4%B8%80%E8%87%B4%E6%80%A7">后续小节</a> 将详细描述相关机制), 至此发生了已提交日志的丢失;</li></ol><p>&nbsp;<br>造成上述问题的根本原因是: 选举资格判定只能比较日志的新旧 (term 和 index), 而不关心日志是否已提交, 因此 Raft 必须禁止新 Leader 直接提交之前任期的日志;<br>为了解决该问题, <strong>Raft 要求新 Leader 必须先在当前任期开始时写入一条空命令日志 (no-op) 并提交它</strong>, 从而实现间接提交可能存在的之前任期的日志; 加上这个限制后, 我们再来推演一遍刚才的 case:</p><ol><li><strong>term &#x3D; 1</strong>: R1 是 Leader, 产生 entry <em>&lt; term&#x3D;1, index&#x3D;1 &gt;</em>, 尚未开始复制, 接着 R1 崩溃了;</li><li><strong>term &#x3D; 2</strong>: R3 当选新 Leader (由 R2、R3 投票产生), 产生 entry <em>&lt; term&#x3D;2, index&#x3D;1, cmd&#x3D;no-op &gt;</em>, 尚未开始复制, 也崩溃了;</li><li><strong>term &#x3D; 3</strong>: R1 恢复, 并重新当选新 Leader, R1 立即写入 entry <em>&lt; term&#x3D;3, index&#x3D;2, cmd&#x3D;no-op &gt;</em> 并发送到 R2, <a href="#%E6%97%A5%E5%BF%97%E5%BA%8F%E5%88%97%E7%9A%84%E5%BC%BA%E4%B8%80%E8%87%B4%E6%80%A7">Raft 的日志一致性机制</a> 可以保证 R2 最终能接收到 <em>&lt; term&#x3D;1, index&#x3D;1 &gt;</em> 和 <em>&lt; term&#x3D;3, index&#x3D;2 &gt;</em> 两条日志, R1 完成多数派的复制后提交该 entry, 然后 R1 再一次崩溃;</li><li><strong>term &#x3D; 4</strong>: R3 恢复, 并尝试再次竞选 Leader, 可是它 RequestVote 请求携带的 lastLogTerm &#x3D; 2, R1 和 R2 均认为其进度落后于自己, 不会投票给 R3, R3 将无法成为 Leader;</li></ol><p>&nbsp;<br>可见, 当任期开始立即写入一条 <strong>no-op</strong> 日志, 可以为新 Leader 建立起当前任期的权威, 向所有 Follower 宣告新王登基的事实; 在此之上 Raft 得以确保了已经 commit 的日志一定不会丢失: 当一个 Candidate 自身缺少已经在集群中 commit 的 entry 时, 一定不可能获得大多数选票, 从而不可能成为 Leader;</p><h3 id="Leader-选举失败"><a href="#Leader-选举失败" class="headerlink" title="Leader 选举失败"></a><strong>Leader 选举失败</strong></h3><p>当 raft 节点数为偶数时, 选举失败的可能性远大于奇数:</p><ul><li>节点数为偶数: 只要有两个 Candidate 同时发起投票, 就有可能造成无人获得多数派, 导致本轮选举失败;</li><li>节点数为奇数: 至少要有三个 Candidate 同时发起投票才可能造成无人获得多数派;</li></ul><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://github.com/zshell-zhang/static-content/raw/master/cs/consensus/raft/Raft%20%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/5.gif" alt="" title="">                </div>                <div class="image-caption"></div>            </figure><p>但是 raft 随机 election timeout 的机制保证了 leader 选举的活性, 最终一定能通过有限的轮次选出 Leader;</p><h2 id="强一致性保证"><a href="#强一致性保证" class="headerlink" title="强一致性保证"></a><strong>强一致性保证</strong></h2><h3 id="日志序列的强一致性"><a href="#日志序列的强一致性" class="headerlink" title="日志序列的强一致性"></a><strong>日志序列的强一致性</strong></h3><p>raft 的日志有两个基本性质:</p><blockquote><p><em>1. 在两份日志里的两个 entry: 如果拥有相同的 index 和 term, 那么它们一定有相同的 cmd (即存储的命令是相同的)</em>;</p></blockquote><ul><li>第一个基本性质由 Leader 对 index 的唯一性保证: 在一个 term 内, 给定一个 index 最多创建一个 entry;</li></ul><blockquote><p><em>2. 在两份日志里的两个 entry: 如果拥有相同的 index 和 term, 那么它们各自前一个 entry 也一定相同</em>;</p></blockquote><ul><li><p>第二个基本性质由 Leader AppendEntries RPC 请求的 <a href="#%E7%8A%B6%E6%80%81%E5%90%8C%E6%AD%A5">数据结构</a> 保证: Leader 会将新 entry 的前一条 entry 的 <em>&lt; prevLogIndex, prevLogTerm &gt;</em> 捎带进 AppendEntries RPC 中, 随后 Follower 的处理流程如下:</p><ul><li>检查 term: 如果 Leader 的任期小于自己的当前任期, 则拒绝该 RPC (并返回 Follower 自己的 term);</li><li>检查 prevLogIndex 和 prevLogTerm: 如果 <em>index - 1 !&#x3D; prevLogIndex || term - 1 !&#x3D; prevLogTerm</em>, 则拒绝该 RPC;</li><li>上两个检查通过: 将 entries 中的条目追加到自己的日志中;</li><li>根据 leaderCommit 决定提交自己日志的 index, 并应用到状态机;</li></ul></li><li><p>Leader 强制对 Follower 作日志回退的场景:</p><ul><li>Leader 给每一个 Follower 都维护了一个 <code>nextIndex</code>, 它表示 Leader 将要发送给该追随者的下一个 entry 的索引;</li><li>当一个 Leader 刚开始自己的任期时, 它会将每个 Follower 的 nextIndex 初始化为它的最新的 entry index + 1;</li><li>Leader 根据 Follower 的 nextIndex 决定发送给目标 Follower 的 AppendEntries 请求内容;</li><li>当 Follower 收到 Leader 的 AppendEntries 请求, 但检查 prevLogIndex 和 prevLogTerm 不通过而拒绝时:<ul><li>Leader 会将 nextIndex 递减然后重试 AppendEntries RPC;</li><li>最终 nextIndex 会达到一个 Leader 和 Follower 日志一致的地方, AppendEntries 将会返回成功, Follower 中冲突的 entries 会被移除, 并且补全所缺少的 Leader 的 entries;</li></ul></li></ul></li></ul><p><strong>注意:</strong> 被 Leader 强制回退的 Follower 日志一定是旧 Leader 尚未提交的 entry: 因为 Leader 选举限制规则已经强保证了被选出来的新 Leader 一定拥有完整的已提交日志, 如果某个 entry 在新 Leader 没有记录, 它一定是没提交的 entry;</p><h3 id="网络分区下的强一致性"><a href="#网络分区下的强一致性" class="headerlink" title="网络分区下的强一致性"></a><strong>网络分区下的强一致性</strong></h3><p>当 raft 节点数为奇数时, 可以保证当集群内出现两个分区时, 有大多数节点的分区可以正常运行 (另一个分区因无法形成多数派共识而不能提交日志);</p><ul><li>当原 Leader 就在大多数节点的分区:<ul><li>大多数节点的分区继续运行, Leader 任期也不用变;</li><li>另一个分区的节点将一直处于 Candidate 的状态, 无法选举出自己的 Leader;</li></ul></li><li>当原 Leader 不在大多数节点的分区:<ul><li>大多数节点的分区将选出自己的 Leader, 任期 + 1, 并且能正常响应请求, 提交日志;</li><li>另一个分区将无法提交日志;<ul><li>当网络分区消失后, 另一个分区的 Leader 将退变为 Follower, 分区内所有的节点将回退自己未提交的日志, 重新复制新 Leader 的日志;</li></ul></li></ul></li></ul><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://github.com/zshell-zhang/static-content/raw/master/cs/consensus/raft/Raft%20%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/6.gif" alt="" title="">                </div>                <div class="image-caption"></div>            </figure><h2 id="成员变更"><a href="#成员变更" class="headerlink" title="成员变更"></a><strong>成员变更</strong></h2><h1 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a><strong>参考资料</strong></h1><ul><li><a href="http://thesecretlivesofdata.com/raft/" target="_blank" rel="noopener">Raft: Understandable Distributed Consensus</a></li><li><a href="http://www.kailing.pub/raft/index.html" target="_blank" rel="noopener">Raft: 易理解的分布式共识 (译)</a></li><li><a href="https://raft.github.io/raft.pdf" target="_blank" rel="noopener">Raft: In Search of an Understandable Consensus Algorithm</a></li><li><a href="https://zhuanlan.zhihu.com/p/693936991" target="_blank" rel="noopener">Raft: 探寻一种可理解的共识算法 (译)</a></li><li><a href="https://mp.weixin.qq.com/s/DVEwMui71GdIwZpV9ZsJow" target="_blank" rel="noopener">为什么Raft算法是分布式系统的首选</a></li><li><a href="https://www.yuankang.top/2023/06/26/Protocol/Protocol-03-Raft/" target="_blank" rel="noopener">Protocol-03-Raft</a></li><li><a href="https://blog.csdn.net/weixin_44571270/article/details/126321867" target="_blank" rel="noopener">kubernetes 控制平面组件: etcd</a></li></ul>]]></content>
    
    <summary type="html">
    
      &lt;blockquote&gt;
&lt;p&gt;Raft (Resilience And Fault Tolerance) 于 2014 年由斯坦福大学的 Diego Ongaro 和 John Ousterhout 首次提出, 旨在用一种更简单易懂的算法来代替艰涩的 Paxos 算法;&lt;br&gt;本文将详细记录一下 Raft 的原理及各种细节;&lt;/p&gt;
&lt;/blockquote&gt;
    
    </summary>
    
      <category term="consensus" scheme="http://zshell.cc/categories/consensus/"/>
    
      <category term="raft" scheme="http://zshell.cc/categories/consensus/raft/"/>
    
    
      <category term="consensus" scheme="http://zshell.cc/tags/consensus/"/>
    
      <category term="raft" scheme="http://zshell.cc/tags/raft/"/>
    
  </entry>
  
  <entry>
    <title>G1 收集器学习</title>
    <link href="http://zshell.cc/2025/02/09/jvm-gc--G1%E6%94%B6%E9%9B%86%E5%99%A8%E5%AD%A6%E4%B9%A0/"/>
    <id>http://zshell.cc/2025/02/09/jvm-gc--G1收集器学习/</id>
    <published>2025-02-09T10:19:51.000Z</published>
    <updated>2025-07-28T13:35:01.512Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>在吸取了前辈 CMS 的经验教训之后, G1 的设计团队最终以一种脱胎换骨的姿态重磅推出了具有划时代意义的 <strong>Gabage First</strong> 收集器;<br>跳脱出 jvm 历史 GC 框架的局限性从 0 开始重新设计, G1 引入了 Region、Card、RSet 等多种创新性的数据结构, 极大地提升了年轻代及局部老年代收集的效率, 而这些设计思想也为后续的 ZGC、Shenandoah、Dragonwell Jade 等更先进的收集器所吸收采纳;</p></blockquote><a id="more"></a><h1 id="G1-的数据结构"><a href="#G1-的数据结构" class="headerlink" title="G1 的数据结构"></a><strong>G1 的数据结构</strong></h1><p>G1 为了实现高效的垃圾收集, 使用了很多复杂的数据结构以记录并控制内存的使用情况:</p><h2 id="Region"><a href="#Region" class="headerlink" title="Region"></a><strong>Region</strong></h2><p>G1 将堆划分为一系列大小不等的内存区域, 称为 <strong>Region</strong>, 每个 Region 大小为 1 ~ 32M, 且必须是 2 的幂次方:</p><ul><li>2 的幂次方大小可以确保堆区域的起始地址和结束地址对齐到特定的边界, 这有助于提高内存访问效率, 对齐后 CPU 可以更高效地访问内存, 减少缓存未命中和内存碎片;</li><li>G1 内部使用位运算来管理堆区域, 2 的幂次方大小使得这些运算更高效;</li></ul><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://github.com/zshell-zhang/static-content/raw/master/cs/jvm/gc/G1%20%E6%94%B6%E9%9B%86%E5%99%A8%E5%AD%A6%E4%B9%A0/1.png" alt="G1 Heap Region" title="">                </div>                <div class="image-caption">G1 Heap Region</div>            </figure><p>虽然 Eden、Survivor、Old 的概念和传统收集器类似, 但 G1 的内存管理是以 Region 为单位的, 这为更精细的内存回收提供了可能, 而不像传统 GC 只能以整个 Young 区 &#x2F; Old 区为单位进行回收, 粒度很粗, 回收时长不容易控制;</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://github.com/zshell-zhang/static-content/raw/master/cs/jvm/gc/G1%20%E6%94%B6%E9%9B%86%E5%99%A8%E5%AD%A6%E4%B9%A0/2.png" alt="G1 Heap Region" title="">                </div>                <div class="image-caption">G1 Heap Region</div>            </figure><h2 id="Card"><a href="#Card" class="headerlink" title="Card"></a><strong>Card</strong></h2><p>G1 对 Region 内部的空间做了更进一步的精细管理:</p><ul><li>G1 将 Region 按照 512 Byte 为单位划分成多个 <strong>卡 (Card)</strong>;</li><li>G1 定义了一个全局字节数组 <strong>卡表 (Card Table)</strong>, 其中的每个字节元素都唯一映射到 Region 中的一个 Card (稀疏索引);<br>卡表的元素有两种值:<ul><li><strong>0 (Clean Card)</strong>: 表示该 Card 内的对象对其他 Region Card 的引用关系已经全部更新到了 RSet;</li><li><strong>1 (Dirty Card)</strong>: 表示该 Card 内的对象对其他 Region 的引用关系发生了新的变更, 暂未同步到 RSet;</li></ul></li><li>因为 Region 是连续分配的, 所以当给定任何一个对象的内存地址, 都可以快速定位到其在卡表中的索引 <strong>Card Index</strong>: $\frac{addr(instance) - addr(heap_start)}{512B}$;</li></ul><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://github.com/zshell-zhang/static-content/raw/master/cs/jvm/gc/G1%20%E6%94%B6%E9%9B%86%E5%99%A8%E5%AD%A6%E4%B9%A0/3.png" alt="G1 Card Table" title="">                </div>                <div class="image-caption">G1 Card Table</div>            </figure><p>需要注意的是: 卡表本身不是目的, G1 引入卡表是高效维护 Remember Set 的一种手段, 这一点在下一小节将会详细介绍;</p><h2 id="Remember-Set"><a href="#Remember-Set" class="headerlink" title="Remember Set"></a><strong>Remember Set</strong></h2><p><strong>记忆集 (Remember Set)</strong> 简称 <strong>RSet</strong>, 是一个哈希表结构:</p><ul><li>key: 引用本 Region 的其他 Region 的起始地址;</li><li>value: 本 Region 中被 key 对应的 Region 引用的 Card Index 索引位置;</li></ul><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://github.com/zshell-zhang/static-content/raw/master/cs/jvm/gc/G1%20%E6%94%B6%E9%9B%86%E5%99%A8%E5%AD%A6%E4%B9%A0/4.png" alt="G1 RSet" title="">                </div>                <div class="image-caption">G1 RSet</div>            </figure><p>每一个 Region 都有自己的 RSet, 于是当给定了一个 Card, 我们通过其对应的 RSet 可以非常高效地查询到哪些 Region 引用了目标 Card 中的对象;<br>理论上 RSet 应该记录任何跨 Region 的引用关系, 但实际上 G1 做了特定的优化, 并不会全部记录; 我们将 Region 按照代际划分, 那么 RSet 的引用关系可以分为四类:</p><ol><li><em>年轻代 Region</em> 引用 <em>年轻代 Region</em></li><li><em>年轻代 Region</em> 引用 <em>年老代 Region</em></li><li><em>年老代 Region</em> 引用 <em>年轻代 Region</em></li><li><em>年老代 Region</em> 引用 <em>年老代 Region</em></li></ol><p>其中 G1 不会记录年轻代 Region 对其他 Region Card 的引用关系, 也就是不会记录上述第 1、2 种类型:</p><figure class="highlight c"><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">void</span> HeapRegionRemSet::add_reference(oop* from, HeapRegion* from_hr) &#123;</span><br><span class="line">  <span class="keyword">if</span> (from_hr-&gt;is_young()) &#123;</span><br><span class="line">    <span class="comment">// 如果引用来自年轻代，则不需要记录</span></span><br><span class="line">    <span class="keyword">return</span>;</span><br><span class="line">  &#125;</span><br><span class="line">  _other_regions.add_reference(from, from_hr);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>因为 RSet 的主要使用场景有两种 (后面章节会详细说明):</p><ol><li>在 Young GC 时, 跳过对 <em>被老年代 Region 引用的</em> 年轻代对象的扫描, 根据 RSet 直接执行快速标记, 从而降低整体标记阶段的开销;</li><li>在 Mixed GC 时, 跳过对 <em>被其他老年代 Region 引用的</em> 需要收集的老年代对象的扫描, 根据 RSet 直接执行快速标记, 从而降低整体标记阶段的开销;</li></ol><p>无论是什么类型的 GC, G1 本来就会扫描整个年轻代, 不存在需要通过 RSet 排除某些年轻代对象的场景, 额外记录年轻代 Region 对其他 Region Card 的引用关系反而会占用更多的内存;</p><h3 id="RSet-的三级结构"><a href="#RSet-的三级结构" class="headerlink" title="RSet 的三级结构"></a><strong>RSet 的三级结构</strong></h3><p>RSet 并非永远都会记录 Region &#x3D;&gt; Card Index 的关系, 因为 RSet 本质是一个哈希表, 当表中某个 slot 冲突严重, 就会发生退化:</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://github.com/zshell-zhang/static-content/raw/master/cs/jvm/gc/G1%20%E6%94%B6%E9%9B%86%E5%99%A8%E5%AD%A6%E4%B9%A0/5.png" alt="RSet level" title="">                </div>                <div class="image-caption">RSet level</div>            </figure><ul><li>无退化场景 (最细粒度): regionId &#x3D;&gt; cardIndex (数组);</li><li>无损压缩退化 (中等粒度): regionId &#x3D;&gt; cardIndex (bitmap, 仅压缩, 信息不会丢失);</li><li>有损压缩退化 (粗粒度):  regionId &#x3D;&gt; 是否引用了当前 Region 中的对象 (布尔值);</li></ul><h1 id="G1-的收集过程"><a href="#G1-的收集过程" class="headerlink" title="G1 的收集过程"></a><strong>G1 的收集过程</strong></h1><p>从宏观框架上看, G1 和 CMS 有着类似的思路, 都经历了几个经典过程: 初始标记、并发标记、最终标记、回收; 但在实现细节上, G1 相比 CMS 的差异就十分巨大了, 基于更精细的数据结构和引用关系追踪算法, G1 以空间换时间, 获得了比 CMS 更高的回收效率;<br>在展开 G1 的详细收集过程之前, 需要先讨论一下 G1 的写屏障实现思路:</p><h2 id="G1-的写屏障"><a href="#G1-的写屏障" class="headerlink" title="G1 的写屏障"></a><strong>G1 的写屏障</strong></h2><p>与 CMS 类似, 为了能与用户线程并发执行, G1 的收集也使用了三色标记法, 这意味着 G1 也必须要有自己的方案解决三色标记的漏标和多标问题;<br>与 CMS 使用 <strong>Incremental Update</strong> 聚焦于引用源头的写屏障不同, G1 垃圾收集器将写屏障拆分成了 <strong>写前屏障 (Pre-Write Barrier) 和写后屏障 (Post-Write Barrier)</strong>, 它们的用途和触发时机不同, 共同协作实现了比 CMS 更加高效的并发垃圾回收;</p><h3 id="写后屏障"><a href="#写后屏障" class="headerlink" title="写后屏障"></a><strong>写后屏障</strong></h3><p>G1 写后屏障的主要作用是维护 RSet (具体手段是通过标记 Card Table 再间接维护 RSet), 这是一个在整个 jvm 生命周期内都一直启用的特性;<br>当 jvm 执行到下面这个语句:</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">// 假设 oldObj 位于老年代 Region, newObj 位于年轻代 Region </span></span><br><span class="line">oldObj.field = newObj;</span><br></pre></td></tr></table></figure><p>以上语句会改变 oldObj.field 的引用值, jvm 需要及时维护 newObj 对象所属 Region 的 RSet: 将 oldObj 对象所在的 Region 引用到 newObj 对象所属的 Card Index;<br>为了尽量减少 java 语句的执行开销, G1 将该逻辑分成了两个部分:</p><ul><li><p><strong>第一部分</strong>: 引用变更发生后, 先做标记, 而不做真正处理 (相对轻量的操作):</p><ul><li>先将 oldObj 所在的 Card 标记为 Dirty Card:<br>  <img src="https://github.com/zshell-zhang/static-content/raw/master/cs/jvm/gc/G1%20%E6%94%B6%E9%9B%86%E5%99%A8%E5%AD%A6%E4%B9%A0/6.png" alt="dirty card"></li><li>再将该 Dirty Card 放入脏卡队列 (<strong>Dirty Card Queue, DCQ</strong>):<ul><li>优先加入当前线程的本地队列 (<strong>DirtyCardQueue</strong>);</li><li>当本地队列已满, 将本地队列的脏卡刷入全局队列 (<strong>DirtyCardQueueSet, DCQS</strong>), 并申请新的本地缓存;  <figure class="highlight c"><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">void</span> G1BarrierSet::write_ref_field_post(<span class="keyword">void</span>* field, oop new_val) &#123;</span><br><span class="line">    <span class="keyword">if</span> (new_val != <span class="literal">NULL</span>) &#123;</span><br><span class="line">        <span class="comment">// 标记脏卡</span></span><br><span class="line">        CardTable::card_mark(field);</span><br><span class="line">        <span class="comment">// 将脏卡加入队列</span></span><br><span class="line">        G1ThreadLocalData::dirty_card_queue(Thread::current()).enqueue(field);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">void</span> G1DirtyCardQueueSet::enqueue(<span class="keyword">void</span>* card) &#123;</span><br><span class="line">    <span class="comment">// 优先将脏卡加入本地队列</span></span><br><span class="line">    _queue-&gt;enqueue(card);</span><br><span class="line">    <span class="keyword">if</span> (_queue-&gt;is_full()) &#123;</span><br><span class="line">        <span class="comment">// 如果本地队列已满，刷入全局队列</span></span><br><span class="line">        flush();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></li></ul></li></ul></li><li><p><strong>第二部分</strong>: 使用指定线程消费 Dirty Card Queue (相对重量的操作):</p><ul><li>G1 会根据全局队列 DCQS 的容量变化自适应决定脏卡消费处理的线程数量:<br>  <img src="https://github.com/zshell-zhang/static-content/raw/master/cs/jvm/gc/G1%20%E6%94%B6%E9%9B%86%E5%99%A8%E5%AD%A6%E4%B9%A0/7.png" alt="DCQ 状态"><ul><li>当队列容量水位很低, 处于上图白色区域时: 没有线程会消费队列;</li><li>当队列容量进入上图绿色区域时: G1 中专门用于消费 DCQS 的线程 <strong>Refinement</strong> 被激活, 可以用 <code>-XX:G1ConcRefinementGreenZone=N</code> 指定该阶段的线程数量;</li><li>当队列容量进入上图黄色区域时: 说明产生 Dirty Card 的速度过快, G1 将激活更多的 Refinement 线程, 可以用 <code>-XX:G1ConcRefinementYellowZone=N</code> 指定该阶段的线程数量;</li><li>当队列容量进入上图红色区域时: 队列即将溢出, 除了 Refinement 之外, 用户线程 (Mutator Thread) 也会一起参与消费 DCQS 处理脏卡;<br>  注意: 为了保障引用变更写屏障的高效和轻量, 用户线程参与处理脏卡, 依旧会先将脏卡放入队列, 后续再异步从队列中取出脏卡处理;</li></ul></li><li>当 G1 Refinement 线程取出一个 Dirty Card 的处理逻辑:<ul><li>先反向定位出该 Card 所在的 Region;</li><li>再扫描该 Card 中的每一个对象, 找出每个对象所引用的其他对象, 根据被引用对象的地址定位出 Card Index;</li><li>根据上面两个步骤, 便可以建立 Region &#x3D;&gt; Card Index 的引用关系, 更新到 RSet;</li></ul></li></ul></li></ul><h3 id="写前屏障"><a href="#写前屏障" class="headerlink" title="写前屏障"></a><strong>写前屏障</strong></h3><p>G1 的写前屏障通过 <strong>SATB (Snapshot-At-The-Beginning)</strong> 实现, 这是一个只在 G1 开始执行垃圾收集才激活的特性; SATB 的发明者 <code>Taiichi Yuasa</code> 用两句话概括了该算法的核心思想:</p><blockquote><ol><li>Anything live at Initial Marking is considered live.</li><li>Anything allocated since Initial Marking is considered live.</li></ol></blockquote><p>也就是说, 当 G1 开始新的一轮收集时, 在开始时刻之前还活着的对象、以及开始时刻之后新创建的对象, G1 都认为是活着的, 不会被当前收集轮次给清理掉, 所以这个算法的名字很形象: <strong>初始时刻快照</strong>;<br>虽然名字叫快照, 但是 SATB 并不是真正遍历去建立一个关于所有存活对象的物理快照 (如果那样就太耗费时间了, 而且还得 stw 才能准确构建不重不漏), 它以一种轻量级的方式对堆中的对象图建立了一个逻辑快照:</p><ol><li>在一个 GC 周期中, 如果创建了一个新对象, 那么不管它后续是否死掉, 本次 GC 都认为它存活;</li><li>在一个 GC 周期中, 如果某个存活对象的引用从未被修改, 那么它最终一定能被三色标记法成功标记, 我们不需要额外干涉;</li><li>在一个 GC 周期中, 一个之前就死掉的对象一定不会被三色标记法给标记到, 我们也不需要额外干涉;</li><li>在一个 GC 周期中, 如果某个对象的引用被切断了 (比如从 obj.a &#x3D; b 变成了 obj.a &#x3D; null 或 obj.a &#x3D; c, 则 obj 对 b 的引用被切断), 证明其在切断前 (收集初始时刻) 一定是存活的 (否则将不可达, 也就不会有被切断的机会), 那么需要主动将该对象 (b) 及其成员逐一标记;</li></ol><p>&nbsp;<br>要实现以上逻辑, 主要有两个关键点:</p><ul><li><p>针对上文第 (1) 点: 我们需要一个高效的方案快速排除本轮 GC 中新建的对象, 将收集区域限定在旧对象里: 鉴于 G1 使用的是标记整理算法, 可以在给定空间里顺序分配对象, 那么在一个 region 中我们便可以引入几个指针:</p><ul><li><strong>BOTTOM</strong>: 指向当前 region 的起始地址;</li><li><strong>TOP</strong>: 指向当前 region 分配下一个新对象的起始地址;</li><li><strong>TAMS</strong>: Top-At-Mark-Start, 在标记开始时 <strong>TOP</strong> 指针指向的地址;<figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://github.com/zshell-zhang/static-content/raw/master/cs/jvm/gc/G1%20%E6%94%B6%E9%9B%86%E5%99%A8%E5%AD%A6%E4%B9%A0/8.png" alt="G1 TAMS" title="">                </div>                <div class="image-caption">G1 TAMS</div>            </figure>在新一轮 GC 启动, 进行初始标记时, 将 TOP 赋值给 TAMS, 那么在整个 GC 周期期间分配的新对象都落在 <strong>[TAMS, TOP)</strong> 之间, G1 直接对这个区间内的对象做隐式标记, 而只在 <strong>[BOTTOM, TAMS)</strong> 区间内应用三色标记, 就可以实现上文第 (1) 点;</li></ul></li><li><p>针对上文第 (4) 点: 首先我们可以明确地发现, 只有 <strong>[BOTTOM, TAMS)</strong> 区间内的对象会遇到相关问题; 与写后屏障类似, 为了尽量减少 java 语句的执行开销, 以及尽可能快速执行并发标记, G1 将 “主动标记被切断引用的对象” 这个逻辑分成了两部分:</p><ol><li><p>引用变更之前: 只将目标引用放入目标队列, 而不做真正处理 (相对轻量的操作):</p>  <figure class="highlight c"><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="function"><span class="keyword">void</span> <span class="title">pre_write_barrier</span><span class="params">(oop* field)</span> </span>&#123;</span><br><span class="line">    oop old_value = *field;</span><br><span class="line">    <span class="keyword">if</span> (old_value != null &amp;&amp; is_marking_active()) &#123;</span><br><span class="line">        <span class="comment">// 将旧引用加入 SATB 队列, 确保后续扫描</span></span><br><span class="line">        satb_queue.push(old_value);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></li><li><p>在最终标记 (Remark) 阶段消费 SATB 队列, 取出旧引用并对其及其成员做真正的重新标记 (相对重量的操作);</p></li></ol></li></ul><p>&nbsp;<br>总体来看, SATB 以极小的代价解决了漏标问题, 但也同时导致多标问题变得更严重: 因为某个对象的引用被切断了, 只要后续没有再被其他对象引用, 那这个对象确实就不可达了, 可当前轮次的 GC 并不会将其回收, 只能等下一次 GC 了;<br>其实如果将写前屏障与 RSet 结合起来, 就可以很大程度上缓解多标问题了: 只需在写前屏障第二阶段做 Remark 时:</p><ol><li>取出旧引用, 计算其所属 Region 和所属 Card Index;</li><li>遍历 Region 对应的 RSet, 判断是否存在目标 Card Index 的引用关系:<ul><li>如有: 说明该对象很可能后续又被其他对象引用 (并不能 100% 肯定, 因为 RSet 的记录粒度最多只到 Card, 而一个 Card 内可能有多个对象), 本轮可以对其做标记;</li><li>如没有: 说明该对象已不可达, 不应对其标记;</li></ul></li></ol><p>上述优化思路在某些资料中有描述, 但笔者并未找到 jvm 使用该方案的直接证据, 故将其作为额外补充记录下来;</p><h2 id="Young-GC-流程"><a href="#Young-GC-流程" class="headerlink" title="Young GC 流程"></a><strong>Young GC 流程</strong></h2><p>Young GC 主要针对年轻代 Region, 包括 Eden 和 Survivor; 当所有 Eden 区使用率达到了最大阈值 (默认 60%) 或者 G1 计算出来的回收时间接近用户设定的最大暂停时间时, 会触发 Young GC; <strong>G1 的 Young GC 是全程 Stop-The-World 的, 且一次 Young GC 会回收全部年轻代 Region;</strong><br>Young GC 的流程:</p><ol><li>初始标记 (Initial Mark): 从合并过 RSet 的 GC Roots 出发遍历标记所有存活的新生代对象<ul><li>部分线程直接从 GC Roots 开始扫描标记;<ul><li>注意: 需要过滤掉指向老年代对象的 GC Roots: 因为可以使用 RSet 更高效地获取被老年代引用的年轻代对象;</li></ul></li><li>部分线程排空 Dirty Card Queue: 把存量引用关系更新到 RSet, 再将 RSet 映射中记录的 Card Index 对应的对象作为 GC Roots 继续扫描标记;</li></ul></li><li>并发复制:<ul><li>对象年龄 &lt; 15: 复制对象到 Survivor Region, 记录对象年龄 + 1;</li><li>对象年龄 &#x3D; 15: 晋升复制到 Old Region;</li></ul></li><li>清理垃圾;</li></ol><h2 id="Mixed-GC-流程"><a href="#Mixed-GC-流程" class="headerlink" title="Mixed GC 流程"></a><strong>Mixed GC 流程</strong></h2><p>Mixed GC 是 G1 特有的收集类型, 它会收集所有的 Young Region + 收益高的若干个 Old Region;</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"><span class="comment"># 触发 全局并发标记 的已分配内存占整堆大小的比例, 简称 IHOP, 默认 45 (单位是 %)</span></span><br><span class="line"><span class="comment"># 关于已分配内存: jdk 8 之前是整体使用量; jdk 8 之后是老年代使用量</span></span><br><span class="line">-XX:InitiatingHeapOccupancyPercent=n</span><br></pre></td></tr></table></figure><p>当已分配的内存占内存总量比例超过了 IHOP 设置的阈值, 将会启动一轮 <strong>Global Concurrent Marking</strong>; 相比于 Young GC 的 stw, 全局并发标记因为涉及 Old Region, 其执行时间必然会远超 Young GC, 便不能再全程阻塞用户线程了, 对此 G1 使用了允许部分阶段与用户线程并发执行的策略, Global Concurrent Marking 的主要阶段和停顿类型如下:</p><table><thead><tr><th align="center">阶段</th><th align="center">停顿类型</th><th align="center">注释</th></tr></thead><tbody><tr><td align="center">Initial Mark</td><td align="center">stw</td><td align="center">复用了 Young GC 的 stw</td></tr><tr><td align="center">Concurrent Mark</td><td align="center">concurrent</td><td align="center"></td></tr><tr><td align="center">Remark (Final Mark)</td><td align="center">stw</td><td align="center"></td></tr><tr><td align="center">Cleanup</td><td align="center">stw</td><td align="center">并非真实清理, 而是统计标记为活的对象</td></tr></tbody></table><p>当已分配内存达到 IHOP 阈值时, 仅仅是触发了 Global Concurrent Marking, 而非真正的 Mixed GC, Mixed GC 的触发时机还受到以下几个条件的限制:</p><ol><li><code>-XX:G1HeapWastePercent</code>: 允许垃圾大小占内存的最大比例, 当低于此值时不会触发 Mixed GC;</li><li><code>-XX:G1MixedGCLiveThresholdPercent</code>: 老年代 Region 中的存活对象的占比, 只有在此参数之下才会被选入 Collection Set;</li><li><code>-XX:G1MixedGCCountTarget</code>: 一次 Global Concurrent Marking 之后, 最多执行 Mixed GC 的次数;</li></ol><p>我们可以设置一次 Mixed GC 需要回收的 Old Region 数量占比:</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"># 默认是 8: 意味着要在 8 次以内回收完所有的 Old Region</span></span><br><span class="line">-XX:MixedGCCountTarget</span><br></pre></td></tr></table></figure><h2 id="极端场景-Full-GC"><a href="#极端场景-Full-GC" class="headerlink" title="极端场景: Full GC"></a><strong>极端场景: Full GC</strong></h2><p>当 Mixed GC 实在无法跟上程序分配内存的速度, 导致老年代填满无法继续进行 Mixed GC, 就会使用 Serial Old GC (后来被优化为多线程, 但相比 Mixed GC 依旧很慢) 来收集整个堆:</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://github.com/zshell-zhang/static-content/raw/master/cs/jvm/gc/G1%20%E6%94%B6%E9%9B%86%E5%99%A8%E5%AD%A6%E4%B9%A0/9.png" alt="gc phase state transform" title="">                </div>                <div class="image-caption">gc phase state transform</div>            </figure><h1 id="G1-实战"><a href="#G1-实战" class="headerlink" title="G1 实战"></a><strong>G1 实战</strong></h1><h2 id="相关选项"><a href="#相关选项" class="headerlink" title="相关选项"></a><strong>相关选项</strong></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><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="comment"># 开启 g1 收集</span></span><br><span class="line">-XX:+UseG1GC</span><br><span class="line"></span><br><span class="line"><span class="comment"># 设置最大暂停时间 (默认 200 ms)</span></span><br><span class="line">-XX:MaxGCPauseMillis=n</span><br><span class="line"></span><br><span class="line"><span class="comment"># 指定Region的内存大小, n 必须是 2 的幂次方, 其取值范围是从 1M 到 32M</span></span><br><span class="line">-XX:G1HeapRegionSize=n</span><br><span class="line"></span><br><span class="line"><span class="comment"># 指定垃圾回收工作的线程数量, 8 核机器默认 5 个线程</span></span><br><span class="line">-XX:ParallelGCThreads=n</span><br><span class="line"><span class="comment"># 默认值跟随 -XX:ParallelGCThread</span></span><br><span class="line">-XX:ConcGCThreads=N</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><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 开启并发标记阶段的时间消耗记录诊断</span></span><br><span class="line">-XX:+UnlockDiagnosticVMOptions</span><br><span class="line">-XX:+G1SummarizeConcMark</span><br></pre></td></tr></table></figure><h2 id="GC-线程"><a href="#GC-线程" class="headerlink" title="GC 线程"></a><strong>GC 线程</strong></h2><ul><li><p>STW 线程:</p><ul><li>线程名: <code>GC Thread#{n}</code></li><li>作用:<ul><li>young gc;</li><li>mixed gc;</li></ul></li><li>设置方式:  <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">-XX:ParallelGCThreads=N</span><br></pre></td></tr></table></figure></li></ul></li><li><p>并发线程:</p><ul><li>线程名: <code>G1 Conc#{n}</code></li><li>作用:<ul><li>concurrent mark;</li><li>并发清理;</li><li>并发引用处理;</li></ul></li><li>设置方式:  <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">-XX:ConcGCThreads=N</span><br></pre></td></tr></table></figure></li></ul></li></ul><h1 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a><strong>参考资料</strong></h1><ul><li><a href="http://www.cnblogs.com/quan-qunar/p/5113762.html" target="_blank" rel="noopener">G1 Young GC时的to-space</a></li><li><a href="https://blog.gceasy.io/2017/12/06/gc-duration-vs-gc-pause-duration/" target="_blank" rel="noopener">GC DURATION VS GC PAUSE DURATION</a></li><li><a href="http://hllvm.group.iteye.com/group/topic/42352" target="_blank" rel="noopener">G1 垃圾回收的survivor 0区貌似永远是0</a></li><li><a href="http://www.oracle.com/technetwork/articles/java/g1gc-1984535.html" target="_blank" rel="noopener">Garbage First Garbage Collector Tuning</a></li><li><a href="https://segmentfault.com/a/1190000007815623" target="_blank" rel="noopener">译文-调整G1收集器窍门</a></li><li><a href="https://blog.csdn.net/weixin_43390345/article/details/108813288" target="_blank" rel="noopener">G1详解</a></li><li><a href="https://mp.weixin.qq.com/s/Ywj3XMws0IIK-kiUllN87Q" target="_blank" rel="noopener">极致八股文之JVM垃圾回收器G1&amp;ZGC详解</a></li><li><a href="https://www.cnblogs.com/juniorMa/articles/13883651.html" target="_blank" rel="noopener">G1的SATB</a></li><li><a href="https://mp.weixin.qq.com/s/f3hDYXyDb8jubquLoe0e1A" target="_blank" rel="noopener">图解G1垃圾回收器</a></li><li><a href="https://heapdump.cn/article/2604159" target="_blank" rel="noopener">G1源码从写屏障到Rset全面解析</a></li><li><a href="https://mp.weixin.qq.com/s/Mnmtgzcfx5CrA6l6sSrKLA" target="_blank" rel="noopener">聊聊JVM G1（Garbage First）垃圾收集器</a></li><li><a href="https://zhuanlan.zhihu.com/p/571355170" target="_blank" rel="noopener">SATB Desc</a></li><li><a href="https://www.cnblogs.com/mjunz/p/18666171" target="_blank" rel="noopener">G1原理—4.G1垃圾回收的过程之Young GC</a></li><li><a href="https://zhuanlan.zhihu.com/p/667780240" target="_blank" rel="noopener">InitiatingHeapOccupancyPercent介绍</a></li></ul>]]></content>
    
    <summary type="html">
    
      &lt;blockquote&gt;
&lt;p&gt;在吸取了前辈 CMS 的经验教训之后, G1 的设计团队最终以一种脱胎换骨的姿态重磅推出了具有划时代意义的 &lt;strong&gt;Gabage First&lt;/strong&gt; 收集器;&lt;br&gt;跳脱出 jvm 历史 GC 框架的局限性从 0 开始重新设计, G1 引入了 Region、Card、RSet 等多种创新性的数据结构, 极大地提升了年轻代及局部老年代收集的效率, 而这些设计思想也为后续的 ZGC、Shenandoah、Dragonwell Jade 等更先进的收集器所吸收采纳;&lt;/p&gt;
&lt;/blockquote&gt;
    
    </summary>
    
      <category term="jvm" scheme="http://zshell.cc/categories/jvm/"/>
    
      <category term="gc" scheme="http://zshell.cc/categories/jvm/gc/"/>
    
    
      <category term="jvm" scheme="http://zshell.cc/tags/jvm/"/>
    
      <category term="jvm::gc" scheme="http://zshell.cc/tags/jvm-gc/"/>
    
      <category term="低停顿收集器" scheme="http://zshell.cc/tags/%E4%BD%8E%E5%81%9C%E9%A1%BF%E6%94%B6%E9%9B%86%E5%99%A8/"/>
    
  </entry>
  
  <entry>
    <title>redis 哨兵模式</title>
    <link href="http://zshell.cc/2025/01/12/redis-cluster--redis%E5%93%A8%E5%85%B5%E6%A8%A1%E5%BC%8F/"/>
    <id>http://zshell.cc/2025/01/12/redis-cluster--redis哨兵模式/</id>
    <published>2025-01-12T02:45:05.000Z</published>
    <updated>2025-07-28T13:35:01.532Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>在 redis 2.8 之前, 基本可以认为 redis 不支持高可用部署模式, 因为仅靠 redis 主从同步机制, 当主服务器出现故障时, 并不支持自动将从服务器提升为主服务器, 而是需要运维人员手动介入; 很明显, 单独的 redis 主从模式不具备在生产环境部署的条件;<br>2013 年 9 发布的 redis 2.8 提供了一种哨兵模式, 终于在实质上解决了 redis 的高可用部署问题;</p></blockquote><a id="more"></a><h2 id="哨兵系统的构成"><a href="#哨兵系统的构成" class="headerlink" title="哨兵系统的构成"></a><strong>哨兵系统的构成</strong></h2><p>哨兵系统内由两部分组成 —— 哨兵节点和数据节点:</p><ul><li>哨兵节点: 哨兵节点是特殊的 redis 节点, 不存储数据, 只用于管控数据节点, 一个或多个哨兵节点组成了一个哨兵集群;</li><li>数据节点: redis 主节点和从节点都是数据节点, 其存储真正的数据;</li></ul><p>哨兵节点的作用:</p><ul><li>监控: 哨兵会不断地检查主节点和从节点是否运作正常;</li><li>自动故障转移: 当主节点不能正常工作时, 哨兵会开始自动故障转移操作, 它将失效主节点的其中一个从节点提升为新的主节点, 并让其它从节点改为复制新的主节点;</li></ul><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://github.com/zshell-zhang/static-content/raw/master/cs/redis/cluster/1.png" alt="redis 哨兵模式" title="">                </div>                <div class="image-caption">redis 哨兵模式</div>            </figure><h2 id="节点的发现"><a href="#节点的发现" class="headerlink" title="节点的发现"></a><strong>节点的发现</strong></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><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"># sentinel.conf</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># sentinel monitor master-name ip redis-port quorum</span></span><br><span class="line"><span class="comment"># master-name: 要监控的主节点名称</span></span><br><span class="line"><span class="comment"># ip: 主节点 (master) 的地址</span></span><br><span class="line"><span class="comment"># redis-port: 主数据库的端口</span></span><br><span class="line"><span class="comment"># quorum: 触发客观下线的最低通过票数</span></span><br><span class="line">sentinel monitor mymaster 127.0.0.1 6379 2</span><br></pre></td></tr></table></figure><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">&gt; src/redis-sentinel sentinel.conf</span><br></pre></td></tr></table></figure><p>哨兵启动后会与要监控的主节点建立两条连接:</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://github.com/zshell-zhang/static-content/raw/master/cs/redis/cluster/2.png" alt="哨兵与主节点的连接" title="">                </div>                <div class="image-caption">哨兵与主节点的连接</div>            </figure><p>其中 <code>_sentinel__:hello</code> 频道是一个特殊的发布&#x2F;订阅频道, 用于哨兵实例之间的通信;<br>&nbsp;<br>和数据节点建立连接完成后, 哨兵会使用 连接2 发送如下命令:</p><ul><li>每 10 秒钟哨兵会向数据节点发送 INFO 命令; 数据节点响应 INFO 命令并返回当前节点的相关信息 (运行 id、从节点信息等) 从而实现新节点的自动发现;</li><li>每 2 秒钟哨兵会向主节点的 <code>_sentinel_:hello</code> 频道发布自己的消息, 以与同样监控该节点的其他哨兵分享自己的信息;<figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://github.com/zshell-zhang/static-content/raw/master/cs/redis/cluster/3.png" alt="哨兵集群的发现" title="">                </div>                <div class="image-caption">哨兵集群的发现</div>            </figure>  发布内容为: &lt;哨兵的地址&gt;, &lt;哨兵的端口&gt;, &lt;哨兵的运行ID&gt;, &lt;哨兵的配置版本&gt;,<br>&lt;主数据库的名字&gt;, &lt;主数据库的地址&gt;, &lt;主数据库的端口&gt;, &lt;主数据库的配置版本&gt;;</li><li>每 1 秒钟哨兵会向主数据、从数据库和其他哨兵节点发送 PING 命令;</li></ul><h2 id="故障转移流程"><a href="#故障转移流程" class="headerlink" title="故障转移流程"></a><strong>故障转移流程</strong></h2><p>通过如下配置 PING 命令健康检查的超时时间:</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"># sentinel.conf</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 每隔 1s 发送一次 PING 命令 (因为配置值超过 1s), 当超过 6s 没回复, 认为节点主观下线</span></span><br><span class="line">sentinel down-after-milliseconds mymaster 6000</span><br><span class="line"><span class="comment"># 每隔 600 ms 发送一次 PING 命令, 当超过 600 ms 没回复, 认为节点主观下线</span></span><br><span class="line">sentinel down-after-milliseconds mymaster 600</span><br></pre></td></tr></table></figure><p>具体流程如下:</p><ol><li>每个哨兵节点每隔指定时间会向主节点、从节点及其它哨兵节点发送一次 PING 命令做一次心跳检测; 如果主节点在指定时间内不回复或者是回复一个错误消息, 那么这个哨兵就会认为这个主节点已主观下线 (Subjectively Down, SDOWN);</li><li>如果一个主节点被标记为 SDOWN, 则正在监视这个主节点的所有哨兵节点 要以每秒一次的频率确认该主节点的确进入了 SDOWN 状态;</li><li>当超过指定数量 (sentinel.conf 配置的 quorum) 的哨兵节点认为某主节点主观下线了, 这个节点就被认为已客观下线 (Objectively Down, ODOWN);</li><li>哨兵节点会通过 <strong>Raft 算法</strong> 共同选举出一个哨兵节点为 leader, 以负责处理主节点的故障转移和通知;</li><li>由 leader 哨兵节点负责真正的故障转移:<ul><li>过滤掉不健康的 (已下线的), 没有回复哨兵 PING 命令的从节点;</li><li>选择配置文件中从节点优先级配置最高的 (redis.conf 中的 replica-priority, 默认值为 100);</li><li>若优先级相同, 选择复制偏移量最大的从节点, 也就是复制最完整的从节点;</li><li>若复制偏移量依然相同, 则直接选择 runID 最大的从节点;</li><li>将选定的从节点提升为新的主节点, 让其它从节点指向新的主节点;</li><li>若原主节点恢复也变成从节点, 并指向新的主节点;</li><li>通知客户端主节点已经更换;</li></ul></li></ol><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://github.com/zshell-zhang/static-content/raw/master/cs/redis/cluster/4.png" alt="哨兵自动转移故障" title="">                </div>                <div class="image-caption">哨兵自动转移故障</div>            </figure><h2 id="基于哨兵自研-redis-集群"><a href="#基于哨兵自研-redis-集群" class="headerlink" title="基于哨兵自研 redis 集群"></a><strong>基于哨兵自研 redis 集群</strong></h2><p>redis 哨兵模式虽然解决了 redis 高可用部署的问题, 但是没有解决单点瓶颈横向扩展的问题, 这对于规模扩张十分迅速的互联网行业来说, 也是不可接受的;<br>redis 3.0 推出原生集群模式是在 2015 年, 而那在之前, 互联网公司想要实现一个 redis 集群就只能自己动手了, 其实我们完全可以在哨兵模式的基础上, 通过旁路数据控制、客户端能力增强, 扩展出可横向数据切分、可动态扩缩容的 redis 集群;<br>概括来说, 一个 redis 集群的核心能力只有几点:</p><ol><li>多副本、数据分区冗余能力;</li><li>副本故障自动转移能力;</li><li>横向切分, 多数据分片能力;</li><li>可扩缩容、数据迁移能力;</li></ol><p>其中, 第 1 点由 redis 原生的主从复制能力解决; 第 2 点由 redis 原生的哨兵模式解决; 要实现一个真正意义上的 redis 集群, 就只需要自主实现第 3、4 两点即可;<br>我们以<strong>去哪儿网 (qunar.com)</strong> 的自研 redis 集群为例介绍:</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://github.com/zshell-zhang/static-content/raw/master/cs/redis/cluster/5.png" alt="qunar redis 集群架构" title="">                </div>                <div class="image-caption">qunar redis 集群架构</div>            </figure><p>去哪儿网是国内较早在生产环境大规模部署 redis 实例的互联网公司之一, 由于历史早期 redis 不支持原生集群模式, 去哪儿网只能基于仅具备主从&#x2F;哨兵能力的 redis 版本自研了一套 redis 集群:</p><ol><li>实现一个旁路的配置中心, 存储 redis 集群内各个实例的信息, 包括: 实例的 ip&#x2F;port、slot 的 start&#x2F;end 偏移等;<figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://github.com/zshell-zhang/static-content/raw/master/cs/redis/cluster/6.png" alt="redis 集群配置信息" title="">                </div>                <div class="image-caption">redis 集群配置信息</div>            </figure></li><li>实现 redis 客户端的路由能力: 3.0 之前的 redis 版本不感知什么叫做分片, 没有路由的概念, 无法像 3.0 之后的版本针对不属于当前实例的 key 返回 MOVED 错误, 必须由客户端自己去感知分片实现路由, 需要能够从配置中心读取集群配置, 当查询具体的 key 时, 通过协商一致的 hash 算法计算 key 对应的 slot, 再继续定位到 slot 所在的实例;</li><li>实现配置的及时更新:<ul><li>增强哨兵集群的配置变更能力, 当主节点故障实施自动切换的同时 (也适用于扩缩容的场景):<ul><li>更新配置中心, 替换实例的 ip&#x2F;port;</li><li>将最新集群信息写入 zookeeper 对应节点, 方便客户端及时感知集群实例变更, 拉取最新配置;</li></ul></li><li>增强客户端的配置感知能力:<ul><li>定时从配置中心获取最新配置, 当配置变更, 重新建连;</li><li>监听目标集群对应的 zookeeper 节点, 当收到节点变更通知, 及时去配置中心获取最新配置, 重新建连;</li></ul></li></ul></li></ol><h3 id="扩缩容的实现"><a href="#扩缩容的实现" class="headerlink" title="扩缩容的实现"></a><strong>扩缩容的实现</strong></h3><p>动态路由感知不是 qunar 自研 redis 集群最复杂的地方, 扩缩容才是, 因为扩缩容涉及数据的 reshard &amp; migrate; 为了避免影响线上请求, qunar 采用了新建一个扩缩容后的独立目标集群, 然后将路由切过去的解决方案, 该方案的核心就是实现一个主从复制的双向代理中间件:</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://github.com/zshell-zhang/static-content/raw/master/cs/redis/cluster/7.png" alt="qunar redis 集群扩缩容" title="">                </div>                <div class="image-caption">qunar redis 集群扩缩容</div>            </figure><ol><li>作为源集群的从库, 同步源集群的数据;</li><li>作为目的集群的主库, 实现数据迁移到目的集群;</li><li>根据目的端集群的拓扑信息, 按照客户端分片算法, 将数据分发到目的端集群的各个分片;</li></ol><p>当源目两库完成全量同步, 进入增量同步的阶段, 进行数据校验, 若数据一致, 实行配置切换, 即完成了扩缩容操作;</p><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a><strong>参考资料</strong></h2><ul><li><a href="https://mp.weixin.qq.com/s/C1pzOKBKc0kUonarkevWgw" target="_blank" rel="noopener">Redis集群方案：主从、哨兵和Cluster</a></li><li><a href="https://www.cnblogs.com/trist-commot/p/17276718.html" target="_blank" rel="noopener">redis哨兵和集群</a></li><li><a href="https://baijiahao.baidu.com/s?id=1708996271967813518&wfr=spider" target="_blank" rel="noopener">深度好文：Redis哨兵集群</a></li><li><a href="https://blog.csdn.net/2401_89317354/article/details/145121952" target="_blank" rel="noopener">Redis哨兵模式详解</a></li><li><a href="https://baijiahao.baidu.com/s?id=1791590091054419881&wfr=spider" target="_blank" rel="noopener">去哪儿网 Redis 自动化运维体系</a></li></ul>]]></content>
    
    <summary type="html">
    
      &lt;blockquote&gt;
&lt;p&gt;在 redis 2.8 之前, 基本可以认为 redis 不支持高可用部署模式, 因为仅靠 redis 主从同步机制, 当主服务器出现故障时, 并不支持自动将从服务器提升为主服务器, 而是需要运维人员手动介入; 很明显, 单独的 redis 主从模式不具备在生产环境部署的条件;&lt;br&gt;2013 年 9 发布的 redis 2.8 提供了一种哨兵模式, 终于在实质上解决了 redis 的高可用部署问题;&lt;/p&gt;
&lt;/blockquote&gt;
    
    </summary>
    
      <category term="redis" scheme="http://zshell.cc/categories/redis/"/>
    
      <category term="cluster" scheme="http://zshell.cc/categories/redis/cluster/"/>
    
    
      <category term="raft" scheme="http://zshell.cc/tags/raft/"/>
    
      <category term="高可用" scheme="http://zshell.cc/tags/%E9%AB%98%E5%8F%AF%E7%94%A8/"/>
    
      <category term="redis::cluster" scheme="http://zshell.cc/tags/redis-cluster/"/>
    
  </entry>
  
  <entry>
    <title>java 虚拟线程学习</title>
    <link href="http://zshell.cc/2024/12/28/jdk--java%E8%99%9A%E6%8B%9F%E7%BA%BF%E7%A8%8B%E5%AD%A6%E4%B9%A0/"/>
    <id>http://zshell.cc/2024/12/28/jdk--java虚拟线程学习/</id>
    <published>2024-12-28T15:05:14.000Z</published>
    <updated>2025-07-28T13:35:01.510Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>虚拟线程作为 jdk 19+ 最重要的特性之一, 足以对所有 java 开发者构成巨大的吸引力; 我们即便无法很快在生产环境中升级到最新版本的 jdk, 但也应该足够重视, 尽早学习, 跟上时代节奏, 避免被职场淘汰;</p></blockquote><a id="more"></a><h2 id="虚拟线程相关的-jvm-配置参数"><a href="#虚拟线程相关的-jvm-配置参数" class="headerlink" title="虚拟线程相关的 jvm 配置参数"></a><strong>虚拟线程相关的 jvm 配置参数</strong></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"># ForkJoinPool 默认并发度 / 平台线程 (物理 OS 线程) 数</span></span><br><span class="line">-Djdk.virtualThreadScheduler.parallelism=128</span><br><span class="line"><span class="comment"># 调度器的最大并发度</span></span><br><span class="line">-Djdk.virtualThreadScheduler.maxPoolSize=2048</span><br><span class="line"><span class="comment"># 最少运行的线程数</span></span><br><span class="line">-Djdk.virtualThreadScheduler.minRunnable=32</span><br><span class="line"></span><br><span class="line"><span class="comment"># 追踪被 pin 住的虚拟线程: full 表示全量采样 / short 表示部分采样</span></span><br><span class="line">-Djdk.tracePinnedThreads=full/short</span><br></pre></td></tr></table></figure><h2 id="虚拟线程的使用"><a href="#虚拟线程的使用" class="headerlink" title="虚拟线程的使用"></a><strong>虚拟线程的使用</strong></h2><p>方式一: 直接构造 ThreadFactory 并调用其 newThread 方法</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="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">final</span> ThreadFactory <span class="title">newVirtualThreadFactory</span><span class="params">(<span class="keyword">final</span> String prefix)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">return</span> Thread.ofVirtual()</span><br><span class="line">        .name(prefix + <span class="string">"-"</span>)</span><br><span class="line">        .uncaughtExceptionHandler((t, e) -&gt; &#123;</span><br><span class="line">            log.error(<span class="string">"Uncaught exception, thread:[&#123;&#125;]"</span>, t, e);</span><br><span class="line">        &#125;)</span><br><span class="line">        .factory();</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">final</span> ThreadFactory vTreadFactory = newVirtualThreadFactory(<span class="string">"myThreadPrefix-"</span>);</span><br><span class="line"><span class="keyword">final</span> Thread vThread = vThreadFactory.newThread(() -&gt; &#123;......&#125;);</span><br><span class="line">vThread.start();</span><br><span class="line"><span class="keyword">if</span> (vThread.getState() == Thread.State.TERMINATED) &#123;</span><br><span class="line">    <span class="keyword">throw</span> <span class="keyword">new</span> IllegalStateException(<span class="string">"thread is terminated"</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>方式二: 使用 Executors 封装的工具方法</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="comment">// 默认无线程名, 为空字符串</span></span><br><span class="line">var executor = Executors.newVirtualThreadPerTaskExecutor();</span><br><span class="line"><span class="comment">// 指定线程名前缀及起始计数, 线程名: myThreadPrefix-$&#123;i&#125;</span></span><br><span class="line">var executor = Executors.newThreadPerTaskExecutor(Thread.ofVirtual().name(<span class="string">"myThreadPrefix-"</span>, <span class="number">1</span>).factory());</span><br></pre></td></tr></table></figure><h2 id="虚拟线程的火焰图"><a href="#虚拟线程的火焰图" class="headerlink" title="虚拟线程的火焰图"></a><strong>虚拟线程的火焰图</strong></h2><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://github.com/zshell-zhang/static-content/raw/master/cs/jdk/java%20%E8%99%9A%E6%8B%9F%E7%BA%BF%E7%A8%8B%E5%AD%A6%E4%B9%A0/1.png" alt="VT flame graph" title="">                </div>                <div class="image-caption">VT flame graph</div>            </figure><h2 id="使用虚拟线程的注意事项"><a href="#使用虚拟线程的注意事项" class="headerlink" title="使用虚拟线程的注意事项"></a><strong>使用虚拟线程的注意事项</strong></h2><h3 id="池化问题"><a href="#池化问题" class="headerlink" title="池化问题"></a><strong>池化问题</strong></h3><p>传统的 java 线程与 OS 线程是一一对应的, 创建代价较为昂贵, 在 Thread-Per-Request 编程模式下, 请求数远超 OS 所支持的最大线程数, 因此我们通常会使用线程池 (ThreadPoolExecutor) 来避免不必要的线程创建和销毁; 与此同时, ThreadPoolExecutor 内部存在较为复杂的状态管理, 并引入阻塞队列 workQueue, 在特定状态下引导 command 排队挂起&#x2F;唤醒, 这是管理池化资源不得不引入的复杂性;<br>而虚拟线程本质上只是一个轻量级的 java 对象, 创建和回收的成本很低, 在这种情况下如果使用复杂的 ThreadPoolExecutor 管理 virtual thread, 显得有些小题大做, 因此 java 官方不推荐对虚拟线程做池化缓存;<br>但虚拟线程作为 java 对象毕竟还是占用有限的内存资源的, 不可能无限创建, 对此 java 官方推荐直接使用 <code>java.util.concurrent.Semaphore</code> (信号量) 来限制 virtual thread 的总量; 同 ThreadPoolExecutor 中的 Worker 类似, Semaphore 也是基于 AbstractQueuedSynchronizer 的同步器, 但 Semaphore 只关心资源总量的限制, 没有 ThreadPoolExecutor 其他的复杂管理逻辑, 使用效率更高;</p><h3 id="FJP-虚拟线程回收速度"><a href="#FJP-虚拟线程回收速度" class="headerlink" title="FJP 虚拟线程回收速度"></a><strong>FJP 虚拟线程回收速度</strong></h3><p>在 jdk 21 下, 当使用 virtual thread 的应用流量卸掉后, ForkJoinPool 线程回缩得很慢, 每次 keepAlive 时间后只缩小 1 个 virtual thread, 官方回复 jdk 22 修复: <a href="https://bugs.openjdk.org/browse/JDK-8319662" target="_blank" rel="noopener">JDK-8319662</a>;</p><h3 id="pinning-问题"><a href="#pinning-问题" class="headerlink" title="pinning 问题"></a><strong>pinning 问题</strong></h3><p>受限于操作系统或 virtual thread 的实现, 有一些阻塞操作是无法被 carrier 线程卸载的, 即虚拟线程会被禁锢在了 carrier 线程上, 致使 carrier 线程无法调度其他虚拟线程, 我们应当尽可能避免这种情况的发生, 例如:</p><ul><li><p>synchronized 代码块内的阻塞操作, 比如 synchronized 里去调用 Thread#sleep;</p><ul><li>典型 bad case: guava cache 使用了 synchronized, 所以不能在使用了 guava cache 的业务应用里直接使用 virtual thread, 需要先替换为 caffeine cache;</li><li>java 官方对虚拟线程未来支持 synchronized 的态度: 不够重视, 预计很难在 jdk 23 之前支持;<br><img src="https://github.com/zshell-zhang/static-content/raw/master/cs/jdk/java%20%E8%99%9A%E6%8B%9F%E7%BA%BF%E7%A8%8B%E5%AD%A6%E4%B9%A0/2.png" alt="官方对虚拟线程支持 synchronized 的态度"></li></ul></li><li><p>Object#wait() 方法的执行;</p></li><li><p>native 方法的执行;</p></li><li><p>外部函数的执行;</p></li><li><p>大部分对文件系统的操作;</p></li><li><p>类加载 (致命弱点)</p><ul><li>dd  <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">-Djdk.virtualThreadScheduler.parallelism=1</span><br><span class="line">-Djdk.virtualThreadScheduler.maxPoolSize=1</span><br><span class="line">-Djdk.virtualThreadScheduler.minRunnable=1</span><br></pre></td></tr></table></figure></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><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">VTTest</span> </span>&#123;</span><br><span class="line">    <span class="keyword">static</span> <span class="keyword">final</span> CountDownLatch countDownLatch = <span class="keyword">new</span> CountDownLatch(<span class="number">1</span>);</span><br><span class="line">    <span class="keyword">static</span> <span class="keyword">final</span> ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> IOException </span>&#123;</span><br><span class="line">        executor.execute(() -&gt; &#123;</span><br><span class="line">            <span class="keyword">try</span> &#123;</span><br><span class="line">                Thread.sleep(<span class="number">1000</span>);</span><br><span class="line">            &#125; <span class="keyword">catch</span> (InterruptedException e) &#123;</span><br><span class="line">                <span class="keyword">throw</span> <span class="keyword">new</span> RuntimeException(e);</span><br><span class="line">            &#125;</span><br><span class="line">            countDownLatch.countDown();</span><br><span class="line">        &#125;);</span><br><span class="line"></span><br><span class="line">        executor.execute(InnerSleepClass::hello);</span><br><span class="line">        System.in.read();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="class"><span class="keyword">class</span> <span class="title">InnerSleepClass</span> </span>&#123;</span><br><span class="line">        <span class="keyword">static</span> &#123;</span><br><span class="line">            <span class="keyword">try</span> &#123;</span><br><span class="line">                countDownLatch.await();</span><br><span class="line">                System.out.println(<span class="string">"exit count down."</span>);</span><br><span class="line">            &#125; <span class="keyword">catch</span> (InterruptedException e) &#123;</span><br><span class="line">                <span class="keyword">throw</span> <span class="keyword">new</span> RuntimeException(e);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="function"><span class="keyword">static</span> <span class="keyword">void</span> <span class="title">hello</span><span class="params">()</span> </span>&#123;</span><br><span class="line">            System.out.println(<span class="string">"hello"</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></li></ul><p>当 OS 线程因为这些特殊的阻塞操作无法卸载虚拟线程时, 调度器 (ForkJoinPool) 会临时增大并发度, 可通过该选项来调整最大并发度:</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">-Djdk.virtualThreadScheduler.maxPoolSize=2048</span><br></pre></td></tr></table></figure><p>JDK 也提供了工具帮助我们发现这些无法被卸载的阻塞操作:</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"><span class="comment"># full 表示全量采样</span></span><br><span class="line"><span class="comment"># short 表示部分采样</span></span><br><span class="line">-Djdk.tracePinnedThreads=full/short</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><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">Thread[<span class="comment">#22,ForkJoinPool-1-worker-1,5,CarrierThreads]</span></span><br><span class="line">    java.base/java.lang.VirtualThread<span class="variable">$VThreadContinuation</span>.onPinned(VirtualThread.java:185)</span><br><span class="line">    java.base/jdk.internal.vm.Continuation.onPinned0(Continuation.java:393)</span><br><span class="line">    java.base/java.lang.VirtualThread.parkNanos(VirtualThread.java:634)</span><br><span class="line">    java.base/java.lang.VirtualThread.sleepNanos(VirtualThread.java:806)</span><br><span class="line">    java.base/java.lang.Thread.sleep(Thread.java:594)</span><br><span class="line">    org.example.Main.lambda<span class="variable">$executeTest</span><span class="variable">$0</span>(Main.java:35) &lt;== monitors:1</span><br><span class="line">    java.base/java.util.concurrent.Executors<span class="variable">$RunnableAdapter</span>.call(Executors.java:572)</span><br><span class="line">    java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317)</span><br><span class="line">    java.base/java.lang.VirtualThread.run(VirtualThread.java:314)</span><br></pre></td></tr></table></figure><h2 id="参考链接"><a href="#参考链接" class="headerlink" title="参考链接"></a><strong>参考链接</strong></h2><ul><li><a href="https://bugs.openjdk.org/browse/JDK-8319662" target="_blank" rel="noopener">ForkJoinPool trims worker threads too slowly</a></li></ul>]]></content>
    
    <summary type="html">
    
      &lt;blockquote&gt;
&lt;p&gt;虚拟线程作为 jdk 19+ 最重要的特性之一, 足以对所有 java 开发者构成巨大的吸引力; 我们即便无法很快在生产环境中升级到最新版本的 jdk, 但也应该足够重视, 尽早学习, 跟上时代节奏, 避免被职场淘汰;&lt;/p&gt;
&lt;/blockquote&gt;
    
    </summary>
    
      <category term="jdk" scheme="http://zshell.cc/categories/jdk/"/>
    
    
      <category term="jdk" scheme="http://zshell.cc/tags/jdk/"/>
    
      <category term="jdk21" scheme="http://zshell.cc/tags/jdk21/"/>
    
      <category term="协程" scheme="http://zshell.cc/tags/%E5%8D%8F%E7%A8%8B/"/>
    
      <category term="虚拟线程" scheme="http://zshell.cc/tags/%E8%99%9A%E6%8B%9F%E7%BA%BF%E7%A8%8B/"/>
    
  </entry>
  
  <entry>
    <title>美国的航空母舰</title>
    <link href="http://zshell.cc/2024/11/16/military-America--%E7%BE%8E%E5%9B%BD%E7%9A%84%E8%88%AA%E7%A9%BA%E6%AF%8D%E8%88%B0/"/>
    <id>http://zshell.cc/2024/11/16/military-America--美国的航空母舰/</id>
    <published>2024-11-16T04:01:51.000Z</published>
    <updated>2025-07-28T13:35:01.526Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>从一部小小的美国航母兴衰史, 可以影射出美国造船业的兴衰史, 甚至由此瞥见了整个美国工业的兴衰史;<br>从上世纪尼米茨级的一骑绝尘, 到如今福特级的拉胯与不堪用, 美国海军已经走到了一个历史极点, 军事霸权的基石摇摇欲坠, 待后续尼米茨的退役潮来临, 福特级将何去何从?</p></blockquote><a id="more"></a><h1 id="二战后的美国航母"><a href="#二战后的美国航母" class="headerlink" title="二战后的美国航母"></a><strong>二战后的美国航母</strong></h1><p>弦号前缀释义:</p><ul><li>CV (航空母舰): <code>Carrier Vessel</code></li><li>CVA (攻击型航空母舰): <code>Carrier Vessel Attack</code> (已弃用)</li><li>CVS (反潜型航空母舰): <code>Carrier Vessel Anti-Submarine</code> (已弃用)</li><li>CVN (核动力航空母舰): <code>Carrier Vessel Nuclear</code> (CVA &#x2F; CVS 并轨, 后续统一为 CVN)</li></ul><table><thead><tr><th align="center">型号</th><th align="center">标准&#x2F;满载排水量</th><th align="center">代表航母</th><th align="center">弦号</th><th align="center">服役时间</th><th align="center">退役时间</th><th align="center">备注</th></tr></thead><tbody><tr><td align="center">福莱斯特级</td><td align="center">6 万吨</td><td align="center">福莱斯特号<br>萨拉托加号<br>游骑兵号<br>独立号</td><td align="center">CVA-59<br>CVA-60<br>CVA-61<br>CVA-62</td><td align="center">1955 年<br>1956 年<br>1957 年<br>1959 年</td><td align="center">1993 年<br>1994 年<br>1993 年<br>1998 年</td><td align="center">二战结束后建造的第一代航母<br>第一代专门为供喷气式飞机使用而设计的航母</td></tr><tr><td align="center">小鹰级</td><td align="center">6 万吨 &#x2F; 8.2 万吨</td><td align="center">小鹰号<br>星座号<br>美国号<br>肯尼迪号</td><td align="center">CV-63<br>CV-64<br>CV-66<br>CV-67</td><td align="center">1961 年<br>1961 年<br>1965 年 <br>1968 年</td><td align="center">2009 年<br>2003 年<br>1996 年<br>2007 年</td><td align="center">最后一代常规动力航母</td></tr><tr><td align="center">企业级</td><td align="center">7.5 万吨 &#x2F; 9.4 万吨</td><td align="center">企业号</td><td align="center">CVN-65</td><td align="center">1961 年</td><td align="center">2013 年</td><td align="center">第一代核动力航母，使用 8 个核潜艇的反应堆作为动力源</td></tr><tr><td align="center">尼米茨级</td><td align="center">由于建造时间相隔较<br>长，不断有技术升级，<br>其满载排水量介于<br> 9.1 ~ 10.4 万吨之间</td><td align="center">尼米茨号<br>艾森豪威尔号<br>卡尔·文森号<br>罗斯福号<br>林肯号<br>华盛顿号<br>斯坦尼斯号<br>杜鲁门号<br>里根号<br>布什号</td><td align="center">CVN-68<br>CVN-69<br>CVN-70<br>CVN-71<br>CVN-72<br>CVN-73<br>CVN-74<br>CVN-75<br>CVN-76<br>CVN-77</td><td align="center">1975 年<br>1977 年<br>1982 年<br>1986 年<br>1989 年<br>1992 年<br>1995 年<br>1998 年<br>2003 年<br>2009 年</td><td align="center">暂无</td><td align="center">第二代核动力航母, 美国当下的海军主力, 现役 10 艘</td></tr><tr><td align="center">福特级</td><td align="center">满载 11.2 万吨</td><td align="center">福特号<br>肯尼迪号<br>企业号</td><td align="center">CVN-78<br>CVN-79<br>CVN-80</td><td align="center">2017 年 (仅下水, 未服役)<br>已开工<br>未开工</td><td align="center">暂无</td><td align="center">美国研发的下一代航母, 使用电磁弹射，综合电力系统，<br>计划在未来建造 10 艘</td></tr></tbody></table><h1 id="下一代航母-福特级"><a href="#下一代航母-福特级" class="headerlink" title="下一代航母: 福特级"></a><strong>下一代航母: 福特级</strong></h1><p>福特级航母的第一艘 (CVN-78) 于 2009 年开工建造, 2017 年下水;<br>CVN-78 的基本参数:</p><ul><li>长度为 333 米, 最大宽度为 78 米;</li><li>使用了 23 种全新的技术, 但步子迈得太大, 新技术不稳定导致故障不断, 无法形成有效战斗力;</li></ul><h2 id="电磁弹射问题"><a href="#电磁弹射问题" class="headerlink" title="电磁弹射问题"></a><strong>电磁弹射问题</strong></h2><p>福特级的电磁弹射使用高压交流系统, 优点是能量密度大, 功率高, 缺点是稳定性不够, 故障率高;<br>福特级的电磁弹射的设计平均故障周期指标是 4100 次弹射, 实际故障周期为:</p><ul><li>2017 ~ 2020 年: 181 次弹射;</li><li>2021 年: 380 次弹射;</li><li>2022 年之后: 400+ 次弹射;</li></ul><p>虽然每年都在优化与提升, 但还是远远达不到军方标准;</p><h3 id="储能-转换系统"><a href="#储能-转换系统" class="headerlink" title="储能&#x2F;转换系统"></a><strong>储能&#x2F;转换系统</strong></h3><p>福特级航母的每套弹射器都共用 3 套总容量 720 兆焦的飞轮储能系统, 整个供电与储能系统用的是中压交流供电系统; 飞轮储能可以发出交流电, 但结构太复杂, 机械能转换能量损耗大;<br>福特级电磁弹射系统的部分问题也来自于这个中压交流系统, 问题集中在储能和转换系统上, 而四套弹射器共用整个储能和转换系统, 导致无法实现单台电磁弹射器的电气隔离, 这就是福特级电磁弹射器出故障时必须四套同时停工才能维修的原因;<br>美军打算在后续的电弹系统中将储能子系统换成超级电容; 虽然超级电容储能比较简单, 但是电容储存的是直流电, 放电时需要转换交流电, 这样整个体系中就需要一道 AC-DC-AC (充电时交流转直流, 放电时直流转交流) 的转换, 依旧麻烦无比; 因为基础技术选型的失误, 现在福特级已经处于一种进退失据的境地, 尤其在我国的福建号进展迅速的前提下, 重新回炉设计时间完全来不及, 在现有基础上硬着头皮改也没法从根本上解决问题;<br>与之对应的是, 我国福建号的技术路线是马伟明院士提出的中压直流系统, 全链路的直流体系完美避免了交流系统的复杂性;</p><h3 id="弹射器的数量"><a href="#弹射器的数量" class="headerlink" title="弹射器的数量"></a><strong>弹射器的数量</strong></h3><p>福特级沿用了前型尼米茨级的 4 条弹射器;<br>尼米茨级使用四条弹射器的原因是蒸汽弹射器不能持续弹射, 每次弹射存在 2 吨的蒸汽损耗, 以美军航母通用的 C13B2 型蒸汽弹射器为例, 其最多能在 30 分钟内进行 18 次快速弹射, 此后弹射器的效率会降低, 每隔 5 分钟才能弹射一架飞机, 期间需要补充蒸汽; 所以一般两个一组, 一条弹射另一条储压, 交替弹射;</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://github.com/zshell-zhang/static-content/raw/master/military/America/%E7%BE%8E%E5%9B%BD%E8%88%AA%E7%A9%BA%E6%AF%8D%E8%88%B0/1.png" alt="中美航母弹射器布局对比" title="">                </div>                <div class="image-caption">中美航母弹射器布局对比</div>            </figure><p>福特级的电磁弹射系统在 4 条弹射器之间共用三个储能飞轮, 各套弹射器之间没有实现电气隔离, 并不适用于蒸汽弹射交替弹射的场景, 达不到一条弹射另一条储能的效果; 而左舷多出来的第四条弹射器过多侵入了着舰区, 也无法实现舰载机着舰时同时使用第四条弹射器;</p><h3 id="体积超标"><a href="#体积超标" class="headerlink" title="体积超标"></a><strong>体积超标</strong></h3><p>因为散热控制以及电磁屏蔽, 因为弹射过程中多个结构都需要快速散热, 比如电机工作时的高温以及导轨加速产生的巨大热量, 还有电机工作时极端电磁场, 相当于一枚小型 EMP 爆炸, 需要将其屏蔽在甲板下, 避免其影响战斗机航电系统;<br>福特号航母在增加了这些散热结构和电磁屏蔽系统以及储能与转换与控制系统后, 电磁弹射系统的体积居然达到了 1061.4 立方米, 和蒸汽弹射的 1100 立方米相差无几, 而重量则超过了 630 吨, 已经高于蒸汽弹射;</p><h2 id="阻拦索问题"><a href="#阻拦索问题" class="headerlink" title="阻拦索问题"></a><strong>阻拦索问题</strong></h2><p>福特级使用了先进的电磁阻拦索 (AAG), 以液压涡轮来提供阻拦力, 通过软件实时分析拉力传感器反馈的力度数据, 智能控制电动机带动拦阻索, 实现阻拦过程的负载均衡, 从而解决以往通过巨型液压缸、活塞和加压空气等机械结构阻拦 “力度过猛” 导致对舰载机结构和拦截钢缆产生显著损耗的问题;<br>福特级电磁阻拦索的设计评估故障周期指标是 16500 次阻拦, 实际故障周期为 41 次;</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://github.com/zshell-zhang/static-content/raw/master/military/America/%E7%BE%8E%E5%9B%BD%E8%88%AA%E7%A9%BA%E6%AF%8D%E8%88%B0/2.png" alt="电磁阻拦索内部结构" title="">                </div>                <div class="image-caption">电磁阻拦索内部结构</div>            </figure><h2 id="升降机问题"><a href="#升降机问题" class="headerlink" title="升降机问题"></a><strong>升降机问题</strong></h2><p>福特级的 11 个弹药升降机使用了先进的电磁升降机, 而是依靠光滑的接触面和电磁力, 让活动台面吸附到提升上下轨道的边框上, 进行弹药的运输, 不再需要电缆和钢索等过多的笨重设备;<br>福特级电磁升降机的设计平均故障周期指标是 932 小时, 实际故障周期为 218 小时, 且 11 个升降机只有 8 个验收通过;</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://github.com/zshell-zhang/static-content/raw/master/military/America/%E7%BE%8E%E5%9B%BD%E8%88%AA%E7%A9%BA%E6%AF%8D%E8%88%B0/3.png" alt="电磁升降机内部结构" title="">                </div>                <div class="image-caption">电磁升降机内部结构</div>            </figure><h1 id="参考链接"><a href="#参考链接" class="headerlink" title="参考链接"></a><strong>参考链接</strong></h1><ul><li><a href="https://baijiahao.baidu.com/s?id=1746845070365131435&wfr=spider" target="_blank" rel="noopener">福特号首次战斗部署，暴露美国制造的两大顽疾，给福建舰提了个醒</a></li><li><a href="https://www.huangpucn.com/info/228740.html" target="_blank" rel="noopener">福特级航母有哪些革命性的技术应用，为什么能称「世界最强」</a></li><li><a href="https://baijiahao.baidu.com/s?id=1736489034591347706&wfr=spider" target="_blank" rel="noopener">除了电磁弹射器之外，福建舰还会装备电磁拦阻系统吗</a></li><li><a href="https://baijiahao.baidu.com/s?id=1618915184722411487&wfr=spider" target="_blank" rel="noopener">美国最新福特号航母再曝漏洞：AAG先进拦阻系统质量不过关</a></li><li><a href="https://baijiahao.baidu.com/s?id=1668201646604584320&wfr=spider" target="_blank" rel="noopener">到底是3台弹射器还是4台弹射器？几个升降机</a></li><li><a href="https://baijiahao.baidu.com/s?id=1736059081816743111&wfr=spider" target="_blank" rel="noopener">003航母：电磁弹射起飞一架飞机到底要消耗多少电能</a></li></ul>]]></content>
    
    <summary type="html">
    
      &lt;blockquote&gt;
&lt;p&gt;从一部小小的美国航母兴衰史, 可以影射出美国造船业的兴衰史, 甚至由此瞥见了整个美国工业的兴衰史;&lt;br&gt;从上世纪尼米茨级的一骑绝尘, 到如今福特级的拉胯与不堪用, 美国海军已经走到了一个历史极点, 军事霸权的基石摇摇欲坠, 待后续尼米茨的退役潮来临, 福特级将何去何从?&lt;/p&gt;
&lt;/blockquote&gt;
    
    </summary>
    
      <category term="military" scheme="http://zshell.cc/categories/military/"/>
    
      <category term="America" scheme="http://zshell.cc/categories/military/America/"/>
    
    
      <category term="海军" scheme="http://zshell.cc/tags/%E6%B5%B7%E5%86%9B/"/>
    
      <category term="航空母舰" scheme="http://zshell.cc/tags/%E8%88%AA%E7%A9%BA%E6%AF%8D%E8%88%B0/"/>
    
  </entry>
  
  <entry>
    <title>LSM-Tree 的不可能三角</title>
    <link href="http://zshell.cc/2024/10/06/nosql-lsm--LSM-Tree%E7%9A%84%E4%B8%8D%E5%8F%AF%E8%83%BD%E4%B8%89%E8%A7%92/"/>
    <id>http://zshell.cc/2024/10/06/nosql-lsm--LSM-Tree的不可能三角/</id>
    <published>2024-10-06T15:23:04.000Z</published>
    <updated>2025-07-28T13:35:01.529Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>在 LSM 树 (Log-Structured Merge-Tree) 的设计中，读放大 (Read Amplification)、写放大 (Write Amplification) 和空间放大 (Space Amplification) 形成了一个类似 <strong>不可能三角</strong> 的权衡关系, 优化其中任意两个通常会恶化第三个; 这一现象类似于存储系统中的 <strong>RUM Conjecture</strong> (Read、Update、Memory overhead 不可能同时最优);</p></blockquote><a id="more"></a><h2 id="LSM-Tree-的放大问题"><a href="#LSM-Tree-的放大问题" class="headerlink" title="LSM-Tree 的放大问题"></a><strong>LSM-Tree 的放大问题</strong></h2><h3 id="读放大"><a href="#读放大" class="headerlink" title="读放大"></a><strong>读放大</strong></h3><p>完成一次查询需要读取的数据量比实际所需数据多;<br><strong>原因</strong>：</p><ul><li>数据可能分布在多个SSTable中（MemTable + 多级SSTable），查询时需要检查多个文件。</li><li>即使使用Bloom Filter减少无效IO，范围查询仍可能需要合并多个SSTable的结果。</li></ul><p><strong>优化方法</strong>：</p><ul><li>增加Bloom Filter的精度，减少无效磁盘访问。</li><li>使用分层Compaction（如Leveled Compaction），减少每层SSTable的重叠范围。</li></ul><h3 id="写放大"><a href="#写放大" class="headerlink" title="写放大"></a><strong>写放大</strong></h3><p>实际写入磁盘的数据量比用户写入的数据量大;<br><strong>原因</strong>：LSM树通过后台 Compaction 合并 SSTable, 导致数据被反复重写 (例如: LevelDB 的 Leveled Compaction 可能产生 10 倍以上的写放大);</p><p><strong>优化方法</strong>：</p><ul><li>采用 Tiered Compaction (类似 Cassandra 的 STCS), 减少 Compaction 频率;</li><li>使用增量压缩（如ZSTD）减少写入数据量。</li></ul><h3 id="空间放大"><a href="#空间放大" class="headerlink" title="空间放大"></a><strong>空间放大</strong></h3><p>存储的冗余数据比实际有效数据多;<br><strong>原因</strong>：</p><ul><li>由于Compaction不是实时进行，同一份数据可能在多个SSTable中存在（未合并前）。</li><li>删除操作只是标记（Tombstone），直到Compaction才真正清理。</li></ul><p><strong>优化方法</strong>：</p><ul><li>更激进的Compaction策略（如Size-Tiered → Leveled）。</li><li>尽早合并Tombstone，减少无效数据占用空间。</li></ul><h2 id="LSM-Tree-的不可能三角"><a href="#LSM-Tree-的不可能三角" class="headerlink" title="LSM-Tree 的不可能三角"></a><strong>LSM-Tree 的不可能三角</strong></h2><table><thead><tr><th><strong>优化目标</strong></th><th><strong>对其它放大的影响</strong></th></tr></thead><tbody><tr><td><strong>降低读放大</strong>（如增加Bloom Filter、减少SSTable层级）</td><td>➔ 可能增加写放大（更频繁Compaction）或空间放大（更多未合并数据）</td></tr><tr><td><strong>降低写放大</strong>（如减少Compaction频率）</td><td>➔ 可能增加读放大（更多SSTable需要检查）和空间放大（数据冗余更多）</td></tr><tr><td><strong>降低空间放大</strong>（如更激进的Compaction）</td><td>➔ 可能增加写放大（更多数据重写）和读放大（Compaction占用IO带宽）</td></tr></tbody></table><h3 id="现实中的权衡案例"><a href="#现实中的权衡案例" class="headerlink" title="现实中的权衡案例"></a><strong>现实中的权衡案例</strong></h3><ul><li>RocksDB（Leveled Compaction）：优化读放大和空间放大，但写放大较高（适合读多场景）。</li><li>Cassandra（Size-Tiered Compaction, STCS）：优化写放大，但读放大和空间放大较高（适合写多场景）。</li><li>WiscKey（KV分离）：优化写放大和空间放大，但读放大增加（需额外查询Value日志）。</li></ul><h2 id="如何根据业务场景选择策略"><a href="#如何根据业务场景选择策略" class="headerlink" title="如何根据业务场景选择策略"></a><strong>如何根据业务场景选择策略</strong></h2><table><thead><tr><th><strong>业务需求</strong></th><th><strong>推荐优化方向</strong></th><th><strong>典型Compaction策略</strong></th></tr></thead><tbody><tr><td><strong>低延迟读</strong>（OLTP、索引）</td><td>优化读放大</td><td>Leveled Compaction</td></tr><tr><td><strong>高吞吐写入</strong>（日志、时序数据）</td><td>优化写放大</td><td>Tiered Compaction (STCS)</td></tr><tr><td><strong>存储成本敏感</strong>（冷数据归档）</td><td>优化空间放大</td><td>定期Major Compaction + 压缩</td></tr></tbody></table><h2 id="优化"><a href="#优化" class="headerlink" title="优化"></a><strong>优化</strong></h2><p>LSM树的 “三个放大” 问题确实是一个<strong>不可能三角</strong>，但通过合理的Compaction策略、索引优化（Bloom Filter、SSTable索引）和存储分层（Hot&#x2F;Cold Tiering），可以在特定业务场景下取得较好的平衡。现代LSM引擎（如RocksDB）通过动态调整 Compaction 策略、使用 KV 分离（如WiscKey）等技术，正在不断逼近这个三角的更优解;</p>]]></content>
    
    <summary type="html">
    
      &lt;blockquote&gt;
&lt;p&gt;在 LSM 树 (Log-Structured Merge-Tree) 的设计中，读放大 (Read Amplification)、写放大 (Write Amplification) 和空间放大 (Space Amplification) 形成了一个类似 &lt;strong&gt;不可能三角&lt;/strong&gt; 的权衡关系, 优化其中任意两个通常会恶化第三个; 这一现象类似于存储系统中的 &lt;strong&gt;RUM Conjecture&lt;/strong&gt; (Read、Update、Memory overhead 不可能同时最优);&lt;/p&gt;
&lt;/blockquote&gt;
    
    </summary>
    
      <category term="nosql" scheme="http://zshell.cc/categories/nosql/"/>
    
      <category term="lsm" scheme="http://zshell.cc/categories/nosql/lsm/"/>
    
    
      <category term="LSM-Tree" scheme="http://zshell.cc/tags/LSM-Tree/"/>
    
      <category term="不可能三角" scheme="http://zshell.cc/tags/%E4%B8%8D%E5%8F%AF%E8%83%BD%E4%B8%89%E8%A7%92/"/>
    
  </entry>
  
  <entry>
    <title>LSM-Tree 学习笔记</title>
    <link href="http://zshell.cc/2024/10/05/nosql-lsm--LSM-Tree%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"/>
    <id>http://zshell.cc/2024/10/05/nosql-lsm--LSM-Tree学习笔记/</id>
    <published>2024-10-05T05:38:03.000Z</published>
    <updated>2025-07-28T13:35:01.529Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>日志结构合并树是一种巧妙的数据结构, 它是一种<strong>妥协的艺术</strong>: 它在关注写性能的同时也尽量照顾到了读性能, 适用于写多读少、写多读中等的场景, 甚至在一些数据库产品中直接将其作为核心存储引擎 (如 TiDB -&gt; TiKV 基于 RocksDB);<br>因此很有必要学习一下 LSM-Tree 的原理;</p></blockquote><a id="more"></a><p>B+ 树 (如数据库索引) 和 log append 型操作 (如数据库 WAL 日志) 是数据读写效率的两个极端; 磁盘读写分为随机读写和顺序读写，一般来说随机读写的效率要低于顺序读写。</p><ul><li>B+ 树解决的是磁盘随机读慢的问题, 但随机写无法保证效率, 因为写入数据时涉及到树的重构: 读效率高而写效率差; </li><li>log append 型文件操作解决的是磁盘随机写慢的问题, 但读时需要遍历整个文件: 写效率高而读效率差;</li></ul><p>为了在排序和 log append 型文件操作之间做个折中, 就引入了 Log-Structed Merge Tree 模型, LSM Tree 既有日志型的文件操作提升写效率, 又在每个 sstable 中排序保证了查询效率;</p><h2 id="LSM-Tree-的数据结构"><a href="#LSM-Tree-的数据结构" class="headerlink" title="LSM-Tree 的数据结构"></a><strong>LSM-Tree 的数据结构</strong></h2><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://github.com/zshell-zhang/static-content/raw/master/cs/nosql/lsm/LSM-Tree%20%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/1.png" alt="" title="">                </div>                <div class="image-caption"></div>            </figure><h3 id="WAL"><a href="#WAL" class="headerlink" title="WAL"></a><strong>WAL</strong></h3><h3 id="MemTable"><a href="#MemTable" class="headerlink" title="MemTable"></a><strong>MemTable</strong></h3><h3 id="SST-Sorted-String-Table"><a href="#SST-Sorted-String-Table" class="headerlink" title="SST (Sorted String Table)"></a><strong>SST (Sorted String Table)</strong></h3><h2 id="LSM-Tree-的压缩"><a href="#LSM-Tree-的压缩" class="headerlink" title="LSM-Tree 的压缩"></a><strong>LSM-Tree 的压缩</strong></h2><p>在 LSM-Tree 中，压缩算法主要有两种策略：<strong>Tiering</strong> 和 <strong>Leveling</strong>, 它们的区别主要体现在如何组织和管理不同层级的数据，以及如何执行合并操作;</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://github.com/zshell-zhang/static-content/raw/master/cs/nosql/lsm/LSM-Tree%20%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/2.png" alt="" title="">                </div>                <div class="image-caption"></div>            </figure><h3 id="Tiering"><a href="#Tiering" class="headerlink" title="Tiering"></a><strong>Tiering</strong></h3><p>Tiering 策略允许每一层（Level）包含多个 SSTable，并且每一层的 SSTable 之间可以有重叠的键范围。压缩操作主要发生在同一层内，将多个小的 SSTable 合并成一个更大的 SSTable，然后将其推送到下一层。</p><p>特点:</p><ul><li><strong>写放大较低</strong>：由于每一层可以包含多个 SSTable，压缩操作不需要频繁地将数据向下层移动，因此写放大（Write Amplification）较低。</li><li><strong>读放大较高</strong>：由于每一层的 SSTable 之间可能有重叠的键范围，查询时可能需要检查多个 SSTable，导致读放大（Read Amplification）较高。</li><li><strong>空间放大较高</strong>：每一层可能包含多个 SSTable，且这些 SSTable 之间可能有重复的数据，因此空间放大（Space Amplification）较高。</li></ul><p>适用场景:</p><ul><li>Tiering 适合写密集型工作负载，尤其是对写性能要求较高的场景。</li></ul><h3 id="Leveling"><a href="#Leveling" class="headerlink" title="Leveling"></a><strong>Leveling</strong></h3><p>Leveling 策略要求每一层（Level）只能包含一个 SSTable，并且每一层的 SSTable 的键范围不重叠。压缩操作主要发生在相邻的两层之间，将上层的 SSTable 与下层的 SSTable 合并，生成一个新的 SSTable 并推送到下层。</p><p>特点:</p><ul><li><strong>写放大较高</strong>：由于每一层只能包含一个 SSTable，压缩操作需要频繁地将数据向下层移动，导致写放大较高。</li><li><strong>读放大较低</strong>：由于每一层的 SSTable 之间没有重叠的键范围，查询时通常只需要检查一个 SSTable，因此读放大较低。</li><li><strong>空间放大较低</strong>：每一层只有一个 SSTable，且键范围不重叠，因此空间放大较低。</li></ul><p>适用场景:</p><ul><li>Leveling 适合读密集型工作负载，尤其是对读性能要求较高的场景。</li></ul><h3 id="策略对比"><a href="#策略对比" class="headerlink" title="策略对比"></a><strong>策略对比</strong></h3><table><thead><tr><th>特性</th><th>Tiering</th><th>Leveling</th></tr></thead><tbody><tr><td><strong>写放大</strong></td><td>较低</td><td>较高</td></tr><tr><td><strong>读放大</strong></td><td>较高</td><td>较低</td></tr><tr><td><strong>空间放大</strong></td><td>较高</td><td>较低</td></tr><tr><td><strong>适用场景</strong></td><td>写密集型工作负载</td><td>读密集型工作负载</td></tr><tr><td><strong>压缩策略</strong></td><td>同一层内合并多个 SSTable</td><td>相邻层之间合并 SSTable</td></tr><tr><td><strong>SSTable 重叠</strong></td><td>允许同一层内 SSTable 键范围重叠</td><td>不允许同一层内 SSTable 键范围重叠</td></tr></tbody></table><p><strong>混合策略:</strong><br>在实际应用中，有些系统会采用 <strong>混合策略</strong>，结合 Tiering 和 Leveling 的优点。例如，在较低的层级使用 Tiering 以减少写放大，而在较高的层级使用 Leveling 以降低读放大和空间放大。</p><p>通过选择合适的压缩策略，LSM-Tree 可以在不同的工作负载下实现性能的优化。</p><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a><strong>参考资料</strong></h2><ul><li><a href="https://mp.weixin.qq.com/s/9Mem5ji-NlJAjiUecezxcQ" target="_blank" rel="noopener">RocksDB的一些基本概念及简单应用</a></li></ul>]]></content>
    
    <summary type="html">
    
      &lt;blockquote&gt;
&lt;p&gt;日志结构合并树是一种巧妙的数据结构, 它是一种&lt;strong&gt;妥协的艺术&lt;/strong&gt;: 它在关注写性能的同时也尽量照顾到了读性能, 适用于写多读少、写多读中等的场景, 甚至在一些数据库产品中直接将其作为核心存储引擎 (如 TiDB -&amp;gt; TiKV 基于 RocksDB);&lt;br&gt;因此很有必要学习一下 LSM-Tree 的原理;&lt;/p&gt;
&lt;/blockquote&gt;
    
    </summary>
    
      <category term="nosql" scheme="http://zshell.cc/categories/nosql/"/>
    
      <category term="lsm" scheme="http://zshell.cc/categories/nosql/lsm/"/>
    
    
      <category term="nosql" scheme="http://zshell.cc/tags/nosql/"/>
    
      <category term="LSM-Tree" scheme="http://zshell.cc/tags/LSM-Tree/"/>
    
      <category term="RocksDB" scheme="http://zshell.cc/tags/RocksDB/"/>
    
  </entry>
  
  <entry>
    <title>Paxos 学习笔记</title>
    <link href="http://zshell.cc/2024/09/22/consensus-paxos--Paxos%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"/>
    <id>http://zshell.cc/2024/09/22/consensus-paxos--Paxos学习笔记/</id>
    <published>2024-09-22T03:40:56.000Z</published>
    <updated>2025-07-28T13:35:01.505Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>Leslie Lamport 于 1990 年提出的 <a href="https://lamport.azurewebsites.net/pubs/lamport-paxos.pdf" target="_blank" rel="noopener">经典 Paxos</a> 是分布式共识算法的开山鼻祖, Google Chubby 的作者 Mike Burrows 就曾说过: 「这个世界上只有一种一致性算法, 那就是 Paxos」, 其他所有的一致性算法, 本质上都是对 Paxos 在实现层面的变体、优化或扩展;<br>因此 Paxos 算法是分布式理论入门的第一块基石, 欲涉足该领域, 就必须将其完全地、彻底地研究清楚;</p></blockquote><a id="more"></a><h1 id="分布式系统"><a href="#分布式系统" class="headerlink" title="分布式系统"></a><strong>分布式系统</strong></h1><p>我们为什么需要分布式系统?<br>因为物理硬件是不可靠的, 磁盘损坏、服务器宕机、网络丢包等各种问题都会造成传统单机服务的不可用, 所以我们必须使用分区冗余容错的方式 (多副本) 来对抗物理硬件的不可靠, 也就是说, 我们只有使用分布式系统, 才能架设起高可用、高可靠的业务服务;<br>但要让一群原本毫无关联的机器实现协调统一的默契合作却绝非易事, 我们设计出的分布式算法至少要满足以下能力:</p><ul><li><strong>高可靠性</strong>: 当客户端写入数据, 服务端返回成功, 必须保证客户端下次读取时一定能查询到, 不能丢失;</li><li><strong>高可用性</strong>: 当分布式系统内的若干台机器宕机时, 系统整体依然能够对外正常提供服务;</li><li><strong>强一致性</strong>: 整个系统必须对外保持一致的状态视图, 即同一时刻下客户端无论怎么请求读, 总能获取到相同的值;</li></ul><p>有一个值得注意的点是: 并不是说, 凡存在多台机器分区部署的系统都叫做分布式系统; 比如我们绝大部分的业务应用, 会在很多台机器上部署相同的代码, 通过流量接入层的反向代理与负载均衡, 对外提供统一的服务, 这些机器之间没有任何交互, 甚至感知不到其他机器的存在, 这显然不能算分布式系统, 只能算是分布式集群;<br>区别分布式系统和分布式集群最关键的一点是有无状态:</p><ul><li>当提供的是有状态服务 (比如存储系统), 需要多副本和跨机器复制, 需要使用共识算法来协调各机器间的状态同步时, 叫做分布式系统;</li><li>当提供的是无状态服务 (如普通的业务服务), 无需亲自实现状态存储的逻辑 (通常是代理给了下游的分布式系统去实现, 比如数据库), 叫做分布式集群;</li></ul><h1 id="Paxos-之前的分布式算法"><a href="#Paxos-之前的分布式算法" class="headerlink" title="Paxos 之前的分布式算法"></a><strong>Paxos 之前的分布式算法</strong></h1><p>在 Paxos 诞生之前, 其实已经有很多方案尝试去解决分布式系统的难题, 但他们都有自己的局限性和缺陷, 无法全部解决上述三个问题;</p><h2 id="主从异步复制"><a href="#主从异步复制" class="headerlink" title="主从异步复制"></a><strong>主从异步复制</strong></h2><p>主从异步复制是最简单的策略, 但存在一个最基本的问题: 客户端请求写入数据, 收到服务端的响应成功, 离数据真正安全 (数据复制到全部的机器上) 在时间上有一个空隙, 如果接收请求的 master 在响应成功后立刻宕机, 数据就会丢失, 因此<strong>主从异步复制不可靠</strong>;</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://github.com/zshell-zhang/static-content/raw/master/cs/consensus/paxos/Paxos%20%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/1.png" alt="主从异步复制" title="">                </div>                <div class="image-caption">主从异步复制</div>            </figure><h2 id="主从同步复制"><a href="#主从同步复制" class="headerlink" title="主从同步复制"></a><strong>主从同步复制</strong></h2><p>跟主从异步复制相比, 主从同步复制提供了完整的可靠性: 直到数据真的安全的复制到全部的机器上之后, master 才会向客户端响应成功; 但主从同步复制有个致命的缺点: 整个系统中有任何一个机器宕机, 写入就进行不下去了, 相当于系统的可用性随着副本数量的增加而指数性降低, 因此<strong>主从同步复制不具有高可用性</strong>;</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://github.com/zshell-zhang/static-content/raw/master/cs/consensus/paxos/Paxos%20%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/2.png" alt="主从同步复制" title="">                </div>                <div class="image-caption">主从同步复制</div>            </figure><h2 id="主从半同步复制"><a href="#主从半同步复制" class="headerlink" title="主从半同步复制"></a><strong>主从半同步复制</strong></h2><p>如果在主从同步复制和主从异步复制之间做一个折中, 要求 master 在应答客户端之前必须把数据复制到足够多的机器上, 但不需要是全部, 这就是主从半同步复制; 这样副本数够多可以提供比较高的可靠性, 同时在 1 台机器宕机时也不会让整个系统 hang 住;<br>这种方案似乎同时解决了高可靠和高可用的问题, 但是主从半同步复制依然无法解决一种情况,例如:</p><ul><li>数据 a 复制到了 slave1, 但没有复制到 slave2;</li><li>数据 b 复制到了 slave2, 但没有复制到 slave1;</li></ul><p>这时如果 master 挂掉了需要从某个 slave 恢复出数据, 任何一个 slave 都不能提供完整的数据 a 和 b, 因此<strong>主从半同步复制不完全可靠</strong>;</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://github.com/zshell-zhang/static-content/raw/master/cs/consensus/paxos/Paxos%20%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/3.png" alt="主从半同步复制" title="">                </div>                <div class="image-caption">主从半同步复制</div>            </figure><h2 id="多数派读写"><a href="#多数派读写" class="headerlink" title="多数派读写"></a><strong>多数派读写</strong></h2><p>为了解决半同步复制中数据不一致的问题, 可以将这个复制策略稍做改进: 多数派 (Quorum) 读写, 每条数据必须写入到半数以上的机器才能返回成功, 每次读取数据也都必须从半数以上的机器获取, 以确认数据是否存在;<br>这种方案已经同时解决了可靠性和可用性, 但可惜它解决不了一致性的问题, 例如:</p><ul><li>第一次更新向 node1 和 node2 写入了 a &#x3D; x;</li><li>第二次更新向 node2 和 node3 写入了 a &#x3D; y;</li></ul><p>那么此时客户端分别从 node1、node2 读取 a, 将读出两个不同的值, 从而无法确定真正的结果, 因此<strong>多数派读写不具有强一致性</strong>;</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://github.com/zshell-zhang/static-content/raw/master/cs/consensus/paxos/Paxos%20%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/4.png" alt="多数派读写" title="">                </div>                <div class="image-caption">多数派读写</div>            </figure><h2 id="带版本的多数派读写"><a href="#带版本的多数派读写" class="headerlink" title="带版本的多数派读写"></a><strong>带版本的多数派读写</strong></h2><p>针对上述多数派读写的一致性问题, 可以给每次请求写入带上一个全局单调递增的 version, 读取的过程中优先采纳版本更高的值; 只不过这种方式依然有缺陷: 当客户端没有完成一次完整的多数派写时, 比如:</p><ul><li>第一次更新向 node1 和 node2 写入了 a &#x3D; x (version&#x3D;1);</li><li>第二次更新只向 node3 写入了 a &#x3D; y (version&#x3D;2) 后就宕机或后续逻辑执行延迟;</li></ul><p>若另一个客户端此时试图读取 a 时:</p><ul><li>当从 node1 和 node2 读取时, 得到的结果为 x;</li><li>当从 node2 和 node3 读取时, 得到的结果为 y;</li></ul><p>因为这种方式不能做到事务性更新 (没有原子性、没有隔离性), 因此<strong>带版本的多数派读写仍不具备严谨的强一致性</strong>;</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://github.com/zshell-zhang/static-content/raw/master/cs/consensus/paxos/Paxos%20%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/5.png" alt="带版本的多数派读写" title="">                </div>                <div class="image-caption">带版本的多数派读写</div>            </figure><h1 id="Paxos-的实现"><a href="#Paxos-的实现" class="headerlink" title="Paxos 的实现"></a><strong>Paxos 的实现</strong></h1><p>Paxos 是多数派读写的进一步升级, 其通过 2 次原本并不严谨的带版本多数派读写, 实现了严谨的强一致性共识算法;</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://github.com/zshell-zhang/static-content/raw/master/cs/consensus/paxos/Paxos%20%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/6.png" alt="" title="">                </div>                <div class="image-caption"></div>            </figure><h2 id="Phase-1-Prepare"><a href="#Phase-1-Prepare" class="headerlink" title="Phase-1: Prepare"></a><strong>Phase-1: Prepare</strong></h2><p>phase-1 的 Proposer 请求与 Acceptor 响应:</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">&gt; request (Proposer):</span><br><span class="line">    rnd: int</span><br><span class="line"></span><br><span class="line">&gt; response (Acceptor):</span><br><span class="line">    <span class="keyword">if</span> (rnd &lt; last_rnd):</span><br><span class="line">        <span class="built_in">return</span> fail // 中断拒绝</span><br><span class="line">    <span class="built_in">return</span> &#123;</span><br><span class="line">        last_rnd: int</span><br><span class="line">        last_v: <span class="string">"xxx"</span></span><br><span class="line">        last_vrnd: int</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://github.com/zshell-zhang/static-content/raw/master/cs/consensus/paxos/Paxos%20%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/7.png" alt="" title="">                </div>                <div class="image-caption"></div>            </figure><p>Proposer 对 phase-1 中 Acceptor 响应的处理:</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">&gt; response (Acceptor):</span><br><span class="line">    last_rnd: <span class="keyword">int</span></span><br><span class="line">    last_v: <span class="string">"xxx"</span></span><br><span class="line">    last_vrnd: <span class="keyword">int</span></span><br><span class="line">    </span><br><span class="line">&gt; resp_process (Proposer):</span><br><span class="line">    <span class="comment">// 所有 acceptor 应答的 last_v / last_vrnd 都是空: 用自己的 v / vrnd</span></span><br><span class="line">    <span class="keyword">if</span> (last_v == nil &amp;&amp; last_vrnd == nil):</span><br><span class="line">        use(v, vrnd)</span><br><span class="line">    <span class="comment">// 从所有 acceptor 应答中选择 last_vrnd 最大的 last_v 作为 phase-2 要写入的值, 但 vrnd 依旧用自己的</span></span><br><span class="line">    v_with_biggest_rnd = chooseBiggestRndV(last_v, last_vrnd)</span><br><span class="line">    use(v_with_biggest_rnd, vrnd)</span><br></pre></td></tr></table></figure><p><strong>注意:</strong> 如果 Proposer 收到了某个应答包含被写入的 last_v 和 last_vrnd, 这时当前 Proposer 必须假设有其他客户端 (Other Proposer) 正在运行, 虽然不知道对方是否已经成功结束, 但任何已经写入的值都不能被修改, 所以当前 Proposer 必须保持原有的值: 将看到的最大 last_vrnd 对应的 last_v 作为下一阶段将要写入的值;</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://github.com/zshell-zhang/static-content/raw/master/cs/consensus/paxos/Paxos%20%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/8.png" alt="" title="">                </div>                <div class="image-caption"></div>            </figure><h2 id="Phase-2-Commit"><a href="#Phase-2-Commit" class="headerlink" title="Phase-2: Commit"></a><strong>Phase-2: Commit</strong></h2><p>phase-2 的 Proposer 请求与 Acceptor 响应:</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></pre></td><td class="code"><pre><span class="line">request (Proposer):</span><br><span class="line">    v: <span class="string">"xxx"</span></span><br><span class="line">    rnd: int</span><br><span class="line"></span><br><span class="line">reponse (Acceptor):</span><br><span class="line">    <span class="keyword">if</span> (rnd &lt; last_rnd):</span><br><span class="line">        <span class="built_in">return</span> fail // 中断拒绝</span><br><span class="line">    record (v, vrnd)</span><br></pre></td></tr></table></figure><p>Acceptor 通过比较 phase-2 请求中的 rnd 和自己本地记录的 last_rnd, 来确定当前请求的 Proposer 是否还有权写入:</p><ul><li>当 rnd &#x3D;&#x3D; last_rnd: 这次写入是被允许的, Acceptor 将 v 写入本地, 并将 phase-2 请求中的 rnd 记录到本地的 vrnd 中;</li><li>当 rnd &lt; last_rnd: 拒绝返回失败;</li><li><strong>不可能存在</strong> rnd &gt; last_rnd 的情况: 因为 phase-1 保证了 Proposer 的 rnd 一定曾被 Acceptor 记录下来过;</li></ul><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://github.com/zshell-zhang/static-content/raw/master/cs/consensus/paxos/Paxos%20%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/9.png" alt="" title="">                </div>                <div class="image-caption"></div>            </figure><h1 id="Paxos-的完备性证明"><a href="#Paxos-的完备性证明" class="headerlink" title="Paxos 的完备性证明"></a><strong>Paxos 的完备性证明</strong></h1><h2 id="Paxos-的安全性"><a href="#Paxos-的安全性" class="headerlink" title="Paxos 的安全性"></a><strong>Paxos 的安全性</strong></h2><h2 id="Paxos-的活性"><a href="#Paxos-的活性" class="headerlink" title="Paxos 的活性"></a><strong>Paxos 的活性</strong></h2><h1 id="Paxos-的变种"><a href="#Paxos-的变种" class="headerlink" title="Paxos 的变种"></a><strong>Paxos 的变种</strong></h1><h2 id="Multi-Paxos"><a href="#Multi-Paxos" class="headerlink" title="Multi Paxos"></a><strong>Multi Paxos</strong></h2><h2 id="Fast-Paxos"><a href="#Fast-Paxos" class="headerlink" title="Fast Paxos"></a><strong>Fast Paxos</strong></h2><h1 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a><strong>参考资料</strong></h1><ul><li><a href="https://lamport.azurewebsites.net/pubs/lamport-paxos.pdf" target="_blank" rel="noopener">The Part-Time Parliament —— Leslie Lamport</a></li><li><a href="https://mp.weixin.qq.com/s/-ULrqBZ_GLY1_LN9eNPpXg" target="_blank" rel="noopener">分布式算法 Paxos 的直观解释</a></li></ul>]]></content>
    
    <summary type="html">
    
      &lt;blockquote&gt;
&lt;p&gt;Leslie Lamport 于 1990 年提出的 &lt;a href=&quot;https://lamport.azurewebsites.net/pubs/lamport-paxos.pdf&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;经典 Paxos&lt;/a&gt; 是分布式共识算法的开山鼻祖, Google Chubby 的作者 Mike Burrows 就曾说过: 「这个世界上只有一种一致性算法, 那就是 Paxos」, 其他所有的一致性算法, 本质上都是对 Paxos 在实现层面的变体、优化或扩展;&lt;br&gt;因此 Paxos 算法是分布式理论入门的第一块基石, 欲涉足该领域, 就必须将其完全地、彻底地研究清楚;&lt;/p&gt;
&lt;/blockquote&gt;
    
    </summary>
    
      <category term="consensus" scheme="http://zshell.cc/categories/consensus/"/>
    
      <category term="paxos" scheme="http://zshell.cc/categories/consensus/paxos/"/>
    
    
      <category term="consensus" scheme="http://zshell.cc/tags/consensus/"/>
    
      <category term="Paxos" scheme="http://zshell.cc/tags/Paxos/"/>
    
  </entry>
  
  <entry>
    <title>美国三军航空器命名体系</title>
    <link href="http://zshell.cc/2024/08/11/military-America--%E7%BE%8E%E5%9B%BD%E4%B8%89%E5%86%9B%E8%88%AA%E7%A9%BA%E5%99%A8%E5%91%BD%E5%90%8D%E4%BD%93%E7%B3%BB/"/>
    <id>http://zshell.cc/2024/08/11/military-America--美国三军航空器命名体系/</id>
    <published>2024-08-11T10:17:09.000Z</published>
    <updated>2025-07-28T13:35:01.525Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>1962 年美国订制了一套统一三军的航空器命名规范, 在此全面梳理一下, 方便学习与记忆;</p></blockquote><a id="more"></a><h2 id="普通飞行器命名规则"><a href="#普通飞行器命名规则" class="headerlink" title="普通飞行器命名规则"></a><strong>普通飞行器命名规则</strong></h2><h3 id="以状态为前缀命名"><a href="#以状态为前缀命名" class="headerlink" title="以状态为前缀命名"></a><strong>以状态为前缀命名</strong></h3><ul><li>G: 永久停飞</li><li>J: 暂时特别测试 (Special test, temporary)</li><li>N: 永久特别测试 (Special test, permanent)</li><li>X: 实验机</li><li>Y: 原型机</li><li>Z: 纸上计划</li></ul><h3 id="以任务为后缀命名"><a href="#以任务为后缀命名" class="headerlink" title="以任务为后缀命名"></a><strong>以任务为后缀命名</strong></h3><p>表示航空器的主要任务类型:</p><ul><li>A: 攻击机 (Attack aircraft)</li><li>B: 轰炸机 (Bomber)</li><li>C: 运输机 (Transport)</li><li>E: 电子战机 (Special electronic installation)</li><li>F: 战斗机 (Fighter)</li><li>K: 加油机 (Tanker)</li><li>H: 直升机 (Helicopter)</li><li>L: 激光测试机 (Laser-equipped)</li><li>O: 前线观测机 (Observation)</li><li>P: 海上巡逻机 (Maritime patrol)</li><li>R: 侦察机 (Reconnaissance)</li><li>S: 反潜机 (Anti-submarine warfare)</li><li>T: 教练机 (Trainer)</li><li>U: 通用机 (Utility)</li><li>X: 实验机 (Special research)</li></ul><h4 id="举例"><a href="#举例" class="headerlink" title="举例"></a><strong>举例</strong></h4><ul><li>F-22: “猛禽” 战斗机</li><li>B-2: “幽灵” 轰炸机</li><li>B-21: “地狱犬” 轰炸机</li><li>E-2: “鹰眼” 预警机</li><li>A-10: “疣猪” 对地攻击机</li><li>C-5: “银河” 运输机</li><li>C-17: “环球霸王” 运输机</li><li>C-130: “大力神” 运输机</li><li>V-22: “鱼鹰” 人员运输直升机</li></ul><h3 id="以任务为前缀命名"><a href="#以任务为前缀命名" class="headerlink" title="以任务为前缀命名"></a><strong>以任务为前缀命名</strong></h3><p>表示航空器所执行的任务是附加在原主要任务之上的不同任务:</p><ul><li>A: 对地攻击 (Attack)</li><li>C: 运输 (Transport)</li><li>D: 靶机拖曳 (Drone director)</li><li>E: 特殊电子战 (Special electronic mission)</li><li>F: 空中优势战斗 (Fighter)</li><li>H: 搜索与救援 (Search and rescue, MEDEVAC)</li><li>K: 空中加油 (Tanker)</li><li>L: 极端天气任务 (Equipped for cold weather operations)</li><li>M: 导弹运载 (Missile carrier 1962 – 1972 年), 反水雷 (Mine countermeasures 1973 – 1976 年), 多任务 (Multi-mission 1977 年起一直延用)</li><li>O: 侦测 (Observation)</li><li>P: 海上巡逻 (Maritime patrol)</li><li>Q: 无人操作 (Unmanned drone)</li><li>R: 侦察 (Reconnaissance)</li><li>S: 反潜 (Antisubmarine warfare)</li><li>T: 教练 (Trainer)</li><li>U: 通用 (Utility)</li><li>V: 人员输送 (Staff transport)</li><li>W: 天气观测 (Weather reconnaissance)</li></ul><h4 id="举例-1"><a href="#举例-1" class="headerlink" title="举例"></a><strong>举例</strong></h4><ul><li>EA-18G: “咆哮者” 电子战机</li><li>EP-3C: “” 电子侦察机</li><li>UH-60: “黑鹰” 通用直升机</li><li>AH-64: “阿帕奇” 武装直升机</li><li>CH-47: “支奴干” 运输直升机</li><li>HH-60: “铺路鹰” 搜救直升机</li><li>SR-71: “黑鸟” 侦察机</li><li>YF-22: 第五代空优战斗机验证机 (F-22 原型机)</li><li>QF-16: “战隼” F-16 的无人操作改型</li><li>KC-135: 加油机</li><li>WC-130: “飓风猎人” 气象观测机</li><li>LC-130: 极地保障运输机</li></ul><h2 id="导弹-火箭命名规则"><a href="#导弹-火箭命名规则" class="headerlink" title="导弹&#x2F;火箭命名规则"></a><strong>导弹&#x2F;火箭命名规则</strong></h2><p>美国导弹和火箭一般以三位字母 + 编号命名, 其中三位字母分别代表:</p><ul><li>武器的发射载台&#x2F;环境</li><li>武器的主要任务</li><li>武器的类型</li></ul><p>首字母代表:</p><table><thead><tr><th align="center">字母</th><th align="center">代表</th><th align="center">详释</th></tr></thead><tbody><tr><td align="center">A</td><td align="center">Air</td><td align="center">空中载台发射</td></tr><tr><td align="center">B</td><td align="center">Multiple</td><td align="center">多平台发射</td></tr><tr><td align="center">C</td><td align="center">Coffin or Container</td><td align="center">路基平台，倾斜发射</td></tr><tr><td align="center">F</td><td align="center">Individual or Infantry</td><td align="center">单兵携带，单兵发射</td></tr><tr><td align="center">L</td><td align="center">Land or Silo</td><td align="center">陆基固定发射，井发射</td></tr><tr><td align="center">M</td><td align="center">Mobile</td><td align="center">陆基移动发射，车辆发射</td></tr><tr><td align="center">P</td><td align="center">Soft Pad</td><td align="center">陆基临时固定发射平台发射</td></tr><tr><td align="center">U</td><td align="center">Underwater</td><td align="center">水下潜射</td></tr><tr><td align="center">R</td><td align="center">Surface ship</td><td align="center">水面载台&#x2F;舰艇发射</td></tr></tbody></table><p>次字母代表:</p><table><thead><tr><th align="center">字母</th><th align="center">代表</th><th align="center">详释</th></tr></thead><tbody><tr><td align="center">D</td><td align="center">Decoy</td><td align="center">诱饵&#x2F;迷惑武器，情报战武器</td></tr><tr><td align="center">E</td><td align="center">Special Electronic</td><td align="center">特殊电子战武器，电子&#x2F;辐射&#x2F;干扰武器</td></tr><tr><td align="center">G</td><td align="center">Surface Attack</td><td align="center">对面攻击武器，打击车辆&#x2F;舰船&#x2F;地面目标</td></tr><tr><td align="center">I</td><td align="center">Intercept-Aerial</td><td align="center">防空拦截，打击空中目标</td></tr><tr><td align="center">Q</td><td align="center">Drone</td><td align="center">侦察、遥测</td></tr><tr><td align="center">S</td><td align="center">Space</td><td align="center">太空战，空间目标摧毁</td></tr><tr><td align="center">T</td><td align="center">Training</td><td align="center">训练</td></tr><tr><td align="center">U</td><td align="center">Underwater attack</td><td align="center">水下攻击，反潜</td></tr><tr><td align="center">W</td><td align="center">Weather</td><td align="center">天气侦测</td></tr></tbody></table><p>末字母代表:</p><table><thead><tr><th align="center">字母</th><th align="center">代表</th><th align="center">详释</th></tr></thead><tbody><tr><td align="center">M</td><td align="center">Guided Missile</td><td align="center">导弹，可控无人航空航天器&#x2F;车辆&#x2F;鱼雷</td></tr><tr><td align="center">R</td><td align="center">Rocket</td><td align="center">火箭、不可控式火箭、自由落体火箭</td></tr><tr><td align="center">N</td><td align="center">Probe</td><td align="center">探测器、卫星</td></tr></tbody></table><h3 id="举例-2"><a href="#举例-2" class="headerlink" title="举例"></a><strong>举例</strong></h3><ul><li>战斧巡航导弹<ul><li>BGM-109: 陆基型</li><li>AGM-109: 空射型</li><li>UGM-109: 潜射反舰型</li><li>RGM-109: 舰射型</li></ul></li><li>鱼叉反舰导弹<ul><li>AGM-84: 空射型</li><li>RGM-84: 舰射型</li><li>UGM-84: 潜射型</li></ul></li><li>先进中程空空导弹:<ul><li>AIM-120: 空射型</li></ul></li></ul><h2 id="参考链接"><a href="#参考链接" class="headerlink" title="参考链接"></a><strong>参考链接</strong></h2><ul><li><a href="https://baike.baidu.com/item/1962%E5%B9%B4%E7%BE%8E%E5%9B%BD%E4%B8%89%E5%86%9B%E8%88%AA%E7%A9%BA%E5%99%A8%E5%91%BD%E5%90%8D%E4%BD%93%E7%B3%BB" target="_blank" rel="noopener">1962年美国三军航空器命名体系</a></li></ul>]]></content>
    
    <summary type="html">
    
      &lt;blockquote&gt;
&lt;p&gt;1962 年美国订制了一套统一三军的航空器命名规范, 在此全面梳理一下, 方便学习与记忆;&lt;/p&gt;
&lt;/blockquote&gt;
    
    </summary>
    
      <category term="military" scheme="http://zshell.cc/categories/military/"/>
    
      <category term="America" scheme="http://zshell.cc/categories/military/America/"/>
    
    
      <category term="空军" scheme="http://zshell.cc/tags/%E7%A9%BA%E5%86%9B/"/>
    
      <category term="导弹" scheme="http://zshell.cc/tags/%E5%AF%BC%E5%BC%B9/"/>
    
      <category term="飞行器" scheme="http://zshell.cc/tags/%E9%A3%9E%E8%A1%8C%E5%99%A8/"/>
    
  </entry>
  
  <entry>
    <title>kafka broker 受控关机流程</title>
    <link href="http://zshell.cc/2024/06/09/kafka-broker--kafka_broker%E5%8F%97%E6%8E%A7%E5%85%B3%E6%9C%BA%E6%B5%81%E7%A8%8B/"/>
    <id>http://zshell.cc/2024/06/09/kafka-broker--kafka_broker受控关机流程/</id>
    <published>2024-06-09T03:24:32.000Z</published>
    <updated>2025-07-28T13:35:01.514Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>kafka 在协调机制下实现无感切流 &amp; 受控关机是一个复杂的流程, 且在 zookeeper 时代和 KRaft 时代下的处理逻辑迥异;<br>通过 ZK 和 KRaft 两种机制对该场景处理的差异, 我们也能体会到 KRaft 模式对于 kafka 革命般的意义;</p></blockquote><a id="more"></a><h2 id="Zookeeper-下的-broker-受控关机流程"><a href="#Zookeeper-下的-broker-受控关机流程" class="headerlink" title="Zookeeper 下的 broker 受控关机流程"></a><strong>Zookeeper 下的 broker 受控关机流程</strong></h2><p>假设一个 kafka 集群初始状态如下:</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://github.com/zshell-zhang/static-content/raw/master/cs/kafka/broker/broker%20%E5%8F%97%E6%8E%A7%E5%85%B3%E6%9C%BA%E6%B5%81%E7%A8%8B/1.png" alt="初始状态" title="">                </div>                <div class="image-caption">初始状态</div>            </figure><h3 id="目标-Broker-向-Controller-请求关机"><a href="#目标-Broker-向-Controller-请求关机" class="headerlink" title="目标 Broker 向 Controller 请求关机"></a><strong>目标 Broker 向 Controller 请求关机</strong></h3><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://github.com/zshell-zhang/static-content/raw/master/cs/kafka/broker/broker%20%E5%8F%97%E6%8E%A7%E5%85%B3%E6%9C%BA%E6%B5%81%E7%A8%8B/2.png" alt="ControlledShutdownRequest" title="">                </div>                <div class="image-caption">ControlledShutdownRequest</div>            </figure><p>当在 Broker-0 机器上执行 kill kafka broker 进程的命令, 该机器上的 broker 进程响应命令, 会先向 Controller 发送 ControlledShutdownRequest, 随后进程阻塞, 等待 Controller 的回复;</p><h3 id="Controller-处理-Broker-关机请求"><a href="#Controller-处理-Broker-关机请求" class="headerlink" title="Controller 处理 Broker 关机请求"></a><strong>Controller 处理 Broker 关机请求</strong></h3><p>当 Controller 接收到 ControlledShutdownRequest 后, 将会对目标 broker 上的<strong>每一个 topic 的每一个 partition</strong> 依次执行以下步骤:</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://github.com/zshell-zhang/static-content/raw/master/cs/kafka/broker/broker%20%E5%8F%97%E6%8E%A7%E5%85%B3%E6%9C%BA%E6%B5%81%E7%A8%8B/3.png" alt="" title="">                </div>                <div class="image-caption"></div>            </figure><ol><li>Controller 会检查下线的 broker 是否是目标 partition 的 leader, 如是, 则 Controller 需要为该 partition 重新选举新的 leader:<ul><li>Controller 会从该 partition 的 ISR (In-Sync Replicas) 列表中选择一个新的 leader, 默认选择 ISR 列表中的第一个副本作为新的 leader;</li><li>如果 ISR 列表为空, Controller 会根据配置决定是否允许从非同步副本 (Out-of-Sync Replicas) 中选举 leader (通过配置 unclean.leader.election.enable);</li></ul></li><li>收缩 ISR, 将下线的 broker 上的 partition 副本从 ISR 中摘除:<br>从:<figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://github.com/zshell-zhang/static-content/raw/master/cs/kafka/broker/broker%20%E5%8F%97%E6%8E%A7%E5%85%B3%E6%9C%BA%E6%B5%81%E7%A8%8B/4.png" alt="" title="">                </div>                <div class="image-caption"></div>            </figure>变为:<figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://github.com/zshell-zhang/static-content/raw/master/cs/kafka/broker/broker%20%E5%8F%97%E6%8E%A7%E5%85%B3%E6%9C%BA%E6%B5%81%E7%A8%8B/5.png" alt="" title="">                </div>                <div class="image-caption"></div>            </figure></li><li>将最新的 ISR 和 leader 信息写入 zookeeper;</li><li>Controller 以同步阻塞的方式依次向目标 partition 其他副本所在的 broker 发送 LeaderAndIsrRequest:<ul><li>收到请求的 broker 从请求中解析出 leader、ISR 等信息并存储到本地后, 响应 Controller;</li><li>Controller 收到上一个 broker 的成功响应后才会向下一个 broker 发送请求;</li></ul></li></ol><h3 id="Broker-正式下线"><a href="#Broker-正式下线" class="headerlink" title="Broker 正式下线"></a><strong>Broker 正式下线</strong></h3><p>当 Controller 处理完所有的 topic 的 partition 后, 会向最初发起请求关机的 Broker-0 回复 ControlledShutdownResponse, 此时 Broker-0 正式结束进程;</p><h3 id="性能改善"><a href="#性能改善" class="headerlink" title="性能改善"></a><strong>性能改善</strong></h3><ul><li>在 kafka 1.1.0 之前, 以上过程是单线程全程同步阻塞执行的, 耗时很长;<ul><li>分区在重新选举 leader 时, 会暂停对外提供读写服务, 严重时会导致生产故障;</li></ul></li><li>从 kafka 1.1.0 开始, kafka 将上述过程改为多线程异步非阻塞的方式执行, 显著提升了处理速度;</li></ul><h2 id="KRaft-下的-broker-受控关机流程"><a href="#KRaft-下的-broker-受控关机流程" class="headerlink" title="KRaft 下的 broker 受控关机流程"></a><strong>KRaft 下的 broker 受控关机流程</strong></h2><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a><strong>参考资料</strong></h2><ul><li><a href="https://blog.csdn.net/LUCIAZZZ/article/details/145504594" target="_blank" rel="noopener">Kafka中的KRaft算法</a></li></ul>]]></content>
    
    <summary type="html">
    
      &lt;blockquote&gt;
&lt;p&gt;kafka 在协调机制下实现无感切流 &amp;amp; 受控关机是一个复杂的流程, 且在 zookeeper 时代和 KRaft 时代下的处理逻辑迥异;&lt;br&gt;通过 ZK 和 KRaft 两种机制对该场景处理的差异, 我们也能体会到 KRaft 模式对于 kafka 革命般的意义;&lt;/p&gt;
&lt;/blockquote&gt;
    
    </summary>
    
      <category term="kafka" scheme="http://zshell.cc/categories/kafka/"/>
    
      <category term="broker" scheme="http://zshell.cc/categories/kafka/broker/"/>
    
    
      <category term="raft" scheme="http://zshell.cc/tags/raft/"/>
    
      <category term="kafka" scheme="http://zshell.cc/tags/kafka/"/>
    
      <category term="zookeeper" scheme="http://zshell.cc/tags/zookeeper/"/>
    
  </entry>
  
  <entry>
    <title>redis的线程模型</title>
    <link href="http://zshell.cc/2024/04/14/redis--redis%E7%9A%84%E7%BA%BF%E7%A8%8B%E6%A8%A1%E5%9E%8B/"/>
    <id>http://zshell.cc/2024/04/14/redis--redis的线程模型/</id>
    <published>2024-04-14T02:51:33.000Z</published>
    <updated>2025-07-28T13:35:01.531Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>redis 的线程模型从最刚开始的纯单线程, 到引入异步线程处理耗时操作, 再到最终使用多线程处理 IO 读写操作, 完成了线程模型的全面升级重构;<br>和普通常见的 IO 密集型应用不同, redis 作为一个内存密集型应用, 使用了绝无仅有的 <strong>多线程处理 IO、单线程处理核心逻辑</strong> 的模式, 值得我们学习了解, 也为我们在日常工作中, 跳出传统思维框架, 紧贴自身业务特征做最合适的技术选型提供了启发与参考;</p></blockquote><a id="more"></a><h2 id="单线程模型"><a href="#单线程模型" class="headerlink" title="单线程模型"></a><strong>单线程模型</strong></h2><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://github.com/zshell-zhang/static-content/raw/master/cs/redis/redis%20%E7%9A%84%E7%BA%BF%E7%A8%8B%E6%A8%A1%E5%9E%8B/1.png" alt="redis 单线程模型" title="">                </div>                <div class="image-caption">redis 单线程模型</div>            </figure><p>借助于 linux epoll I&#x2F;O 多路复用机制, redis 可以仅凭单线程实现高效的客户端请求处理一条龙服务:</p><ul><li>读取数据</li><li>命令解析</li><li>(合并请求)</li><li>命令执行</li><li>结果写回客户端缓冲区</li></ul><p>因为单线程无需考虑锁竞争、线程安全及事务可见性等多线程才会引入的问题, 且不存在线程切换的开销, redis 单线程可以支撑起非常高的单机 qps;</p><h2 id="异步线程"><a href="#异步线程" class="headerlink" title="异步线程"></a><strong>异步线程</strong></h2><p>redis 存在<strong>文件事件</strong>与<strong>时间事件</strong>两种事件类型, 对于事件的处理 redis 只在单独的主线程中做, 但是在很多其他逻辑的处理上, 并不是使用单线程:</p><ul><li>生成 RDB 文件: fork 一个子进程来实现;</li><li>big key 删除: 引入了 Lazy Free 机制将其异步化, 主线程只做关系解除以快速返回, 方便继续处理其他事件;</li></ul><h2 id="多线程模型"><a href="#多线程模型" class="headerlink" title="多线程模型"></a><strong>多线程模型</strong></h2><p>redis 几乎不存在 CPU 或者线程成为瓶颈的情况, 而是主要受限于内存和网络带宽, 即 redis 执行期间大部分 CPU 时间片是用来等待 IO 读写和网络传输;<br>所以 <strong>redis 6.0</strong> 为了解决 IO 读写的瓶颈, 引入了多线程以提升 IO 读写性能, 但同时也保留了事件处理的单线程特性, 从而保留了 redis 传统的高效率;</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://github.com/zshell-zhang/static-content/raw/master/cs/redis/redis%20%E7%9A%84%E7%BA%BF%E7%A8%8B%E6%A8%A1%E5%9E%8B/2.png" alt="redis 多线程模型" title="">                </div>                <div class="image-caption">redis 多线程模型</div>            </figure><p>redis 的 IO 线程代替原本的主线程做了如下事情:</p><ul><li>读取数据</li><li>命令解析</li><li>(合并请求)<br>……</li><li>结果写回</li></ul><p>只有最核心的命令执行是交给了主线程去做; 可以看到, 只要不涉及数据状态的操作, 都可以交给 IO 多线程而不用关心线程安全等问题;</p><h3 id="多线程特性的启用"><a href="#多线程特性的启用" class="headerlink" title="多线程特性的启用"></a><strong>多线程特性的启用</strong></h3><p>redis 6.0 默认是不开启多线程特性的, 如果需要使用多线程功能, 需要在 redis.conf 中完成两个设置:</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></pre></td><td class="code"><pre><span class="line"><span class="comment"># 启用多线程</span></span><br><span class="line">io-thread-do-reads yes</span><br><span class="line"><span class="comment"># 线程数量</span></span><br><span class="line">io-threads 6</span><br></pre></td></tr></table></figure><p>线程个数要小于 Redis 实例所在机器的 CPU 核个数, 例如对于一个 8 核的机器来说, redis 官方建议配置 6 个 IO 线程;</p><h3 id="更高效的实现-Tair-RDB-多线程模型"><a href="#更高效的实现-Tair-RDB-多线程模型" class="headerlink" title="更高效的实现: Tair RDB 多线程模型"></a><strong>更高效的实现: Tair RDB 多线程模型</strong></h3><p>阿里巴巴的缓存产品 Tair RDB 实现了更高效的多线程请求模型:</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://github.com/zshell-zhang/static-content/raw/master/cs/redis/redis%20%E7%9A%84%E7%BA%BF%E7%A8%8B%E6%A8%A1%E5%9E%8B/3.png" alt="tair rdb 的线程模型" title="">                </div>                <div class="image-caption">tair rdb 的线程模型</div>            </figure><ul><li><strong>Main Thread</strong>: 负责客户端连接建立等;</li><li><strong>IO Thread</strong>: 负责请求读取、响应发送、命令解析等;</li><li><strong>Worker Thread</strong>: 专门用于事件处理、命令执行;</li></ul><p>IO Thread 读取用户的请求并进行解析, 之后将解析结果以命令的形式放在队列中发送给 Worker Thread 处理;<br>Worker Thread 将命令处理完成后生成响应, 通过另一条队列发送给 IO Thread;<br>为了提高线程的并行度, IO Thread 和 Worker Thread 之间采用无锁队列和管道进行数据交换;</p><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a><strong>参考资料</strong></h2><ul><li><a href="https://www.zhihu.com/question/626939810/answer/3285039910" target="_blank" rel="noopener">Redis6.0多线程的实现原理是什么？</a></li><li><a href="">Redis的多线程到底该怎么理解</a></li></ul>]]></content>
    
    <summary type="html">
    
      &lt;blockquote&gt;
&lt;p&gt;redis 的线程模型从最刚开始的纯单线程, 到引入异步线程处理耗时操作, 再到最终使用多线程处理 IO 读写操作, 完成了线程模型的全面升级重构;&lt;br&gt;和普通常见的 IO 密集型应用不同, redis 作为一个内存密集型应用, 使用了绝无仅有的 &lt;strong&gt;多线程处理 IO、单线程处理核心逻辑&lt;/strong&gt; 的模式, 值得我们学习了解, 也为我们在日常工作中, 跳出传统思维框架, 紧贴自身业务特征做最合适的技术选型提供了启发与参考;&lt;/p&gt;
&lt;/blockquote&gt;
    
    </summary>
    
      <category term="redis" scheme="http://zshell.cc/categories/redis/"/>
    
    
      <category term="redis" scheme="http://zshell.cc/tags/redis/"/>
    
  </entry>
  
  <entry>
    <title>mysql json 查询</title>
    <link href="http://zshell.cc/2024/03/17/mysql--mysql_json%E6%9F%A5%E8%AF%A2/"/>
    <id>http://zshell.cc/2024/03/17/mysql--mysql_json查询/</id>
    <published>2024-03-16T17:00:32.000Z</published>
    <updated>2025-07-28T13:35:01.527Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>MySQL 从 5.7.8 版本开始支持原生 JSON 数据类型; 这是一种非常实用的类型, 允许我们免于设计复杂的表关系结构, 从而更加专注于业务本身;</p></blockquote><a id="more"></a><h2 id="查询指定的-json-字段"><a href="#查询指定的-json-字段" class="headerlink" title="查询指定的 json 字段"></a><strong>查询指定的 json 字段</strong></h2><p>json 内容举例:</p><figure class="highlight json"><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">&#123;</span><br><span class="line">    <span class="attr">"type"</span>: <span class="string">"XXX"</span>,</span><br><span class="line">    <span class="attr">"content"</span>: <span class="string">"YYY"</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>查询条件为 json 对象中指定字段是否为指定的值:</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="keyword">FROM</span> <span class="string">`table_name`</span> <span class="keyword">WHERE</span> <span class="string">`field_name`</span>-&gt;<span class="string">'$.type'</span> = <span class="string">'XXX'</span></span><br></pre></td></tr></table></figure><h2 id="查询-json-数组是否存在指定-value"><a href="#查询-json-数组是否存在指定-value" class="headerlink" title="查询 json 数组是否存在指定 value"></a><strong>查询 json 数组是否存在指定 value</strong></h2><p>json 内容举例:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">[1, 2, 3, 4, 5]</span><br></pre></td></tr></table></figure><p>查询条件为 json 数组中是否存在指定的值:</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">SELECT</span> * <span class="keyword">FROM</span> <span class="string">`table_name`</span> <span class="keyword">WHERE</span> json_contains(<span class="string">`field_name`</span>, <span class="string">'4'</span>)</span><br><span class="line"><span class="keyword">SELECT</span> * <span class="keyword">FROM</span> <span class="string">`table_name`</span> <span class="keyword">WHERE</span> json_contains(<span class="string">`field_name`</span>, json_array(<span class="number">1</span>, <span class="number">4</span>))</span><br></pre></td></tr></table></figure><h2 id="查询-json-对象是否存在指定-key"><a href="#查询-json-对象是否存在指定-key" class="headerlink" title="查询 json 对象是否存在指定 key"></a><strong>查询 json 对象是否存在指定 key</strong></h2><p>json 内容举例:</p><figure class="highlight json"><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">&#123;</span><br><span class="line">  <span class="attr">"type"</span> : <span class="string">"XXX"</span>,</span><br><span class="line">  <span class="attr">"content"</span> : &#123;</span><br><span class="line">    <span class="attr">"subField1"</span> : <span class="number">1</span>,</span><br><span class="line">    <span class="attr">"subField2"</span> : <span class="number">2</span>,</span><br><span class="line">    <span class="attr">"subField3"</span> : <span class="number">3</span>,</span><br><span class="line">    <span class="attr">"subField4"</span> : <span class="number">4</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>查询条件为 json 对象中是否存在指定的字段 (json_contains_path 函数第二个入参表示 anyMatch 或者 allMatch):</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 class="keyword">FROM</span> <span class="string">`table_name`</span> <span class="keyword">WHERE</span> json_contains_path(<span class="string">`field_name`</span> -&gt; <span class="string">'$.content'</span>, <span class="string">'one'</span>, <span class="string">'$."subField2"'</span>)</span><br><span class="line">// subField1 和 subField5 只要存在一个就匹配</span><br><span class="line"><span class="keyword">SELECT</span> * <span class="keyword">FROM</span> <span class="string">`table_name`</span> <span class="keyword">WHERE</span> json_contains_path(<span class="string">`field_name`</span> -&gt; <span class="string">'$.content'</span>, <span class="string">'one'</span>, <span class="string">'$."subField1"'</span>, <span class="string">'$."subField5"'</span>)</span><br><span class="line">// subField1 和 subField5 必须都存在才匹配</span><br><span class="line"><span class="keyword">SELECT</span> * <span class="keyword">FROM</span> <span class="string">`table_name`</span> <span class="keyword">WHERE</span> json_contains_path(<span class="string">`field_name`</span> -&gt; <span class="string">'$.content'</span>, <span class="string">'all'</span>, <span class="string">'$."subField1"'</span>, <span class="string">'$."subField5"'</span>)</span><br></pre></td></tr></table></figure><h2 id="拼接-json"><a href="#拼接-json" class="headerlink" title="拼接 json"></a><strong>拼接 json</strong></h2><p>语法: JSON_INSERT(json, path1, value1[, path2, value2] …)</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><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">select</span> <span class="keyword">version</span>, gmtModified, recordStatus, spec, recordType <span class="keyword">from</span> (</span><br><span class="line">    <span class="keyword">select</span>  revision <span class="keyword">as</span> <span class="keyword">version</span>, </span><br><span class="line">            gmt_modified <span class="keyword">as</span> gmtModified,</span><br><span class="line">            <span class="keyword">CASE</span> <span class="keyword">WHEN</span> record_status = <span class="string">'LATEST'</span> <span class="keyword">THEN</span> <span class="string">'LATEST'</span> <span class="keyword">ELSE</span> <span class="string">'OUTDATED'</span> <span class="keyword">END</span> <span class="keyword">as</span> recordStatus,</span><br><span class="line">            JSON_INSERT(<span class="string">'&#123;&#125;'</span>, <span class="string">'$.meta'</span>, api_meta, <span class="string">'$.traffic'</span>, traffic_distributions, <span class="string">'$.policies'</span>, policies) <span class="keyword">as</span> spec,</span><br><span class="line">            <span class="string">'draft'</span> <span class="keyword">as</span> <span class="keyword">type</span> </span><br><span class="line">    <span class="keyword">from</span> <span class="string">`ultramax_api_draft_history`</span> <span class="keyword">where</span> <span class="keyword">name</span> = <span class="string">'RPC^com.mtop.custom.type^1.0'</span></span><br><span class="line">    <span class="keyword">union</span> all</span><br><span class="line">    <span class="keyword">select</span>  <span class="keyword">version</span> <span class="keyword">as</span> <span class="keyword">version</span>,</span><br><span class="line">            gmt_modified <span class="keyword">as</span> gmtModified,</span><br><span class="line">            <span class="keyword">CASE</span> <span class="keyword">WHEN</span> record_status = <span class="string">'VALID'</span> <span class="keyword">THEN</span> <span class="string">'LATEST'</span> <span class="keyword">ELSE</span> <span class="string">'OUTDATED'</span> <span class="keyword">END</span> <span class="keyword">as</span> recordStatus,</span><br><span class="line">            JSON_INSERT(<span class="string">'&#123;&#125;'</span>, <span class="string">'$.meta'</span>, api_spec, <span class="string">'$.traffic'</span>, traffic_distributions, <span class="string">'$.policies'</span>, plan) <span class="keyword">as</span> spec,</span><br><span class="line">            <span class="string">'daily_snapshot'</span> <span class="keyword">as</span> <span class="keyword">type</span></span><br><span class="line">    <span class="keyword">from</span> <span class="string">`ultramax_api_snapshot_history_daily`</span> <span class="keyword">WHERE</span> <span class="string">`name`</span> = <span class="string">'RPC^com.mtop.custom.type^1.0'</span></span><br><span class="line">  ) <span class="keyword">as</span> <span class="keyword">merge</span></span><br><span class="line"><span class="keyword">ORDER</span> <span class="keyword">BY</span> t <span class="keyword">desc</span>, v <span class="keyword">desc</span></span><br><span class="line"><span class="keyword">limit</span> <span class="number">10</span></span><br></pre></td></tr></table></figure><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a><strong>参考资料</strong></h2><ul><li><a href="https://blog.csdn.net/qq_58772217/article/details/125219985" target="_blank" rel="noopener">MySQL的json查询之json_contains、json_contains_path</a></li></ul>]]></content>
    
    <summary type="html">
    
      &lt;blockquote&gt;
&lt;p&gt;MySQL 从 5.7.8 版本开始支持原生 JSON 数据类型; 这是一种非常实用的类型, 允许我们免于设计复杂的表关系结构, 从而更加专注于业务本身;&lt;/p&gt;
&lt;/blockquote&gt;
    
    </summary>
    
      <category term="mysql" scheme="http://zshell.cc/categories/mysql/"/>
    
    
      <category term="mysql" scheme="http://zshell.cc/tags/mysql/"/>
    
      <category term="json" scheme="http://zshell.cc/tags/json/"/>
    
  </entry>
  
  <entry>
    <title>hbase 的数据存储结构</title>
    <link href="http://zshell.cc/2024/02/12/nosql-hbase--hbase%E7%9A%84%E6%95%B0%E6%8D%AE%E5%AD%98%E5%82%A8%E7%BB%93%E6%9E%84/"/>
    <id>http://zshell.cc/2024/02/12/nosql-hbase--hbase的数据存储结构/</id>
    <published>2024-02-12T01:54:09.000Z</published>
    <updated>2025-07-28T13:35:01.529Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>之前对列式存储接触的不太多, 我想从 hbase 入手, 学习一下列式数据库的基本原理和存储结构;</p></blockquote><a id="more"></a><h2 id="逻辑结构"><a href="#逻辑结构" class="headerlink" title="逻辑结构"></a><strong>逻辑结构</strong></h2><p>hbase 不是传统的列式存储结构, 而是按列族聚合的存储结构;</p><p>hbase 表的行结构:</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"># 按照 rowkey 的字典序升序排列</span></span><br><span class="line">(rowkey, columnFamily, column) -&gt; (value, timestamp)</span><br></pre></td></tr></table></figure><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">SortedMap&lt;RowKey, List&lt;SortedMap&lt;Column, List&lt;value, Timestamp&gt;&gt;&gt;&gt;</span><br></pre></td></tr></table></figure><h2 id="详细示例"><a href="#详细示例" class="headerlink" title="详细示例"></a><strong>详细示例</strong></h2><h3 id="逻辑数据"><a href="#逻辑数据" class="headerlink" title="逻辑数据"></a><strong>逻辑数据</strong></h3><table><thead><tr><th>RowKey</th><th>name</th><th>age</th><th>email</th><th>phone</th><th>last_login</th><th>login_count</th></tr></thead><tbody><tr><td>user001</td><td>张三</td><td>28</td><td><a href="mailto:&#x7a;&#115;&#64;&#x65;&#108;&#101;&#x2e;&#x63;&#111;&#109;" target="_blank" rel="noopener">&#x7a;&#115;&#64;&#x65;&#108;&#101;&#x2e;&#x63;&#111;&#109;</a></td><td>13800138001</td><td>10:30:00</td><td>42</td></tr><tr><td>user002</td><td>李四</td><td>32</td><td><a href="mailto:&#x6c;&#115;&#x40;&#101;&#x6c;&#101;&#46;&#99;&#111;&#x6d;" target="_blank" rel="noopener">&#x6c;&#115;&#x40;&#101;&#x6c;&#101;&#46;&#99;&#111;&#x6d;</a></td><td>13900139002</td><td>14:15:00</td><td>27</td></tr></tbody></table><h3 id="表结构"><a href="#表结构" class="headerlink" title="表结构"></a><strong>表结构</strong></h3><p>列族定义:</p><ul><li>basic_info (基本信息)<ul><li>列: name, age</li></ul></li><li>contact_info (联系信息)<ul><li>列: email, phone</li></ul></li><li>activity_info (活动信息)<ul><li>列: last_login, login_count</li></ul></li></ul><p>建表语句:</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">create <span class="string">'my_table'</span>, &#123;NAME =&gt; <span class="string">'basic_info'</span>, VERSIONS =&gt; 1&#125;, &#123;NAME =&gt; <span class="string">'contact_info'</span>, VERSIONS =&gt; 1&#125;, &#123;NAME =&gt; <span class="string">'activity_info'</span>, VERSIONS =&gt; 1&#125;</span><br></pre></td></tr></table></figure><h3 id="物理存储的逻辑结构"><a href="#物理存储的逻辑结构" class="headerlink" title="物理存储的逻辑结构"></a><strong>物理存储的逻辑结构</strong></h3><p>hbase 在磁盘上会<strong>按列族分开存储</strong>, 每个列族对应一个独立的存储文件 (HFile), 以下是实际存储的逻辑视图:</p><ul><li><p>列族 basic_info 的存储文件</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">RowKey    | Column Qualifier | Timestamp     | Value</span><br><span class="line">----------|------------------|---------------|---------</span><br><span class="line">user001   | name             | 1692066600000 | 张三</span><br><span class="line">user001   | age              | 1692066600000 | 28</span><br><span class="line">user002   | name             | 1692159300000 | 李四</span><br><span class="line">user002   | age              | 1692159300000 | 32</span><br></pre></td></tr></table></figure></li><li><p>列族 contact_info 的存储文件</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">RowKey    | Column Qualifier | Timestamp     | Value</span><br><span class="line">----------|------------------|---------------|-----------------------</span><br><span class="line">user001   | email            | 1692066600000 | zs@ele.com</span><br><span class="line">user001   | phone            | 1692066600000 | 13800138001</span><br><span class="line">user002   | email            | 1692159300000 | ls@ele.com</span><br><span class="line">user002   | phone            | 1692159300000 | 13900139002</span><br></pre></td></tr></table></figure></li><li><p>列族 activity_info 的存储文件</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">RowKey    | Column Qualifier | Timestamp     | Value</span><br><span class="line">----------|------------------|---------------|-----------------------</span><br><span class="line">user001   | last_login       | 1692066600000 | 10:30:00</span><br><span class="line">user001   | login_count      | 1692066600000 | 42</span><br><span class="line">user002   | last_login       | 1692159300000 | 14:15:00</span><br><span class="line">user002   | login_count      | 1692159300000 | 27</span><br></pre></td></tr></table></figure></li></ul><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a><strong>参考资料</strong></h2><ul><li><a href="https://juejin.cn/post/6911885877408956429" target="_blank" rel="noopener">HBase 存储原理</a></li></ul>]]></content>
    
    <summary type="html">
    
      &lt;blockquote&gt;
&lt;p&gt;之前对列式存储接触的不太多, 我想从 hbase 入手, 学习一下列式数据库的基本原理和存储结构;&lt;/p&gt;
&lt;/blockquote&gt;
    
    </summary>
    
      <category term="nosql" scheme="http://zshell.cc/categories/nosql/"/>
    
      <category term="hbase" scheme="http://zshell.cc/categories/nosql/hbase/"/>
    
    
      <category term="nosql" scheme="http://zshell.cc/tags/nosql/"/>
    
      <category term="hbase" scheme="http://zshell.cc/tags/hbase/"/>
    
      <category term="列式存储" scheme="http://zshell.cc/tags/%E5%88%97%E5%BC%8F%E5%AD%98%E5%82%A8/"/>
    
  </entry>
  
  <entry>
    <title>mysql explain 执行计划</title>
    <link href="http://zshell.cc/2024/01/06/mysql-index--mysql_explain%E6%89%A7%E8%A1%8C%E8%AE%A1%E5%88%92/"/>
    <id>http://zshell.cc/2024/01/06/mysql-index--mysql_explain执行计划/</id>
    <published>2024-01-06T04:20:44.000Z</published>
    <updated>2025-07-28T13:35:01.528Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>mysql 提供的 explain 执行计划能力是我们做 sql 调优的好帮手, 甚至可以说是我们观察 sql 执行效率的重要理论依据;<br>学好并用好 mysql explain 可以方便地写出高效率的 sql;</p></blockquote><a id="more"></a><h2 id="explain-详细说明"><a href="#explain-详细说明" class="headerlink" title="explain 详细说明"></a><strong>explain 详细说明</strong></h2><h3 id="type"><a href="#type" class="headerlink" title="type"></a><strong>type</strong></h3><p>全称 <code>join type</code>, 表示查询访问类型, 共存在如下几种枚举类型 (性能由好到坏排序):</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">system &gt; const &gt; eq_ref &gt; ref &gt; fulltext &gt; ref_or_null &gt; index_merge &gt; unique_subquery &gt; index_subquery &gt; range &gt; index &gt; all</span><br></pre></td></tr></table></figure><p>以下各列按照好坏顺序从上到下排列:</p><table><thead><tr><th align="center">名称 (由好到坏排序)</th><th align="center">含义</th></tr></thead><tbody><tr><td align="center">system</td><td align="center">系统表, 只有一行记录</td></tr><tr><td align="center">const</td><td align="center">索引一次即找到, 表示使用了<strong>主键</strong>索引</td></tr><tr><td align="center">eq_ref</td><td align="center">使用了<strong>唯一</strong>索引</td></tr><tr><td align="center">ref</td><td align="center">使用了<strong>普通</strong>索引</td></tr><tr><td align="center">fulltext</td><td align="center"></td></tr><tr><td align="center">ref_or_null</td><td align="center">表示使用了<strong>普通</strong>索引, 同时使用了 <strong>is null</strong></td></tr><tr><td align="center">index_merge</td><td align="center"></td></tr><tr><td align="center">unique_subquery</td><td align="center"></td></tr><tr><td align="center">index_subquery</td><td align="center"></td></tr><tr><td align="center">range</td><td align="center">范围搜索 (一定用到了索引, 但存在范围匹配条件, 如 &lt;, &gt;, in, or, between..and..)</td></tr><tr><td align="center">index</td><td align="center">按索引顺序全表扫描</td></tr><tr><td align="center">all</td><td align="center">全表扫描</td></tr></tbody></table><p>index 和 all 的区别:</p><ul><li>all: 不使用索引的全表扫描;</li><li>index: 根据索引先查询到有序排列的所有 id, 然后依次回表获取数据;</li></ul><p>当需要排序时, index 的效率优于 all;</p><h3 id="possible-keys"><a href="#possible-keys" class="headerlink" title="possible_keys"></a><strong>possible_keys</strong></h3><p>当查询涉及的字段只要存在索引, 将会在 possible_keys 中被列出, 但不一定被实际使用到</p><h3 id="key"><a href="#key" class="headerlink" title="key"></a><strong>key</strong></h3><p>表示查询中实际使用的索引, 如果为 NULL, 表示实际没有使用索引;</p><h3 id="key-len"><a href="#key-len" class="headerlink" title="key_len"></a><strong>key_len</strong></h3><p>表示索引中用到的字节数;</p><h3 id="ref"><a href="#ref" class="headerlink" title="ref"></a><strong>ref</strong></h3><p>表示索引的具体哪一列被使用了;</p><h3 id="rows"><a href="#rows" class="headerlink" title="rows"></a><strong>rows</strong></h3><p>表示 mysql 预估认为它执行查询时必须扫描的行数;</p><h3 id="filtered"><a href="#filtered" class="headerlink" title="filtered"></a><strong>filtered</strong></h3><p>表示存储引擎返回的数据在 server 层过滤后, 剩下多少满足查询的记录数量的百分比;</p><h3 id="Extra"><a href="#Extra" class="headerlink" title="Extra"></a><strong>Extra</strong></h3><h2 id="参考链接"><a href="#参考链接" class="headerlink" title="参考链接"></a><strong>参考链接</strong></h2><ul><li><a href="https://www.cnblogs.com/yhtboke/p/9467763.html" target="_blank" rel="noopener">SQL执行计划详解explain</a></li><li><a href="https://blog.csdn.net/zhiwei_bian/article/details/105817821" target="_blank" rel="noopener">mysql中explain的各个type的含义</a></li><li><a href="https://blog.csdn.net/qq_39311377/article/details/133614068" target="_blank" rel="noopener">Explain执行计划字段解释说明—possible_keys、key、key_len、ref、rows、filtered字段说明</a></li><li><a href="https://blog.csdn.net/DrMickeys/article/details/128960863" target="_blank" rel="noopener">const、eq_ref、ref、ref_or_null、index、all的简介</a></li></ul>]]></content>
    
    <summary type="html">
    
      &lt;blockquote&gt;
&lt;p&gt;mysql 提供的 explain 执行计划能力是我们做 sql 调优的好帮手, 甚至可以说是我们观察 sql 执行效率的重要理论依据;&lt;br&gt;学好并用好 mysql explain 可以方便地写出高效率的 sql;&lt;/p&gt;
&lt;/blockquote&gt;
    
    </summary>
    
      <category term="mysql" scheme="http://zshell.cc/categories/mysql/"/>
    
      <category term="index" scheme="http://zshell.cc/categories/mysql/index/"/>
    
    
      <category term="mysql" scheme="http://zshell.cc/tags/mysql/"/>
    
      <category term="索引" scheme="http://zshell.cc/tags/%E7%B4%A2%E5%BC%95/"/>
    
  </entry>
  
  <entry>
    <title>k8s 资源在 etcd 的存储结构</title>
    <link href="http://zshell.cc/2023/12/23/k8s--k8s%E8%B5%84%E6%BA%90%E5%9C%A8etcd%E7%9A%84%E5%AD%98%E5%82%A8%E7%BB%93%E6%9E%84/"/>
    <id>http://zshell.cc/2023/12/23/k8s--k8s资源在etcd的存储结构/</id>
    <published>2023-12-22T16:46:27.000Z</published>
    <updated>2025-07-28T13:35:01.514Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>我们平常用惯了 k8s 提供的 sdk, 却很少了解 k8s 是如何存储我们提交给它的资源配置的;<br>其实 k8s 充分运用了 etcd 的前缀查询能力, 以前缀对齐匹配的模式存储 &#x2F; 查询不同类型的 k8s 资源;</p></blockquote><a id="more"></a><h2 id="Key-的存储"><a href="#Key-的存储" class="headerlink" title="Key 的存储"></a><strong>Key 的存储</strong></h2><p>Kubernetes 将所有资源 (如 Pod、Service、Ingress 等) 以 Key-Value 的形式存储在 etcd 中;<br>除了特殊的 minions、ranges 之外, 其余资源存储的 Key 结构如下:</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">/registry/&lt;资源类型&gt;/&lt;命名空间&gt;/&lt;资源名称&gt;</span><br></pre></td></tr></table></figure><ul><li><strong>&#x2F;registry</strong>: Kubernetes 资源的根路径;</li><li><strong>&lt;资源类型&gt;</strong>: 资源类型的<strong>复数形式</strong>, 例如 <code>ingresses</code>、<code>pods</code>、<code>services</code>、<code>ranges</code> 等;</li><li><strong>&lt;命名空间&gt;</strong>: 资源所属的命名空间 (Namespace), 如果是集群级别的资源 (如 Node), 则省略命名空间部分;</li><li><strong>&lt;资源名称&gt;</strong>: 资源的名称;</li></ul><h3 id="特殊场景"><a href="#特殊场景" class="headerlink" title="特殊场景"></a><strong>特殊场景</strong></h3><ul><li>minions 是 node 信息, Kubernetes 之前节点叫 minion, 没有改过来, 因此依然使用: <code>/registry/minions</code>;</li><li>ranges 对应 Service 网段以及 NodePort 端口范围:<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">&gt; ./etcd_ls.sh /registry/ranges</span><br><span class="line">/registry/ranges/serviceips</span><br><span class="line">/registry/ranges/servicenodeports</span><br></pre></td></tr></table></figure></li></ul><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></pre></td><td class="code"><pre><span class="line">&gt; ./etcd_ls.sh /registry/ranges/servicenodeports | strings</span><br><span class="line">/registry/ranges/servicenodeports</span><br><span class="line">RangeAllocation</span><br><span class="line">30000-32767</span><br></pre></td></tr></table></figure><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></pre></td><td class="code"><pre><span class="line">&gt; ./etcd_ls.sh /registry/ranges/serviceips | strings</span><br><span class="line">/registry/ranges/serviceips</span><br><span class="line">RangeAllocation</span><br><span class="line">10.96.0.0/12</span><br></pre></td></tr></table></figure><h3 id="典型示例"><a href="#典型示例" class="headerlink" title="典型示例"></a><strong>典型示例</strong></h3><p>假设有一个 Ingress 资源:</p><ul><li>namespace: <code>alibaba-tao</code></li><li>name: <code>tao-acs-m-taobao-com</code></li></ul><p>那么它在 etcd 中的存储 Key 为:</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">/registry/ingresses/alibaba-tao/tao-acs-m-taobao-com</span><br></pre></td></tr></table></figure><h2 id="Value-的存储"><a href="#Value-的存储" class="headerlink" title="Value 的存储"></a><strong>Value 的存储</strong></h2><p>etcd 中存储的 Value 是资源的 <strong>序列化 JSON 或 Protobuf 数据</strong>:</p><ul><li>&#x2F;registry&#x2F;apiregistration.k8s.io: 直接存储 JSON 格式;<ul><li>API Registration‌是 Kubernetes 中用于注册和管理自定义资源定义 (Custom Resource Definitions, CRDs) 的一种机制;</li></ul></li><li>其余资源: 默认存储 Protobuf 格式, 以提升性能;</li></ul><p>如想改变默认存储格式:</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">--storage-media-type=application/json</span><br></pre></td></tr></table></figure><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a><strong>参考资料</strong></h2><ul><li><a href="https://www.modb.pro/db/226813" target="_blank" rel="noopener">如何读取Kubernetes存储在etcd上的数据</a></li></ul>]]></content>
    
    <summary type="html">
    
      &lt;blockquote&gt;
&lt;p&gt;我们平常用惯了 k8s 提供的 sdk, 却很少了解 k8s 是如何存储我们提交给它的资源配置的;&lt;br&gt;其实 k8s 充分运用了 etcd 的前缀查询能力, 以前缀对齐匹配的模式存储 &amp;#x2F; 查询不同类型的 k8s 资源;&lt;/p&gt;
&lt;/blockquote&gt;
    
    </summary>
    
      <category term="k8s" scheme="http://zshell.cc/categories/k8s/"/>
    
    
      <category term="k8s" scheme="http://zshell.cc/tags/k8s/"/>
    
      <category term="etcd" scheme="http://zshell.cc/tags/etcd/"/>
    
  </entry>
  
  <entry>
    <title>Resource 与 Autowired 的区别</title>
    <link href="http://zshell.cc/2023/11/25/spring-beans--Resource%E4%B8%8EAutowired%E7%9A%84%E5%8C%BA%E5%88%AB/"/>
    <id>http://zshell.cc/2023/11/25/spring-beans--Resource与Autowired的区别/</id>
    <published>2023-11-25T01:11:33.000Z</published>
    <updated>2025-07-28T13:35:01.534Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>转自大钉钉应用研发平台的处轩同学, 稍加整理与补充, 原文链接: <a href="https://baijiahao.baidu.com/s?id=1762105096646943310" target="_blank" rel="noopener">既生@Resource，何生@Autowired，是Spring官方没事做？</a></p></blockquote><a id="more"></a><h2 id="官方定义"><a href="#官方定义" class="headerlink" title="官方定义"></a><strong>官方定义</strong></h2><h3 id="javax-annotation-Resource"><a href="#javax-annotation-Resource" class="headerlink" title="javax.annotation.Resource"></a><strong>javax.annotation.Resource</strong></h3><p>@Resource 于 2006 随着 JSR-250 发布, 官方解释是:</p><blockquote><p>Resource 注释标记了应用程序需要的资源; 该注解可以应用于应用程序组件类, 或组件类的字段或方法; 当注解应用于字段或方法时, 容器将在组件初始化时将所请求资源的实例注入到应用程序组件中; 如果注释应用于组件类, 则注释声明应用程序将在运行时查找的资源;</p></blockquote><p>可以看到它是一个通用定义, 由第三方组件或框架自主实现;</p><h3 id="org-springframework-beans-factory-annotation-Autowired"><a href="#org-springframework-beans-factory-annotation-Autowired" class="headerlink" title="org.springframework.beans.factory.annotation.Autowired"></a><strong>org.springframework.beans.factory.annotation.Autowired</strong></h3><p>@Autowired 于 2007 年随着 spring 2.5 发布, 同时官方也对 @Resource 进行了支持; @Autowired 的官方解释是:</p><blockquote><p>将构造函数、字段、设置方法或配置方法标记为由 spring 的依赖注入工具自动装配;</p></blockquote><p>可以看到, @Autowired 是 spring 的亲儿子, 而 @Resource 是 spring 对它定义的一种实现, 它们的功能如此相似; 那么为什么要支持了 @Resource, 又要自己搞个 @Autowired 呢?<br>﻿<br>为此专门查了一下 spring 2.5 的官方文档, 文档中有一段这么说到:</p><blockquote><p>However, Spring 2.5 dramatically changes the landscape. As described above, the autowiring choices have now been extended with support for the JSR-250 @Resource annotation to enable autowiring of named resources on a per-method or per-field basis. However, the @Resource annotation alone does have some limitations. Spring 2.5 therefore introduces an @Autowired annotation to further increase the level of control.</p></blockquote><p>大概的意思是说, spring 2.5 开始支持注解自动装配, 现已经支持 JSR-250 @Resource 基于每个方法或每个字段的命名资源的自动装配, 但是只有 @Resource 是不行的, 我们还推出了 “粒度” 更大的 @Autowired, 来覆盖更多场景;<br>那么 “粒度 “指的是什么呢?﻿</p><h2 id="本质区别"><a href="#本质区别" class="headerlink" title="本质区别"></a><strong>本质区别</strong></h2><p>要想找到粒度是什么, 我们先从两个注解的功能下手:</p><ul><li>@Autowired: 按照类型注入</li><li>@Resource: 按照名字注入优先, 若找不到再根据名字找类型</li></ul><p>若要论功能的 “粒度”, 按理说 @Resource 已经包含 @Autowired 了, 难道是 spring 2.5 的时候还不支持? 再次翻阅 spring 2.5 的文档, 上面明确的写到 @Resource 的功能和现在是一致的:</p><blockquote><p>When using @Resource without an explicitly provided name, if no Spring-managed object is found for the default name, the injection mechanism will fallback to a type-match.</p></blockquote><p>那么 “粒度” 到底指的是什么? stackoverflow 有一个回答似乎指出了关键所在:</p><blockquote><p>Both @Autowired and @Resource work equally well. But there is a conceptual difference or a difference in the meaning:<br>– @Resource means get me a known resource by name. The name is extracted from the name of the annotated setter or field, or it is taken from the name-Parameter.<br>– @Inject or @Autowired try to wire in a suitable other component by type.<br>So, basically these are two quite distinct concepts. Unfortunately the Spring-Implementation of @Resource has a built-in fallback, which kicks in when resolution by-name fails. In this case, it falls back to the @Autowired-kind resolution by-type. While this fallback is convenient, IMHO it causes a lot of confusion. because people are unaware of the conceptual difference and tend to use @Resource for type-based autowiring.</p></blockquote><p>spring 虽然实现了两个类似的功能, 但是存在概念或含义上的差异:</p><ul><li>@Autowired 尝试按类型匹配合适的组件;</li><li>@Resource 意图按名称直接获取一个确定的资源, 尽管当 @Resource 按名称解析失败时它会按类型解析, 但这只是 spring 提供给开发者的一种便利, 甚至不排除在未来的 spring 版本中取消这一特性;</li></ul><p>所以 Spring 官方说的粒度是指 “资源范围”:</p><ul><li>@Resource 默认寻找的是确定的的资源, 相当于给你一个坐标, 你直接去获取;</li><li>@Autowired 则是在一片区域里面尝试搜索并匹配合适的资源;</li></ul><h3 id="如何佐证"><a href="#如何佐证" class="headerlink" title="如何佐证"></a><strong>如何佐证</strong></h3><p>stackoverflow 的回答很有道理, 但毕竟不是官方正式说明, 如果能从其他角度找到一些证据, 将更有说服力;</p><p><strong>证据一: 集合注入场景的区别</strong></p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://github.com/zshell-zhang/static-content/raw/master/cs/spring/resource_autowired/1.png" alt="不同注解集合注入的区别" title="">                </div>                <div class="image-caption">不同注解集合注入的区别</div>            </figure><p>使用 @Autowired 在左侧就有 IEAD 的小绿标, 而使用 @Resource 就不会有 (尽管 @Resource 最终也能生效), 因为本质上集合注入不是单一、也是不确定性的;<br>@Resource 没有出现小绿标证明了其本意并非要做基于类型的搜索及匹配;</p><p><strong>证据二: 使用 @Primary 场景的区别</strong><br>当存在两个类型相同的 spring bean, 若不以 bean name 加以区分, 在使用 @Resource 注入时便会报 NoUniqueBeanDefinitionException; 尽管 @Autowired 注入也一样存在该问题, 不过只要使用 @Primary 注解指定其中一个 bean 为优先的, @Autowired 就能使用该优先 bean 注入成功; 但是 @Resource 却不能识别 @Primary 注解并匹配到高优先级的 bean, 这直接证明了其按类型匹配的能力相比于 @Autowired 也是不完整的;<br>﻿<br>在理解了其中本质区别后, 就自然引出了另外两个问题:<br>spring 为什么要支持两个功能相似的注解呢？</p><ul><li>为了方便其他框架迁移: @Resource 是一种通用规范, 只要符合 JSR-250 的其他框架, spring 就可以兼容;</li></ul><p>既然 @Resource 更倾向于找已知资源, 为什么也有按类型注入的功能?</p><ul><li>为了全面兼容从其他框架切换到 spring, 开发者就算只使用 @Resource 也能保持 spring 强大的依赖注入功能;</li></ul><h2 id="最佳实践"><a href="#最佳实践" class="headerlink" title="最佳实践"></a><strong>最佳实践</strong></h2><p>在日常写代码的时候, 使用 @Autowired 进行属性注入的时候 IDEA 会报出黄色警告, 并且推荐我们使用构造方法注入, 这是为什么呢?</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://github.com/zshell-zhang/static-content/raw/master/cs/spring/resource_autowired/2.png" alt="@Autowired 报警" title="">                </div>                <div class="image-caption">@Autowired 报警</div>            </figure><p>主要原因有几点:</p><ol><li><strong>基于属性的依赖注入不适用于声明为 final 的字段</strong><br>因为此字段必须在类实例化时才能实例化; 声明不可变依赖项的唯一方法是使用基于构造函数的依赖项注入;</li><li><strong>容易忽视类的单一原则</strong><br>一个类应该只负责软件应用程序功能的单个部分, 并且它的所有服务都应该与该职责紧密结合; 如果使用属性的依赖注入, 在你的类中很容易有很多依赖, 一切看起来都很正常; 但是如果改用基于构造函数的依赖注入, 随着更多的依赖被添加到你的类中, 构造函数会变得越来越大, 代码开始就开始出现异味, 发出明确的信号表明有问题; 具有超过十个参数的构造函数清楚地表明该类有太多的依赖, 让你不得不注意该类的单一问题了; 因此属性注入虽然不直接打破单一原则, 但它却会诱导你忽视单一原则;</li><li><strong>不易发现循环依赖</strong><br>A 类通过构造函数注入需要 B 类的实例, B 类通过构造函数注入需要 A 类的实例; 如果你为类 A 和 B 配置 bean 以相互注入, 使用构造方法就能很快发现;</li><li><strong>依赖赋值强依赖 IoC 容器</strong><br>如果您想在容器之外使用这的类, 例如用于单元测试, 不得不使用 spring 容器来实例化它, 因为没有其他可能的方法 (除了反射) 来设置自动装配的字段;</li></ol><p>但为什么使用 @Resource 进行属性注入没有 IDEA 的黄色告警呢?</p><ul><li>@Autowired 是 spring 提供的, 无法在其他 IoC 框架内支持; 所以当使用 @Autowired, IDEA 判断当前项目没有迁移到其他 IoC 框架的可能性, 那么 IDEA 便建议开发者使用 spring 框架下更优的解决方案 (构造器注入);</li><li>而 @Resource 是 JSR-250 标准, 当使用 @Resource, IDEA 判断当前项目存在切换到其他 IoC 容器的可能性; 在不知道其他 IoC 容器是否支持构造器注入的情况下, IDEA 不会冒然建议开发者更改注入方式;</li></ul><h3 id="构造器注入的简化"><a href="#构造器注入的简化" class="headerlink" title="构造器注入的简化"></a><strong>构造器注入的简化</strong></h3><p>尽管 IDEA 不会对使用 @Resource 的属性注入给出黄色警告, 但毕竟属性注入有大量缺点, 所以我们也不建议使用 @Resource, 而是建议使用构造器注入;<br>有人会说, 使用构造器注入相比属性注入, 需要额外写一个有参构造方法, 造成了不必要的工作量; 其实这个问题可以用 lombok 解决:</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></pre></td><td class="code"><pre><span class="line"><span class="meta">@Service</span></span><br><span class="line"><span class="meta">@RequiredArgsConstructor</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">XXXService</span> </span>&#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> AService aService;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> BService bService;</span><br><span class="line">    </span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">xxxService</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        aService.method1();</span><br><span class="line">        bService.method2();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>需要注意的是: 以上方法尽管避免了有参构造器的显式编码, 但却再次引入了与属性注入相同的两个问题:</p><ul><li>容易忽视类的单一原则;</li><li>不易发现循环依赖;</li></ul><p>所以在项目中究竟要采用何种方案, 就见仁见智了!</p><h2 id="参考链接"><a href="#参考链接" class="headerlink" title="参考链接"></a><strong>参考链接</strong></h2><ul><li><a href="https://baijiahao.baidu.com/s?id=1762105096646943310" target="_blank" rel="noopener">既生@Resource, 何生@Autowired?</a></li><li><a href="https://stackoverflow.com/questions/4093504/resource-vs-autowired" target="_blank" rel="noopener">@Resource vs @Autowired</a></li></ul>]]></content>
    
    <summary type="html">
    
      &lt;blockquote&gt;
&lt;p&gt;转自大钉钉应用研发平台的处轩同学, 稍加整理与补充, 原文链接: &lt;a href=&quot;https://baijiahao.baidu.com/s?id=1762105096646943310&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;既生@Resource，何生@Autowired，是Spring官方没事做？&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
    
    </summary>
    
      <category term="spring" scheme="http://zshell.cc/categories/spring/"/>
    
      <category term="beans" scheme="http://zshell.cc/categories/spring/beans/"/>
    
    
      <category term="spring" scheme="http://zshell.cc/tags/spring/"/>
    
  </entry>
  
</feed>
