<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://cafewang.github.io//feed.xml" rel="self" type="application/atom+xml" /><link href="https://cafewang.github.io//" rel="alternate" type="text/html" /><updated>2026-05-24T14:49:43+00:00</updated><id>https://cafewang.github.io//feed.xml</id><title type="html">cafewang的魔法小屋</title><subtitle>整点能看懂的技术内容💻</subtitle><author><name>cafewang</name></author><entry><title type="html">RustLiveExample-所有权</title><link href="https://cafewang.github.io//rust-live-example/ownership-explained" rel="alternate" type="text/html" title="RustLiveExample-所有权" /><published>2026-04-28T14:55:00+00:00</published><updated>2026-04-28T14:55:00+00:00</updated><id>https://cafewang.github.io//rust-live-example/rust-ownership</id><content type="html" xml:base="https://cafewang.github.io//rust-live-example/ownership-explained"><![CDATA[<p>所有权是Rust语言的核心概念，也是Rust语言的难点，正确理解所有权，是读懂和写好Rust代码的必要条件。<br />
文章主要参考了<code class="language-plaintext highlighter-rouge">Programming Rust</code>一书，需要更细致地解读请参考该书。</p>

<h2 id="无所不在的所有权">无所不在的所有权</h2>
<p>所有权，简单来说，就是同一个值(在堆或栈中)赋值给多个变量，这些变量会如何处理这个值。<br />
以一段C代码来举例</p>

<script src="https://gist.github.com/cafewang/6ff4386626d5bba5097f5466d38ab5df.js"></script>

<iframe frameborder="0" height="350px" scrolling="no" src="https://onecompiler.com/embed/c/44mq8gcx7?hideLanguageSelection=true&amp;hideNew=true&amp;hideNewFileOption=true&amp;disableCopyPaste=true&amp;disableAutoComplete=true&amp;hideStdin=true&amp;hideEditorOptions=true" width="800px">
</iframe>

<h2 id="move">Move</h2>

<h2 id="copy">Copy</h2>

<h2 id="reference-counter">Reference Counter</h2>]]></content><author><name>cafewang</name></author><category term="rust" /><category term="ownership" /><summary type="html"><![CDATA[所有权是Rust语言的核心概念，也是Rust语言的难点，正确理解所有权，是读懂和写好Rust代码的必要条件。 文章主要参考了Programming Rust一书，需要更细致地解读请参考该书。]]></summary></entry><entry><title type="html">Linux信号机制</title><link href="https://cafewang.github.io//linux-intro/signal" rel="alternate" type="text/html" title="Linux信号机制" /><published>2026-04-08T12:55:00+00:00</published><updated>2026-04-08T12:55:00+00:00</updated><id>https://cafewang.github.io//linux-intro/linux-signal</id><content type="html" xml:base="https://cafewang.github.io//linux-intro/signal"><![CDATA[<p>信号（Signal）是 Linux 系统中最古老、最基础的进程间通信机制之一，也是理解 Unix/Linux 系统编程的核心概念。作为一种异步通知机制，信号允许内核或一个进程向另一个进程发送简短的通知，告知其某个特定事件的发生——无论是用户按下 Ctrl+C 中断程序，还是子进程终止通知父进程，亦或是定时器到期提醒应用程序。</p>

<p>掌握信号机制对于深入理解 Linux 系统至关重要：它不仅是进程管理和控制的基础，更是编写健壮、可靠的系统级程序的必备知识。本文将带你从零开始，系统地探索 Linux 信号的方方面面。</p>

<h2 id="信号触发">信号触发</h2>
<p>首先，信号分为两类</p>
<ul>
  <li>standard signal，较早实现的传统信号，范围是1~31，通常有默认的处理机制，如关闭程序、忽略信号。</li>
  <li>real-time signal，实时信号是后来扩展的信号类型，范围与系统实现有关，宏定义是<code class="language-plaintext highlighter-rouge">SIGRTMIN</code>到<code class="language-plaintext highlighter-rouge">SIGRTMAX</code>，默认处理机制是关闭程序。</li>
</ul>

<p>触发信号的方式有很多种，我们列举如下几种常用的方式</p>

<h3 id="进程给自己发送信号">进程给自己发送信号</h3>

<iframe frameborder="0" height="300px" scrolling="no" src="https://onecompiler.com/embed/c/44jrszjej?hideLanguageSelection=true&amp;hideNew=true&amp;hideNewFileOption=true&amp;disableCopyPaste=true&amp;disableAutoComplete=true&amp;hideStdin=true&amp;hideEditorOptions=true" width="100%">
</iframe>

<p>我们使用<code class="language-plaintext highlighter-rouge">raise</code>发送了SIGKILL信号给当前进程，程序会直接退出，所以只打印了<code class="language-plaintext highlighter-rouge">before SIGKILL</code></p>
<h3 id="给其他进程发送信号">给其他进程发送信号</h3>
<iframe frameborder="0" height="300px" scrolling="no" src="https://onecompiler.com/embed/c/44jrtufs4?hideLanguageSelection=true&amp;hideNew=true&amp;hideNewFileOption=true&amp;disableCopyPaste=true&amp;disableAutoComplete=true&amp;hideStdin=true&amp;hideEditorOptions=true" width="100%">
</iframe>

<p>使用<code class="language-plaintext highlighter-rouge">kill</code>指定PID发送信号，还可以发送给所有有权限发信号的进程，也可以发送给进程组，后两种情况请参考手册</p>

<h3 id="给线程发送信号">给线程发送信号</h3>

<iframe frameborder="0" height="750px" scrolling="no" src="https://onecompiler.com/embed/c/44jrudkze?hideLanguageSelection=true&amp;hideNew=true&amp;hideNewFileOption=true&amp;disableCopyPaste=true&amp;disableAutoComplete=true&amp;hideStdin=true&amp;hideEditorOptions=true" width="100%">
</iframe>

<p>线程的信号处理方式与进程的信号处理方式类似，使用<code class="language-plaintext highlighter-rouge">pthread_kill</code>发送信号给当前进程中的线程，使得子线程被关闭，代码的主要流程为</p>
<ol>
  <li>主线程注册了信号处理函数，被子线程继承</li>
  <li>子线程使用<code class="language-plaintext highlighter-rouge">pause</code>阻塞等待信号到达</li>
  <li>主线程发送信号给子线程，然后使用<code class="language-plaintext highlighter-rouge">pthread_join</code>等待子线程结束</li>
</ol>

<h3 id="信号携带数据">信号携带数据</h3>
<iframe frameborder="0" height="980px" scrolling="no" src="https://onecompiler.com/embed/c/44jrvwn5z?hideLanguageSelection=true&amp;hideNew=true&amp;hideNewFileOption=true&amp;disableCopyPaste=true&amp;disableAutoComplete=true&amp;hideStdin=true&amp;hideEditorOptions=true" width="100%">
</iframe>

<p><strong>代码说明</strong>：</p>
<ol>
  <li>使用 <code class="language-plaintext highlighter-rouge">sigaction()</code> 注册信号处理函数，设置 <code class="language-plaintext highlighter-rouge">SA_SIGINFO</code> 标志以启用扩展信息</li>
  <li>信号处理函数签名为 <code class="language-plaintext highlighter-rouge">void handler(int sig, siginfo_t *info, void *context)</code>，其中 <code class="language-plaintext highlighter-rouge">info-&gt;si_value</code> 包含发送的数据</li>
  <li>主进程通过 <code class="language-plaintext highlighter-rouge">sigqueue()</code> 发送两次信号<code class="language-plaintext highlighter-rouge">SIGRTMIN</code>给子进程，注意，只有实时信号可以携带数据</li>
  <li>子进程接收到信号后，从 <code class="language-plaintext highlighter-rouge">siginfo_t</code> 结构中提取数据并打印，第一次打印数字，第二次打印传递的指针数据</li>
</ol>

<h2 id="信号阻塞和队列">信号阻塞和队列</h2>
<p>同种信号多次发送时，我们看一看两类信号的表现</p>

<iframe frameborder="0" height="500px" scrolling="no" src="https://onecompiler.com/embed/c/44jumtr9s?hideLanguageSelection=true&amp;hideNew=true&amp;hideNewFileOption=true&amp;disableCopyPaste=true&amp;disableAutoComplete=true&amp;hideStdin=true&amp;hideEditorOptions=true" width="100%">
</iframe>

<p>可以看到，两类信号都会按顺序被处理<br />
为了屏蔽或延迟信号的处理，我们可以使用<code class="language-plaintext highlighter-rouge">sigprocmask</code>或<code class="language-plaintext highlighter-rouge">pthread_sigmask</code>来阻塞信号</p>

<iframe frameborder="0" height="450px" scrolling="no" src="https://onecompiler.com/embed/c/44jung893?hideLanguageSelection=true&amp;hideNew=true&amp;hideNewFileOption=true&amp;disableCopyPaste=true&amp;disableAutoComplete=true&amp;hideStdin=true&amp;hideEditorOptions=true" width="100%">
</iframe>

<p><strong>代码说明</strong>：</p>
<ol>
  <li>sigset_t是存储阻塞信号集合的结构，可以理解为一个bitmap，每位代表一种信号，通过<code class="language-plaintext highlighter-rouge">sigemptyset</code>、<code class="language-plaintext highlighter-rouge">sigaddset</code>等方法来操作</li>
  <li><code class="language-plaintext highlighter-rouge">sigprocmask</code>可以设置当前进程的阻塞信号集合，<code class="language-plaintext highlighter-rouge">SIG_BLOCK</code>表示在原有阻塞信号中加入新的信号，<code class="language-plaintext highlighter-rouge">SIG_UNBLOCK</code>表示解除阻塞，<code class="language-plaintext highlighter-rouge">SIG_SETMASK</code>表示设置新的阻塞信号集合</li>
  <li>可以看到，<code class="language-plaintext highlighter-rouge">SIGUSR1</code>在解除阻塞后才开始执行</li>
</ol>

<p>如果多个同种信号在阻塞过程中被发送，那么信号会被怎样处理呢？</p>

<iframe frameborder="0" height="800px" scrolling="no" src="https://onecompiler.com/embed/c/44juq4nbe?hideLanguageSelection=true&amp;hideNew=true&amp;hideNewFileOption=true&amp;disableCopyPaste=true&amp;disableAutoComplete=true&amp;hideStdin=true&amp;hideEditorOptions=true" width="100%">
</iframe>

<p>标准信号在阻塞期间多次接收，只会保留最后一个信号，所以只处理了一次<br />
而实时信号会按照顺序处理，所以会打印3次<br />
如果两种信号同时解除阻塞，谁会优先执行呢？根据man手册，我们得到如下解释</p>
<ol>
  <li>如果两类信号同时发送，Linux系统中一般优先处理标准信号</li>
  <li>如果多种标准信号同时发送，处理顺序标准中没有指定</li>
  <li>如果多种实时信号同时发送，处理顺序是按照数字小的优先级高</li>
</ol>

<p>在多线程中，子线程会继承父线程的sigmask，发送给进程的信号会选择任意一个没有阻塞信号的线程。<br />
通常，我们使用如下方式在多线程中处理信号</p>

<iframe frameborder="0" height="1200px" scrolling="no" src="https://onecompiler.com/embed/c/44jusa4jq?hideLanguageSelection=true&amp;hideNew=true&amp;hideNewFileOption=true&amp;disableCopyPaste=true&amp;disableAutoComplete=true&amp;hideStdin=true&amp;hideEditorOptions=true" width="100%">
</iframe>

<p><strong>代码说明</strong>：</p>
<ol>
  <li><strong>主线程设置信号掩码</strong>：在创建子线程前，使用 <code class="language-plaintext highlighter-rouge">pthread_sigmask()</code> 阻塞 <code class="language-plaintext highlighter-rouge">SIGUSR1</code> 和 <code class="language-plaintext highlighter-rouge">SIGUSR2</code>，这样子线程会继承这个阻塞状态</li>
  <li><strong>pthread_barrier</strong>：用来同步主子线程，确保子线程启动后才发送信号，避免信号丢失</li>
  <li><strong>专用信号处理线程</strong>：<code class="language-plaintext highlighter-rouge">signal_handler_thread</code> 函数使用 <code class="language-plaintext highlighter-rouge">sigwait()</code> 同步等待信号，这是一种线程安全的信号处理方式</li>
  <li><strong>信号发送</strong>：主线程使用 <code class="language-plaintext highlighter-rouge">kill()</code> 向整个进程发送信号，由于只有信号处理线程未阻塞这些信号，所以由它来接收和处理</li>
  <li><strong>sigwait() 的优势</strong>：相比异步信号处理函数，<code class="language-plaintext highlighter-rouge">sigwait()</code> 是同步的，可以在普通代码中安全调用，避免了信号处理函数的诸多限制（如不能调用非异步信号安全的函数）</li>
</ol>

<h2 id="信号处理">信号处理</h2>
<p>下面介绍信号处理的过程</p>
<ol>
  <li>系统从内核态进入到用户态前（一般是系统调用结束后，或CPU时间片调度到某个进程），检查未处理的信号，如果有，按照上文中的优先级进行处理</li>
  <li>未设置处理函数，则按照默认方式处理，实时信号默认会让进程退出</li>
  <li>如果设置了处理函数，则进行如下步骤，注意<code class="language-plaintext highlighter-rouge">SIGKILL</code>和<code class="language-plaintext highlighter-rouge">SIGSTOP</code>设置处理函数无效
    <ol>
      <li>系统首先在用户空间栈中新建栈帧，保存运行前的状态，比如寄存器、sigmask等</li>
      <li>系统设置后续执行进入信号处理函数的入口，处理函数返回后调用<code class="language-plaintext highlighter-rouge">sigreturn</code>，这个函数负责恢复运行状态，然后跳转回代码原来的执行位置</li>
    </ol>
  </li>
  <li>系统回到用户态，执行完处理函数后，执行<code class="language-plaintext highlighter-rouge">sigreturn</code>返回内核态，执行完后回到用户态，继续执行用户态代码</li>
</ol>

<pre><code class="language-mermaid">sequenceDiagram
    participant User as 用户程序
    participant Kernel as 内核
    
    Note over User,Kernel: 正常运行阶段
    User-&gt;&gt;User: 执行用户代码
    
    Note over User,Kernel: 进入内核态(系统调用/时间片调度)
    User-&gt;&gt;Kernel: 陷入内核态
    
    Kernel-&gt;&gt;Kernel: 检查未处理信号
    
    alt 无未处理信号
        Kernel--&gt;&gt;User: 返回用户态
        User-&gt;&gt;User: 继续执行
    else 有未处理信号且未设置处理函数
        Kernel-&gt;&gt;Kernel: 按默认方式处理
        Kernel--&gt;&gt;User: 进程退出、进程暂停，或忽略信号
    else 有未处理信号且已设置处理函数
        Kernel-&gt;&gt;Kernel: 在用户栈创建栈帧&lt;br/&gt;保存寄存器、sigmask等
        Kernel-&gt;&gt;Kernel: 设置返回入口为sigreturn
        
        Note over User,Kernel: 切换到用户态执行处理函数
        Kernel--&gt;&gt;User: 跳转到信号处理函数
        
        User-&gt;&gt;User: 执行信号处理函数
        User-&gt;&gt;Kernel: 调用sigreturn(进入内核态)
        
        Kernel-&gt;&gt;Kernel: 恢复运行状态&lt;br/&gt;(寄存器、sigmask等)
        
        Note over User,Kernel: 返回原执行位置
        Kernel--&gt;&gt;User: 切换回用户态
        User-&gt;&gt;User: 从断点继续执行
    end
</code></pre>

<p>关于信号还有许多内容可以讨论，比如信号安全(信号处理函数怎样才能安全处理信号)、系统调用中信号的使用等，后续也许会新开篇目来介绍。</p>]]></content><author><name>cafewang</name></author><category term="linux" /><category term="signal" /><summary type="html"><![CDATA[信号（Signal）是 Linux 系统中最古老、最基础的进程间通信机制之一，也是理解 Unix/Linux 系统编程的核心概念。作为一种异步通知机制，信号允许内核或一个进程向另一个进程发送简短的通知，告知其某个特定事件的发生——无论是用户按下 Ctrl+C 中断程序，还是子进程终止通知父进程，亦或是定时器到期提醒应用程序。]]></summary></entry><entry><title type="html">redis调试</title><link href="https://cafewang.github.io//redis/debug-setup" rel="alternate" type="text/html" title="redis调试" /><published>2025-05-22T10:55:00+00:00</published><updated>2025-05-22T10:55:00+00:00</updated><id>https://cafewang.github.io//redis/redis-debug-setup</id><content type="html" xml:base="https://cafewang.github.io//redis/debug-setup"><![CDATA[<p>redis是比较适合学习的开源框架，而调试又是研究源码最佳的方式，本文将两者结合，讨论一下怎样调试redis。</p>

<h2 id="环境准备">环境准备</h2>
<ul>
  <li>使用主机为win10，安装vscode作为IDE</li>
  <li>安装WSL，参考<a href="https://learn.microsoft.com/zh-cn/windows/wsl/install">文档</a>，安装<code class="language-plaintext highlighter-rouge">Ubuntu 20.04.6 LTS</code>系统</li>
  <li>登录WSL系统，克隆项目<code class="language-plaintext highlighter-rouge">https://github.com/cafewang/redis-deep-dive</code></li>
  <li>在项目的<code class="language-plaintext highlighter-rouge">redis-8.0.0</code>目录下安装redis，参考<a href="https://redis.io/docs/latest/operate/oss_and_stack/install/build-stack/ubuntu-focal/">文档</a>
    <ul>
      <li>在项目的根目录下解压<code class="language-plaintext highlighter-rouge">redis-8.0.0.tar.gz</code>文件</li>
      <li>执行make时添加<code class="language-plaintext highlighter-rouge">noopt</code>选项，更适合调试</li>
      <li>执行make时，添加<code class="language-plaintext highlighter-rouge">-k -d</code>选项，显示更多信息，便于排查安装问题</li>
    </ul>
  </li>
  <li>在项目的<code class="language-plaintext highlighter-rouge">shunit2-2.1.8</code>目录下安装shunit2，参考<a href="https://github.com/kward/shunit2">文档</a> ，用于运行测试
    <ul>
      <li>在项目的根目录下解压<code class="language-plaintext highlighter-rouge">shunit2-2.1.8.tar.gz</code>文件</li>
    </ul>
  </li>
  <li>在项目根目录下运行<code class="language-plaintext highlighter-rouge">code .</code>，启动vscode（运行在ubuntu系统上，可以执行make、gcc等命令）</li>
  <li>创建调试配置文件<code class="language-plaintext highlighter-rouge">.vscode/launch.json</code>
    <div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"version"</span><span class="p">:</span><span class="w"> </span><span class="s2">"0.2.0"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"configurations"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
      </span><span class="p">{</span><span class="w">
          </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"debug redis"</span><span class="p">,</span><span class="w">    
          </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"cppdbg"</span><span class="p">,</span><span class="w">                       </span><span class="err">//</span><span class="w"> </span><span class="err">默认这个就好</span><span class="w">
          </span><span class="nl">"request"</span><span class="p">:</span><span class="w"> </span><span class="s2">"launch"</span><span class="p">,</span><span class="w">                    </span><span class="err">//</span><span class="w"> </span><span class="err">默认这个就好</span><span class="w">
          </span><span class="nl">"program"</span><span class="p">:</span><span class="w"> </span><span class="s2">"${workspaceFolder}/redis-8.0.0/src/redis-server"</span><span class="p">,</span><span class="w">  </span><span class="err">//</span><span class="w"> </span><span class="err">编译出来的文件的路径</span><span class="p">,</span><span class="err">得包含文件名</span><span class="w">
          </span><span class="nl">"args"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">                               </span><span class="err">//</span><span class="w"> </span><span class="err">程序运行时的命令行参数。</span><span class="w">
              </span><span class="s2">"../redis.conf"</span><span class="w">
          </span><span class="p">],</span><span class="w">
          </span><span class="nl">"stopAtEntry"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">                    </span><span class="err">//</span><span class="w"> </span><span class="err">是否在程序入口处暂停调试。不严谨的讲就是是否在main函数处暂停。</span><span class="w">
          </span><span class="nl">"cwd"</span><span class="p">:</span><span class="w"> </span><span class="s2">"${workspaceFolder}/redis-8.0.0/src"</span><span class="p">,</span><span class="w">            </span><span class="err">//</span><span class="w"> </span><span class="err">调试时程序运行的目录</span><span class="w">
          </span><span class="nl">"environment"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">                      </span><span class="err">//</span><span class="w"> </span><span class="err">环境变量设置，一般默认为空就好</span><span class="w">
          </span><span class="nl">"externalConsole"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">               </span><span class="err">//</span><span class="w"> </span><span class="err">一般不打开外部控制台</span><span class="w">
          </span><span class="nl">"MIMode"</span><span class="p">:</span><span class="w"> </span><span class="s2">"gdb"</span><span class="p">,</span><span class="w">                        </span><span class="err">//</span><span class="w"> </span><span class="err">调试器模式，</span><span class="s2">"gdb"</span><span class="w"> </span><span class="err">表示使用</span><span class="w"> </span><span class="err">GNU</span><span class="w"> </span><span class="err">调试器。</span><span class="w">
          </span><span class="nl">"miDebuggerPath"</span><span class="p">:</span><span class="w"> </span><span class="s2">"/usr/bin/gdb"</span><span class="p">,</span><span class="w">
          </span><span class="err">//</span><span class="w"> </span><span class="nl">"preLaunchTask"</span><span class="p">:</span><span class="w"> </span><span class="s2">"shell"</span><span class="p">,</span><span class="w">            </span><span class="err">//</span><span class="w"> </span><span class="err">调试前需要执行的任务名称，那当然是先编译啦，这里不需要</span><span class="w">
      </span><span class="p">},</span><span class="w">
  </span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div>    </div>
  </li>
  <li>在redis源文件中打上断点，点击下方所示按钮即可开始调试
<img src="../assets/img/redis/debug.png" alt="" /></li>
  <li>redis启动成功会在终端中显示如下信息
<img src="../assets/img/redis/terminal.png" alt="terminal.png" /></li>
</ul>

<h2 id="测试框架">测试框架</h2>
<p>我们期望采取<code class="language-plaintext highlighter-rouge">自下而上</code>的方式来分析redis项目，即先了解独立的模块和类，再串联整体的流程和功能。<br />
测试位于<code class="language-plaintext highlighter-rouge">integration-test</code>下，使用<code class="language-plaintext highlighter-rouge">shunit2</code>框架驱动，需要从github上下载，解压到<code class="language-plaintext highlighter-rouge">shunit2-2.1.8</code>。<br />
以测试sds为例</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#! /usr/bin/env bash</span>

oneTimeSetUp<span class="o">()</span> <span class="o">{</span>
  <span class="nv">pythondir</span><span class="o">=</span><span class="sb">`</span>gdb <span class="nt">-q</span> <span class="nt">--command</span><span class="o">=</span>../pythondir.gdb<span class="sb">`</span>
  <span class="nv">redisdir</span><span class="o">=</span><span class="s2">"../../redis-8.0.0"</span>
  <span class="nb">cp</span> ../gdb_util.py <span class="nv">$pythondir</span>
<span class="o">}</span>

testSds5Struct<span class="o">()</span> <span class="o">{</span>
  <span class="nb">export </span><span class="nv">testFunction</span><span class="o">=</span><span class="k">${</span><span class="nv">FUNCNAME</span><span class="p">[0]</span><span class="k">}</span>
  gdb <span class="nt">-q</span> <span class="nt">-ex</span><span class="o">=</span><span class="s1">'source test.py'</span> <span class="nt">--args</span> <span class="nv">$redisdir</span><span class="s2">"/src/redis-server"</span> <span class="nv">$redisdir</span><span class="s2">"/redis.conf"</span>
<span class="o">}</span>

<span class="nb">.</span> ../../shunit2-2.1.8/shunit2
</code></pre></div></div>
<ul>
  <li>oneTimeSetUp会在整个测试的开始时执行一次，这里主要是初始<code class="language-plaintext highlighter-rouge">gdb_util.py</code>模块，放到python的include路径下</li>
  <li>testSds5Struct为测试方法，必须以test开头
    <ul>
      <li>testFunction是执行的方法名，在<code class="language-plaintext highlighter-rouge">test.py</code>中</li>
      <li>通过gdb调试redis-server，并且执行<code class="language-plaintext highlighter-rouge">test.py</code>脚本</li>
    </ul>
  </li>
  <li>通过<code class="language-plaintext highlighter-rouge">. ../../shunit2-2.1.8/shunit2</code>引入shunit2框架</li>
</ul>

<p>使用gdb，能够不重新编译整个c程序，来调用c程序中的方法。</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">gdb_util</span>
<span class="kn">import</span> <span class="nn">os</span>

<span class="k">def</span> <span class="nf">testSds5Struct</span><span class="p">():</span>
    <span class="n">gdb_util</span><span class="p">.</span><span class="n">setup</span><span class="p">()</span>
    <span class="nb">str</span> <span class="o">=</span> <span class="s">"abc"</span>
    <span class="n">gdb</span><span class="p">.</span><span class="n">execute</span><span class="p">(</span><span class="s">'start'</span><span class="p">)</span>
    <span class="n">gdb</span><span class="p">.</span><span class="n">execute</span><span class="p">(</span><span class="sa">f</span><span class="s">'set $obj = sdsnew("</span><span class="si">{</span><span class="nb">str</span><span class="si">}</span><span class="s">")'</span><span class="p">)</span>
    <span class="n">gdb</span><span class="p">.</span><span class="n">execute</span><span class="p">(</span><span class="s">'set $flag = *(((char*)$obj) - 1)'</span><span class="p">)</span>
    <span class="n">gdb</span><span class="p">.</span><span class="n">execute</span><span class="p">(</span><span class="s">'set $type = $flag &amp; 0b111'</span><span class="p">)</span>
    <span class="n">gdb</span><span class="p">.</span><span class="n">execute</span><span class="p">(</span><span class="s">'set $len = $flag &gt;&gt; 3'</span><span class="p">)</span>
    <span class="nb">type</span> <span class="o">=</span> <span class="n">gdb</span><span class="p">.</span><span class="n">parse_and_eval</span><span class="p">(</span><span class="s">"$type"</span><span class="p">)</span>
    <span class="nb">len</span> <span class="o">=</span> <span class="n">gdb</span><span class="p">.</span><span class="n">parse_and_eval</span><span class="p">(</span><span class="s">"$len"</span><span class="p">)</span>
    <span class="n">code</span> <span class="o">=</span> <span class="mi">0</span>
    <span class="k">if</span> <span class="nb">type</span> <span class="o">!=</span> <span class="mi">0</span> <span class="ow">or</span> <span class="nb">len</span> <span class="o">!=</span> <span class="mi">3</span><span class="p">:</span>
        <span class="n">code</span> <span class="o">=</span> <span class="mi">1</span>
        <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"Error: type = </span><span class="si">{</span><span class="nb">type</span><span class="si">}</span><span class="s">, len = </span><span class="si">{</span><span class="nb">len</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
    <span class="n">gdb</span><span class="p">.</span><span class="n">execute</span><span class="p">(</span><span class="sa">f</span><span class="s">'quit </span><span class="si">{</span><span class="n">code</span><span class="si">}</span><span class="s">'</span><span class="p">)</span>

<span class="n">func</span> <span class="o">=</span> <span class="n">os</span><span class="p">.</span><span class="n">environ</span><span class="p">[</span><span class="s">'testFunction'</span><span class="p">]</span>
<span class="nb">locals</span><span class="p">()[</span><span class="n">func</span><span class="p">]()</span>
</code></pre></div></div>
<ul>
  <li>gdb模块是gdb自动引入的，通过<code class="language-plaintext highlighter-rouge">gdb.execute(...)</code>可以执行gdb命令</li>
  <li>gdb.execute(‘start’)启动redis-server，并在main方法入口暂停</li>
  <li>程序暂停时，可以自定义参数，执行程序中的方法</li>
  <li>上述代码执行了<code class="language-plaintext highlighter-rouge">sdsnew</code>方法创建了sds对象，并验证了type和len字段的值</li>
  <li>如果验证失败，返回code=1</li>
  <li>在shell中，函数最后一行命令的返回码不为0，判断为执行失败</li>
</ul>

<p>使用这种方式测试有如下几种优点</p>
<ul>
  <li>可以在不修改源码的情况下，调试c程序中特定的函数，以了解特定部分的功能</li>
  <li>可以通过断点，查看任意位置变量的值</li>
  <li>可以自动化测试，不需要手动执行调试</li>
</ul>

<p>后续我们也会基于此种方式，了解redis不同模块的逻辑。</p>]]></content><author><name>cafewang</name></author><category term="redis" /><category term="debug" /><summary type="html"><![CDATA[redis是比较适合学习的开源框架，而调试又是研究源码最佳的方式，本文将两者结合，讨论一下怎样调试redis。]]></summary></entry><entry><title type="html">redis杂谈</title><link href="https://cafewang.github.io//microservices/redis-misc" rel="alternate" type="text/html" title="redis杂谈" /><published>2025-05-09T10:35:00+00:00</published><updated>2025-05-09T10:35:00+00:00</updated><id>https://cafewang.github.io//microservices/redis-misc</id><content type="html" xml:base="https://cafewang.github.io//microservices/redis-misc"><![CDATA[<p>本文主要是总结redis相关的重要知识点和应用，通过实操来深入理解redis。</p>

<h2 id="项目环境">项目环境</h2>
<p>克隆<code class="language-plaintext highlighter-rouge">https://github.com/cafewang/redis-ready.git</code> ，项目由gradle管理，无需单独下载。<br />
导入Idea，运行前安装k3d，按照文件<code class="language-plaintext highlighter-rouge">deployment/deploy.bat</code>安装集群并创建redis实例。<br />
部署完成后，redis实例运行在本地的<code class="language-plaintext highlighter-rouge">8081</code>端口，可以通过redis gui连接。<br />
这时可以运行项目中的测试。</p>

<h2 id="数据结构">数据结构</h2>
<h3 id="string">string</h3>
<p>string是redis中最基础的结构，就是记录了大小的字符数组，注意这是2010年的实现，但是原理没有大的变化。</p>
<pre><code class="language-textmate">struct sdshdr {
    long len;
    long free;
    char buf[];
};
</code></pre>
<p>string的上限是512MB，即\(2^32\)比特。<br />
string大部分操作都是\(O(1)\)，只有substr、getrange、setrange等是\(O(n)\)。<br />
下面展示了setrange的使用</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// StringTest</span>
<span class="nd">@Test</span>
<span class="kt">void</span> <span class="nf">testLinearOps</span><span class="o">()</span> <span class="o">{</span>
    <span class="nc">String</span> <span class="n">key</span> <span class="o">=</span> <span class="s">"key"</span><span class="o">,</span> <span class="n">value</span> <span class="o">=</span> <span class="s">"Hello World"</span><span class="o">;</span>
    <span class="n">stringRedisTemplate</span><span class="o">.</span><span class="na">opsForValue</span><span class="o">().</span><span class="na">set</span><span class="o">(</span><span class="n">key</span><span class="o">,</span> <span class="n">value</span><span class="o">);</span>
    <span class="n">stringRedisTemplate</span><span class="o">.</span><span class="na">opsForValue</span><span class="o">().</span><span class="na">set</span><span class="o">(</span><span class="n">key</span><span class="o">,</span> <span class="s">"Redis"</span><span class="o">,</span> <span class="mi">6</span><span class="o">);</span>
    <span class="nc">Assertions</span><span class="o">.</span><span class="na">assertEquals</span><span class="o">(</span><span class="s">"Hello Redis"</span><span class="o">,</span> <span class="n">stringRedisTemplate</span><span class="o">.</span><span class="na">opsForValue</span><span class="o">().</span><span class="na">getAndDelete</span><span class="o">(</span><span class="n">key</span><span class="o">));</span>
<span class="o">}</span>
</code></pre></div></div>
<p>注意以下几点</p>
<ul>
  <li>setrange的位置offset是按字节算的，多字节编码需要自行计算正确的位置</li>
  <li>offset范围不能超过string上限512MB</li>
  <li>如果offset超过字符串长度，redis会申请新空间，空间很大的情况下非常耗时</li>
</ul>

<h3 id="json">JSON</h3>
<p>redis拥有非常强大的JSON处理能力，能通过query engine提供类似sql的查询能力，还能提供ORM映射，这块内容非常丰富，不在本文展开</p>

<h3 id="list">List</h3>
<p>redis的List是基于双向链表实现的，所以队头队尾的操作都是\(O(1)\)，而随机访问操作是\(O(n)\)。<br />
List的元素上限是\(2^32-1\)，下面演示基本操作。</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ListTest</span>
    <span class="nd">@Test</span>
    <span class="kt">void</span> <span class="nf">testPushAndPop</span><span class="o">()</span> <span class="o">{</span>
        <span class="nc">String</span> <span class="n">key</span> <span class="o">=</span> <span class="s">"list"</span><span class="o">;</span>
        <span class="n">stringRedisTemplate</span><span class="o">.</span><span class="na">opsForList</span><span class="o">().</span><span class="na">leftPushAll</span><span class="o">(</span><span class="n">key</span><span class="o">,</span> <span class="s">"1"</span><span class="o">,</span> <span class="s">"2"</span><span class="o">,</span> <span class="s">"3"</span><span class="o">);</span>
        <span class="nc">Assertions</span><span class="o">.</span><span class="na">assertEquals</span><span class="o">(</span><span class="s">"1"</span><span class="o">,</span> <span class="n">stringRedisTemplate</span><span class="o">.</span><span class="na">opsForList</span><span class="o">().</span><span class="na">rightPop</span><span class="o">(</span><span class="n">key</span><span class="o">));</span>
        <span class="nc">Assertions</span><span class="o">.</span><span class="na">assertEquals</span><span class="o">(</span><span class="mi">2</span><span class="o">,</span> <span class="n">stringRedisTemplate</span><span class="o">.</span><span class="na">opsForList</span><span class="o">().</span><span class="na">size</span><span class="o">(</span><span class="n">key</span><span class="o">));</span>
        <span class="n">stringRedisTemplate</span><span class="o">.</span><span class="na">opsForList</span><span class="o">().</span><span class="na">leftPop</span><span class="o">(</span><span class="n">key</span><span class="o">,</span> <span class="mi">3</span><span class="o">);</span>
    <span class="o">}</span>
</code></pre></div></div>
<p>首先插入三个元素，List为\([3,2,1]\)，出队一个元素后，长度变为2。<br />
需要注意，redis中的集合类型有如下性质</p>
<ul>
  <li>向不存在的集合插入元素，会创建集合</li>
  <li>删除空集合元素，会删除集合(Stream除外)</li>
  <li>对空集合执行只读操作(如取大小)，也会删除集合</li>
</ul>

<h3 id="set">Set</h3>
<p>这里将redis字典类型的结构汇总，包含Set、Hash、SortedSet。<br />
Set和Hash本质是一样的，都是无序字典，长度较小时实现为listpack(可以理解为键值对的列表)，较大时转为map(拉链法)。<br />
键值对最多为\(2^(32)-1\)个。
Set比较耗时的是SINTER求交集的操作，为\(O(N*M)\)，N是最小集合的大小，M是集合个数。<br />
重点介绍SortedSet，和java中的TreeSet不同，redis中的SortedSet是string和weight(浮点数)的pair，string必须不同，排序先比较weight然后比较string。<br />
举个例子</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ZSetTest</span>
    <span class="nd">@Test</span>
    <span class="kt">void</span> <span class="nf">testOder</span><span class="o">()</span> <span class="o">{</span>
        <span class="nc">String</span> <span class="n">key</span> <span class="o">=</span> <span class="s">"zset"</span><span class="o">;</span>
        <span class="nc">String</span> <span class="n">v1</span> <span class="o">=</span> <span class="s">"c"</span><span class="o">,</span> <span class="n">v2</span> <span class="o">=</span> <span class="s">"d"</span><span class="o">,</span> <span class="n">v3</span> <span class="o">=</span> <span class="s">"a"</span><span class="o">;</span>
        <span class="kt">double</span> <span class="n">score1</span> <span class="o">=</span> <span class="mi">1</span><span class="o">,</span> <span class="n">score2</span> <span class="o">=</span> <span class="mi">1</span><span class="o">,</span> <span class="n">score3</span> <span class="o">=</span> <span class="mi">2</span><span class="o">;</span>
        <span class="n">stringRedisTemplate</span><span class="o">.</span><span class="na">opsForZSet</span><span class="o">().</span><span class="na">add</span><span class="o">(</span><span class="n">key</span><span class="o">,</span> <span class="n">v1</span><span class="o">,</span> <span class="n">score1</span><span class="o">);</span>
        <span class="n">stringRedisTemplate</span><span class="o">.</span><span class="na">opsForZSet</span><span class="o">().</span><span class="na">add</span><span class="o">(</span><span class="n">key</span><span class="o">,</span> <span class="n">v2</span><span class="o">,</span> <span class="n">score2</span><span class="o">);</span>
        <span class="n">stringRedisTemplate</span><span class="o">.</span><span class="na">opsForZSet</span><span class="o">().</span><span class="na">add</span><span class="o">(</span><span class="n">key</span><span class="o">,</span> <span class="n">v3</span><span class="o">,</span> <span class="n">score3</span><span class="o">);</span>
        <span class="nc">Assertions</span><span class="o">.</span><span class="na">assertEquals</span><span class="o">(</span><span class="n">v3</span><span class="o">,</span> <span class="n">stringRedisTemplate</span><span class="o">.</span><span class="na">opsForZSet</span><span class="o">().</span><span class="na">popMax</span><span class="o">(</span><span class="n">key</span><span class="o">).</span><span class="na">getValue</span><span class="o">());</span>
        <span class="nc">Assertions</span><span class="o">.</span><span class="na">assertEquals</span><span class="o">(</span><span class="n">v2</span><span class="o">,</span> <span class="n">stringRedisTemplate</span><span class="o">.</span><span class="na">opsForZSet</span><span class="o">().</span><span class="na">popMax</span><span class="o">(</span><span class="n">key</span><span class="o">).</span><span class="na">getValue</span><span class="o">());</span>
        <span class="nc">Assertions</span><span class="o">.</span><span class="na">assertEquals</span><span class="o">(</span><span class="n">v1</span><span class="o">,</span> <span class="n">stringRedisTemplate</span><span class="o">.</span><span class="na">opsForZSet</span><span class="o">().</span><span class="na">popMax</span><span class="o">(</span><span class="n">key</span><span class="o">).</span><span class="na">getValue</span><span class="o">());</span>
        <span class="n">stringRedisTemplate</span><span class="o">.</span><span class="na">opsForZSet</span><span class="o">().</span><span class="na">popMax</span><span class="o">(</span><span class="n">key</span><span class="o">);</span>
    <span class="o">}</span>
</code></pre></div></div>
<p>依次插入了<code class="language-plaintext highlighter-rouge">1:c，1:d，2:a</code>，每次pop最大的值，由于a的score最大先被pop，c和d的score相同，所以字典序大的d先被pop，最后是c。<br />
SortedSet是通过SkipList实现的，是一种随机的、由多层链表构成的有序集合。<br />
<img src="/assets/img/skiplist.png" alt="" /><br />
如上图，查询key=50，先从上层链表查询到30的下个元素大于50，然后转到下层链表，顺序搜索到50。<br />
SkipList有如下优缺点</p>
<ul>
  <li>优点
    <ul>
      <li>搜索、插入、删除平均复杂度为\(O(log_2{n})\)</li>
      <li>实现相较红黑树要简单</li>
    </ul>
  </li>
  <li>缺点
    <ul>
      <li>存在极端情况下复杂度退化到\(O(n)\)</li>
      <li>需要\(O(n)\)的额外空间</li>
    </ul>
  </li>
</ul>

<p>根据上述分析可知，SortedSet大部分操作都是\(O(log_2{n})\)，除了ZRange是\(O(log_2{n}+m)\)，m是返回元素的个数，操作大集合时需要注意。</p>

<h3 id="bitmap">Bitmap</h3>
<p>Bitmap不是单独的结构，而是用string表示的bit向量，所以最多表示\(2^32\)个二进制位。<br />
Bitmap支持按位读写，以及与或非这些基本位运算，下面演示基本操作。</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// BitmapTest</span>
    <span class="nd">@Test</span>
    <span class="kt">void</span> <span class="nf">testCount</span><span class="o">()</span> <span class="o">{</span>
        <span class="nc">String</span> <span class="n">key</span> <span class="o">=</span> <span class="s">"bitmap"</span><span class="o">;</span>
        <span class="n">stringRedisTemplate</span><span class="o">.</span><span class="na">opsForValue</span><span class="o">().</span><span class="na">setBit</span><span class="o">(</span><span class="n">key</span><span class="o">,</span> <span class="mi">0</span><span class="o">,</span> <span class="kc">true</span><span class="o">);</span>
        <span class="n">stringRedisTemplate</span><span class="o">.</span><span class="na">opsForValue</span><span class="o">().</span><span class="na">setBit</span><span class="o">(</span><span class="n">key</span><span class="o">,</span> <span class="mi">10</span><span class="o">,</span> <span class="kc">true</span><span class="o">);</span>
        <span class="n">stringRedisTemplate</span><span class="o">.</span><span class="na">opsForValue</span><span class="o">().</span><span class="na">setBit</span><span class="o">(</span><span class="n">key</span><span class="o">,</span> <span class="mi">100</span><span class="o">,</span> <span class="kc">true</span><span class="o">);</span>
        <span class="nc">Assertions</span><span class="o">.</span><span class="na">assertEquals</span><span class="o">(</span><span class="kc">true</span><span class="o">,</span> <span class="n">stringRedisTemplate</span><span class="o">.</span><span class="na">opsForValue</span><span class="o">().</span><span class="na">getBit</span><span class="o">(</span><span class="n">key</span><span class="o">,</span> <span class="mi">100</span><span class="o">));</span>
        <span class="nc">Assertions</span><span class="o">.</span><span class="na">assertEquals</span><span class="o">(</span><span class="kc">false</span><span class="o">,</span> <span class="n">stringRedisTemplate</span><span class="o">.</span><span class="na">opsForValue</span><span class="o">().</span><span class="na">getBit</span><span class="o">(</span><span class="n">key</span><span class="o">,</span> <span class="mi">99</span><span class="o">));</span>
        <span class="nc">Assertions</span><span class="o">.</span><span class="na">assertEquals</span><span class="o">(</span><span class="mi">3</span><span class="o">,</span> <span class="n">stringRedisTemplate</span><span class="o">.</span><span class="na">execute</span><span class="o">((</span><span class="nc">RedisCallback</span><span class="o">&lt;</span><span class="nc">Long</span><span class="o">&gt;)</span> <span class="n">connection</span> <span class="o">-&gt;</span>
                <span class="n">connection</span><span class="o">.</span><span class="na">stringCommands</span><span class="o">().</span><span class="na">bitCount</span><span class="o">(</span><span class="n">key</span><span class="o">.</span><span class="na">getBytes</span><span class="o">())));</span>
        <span class="n">stringRedisTemplate</span><span class="o">.</span><span class="na">opsForValue</span><span class="o">().</span><span class="na">getAndDelete</span><span class="o">(</span><span class="n">key</span><span class="o">);</span>
    <span class="o">}</span>
</code></pre></div></div>
<p>测试在0、10、100置一，所以取位置100为true，99为false，总共有3位为一。</p>

<h3 id="probabilistic">Probabilistic</h3>
<p>Probabilistic在redis中是统计概率、分布的结构，作为统计不追求绝对的准确，而是追求效率。</p>

<h4 id="hyperloglog">HyperLogLog</h4>
<p>简称为HLL，这是一种神奇的结构，能够用最多12KB的内存来统计集合大小，即不同元素的个数，标准误差为0.81%。<br />
HLL的典型应用场景就是统计活跃用户，效率高但不完全准确。</p>

<h3 id="bloom-and-cuckoo-filter">Bloom and Cuckoo Filter</h3>
<p>Bloom和Cuckoo Filter给我们提供了这种功能，通过较少的内存消耗判断元素是否存在于集合中</p>
<ul>
  <li>如果是，则元素有很大概率存在</li>
  <li>不是，则元素一定不在集合中</li>
</ul>

<p>Bloom的关键在于失误率的设定，0.01%的失误率，对应每个元素占用19位，以及使用14个独立的hash函数。<br />
Bloom的插入性能为\(O(k)\)，k为hash函数个数，读性能为\(O(k*n)\)，n为堆叠的filter层数(capacity设置过小会引起堆叠，所以需合理设置)。<br />
Cuckoo的插入、读和删除性能均为\(O(1)\)。
两种算法比较如下</p>
<ul>
  <li>Bloom适合大量写入的场景，而cuckoo的读性能更佳，且支持删除操作</li>
</ul>

<p>我们将在应用部分来讲解使用的细节。</p>

<h2 id="淘汰策略">淘汰策略</h2>
<p>redis在占用内存达到maxmemory(默认为2MB)时，会触发淘汰策略，有如下几种</p>
<ul>
  <li>no eviction，不淘汰</li>
  <li>all keys/volatile，可搭配如下淘汰策略，前者是所有的对象，后者是设置了过期时间的
    <ul>
      <li>lru，最近最少使用，注意redis使用的是近似算法，效率更高</li>
      <li>lfu，最少使用频率</li>
      <li>random，随机选择</li>
    </ul>
  </li>
  <li>volatile-ttl，淘汰最接近过期的对象</li>
</ul>

<p>可以使用info命令查看redis信息，用来调整淘汰策略</p>
<ul>
  <li>查看最大使用内存(包含被淘汰的对象)，如果比最大内存大太多，考虑调大最大内存</li>
  <li>查看缓存命中率，如果比预期低(热点数据访问占总访问的比例)
    <ul>
      <li>查看淘汰的key数量，数量太多，可以考虑将volatile策略调整为all keys</li>
      <li>查看过期的key数量，如果太多，可以查看ttl时间是否过短、或者不应该过期</li>
    </ul>
  </li>
</ul>

<h2 id="持久化">持久化</h2>
<h3 id="rdb">RDB</h3>
<p>RDB(redis database)是紧凑格式的单文件的redis数据快照。<br />
配置RDB很简单，如下配置表示，每60秒内有1000次修改就会触发快照(即生成新的RDB文件)，文件名默认为<code class="language-plaintext highlighter-rouge">dump.rdb</code>。</p>
<div class="language-properties highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">save</span> <span class="err">60</span> <span class="err">1000</span>
</code></pre></div></div>
<p>生成过程如下</p>
<ul>
  <li>redis fork产生子进程</li>
  <li>子进程将数据集写入临时的rdb文件</li>
  <li>写入完成，通过原子操作替换rdb文件</li>
</ul>

<p>需要注意</p>
<ul>
  <li>当数据集很大时，fork会严重占用CPU，导致redis在几毫秒甚至一秒内不能提供服务</li>
</ul>

<h3 id="aof">AOF</h3>
<p>AOF(append only file)可以提供rdb不能提供的实时数据备份。<br />
通过如下配置开启</p>
<div class="language-properties highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">appendonly</span> <span class="err">yes</span>
</code></pre></div></div>
<p>自redis7.0起，aof由多个文件组成</p>
<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>appendonly.aof.1.base.rdb
appendonly.aof.1.incr.aof
appendonly.aof.manifest
</code></pre></div></div>
<p>base文件(rdb或aof格式)表示某个数据快照(每次rewrite时的快照)，incr文件表示增量变更，可能有多个。<br />
redis通过rewriting压缩aof，创建新的incr文件追踪变更，历史数据全部写入新的base文件中，然后原子替换成新的base和incr文件。<br />
redis提供三种aof文件同步策略(fsync)</p>
<ul>
  <li>appendfsync always：每次变更都会落盘，但效率非常低。</li>
  <li>appendfsync everysec：每秒落盘，这个是默认策略。</li>
  <li>appendfsync no：由操作系统决定落盘时机，效率最高但也最危险。</li>
</ul>

<p>aof工作流程如下(redis&gt;=7.0)</p>
<ul>
  <li>redis fork子进程</li>
  <li>子进程写入临时base文件</li>
  <li>父进程创建新incr文件，并append更新，这样即使子进程失败，当前状态仍由旧base+旧incr+新incr文件保持</li>
  <li>子进程写入完毕，父进程收到signal，切换到新的base和incr文件</li>
  <li>redis会清理旧的base和incr文件</li>
</ul>

<h3 id="对比">对比</h3>
<ul>
  <li>对于大数据集，RDB在重启方面一般快于AOF</li>
  <li>rdb的体积相较于aof更小，而aof可读性更好，可以手动修改</li>
  <li>使用aof可以保证不丢失或只丢失一秒的数据，而rdb很难做到</li>
</ul>

<h3 id="重启">重启</h3>
<p>redis会根据配置恢复数据，默认情况下rdb开启，会读取dump.rdb作为初始数据。<br />
如果开启了aof，则优先读取base+incr文件。<br />
所以，如果重启时开启aof，需要先将dump.rdb文件转为aof文件，这样数据才能正常恢复。</p>

<h3 id="备份">备份</h3>
<p>由于rdb文件只有一个，且产生后不会修改，只会原子替换，可以直接备份。<br />
aof文件不能直接拷贝，需要先关闭rewriting并等待现有rewriting结束后再拷贝，然后开启rewriting，不然可能得到无效的备份。</p>

<h2 id="高可用">高可用</h2>
<p>单机的redis实例简单容易理解，但单机性能总有上限，还会遇到单点故障的问题，来看看redis的高可用方案。</p>

<h3 id="replication">replication</h3>
<p>redis也支持主从模式，首先介绍重要概念</p>
<ul>
  <li>master，redis实例默认为master，支持读写</li>
  <li>replica，同位redis实例，默认只读</li>
  <li>master和replica间通过异步请求同步</li>
  <li>如果master和replica间连接断开一段时间后，replica重连，会尝试进行partial resync，同步断开期间丢失的更新</li>
  <li>如果partial resync不能成功，replica会执行full resync，获取master的全量数据</li>
</ul>

<h4 id="replication-id">replication id</h4>
<p>redis的数据状态是怎么表示的？简单来说就是replication id+offset。<br />
replication id是一串表示redis初始状态的随机数，在两种情况下生成</p>
<ul>
  <li>redis重启</li>
  <li>replica晋升为master</li>
</ul>

<p>replica连接上master后，会继承master的replication id。举个例子，我们有一个master和两个replica。</p>
<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code># 输入命令 info replication
# master
master_replid c93295a0f2369023b49295072f4db6fb7428064f
master_replid2 0000000000000000000000000000000000000000
master_repl_offset 19770
second_repl_offset -1
# replica1
master_replid c93295a0f2369023b49295072f4db6fb7428064f
master_replid2 0000000000000000000000000000000000000000
master_repl_offset 19740
second_repl_offset -1
# replica2
master_replid c93295a0f2369023b49295072f4db6fb7428064f
master_replid2 0000000000000000000000000000000000000000
master_repl_offset 19754
second_repl_offset -1
</code></pre></div></div>
<p>可以看到，replica的replication id和master一致，但是offset不一样，而offset就是变更命令在replication stream中的位置，每当发生变更，offset就会增大。<br />
那master_replid2和second_offset是什么呢？当replica晋升为master后，原master由于网络原因可能仍在接收写请求，导致状态不一致，所以新master要再生成一个replication id，而原来的就放在master_replid2里。</p>

<h4 id="replication流程">replication流程</h4>
<p>replica使用PSYNC命令执行同步，将自己的replication id+offset发给master，master将增量的命令发回replica。<br />
有两种情况不能增量同步</p>
<ul>
  <li>master的backlog缺失，即增量日志缺失</li>
  <li>replica的replication id对于master未知</li>
</ul>

<p>这时只能进行full replication。</p>
<h4 id="full-replication">full replication</h4>
<p>master启动后台进程保存rdb文件，同时记录新的写请求。<br />
rdb保存成功时，master将rdb发送给replica，replica落盘后加载rdb。<br />
然后master发送新的写命令到replica。</p>
<pre><code class="language-mermaid">sequenceDiagram
    participant m as master
    participant b as bg process
    participant r as replica
    
    m-&gt;&gt;b: 创建
    note over b: 生成rdb
    note over m: 记录写请求
    m-&gt;&gt;r: 发送rdb
    note over r: rdb落盘
    note over r: 载入rdb
    m-&gt;&gt;r: 发送写请求
</code></pre>

<h3 id="心跳机制">心跳机制</h3>
<p>replica每秒钟会ping maser，发送已确认的offset，master会记录每个replica最近回复的时间。<br />
用户可配置仅当master已连接N个replica，且每个replica延迟不超过M秒，才接收写请求。<br />
这样能缩短数据丢失的时间窗口，但不能保证写请求的一致性(在每个实例上都能读取到更新)，这不是replication框架能解决的。</p>
<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code># 配置如下
min-replicas-to-write N
min-replicas-max-lag M
</code></pre></div></div>

<h3 id="数据一致性">数据一致性</h3>
<p>由于主从同步的延迟，路由到replica的读请求可能会读取到旧数据。<br />
想获取最新数据，我们可以使用wait命令，比如<code class="language-plaintext highlighter-rouge">wait 2 1000</code>，会client等待，直到至少2个replica同步了之前的全部写请求或者超时。<br />
这样并不能保证获取到最新数据，或保证最新数据不会丢失(未获取到最新数据的replica可能晋升为master)，但能提供大概率的兜底。</p>

<h3 id="重启-1">重启</h3>
<p>master和replica都需要开启rdb，因为rdb会保存replication id+offset，而仅使用aof，这些状态会丢失。</p>

<h3 id="淘汰策略-1">淘汰策略</h3>
<p>和master不同，replica不会主动过期key</p>
<ul>
  <li>master过期或淘汰key时，会向replica发送del命令</li>
  <li>为了保持读请求一致，replica会隐藏已过期的数据，但不会直接删除</li>
  <li>lua脚本执行过程中不会过期数据，防止master和replica执行结果不一致</li>
  <li>业务中需要使用expireAt或pExpireAt，减少过期数据不一致的窗口期</li>
</ul>

<h3 id="最大内存">最大内存</h3>
<p>replica默认会忽略maxmemory配置，因为replica会跟踪master的状态，大部分时间下，都不会超过master设置的maxmemory，但是应用需要留出一些空间做缓冲。</p>

<h3 id="sentinel">sentinel</h3>
<p>sentinel可以解决主从架构下的这两个问题</p>
<ul>
  <li>master宕机后replica怎么晋升为master</li>
  <li>client怎么知道当前master是谁</li>
</ul>

<h4 id="部署">部署</h4>
<p>我们部署一个一主两从三个sentinel的集群，在<code class="language-plaintext highlighter-rouge">deployment/redis-sentinel</code>文件夹下。</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">ConfigMap</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">redis-config-master</span>
<span class="na">data</span><span class="pi">:</span>
  <span class="na">redis-config</span><span class="pi">:</span> <span class="pi">|</span>
    <span class="s">maxmemory 128mb</span>
    <span class="s">maxmemory-policy allkeys-lru</span>
    <span class="s">save 60 1000</span>
    <span class="s">appendonly yes</span>
    <span class="s">replica-announce-ip redis-service-master</span>
    <span class="s">replica-announce-port 6379</span>
    <span class="s">client-output-buffer-limit replica 512mb 256mb 120</span>
<span class="s">---</span>
<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">ConfigMap</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">redis-config-rep1</span>
<span class="na">data</span><span class="pi">:</span>
  <span class="na">redis-config</span><span class="pi">:</span> <span class="pi">|</span>
    <span class="s">maxmemory 128mb</span>
    <span class="s">maxmemory-policy allkeys-lru</span>
    <span class="s">save 60 1000</span>
    <span class="s">appendonly yes</span>
    <span class="s">replicaof redis-service-master 6379</span>
    <span class="s">replica-announce-ip redis-service-rep1</span>
    <span class="s">replica-announce-port 6379</span>
    <span class="s">client-output-buffer-limit replica 512mb 256mb 120</span>
</code></pre></div></div>
<p>上面是master和replica1的配置(replica2类似)</p>
<ul>
  <li>maxmemory和maxmemory-policy是内存上限和淘汰策略</li>
  <li>save是rdb配置，表示60秒内有1000次以上的更新触发创建rdb文件</li>
  <li>appendonly是aof开关</li>
  <li>replicaof指定replica对应的master，这里<code class="language-plaintext highlighter-rouge">redis-service-master</code>是service name，6379是端口</li>
  <li>replica-announce-ip和replica-announce-port表示sentinel给client展示的域名(或ip)和端口</li>
  <li>client-output-buffer-limit指定主从同步时的缓冲区大小，512mb是上限，120秒内256mb是软上限，达到上限或一段时间内达到软上限，会断开连接，所以要按需设置大一些</li>
</ul>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">ConfigMap</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">redis-sentinel-config-s0</span>
<span class="na">data</span><span class="pi">:</span>
  <span class="na">redis-config</span><span class="pi">:</span> <span class="pi">|</span>
    <span class="s">port 5004</span>
    <span class="s">sentinel monitor mymaster redis-service-master 6379 2</span>
    <span class="s">sentinel down-after-milliseconds mymaster 5000</span>
    <span class="s">sentinel failover-timeout mymaster 60000</span>
    <span class="s">sentinel parallel-syncs mymaster 1</span>
    <span class="s">sentinel resolve-hostnames yes</span>
    <span class="s">sentinel announce-hostnames yes</span>
    <span class="s">sentinel announce-ip redis-service-s0</span>
    <span class="s">sentinel announce-port 5004</span>
<span class="s">---</span>
</code></pre></div></div>
<p>上面是sentinel1的配置，另外两个配置类似</p>
<ul>
  <li>port是sentinel的端口</li>
  <li>mymaster是集群名称，monitor后面跟监控的master的地址，最后的2是quorum大小，表示同时有两个sentinel发现master宕机才会发起选举(选举成功至少要majority数量的sentinel)</li>
  <li>down-after-milliseconds表示sentinel判断master宕机需要的时长(即最后一次成功的ping和当前时间的差)</li>
  <li>parallel-syncs表示新master同时reconfigure的replica数量，reconfigure的过程中会阻塞replica的读请求，所以不要设置为replica总数</li>
  <li>resolve-hostnames、announce-hostnames以及announce-ip和announce-port指定sentinel之间通信的地址</li>
</ul>

<blockquote>
  <p>注意<br />
redis-sentinel中，要么全部使用域名作为地址，要么全部使用ip，不要混用<br />
kubernetes集群中部署redis-sentinel，最好使用service name作为域名通信，
这样不会受pod启停的ip变更影响</p>
</blockquote>

<h4 id="状态查询">状态查询</h4>
<p>通过sentinel，我们可以查询master和replica的状态。<br />
先通过redis-cli登录sentinel，我们有三个sentinel实例，这里登录第一个，登录其他的没有区别</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># run redis container and log in</span>
k run <span class="nt">-it</span> <span class="nt">--image</span><span class="o">=</span>redis:alpine <span class="nt">--</span> cli
k <span class="nb">exec</span> <span class="nt">-it</span> cli <span class="nt">--</span> sh
<span class="c"># login redis sentinel</span>
redis-cli <span class="nt">-h</span> redis-service-s0 <span class="nt">-p</span> 5004
</code></pre></div></div>
<p>查询master</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sentinel master mymaster
 1<span class="o">)</span> <span class="s2">"name"</span>
 2<span class="o">)</span> <span class="s2">"mymaster"</span>
 3<span class="o">)</span> <span class="s2">"ip"</span>
 4<span class="o">)</span> <span class="s2">"redis-service-rep1"</span>
 5<span class="o">)</span> <span class="s2">"port"</span>
 6<span class="o">)</span> <span class="s2">"6379"</span>
31<span class="o">)</span> <span class="s2">"num-slaves"</span>
32<span class="o">)</span> <span class="s2">"2"</span>
33<span class="o">)</span> <span class="s2">"num-other-sentinels"</span>
34<span class="o">)</span> <span class="s2">"2"</span>
35<span class="o">)</span> <span class="s2">"quorum"</span>
36<span class="o">)</span> <span class="s2">"2"</span>
</code></pre></div></div>
<p>这里只保留了一些关键信息，比如master的地址、replica数量、其他sentinel数量、quorum数量。<br />
查询replica</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sentinel replicas mymaster
1<span class="o">)</span>  1<span class="o">)</span> <span class="s2">"name"</span>
    2<span class="o">)</span> <span class="s2">"redis-service-rep2:6379"</span>
    3<span class="o">)</span> <span class="s2">"ip"</span>
    4<span class="o">)</span> <span class="s2">"redis-service-rep2"</span>
    5<span class="o">)</span> <span class="s2">"port"</span>
    6<span class="o">)</span> <span class="s2">"6379"</span>
   33<span class="o">)</span> <span class="s2">"master-host"</span>
   34<span class="o">)</span> <span class="s2">"redis-service-rep1"</span>
   35<span class="o">)</span> <span class="s2">"master-port"</span>
   36<span class="o">)</span> <span class="s2">"6379"</span>
2<span class="o">)</span>  1<span class="o">)</span> <span class="s2">"name"</span>
    2<span class="o">)</span> <span class="s2">"redis-service-master:6379"</span>
    3<span class="o">)</span> <span class="s2">"ip"</span>
    4<span class="o">)</span> <span class="s2">"redis-service-master"</span>
    5<span class="o">)</span> <span class="s2">"port"</span>
    6<span class="o">)</span> <span class="s2">"6379"</span>
   33<span class="o">)</span> <span class="s2">"master-host"</span>
   34<span class="o">)</span> <span class="s2">"redis-service-rep1"</span>
   35<span class="o">)</span> <span class="s2">"master-port"</span>
   36<span class="o">)</span> <span class="s2">"6379"</span>
</code></pre></div></div>
<p>综合以上信息，我们可以看到集群当前的master是replica1，而replica有replica2和原来的master，集群经历过重新选主。<br />
获取master地址，这是sentinel和client交互中最常用的命令</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>SENTINEL get-master-addr-by-name mymaster
1<span class="o">)</span> <span class="s2">"redis-service-rep1"</span>
2<span class="o">)</span> <span class="s2">"6379"</span>
</code></pre></div></div>

<h4 id="failover测试">failover测试</h4>
<p>当前master为replica1，我们删除对应的deployment(保留service)，过一会再创建deployment，这样剩下的replica2和原master就会有一个晋升为新的master。</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>k delete deploy redis-deployment-rep1
<span class="c"># switch to sentinel0</span>
SENTINEL get-master-addr-by-name mymaster
1<span class="o">)</span> <span class="s2">"redis-service-master"</span>
2<span class="o">)</span> <span class="s2">"6379"</span>
</code></pre></div></div>
<p>由于down-after-milliseconds设置为5秒，经过5秒后，sentinel判定replica1宕机，将master选为新的master。<br />
我们再恢复replica1</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>k apply <span class="nt">-f</span> .<span class="se">\r</span>edis-rep1.yaml
<span class="c"># switch to sentinel0</span>
sentinel replicas mymaster
1<span class="o">)</span>  1<span class="o">)</span> <span class="s2">"name"</span>
    2<span class="o">)</span> <span class="s2">"redis-service-rep1:6379"</span>
    3<span class="o">)</span> <span class="s2">"ip"</span>
    4<span class="o">)</span> <span class="s2">"redis-service-rep1"</span>
    5<span class="o">)</span> <span class="s2">"port"</span>
    6<span class="o">)</span> <span class="s2">"6379"</span>
   33<span class="o">)</span> <span class="s2">"master-host"</span>
   34<span class="o">)</span> <span class="s2">"redis-service-master"</span>
   35<span class="o">)</span> <span class="s2">"master-port"</span>
   36<span class="o">)</span> <span class="s2">"6379"</span>
2<span class="o">)</span>  1<span class="o">)</span> <span class="s2">"name"</span>
    2<span class="o">)</span> <span class="s2">"redis-service-rep2:6379"</span>
    3<span class="o">)</span> <span class="s2">"ip"</span>
    4<span class="o">)</span> <span class="s2">"redis-service-rep2"</span>
    5<span class="o">)</span> <span class="s2">"port"</span>
    6<span class="o">)</span> <span class="s2">"6379"</span>
   33<span class="o">)</span> <span class="s2">"master-host"</span>
   34<span class="o">)</span> <span class="s2">"redis-service-master"</span>
   35<span class="o">)</span> <span class="s2">"master-port"</span>
   36<span class="o">)</span> <span class="s2">"6379"</span>
</code></pre></div></div>
<p>可以看到恢复后replica1确认master为新的master。<br />
sentinel的命令还有很多，我们会在后续章节结合使用场景讲解。</p>

<h4 id="pubsub">pub/sub</h4>
<p>client可以监听sentinel发出的事件，从而做出相应决策。</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># sentinel0</span>
psubscribe <span class="k">*</span>
</code></pre></div></div>
<p>我们监听了所有事件，然后删除replica1对应的deployment(此时replica1为master)</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>k delete deploy redis-deployment-rep1
<span class="c"># switch to sentinel0</span>
3<span class="o">)</span> <span class="s2">"+sdown"</span>
4<span class="o">)</span> <span class="s2">"master mymaster redis-service-rep1 6379"</span>
3<span class="o">)</span> <span class="s2">"+odown"</span>
4<span class="o">)</span> <span class="s2">"master mymaster redis-service-rep1 6379 #quorum 2/2"</span>
3<span class="o">)</span> <span class="s2">"+try-failover"</span>
4<span class="o">)</span> <span class="s2">"master mymaster redis-service-rep1 6379"</span>
3<span class="o">)</span> <span class="s2">"+selected-slave"</span>
4<span class="o">)</span> <span class="s2">"slave redis-service-master:6379 redis-service-master 6379 @ mymaster redis-service-rep1 6379"</span>
3<span class="o">)</span> <span class="s2">"+promoted-slave"</span>
4<span class="o">)</span> <span class="s2">"slave redis-service-master:6379 redis-service-master 6379 @ mymaster redis-service-rep1 6379"</span>
3<span class="o">)</span> <span class="s2">"+slave-reconf-done"</span>
4<span class="o">)</span> <span class="s2">"slave redis-service-rep2:6379 redis-service-rep2 6379 @ mymaster redis-service-rep1 6379"</span>
3<span class="o">)</span> <span class="s2">"-odown"</span>
4<span class="o">)</span> <span class="s2">"master mymaster redis-service-rep1 6379"</span>
3<span class="o">)</span> <span class="s2">"+failover-end"</span>
4<span class="o">)</span> <span class="s2">"master mymaster redis-service-rep1 6379"</span>
3<span class="o">)</span> <span class="s2">"+switch-master"</span>
4<span class="o">)</span> <span class="s2">"mymaster redis-service-rep1 6379 redis-service-master 6379"</span>
3<span class="o">)</span> <span class="s2">"+slave"</span>
4<span class="o">)</span> <span class="s2">"slave redis-service-rep2:6379 redis-service-rep2 6379 @ mymaster redis-service-master 6379"</span>
3<span class="o">)</span> <span class="s2">"+slave"</span>
4<span class="o">)</span> <span class="s2">"slave redis-service-rep1:6379 redis-service-rep1 6379 @ mymaster redis-service-master 6379"</span>
3<span class="o">)</span> <span class="s2">"+sdown"</span>
4<span class="o">)</span> <span class="s2">"slave redis-service-rep1:6379 redis-service-rep1 6379 @ mymaster redis-service-master 6379"</span>
3<span class="o">)</span> <span class="s2">"-sdown"</span>
4<span class="o">)</span> <span class="s2">"slave redis-service-rep1:6379 redis-service-rep1 6379 @ mymaster redis-service-master 6379"</span>
</code></pre></div></div>
<p>中间部分消息被忽略，我们可以看出整个failover的过程</p>
<ul>
  <li>sentinel察觉master宕机，发出+sdown</li>
  <li>quorum个sentinel认为master宕机，发出+odown，启动failover</li>
  <li>选出待晋升的replica，这里是<code class="language-plaintext highlighter-rouge">redis-service-master</code></li>
  <li>晋升成功，开始reconf过程，更新新的replica的配置以及数据</li>
  <li>reconf完成，发出+switch-master，client可以监听此事件更新master</li>
  <li>发出+slave，表示新的replica已添加</li>
  <li>手动重启replica1后，-sdown显示replica1已作为replica恢复运行</li>
</ul>

<h4 id="sentinelreplica-discovery">sentinel/replica discovery</h4>
<p>在sentinel中只需要指定最初的master地址，就可以自动发现其他的replica以及sentinel的地址，这也是通过pub/sub实现的。<br />
sentinel会订阅master和replica的<code class="language-plaintext highlighter-rouge">__sentinel__:hello</code>这个channel，并定时向其中发布自己的地址。<br />
hello消息中包含sentinel最新的地址以及master的配置信息，所以sentinel会自动同步最新配置。</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>subscribe __sentinel__:hello
3<span class="o">)</span> <span class="s2">"redis-service-s0,5004,2a3bc966803f957bc5921319a0f3d9fb69ffeddc,4,mymaster,redis-service-master,6379,4"</span>
3<span class="o">)</span> <span class="s2">"redis-service-s1,5005,8a4604ca225eedfd2ac0e145729f01f5a5a3e0db,4,mymaster,redis-service-master,6379,4"</span>
3<span class="o">)</span> <span class="s2">"redis-service-s2,5006,d840ae66fb00df8aea558fe8b7e046ac8d2a37bf,4,mymaster,redis-service-master,6379,4"</span>
</code></pre></div></div>
<p>上面就是用redis-cli登录replica1订阅的结果，可以看到所有sentinel的地址。</p>

<h4 id="failover启动">failover启动</h4>
<p>成功启动failover，需要经历下面的过程</p>
<ul>
  <li>部分sentinel发现master在down-after-milliseconds(项目设为5秒)的时间内，没有回复正确的响应，发出+sdown事件</li>
  <li>如果某sentinel发现quorum个sentinel在一段时间内都认为master处于离线状态，则发出+odown事件</li>
  <li>+odown事件会触发检测majority个sentinel都在线，这时才会真正启动failover</li>
</ul>

<h4 id="reconfiguration">reconfiguration</h4>
<p>除了failover，sentinel还会在以下情况执行reconf</p>
<ul>
  <li>自认为是master的replica，会被配置为真正的master的replica</li>
  <li>连接到错误master的replica，会被配置为连接真正的master</li>
</ul>

<p>sentinel不会马上修正配置错误的replica，而是确认错误持续时间超过新配置广播的时间，这样是为了防止sentinel从分区恢复时，将过期配置同步到实例。</p>

<h4 id="replica-selection">replica selection</h4>
<p>failover启动后（+odown事件已发出，且已经从majority中选举出执行failover的sentinel），就会开始选举新的master。<br />
首先筛选和master断连时间不超过如下公式的replica。</p>

\[(down-after-milliseconds * 10) + SDOWN\_state\_ms\]

<p>然后按以下指标排序</p>
<ul>
  <li>replica priority，越小越靠前，0表示不参与选举</li>
  <li>offset，越大越靠前</li>
  <li>run id，字典序小的被选中</li>
</ul>

<p>举个例子，如下replica是这样排序的</p>

<table>
  <thead>
    <tr>
      <th><strong>order</strong></th>
      <th><strong>priority</strong></th>
      <th><strong>offset</strong></th>
      <th><strong>run id</strong></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1</td>
      <td>1</td>
      <td>100</td>
      <td>abc</td>
    </tr>
    <tr>
      <td>2</td>
      <td>1</td>
      <td>100</td>
      <td>bcd</td>
    </tr>
    <tr>
      <td>3</td>
      <td>1</td>
      <td>95</td>
      <td>cad</td>
    </tr>
    <tr>
      <td>4</td>
      <td>2</td>
      <td>100</td>
      <td>acd</td>
    </tr>
    <tr>
      <td>5</td>
      <td>0</td>
      <td>100</td>
      <td>bde</td>
    </tr>
  </tbody>
</table>

<h4 id="quorum">quorum</h4>
<p>quorum的设置在failover中起到极其重要的作用</p>
<ul>
  <li>quorum个sentinel触发+sdown并持续一段时间，才会触发+odown</li>
  <li>\(min(quorum,majority)\)个sentinel连通才能选出执行failover的sentinel</li>
</ul>

<p>所以通过设置quorum可以调节集群failover的灵敏度。</p>

<h2 id="应用">应用</h2>
<h3 id="分布式锁">分布式锁</h3>
<h3 id="rate-limiter">rate limiter</h3>
<h3 id="leaderboard">leaderboard</h3>
<h3 id="daily-active-users">daily active users</h3>
<h2 id="常见问题">常见问题</h2>]]></content><author><name>cafewang</name></author><category term="redis" /><summary type="html"><![CDATA[本文主要是总结redis相关的重要知识点和应用，通过实操来深入理解redis。]]></summary></entry><entry><title type="html">微服务缓存实战</title><link href="https://cafewang.github.io//microservices/cache" rel="alternate" type="text/html" title="微服务缓存实战" /><published>2025-05-06T10:56:00+00:00</published><updated>2025-05-06T10:56:00+00:00</updated><id>https://cafewang.github.io//microservices/cache-in-action</id><content type="html" xml:base="https://cafewang.github.io//microservices/cache"><![CDATA[<p>微服务下缓存实践是一个很大的话题，本文主要关注缓存和数据库的交互方式以及缓存一致性的讨论。</p>
<h2 id="交互方式">交互方式</h2>
<h3 id="cache-aside">cache aside</h3>
<p><img src="/assets/img/cache/cache-aside.png" alt="" /><br />
最常见的交互方式，由业务方直接操作缓存和数据库，维持两者的一致。</p>
<blockquote>
  <p>注意<br />
写数据时先更新数据库，然后清空对应缓存，这种方式优于覆盖缓存，造成不一致的概率较低</p>
</blockquote>

<h3 id="write-through">write through</h3>
<p><img src="/assets/img/cache/write-through.png" alt="" /><br />
将缓存置于数据库之前，由缓存负责更新数据库，而不是业务方。</p>

<h3 id="write-back">write back</h3>
<p><img src="/assets/img/cache/write-back.png" alt="" />
和write through类似，区别在于异步更新数据库。</p>

<h2 id="缓存一致性">缓存一致性</h2>
<p>首先必须澄清这里<code class="language-plaintext highlighter-rouge">缓存一致性</code>的概念，缓存和数据库中的数据在物理上当然不可能随时都一致，我们能做到的有以下几点</p>
<ul>
  <li>提供统一的访问层，例如write through/back的方式，屏蔽业务层对数据库的感知，当然这样需要缓存做更多的工作</li>
  <li>提供最终一致性方案，在业务可接受的时间内，达到缓存和数据库的一致</li>
  <li>针对极端情况做优化</li>
</ul>

<p>我们先研究基本的cache aside</p>
<h3 id="cache-aside-1">cache aside</h3>
<p>项目准备</p>
<ul>
  <li>克隆<code class="language-plaintext highlighter-rouge">https://github.com/cafewang/cache-chemist.git</code> ，使用Idea导入</li>
  <li>jdk版本为17</li>
  <li>项目框架为springboot，包含web、jpa、redis模块</li>
  <li>使用k3d部署Kubernetes集群，包含mysql和redis，分别映射主机的8082和8081端口</li>
  <li>主机需安装docker和k3d环境，以创建集群</li>
  <li>使用前运行<code class="language-plaintext highlighter-rouge">deployment/deploy.bat</code>，linux系统也可运行其中内容</li>
</ul>

<p>表结构如下</p>

<table>
  <thead>
    <tr>
      <th style="text-align: center">id</th>
      <th style="text-align: center">name</th>
      <th style="text-align: center">age</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: center">1</td>
      <td style="text-align: center">bob</td>
      <td style="text-align: center">10</td>
    </tr>
  </tbody>
</table>

<p>redis中存储了name/age的kv结构，即<code class="language-plaintext highlighter-rouge">bob/10</code>。<br />
cache aside实现如下</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// CacheAsideService</span>
<span class="kd">public</span> <span class="nc">Long</span> <span class="nf">getAge</span><span class="o">(</span><span class="nc">String</span> <span class="n">name</span><span class="o">,</span> <span class="nc">Long</span> <span class="n">sleep</span><span class="o">)</span> <span class="o">{</span>
    <span class="nc">String</span> <span class="n">ageStr</span> <span class="o">=</span> <span class="n">stringRedisTemplate</span><span class="o">.</span><span class="na">opsForValue</span><span class="o">().</span><span class="na">get</span><span class="o">(</span><span class="n">name</span><span class="o">);</span>
    <span class="k">if</span> <span class="o">(</span><span class="nc">Objects</span><span class="o">.</span><span class="na">nonNull</span><span class="o">(</span><span class="n">ageStr</span><span class="o">))</span> <span class="o">{</span>
        <span class="k">return</span> <span class="nc">Long</span><span class="o">.</span><span class="na">parseLong</span><span class="o">(</span><span class="n">ageStr</span><span class="o">);</span>
    <span class="o">}</span>
    <span class="nc">Person</span> <span class="n">person</span> <span class="o">=</span> <span class="n">personService</span><span class="o">.</span><span class="na">getByName</span><span class="o">(</span><span class="n">name</span><span class="o">);</span>
    <span class="k">try</span> <span class="o">{</span>
        <span class="nc">Thread</span><span class="o">.</span><span class="na">sleep</span><span class="o">(</span><span class="n">sleep</span><span class="o">);</span>
    <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">InterruptedException</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">throw</span> <span class="k">new</span> <span class="nf">RuntimeException</span><span class="o">(</span><span class="n">e</span><span class="o">);</span>
    <span class="o">}</span>
    <span class="k">if</span> <span class="o">(</span><span class="nc">Objects</span><span class="o">.</span><span class="na">isNull</span><span class="o">(</span><span class="n">person</span><span class="o">))</span> <span class="o">{</span>
        <span class="k">return</span> <span class="kc">null</span><span class="o">;</span>
    <span class="o">}</span>
    <span class="nc">String</span> <span class="n">newAgeStr</span> <span class="o">=</span> <span class="n">person</span><span class="o">.</span><span class="na">getAge</span><span class="o">().</span><span class="na">toString</span><span class="o">();</span>
    <span class="n">stringRedisTemplate</span><span class="o">.</span><span class="na">opsForValue</span><span class="o">().</span><span class="na">set</span><span class="o">(</span><span class="n">name</span><span class="o">,</span> <span class="n">newAgeStr</span><span class="o">,</span> <span class="nc">Duration</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="mi">1</span><span class="o">,</span> <span class="nc">ChronoUnit</span><span class="o">.</span><span class="na">MINUTES</span><span class="o">));</span>
    <span class="k">return</span> <span class="n">person</span><span class="o">.</span><span class="na">getAge</span><span class="o">();</span>
<span class="o">}</span>

<span class="kd">public</span> <span class="nc">Long</span> <span class="nf">getAge</span><span class="o">(</span><span class="nc">String</span> <span class="n">name</span><span class="o">)</span> <span class="o">{</span>
    <span class="k">return</span> <span class="nf">getAge</span><span class="o">(</span><span class="n">name</span><span class="o">,</span> <span class="mi">0L</span><span class="o">);</span>
<span class="o">}</span>

<span class="kd">public</span> <span class="kt">void</span> <span class="nf">addPerson</span><span class="o">(</span><span class="nc">Person</span> <span class="n">person</span><span class="o">)</span> <span class="o">{</span>
    <span class="n">personService</span><span class="o">.</span><span class="na">save</span><span class="o">(</span><span class="n">person</span><span class="o">);</span>
    <span class="n">stringRedisTemplate</span><span class="o">.</span><span class="na">opsForValue</span><span class="o">().</span><span class="na">getAndDelete</span><span class="o">(</span><span class="n">person</span><span class="o">.</span><span class="na">getName</span><span class="o">());</span>
<span class="o">}</span>

<span class="kd">public</span> <span class="kt">void</span> <span class="nf">updateAge</span><span class="o">(</span><span class="nc">String</span> <span class="n">name</span><span class="o">,</span> <span class="nc">Long</span> <span class="n">age</span><span class="o">)</span> <span class="o">{</span>
    <span class="n">personService</span><span class="o">.</span><span class="na">updateAge</span><span class="o">(</span><span class="n">name</span><span class="o">,</span> <span class="n">age</span><span class="o">);</span>
    <span class="n">stringRedisTemplate</span><span class="o">.</span><span class="na">opsForValue</span><span class="o">().</span><span class="na">getAndDelete</span><span class="o">(</span><span class="n">name</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>
<p>更新操作都是删除缓存，而不是更新缓存。<br />
下面看看如下流程</p>
<ul>
  <li>插入记录<code class="language-plaintext highlighter-rouge">1/bob/10</code>，删除<code class="language-plaintext highlighter-rouge">bob</code>对应缓存</li>
  <li>查询缓存为空，查询数据库并更新缓存<code class="language-plaintext highlighter-rouge">bob/10</code></li>
  <li>更新记录为<code class="language-plaintext highlighter-rouge">1/bob/12</code>，删除<code class="language-plaintext highlighter-rouge">bob</code>对应缓存</li>
  <li>查询缓存为空，查询数据库并更新缓存<code class="language-plaintext highlighter-rouge">bob/12</code></li>
</ul>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// CacheChemistApplicationTests</span>
	<span class="nd">@Test</span>
	<span class="kt">void</span> <span class="nf">testCacheAside</span><span class="o">()</span> <span class="o">{</span>
		<span class="nc">String</span> <span class="n">name</span> <span class="o">=</span> <span class="s">"bob"</span><span class="o">;</span>
		<span class="nc">Long</span> <span class="n">age</span> <span class="o">=</span> <span class="mi">10L</span><span class="o">;</span>
		<span class="n">cacheAsideService</span><span class="o">.</span><span class="na">addPerson</span><span class="o">(</span><span class="k">new</span> <span class="nc">Person</span><span class="o">(</span><span class="mi">1L</span><span class="o">,</span> <span class="n">name</span><span class="o">,</span> <span class="n">age</span><span class="o">));</span>
        <span class="nc">Assertions</span><span class="o">.</span><span class="na">assertNull</span><span class="o">(</span><span class="n">stringRedisTemplate</span><span class="o">.</span><span class="na">opsForValue</span><span class="o">().</span><span class="na">get</span><span class="o">(</span><span class="n">name</span><span class="o">));</span>
		<span class="nc">Assertions</span><span class="o">.</span><span class="na">assertEquals</span><span class="o">(</span><span class="n">age</span><span class="o">,</span> <span class="n">cacheAsideService</span><span class="o">.</span><span class="na">getAge</span><span class="o">(</span><span class="n">name</span><span class="o">));</span>
		<span class="nc">Long</span> <span class="n">newAge</span> <span class="o">=</span> <span class="mi">12L</span><span class="o">;</span>
		<span class="n">cacheAsideService</span><span class="o">.</span><span class="na">updateAge</span><span class="o">(</span><span class="n">name</span><span class="o">,</span> <span class="n">newAge</span><span class="o">);</span>
		<span class="nc">Assertions</span><span class="o">.</span><span class="na">assertNull</span><span class="o">(</span><span class="n">stringRedisTemplate</span><span class="o">.</span><span class="na">opsForValue</span><span class="o">().</span><span class="na">get</span><span class="o">(</span><span class="n">name</span><span class="o">));</span>
		<span class="nc">Assertions</span><span class="o">.</span><span class="na">assertEquals</span><span class="o">(</span><span class="n">newAge</span><span class="o">,</span> <span class="n">cacheAsideService</span><span class="o">.</span><span class="na">getAge</span><span class="o">(</span><span class="n">name</span><span class="o">));</span>
	<span class="o">}</span>
</code></pre></div></div>

<p>下面查看异常流程</p>
<ul>
  <li>插入记录<code class="language-plaintext highlighter-rouge">1/bob/10</code>，删除缓存</li>
  <li>查询缓存为空，查询数据库得到<code class="language-plaintext highlighter-rouge">bob/10</code>，由于GC或线程调度，线程暂停了1s
    <ul>
      <li>在这1s中，另一个线程更新记录为<code class="language-plaintext highlighter-rouge">1/bob/12</code>，删除缓存，然后查询缓存为空，查询数据库，更新缓存为<code class="language-plaintext highlighter-rouge">bob/12</code></li>
    </ul>
  </li>
  <li>线程继续执行，更新缓存为<code class="language-plaintext highlighter-rouge">bob/10</code>，出现数据不一致</li>
</ul>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>	<span class="nd">@Test</span>
	<span class="kt">void</span> <span class="nf">testCacheAsideCornerCase</span><span class="o">()</span> <span class="o">{</span>
		<span class="nc">String</span> <span class="n">name</span> <span class="o">=</span> <span class="s">"bob"</span><span class="o">;</span>
		<span class="nc">Long</span> <span class="n">age</span> <span class="o">=</span> <span class="mi">10L</span><span class="o">;</span>
		<span class="n">cacheAsideService</span><span class="o">.</span><span class="na">addPerson</span><span class="o">(</span><span class="k">new</span> <span class="nc">Person</span><span class="o">(</span><span class="mi">1L</span><span class="o">,</span> <span class="n">name</span><span class="o">,</span> <span class="n">age</span><span class="o">));</span>
		<span class="nc">CompletableFuture</span><span class="o">&lt;</span><span class="nc">Void</span><span class="o">&gt;</span> <span class="n">future1</span> <span class="o">=</span> <span class="nc">CompletableFuture</span><span class="o">.</span><span class="na">runAsync</span><span class="o">(()</span> <span class="o">-&gt;</span> <span class="n">cacheAsideService</span><span class="o">.</span><span class="na">getAge</span><span class="o">(</span><span class="n">name</span><span class="o">,</span> <span class="mi">1000L</span><span class="o">));</span>
		<span class="nc">Long</span> <span class="n">newAge</span> <span class="o">=</span> <span class="mi">12L</span><span class="o">;</span>
		<span class="nc">CompletableFuture</span><span class="o">&lt;</span><span class="nc">Void</span><span class="o">&gt;</span> <span class="n">future2</span> <span class="o">=</span> <span class="nc">CompletableFuture</span><span class="o">.</span><span class="na">runAsync</span><span class="o">(()</span> <span class="o">-&gt;</span> <span class="o">{</span>
			<span class="n">cacheAsideService</span><span class="o">.</span><span class="na">updateAge</span><span class="o">(</span><span class="n">name</span><span class="o">,</span> <span class="n">newAge</span><span class="o">);</span>
			<span class="n">cacheAsideService</span><span class="o">.</span><span class="na">getAge</span><span class="o">(</span><span class="n">name</span><span class="o">);</span>
		<span class="o">});</span>
        <span class="k">try</span> <span class="o">{</span>
            <span class="n">future1</span><span class="o">.</span><span class="na">get</span><span class="o">();</span>
			<span class="n">future2</span><span class="o">.</span><span class="na">get</span><span class="o">();</span>
        <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">InterruptedException</span> <span class="o">|</span> <span class="nc">ExecutionException</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>
            <span class="k">throw</span> <span class="k">new</span> <span class="nf">RuntimeException</span><span class="o">(</span><span class="n">e</span><span class="o">);</span>
        <span class="o">}</span>
		<span class="nc">Assertions</span><span class="o">.</span><span class="na">assertEquals</span><span class="o">(</span><span class="n">age</span><span class="o">,</span> <span class="n">cacheAsideService</span><span class="o">.</span><span class="na">getAge</span><span class="o">(</span><span class="n">name</span><span class="o">));</span>
		<span class="nc">Assertions</span><span class="o">.</span><span class="na">assertEquals</span><span class="o">(</span><span class="n">newAge</span><span class="o">,</span> <span class="n">personService</span><span class="o">.</span><span class="na">getByName</span><span class="o">(</span><span class="n">name</span><span class="o">).</span><span class="na">getAge</span><span class="o">());</span>
    <span class="o">}</span>
</code></pre></div></div>

<p>用流程图表示</p>
<pre><code class="language-mermaid">sequenceDiagram
    participant t1 as Thread1
    participant t2 as Thread2
    participant r as redis
    participant m as mysql

    note over m: 记录 1/bob/10
    note over r: 缓存为空
    t1-&gt;&gt;r: 查询bob，返回空
    t1-&gt;&gt;m: 查询bob，返回 1/bob/10
    activate t1
    note over t1: GC pause for 1s
    t2-&gt;&gt;m: 更新记录为 1/bob/12
    note over m: 记录 1/bob/12
    t2-&gt;&gt;r: 删除缓存
    t2-&gt;&gt;r: 查询bob，返回空
    t2-&gt;&gt;m: 查询bob，返回 1/bob/12
    t2-&gt;&gt;r: 更新缓存为 bob/12
    note over r: 缓存为 bob/12
    deactivate t1
    t1-&gt;&gt;r: 更新缓存为 bob/10
    note over r: 缓存为 bob/10
</code></pre>

<p>虽然异常场景发生的概率不高，一旦发生，只有脏数据再次被更新，数据才会一致，这个时间可能长达几个小时甚至几天，解决方式如下</p>
<ul>
  <li>采用定时跑批的形式，重点数据分钟级别或者小时级别更新到缓存，全量数据周级别更新到缓存，保证最终一致性</li>
  <li>参考<a href="https://github.com/dtm-labs/rockscache/blob/main/helper/README-cn.md">dtm项目</a>的缓存一致性方案</li>
</ul>

<h3 id="dtm-cache">dtm cache</h3>
<p>dtm cache是cache aside的改进方案。<br />
其本质是在缓存上加上限时锁，每个锁请求通过uuid(由owner字段表示)区分</p>
<ul>
  <li>获得锁的查询线程，可以在锁周期内查库并更新缓存，这样锁周期被分为两个部分，缓存未更新和已更新
    <ul>
      <li>如果缓存有数据，则异步查库并更新缓存，先返回旧数据</li>
      <li>缓存无数据，则同步查库并更新缓存，返回新数据</li>
      <li>更新缓存先判断owner是否变更，未变更说明未过期或没有新的锁请求，可以更新缓存，然后释放锁，已变更说明有别的查询线程获得了锁，这时重新执行查询流程</li>
    </ul>
  </li>
  <li>未获得锁的查询线程，查到数据直接返回，查不到则等待一段时间(短于锁限时)再次尝试获取锁<br />
缓存采用hash存储(即java中的Map)，结构为</li>
  <li>data，缓存的内容，有独立的过期时间，应大于锁限时</li>
  <li>owner，即锁id，通过uuid生成</li>
  <li>lockUntil，表示锁限时</li>
</ul>

<p>这样不会出现并发更新缓存的情况，而且锁过期后，一旦有新的查询线程获取到锁，原查询线程会放弃更新缓存，不会导致旧数据更新缓存。<br />
数据库更新时，过期缓存对应的锁(lockUntil设为0)，使得下次查询触发异步更新。<br />
需要注意锁限时的设置，需要比<code class="language-plaintext highlighter-rouge">查库+更新缓存</code>时间更长，以免还没查询完锁就过期，时间也不能过长，导致其他查询线程等待时间太长。<br />
数据过期时间需要比锁限时长，否则会导致数据在锁限时内过期，查询线程重复等待的情况发生。<br />
我们来看看核心的代码实现</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// DtmService</span>
<span class="kd">public</span> <span class="nc">Long</span> <span class="nf">getAge</span><span class="o">(</span><span class="nc">String</span> <span class="n">name</span><span class="o">,</span> <span class="nc">Long</span> <span class="n">sleep</span><span class="o">)</span> <span class="o">{</span>
  <span class="nc">CacheStatus</span> <span class="n">cacheStatus</span><span class="o">;</span>
  <span class="k">while</span> <span class="o">(</span><span class="nc">Objects</span><span class="o">.</span><span class="na">isNull</span><span class="o">((</span><span class="n">cacheStatus</span> <span class="o">=</span> <span class="n">getAndLock</span><span class="o">(</span><span class="n">name</span><span class="o">,</span> <span class="mi">50</span><span class="o">)).</span><span class="na">getData</span><span class="o">())</span>
          <span class="o">&amp;&amp;</span> <span class="nc">Objects</span><span class="o">.</span><span class="na">isNull</span><span class="o">(</span><span class="n">cacheStatus</span><span class="o">.</span><span class="na">getNewLock</span><span class="o">()))</span> <span class="o">{</span>
    <span class="k">try</span> <span class="o">{</span>
      <span class="nc">Thread</span><span class="o">.</span><span class="na">sleep</span><span class="o">(</span><span class="mi">10</span><span class="o">);</span>
    <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">InterruptedException</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>
      <span class="k">throw</span> <span class="k">new</span> <span class="nf">RuntimeException</span><span class="o">(</span><span class="n">e</span><span class="o">);</span>
    <span class="o">}</span>
  <span class="o">}</span>
  <span class="nc">String</span> <span class="n">newLock</span> <span class="o">=</span> <span class="n">cacheStatus</span><span class="o">.</span><span class="na">getNewLock</span><span class="o">();</span>
  <span class="k">if</span> <span class="o">(</span><span class="nc">Objects</span><span class="o">.</span><span class="na">nonNull</span><span class="o">(</span><span class="n">newLock</span><span class="o">))</span> <span class="o">{</span>
    <span class="k">if</span> <span class="o">(</span><span class="nc">Objects</span><span class="o">.</span><span class="na">isNull</span><span class="o">(</span><span class="n">cacheStatus</span><span class="o">.</span><span class="na">getData</span><span class="o">()))</span> <span class="o">{</span>
      <span class="nc">FetchResult</span> <span class="n">fetchResult</span> <span class="o">=</span> <span class="n">fetchData</span><span class="o">(</span><span class="n">name</span><span class="o">,</span> <span class="n">newLock</span><span class="o">,</span> <span class="n">sleep</span><span class="o">);</span>
      <span class="k">return</span> <span class="n">fetchResult</span><span class="o">.</span><span class="na">getSuccess</span><span class="o">()</span> <span class="o">?</span> <span class="n">fetchResult</span><span class="o">.</span><span class="na">getData</span><span class="o">()</span> <span class="o">:</span> <span class="n">getAge</span><span class="o">(</span><span class="n">name</span><span class="o">);</span>
    <span class="o">}</span>
    <span class="nc">CompletableFuture</span><span class="o">.</span><span class="na">runAsync</span><span class="o">(()</span> <span class="o">-&gt;</span> <span class="n">fetchData</span><span class="o">(</span><span class="n">name</span><span class="o">,</span> <span class="n">newLock</span><span class="o">,</span> <span class="n">sleep</span><span class="o">));</span>
  <span class="o">}</span>
  <span class="k">return</span> <span class="n">cacheStatus</span><span class="o">.</span><span class="na">getData</span><span class="o">();</span>
<span class="o">}</span>
</code></pre></div></div>
<p>getAndLock通过lua脚本完成两个操作，获取data以及尝试获取锁，如果获取到锁会返回锁的uuid</p>
<ul>
  <li>data为空且未获取到锁(其他查询线程持有锁)，则等待10ms再尝试</li>
  <li>获取到锁，如果数据为空，则同步查库并更新缓存，更新成功则返回，更新失败(锁被修改)则重新查询</li>
  <li>获取到锁，数据不为空，则返回数据，异步更新缓存</li>
  <li>未获取到锁，但是有数据则直接返回</li>
</ul>

<p>来看下面两个测试场景</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// CacheChemistApplicationTests</span>
	<span class="nd">@Test</span>
	<span class="kt">void</span> <span class="nf">testDtm</span><span class="o">()</span> <span class="kd">throws</span> <span class="nc">InterruptedException</span> <span class="o">{</span>
		<span class="nc">String</span> <span class="n">name</span> <span class="o">=</span> <span class="s">"bob"</span><span class="o">;</span>
		<span class="nc">Long</span> <span class="n">age</span> <span class="o">=</span> <span class="mi">10L</span><span class="o">;</span>
		<span class="n">dtmService</span><span class="o">.</span><span class="na">addPerson</span><span class="o">(</span><span class="k">new</span> <span class="nc">Person</span><span class="o">(</span><span class="mi">1L</span><span class="o">,</span> <span class="n">name</span><span class="o">,</span> <span class="n">age</span><span class="o">));</span>
		<span class="nc">Assertions</span><span class="o">.</span><span class="na">assertEquals</span><span class="o">(</span><span class="n">age</span><span class="o">,</span> <span class="n">dtmService</span><span class="o">.</span><span class="na">getAge</span><span class="o">(</span><span class="n">name</span><span class="o">));</span>
		<span class="nc">Long</span> <span class="n">newAge</span> <span class="o">=</span> <span class="mi">12L</span><span class="o">;</span>
		<span class="n">dtmService</span><span class="o">.</span><span class="na">updateAge</span><span class="o">(</span><span class="n">name</span><span class="o">,</span> <span class="n">newAge</span><span class="o">);</span>
		<span class="nc">Assertions</span><span class="o">.</span><span class="na">assertEquals</span><span class="o">(</span><span class="n">age</span><span class="o">,</span> <span class="n">dtmService</span><span class="o">.</span><span class="na">getAge</span><span class="o">(</span><span class="n">name</span><span class="o">));</span>
		<span class="nc">Thread</span><span class="o">.</span><span class="na">sleep</span><span class="o">(</span><span class="mi">100</span><span class="o">);</span>
		<span class="nc">Assertions</span><span class="o">.</span><span class="na">assertEquals</span><span class="o">(</span><span class="n">newAge</span><span class="o">,</span> <span class="n">dtmService</span><span class="o">.</span><span class="na">getAge</span><span class="o">(</span><span class="n">name</span><span class="o">));</span>
		<span class="n">stringRedisTemplate</span><span class="o">.</span><span class="na">opsForHash</span><span class="o">().</span><span class="na">delete</span><span class="o">(</span><span class="n">name</span><span class="o">,</span> <span class="s">"data"</span><span class="o">,</span> <span class="s">"owner"</span><span class="o">,</span> <span class="s">"lockUntil"</span><span class="o">);</span>
	<span class="o">}</span>
</code></pre></div></div>
<ul>
  <li>首次查询同步更新缓存，查询到新插入数据</li>
  <li>updateAge将锁过期，后续查询返回旧数据，但是触发了异步更新缓存</li>
  <li>等待100ms后缓存更新，查询到新数据</li>
</ul>

<p>再看一下corner case</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>	<span class="kt">void</span> <span class="nf">testDtmCornerCase</span><span class="o">()</span> <span class="kd">throws</span> <span class="nc">InterruptedException</span> <span class="o">{</span>
		<span class="nc">String</span> <span class="n">name</span> <span class="o">=</span> <span class="s">"bob"</span><span class="o">;</span>
		<span class="nc">Long</span> <span class="n">age</span> <span class="o">=</span> <span class="mi">10L</span><span class="o">;</span>
		<span class="n">dtmService</span><span class="o">.</span><span class="na">addPerson</span><span class="o">(</span><span class="k">new</span> <span class="nc">Person</span><span class="o">(</span><span class="mi">1L</span><span class="o">,</span> <span class="n">name</span><span class="o">,</span> <span class="n">age</span><span class="o">));</span>
		<span class="nc">CompletableFuture</span><span class="o">&lt;</span><span class="nc">Void</span><span class="o">&gt;</span> <span class="n">future1</span> <span class="o">=</span> <span class="nc">CompletableFuture</span><span class="o">.</span><span class="na">runAsync</span><span class="o">(()</span> <span class="o">-&gt;</span> <span class="n">dtmService</span><span class="o">.</span><span class="na">getAge</span><span class="o">(</span><span class="n">name</span><span class="o">,</span> <span class="mi">1000L</span><span class="o">));</span>
		<span class="nc">Long</span> <span class="n">newAge</span> <span class="o">=</span> <span class="mi">12L</span><span class="o">;</span>
		<span class="nc">CompletableFuture</span><span class="o">&lt;</span><span class="nc">Void</span><span class="o">&gt;</span> <span class="n">future2</span> <span class="o">=</span> <span class="nc">CompletableFuture</span><span class="o">.</span><span class="na">runAsync</span><span class="o">(()</span> <span class="o">-&gt;</span> <span class="o">{</span>
			<span class="n">dtmService</span><span class="o">.</span><span class="na">updateAge</span><span class="o">(</span><span class="n">name</span><span class="o">,</span> <span class="n">newAge</span><span class="o">);</span>
			<span class="n">dtmService</span><span class="o">.</span><span class="na">getAge</span><span class="o">(</span><span class="n">name</span><span class="o">);</span>
		<span class="o">});</span>
		<span class="k">try</span> <span class="o">{</span>
			<span class="n">future1</span><span class="o">.</span><span class="na">get</span><span class="o">();</span>
			<span class="n">future2</span><span class="o">.</span><span class="na">get</span><span class="o">();</span>
		<span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">InterruptedException</span> <span class="o">|</span> <span class="nc">ExecutionException</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>
			<span class="k">throw</span> <span class="k">new</span> <span class="nf">RuntimeException</span><span class="o">(</span><span class="n">e</span><span class="o">);</span>
		<span class="o">}</span>
		<span class="nc">Assertions</span><span class="o">.</span><span class="na">assertEquals</span><span class="o">(</span><span class="n">newAge</span><span class="o">,</span> <span class="n">personService</span><span class="o">.</span><span class="na">getByName</span><span class="o">(</span><span class="n">name</span><span class="o">).</span><span class="na">getAge</span><span class="o">());</span>
		<span class="nc">Assertions</span><span class="o">.</span><span class="na">assertEquals</span><span class="o">(</span><span class="n">newAge</span><span class="o">,</span> <span class="n">dtmService</span><span class="o">.</span><span class="na">getAge</span><span class="o">(</span><span class="n">name</span><span class="o">));</span>
		<span class="n">stringRedisTemplate</span><span class="o">.</span><span class="na">opsForHash</span><span class="o">().</span><span class="na">delete</span><span class="o">(</span><span class="n">name</span><span class="o">,</span> <span class="s">"data"</span><span class="o">,</span> <span class="s">"owner"</span><span class="o">,</span> <span class="s">"lockUntil"</span><span class="o">);</span>
	<span class="o">}</span>
</code></pre></div></div>
<ul>
  <li>首个查询线程查库后暂停了1s，更新缓存是发现锁被修改，所以没有更新，而是重新查询</li>
  <li>第二个查询线程等待锁过期后获取到锁，更新缓存成功，缓存中存的是新数据</li>
</ul>

<p>用时序图来描述</p>
<pre><code class="language-mermaid">sequenceDiagram
    participant t1 as Thread1
    participant t2 as Thread2
    participant r as redis
    participant m as mysql

    note over m: 记录 1/bob/10
    note over r: 缓存为空，锁过期
    t1-&gt;&gt;r: 查询到数据为空，获得锁lock_t1，限时50ms
    t1-&gt;&gt;m: 查询bob，返回 1/bob/10
    activate t1
    note over t1: pause for 1s
    t2-&gt;&gt;m: 更新记录为 1/bob/12
    note over m: 记录 1/bob/12
    note over r: 缓存为空，锁lock_t1过期
    t2-&gt;&gt;r: 查询到数据为空，获得锁lock_t2，限时50ms
    t2-&gt;&gt;m: 查询bob，返回 1/bob/12
    t2-&gt;&gt;r: 更新缓存为 bob/12
    note over r: 缓存为 bob/12，锁为lock_t2
    deactivate t1
    t1-&gt;&gt;r: 更新缓存失败，重新查询
</code></pre>

<p>生产环境中实现Dtm需要注意以下几点</p>
<ul>
  <li>锁限时、未获取到锁的等待时间、数据过期时间设置需参考业务场景和数据库、缓存性能</li>
  <li>需要使用lua脚本，保证redis操作的顺序性</li>
  <li>需要根据业务场景，选择是否缓存空数据</li>
</ul>

<h2 id="总结">总结</h2>
<p>本文介绍了在微服务中实现缓存一致性的方案，cache aside一般是基础方案，dtm可以提供更强的一致性，不会出现旧数据覆盖新数据的情况。<br />
如果要保证最终一致性，还可以通过定时跑批补偿、领域事件、binlog异步更新作为补充，以更新时间作为版本，防止旧数据覆盖新数据。</p>]]></content><author><name>cafewang</name></author><category term="microservices" /><category term="cache" /><summary type="html"><![CDATA[微服务下缓存实践是一个很大的话题，本文主要关注缓存和数据库的交互方式以及缓存一致性的讨论。 交互方式 cache aside 最常见的交互方式，由业务方直接操作缓存和数据库，维持两者的一致。 注意 写数据时先更新数据库，然后清空对应缓存，这种方式优于覆盖缓存，造成不一致的概率较低]]></summary></entry><entry><title type="html">Sagas深度解析-介绍</title><link href="https://cafewang.github.io//microservices/sagas/intro" rel="alternate" type="text/html" title="Sagas深度解析-介绍" /><published>2025-04-26T08:39:00+00:00</published><updated>2025-04-26T08:39:00+00:00</updated><id>https://cafewang.github.io//microservices/sagas/sagas-in-depth-intro</id><content type="html" xml:base="https://cafewang.github.io//microservices/sagas/intro"><![CDATA[<p>本文以<code class="language-plaintext highlighter-rouge">微服务设计模式</code>书中的demo项目<code class="language-plaintext highlighter-rouge">ftgo-application</code>为例，讲解sagas模式在微服务架构中的具体实现。<br />
文中不会专门讲解sagas的概念，而是在演示过程中穿插解释sagas是什么，在阅读和操练项目的过程中，您会加深对sagas的理解，同时也能学到sagas的落地方式。</p>

<h2 id="项目准备">项目准备</h2>
<p>首先克隆<code class="language-plaintext highlighter-rouge">https://github.com/cafewang/ftgo-application</code> 项目，是<code class="language-plaintext highlighter-rouge">Chris Richardson</code>原项目的fork，项目包含如下模块</p>
<ul>
  <li>基础架构
    <ul>
      <li>mysql，存储业务数据和sagas状态</li>
      <li>zookeeper，被kafka和cdc-service使用</li>
      <li>kafka，用于sagas中的通信</li>
      <li>kafka-gui，查看消息内容</li>
      <li>zipkin，分布式链路追踪</li>
      <li>dynamodblocal，本地的nosql数据库，用于CQRS</li>
      <li>dynamodblocal-init，用于初始化数据库，创建相关表</li>
    </ul>
  </li>
  <li>服务
    <ul>
      <li>ftgo-api-gateway，服务网关</li>
      <li>cdc-service，服务于eventuate-tram组件，实现outbox pattern</li>
      <li>ftgo-order-service，订单服务，也是sagas的组织者</li>
      <li>ftgo-order-history-service，订单历史服务，使用eventuate-tram实现CQRS</li>
      <li>ftgo-consumer-service，顾客服务</li>
      <li>ftgo-accounting-service，账户服务，使用eventuate-local用event-sourcing实现Account</li>
      <li>ftgo-restaurant-service，餐厅服务</li>
      <li>ftgo-kitchen-service，厨房服务</li>
      <li>ftgo-delivery-service，送餐服务</li>
    </ul>
  </li>
</ul>

<p>环境要求</p>
<ul>
  <li>使用Idea导入项目，项目由gradle管理，会自动下载</li>
  <li>使用jdk8</li>
  <li>保证本地有已启动的docker环境(包含docker compose)，使用<code class="language-plaintext highlighter-rouge">gradle :composeUp</code>命令启动所有服务</li>
  <li>使用<code class="language-plaintext highlighter-rouge">gradle :composeDown</code>关闭所有服务</li>
  <li>k8s部署请参考项目的readme</li>
</ul>

<p>主要依赖</p>
<ul>
  <li>springboot，微服务框架</li>
  <li>spring-data-JPA，ORM框架</li>
  <li>eventuate-tram，实现服务间的异步通信，基于消息队列(kafka)和outbox模式</li>
  <li>eventuate-sagas，基于eventuate-tram实现sagas</li>
  <li>eventuate-local，event-sourcing框架</li>
</ul>

<h2 id="订单创建">订单创建</h2>
<h3 id="创建consumer">创建Consumer</h3>
<ul>
  <li>打开cusotomer-service的swagger<code class="language-plaintext highlighter-rouge">http://localhost:8081/swagger-ui/index.html</code> ，调用创建接口</li>
  <li>连接数据库(端口为3306，root / rootpassword)，查看customers表，创建了id=1的顾客<br />
<img src="/assets/img/sagas/create-order/customer.png" alt="" /></li>
</ul>

<h4 id="领域事件发布">领域事件发布</h4>
<p>查看创建customer的接口，位于CustomerService</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="nd">@Transactional</span>
  <span class="kd">public</span> <span class="nc">ResultWithEvents</span><span class="o">&lt;</span><span class="nc">Consumer</span><span class="o">&gt;</span> <span class="nf">create</span><span class="o">(</span><span class="nc">PersonName</span> <span class="n">name</span><span class="o">)</span> <span class="o">{</span>
      <span class="nc">ResultWithEvents</span><span class="o">&lt;</span><span class="nc">Consumer</span><span class="o">&gt;</span> <span class="n">rwe</span> <span class="o">=</span> <span class="nc">Consumer</span><span class="o">.</span><span class="na">create</span><span class="o">(</span><span class="n">name</span><span class="o">);</span>
      <span class="n">consumerRepository</span><span class="o">.</span><span class="na">save</span><span class="o">(</span><span class="n">rwe</span><span class="o">.</span><span class="na">result</span><span class="o">);</span>
      <span class="n">domainEventPublisher</span><span class="o">.</span><span class="na">publish</span><span class="o">(</span><span class="nc">Consumer</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="n">rwe</span><span class="o">.</span><span class="na">result</span><span class="o">.</span><span class="na">getId</span><span class="o">(),</span> <span class="n">rwe</span><span class="o">.</span><span class="na">events</span><span class="o">);</span>
      <span class="k">return</span> <span class="n">rwe</span><span class="o">;</span>
  <span class="o">}</span>
</code></pre></div></div>

<p>重点关注<code class="language-plaintext highlighter-rouge">publish</code>操作，使用<code class="language-plaintext highlighter-rouge">code graph</code>插件，一路追踪到<code class="language-plaintext highlighter-rouge">MessageProducerImpl.send</code>方法</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="kd">protected</span> <span class="kt">void</span> <span class="nf">send</span><span class="o">(</span><span class="nc">Message</span> <span class="n">message</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">this</span><span class="o">.</span><span class="na">preSend</span><span class="o">(</span><span class="n">message</span><span class="o">);</span>

        <span class="k">try</span> <span class="o">{</span>
            <span class="k">this</span><span class="o">.</span><span class="na">implementation</span><span class="o">.</span><span class="na">send</span><span class="o">(</span><span class="n">message</span><span class="o">);</span>
            <span class="k">this</span><span class="o">.</span><span class="na">postSend</span><span class="o">(</span><span class="n">message</span><span class="o">,</span> <span class="o">(</span><span class="nc">RuntimeException</span><span class="o">)</span><span class="kc">null</span><span class="o">);</span>
        <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">RuntimeException</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>
            <span class="k">this</span><span class="o">.</span><span class="na">logger</span><span class="o">.</span><span class="na">error</span><span class="o">(</span><span class="s">"Sending failed"</span><span class="o">,</span> <span class="n">e</span><span class="o">);</span>
            <span class="k">this</span><span class="o">.</span><span class="na">postSend</span><span class="o">(</span><span class="n">message</span><span class="o">,</span> <span class="n">e</span><span class="o">);</span>
            <span class="k">throw</span> <span class="n">e</span><span class="o">;</span>
        <span class="o">}</span>
    <span class="o">}</span>
</code></pre></div></div>

<ul>
  <li><code class="language-plaintext highlighter-rouge">preSend</code>和<code class="language-plaintext highlighter-rouge">postSend</code>是拓展点，无需关注，重点是<code class="language-plaintext highlighter-rouge">this.implementation.send</code></li>
  <li>其实现是<code class="language-plaintext highlighter-rouge">MessageProducerJdbcImpl.send</code>，可以看到是向Message表中插入一条记录</li>
</ul>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">send</span><span class="o">(</span><span class="nc">Message</span> <span class="n">message</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">String</span> <span class="n">id</span> <span class="o">=</span>  <span class="n">eventuateCommonJdbcOperations</span><span class="o">.</span><span class="na">insertIntoMessageTable</span><span class="o">(</span><span class="n">idGenerator</span><span class="o">,</span>
                  <span class="n">message</span><span class="o">.</span><span class="na">getPayload</span><span class="o">(),</span>
                  <span class="n">message</span><span class="o">.</span><span class="na">getRequiredHeader</span><span class="o">(</span><span class="nc">Message</span><span class="o">.</span><span class="na">DESTINATION</span><span class="o">),</span>
                  <span class="n">message</span><span class="o">.</span><span class="na">getHeaders</span><span class="o">(),</span>
                  <span class="n">eventuateSchema</span><span class="o">);</span>
    
        <span class="n">message</span><span class="o">.</span><span class="na">setHeader</span><span class="o">(</span><span class="nc">Message</span><span class="o">.</span><span class="na">ID</span><span class="o">,</span> <span class="n">id</span><span class="o">);</span>
    <span class="o">}</span>
</code></pre></div></div>

<ul>
  <li>查看Message表，字段如下
<img src="/assets/img/sagas/create-order/message.png" alt="" />
    <div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"PARTITION_ID"</span><span class="p">:</span><span class="w"> </span><span class="s2">"1"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"event-aggregate-type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"net.chrisrichardson.ftgo.consumerservice.domain.Consumer"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"DATE"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Sat, 26 Apr 2025 15:37:42 GMT"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"event-aggregate-id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"1"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"event-type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"net.chrisrichardson.ftgo.consumerservice.domain.ConsumerCreated"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"DESTINATION"</span><span class="p">:</span><span class="w"> </span><span class="s2">"net.chrisrichardson.ftgo.consumerservice.domain.Consumer"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"ID"</span><span class="p">:</span><span class="w"> </span><span class="s2">"0000019672bf4f6d-8e7a23b4ef820000"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div>    </div>
    <ul>
      <li>消息id，是生成器生成的，全局唯一</li>
      <li>destination，对应消息存储的channel，kafka中就是topic</li>
      <li>headers，type对应事件类型，aggregate-id对应业务id，这里是cusomer id，aggregate-type是聚合类型，也对应消息channel</li>
      <li>payload，负载，这里为空</li>
      <li>published</li>
      <li>creationTime</li>
    </ul>
  </li>
  <li>登录kafka-gui，<code class="language-plaintext highlighter-rouge">http://localhost:9088/</code> 查看topic<code class="language-plaintext highlighter-rouge">net.chrisrichardson.ftgo.consumerservice.domain.Consumer</code>，可以看到一条消息
<img src="/assets/img/sagas/create-order/consumer-message.png" alt="" /></li>
</ul>

<p>总结一下，领域事件发布流程如下</p>
<ul>
  <li>publish方法将领域事件（这里是ConsumerCreated）保存为Message表中的记录</li>
  <li>cdc-service获取到binlog，向destination的topic中发送消息</li>
</ul>

<h4 id="领域事件接收">领域事件接收</h4>
<p>注册：在<code class="language-plaintext highlighter-rouge">AccountingMessagingConfiguration</code>类中，<code class="language-plaintext highlighter-rouge">DomainEventDispatcher</code>作为接收方接收并路由领域事件</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="nd">@Bean</span>
    <span class="kd">public</span> <span class="nc">DomainEventDispatcher</span> <span class="nf">domainEventDispatcher</span><span class="o">(</span><span class="nc">AccountingEventConsumer</span> <span class="n">accountingEventConsumer</span><span class="o">,</span> <span class="nc">DomainEventDispatcherFactory</span> <span class="n">domainEventDispatcherFactory</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="n">domainEventDispatcherFactory</span><span class="o">.</span><span class="na">make</span><span class="o">(</span><span class="s">"accountingServiceDomainEventDispatcher"</span><span class="o">,</span> <span class="n">accountingEventConsumer</span><span class="o">.</span><span class="na">domainEventHandlers</span><span class="o">());</span>
    <span class="o">}</span>
</code></pre></div></div>
<p>处理：在类<code class="language-plaintext highlighter-rouge">AccountingEventConsumer</code>中定义了处理方法，接收的topic由aggregateType指定，事件类型由header中eventType指定，这里为<code class="language-plaintext highlighter-rouge">net.chrisrichardson.ftgo.consumerservice.domain.ConsumerCreated</code><br />
收到<code class="language-plaintext highlighter-rouge">ConsumerCreated</code>事件后会以event-sourcing的方式创建account，这里不展开</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="kd">public</span> <span class="nc">DomainEventHandlers</span> <span class="nf">domainEventHandlers</span><span class="o">()</span> <span class="o">{</span>
    <span class="k">return</span> <span class="nc">DomainEventHandlersBuilder</span>
            <span class="o">.</span><span class="na">forAggregateType</span><span class="o">(</span><span class="s">"net.chrisrichardson.ftgo.consumerservice.domain.Consumer"</span><span class="o">)</span>
            <span class="o">.</span><span class="na">onEvent</span><span class="o">(</span><span class="nc">ConsumerCreated</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="k">this</span><span class="o">::</span><span class="n">createAccount</span><span class="o">)</span> 
            <span class="o">.</span><span class="na">build</span><span class="o">();</span>
  <span class="o">}</span>

  <span class="kd">private</span> <span class="kt">void</span> <span class="nf">createAccount</span><span class="o">(</span><span class="nc">DomainEventEnvelope</span><span class="o">&lt;</span><span class="nc">ConsumerCreated</span><span class="o">&gt;</span> <span class="n">dee</span><span class="o">)</span> <span class="o">{</span>
    <span class="n">accountingService</span><span class="o">.</span><span class="na">create</span><span class="o">(</span><span class="n">dee</span><span class="o">.</span><span class="na">getAggregateId</span><span class="o">());</span>
  <span class="o">}</span>
</code></pre></div></div>

<h3 id="创建restaurant">创建Restaurant</h3>
<p>打开swagger<code class="language-plaintext highlighter-rouge">http://localhost:8084/swagger-ui/index.html</code> ，调用创建接口</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
    </span><span class="nl">"address"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"city"</span><span class="p">:</span><span class="w"> </span><span class="s2">"string"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"state"</span><span class="p">:</span><span class="w"> </span><span class="s2">"string"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"street1"</span><span class="p">:</span><span class="w"> </span><span class="s2">"string"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"street2"</span><span class="p">:</span><span class="w"> </span><span class="s2">"string"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"zip"</span><span class="p">:</span><span class="w"> </span><span class="s2">"string"</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="nl">"menu"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"menuItems"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
            </span><span class="p">{</span><span class="w">
                </span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"1"</span><span class="p">,</span><span class="w">
                </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"沙拉"</span><span class="p">,</span><span class="w">
                </span><span class="nl">"price"</span><span class="p">:</span><span class="w"> </span><span class="s2">"10"</span><span class="w">
            </span><span class="p">},</span><span class="w">
            </span><span class="p">{</span><span class="w">
                </span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2"</span><span class="p">,</span><span class="w">
                </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"披萨"</span><span class="p">,</span><span class="w">
                </span><span class="nl">"price"</span><span class="p">:</span><span class="w"> </span><span class="s2">"20"</span><span class="w">
            </span><span class="p">}</span><span class="w">
        </span><span class="p">]</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"安妮餐厅"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>查询数据库<code class="language-plaintext highlighter-rouge">ftgo-restaurant-service</code>，查看restaurant表和menu_items表中新建的记录<br />
<img src="/assets/img/sagas/create-order/restaurant-in-db.png" alt="" /><br />
<img src="/assets/img/sagas/create-order/menu-items-in-db.png" alt="" /><br />
同样创建Restaurant会触发<code class="language-plaintext highlighter-rouge">RestaurantCreated</code>领域事件，可以在Message表和kafka中查看到<br />
<img src="/assets/img/sagas/create-order/restaurant-created-in-db.png" alt="" /><br />
<img src="/assets/img/sagas/create-order/restaurant-created-in-topic.png" alt="" /></p>

<h3 id="创建order">创建Order</h3>
<h4 id="本地化">本地化</h4>
<p>order-service会备份restaurant和menuItem信息，监听<code class="language-plaintext highlighter-rouge">RestaurantCreated</code>和<code class="language-plaintext highlighter-rouge">RestaurantMenuRevised</code>事件。<br />
这种冗余变化不频繁的外部数据的方式经常在微服务中使用。</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="kd">public</span> <span class="nc">DomainEventHandlers</span> <span class="nf">domainEventHandlers</span><span class="o">()</span> <span class="o">{</span>
    <span class="k">return</span> <span class="nc">DomainEventHandlersBuilder</span>
            <span class="o">.</span><span class="na">forAggregateType</span><span class="o">(</span><span class="s">"net.chrisrichardson.ftgo.restaurantservice.domain.Restaurant"</span><span class="o">)</span>
            <span class="o">.</span><span class="na">onEvent</span><span class="o">(</span><span class="nc">RestaurantCreated</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="k">this</span><span class="o">::</span><span class="n">createMenu</span><span class="o">)</span>
            <span class="o">.</span><span class="na">onEvent</span><span class="o">(</span><span class="nc">RestaurantMenuRevised</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="k">this</span><span class="o">::</span><span class="n">reviseMenu</span><span class="o">)</span>
            <span class="o">.</span><span class="na">build</span><span class="o">();</span>
  <span class="o">}</span>
</code></pre></div></div>

<h4 id="创建接口">创建接口</h4>
<p>创建接口在<code class="language-plaintext highlighter-rouge">OrderService</code>中</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="nd">@Transactional</span>
  <span class="kd">public</span> <span class="nc">Order</span> <span class="nf">createOrder</span><span class="o">(</span><span class="kt">long</span> <span class="n">consumerId</span><span class="o">,</span> <span class="kt">long</span> <span class="n">restaurantId</span><span class="o">,</span> <span class="nc">DeliveryInformation</span> <span class="n">deliveryInformation</span><span class="o">,</span>
                           <span class="nc">List</span><span class="o">&lt;</span><span class="nc">MenuItemIdAndQuantity</span><span class="o">&gt;</span> <span class="n">lineItems</span><span class="o">)</span> <span class="o">{</span>
    <span class="nc">Restaurant</span> <span class="n">restaurant</span> <span class="o">=</span> <span class="n">restaurantRepository</span><span class="o">.</span><span class="na">findById</span><span class="o">(</span><span class="n">restaurantId</span><span class="o">)</span>
            <span class="o">.</span><span class="na">orElseThrow</span><span class="o">(()</span> <span class="o">-&gt;</span> <span class="k">new</span> <span class="nc">RestaurantNotFoundException</span><span class="o">(</span><span class="n">restaurantId</span><span class="o">));</span>

    <span class="nc">List</span><span class="o">&lt;</span><span class="nc">OrderLineItem</span><span class="o">&gt;</span> <span class="n">orderLineItems</span> <span class="o">=</span> <span class="n">makeOrderLineItems</span><span class="o">(</span><span class="n">lineItems</span><span class="o">,</span> <span class="n">restaurant</span><span class="o">);</span>

    <span class="nc">ResultWithDomainEvents</span><span class="o">&lt;</span><span class="nc">Order</span><span class="o">,</span> <span class="nc">OrderDomainEvent</span><span class="o">&gt;</span> <span class="n">orderAndEvents</span> <span class="o">=</span>
            <span class="nc">Order</span><span class="o">.</span><span class="na">createOrder</span><span class="o">(</span><span class="n">consumerId</span><span class="o">,</span> <span class="n">restaurant</span><span class="o">,</span> <span class="n">deliveryInformation</span><span class="o">,</span> <span class="n">orderLineItems</span><span class="o">);</span>

    <span class="nc">Order</span> <span class="n">order</span> <span class="o">=</span> <span class="n">orderAndEvents</span><span class="o">.</span><span class="na">result</span><span class="o">;</span>
    <span class="n">orderRepository</span><span class="o">.</span><span class="na">save</span><span class="o">(</span><span class="n">order</span><span class="o">);</span>

    <span class="n">orderAggregateEventPublisher</span><span class="o">.</span><span class="na">publish</span><span class="o">(</span><span class="n">order</span><span class="o">,</span> <span class="n">orderAndEvents</span><span class="o">.</span><span class="na">events</span><span class="o">);</span>

    <span class="nc">OrderDetails</span> <span class="n">orderDetails</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">OrderDetails</span><span class="o">(</span><span class="n">consumerId</span><span class="o">,</span> <span class="n">restaurantId</span><span class="o">,</span> <span class="n">orderLineItems</span><span class="o">,</span> <span class="n">order</span><span class="o">.</span><span class="na">getOrderTotal</span><span class="o">());</span>

    <span class="nc">CreateOrderSagaState</span> <span class="n">data</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">CreateOrderSagaState</span><span class="o">(</span><span class="n">order</span><span class="o">.</span><span class="na">getId</span><span class="o">(),</span> <span class="n">orderDetails</span><span class="o">);</span>
    <span class="n">sagaInstanceFactory</span><span class="o">.</span><span class="na">create</span><span class="o">(</span><span class="n">createOrderSaga</span><span class="o">,</span> <span class="n">data</span><span class="o">);</span>
    <span class="k">return</span> <span class="n">order</span><span class="o">;</span>
  <span class="o">}</span>
</code></pre></div></div>
<p>做了这几件事</p>
<ul>
  <li>查询Restaurant以组装OrderLineItem</li>
  <li>保存order(初始状态为<code class="language-plaintext highlighter-rouge">APPROVAL_PENDING</code>，订单状态后续会专门讲解)，发布领域事件<code class="language-plaintext highlighter-rouge">OrderCreatedEvent</code></li>
  <li>组装sagas状态并启动<code class="language-plaintext highlighter-rouge">CreateOrderSaga</code></li>
</ul>

<p>领域事件我们前面已经讲过，下面进入主题sagas的讲解</p>

<h4 id="createordersaga">CreateOrderSaga</h4>
<p>首先看定义sagas的方法签名</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="kd">public</span> <span class="nf">CreateOrderSaga</span><span class="o">(</span><span class="nc">OrderServiceProxy</span> <span class="n">orderService</span><span class="o">,</span> <span class="nc">ConsumerServiceProxy</span> <span class="n">consumerService</span><span class="o">,</span> <span class="nc">KitchenServiceProxy</span> <span class="n">kitchenService</span><span class="o">,</span>
                         <span class="nc">AccountingServiceProxy</span> <span class="n">accountingService</span><span class="o">)</span> <span class="o">{}</span>
</code></pre></div></div>

<p>可以看到有很多Proxy，这些都是定义Command的类</p>

<h5 id="command定义">Command定义</h5>
<p>Command-Reply是eventuate-tram中实现的一种异步交互方式</p>
<ul>
  <li>服务A通过channel发送command到服务B</li>
  <li>服务B将结果通过channel返回给服务A</li>
</ul>

<p>来看<code class="language-plaintext highlighter-rouge">OrderServiceProxy</code>中的定义</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="kd">public</span> <span class="kd">final</span> <span class="nc">CommandEndpoint</span><span class="o">&lt;</span><span class="nc">RejectOrderCommand</span><span class="o">&gt;</span> <span class="n">reject</span> <span class="o">=</span> <span class="nc">CommandEndpointBuilder</span>
          <span class="o">.</span><span class="na">forCommand</span><span class="o">(</span><span class="nc">RejectOrderCommand</span><span class="o">.</span><span class="na">class</span><span class="o">)</span>
          <span class="o">.</span><span class="na">withChannel</span><span class="o">(</span><span class="nc">OrderServiceChannels</span><span class="o">.</span><span class="na">COMMAND_CHANNEL</span><span class="o">)</span>
          <span class="o">.</span><span class="na">withReply</span><span class="o">(</span><span class="nc">Success</span><span class="o">.</span><span class="na">class</span><span class="o">)</span>
          <span class="o">.</span><span class="na">build</span><span class="o">();</span>

  <span class="kd">public</span> <span class="kd">final</span> <span class="nc">CommandEndpoint</span><span class="o">&lt;</span><span class="nc">ApproveOrderCommand</span><span class="o">&gt;</span> <span class="n">approve</span> <span class="o">=</span> <span class="nc">CommandEndpointBuilder</span>
          <span class="o">.</span><span class="na">forCommand</span><span class="o">(</span><span class="nc">ApproveOrderCommand</span><span class="o">.</span><span class="na">class</span><span class="o">)</span>
          <span class="o">.</span><span class="na">withChannel</span><span class="o">(</span><span class="nc">OrderServiceChannels</span><span class="o">.</span><span class="na">COMMAND_CHANNEL</span><span class="o">)</span>
          <span class="o">.</span><span class="na">withReply</span><span class="o">(</span><span class="nc">Success</span><span class="o">.</span><span class="na">class</span><span class="o">)</span>
          <span class="o">.</span><span class="na">build</span><span class="o">();</span>
</code></pre></div></div>
<p>这里定义了orderService要处理的两类命令，reject和approve</p>
<ul>
  <li>forCommand定义命令类型</li>
  <li>withChannel定义通信的channel，即topic</li>
  <li>withReply定义返回的结果，有Success/Failure两种</li>
</ul>

<p>命令接收逻辑如下</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// OrderCommandHandlersConfiguration</span>
<span class="nd">@Bean</span>
<span class="kd">public</span> <span class="nc">SagaCommandDispatcher</span> <span class="nf">orderCommandHandlersDispatcher</span><span class="o">(</span><span class="nc">OrderCommandHandlers</span> <span class="n">orderCommandHandlers</span><span class="o">,</span> <span class="nc">SagaCommandDispatcherFactory</span> <span class="n">sagaCommandDispatcherFactory</span><span class="o">)</span> <span class="o">{</span>
  <span class="k">return</span> <span class="n">sagaCommandDispatcherFactory</span><span class="o">.</span><span class="na">make</span><span class="o">(</span><span class="s">"orderService"</span><span class="o">,</span> <span class="n">orderCommandHandlers</span><span class="o">.</span><span class="na">commandHandlers</span><span class="o">());</span>
<span class="o">}</span>
<span class="c1">// OrderCommandHandlers</span>
<span class="kd">public</span> <span class="nc">CommandHandlers</span> <span class="nf">commandHandlers</span><span class="o">()</span> <span class="o">{</span>
    <span class="k">return</span> <span class="nc">SagaCommandHandlersBuilder</span>
            <span class="o">.</span><span class="na">fromChannel</span><span class="o">(</span><span class="s">"orderService"</span><span class="o">)</span>
            <span class="o">.</span><span class="na">onMessage</span><span class="o">(</span><span class="nc">ApproveOrderCommand</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="k">this</span><span class="o">::</span><span class="n">approveOrder</span><span class="o">)</span>
            <span class="o">.</span><span class="na">onMessage</span><span class="o">(</span><span class="nc">RejectOrderCommand</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="k">this</span><span class="o">::</span><span class="n">rejectOrder</span><span class="o">).</span><span class="na">build</span><span class="o">();</span>
<span class="o">}</span>
</code></pre></div></div>
<p>注册SagaCommandDispatcher绑定对命令的处理逻辑，CommandHandlers中包含命令进入的channel和不同命令的处理逻辑，返回的channel在命令中定义。</p>

<h5 id="sagas定义">sagas定义</h5>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// CreateOrderSaga</span>
<span class="kd">public</span> <span class="nf">CreateOrderSaga</span><span class="o">(</span><span class="nc">OrderServiceProxy</span> <span class="n">orderService</span><span class="o">,</span> <span class="nc">ConsumerServiceProxy</span> <span class="n">consumerService</span><span class="o">,</span> <span class="nc">KitchenServiceProxy</span> <span class="n">kitchenService</span><span class="o">,</span>
                       <span class="nc">AccountingServiceProxy</span> <span class="n">accountingService</span><span class="o">)</span> <span class="o">{</span>
  <span class="k">this</span><span class="o">.</span><span class="na">sagaDefinition</span> <span class="o">=</span>
          <span class="n">step</span><span class="o">()</span>
          <span class="o">.</span><span class="na">withCompensation</span><span class="o">(</span><span class="n">orderService</span><span class="o">.</span><span class="na">reject</span><span class="o">,</span> <span class="nl">CreateOrderSagaState:</span><span class="o">:</span><span class="n">makeRejectOrderCommand</span><span class="o">)</span>
          <span class="o">.</span><span class="na">step</span><span class="o">()</span>
          <span class="o">.</span><span class="na">invokeParticipant</span><span class="o">(</span><span class="n">consumerService</span><span class="o">.</span><span class="na">validateOrder</span><span class="o">,</span> <span class="nl">CreateOrderSagaState:</span><span class="o">:</span><span class="n">makeValidateOrderByConsumerCommand</span><span class="o">)</span>
          <span class="o">.</span><span class="na">step</span><span class="o">()</span>
          <span class="o">.</span><span class="na">invokeParticipant</span><span class="o">(</span><span class="n">kitchenService</span><span class="o">.</span><span class="na">create</span><span class="o">,</span> <span class="nl">CreateOrderSagaState:</span><span class="o">:</span><span class="n">makeCreateTicketCommand</span><span class="o">)</span>
          <span class="o">.</span><span class="na">onReply</span><span class="o">(</span><span class="nc">CreateTicketReply</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="nl">CreateOrderSagaState:</span><span class="o">:</span><span class="n">handleCreateTicketReply</span><span class="o">)</span>
          <span class="o">.</span><span class="na">withCompensation</span><span class="o">(</span><span class="n">kitchenService</span><span class="o">.</span><span class="na">cancel</span><span class="o">,</span> <span class="nl">CreateOrderSagaState:</span><span class="o">:</span><span class="n">makeCancelCreateTicketCommand</span><span class="o">)</span>
          <span class="o">.</span><span class="na">step</span><span class="o">()</span>
          <span class="o">.</span><span class="na">invokeParticipant</span><span class="o">(</span><span class="n">accountingService</span><span class="o">.</span><span class="na">authorize</span><span class="o">,</span> <span class="nl">CreateOrderSagaState:</span><span class="o">:</span><span class="n">makeAuthorizeCommand</span><span class="o">)</span>
          <span class="o">.</span><span class="na">step</span><span class="o">()</span>
          <span class="o">.</span><span class="na">invokeParticipant</span><span class="o">(</span><span class="n">kitchenService</span><span class="o">.</span><span class="na">confirmCreate</span><span class="o">,</span> <span class="nl">CreateOrderSagaState:</span><span class="o">:</span><span class="n">makeConfirmCreateTicketCommand</span><span class="o">)</span>
          <span class="o">.</span><span class="na">step</span><span class="o">()</span>
          <span class="o">.</span><span class="na">invokeParticipant</span><span class="o">(</span><span class="n">orderService</span><span class="o">.</span><span class="na">approve</span><span class="o">,</span> <span class="nl">CreateOrderSagaState:</span><span class="o">:</span><span class="n">makeApproveOrderCommand</span><span class="o">)</span>
          <span class="o">.</span><span class="na">build</span><span class="o">();</span>
<span class="o">}</span>
<span class="c1">// CreateOrderSagaState</span>
<span class="nc">RejectOrderCommand</span> <span class="nf">makeRejectOrderCommand</span><span class="o">()</span> <span class="o">{</span>
  <span class="k">return</span> <span class="k">new</span> <span class="nf">RejectOrderCommand</span><span class="o">(</span><span class="n">getOrderId</span><span class="o">());</span>
<span class="o">}</span>
</code></pre></div></div>
<p>我们预期的效果是，step1执行完就去执行step2，然后是step3，如果任何一步执行失败，会执行之前每一步的withCompensation操作，按相反的顺序。<br />
先看看step()定义</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// SimpleSagaDefinitionBuilder</span>
<span class="kd">private</span> <span class="nc">List</span><span class="o">&lt;</span><span class="nc">SagaStep</span><span class="o">&lt;</span><span class="nc">Data</span><span class="o">&gt;&gt;</span> <span class="n">sagaSteps</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">LinkedList</span><span class="o">&lt;&gt;();</span>

<span class="kd">public</span> <span class="kt">void</span> <span class="nf">addStep</span><span class="o">(</span><span class="nc">SagaStep</span><span class="o">&lt;</span><span class="nc">Data</span><span class="o">&gt;</span> <span class="n">sagaStep</span><span class="o">)</span> <span class="o">{</span>
  <span class="n">sagaSteps</span><span class="o">.</span><span class="na">add</span><span class="o">(</span><span class="n">sagaStep</span><span class="o">);</span>
<span class="o">}</span>

<span class="kd">public</span> <span class="nc">SagaDefinition</span><span class="o">&lt;</span><span class="nc">Data</span><span class="o">&gt;</span> <span class="nf">build</span><span class="o">()</span> <span class="o">{</span>
  <span class="k">return</span> <span class="k">new</span> <span class="nc">SimpleSagaDefinition</span><span class="o">&lt;&gt;(</span><span class="n">sagaSteps</span><span class="o">);</span>
<span class="o">}</span>
<span class="c1">// SimpleSagaDsl</span>
<span class="k">default</span> <span class="nc">StepBuilder</span><span class="o">&lt;</span><span class="nc">Data</span><span class="o">&gt;</span> <span class="nf">step</span><span class="o">()</span> <span class="o">{</span>
  <span class="nc">SimpleSagaDefinitionBuilder</span><span class="o">&lt;</span><span class="nc">Data</span><span class="o">&gt;</span> <span class="n">builder</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">SimpleSagaDefinitionBuilder</span><span class="o">&lt;&gt;();</span>
  <span class="k">return</span> <span class="k">new</span> <span class="nc">StepBuilder</span><span class="o">&lt;&gt;(</span><span class="n">builder</span><span class="o">);</span>
<span class="o">}</span>
<span class="c1">// StepBuilder</span>
<span class="kd">private</span> <span class="kd">final</span> <span class="nc">SimpleSagaDefinitionBuilder</span><span class="o">&lt;</span><span class="nc">Data</span><span class="o">&gt;</span> <span class="n">parent</span><span class="o">;</span>

<span class="kd">public</span> <span class="nf">StepBuilder</span><span class="o">(</span><span class="nc">SimpleSagaDefinitionBuilder</span><span class="o">&lt;</span><span class="nc">Data</span><span class="o">&gt;</span> <span class="n">builder</span><span class="o">)</span> <span class="o">{</span>
  <span class="k">this</span><span class="o">.</span><span class="na">parent</span> <span class="o">=</span> <span class="n">builder</span><span class="o">;</span>
<span class="o">}</span>

<span class="kd">public</span> <span class="nc">LocalStepBuilder</span><span class="o">&lt;</span><span class="nc">Data</span><span class="o">&gt;</span> <span class="nf">invokeLocal</span><span class="o">(</span><span class="nc">Consumer</span><span class="o">&lt;</span><span class="nc">Data</span><span class="o">&gt;</span> <span class="n">localFunction</span><span class="o">)</span> <span class="o">{</span>
  <span class="k">return</span> <span class="k">new</span> <span class="nc">LocalStepBuilder</span><span class="o">&lt;&gt;(</span><span class="n">parent</span><span class="o">,</span> <span class="n">localFunction</span><span class="o">);</span>
<span class="o">}</span>

<span class="kd">public</span> <span class="nc">InvokeParticipantStepBuilder</span><span class="o">&lt;</span><span class="nc">Data</span><span class="o">&gt;</span> <span class="nf">invokeParticipant</span><span class="o">(</span><span class="nc">Function</span><span class="o">&lt;</span><span class="nc">Data</span><span class="o">,</span> <span class="nc">CommandWithDestination</span><span class="o">&gt;</span> <span class="n">action</span><span class="o">)</span> <span class="o">{</span>
  <span class="k">return</span> <span class="k">new</span> <span class="nc">InvokeParticipantStepBuilder</span><span class="o">&lt;&gt;(</span><span class="n">parent</span><span class="o">).</span><span class="na">withAction</span><span class="o">(</span><span class="nc">Optional</span><span class="o">.</span><span class="na">empty</span><span class="o">(),</span> <span class="n">action</span><span class="o">);</span>
<span class="o">}</span>

<span class="nd">@Override</span>
<span class="kd">public</span> <span class="nc">InvokeParticipantStepBuilder</span><span class="o">&lt;</span><span class="nc">Data</span><span class="o">&gt;</span> <span class="nf">withCompensation</span><span class="o">(</span><span class="nc">Function</span><span class="o">&lt;</span><span class="nc">Data</span><span class="o">,</span> <span class="nc">CommandWithDestination</span><span class="o">&gt;</span> <span class="n">compensation</span><span class="o">)</span> <span class="o">{</span>
  <span class="k">return</span> <span class="k">new</span> <span class="nc">InvokeParticipantStepBuilder</span><span class="o">&lt;&gt;(</span><span class="n">parent</span><span class="o">).</span><span class="na">withCompensation</span><span class="o">(</span><span class="n">compensation</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>
<p>首先SimpleSagaDefinitionBuilder中包含SagaStep的列表，SagaStep有两种类型</p>
<ul>
  <li>LocalStep，表示本地操作，不用发送消息
    <ul>
      <li>Consumer<Data> localFunction，本地操作</Data></li>
      <li>Optional&lt;Consumer<Data>&gt; compensation，补偿操作</Data></li>
    </ul>
  </li>
  <li>ParticipantInvocationStep，表示Command-Reply形式的远程操作
    <ul>
      <li>Optional&lt;ParticipantInvocation<Data>&gt; participantInvocation，远程操作，包含要发送的Command</Data></li>
      <li>Optional&lt;ParticipantInvocation<Data>&gt; compensation，补偿操作</Data></li>
      <li>Map&lt;String, BiConsumer&lt;Data, Object» actionReplyHandlers # todo</li>
      <li>Map&lt;String, BiConsumer&lt;Data, Object» compensationReplyHandlers # todo</li>
    </ul>
  </li>
</ul>

<p>再回到上面CreateOrderSaga的定义，所有的step都是ParticipantInvocationStep</p>

<table>
  <thead>
    <tr>
      <th><strong>step</strong></th>
      <th><strong>action</strong></th>
      <th><strong>compensation</strong></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1</td>
      <td> </td>
      <td>orderService.reject</td>
    </tr>
    <tr>
      <td>2</td>
      <td>consumerService.validateOrder</td>
      <td> </td>
    </tr>
    <tr>
      <td>3</td>
      <td>kitchenService.create</td>
      <td>kitchenService.cancel</td>
    </tr>
    <tr>
      <td>4</td>
      <td>accountingService.authorize</td>
      <td> </td>
    </tr>
    <tr>
      <td>5</td>
      <td>kitchenService.confirmCreate</td>
      <td> </td>
    </tr>
    <tr>
      <td>6</td>
      <td>orderService.approve</td>
      <td> </td>
    </tr>
  </tbody>
</table>

<h5 id="sagas执行">sagas执行</h5>
<p>大概了解了CreateOrderSaga的定义，下面我们看看具体的执行流程
sagas入口在OrderService的createOrder方法中</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="nc">CreateOrderSagaState</span> <span class="n">data</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">CreateOrderSagaState</span><span class="o">(</span><span class="n">order</span><span class="o">.</span><span class="na">getId</span><span class="o">(),</span> <span class="n">orderDetails</span><span class="o">);</span>
    <span class="n">sagaInstanceFactory</span><span class="o">.</span><span class="na">create</span><span class="o">(</span><span class="n">createOrderSaga</span><span class="o">,</span> <span class="n">data</span><span class="o">);</span>
</code></pre></div></div>
<p>CreateOrderSagaState存储saga的状态，在这里就是订单详情。<br />
SagaInstanceFactory是我们研究的重点</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// SagaOrchestratorConfiguration</span>
<span class="nd">@Bean</span>
<span class="kd">public</span> <span class="nc">SagaInstanceFactory</span> <span class="nf">sagaInstanceFactory</span><span class="o">(</span><span class="nc">SagaInstanceRepository</span> <span class="n">sagaInstanceRepository</span><span class="o">,</span> <span class="nc">CommandProducer</span>
                                                       <span class="n">commandProducer</span><span class="o">,</span> <span class="nc">MessageConsumer</span> <span class="n">messageConsumer</span><span class="o">,</span>
                                               <span class="nc">SagaLockManager</span> <span class="n">sagaLockManager</span><span class="o">,</span> <span class="nc">SagaCommandProducer</span> <span class="n">sagaCommandProducer</span><span class="o">,</span> <span class="nc">Collection</span><span class="o">&lt;</span><span class="nc">Saga</span><span class="o">&lt;?&gt;&gt;</span> <span class="n">sagas</span><span class="o">)</span> <span class="o">{</span>
  <span class="nc">SagaManagerFactory</span> <span class="n">smf</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">SagaManagerFactory</span><span class="o">(</span><span class="n">sagaInstanceRepository</span><span class="o">,</span> <span class="n">commandProducer</span><span class="o">,</span> <span class="n">messageConsumer</span><span class="o">,</span>
          <span class="n">sagaLockManager</span><span class="o">,</span> <span class="n">sagaCommandProducer</span><span class="o">);</span>
  <span class="k">return</span> <span class="k">new</span> <span class="nf">SagaInstanceFactory</span><span class="o">(</span><span class="n">smf</span><span class="o">,</span> <span class="n">sagas</span><span class="o">);</span>
<span class="o">}</span>
<span class="c1">// SagaInstanceFactory</span>
<span class="kd">private</span> <span class="nc">ConcurrentMap</span><span class="o">&lt;</span><span class="nc">Saga</span><span class="o">&lt;?&gt;,</span> <span class="nc">SagaManager</span><span class="o">&lt;?&gt;&gt;</span> <span class="n">sagaManagers</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ConcurrentHashMap</span><span class="o">&lt;&gt;();</span>

<span class="kd">public</span> <span class="nf">SagaInstanceFactory</span><span class="o">(</span><span class="nc">SagaManagerFactory</span> <span class="n">sagaManagerFactory</span><span class="o">,</span> <span class="nc">Collection</span><span class="o">&lt;</span><span class="nc">Saga</span><span class="o">&lt;?&gt;&gt;</span> <span class="n">sagas</span><span class="o">)</span> <span class="o">{</span>
  <span class="k">for</span> <span class="o">(</span><span class="nc">Saga</span><span class="o">&lt;?&gt;</span> <span class="n">saga</span> <span class="o">:</span> <span class="n">sagas</span><span class="o">)</span> <span class="o">{</span>
    <span class="n">sagaManagers</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="n">saga</span><span class="o">,</span> <span class="n">makeSagaManager</span><span class="o">(</span><span class="n">sagaManagerFactory</span><span class="o">,</span> <span class="n">saga</span><span class="o">));</span>
  <span class="o">}</span>
<span class="o">}</span>

<span class="kd">public</span> <span class="o">&lt;</span><span class="nc">SagaData</span><span class="o">&gt;</span> <span class="nc">SagaInstance</span> <span class="nf">create</span><span class="o">(</span><span class="nc">Saga</span><span class="o">&lt;</span><span class="nc">SagaData</span><span class="o">&gt;</span> <span class="n">saga</span><span class="o">,</span> <span class="nc">SagaData</span> <span class="n">data</span><span class="o">)</span> <span class="o">{</span>
  <span class="nc">SagaManager</span><span class="o">&lt;</span><span class="nc">SagaData</span><span class="o">&gt;</span>  <span class="n">sagaManager</span> <span class="o">=</span> <span class="o">(</span><span class="nc">SagaManager</span><span class="o">&lt;</span><span class="nc">SagaData</span><span class="o">&gt;)</span><span class="n">sagaManagers</span><span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="n">saga</span><span class="o">);</span>
  <span class="k">if</span> <span class="o">(</span><span class="n">sagaManager</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span>
    <span class="k">throw</span> <span class="k">new</span> <span class="nf">RuntimeException</span><span class="o">((</span><span class="s">"No SagaManager for "</span> <span class="o">+</span> <span class="n">saga</span><span class="o">));</span>
  <span class="k">return</span> <span class="n">sagaManager</span><span class="o">.</span><span class="na">create</span><span class="o">(</span><span class="n">data</span><span class="o">);</span>
<span class="o">}</span>

<span class="kd">private</span> <span class="o">&lt;</span><span class="nc">SagaData</span><span class="o">&gt;</span> <span class="nc">SagaManager</span><span class="o">&lt;</span><span class="nc">SagaData</span><span class="o">&gt;</span> <span class="nf">makeSagaManager</span><span class="o">(</span><span class="nc">SagaManagerFactory</span> <span class="n">sagaManagerFactory</span><span class="o">,</span> <span class="nc">Saga</span><span class="o">&lt;</span><span class="nc">SagaData</span><span class="o">&gt;</span> <span class="n">saga</span><span class="o">)</span> <span class="o">{</span>
  <span class="nc">SagaManagerImpl</span><span class="o">&lt;</span><span class="nc">SagaData</span><span class="o">&gt;</span> <span class="n">sagaManager</span> <span class="o">=</span> <span class="n">sagaManagerFactory</span><span class="o">.</span><span class="na">make</span><span class="o">(</span><span class="n">saga</span><span class="o">);</span>
  <span class="n">sagaManager</span><span class="o">.</span><span class="na">subscribeToReplyChannel</span><span class="o">();</span>
  <span class="k">return</span> <span class="n">sagaManager</span><span class="o">;</span>
<span class="o">}</span>
</code></pre></div></div>
<p>SagaInstanceFactory由自动配置注入，会扫描所有Saga类，每个类创建一个SagaManager。<br />
SagaManager负责saga流程的创建和监听并处理Reply。</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// SagaManagerImpl</span>
  <span class="nd">@Override</span>
  <span class="kd">public</span> <span class="nc">SagaInstance</span> <span class="nf">create</span><span class="o">(</span><span class="nc">Data</span> <span class="n">sagaData</span><span class="o">,</span> <span class="nc">Optional</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;</span> <span class="n">resource</span><span class="o">)</span> <span class="o">{</span>
    <span class="nc">SagaInstance</span> <span class="n">sagaInstance</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">SagaInstance</span><span class="o">(</span><span class="n">getSagaType</span><span class="o">(),</span>
            <span class="kc">null</span><span class="o">,</span>
            <span class="s">"????"</span><span class="o">,</span>
            <span class="kc">null</span><span class="o">,</span>
            <span class="nc">SagaDataSerde</span><span class="o">.</span><span class="na">serializeSagaData</span><span class="o">(</span><span class="n">sagaData</span><span class="o">),</span> <span class="k">new</span> <span class="nc">HashSet</span><span class="o">&lt;&gt;());</span>
    <span class="n">sagaInstanceRepository</span><span class="o">.</span><span class="na">save</span><span class="o">(</span><span class="n">sagaInstance</span><span class="o">);</span>
    <span class="nc">String</span> <span class="n">sagaId</span> <span class="o">=</span> <span class="n">sagaInstance</span><span class="o">.</span><span class="na">getId</span><span class="o">();</span>

    <span class="nc">SagaActions</span><span class="o">&lt;</span><span class="nc">Data</span><span class="o">&gt;</span> <span class="n">actions</span> <span class="o">=</span> <span class="n">getStateDefinition</span><span class="o">().</span><span class="na">start</span><span class="o">(</span><span class="n">sagaData</span><span class="o">);</span>
    <span class="n">actions</span><span class="o">.</span><span class="na">getLocalException</span><span class="o">().</span><span class="na">ifPresent</span><span class="o">(</span><span class="n">e</span> <span class="o">-&gt;</span> <span class="o">{</span>
      <span class="k">throw</span> <span class="n">e</span><span class="o">;</span>
    <span class="o">});</span>

    <span class="n">processActions</span><span class="o">(</span><span class="n">saga</span><span class="o">.</span><span class="na">getSagaType</span><span class="o">(),</span> <span class="n">sagaId</span><span class="o">,</span> <span class="n">sagaInstance</span><span class="o">,</span> <span class="n">sagaData</span><span class="o">,</span> <span class="n">actions</span><span class="o">);</span>
    <span class="k">return</span> <span class="n">sagaInstance</span><span class="o">;</span>
  <span class="o">}</span>
</code></pre></div></div>
<p>这里我们删除一些不关注的代码，sagas创建涉及这几部分</p>
<ul>
  <li>创建SagaInstance并落库</li>
  <li>将SagaDefinition转为SagaActions并执行</li>
</ul>

<p>具体的执行过程在第二步，首先是SagaActions的转换</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// SimpleSagaDefinition</span>
<span class="nd">@Override</span>
<span class="kd">public</span> <span class="nc">SagaActions</span><span class="o">&lt;</span><span class="nc">Data</span><span class="o">&gt;</span> <span class="nf">start</span><span class="o">(</span><span class="nc">Data</span> <span class="n">sagaData</span><span class="o">)</span> <span class="o">{</span>
  <span class="k">return</span> <span class="nf">toSagaActions</span><span class="o">(</span><span class="n">firstStepToExecute</span><span class="o">(</span><span class="n">sagaData</span><span class="o">));</span>
<span class="o">}</span>
<span class="c1">// AbstractSimpleSagaDefinition</span>
<span class="kd">protected</span> <span class="nc">Provider</span> <span class="nf">firstStepToExecute</span><span class="o">(</span><span class="nc">Data</span> <span class="n">data</span><span class="o">)</span> <span class="o">{</span>
  <span class="k">return</span> <span class="nf">nextStepToExecute</span><span class="o">(</span><span class="nc">SagaExecutionState</span><span class="o">.</span><span class="na">startingState</span><span class="o">(),</span> <span class="n">data</span><span class="o">);</span>
<span class="o">}</span>
<span class="c1">//</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">SagaExecutionState</span> <span class="o">{</span>
    <span class="kd">private</span> <span class="kt">int</span> <span class="n">currentlyExecuting</span><span class="o">;</span>
    <span class="kd">private</span> <span class="kt">boolean</span> <span class="n">compensating</span><span class="o">;</span>
    <span class="kd">private</span> <span class="kt">boolean</span> <span class="n">endState</span><span class="o">;</span>
    <span class="kd">private</span> <span class="kt">boolean</span> <span class="n">failed</span><span class="o">;</span>

    <span class="kd">public</span> <span class="kd">static</span> <span class="nc">SagaExecutionState</span> <span class="nf">startingState</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">return</span> <span class="k">new</span> <span class="nf">SagaExecutionState</span><span class="o">(-</span><span class="mi">1</span><span class="o">,</span> <span class="kc">false</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>SagaExecutionState对应sagas执行到的下标，假设共有n步，则下标范围是\([1,n-1]\)<br />
currentlyExecuting初始为-1，初始时，compensating为false，每次+1，执行失败时，compensating设为true，反向执行每次-1。</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// AbstractSimpleSagaDefinition</span>
<span class="kd">protected</span> <span class="nc">Provider</span> <span class="nf">nextStepToExecute</span><span class="o">(</span><span class="nc">SagaExecutionState</span> <span class="n">state</span><span class="o">,</span> <span class="nc">Data</span> <span class="n">data</span><span class="o">)</span> <span class="o">{</span>
    <span class="kt">int</span> <span class="n">skipped</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span>
    <span class="kt">boolean</span> <span class="n">compensating</span> <span class="o">=</span> <span class="n">state</span><span class="o">.</span><span class="na">isCompensating</span><span class="o">();</span>
    <span class="kt">int</span> <span class="n">direction</span> <span class="o">=</span> <span class="n">compensating</span> <span class="o">?</span> <span class="o">-</span><span class="mi">1</span> <span class="o">:</span> <span class="o">+</span><span class="mi">1</span><span class="o">;</span>
    <span class="k">for</span> <span class="o">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="n">state</span><span class="o">.</span><span class="na">getCurrentlyExecuting</span><span class="o">()</span> <span class="o">+</span> <span class="n">direction</span><span class="o">;</span> <span class="n">i</span> <span class="o">&gt;=</span> <span class="mi">0</span> <span class="o">&amp;&amp;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">steps</span><span class="o">.</span><span class="na">size</span><span class="o">();</span> <span class="n">i</span> <span class="o">=</span> <span class="n">i</span> <span class="o">+</span> <span class="n">direction</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">Step</span> <span class="n">step</span> <span class="o">=</span> <span class="n">steps</span><span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="n">i</span><span class="o">);</span>
        <span class="k">if</span> <span class="o">((</span><span class="n">compensating</span> <span class="o">?</span> <span class="n">step</span><span class="o">.</span><span class="na">hasCompensation</span><span class="o">(</span><span class="n">data</span><span class="o">)</span> <span class="o">:</span> <span class="n">step</span><span class="o">.</span><span class="na">hasAction</span><span class="o">(</span><span class="n">data</span><span class="o">)))</span> <span class="o">{</span>
            <span class="nc">ToExecute</span> <span class="n">stepToExecute</span> <span class="o">=</span> <span class="n">makeStepToExecute</span><span class="o">(</span><span class="n">skipped</span><span class="o">,</span> <span class="n">compensating</span><span class="o">,</span> <span class="n">step</span><span class="o">);</span>
            <span class="k">return</span> <span class="nf">makeSagaActionsProvider</span><span class="o">(</span><span class="n">stepToExecute</span><span class="o">,</span> <span class="n">data</span><span class="o">,</span> <span class="n">state</span><span class="o">);</span>
        <span class="o">}</span> <span class="k">else</span>
            <span class="n">skipped</span><span class="o">++;</span>
    <span class="o">}</span>
    <span class="k">return</span> <span class="nf">makeSagaActionsProvider</span><span class="o">(</span><span class="n">makeEndStateSagaActions</span><span class="o">(</span><span class="n">state</span><span class="o">));</span>
<span class="o">}</span>
</code></pre></div></div>
<p>如上所述，获取下一步执行的操作，根据以下条件</p>
<ul>
  <li>compensating决定下标移动的方向</li>
  <li>如果当前Step没有对应操作(hasAction/hasCompensation)，跳过该Step</li>
  <li>如果所有Step执行完，成功则下标为steps.size()，失败则为-1，执行makeEndStateSagaActions</li>
</ul>

<p>来具体看一下makeSagaActionsProvider，这个方法非常关键</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// AbstractSimpleSagaDefinition.nextStepToExecute</span>
<span class="n">makeSagaActionsProvider</span><span class="o">(</span><span class="n">stepToExecute</span><span class="o">,</span> <span class="n">data</span><span class="o">,</span> <span class="n">state</span><span class="o">);</span>
<span class="c1">// SimpleSagaDefinition</span>
<span class="kd">protected</span> <span class="nc">SagaActionsProvider</span><span class="o">&lt;</span><span class="nc">Data</span><span class="o">&gt;</span> <span class="nf">makeSagaActionsProvider</span><span class="o">(</span><span class="nc">StepToExecute</span><span class="o">&lt;</span><span class="nc">Data</span><span class="o">&gt;</span> <span class="n">stepToExecute</span><span class="o">,</span> <span class="nc">Data</span> <span class="n">data</span><span class="o">,</span> <span class="nc">SagaExecutionState</span> <span class="n">state</span><span class="o">)</span> <span class="o">{</span>
  <span class="k">return</span> <span class="k">new</span> <span class="nc">SagaActionsProvider</span><span class="o">&lt;&gt;(()</span> <span class="o">-&gt;</span> <span class="n">stepToExecute</span><span class="o">.</span><span class="na">executeStep</span><span class="o">(</span><span class="n">data</span><span class="o">,</span> <span class="n">state</span><span class="o">));</span>
<span class="o">}</span>
<span class="c1">// StepToExecute</span>
<span class="kd">public</span> <span class="nc">SagaActions</span><span class="o">&lt;</span><span class="nc">Data</span><span class="o">&gt;</span> <span class="nf">executeStep</span><span class="o">(</span><span class="nc">Data</span> <span class="n">data</span><span class="o">,</span> <span class="nc">SagaExecutionState</span> <span class="n">currentState</span><span class="o">)</span> <span class="o">{</span>
  <span class="nc">SagaExecutionState</span> <span class="n">newState</span> <span class="o">=</span> <span class="n">currentState</span><span class="o">.</span><span class="na">nextState</span><span class="o">(</span><span class="n">size</span><span class="o">());</span>
  <span class="nc">SagaActions</span><span class="o">.</span><span class="na">Builder</span><span class="o">&lt;</span><span class="nc">Data</span><span class="o">&gt;</span> <span class="n">builder</span> <span class="o">=</span> <span class="nc">SagaActions</span><span class="o">.</span><span class="na">builder</span><span class="o">();</span>
  <span class="kt">boolean</span> <span class="n">compensating</span> <span class="o">=</span> <span class="n">currentState</span><span class="o">.</span><span class="na">isCompensating</span><span class="o">();</span>

  <span class="n">step</span><span class="o">.</span><span class="na">makeStepOutcome</span><span class="o">(</span><span class="n">data</span><span class="o">,</span> <span class="k">this</span><span class="o">.</span><span class="na">compensating</span><span class="o">).</span><span class="na">visit</span><span class="o">(</span><span class="nl">builder:</span><span class="o">:</span><span class="n">withIsLocal</span><span class="o">,</span> <span class="nl">builder:</span><span class="o">:</span><span class="n">withCommands</span><span class="o">);</span>

  <span class="k">return</span> <span class="nf">makeSagaActions</span><span class="o">(</span><span class="n">builder</span><span class="o">,</span> <span class="n">data</span><span class="o">,</span> <span class="n">newState</span><span class="o">,</span> <span class="n">compensating</span><span class="o">);</span>
<span class="o">}</span>
<span class="c1">// LocalStep</span>
<span class="kd">public</span> <span class="nc">StepOutcome</span> <span class="nf">makeStepOutcome</span><span class="o">(</span><span class="nc">Data</span> <span class="n">data</span><span class="o">,</span> <span class="kt">boolean</span> <span class="n">compensating</span><span class="o">)</span> <span class="o">{</span>
  <span class="k">try</span> <span class="o">{</span>
    <span class="k">if</span> <span class="o">(</span><span class="n">compensating</span><span class="o">)</span> <span class="o">{</span>
      <span class="n">compensation</span><span class="o">.</span><span class="na">ifPresent</span><span class="o">(</span><span class="n">localStep</span> <span class="o">-&gt;</span> <span class="n">localStep</span><span class="o">.</span><span class="na">accept</span><span class="o">(</span><span class="n">data</span><span class="o">));</span>
    <span class="o">}</span> <span class="k">else</span> <span class="o">{</span>
      <span class="n">localFunction</span><span class="o">.</span><span class="na">accept</span><span class="o">(</span><span class="n">data</span><span class="o">);</span>
    <span class="o">}</span>
    <span class="k">return</span> <span class="nf">makeLocalOutcome</span><span class="o">(</span><span class="nc">Optional</span><span class="o">.</span><span class="na">empty</span><span class="o">());</span>
  <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">RuntimeException</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>
    <span class="k">return</span> <span class="nf">makeLocalOutcome</span><span class="o">(</span><span class="nc">Optional</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="n">e</span><span class="o">));</span>
  <span class="o">}</span>
<span class="o">}</span>
<span class="c1">// ParticipantInvocationStep</span>
<span class="kd">public</span> <span class="nc">StepOutcome</span> <span class="nf">makeStepOutcome</span><span class="o">(</span><span class="nc">Data</span> <span class="n">data</span><span class="o">,</span> <span class="kt">boolean</span> <span class="n">compensating</span><span class="o">)</span> <span class="o">{</span>
  <span class="k">return</span> <span class="nc">StepOutcome</span><span class="o">.</span><span class="na">makeRemoteStepOutcome</span><span class="o">(</span><span class="n">getParticipantInvocation</span><span class="o">(</span><span class="n">compensating</span><span class="o">)</span>
          <span class="o">.</span><span class="na">map</span><span class="o">(</span><span class="n">x</span> <span class="o">-&gt;</span> <span class="n">x</span><span class="o">.</span><span class="na">makeCommandToSend</span><span class="o">(</span><span class="n">data</span><span class="o">))</span>
          <span class="o">.</span><span class="na">map</span><span class="o">(</span><span class="nl">Collections:</span><span class="o">:</span><span class="n">singletonList</span><span class="o">)</span>
          <span class="o">.</span><span class="na">orElseGet</span><span class="o">(</span><span class="nl">Collections:</span><span class="o">:</span><span class="n">emptyList</span><span class="o">));</span>
<span class="o">}</span>
</code></pre></div></div>
<p>可以看到SagaActionsProvider内部执行了executeStep，又执行了makeStepOutcome，将结果保存到SagaActions中</p>
<ul>
  <li>LocalStep在makeStepOutcome时就已经执行了localFunction，并保存出现的异常</li>
  <li>ParticipantInvocationStep只是生成了要发送的Command，并没有实际执行</li>
</ul>

<p>回到start方法</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// SimpleSagaDefinition</span>
<span class="kd">public</span> <span class="nc">SagaActions</span><span class="o">&lt;</span><span class="nc">Data</span><span class="o">&gt;</span> <span class="nf">start</span><span class="o">(</span><span class="nc">Data</span> <span class="n">sagaData</span><span class="o">)</span> <span class="o">{</span>
  <span class="k">return</span> <span class="nf">toSagaActions</span><span class="o">(</span><span class="n">firstStepToExecute</span><span class="o">(</span><span class="n">sagaData</span><span class="o">));</span>
<span class="o">}</span>

<span class="kd">private</span> <span class="nc">SagaActions</span><span class="o">&lt;</span><span class="nc">Data</span><span class="o">&gt;</span> <span class="nf">toSagaActions</span><span class="o">(</span><span class="nc">SagaActionsProvider</span><span class="o">&lt;</span><span class="nc">Data</span><span class="o">&gt;</span> <span class="n">sap</span><span class="o">)</span> <span class="o">{</span>
  <span class="k">return</span> <span class="n">sap</span><span class="o">.</span><span class="na">toSagaActions</span><span class="o">(</span><span class="n">identity</span><span class="o">(),</span> <span class="n">identity</span><span class="o">());</span>
<span class="o">}</span>
<span class="c1">// AbstractSagaActionsProvider</span>
<span class="kd">private</span> <span class="kd">final</span> <span class="nc">SagaActions</span><span class="o">&lt;</span><span class="nc">Data</span><span class="o">&gt;</span> <span class="n">sagaActions</span><span class="o">;</span>
<span class="kd">private</span> <span class="kd">final</span> <span class="nc">Supplier</span><span class="o">&lt;</span><span class="nc">SuppliedValue</span><span class="o">&gt;</span> <span class="n">sagaActionsSupplier</span><span class="o">;</span>
<span class="kd">public</span> <span class="nc">SuppliedValue</span> <span class="nf">toSagaActions</span><span class="o">(</span><span class="nc">Function</span><span class="o">&lt;</span><span class="nc">SagaActions</span><span class="o">&lt;</span><span class="nc">Data</span><span class="o">&gt;,</span> <span class="nc">SuppliedValue</span><span class="o">&gt;</span> <span class="n">f1</span><span class="o">,</span> <span class="nc">Function</span><span class="o">&lt;</span><span class="nc">SuppliedValue</span><span class="o">,</span> <span class="nc">SuppliedValue</span><span class="o">&gt;</span> <span class="n">f2</span><span class="o">)</span> <span class="o">{</span>
  <span class="k">return</span> <span class="n">sagaActions</span> <span class="o">!=</span> <span class="kc">null</span> <span class="o">?</span> <span class="n">f1</span><span class="o">.</span><span class="na">apply</span><span class="o">(</span><span class="n">sagaActions</span><span class="o">)</span> <span class="o">:</span> <span class="n">f2</span><span class="o">.</span><span class="na">apply</span><span class="o">(</span><span class="n">sagaActionsSupplier</span><span class="o">.</span><span class="na">get</span><span class="o">());</span>
<span class="o">}</span>
</code></pre></div></div>
<p>可以看到makeSagaActionsProvider返回的SagaActionsProvider包含两个字段</p>
<ul>
  <li>sagaActionsSupplier，对应本地和远程操作，<code class="language-plaintext highlighter-rouge">sagaActionsSupplier.get()</code>会触发executeStep的执行</li>
  <li>sagaActions，对应endState或Reply异常的step</li>
</ul>

<p>现在清楚了，start执行完，如果是LocalStep，其操作已经执行，所以会检查是否报错，下面进入saga执行的主流程</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// SagaManagerImpl</span>
<span class="nc">SagaActions</span><span class="o">&lt;</span><span class="nc">Data</span><span class="o">&gt;</span> <span class="n">actions</span> <span class="o">=</span> <span class="n">getStateDefinition</span><span class="o">().</span><span class="na">start</span><span class="o">(</span><span class="n">sagaData</span><span class="o">);</span>
<span class="n">actions</span><span class="o">.</span><span class="na">getLocalException</span><span class="o">().</span><span class="na">ifPresent</span><span class="o">(</span><span class="n">e</span> <span class="o">-&gt;</span> <span class="o">{</span> <span class="k">throw</span> <span class="n">e</span><span class="o">;</span> <span class="o">});</span>
<span class="n">processActions</span><span class="o">(</span><span class="n">saga</span><span class="o">.</span><span class="na">getSagaType</span><span class="o">(),</span> <span class="n">sagaId</span><span class="o">,</span> <span class="n">sagaInstance</span><span class="o">,</span> <span class="n">sagaData</span><span class="o">,</span> <span class="n">actions</span><span class="o">);</span>

<span class="kd">private</span> <span class="kt">void</span> <span class="nf">processActions</span><span class="o">(</span><span class="nc">String</span> <span class="n">sagaType</span><span class="o">,</span> <span class="nc">String</span> <span class="n">sagaId</span><span class="o">,</span> <span class="nc">SagaInstance</span> <span class="n">sagaInstance</span><span class="o">,</span> <span class="nc">Data</span> <span class="n">sagaData</span><span class="o">,</span> <span class="nc">SagaActions</span><span class="o">&lt;</span><span class="nc">Data</span><span class="o">&gt;</span> <span class="n">actions</span><span class="o">)</span> <span class="o">{</span>
    <span class="k">while</span> <span class="o">(</span><span class="kc">true</span><span class="o">)</span> <span class="o">{</span>
    <span class="k">if</span> <span class="o">(</span><span class="n">actions</span><span class="o">.</span><span class="na">getLocalException</span><span class="o">().</span><span class="na">isPresent</span><span class="o">())</span> <span class="o">{</span>
      <span class="n">actions</span> <span class="o">=</span> <span class="n">getStateDefinition</span><span class="o">().</span><span class="na">handleReply</span><span class="o">(</span><span class="n">sagaType</span><span class="o">,</span> <span class="n">sagaId</span><span class="o">,</span> <span class="n">actions</span><span class="o">.</span><span class="na">getUpdatedState</span><span class="o">().</span><span class="na">get</span><span class="o">(),</span> <span class="n">actions</span><span class="o">.</span><span class="na">getUpdatedSagaData</span><span class="o">().</span><span class="na">get</span><span class="o">(),</span> <span class="nc">MessageBuilder</span>
              <span class="o">.</span><span class="na">withPayload</span><span class="o">(</span><span class="s">"{}"</span><span class="o">)</span>
              <span class="o">.</span><span class="na">withHeader</span><span class="o">(</span><span class="nc">ReplyMessageHeaders</span><span class="o">.</span><span class="na">REPLY_OUTCOME</span><span class="o">,</span> <span class="nc">CommandReplyOutcome</span><span class="o">.</span><span class="na">FAILURE</span><span class="o">.</span><span class="na">name</span><span class="o">())</span>
              <span class="o">.</span><span class="na">withHeader</span><span class="o">(</span><span class="nc">ReplyMessageHeaders</span><span class="o">.</span><span class="na">REPLY_TYPE</span><span class="o">,</span> <span class="nc">Failure</span><span class="o">.</span><span class="na">class</span><span class="o">.</span><span class="na">getName</span><span class="o">())</span>
              <span class="o">.</span><span class="na">build</span><span class="o">());</span>
    <span class="o">}</span> <span class="k">else</span> <span class="o">{</span>
      <span class="c1">// only do this if successful</span>
      <span class="nc">String</span> <span class="n">lastRequestId</span> <span class="o">=</span> <span class="n">sagaCommandProducer</span><span class="o">.</span><span class="na">sendCommands</span><span class="o">(</span><span class="k">this</span><span class="o">.</span><span class="na">getSagaType</span><span class="o">(),</span> <span class="n">sagaId</span><span class="o">,</span> <span class="n">actions</span><span class="o">.</span><span class="na">getCommands</span><span class="o">(),</span> <span class="k">this</span><span class="o">.</span><span class="na">makeSagaReplyChannel</span><span class="o">());</span>
      <span class="n">sagaInstance</span><span class="o">.</span><span class="na">setLastRequestId</span><span class="o">(</span><span class="n">lastRequestId</span><span class="o">);</span>

      <span class="n">updateState</span><span class="o">(</span><span class="n">sagaInstance</span><span class="o">,</span> <span class="n">actions</span><span class="o">);</span>

      <span class="n">sagaInstance</span><span class="o">.</span><span class="na">setSerializedSagaData</span><span class="o">(</span><span class="nc">SagaDataSerde</span><span class="o">.</span><span class="na">serializeSagaData</span><span class="o">(</span><span class="n">actions</span><span class="o">.</span><span class="na">getUpdatedSagaData</span><span class="o">().</span><span class="na">orElse</span><span class="o">(</span><span class="n">sagaData</span><span class="o">)));</span>

      <span class="k">if</span> <span class="o">(</span><span class="n">actions</span><span class="o">.</span><span class="na">isEndState</span><span class="o">())</span> <span class="o">{</span>
        <span class="n">performEndStateActions</span><span class="o">(</span><span class="n">sagaId</span><span class="o">,</span> <span class="n">sagaInstance</span><span class="o">,</span> <span class="n">actions</span><span class="o">.</span><span class="na">isCompensating</span><span class="o">(),</span> <span class="n">actions</span><span class="o">.</span><span class="na">isFailed</span><span class="o">(),</span> <span class="n">sagaData</span><span class="o">);</span>
      <span class="o">}</span>

      <span class="n">sagaInstanceRepository</span><span class="o">.</span><span class="na">update</span><span class="o">(</span><span class="n">sagaInstance</span><span class="o">);</span>

      <span class="k">if</span> <span class="o">(!</span><span class="n">actions</span><span class="o">.</span><span class="na">isLocal</span><span class="o">())</span>
        <span class="k">break</span><span class="o">;</span>

      <span class="n">actions</span> <span class="o">=</span> <span class="n">getStateDefinition</span><span class="o">().</span><span class="na">handleReply</span><span class="o">(</span><span class="n">sagaType</span><span class="o">,</span> <span class="n">sagaId</span><span class="o">,</span> <span class="n">actions</span><span class="o">.</span><span class="na">getUpdatedState</span><span class="o">().</span><span class="na">get</span><span class="o">(),</span> <span class="n">actions</span><span class="o">.</span><span class="na">getUpdatedSagaData</span><span class="o">().</span><span class="na">get</span><span class="o">(),</span> <span class="nc">MessageBuilder</span>
              <span class="o">.</span><span class="na">withPayload</span><span class="o">(</span><span class="s">"{}"</span><span class="o">)</span>
              <span class="o">.</span><span class="na">withHeader</span><span class="o">(</span><span class="nc">ReplyMessageHeaders</span><span class="o">.</span><span class="na">REPLY_OUTCOME</span><span class="o">,</span> <span class="nc">CommandReplyOutcome</span><span class="o">.</span><span class="na">SUCCESS</span><span class="o">.</span><span class="na">name</span><span class="o">())</span>
              <span class="o">.</span><span class="na">withHeader</span><span class="o">(</span><span class="nc">ReplyMessageHeaders</span><span class="o">.</span><span class="na">REPLY_TYPE</span><span class="o">,</span> <span class="nc">Success</span><span class="o">.</span><span class="na">class</span><span class="o">.</span><span class="na">getName</span><span class="o">())</span>
              <span class="o">.</span><span class="na">build</span><span class="o">());</span>
    <span class="o">}</span>
  <span class="o">}</span>
<span class="o">}</span>

<span class="kd">private</span> <span class="kt">void</span> <span class="nf">updateState</span><span class="o">(</span><span class="nc">SagaInstance</span> <span class="n">sagaInstance</span><span class="o">,</span> <span class="nc">SagaActions</span><span class="o">&lt;</span><span class="nc">Data</span><span class="o">&gt;</span> <span class="n">actions</span><span class="o">)</span> <span class="o">{</span>
  <span class="n">actions</span><span class="o">.</span><span class="na">getUpdatedState</span><span class="o">().</span><span class="na">ifPresent</span><span class="o">(</span><span class="n">stateName</span> <span class="o">-&gt;</span> <span class="o">{</span>
    <span class="n">sagaInstance</span><span class="o">.</span><span class="na">setStateName</span><span class="o">(</span><span class="n">stateName</span><span class="o">);</span>
    <span class="n">sagaInstance</span><span class="o">.</span><span class="na">setEndState</span><span class="o">(</span><span class="n">actions</span><span class="o">.</span><span class="na">isEndState</span><span class="o">());</span>
    <span class="n">sagaInstance</span><span class="o">.</span><span class="na">setCompensating</span><span class="o">(</span><span class="n">actions</span><span class="o">.</span><span class="na">isCompensating</span><span class="o">());</span>
    <span class="n">sagaInstance</span><span class="o">.</span><span class="na">setFailed</span><span class="o">(</span><span class="n">actions</span><span class="o">.</span><span class="na">isFailed</span><span class="o">());</span>
  <span class="o">});</span>
<span class="o">}</span>
</code></pre></div></div>
<ul>
  <li>LocalStep执行报错，构造Failure的Message作为Reply处理，handleReply在后续讲解</li>
  <li>LocalStep没报错，或执行ParticipantInvocationStep，执行如下步骤
    <ul>
      <li>发送所有Command(当前实现中只有一个Command)，将messageId记录到sagaInstance.lastRequestId</li>
      <li>将sagaActions中更新的状态同步到sagaInstance中，然后落库</li>
      <li>如果是EndState，执行相关清理和回调，这里不展开</li>
      <li>如果是LocalStep，构造Success的Message作为Reply处理</li>
      <li>如果是ParticipantInvocationStep，跳出<code class="language-plaintext highlighter-rouge">while(true)</code>循环，等待Reply进入后续阶段</li>
    </ul>
  </li>
</ul>

<p>现在还剩Command-Reply的处理，让我们再接再厉</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// SimpleSagaDefinition</span>
  <span class="kd">public</span> <span class="nc">SagaActions</span><span class="o">&lt;</span><span class="nc">Data</span><span class="o">&gt;</span> <span class="nf">handleReply</span><span class="o">(</span><span class="nc">String</span> <span class="n">sagaType</span><span class="o">,</span> <span class="nc">String</span> <span class="n">sagaId</span><span class="o">,</span> <span class="nc">String</span> <span class="n">currentState</span><span class="o">,</span> <span class="nc">Data</span> <span class="n">sagaData</span><span class="o">,</span> <span class="nc">Message</span> <span class="n">message</span><span class="o">)</span> <span class="o">{</span>
    <span class="nc">SagaExecutionState</span> <span class="n">state</span> <span class="o">=</span> <span class="nc">SagaExecutionStateJsonSerde</span><span class="o">.</span><span class="na">decodeState</span><span class="o">(</span><span class="n">currentState</span><span class="o">);</span>
    <span class="nc">SagaStep</span><span class="o">&lt;</span><span class="nc">Data</span><span class="o">&gt;</span> <span class="n">currentStep</span> <span class="o">=</span> <span class="n">steps</span><span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="n">state</span><span class="o">.</span><span class="na">getCurrentlyExecuting</span><span class="o">());</span>
    <span class="kt">boolean</span> <span class="n">compensating</span> <span class="o">=</span> <span class="n">state</span><span class="o">.</span><span class="na">isCompensating</span><span class="o">();</span>

    <span class="n">currentStep</span><span class="o">.</span><span class="na">getReplyHandler</span><span class="o">(</span><span class="n">message</span><span class="o">,</span> <span class="n">compensating</span><span class="o">).</span><span class="na">ifPresent</span><span class="o">(</span><span class="n">handler</span> <span class="o">-&gt;</span> <span class="n">invokeReplyHandler</span><span class="o">(</span><span class="n">message</span><span class="o">,</span> <span class="n">sagaData</span><span class="o">,</span> <span class="o">(</span><span class="n">d</span><span class="o">,</span> <span class="n">m</span><span class="o">)</span> <span class="o">-&gt;</span> <span class="o">{</span>
      <span class="n">handler</span><span class="o">.</span><span class="na">accept</span><span class="o">(</span><span class="n">d</span><span class="o">,</span> <span class="n">m</span><span class="o">);</span>
      <span class="k">return</span> <span class="kc">null</span><span class="o">;</span>
    <span class="o">}));</span>

    <span class="nc">SagaActionsProvider</span><span class="o">&lt;</span><span class="nc">Data</span><span class="o">&gt;</span> <span class="n">sap</span> <span class="o">=</span> <span class="n">sagaActionsForNextStep</span><span class="o">(</span><span class="n">sagaType</span><span class="o">,</span> <span class="n">sagaId</span><span class="o">,</span> <span class="n">sagaData</span><span class="o">,</span> <span class="n">message</span><span class="o">,</span> <span class="n">state</span><span class="o">,</span> <span class="n">currentStep</span><span class="o">,</span> <span class="n">compensating</span><span class="o">);</span>
    <span class="k">return</span> <span class="nf">toSagaActions</span><span class="o">(</span><span class="n">sap</span><span class="o">);</span>
  <span class="o">}</span>
<span class="c1">// AbstractSimpleSagaDefinition</span>
<span class="kd">protected</span> <span class="nc">Provider</span> <span class="nf">sagaActionsForNextStep</span><span class="o">(</span><span class="nc">String</span> <span class="n">sagaType</span><span class="o">,</span> <span class="nc">String</span> <span class="n">sagaId</span><span class="o">,</span> <span class="nc">Data</span> <span class="n">sagaData</span><span class="o">,</span> <span class="nc">Message</span> <span class="n">message</span><span class="o">,</span>
                                          <span class="nc">SagaExecutionState</span> <span class="n">state</span><span class="o">,</span> <span class="nc">Step</span> <span class="n">currentStep</span><span class="o">,</span> <span class="kt">boolean</span> <span class="n">compensating</span><span class="o">)</span> <span class="o">{</span>
  <span class="k">if</span> <span class="o">(</span><span class="n">currentStep</span><span class="o">.</span><span class="na">isSuccessfulReply</span><span class="o">(</span><span class="n">compensating</span><span class="o">,</span> <span class="n">message</span><span class="o">))</span> <span class="o">{</span>
    <span class="k">return</span> <span class="nf">nextStepToExecute</span><span class="o">(</span><span class="n">state</span><span class="o">,</span> <span class="n">sagaData</span><span class="o">);</span>
  <span class="o">}</span> <span class="k">else</span> <span class="k">if</span> <span class="o">(</span><span class="n">compensating</span><span class="o">)</span> <span class="o">{</span>
    <span class="k">return</span> <span class="nf">handleFailedCompensatingTransaction</span><span class="o">(</span><span class="n">sagaType</span><span class="o">,</span> <span class="n">sagaId</span><span class="o">,</span> <span class="n">state</span><span class="o">,</span> <span class="n">message</span><span class="o">);</span>
  <span class="o">}</span> <span class="k">else</span> <span class="o">{</span>
    <span class="k">return</span> <span class="nf">nextStepToExecute</span><span class="o">(</span><span class="n">state</span><span class="o">.</span><span class="na">startCompensating</span><span class="o">(),</span> <span class="n">sagaData</span><span class="o">);</span>
  <span class="o">}</span>
<span class="o">}</span>

<span class="kd">protected</span> <span class="nc">Provider</span> <span class="nf">handleFailedCompensatingTransaction</span><span class="o">(</span><span class="nc">String</span> <span class="n">sagaType</span><span class="o">,</span> <span class="nc">String</span> <span class="n">sagaId</span><span class="o">,</span> <span class="nc">SagaExecutionState</span> <span class="n">state</span><span class="o">,</span> <span class="nc">Message</span> <span class="n">message</span><span class="o">)</span> <span class="o">{</span>
  <span class="n">logger</span><span class="o">.</span><span class="na">error</span><span class="o">(</span><span class="s">"Saga {} {} failed due to failed compensating transaction {}"</span><span class="o">,</span> <span class="n">sagaType</span><span class="o">,</span> <span class="n">sagaId</span><span class="o">,</span> <span class="n">message</span><span class="o">);</span>
  <span class="k">return</span> <span class="nf">makeSagaActionsProvider</span><span class="o">(</span><span class="nc">SagaActions</span><span class="o">.&lt;</span><span class="nc">Data</span><span class="o">&gt;</span><span class="n">builder</span><span class="o">()</span>
          <span class="o">.</span><span class="na">withUpdatedState</span><span class="o">(</span><span class="nc">SagaExecutionStateJsonSerde</span><span class="o">.</span><span class="na">encodeState</span><span class="o">(</span><span class="nc">SagaExecutionState</span><span class="o">.</span><span class="na">makeFailedEndState</span><span class="o">()))</span>
          <span class="o">.</span><span class="na">withIsEndState</span><span class="o">(</span><span class="kc">true</span><span class="o">)</span>
          <span class="o">.</span><span class="na">withIsCompensating</span><span class="o">(</span><span class="n">state</span><span class="o">.</span><span class="na">isCompensating</span><span class="o">())</span>
          <span class="o">.</span><span class="na">withIsFailed</span><span class="o">(</span><span class="kc">true</span><span class="o">)</span>
          <span class="o">.</span><span class="na">build</span><span class="o">());</span>
<span class="o">}</span>
<span class="c1">// SagaExecutionState</span>
<span class="kd">public</span> <span class="nc">SagaExecutionState</span> <span class="nf">startCompensating</span><span class="o">()</span> <span class="o">{</span>
  <span class="k">return</span> <span class="k">new</span> <span class="nf">SagaExecutionState</span><span class="o">(</span><span class="n">currentlyExecuting</span><span class="o">,</span> <span class="kc">true</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>
<ul>
  <li>handleReply首先将Message根据header中的REPLY_TYPE转换为Reply对象，然后使用handler处理</li>
  <li>然后计算saga的下一步，分为三种情况
    <ul>
      <li>先判断Reply是否为SUCCESS，判断条件为Message的header<code class="language-plaintext highlighter-rouge">REPLY_OUTCOME</code>为SUCCESS，是则用之前讲过的nextStepToExecute</li>
      <li>如果是补偿操作且失败了，创建failed=true、compensating=true的EndState的SagaActions，这种情况需要人工介入</li>
      <li>如果是非补偿操作失败了，从currentlyExecuting的上一步开始补偿操作
        <ul>
          <li><code class="language-plaintext highlighter-rouge">nextStepToExecute(state.startCompensating(), sagaData)</code>中startCompensating是当前状态设置compensating为true，nextStep就是上一步</li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<h2 id="正向流程">正向流程</h2>
<p>现在开始演示CreateOrderSaga的正向流程，验证是否和我们对代码的理解一致。<br />
首先orderService创建Order，<code class="language-plaintext highlighter-rouge">http://localhost:8082/swagger-ui/index.html</code>， 参数如下</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"consumerId"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w">
  </span><span class="nl">"deliveryAddress"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"city"</span><span class="p">:</span><span class="w"> </span><span class="s2">"string"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"state"</span><span class="p">:</span><span class="w"> </span><span class="s2">"string"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"street1"</span><span class="p">:</span><span class="w"> </span><span class="s2">"string"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"street2"</span><span class="p">:</span><span class="w"> </span><span class="s2">"string"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"zip"</span><span class="p">:</span><span class="w"> </span><span class="s2">"string"</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"deliveryTime"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2025-04-28T14:23:49.111Z"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"lineItems"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
    </span><span class="p">{</span><span class="w">
      </span><span class="nl">"menuItemId"</span><span class="p">:</span><span class="w"> </span><span class="s2">"1"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"quantity"</span><span class="p">:</span><span class="w"> </span><span class="mi">10</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">],</span><span class="w">
  </span><span class="nl">"restaurantId"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>初始状态为APPROVAL_PENDING，发布OrderCreatedEvent，然后执行step1</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">step</span><span class="o">()</span>
<span class="o">.</span><span class="na">withCompensation</span><span class="o">(</span><span class="n">orderService</span><span class="o">.</span><span class="na">reject</span><span class="o">,</span> <span class="nl">CreateOrderSagaState:</span><span class="o">:</span><span class="n">makeRejectOrderCommand</span><span class="o">)</span>
</code></pre></div></div>
<p>由于未定义withAction，step1跳过，进入step2</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">.</span><span class="na">step</span><span class="o">()</span>
<span class="o">.</span><span class="na">invokeParticipant</span><span class="o">(</span><span class="n">consumerService</span><span class="o">.</span><span class="na">validateOrder</span><span class="o">,</span> <span class="nl">CreateOrderSagaState:</span><span class="o">:</span><span class="n">makeValidateOrderByConsumerCommand</span><span class="o">)</span>
</code></pre></div></div>
<p>这时向consumerService发送validateOrder命令，channel为<code class="language-plaintext highlighter-rouge">consumerService</code>，我们分别在Message表和kafka topic中找到对应记录
<img src="/assets/img/sagas/create-order/validate-order-in-db.png" alt="" /><br />
<img src="/assets/img/sagas/create-order/validate-order-in-topic.png" alt="" /><br />
接下来consumerService收到命令并执行，只是验证consumerId存在并返回Success，Reply发送到<code class="language-plaintext highlighter-rouge">net.chrisrichardson.ftgo.orderservice.sagas.createorder.CreateOrderSaga-reply</code>
<img src="/assets/img/sagas/create-order/validate-order-reply-in-db.png" alt="" /><br />
<img src="/assets/img/sagas/create-order/validate-order-reply-in-topic.png" alt="" /><br />
OrderService收到Reply后，触发step3</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">.</span><span class="na">step</span><span class="o">()</span>
  <span class="o">.</span><span class="na">invokeParticipant</span><span class="o">(</span><span class="n">kitchenService</span><span class="o">.</span><span class="na">create</span><span class="o">,</span> <span class="nl">CreateOrderSagaState:</span><span class="o">:</span><span class="n">makeCreateTicketCommand</span><span class="o">)</span>
  <span class="o">.</span><span class="na">onReply</span><span class="o">(</span><span class="nc">CreateTicketReply</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="nl">CreateOrderSagaState:</span><span class="o">:</span><span class="n">handleCreateTicketReply</span><span class="o">)</span>
  <span class="o">.</span><span class="na">withCompensation</span><span class="o">(</span><span class="n">kitchenService</span><span class="o">.</span><span class="na">cancel</span><span class="o">,</span> <span class="nl">CreateOrderSagaState:</span><span class="o">:</span><span class="n">makeCancelCreateTicketCommand</span><span class="o">)</span>
</code></pre></div></div>
<p>向kitchenService发送create Command，channel为<code class="language-plaintext highlighter-rouge">kitchenService</code>
<img src="/assets/img/sagas/create-order/kitchen-create-ticket-in-db.png" alt="" /><br />
<img src="/assets/img/sagas/create-order/create-ticket-in-topic.png" alt="" /><br />
主要流程我们已经清楚了，现在直接跳到step6</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">.</span><span class="na">step</span><span class="o">()</span>
    <span class="o">.</span><span class="na">invokeParticipant</span><span class="o">(</span><span class="n">orderService</span><span class="o">.</span><span class="na">approve</span><span class="o">,</span> <span class="nl">CreateOrderSagaState:</span><span class="o">:</span><span class="n">makeApproveOrderCommand</span><span class="o">)</span>
</code></pre></div></div>
<p>OrderService发送approve command
<img src="/assets/img/sagas/create-order/approve-in-db.png" alt="" />  <br />
<img src="/assets/img/sagas/create-order/approve-in-topic.png" alt="" /><br />
OrderService收到消息并消费，更新Order状态为APPROVED，并发布OrderAuthorized领域事件，然后发送SUCCESS的Reply<br />
<img src="/assets/img/sagas/create-order/order-approved-in-db.png" alt="" /><br />
<img src="/assets/img/sagas/create-order/order-authorized-event.png" alt="" /><br />
<img src="/assets/img/sagas/create-order/approve-order-reply.png" alt="" /><br />
最后进入step7，就是EndStateAction，会更新SagaInstance中end_state的状态
<img src="/assets/img/sagas/create-order/end-state.png" alt="" /><br />
至此CreateOrderSaga的流程就完全结束了。</p>

<h2 id="补偿流程">补偿流程</h2>
<p>再次拿出我们的saga流程表</p>

<table>
  <thead>
    <tr>
      <th><strong>step</strong></th>
      <th><strong>action</strong></th>
      <th><strong>compensation</strong></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1</td>
      <td> </td>
      <td>orderService.reject</td>
    </tr>
    <tr>
      <td>2</td>
      <td>consumerService.validateOrder</td>
      <td> </td>
    </tr>
    <tr>
      <td>3</td>
      <td>kitchenService.create</td>
      <td>kitchenService.cancel</td>
    </tr>
    <tr>
      <td>4</td>
      <td>accountingService.authorize</td>
      <td> </td>
    </tr>
    <tr>
      <td>5</td>
      <td>kitchenService.confirmCreate</td>
      <td> </td>
    </tr>
    <tr>
      <td>6</td>
      <td>orderService.approve</td>
      <td> </td>
    </tr>
  </tbody>
</table>

<p>假如我们让step3的kitchenService.create失败，则会从step2开始执行补偿流程，所以会执行<code class="language-plaintext highlighter-rouge">orderService.reject</code>。<br />
让我们修改一下代码重新部署。</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ftgo-kitchen-service.KitchenServiceCommandHandler</span>
<span class="kd">private</span> <span class="nc">Message</span> <span class="nf">createTicket</span><span class="o">(</span><span class="nc">CommandMessage</span><span class="o">&lt;</span><span class="nc">CreateTicket</span><span class="o">&gt;</span> <span class="n">cm</span><span class="o">)</span> <span class="o">{</span>
  <span class="nc">CreateTicket</span> <span class="n">command</span> <span class="o">=</span> <span class="n">cm</span><span class="o">.</span><span class="na">getCommand</span><span class="o">();</span>
  <span class="kt">long</span> <span class="n">restaurantId</span> <span class="o">=</span> <span class="n">command</span><span class="o">.</span><span class="na">getRestaurantId</span><span class="o">();</span>
  <span class="nc">Long</span> <span class="n">ticketId</span> <span class="o">=</span> <span class="n">command</span><span class="o">.</span><span class="na">getOrderId</span><span class="o">();</span>
  <span class="nc">TicketDetails</span> <span class="n">ticketDetails</span> <span class="o">=</span> <span class="n">command</span><span class="o">.</span><span class="na">getTicketDetails</span><span class="o">();</span>

  <span class="k">try</span> <span class="o">{</span>
    <span class="nc">Ticket</span> <span class="n">ticket</span> <span class="o">=</span> <span class="n">kitchenService</span><span class="o">.</span><span class="na">createTicket</span><span class="o">(</span><span class="n">restaurantId</span><span class="o">,</span> <span class="n">ticketId</span><span class="o">,</span> <span class="n">ticketDetails</span><span class="o">);</span>
    <span class="nc">CreateTicketReply</span> <span class="n">reply</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">CreateTicketReply</span><span class="o">(</span><span class="n">ticket</span><span class="o">.</span><span class="na">getId</span><span class="o">());</span>
    <span class="k">throw</span> <span class="k">new</span> <span class="nf">RestaurantDetailsVerificationException</span><span class="o">();</span>
    <span class="c1">// return withLock(Ticket.class, ticket.getId()).withSuccess(reply);</span>
  <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">RestaurantDetailsVerificationException</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>
    <span class="k">return</span> <span class="nf">withFailure</span><span class="o">();</span>
  <span class="o">}</span>
<span class="o">}</span>
<span class="c1">// Ticket</span>
<span class="kd">public</span> <span class="nc">List</span><span class="o">&lt;</span><span class="nc">TicketDomainEvent</span><span class="o">&gt;</span> <span class="nf">cancelCreate</span><span class="o">()</span> <span class="o">{</span>
  <span class="n">state</span> <span class="o">=</span> <span class="nc">TicketState</span><span class="o">.</span><span class="na">CANCELLED</span><span class="o">;</span>
  <span class="k">return</span> <span class="nc">Collections</span><span class="o">.</span><span class="na">emptyList</span><span class="o">();</span>
<span class="o">}</span>
</code></pre></div></div>
<p>和之前一样，先创建Consumer和Restaurant，然后创建Order。<br />
接下来我们直接从kitchenService.create开始检查，首先是orderService发送Command到<code class="language-plaintext highlighter-rouge">kitchenService</code>
<img src="/assets/img/sagas/create-order-compensating/kitchen-create-command.png" alt="" /><br />
kitchenService接收Command，创建Ticket状态为CREATE_PENDING，发送Failure的Reply到<code class="language-plaintext highlighter-rouge">CreateOrderSaga-reply</code>
<img src="/assets/img/sagas/create-order-compensating/kitchen-create-failure-reply.png" alt="" /><br />
orderService发送orderService.reject命令到<code class="language-plaintext highlighter-rouge">orderService</code>
<img src="/assets/img/sagas/create-order-compensating/reject-order.png" alt="" /><br />
orderService将状态改为REJECTED，并发送Success的Reply到<code class="language-plaintext highlighter-rouge">CreateOrderSaga-reply</code>
<img src="/assets/img/sagas/create-order-compensating/reject-order-reply.png" alt="" /><br />
最后进行EndState处理，修改SagaInstance的状态，补偿流程完毕。<br />
<img src="/assets/img/sagas/create-order-compensating/end-state.png" alt="" /><br />
可以看到endsSate和compensating都为true</p>

<h2 id="流程总结">流程总结</h2>
<p>我们通过只有三步的sagas流程总结一下本章的内容。
正向流程如下</p>
<pre><code class="language-mermaid">sequenceDiagram
    participant o as Orchestrator
    participant p as Participant
    
    o-&gt;&gt;p: request
    Note over p: step1 action
    p-&gt;&gt;o: reply
    o-&gt;&gt;p: request
    Note over p: step2 action
    p-&gt;&gt;o: reply
    o-&gt;&gt;p: request
    Note over p: step3 action
    p-&gt;&gt;o: reply
    Note over o: endState action
</code></pre>
<p>假设step3执行失败，补偿流程如下</p>
<pre><code class="language-mermaid">sequenceDiagram
    participant o as Orchestrator
    participant p as Participant
    
    o-&gt;&gt;p: request
    Note over p: step1 action
    p-&gt;&gt;o: reply
    o-&gt;&gt;p: request
    Note over p: step2 action
    p-&gt;&gt;o: reply
    o-&gt;&gt;p: request
    Note over p: step3 action(failed)
    p-&gt;&gt;o: reply(failure)
    o-&gt;&gt;p: compensating request
    Note over p: step2 compensation
    p-&gt;&gt;o: reply
    o-&gt;&gt;p: compensating request
    Note over p: step1 compensation
    p-&gt;&gt;o: reply
    Note over o: endState action
</code></pre>]]></content><author><name>cafewang</name></author><category term="microservices" /><category term="sagas" /><summary type="html"><![CDATA[本文以微服务设计模式书中的demo项目ftgo-application为例，讲解sagas模式在微服务架构中的具体实现。 文中不会专门讲解sagas的概念，而是在演示过程中穿插解释sagas是什么，在阅读和操练项目的过程中，您会加深对sagas的理解，同时也能学到sagas的落地方式。]]></summary></entry><entry><title type="html">JVM问题排查</title><link href="https://cafewang.github.io//jvm-in-depth/jvm-trouble-shooting" rel="alternate" type="text/html" title="JVM问题排查" /><published>2025-04-15T08:39:00+00:00</published><updated>2025-04-15T08:39:00+00:00</updated><id>https://cafewang.github.io//jvm-in-depth/jvm-problem-shooting</id><content type="html" xml:base="https://cafewang.github.io//jvm-in-depth/jvm-trouble-shooting"><![CDATA[<p>jvm相关的问题通常是很难遇到的，因此积累排查问题的经验并不容易，但是我们可以借鉴他人的排查案例，来总结排查的流程和方法。</p>

<h2 id="问题分类">问题分类</h2>
<p>首先是最关键的一点，问题究竟是不是和jvm有关？和jvm相关的问题一般有如下几个特征</p>
<ul>
  <li>gc变得频繁，导致吞吐量变小</li>
  <li>单次gc时间变长，容易引发接口超时</li>
  <li>新生代或老年代(或永久代、元空间)占有率异常，比之前要大，从而引发gc</li>
  <li>栈空间不足，通常引发stack overflow</li>
  <li>堆外空间申请过多，也会触发oom</li>
</ul>

<p>总结起来，就是空间不足(可能是代码bug导致使用了过量对象)或者空间分配不合理(比如新生代和老年代、eden和survivor)。<br />
如果问题没有显现类似特征，我们有理由相信是问题是其他原因引发的。</p>

<h2 id="oom类型">OOM类型</h2>
<ul>
  <li>堆内存不足，<code class="language-plaintext highlighter-rouge">java.lang.OutOfMemoryError: Java heap space</code></li>
  <li>1.7及之前，永久代不足，<code class="language-plaintext highlighter-rouge">java.lang.OutOfMemoryError: PermGen space</code></li>
  <li>1.8及之后，元空间不足，<code class="language-plaintext highlighter-rouge">java.lang.OutOfMemoryError: Metaspace</code></li>
  <li>栈空间不足，<code class="language-plaintext highlighter-rouge">java.lang.StackOverflowError</code></li>
  <li>直接内存不足，<code class="language-plaintext highlighter-rouge">java.lang.OutOfMemoryError: Direct buffer memory</code></li>
</ul>

<h2 id="分代空间比例">分代空间比例</h2>
<p>知道哪块空间不足，并不是要盲目扩充整个堆大小，最重要的是确定不同分代间的空间分布。<br />
<img src="/assets/img/collector/space-ratio.png" alt="" /></p>
<ul>
  <li>整个堆空间分为已提交和未提交，即最小到最大堆内存的差值，在所需空间增大过程中也会产生GC</li>
  <li>已提交空间分为新生代和老年代，需根据对象分布调整，一般老年代大小应为长期对象体积的2到3倍</li>
  <li>新生代分为eden和survivor，survivor相当于到老年代的缓冲，过小可能导致过早的full GC</li>
  <li>堆外内存一般有元空间和直接内存，可以根据需要调整大小</li>
</ul>

<h2 id="常见场景">常见场景</h2>
<h3 id="查询大对象">查询大对象</h3>
<p>大对象经常是引发频繁GC的元凶，在CMS和G1中，过多的大对象容易引发内部碎片，导致oom。<br />
这里我们设置一个场景，设定最大堆内存为500MB，循环申请32MB数组(不释放)，显然循环到第16次时，会发生oom。<br />
代码见测试类<code class="language-plaintext highlighter-rouge">ObjectAddressTest</code>的<code class="language-plaintext highlighter-rouge">test_big_object</code>方法。<br />
对应到服务端，就会出现服务宕机的情况，排查过程如下</p>

<ul>
  <li>首先查看监控上的gc频率、暂停时间、各个分代大小，可以看到老年代占用率过高(大对象在G1中直接分配到老年代)</li>
  <li>在k8s上配置vm options
    <div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>-XX:+HeapDumpOnOutOfMemoryError // 导出堆快照
-XX:HeapDumpPath=&lt;file-path&gt; // 导出路径
-Xlog:gc* (针对JVM 9及以上，打印详细gc信息)
-XX:+PrintGCDetails -Xloggc:&lt;file-path&gt; (针对JVM 8及以下)
</code></pre></div>    </div>
  </li>
  <li>分析日志，G1日志显示的是清理前后已使用的区域个数，以eden举例，清理前是3个，清理了3个，可以看到大对象区域占用最多
    <div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[0.144s][info][gc,heap      ] GC(0) Eden regions: 3-&gt;0(124)
[0.144s][info][gc,heap      ] GC(0) Survivor regions: 0-&gt;1(3)
[0.144s][info][gc,heap      ] GC(0) Old regions: 0-&gt;0
[0.144s][info][gc,heap      ] GC(0) Humongous regions: 99-&gt;99
[0.144s][info][gc,metaspace ] GC(0) Metaspace: 4788K(5120K)-&gt;4788K(5120K) NonClass: 4394K(4608K)-&gt;4394K(4608K) Class: 393K(512K)-&gt;393K(512K)
</code></pre></div>    </div>
  </li>
  <li>下载mat解析堆快照，注意版本不超过1.11(支持jdk8)</li>
  <li>编辑文件 MemoryAnalyzer.ini 添加 -vmargs – Xmx4g，保证能解析几个G的堆快照</li>
  <li>由于程序比较简单，可以看到占用内存最大的就是我们申请的byte[]，真实程序的分析更加复杂，需要结合不同分代的占用率和代码变更来看
<img src="/assets/img/gc/mat-histogram.png" alt="" /></li>
</ul>

<p>原因：最常见的原因是数据库查询时未加<code class="language-plaintext highlighter-rouge">limit</code>限制</p>

<h3 id="缓存未释放">缓存未释放</h3>
<p>业务中常用的本地缓存如guava，一般是通过静态Map实现的，如果不设置上限，内存占用率会一直累积。<br />
首先会逐步填满新生代，导致频繁的minor GC，之后填满老年代，引发full GC。<br />
我们用静态Map不断插入新对象来，设置最大堆内存200MB，见方法<code class="language-plaintext highlighter-rouge">test_static_map</code>。<br />
从GC日志可以看到，每次minor GC都会使老年代占用率变高，然后逐渐引发full GC。</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[0.131s][info][gc,start     ] GC(0) Pause Young (Normal) (G1 Evacuation Pause)
[0.139s][info][gc,heap      ] GC(0) Eden regions: 24-&gt;0(13)
[0.139s][info][gc,heap      ] GC(0) Survivor regions: 0-&gt;3(3)
[0.139s][info][gc,heap      ] GC(0) Old regions: 0-&gt;20
[0.139s][info][gc,heap      ] GC(0) Humongous regions: 0-&gt;0
[0.139s][info][gc,metaspace ] GC(0) Metaspace: 3690K(4864K)-&gt;3690K(4864K) NonClass: 3374K(4352K)-&gt;3374K(4352K) Class: 316K(512K)-&gt;316K(512K)
[0.139s][info][gc           ] GC(0) Pause Young (Normal) (G1 Evacuation Pause) 24M-&gt;22M(200M) 7.537ms

[0.165s][info][gc,start     ] GC(3) Pause Young (Normal) (G1 Evacuation Pause)
[0.171s][info][gc,heap      ] GC(3) Eden regions: 25-&gt;0(25)
[0.171s][info][gc,heap      ] GC(3) Survivor regions: 4-&gt;4(4)
[0.171s][info][gc,heap      ] GC(3) Old regions: 59-&gt;85
[0.171s][info][gc,heap      ] GC(3) Humongous regions: 0-&gt;0
[0.171s][info][gc,metaspace ] GC(3) Metaspace: 3690K(4864K)-&gt;3690K(4864K) NonClass: 3374K(4352K)-&gt;3374K(4352K) Class: 316K(512K)-&gt;316K(512K)
[0.171s][info][gc           ] GC(3) Pause Young (Normal) (G1 Evacuation Pause) 88M-&gt;88M(200M) 6.339ms

[0.354s][info][gc,start       ] GC(45) Pause Full (G1 Evacuation Pause)
[0.359s][info][gc,heap        ] GC(45) Eden regions: 0-&gt;0(10)
[0.359s][info][gc,heap        ] GC(45) Survivor regions: 0-&gt;0(2)
[0.359s][info][gc,heap        ] GC(45) Old regions: 200-&gt;200
[0.359s][info][gc,heap        ] GC(45) Humongous regions: 0-&gt;0
[0.359s][info][gc,metaspace   ] GC(45) Metaspace: 3693K(4864K)-&gt;3693K(4864K) NonClass: 3376K(4352K)-&gt;3376K(4352K) Class: 316K(512K)-&gt;316K(512K)
[0.359s][info][gc             ] GC(45) Pause Full (G1 Evacuation Pause) 198M-&gt;198M(200M) 4.601ms
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">pause young</code>即minor GC，可以看到old regions在变大，<code class="language-plaintext highlighter-rouge">pause full</code>即full GC，可以看到old regions已经满了(这里每个region是1MB)。</p>

<h3 id="直接内存未释放">直接内存未释放</h3>
<p>java生态中一般用到直接内存的地方有NIO和netty，如果申请超过上限<code class="language-plaintext highlighter-rouge">MaxDirectMemorySize</code>也会报oom，提示是<code class="language-plaintext highlighter-rouge">Direct buffer memory</code>，所以这种问题相对好定位。<br />
测试方法为<code class="language-plaintext highlighter-rouge">test_direct_buffer</code>，通过ByteBuffer申请直接内存直到报错。<br />
直接内存不足导致的宕机并不会留下堆快照。</p>

<h2 id="总结">总结</h2>
<p>定位问题的一般步骤如下</p>
<ul>
  <li>发现问题，如服务报错增多、超时增多、gc频率或分代占用率高于平均值，依赖监控组件</li>
  <li>变量分析，变化来自外部或内部，首先一定是代码diff，除了代码还需要检查依赖项的版本是否变更，如果是对外的api还需要关注对接方是否有变更通告</li>
  <li>如果上述分析还不能定位到问题，需要打开gc日志，如果宕机需要到处堆快照，通过日志和MAT工具分析异常的分代和对象(体积过大或数量过多)</li>
  <li>如果堆正常，检查元空间、直接内存的使用，元空间占用主要和代理类有关，直接内存使用主要和NIO和netty相关。</li>
</ul>]]></content><author><name>cafewang</name></author><category term="jvm" /><category term="trouble-shooting" /><summary type="html"><![CDATA[jvm相关的问题通常是很难遇到的，因此积累排查问题的经验并不容易，但是我们可以借鉴他人的排查案例，来总结排查的流程和方法。]]></summary></entry><entry><title type="html">SSO实战</title><link href="https://cafewang.github.io//security/sso-in-action" rel="alternate" type="text/html" title="SSO实战" /><published>2025-04-15T08:39:00+00:00</published><updated>2025-04-15T08:39:00+00:00</updated><id>https://cafewang.github.io//security/sso-in-action</id><content type="html" xml:base="https://cafewang.github.io//security/sso-in-action"><![CDATA[<p>说出来可能让人意外，登录系统可能是最复杂的系统之一，并不是看起来的<code class="language-plaintext highlighter-rouge">注册</code>、<code class="language-plaintext highlighter-rouge">登录</code>、<code class="language-plaintext highlighter-rouge">退出</code>这么简单。<br />
列举一下登录系统中涉及的重要问题：</p>
<ul>
  <li>密码在前后端怎样存储和验证最安全？</li>
  <li>网站的密码复杂度要求高吗？</li>
  <li>更改密码时怎样退出其他登录的设备？</li>
  <li>怎么保持认证状态，使用session还是JWT？</li>
  <li>session时长设置成多久比较合适？</li>
  <li>多因素认证怎么实现？</li>
</ul>

<p>看吧，这些问题涉及面太广了，与其实现一个玩具系统，不如研究市面上的成熟框架是怎么解决问题的。<br />
本文重点讨论单点登录的成熟实现，其他登录相关问题不在讨论范围内。</p>

<h2 id="方案选型">方案选型</h2>
<p>我们选择的方案是适用于微服务的<code class="language-plaintext highlighter-rouge">keycloak+spring security oauth2</code>，下面讲解一下重要概念</p>

<h3 id="oauth2">OAuth2</h3>
<p>OAuth2是成熟的<code class="language-plaintext highlighter-rouge">授权</code>方案，注意只是<code class="language-plaintext highlighter-rouge">授权</code>，不涉及<code class="language-plaintext highlighter-rouge">认证</code>，要理解OAuth2首先需要明确其中的角色</p>
<ul>
  <li>Resource Owner：资源所有者，通常是拥有某个账号的用户</li>
  <li>Resource Server：待访问的资源，通常是api服务</li>
  <li>Client：要访问资源的主体，换句话说就是Resource Owner在使用的应用，需要访问Resource Server中的资源</li>
  <li>Authorization Server：授权服务器，验证Client代表了谁，能做什么事，给Client下发Access Token，使其能访问资源。</li>
</ul>

<p><img src="/assets/img/oauth2/oauth2.webp" alt="" /><br />
结合图片理解，用户要通过浏览器浏览网页，用户就是Resource Owner，通过Client浏览器，获取Resource Server上的网页，大体步骤为</p>
<ul>
  <li>用户输入网址并访问，由于没有授权，网页跳转到Authorization Server管理的登录页面</li>
  <li>登录成功，Authorization Server返回一个access token，跳转回原页面</li>
  <li>页面利用access token访问Resource Server上受保护的资源</li>
</ul>

<p>可以通过以下场景加深理解</p>
<ul>
  <li>你约了保洁上门打扫，但是临时有事不在家，所以让门房准备了一把备用钥匙，保洁开门后只能打扫，而且必须下午3点前归还钥匙</li>
  <li>场景中<code class="language-plaintext highlighter-rouge">你</code>就是Resource Owner，<code class="language-plaintext highlighter-rouge">保洁</code>就是Client，<code class="language-plaintext highlighter-rouge">门卫</code>是Authorization Server，<code class="language-plaintext highlighter-rouge">家</code>是Resource Service，<code class="language-plaintext highlighter-rouge">备用钥匙</code>是access key，只能限时访问家里的<code class="language-plaintext highlighter-rouge">打扫资源</code>(如果算资源的话)</li>
</ul>

<p>OAuth2有多重授权方式(即获取access token的方式)，对应不同流程和场景，这里我们介绍和使用其中两种</p>
<ul>
  <li>授权码模式(Authorization Code)，适用于用户通过浏览器访问资源的授权</li>
  <li>客户端凭证模式(Client Credential)，适用于服务内部授权</li>
</ul>

<h3 id="oidc">OIDC</h3>
<p>全称为OpenID Connect，是基于OAuth2的<code class="language-plaintext highlighter-rouge">认证</code>规范，它主要负责用户身份在不同主机、设备上的识别、维护，不包括<code class="language-plaintext highlighter-rouge">授权</code>。<br />
也就是说，OAuth2和OIDC并不是同类型的规范，它们可以结合使用。</p>

<h3 id="jwt">JWT</h3>
<p>JWT就是用非对称加密来传递<code class="language-plaintext highlighter-rouge">明文</code>的json消息，使得接收方可以通过公钥验证消息来源，而发送方可以通过私钥确认回传的消息没有被篡改。<br />
json消息分为header和payload两部分，通过base64转码(最后两个字符为-和_，这样可以通过url传输)后用加密算法签名，将三部分一起发送给接收方。<br />
header和payload都是明文，所以不能传输敏感信息，可以通过<a href="https://jwt.io/">jwt.io</a>解码，如下图
<img src="/assets/img/oauth2/jwt.png" alt="" /><br />
jwt常见携带的信息有用户信息、权限、过期时间。</p>

<blockquote>
  <p>注意<br />
jwt是无状态的，从创建之时起就不能修改，直到过期
仅使用无状态的jwt代替有状态的session是不可取的，可以看后续内容OAuth2对JWT的使用</p>
</blockquote>

<h3 id="keycloak">keycloak</h3>
<p>keycloak是JBoss公司开发的，支持OAuth2和OIDC以及SAML(微软推出的认证规范)的Authorization Server。<br />
它的主要功能有维护用户、权限信息，支持授权操作(即给Client发放access token)，非常适合集群上的微服务应用授权。</p>

<h3 id="spring-security-oauth2">Spring Security OAuth2</h3>
<p>将springboot应用接入基于OAuth2/OIDC的认证授权流程，应用可以作为Resource Server或Client，甚至是Authorization Server</p>

<h2 id="实战项目">实战项目</h2>
<p>克隆项目<code class="language-plaintext highlighter-rouge">https://github.com/cafewang/oauth-sso</code>
(参考<a href="https://github.com/sivaprasadreddy/spring-security-oauth2-microservices-demo">项目</a>和<a href="https://www.sivalabs.in/spring-security-oauth2-tutorial-introduction/">教程</a>，感谢siva大佬的分享)<br />
运行环境为jdk17，maven3，docker-compose，分为四个服务</p>
<ul>
  <li>运行在docker上的keycloak，在项目路径下执行<code class="language-plaintext highlighter-rouge">docker compose up -d</code>启动，使用 <code class="language-plaintext highlighter-rouge">admin/admin1234</code> 登录<a href="http://localhost:9191">控制台</a></li>
  <li>messages-service，作为Resource Server，提供消息创建和查询接口</li>
  <li>messages-webapp，作为Client，前端页面，展示消息，并提供创建消息、归档消息的入口</li>
  <li>archival-service，定时执行归档消息的跑批，跑批时作为Client调用messages-service的归档接口，同时作为Resource Server提供触发跑批的入口，演示服务作为两种角色的场景</li>
</ul>

<h3 id="环境准备">环境准备</h3>
<h4 id="keycloak配置">keycloak配置</h4>
<p>首先我们要在keycloak中配置用户、权限、授权流程等信息，都保存在<code class="language-plaintext highlighter-rouge">sivalabs-realm.json</code>中，可以在控制台查看。
<img src="/assets/img/oauth2/realm.png" alt="" /><br />
realm就是keycloak中的命名空间，用户、角色、权限都属于单独的realm，这里admin用户属于master，而sivalabs是工程realm。<br />
<img src="/assets/img/oauth2/users.png" alt="" /><br />
sivalabs中的用户有两个 <code class="language-plaintext highlighter-rouge">siva/siva1234</code> 和 <code class="language-plaintext highlighter-rouge">prasad/prasad1234</code>，在应用中登录时都可以使用
<img src="/assets/img/oauth2/roles.png" alt="" /><br />
<code class="language-plaintext highlighter-rouge">ROLE_</code>开头的角色都是自定义的，在应用中会用到，后面三个是系统权限，无需关注
<img src="/assets/img/oauth2/clients.png" alt="" /><br />
重头戏来了，这就是OAuth2规范中的Client，黄框中的两个应用是我们自定义的，其他的是系统相关的<br />
来看看Client中最重要的一部分配置
<img src="/assets/img/oauth2/client-config.png" alt="" /><br />
这里是archival-service的Client配置，采用的流程是<code class="language-plaintext highlighter-rouge">客户端凭证</code></p>
<ul>
  <li>Client authentication选中表示Client是服务端应用，置空表示客户端应用</li>
  <li>authorization即授权，不在本文讨论范畴，这里关闭即可</li>
  <li>Authentication flow，选择Client支持的认证流程
    <ul>
      <li>Standard flow即授权码模式</li>
      <li>Service accounts roles即客户端凭证</li>
    </ul>
  </li>
</ul>

<p>授权码模式需要设置<code class="language-plaintext highlighter-rouge">redirect uri</code>，即登录完成跳转回的页面。<br />
还可以设置<code class="language-plaintext highlighter-rouge">log out redirect uri</code>，即退出后跳转的页面，前端跳转到其他页面会报错。
<img src="/assets/img/oauth2/redirect-url-config.png" alt="" /><br />
还有Client凭证，在认证流程中会用到
<img src="/assets/img/oauth2/credentials.png" alt="" /><br />
其余配置在用到时再讲解。</p>

<h3 id="resource-server配置">Resource server配置</h3>
<p>我们来看看messages-service作为Resource server是怎么配置的</p>
<div class="language-properties highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># application.properties
</span><span class="py">spring.application.name</span><span class="p">=</span><span class="s">messages-service</span>
<span class="py">server.port</span><span class="p">=</span><span class="s">8181</span>
<span class="py">OAUTH_SERVER</span><span class="p">=</span><span class="s">http://localhost:9191/realms/sivalabs</span>
<span class="py">spring.security.oauth2.resourceserver.jwt.issuer-uri</span><span class="p">=</span><span class="s">${OAUTH_SERVER}</span>
</code></pre></div></div>
<p>由于Resource server不需要在Authorization server上注册，只需要配置issuer-uri，OAuth2客户端在地址后拼接<code class="language-plaintext highlighter-rouge">/.well-known/openid-configuration</code>能得到重要的api端口，后面我们会用到
<img src="/assets/img/oauth2/openid-config.png" alt="" /></p>

<p>下面是核心的配置逻辑</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Configuration</span>
<span class="nd">@EnableWebSecurity</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">SecurityConfig</span> <span class="o">{</span>

    <span class="nd">@Bean</span>
    <span class="nc">SecurityFilterChain</span> <span class="nf">securityFilterChain</span><span class="o">(</span><span class="nc">HttpSecurity</span> <span class="n">http</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">Exception</span> <span class="o">{</span>
        <span class="n">http</span>
            <span class="o">.</span><span class="na">authorizeHttpRequests</span><span class="o">(</span><span class="n">c</span> <span class="o">-&gt;</span>
                <span class="n">c</span>
                    <span class="o">.</span><span class="na">requestMatchers</span><span class="o">(</span><span class="nc">HttpMethod</span><span class="o">.</span><span class="na">GET</span><span class="o">,</span> <span class="s">"/api/messages"</span><span class="o">).</span><span class="na">permitAll</span><span class="o">()</span>
                    <span class="o">.</span><span class="na">requestMatchers</span><span class="o">(</span><span class="nc">HttpMethod</span><span class="o">.</span><span class="na">POST</span><span class="o">,</span> <span class="s">"/api/messages/archive"</span><span class="o">).</span><span class="na">hasAnyRole</span><span class="o">(</span><span class="s">"ADMIN"</span><span class="o">,</span> <span class="s">"ADMIN_JOB"</span><span class="o">)</span>
                    <span class="o">.</span><span class="na">anyRequest</span><span class="o">().</span><span class="na">authenticated</span><span class="o">()</span>
            <span class="o">)</span>
            <span class="o">.</span><span class="na">sessionManagement</span><span class="o">(</span><span class="n">c</span> <span class="o">-&gt;</span> <span class="n">c</span><span class="o">.</span><span class="na">sessionCreationPolicy</span><span class="o">(</span><span class="nc">SessionCreationPolicy</span><span class="o">.</span><span class="na">STATELESS</span><span class="o">))</span>
            <span class="o">.</span><span class="na">cors</span><span class="o">(</span><span class="nl">CorsConfigurer:</span><span class="o">:</span><span class="n">disable</span><span class="o">)</span>
            <span class="o">.</span><span class="na">csrf</span><span class="o">(</span><span class="nl">CsrfConfigurer:</span><span class="o">:</span><span class="n">disable</span><span class="o">)</span>
            <span class="o">.</span><span class="na">oauth2ResourceServer</span><span class="o">(</span><span class="n">oauth2</span> <span class="o">-&gt;</span>
                <span class="n">oauth2</span><span class="o">.</span><span class="na">jwt</span><span class="o">(</span><span class="n">jwt</span> <span class="o">-&gt;</span> <span class="n">jwt</span><span class="o">.</span><span class="na">jwtAuthenticationConverter</span><span class="o">(</span><span class="k">new</span> <span class="nc">KeycloakJwtAuthenticationConverter</span><span class="o">()))</span>
            <span class="o">);</span>
        <span class="k">return</span> <span class="n">http</span><span class="o">.</span><span class="na">build</span><span class="o">();</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<ul>
  <li>权限控制，GET的<code class="language-plaintext highlighter-rouge">/api/messages</code>方法没有限制，POST的<code class="language-plaintext highlighter-rouge">/api/messages/archive</code>方法只有ADMIN、ADMIN_JOB两个角色能访问，其余接口登录后才能访问</li>
  <li>session设置为不创建，因为服务是纯后端</li>
  <li>cors和csrf为网络攻击相关设置</li>
  <li>KeycloakJwtAuthenticationConverter将自定义的<code class="language-plaintext highlighter-rouge">ROLE_</code>开头的角色添加到权限里</li>
</ul>

<h3 id="client配置">Client配置</h3>
<div class="language-properties highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="py">OAUTH_SERVER</span><span class="p">=</span><span class="s">http://localhost:9191/realms/sivalabs</span>

<span class="py">spring.security.oauth2.client.registration.messages-webapp.client-id</span><span class="p">=</span><span class="s">messages-webapp</span>
<span class="py">spring.security.oauth2.client.registration.messages-webapp.client-secret</span><span class="p">=</span><span class="s">qVcg0foCUNyYbgF0Sg52zeIhLYyOwXpQ</span>
<span class="py">spring.security.oauth2.client.registration.messages-webapp.authorization-grant-type</span><span class="p">=</span><span class="s">authorization_code</span>
<span class="py">spring.security.oauth2.client.registration.messages-webapp.scope</span><span class="p">=</span><span class="s">openid, profile</span>
<span class="py">spring.security.oauth2.client.registration.messages-webapp.redirect-uri</span><span class="p">=</span><span class="s">{baseUrl}/login/oauth2/code/messages-webapp</span>

<span class="py">spring.security.oauth2.client.provider.messages-webapp.issuer-uri</span><span class="p">=</span><span class="s">${OAUTH_SERVER}</span>
</code></pre></div></div>
<p>messages-webapp作为Client需要配置registration和provider两块
registration</p>
<ul>
  <li>client id和secret组成凭证，从keycloak配置中获取</li>
  <li>authorization-grant-type即认证流程，必须在keycloak中勾选，这里是授权码模式</li>
  <li>scope即认证结果附加的信息，profile为基本信息</li>
  <li>redirect-uri即跳转地址，必须和keycloak上配置一致
provider即Authorization Server的信息，这里就是keycloak的realm地址</li>
</ul>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Configuration</span>
<span class="nd">@EnableWebSecurity</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">SecurityConfig</span> <span class="o">{</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">ClientRegistrationRepository</span> <span class="n">clientRegistrationRepository</span><span class="o">;</span>

    <span class="kd">public</span> <span class="nf">SecurityConfig</span><span class="o">(</span><span class="nc">ClientRegistrationRepository</span> <span class="n">clientRegistrationRepository</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">this</span><span class="o">.</span><span class="na">clientRegistrationRepository</span> <span class="o">=</span> <span class="n">clientRegistrationRepository</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="nd">@Bean</span>
    <span class="nc">SecurityFilterChain</span> <span class="nf">securityFilterChain</span><span class="o">(</span><span class="nc">HttpSecurity</span> <span class="n">http</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">Exception</span> <span class="o">{</span>
        <span class="n">http</span>
            <span class="o">.</span><span class="na">authorizeHttpRequests</span><span class="o">(</span><span class="n">c</span> <span class="o">-&gt;</span>
                    <span class="n">c</span><span class="o">.</span><span class="na">requestMatchers</span><span class="o">(</span><span class="s">"/"</span><span class="o">).</span><span class="na">permitAll</span><span class="o">()</span>
                    <span class="o">.</span><span class="na">anyRequest</span><span class="o">().</span><span class="na">authenticated</span><span class="o">()</span>
            <span class="o">)</span>
            <span class="o">.</span><span class="na">cors</span><span class="o">(</span><span class="nl">CorsConfigurer:</span><span class="o">:</span><span class="n">disable</span><span class="o">)</span>
            <span class="o">.</span><span class="na">csrf</span><span class="o">(</span><span class="nl">CsrfConfigurer:</span><span class="o">:</span><span class="n">disable</span><span class="o">)</span>
            <span class="o">.</span><span class="na">oauth2Login</span><span class="o">(</span><span class="n">oauth2</span> <span class="o">-&gt;</span>
                <span class="n">oauth2</span><span class="o">.</span><span class="na">userInfoEndpoint</span><span class="o">(</span><span class="n">userInfo</span> <span class="o">-&gt;</span> <span class="n">userInfo</span>
                        <span class="o">.</span><span class="na">userAuthoritiesMapper</span><span class="o">(</span><span class="k">new</span> <span class="nc">KeycloakAuthoritiesMapper</span><span class="o">())))</span>
            <span class="o">.</span><span class="na">logout</span><span class="o">(</span><span class="n">logout</span> <span class="o">-&gt;</span> <span class="n">logout</span>
                    <span class="o">.</span><span class="na">clearAuthentication</span><span class="o">(</span><span class="kc">true</span><span class="o">)</span>
                    <span class="o">.</span><span class="na">invalidateHttpSession</span><span class="o">(</span><span class="kc">true</span><span class="o">)</span>
                    <span class="o">.</span><span class="na">logoutSuccessHandler</span><span class="o">(</span><span class="n">oidcLogoutSuccessHandler</span><span class="o">())</span>
            <span class="o">);</span>
        <span class="k">return</span> <span class="n">http</span><span class="o">.</span><span class="na">build</span><span class="o">();</span>
    <span class="o">}</span>

    <span class="kd">private</span> <span class="nc">LogoutSuccessHandler</span> <span class="nf">oidcLogoutSuccessHandler</span><span class="o">()</span> <span class="o">{</span>
        <span class="nc">OidcClientInitiatedLogoutSuccessHandler</span> <span class="n">oidcLogoutSuccessHandler</span> <span class="o">=</span>
                <span class="k">new</span> <span class="nf">OidcClientInitiatedLogoutSuccessHandler</span><span class="o">(</span><span class="k">this</span><span class="o">.</span><span class="na">clientRegistrationRepository</span><span class="o">);</span>
        <span class="n">oidcLogoutSuccessHandler</span><span class="o">.</span><span class="na">setPostLogoutRedirectUri</span><span class="o">(</span><span class="s">"{baseUrl}/"</span><span class="o">);</span>
        <span class="k">return</span> <span class="n">oidcLogoutSuccessHandler</span><span class="o">;</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<ul>
  <li>除了访问根路径<code class="language-plaintext highlighter-rouge">/</code>都需要认证</li>
  <li>KeycloakAuthoritiesMapper同上，也是将<code class="language-plaintext highlighter-rouge">ROLE_</code>开头的角色添加到权限中</li>
  <li>设置退出时清空认证和session，并设置跳转地址</li>
</ul>

<h4 id="项目启动">项目启动</h4>
<p>启动完keycloak，文件导入完即配置完成，不需要手动修改，然后分别启动三个服务即可。<br />
前端入口为<code class="language-plaintext highlighter-rouge">http://localhost:8080</code>
<img src="/assets/img/oauth2/index.png" alt="" /><br />
初始登录时未认证，只能查看消息列表，不能新增或归档，右上角有登录按钮。</p>

<h2 id="授权码模式">授权码模式</h2>
<h3 id="登录">登录</h3>
<p>我们进入首页后点击右上角登录，看看每一步都发生了什么</p>
<ul>
  <li>点击登录按钮，对应请求为<code class="language-plaintext highlighter-rouge">http://localhost:8080/oauth2/authorization/messages-webapp</code> ，地址是OAuth2框架生成的，未授权时会跳转到登录页面<br />
可以看到跳转地址为<code class="language-plaintext highlighter-rouge">http://localhost:9191/realms/sivalabs/protocol/openid-connect/auth</code> ，为Authorization Server获取auth_code的端点，参数为
    <ul>
      <li>response_type=code，auth_code类型，还有其他的</li>
      <li>client_id=messages-webapp</li>
      <li>scope=openid profile，除了profile基本信息，还加上openid获取其他用户信息</li>
      <li>redirect_uri=http://localhost:8080/login/oauth2/code/messages-webapp 跳转回的地址
<img src="/assets/img/authorization-code/first-jump.png" alt="" /></li>
    </ul>
  </li>
  <li>在登录页面，点击登录按钮，请求为<code class="language-plaintext highlighter-rouge">http://localhost:9191/realms/sivalabs/login-actions/authenticate</code>  ，<br />
重定向到<code class="language-plaintext highlighter-rouge">http://localhost:8080/login/oauth2/code/messages-webapp</code> 主要参数为
    <ul>
      <li>code=115b0a30-855f-4208-943f-0157a2b83c75.f9feeaa5-8269-49fc-9074-4e1a4739d67e.27a26554-da99-417c-a775-bf6559c98f02</li>
      <li>可以看到跳转回了应用，并返回auth_code，后续就需要用auth_code来申请access code
<img src="/assets/img/authorization-code/login-page.png" alt="" /><br />
 <img src="/assets/img/authorization-code/code-returned.png" alt="" /></li>
    </ul>
  </li>
  <li>现在又跳转回了应用，并且得到了auth_code，这时spring security OAuth2就会用auth_code请求获取access code的端点，我们debug<code class="language-plaintext highlighter-rouge">org.springframework.security.oauth2.client.endpoint.DefaultAuthorizationCodeTokenResponseClient#getTokenResponse</code>这个方法
    <ul>
      <li>请求包含code，即前面得到的auth_code
<img src="/assets/img/authorization-code/access-code-request.png" alt="" /></li>
      <li>响应包含access code和refresh code，refresh code过期时间长，可以通过它重新获取access code，它们的格式都是JWT
<img src="/assets/img/authorization-code/access-code-response.png" alt="" /></li>
      <li>后续Spring Security OAuth2就会管理refresh code和access code，按需进行刷新</li>
    </ul>
  </li>
  <li>请求结束后，跳转回登录前的页面，这时已经完成认证流程，可以访问Resource Server上的资源了
<img src="/assets/img/authorization-code/redirect-after-authenticated.png" alt="" /></li>
</ul>

<pre><code class="language-mermaid">sequenceDiagram
    participant c as Client
    participant a as Authorization Server
    participant r as Resource Server

    c-&gt;&gt;a: 跳转登录页面
    a-&gt;&gt;c: 登录成功，返回auth code
    c-&gt;&gt;a: 请求refresh/access token
    c-&gt;&gt;r: 使用access token访问资源

</code></pre>

<h3 id="退出">退出</h3>
<p>登录完就可以看到新增和归档消息的入口了，右上角也变为了logout。<br />
<img src="/assets/img/authorization-code/authenticated.png" alt="" />
来看看登录后的session<br />
<img src="/assets/img/authorization-code/session.png" alt="" />
可以看到session的属性中包含了<code class="language-plaintext highlighter-rouge">SPRING_SECURITY_CONTEXT</code>，其中包含用户信息、权限和登录状态</p>
<ul>
  <li>我们点击logout，请求为<code class="language-plaintext highlighter-rouge">http://localhost:8080/logout</code> ，这个也是Spring Security创建的端点，它的操作有
    <ul>
      <li>将Spring Session清空，JSESSIONID无效化</li>
      <li>清空认证状态</li>
      <li>最后调用Authorization Server的退出端点<code class="language-plaintext highlighter-rouge">http://localhost:9191/realms/sivalabs/protocol/openid-connect/logout</code>
<img src="/assets/img/authorization-code/logout_url.png" alt="" /></li>
    </ul>
  </li>
  <li>Authorization Server退出端点会清空keycloak的session和浏览器相关Cookie，然后跳转回页面<br />
<img src="/assets/img/authorization-code/keycloak_logout.png" alt="" /></li>
  <li>回到页面后原JSESSIONID已经无效，服务端分配了新ID
<img src="/assets/img/authorization-code/session-changed.png" alt="" /></li>
</ul>

<pre><code class="language-mermaid">sequenceDiagram
    participant c as Client
    participant a as Authorization Server

    Note over c: 清空Spring Session
    c-&gt;&gt;a: 访问退出端点
    Note over a: 清空keycloak Session和相关Cookie
    a-&gt;&gt;c: 跳转回页面
    Note over c: 分配新JSESSIONID

</code></pre>

<h3 id="访问">访问</h3>
<p>下面通过messages-webapp访问messages-service的新增消息接口演示授权码的应用。</p>
<ul>
  <li>请求打到messages-webapp对应接口，首先通过<code class="language-plaintext highlighter-rouge">OAuth2AuthorizedClientService.loadAuthorizedClient</code>获取<code class="language-plaintext highlighter-rouge">OAuth2AuthorizedClient</code>，后面的类中管理着refresh token和access token
<img src="/assets/img/authorization-code/get-access-token.png" alt="" /></li>
  <li>将access token置入header<code class="language-plaintext highlighter-rouge">Authorization</code>中，就能访问到messages-service的新增消息接口
<img src="/assets/img/authorization-code/session-changed.png" alt="" /></li>
</ul>

<h2 id="客户端凭证">客户端凭证</h2>
<p>客户端凭证不需要User的介入，适合服务端之间的通信，我们以archival-service调用messages-service的归档接口为例演示。</p>
<ul>
  <li>archival-service想调用messages-service的<code class="language-plaintext highlighter-rouge">/api/messages/archive</code>接口，首先需要获取access token</li>
  <li>拦截<code class="language-plaintext highlighter-rouge">DefaultClientCredentialsTokenResponseClient#getTokenResponse</code>方法，可以访问了Authorization Server的端点，需要注意的是header中的<code class="language-plaintext highlighter-rouge">Authorization</code>，包含了ClientId/ClientSecret的base64编码
<img src="/assets/img/client-credential/get-access-code.png" alt="" /></li>
  <li>然后archival-service可以使用结果中的access token访问归档接口</li>
</ul>

<h2 id="sso">SSO</h2>
<p>做完大量铺垫，终于来到正题了，SSO(Single Sign-On/Off)即单点登录(退出)可以通过keycloak+Spring Security OAuth2来实现</p>
<ul>
  <li>首先我们新增一个服务messages-apptwo，代码和messages-webapp相同，区别如下
    <ul>
      <li>端口设为8585，messages-webapp为8080</li>
      <li>Client仍使用messages-webapp，但是跳转地址加上了apptwo的地址
<img src="/assets/img/sso/client-config.png" alt="" /></li>
    </ul>
  </li>
  <li>为了让webapp和apptwo两个服务共享session，我们引入redis管理session
    <ul>
      <li>首先在docker compose文件中加入redis服务，端口为6379</li>
      <li>在两个服务中加入<code class="language-plaintext highlighter-rouge">spring-session-data-redis</code>和<code class="language-plaintext highlighter-rouge">spring-boot-starter-data-redis</code>依赖</li>
      <li>在两个服务中加入配置
        <div class="language-properties highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="py">spring.redis.host</span><span class="p">=</span><span class="s">localhost</span>
<span class="py">spring.redis.port</span><span class="p">=</span><span class="s">6379</span>
<span class="py">spring.session.store-type</span><span class="p">=</span><span class="s">redis</span>
</code></pre></div>        </div>
      </li>
    </ul>
  </li>
</ul>

<h3 id="同域名登录退出">同域名登录/退出</h3>
<ul>
  <li>在同一浏览器中访问<code class="language-plaintext highlighter-rouge">http://localhost:8080</code>和<code class="language-plaintext highlighter-rouge">http://localhost:8585</code>，都是未登录状态，cookie中<code class="language-plaintext highlighter-rouge">SESSION=ODdlZmJiN2UtMDNmYy00ZGVkLWE3OWEtNmQxZTdkZjdiZDc3</code>，base64解码为<code class="language-plaintext highlighter-rouge">87efbb7e-03fc-4ded-a79a-6d1e7df7bd77</code></li>
  <li>在redis-cli中查找全部hash，能找到对应session
<img src="/assets/img/sso/session-not-login.png" alt="" /></li>
  <li>在<code class="language-plaintext highlighter-rouge">http://localhost:8080</code>上登录，仍是标准的授权码模式流程，登录成功后cookie中<code class="language-plaintext highlighter-rouge">SESSION=MDgwZmFiNmQtMDVjOS00NGZhLTg5ZjUtNmFmZGZjZDYxNTFk</code>，base64解码为<code class="language-plaintext highlighter-rouge">080fab6d-05c9-44fa-89f5-6afdfcd6151d</code>，在redis-cli中可以查询到</li>
  <li>切换到<code class="language-plaintext highlighter-rouge">http://localhost:8585</code>，刷新页面，由于请求头带有<code class="language-plaintext highlighter-rouge">SESSION=MDgwZmFiNmQtMDVjOS00NGZhLTg5ZjUtNmFmZGZjZDYxNTFk</code>，直接获取到已登录session，没有走认证流程</li>
  <li>从页面<code class="language-plaintext highlighter-rouge">http://localhost:8585</code>退出，流程和之前并没有区别，返回新的header<code class="language-plaintext highlighter-rouge">SESSION=YzdlNWQyZDEtMmViNC00NTJhLWEzOGQtNTI0MzVkZGM1OGYx</code></li>
  <li>刷新页面<code class="language-plaintext highlighter-rouge">http://localhost:8080</code>，请求头为<code class="language-plaintext highlighter-rouge">SESSION=YzdlNWQyZDEtMmViNC00NTJhLWEzOGQtNTI0MzVkZGM1OGYx</code>，所以是未登录状态</li>
</ul>

<h3 id="session管理">session管理</h3>
<p>上述的同域名SSO中存在两个session</p>
<ul>
  <li>未登录时，只有spring维护在redis中的session，对应cookie<code class="language-plaintext highlighter-rouge">SESSION</code>，可以在redis-cli中查到</li>
  <li>登录后，有两个session
    <ul>
      <li>spring维护在redis中的session，包含<code class="language-plaintext highlighter-rouge">SPRING_SECURITY_CONTEXT</code>，即用户信息、权限等属性</li>
      <li>keycloak中的session，即用户通过授权码模式登录的状态
<img src="/assets/img/sso/keycloak-session.png" alt="" /></li>
    </ul>
  </li>
  <li>从页面退出会无效旧的cookie<code class="language-plaintext highlighter-rouge">SESSION</code>，返回一个新的未登录的cookie，并且会退出keycloak的session</li>
  <li>如果只退出了keycloak的session，spring session仍是登录状态，但这时已经无法访问Resource Server的资源了(会返回401)，需要主动退出登录或清理cookie</li>
</ul>]]></content><author><name>cafewang</name></author><category term="sso" /><category term="security" /><summary type="html"><![CDATA[说出来可能让人意外，登录系统可能是最复杂的系统之一，并不是看起来的注册、登录、退出这么简单。 列举一下登录系统中涉及的重要问题： 密码在前后端怎样存储和验证最安全？ 网站的密码复杂度要求高吗？ 更改密码时怎样退出其他登录的设备？ 怎么保持认证状态，使用session还是JWT？ session时长设置成多久比较合适？ 多因素认证怎么实现？]]></summary></entry><entry><title type="html">JVM内存管理</title><link href="https://cafewang.github.io//jvm-in-depth/jvm-memory-management" rel="alternate" type="text/html" title="JVM内存管理" /><published>2025-04-11T00:39:00+00:00</published><updated>2025-04-11T00:39:00+00:00</updated><id>https://cafewang.github.io//jvm-in-depth/jvm-memeory-management</id><content type="html" xml:base="https://cafewang.github.io//jvm-in-depth/jvm-memory-management"><![CDATA[<p>要深入理解jvm内存管理，我们需要从两方面来看，一是jvm规范中对内存管理的定义，二是hotspot对jvm规范的具体实现。</p>

<h2 id="jvm运行时区域">jvm运行时区域</h2>
<p>先看两张总览图
java1.7<br />
<img src="/assets/img/java-runtime-data-areas-jdk1.7.png" alt="" />
java1.8<br />
<img src="/assets/img/java-runtime-data-areas-jdk1.8.png" alt="" /></p>

<p>jvm运行时区域分为两部分：</p>
<ul>
  <li>线程共享区域
    <ul>
      <li>堆(heap)，就是绝大部分对象所处的位置，因为java1.7默认开启了逃逸分析，没有被外部引用的对象可以在栈上分配，而后续的<code class="language-plaintext highlighter-rouge">垃圾收集器</code>的讨论也主要是围绕堆进行</li>
      <li>方法区(method area)，运行时类信息存储的位置，包含运行时常量池(加载类文件中的常量池)、字段和方法信息、字节码信息等。</li>
    </ul>
  </li>
  <li>线程独有区域
    <ul>
      <li>pc寄存器(program counter register)，类似CPU中的pc，记录字节码的运行位置</li>
      <li>虚拟机栈(virtual machine stack)，java方法运行产生的栈，栈帧将在后面详细讨论</li>
      <li>本地方法栈(native method stack)，native方法运行产生的栈</li>
    </ul>
  </li>
</ul>

<p>版本区别：1.7中，方法区的实现是永久代(permGeneration)，存在于java运行时区域中。<br />
1.8中，方法区的实现是元空间(metaSpace)，在本地内存中，所以运行时常量池也在元空间中。</p>

<h3 id="栈帧">栈帧</h3>
<ul>
  <li>局部变量表(local variables)，即方法中定义的参数、局部变量，值得注意的是返回地址也在局部变量表中</li>
  <li>操作数栈(operand stacks)，保存字节码执行过程中的中间结果，最大深度在<code class="language-plaintext highlighter-rouge">编译期间</code>就确定了</li>
  <li>动态链接(dynamic linking),当前方法的符号引用解析成的动态链接，简单来说就是方法在运行时区域中的位置</li>
</ul>

<p>运行时区域的划分介绍完了，下面开始详细介绍jvm的内存管理工具-垃圾收集器。</p>

<h2 id="垃圾收集器">垃圾收集器</h2>
<p>垃圾收集器(garbage collector)的命名其实有些歧义，更准确的说法应该是内存管理器(memory maneger)，因为垃圾收集器的职责不仅是回收不可达对象，还负责堆空间的划分和管理、堆内存的申请和回收。<br />
垃圾收集器也是有代际划分的，我们重点介绍1.7和1.7之后推荐使用的垃圾收集器。后文简称收集器。
首先介绍一下分代收集(generational collection)，收集器中除了ZGC都是分代收集的。</p>
<ul>
  <li>新生代(young generation)，短期存活对象所处的区域，分为如下两块
    <ul>
      <li>eden，大部分对象最开始都分配在eden区，相当于年龄为0</li>
      <li>survivor，分为两块，from区和to区，to区平时为空，仅在进行minor GC或full GC时，保存eden和from区的存活对象，然后from区和to区交换</li>
    </ul>
  </li>
  <li>老年代(old generation)，保存年龄超过阈值（阈值由算法动态计算，或手动指定）</li>
</ul>

<p>可以这样理解，对象按年龄(即对象被移动的次数，因为一般情况下，无论minor GC或full GC都会移动存活对象，CMS除外)分为3个区域，刚创建的对象在eden，<code class="language-plaintext highlighter-rouge">中年对象</code>在survivor，最后进入老年代。<br />
一般来说，新生代满的时候会触发minor GC，使用指定的新生代收集算法清理，完成后eden区会清空，部分对象会晋升到老年代。<br />
老年代或永久代(1.7及之前版本的方法区)满时，会触发full GC，一般会先用新生代收集算法进行minor GC，然后用老年代收集算法清理老年代和永久代。<br />
如果涉及压缩，每一代会单独进行。
如果minor GC晋升的对象无法被老年代容纳，会以老年代收集算法清理整个堆(CMS除外，它只能清理老生代)。</p>

<h3 id="serial">Serial</h3>
<p>serial收集器可以收集新生代和老年代，以<code class="language-plaintext highlighter-rouge">stop-the-world</code>的方式进行，即暂停所有工作线程。</p>

<h4 id="新生代收集">新生代收集</h4>
<p><img src="/assets/img/collector/serial-young-generation.png" alt="" /></p>
<ul>
  <li>eden区对象被移到to区，除了部分较大对象直接移到老年代</li>
  <li>from区对象根据年龄移到to区或老年代</li>
  <li>to区满时，后续eden和from区对象都移到老年代</li>
</ul>

<h4 id="老年代收集">老年代收集</h4>
<p><img src="/assets/img/collector/serial-old.png" alt="" /><br />
老年代收集算法称为<code class="language-plaintext highlighter-rouge">mark-sweep-compact</code></p>
<ul>
  <li>mark即标记存活对象，时长和存活对象数量有关
    <ul>
      <li>jvm从GC root向下扫描出所有存活对象，典型的GC root有
        <ul>
          <li>当前方法的参数和局部变量</li>
          <li>活跃线程</li>
          <li>已加载类的字段</li>
          <li>JNI引用</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>sweep即清理垃圾对象，实际上jvm并不执行<code class="language-plaintext highlighter-rouge">清理内存</code>的工作，只是在不同代中标记出垃圾对象</li>
  <li>compact即将存活对象移动到老年代(永久代类似)的一端，方便内存分配</li>
</ul>

<p>Serial收集器主要用于运行在客户端且对pause time要求不高的场景，一般不会用在服务端。</p>

<h3 id="parallel">Parallel</h3>
<p>parallel收集器又被称为throughput(吞吐量)收集器，注意在收集器中parallel和concurrent的区别，parallel是指多个回收线程同时执行，而concurrent是指回收线程和工作线程同时执行。</p>

<h4 id="新生代收集-1">新生代收集</h4>
<p><img src="/assets/img/collector/serial-old.png" alt="" />
仍使用和serial相同的新生代收集算法，只是使用多线程执行<code class="language-plaintext highlighter-rouge">stop-the-world</code>的操作，减少pause time，从而增加throughput。</p>

<h4 id="老年代收集-1">老年代收集</h4>
<p>默认仍使用serial收集，需要设置<code class="language-plaintext highlighter-rouge">-XX:+UseParallelGC</code>执行parallel老年代收集。收集分为3步，首先将老年代(永久代)分为固定大小的区域。</p>
<ul>
  <li>marking阶段，给每个回收线程分配一些初始的可达对象，并行查找所有的可达对象，并分区域记录可达对象的信息。</li>
  <li>summary阶段，单线程执行，将区域从左到右扫描，从中间一分为二，左边的区域包含大量可达对象，所以不做清理，称为dense prefix，右侧区域会执行清理，记录清理后的地址区间，如图中的区域3和4，对应下方的绿色区间</li>
  <li>compaction阶段，多线程移动summary时记录的区域，最终压缩为左端填满对象，右端为空的结构。</li>
</ul>

<script defer="" type="text/tikz" data-tikz-libraries="positioning, decorations.pathreplacing">
\definecolor{ashgrey}{rgb}{0.7, 0.75, 0.71}
\definecolor{inchworm}{rgb}{0.7, 0.93, 0.36}
\begin{tikzpicture}[
every node/.style={node font=\large\bfseries, line width=1.5pt, minimum size=12mm},
gray node/.style={fill=ashgrey},
worm node/.style={fill=inchworm},
]

\foreach \x/\t in {0/gray node,1/gray node,2/gray node,3/gray node,4/gray node,5/,6/,7/} {
  \pgfmathsetmacro{\next}{\x+1};
  \draw[\t] (\x,0) rectangle (\next,1) node[midway] (a\x) {};
  \node[anchor=south east, font=\small, minimum size=0] at (\next,0) {\x};
}

\draw [decorate, decoration = {brace,mirror}, thick] (0,-.2) --  (3,-.2) 
node[pos=0.5, anchor=north] {dense prefix};

\foreach \x/\t in {0/gray node,1/gray node,2/gray node,3/,4/,5/,6/,7/} {
  \pgfmathsetmacro{\next}{\x+1};
  \draw[\t] (\x,-3) rectangle (\next,-2) node[midway] (b\x) {};
  \node[anchor=south east, font=\small, minimum size=0] at (\next,-3) {\x};
}

\draw [decorate, decoration = {brace,mirror}, thick] (0,-3.2) --  (3,-3.2) 
node[pos=0.5, anchor=north] {dense prefix};

\draw[worm node] (3,-3) rectangle (3.3,-2) node[midway] (c0) {};
\draw[worm node] (3.3,-3) rectangle (3.5,-2) node[midway] (c1) {};

\draw[->] (a3) -- (c0);
\draw[->] (a4) -- (c1);
\end{tikzpicture}
</script>

<p>parallel收集器适用于多核且对吞吐量要求高的场景。</p>

<h3 id="cmsconcurrent-mark-sweep">CMS(Concurrent Mark Sweep)</h3>
<p>CMS，即<code class="language-plaintext highlighter-rouge">并发标记清除</code>收集器，又称为low latency(低延迟)收集器，是为解决老年代长暂停(long pause)设计的收集器。</p>

<h4 id="新生代收集-2">新生代收集</h4>
<p>同parallel</p>

<h4 id="老年代收集-2">老年代收集</h4>
<p><img src="/assets/img/collector/cms-old.png" alt="" />
如名称所示，CMS包含concurrent mark和concurrent sweep两个并发阶段，还有initial mark和remark两个<code class="language-plaintext highlighter-rouge">stop-the-world</code>阶段</p>
<ul>
  <li>initial mark，单线程标记应用代码直接可达的对象</li>
  <li>concurrent mark，在代码运行的同时，标记间接可达的对象</li>
  <li>remark，并行标记concurrent mark期间修改的对象，完成最终标记</li>
  <li>concurrent sweep，CMS在代码运行同时清理垃圾，但是不执行compact，而是使用free-list管理</li>
</ul>

<p>CMS的老年代收集<code class="language-plaintext highlighter-rouge">并不</code>在老年代满时触发</p>
<ul>
  <li>CMS根据垃圾收集的统计数据<code class="language-plaintext highlighter-rouge">定时</code>触发老年代收集</li>
  <li>CMS根据设定的老年代空间占用阈值触发收集</li>
  <li>如果老年代确实满了，CMS会回退使用<code class="language-plaintext highlighter-rouge">mark-sweep-compact</code>的<code class="language-plaintext highlighter-rouge">stop-the-world</code>算法，这样会更耗时</li>
</ul>

<h4 id="优缺点">优缺点</h4>
<p>CMS是更适用于服务端的收集器，能带来更低的pause time，但是：</p>
<ul>
  <li>由于没有compact，CMS中申请内存需要在free-list中查找，使得minor GC时间变长，因为minor GC对象进入老年代需要申请内存</li>
  <li>没有compact带来了内部碎片，CMS需要按需合并或拆分空闲区域</li>
  <li>CMS虽然保证标记所有垃圾对象，但是部分垃圾对象在下次收集才会清除，这些对象称为悬浮垃圾(floating garbage)</li>
</ul>

<h3 id="g1">G1</h3>
<p>G1收集器在1.7引入，1.9作为默认收集器，适用于多核大内存的场景，能够在尽力满足暂停时间目标的情况下保持高吞吐量。</p>

<h4 id="内存布局">内存布局</h4>
<p><img src="/assets/img/collector/g1-heap-layout.png" alt="" />
G1也是分代收集器，和其他收集器的不同点是，G1将内存划为大小相等的区域。</p>
<ul>
  <li>灰色区域表示未分配，不属于哪一代</li>
  <li>蓝色区域属于老年代，红色属于新生代</li>
  <li>新生代中S表示survivor，其他的表示eden</li>
  <li>比较特殊的是H标记(humongous)的老年代区域，由多个区域组合而成，用于放置大对象</li>
</ul>

<p>大部分对象初始分配在eden，大对象直接分配在H区。</p>

<h4 id="收集循环">收集循环</h4>
<p><img src="/assets/img/collector/g1-phrase.png" alt="" />
G1循环执行young-only和space-reclamation两个阶段。</p>
<ul>
  <li>young-only阶段执行若干次minor GC，填充老年代直到达到阈值，此时G1执行concurrent start阶段
    <ul>
      <li>concurrent start分为三个阶段外加一次minor GC
        <ul>
          <li>先执行concurrent marking，标记出在space-reclamation阶段保留的老年代对象，minor GC可以在marking结束前执行</li>
          <li>remark(stop-the-world)类似于CMS，处理全局引用信息、回收全空的区域</li>
          <li>remark和cleanup之间还会计算老年代收集用到的信息，和工作线程同时执行</li>
          <li>cleanup(stop-the-world)确认后续是否执行space-reclamation，是的话会跟一次Prepare mixed minor GC。</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>space-reclamation，如图中粉红点所示，执行多次mixed GC，包含新生代收集，以及部分老生代区域收集，直到G1判断释放的空间抵不上收集的代价，就会重启young-only。</li>
  <li>full GC，当收集GC信息过程中内存耗尽，G1会执行in place的stop the world full GC。</li>
</ul>

<h4 id="对象移动">对象移动</h4>
<p>一般对象移动时：</p>
<ul>
  <li>新生代对象根据年龄移动到survivor或老年代</li>
  <li>老年代对象移动到新区域中</li>
</ul>

<p>大对象只会在不可达时被回收，不会移动。</p>

<script defer="" type="text/tikz" data-tikz-libraries="positioning">
\definecolor{ashgrey}{rgb}{0.7, 0.75, 0.71}
\definecolor{inchworm}{rgb}{0.7, 0.93, 0.36}
\begin{tikzpicture}[
every node/.style={node font=\large\bfseries, line width=1.5pt, minimum size=12mm},
gray node/.style={fill=ashgrey},
worm node/.style={fill=inchworm},
]
\node at (-2,0.5) {young generation};
\foreach \x/\v/\t in {0/E/,1/F/,2/T/} {
  \pgfmathsetmacro{\next}{\x+1};
  \draw[\t] (\x,0) rectangle (\next,1) node[midway] (a\x) {\v};
}

\draw[->] (a0.north) .. controls ([yshift=15pt]a1.north) .. (a2.north);
\draw[->] (a1.south) .. controls ([yshift=-10pt]a1.south east) .. (a2.south);

\node at (-2,-1.5) {old generation};

\foreach \x/\l/\v/\t in {0/1//,1/1//,2/2/H/} {
  \pgfmathsetmacro{\next}{\x+\l};
  \draw[\t] (\x,-2) rectangle (\next,-1) node[midway] (b\x) {\v};
}

\draw[->] (a1) -- (b1);
\draw[->] (b0.north) .. controls ([yshift=5pt]b0.north east) .. ([xshift=-5pt]b1.north);
\end{tikzpicture}
</script>

<h4 id="回收集合">回收集合</h4>
<p>回收集合(Collection set)就是G1选出的需要回收的区域集合</p>
<ul>
  <li>young-only时，只有新生代和老年代的大对象区域会被选中</li>
  <li>space-reclamation时，新生代、大对象区域和部分老年代区域会被选中</li>
</ul>

<p>Remark阶段，G1会选中低占用率(空闲空间大)的区域。在Cleanup阶段前，会并发收集这些区域的GC信息。<br />
Cleanup时，根据efficiency(垃圾对象含量)排序，结合<code class="language-plaintext highlighter-rouge">占用率</code>和<code class="language-plaintext highlighter-rouge">efficiency</code>两个指标选择回收区域。</p>

<h4 id="大小调节">大小调节</h4>
<ul>
  <li>G1可以和其他收集器一样，设置最小和最大堆内存，还可以设置最小和最大空闲比例，以动态调节堆内存</li>
  <li>新生代大小可以设置最小和最大比例(新生代和老年代占比)，也可以直接设置最大最小值，G1会根据设置的最大pause time和pause time间隔在范围内调整</li>
</ul>

<h4 id="老年代回收细节">老年代回收细节</h4>
<p>space-reclamation被分为多个mixed GC，每次都会从回收集合中选择三部分的区域结合来收集</p>
<ul>
  <li>minimum set，即最少要回收的区域集合，大小为<code class="language-plaintext highlighter-rouge">回收集合的区域数量/mixed GC的设定次数</code>，显然设定次数越大，space-reclamation的时间越长</li>
  <li>额外集合，当G1预测最少集合执行完还有剩余时间时，会添加额外集合，直到预测到80%的时间会被消耗时停止添加</li>
  <li>可选集合，当最少集合和额外集合都清理完，剩余时间会添加可选集合来回收。</li>
</ul>

<script defer="" type="text/tikz" data-tikz-libraries="positioning,decorations.pathreplacing">
\definecolor{ashgrey}{rgb}{0.7, 0.75, 0.71}
\definecolor{inchworm}{rgb}{0.7, 0.93, 0.36}
\begin{tikzpicture}[
every node/.style={node font=\large\bfseries, line width=1.5pt, minimum size=12mm},
gray node/.style={fill=ashgrey},
worm node/.style={fill=inchworm},
]
\node at (-2,0.5) {old generation};
\foreach \x/\l/\v/\t in {0/6/min set/,6/2/addt set/,8/2/opt set/} {
  \pgfmathsetmacro{\next}{\x+\l};
  \draw[\t] (\x,0) rectangle (\next,1) node[midway] (a\x) {\v};
}

\draw [decorate, decoration = {brace}, thick] (0,1.2) --  (8,1.2) node[pos=0.5, anchor=south] {80\%};

\end{tikzpicture}
</script>

<h4 id="标记">标记</h4>
<p>G1的标记算法称为Snapshot-At-The-Beginning，望文生义，保存堆的所有可达对象的镜像，在后续收集中不会清除。<br />
由于对象在收集过程中可能变为垃圾，这些对象只能在后一轮收集中被清除。</p>

<h4 id="大对象">大对象</h4>
<p>大对象就是大小不小于区域的一半的对象，区域大小可以手动设置。</p>
<ul>
  <li>大对象会分配在老年代的连续区域中，左对齐区域头部，尾部剩余空间不会再分配</li>
  <li>大对象一般只在cleanup阶段或full GC时被回收。原始类型数组的大对象除外，会在每种GC时被回收。</li>
  <li>大对象分配会导致超过老年代回收阈值，触发concurrent-start。</li>
  <li>大对象永远不会被移动，过多的大对象会导致内部碎片增多，严重时会引发full GC或内存不足。</li>
</ul>

<h3 id="zgc">ZGC</h3>
<p>ZGC是可伸缩的低延迟收集器。为了达到低延迟，ZGC将耗时的GC操作和工作线程同时执行，当然同时会降低吞吐量。<br />
ZGC适用于追求低延迟的场景。pause time和内存大小无关，ZGC支持8MB到16TB内存。<br />
由于ZGC不是服务端场景的重点，我们只介绍几个重要的调优点</p>
<ul>
  <li>ZGC必须设置最大堆内存，因为类似CMS，concurrent收集过程中，工作线程还会申请新内存，需要保证有足够空闲空间</li>
  <li>GC线程数量需要设置适中，过大会占用太多吞吐量，过小收集速度太慢，会增大暂停时间</li>
</ul>

<h3 id="测试">测试</h3>
<p>我们来简单测试一下，对象gc前后地址变化，测试类在<code class="language-plaintext highlighter-rouge">ObjectAddressTest</code></p>
<ul>
  <li>用<code class="language-plaintext highlighter-rouge">jol-core</code>包获取对象地址(虚拟地址)</li>
  <li>通过<code class="language-plaintext highlighter-rouge">System.gc()</code>手动触发GC</li>
</ul>

<h3 id="对比">对比</h3>
<p>吞吐量和暂停时间是衡量收集器的两个重要指标</p>
<ul>
  <li>serial收集器适用于客户端小内存，对吞吐量和暂停时间要求不高的场景</li>
  <li>parallel收集器为高吞吐量设计，适用于多核场景</li>
  <li>CMS和G1都是适用于服务端多核大内存的应用，1.7及之后的版本推荐使用G1
    <ul>
      <li>G1粒度更细，每次清理部分老年代空间，而CMS是清理整个空间(老年代和永久代)</li>
      <li>G1的算法复杂度和对区域的管理会引入更多的CPU占用和内存消耗，压缩了吞吐量</li>
    </ul>
  </li>
  <li>ZGC为极致的暂停时间设计，进一步占用了吞吐量</li>
</ul>]]></content><author><name>cafewang</name></author><category term="jvm" /><category term="memory-management" /><summary type="html"><![CDATA[要深入理解jvm内存管理，我们需要从两方面来看，一是jvm规范中对内存管理的定义，二是hotspot对jvm规范的具体实现。]]></summary></entry><entry><title type="html">虚拟内存-TLB和swap</title><link href="https://cafewang.github.io//os/tlb_and_swap_space" rel="alternate" type="text/html" title="虚拟内存-TLB和swap" /><published>2025-04-05T11:45:00+00:00</published><updated>2025-04-05T11:45:00+00:00</updated><id>https://cafewang.github.io//os/virtual-memory-tlb-and-swap</id><content type="html" xml:base="https://cafewang.github.io//os/tlb_and_swap_space"><![CDATA[<p><a href="/os/pagination">上一章</a>我们讲过通过多级分页解决虚拟内存管理占用空间过大的问题，但是访问效率的问题还没有解决。关于效率的问题一般都可以通过空间来换时间，也就是使用缓存。用于分页的缓存硬件就称为TLB(Translation Lookaside Buffer)。</p>

<h2 id="tlb">TLB</h2>
<p>TLB就是用于VFN到PFN转换的缓存，配合CPU加速内存的访问。对于多级缓存，TLB存储dirNo+VFN到PFN的转换。<br />
除了PFN，TLB还会存储valid及其他必要字段。TLB一般能记录64、128或者更多条映射，TLB满之后就需要应用缓存置换策略来更新。<br />
加上TLB之后，访问内存变为如下步骤:</p>
<ul>
  <li>查询TLB是否包含dirNo_VFN的映射
    <ul>
      <li>存在，校验是否为valid
        <ul>
          <li>是，则获得PFN访问内存</li>
          <li>否，则抛出segmentation fault的异常</li>
        </ul>
      </li>
      <li>不存在，则通过多级页表查询到PFN和其他字段，回写TLB，然后重新访问</li>
    </ul>
  </li>
</ul>

<p>TLB查询不到称为TLB miss，每次TLB miss的成本是很高的，提高TLB的命中率和缓存置换策略息息相关，这个我们在下一节具体说。</p>

<h2 id="swap-space">swap space</h2>
<p>我们之前举的例子中，虚拟空间都不大于物理空间，但实际上是可以大于的，那多的部分存在哪里呢？<br />
没错，就放在磁盘中，以下图举例，内存总共分为4页，运行了3个进程。</p>

<script defer="" type="text/tikz" data-tikz-libraries="positioning">
\definecolor{ashgrey}{rgb}{0.7, 0.75, 0.71}
\definecolor{aliceblue}{rgb}{0.94, 0.97, 1.0}
\definecolor{coolgrey}{rgb}{0.55, 0.57, 0.67}
\definecolor{arylideyellow}{rgb}{0.91, 0.84, 0.42}
\begin{tikzpicture}[
every node/.style={node font=\large\bfseries, line width=1.5pt, minimum size=12mm},
grey node/.style={fill=coolgrey},
ash node/.style={fill=ashgrey},
black node/.style={fill=gray},
yellow node/.style={fill=arylideyellow},
]

\foreach \x/\v/\t in {0/Proc 0 VPN 0/ash node,2/Proc 1 VPN 0/grey node,4/Proc 1 VPN 1/grey node,6/Proc 2 VPN 0/yellow node} {
  \pgfmathsetmacro{\next}{int{\x+2}};
  \draw[\t] (\x,0) rectangle node[text width=2cm, align=center] (a\x) {\v} (\next,2);
}

\foreach \x/\v in {0/PFN0,2/PFN1,4/PFN2,6/PFN3} {
  \node[above=1pt of a\x] {\v};
}

\node[text width=2cm, align=center] at (-1,1) {Physical Memory};

\foreach \x/\v/\t in {0/Proc 0 VPN 1/ash node,2/Proc 0 VPN 2/ash node,4/Free/,6/Proc 3 VPN 0/black node} {
  \pgfmathsetmacro{\next}{int{\x+2}};
  \draw[\t] (\x,-3) rectangle node[text width=2cm, align=center] (b\x) {\v} (\next,-1);
}

\node[text width=2cm, align=center] at (-1,-2) {Swap Space};

\foreach \x/\v in {0/BLOCK0,2/BLOCK1,4/BLOCK2,6/BLOCK3} {
  \node[above=1pt of b\x] {\v};
}

\end{tikzpicture}
</script>

<p>如上图，swap space在磁盘中，由4块组成，每块大小等于一页。<br />
Proc0一页在内存中，两页在磁盘中；Proc1两页都在内存中；Proc2一页在内存中；Proc3全部在磁盘中，显然没有正在运行。</p>

<h2 id="页面置换策略">页面置换策略</h2>
<p>我们在页表中添加标志present，表示页面是否在内存中。如果读写时发现不在，就会触发page fault中断，然后中断处理器会找到一个空页，将页面从磁盘中加载到内存里，再重新读写。<br />
系统一般会启动后台线程，检查内存是否大于某临界值，是的话，就会将部分页存储到swap space，也就是磁盘中，判断哪些页面被置换的策略就称为<code class="language-plaintext highlighter-rouge">页面置换策略</code>。<br />
常见策略有</p>
<ul>
  <li>FIFO，即先进先出，置换最早被使用的页面</li>
  <li>random，随机置换</li>
  <li>LRU，置换最近未被使用的页面</li>
  <li>LFU，置换最少被使用的页面</li>
</ul>

<p>一般LRU的缓存命中率是比较高的，但是纯硬件实现过于复杂，下面介绍一种approximating LRU。<br />
在页表中加入3个字段</p>
<ul>
  <li>present，表示页面在内存(1)还是磁盘中(0)</li>
  <li>blockNo，如果在磁盘中，表示在磁盘中的起始位置</li>
  <li>use，在读写页面时被置为1</li>
</ul>

<p>这样寻找将被替换的内存页</p>
<ul>
  <li>遍历页表中present为1的页
    <ul>
      <li>如果use=1，将use置为0</li>
      <li>如果use=0，将该页和磁盘中要加载的页交换</li>
    </ul>
  </li>
</ul>

<h3 id="测试">测试</h3>
<p>克隆<code class="language-plaintext highlighter-rouge">https://github.com/cafewang/playground.git</code> 项目，运行测试类<code class="language-plaintext highlighter-rouge">ApproximatingLRUTest</code>。<br />
测试以字节数组代替物理内存和磁盘，假设物理内存为4页，磁盘为8页，测试场景如下</p>
<ul>
  <li>申请12个页面后，无法再申请新的页面，因为内存和磁盘都满了</li>
  <li>申请4个页面后，再申请一个页面，查看页表中有一个页面在磁盘中(present=0)</li>
  <li>申请5个页面，读取磁盘中的页面，检查页表确认该页面已经加载到内存中</li>
</ul>]]></content><author><name>cafewang</name></author><category term="os" /><category term="virtual-memory" /><category term="tlb" /><category term="swap-space" /><summary type="html"><![CDATA[上一章我们讲过通过多级分页解决虚拟内存管理占用空间过大的问题，但是访问效率的问题还没有解决。关于效率的问题一般都可以通过空间来换时间，也就是使用缓存。用于分页的缓存硬件就称为TLB(Translation Lookaside Buffer)。]]></summary></entry></feed>