<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <author>
    <name>Prayer</name>
  </author>
  <generator uri="https://hexo.io/">Hexo</generator>
  <id>https://evasnowind.github.io/</id>
  <link href="https://evasnowind.github.io/" rel="alternate"/>
  <link href="https://evasnowind.github.io/atom.xml" rel="self"/>
  <rights>All rights reserved 2026, Prayer</rights>
  <subtitle>后端 / Java / 架构 / 技术笔记</subtitle>
  <title>Prayer的数字城堡</title>
  <updated>2026-09-04T04:44:28.357Z</updated>
  <entry>
    <author>
      <name>Prayer</name>
    </author>
    <category term="中间件" scheme="https://evasnowind.github.io/categories/%E4%B8%AD%E9%97%B4%E4%BB%B6/"/>
    <category term="ActiveMQ" scheme="https://evasnowind.github.io/tags/ActiveMQ/"/>
    <content>
      <![CDATA[<h2 id="部署"><a href="#部署" class="headerlink" title="部署"></a>部署</h2><p>以centos为例，下载jar包，<code>tar -zxvf xxx</code>解压后，进入activemq解压文件夹，执行<code>./bin/activemq console</code>即可在shell前端启动，想后台运行的话执行<code>./bin/activemq start</code>即可。</p><span id="more"></span><p>官方文档在这里，很简单：<a href="http://activemq.apache.org/getting-started">http://activemq.apache.org/getting-started</a></p><h2 id="遇到的问题"><a href="#遇到的问题" class="headerlink" title="遇到的问题"></a>遇到的问题</h2><h3 id="1、activemq启动失败，报错"><a href="#1、activemq启动失败，报错" class="headerlink" title="1、activemq启动失败，报错"></a>1、activemq启动失败，报错</h3><p>报错信息如下：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">Caused by: java.lang.IllegalStateException: BeanFactory not initialized or already closed - call &#x27;refresh&#x27; before accessing beans via the ApplicationContext</span><br><span class="line">at org.springframework.context.support.AbstractRefreshableApplicationContext.getBeanFactory(AbstractRefreshableApplicationContext.java:171)</span><br><span class="line">at org.springframework.context.support.AbstractApplicationContext.destroyBeans(AbstractApplicationContext.java:1090)</span><br><span class="line">at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:487)</span><br><span class="line">at org.apache.xbean.spring.context.ResourceXmlApplicationContext.&lt;init&gt;(ResourceXmlApplicationContext.java:64)</span><br><span class="line">at org.apache.xbean.spring.context.ResourceXmlApplicationContext.&lt;init&gt;(ResourceXmlApplicationContext.java:52)</span><br><span class="line">at org.apache.activemq.xbean.XBeanBrokerFactory$1.&lt;init&gt;(XBeanBrokerFactory.java:104)</span><br><span class="line">at org.apache.activemq.xbean.XBeanBrokerFactory.createApplicationContext(XBeanBrokerFactory.java:104)</span><br><span class="line">at org.apache.activemq.xbean.XBeanBrokerFactory.createBroker(XBeanBrokerFactory.java:67)</span><br><span class="line">at org.apache.activemq.broker.BrokerFactory.createBroker(BrokerFactory.java:71)</span><br><span class="line">at org.apache.activemq.broker.BrokerFactory.createBroker(BrokerFactory.java:54)</span><br><span class="line">at org.apache.activemq.console.command.StartCommand.runTask(StartCommand.java:87)</span><br><span class="line">... 10 more</span><br></pre></td></tr></table></figure><h4 id="解决"><a href="#解决" class="headerlink" title="解决"></a>解决</h4><ul><li><p>1.确认计算机主机名名称没有下划线</p><ul><li>centos修改主机名可以参考这篇 <a href="https://www.cnblogs.com/mingyue5826/p/11528121.html">https://www.cnblogs.com/mingyue5826/p/11528121.html</a></li></ul></li><li><p>2.如果是win7，停止ICS(运行–&gt;services.msc找到Internet Connection Sharing (ICS)服务,改成手动启动或禁用)</p></li></ul><h3 id="2、部署在VPS或是虚拟机上，也关闭了防火墙，但无法访问web页面xx-xx-xx-xx-8161-admin"><a href="#2、部署在VPS或是虚拟机上，也关闭了防火墙，但无法访问web页面xx-xx-xx-xx-8161-admin" class="headerlink" title="2、部署在VPS或是虚拟机上，也关闭了防火墙，但无法访问web页面xx.xx.xx.xx:8161&#x2F;admin"></a>2、部署在VPS或是虚拟机上，也关闭了防火墙，但无法访问web页面xx.xx.xx.xx:8161&#x2F;admin</h3><p>修改<code>conf/jetty.xml</code>，注释掉host属性这行：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line">&lt;bean id=&quot;jettyPort&quot; class=&quot;org.apache.activemq.web.WebConsolePort&quot; init-method=&quot;start&quot;&gt;</span><br><span class="line">           &lt;!--默认是127.0.0.1， 注释掉这一行--&gt;</span><br><span class="line">        &lt;!-- &lt;property name=&quot;host&quot; value=&quot;127.0.0.1&quot;/&gt; --&gt;</span><br><span class="line">        &lt;property name=&quot;port&quot; value=&quot;8161&quot;/&gt;</span><br><span class="line">&lt;/bean&gt;</span><br></pre></td></tr></table></figure><p>重新启动activemq即可</p><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><ul><li><a href="https://blog.csdn.net/vtopqx/article/details/51787888">ActiveMQ常见错误一: BeanFactory not initialized or already closed – call ‘refresh’ before acces</a></li><li><a href="https://www.cnblogs.com/mingyue5826/p/11528121.html">centos修改主机名称的方式</a></li><li><a href="https://www.cnblogs.com/ytmm/p/14198680.html">activeMq不能被主机访问的问题</a></li></ul>]]>
    </content>
    <id>https://evasnowind.github.io/2021/01/10/2021-01-11_%E5%A6%82%E4%BD%95%E9%83%A8%E7%BD%B2activemq%E4%BB%A5%E5%8F%8A%E9%83%A8%E7%BD%B2%E6%97%B6%E9%81%87%E5%88%B0%E7%9A%84%E7%9B%B8%E5%85%B3%E9%97%AE%E9%A2%98/</id>
    <link href="https://evasnowind.github.io/2021/01/10/2021-01-11_%E5%A6%82%E4%BD%95%E9%83%A8%E7%BD%B2activemq%E4%BB%A5%E5%8F%8A%E9%83%A8%E7%BD%B2%E6%97%B6%E9%81%87%E5%88%B0%E7%9A%84%E7%9B%B8%E5%85%B3%E9%97%AE%E9%A2%98/"/>
    <published>2021-01-10T16:00:00.000Z</published>
    <summary>以centos为例，下载jar包，tar -zxvf xxx解压后，进入activemq解压文件夹，执行./bin/activemq console即可在shell前端启动，想。</summary>
    <title>如何部署activemq以及部署时遇到的相关问题</title>
    <updated>2026-09-04T04:44:28.357Z</updated>
  </entry>
  <entry>
    <author>
      <name>Prayer</name>
    </author>
    <category term="Java" scheme="https://evasnowind.github.io/categories/Java/"/>
    <category term="Spring Cloud" scheme="https://evasnowind.github.io/categories/Java/Spring-Cloud/"/>
    <category term="Spring Cloud" scheme="https://evasnowind.github.io/tags/Spring-Cloud/"/>
    <content>
      <![CDATA[<p>spring cloud gateway（后续简称SCG）本身就是spring cloud体系的一员，在对于java技术栈的系统使用起来比较方便，此外，SCG扩展性比较好，适合作为<strong>业务网关</strong>的基础、根据自身需求进行二次开发。</p><span id="more"></span><h3 id="关键组成部分"><a href="#关键组成部分" class="headerlink" title="关键组成部分"></a>关键组成部分</h3><ul><li>谓词 predicate: 将请求匹配到对应的route上</li><li>路由 route: 网关的基本构建块。它由ID，目标URI，谓词集合和过滤器集合定义</li><li>过滤器 filter: 由特定工厂构造生成</li></ul><p><img src="/images/spring-cloud-gateway-structure_ff83e600.png"></p><h3 id="工作过程"><a href="#工作过程" class="headerlink" title="工作过程"></a>工作过程</h3><ul><li><ol><li>SCG接收客户端请求。</li></ol></li><li><ol start="2"><li>请求与谓词匹配，获得对应的路由，匹配成功后，继续往下执行。</li></ol></li><li><ol start="3"><li>请求交给前置filter处理</li></ol></li><li><ol start="4"><li>请求经过代理服务（类似与RPC），发送给目标URI</li></ol></li><li><ol start="5"><li>响应交给后置filter处理</li></ol></li><li><ol start="6"><li>SCG将响应返回给客户端</li></ol></li></ul><p><img src="/images/tCZIYxXepbNLnjPp_62cf7bf7.png"></p><h3 id="应用场景"><a href="#应用场景" class="headerlink" title="应用场景"></a>应用场景</h3><ul><li><p>灰度发布</p><ul><li>可以使用Weight Route Predicate实现</li></ul></li><li><p>请求限流</p></li><li><p>服务容错</p><ul><li>结合Sentinel或是Hystrix，基于令牌桶算法</li></ul></li></ul><h3 id="其他"><a href="#其他" class="headerlink" title="其他"></a>其他</h3><ul><li><p>监控</p></li><li><p>故障排查</p><ul><li>通过日志</li><li>Wiretap</li></ul></li></ul><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><ul><li><a href="https://www.zhihu.com/question/280850489/answer/522263176">如何评价 spring cloud gateway? 对比 zuul2.0 主要的优势是什么?</a></li><li><a href="https://blog.csdn.net/W_317/article/details/108691740">Spring Cloud Gateway组成</a></li><li><a href="http://www.iocoder.cn/Spring-Cloud/Spring-Cloud-Gateway/?self#">芋道 Spring Cloud 网关 Spring Cloud Gateway 入门</a></li></ul>]]>
    </content>
    <id>https://evasnowind.github.io/2020/12/28/2020-12-29_spring_cloud_gateway%E5%9F%BA%E7%A1%80/</id>
    <link href="https://evasnowind.github.io/2020/12/28/2020-12-29_spring_cloud_gateway%E5%9F%BA%E7%A1%80/"/>
    <published>2020-12-28T16:00:00.000Z</published>
    <summary>spring cloud gateway（后续简称SCG）本身就是spring cloud体系的一员，在对于java技术栈的系统使用起来比较方便，此外，SCG扩展性比较好，适合。</summary>
    <title>spring cloud gateway基础</title>
    <updated>2026-09-04T04:44:28.356Z</updated>
  </entry>
  <entry>
    <author>
      <name>Prayer</name>
    </author>
    <category term="工具" scheme="https://evasnowind.github.io/categories/%E5%B7%A5%E5%85%B7/"/>
    <category term="构建工具" scheme="https://evasnowind.github.io/categories/%E5%B7%A5%E5%85%B7/%E6%9E%84%E5%BB%BA%E5%B7%A5%E5%85%B7/"/>
    <category term="Maven" scheme="https://evasnowind.github.io/tags/Maven/"/>
    <content>
      <![CDATA[<p>如果项目较大，需要尽可能跳过无关痛痒的环节（比如自己本地编译、使用的话，可能没必要编译javadoc）、只要最终jar的时候，可以用下面命令</p><blockquote><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">mvn clean package install -Dmaven.test.skip=true -Dmaven.javadoc.skip=true -Drat.skip=true -Dcheckstyle.skip=true</span><br></pre></td></tr></table></figure></blockquote><span id="more"></span>]]>
    </content>
    <id>https://evasnowind.github.io/2020/12/09/2020-12-10_maven%E7%BC%96%E8%AF%91%E5%A4%A7%E5%9E%8B%E9%A1%B9%E7%9B%AE%E6%97%B6%E7%AE%80%E5%8C%96%E7%BC%96%E8%AF%91%E8%BF%87%E7%A8%8B%E7%9A%84%E5%91%BD%E4%BB%A4/</id>
    <link href="https://evasnowind.github.io/2020/12/09/2020-12-10_maven%E7%BC%96%E8%AF%91%E5%A4%A7%E5%9E%8B%E9%A1%B9%E7%9B%AE%E6%97%B6%E7%AE%80%E5%8C%96%E7%BC%96%E8%AF%91%E8%BF%87%E7%A8%8B%E7%9A%84%E5%91%BD%E4%BB%A4/"/>
    <published>2020-12-09T16:00:00.000Z</published>
    <summary>记录 Maven 编译大型项目时常用的简化命令，适合在打包阶段跳过测试、javadoc、RAT 和 checkstyle 等非关键环节。</summary>
    <title>maven编译大型项目时简化编译过程的命令</title>
    <updated>2026-09-04T04:44:28.356Z</updated>
  </entry>
  <entry>
    <author>
      <name>Prayer</name>
    </author>
    <category term="Linux" scheme="https://evasnowind.github.io/categories/Linux/"/>
    <category term="Linux" scheme="https://evasnowind.github.io/tags/Linux/"/>
    <category term="MySQL" scheme="https://evasnowind.github.io/tags/MySQL/"/>
    <content>
      <![CDATA[<p>mysql安装有多种方式：</p><blockquote><p>具体详细参考官网 （<a href="https://dev.mysql.com/doc/refman/5.7/en/installing.html">https://dev.mysql.com/doc/refman/5.7/en/installing.html</a>）</p><ul><li>mysql的安装方法有多种，如二进制安装、源码编译安装、yum安装;</li><li>yum安装都是默认路径，不利于后期维护，安装相对简单；</li><li>源码安装编译的过程比较长，若没有对源码进行修改且要求使用mysql较高版本；</li></ul></blockquote><p>建议使用二进制安装，比较方便后期维护。本文就是采用这种方式启动多个实例。</p><p>本文在centos 7.5下进行操作。</p><span id="more"></span><h2 id="准备工作"><a href="#准备工作" class="headerlink" title="准备工作"></a>准备工作</h2><p>下载mysql 压缩包，注意需要下载二进制包。下载地址</p><ul><li><a href="https://dev.mysql.com/downloads/mysql/">https://dev.mysql.com/downloads/mysql/</a> 目前只提供mysql 8的下载</li><li><a href="https://downloads.mysql.com/archives/community/">https://downloads.mysql.com/archives/community/</a> mysql历史版本下载，如果没有历史包袱，当然选用8最好，但求稳的话建议先使用mysql 5.7，地址：<a href="https://cdn.mysql.com//Downloads/MySQL-5.7/mysql-5.7.32-el7-x86_64.tar.gz">https://cdn.mysql.com//Downloads/MySQL-5.7/mysql-5.7.32-el7-x86_64.tar.gz</a></li></ul><p>centos 下可以使用如下命令：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">wget https://cdn.mysql.com//Downloads/MySQL-5.7/mysql-5.7.32-el7-x86_64.tar.gz</span><br></pre></td></tr></table></figure><p>解压，移动到&#x2F;usr&#x2F;local&#x2F;文件夹下：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">tar -zxvf mysql-5.7.32-el7-x86_64.tar.gz</span><br><span class="line"># 可以顺手给文件夹改个名字,比如我就已经改成了mysql，所以下面大家会看到我的目录是/usr/local/mysql</span><br><span class="line">mv mysql-5.7.32-el7-x86_64 /usr/local/</span><br></pre></td></tr></table></figure><h2 id="初始化用户组以及用户"><a href="#初始化用户组以及用户" class="headerlink" title="初始化用户组以及用户"></a>初始化用户组以及用户</h2><p>创建一个mysql用户组及用户，且这个用户是不可登录的 创建用户组：groupadd mysql 创建不可登录用户：useradd -g mysql -s &#x2F;sbin&#x2F;nologin -d &#x2F;opt&#x2F;mysql mysql 查看下创建后的用户信息：id msyql</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">id mysql</span><br><span class="line">uid=501(mysql) gid=501(mysql) groups=501(mysql)</span><br></pre></td></tr></table></figure><h2 id="创建相关目录"><a href="#创建相关目录" class="headerlink" title="创建相关目录"></a>创建相关目录</h2><p>此处我选择使用在&#x2F;data&#x2F;mysql文件夹下保存数据、日志，因此按如下结构创建目录（我打算启动2个实例），命令略去：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">-data</span><br><span class="line">-- mysql</span><br><span class="line">    |-- mysql_3307</span><br><span class="line">        |-- data</span><br><span class="line">        |-- log</span><br><span class="line">        `-- tmp</span><br><span class="line">    `-- mysql_3308</span><br><span class="line">        |-- data</span><br><span class="line">        |-- log</span><br><span class="line">        `-- tmp</span><br><span class="line"></span><br></pre></td></tr></table></figure><h2 id="更改目录权限"><a href="#更改目录权限" class="headerlink" title="更改目录权限"></a>更改目录权限</h2><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">chown -R mysql:mysql /data/mysql/  </span><br><span class="line">chown -R mysql:mysql /usr/local/mysql/  </span><br></pre></td></tr></table></figure><h2 id="添加环境变量"><a href="#添加环境变量" class="headerlink" title="添加环境变量"></a>添加环境变量</h2><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">echo &#x27;export PATH=$PATH:/usr/local/mysql/bin&#x27; &gt;&gt;  /etc/profile  </span><br><span class="line">source /etc/profile  </span><br></pre></td></tr></table></figure><h2 id="添加配置文件"><a href="#添加配置文件" class="headerlink" title="添加配置文件"></a>添加配置文件</h2><p>找一个mysql配置文件my.cnf，如果没有可以手工创建，位置放在：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">/etc/my_3307.cnf</span><br></pre></td></tr></table></figure><p>下面给出一个mysql配置文件参考，请按需修改：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br></pre></td><td class="code"><pre><span class="line"># my_3307.cnf</span><br><span class="line"># For advice on how to change settings please see</span><br><span class="line"># http://dev.mysql.com/doc/refman/5.7/en/server-configuration-defaults.html</span><br><span class="line"></span><br><span class="line">[mysqld]</span><br><span class="line">#</span><br><span class="line"># Remove leading # and set to the amount of RAM for the most important data</span><br><span class="line"># cache in MySQL. Start at 70% of total RAM for dedicated server, else 10%.</span><br><span class="line"># innodb_buffer_pool_size = 128M</span><br><span class="line">#</span><br><span class="line"># Remove leading # to turn on a very important data integrity option: logging</span><br><span class="line"># changes to the binary log between backups.</span><br><span class="line"># log_bin</span><br><span class="line">#</span><br><span class="line"># Remove leading # to set options mainly useful for reporting servers.</span><br><span class="line"># The server defaults are faster for transactions and fast SELECTs.</span><br><span class="line"># Adjust sizes as needed, experiment to find the optimal values.</span><br><span class="line"># join_buffer_size = 128M</span><br><span class="line"># sort_buffer_size = 2M</span><br><span class="line"># read_rnd_buffer_size = 2M</span><br><span class="line">port=3307</span><br><span class="line">basedir=/usr/local/mysql-5.7.32</span><br><span class="line">datadir=/data/mysql/mysql_3307/data</span><br><span class="line">socket=/data/mysql/mysql_3307/mysql.sock</span><br><span class="line">server_id=3307</span><br><span class="line"></span><br><span class="line">sql_mode=NO_ENGINE_SUBSTITUTION,STRICT_TRANS_TABLES</span><br><span class="line"></span><br><span class="line"># Disabling symbolic-links is recommended to prevent assorted security risks</span><br><span class="line">symbolic-links=0</span><br><span class="line">log-output=file</span><br><span class="line">slow_query_log=1</span><br><span class="line">long_query_time=1</span><br><span class="line">slow_query_log_file = /data/mysql/mysql_3307/log/slow.log</span><br><span class="line">log-error=/data/mysql/mysql_3307/log/mysqld.log</span><br><span class="line">log-bin = /data/mysql/mysql_3307/log/mysql3307_bin</span><br><span class="line">binlog-format=Row</span><br><span class="line">pid-file=/var/run/mysqld/mysqld-3307.pid</span><br><span class="line">collation-server = utf8_unicode_ci</span><br><span class="line">init-connect=&#x27;SET NAMES utf8&#x27;</span><br><span class="line">character-set-server = utf8</span><br><span class="line">language =/usr/local/mysql-5.7.32/share/english</span><br><span class="line"></span><br><span class="line">[client]</span><br><span class="line">default-character-set=utf8</span><br><span class="line"></span><br><span class="line">[mysql]</span><br><span class="line">default-character-set=utf8</span><br><span class="line"></span><br></pre></td></tr></table></figure><h2 id="启动mysql"><a href="#启动mysql" class="headerlink" title="启动mysql"></a>启动mysql</h2><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">cd /usr/local/mysql/</span><br><span class="line"># 注意此处指定配置文件是必须的，--initialize-insecure是在初始化时不给root设置密码，方便操作；使用./bin/mysqld --defaults-file=/etc/my_3307.cnf --user=mysql --initialize也可以初始化，此时的密码会保存在mysqld.log中</span><br><span class="line">./bin/mysqld --defaults-file=/etc/my_3307.cnf --user=mysql --initialize-insecure</span><br><span class="line">./bin/mysqld --defaults-file=/etc/my_3307.cnf --user=mysql &amp;</span><br><span class="line"># 注意，刚初始化、启动时，如果不指定host或是-hlocalhost，无法连接，必须指定为-h127.0.0.1才能连接。并且此处注意使用-P指定端口，毕竟我们是要启动多个实例，端口别弄乱。</span><br><span class="line">mysql -P3307 -uroot -h127.0.0.1</span><br></pre></td></tr></table></figure><h2 id="设置远程连接mysql"><a href="#设置远程连接mysql" class="headerlink" title="设置远程连接mysql"></a>设置远程连接mysql</h2><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">GRANT ALL PRIVILEGES ON *.* TO &#x27;root&#x27;@&#x27;%&#x27; IDENTIFIED BY &#x27;password&#x27; WITH GRANT OPTION;</span><br><span class="line"></span><br><span class="line">FLUSH PRIVILEGES;</span><br></pre></td></tr></table></figure><p>这样就能成功启动一个实例，接下来同样的操作，即可启动多个实例。</p><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><ul><li><a href="https://blog.51cto.com/13799042/2126621">https://blog.51cto.com/13799042/2126621</a></li></ul>]]>
    </content>
    <id>https://evasnowind.github.io/2020/12/01/2020-12-02_centos%E4%B8%8B%E5%A6%82%E4%BD%95%E5%90%AF%E5%8A%A8mysql%E5%A4%9A%E5%AE%9E%E4%BE%8B/</id>
    <link href="https://evasnowind.github.io/2020/12/01/2020-12-02_centos%E4%B8%8B%E5%A6%82%E4%BD%95%E5%90%AF%E5%8A%A8mysql%E5%A4%9A%E5%AE%9E%E4%BE%8B/"/>
    <published>2020-12-01T16:00:00.000Z</published>
    <summary>具体详细参考官网 （） - mysql的安装方法有多种，如二进制安装、源码编译安装、yum安装; - yum安装都是默认路径，不利于后期维护，安装相对简单；</summary>
    <title>centos下如何启动mysql多实例</title>
    <updated>2026-09-04T04:44:28.356Z</updated>
  </entry>
  <entry>
    <author>
      <name>Prayer</name>
    </author>
    <category term="数据库" scheme="https://evasnowind.github.io/categories/%E6%95%B0%E6%8D%AE%E5%BA%93/"/>
    <category term="MySQL" scheme="https://evasnowind.github.io/tags/MySQL/"/>
    <content>
      <![CDATA[<h3 id="测试环境说明"><a href="#测试环境说明" class="headerlink" title="测试环境说明"></a>测试环境说明</h3><h4 id="硬件"><a href="#硬件" class="headerlink" title="硬件"></a>硬件</h4><ul><li><p>腾讯云标准型SA2</p><ul><li>1核 2GB 1Mbps</li><li>系统盘：高性能云硬盘(50G)</li></ul></li></ul><h4 id="软件"><a href="#软件" class="headerlink" title="软件"></a>软件</h4><ul><li>centos 7.5 64位</li><li>mysql 5.7.31</li><li>sysbench 1.0.20</li></ul><span id="more"></span><h3 id="测试过程"><a href="#测试过程" class="headerlink" title="测试过程"></a>测试过程</h3><p>使用sysbench。</p><h4 id="1-测试innodb-100w条记录的读写性能"><a href="#1-测试innodb-100w条记录的读写性能" class="headerlink" title="1. 测试innodb 100w条记录的读写性能"></a>1. 测试innodb 100w条记录的读写性能</h4><h5 id="1-1-生成100w数据"><a href="#1-1-生成100w数据" class="headerlink" title="1.1 生成100w数据"></a>1.1 生成100w数据</h5><p>此处偷懒了，直接使用了sysbench默认的脚本创建数据库表，如果需要测试特定的表，需要修改<code>/usr/share/sysbench</code>下的脚本，基本只需要修改<code>oltp_common.lua</code>脚本即可，这个需要花时间细致的修改，这次就不在这方面多花时间了。本次主要目的是做基准测试、看看Mysql性能。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sysbench --db-driver=mysql --mysql-host=localhost --mysql-port=3306 --mysql-user=root --mysql-password=1qaz@WSX --mysql-db=sbtest --table_size=1000000 --tables=10 --events=0 --time=300  --threads=20  oltp_read_write prepare</span><br></pre></td></tr></table></figure><h5 id="1-2-进行读写测试"><a href="#1-2-进行读写测试" class="headerlink" title="1.2 进行读写测试"></a>1.2 进行读写测试</h5><h6 id="1-2-1-第一次直接执行100w次读写"><a href="#1-2-1-第一次直接执行100w次读写" class="headerlink" title="1.2.1 第一次直接执行100w次读写"></a>1.2.1 第一次直接执行100w次读写</h6><p>执行命令：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sysbench --db-driver=mysql --mysql-host=localhost --mysql-port=3306 --mysql-user=root --mysql-password=1qaz@WSX --mysql-db=sbtest --table_size=1000000 --tables=10 --events=0 --time=300  --threads=20 --percentile=95 --report-interval=10 oltp_read_write run</span><br></pre></td></tr></table></figure><p>参数说明：</p><ul><li>指定测试mysql，默认存储引擎是innodb，指定mysql地址、端口、用户等信息</li><li>选取测试的数据库为sbtest，每个表的大小为100w条数据，创建10张表</li><li>测试时间300s, 20个线程同时测试</li><li>输出测试结果95分位</li><li>每隔10s输出一次当前测试情况</li><li>选取sysbench中的oltp_read_write.lua，测试读写情况</li></ul><p>注意此处没有预热，先观察下没有预热情况下的读写性能，得到如下执行结果：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br></pre></td><td class="code"><pre><span class="line">[root@VM_0_5_centos ~]# sysbench --db-driver=mysql --mysql-host=localhost --mysql-port=3306 --mysql-user=root --mysql-password=1qaz@WSX --mysql-db=sbtest --table_size=1000000 --tables=10 --events=0 --time=300  --threads=20 --percentile=95 --report-interval=10 oltp_read_write run</span><br><span class="line">sysbench 1.0.20 (using bundled LuaJIT 2.1.0-beta2)</span><br><span class="line"></span><br><span class="line">Running the test with following options:</span><br><span class="line">Number of threads: 20</span><br><span class="line">Report intermediate results every 10 second(s)</span><br><span class="line">Initializing random number generator from current time</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">Initializing worker threads...</span><br><span class="line"></span><br><span class="line">Threads started!</span><br><span class="line"></span><br><span class="line">[ 10s ] thds: 20 tps: 115.28 qps: 2327.70 (r/w/o: 1633.92/461.22/232.56) lat (ms,95%): 337.94 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 20s ] thds: 20 tps: 107.65 qps: 2154.96 (r/w/o: 1508.64/431.01/215.31) lat (ms,95%): 350.33 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 30s ] thds: 20 tps: 107.57 qps: 2155.23 (r/w/o: 1509.13/430.97/215.13) lat (ms,95%): 337.94 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 40s ] thds: 20 tps: 102.08 qps: 2027.14 (r/w/o: 1414.88/408.11/204.16) lat (ms,95%): 369.77 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 50s ] thds: 20 tps: 111.17 qps: 2239.64 (r/w/o: 1571.11/446.19/222.34) lat (ms,95%): 337.94 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 60s ] thds: 20 tps: 118.82 qps: 2376.16 (r/w/o: 1663.62/474.89/237.65) lat (ms,95%): 297.92 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 70s ] thds: 20 tps: 118.57 qps: 2369.40 (r/w/o: 1657.88/474.48/237.04) lat (ms,95%): 325.98 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 80s ] thds: 20 tps: 125.53 qps: 2512.64 (r/w/o: 1758.58/502.91/251.15) lat (ms,95%): 287.38 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 90s ] thds: 20 tps: 115.44 qps: 2298.71 (r/w/o: 1609.27/458.76/230.68) lat (ms,95%): 314.45 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 100s ] thds: 20 tps: 120.28 qps: 2412.81 (r/w/o: 1689.53/482.52/240.76) lat (ms,95%): 314.45 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 110s ] thds: 20 tps: 123.70 qps: 2460.59 (r/w/o: 1719.30/494.00/247.30) lat (ms,95%): 287.38 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 120s ] thds: 20 tps: 121.74 qps: 2455.40 (r/w/o: 1720.96/490.86/243.58) lat (ms,95%): 287.38 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 130s ] thds: 20 tps: 129.32 qps: 2580.95 (r/w/o: 1808.11/514.19/258.64) lat (ms,95%): 292.60 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 140s ] thds: 20 tps: 125.38 qps: 2508.77 (r/w/o: 1756.50/501.51/250.76) lat (ms,95%): 282.25 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 150s ] thds: 20 tps: 128.26 qps: 2564.90 (r/w/o: 1794.64/513.74/256.52) lat (ms,95%): 287.38 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 160s ] thds: 20 tps: 129.90 qps: 2596.47 (r/w/o: 1817.98/518.79/259.70) lat (ms,95%): 267.41 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 170s ] thds: 20 tps: 130.30 qps: 2597.18 (r/w/o: 1816.16/520.32/260.71) lat (ms,95%): 262.64 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 180s ] thds: 20 tps: 134.10 qps: 2685.40 (r/w/o: 1881.30/535.90/268.20) lat (ms,95%): 267.41 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 190s ] thds: 20 tps: 131.38 qps: 2636.25 (r/w/o: 1842.79/530.91/262.56) lat (ms,95%): 282.25 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 200s ] thds: 20 tps: 134.40 qps: 2687.60 (r/w/o: 1883.30/535.30/269.00) lat (ms,95%): 282.25 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 210s ] thds: 20 tps: 130.51 qps: 2610.40 (r/w/o: 1827.61/521.76/261.03) lat (ms,95%): 292.60 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 220s ] thds: 20 tps: 134.91 qps: 2687.87 (r/w/o: 1880.32/537.73/269.82) lat (ms,95%): 272.27 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 230s ] thds: 20 tps: 131.30 qps: 2633.77 (r/w/o: 1845.48/525.69/262.60) lat (ms,95%): 277.21 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 240s ] thds: 20 tps: 133.78 qps: 2676.67 (r/w/o: 1872.87/536.23/267.57) lat (ms,95%): 253.35 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 250s ] thds: 20 tps: 134.10 qps: 2681.82 (r/w/o: 1876.34/537.28/268.19) lat (ms,95%): 292.60 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 260s ] thds: 20 tps: 136.32 qps: 2711.48 (r/w/o: 1896.94/541.90/272.65) lat (ms,95%): 253.35 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 270s ] thds: 20 tps: 134.20 qps: 2692.50 (r/w/o: 1884.80/539.30/268.40) lat (ms,95%): 262.64 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 280s ] thds: 20 tps: 133.03 qps: 2664.24 (r/w/o: 1866.85/531.33/266.06) lat (ms,95%): 262.64 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 290s ] thds: 20 tps: 134.52 qps: 2687.68 (r/w/o: 1880.73/537.90/269.05) lat (ms,95%): 262.64 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 300s ] thds: 20 tps: 130.74 qps: 2615.97 (r/w/o: 1830.71/523.77/261.49) lat (ms,95%): 277.21 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">SQL statistics:</span><br><span class="line">    queries performed:</span><br><span class="line">        read:                            527282</span><br><span class="line">        write:                           150652</span><br><span class="line">        other:                           75326</span><br><span class="line">        total:                           753260</span><br><span class="line">    transactions:                        37663  (125.47 per sec.)</span><br><span class="line">    queries:                             753260 (2509.43 per sec.)</span><br><span class="line">    ignored errors:                      0      (0.00 per sec.)</span><br><span class="line">    reconnects:                          0      (0.00 per sec.)</span><br><span class="line"></span><br><span class="line">General statistics:</span><br><span class="line">    total time:                          300.1706s</span><br><span class="line">    total number of events:              37663</span><br><span class="line"></span><br><span class="line">Latency (ms):</span><br><span class="line">         min:                                    7.13</span><br><span class="line">         avg:                                  159.35</span><br><span class="line">         max:                                  873.88</span><br><span class="line">         95th percentile:                      292.60</span><br><span class="line">         sum:                              6001658.82</span><br><span class="line"></span><br><span class="line">Threads fairness:</span><br><span class="line">    events (avg/stddev):           1883.1500/14.40</span><br><span class="line">    execution time (avg/stddev):   300.0829/0.05</span><br></pre></td></tr></table></figure><h6 id="1-2-2-第二次执行100w次读写"><a href="#1-2-2-第二次执行100w次读写" class="headerlink" title="1.2.2 第二次执行100w次读写"></a>1.2.2 第二次执行100w次读写</h6><p>在第一次执行完成后，再一次执行相同的压测命令：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sysbench --db-driver=mysql --mysql-host=localhost --mysql-port=3306 --mysql-user=root --mysql-password=1qaz@WSX --mysql-db=sbtest --table_size=1000000 --tables=10 --events=0 --time=300  --threads=20 --percentile=95 --report-interval=10 oltp_read_write run</span><br></pre></td></tr></table></figure><p>也就是说，将第一次执行当做预热，然后得到了如下结果：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br></pre></td><td class="code"><pre><span class="line">[root@VM_0_5_centos ~]# sysbench --db-driver=mysql --mysql-host=localhost --mysql-port=3306 --mysql-user=root --mysql-password=1qaz@WSX --mysql-db=sbtest --table_size=1000000 --tables=10 --events=0 --time=300  --threads=20 --percentile=95 --report-interval=10 oltp_read_write run</span><br><span class="line">sysbench 1.0.20 (using bundled LuaJIT 2.1.0-beta2)</span><br><span class="line"></span><br><span class="line">Running the test with following options:</span><br><span class="line">Number of threads: 20</span><br><span class="line">Report intermediate results every 10 second(s)</span><br><span class="line">Initializing random number generator from current time</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">Initializing worker threads...</span><br><span class="line"></span><br><span class="line">Threads started!</span><br><span class="line"></span><br><span class="line">[ 10s ] thds: 20 tps: 134.18 qps: 2711.59 (r/w/o: 1901.91/539.32/270.36) lat (ms,95%): 272.27 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 20s ] thds: 20 tps: 137.73 qps: 2743.17 (r/w/o: 1918.90/549.01/275.26) lat (ms,95%): 262.64 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 30s ] thds: 20 tps: 135.00 qps: 2700.12 (r/w/o: 1889.21/540.70/270.20) lat (ms,95%): 272.27 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 40s ] thds: 20 tps: 133.61 qps: 2680.90 (r/w/o: 1880.57/533.12/267.21) lat (ms,95%): 262.64 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 50s ] thds: 20 tps: 133.36 qps: 2673.78 (r/w/o: 1869.10/537.96/266.73) lat (ms,95%): 253.35 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 60s ] thds: 20 tps: 139.24 qps: 2776.62 (r/w/o: 1945.18/553.07/278.38) lat (ms,95%): 253.35 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 70s ] thds: 20 tps: 132.31 qps: 2646.75 (r/w/o: 1852.27/529.75/264.72) lat (ms,95%): 272.27 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 80s ] thds: 20 tps: 138.52 qps: 2764.91 (r/w/o: 1934.31/553.56/277.03) lat (ms,95%): 267.41 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 90s ] thds: 20 tps: 133.20 qps: 2673.72 (r/w/o: 1874.85/532.48/266.39) lat (ms,95%): 267.41 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 100s ] thds: 20 tps: 139.13 qps: 2783.76 (r/w/o: 1946.06/559.43/278.27) lat (ms,95%): 262.64 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 110s ] thds: 20 tps: 133.60 qps: 2673.61 (r/w/o: 1872.01/534.40/267.20) lat (ms,95%): 267.41 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 120s ] thds: 20 tps: 137.21 qps: 2739.02 (r/w/o: 1916.55/548.04/274.42) lat (ms,95%): 253.35 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 130s ] thds: 20 tps: 136.59 qps: 2736.29 (r/w/o: 1916.22/546.88/273.19) lat (ms,95%): 272.27 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 140s ] thds: 20 tps: 133.31 qps: 2667.76 (r/w/o: 1865.01/536.33/266.42) lat (ms,95%): 272.27 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 150s ] thds: 20 tps: 136.29 qps: 2728.42 (r/w/o: 1910.27/545.36/272.78) lat (ms,95%): 257.95 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 160s ] thds: 20 tps: 137.75 qps: 2747.96 (r/w/o: 1925.34/547.21/275.41) lat (ms,95%): 248.83 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 170s ] thds: 20 tps: 136.25 qps: 2727.97 (r/w/o: 1910.08/545.29/272.60) lat (ms,95%): 257.95 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 180s ] thds: 20 tps: 139.96 qps: 2799.77 (r/w/o: 1958.39/561.45/279.93) lat (ms,95%): 253.35 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 190s ] thds: 20 tps: 136.37 qps: 2724.15 (r/w/o: 1907.31/544.09/272.74) lat (ms,95%): 253.35 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 200s ] thds: 20 tps: 137.35 qps: 2748.81 (r/w/o: 1925.74/548.38/274.69) lat (ms,95%): 257.95 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 210s ] thds: 20 tps: 138.32 qps: 2760.41 (r/w/o: 1930.89/552.88/276.64) lat (ms,95%): 257.95 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 220s ] thds: 20 tps: 136.57 qps: 2730.27 (r/w/o: 1912.63/544.89/272.75) lat (ms,95%): 272.27 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 230s ] thds: 20 tps: 135.90 qps: 2725.03 (r/w/o: 1905.42/547.41/272.20) lat (ms,95%): 262.64 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 240s ] thds: 20 tps: 139.06 qps: 2780.47 (r/w/o: 1948.78/553.56/278.13) lat (ms,95%): 267.41 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 250s ] thds: 20 tps: 136.23 qps: 2719.64 (r/w/o: 1902.58/544.61/272.45) lat (ms,95%): 267.41 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 260s ] thds: 20 tps: 136.39 qps: 2736.95 (r/w/o: 1913.82/550.35/272.77) lat (ms,95%): 257.95 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 270s ] thds: 20 tps: 141.91 qps: 2829.29 (r/w/o: 1980.91/564.56/283.83) lat (ms,95%): 248.83 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 280s ] thds: 20 tps: 138.60 qps: 2776.59 (r/w/o: 1944.46/554.92/277.21) lat (ms,95%): 253.35 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 290s ] thds: 20 tps: 137.72 qps: 2747.78 (r/w/o: 1923.26/549.08/275.44) lat (ms,95%): 267.41 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 300s ] thds: 20 tps: 139.27 qps: 2798.45 (r/w/o: 1958.44/561.47/278.54) lat (ms,95%): 253.35 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">SQL statistics:</span><br><span class="line">    queries performed:</span><br><span class="line">        read:                            574434</span><br><span class="line">        write:                           164124</span><br><span class="line">        other:                           82062</span><br><span class="line">        total:                           820620</span><br><span class="line">    transactions:                        41031  (136.74 per sec.)</span><br><span class="line">    queries:                             820620 (2734.82 per sec.)</span><br><span class="line">    ignored errors:                      0      (0.00 per sec.)</span><br><span class="line">    reconnects:                          0      (0.00 per sec.)</span><br><span class="line"></span><br><span class="line">General statistics:</span><br><span class="line">    total time:                          300.0629s</span><br><span class="line">    total number of events:              41031</span><br><span class="line"></span><br><span class="line">Latency (ms):</span><br><span class="line">         min:                                    6.62</span><br><span class="line">         avg:                                  146.24</span><br><span class="line">         max:                                  709.59</span><br><span class="line">         95th percentile:                      262.64</span><br><span class="line">         sum:                              6000331.95</span><br><span class="line"></span><br><span class="line">Threads fairness:</span><br><span class="line">    events (avg/stddev):           2051.5500/16.35</span><br><span class="line">    execution time (avg/stddev):   300.0166/0.02</span><br></pre></td></tr></table></figure><h6 id="1-2-3-第三次执行100w次读写"><a href="#1-2-3-第三次执行100w次读写" class="headerlink" title="1.2.3 第三次执行100w次读写"></a>1.2.3 第三次执行100w次读写</h6><p>还是这条命令，再次执行看下：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sysbench --db-driver=mysql --mysql-host=localhost --mysql-port=3306 --mysql-user=root --mysql-password=1qaz@WSX --mysql-db=sbtest --table_size=1000000 --tables=10 --events=0 --time=300  --threads=20 --percentile=95 --report-interval=10 oltp_read_write run</span><br></pre></td></tr></table></figure><p>最后执行结果如下：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br></pre></td><td class="code"><pre><span class="line">[root@VM_0_5_centos ~]# sysbench --db-driver=mysql --mysql-host=localhost --mysql-port=3306 --mysql-user=root --mysql-password=1qaz@WSX --mysql-db=sbtest --table_size=1000000 --tables=10 --events=0 --time=300  --threads=20 --percentile=95 --report-interval=10 oltp_read_write run</span><br><span class="line">sysbench 1.0.20 (using bundled LuaJIT 2.1.0-beta2)</span><br><span class="line"></span><br><span class="line">Running the test with following options:</span><br><span class="line">Number of threads: 20</span><br><span class="line">Report intermediate results every 10 second(s)</span><br><span class="line">Initializing random number generator from current time</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">Initializing worker threads...</span><br><span class="line"></span><br><span class="line">Threads started!</span><br><span class="line"></span><br><span class="line">[ 10s ] thds: 20 tps: 137.99 qps: 2785.30 (r/w/o: 1953.79/553.54/277.97) lat (ms,95%): 253.35 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 20s ] thds: 20 tps: 139.98 qps: 2796.53 (r/w/o: 1955.64/561.23/279.66) lat (ms,95%): 257.95 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 30s ] thds: 20 tps: 135.89 qps: 2721.84 (r/w/o: 1905.59/544.37/271.88) lat (ms,95%): 257.95 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 40s ] thds: 20 tps: 143.80 qps: 2874.52 (r/w/o: 2013.41/573.40/287.70) lat (ms,95%): 257.95 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 50s ] thds: 20 tps: 141.73 qps: 2838.10 (r/w/o: 1986.65/567.90/283.55) lat (ms,95%): 277.21 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 60s ] thds: 20 tps: 142.10 qps: 2835.57 (r/w/o: 1984.28/567.09/284.20) lat (ms,95%): 248.83 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 70s ] thds: 20 tps: 141.93 qps: 2838.68 (r/w/o: 1987.87/566.94/283.87) lat (ms,95%): 267.41 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 80s ] thds: 20 tps: 142.27 qps: 2841.13 (r/w/o: 1987.20/569.39/284.54) lat (ms,95%): 244.38 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 90s ] thds: 20 tps: 144.78 qps: 2904.14 (r/w/o: 2034.24/580.63/289.27) lat (ms,95%): 231.53 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 100s ] thds: 20 tps: 141.03 qps: 2822.14 (r/w/o: 1977.08/562.71/282.35) lat (ms,95%): 257.95 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 110s ] thds: 20 tps: 138.68 qps: 2775.16 (r/w/o: 1941.56/556.23/277.37) lat (ms,95%): 257.95 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 120s ] thds: 20 tps: 140.70 qps: 2799.50 (r/w/o: 1957.90/560.50/281.10) lat (ms,95%): 257.95 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 130s ] thds: 20 tps: 142.10 qps: 2847.50 (r/w/o: 1994.40/569.00/284.10) lat (ms,95%): 257.95 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 140s ] thds: 20 tps: 142.10 qps: 2847.17 (r/w/o: 1994.18/568.39/284.60) lat (ms,95%): 267.41 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 150s ] thds: 20 tps: 141.69 qps: 2834.74 (r/w/o: 1983.99/567.37/283.38) lat (ms,95%): 240.02 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 160s ] thds: 20 tps: 142.69 qps: 2857.88 (r/w/o: 1999.55/572.96/285.38) lat (ms,95%): 257.95 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 170s ] thds: 20 tps: 148.01 qps: 2953.24 (r/w/o: 2067.20/590.03/296.01) lat (ms,95%): 244.38 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 180s ] thds: 20 tps: 142.16 qps: 2845.84 (r/w/o: 1993.50/568.03/284.31) lat (ms,95%): 257.95 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 190s ] thds: 20 tps: 141.93 qps: 2838.87 (r/w/o: 1987.80/567.21/283.86) lat (ms,95%): 262.64 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 200s ] thds: 20 tps: 137.03 qps: 2742.24 (r/w/o: 1919.74/548.43/274.06) lat (ms,95%): 267.41 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 210s ] thds: 20 tps: 141.14 qps: 2822.53 (r/w/o: 1975.28/564.97/282.28) lat (ms,95%): 253.35 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 220s ] thds: 20 tps: 143.06 qps: 2859.26 (r/w/o: 2001.71/571.43/286.12) lat (ms,95%): 257.95 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 230s ] thds: 20 tps: 145.89 qps: 2911.60 (r/w/o: 2036.39/583.54/291.67) lat (ms,95%): 248.83 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 240s ] thds: 20 tps: 142.86 qps: 2862.32 (r/w/o: 2003.09/573.42/285.81) lat (ms,95%): 244.38 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 250s ] thds: 20 tps: 142.36 qps: 2845.35 (r/w/o: 1992.70/567.93/284.71) lat (ms,95%): 248.83 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 260s ] thds: 20 tps: 140.64 qps: 2816.09 (r/w/o: 1971.96/562.86/281.28) lat (ms,95%): 267.41 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 270s ] thds: 20 tps: 143.49 qps: 2865.80 (r/w/o: 2004.86/573.96/286.98) lat (ms,95%): 282.25 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 280s ] thds: 20 tps: 145.81 qps: 2922.99 (r/w/o: 2047.60/583.76/291.63) lat (ms,95%): 253.35 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 290s ] thds: 20 tps: 142.26 qps: 2838.44 (r/w/o: 1985.40/568.53/284.51) lat (ms,95%): 253.35 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">[ 300s ] thds: 20 tps: 137.89 qps: 2755.07 (r/w/o: 1928.14/551.15/275.78) lat (ms,95%): 253.35 err/s: 0.00 reconn/s: 0.00</span><br><span class="line">SQL statistics:</span><br><span class="line">    queries performed:</span><br><span class="line">        read:                            595840</span><br><span class="line">        write:                           170240</span><br><span class="line">        other:                           85120</span><br><span class="line">        total:                           851200</span><br><span class="line">    transactions:                        42560  (141.79 per sec.)</span><br><span class="line">    queries:                             851200 (2835.86 per sec.)</span><br><span class="line">    ignored errors:                      0      (0.00 per sec.)</span><br><span class="line">    reconnects:                          0      (0.00 per sec.)</span><br><span class="line"></span><br><span class="line">General statistics:</span><br><span class="line">    total time:                          300.1547s</span><br><span class="line">    total number of events:              42560</span><br><span class="line"></span><br><span class="line">Latency (ms):</span><br><span class="line">         min:                                    6.18</span><br><span class="line">         avg:                                  141.02</span><br><span class="line">         max:                                  687.11</span><br><span class="line">         95th percentile:                      257.95</span><br><span class="line">         sum:                              6001887.46</span><br><span class="line"></span><br><span class="line">Threads fairness:</span><br><span class="line">    events (avg/stddev):           2128.0000/14.78</span><br><span class="line">    execution time (avg/stddev):   300.0944/0.03</span><br></pre></td></tr></table></figure><h5 id="1-3-清理数据"><a href="#1-3-清理数据" class="headerlink" title="1.3 清理数据"></a>1.3 清理数据</h5><p>使用如下命令清理数据</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sysbench --db-driver=mysql --mysql-host=localhost --mysql-port=3306 --mysql-user=root --mysql-password=1qaz@WSX --mysql-db=sbtest --table_size=1000000 --tables=10 --events=0 --time=300   --threads=20 --percentile=95 oltp_read_write cleanup</span><br></pre></td></tr></table></figure><h4 id="2-测试myisam-100w条记录的读写性能"><a href="#2-测试myisam-100w条记录的读写性能" class="headerlink" title="2. 测试myisam 100w条记录的读写性能"></a>2. 测试myisam 100w条记录的读写性能</h4><h5 id="2-1-生成100w数据"><a href="#2-1-生成100w数据" class="headerlink" title="2.1 生成100w数据"></a>2.1 生成100w数据</h5><p>使用</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sysbench --db-driver=mysql --mysql-host=localhost --mysql-port=3306 --mysql-user=root --mysql-password=1qaz@WSX --mysql-db=sbtest --table_size=1000000 --tables=10 --events=0 --time=300  --threads=20  oltp_read_write help</span><br></pre></td></tr></table></figure><p>可以看到oltp_read_write脚本所支持的参数，如下所示：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line">[root@VM_0_5_centos ~]# sysbench --db-driver=mysql --mysql-host=localhost --mysql-port=3306 --mysql-user=root --mysql-password=1qaz@WSX --mysql-db=sbtest --table_size=1000000 --tables=10 --events=0 --time=300  --threads=20  oltp_read_write help</span><br><span class="line">sysbench 1.0.20 (using bundled LuaJIT 2.1.0-beta2)</span><br><span class="line"></span><br><span class="line">oltp_read_write options:</span><br><span class="line">  --auto_inc[=on|off]           Use AUTO_INCREMENT column as Primary Key (for MySQL), or its alternatives in other DBMS. When disabled, use client-generated IDs [on]</span><br><span class="line">  --create_secondary[=on|off]   Create a secondary index in addition to the PRIMARY KEY [on]</span><br><span class="line">  --delete_inserts=N            Number of DELETE/INSERT combinations per transaction [1]</span><br><span class="line">  --distinct_ranges=N           Number of SELECT DISTINCT queries per transaction [1]</span><br><span class="line">  --index_updates=N             Number of UPDATE index queries per transaction [1]</span><br><span class="line">  --mysql_storage_engine=STRING Storage engine, if MySQL is used [innodb]</span><br><span class="line">  --non_index_updates=N         Number of UPDATE non-index queries per transaction [1]</span><br><span class="line">  --order_ranges=N              Number of SELECT ORDER BY queries per transaction [1]</span><br><span class="line">  --pgsql_variant=STRING        Use this PostgreSQL variant when running with the PostgreSQL driver. The only currently supported variant is &#x27;redshift&#x27;. When enabled, create_secondary is automatically disabled, and delete_inserts is set to 0</span><br><span class="line">  --point_selects=N             Number of point SELECT queries per transaction [10]</span><br><span class="line">  --range_selects[=on|off]      Enable/disable all range SELECT queries [on]</span><br><span class="line">  --range_size=N                Range size for range SELECT queries [100]</span><br><span class="line">  --secondary[=on|off]          Use a secondary index in place of the PRIMARY KEY [off]</span><br><span class="line">  --simple_ranges=N             Number of simple range SELECT queries per transaction [1]</span><br><span class="line">  --skip_trx[=on|off]           Don&#x27;t start explicit transactions and execute all queries in the AUTOCOMMIT mode [off]</span><br><span class="line">  --sum_ranges=N                Number of SELECT SUM() queries per transaction [1]</span><br><span class="line">  --table_size=N                Number of rows per table [10000]</span><br><span class="line">  --tables=N                    Number of tables [1]</span><br></pre></td></tr></table></figure><p>此处我们指定下数据库存储引擎为myisam，执行：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sysbench --db-driver=mysql --mysql-host=localhost --mysql-port=3306 --mysql-user=root --mysql-password=1qaz@WSX --mysql-db=sbtest --table_size=1000000 --tables=10 --events=0 --time=300  --threads=20  oltp_read_write --mysql_storage_engine=myisam prepare</span><br></pre></td></tr></table></figure><p>即可创建MyIsam的表。</p><h5 id="2-2-进行读写测试"><a href="#2-2-进行读写测试" class="headerlink" title="2.2 进行读写测试"></a>2.2 进行读写测试</h5><h6 id="2-2-1-第一次执行100w次读写"><a href="#2-2-1-第一次执行100w次读写" class="headerlink" title="2.2.1 第一次执行100w次读写"></a>2.2.1 第一次执行100w次读写</h6><p>执行命令</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sysbench --db-driver=mysql --mysql-host=localhost --mysql-port=3306 --mysql-user=root --mysql-password=1qaz@WSX --mysql-db=sbtest --table_size=1000000 --tables=10 --events=0 --time=300  --threads=20 --percentile=95 --report-interval=10 oltp_read_write   --mysql_storage_engine=myisam run</span><br></pre></td></tr></table></figure><p>测试结果如下：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line">......</span><br><span class="line">SQL statistics:</span><br><span class="line">    queries performed:</span><br><span class="line">        read:                            722036</span><br><span class="line">        write:                           206294</span><br><span class="line">        other:                           103150</span><br><span class="line">        total:                           1031480</span><br><span class="line">    transactions:                        51574  (171.84 per sec.)</span><br><span class="line">    queries:                             1031480 (3436.86 per sec.)</span><br><span class="line">    ignored errors:                      0      (0.00 per sec.)</span><br><span class="line">    reconnects:                          0      (0.00 per sec.)</span><br><span class="line"></span><br><span class="line">General statistics:</span><br><span class="line">    total time:                          300.1224s</span><br><span class="line">    total number of events:              51574</span><br><span class="line"></span><br><span class="line">Latency (ms):</span><br><span class="line">         min:                                    2.91</span><br><span class="line">         avg:                                  116.36</span><br><span class="line">         max:                                 2703.01</span><br><span class="line">         95th percentile:                      153.02</span><br><span class="line">         sum:                              6001171.45</span><br><span class="line"></span><br><span class="line">Threads fairness:</span><br><span class="line">    events (avg/stddev):           2578.7000/7.62</span><br><span class="line">    execution time (avg/stddev):   300.0586/0.04</span><br></pre></td></tr></table></figure><h6 id="2-2-2-第二次执行100w次读写"><a href="#2-2-2-第二次执行100w次读写" class="headerlink" title="2.2.2 第二次执行100w次读写"></a>2.2.2 第二次执行100w次读写</h6><p>相同命令，再来一次：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line">......</span><br><span class="line">SQL statistics:</span><br><span class="line">    queries performed:</span><br><span class="line">        read:                            697480</span><br><span class="line">        write:                           199279</span><br><span class="line">        other:                           99641</span><br><span class="line">        total:                           996400</span><br><span class="line">    transactions:                        49820  (165.95 per sec.)</span><br><span class="line">    queries:                             996400 (3319.08 per sec.)</span><br><span class="line">    ignored errors:                      0      (0.00 per sec.)</span><br><span class="line">    reconnects:                          0      (0.00 per sec.)</span><br><span class="line"></span><br><span class="line">General statistics:</span><br><span class="line">    total time:                          300.2032s</span><br><span class="line">    total number of events:              49820</span><br><span class="line"></span><br><span class="line">Latency (ms):</span><br><span class="line">         min:                                    2.86</span><br><span class="line">         avg:                                  120.45</span><br><span class="line">         max:                                 3435.92</span><br><span class="line">         95th percentile:                      176.73</span><br><span class="line">         sum:                              6000916.19</span><br><span class="line"></span><br><span class="line">Threads fairness:</span><br><span class="line">    events (avg/stddev):           2491.0000/9.31</span><br><span class="line">    execution time (avg/stddev):   300.0458/0.06</span><br></pre></td></tr></table></figure><h6 id="2-2-3-第三次执行100w次读写"><a href="#2-2-3-第三次执行100w次读写" class="headerlink" title="2.2.3 第三次执行100w次读写"></a>2.2.3 第三次执行100w次读写</h6><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line">······</span><br><span class="line">SQL statistics:</span><br><span class="line">    queries performed:</span><br><span class="line">        read:                            680372</span><br><span class="line">        write:                           194389</span><br><span class="line">        other:                           97199</span><br><span class="line">        total:                           971960</span><br><span class="line">    transactions:                        48598  (161.84 per sec.)</span><br><span class="line">    queries:                             971960 (3236.88 per sec.)</span><br><span class="line">    ignored errors:                      0      (0.00 per sec.)</span><br><span class="line">    reconnects:                          0      (0.00 per sec.)</span><br><span class="line"></span><br><span class="line">General statistics:</span><br><span class="line">    total time:                          300.2761s</span><br><span class="line">    total number of events:              48598</span><br><span class="line"></span><br><span class="line">Latency (ms):</span><br><span class="line">         min:                                    2.88</span><br><span class="line">         avg:                                  123.52</span><br><span class="line">         max:                                 2916.81</span><br><span class="line">         95th percentile:                      189.93</span><br><span class="line">         sum:                              6002904.07</span><br><span class="line"></span><br><span class="line">Threads fairness:</span><br><span class="line">    events (avg/stddev):           2429.9000/10.74</span><br><span class="line">    execution time (avg/stddev):   300.1452/0.07</span><br></pre></td></tr></table></figure><h5 id="2-3-清理数据"><a href="#2-3-清理数据" class="headerlink" title="2.3 清理数据"></a>2.3 清理数据</h5><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sysbench --db-driver=mysql --mysql-host=localhost --mysql-port=3306 --mysql-user=root --mysql-password=1qaz@WSX --mysql-db=sbtest --table_size=1000000 --tables=10 --events=0 --time=300  --threads=20  oltp_read_write cleanup</span><br></pre></td></tr></table></figure><h2 id="测试总结"><a href="#测试总结" class="headerlink" title="测试总结"></a>测试总结</h2><table><thead><tr><th></th><th>压测指标</th><th>InnoDB</th><th>MyISAM</th></tr></thead><tbody><tr><td>第一次</td><td>读、写次数</td><td>读：527282 写：150652 其他：75326 事务：37663 总计：753260</td><td>读：722036 写：206294 其他：103150 总计：1031480</td></tr><tr><td></td><td>耗时</td><td>最小：7.13ms 平均：159.35ms 最大：873.88ms 95分位数：292.60ms</td><td>最小：2.91ms 平均：116.36ms 最大：2703.01ms 95分位数：153.02ms</td></tr><tr><td></td><td>吞吐量</td><td>1883.1500</td><td>2578.7000</td></tr><tr><td>第二次</td><td>读、写次数</td><td>读：574434 写：164124 其他：82062 事务：41031 总计：820620</td><td>读：697480 写：199279 其他：99641 总计：996400</td></tr><tr><td></td><td>耗时</td><td>最小：6.62ms 平均：146.24ms 最大：709.59ms 95分位数：262.64ms</td><td>最小：2.86ms 平均：120.45ms 最大：3435.92ms 95分位数：176.73ms</td></tr><tr><td></td><td>吞吐量</td><td>2051.5500</td><td>2491.0000</td></tr><tr><td>第三次</td><td>读、写次数</td><td>读：595840 写：170240 其他：85120 事务：42560 总计：851200</td><td>读：680372 写：194389 其他：97199 总计：971960</td></tr><tr><td></td><td>耗时</td><td>最小：6.18ms 平均：141.02ms 最大：687.11ms 95分位数：257.95ms</td><td>最小：2.88ms 平均：123.52ms 最大：2916.81ms 95分位数：189.93ms</td></tr><tr><td></td><td>吞吐量</td><td>2128.0000</td><td>2429.9000</td></tr><tr><td></td><td></td><td></td><td></td></tr></tbody></table><h3 id="分析"><a href="#分析" class="headerlink" title="分析"></a>分析</h3><h4 id="纵向对比"><a href="#纵向对比" class="headerlink" title="纵向对比"></a>纵向对比</h4><p>经过预热之后，可以看到InnoDB的性能越来越好，每秒能处理2000多请求。不排除预热充分后能反超MyISAM的可能性，不过这个我就没继续多测试了。</p><p>MyISAM每秒能处理2500左右。</p><h4 id="横向对比"><a href="#横向对比" class="headerlink" title="横向对比"></a>横向对比</h4><p>从目前做的测试来看，InnoDB在读写性能上还是不及MyISAM，不排除预热充分后能反超MyISAM的可能性，不过这个我就没继续多测试了。</p><h2 id="其他"><a href="#其他" class="headerlink" title="其他"></a>其他</h2><h3 id="图形化"><a href="#图形化" class="headerlink" title="图形化"></a>图形化</h3><p>可以将sysbench压测结果输出日志、然后绘制成图片，这样看着更高大上、更加直观。可以参考这篇文章：<a href="https://yq.aliyun.com/articles/498680">https://yq.aliyun.com/articles/498680</a></p><h2 id="遇到的问题"><a href="#遇到的问题" class="headerlink" title="遇到的问题"></a>遇到的问题</h2><ol><li>本来想使用sysbench的预热功能，使用如下命令</li></ol><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sysbench --db-driver=mysql --mysql-host=localhost --mysql-port=3306 --mysql-user=root --mysql-password=1qaz@WSX --mysql-db=sbtest --table_size=1000000 --tables=10 --events=0 --time=300  --threads=20 --percentile=95 --report-interval=10 oltp_read_write prewarm</span><br></pre></td></tr></table></figure><p>但有报如下错误，时间有限，没仔细排查：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line">[root@VM_0_5_centos ~]# sysbench --db-driver=mysql --mysql-host=localhost --mysql-port=3306 --mysql-user=root --mysql-password=1qaz@WSX --mysql-db=sbtest --table_size=1000000 --tables=10 --events=0 --time=300  --threads=20 --percentile=95 --report-interval=10 oltp_read_write prewarm</span><br><span class="line">sysbench 1.0.20 (using bundled LuaJIT 2.1.0-beta2)</span><br><span class="line"></span><br><span class="line">Initializing worker threads...</span><br><span class="line"></span><br><span class="line">Prewarming table sbtest6</span><br><span class="line">Prewarming table sbtest7</span><br><span class="line">Prewarming table sbtest9</span><br><span class="line">Prewarming table sbtest8</span><br><span class="line">Prewarming table sbtest1</span><br><span class="line">Prewarming table sbtest5</span><br><span class="line">Prewarming table sbtest3</span><br><span class="line">Prewarming table sbtest10</span><br><span class="line">Prewarming table sbtest2</span><br><span class="line">Prewarming table sbtest4</span><br><span class="line">FATAL: mysql_drv_query() returned error 2013 (Lost connection to MySQL server during query) for query &#x27;SELECT AVG(id) FROM (SELECT * FROM sbtest2 FORCE KEY (PRIMARY) LIMIT 1000000) t&#x27;</span><br><span class="line">FATAL: mysql_drv_query() returned error 2013 (Lost connection to MySQL server during query) for query &#x27;SELECT AVG(id) FROM (SELECT * FROM sbtest3 FORCE KEY (PRIMARY) LIMIT 1000000) t&#x27;</span><br><span class="line">FATAL: mysql_drv_query() returned error 2013 (Lost connection to MySQL server during query) for query &#x27;SELECT AVG(id) FROM (SELECT * FROM sbtest4 FORCE KEY (PRIMARY) LIMIT 1000000) t&#x27;</span><br><span class="line">FATAL: mysql_drv_query() returned error 2013 (Lost connection to MySQL server during query) for query &#x27;SELECT AVG(id) FROM (SELECT * FROM sbtest5 FORCE KEY (PRIMARY) LIMIT 1000000) t&#x27;</span><br><span class="line">FATAL: mysql_drv_query() returned error 2013 (Lost connection to MySQL server during query) for query &#x27;SELECT AVG(id) FROM (SELECT * FROM sbtest6 FORCE KEY (PRIMARY) LIMIT 1000000) t&#x27;</span><br><span class="line">FATAL: mysql_drv_query() returned error 2013 (Lost connection to MySQL server during query) for query &#x27;SELECT AVG(id) FROM (SELECT * FROM sbtest7 FORCE KEY (PRIMARY) LIMIT 1000000) t&#x27;</span><br><span class="line">FATAL: mysql_drv_query() returned error 2013 (Lost connection to MySQL server during query) for query &#x27;SELECT AVG(id) FROM (SELECT * FROM sbtest8 FORCE KEY (PRIMARY) LIMIT 1000000) t&#x27;</span><br><span class="line">FATAL: mysql_drv_query() returned error 2013 (Lost connection to MySQL server during query) for query &#x27;SELECT AVG(id) FROM (SELECT * FROM sbtest9 FORCE KEY (PRIMARY) LIMIT 1000000) t&#x27;</span><br><span class="line">FATAL: mysql_drv_query() returned error 2013 (Lost connection to MySQL server during query) for query &#x27;SELECT AVG(id) FROM (SELECT * FROM sbtest10 FORCE KEY (PRIMARY) LIMIT 1000000) t&#x27;</span><br><span class="line">FATAL: mysql_drv_query() returned error 2013 (Lost connection to MySQL server during query) for query &#x27;SELECT AVG(id) FROM (SELECT * FROM sbtest1 FORCE KEY (PRIMARY) LIMIT 1000000) t&#x27;</span><br><span class="line">FATAL: `sysbench.cmdline.call_command&#x27; function failed: /usr/share/sysbench/oltp_common.lua:111: SQL error, errno = 2013, state = &#x27;HY000&#x27;: Lost connection to MySQL server during query</span><br></pre></td></tr></table></figure>]]>
    </content>
    <id>https://evasnowind.github.io/2020/11/26/2020-11-27_%E4%BD%BF%E7%94%A8sysbench%E5%AF%B9mysql%E8%BF%9B%E8%A1%8C%E5%9F%BA%E5%87%86%E6%B5%8B%E8%AF%95/</id>
    <link href="https://evasnowind.github.io/2020/11/26/2020-11-27_%E4%BD%BF%E7%94%A8sysbench%E5%AF%B9mysql%E8%BF%9B%E8%A1%8C%E5%9F%BA%E5%87%86%E6%B5%8B%E8%AF%95/"/>
    <published>2020-11-26T16:00:00.000Z</published>
    <summary>记录使用 sysbench 对 MySQL 进行基准测试的环境、常用命令和测试过程，适合做数据库性能摸底与对比。</summary>
    <title>使用sysbench对mysql进行基准测试</title>
    <updated>2026-09-04T04:44:28.356Z</updated>
  </entry>
  <entry>
    <author>
      <name>Prayer</name>
    </author>
    <category term="Java" scheme="https://evasnowind.github.io/categories/Java/"/>
    <category term="Spring" scheme="https://evasnowind.github.io/categories/Java/Spring/"/>
    <category term="Spring" scheme="https://evasnowind.github.io/tags/Spring/"/>
    <content>
      <![CDATA[<h2 id="现象"><a href="#现象" class="headerlink" title="现象"></a>现象</h2><p>示例：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">public class A &#123;</span><br><span class="line">    //......</span><br><span class="line">    </span><br><span class="line">    @Transactional</span><br><span class="line">    public void serviceA() &#123;</span><br><span class="line">        ......</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    public void serviceB() &#123;</span><br><span class="line">        ......</span><br><span class="line">        serviceA()</span><br><span class="line">        ......</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>大概多数人都会遇到这么一个坑，如果使用spring的@Transactional给A类中的某个业务方法serviceA()加事务，在controller层直接调用serviceA()时，该方法的事务注解能正常生效；但如果在A类中的serviceB()调用serviceA()，并且servcieB没有加事务@Transactional，那么此时实际上serviceA()上的@Transactional并未生效。</p><span id="more"></span><h2 id="原因"><a href="#原因" class="headerlink" title="原因"></a>原因</h2><p>想讲清这个问题就必须了解spring AOP的内部实现原理。</p><p>在spring中经常使用自定义注解或是spring已封装好的注解，通过AOP的方式复用代码、避免重复劳动。而spring实现AOP是通过动态代理来实现的，默认有接口的情况使用JDK的动态代理（也可以通过配置proxyTargetClass来指定使用CGLIB）、没有接口的情况使用CGLIB。</p><p>但无论是哪一种代理，都是在原有方法的外面包一层、在方法外的代理层来实现AOP的逻辑。以上面为例，在代理后会在serviceA()外层增加事务相关逻辑：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">//原始代码</span><br><span class="line">serviceA() &#123;</span><br><span class="line">......</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">//加上注解后</span><br><span class="line">serviceAProxy() &#123;</span><br><span class="line">//AOP执行前逻辑</span><br><span class="line">......</span><br><span class="line">serviceA()</span><br><span class="line">//AOP执行后逻辑</span><br><span class="line">......</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>但这个前提就是spring直接调用加了注解的方法才会调用代理方法，如果serviceA被同一个类内部其他方法调用，那servcieA()就只是一段普通代码、AOP相关的信息不会被spring看到，那自然就无法执行AOP的逻辑。</p><h2 id="解决"><a href="#解决" class="headerlink" title="解决"></a>解决</h2><p>知道原理后，自然也就好解决了。</p><p>AOP的逻辑在代理后的方法中，那么我们就去执行spring生成的代理后的方法。获取方法如下：</p><h3 id="方法1：通过当前类对象获取"><a href="#方法1：通过当前类对象获取" class="headerlink" title="方法1：通过当前类对象获取"></a>方法1：通过当前类对象获取</h3><p>这个有点取巧。还是以上面serviceB()为例，我们如果调用this.serviceA()，this指向对象本身、不会指向代理后的对象，因此肯定不可以，但我们可以让spring提供给我们代理后的对象：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">public class A&#123;</span><br><span class="line"></span><br><span class="line">    //通过spring将代理后对象注入到self变量</span><br><span class="line">    @Autowired</span><br><span class="line">    private A self;</span><br><span class="line"></span><br><span class="line">    public void serviceB() &#123;</span><br><span class="line">            ......</span><br><span class="line">            //此处调用的就是代理后的方法</span><br><span class="line">            self.serviceA()</span><br><span class="line">            ......</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="方法2-从ApplicationContext中直接获取"><a href="#方法2-从ApplicationContext中直接获取" class="headerlink" title="方法2 从ApplicationContext中直接获取"></a>方法2 从ApplicationContext中直接获取</h3><p>获取ApplicationContext有多种方法：</p><h4 id="方法2-1-使用AopContext-currentProxy"><a href="#方法2-1-使用AopContext-currentProxy" class="headerlink" title="方法2.1 使用AopContext.currentProxy()"></a>方法2.1 使用AopContext.currentProxy()</h4><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">public class A &#123;</span><br><span class="line">    public void serviceB() &#123;</span><br><span class="line">            ......</span><br><span class="line">            //此处调用的就是代理后的方法</span><br><span class="line">            ((A)AopContext.currentProxy()).serviceA();</span><br><span class="line">            ......</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>使用AopContext.currentProxy()注意必须在程序启动时开启EnableAspectJAutoProxy注解，设置代理暴露方式为true，如下面所示：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">/**</span><br><span class="line"> * EnableAspectJAutoProxy注解两个参数作用分别为：</span><br><span class="line"> *</span><br><span class="line"> * 一个是控制aop的具体实现方式,为true的话使用cglib,为false的话使用java的Proxy，默认为false。</span><br><span class="line"> * 第二个参数控制代理的暴露方式,解决内部调用不能使用代理的场景，默认为false。</span><br><span class="line"> *</span><br><span class="line"> *</span><br><span class="line"> */</span><br><span class="line">@EnableAspectJAutoProxy(proxyTargetClass = true, exposeProxy = true)</span><br><span class="line">@SpringBootApplication</span><br><span class="line">public class SpringAopApplication &#123;</span><br><span class="line"></span><br><span class="line">    public static void main(String[] args) &#123;</span><br><span class="line">        SpringApplication.run(SpringAopApplication.class, args);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="方法2-2-使用ApplicationContextAware"><a href="#方法2-2-使用ApplicationContextAware" class="headerlink" title="方法2.2 使用ApplicationContextAware"></a>方法2.2 使用ApplicationContextAware</h4><p>通过spring生命周期，直接将ApplicationContext注入进来：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">public class A implements ApplicationContextAware &#123;</span><br><span class="line">private ApplicationContext applicationContext;</span><br><span class="line">    </span><br><span class="line">    @Override</span><br><span class="line">    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException &#123;</span><br><span class="line">        this.applicationContext = applicationContext;</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    </span><br><span class="line">    public void serviceB() &#123;</span><br><span class="line">            ......</span><br><span class="line">            //此处调用的就是代理后的方法</span><br><span class="line">            applicationContext.getBean(A.class).serviceA();</span><br><span class="line">            ......</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>]]>
    </content>
    <id>https://evasnowind.github.io/2020/11/24/2020-11-25_spring%E4%B8%AD%E5%8A%A0%E6%B3%A8%E8%A7%A3%E6%96%B9%E6%B3%95%E8%A2%AB%E5%90%8C%E4%B8%80%E4%B8%AA%E7%B1%BB%E5%86%85%E9%83%A8%E6%96%B9%E6%B3%95%E8%B0%83%E7%94%A8%E5%AF%BC%E8%87%B4AOP%E5%A4%B1%E6%95%88%E7%9A%84%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88(%E5%A6%82@Transactional%E6%B3%A8%E8%A7%A3%E5%A4%B1%E6%95%88)/</id>
    <link href="https://evasnowind.github.io/2020/11/24/2020-11-25_spring%E4%B8%AD%E5%8A%A0%E6%B3%A8%E8%A7%A3%E6%96%B9%E6%B3%95%E8%A2%AB%E5%90%8C%E4%B8%80%E4%B8%AA%E7%B1%BB%E5%86%85%E9%83%A8%E6%96%B9%E6%B3%95%E8%B0%83%E7%94%A8%E5%AF%BC%E8%87%B4AOP%E5%A4%B1%E6%95%88%E7%9A%84%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88(%E5%A6%82@Transactional%E6%B3%A8%E8%A7%A3%E5%A4%B1%E6%95%88)/"/>
    <published>2020-11-24T16:00:00.000Z</published>
    <summary>解释同类内部调用为何会导致 @Transactional 等 AOP 注解失效，并基于 Spring 代理机制给出对应解决方案。</summary>
    <title>spring中加注解方法被同一个类内部方法调用导致AOP失效的解决方案(如@Transactional注解失效)</title>
    <updated>2026-09-04T04:44:28.356Z</updated>
  </entry>
  <entry>
    <author>
      <name>Prayer</name>
    </author>
    <category term="Java" scheme="https://evasnowind.github.io/categories/Java/"/>
    <category term="Spring" scheme="https://evasnowind.github.io/categories/Java/Spring/"/>
    <category term="Spring" scheme="https://evasnowind.github.io/tags/Spring/"/>
    <content>
      <![CDATA[<p>事情是这样子的：</p><p>我想自定义一个缓存注解，用来缓存方法返回值，并且支持自定义缓存超时时间，注解定义是这样：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line">@Retention(RetentionPolicy.RUNTIME)</span><br><span class="line">@Target(ElementType.METHOD)</span><br><span class="line">@Inherited</span><br><span class="line">public @interface MyCache &#123;</span><br><span class="line"></span><br><span class="line">    @AliasFor(&quot;value&quot;)</span><br><span class="line">    int expireTime() default 60;</span><br><span class="line"></span><br><span class="line">    @AliasFor(&quot;expireTime&quot;)</span><br><span class="line">    int value() default 60;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>本来只想写一个expireTime，但想着大家肯定能偷懒尽量偷懒嘛，那最好还是支持 <code>@MyCache(50)</code> 这种写法、省去一步。于是也有了上面的写法，使用spring提供的<code>@AliasFor</code>，显式的定义两个属性互为别名，目的就是如果不在注解中指定属性，则默认就是将值给到超时时间expireTime。</p><p>这样写也是看了spring里<code>@Transactional</code>注解的定义，按道理，应该是我配置了 <code>@MyCache(50)</code> 后，我在Advice代码中取这个注解的<code>expireTime</code>属性值时，应该是拿到<code>value</code>的值，即50，但实际效果却不是这样，<code>expireTime</code>的值仍为默认的60。</p><p>而我取值的代码如下：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line">@Around(&quot;myCache()&quot;)</span><br><span class="line">public Object around(ProceedingJoinPoint pjp) &#123;</span><br><span class="line">    ......</span><br><span class="line">    //获取方法对象</span><br><span class="line">    Method curMethod = getMethodByJoinPoint(pjp, methodSignature);</span><br><span class="line">    //出问题的是这一句！</span><br><span class="line">    Annotation annotation = curMethod.getAnnotation(MyCache.class);</span><br><span class="line">    Integer expireTime = (Integer) getAnnotationConfig(annotation, &quot;expireTime&quot;);</span><br><span class="line">    Integer annotationValue = (Integer) getAnnotationConfig(annotation, &quot;value&quot;);</span><br><span class="line">    log.info(&quot;annotationValue=&#123;&#125;, expireTime=&#123;&#125;.&quot;, annotationValue, expireTime);</span><br><span class="line">    ......</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">private Method getMethodByJoinPoint(ProceedingJoinPoint pjp, MethodSignature methodSignature) throws NoSuchMethodException &#123;</span><br><span class="line">    Object target = pjp.getTarget();</span><br><span class="line">    return target.getClass().getMethod(methodSignature.getName(), methodSignature.getParameterTypes());</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">private Object getAnnotationConfig(Annotation annotation, String name) &#123;</span><br><span class="line">    if (null == annotation || StringUtils.isEmpty(name)) &#123;</span><br><span class="line">        return null;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    try &#123;</span><br><span class="line">        return annotation.annotationType().getMethod(name).invoke(annotation);</span><br><span class="line">    &#125; catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) &#123;</span><br><span class="line">        log.error(&quot;getAnnotationConfig error:&quot;, e);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    return null;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>获取注解这一步有问题，<code>@AliasFor</code>是spring提供的注解，想要保证获取属性值能感知到，需要使用spring提供的工具方法获得注解，如下所示：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">......</span><br><span class="line">Annotation annotation2 = AnnotationUtils.getAnnotation(curMethod, MyCache.class);</span><br><span class="line">Integer annotationVal2 = (Integer) getAnnotationConfig(annotation2, &quot;expireTime&quot;);</span><br><span class="line">log.info(&quot;annotationVal2=&#123;&#125;.&quot;, annotationVal2);</span><br><span class="line">......</span><br></pre></td></tr></table></figure><span id="more"></span><h3 id="进一步延伸：如果注解里的两个属性相互加别名后，使用注解时两个都设置了值，会怎样？"><a href="#进一步延伸：如果注解里的两个属性相互加别名后，使用注解时两个都设置了值，会怎样？" class="headerlink" title="进一步延伸：如果注解里的两个属性相互加别名后，使用注解时两个都设置了值，会怎样？"></a>进一步延伸：如果注解里的两个属性相互加别名后，使用注解时两个都设置了值，会怎样？</h3><p>类似这样：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">@MyCache(expireTime = 40, value = 50)</span><br><span class="line">public String getValByCache(String key) &#123;</span><br><span class="line">    log.info(&quot;getValByCache running...&quot;);</span><br><span class="line">    return key + &quot;-&quot; + idx.incrementAndGet();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>**程序直接无法启动！**会报如下异常：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br></pre></td><td class="code"><pre><span class="line">......</span><br><span class="line">2020-11-20 16:54:18.628  INFO 12076 --- [  restartedMain] .e.DevToolsPropertyDefaultsPostProcessor : For additional web related logging consider setting the &#x27;logging.level.web&#x27; property to &#x27;DEBUG&#x27;</span><br><span class="line">2020-11-20 16:54:18.878  WARN 12076 --- [  restartedMain] ConfigServletWebServerApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.BeanDefinitionStoreException: Failed to read candidate component class: file [D:\GitRepository\framework-dev-learning\spring-aop-2-customzed-cache-annotation\target\classes\com\prayerlaputa\service\MyCacheDemoService.class]; nested exception is org.springframework.core.annotation.AnnotationConfigurationException: Different @AliasFor mirror values for annotation [com.prayerlaputa.annotation.MyCache] declared on com.prayerlaputa.service.MyCacheDemoService.getValByCache(java.lang.String); attribute &#x27;expireTime&#x27; and its alias &#x27;value&#x27; are declared with values of [40] and [50].</span><br><span class="line">2020-11-20 16:54:18.945 ERROR 12076 --- [  restartedMain] o.s.boot.SpringApplication               : Application run failed</span><br><span class="line"></span><br><span class="line">org.springframework.beans.factory.BeanDefinitionStoreException: Failed to read candidate component class: file [D:\GitRepository\framework-dev-learning\spring-aop-2-customzed-cache-annotation\target\classes\com\prayerlaputa\service\MyCacheDemoService.class]; nested exception is org.springframework.core.annotation.AnnotationConfigurationException: Different @AliasFor mirror values for annotation [com.prayerlaputa.annotation.MyCache] declared on com.prayerlaputa.service.MyCacheDemoService.getValByCache(java.lang.String); attribute &#x27;expireTime&#x27; and its alias &#x27;value&#x27; are declared with values of [40] and [50].</span><br><span class="line">    at org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider.scanCandidateComponents(ClassPathScanningCandidateComponentProvider.java:452) ~[spring-context-5.2.5.RELEASE.jar:5.2.5.RELEASE]</span><br><span class="line">    at org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider.findCandidateComponents(ClassPathScanningCandidateComponentProvider.java:315) ~[spring-context-5.2.5.RELEASE.jar:5.2.5.RELEASE]</span><br><span class="line">    at org.springframework.context.annotation.ClassPathBeanDefinitionScanner.doScan(ClassPathBeanDefinitionScanner.java:276) ~[spring-context-5.2.5.RELEASE.jar:5.2.5.RELEASE]</span><br><span class="line">    at org.springframework.context.annotation.ComponentScanAnnotationParser.parse(ComponentScanAnnotationParser.java:132) ~[spring-context-5.2.5.RELEASE.jar:5.2.5.RELEASE]</span><br><span class="line">    at org.springframework.context.annotation.ConfigurationClassParser.doProcessConfigurationClass(ConfigurationClassParser.java:295) ~[spring-context-5.2.5.RELEASE.jar:5.2.5.RELEASE]</span><br><span class="line">    at org.springframework.context.annotation.ConfigurationClassParser.processConfigurationClass(ConfigurationClassParser.java:249) ~[spring-context-5.2.5.RELEASE.jar:5.2.5.RELEASE]</span><br><span class="line">    at org.springframework.context.annotation.ConfigurationClassParser.parse(ConfigurationClassParser.java:206) ~[spring-context-5.2.5.RELEASE.jar:5.2.5.RELEASE]</span><br><span class="line">    at org.springframework.context.annotation.ConfigurationClassParser.parse(ConfigurationClassParser.java:174) ~[spring-context-5.2.5.RELEASE.jar:5.2.5.RELEASE]</span><br><span class="line">    at org.springframework.context.annotation.ConfigurationClassPostProcessor.processConfigBeanDefinitions(ConfigurationClassPostProcessor.java:319) ~[spring-context-5.2.5.RELEASE.jar:5.2.5.RELEASE]</span><br><span class="line">    at org.springframework.context.annotation.ConfigurationClassPostProcessor.postProcessBeanDefinitionRegistry(ConfigurationClassPostProcessor.java:236) ~[spring-context-5.2.5.RELEASE.jar:5.2.5.RELEASE]</span><br><span class="line">    at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanDefinitionRegistryPostProcessors(PostProcessorRegistrationDelegate.java:275) ~[spring-context-5.2.5.RELEASE.jar:5.2.5.RELEASE]</span><br><span class="line">    at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(PostProcessorRegistrationDelegate.java:95) ~[spring-context-5.2.5.RELEASE.jar:5.2.5.RELEASE]</span><br><span class="line">    at org.springframework.context.support.AbstractApplicationContext.invokeBeanFactoryPostProcessors(AbstractApplicationContext.java:706) ~[spring-context-5.2.5.RELEASE.jar:5.2.5.RELEASE]</span><br><span class="line">    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:532) ~[spring-context-5.2.5.RELEASE.jar:5.2.5.RELEASE]</span><br><span class="line">    at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:141) ~[spring-boot-2.2.6.RELEASE.jar:2.2.6.RELEASE]</span><br><span class="line">    at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:747) [spring-boot-2.2.6.RELEASE.jar:2.2.6.RELEASE]</span><br><span class="line">    at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:397) [spring-boot-2.2.6.RELEASE.jar:2.2.6.RELEASE]</span><br><span class="line">    at org.springframework.boot.SpringApplication.run(SpringApplication.java:315) [spring-boot-2.2.6.RELEASE.jar:2.2.6.RELEASE]</span><br><span class="line">    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1226) [spring-boot-2.2.6.RELEASE.jar:2.2.6.RELEASE]</span><br><span class="line">    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1215) [spring-boot-2.2.6.RELEASE.jar:2.2.6.RELEASE]</span><br><span class="line">    at com.prayerlaputa.SpringAopApplication.main(SpringAopApplication.java:10) [classes/:na]</span><br><span class="line">    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_161]</span><br><span class="line">    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_161]</span><br><span class="line">    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_161]</span><br><span class="line">    at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_161]</span><br><span class="line">    at org.springframework.boot.devtools.restart.RestartLauncher.run(RestartLauncher.java:49) [spring-boot-devtools-2.2.6.RELEASE.jar:2.2.6.RELEASE]</span><br><span class="line">Caused by: org.springframework.core.annotation.AnnotationConfigurationException: Different @AliasFor mirror values for annotation [com.prayerlaputa.annotation.MyCache] declared on com.prayerlaputa.service.MyCacheDemoService.getValByCache(java.lang.String); attribute &#x27;expireTime&#x27; and its alias &#x27;value&#x27; are declared with values of [40] and [50].</span><br><span class="line">    at org.springframework.core.annotation.AnnotationTypeMapping$MirrorSets$MirrorSet.resolve(AnnotationTypeMapping.java:656) ~[spring-core-5.2.5.RELEASE.jar:5.2.5.RELEASE]</span><br><span class="line">    at org.springframework.core.annotation.AnnotationTypeMapping$MirrorSets.resolve(AnnotationTypeMapping.java:611) ~[spring-core-5.2.5.RELEASE.jar:5.2.5.RELEASE]</span><br><span class="line">    at org.springframework.core.annotation.TypeMappedAnnotation.&lt;init&gt;(TypeMappedAnnotation.java:136) ~[spring-core-5.2.5.RELEASE.jar:5.2.5.RELEASE]</span><br><span class="line">    at org.springframework.core.annotation.TypeMappedAnnotation.&lt;init&gt;(TypeMappedAnnotation.java:120) ~[spring-core-5.2.5.RELEASE.jar:5.2.5.RELEASE]</span><br><span class="line">    at org.springframework.core.annotation.TypeMappedAnnotation.of(TypeMappedAnnotation.java:636) ~[spring-core-5.2.5.RELEASE.jar:5.2.5.RELEASE]</span><br><span class="line">    at org.springframework.core.annotation.MergedAnnotation.of(MergedAnnotation.java:596) ~[spring-core-5.2.5.RELEASE.jar:5.2.5.RELEASE]</span><br><span class="line">    at org.springframework.core.type.classreading.MergedAnnotationReadingVisitor.visitEnd(MergedAnnotationReadingVisitor.java:96) ~[spring-core-5.2.5.RELEASE.jar:5.2.5.RELEASE]</span><br><span class="line">    at org.springframework.asm.ClassReader.readElementValues(ClassReader.java:2985) ~[spring-core-5.2.5.RELEASE.jar:5.2.5.RELEASE]</span><br><span class="line">    at org.springframework.asm.ClassReader.readMethod(ClassReader.java:1393) ~[spring-core-5.2.5.RELEASE.jar:5.2.5.RELEASE]</span><br><span class="line">    at org.springframework.asm.ClassReader.accept(ClassReader.java:718) ~[spring-core-5.2.5.RELEASE.jar:5.2.5.RELEASE]</span><br><span class="line">    at org.springframework.asm.ClassReader.accept(ClassReader.java:401) ~[spring-core-5.2.5.RELEASE.jar:5.2.5.RELEASE]</span><br><span class="line">    at org.springframework.core.type.classreading.SimpleMetadataReader.&lt;init&gt;(SimpleMetadataReader.java:50) ~[spring-core-5.2.5.RELEASE.jar:5.2.5.RELEASE]</span><br><span class="line">    at org.springframework.core.type.classreading.SimpleMetadataReaderFactory.getMetadataReader(SimpleMetadataReaderFactory.java:103) ~[spring-core-5.2.5.RELEASE.jar:5.2.5.RELEASE]</span><br><span class="line">    at org.springframework.core.type.classreading.CachingMetadataReaderFactory.getMetadataReader(CachingMetadataReaderFactory.java:123) ~[spring-core-5.2.5.RELEASE.jar:5.2.5.RELEASE]</span><br><span class="line">    at org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider.scanCandidateComponents(ClassPathScanningCandidateComponentProvider.java:428) ~[spring-context-5.2.5.RELEASE.jar:5.2.5.RELEASE]</span><br><span class="line">    ... 25 common frames omitted</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">Process finished with exit code 0</span><br><span class="line"></span><br></pre></td></tr></table></figure><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><ul><li><a href="https://zhuanlan.zhihu.com/p/74471219">Spring 注解编程之注解属性别名与覆盖</a></li></ul>]]>
    </content>
    <id>https://evasnowind.github.io/2020/11/19/2020-11-20_spring_%E8%87%AA%E5%AE%9A%E4%B9%89%E6%B3%A8%E8%A7%A3%E6%97%B6%E4%BD%BF%E7%94%A8AliasFor%E6%B3%A8%E8%A7%A3%E5%88%AB%E5%90%8D%E6%97%B6_%E9%BB%98%E8%AE%A4%E5%80%BC%E6%97%A0%E6%B3%95%E8%A2%AB%E8%A6%86%E7%9B%96/</id>
    <link href="https://evasnowind.github.io/2020/11/19/2020-11-20_spring_%E8%87%AA%E5%AE%9A%E4%B9%89%E6%B3%A8%E8%A7%A3%E6%97%B6%E4%BD%BF%E7%94%A8AliasFor%E6%B3%A8%E8%A7%A3%E5%88%AB%E5%90%8D%E6%97%B6_%E9%BB%98%E8%AE%A4%E5%80%BC%E6%97%A0%E6%B3%95%E8%A2%AB%E8%A6%86%E7%9B%96/"/>
    <published>2020-11-19T16:00:00.000Z</published>
    <summary>分析 @AliasFor 互为别名时默认值为何看似未生效，并说明如何使用 Spring 提供的注解工具正确读取属性值。</summary>
    <title>spring 自定义注解时使用AliasFor注解别名时 默认值无法被覆盖</title>
    <updated>2026-09-04T04:44:28.356Z</updated>
  </entry>
  <entry>
    <author>
      <name>Prayer</name>
    </author>
    <category term="架构" scheme="https://evasnowind.github.io/categories/%E6%9E%B6%E6%9E%84/"/>
    <category term="分布式" scheme="https://evasnowind.github.io/tags/%E5%88%86%E5%B8%83%E5%BC%8F/"/>
    <content>
      <![CDATA[<h2 id="Raft基础"><a href="#Raft基础" class="headerlink" title="Raft基础"></a>Raft基础</h2><p>在raft中，服务器节点共有3种状态（或者说角色）：</p><ul><li><p>leader</p><ul><li>负责client交互和log复制，同一时刻系统中最多存在1个。</li></ul></li><li><p>follower</p><ul><li>节点的初始状态就是follower，follower只能被动响应请求RPC，从不主动发起请求RPC。</li></ul></li></ul><span id="more"></span><ul><li><p>如果follower在一定时间范围（后面会讲，这个叫election timeout）内没有收到leader的请求，则follower可以转变成candidate（If followers don’t hear from a leader then they can become a candidate.）</p></li><li><p>candidate</p><ul><li>一个临时的状态，只存在于选举leader阶段。一个节点想要变成leader，那么就发起投票请求，同时自己变成candidate。如果选举成功，则变为candidate，否则退回为follower。其他节点根据当前状态回复是否，如果已经投票给其他candidate，则不会再投票给这个candidate（一个节点只有一票）。</li><li>获得多数选票的candidate（n&#x2F;2+1）将会变成leader。</li></ul></li></ul><p>3种状态的流转参见此图：</p><p><img src="/images/raft-state-change-300x125_54bf77b5.png"></p><h3 id="Raft关键问题"><a href="#Raft关键问题" class="headerlink" title="Raft关键问题"></a>Raft关键问题</h3><ul><li>领导选取</li><li>日志复制</li><li>安全</li><li>成员变化</li></ul><p>Raft采用复制状态机(state machine replication)模型，通过复制日志来保证状态的一致性，详细内容参见wiki <a href="https://en.wikipedia.org/wiki/State_machine_replication">https://en.wikipedia.org/wiki/State_machine_replication</a> 基本思想是：</p><ul><li>日志：每台机器保存一份日志，日志来自于客户端的请求，包含一系列的命令</li><li>状态机：状态机会按顺序执行这些命令</li><li>一致性模型：分布式环境下，保证多机的日志是一致的，这样回放到状态机中的状态是一致的</li></ul><p>复制状态机机制可以参考这张图：</p><p><img src="/images/replicated-state-machine_0a99b804.jpg"></p><p><img src="/images/replicated-state-machine2_df26b508.jpg"></p><h2 id="Raft算法流程"><a href="#Raft算法流程" class="headerlink" title="Raft算法流程"></a>Raft算法流程</h2><p>先看 动画演示 <a href="http://thesecretlivesofdata.com/raft/">http://thesecretlivesofdata.com/raft/</a></p><h3 id="领导选举-leader-election"><a href="#领导选举-leader-election" class="headerlink" title="领导选举 leader election"></a>领导选举 leader election</h3><p>Raft中使用心跳机制来出发leader选举。当服务器启动的时候，服务器成为follower。只要follower从leader或者candidate收到有效的RPCs就会保持follower状态。如果follower在一段时间内（该段时间被称为election timeout）没有收到消息，则它会假设当前没有可用的leader，然后开启选举新leader的流程。</p><h4 id="1-任期-term"><a href="#1-任期-term" class="headerlink" title="1. 任期 term"></a>1. 任期 term</h4><p>Raft算法将时间划分成为任意不同长度的任期（term），任期用连续的数字进行表示。每一个任期的开始都是一次选举（election），一个或多个候选人会试图成为领导人。如果一个候选人赢得了选举，它就会在该任期的剩余时间担任领导人。在某些情况下，选票会被瓜分，有可能没有选出领导人，那么，将会开始另一个任期，并且立刻开始下一次选举。Raft 算法保证在给定的一个任期最多只有一个领导人。</p><h4 id="2-RPC"><a href="#2-RPC" class="headerlink" title="2. RPC"></a>2. RPC</h4><p>Raft 算法中服务器节点之间通信使用远程过程调用（RPCs），并且基本的一致性算法只需要两种类型的 RPCs，为了在服务器之间传输快照增加了第三种 RPC。</p><p>RPC有三种：</p><ul><li>RequestVote RPC：候选人在选举期间发起</li><li>AppendEntries RPC：领导人发起的一种心跳机制，复制日志也在该命令中完成</li><li>InstallSnapshot RPC: 领导者使用该RPC来发送快照给太落后的追随者</li></ul><h4 id="3-选举流程说明"><a href="#3-选举流程说明" class="headerlink" title="3. 选举流程说明"></a>3. 选举流程说明</h4><p>（1）follower增加当前的term，转变为candidate。 （2）candidate投票给自己，并发送RequestVote RPC给集群中的其他服务器。 （3）收到RequestVote的服务器，在同一term中只会按照先到先得投票给至多一个candidate。且只会投票给log至少和自身一样新的candidate。</p><p>一个节点node1发起选举后，会发生如下3种情况中的一种：</p><ul><li>a. 该节点赢得选举。即收到大多数的节点的投票。则node1转变为leader状态。</li><li>b. 另一个节点node2成为了leader。即收到了leader的合法心跳包（term值等于或大于当前自身term值）。则node1转变为follower状态。</li><li>c. 一段时间后依然没有胜者。该种情况下会开启新一轮的选举。</li></ul><h4 id="4-2个超时时间：election-timeout和heartbeat-timeout"><a href="#4-2个超时时间：election-timeout和heartbeat-timeout" class="headerlink" title="4. 2个超时时间：election timeout和heartbeat timeout"></a>4. 2个超时时间：election timeout和heartbeat timeout</h4><h5 id="election-timeout"><a href="#election-timeout" class="headerlink" title="election timeout"></a>election timeout</h5><p>从follower状态成为candidate状态需要等待的时间。在这个时间内，节点必须等待，不能成为candidate状态。</p><p><strong>该超时时长随机</strong>，在150ms-300ms之间，超时后，follower将成为candidate，启动一个新的election term，并投自己一票，并发送request vote 给其他节点。如果接收到的节点在当前term中还未投票（vote），就会将投票给这个candidate，并重置自己的election timeout。</p><h5 id="heartbeat-timeout"><a href="#heartbeat-timeout" class="headerlink" title="heartbeat timeout"></a>heartbeat timeout</h5><p>心跳超时时间。candidate获得多数选票后将会变成leader，leader将开始发送append entries消息给它的follower，这是一个心跳消息，可能为空，也可能带有需要复制的日志数据，这类消息在heartbeat timeout时间间隔内发送；follower将会响应每个append entries。如果超过心跳超时时间内，follower没收到leader的消息，将会认为leader挂掉、会开始新的选举周期。</p><p>有关append entries的解析，可以参考这篇：<a href="https://blog.csdn.net/chicm/article/details/41909261">Raft系列文章之三：Raft RPC详解</a></p><p>另一方面，这也说明，日志数据实际上都是跟着心跳消息发送给follower的，并不是说client发起一个修改、leader就立马开始同步数据。参见下文“日志复制”相关。</p><h4 id="5-问题：split-vote"><a href="#5-问题：split-vote" class="headerlink" title="5. 问题：split vote"></a>5. 问题：split vote</h4><p>上述”3.选举流程”中的情况c.，最典型的就是<code>split vote</code>，当两个节点同时转变为candidate、发起选举时，就会出现该情况。</p><p>假设4个节点的情况，A B分别发起选举，两个节点在同一个term中启动election，并且这两个vote都在对方之前抵达一个单独的节点，此时A B分别拿到2个投票（包括自己投自己的那一票）、且在这个term中不会有更多votes，两个节点都没拿到多数选票。这些节点将会等待、在下个term中启动选举。由于选举超时时间(election timeout)是随机的，这两个candidate节点不可能每次election timeout时间都相同，也就可以保证这个选举过程最终会选出一个leader。</p><p><img src="/images/raft-split-vote_d07719fd.png"></p><h3 id="日志复制-log-replication"><a href="#日志复制-log-replication" class="headerlink" title="日志复制 log replication"></a>日志复制 log replication</h3><p>日志复制（Log Replication）主要作用是用于保证节点的一致性，这阶段所做的操作也是为了保证一致性与高可用性。</p><p>注意，在Raft中的日志，是指可能引起状态变化的操作，查看操作不引起状态变化、并不包含在下面讨论的日志范围内。</p><p>当leader选举出来后便开始负责客户端的请求，所有事务（更新操作）请求都必须先经过leader处理，日志复制（Log Replication）就是为了保证执行相同的操作序列所做的工作。</p><p>leader需要将所有数据变化复制给系统中所有节点。这需要用到在维持心跳中用到的append entries消息。</p><p>整体流程如下：</p><ul><li>在Raft中当leader接收到客户端的日志（事务请求）后先把该日志追加到本地的log中，此时的日志是uncommitted的，并不会更新节点值；</li><li>接着，leader节点将复制这条日志给所有follower节点（数据会通过下一个heartbeat中append entry消息发送给所有follower），follower接收到日志后记录日志然后向leader发送ACK；</li><li>当leader收到大多数（n&#x2F;2+1）节点的ACK信息后，leader将这条日志设置为committed、将日志应用到本地的状态机（根据client请求修改状态），并返回给client。</li></ul><p>可以参考这篇<a href="https://www.jianshu.com/p/b28e73eefa88">Raft 日志复制 Log replication</a></p><h3 id="网络分区问题与解决-network-partition"><a href="#网络分区问题与解决-network-partition" class="headerlink" title="网络分区问题与解决 network partition"></a>网络分区问题与解决 network partition</h3><p>Raft在面对网络分区问题时仍能保证一致性。 假设，由于网络分区，导致一个Raft集群分成了两部分，有2个leader</p><p><img src="/images/raft-network-partition_e0456c40.png"></p><p>此时若有client去更新数据，如果分区内能够达到整个集群的多数，就能commit成功，但可能出现以给一个分区提交成功、另外一个分区的节点无感知。 如</p><p><img src="/images/raft-network-partition-modify-value_4d7cf158.png"></p><p>故障恢复时，重新接入的分区会比较election term，比较高版本的leader的日志，使得自身的版本与新版本一致</p><p><img src="/images/raft-network-partition-heal_ffa350ee.png"></p><p>这样整个集群将会恢复成一致的状态。</p><h2 id="Raft的算法实现与应用"><a href="#Raft的算法实现与应用" class="headerlink" title="Raft的算法实现与应用"></a>Raft的算法实现与应用</h2><p>Raft算法的论文相比Paxos直观很多，更容易在工程上实现。</p><p>目前Raft算法实现已经很多，参见: <a href="https://raft.github.io/#implementations">https://raft.github.io/#implementations</a></p><h3 id="应用场景"><a href="#应用场景" class="headerlink" title="应用场景"></a>应用场景</h3><p>典型的如etcd、nacos等。</p><p>etcd目标是构建一个高可用的分布式键值（key-value）数据库，基于 Go 语言实现。Etcd 主要用途是共享配置和服务发现，实现一致性使用了Raft算法。</p><p>etcd参见<a href="https://etcd.io/">https://etcd.io/</a></p><h2 id="其他"><a href="#其他" class="headerlink" title="其他"></a>其他</h2><p>本文主要参考、整理自 <a href="https://www.cnblogs.com/binyue/p/8647733.html">https://www.cnblogs.com/binyue/p/8647733.html</a> 这篇文章还有一些内容不错，引用如下：</p><blockquote><h2 id="三、Raft和Paxos的工程应用"><a href="#三、Raft和Paxos的工程应用" class="headerlink" title="三、Raft和Paxos的工程应用"></a>三、Raft和Paxos的工程应用</h2><p>……</p><h4 id="2-Zookeeper-中的-Paxos"><a href="#2-Zookeeper-中的-Paxos" class="headerlink" title="2.Zookeeper 中的 Paxos"></a>2.Zookeeper 中的 Paxos</h4><p>Zookeeper 使用了一种修改后的 Paxos 协议。</p><p>在 Zookeeper 中，始终分为两种场景:</p><ul><li>Leader activation</li></ul><p>在这个场景里，系统中缺乏 Leader(primary)，通过一个类似 paxos 协议的过程完成 Leader 选举。</p><ul><li>Active messaging 在 这个场景里，Leader 接收客户端发送的更新操作，以一种类似两阶段提交的过程在各个 follower (secondary)节点上进行更新操作。</li></ul><p>在 Leader activation 场景中完成 leader 选举及数据同步后，系统转入 Active messaging 场景，在 active messaging 中 leader 异常后，系统转入 Leader activation 场景。</p><p>无论在那种场景，Zookeeper 依赖于一个全局版本号:zxid。zxid 由(epoch, count)两部分组成， 高位的 epoch 部分是选举编号，每次提议进行新的 leader 选举时 epoch 都会增加，低位的 count 部分 是 leader 为每个更新操作决定的序号。可以认为，一个 leader 对应一个唯一的 epoch，每个 leader 任期内产生的更新操作对应一个唯一的有序的 count，从而从全局的视野，一个 zxid 代表了一个更新操作的全局序号(版本号)。</p><p>Zookeeper 通过 zxid 将两个场景阶段较好的结合起来，且能保证全局的强一致性。由于同一时刻只有一个 zookeeper 节点能获得超过半数的 follower，所以同一时刻最多只存在唯一的 leader;每个 leader 利用 FIFO 以 zxid 顺序更新各个 follower，只有成功完成前一个更新操作的才会进行下一个更新操作，在同一个 leader 任期内，数据在全局满足 quorum 约束的强一致，即读超过半数的节点 一定可以读到最新已提交的数据;每个成功的更新操作都至少被超过半数的节点确认，使得新选举 的 leader 一定可以包括最新的已成功提交的数据。</p><h4 id="3-如何解决split-brain问题"><a href="#3-如何解决split-brain问题" class="headerlink" title="3.如何解决split brain问题"></a>3.如何解决split brain问题</h4><p>分布式协议一个著名问题就是 split brain 问题。</p><p>简单说，就是比如当你的 cluster 里面有两个结点，它们都知道在这个 cluster 里需要选举出一个 master。那么当它们两之间的通信完全没有问题的时候，就会达成共识，选出其中一个作为 master。但是如果它们之间的通信出了问题，那么两个结点都会觉得现在没有 master，所以每个都把自己选举成 master。于是 cluster 里面就会有两个 master。</p><p>区块链的分叉其实类似分布式系统的split brain。</p><p>一般来说，Zookeeper会默认设置：</p><ul><li>zookeeper cluster的节点数目必须是奇数。</li><li>zookeeper 集群中必须超过半数节点(Majority)可用，整个集群才能对外可用。</li></ul><p>Majority 就是一种 Qunroms 的方式来支持Leader选举，可以防止 split brain出现。奇数个节点可以在相同容错能力的情况下节省资源。</p><h2 id="四、从CAP的角度理解几种不同的算法"><a href="#四、从CAP的角度理解几种不同的算法" class="headerlink" title="四、从CAP的角度理解几种不同的算法"></a>四、从CAP的角度理解几种不同的算法</h2><h4 id="1-两阶段提交协议"><a href="#1-两阶段提交协议" class="headerlink" title="1.两阶段提交协议"></a>1.两阶段提交协议</h4><p>两阶段提交系统具有完全的C，很糟糕的A，很糟糕的P。 首先，两阶段提交协议保证了副本间是完全一致的，这也是协议的设计目的。再者，协议在一个节点出现异常时，就无法更新数据，其服务可用性较低。最后，一旦协调者与参与者之间网络分化，无法提供服务。</p><h4 id="2-Paxos和Raft算法"><a href="#2-Paxos和Raft算法" class="headerlink" title="2.Paxos和Raft算法"></a>2.Paxos和Raft算法</h4><p>Paxos 协议和Raft算法都是强一致性协议。Paxos只有两种情况下服务不可用:一是超过半数的 Proposer 异常，二是出现活锁。前者可以通过增加 Proposer 的个数来 降低由于 Proposer 异常影响服务的概率，后者本身发生的概率就极低。最后，只要能与超过半数的 Proposer 通信就可以完成协议流程，协议本身具有较好的容忍网络分区的能力。</p><p>参考 <a href="https://blog.csdn.net/cszhouwei/article/details/38374603">Raft一致性算法</a> <a href="http://www.infoq.com/cn/articles/raft-paper">Raft 一致性算法论文译文</a></p></blockquote><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><ul><li><a href="https://www.cnblogs.com/binyue/p/8647733.html">从分布式一致性到共识机制（二）Raft算法</a></li><li><a href="https://www.hellodemos.com/hello-raft-text/raft-text-raft-overtime.html">Raft协议中的两种超时</a></li><li><a href="https://blog.csdn.net/chicm/article/details/41909261">Raft系列文章之三：Raft RPC详解</a></li><li><a href="https://www.jianshu.com/p/b28e73eefa88">Raft 日志复制 Log replication</a></li></ul>]]>
    </content>
    <id>https://evasnowind.github.io/2020/10/21/2020-10-22_%E5%88%86%E5%B8%83%E5%BC%8F%E4%B8%80%E8%87%B4%E6%80%A7%E4%B8%8E%E5%85%B1%E8%AF%86%E4%B9%8B%EF%BC%88%E4%B8%80%EF%BC%89Raft/</id>
    <link href="https://evasnowind.github.io/2020/10/21/2020-10-22_%E5%88%86%E5%B8%83%E5%BC%8F%E4%B8%80%E8%87%B4%E6%80%A7%E4%B8%8E%E5%85%B1%E8%AF%86%E4%B9%8B%EF%BC%88%E4%B8%80%EF%BC%89Raft/"/>
    <published>2020-10-21T16:00:00.000Z</published>
    <summary>在raft中，服务器节点共有3种状态（或者说角色）：。</summary>
    <title>分布式一致性与共识之（一）Raft</title>
    <updated>2026-09-04T04:44:28.356Z</updated>
  </entry>
  <entry>
    <author>
      <name>Prayer</name>
    </author>
    <category term="Java" scheme="https://evasnowind.github.io/categories/Java/"/>
    <category term="Spring Cloud" scheme="https://evasnowind.github.io/categories/Java/Spring-Cloud/"/>
    <category term="源码分析" scheme="https://evasnowind.github.io/tags/%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/"/>
    <category term="Spring Cloud" scheme="https://evasnowind.github.io/tags/Spring-Cloud/"/>
    <content>
      <![CDATA[<h2 id="说明"><a href="#说明" class="headerlink" title="说明"></a>说明</h2><p>若无特别说明，本文所涉及的代码版本为spring boot&#x2F;cloud 2.2.6。</p><h2 id="eureka-server基本功能"><a href="#eureka-server基本功能" class="headerlink" title="eureka server基本功能"></a>eureka server基本功能</h2><ul><li>接受服务注册</li><li>接受服务心跳</li><li>服务剔除</li><li>服务下线</li><li>集群同步</li><li>获取注册表中服务实例信息</li></ul><span id="more"></span><p>需要注意的是，Eureka Server同时也是一个Eureka Client，在不禁止Eureka Server的客户端行为时，它会向它配置文件中的其他Eureka Server进行拉取注册表、服务注册和发送心跳等操作</p><p>eureka架构放一张图，镇楼</p><p><img src="/images/eureka-architecture-1-1024x548_df56eaab.png"></p><h3 id="启动eureka-server注册相关bean"><a href="#启动eureka-server注册相关bean" class="headerlink" title="启动eureka-server注册相关bean"></a>启动eureka-server注册相关bean</h3><p>与eureka-client类似，eureka-server启动也是依赖spring factories机制初始化、并将对象加载到spring容器中。</p><p>以<code>spring-cloud-netflix-eureka-server-2.2.2.REALEASE.jar</code>为例，打开该包中的spring.factories，可以看到</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">org.springframework.boot.autoconfigure.EnableAutoConfiguration=\</span><br><span class="line">  org.springframework.cloud.netflix.eureka.server.EurekaServerAutoConfiguration</span><br></pre></td></tr></table></figure><p>即启动时自动加载EurekaServerAutoConfiguration类，而该类的功能是向spring的bean工厂添加eureka-server相关功能的bean。</p><p>EurekaServerAutoConfiguration上有一个注解：@ConditionalOnBean(EurekaServerMarkerConfiguration.Marker.class)，意思是：只有在Spring容器里有Marker这个类的实例时，才会加载EurekaServerAutoConfiguration，这个就是控制是否开启Eureka Server的关键。</p><h3 id="开启eureka-server"><a href="#开启eureka-server" class="headerlink" title="开启eureka server"></a>开启eureka server</h3><p>在使用eureka-server时，我们所加的<code>@EnableEurekaServer</code>注解，实现如下：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">@Target(ElementType.TYPE)</span><br><span class="line">@Retention(RetentionPolicy.RUNTIME)</span><br><span class="line">@Documented</span><br><span class="line">@Import(EurekaServerMarkerConfiguration.class)</span><br><span class="line">public @interface EnableEurekaServer &#123;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>此处利用spring提供的<code>@Import</code>注解，将<code>EurekaServerMarkerConfiguration</code>动态注入到spring 容器中，这也刚好提供了上面spring factories里<code>EurekaServerAutoConfiguration</code>实例化的前提：必须有<code>EurekaServerMarkerConfiguration</code>。</p><p>即启动时初始化顺序：</p><ul><li>添加<code>EnableEurekaServer</code>注解后，通过@Import引入<code>EurekaServerMarkerConfiguration</code></li><li>利用spring factories，加载<code>EurekaServerAutoConfiguration</code></li></ul><h3 id="开启注册"><a href="#开启注册" class="headerlink" title="开启注册"></a>开启注册</h3><p>在spring-cloud-netflix-eureka-server:2.2.2.RELEASE中，<code>EurekaServerAutoConfiguration</code>声明如下：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">@Configuration(proxyBeanMethods = false)</span><br><span class="line">@Import(EurekaServerInitializerConfiguration.class)</span><br><span class="line">@ConditionalOnBean(EurekaServerMarkerConfiguration.Marker.class)</span><br><span class="line">@EnableConfigurationProperties(&#123; EurekaDashboardProperties.class,</span><br><span class="line">        InstanceRegistryProperties.class &#125;)</span><br><span class="line">@PropertySource(&quot;classpath:/eureka/server.properties&quot;)</span><br><span class="line">public class EurekaServerAutoConfiguration implements WebMvcConfigurer &#123;</span><br><span class="line">    ......</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>其中，通过@Import引入的<code>EurekaServerInitializerConfiguration</code>类声明如下：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">@Configuration(proxyBeanMethods = false)</span><br><span class="line">public class EurekaServerInitializerConfiguration</span><br><span class="line">        implements ServletContextAware, SmartLifecycle, Ordered &#123;</span><br><span class="line">    ......</span><br><span class="line">&#125;</span><br><span class="line">    </span><br></pre></td></tr></table></figure><p>SmartLifecycle的作用是：初始化完之后，执行public void start()方法。</p><p>而<code>EurekaServerInitializerConfiguration</code>中start()方法：</p><ul><li>启动一个线程；</li><li>上下文初始化</li><li>利用spring的事件机制，发布一个EurekaRegistryAvailableEvent事件</li><li>更改running状态</li><li>利用spring的事件机制，发布一个EurekaServerStartedEvent事件</li></ul><p>在这个start() 上下文初始化这一步，还会从相邻eureka节点注册表，方法调用路径如下：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">EurekaServerInitializerConfiguration#start()</span><br><span class="line">    EurekaServerBoostrap#contextInitialized(ServletContext context)</span><br><span class="line">        EurekaServerBoostrap#initEurekaServerContext()</span><br><span class="line">            // Copy registry from neighboring eureka node  此处进行复制相邻eureka注册表的操作</span><br><span class="line">            int registryCount = this.registry.syncUp();</span><br></pre></td></tr></table></figure><p>在EurekaServerAutoConfiguration中会实例化<code>EurekaServerContext</code>，代码如下：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">@Bean</span><br><span class="line">@ConditionalOnMissingBean</span><br><span class="line">public EurekaServerContext eurekaServerContext(ServerCodecs serverCodecs,</span><br><span class="line">        PeerAwareInstanceRegistry registry, PeerEurekaNodes peerEurekaNodes) &#123;</span><br><span class="line">    return new DefaultEurekaServerContext(this.eurekaServerConfig, serverCodecs,</span><br><span class="line">            registry, peerEurekaNodes, this.applicationInfoManager);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>DefaultEurekaServerContext</code>中初始化代码如下：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">@PostConstruct</span><br><span class="line">@Override</span><br><span class="line">public void initialize() &#123;</span><br><span class="line">    logger.info(&quot;Initializing ...&quot;);</span><br><span class="line">    //启动一个只拥有一个线程的线程池，第一次进去会更新一下集群其他节点信息。</span><br><span class="line">    peerEurekaNodes.start();</span><br><span class="line">    try &#123;</span><br><span class="line">    //调用PeerAwareInstanceRegistryImpl的init方法</span><br><span class="line">        registry.init(peerEurekaNodes);</span><br><span class="line">    &#125; catch (Exception e) &#123;</span><br><span class="line">        throw new RuntimeException(e);</span><br><span class="line">    &#125;</span><br><span class="line">    logger.info(&quot;Initialized&quot;);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在spring-cloud-netflix-eureka-server中，<code>PeerAwareInstanceRegistryImpl</code>的继承&#x2F;实现关系参见下图：</p><h2 id=""><a href="#" class="headerlink" title=""></a></h2><p>eureka server围绕注册表管理的，也就是上面的InstanceRegistry。</p><h3 id="注册表管理核心类-InstanceRegistry"><a href="#注册表管理核心类-InstanceRegistry" class="headerlink" title="注册表管理核心类 InstanceRegistry"></a>注册表管理核心类 InstanceRegistry</h3><p>参考上图，依次说明如下：</p><ul><li><p>InstanceRegistry</p><ul><li>有两种InstanceRegistry，参考如下：</li><li>com.netflix.eureka.registry.InstanceRegistry是euraka server中注册表管理的核心接口。职责是在内存中管理注册到Eureka Server中的服务实例信息。实现类有PeerAwareInstanceRegistryImpl。</li><li>org.springframework.cloud.netflix.eureka.server.InstanceRegistry 对PeerAwareInstanceRegistryImpl进行了继承和扩展，使其适配Spring cloud的使用环境，主要的实现由PeerAwareInstanceRegistryImpl提供。</li></ul></li><li><p>LeaseManager</p><ul><li>对注册到Eureka Server中的服务实例租约进行管理，方法有：服务注册，下线，续约，剔除。此接口管理的类目前是InstanceInfo。InstanceInfo代表服务实例信息。</li></ul></li><li><p>LookupService</p><ul><li>提供服务实例的检索查询功能。</li></ul></li><li><p>PeerAwareInstanceRegistryImpl</p><ul><li>增加了对peer节点的同步复制操作。使得eureka server集群中注册表信息保持一致。</li></ul></li></ul><h3 id="服务注册"><a href="#服务注册" class="headerlink" title="服务注册"></a>服务注册</h3><p>Eureka Client在发起服务注册时会将自身的服务实例元数据封装在InstanceInfo中，然后将InstanceInfo发送到Eureka Server。Eureka Server在接收到Eureka Client发送的InstanceInfo后将会尝试将其放到本地注册表中以供其他Eureka Client进行服务发现。</p><p>EurekaServerAutoConfiguration中定义了 public FilterRegistrationBean jerseyFilterRegistration ，表明eureka-server使用了Jersey实现 对外的RESTful接口。注册一个 Jersey 的 filter ，配置好相应的Filter 和 url映射。</p><p>在com.netflix.eureka.resources包下，是Eureka Server对于Eureka client的REST请求的定义。看ApplicationResource类（这是一类请求，应用类的请求），类似于应用@Controller注解：@Produces({“application&#x2F;xml”, “application&#x2F;json”})，接受xml和json。</p><p>例如ApplicationResource类中，有如下方法（添加实例接口）：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">@POST</span><br><span class="line">@Consumes(&#123;&quot;application/json&quot;, &quot;application/xml&quot;&#125;)</span><br><span class="line">public Response addInstance(InstanceInfo info,</span><br><span class="line">                            @HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication) &#123;</span><br><span class="line">    ......</span><br><span class="line">     //registry是PeerAwareInstanceRegistry类型</span><br><span class="line">    registry.register(info, &quot;true&quot;.equals(isReplication));</span><br><span class="line">    ......</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>上面的<code>registry.register</code>方法，可以追溯到<code>PeerAwareInstanceRegistryImpl</code>的方法：public void register(final InstanceInfo info, final boolean isReplication) ，中有一句：super.register(info, leaseDuration, isReplication);   将会进入：<code>com.netflix.eureka.registry.AbstractInstanceRegistry</code>的 register方法</p><p>在register方法中，服务实例的InstanceInfo保存在Lease中，Lease在AbstractInstanceRegistry中统一通过ConcurrentHashMap保存在内存中。在服务注册过程中，会先获取一个读锁，防止其他线程对registry注册表进行数据操作，避免数据的不一致。然后从resgitry查询对应的InstanceInfo租约是否已经存在注册表中，根据appName划分服务集群，使用InstanceId唯一标记服务实例。</p><ul><li>如果租约存在，比较两个租约中的InstanceInfo的最后更新时间lastDirtyTimestamp，保留时间戳大的服务实例信息InstanceInfo。</li><li>如果租约不存在，意味这是一次全新的服务注册，将会进行自我保护的统计，创建新的租约保存InstanceInfo。接着将租约放到resgitry注册表中。</li></ul><p>之后将进行一系列缓存操作并根据覆盖状态规则设置服务实例的状态，缓存操作包括将InstanceInfo加入用于统计Eureka Client增量式获取注册表信息的recentlyChangedQueue和失效responseCache中对应的缓存。最后设置服务实例租约的上线时间用于计算租约的有效时间，释放读锁并完成服务注册。</p><p>流程图如下：</p><p><img src="/images/Eureka%E6%9C%8D%E5%8A%A1%E7%AB%AF%E6%B3%A8%E5%86%8C-579x1024_388ab730.png"></p><h3 id="接收服务心跳请求"><a href="#接收服务心跳请求" class="headerlink" title="接收服务心跳请求"></a>接收服务心跳请求</h3><p>在Eureka Client完成服务注册之后，它需要定时向Eureka Server发送心跳请求(默认30秒一次)，维持自己在Eureka Server中租约的有效性。</p><p>此代码实现在com.netflix.eureka.resources.InstanceResource中，参见此方法：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">@PUT</span><br><span class="line">public Response renewLease(</span><br><span class="line">            @HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication,</span><br><span class="line">            @QueryParam(&quot;overriddenstatus&quot;) String overriddenStatus,</span><br><span class="line">            @QueryParam(&quot;status&quot;) String status,</span><br><span class="line">            @QueryParam(&quot;lastDirtyTimestamp&quot;) String lastDirtyTimestamp) &#123;</span><br><span class="line">    ......</span><br><span class="line">    boolean isSuccess = registry.renew(app.getName(), id, isFromReplicaNode);</span><br><span class="line">    ......</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>registry类型为<code>PeerAwareInstanceRegistry</code>，追溯后会发现最终实现是在<code>AbstractInstanceRegistry</code>#renew方法。</p><p>renew方法是对Eureka Client位于注册表中的租约的续租操作，不像register方法需要服务实例信息，仅根据服务实例的服务名和服务实例id即可更新对应租约的有效时间。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br></pre></td><td class="code"><pre><span class="line">public boolean renew(String appName, String id, boolean isReplication) &#123;</span><br><span class="line">        RENEW.increment(isReplication);</span><br><span class="line">     //根据appName获取服务集群的租约集合</span><br><span class="line">        Map&lt;String, Lease&lt;InstanceInfo&gt;&gt; gMap = registry.get(appName);</span><br><span class="line">        Lease&lt;InstanceInfo&gt; leaseToRenew = null;</span><br><span class="line">        if (gMap != null) &#123;</span><br><span class="line">            leaseToRenew = gMap.get(id);</span><br><span class="line">        &#125;</span><br><span class="line">        if (leaseToRenew == null) &#123;</span><br><span class="line">            RENEW_NOT_FOUND.increment(isReplication);</span><br><span class="line">            logger.warn(&quot;DS: Registry: lease doesn&#x27;t exist, registering resource: &#123;&#125; - &#123;&#125;&quot;, appName, id);</span><br><span class="line">            return false;</span><br><span class="line">        &#125; else &#123;</span><br><span class="line">            InstanceInfo instanceInfo = leaseToRenew.getHolder();</span><br><span class="line">            if (instanceInfo != null) &#123;</span><br><span class="line">                // touchASGCache(instanceInfo.getASGName());</span><br><span class="line">                //查看服务实例状态</span><br><span class="line">                InstanceStatus overriddenInstanceStatus = this.getOverriddenInstanceStatus(</span><br><span class="line">                        instanceInfo, leaseToRenew, isReplication);</span><br><span class="line">                if (overriddenInstanceStatus == InstanceStatus.UNKNOWN) &#123;</span><br><span class="line">                    logger.info(&quot;Instance status UNKNOWN possibly due to deleted override for instance &#123;&#125;&quot;</span><br><span class="line">                            + &quot;; re-register required&quot;, instanceInfo.getId());</span><br><span class="line">                    RENEW_NOT_FOUND.increment(isReplication);</span><br><span class="line">                    return false;</span><br><span class="line">                &#125;</span><br><span class="line">                if (!instanceInfo.getStatus().equals(overriddenInstanceStatus)) &#123;</span><br><span class="line">                    logger.info(</span><br><span class="line">                            &quot;The instance status &#123;&#125; is different from overridden instance status &#123;&#125; for instance &#123;&#125;. &quot;</span><br><span class="line">                                    + &quot;Hence setting the status to overridden status&quot;, instanceInfo.getStatus().name(),</span><br><span class="line">                                    instanceInfo.getOverriddenStatus().name(),</span><br><span class="line">                                    instanceInfo.getId());</span><br><span class="line">                    instanceInfo.setStatusWithoutDirty(overriddenInstanceStatus);</span><br><span class="line"></span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">             //统计每分钟续租次数</span><br><span class="line">            renewsLastMin.increment();</span><br><span class="line">             //更新租约</span><br><span class="line">            leaseToRenew.renew();</span><br><span class="line">            return true;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure><p>此方法中不关注InstanceInfo，仅关注于租约本身以及租约的服务实例状态。如果根据服务实例的appName和instanceInfoId查询出服务实例的租约，并且根据#getOverriddenInstanceStatus方法得到的instanceStatus不为InstanceStatus.UNKNOWN，那么更新租约中的有效时间，即更新租约Lease中的lastUpdateTimestamp，达到续约的目的；如果租约不存在，那么返回续租失败的结果。</p><p>下图给出了eureka服务端接收心跳、租约更新流程。</p><h2 id="-1"><a href="#-1" class="headerlink" title=""></a></h2><h3 id="服务剔除"><a href="#服务剔除" class="headerlink" title="服务剔除"></a>服务剔除</h3><p>如果Eureka Client在注册后，既没有续约，也没有下线(服务崩溃或者网络异常等原因)，那么服务的状态就处于不可知的状态，不能保证能够从该服务实例中获取到回馈，所以需要服务剔除此方法定时清理这些不稳定的服务，该方法会批量将注册表中所有过期租约剔除。</p><p>剔除是定时任务，默认60秒执行一次。延时60秒，间隔60秒  evictionTimer.schedule(evictionTaskRef.get(),  serverConfig.getEvictionIntervalTimerInMs(),  serverConfig.getEvictionIntervalTimerInMs());  从上面eureka server启动来看，剔除的任务，是线程启动的，执行的是下面的方法。 <code>com.netflix.eureka.registry.AbstractInstanceRegistry</code>#evict</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line">//isLeaseExpirationEnabled在</span><br><span class="line">public boolean isLeaseExpirationEnabled在() &#123;</span><br><span class="line">    if (!isSelfPreservationModeEnabled()) &#123;</span><br><span class="line">        //此处获取自我保护模式开关状态，最终对应于EurekaServerConfigBean类中的enableSelfPreservation配置项，</span><br><span class="line">        // The self preservation mode is disabled, hence allowing the instances to expire.</span><br><span class="line">        return true;</span><br><span class="line">    &#125;</span><br><span class="line">    return numberOfRenewsPerMinThreshold &gt; 0 &amp;&amp; getNumOfRenewsInLastMin() &gt; numberOfRenewsPerMinThreshold;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">public void evict(long additionalLeaseMs) &#123;</span><br><span class="line">    logger.debug(&quot;Running the evict task&quot;);</span><br><span class="line"></span><br><span class="line">    if (!isLeaseExpirationEnabled()) &#123;</span><br><span class="line">        logger.debug(&quot;DS: lease expiration is currently disabled.&quot;);</span><br><span class="line">        return;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    // We collect first all expired items, to evict them in random order. For large eviction sets,</span><br><span class="line">    // if we do not that, we might wipe out whole apps before self preservation kicks in. By randomizing it,</span><br><span class="line">    // the impact should be evenly distributed across all applications.</span><br><span class="line">    List&lt;Lease&lt;InstanceInfo&gt;&gt; expiredLeases = new ArrayList&lt;&gt;();</span><br><span class="line">    /*</span><br><span class="line">    遍历注册表register，依次判断租约是否过期。一次性获取所有的过期租约。</span><br><span class="line">    */</span><br><span class="line">    for (Entry&lt;String, Map&lt;String, Lease&lt;InstanceInfo&gt;&gt;&gt; groupEntry : registry.entrySet()) &#123;</span><br><span class="line">        Map&lt;String, Lease&lt;InstanceInfo&gt;&gt; leaseMap = groupEntry.getValue();</span><br><span class="line">        if (leaseMap != null) &#123;</span><br><span class="line">            for (Entry&lt;String, Lease&lt;InstanceInfo&gt;&gt; leaseEntry : leaseMap.entrySet()) &#123;</span><br><span class="line">                Lease&lt;InstanceInfo&gt; lease = leaseEntry.getValue();</span><br><span class="line">                if (lease.isExpired(additionalLeaseMs) &amp;&amp; lease.getHolder() != null) &#123;</span><br><span class="line">                    expiredLeases.add(lease);</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    // To compensate for GC pauses or drifting local time, we need to use current registry size as a base for</span><br><span class="line">    // triggering self-preservation. Without that we would wipe out full registry.</span><br><span class="line">    //获取注册表租约总数</span><br><span class="line">    int registrySize = (int) getLocalRegistrySize();</span><br><span class="line">    //计算注册表租约的阈值 （总数乘以 续租百分比），得出要续租的数量</span><br><span class="line">    int registrySizeThreshold = (int) (registrySize * serverConfig.getRenewalPercentThreshold());</span><br><span class="line">    //总数减去要续租的数量，就是理论要剔除的数量        </span><br><span class="line">    int evictionLimit = registrySize - registrySizeThreshold;</span><br><span class="line">    //求 上面理论剔除数量，和过期租约总数的最小值。就是最终要提出的数量。</span><br><span class="line">    int toEvict = Math.min(expiredLeases.size(), evictionLimit);</span><br><span class="line">    if (toEvict &gt; 0) &#123;</span><br><span class="line">        logger.info(&quot;Evicting &#123;&#125; items (expired=&#123;&#125;, evictionLimit=&#123;&#125;)&quot;, toEvict, expiredLeases.size(), evictionLimit);</span><br><span class="line"></span><br><span class="line">        Random random = new Random(System.currentTimeMillis());</span><br><span class="line">        for (int i = 0; i &lt; toEvict; i++) &#123;</span><br><span class="line">            // Pick a random item (Knuth shuffle algorithm)</span><br><span class="line">            int next = i + random.nextInt(expiredLeases.size() - i);</span><br><span class="line">            Collections.swap(expiredLeases, i, next);</span><br><span class="line">            Lease&lt;InstanceInfo&gt; lease = expiredLeases.get(i);</span><br><span class="line"></span><br><span class="line">            String appName = lease.getHolder().getAppName();</span><br><span class="line">            String id = lease.getHolder().getId();</span><br><span class="line">            EXPIRED.increment();</span><br><span class="line">            logger.warn(&quot;DS: Registry: expired lease for &#123;&#125;/&#123;&#125;&quot;, appName, id);</span><br><span class="line">            //执行 服务下线将服务从注册表清除掉。</span><br><span class="line">            internalCancel(appName, id, false);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><h4 id="剔除的限制"><a href="#剔除的限制" class="headerlink" title="剔除的限制"></a>剔除的限制</h4><p>1.自我保护期间不清除。  2.分批次清除。  3.服务是逐个随机剔除，剔除均匀分布在所有应用中，防止在同一时间内同一服务集群中的服务全部过期被剔除，造成在大量剔除服务时，并在进行自我保护时，促使程序崩溃。</p><h4 id="剔除服务的定时任务"><a href="#剔除服务的定时任务" class="headerlink" title="剔除服务的定时任务"></a>剔除服务的定时任务</h4><p>剔除服务是个定时任务，用EvictionTask执行，默认60秒执行一次，延时60秒执行。定时剔除过期服务。</p><p>代码：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">EurekaServerInitializerConfiguration#start()</span><br><span class="line">    eurekaServerBootstrap.contextInitialized(EurekaServerInitializerConfiguration.this.servletContext);</span><br><span class="line">        EurekaServerBoostrap#initEurekaServerContext()</span><br><span class="line">            registry.openForTraffic(this.applicationInfoManager, registryCount);</span><br><span class="line">                PeerAwareInstanceRegistryImpl#openForTraffic</span><br><span class="line">                    AbstractInstanceRegistry#postInit()</span><br></pre></td></tr></table></figure><p>服务剔除将会遍历registry注册表，找出其中所有的过期租约，然后根据配置文件中续租百分比阀值和当前注册表的租约总数量计算出最大允许的剔除租约的数量(当前注册表中租约总数量减去当前注册表租约阀值)，分批次剔除过期的服务实例租约。对过期的服务实例租约调用AbstractInstanceRegistry#internalCancel服务下线的方法将其从注册表中清除掉。</p><h4 id="自我保护机制"><a href="#自我保护机制" class="headerlink" title="自我保护机制"></a>自我保护机制</h4><p>自我保护机制主要在Eureka Client和Eureka Server之间存在网络分区的情况下发挥保护作用，在服务器端和客户端都有对应实现。假设在某种特定的情况下(如网络故障)，Eureka Client和Eureka Server无法进行通信，此时Eureka Client无法向Eureka Server发起注册和续约请求，Eureka Server中就可能因注册表中的服务实例租约出现大量过期而面临被剔除的危险，然而此时的Eureka Client可能是处于健康状态的(可接受服务访问)，如果直接将注册表中大量过期的服务实例租约剔除显然是不合理的。</p><p>针对这种情况，Eureka设计了“自我保护机制”。在Eureka Server处，如果出现大量的服务实例过期被剔除的现象，那么该Server节点将进入自我保护模式，保护注册表中的信息不再被剔除，在通信稳定后再退出该模式；在Eureka Client处，如果向Eureka Server注册失败，将快速超时并尝试与其他的Eureka Server进行通信。“自我保护机制”的设计大大提高了Eureka的可用性。</p><p>有关自我保护机制的流程图：</p><h2 id="-2"><a href="#-2" class="headerlink" title=""></a></h2><h3 id="服务下线"><a href="#服务下线" class="headerlink" title="服务下线"></a>服务下线</h3><p>Eureka Client在应用销毁时，会向Eureka Server发送服务下线请求，清除注册表中关于本应用的租约，避免无效的服务调用。在服务剔除的过程中，也是通过服务下线的逻辑完成对单个服务实例过期租约的清除工作。</p><p>对应代码：com.netflix.eureka.resources.InstanceResource 的</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">@DELETE</span><br><span class="line">public Response cancelLease(</span><br><span class="line">        @HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication) &#123;</span><br><span class="line">    ......</span><br><span class="line">    boolean isSuccess = registry.cancel(app.getName(), id, &quot;true&quot;.equals(isReplication));</span><br><span class="line">    ......</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>继续追踪：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">InstanceResource#cancelLease</span><br><span class="line">    PeerAwareInstanceRegistryImpl#cancel //此方法中还有将取消操作复制给其他eureka节点的逻辑</span><br><span class="line">        AbstractInstanceRegistry#cancel</span><br><span class="line">            AbstractInstanceRegistry#internalCancel</span><br></pre></td></tr></table></figure><p>最终实现代码如下：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br></pre></td><td class="code"><pre><span class="line">protected boolean internalCancel(String appName, String id, boolean isReplication) &#123;</span><br><span class="line">    try &#123;</span><br><span class="line">        //先获取读锁，防止被其他线程修改</span><br><span class="line">        read.lock();</span><br><span class="line">        CANCEL.increment(isReplication);</span><br><span class="line">        //根据appName获取服务实例集群。</span><br><span class="line">        Map&lt;String, Lease&lt;InstanceInfo&gt;&gt; gMap = registry.get(appName);</span><br><span class="line">        Lease&lt;InstanceInfo&gt; leaseToCancel = null;</span><br><span class="line">        //  在内存中取消实例 id的服务</span><br><span class="line">        if (gMap != null) &#123;</span><br><span class="line">            leaseToCancel = gMap.remove(id);</span><br><span class="line">        &#125;</span><br><span class="line">        //添加到最近下线服务的统计队列</span><br><span class="line">        recentCanceledQueue.add(new Pair&lt;Long, String&gt;(System.currentTimeMillis(), appName + &quot;(&quot; + id + &quot;)&quot;));</span><br><span class="line">        InstanceStatus instanceStatus = overriddenInstanceStatusMap.remove(id);</span><br><span class="line">        if (instanceStatus != null) &#123;</span><br><span class="line">            logger.debug(&quot;Removed instance id &#123;&#125; from the overridden map which has value &#123;&#125;&quot;, id, instanceStatus.name());</span><br><span class="line">        &#125;</span><br><span class="line">        // 往下判断leaseToCancel是否为空，租约不存在，返回false</span><br><span class="line">        if (leaseToCancel == null) &#123;</span><br><span class="line">            CANCEL_NOT_FOUND.increment(isReplication);</span><br><span class="line">            logger.warn(&quot;DS: Registry: cancel failed because Lease is not registered for: &#123;&#125;/&#123;&#125;&quot;, appName, id);</span><br><span class="line">            return false;</span><br><span class="line">        &#125; else &#123;</span><br><span class="line">            //若存在，设置租约下线时间</span><br><span class="line">            leaseToCancel.cancel();</span><br><span class="line">            InstanceInfo instanceInfo = leaseToCancel.getHolder();</span><br><span class="line">            String vip = null;</span><br><span class="line">            String svip = null;</span><br><span class="line">            if (instanceInfo != null) &#123;</span><br><span class="line">                //获取持有租约的服务信息，标记服务实例为instanceInfo.setActionType(ActionType.DELETED);</span><br><span class="line">                instanceInfo.setActionType(ActionType.DELETED);</span><br><span class="line">                //添加到租约变更记录队列</span><br><span class="line">                recentlyChangedQueue.add(new RecentlyChangedItem(leaseToCancel));//用于eureka client的增量拉取注册表信息。</span><br><span class="line">                instanceInfo.setLastUpdatedTimestamp();</span><br><span class="line">                vip = instanceInfo.getVIPAddress();</span><br><span class="line">                svip = instanceInfo.getSecureVipAddress();</span><br><span class="line">            &#125;</span><br><span class="line">            invalidateCache(appName, vip, svip);</span><br><span class="line">            logger.info(&quot;Cancelled instance &#123;&#125;/&#123;&#125; (replication=&#123;&#125;)&quot;, appName, id, isReplication);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125; finally &#123;</span><br><span class="line">        read.unlock();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    synchronized (lock) &#123;</span><br><span class="line">        if (this.expectedNumberOfClientsSendingRenews &gt; 0) &#123;</span><br><span class="line">            // Since the client wants to cancel it, reduce the number of clients to send renews.</span><br><span class="line">            this.expectedNumberOfClientsSendingRenews = this.expectedNumberOfClientsSendingRenews - 1;</span><br><span class="line">            updateRenewsPerMinThreshold();</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    return true;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>上述代码主要功能： 首先通过registry根据服务名和服务实例id查询关于服务实例的租约Lease是否存在，统计最近请求下线的服务实例用于Eureka Server主页展示。如果租约不存在，返回下线失败；如果租约存在，从registry注册表中移除，设置租约的下线时间，同时在最近租约变更记录队列中添加新的下线记录，以用于Eureka Client的增量式获取注册表信息。</p><p><img src="/images/Eureka%E6%9C%8D%E5%8A%A1%E4%B8%8B%E7%BA%BF_a31244a8.png"></p><h3 id="集群同步"><a href="#集群同步" class="headerlink" title="集群同步"></a>集群同步</h3><p>如果Eureka Server是通过集群的方式进行部署，那么为了维护整个集群中Eureka Server注册表数据的一致性，势必需要一个机制同步Eureka Server集群中的注册表数据。</p><p>Eureka Server集群同步包含两个部分：</p><ul><li>Eureka Server在启动过程中从它的peer节点中拉取注册表信息，并将这些服务实例的信息注册到本地注册表中；</li><li>Eureka Server每次对本地注册表进行操作时，同时会将操作同步到它的peer节点中，达到集群注册表数据统一的目的。</li></ul><h4 id="启动拉取别的peer"><a href="#启动拉取别的peer" class="headerlink" title="启动拉取别的peer"></a>启动拉取别的peer</h4><p>Eureka Server启动类中，<code>EurekaServerAutoConfiguration</code>通过@Import注解加载<code>EurekaServerInitializerConfiguration</code></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">EurekaServerInitializerConfiguration#start()</span><br><span class="line">    eurekaServerBootstrap.contextInitialized(...)</span><br><span class="line">        EurekaServerBootstrap#initEurekaServerContext()</span><br><span class="line">            PeerAwareInstanceRegistryImpl#syncUp();</span><br></pre></td></tr></table></figure><p>代码实现：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line">public int syncUp() &#123;</span><br><span class="line">        // Copy entire entry from neighboring DS node</span><br><span class="line">        int count = 0;</span><br><span class="line"></span><br><span class="line">        for (int i = 0; ((i &lt; serverConfig.getRegistrySyncRetries()) &amp;&amp; (count == 0)); i++) &#123;</span><br><span class="line">            if (i &gt; 0) &#123;</span><br><span class="line">                try &#123;</span><br><span class="line">                    Thread.sleep(serverConfig.getRegistrySyncRetryWaitMs());</span><br><span class="line">                &#125; catch (InterruptedException e) &#123;</span><br><span class="line">                    logger.warn(&quot;Interrupted during registry transfer..&quot;);</span><br><span class="line">                    break;</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">            Applications apps = eurekaClient.getApplications();</span><br><span class="line">            for (Application app : apps.getRegisteredApplications()) &#123;</span><br><span class="line">                for (InstanceInfo instance : app.getInstances()) &#123;</span><br><span class="line">                    try &#123;</span><br><span class="line">                        if (isRegisterable(instance)) &#123;</span><br><span class="line">                            register(instance, instance.getLeaseInfo().getDurationInSecs(), true);</span><br><span class="line">                            count++;</span><br><span class="line">                        &#125;</span><br><span class="line">                    &#125; catch (Throwable t) &#123;</span><br><span class="line">                        logger.error(&quot;During DS init copy&quot;, t);</span><br><span class="line">                    &#125;</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        return count;</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure><p>看循环：意思是，如果是i第一次进来，为0，不够等待的代码，直接执行下面的拉取服务实例。  将自己作为一个eureka client，拉取注册表。并通过register(instance, instance.getLeaseInfo().getDurationInSecs(), true)注册到自身的注册表中。</p><p>Eureka Server也是一个Eureka Client，在启动的时候也会进行DiscoveryClient的初始化，会从其对应的Eureka Server中拉取全量的注册表信息。在Eureka Server集群部署的情况下，Eureka Server从它的peer节点中拉取到注册表信息后，将遍历这个Applications，将所有的服务实例通过AbstractRegistry#register方法注册到自身注册表中。</p><p>EurekaServerBootstrap#initEurekaServerContext()中有如下代码：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">。。。</span><br><span class="line">// Copy registry from neighboring eureka node</span><br><span class="line">int registryCount = this.registry.syncUp();</span><br><span class="line">this.registry.openForTraffic(this.applicationInfoManager, registryCount);</span><br><span class="line">。。。</span><br></pre></td></tr></table></figure><p>当执行完上面的syncUp逻辑后，在下面的openForTraffic，<strong>开启此server接受别的client注册，拉取注册表等操作。而在它首次拉取其他peer节点时，是不允许client的通信请求的。</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line">public void openForTraffic(ApplicationInfoManager applicationInfoManager, int count) &#123;</span><br><span class="line">    // Renewals happen every 30 seconds and for a minute it should be a factor of 2.</span><br><span class="line">    this.expectedNumberOfClientsSendingRenews = count;</span><br><span class="line">    updateRenewsPerMinThreshold();</span><br><span class="line">    logger.info(&quot;Got &#123;&#125; instances from neighboring DS node&quot;, count);</span><br><span class="line">    logger.info(&quot;Renew threshold is: &#123;&#125;&quot;, numberOfRenewsPerMinThreshold);</span><br><span class="line">    this.startupTime = System.currentTimeMillis();</span><br><span class="line">    //如果count=0，没有拉取到注册表信息，将此值设为true，表示其他peer来取空的实例信息，意味着，将不允许client从此server获取注册表信息。如果count&gt;0，将此值设置为false，允许client来获取注册表。   </span><br><span class="line">    if (count &gt; 0) &#123;</span><br><span class="line">        this.peerInstancesTransferEmptyOnStartup = false;</span><br><span class="line">    &#125;</span><br><span class="line">    DataCenterInfo.Name selfName = applicationInfoManager.getInfo().getDataCenterInfo().getName();</span><br><span class="line">    boolean isAws = Name.Amazon == selfName;</span><br><span class="line">    if (isAws &amp;&amp; serverConfig.shouldPrimeAwsReplicaConnections()) &#123;</span><br><span class="line">        logger.info(&quot;Priming AWS connections for all replicas..&quot;);</span><br><span class="line">        primeAwsReplicas(applicationInfoManager);</span><br><span class="line">    &#125;</span><br><span class="line">    logger.info(&quot;Changing status to UP&quot;);</span><br><span class="line">    applicationInfoManager.setInstanceStatus(InstanceStatus.UP);</span><br><span class="line">    super.postInit();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="Server之间注册表信息的同步复制"><a href="#Server之间注册表信息的同步复制" class="headerlink" title="Server之间注册表信息的同步复制"></a>Server之间注册表信息的同步复制</h4><p><strong>为了保证Eureka Server集群运行时注册表信息的一致性，每个Eureka Server在对本地注册表进行管理操作时，会将相应的操作同步到所有peer节点中。</strong></p><p>在外部调用server的restful方法时，在com.netflix.eureka.resources包下的ApplicationResource资源中，查看每个服务的操作。比如服务注册public Response addInstance(，此方法中有  registry.register(info, “true”.equals(isReplication));点进去实现类：replicateToPeers(Action.Register, info.getAppName(), info.getId(), info, null, isReplication);这是一种情况。</p><p>在PeerAwareInstanceRegistryImpl类中，其他操作，如cancel，renew等中都有replicateToPeers，  此方法中有个peerEurekaNodes，代表一个可同步数据的eureka Server的集合，如果注册表有变化，向此中的peer节点同步。</p><p>replicateToPeers方法，它将遍历Eureka Server中peer节点，向每个peer节点发送同步请求。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line">private void replicateToPeers(Action action, String appName, String id,</span><br><span class="line">                                  InstanceInfo info /* optional */,</span><br><span class="line">                                  InstanceStatus newStatus /* optional */, boolean isReplication) &#123;</span><br><span class="line">        Stopwatch tracer = action.getTimer().start();</span><br><span class="line">        try &#123;</span><br><span class="line">            if (isReplication) &#123;</span><br><span class="line">                numberOfReplicationsLastMin.increment();</span><br><span class="line">            &#125;</span><br><span class="line">            // If it is a replication already, do not replicate again as this will create a poison replication</span><br><span class="line">            if (peerEurekaNodes == Collections.EMPTY_LIST || isReplication) &#123;</span><br><span class="line">                return;</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            for (final PeerEurekaNode node : peerEurekaNodes.getPeerEurekaNodes()) &#123;</span><br><span class="line">                // If the url represents this host, do not replicate to yourself.</span><br><span class="line">                if (peerEurekaNodes.isThisMyUrl(node.getServiceUrl())) &#123;</span><br><span class="line">                    continue;</span><br><span class="line">                &#125;</span><br><span class="line">                replicateInstanceActionsToPeers(action, appName, id, info, newStatus, node);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125; finally &#123;</span><br><span class="line">            tracer.stop();</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure><p>此replicateInstanceActionsToPeers方法中，类PeerEurekaNode的实例node的各种方法，cancel，register，等，用了batchingDispatcher.process(，作用是将同一时间段内，相同服务实例的相同操作将使用相同的任务编号，在进行同步复制的时候，将根据任务编号合并操作，<strong>减少同步操作的数量和网络消耗，但是同时也造成了同步复制的延时性，不满足CAP中的C（强一致性）。  所以Eureka，只满足AP。</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line">//示例：PeerEurekaNode</span><br><span class="line">public void cancel(final String appName, final String id) throws Exception &#123;</span><br><span class="line">    long expiryTime = System.currentTimeMillis() + maxProcessingDelayMs;</span><br><span class="line">    batchingDispatcher.process(</span><br><span class="line">        taskId(&quot;cancel&quot;, appName, id),</span><br><span class="line">        new InstanceReplicationTask(targetHost, Action.Cancel, appName, id) &#123;</span><br><span class="line">            @Override</span><br><span class="line">            public EurekaHttpResponse&lt;Void&gt; execute() &#123;</span><br><span class="line">                return replicationClient.cancel(appName, id);</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            @Override</span><br><span class="line">            public void handleFailure(int statusCode, Object responseEntity) throws Throwable &#123;</span><br><span class="line">                super.handleFailure(statusCode, responseEntity);</span><br><span class="line">                if (statusCode == 404) &#123;</span><br><span class="line">                    logger.warn(&quot;&#123;&#125;: missing entry.&quot;, getTaskName());</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;,</span><br><span class="line">        expiryTime</span><br><span class="line">    );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>通过Eureka Server在启动过程中初始化本地注册表信息和Eureka Server集群间的同步复制操作，最终达到了集群中Eureka Server注册表信息一致的目的。</p><h3 id="获取注册表中服务实例信息"><a href="#获取注册表中服务实例信息" class="headerlink" title="获取注册表中服务实例信息"></a>获取注册表中服务实例信息</h3><p>Eureka Server中获取注册表的服务实例信息主要通过两个方法实现：</p><ul><li>AbstractInstanceRegistry#getApplicationsFromMultipleRegions从多地区获取全量注册表数据</li><li>AbstractInstanceRegistry#getApplicationDeltasFromMultipleRegions从多地区获取增量式注册表数据。</li></ul><h5 id="1、全量"><a href="#1、全量" class="headerlink" title="1、全量"></a>1、全量</h5><p>上面讲到从节点复制注册信息的时候，用方法public int syncUp() ，一行Applications apps &#x3D; eurekaClient.getApplications();点进去实现类，有一行getApplicationsFromAllRemoteRegions(); 下面getApplicationsFromMultipleRegions，作用从多个地区中获取全量注册表信息，并封装成Applications返回，它首先会将本地注册表registry中的所有服务实例信息提取出来封装到Applications中，再根据是否需要拉取Region的注册信息，将远程拉取过来的Application放到上面的Applications中。最后得到一个全量的Applications。</p><h5 id="2、增量"><a href="#2、增量" class="headerlink" title="2、增量"></a>2、增量</h5><p>在前面提到接受服务注册，接受心跳等方法中，都有recentlyChangedQueue.add(new RecentlyChangedItem(lease));作用是将新变动的服务放到最近变化的服务实例信息队列中，用于记录增量是注册表信息。getApplicationDeltasFromMultipleRegions，实现了从远处eureka server中获取增量式注册表信息的能力。</p><p>在EurekaServer对外restful中，在com.netflix.eureka.resources下，   @GET  public Response getApplication(@PathParam(“version”) String version,  @HeaderParam(“Accept”) final String acceptHeader,  @HeaderParam(EurekaAccept.HTTP_X_EUREKA_ACCEPT) String eurekaAccept) {  其中有一句：String payLoad &#x3D; responseCache.get(cacheKey);在responseCache初始化的时候，它的构造方法ResponseCacheImpl(EurekaServerConfig serverConfig, ServerCodecs serverCodecs, AbstractInstanceRegistry registry) {中，Value value &#x3D; generatePayload(key);点进去有一句：registry.getApplicationDeltasFromMultipleRegions(key.getRegions()));<strong>从远程获取delta增量注册信息。但是这个只是向client提供，不向server提供，因为server可以通过每次变更自动同步到peer。</strong></p><p>获取增量式注册表信息将会从recentlyChangedQueue中获取最近变化的服务实例信息。recentlyChangedQueue中统计了近3分钟内进行注册、修改和剔除的服务实例信息，在服务注册AbstractInstanceRegistry#registry、接受心跳请求AbstractInstanceRegistry#renew和服务下线AbstractInstanceRegistry#internalCancel等方法中均可见到recentlyChangedQueue对这些服务实例进行登记，用于记录增量式注册表信息。#getApplicationsFromMultipleRegions方法同样提供了从远程Region的Eureka Server获取增量式注册表信息的能力。</p><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><ul><li><a href="https://github.com/bjmashibing/InternetArchitect/tree/master/20%20%E6%9E%B6%E6%9E%84%E5%B8%88%E4%B8%89%E6%9C%9F%20SpringCloud%E5%BE%AE%E6%9C%8D%E5%8A%A1%E6%9E%B6%E6%9E%84/%E5%B8%B8%E8%80%81%E5%B8%88%E7%BD%91%E7%BA%A6%E8%BD%A6%E9%A1%B9%E7%9B%AE%E9%A2%84%E4%B9%A0%E8%B5%84%E6%96%99/%E6%8C%89%E7%BB%84%E4%BB%B6%E5%8C%BA%E5%88%86">spring cloud公开课资料</a></li></ul>]]>
    </content>
    <id>https://evasnowind.github.io/2020/09/28/2020-09-29_%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90%E4%B9%8BEureka%E6%9C%8D%E5%8A%A1%E7%AB%AF%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90/</id>
    <link href="https://evasnowind.github.io/2020/09/28/2020-09-29_%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90%E4%B9%8BEureka%E6%9C%8D%E5%8A%A1%E7%AB%AF%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90/"/>
    <published>2020-09-28T16:00:00.000Z</published>
    <summary>围绕Eureka服务端源码解析的实现原理、核心流程与关键细节做源码分析。</summary>
    <title>源码分析之Eureka服务端源码解析</title>
    <updated>2026-09-04T04:44:28.356Z</updated>
  </entry>
  <entry>
    <author>
      <name>Prayer</name>
    </author>
    <category term="Java" scheme="https://evasnowind.github.io/categories/Java/"/>
    <category term="Spring Cloud" scheme="https://evasnowind.github.io/categories/Java/Spring-Cloud/"/>
    <category term="源码分析" scheme="https://evasnowind.github.io/tags/%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/"/>
    <category term="Spring Cloud" scheme="https://evasnowind.github.io/tags/Spring-Cloud/"/>
    <content>
      <![CDATA[<h2 id="说明"><a href="#说明" class="headerlink" title="说明"></a>说明</h2><p>本文所分析代码版本为spring boot&#x2F;cloud 2.2.6。</p><h3 id="预备知识"><a href="#预备知识" class="headerlink" title="预备知识"></a>预备知识</h3><h4 id="一些注解的说明"><a href="#一些注解的说明" class="headerlink" title="一些注解的说明"></a>一些注解的说明</h4><p>eureka中用到几个比较有意思的注解，简化程序实现。</p><h5 id="ConfigurationProperties-“eureka-instance”"><a href="#ConfigurationProperties-“eureka-instance”" class="headerlink" title="@ConfigurationProperties(“eureka.instance”)"></a>@ConfigurationProperties(“eureka.instance”)</h5><span id="more"></span><p>表示从外部配置文件中（properties或是yml文件）读取”eureka.instance”对应的配置。</p><h5 id="ConditionalOnBean-ConditionalOnClass"><a href="#ConditionalOnBean-ConditionalOnClass" class="headerlink" title="@ConditionalOnBean&#x2F;@ConditionalOnClass"></a>@ConditionalOnBean&#x2F;@ConditionalOnClass</h5><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">@ConditionalOnBean         //   当给定的在bean存在时,则实例化当前Bean</span><br><span class="line">@ConditionalOnMissingBean  //   当给定的在bean不存在时,则实例化当前Bean</span><br><span class="line">@ConditionalOnClass        //   当给定的类名在类路径上存在，则实例化当前Bean</span><br><span class="line">@ConditionalOnMissingClass //   当给定的类名在类路径上不存在，则实例化当前Bean</span><br></pre></td></tr></table></figure><p>可以参考这篇文章：<a href="https://www.cnblogs.com/qdhxhz/p/11027546.html">SpringBoot(16)—@ConditionalOnBean与@ConditionalOnClass</a></p><p>比如<code>EurekaClientAutoConfiguration</code>类定义中，类上面注解了<code>@ConditionalOnClass(EurekaClientConfig.class)</code>,表示当在类路径中存在EurekaClientConfig.class，则实例化当前<code>EurekaClientAutoConfiguration</code>。</p><h5 id="ImplementedBy"><a href="#ImplementedBy" class="headerlink" title="@ImplementedBy"></a>@ImplementedBy</h5><p>google guice注解，指定接口默认的实现类。</p><h5 id="Singleton"><a href="#Singleton" class="headerlink" title="@Singleton"></a>@Singleton</h5><p>jdk 提供的注解，将当前类实现为单例模式。</p><h4 id="eureka架构"><a href="#eureka架构" class="headerlink" title="eureka架构"></a>eureka架构</h4><p><img src="/images/eureka-architecture-1024x548_c59397f0.png"></p><h2 id="eureka客户端源码"><a href="#eureka客户端源码" class="headerlink" title="eureka客户端源码"></a>eureka客户端源码</h2><h3 id="eureka客户端工作流程"><a href="#eureka客户端工作流程" class="headerlink" title="eureka客户端工作流程"></a>eureka客户端工作流程</h3><p><img src="/images/eureka-client-work-flow-1024x598_c91e0794.png">\</p><p>下面将简要说明上图的过程。</p><p>eureka客户端比较关键的类如下：</p><p><img src="/images/eureka-client-main-class-300x150_5e20efa1.png"></p><p>其中，DiscoveryClient是核心，它实现了EurekaClient接口，是该接口的默认实现（@ImplementedBy传入的是DiscoveryClient.class）。另外，@Singleton注解声明了DiscoveryClient是单例。</p><h4 id="DiscoverClient"><a href="#DiscoverClient" class="headerlink" title="DiscoverClient"></a>DiscoverClient</h4><p>该类的关键方法如下：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">//服务注册相关</span><br><span class="line">boolean register()</span><br><span class="line">void unregister()</span><br><span class="line"></span><br><span class="line">//服务续约</span><br><span class="line">boolean renew()</span><br><span class="line"></span><br><span class="line">//关闭eureka client</span><br><span class="line">public synchronized void shutdown()</span><br><span class="line"></span><br><span class="line">//检查当前client状态，当client状态发生变化时，将会触发新的注册事件，去更新eureka server的注册表中的服务实例信息。</span><br><span class="line">public void registerHealthCheck(HealthCheckHandler healthCheckHandler)</span><br><span class="line"></span><br><span class="line">//注册事件监听器，当实例信息有变时，触发对应的处理事件。</span><br><span class="line">public void registerEventListener(EurekaEventListener eventListener)</span><br></pre></td></tr></table></figure><p>其构造方法<code>DiscoveryClient(ApplicationInfoManager applicationInfoManager, EurekaClientConfig config, AbstractDiscoveryClientOptionalArgs args, Provider&lt;BackupRegistry&gt; backupRegistryProvider, EndpointRandomizer endpointRandomizer)</code>中，此方法中依次执行了 从eureka server中拉取注册表，服务注册，初始化发送心跳，缓存刷新（定时拉取注册表信息），按需注册定时任务等，贯穿了Eureka Client启动阶段的各项工作。</p><p>此处给出一些简单的分析：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br></pre></td><td class="code"><pre><span class="line">DiscoveryClient(ApplicationInfoManager applicationInfoManager, EurekaClientConfig config, AbstractDiscoveryClientOptionalArgs args, Provider&lt;BackupRegistry&gt; backupRegistryProvider, EndpointRandomizer endpointRandomizer) &#123;</span><br><span class="line">      ......</span><br><span class="line">      //shouldFetchRegistry，其实现类为EurekaClientConfigBean，找到它其实对应于：eureka.client.fetch-register，true：表示client从server拉取注册表信息。</span><br><span class="line">     if (config.shouldFetchRegistry()) &#123;</span><br><span class="line">         this.registryStalenessMonitor = new ThresholdLevelsMetric(this, METRIC_REGISTRY_PREFIX + &quot;lastUpdateSec_&quot;, new long[]&#123;15L, 30L, 60L, 120L, 240L, 480L&#125;);</span><br><span class="line">     &#125; else &#123;</span><br><span class="line">         this.registryStalenessMonitor = ThresholdLevelsMetric.NO_OP_METRIC;</span><br><span class="line">     &#125;</span><br><span class="line">    </span><br><span class="line">    //shouldRegisterWithEureka，点其实现类EurekaClientConfigBean，找到它其实对应于：eureka.client.register-with-eureka：true：表示client将注册到server。</span><br><span class="line">    if (config.shouldRegisterWithEureka()) &#123;</span><br><span class="line">        this.heartbeatStalenessMonitor = new ThresholdLevelsMetric(this, METRIC_REGISTRATION_PREFIX + &quot;lastHeartbeatSec_&quot;, new long[]&#123;15L, 30L, 60L, 120L, 240L, 480L&#125;);</span><br><span class="line">    &#125; else &#123;</span><br><span class="line">        this.heartbeatStalenessMonitor = ThresholdLevelsMetric.NO_OP_METRIC;</span><br><span class="line">    &#125;</span><br><span class="line">    ......</span><br><span class="line">        </span><br><span class="line">// default size of 2 - 1 each for heartbeat and cacheRefresh</span><br><span class="line">    scheduler = Executors.newScheduledThreadPool(2,</span><br><span class="line">                                                     new ThreadFactoryBuilder()</span><br><span class="line">                                                     .setNameFormat(&quot;DiscoveryClient-%d&quot;)</span><br><span class="line">                                                     .setDaemon(true)</span><br><span class="line">                                                     .build());</span><br><span class="line">    //用于发送心跳</span><br><span class="line">    heartbeatExecutor = new ThreadPoolExecutor(</span><br><span class="line">        1, clientConfig.getHeartbeatExecutorThreadPoolSize(), 0, TimeUnit.SECONDS,</span><br><span class="line">        new SynchronousQueue&lt;Runnable&gt;(),</span><br><span class="line">        new ThreadFactoryBuilder()</span><br><span class="line">        .setNameFormat(&quot;DiscoveryClient-HeartbeatExecutor-%d&quot;)</span><br><span class="line">        .setDaemon(true)</span><br><span class="line">        .build()</span><br><span class="line">    );  // use direct handoff</span><br><span class="line">    //用于刷新缓存</span><br><span class="line">    cacheRefreshExecutor = new ThreadPoolExecutor(</span><br><span class="line">        1, clientConfig.getCacheRefreshExecutorThreadPoolSize(), 0, TimeUnit.SECONDS,</span><br><span class="line">        new SynchronousQueue&lt;Runnable&gt;(),</span><br><span class="line">        new ThreadFactoryBuilder()</span><br><span class="line">        .setNameFormat(&quot;DiscoveryClient-CacheRefreshExecutor-%d&quot;)</span><br><span class="line">        .setDaemon(true)</span><br><span class="line">        .build()</span><br><span class="line">    );  // use direct handoff</span><br><span class="line">    </span><br><span class="line">    //eurekaTransport是eureka Client和eureka server进行http交互jersey客户端。点开EurekaTransport，看到许多httpclient相关的属性。</span><br><span class="line">    eurekaTransport = new EurekaTransport();</span><br><span class="line">    </span><br><span class="line">    if (clientConfig.shouldFetchRegistry()) &#123;</span><br><span class="line">        ....</span><br><span class="line">            //从eureka server拉取注册表中的信息，将注册表缓存到本地，可以就近获取其他服务信息，减少于server的交互。</span><br><span class="line">         boolean primaryFetchRegistryResult = fetchRegistry(false);</span><br><span class="line">         ......</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    //register()进行注册，若注册失败则抛出异常</span><br><span class="line">    if (clientConfig.shouldRegisterWithEureka() &amp;&amp; clientConfig.shouldEnforceRegistrationAtInit()) &#123;</span><br><span class="line">        try &#123;</span><br><span class="line">            if (!register() ) &#123;</span><br><span class="line">                throw new IllegalStateException(&quot;Registration error at startup. Invalid server response.&quot;);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125; catch (Throwable th) &#123;</span><br><span class="line">            logger.error(&quot;Registration error at startup: &#123;&#125;&quot;, th.getMessage());</span><br><span class="line">            throw new IllegalStateException(th);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    //启动调度任务</span><br><span class="line">    initScheduledTasks();</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><h4 id="DiscoveryClient构造方法小结"><a href="#DiscoveryClient构造方法小结" class="headerlink" title="DiscoveryClient构造方法小结"></a>DiscoveryClient构造方法小结</h4><p>构造方法中主要进行如下工作：</p><ul><li>初始化各种信息</li><li>从eureka server拉取注册表信息</li><li>向server注册自己</li><li>初始化3个任务</li></ul><p>以下对重要过程进行分析：</p><h5 id="拉取注册表过程"><a href="#拉取注册表过程" class="headerlink" title="拉取注册表过程"></a>拉取注册表过程</h5><p>对应<code>private boolean fetchRegistry(boolean forceFullRegistryFetch)</code>方法，参考注释、源码可以知道逻辑如下：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">如果增量式拉取被禁止或第一次拉取注册表，则进行全量拉取：getAndStoreFullRegistry()。</span><br><span class="line">否则进行增量拉取注册表信息getAndUpdateDelta(applications)。</span><br><span class="line">一般情况，在Eureka client第一次启动，会进行全量拉取。之后的拉取都尽量尝试只进行增量拉取。</span><br><span class="line"></span><br><span class="line">  拉取服务注册表：</span><br><span class="line">  全量拉取：getAndStoreFullRegistry();</span><br><span class="line">  增量拉取：getAndUpdateDelta(applications);</span><br></pre></td></tr></table></figure><h6 id="全量拉取-getAndStoreFullRegistry"><a href="#全量拉取-getAndStoreFullRegistry" class="headerlink" title="全量拉取 getAndStoreFullRegistry()"></a>全量拉取 getAndStoreFullRegistry()</h6><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">进入getAndStoreFullRegistry() 方法，有一方法：eurekaTransport.queryClient.getApplications。</span><br><span class="line">  通过debug发现 实现类是AbstractJerseyEurekaHttpClient，点开，debug出 </span><br><span class="line">  webResource 地址为：`http://root:root@eureka-7900:7900/eureka/apps/`，此端点用于获取 server 中所有的注册表信息。</span><br><span class="line">  getAndStoreFullRegistry()可能被多个线程同时调用，导致新拉取的注册表被旧的覆盖(如果新拉取的动作设置apps阻塞的情况下）。</span><br><span class="line">  此时用了AutomicLong来进行版本管理，如果更新时版本不一致，不保存apps。</span><br><span class="line">  通过这个判断fetchRegistryGeneration.compareAndSet(currentUpdateGeneration, currentUpdateGeneration + 1)，如果版本一致，并设置新版本（+1），</span><br><span class="line">  接着执行localRegionApps.set(this.filterAndShuffle(apps));过滤并洗牌apps。点开this.filterAndShuffle(apps)实现，继续点apps.shuffleAndIndexInstances，继续点shuffleInstances，继续点application.shuffleAndStoreInstances，继续点_shuffleAndStoreInstances，发现if (filterUpInstances &amp;&amp; InstanceStatus.UP != instanceInfo.getStatus())。只保留状态为UP的服务。</span><br></pre></td></tr></table></figure><h6 id="增量拉取getAndUpdateDelta-applications"><a href="#增量拉取getAndUpdateDelta-applications" class="headerlink" title="增量拉取getAndUpdateDelta(applications)"></a>增量拉取getAndUpdateDelta(applications)</h6><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">回到刚才的fetchRegistry方法中，getAndUpdateDelta，增量拉取。通过getDelta方法，看到实际拉取的地址是：apps/delta，如果获取到的delta为空，则全量拉取。</span><br><span class="line">  通常来讲是3分钟之内注册表的信息变化（在server端判断），获取到delta后，会更新本地注册表。</span><br><span class="line">  增量式拉取是为了维护client和server端 注册表的一致性，防止本地数据过久，而失效，采用增量式拉取的方式，减少了client和server的通信量。</span><br><span class="line">  client有一个注册表缓存刷新定时器，专门负责维护两者之间的信息同步，但是当增量出现意外时，定时器将执行，全量拉取以更新本地缓存信息。更新本地注册表方法updateDelta，有一个细节。</span><br><span class="line">  if (ActionType.ADDED.equals(instance.getActionType())) ，public enum ActionType &#123;</span><br><span class="line">          ADDED, // Added in the discovery server</span><br><span class="line">          MODIFIED, // Changed in the discovery server</span><br><span class="line">          DELETED</span><br><span class="line">          // Deleted from the discovery server</span><br><span class="line">      &#125;，</span><br><span class="line">  在InstanceInfo instance中有一个instance.getActionType()，ADDED和MODIFIED状态的将更新本地注册表applications.addApplication，DELETED将从本地剔除掉existingApp.removeInstance(instance)。</span><br></pre></td></tr></table></figure><h5 id="服务注册过程"><a href="#服务注册过程" class="headerlink" title="服务注册过程"></a>服务注册过程</h5><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">DiscoveryClient构造函数中，拉取fetchRegistry完后进行register注册。由于构造函数开始时已经将服务实例元数据封装好了instanceInfo，所以此处之间向server发送instanceInfo，</span><br><span class="line">通过方法httpResponse = eurekaTransport.registrationClient.register(instanceInfo);看到String urlPath = &quot;apps/&quot; + info.getAppName();又是一个server端点，退上去f7，httpResponse.getStatusCode() == Status.NO_CONTENT.getStatusCode();204状态码，则注册成功。</span><br></pre></td></tr></table></figure><h5 id="初始化3个定时任务"><a href="#初始化3个定时任务" class="headerlink" title="初始化3个定时任务"></a>初始化3个定时任务</h5><p>有3个：</p><ul><li>心跳定时任务：client会定时向server发送心跳，维持自己服务租约的有效性，用心跳定时任务实现;</li><li>缓存定时任务：server中会有不同的服务实例注册或是下线，所以client需要定时从server拉取注册表信息，用缓存定时任务实现。</li><li>按需注册的定时任务，当instanceinfo和status发生变化时，需要向server同步，去更新自己在server中的实例信息。保证server注册表中服务实例信息的有效和可用。</li></ul><p>参见initScheduledTasks()。</p><p>心跳定时任务和缓存刷新定时任务是有scheduler 的 schedule提交的，查看scheduler变量声明上，看到注释：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">A scheduler to be used for the following 3 tasks:</span><br><span class="line">- updating service urls</span><br><span class="line">- scheduling a TimedSuperVisorTask。</span><br></pre></td></tr></table></figure><p>由此可以知道循环逻辑是由TimedSuperVisorTask实现的。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br></pre></td><td class="code"><pre><span class="line">//心跳定时任务</span><br><span class="line">  new TimedSupervisorTask(</span><br><span class="line">                              &quot;heartbeat&quot;,</span><br><span class="line">                              scheduler,</span><br><span class="line">                              heartbeatExecutor,</span><br><span class="line">                              renewalIntervalInSecs,</span><br><span class="line">                              TimeUnit.SECONDS,</span><br><span class="line">                              expBackOffBound,</span><br><span class="line">                              new HeartbeatThread())</span><br><span class="line">  //HeartbeatThread线程内部run方法定义如下：</span><br><span class="line">class CacheRefreshThread implements Runnable&#123;</span><br><span class="line">    public void run() &#123;</span><br><span class="line">        if (renew()) &#123;</span><br><span class="line">            lastSuccessfulHeartbeatTimestamp = System.currentTimeMillis();</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">  </span><br><span class="line">//缓存定时任务</span><br><span class="line">  scheduler.schedule(</span><br><span class="line">                      new TimedSupervisorTask(</span><br><span class="line">                              &quot;cacheRefresh&quot;,</span><br><span class="line">                              scheduler,</span><br><span class="line">                              cacheRefreshExecutor,</span><br><span class="line">                              registryFetchIntervalSeconds,</span><br><span class="line">                              TimeUnit.SECONDS,</span><br><span class="line">                              expBackOffBound,</span><br><span class="line">                              new CacheRefreshThread()</span><br><span class="line">                      ),</span><br><span class="line"> //CacheRefreshThread线程内部run方法定义如下：</span><br><span class="line">class CacheRefreshThread implements Runnable &#123;</span><br><span class="line">          public void run() &#123;</span><br><span class="line">              refreshRegistry();</span><br><span class="line">          &#125;</span><br><span class="line">      &#125;</span><br><span class="line"></span><br><span class="line">//还有一个定时任务，按需注册。当instanceinfo和status发生变化时，需要向server同步，去更新自己在server中的实例信息。保证server注册表中服务实例信息的有效和可用。</span><br><span class="line">  // InstanceInfo replicator</span><br><span class="line">          instanceInfoReplicator = new InstanceInfoReplicator(</span><br><span class="line">                  this,</span><br><span class="line">                  instanceInfo,</span><br><span class="line">                  clientConfig.getInstanceInfoReplicationIntervalSeconds(),</span><br><span class="line">                  2); // burstSize</span><br><span class="line">  </span><br><span class="line">          statusChangeListener = new ApplicationInfoManager.StatusChangeListener() &#123;</span><br><span class="line">              @Override</span><br><span class="line">              public String getId() &#123;</span><br><span class="line">                  return &quot;statusChangeListener&quot;;</span><br><span class="line">              &#125;</span><br><span class="line">  </span><br><span class="line">           @Override</span><br><span class="line">              public void notify(StatusChangeEvent statusChangeEvent) &#123;</span><br><span class="line">                  if (InstanceStatus.DOWN == statusChangeEvent.getStatus() ||</span><br><span class="line">                          InstanceStatus.DOWN == statusChangeEvent.getPreviousStatus()) &#123;</span><br><span class="line">                      // log at warn level if DOWN was involved</span><br><span class="line">                      logger.warn(&quot;Saw local status change event &#123;&#125;&quot;, statusChangeEvent);</span><br><span class="line">                  &#125; else &#123;</span><br><span class="line">                      logger.info(&quot;Saw local status change event &#123;&#125;&quot;, statusChangeEvent);</span><br><span class="line">                  &#125;</span><br><span class="line">                  instanceInfoReplicator.onDemandUpdate();</span><br><span class="line">              &#125;</span><br><span class="line">          &#125;;</span><br><span class="line">          if (clientConfig.shouldOnDemandUpdateStatusChange()) &#123;</span><br><span class="line">              applicationInfoManager.registerStatusChangeListener(statusChangeListener);</span><br><span class="line">          &#125;</span><br><span class="line">      instanceInfoReplicator.start(clientConfig.getInitialInstanceInfoReplicationIntervalSeconds());    </span><br><span class="line">      </span><br><span class="line"></span><br><span class="line">//此定时任务有2个部分，</span><br><span class="line">//  1：定时刷新服务实例信息和检查应用状态的变化，在服务实例信息发生改变的情况下向server重新发起注册。InstanceInfoReplicator点进去。看到一个方法    </span><br><span class="line">  </span><br><span class="line">  public void run() &#123;</span><br><span class="line">          try &#123;</span><br><span class="line">              discoveryClient.refreshInstanceInfo();//刷新instanceinfo。</span><br><span class="line">            //如果实例信息有变，返回数据更新时间。</span><br><span class="line">              Long dirtyTimestamp = instanceInfo.isDirtyWithTime();</span><br><span class="line">              if (dirtyTimestamp != null) &#123;</span><br><span class="line">                  discoveryClient.register();//注册服务实例。</span><br><span class="line">                  instanceInfo.unsetIsDirty(dirtyTimestamp);</span><br><span class="line">              &#125;</span><br><span class="line">          &#125; catch (Throwable t) &#123;</span><br><span class="line">              logger.warn(&quot;There was a problem with the instance info replicator&quot;, t);</span><br><span class="line">          &#125; finally &#123;</span><br><span class="line">          //延时执行下一个检查任务。用于再次调用run方法，继续检查服务实例信息和状态的变化。</span><br><span class="line">              Future next = scheduler.schedule(this, replicationIntervalSeconds, TimeUnit.SECONDS);</span><br><span class="line">              scheduledPeriodicRef.set(next);</span><br><span class="line">          &#125;</span><br><span class="line">      &#125;      </span><br><span class="line"></span><br><span class="line">refreshInstanceInfo点进去，看方法注释：如果有变化，在下次心跳时，同步向server。</span><br><span class="line"></span><br><span class="line">//2.注册状态改变监听器，在应用状态发生变化时，刷新服务实例信息，在服务实例信息发生改变时向server注册。  看这段            </span><br><span class="line">   statusChangeListener = new ApplicationInfoManager.StatusChangeListener() &#123;</span><br><span class="line">                  @Override</span><br><span class="line">                  public String getId() &#123;</span><br><span class="line">                      return &quot;statusChangeListener&quot;;</span><br><span class="line">                  &#125;</span><br><span class="line"> @Override</span><br><span class="line">              public void notify(StatusChangeEvent statusChangeEvent) &#123;</span><br><span class="line">                  if (InstanceStatus.DOWN == statusChangeEvent.getStatus() ||</span><br><span class="line">                          InstanceStatus.DOWN == statusChangeEvent.getPreviousStatus()) &#123;</span><br><span class="line">                      // log at warn level if DOWN was involved</span><br><span class="line">                      logger.warn(&quot;Saw local status change event &#123;&#125;&quot;, statusChangeEvent);</span><br><span class="line">                  &#125; else &#123;</span><br><span class="line">                      logger.info(&quot;Saw local status change event &#123;&#125;&quot;, statusChangeEvent);</span><br><span class="line">                  &#125;</span><br><span class="line">                  instanceInfoReplicator.onDemandUpdate();</span><br><span class="line">              &#125;</span><br><span class="line">          &#125;;如果状态发生改变，调用onDemandUpdate（），点onDemandUpdate进去，看到InstanceInfoReplicator.this.run();     </span><br><span class="line">          </span><br><span class="line">//总结：两部分，一部分自己去检查，一部分等待状态监听事件。</span><br><span class="line"></span><br><span class="line">//初始化定时任务完成，最后一步启动步骤完成。接下来就是正常服务于业务。然后消亡。          </span><br></pre></td></tr></table></figure><h5 id="服务下线"><a href="#服务下线" class="headerlink" title="服务下线"></a>服务下线</h5><p>服务下线：在应用关闭时，client会向server注销自己，在Discoveryclient销毁前，会执行下面清理方法。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line">//此方法上有一个注解，表示：在销毁前执行此方法。unregisterStatusChangeListener注销监听器。</span><br><span class="line">@PreDestroy</span><br><span class="line">@Override</span><br><span class="line">public synchronized void shutdown() &#123;</span><br><span class="line">    ......</span><br><span class="line">        //取消定时任务。</span><br><span class="line">    cancelScheduledTasks();</span><br><span class="line">    ......</span><br><span class="line">    //服务下线。</span><br><span class="line">    unregister();</span><br><span class="line">    //关闭jersy客户端  </span><br><span class="line">    eurekaTransport.shutdown();</span><br><span class="line">    //其他操作</span><br><span class="line">    ......</span><br><span class="line">&#125; </span><br><span class="line"></span><br><span class="line">unregister点进去。cancel点进去。AbstractJerseyEurekaHttpClient。String urlPath = &quot;apps/&quot; + appName + &#x27;/&#x27; + id;看到url和http请求delete方法。      </span><br></pre></td></tr></table></figure><h4 id="更多"><a href="#更多" class="headerlink" title="更多"></a>更多</h4><p>更进一步的启动流程分析，可以参考这篇文章：<a href="https://www.jianshu.com/p/84d0b7eea882">Eureka之Client端注册</a></p><p>下面这张图即来自该文章，感觉还可以。</p><p><img src="/images/eureka-client-startup-work-flow-1024x728_79011726.png"></p><h3 id="在一个spring-cloud项目中，eureka客户端中的关键类DiscoveryClient是怎样实例化的？"><a href="#在一个spring-cloud项目中，eureka客户端中的关键类DiscoveryClient是怎样实例化的？" class="headerlink" title="在一个spring cloud项目中，eureka客户端中的关键类DiscoveryClient是怎样实例化的？"></a>在一个spring cloud项目中，eureka客户端中的关键类DiscoveryClient是怎样实例化的？</h3><p>首先，需要了解spring boot如何使用spring factories机制加载第三方库，这个可以参考我之前整理的这篇文章：<a href="https://blog.csdn.net/evasnowind/article/details/108647194">源码分析之Spring Boot如何利用Spring Factories机制进行自动注入</a>。</p><p>以笔者本人使用eureka时所使用的spring-cloud-netflix-eureka-client-2.2.2.RELEASE为例，在pom文件中引入该包后，可以IDEA中看到该包下包含了spring.factories</p><p><img src="/images/spring-cloud-netflix-eureka-client-metai_8e79bf68.png"></p><p>该文件内容如下：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">org.springframework.boot.autoconfigure.EnableAutoConfiguration=\</span><br><span class="line">org.springframework.cloud.netflix.eureka.config.EurekaClientConfigServerAutoConfiguration,\</span><br><span class="line">org.springframework.cloud.netflix.eureka.config.EurekaDiscoveryClientConfigServiceAutoConfiguration,\</span><br><span class="line">org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration,\</span><br><span class="line">org.springframework.cloud.netflix.ribbon.eureka.RibbonEurekaAutoConfiguration,\</span><br><span class="line">org.springframework.cloud.netflix.eureka.EurekaDiscoveryClientConfiguration,\</span><br><span class="line">org.springframework.cloud.netflix.eureka.reactive.EurekaReactiveDiscoveryClientConfiguration,\</span><br><span class="line">org.springframework.cloud.netflix.eureka.loadbalancer.LoadBalancerEurekaAutoConfiguration</span><br><span class="line"></span><br><span class="line">org.springframework.cloud.bootstrap.BootstrapConfiguration=\</span><br><span class="line">org.springframework.cloud.netflix.eureka.config.EurekaDiscoveryClientConfigServiceBootstrapConfiguration</span><br></pre></td></tr></table></figure><p>我们点<code>EurekaDiscoveryClientConfigServiceBootstrapConfiguration</code>，可以看到如下代码：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line">@ConditionalOnClass(ConfigServicePropertySourceLocator.class)</span><br><span class="line">@ConditionalOnProperty(value = &quot;spring.cloud.config.discovery.enabled&quot;,</span><br><span class="line">        matchIfMissing = false)</span><br><span class="line">@Configuration(proxyBeanMethods = false)</span><br><span class="line">@Import(&#123; EurekaDiscoveryClientConfiguration.class, // this emulates</span><br><span class="line">        // @EnableDiscoveryClient, the import</span><br><span class="line">        // selector doesn&#x27;t run before the</span><br><span class="line">        // bootstrap phase</span><br><span class="line">        EurekaClientAutoConfiguration.class,</span><br><span class="line">        EurekaReactiveDiscoveryClientConfiguration.class,</span><br><span class="line">        ReactiveCommonsClientAutoConfiguration.class &#125;)</span><br><span class="line">public class EurekaDiscoveryClientConfigServiceBootstrapConfiguration &#123;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在EurekaClientAutoConfiguration中，可以找到如下代码：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">CloudEurekaClient cloudEurekaClient = new CloudEurekaClient(appManager,</span><br><span class="line">                    config, this.optionalArgs, this.context);</span><br></pre></td></tr></table></figure><p>而<code>CloudEurekaClient</code>类，实际上继承自<code>DiscoveryClient</code>：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">public class CloudEurekaClient extends DiscoveryClient &#123;</span><br><span class="line">    ......</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>所以，在spring boot启动时，将通过spring factories机制，实例化DiscoveryClient。</p>]]>
    </content>
    <id>https://evasnowind.github.io/2020/09/21/2020-09-22_%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90%E4%B9%8BEureka%E5%AE%A2%E6%88%B7%E7%AB%AF%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90/</id>
    <link href="https://evasnowind.github.io/2020/09/21/2020-09-22_%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90%E4%B9%8BEureka%E5%AE%A2%E6%88%B7%E7%AB%AF%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90/"/>
    <published>2020-09-21T16:00:00.000Z</published>
    <summary>围绕Eureka客户端源码解析的实现原理、核心流程与关键细节做源码分析。</summary>
    <title>源码分析之Eureka客户端源码解析</title>
    <updated>2026-09-04T04:44:28.356Z</updated>
  </entry>
  <entry>
    <author>
      <name>Prayer</name>
    </author>
    <category term="Java" scheme="https://evasnowind.github.io/categories/Java/"/>
    <category term="并发" scheme="https://evasnowind.github.io/categories/Java/%E5%B9%B6%E5%8F%91/"/>
    <category term="Java" scheme="https://evasnowind.github.io/tags/Java/"/>
    <category term="源码分析" scheme="https://evasnowind.github.io/tags/%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/"/>
    <category term="并发" scheme="https://evasnowind.github.io/tags/%E5%B9%B6%E5%8F%91/"/>
    <content>
      <![CDATA[<p>我个人觉得，学习源码的姿势，首先是需要知道想学习的框架&#x2F;工具如何使用，然后接下来再去看源码注释，看看当时代码作者是如何阐述代码的，再去看代码怎么编写，效果才最佳。</p><p>同样的，接下来要分析的线程池，首先用途自不必说，不管有没有用过，ThreadPoolExecutor的运行机制、传说中的7个参数(核心线程数corePoolSize、最大线程数maxPoolSize、等待时间keepAliveTime、时间单位timeUnit、阻塞队列blockingQueue、线程工厂threadFactory、拒绝策略rejectHandler)，相信大家都已经熟练掌握，此处不再赘述。</p><p>接下来简单过一下ThreadPoolExecutor的注释。\</p><span id="more"></span><h2 id="0-源码注释中的关键点"><a href="#0-源码注释中的关键点" class="headerlink" title="0. 源码注释中的关键点"></a>0. 源码注释中的关键点</h2><p>ThreadPoolExecutor的类注释中，主要阐述如下内容：</p><ul><li>有关运行机制、7个参数如何使用与配合<ul><li>还有一些细节，比如阻塞队列，可以有3种选择：</li><li>direct handoff</li><li>unbounded queue</li><li>bounded queue</li><li>其他可能有用的信息：</li><li>线程池的hook方法：beforeExecute(Thread, Runnable)， afterExecute(Runnable, Throwable)。若hook抛出异常，则当前线程也会异常终止。</li><li>getQueue() 可以获取工作队列，可以用来monitor和debug，不推荐用于其他用途。提供了两个接口： remove(Runnable) ， purge()</li><li>线程池对象在程序中不被引用，并且没有线程时，该线程池将会被自动关闭。</li></ul></li><li>线程池状态的说明<ul><li><p>ThreadPoolExecutor有一个AtomicInteger变量ctl表示，该变量包含了两个含义：</p></li><li><p>高3位表示当前线程池状态</p><ul><li>高 3 位表示当前线程池状态：<ul><li>RUNNING: Accept new tasks and process queued tasks</li><li>SHUTDOWN: Don’t accept new tasks, but process queued tasks</li><li>STOP: Don’t accept new tasks, don’t process queued tasks, and interrupt in-progress tasks</li><li>TIDYING: All tasks have terminated, workerCount is zero, and the thread transitioning to state TIDYING will run the terminated() hook method</li><li>TERMINATED: terminated() has completed</li></ul></li><li>状态转移：这些状态支持比较，因为在 JDK 实现中，runState 会随时间单调递增，但不一定会经历每一个状态。<ul><li>RUNNING -&gt; SHUTDOWN：调用 shutdown() 时发生，finalize() 中也可能隐式触发</li><li>(RUNNING or SHUTDOWN) -&gt; STOP：调用 shutdownNow() 时发生</li><li>SHUTDOWN -&gt; TIDYING：当队列和线程池都为空时</li><li>STOP -&gt; TIDYING：当线程池为空时</li><li>TIDYING -&gt; TERMINATED：当 terminated() hook 方法执行完成时</li></ul></li></ul></li><li><p>剩余的低29位表示线程池的数量（所以目前的版本只有支持2^29-1个线程）</p></li></ul></li></ul><p>下面将结合代码，解说线程池的关键流程。</p><h2 id="1-常用变量的解释"><a href="#1-常用变量的解释" class="headerlink" title="1. 常用变量的解释"></a>1. 常用变量的解释</h2><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br></pre></td><td class="code"><pre><span class="line">// 1. `ctl`，可以看做一个int类型的数字，高3位表示线程池状态，低29位表示worker数量</span><br><span class="line">private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));</span><br><span class="line">// 2. `COUNT_BITS`，`Integer.SIZE`为32，所以`COUNT_BITS`为29</span><br><span class="line">private static final int COUNT_BITS = Integer.SIZE - 3;</span><br><span class="line">// 3. `CAPACITY`，线程池允许的最大线程数。1左移29位，然后减1，即为 2^29 - 1，二进制表示为连续的29个1</span><br><span class="line">private static final int CAPACITY   = (1 &lt;&lt; COUNT_BITS) - 1;</span><br><span class="line"></span><br><span class="line">// runState is stored in the high-order bits</span><br><span class="line">// 4. 线程池有5种状态，按大小排序如下：RUNNING &lt; SHUTDOWN &lt; STOP &lt; TIDYING &lt; TERMINATED .  此处RUNNING实际上是-(1&lt;&lt;COUNT_BITS)，是负数。</span><br><span class="line">private static final int RUNNING    = -1 &lt;&lt; COUNT_BITS;</span><br><span class="line">private static final int SHUTDOWN   =  0 &lt;&lt; COUNT_BITS;</span><br><span class="line">private static final int STOP       =  1 &lt;&lt; COUNT_BITS;</span><br><span class="line">private static final int TIDYING    =  2 &lt;&lt; COUNT_BITS;</span><br><span class="line">private static final int TERMINATED =  3 &lt;&lt; COUNT_BITS;</span><br><span class="line"></span><br><span class="line">// Packing and unpacking ctl</span><br><span class="line">// 5. `runStateOf()`，获取线程池状态，通过按位与操作，低29位将全部变成0</span><br><span class="line">private static int runStateOf(int c)     &#123; return c &amp; ~CAPACITY; &#125;</span><br><span class="line">// 6. `workerCountOf()`，获取线程池worker数量，通过按位与操作，高3位将全部变成0</span><br><span class="line">private static int workerCountOf(int c)  &#123; return c &amp; CAPACITY; &#125;</span><br><span class="line">// 7. `ctlOf()`，根据线程池状态和线程池worker数量，生成ctl值</span><br><span class="line">private static int ctlOf(int rs, int wc) &#123; return rs | wc; &#125;</span><br><span class="line"></span><br><span class="line">/*</span><br><span class="line"> * Bit field accessors that don&#x27;t require unpacking ctl.</span><br><span class="line"> * These depend on the bit layout and on workerCount being never negative.</span><br><span class="line"> */</span><br><span class="line">// 8. `runStateLessThan()`，线程池状态小于xx</span><br><span class="line">private static boolean runStateLessThan(int c, int s) &#123;</span><br><span class="line">    return c &lt; s;</span><br><span class="line">&#125;</span><br><span class="line">// 9. `runStateAtLeast()`，线程池状态大于等于xx</span><br><span class="line">private static boolean runStateAtLeast(int c, int s) &#123;</span><br><span class="line">    return c &gt;= s;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="2-构造方法"><a href="#2-构造方法" class="headerlink" title="2. 构造方法"></a>2. 构造方法</h2><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line">public ThreadPoolExecutor(int corePoolSize,</span><br><span class="line">                          int maximumPoolSize,</span><br><span class="line">                          long keepAliveTime,</span><br><span class="line">                          TimeUnit unit,</span><br><span class="line">                          BlockingQueue&lt;Runnable&gt; workQueue,</span><br><span class="line">                          ThreadFactory threadFactory,</span><br><span class="line">                          RejectedExecutionHandler handler) &#123;</span><br><span class="line">    // 基本类型参数校验</span><br><span class="line">    if (corePoolSize &lt; 0 ||</span><br><span class="line">        maximumPoolSize &lt;= 0 ||</span><br><span class="line">        maximumPoolSize &lt; corePoolSize ||</span><br><span class="line">        keepAliveTime &lt; 0)</span><br><span class="line">        throw new IllegalArgumentException();</span><br><span class="line">    // 空指针校验</span><br><span class="line">    if (workQueue == null || threadFactory == null || handler == null)</span><br><span class="line">        throw new NullPointerException();</span><br><span class="line">    this.corePoolSize = corePoolSize;</span><br><span class="line">    this.maximumPoolSize = maximumPoolSize;</span><br><span class="line">    this.workQueue = workQueue;</span><br><span class="line">    // 根据传入参数`unit`和`keepAliveTime`，将存活时间转换为纳秒存到变量`keepAliveTime `中</span><br><span class="line">    this.keepAliveTime = unit.toNanos(keepAliveTime);</span><br><span class="line">    this.threadFactory = threadFactory;</span><br><span class="line">    this.handler = handler;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="3-提交执行task的过程"><a href="#3-提交执行task的过程" class="headerlink" title="3. 提交执行task的过程"></a>3. 提交执行task的过程</h2><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br></pre></td><td class="code"><pre><span class="line">public void execute(Runnable command) &#123;</span><br><span class="line">    if (command == null)</span><br><span class="line">        throw new NullPointerException();</span><br><span class="line">    /*</span><br><span class="line">     * Proceed in 3 steps:</span><br><span class="line">     *</span><br><span class="line">     * 1. If fewer than corePoolSize threads are running, try to</span><br><span class="line">     * start a new thread with the given command as its first</span><br><span class="line">     * task.  The call to addWorker atomically checks runState and</span><br><span class="line">     * workerCount, and so prevents false alarms that would add</span><br><span class="line">     * threads when it shouldn&#x27;t, by returning false.</span><br><span class="line">     *</span><br><span class="line">     * 2. If a task can be successfully queued, then we still need</span><br><span class="line">     * to double-check whether we should have added a thread</span><br><span class="line">     * (because existing ones died since last checking) or that</span><br><span class="line">     * the pool shut down since entry into this method. So we</span><br><span class="line">     * recheck state and if necessary roll back the enqueuing if</span><br><span class="line">     * stopped, or start a new thread if there are none.</span><br><span class="line">     *</span><br><span class="line">     * 3. If we cannot queue task, then we try to add a new</span><br><span class="line">     * thread.  If it fails, we know we are shut down or saturated</span><br><span class="line">     * and so reject the task.</span><br><span class="line">     */</span><br><span class="line">    int c = ctl.get();</span><br><span class="line">    // worker数量比核心线程数小，直接创建worker执行任务</span><br><span class="line">    if (workerCountOf(c) &lt; corePoolSize) &#123;</span><br><span class="line">        if (addWorker(command, true))</span><br><span class="line">            return;</span><br><span class="line">        c = ctl.get();</span><br><span class="line">    &#125;</span><br><span class="line">    // worker数量超过核心线程数，任务直接进入队列</span><br><span class="line">    if (isRunning(c) &amp;&amp; workQueue.offer(command)) &#123;</span><br><span class="line">        int recheck = ctl.get();</span><br><span class="line">        // 线程池状态不是RUNNING状态，说明执行过shutdown命令，需要对新加入的任务执行reject()操作。</span><br><span class="line">        // 这儿为什么需要recheck，是因为任务入队列前后，线程池的状态可能会发生变化。</span><br><span class="line">        if (! isRunning(recheck) &amp;&amp; remove(command))</span><br><span class="line">            reject(command);</span><br><span class="line">        // 这儿为什么需要判断0值，主要是在线程池构造方法中，核心线程数允许为0</span><br><span class="line">        else if (workerCountOf(recheck) == 0)</span><br><span class="line">            addWorker(null, false);</span><br><span class="line">    &#125;</span><br><span class="line">    // 如果线程池不是运行状态，或者任务进入队列失败，则尝试创建worker执行任务。</span><br><span class="line">    // 这儿有3点需要注意：</span><br><span class="line">    // 1. 线程池不是运行状态时，addWorker内部会判断线程池状态</span><br><span class="line">    // 2. addWorker第2个参数表示是否创建核心线程</span><br><span class="line">    // 3. addWorker返回false，则说明任务执行失败，需要执行reject操作</span><br><span class="line">    else if (!addWorker(command, false))</span><br><span class="line">        reject(command);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="4-addworker源码解析"><a href="#4-addworker源码解析" class="headerlink" title="4. addworker源码解析"></a>4. addworker源码解析</h2><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br></pre></td><td class="code"><pre><span class="line">private boolean addWorker(Runnable firstTask, boolean core) &#123;</span><br><span class="line">    retry:</span><br><span class="line">    //外层自旋</span><br><span class="line">    for (;;) &#123;</span><br><span class="line">        int c = ctl.get();</span><br><span class="line">        int rs = runStateOf(c);</span><br><span class="line"></span><br><span class="line">        // 这个条件写得比较难懂，和下面的条件等价</span><br><span class="line">        // (rs &gt; SHUTDOWN) || </span><br><span class="line">        // (rs == SHUTDOWN &amp;&amp; firstTask != null) || </span><br><span class="line">        // (rs == SHUTDOWN &amp;&amp; workQueue.isEmpty())</span><br><span class="line">        // 1. 线程池状态大于SHUTDOWN时，直接返回false</span><br><span class="line">        // 2. 线程池状态等于SHUTDOWN，且firstTask不为null，直接返回false</span><br><span class="line">        // 3. 线程池状态等于SHUTDOWN，且队列为空，直接返回false</span><br><span class="line">        // Check if queue empty only if necessary.</span><br><span class="line">        if (rs &gt;= SHUTDOWN &amp;&amp;</span><br><span class="line">            ! (rs == SHUTDOWN &amp;&amp;</span><br><span class="line">               firstTask == null &amp;&amp;</span><br><span class="line">               ! workQueue.isEmpty()))</span><br><span class="line">            return false;</span><br><span class="line"></span><br><span class="line">        // 内层自旋</span><br><span class="line">        for (;;) &#123;</span><br><span class="line">            int wc = workerCountOf(c);</span><br><span class="line">            // worker数量超过容量，直接返回false</span><br><span class="line">            if (wc &gt;= CAPACITY ||</span><br><span class="line">                wc &gt;= (core ? corePoolSize : maximumPoolSize))</span><br><span class="line">                return false;</span><br><span class="line">             // 使用CAS的方式增加worker数量。</span><br><span class="line">            // 若增加成功，则直接跳出外层循环进入到第二部分</span><br><span class="line">            if (compareAndIncrementWorkerCount(c))</span><br><span class="line">                break retry;</span><br><span class="line">            c = ctl.get();  // Re-read ctl</span><br><span class="line">            // 线程池状态发生变化，对外层循环进行自旋</span><br><span class="line">            if (runStateOf(c) != rs)</span><br><span class="line">                continue retry;</span><br><span class="line">             // 其他情况，直接内层循环进行自旋即可</span><br><span class="line">            // else CAS failed due to workerCount change; retry inner loop</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    boolean workerStarted = false;</span><br><span class="line">    boolean workerAdded = false;</span><br><span class="line">    Worker w = null;</span><br><span class="line">    try &#123;</span><br><span class="line">        w = new Worker(firstTask);</span><br><span class="line">        final Thread t = w.thread;</span><br><span class="line">        if (t != null) &#123;</span><br><span class="line">            final ReentrantLock mainLock = this.mainLock;</span><br><span class="line">             // worker的添加必须是串行的，因此需要加锁</span><br><span class="line">            mainLock.lock();</span><br><span class="line">            try &#123;</span><br><span class="line">                // 这儿需要重新检查线程池状态</span><br><span class="line">                // Recheck while holding lock.</span><br><span class="line">                // Back out on ThreadFactory failure or if</span><br><span class="line">                // shut down before lock acquired.</span><br><span class="line">                int rs = runStateOf(ctl.get());</span><br><span class="line"></span><br><span class="line">                if (rs &lt; SHUTDOWN ||</span><br><span class="line">                    (rs == SHUTDOWN &amp;&amp; firstTask == null)) &#123;</span><br><span class="line">                    // worker已经调用过了start()方法，则不再创建worker</span><br><span class="line">                    if (t.isAlive()) // precheck that t is startable</span><br><span class="line">                        throw new IllegalThreadStateException();</span><br><span class="line">                    // worker创建并添加到workers成功</span><br><span class="line">                    workers.add(w);</span><br><span class="line">                    // 更新`largestPoolSize`变量</span><br><span class="line">                    int s = workers.size();</span><br><span class="line">                    if (s &gt; largestPoolSize)</span><br><span class="line">                        largestPoolSize = s;</span><br><span class="line">                    workerAdded = true;</span><br><span class="line">                &#125;</span><br><span class="line">            &#125; finally &#123;</span><br><span class="line">                mainLock.unlock();</span><br><span class="line">            &#125;</span><br><span class="line">            // 启动worker线程</span><br><span class="line">            if (workerAdded) &#123;</span><br><span class="line">                t.start();</span><br><span class="line">                workerStarted = true;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125; finally &#123;</span><br><span class="line">        // worker线程启动失败，说明线程池状态发生了变化（关闭操作被执行），需要进行shutdown相关操作</span><br><span class="line">        if (! workerStarted)</span><br><span class="line">            addWorkerFailed(w);</span><br><span class="line">    &#125;</span><br><span class="line">    return workerStarted;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="5-线程池worker任务单元"><a href="#5-线程池worker任务单元" class="headerlink" title="5. 线程池worker任务单元"></a>5. 线程池worker任务单元</h2><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line">private final class Worker</span><br><span class="line">    extends AbstractQueuedSynchronizer</span><br><span class="line">    implements Runnable</span><br><span class="line">&#123;</span><br><span class="line">    /**</span><br><span class="line">     * This class will never be serialized, but we provide a</span><br><span class="line">     * serialVersionUID to suppress a javac warning.</span><br><span class="line">     */</span><br><span class="line">    private static final long serialVersionUID = 6138294804551838833L;</span><br><span class="line"></span><br><span class="line">    /** Thread this worker is running in.  Null if factory fails. */</span><br><span class="line">    final Thread thread;</span><br><span class="line">    /** Initial task to run.  Possibly null. */</span><br><span class="line">    Runnable firstTask;</span><br><span class="line">    /** Per-thread task counter */</span><br><span class="line">    volatile long completedTasks;</span><br><span class="line"></span><br><span class="line">    /**</span><br><span class="line">     * Creates with given first task and thread from ThreadFactory.</span><br><span class="line">     * @param firstTask the first task (null if none)</span><br><span class="line">     */</span><br><span class="line">    Worker(Runnable firstTask) &#123;</span><br><span class="line">        setState(-1); // inhibit interrupts until runWorker</span><br><span class="line">        this.firstTask = firstTask;</span><br><span class="line">        // 这儿是Worker的关键所在，使用了线程工厂创建了一个线程。传入的参数为当前worker</span><br><span class="line">        this.thread = getThreadFactory().newThread(this);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    /** Delegates main run loop to outer runWorker  */</span><br><span class="line">    public void run() &#123;</span><br><span class="line">        //此处分析参见 `核心线程执行逻辑-runworker`</span><br><span class="line">        runWorker(this);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    // 省略代码...</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="6-核心线程执行逻辑-runworker"><a href="#6-核心线程执行逻辑-runworker" class="headerlink" title="6. 核心线程执行逻辑-runworker"></a>6. 核心线程执行逻辑-runworker</h2><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br></pre></td><td class="code"><pre><span class="line">final void runWorker(Worker w) &#123;</span><br><span class="line">    Thread wt = Thread.currentThread();</span><br><span class="line">    Runnable task = w.firstTask;</span><br><span class="line">    w.firstTask = null;</span><br><span class="line">    // 调用unlock()是为了让外部可以中断</span><br><span class="line">    w.unlock(); // allow interrupts</span><br><span class="line">    // 这个变量用于判断是否进入过自旋（while循环）</span><br><span class="line">    boolean completedAbruptly = true;</span><br><span class="line">    try &#123;</span><br><span class="line">        // 这儿是自旋</span><br><span class="line">        // 1. 如果firstTask不为null，则执行firstTask；</span><br><span class="line">        // 2. 如果firstTask为null，则调用getTask()从队列获取任务。</span><br><span class="line">        // 3. 阻塞队列的特性就是：当队列为空时，当前线程会被阻塞等待</span><br><span class="line">        while (task != null || (task = getTask()) != null) &#123;</span><br><span class="line">            // 这儿对worker进行加锁，是为了达到下面的目的</span><br><span class="line">            // 1. 降低锁范围，提升性能</span><br><span class="line">            // 2. 保证每个worker执行的任务是串行的</span><br><span class="line">            w.lock();</span><br><span class="line">            // If pool is stopping, ensure thread is interrupted;</span><br><span class="line">            // if not, ensure thread is not interrupted.  This</span><br><span class="line">            // requires a recheck in second case to deal with</span><br><span class="line">            // shutdownNow race while clearing interrupt</span><br><span class="line">            // 如果线程池正在停止，则对当前线程进行中断操作</span><br><span class="line">            if ((runStateAtLeast(ctl.get(), STOP) ||</span><br><span class="line">                 (Thread.interrupted() &amp;&amp;</span><br><span class="line">                  runStateAtLeast(ctl.get(), STOP))) &amp;&amp;</span><br><span class="line">                !wt.isInterrupted())</span><br><span class="line">                wt.interrupt();</span><br><span class="line">            // 执行任务，且在执行前后通过`beforeExecute()`和`afterExecute()`来扩展其功能。</span><br><span class="line">            // 这两个方法在当前类里面为空实现。</span><br><span class="line">            try &#123;</span><br><span class="line">                beforeExecute(wt, task);</span><br><span class="line">                Throwable thrown = null;</span><br><span class="line">                try &#123;</span><br><span class="line">                    task.run();</span><br><span class="line">                &#125; catch (RuntimeException x) &#123;</span><br><span class="line">                    thrown = x; throw x;</span><br><span class="line">                &#125; catch (Error x) &#123;</span><br><span class="line">                    thrown = x; throw x;</span><br><span class="line">                &#125; catch (Throwable x) &#123;</span><br><span class="line">                    thrown = x; throw new Error(x);</span><br><span class="line">                &#125; finally &#123;</span><br><span class="line">                    afterExecute(task, thrown);</span><br><span class="line">                &#125;</span><br><span class="line">            &#125; finally &#123;</span><br><span class="line">                // 帮助gc</span><br><span class="line">                task = null;</span><br><span class="line">                // 已完成任务数加一 </span><br><span class="line">                w.completedTasks++;</span><br><span class="line">                w.unlock();</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        completedAbruptly = false;</span><br><span class="line">    &#125; finally &#123;</span><br><span class="line">        // 自旋操作被退出，说明线程池正在结束</span><br><span class="line">        processWorkerExit(w, completedAbruptly);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><ul><li><a href="https://github.com/bjmashibing/JUC/">ThreadPoolExecutor源码解析</a></li></ul>]]>
    </content>
    <id>https://evasnowind.github.io/2020/09/20/2020-09-21_%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90%E4%B9%8BJava%E7%BA%BF%E7%A8%8B%E6%B1%A0ThreadPoolExecutor/</id>
    <link href="https://evasnowind.github.io/2020/09/20/2020-09-21_%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90%E4%B9%8BJava%E7%BA%BF%E7%A8%8B%E6%B1%A0ThreadPoolExecutor/"/>
    <published>2020-09-20T16:00:00.000Z</published>
    <summary>我个人觉得，学习源码的姿势，首先是需要知道想学习的框架/工具如何使用，然后接下来再去看源码注释，看看当时代码作者是如何阐述代码的，再去看代码怎么编写，效果才最佳。</summary>
    <title>源码分析之Java线程池ThreadPoolExecutor</title>
    <updated>2026-09-04T04:44:28.356Z</updated>
  </entry>
  <entry>
    <author>
      <name>Prayer</name>
    </author>
    <category term="Java" scheme="https://evasnowind.github.io/categories/Java/"/>
    <category term="Spring Boot" scheme="https://evasnowind.github.io/categories/Java/Spring-Boot/"/>
    <category term="源码分析" scheme="https://evasnowind.github.io/tags/%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/"/>
    <category term="Spring Boot" scheme="https://evasnowind.github.io/tags/Spring-Boot/"/>
    <content>
      <![CDATA[<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>本文所涉及spring&#x2F;spring boot代码，请参考spring boot 2.2.6对应版本。</p><p>我们在刚学习spring boot时，有没有一个困惑：spring boot能够自动实例化很多第三方的依赖库？比如eureka、druid等。这个就涉及到spring boot的扩展机制spring factories。</p><span id="more"></span><p>简单来将，spring factories类似与Java SPI机制，利用该机制，我们能够自定义实现一些SDK或是spring boot starter，其实例化过程由我们来实现，使用方只需要在项目中引入包、不需要或是只需做很少的配置。</p><h2 id="Spring-Factories的核心"><a href="#Spring-Factories的核心" class="headerlink" title="Spring Factories的核心"></a>Spring Factories的核心</h2><p>spring factories机制核心在spring-core包中定义的SpringFactoriesLoader类，该类的公有方法只有2个：\</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">/*</span><br><span class="line">根据接口类获取其实现类的实例，这个方法返回的是对象列表。</span><br><span class="line">Load and instantiate the factory implementations of the given type from &quot;META-INF/spring.factories&quot;, using the given class loader.</span><br><span class="line">The returned factories are sorted through AnnotationAwareOrderComparator.</span><br><span class="line">If a custom instantiation strategy is required, use loadFactoryNames(java.lang.Class&lt;?&gt;, java.lang.ClassLoader) to obtain all registered factory names.</span><br><span class="line">*/</span><br><span class="line">public static &lt;T&gt; List&lt;T&gt; loadFactories(Class&lt;T&gt; factoryType, @Nullable ClassLoader classLoader)</span><br><span class="line"></span><br><span class="line">/*</span><br><span class="line">根据接口获取其接口类的名称，这个方法返回的是类名的列表。</span><br><span class="line">Load the fully qualified class names of factory implementations of the given type from &quot;META-INF/spring.factories&quot;, using the given class loader.</span><br><span class="line">*/</span><br><span class="line">public static List&lt;String&gt; loadFactoryNames(Class&lt;?&gt; factoryType, @Nullable ClassLoader classLoader)</span><br></pre></td></tr></table></figure><p>而上面这两个方法，最终都会调用一个SpringFactoriesLoader的私有方法loadSpringFactories，从指定的ClassLoader中获取spring.factories文件，并解析得到类名列表。具体代码如下：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line">private static Map&lt;String, List&lt;String&gt;&gt; loadSpringFactories(@Nullable ClassLoader classLoader) &#123;</span><br><span class="line">        MultiValueMap&lt;String, String&gt; result = cache.get(classLoader);</span><br><span class="line">        if (result != null) &#123;</span><br><span class="line">            return result;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        try &#123;</span><br><span class="line">            Enumeration&lt;URL&gt; urls = (classLoader != null ?</span><br><span class="line">                    classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :</span><br><span class="line">                    ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));</span><br><span class="line">            result = new LinkedMultiValueMap&lt;&gt;();</span><br><span class="line">            while (urls.hasMoreElements()) &#123;</span><br><span class="line">                URL url = urls.nextElement();</span><br><span class="line">                UrlResource resource = new UrlResource(url);</span><br><span class="line">                Properties properties = PropertiesLoaderUtils.loadProperties(resource);</span><br><span class="line">                for (Map.Entry&lt;?, ?&gt; entry : properties.entrySet()) &#123;</span><br><span class="line">                    String factoryTypeName = ((String) entry.getKey()).trim();</span><br><span class="line">                    for (String factoryImplementationName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) &#123;</span><br><span class="line">                        result.add(factoryTypeName, factoryImplementationName.trim());</span><br><span class="line">                    &#125;</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">            cache.put(classLoader, result);</span><br><span class="line">            return result;</span><br><span class="line">        &#125;</span><br><span class="line">        catch (IOException ex) &#123;</span><br><span class="line">            throw new IllegalArgumentException(&quot;Unable to load factories from location [&quot; +</span><br><span class="line">                    FACTORIES_RESOURCE_LOCATION + &quot;]&quot;, ex);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure><p>**该代码作用：遍历整个ClassLoader中所有jar包下的spring.factories文件。**spring.factories文件的位置：jar包下META-INF&#x2F;spring.factories。</p><p>有没有感觉很熟悉？Java SPI的读取目录在<code>META-INF/services</code>下，其实大家写代码都是相互参考、然后形成一个约定俗成习惯的。</p><p>我们可以在自己的jar中配置spring.factories文件，不会影响到其它地方的配置，也不会被别人的配置覆盖。</p><h2 id="示例"><a href="#示例" class="headerlink" title="示例"></a>示例</h2><p>举个例子，spring boot start的实现中，如下所示：<br><img src="/images/spring-boot-starter-factories-snapshot-3_d3582dd7.jpg"></p><p>spring-boot的spring.factories具体内容如下：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br></pre></td><td class="code"><pre><span class="line"># PropertySource Loaders</span><br><span class="line">org.springframework.boot.env.PropertySourceLoader=\</span><br><span class="line">org.springframework.boot.env.PropertiesPropertySourceLoader,\</span><br><span class="line">org.springframework.boot.env.YamlPropertySourceLoader</span><br><span class="line"></span><br><span class="line"># Run Listeners</span><br><span class="line">org.springframework.boot.SpringApplicationRunListener=\</span><br><span class="line">org.springframework.boot.context.event.EventPublishingRunListener</span><br><span class="line"></span><br><span class="line"># Error Reporters</span><br><span class="line">org.springframework.boot.SpringBootExceptionReporter=\</span><br><span class="line">org.springframework.boot.diagnostics.FailureAnalyzers</span><br><span class="line"></span><br><span class="line"># Application Context Initializers</span><br><span class="line">org.springframework.context.ApplicationContextInitializer=\</span><br><span class="line">org.springframework.boot.context.ConfigurationWarningsApplicationContextInitializer,\</span><br><span class="line">org.springframework.boot.context.ContextIdApplicationContextInitializer,\</span><br><span class="line">org.springframework.boot.context.config.DelegatingApplicationContextInitializer,\</span><br><span class="line">org.springframework.boot.rsocket.context.RSocketPortInfoApplicationContextInitializer,\</span><br><span class="line">org.springframework.boot.web.context.ServerPortInfoApplicationContextInitializer</span><br><span class="line"></span><br><span class="line"># Application Listeners</span><br><span class="line">org.springframework.context.ApplicationListener=\</span><br><span class="line">org.springframework.boot.ClearCachesApplicationListener,\</span><br><span class="line">org.springframework.boot.builder.ParentContextCloserApplicationListener,\</span><br><span class="line">org.springframework.boot.cloud.CloudFoundryVcapEnvironmentPostProcessor,\</span><br><span class="line">org.springframework.boot.context.FileEncodingApplicationListener,\</span><br><span class="line">org.springframework.boot.context.config.AnsiOutputApplicationListener,\</span><br><span class="line">org.springframework.boot.context.config.ConfigFileApplicationListener,\</span><br><span class="line">org.springframework.boot.context.config.DelegatingApplicationListener,\</span><br><span class="line">org.springframework.boot.context.logging.ClasspathLoggingApplicationListener,\</span><br><span class="line">org.springframework.boot.context.logging.LoggingApplicationListener,\</span><br><span class="line">org.springframework.boot.liquibase.LiquibaseServiceLocatorApplicationListener</span><br><span class="line"></span><br><span class="line"># Environment Post Processors</span><br><span class="line">org.springframework.boot.env.EnvironmentPostProcessor=\</span><br><span class="line">org.springframework.boot.cloud.CloudFoundryVcapEnvironmentPostProcessor,\</span><br><span class="line">org.springframework.boot.env.SpringApplicationJsonEnvironmentPostProcessor,\</span><br><span class="line">org.springframework.boot.env.SystemEnvironmentPropertySourceEnvironmentPostProcessor,\</span><br><span class="line">org.springframework.boot.reactor.DebugAgentEnvironmentPostProcessor</span><br><span class="line"></span><br><span class="line"># Failure Analyzers</span><br><span class="line">org.springframework.boot.diagnostics.FailureAnalyzer=\</span><br><span class="line">org.springframework.boot.diagnostics.analyzer.BeanCurrentlyInCreationFailureAnalyzer,\</span><br><span class="line">org.springframework.boot.diagnostics.analyzer.BeanDefinitionOverrideFailureAnalyzer,\</span><br><span class="line">org.springframework.boot.diagnostics.analyzer.BeanNotOfRequiredTypeFailureAnalyzer,\</span><br><span class="line">org.springframework.boot.diagnostics.analyzer.BindFailureAnalyzer,\</span><br><span class="line">org.springframework.boot.diagnostics.analyzer.BindValidationFailureAnalyzer,\</span><br><span class="line">org.springframework.boot.diagnostics.analyzer.UnboundConfigurationPropertyFailureAnalyzer,\</span><br><span class="line">org.springframework.boot.diagnostics.analyzer.ConnectorStartFailureAnalyzer,\</span><br><span class="line">org.springframework.boot.diagnostics.analyzer.NoSuchMethodFailureAnalyzer,\</span><br><span class="line">org.springframework.boot.diagnostics.analyzer.NoUniqueBeanDefinitionFailureAnalyzer,\</span><br><span class="line">org.springframework.boot.diagnostics.analyzer.PortInUseFailureAnalyzer,\</span><br><span class="line">org.springframework.boot.diagnostics.analyzer.ValidationExceptionFailureAnalyzer,\</span><br><span class="line">org.springframework.boot.diagnostics.analyzer.InvalidConfigurationPropertyNameFailureAnalyzer,\</span><br><span class="line">org.springframework.boot.diagnostics.analyzer.InvalidConfigurationPropertyValueFailureAnalyzer</span><br><span class="line"></span><br><span class="line"># FailureAnalysisReporters</span><br><span class="line">org.springframework.boot.diagnostics.FailureAnalysisReporter=\</span><br><span class="line">org.springframework.boot.diagnostics.LoggingFailureAnalysisReporter</span><br></pre></td></tr></table></figure><h2 id="spring-boot如何利用spring-factories进行注入"><a href="#spring-boot如何利用spring-factories进行注入" class="headerlink" title="spring boot如何利用spring.factories进行注入"></a>spring boot如何利用spring.factories进行注入</h2><p>一个spring boot项目，在启动类上会有<code>@SpringBootApplication</code>注解，该注解实现：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">@Target(ElementType.TYPE)</span><br><span class="line">@Retention(RetentionPolicy.RUNTIME)</span><br><span class="line">@Documented</span><br><span class="line">@Inherited</span><br><span class="line">@SpringBootConfiguration</span><br><span class="line">@EnableAutoConfiguration</span><br><span class="line">@ComponentScan(excludeFilters = &#123; @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),</span><br><span class="line">        @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) &#125;)</span><br><span class="line">public @interface SpringBootApplication &#123;</span><br><span class="line">    ...</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>其中，<code>@EnableAutoConfiguration</code>注解定义大体如下：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">@Target(ElementType.TYPE)</span><br><span class="line">@Retention(RetentionPolicy.RUNTIME)</span><br><span class="line">@Documented</span><br><span class="line">@Inherited</span><br><span class="line">@AutoConfigurationPackage</span><br><span class="line">@Import(AutoConfigurationImportSelector.class)</span><br><span class="line">public @interface EnableAutoConfiguration &#123;</span><br><span class="line">    ...</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>其中<code>@Import(AutoConfigurationImportSelector.class)</code> 很关键，@Import注解通过快速导入的方式实现把实例加入spring的IOC容器中，可以用于导入第三方包。AutoConfigurationImportSelector实现了ImportSelector接口，任何实现ImportSelector的类，都会在启动时被spring-context包<code>ConfigurationClassParser</code>中的<code>processImports</code>进行实例化，并执行<code>selectImports</code>方法。</p><p>AutoConfigurationImportSelector的selectImports以及相关的方法实现如下：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br></pre></td><td class="code"><pre><span class="line">/*</span><br><span class="line">这部分是AutoConfigurationImportSelector的代码</span><br><span class="line">*/</span><br><span class="line">public String[] selectImports(AnnotationMetadata annotationMetadata) &#123;</span><br><span class="line">    if (!isEnabled(annotationMetadata)) &#123;</span><br><span class="line">        return NO_IMPORTS;</span><br><span class="line">    &#125;</span><br><span class="line">    AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader</span><br><span class="line">        .loadMetadata(this.beanClassLoader);</span><br><span class="line">    AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(autoConfigurationMetadata,</span><br><span class="line">                                                                              annotationMetadata);</span><br><span class="line">    return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">protected AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata,</span><br><span class="line">            AnnotationMetadata annotationMetadata) &#123;</span><br><span class="line">    if (!isEnabled(annotationMetadata)) &#123;</span><br><span class="line">        return EMPTY_ENTRY;</span><br><span class="line">    &#125;</span><br><span class="line">    AnnotationAttributes attributes = getAttributes(annotationMetadata);</span><br><span class="line">    List&lt;String&gt; configurations = getCandidateConfigurations(annotationMetadata, attributes);</span><br><span class="line">    configurations = removeDuplicates(configurations);</span><br><span class="line">    Set&lt;String&gt; exclusions = getExclusions(annotationMetadata, attributes);</span><br><span class="line">    checkExcludedClasses(configurations, exclusions);</span><br><span class="line">    configurations.removeAll(exclusions);</span><br><span class="line">    configurations = filter(configurations, autoConfigurationMetadata);</span><br><span class="line">    fireAutoConfigurationImportEvents(configurations, exclusions);</span><br><span class="line">    return new AutoConfigurationEntry(configurations, exclusions);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">protected List&lt;String&gt; getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) &#123;</span><br><span class="line">        List&lt;String&gt; configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),</span><br><span class="line">                                                                             getBeanClassLoader());</span><br><span class="line">        Assert.notEmpty(configurations, &quot;No auto configuration classes found in META-INF/spring.factories. If you &quot;</span><br><span class="line">                        + &quot;are using a custom packaging, make sure that file is correct.&quot;);</span><br><span class="line">        return configurations;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">protected Class&lt;?&gt; getSpringFactoriesLoaderFactoryClass() &#123;</span><br><span class="line">    return EnableAutoConfiguration.class;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">/*</span><br><span class="line">下面是AutoConfigurationMetadataLoader中的代码</span><br><span class="line">*/</span><br><span class="line">protected static final String PATH = &quot;META-INF/spring-autoconfigure-metadata.properties&quot;;</span><br><span class="line">static AutoConfigurationMetadata loadMetadata(ClassLoader classLoader) &#123;</span><br><span class="line">        return loadMetadata(classLoader, PATH);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">static AutoConfigurationMetadata loadMetadata(ClassLoader classLoader, String path) &#123;</span><br><span class="line">    try &#123;</span><br><span class="line">        Enumeration&lt;URL&gt; urls = (classLoader != null) ? classLoader.getResources(path)</span><br><span class="line">            : ClassLoader.getSystemResources(path);</span><br><span class="line">        Properties properties = new Properties();</span><br><span class="line">        while (urls.hasMoreElements()) &#123;</span><br><span class="line">            properties.putAll(PropertiesLoaderUtils.loadProperties(new UrlResource(urls.nextElement())));</span><br><span class="line">        &#125;</span><br><span class="line">        return loadMetadata(properties);</span><br><span class="line">    &#125;</span><br><span class="line">    catch (IOException ex) &#123;</span><br><span class="line">        throw new IllegalArgumentException(&quot;Unable to load @ConditionalOnClass location [&quot; + path + &quot;]&quot;, ex);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>可以看到，AutoConfigurationImportSelector的selectImports方法主要是用于加载类，但为了获取哪些类需要加载，则是通过SpringFactoriesLoader去加载对应的spring.factories。大体调用链路：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">AutoConfigurationImportSelector#selectImports()</span><br><span class="line">    -&gt; AutoConfigurationImportSelector#getAutoConfigurationEntry()</span><br><span class="line">        -&gt; AutoConfigurationImportSelector#getCandidateConfigurations()</span><br><span class="line">            -&gt; SpringFactoriesLoader#loadFactoryNames()</span><br></pre></td></tr></table></figure><p>最终执行的是 <code>loadFactoryNames(EnableAutoConfiguration.class, 当前classloader)</code>， 结合上面<code>Spring Factories的核心</code>这一小节，可以获知，SpringFactoriesLoader将会根据EnableAutoConfiguration接口，去所有<code>spring.factories</code>找<code>EnableAutoConfiguration.class</code>所对应的<code>values</code>，并返回。</p><h2 id="常见扩展点"><a href="#常见扩展点" class="headerlink" title="常见扩展点"></a>常见扩展点</h2><p>上面已经提到，spring factories需要给出一个spring.factories文件，该文件规定了bean注入的扩展点。</p><p>常见扩展点如下：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"># Auto Configure（这个扩展是使用的最多的，特别是是一些公共SDK，会这借助这扩展实现Bean的自动注入）</span><br><span class="line">org.springframework.boot.autoconfigure.EnableAutoConfiguration</span><br><span class="line"></span><br><span class="line"># PropertySource Loaders</span><br><span class="line">org.springframework.boot.env.PropertySourceLoader</span><br><span class="line"></span><br><span class="line"># Run Listeners</span><br><span class="line">org.springframework.boot.SpringApplicationRunListener</span><br><span class="line"></span><br><span class="line"># Error Reporters</span><br><span class="line">org.springframework.boot.SpringBootExceptionReporter</span><br><span class="line"></span><br><span class="line"># Application Context Initializers</span><br><span class="line">org.springframework.context.ApplicationContextInitializer</span><br><span class="line"></span><br><span class="line"># Application Listeners</span><br><span class="line">org.springframework.context.ApplicationListener</span><br><span class="line"></span><br><span class="line"># Environment Post Processors</span><br><span class="line">org.springframework.boot.env.EnvironmentPostProcessor</span><br><span class="line"></span><br><span class="line"># Failure Analyzers</span><br><span class="line">org.springframework.boot.diagnostics.FailureAnalyzer</span><br><span class="line"></span><br><span class="line"># FailureAnalysisReporters</span><br><span class="line">org.springframework.boot.diagnostics.FailureAnalysisReporter</span><br></pre></td></tr></table></figure><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><ul><li><a href="https://blog.csdn.net/lvoyee/article/details/82017057">Spring Boot的扩展机制之Spring Factories</a></li><li><a href="https://www.jianshu.com/p/4f03b9911e0c">Spring Boot扩展机制 – Spring Factories</a></li></ul>]]>
    </content>
    <id>https://evasnowind.github.io/2020/09/16/2020-09-17_%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90%E4%B9%8BSpring_Boot%E5%A6%82%E4%BD%95%E5%88%A9%E7%94%A8Spring_Factories%E6%9C%BA%E5%88%B6%E8%BF%9B%E8%A1%8C%E8%87%AA%E5%8A%A8%E6%B3%A8%E5%85%A5/</id>
    <link href="https://evasnowind.github.io/2020/09/16/2020-09-17_%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90%E4%B9%8BSpring_Boot%E5%A6%82%E4%BD%95%E5%88%A9%E7%94%A8Spring_Factories%E6%9C%BA%E5%88%B6%E8%BF%9B%E8%A1%8C%E8%87%AA%E5%8A%A8%E6%B3%A8%E5%85%A5/"/>
    <published>2020-09-16T16:00:00.000Z</published>
    <summary>围绕Spring Boot如何利用Spring Factories机制进行自动注入的实现原理、核心流程与关键细节做源码分析。</summary>
    <title>源码分析之Spring Boot如何利用Spring Factories机制进行自动注入</title>
    <updated>2026-09-04T04:44:28.356Z</updated>
  </entry>
  <entry>
    <author>
      <name>Prayer</name>
    </author>
    <category term="中间件" scheme="https://evasnowind.github.io/categories/%E4%B8%AD%E9%97%B4%E4%BB%B6/"/>
    <category term="源码分析" scheme="https://evasnowind.github.io/tags/%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/"/>
    <category term="Kafka" scheme="https://evasnowind.github.io/tags/Kafka/"/>
    <content>
      <![CDATA[<h2 id="说明"><a href="#说明" class="headerlink" title="说明"></a>说明</h2><p>本文基于Apache Kafka 2.5.1（2020.09.10拉取最新代码）</p><h2 id="Consumer如何使用？"><a href="#Consumer如何使用？" class="headerlink" title="Consumer如何使用？"></a>Consumer如何使用？</h2><p>阅读源码前的首先要做到熟悉相关组件的概念、基本使用。而最靠谱的资料就是官方文档。</p><span id="more"></span><p>建议阅读官方文档（<a href="https://kafka.apache.org/documentation/">https://kafka.apache.org/documentation/</a>）后，自己练习、使用kafka之后再开始阅读源码。</p><p>KafkaConsumer的JavaDoc（参见<a href="https://kafka.apache.org/10/javadoc/?org/apache/kafka/clients/consumer/KafkaConsumer.html">https://kafka.apache.org/10/javadoc/?org/apache/kafka/clients/consumer/KafkaConsumer.html</a>）本身就给出了不少有用信息，下面仅列出一些关键点：</p><ul><li><p>Cross-Version Compatibility 客户端支持0.10.0以及以上版本，如果调用不支持的特性，会抛出UnsupportedVersionException</p></li><li><p>Offsets and Consumer Position position: 有待读取的下一条记录的偏移量 commited position: 已被保存、归档的最后一条记录的偏移量，可以用于恢复数据。</p></li><li><p>Consumer Groups and Topic Subscriptions</p><ul><li><p>一个partition内的消息只会投递给group中的一个consumer</p></li><li><p>以下场景将触发group rebalance</p><ul><li>一个consumer挂掉</li><li>新加入一个consumer</li><li>新的partition加入一个topic</li><li>一个新的topic匹配已有的订阅正则(subscribed regx)</li></ul></li><li><p><a href="https://kafka.apache.org/10/javadoc/org/apache/kafka/clients/consumer/ConsumerRebalanceListener.html"><code>ConsumerRebalanceListener</code></a> 可以监听rebalance</p></li></ul></li><li><p>Detecting Consumer Faillures</p><ul><li><p>调用poll(long)方法时consumer会自动加入到group中，consumer会发心跳给服务器端，超时时间<code>session.timeout.ms</code></p></li><li><p>consumer可能遇到活锁：锁检测机制 <code>session.timeout.ms</code></p></li><li><p>poll方法相关配置：</p><ul><li>max.poll.interval.ms</li><li>max.poll.records</li></ul></li></ul></li></ul><h3 id="代码示例"><a href="#代码示例" class="headerlink" title="代码示例"></a>代码示例</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">//代码1：</span><br><span class="line">Properties props = new Properties();</span><br><span class="line">props.put(&quot;bootstrap.servers&quot;, &quot;localhost:9092&quot;);</span><br><span class="line">props.put(&quot;group.id&quot;, &quot;test&quot;);</span><br><span class="line">props.put(&quot;enable.auto.commit&quot;, &quot;true&quot;);</span><br><span class="line">props.put(&quot;auto.commit.interval.ms&quot;, &quot;1000&quot;);</span><br><span class="line">props.put(&quot;key.deserializer&quot;, &quot;org.apache.kafka.common.serialization.StringDeserializer&quot;);</span><br><span class="line">props.put(&quot;value.deserializer&quot;, &quot;org.apache.kafka.common.serialization.StringDeserializer&quot;);</span><br><span class="line">KafkaConsumer&lt;String, String&gt; consumer = new KafkaConsumer&lt;&gt;(props);</span><br><span class="line">consumer.subscribe(Arrays.asList(&quot;foo&quot;, &quot;bar&quot;));</span><br><span class="line">while (true) &#123;</span><br><span class="line">    ConsumerRecords&lt;String, String&gt; records = consumer.poll(100);</span><br><span class="line">    for (ConsumerRecord&lt;String, String&gt; record : records)</span><br><span class="line">        System.out.printf(&quot;offset = %d, key = %s, value = %s%n&quot;, record.offset(), record.key(), record.value());</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="主要流程"><a href="#主要流程" class="headerlink" title="主要流程"></a>主要流程</h4><ol><li>设置配置信息，包括broker地址，consumer group id, 自动提交消费的位置，序列化配置</li><li>创建KafkaConsumer对象</li><li>订阅2个topic: foo, bar</li><li>循环拉取并打印</li></ol><p>以下将重点解读KafkaConsumer消费流程中的3个问题：</p><ol><li><strong>订阅主题的过程是如何实现的？</strong></li><li><strong>consumer如何与coordinator协商，确定消费哪些partition?</strong></li><li><strong>拉取消息的过程是如何实现的？</strong></li></ol><h2 id="订阅主题的过程是如何实现的？"><a href="#订阅主题的过程是如何实现的？" class="headerlink" title="订阅主题的过程是如何实现的？"></a>订阅主题的过程是如何实现的？</h2><p>仍以上面代码示例为例，跟踪源码中的subscribe方法，最终会看到KafkaConsumer中的如下代码：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line">//代码2：</span><br><span class="line">@Override</span><br><span class="line">public void subscribe(Collection&lt;String&gt; topics, ConsumerRebalanceListener listener) &#123;</span><br><span class="line">    acquireAndEnsureOpen();</span><br><span class="line">    try &#123;</span><br><span class="line">        maybeThrowInvalidGroupIdException();</span><br><span class="line">        if (topics == null)</span><br><span class="line">            throw new IllegalArgumentException(&quot;Topic collection to subscribe to cannot be null&quot;);</span><br><span class="line">        if (topics.isEmpty()) &#123;</span><br><span class="line">            // treat subscribing to empty topic list as the same as unsubscribing</span><br><span class="line">            this.unsubscribe();</span><br><span class="line">        &#125; else &#123;</span><br><span class="line">            for (String topic : topics) &#123;</span><br><span class="line">                if (topic == null || topic.trim().isEmpty())</span><br><span class="line">                    throw new IllegalArgumentException(&quot;Topic collection to subscribe to cannot contain null or empty topic&quot;);</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            throwIfNoAssignorsConfigured();</span><br><span class="line">            fetcher.clearBufferedDataForUnassignedTopics(topics);</span><br><span class="line">            log.info(&quot;Subscribed to topic(s): &#123;&#125;&quot;, Utils.join(topics, &quot;, &quot;));</span><br><span class="line">            if (this.subscriptions.subscribe(new HashSet&lt;&gt;(topics), listener))</span><br><span class="line">                metadata.requestUpdateForNewTopics();</span><br><span class="line">        &#125;</span><br><span class="line">    &#125; finally &#123;</span><br><span class="line">        release();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>基本流程如下：</p><ol><li><p>加轻量级锁（通过CAS方式加锁）</p></li><li><p>参数校验</p><ol><li>注意：如果传入topic集合为空，则直接走unsubscribe的逻辑</li></ol></li><li><p>重置订阅状态subscriptions，更新元数据metadata中的topic信息</p><ol><li>订阅状态维护：订阅的 topic 和 patition 的消费位置等状态信息</li><li>元数据metada维护：Kafka 集群元数据的一个子集，包括集群的 Broker 节点、Topic 和 Partition 在节点上分布，Coordinator 给 Consumer 分配的 Partition 信息。</li></ol></li></ol><h3 id="经典思路：主动检测不支持的情况并抛出异常，避免系统产生不可预期的行为"><a href="#经典思路：主动检测不支持的情况并抛出异常，避免系统产生不可预期的行为" class="headerlink" title="经典思路：主动检测不支持的情况并抛出异常，避免系统产生不可预期的行为"></a>经典思路：主动检测不支持的情况并抛出异常，避免系统产生不可预期的行为</h3><p>KafkaConsumer的<a href="https://kafka.apache.org/10/javadoc/?org/apache/kafka/clients/consumer/KafkaConsumer.html">Javadoc</a>明确声明了，consumer不是线程安全的，被并发调用时会出现不可预期的结果。为了避免这种情况发生，Kafka 做了主动的检测并抛出异常，而不是放任系统产生不可预期的情况。</p><blockquote><p>Kafka“主动检测不支持的情况并抛出异常，避免系统产生不可预期的行为”这种模式，对于增强的系统的健壮性是一种非常有效的做法。如果你的系统不支持用户的某种操作，正确的做法是，检测不支持的操作，直接拒绝用户操作，并给出明确的错误提示，而不应该只是在文档中写上“不要这样做”，却放任用户错误的操作，产生一些不可预期的、奇怪的错误结果。</p><p>引自：极客时间：消息队列高手课</p></blockquote><p>具体代码就是上面的代码2中的<code>acquireAndEnsureOpen()</code>，具体实现如下：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br></pre></td><td class="code"><pre><span class="line">/**</span><br><span class="line">     * Acquire the light lock and ensure that the consumer hasn&#x27;t been closed.</span><br><span class="line">     * @throws IllegalStateException If the consumer has been closed</span><br><span class="line">     */</span><br><span class="line">private void acquireAndEnsureOpen() &#123;</span><br><span class="line">    acquire();</span><br><span class="line">    //KafkaConsumer成员变量，初始值为false，调用close(Duration)方法后才会置为true</span><br><span class="line">    if (this.closed) &#123;</span><br><span class="line">        release();</span><br><span class="line">        throw new IllegalStateException(&quot;This consumer has already been closed.&quot;);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">//变量声明</span><br><span class="line">private static final long NO_CURRENT_THREAD = -1L;</span><br><span class="line"></span><br><span class="line">private void acquire() &#123;</span><br><span class="line">    //拿到当前线程的线程id</span><br><span class="line">    long threadId = Thread.currentThread().getId();</span><br><span class="line">    /*if threadId与当前正执行的线程的id不一致（并发，多线程访问）&amp;&amp; threadId对应的线程没有争抢到锁</span><br><span class="line">         then 抛出异常</span><br><span class="line">    举例：</span><br><span class="line">        现在有两个KafkaConsumer线程，线程id分别是thread1, thread2，要执行acquire()方法。</span><br><span class="line">        thread1先启动，执行完上面这条语句、赋值threadId后， thread1栈帧中threadId=thread1，此时CPU线程调度、执行thread2，</span><br><span class="line">        thread2也走到if语句时，在thread2的栈帧中，threadId已经赋值为thread2，走到这里，currentThread作为成员变量，初始值为NO_CURRENT_THREAD（-1），因此必然不相等，继续走第二判断条件，即利用AtomicInteger的CAS操作，将当前线程id threadId(thread2)赋值给currentThread这个AtomicInteger，必然返回true，因此会继续执行，使得refcount加1；</span><br><span class="line">        接着，此时执行thread1，那么再继续执行if，threadId(thread1) != currentThread.get() (thread2)能满足，但是currentThread的CAS赋值将会失败，因此此时currentThread的值并不是NO_CURRENT_THREAD。</span><br><span class="line">        </span><br><span class="line">        refcount用于记录重入锁的情况，参见release()方法，当refcount=0时，currentThread将重新赋值为NO_CURRENT_THREAD，保证彻底解锁。</span><br><span class="line">    */</span><br><span class="line">    if (threadId != currentThread.get() &amp;&amp; !currentThread.compareAndSet(NO_CURRENT_THREAD, threadId))</span><br><span class="line">        throw new ConcurrentModificationException(&quot;KafkaConsumer is not safe for multi-threaded access&quot;);</span><br><span class="line">    refcount.incrementAndGet();</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">/**</span><br><span class="line">     * Release the light lock protecting the consumer from multi-threaded access.</span><br><span class="line">     */</span><br><span class="line">private void release() &#123;</span><br><span class="line">    if (refcount.decrementAndGet() == 0)</span><br><span class="line">        currentThread.set(NO_CURRENT_THREAD);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>此处通过AtomicLong currentThread, AtomicInteger refcount 这两个变量来实现轻量级锁的机制非常经典，建议学习、理解、运用。具体分析我已经写在了上述源码中供大家参考。</p><h3 id="有关元数据更新"><a href="#有关元数据更新" class="headerlink" title="有关元数据更新"></a>有关元数据更新</h3><p>元数据更新调用的是<code>metadata.requestUpdateForNewTopics();</code>，里面内容是：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">public synchronized int requestUpdateForNewTopics() &#123;</span><br><span class="line">    // Override the timestamp of last refresh to let immediate update.</span><br><span class="line">    this.lastRefreshMs = 0;</span><br><span class="line">    this.needPartialUpdate = true;</span><br><span class="line">    this.requestVersion++;</span><br><span class="line">    return this.updateVersion;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>可以看到，内部并没有更新元数据的操作，只是设置了几个标志位。但我们知道，在消息消息前，我们必须获取元数据，比如broker节点信息、topic和partition的分布，否则根本不知道从哪里获取数据。那么此时，我们就可以根据这几个标志位来去找相应代码。不过从KafkaConsumer的JavaDoc提供的信息也能获知，是在拉取消息时将会根据这些标志位来更新元数据。具体逻辑请看接下来的分析。</p><h2 id="拉取消息的过程是如何实现的？"><a href="#拉取消息的过程是如何实现的？" class="headerlink" title="拉取消息的过程是如何实现的？"></a>拉取消息的过程是如何实现的？</h2><p>我们已经知道，拉取消息是调用KafkaConsumer的poll(Duration)方法，主要流程的序列图参考下图</p><p><img src="/images/kafka-consumer-poll-process-sequence-dia_c8d2a1e0.png"></p><p>注：引自 极客时间 消息队列高手课</p><p>poll源码如下，主要流程在：</p><ol><li><strong>updateAssignmentMetadataIfNeeded(): 更新元数据</strong></li><li><strong>pollForFetches()：拉取消息</strong></li></ol><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br></pre></td><td class="code"><pre><span class="line">@Override</span><br><span class="line">public ConsumerRecords&lt;K, V&gt; poll(final Duration timeout) &#123;</span><br><span class="line">    return poll(time.timer(timeout), true);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">/**</span><br><span class="line">     * @throws KafkaException if the rebalance callback throws exception</span><br><span class="line">     */</span><br><span class="line">private ConsumerRecords&lt;K, V&gt; poll(final Timer timer, final boolean includeMetadataInTimeout) &#123;</span><br><span class="line">    //加锁，保证只有1个线程读取数据</span><br><span class="line">    acquireAndEnsureOpen();</span><br><span class="line">    try &#123;</span><br><span class="line">        //记录开始时间</span><br><span class="line">        this.kafkaConsumerMetrics.recordPollStart(timer.currentTimeMs());</span><br><span class="line"></span><br><span class="line">        if (this.subscriptions.hasNoSubscriptionOrUserAssignment()) &#123;</span><br><span class="line">            //没有订阅信息，抛异常</span><br><span class="line">            throw new IllegalStateException(&quot;Consumer is not subscribed to any topics or assigned any partitions&quot;);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        do &#123;</span><br><span class="line">            //这个地方没太看懂</span><br><span class="line">            client.maybeTriggerWakeup();</span><br><span class="line"></span><br><span class="line">            if (includeMetadataInTimeout) &#123;</span><br><span class="line">                // try to update assignment metadata BUT do not need to block on the timer for join group</span><br><span class="line">                updateAssignmentMetadataIfNeeded(timer, false);</span><br><span class="line">            &#125; else &#123;</span><br><span class="line">                /*</span><br><span class="line">                updateAssignmentMetadataIfNeeded方法有3个作用：</span><br><span class="line">                    - discovery coordinator if necessary</span><br><span class="line">                    - join group if necessary</span><br><span class="line">                    - refresh metadata and fetch position if necessary</span><br><span class="line">                */</span><br><span class="line">                while (!updateAssignmentMetadataIfNeeded(time.timer(Long.MAX_VALUE), true)) &#123;</span><br><span class="line">                    log.warn(&quot;Still waiting for metadata&quot;);</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            final Map&lt;TopicPartition, List&lt;ConsumerRecord&lt;K, V&gt;&gt;&gt; records = pollForFetches(timer);</span><br><span class="line">            if (!records.isEmpty()) &#123;</span><br><span class="line">                // before returning the fetched records, we can send off the next round of fetches</span><br><span class="line">                // and avoid block waiting for their responses to enable pipelining while the user</span><br><span class="line">                // is handling the fetched records.</span><br><span class="line">                //</span><br><span class="line">                // NOTE: since the consumed position has already been updated, we must not allow</span><br><span class="line">                // wakeups or any other errors to be triggered prior to returning the fetched records.</span><br><span class="line">                if (fetcher.sendFetches() &gt; 0 || client.hasPendingRequests()) &#123;</span><br><span class="line">                    client.transmitSends();</span><br><span class="line">                &#125;</span><br><span class="line"></span><br><span class="line">                return this.interceptors.onConsume(new ConsumerRecords&lt;&gt;(records));</span><br><span class="line">            &#125;</span><br><span class="line">        &#125; while (timer.notExpired());</span><br><span class="line"></span><br><span class="line">        return ConsumerRecords.empty();</span><br><span class="line">    &#125; finally &#123;</span><br><span class="line">        release();</span><br><span class="line">        this.kafkaConsumerMetrics.recordPollEnd(timer.currentTimeMs());</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">boolean updateAssignmentMetadataIfNeeded(final Timer timer, final boolean waitForJoinGroup) &#123;</span><br><span class="line">    if (coordinator != null &amp;&amp; !coordinator.poll(timer, waitForJoinGroup)) &#123;</span><br><span class="line">        return false;</span><br><span class="line">    &#125;</span><br><span class="line">    /*</span><br><span class="line">    Set the fetch position to the committed position (if there is one) or reset it using the offset reset policy the user has configured.</span><br><span class="line">    更新position或是根据用户配置重置position</span><br><span class="line">    */</span><br><span class="line">    return updateFetchPositions(timer);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure><h3 id="updateAssignmentMetadataIfNeeded-更新元数据"><a href="#updateAssignmentMetadataIfNeeded-更新元数据" class="headerlink" title="updateAssignmentMetadataIfNeeded() 更新元数据"></a>updateAssignmentMetadataIfNeeded() 更新元数据</h3><p>updateAssignmentMetadataIfNeeded方法有3个作用（此处代码最近有更新，可以看看github中代码更新记录）：</p><ul><li>discovery coordinator if necessary</li><li>join group if necessary</li><li>refresh metadata and fetch position if necessary</li></ul><h4 id="Coordinator-poll-维持心跳，更新元数据"><a href="#Coordinator-poll-维持心跳，更新元数据" class="headerlink" title="Coordinator#poll() 维持心跳，更新元数据"></a>Coordinator#poll() 维持心跳，更新元数据</h4><p>updateAssignmentMetadataIfNeeded调用了coordinator.poll() 方法，即ConsumerCoordinator#poll(Timer timer, boolean waitForJoinGroup)，源码如下</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br></pre></td><td class="code"><pre><span class="line">public boolean poll(Timer timer, boolean waitForJoinGroup) &#123;</span><br><span class="line">    maybeUpdateSubscriptionMetadata();</span><br><span class="line"></span><br><span class="line">    invokeCompletedOffsetCommitCallbacks();</span><br><span class="line">    //先忽略细节逻辑，第一次启动时，将执行此处</span><br><span class="line">    if (subscriptions.hasAutoAssignedPartitions()) &#123;</span><br><span class="line">        if (protocol == null) &#123;</span><br><span class="line">            throw new IllegalStateException(&quot;User configured &quot; + ConsumerConfig.PARTITION_ASSIGNMENT_STRATEGY_CONFIG +</span><br><span class="line">                                            &quot; to empty while trying to subscribe for group protocol to auto assign partitions&quot;);</span><br><span class="line">        &#125;</span><br><span class="line">        /*</span><br><span class="line">        此处是发送心跳逻辑</span><br><span class="line">        */</span><br><span class="line">        pollHeartbeat(timer.currentTimeMs());</span><br><span class="line">        </span><br><span class="line">        if (coordinatorUnknown() &amp;&amp; !ensureCoordinatorReady(timer)) &#123;</span><br><span class="line">            return false;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        if (rejoinNeededOrPending()) &#123;</span><br><span class="line">            if (subscriptions.hasPatternSubscription()) &#123;</span><br><span class="line">                </span><br><span class="line">                if (this.metadata.timeToAllowUpdate(timer.currentTimeMs()) == 0) &#123;</span><br><span class="line">                    this.metadata.requestUpdate();</span><br><span class="line">                &#125;</span><br><span class="line"></span><br><span class="line">                /*</span><br><span class="line">                在此处会根据当前needFullUpdate needPartialUpdate等状态，</span><br><span class="line">                执行元数据更新逻辑，具体的逻辑则是由ConsumerNetworkClient#poll方法执行。</span><br><span class="line">                */</span><br><span class="line">                if (!client.ensureFreshMetadata(timer)) &#123;</span><br><span class="line">                    return false;</span><br><span class="line">                &#125;</span><br><span class="line"></span><br><span class="line">                maybeUpdateSubscriptionMetadata();</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            if (!ensureActiveGroup(waitForJoinGroup ? timer : time.timer(0L))) &#123;</span><br><span class="line">                return false;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125; else &#123;</span><br><span class="line">        if (metadata.updateRequested() &amp;&amp; !client.hasReadyNodes(timer.currentTimeMs())) &#123;</span><br><span class="line">            client.awaitMetadataUpdate(timer);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    maybeAutoCommitOffsetsAsync(timer.currentTimeMs());</span><br><span class="line">    return true;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="ConsumerNetworkClient-poll-封装所有网络通信"><a href="#ConsumerNetworkClient-poll-封装所有网络通信" class="headerlink" title="ConsumerNetworkClient#poll() 封装所有网络通信"></a>ConsumerNetworkClient#poll() 封装所有网络通信</h4><p>ConsumerNetworkClient封装了 consumer和 kafka cluster 之间所有的网络通信的实现，完全异步实现、没有自己维护线程。其中：</p><ul><li><p>所有待发送请求Request都会被存放到ConsumerNetworkClient的成员变量<code>UnsentRequests unsent</code>中。</p><ul><li>注：UnsentRequests 是用<code>ConcurrentMap&lt;Node, ConcurrentLinkedQueue&lt;ClientRequest&gt;&gt; unsent</code>定义一个Map来保存所有待发送请求，作为关键字的Node中包含了kafka节点的id、host、port、rack等信息。</li><li>返回的Response将存放在ConsumerNetworkClient的成员变量pendingCompletion 中。</li></ul></li></ul><p>每次调用ConsumerNetworkClient# poll() 方法的时候，在当前线程（而不是ConsumerNetworkClient自己维护线程）中发送所有待发送的 Request，处理所有收到的 Response。</p><p>这种异步设计的优劣：</p><ul><li>优点：占用线程少，吞吐量高</li><li>不足：增加代码复杂度</li></ul><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br></pre></td><td class="code"><pre><span class="line">public void poll(Timer timer, PollCondition pollCondition, boolean disableWakeup) &#123;</span><br><span class="line">        // 调用handler</span><br><span class="line">        firePendingCompletedRequests();</span><br><span class="line"></span><br><span class="line">        lock.lock();</span><br><span class="line">        try &#123;</span><br><span class="line">            // Handle async disconnects prior to attempting any sends</span><br><span class="line">            handlePendingDisconnects();</span><br><span class="line"></span><br><span class="line">            /*</span><br><span class="line">            遍历unsent中已保存的所有请求，并调用KafkaClient#send，发送请求</span><br><span class="line">            */</span><br><span class="line">            long pollDelayMs = trySend(timer.currentTimeMs());</span><br><span class="line"></span><br><span class="line">            if (pendingCompletion.isEmpty() &amp;&amp; (pollCondition == null || pollCondition.shouldBlock())) &#123;</span><br><span class="line">                // if there are no requests in flight, do not block longer than the retry backoff</span><br><span class="line">                long pollTimeout = Math.min(timer.remainingMs(), pollDelayMs);</span><br><span class="line">                /*</span><br><span class="line">                KafkaClient中维护着一个在途请求映射表，参见InFlightRequests类。NetworkClient#send方法</span><br><span class="line">                在发送网络请求时，将会在在途请求映射表加入一条记录。注意是为了避免请求超时导致请求一直等待、</span><br><span class="line">                占用内存、进而导致内存不足</span><br><span class="line">                */</span><br><span class="line">                if (client.inFlightRequestCount() == 0)</span><br><span class="line">                    pollTimeout = Math.min(pollTimeout, retryBackoffMs);</span><br><span class="line">                client.poll(pollTimeout, timer.currentTimeMs());</span><br><span class="line">            &#125; else &#123;</span><br><span class="line">                client.poll(0, timer.currentTimeMs());</span><br><span class="line">            &#125;</span><br><span class="line">            timer.update();</span><br><span class="line"></span><br><span class="line">            checkDisconnects(timer.currentTimeMs());</span><br><span class="line">            if (!disableWakeup) &#123;</span><br><span class="line">                maybeTriggerWakeup();</span><br><span class="line">            &#125;</span><br><span class="line">            </span><br><span class="line">            maybeThrowInterruptException();</span><br><span class="line"></span><br><span class="line">            trySend(timer.currentTimeMs());</span><br><span class="line"></span><br><span class="line">            failExpiredRequests(timer.currentTimeMs());</span><br><span class="line">            //清理unsent</span><br><span class="line">            unsent.clean();</span><br><span class="line">        &#125; finally &#123;</span><br><span class="line">            lock.unlock();</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        // called without the lock to avoid deadlock potential if handlers need to acquire locks</span><br><span class="line">        firePendingCompletedRequests();</span><br><span class="line"></span><br><span class="line">        metadata.maybeThrowAnyException();</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure><h4 id="KafkaConsumer-updateFetchPositions-更新消费位置"><a href="#KafkaConsumer-updateFetchPositions-更新消费位置" class="headerlink" title="KafkaConsumer#updateFetchPositions() 更新消费位置"></a>KafkaConsumer#updateFetchPositions() 更新消费位置</h4><p>Kafka Consumer 在消费过程中是需要维护消费位置的，Consumer 每次从当前消费位置拉取一批消息，这些消息都被正常消费后，Consumer 会给 Coordinator 发一个提交位置的请求，然后消费位置会向后移动，完成一批消费过程。</p><p>而consumer发起提交位置的请求，还是在<code>updateAssignmentMetadataIfNeeded()</code>方法中，毕竟位置信息也是服务端broker的一个元数据。在<code>`updateAssignmentMetadataIfNeeded</code>方法实现中，最后一句调用<code>KafkaConsumer#updateFetchPositions(timer);</code>，而这个方法又会调用coordinator.refreshCommittedOffsetsIfNeeded()方法。调用链路如下：</p><p><img src="/images/kafka-consumer-refresh-position-sequence_2db1d1e5.png"></p><p>注：同一个类内部的方法调用，用类名头部加了’’’区分了下，主要是为了在Typora里画图方便。不支持可参考images&#x2F;kafka-consumer-refresh-position-sequence-diagram.png 图片。</p><h3 id="pollForFetchs-拉取消息"><a href="#pollForFetchs-拉取消息" class="headerlink" title="pollForFetchs() 拉取消息"></a>pollForFetchs() 拉取消息</h3><p>源码如下</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line">private Map&lt;TopicPartition, List&lt;ConsumerRecord&lt;K, V&gt;&gt;&gt; pollForFetches(Timer timer) &#123;</span><br><span class="line">    // 省略部分代码        </span><br><span class="line">    // 如果缓存里面有未读取的消息，直接返回这些消息</span><br><span class="line">    final Map&lt;TopicPartition, List&lt;ConsumerRecord&lt;K, V&gt;&gt;&gt; records = fetcher.fetchedRecords();</span><br><span class="line">    if (!records.isEmpty()) &#123;</span><br><span class="line">        return records;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    /* </span><br><span class="line">    构造拉取消息请求，并发送。</span><br><span class="line">    注意，该方法内部，将调用ConsumerNetworkClient#send方法，而这个send方法将会把 </span><br><span class="line">    kafka节点信息 --&gt; 请求  存放到ConsumerNetworkClient#unsent成员变量中</span><br><span class="line">    */</span><br><span class="line">    fetcher.sendFetches();</span><br><span class="line"></span><br><span class="line">    // 省略部分代码</span><br><span class="line">    // 发送网络请求拉取消息，等待直到有消息返回或者超时</span><br><span class="line">    client.poll(pollTimer, () -&gt; &#123;</span><br><span class="line">        // since a fetch might be completed by the background thread, we need this poll condition</span><br><span class="line">        // to ensure that we do not block unnecessarily in poll()</span><br><span class="line">        return !fetcher.hasAvailableFetches();</span><br><span class="line">    &#125;);</span><br><span class="line">    //省略部分代码</span><br><span class="line">    //返回拉取到的消息</span><br><span class="line">    return fetcher.fetchedRecords();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>主要逻辑如下：</p><ol><li><p>如果缓存里面有未读取的消息，直接返回这些消息；</p></li><li><p>构造拉取消息请求，并发送；</p></li><li><p>发送网络请求并拉取消息，等待直到有消息返回或者超时；</p></li><li><p>返回拉到的消息。</p><ol><li>fetcher.fetchedRecords会将返回的Response反序列化，返回给调用者。</li></ol></li></ol><p>具体发送操作由fetcher.sendFetches();实现，主要包括如下步骤：</p><ol><li><p>构造拉取消息请求Request对象</p></li><li><p>调用ConsumerNetworkClient#send方法<strong>异步发送Request</strong></p><ol><li>这个send方法将会把kafka节点信息 –&gt; 请求 存放到ConsumerNetworkClient#unsent成员变量中</li></ol></li><li><p>注册一个回调类来处理返回的Response</p><ol><li>Response暂时被存放在Fetcher#completedFetches成员变量中</li></ol></li></ol><p>当调用ConsumerNetworkClient#poll方法时，才会遍历unsent，将其中的请求发出去，并处理收到的Response。</p><h2 id="KafkaConsumer相关类图"><a href="#KafkaConsumer相关类图" class="headerlink" title="KafkaConsumer相关类图"></a>KafkaConsumer相关类图</h2><p><img src="/images/kafka-consumer-class-diagram-300x108_8c2de7cc.png"></p><p>上图引自：极客时间 消息队列高手课</p>]]>
    </content>
    <id>https://evasnowind.github.io/2020/09/10/2020-09-11_%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90%E4%B9%8BKafka_Consumer%E6%B6%88%E8%B4%B9%E6%B6%88%E6%81%AF%E7%9A%84%E8%BF%87%E7%A8%8B/</id>
    <link href="https://evasnowind.github.io/2020/09/10/2020-09-11_%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90%E4%B9%8BKafka_Consumer%E6%B6%88%E8%B4%B9%E6%B6%88%E6%81%AF%E7%9A%84%E8%BF%87%E7%A8%8B/"/>
    <published>2020-09-10T16:00:00.000Z</published>
    <summary>围绕Kafka Consumer消费消息的过程的实现原理、核心流程与关键细节做源码分析。</summary>
    <title>源码分析之Kafka Consumer消费消息的过程</title>
    <updated>2026-09-04T04:44:28.355Z</updated>
  </entry>
  <entry>
    <author>
      <name>Prayer</name>
    </author>
    <category term="中间件" scheme="https://evasnowind.github.io/categories/%E4%B8%AD%E9%97%B4%E4%BB%B6/"/>
    <category term="源码分析" scheme="https://evasnowind.github.io/tags/%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/"/>
    <category term="Netty" scheme="https://evasnowind.github.io/tags/Netty/"/>
    <content>
      <![CDATA[<h2 id="不得不说的Reactor模式"><a href="#不得不说的Reactor模式" class="headerlink" title="不得不说的Reactor模式"></a>不得不说的Reactor模式</h2><p>提到Netty，就必须先说一下Reactor模式，源头应该是Doug Lea大神（学java的如果不知道这位神的请自己反思一下……）的<code>Scalable IO in Java</code>所提出的<code>Multiple Reactors</code>模式，参见下图</p><span id="more"></span><p><img src="/images/multiple-reactors-pattern-300x234_37432a92.jpg"></p><p>如果想知道为何出现Reactor模式，需要将IO发展过程，都说一下可能才会比较清晰，此处就不一一展开，有兴趣的童鞋可以参考这篇帖子：<a href="https://www.cnblogs.com/crazymakercircle/p/9833847.html">Reactor模式</a></p><p>那么，<strong>什么是Reactor模式呢？</strong></p><blockquote><p>Reactor模式是事件驱动模型，有一个或多个并发输入源，有一个Service Handler，有多个Request Handlers；这个Service Handler会同步的将输入的请求（Event）多路复用的分发给相应的Request Handler。从结构上，这有点类似生产者消费者模式，即有一个或多个生产者将事件放入一个Queue中，而一个或多个消费者主动的从这个Queue中Poll事件来处理；而Reactor模式则并没有Queue来做缓冲，每当一个Event输入到Service Handler之后，该Service Handler会主动的根据不同的Event类型将其分发给对应的Request Handler来处理。</p><p><img src="/images/reactor-class-diagram-300x194_f846bdac.jpg"></p><p>该图为Reactor模型类图，实现Reactor模式需要实现以下几个类：</p><p>1）EventHandler：事件处理器，可以根据事件的不同状态创建处理不同状态的处理器；</p><p>2）Handle：可以理解为事件，在网络编程中就是一个Socket，在数据库操作中就是一个DBConnection；</p><p>3）InitiationDispatcher：用于管理EventHandler，分发event的容器，也是一个事件处理调度器，Tomcat的Dispatcher就是一个很好的实现，用于接收到网络请求后进行第一步的任务分发，分发给相应的处理器去异步处理，来保证吞吐量；</p><p>4）Demultiplexer：阻塞等待一系列的Handle中的事件到来，如果阻塞等待返回，即表示在返回的Handle中可以不阻塞的执行返回的事件类型。这个模块一般使用操作系统的select来实现。在Java NIO中用Selector来封装，当Selector.select()返回时，可以调用Selector的selectedKeys()方法获取Set<SelectionKey>，一个SelectionKey表达一个有事件发生的Channel以及该Channel上的事件类型。</p><p>Reactor时序图 如下：</p><p><img src="/images/reactor-sequence-diagram-300x174_505192ef.jpg"></p><p>1）初始化InitiationDispatcher，并初始化一个Handle到EventHandler的Map。</p><p>2）注册EventHandler到InitiationDispatcher中，每个EventHandler包含对相应Handle的引用，从而建立Handle到EventHandler的映射（Map）。</p><p>3）调用InitiationDispatcher的handle_events()方法以启动Event Loop。在Event Loop中，调用select()方法（Synchronous Event Demultiplexer）阻塞等待Event发生。</p><p>4）当某个或某些Handle的Event发生后，select()方法返回，InitiationDispatcher根据返回的Handle找到注册的EventHandler，并回调该EventHandler的handle_events()方法。</p><p>5）在EventHandler的handle_events()方法中还可以向InitiationDispatcher中注册新的Eventhandler，比如对AcceptorEventHandler来，当有新的client连接时，它会产生新的EventHandler以处理新的连接，并注册到InitiationDispatcher中。</p><p>引自：<a href="https://www.jianshu.com/p/188ef8462100">https://www.jianshu.com/p/188ef8462100</a></p></blockquote><h2 id="Reactor的程序实现"><a href="#Reactor的程序实现" class="headerlink" title="Reactor的程序实现"></a>Reactor的程序实现</h2><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line">void Reactor::handle_events()&#123;</span><br><span class="line">  //通过同步事件多路选择器提供的</span><br><span class="line">  //select()方法监听网络事件</span><br><span class="line">  select(handlers);</span><br><span class="line">  //处理网络事件</span><br><span class="line">  for(h in handlers)&#123;</span><br><span class="line">    h.handle_event();</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line">// 在主程序中启动事件循环</span><br><span class="line">while (true) &#123;</span><br><span class="line">  handle_events();</span><br></pre></td></tr></table></figure><h2 id="Netty中的线程模型"><a href="#Netty中的线程模型" class="headerlink" title="Netty中的线程模型"></a>Netty中的线程模型</h2><p>为了实现高性能的IO，Netty的线程模型参考了Reactor模式，但没有完全照搬。</p><p>Netty 中最核心的概念是事件循环（EventLoop），其实也就是 Reactor 模式中的 Reactor，负责监听网络事件并调用事件处理器进行处理。程序实现上，EventLoop其实就是指上一小节中的整个while(true)循环</p><p>在4.x版本Netty中：</p><ul><li><p>网络连接 –&gt; EventLoop ：稳定的 n:1关系，这里的稳定指的是关系一旦确定就不再发生变化。</p></li><li><p>EventLoop –&gt; Java线程 ： 1:1关系</p></li><li><p>所以<strong>一个网络连接只会对应一个Java线程</strong></p><ul><li>最大的好处就是对于一个网络连接的事件处理是单线程的，这样就避免了各种并发问题。</li></ul></li></ul><p>顺便提一句，<strong>Java线程与linux操作系统的线程是1:1关系</strong>，这个可以参考经典的 The C10K Problem那篇文章。</p><p>Netty线程模型可以参考这张图</p><p><img src="/images/netty-io-thread-model-300x149_741ed623.png"></p><p>引自：极客时间-《Java并发编程实战》</p><p>如图，Netty中，EventLoopGroup由一组 EventLoop 组成。实际使用中，一般都会创建两个 EventLoopGroup，一个称为 bossGroup，一个称为 workerGroup。</p><p>之所以会有2个EventLoopGroup，原因在于socket处理网络请求的机制。socket 处理 TCP 网络连接请求，是在一个独立的 socket 中，每当有一个 TCP 连接成功建立，都会创建一个新的 socket，之后对 TCP 连接的读写都是由新创建处理的 socket 完成的。也就是说<strong>处理 TCP 连接请求和读写请求是通过两个不同的 socket 完成的。</strong></p><p>在 Netty 中，bossGroup 就用来处理连接请求的，而 workerGroup 是用来处理读写请求的。bossGroup 处理完连接请求后，会将这个连接提交给 workerGroup 来处理， workerGroup 里面有多个 EventLoop，新的连接会交给哪个 EventLoop 来处理由负载均衡算法决定，目前Netty使用的是<strong>轮询算法</strong>。</p><h3 id="题外话"><a href="#题外话" class="headerlink" title="题外话"></a>题外话</h3><p>Netty 3.x与Netty 4.x 区别较大，有兴趣可以参考这篇文章：<a href="https://cloud.tencent.com/developer/article/1120234">netty3与netty4的区别</a></p><h2 id="使用Netty实现Echo服务"><a href="#使用Netty实现Echo服务" class="headerlink" title="使用Netty实现Echo服务"></a>使用Netty实现Echo服务</h2><p>下面给出一个具体例子，来看看实际编码是怎么用Netty的。</p><p>学习编程语言时，我们往往会先写个HelloWorld，而在网络通信里，这个“HelloWorld”程序就是Echo服务，也就是A发送任意一个消息给B，B将原封不动的把这条消息返回给A。</p><p>学习如何使用Netty，最典型、最权威的当属Netty自己提供的范例，我们可以在Netty源码中找到这些例子，参见<a href="https://github.com/netty/netty">https://github.com/netty/netty</a>项目中的example文件夹。</p><p>注：一般比较完备的开源项目，都会在代码中给出使用范例，至少有单元测试供大家学习参考。</p><h3 id="关键点"><a href="#关键点" class="headerlink" title="关键点"></a>关键点</h3><ul><li>如果 NettybossGroup 只监听一个端口，那 bossGroup 只需要 1 个 EventLoop 就可以了，多了纯属浪费。</li><li>默认情况下，Netty 会创建“2*CPU 核数”个 EventLoop，由于网络连接与 EventLoop 有稳定的关系，所以事件处理器在处理网络事件的时候是不能有阻塞操作的，否则很容易导致请求大面积超时。如果实在无法避免使用阻塞操作，那可以通过线程池来异步处理。</li></ul><h3 id="Server端"><a href="#Server端" class="headerlink" title="Server端"></a>Server端</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br></pre></td><td class="code"><pre><span class="line">public final class EchoServer &#123;</span><br><span class="line"></span><br><span class="line">    static final boolean SSL = System.getProperty(&quot;ssl&quot;) != null;</span><br><span class="line">    static final int PORT = Integer.parseInt(System.getProperty(&quot;port&quot;, &quot;8007&quot;));</span><br><span class="line"></span><br><span class="line">    public static void main(String[] args) throws Exception &#123;</span><br><span class="line">        // Configure SSL.</span><br><span class="line">        final SslContext sslCtx;</span><br><span class="line">        if (SSL) &#123;</span><br><span class="line">            SelfSignedCertificate ssc = new SelfSignedCertificate();</span><br><span class="line">            sslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()).build();</span><br><span class="line">        &#125; else &#123;</span><br><span class="line">            sslCtx = null;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        // Configure the server.</span><br><span class="line">        EventLoopGroup bossGroup = new NioEventLoopGroup(1);</span><br><span class="line">        EventLoopGroup workerGroup = new NioEventLoopGroup();</span><br><span class="line">        final EchoServerHandler serverHandler = new EchoServerHandler();</span><br><span class="line">        try &#123;</span><br><span class="line">            ServerBootstrap b = new ServerBootstrap();</span><br><span class="line">            b.group(bossGroup, workerGroup)</span><br><span class="line">             .channel(NioServerSocketChannel.class)</span><br><span class="line">             .option(ChannelOption.SO_BACKLOG, 100)</span><br><span class="line">             .handler(new LoggingHandler(LogLevel.INFO))</span><br><span class="line">             .childHandler(new ChannelInitializer&lt;SocketChannel&gt;() &#123;</span><br><span class="line">                 @Override</span><br><span class="line">                 public void initChannel(SocketChannel ch) throws Exception &#123;</span><br><span class="line">                     ChannelPipeline p = ch.pipeline();</span><br><span class="line">                     if (sslCtx != null) &#123;</span><br><span class="line">                         p.addLast(sslCtx.newHandler(ch.alloc()));</span><br><span class="line">                     &#125;</span><br><span class="line">                     //p.addLast(new LoggingHandler(LogLevel.INFO));</span><br><span class="line">                     p.addLast(serverHandler);</span><br><span class="line">                 &#125;</span><br><span class="line">             &#125;);</span><br><span class="line"></span><br><span class="line">            // Start the server.</span><br><span class="line">            ChannelFuture f = b.bind(PORT).sync();</span><br><span class="line"></span><br><span class="line">            // Wait until the server socket is closed.</span><br><span class="line">            f.channel().closeFuture().sync();</span><br><span class="line">        &#125; finally &#123;</span><br><span class="line">            // Shut down all event loops to terminate all threads.</span><br><span class="line">            bossGroup.shutdownGracefully();</span><br><span class="line">            workerGroup.shutdownGracefully();</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">@Sharable</span><br><span class="line">public class EchoServerHandler extends ChannelInboundHandlerAdapter &#123;</span><br><span class="line"></span><br><span class="line">    @Override</span><br><span class="line">    public void channelRead(ChannelHandlerContext ctx, Object msg) &#123;</span><br><span class="line">        ctx.write(msg);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    @Override</span><br><span class="line">    public void channelReadComplete(ChannelHandlerContext ctx) &#123;</span><br><span class="line">        ctx.flush();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    @Override</span><br><span class="line">    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) &#123;</span><br><span class="line">        // Close the connection when an exception is raised.</span><br><span class="line">        cause.printStackTrace();</span><br><span class="line">        ctx.close();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line">/**</span><br><span class="line">正常情况下每个 Channel自己的 ChannelPipeline管理的同一个ChannelHandler Class对象的实例都是直接new的一个新实例，也就是原型模式，而不是单例模式。</span><br><span class="line">@Sharable 表示当前handler将被用于多个ChannelPipeline（单例模式）</span><br><span class="line">*/</span><br><span class="line">@Sharable</span><br><span class="line">public class EchoServerHandler extends ChannelInboundHandlerAdapter &#123;</span><br><span class="line"></span><br><span class="line">    @Override</span><br><span class="line">    public void channelRead(ChannelHandlerContext ctx, Object msg) &#123;</span><br><span class="line">        ctx.write(msg);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    @Override</span><br><span class="line">    public void channelReadComplete(ChannelHandlerContext ctx) &#123;</span><br><span class="line">        ctx.flush();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    @Override</span><br><span class="line">    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) &#123;</span><br><span class="line">        // Close the connection when an exception is raised.</span><br><span class="line">        cause.printStackTrace();</span><br><span class="line">        ctx.close();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="Client端"><a href="#Client端" class="headerlink" title="Client端"></a>Client端</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br></pre></td><td class="code"><pre><span class="line">public final class EchoClient &#123;</span><br><span class="line"></span><br><span class="line">    static final boolean SSL = System.getProperty(&quot;ssl&quot;) != null;</span><br><span class="line">    static final String HOST = System.getProperty(&quot;host&quot;, &quot;127.0.0.1&quot;);</span><br><span class="line">    static final int PORT = Integer.parseInt(System.getProperty(&quot;port&quot;, &quot;8007&quot;));</span><br><span class="line">    static final int SIZE = Integer.parseInt(System.getProperty(&quot;size&quot;, &quot;256&quot;));</span><br><span class="line"></span><br><span class="line">    public static void main(String[] args) throws Exception &#123;</span><br><span class="line">        // Configure SSL.git</span><br><span class="line">        final SslContext sslCtx;</span><br><span class="line">        if (SSL) &#123;</span><br><span class="line">            sslCtx = SslContextBuilder.forClient()</span><br><span class="line">                .trustManager(InsecureTrustManagerFactory.INSTANCE).build();</span><br><span class="line">        &#125; else &#123;</span><br><span class="line">            sslCtx = null;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        // Configure the client.</span><br><span class="line">        EventLoopGroup group = new NioEventLoopGroup();</span><br><span class="line">        try &#123;</span><br><span class="line">            Bootstrap b = new Bootstrap();</span><br><span class="line">            b.group(group)</span><br><span class="line">             .channel(NioSocketChannel.class)</span><br><span class="line">             .option(ChannelOption.TCP_NODELAY, true)</span><br><span class="line">             .handler(new ChannelInitializer&lt;SocketChannel&gt;() &#123;</span><br><span class="line">                 @Override</span><br><span class="line">                 public void initChannel(SocketChannel ch) throws Exception &#123;</span><br><span class="line">                     ChannelPipeline p = ch.pipeline();</span><br><span class="line">                     if (sslCtx != null) &#123;</span><br><span class="line">                         p.addLast(sslCtx.newHandler(ch.alloc(), HOST, PORT));</span><br><span class="line">                     &#125;</span><br><span class="line">                     //p.addLast(new LoggingHandler(LogLevel.INFO));</span><br><span class="line">                     p.addLast(new EchoClientHandler());</span><br><span class="line">                 &#125;</span><br><span class="line">             &#125;);</span><br><span class="line"></span><br><span class="line">            // Start the client.</span><br><span class="line">            ChannelFuture f = b.connect(HOST, PORT).sync();</span><br><span class="line"></span><br><span class="line">            // Wait until the connection is closed.</span><br><span class="line">            f.channel().closeFuture().sync();</span><br><span class="line">        &#125; finally &#123;</span><br><span class="line">            // Shut down the event loop to terminate all threads.</span><br><span class="line">            group.shutdownGracefully();</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line">public class EchoClientHandler extends ChannelInboundHandlerAdapter &#123;</span><br><span class="line"></span><br><span class="line">    private final ByteBuf firstMessage;</span><br><span class="line"></span><br><span class="line">    /**</span><br><span class="line">     * Creates a client-side handler.</span><br><span class="line">     */</span><br><span class="line">    public EchoClientHandler() &#123;</span><br><span class="line">        firstMessage = Unpooled.buffer(EchoClient.SIZE);</span><br><span class="line">        for (int i = 0; i &lt; firstMessage.capacity(); i ++) &#123;</span><br><span class="line">            firstMessage.writeByte((byte) i);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    @Override</span><br><span class="line">    public void channelActive(ChannelHandlerContext ctx) &#123;</span><br><span class="line">        ctx.writeAndFlush(firstMessage);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    @Override</span><br><span class="line">    public void channelRead(ChannelHandlerContext ctx, Object msg) &#123;</span><br><span class="line">        ctx.write(msg);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    @Override</span><br><span class="line">    public void channelReadComplete(ChannelHandlerContext ctx) &#123;</span><br><span class="line">       ctx.flush();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    @Override</span><br><span class="line">    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) &#123;</span><br><span class="line">        // Close the connection when an exception is raised.</span><br><span class="line">        cause.printStackTrace();</span><br><span class="line">        ctx.close();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><ul><li><a href="https://www.jianshu.com/p/5e90e0a70446">你真的了解Netty中@Sharable？</a></li><li>极客时间-java并发编程实战</li><li>Scalable IO in Java, Doug Lea</li><li><a href="https://www.jianshu.com/p/188ef8462100">Reactor模式详解＋源码实现</a></li></ul>]]>
    </content>
    <id>https://evasnowind.github.io/2020/09/08/2020-09-09_%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90%E4%B9%8BNetty%E7%BA%BF%E7%A8%8B%E6%A8%A1%E5%9E%8B/</id>
    <link href="https://evasnowind.github.io/2020/09/08/2020-09-09_%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90%E4%B9%8BNetty%E7%BA%BF%E7%A8%8B%E6%A8%A1%E5%9E%8B/"/>
    <published>2020-09-08T16:00:00.000Z</published>
    <summary>提到Netty，就必须先说一下Reactor模式，源头应该是Doug Lea大神（学java的如果不知道这位神的请自己反思一下……）的Scalable IO in Java所提。</summary>
    <title>源码分析之Netty线程模型</title>
    <updated>2026-09-04T04:44:28.355Z</updated>
  </entry>
  <entry>
    <author>
      <name>Prayer</name>
    </author>
    <category term="Java" scheme="https://evasnowind.github.io/categories/Java/"/>
    <category term="源码分析" scheme="https://evasnowind.github.io/categories/Java/%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/"/>
    <category term="Java" scheme="https://evasnowind.github.io/tags/Java/"/>
    <category term="源码分析" scheme="https://evasnowind.github.io/tags/%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/"/>
    <content>
      <![CDATA[<h2 id="Guava-RateLimiter基本使用"><a href="#Guava-RateLimiter基本使用" class="headerlink" title="Guava RateLimiter基本使用"></a>Guava RateLimiter基本使用</h2><p>学东西时我们应该尽量去看官网、看源码、看官方给出的单元测试。</p><p>比如Guava RateLimiter，从RateLimiter类的源码注释中可以看到，官方给出的典型应用场景与使用：</p><span id="more"></span><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line">As an example, imagine that we have a list of tasks to execute, but we don&#x27;t want to submit more than 2 per second:</span><br><span class="line"> </span><br><span class="line"> final RateLimiter rateLimiter = RateLimiter.create(2.0); // rate is &quot;2 permits per second&quot;</span><br><span class="line"> void submitTasks(List&lt;Runnable&gt; tasks, Executor executor) &#123;</span><br><span class="line">   for (Runnable task : tasks) &#123;</span><br><span class="line">     rateLimiter.acquire(); // may wait</span><br><span class="line">     executor.execute(task);</span><br><span class="line">   &#125;</span><br><span class="line"> &#125;</span><br><span class="line"> </span><br><span class="line">As another example, imagine that we produce a stream of data, and we want to cap it at 5kb per second. This could be accomplished by requiring a permit per byte, and specifying a rate of 5000 permits per second:</span><br><span class="line"> </span><br><span class="line"> final RateLimiter rateLimiter = RateLimiter.create(5000.0); // rate = 5000 permits per second</span><br><span class="line"> void submitPacket(byte[] packet) &#123;</span><br><span class="line">   rateLimiter.acquire(packet.length);</span><br><span class="line">   networkService.send(packet);</span><br><span class="line"> &#125;</span><br></pre></td></tr></table></figure><p>一个是限制执行任务的数量，一个是限制每次发送的字节数量。注意，如果超过<code>RateLimiter.create</code>所容许的permits数量，acquire方法将阻塞，直到产生新的permit。当然，如果不想一直阻塞，可以使用tryAcquire(Duration timeout)，该方法，指定一个超时时间，一旦判断出在timeout时间内还无法取得令牌，就返回false。</p><p>此外RateLimiter还支持预热功能，预热后缓存能支持 5 万 TPS 的并发，但是在预热前 5 万 TPS 的并发直接就把缓存击垮。</p><h2 id="原理"><a href="#原理" class="headerlink" title="原理"></a>原理</h2><p>Guava RateLimiter采用的是令牌桶算法。</p><p>经典的令牌桶算法描述如下：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">1. 令牌以固定的速率添加到令牌桶中，假设限流的速率是 r/ 秒，则令牌每 1/r 秒会添加一个；</span><br><span class="line">2. 假设令牌桶的容量是 b （burst，限流器容许的最大突发流量），如果令牌桶已满，则新的令牌会被丢弃；</span><br><span class="line">3. 请求能够通过限流器的前提是令牌桶中有令牌。</span><br></pre></td></tr></table></figure><h3 id="如何用Java实现？"><a href="#如何用Java实现？" class="headerlink" title="如何用Java实现？"></a>如何用Java实现？</h3><p>直观思路：生产者-消费者模式，生产者线程定时向阻塞队列添加令牌，被限流的线程作为消费者从阻塞队列获取令牌。</p><p>但这种实现的问题：定时器。高并发场景下，当系统压力已经临近极限的时候，定时器的精度误差会非常大，同时定时器本身会创建调度线程，也会对系统的性能产生影响。</p><h3 id="Guava如何实现令牌桶算法？"><a href="#Guava如何实现令牌桶算法？" class="headerlink" title="Guava如何实现令牌桶算法？"></a>Guava如何实现令牌桶算法？</h3><p>核心思想：记录并动态计算下一令牌发放的时间。</p><p>基本思路：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line">class SimpleLimiter &#123;</span><br><span class="line">  /*</span><br><span class="line">  当前令牌桶中的令牌数量。</span><br><span class="line">  notes:注意此处的令牌数量是从当前时刻往后开始计算。但是这个变量只有在消费者线程调用acquire()方法时才会更新。这将导致在调用acquire()方法时，storedPermits是滞后的，需要先更新该值。所以有了resync方法</span><br><span class="line">  */</span><br><span class="line">  long storedPermits = 0;</span><br><span class="line">  //令牌桶的容量</span><br><span class="line">  long maxPermits = 3;</span><br><span class="line">  //下一令牌产生时间</span><br><span class="line">  long next = System.nanoTime();</span><br><span class="line">  //发放令牌间隔：纳秒</span><br><span class="line">  long interval = 1000_000_000;</span><br><span class="line">  </span><br><span class="line">  //请求时间在下一令牌产生时间之后,则</span><br><span class="line">  // 1.重新计算令牌桶中的令牌数</span><br><span class="line">  // 2.将下一个令牌发放时间重置为当前时间。</span><br><span class="line">  //notes:因为此时已经更新了令牌桶中已有令牌数量，必须将next更新</span><br><span class="line">  void resync(long now) &#123;</span><br><span class="line">    if (now &gt; next) &#123;</span><br><span class="line">      //新产生的令牌数</span><br><span class="line">      long newPermits=(now-next)/interval;</span><br><span class="line">      //新令牌增加到令牌桶，由于桶的容量有限，通过取两者最小值来丢弃掉多出来的令牌</span><br><span class="line">      storedPermits=min(maxPermits, </span><br><span class="line">        storedPermits + newPermits);</span><br><span class="line">      //将下一个令牌发放时间重置为当前时间。</span><br><span class="line">      next = now;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">  //预占令牌，返回能够获取令牌的时间</span><br><span class="line">  synchronized long reserve(long now)&#123;</span><br><span class="line">    //notes:先更新当前桶中的令牌数量、下一个令牌的发放时间。如果令牌桶中已有令牌够用，此时resync方法中将执行next=now，也就是立即发放令牌。</span><br><span class="line">    //如果令牌不够用、需要等待，那么需要等到next时间。</span><br><span class="line">    resync(now);</span><br><span class="line">    //能够获取令牌的时间</span><br><span class="line">    long at = next;</span><br><span class="line">    //令牌桶中能提供的令牌。本身只需要1个令牌，但需要先看令牌桶中的数量storedPermits是否够用。</span><br><span class="line">    long fb=min(1, storedPermits);</span><br><span class="line">    //令牌净需求：首先减掉令牌桶中的令牌</span><br><span class="line">    /*</span><br><span class="line">    两种情况：</span><br><span class="line">    1. 令牌桶已经有足够的令牌：storedPermits &gt;= 1, fb=1, 此时nr=0, 下一个令牌产生时间 next = next，而通过上面可知此时的next就是当前时间now，接着剩余令牌数量减一、更新库存(this.storedPermits -= fb;)</span><br><span class="line">    2. 令牌桶内令牌不够： &lt;= storedPermits &lt; 1, fb=storedPermits, 此时 0 &lt; nr &lt;= 1, nr是接下来还需要产生的令牌数量（需要平滑产生），next = next + nr*interval,  就是说等待nr*interval时间之后，令牌桶中将产生1个完整的令牌可供调用。</span><br><span class="line">    </span><br><span class="line">    */</span><br><span class="line">    long nr = 1 - fb;</span><br><span class="line">    //重新计算下一令牌产生时间</span><br><span class="line">    next = next + nr*interval;</span><br><span class="line">    //重新计算令牌桶中的令牌</span><br><span class="line">    this.storedPermits -= fb;</span><br><span class="line">    //返回本次调用获取令牌的时间</span><br><span class="line">    return at;</span><br><span class="line">  &#125;</span><br><span class="line">  //申请令牌</span><br><span class="line">  void acquire() &#123;</span><br><span class="line">    //申请令牌时的时间</span><br><span class="line">    long now = System.nanoTime();</span><br><span class="line">    //预占令牌</span><br><span class="line">    long at=reserve(now);</span><br><span class="line">    /*</span><br><span class="line">    还是要分两种情况：</span><br><span class="line">    1. 令牌桶已经有足够的令牌: 此时at=now，waitTime=0，获得令牌，直接返回</span><br><span class="line">    2. 令牌桶内令牌不够： 此时at=下次产生令牌时间，需要等待</span><br><span class="line">    */</span><br><span class="line">    long waitTime=max(at-now, 0);</span><br><span class="line">    //按照条件等待</span><br><span class="line">    if(waitTime &gt; 0) &#123;</span><br><span class="line">      try &#123;</span><br><span class="line">        TimeUnit.NANOSECONDS</span><br><span class="line">          .sleep(waitTime);</span><br><span class="line">      &#125;catch(InterruptedException e)&#123;</span><br><span class="line">        e.printStackTrace();</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>有关算法的简要解释：</p><p>next 变量的意思是下一个令牌的生成时间，可以理解为当前线程请求的令牌的生成时刻，如第一张图所示：线程 T1 的令牌的生成时刻是第三秒。</p><p>线程 T 请求时，存在三种场景：</p><ol><li>桶里有剩余令牌。</li><li>刚创建令牌，线程同时请求。</li><li>桶里无剩余令牌。</li></ol><p>场景 2 可以想象成线程请求的同时令牌刚好生成，没来得及放入桶内就被线程 T 拿走了。因此将场景 2 和场景 3 合并成一种情况，那就是桶里没令牌。即线程请求时，桶里可分为有令牌和没令牌。</p><p>“桶里没令牌”，线程 T 需要等待；需要等待则意味着 now(线程 T 请求时刻) 小于等于 next(线程 T 所需的令牌的生成时刻)。这里可以想象一下线程 T 在苦苦等待令牌生成的场景，只要线程 T 等待那么久之后，就会被放行。放行这一刻令牌同时生成，立马被线程拿走，令牌没放入桶里。对应到代码就是 resync 方法没有进入 if 语句内。</p><p>“桶里有令牌”，线程 T 不需要等待。说明线程 T 对应的令牌已经早早生成，已在桶内。代码就是：now &gt; next（请求时刻大于对应令牌的生成时刻）。因此在分配令牌给线程之前，需要计算线程 T 迟到了多久，迟到的这段时间，有多少个令牌生成¹；然后放入桶内，满了则丢弃²；未来的线程的令牌在这个时刻已经生成放入桶内³（即 resync 方法的逻辑）。线程无需等待，所以不需要增加一个 interval 了。</p><p>角标分别对应 resync 方法内的代码： ¹: long newPermits&#x3D;(now-next)&#x2F;interval; ²: storedPermits&#x3D;min(maxPermits,  storedPermits + newPermits); ³: next &#x3D; now;</p><p>对于reverse方法，首先肯定是计算令牌桶里面的令牌数量，然后取令牌桶中的令牌数量storedPermits 与当前的需要的令牌数量 1 做比较，大于等于 1，说明令牌桶至少有一个令牌，此时下一令牌的获取是不需要等待的，表现为 next 不需要变化；而当令牌桶中的令牌没有了即storedPermits等于 0 时，next 就会变化为下一个令牌的获取时间，注意 nr 的值变化</p><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><ul><li>java并发编程实战 专栏课</li></ul>]]>
    </content>
    <id>https://evasnowind.github.io/2020/09/01/2020-09-02_Guava_RateLimiter_%E7%9A%84%E4%BD%BF%E7%94%A8%E4%B8%8E%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90%EF%BC%88%E4%BB%A4%E7%89%8C%E6%A1%B6%E7%AE%97%E6%B3%95%E5%AE%9E%E7%8E%B0%E6%80%9D%E8%B7%AF%EF%BC%89/</id>
    <link href="https://evasnowind.github.io/2020/09/01/2020-09-02_Guava_RateLimiter_%E7%9A%84%E4%BD%BF%E7%94%A8%E4%B8%8E%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90%EF%BC%88%E4%BB%A4%E7%89%8C%E6%A1%B6%E7%AE%97%E6%B3%95%E5%AE%9E%E7%8E%B0%E6%80%9D%E8%B7%AF%EF%BC%89/"/>
    <published>2020-09-01T16:00:00.000Z</published>
    <summary>学东西时我们应该尽量去看官网、看源码、看官方给出的单元测试。</summary>
    <title>Guava RateLimiter 的使用与源码分析（令牌桶算法实现思路）</title>
    <updated>2026-09-04T04:44:28.355Z</updated>
  </entry>
  <entry>
    <author>
      <name>Prayer</name>
    </author>
    <category term="中间件" scheme="https://evasnowind.github.io/categories/%E4%B8%AD%E9%97%B4%E4%BB%B6/"/>
    <category term="Java" scheme="https://evasnowind.github.io/tags/Java/"/>
    <category term="Tomcat" scheme="https://evasnowind.github.io/tags/Tomcat/"/>
    <category term="并发" scheme="https://evasnowind.github.io/tags/%E5%B9%B6%E5%8F%91/"/>
    <category term="Dubbo" scheme="https://evasnowind.github.io/tags/Dubbo/"/>
    <content>
      <![CDATA[<h2 id="预备知识"><a href="#预备知识" class="headerlink" title="预备知识"></a>预备知识</h2><ul><li><p>计算任务的分类</p><ul><li>CPU密集型：需要线程长时间进行的复杂的运算，这种类型的任务需要少创建线程，过多的线程将会频繁引起上文切换，降低任务处理处理速度。</li><li>IO密集型：由于线程并不是一直在运行，可能大部分时间在等待 IO 读取&#x2F;写入数据，增加线程数量可以提高并发度，尽可能多处理任务。</li></ul></li></ul><span id="more"></span><ul><li><p>JDK线程池，<code>java.util.concurrent.ThreadPoolExecutor</code> 传说中的7个参数，作用，线程池运行机制，参见下图复习</p><p><img src="/images/Java%E7%BA%BF%E7%A8%8B%E6%B1%A0%E8%BF%90%E8%A1%8C%E6%9C%BA%E5%88%B6-300x78_f1ca502b.jpg"></p></li></ul><h2 id="概述"><a href="#概述" class="headerlink" title="概述"></a>概述</h2><p>结合ThreadPoolExecutor的运行过程，可以知道ThreadPoolExecutor主要倾向于CPU密集型任务，但对于对于 <code>io</code> 密集型任务，如数据库查询，rpc 请求调用等，就不是很友好。</p><p>由于 Tomcat&#x2F;Jetty 需要处理大量客户端请求任务，如果采用原生线程池，一旦接受请求数量大于线程池核心线程数，这些请求就会被放入到队列中，等待核心线程处理。这样做显然降低这些请求总体处理速度，所以两者都没采用 JDK 原生线程池。</p><p>Jetty选择自己实现线程池组件，可以定制化开发，但难度较大，而Tomcat选择扩展ThreadPoolExecutor，相对比较简单。</p><h2 id="Tomcat线程池源码分析"><a href="#Tomcat线程池源码分析" class="headerlink" title="Tomcat线程池源码分析"></a>Tomcat线程池源码分析</h2><p>Tomcat源码中，为扩展线程池，主要修改了：</p><ul><li>自定义ThreadPoolExecutor，直接继承JDK的ThreadPoolExecutor，重写部分逻辑</li><li>实现TaskQueue，直接继承<code>LinkedBlockingQueue</code> ，重写 <code>offer</code> 方法。</li></ul><h3 id="ThreadPoolExecutor"><a href="#ThreadPoolExecutor" class="headerlink" title="ThreadPoolExecutor"></a>ThreadPoolExecutor</h3><p>线程池核心方法execute()，Tomcat简单做了修改，还是将工作任务交给父类，也就是Java原生线程池处理，但增加了一个重试策略。如果原生线程池执行拒绝策略的情况，抛出 <code>RejectedExecutionException</code> 异常。这里将会捕获，然后重新再次尝试将任务加入到 <code>TaskQueue</code> ，尽最大可能执行任务。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line">public void execute(Runnable command, long timeout, TimeUnit unit) &#123;</span><br><span class="line">        this.submittedCount.incrementAndGet();</span><br><span class="line"></span><br><span class="line">        try &#123;</span><br><span class="line">            super.execute(command);</span><br><span class="line">        &#125; catch (RejectedExecutionException var9) &#123;</span><br><span class="line">            if (!(super.getQueue() instanceof TaskQueue)) &#123;</span><br><span class="line">                this.submittedCount.decrementAndGet();</span><br><span class="line">                throw var9;</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            TaskQueue queue = (TaskQueue)super.getQueue();</span><br><span class="line"></span><br><span class="line">            try &#123;</span><br><span class="line">                //拒绝后，再尝试入队，还不行则抛出异常</span><br><span class="line">                if (!queue.force(command, timeout, unit)) &#123;</span><br><span class="line">                    this.submittedCount.decrementAndGet();</span><br><span class="line">                    throw new RejectedExecutionException(sm.getString(&quot;threadPoolExecutor.queueFull&quot;));</span><br><span class="line">                &#125;</span><br><span class="line">            &#125; catch (InterruptedException var8) &#123;</span><br><span class="line">                this.submittedCount.decrementAndGet();</span><br><span class="line">                throw new RejectedExecutionException(var8);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure><p>需要注意 <code>submittedCount</code> 变量。这是 Tomcat 线程池内部一个重要的参数，它是一个 <code>AtomicInteger</code> 变量，将会实时统计已经提交到线程池中，但还没有执行结束的任务。也就是说 <code>submittedCount</code> 等于线程池队列中的任务数加上线程池工作线程正在执行的任务。</p><h3 id="TaskQueue"><a href="#TaskQueue" class="headerlink" title="TaskQueue"></a>TaskQueue</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line">public class TaskQueue extends LinkedBlockingQueue&lt;Runnable&gt; &#123;</span><br><span class="line">    ......</span><br><span class="line">        //tomcat-util-10.0.0-M6.jar</span><br><span class="line">    public boolean offer(Runnable o) &#123;</span><br><span class="line">        if (this.parent == null) &#123;</span><br><span class="line">            //1.若没有给出tomcat线程池对象，则调用父类方法</span><br><span class="line">            return super.offer(o);</span><br><span class="line">        &#125; else if (this.parent.getPoolSize() == this.parent.getMaximumPoolSize()) &#123;</span><br><span class="line">            //2.若当前线程数已达到最大线程数，则放入阻塞队列</span><br><span class="line">            return super.offer(o);</span><br><span class="line">        &#125; else if (this.parent.getSubmittedCount() &lt;= this.parent.getPoolSize()) &#123;</span><br><span class="line">            //3.若当前已提交任务数量小于等于最大线程数，说明此时有空闲线程。此时将任务放入队列中，立刻会有空闲线程来处理该任务</span><br><span class="line">            return super.offer(o);</span><br><span class="line">        &#125; else &#123;</span><br><span class="line">            //4.若当前线程数小于最大线程数，发返回false，此时线程池将会创建新线程！！！</span><br><span class="line">            return this.parent.getPoolSize() &lt; this.parent.getMaximumPoolSize() ? false : super.offer(o);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>核心在最后一个三元判断中，这里需要结合Java的ThreadPoolExecutor一起看：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line">public void execute(Runnable command) &#123;</span><br><span class="line">       if (command == null)</span><br><span class="line">           throw new NullPointerException();</span><br><span class="line">       /*</span><br><span class="line">        * Proceed in 3 steps:</span><br><span class="line">        *</span><br><span class="line">        * 1. If fewer than corePoolSize threads are running, try to</span><br><span class="line">        * start a new thread with the given command as its first</span><br><span class="line">        * task.  The call to addWorker atomically checks runState and</span><br><span class="line">        * workerCount, and so prevents false alarms that would add</span><br><span class="line">        * threads when it shouldn&#x27;t, by returning false.</span><br><span class="line">        *</span><br><span class="line">        * 2. If a task can be successfully queued, then we still need</span><br><span class="line">        * to double-check whether we should have added a thread</span><br><span class="line">        * (because existing ones died since last checking) or that</span><br><span class="line">        * the pool shut down since entry into this method. So we</span><br><span class="line">        * recheck state and if necessary roll back the enqueuing if</span><br><span class="line">        * stopped, or start a new thread if there are none.</span><br><span class="line">        *</span><br><span class="line">        * 3. If we cannot queue task, then we try to add a new</span><br><span class="line">        * thread.  If it fails, we know we are shut down or saturated</span><br><span class="line">        * and so reject the task.</span><br><span class="line">        */</span><br><span class="line">       int c = ctl.get();</span><br><span class="line">       if (workerCountOf(c) &lt; corePoolSize) &#123;</span><br><span class="line">           if (addWorker(command, true))</span><br><span class="line">               return;</span><br><span class="line">           c = ctl.get();</span><br><span class="line">       &#125;</span><br><span class="line">       //如果阻塞队列返回false，将会走else分支，去创建新的线程</span><br><span class="line">       if (isRunning(c) &amp;&amp; workQueue.offer(command)) &#123;</span><br><span class="line">           int recheck = ctl.get();</span><br><span class="line">           if (! isRunning(recheck) &amp;&amp; remove(command))</span><br><span class="line">               reject(command);</span><br><span class="line">           else if (workerCountOf(recheck) == 0)</span><br><span class="line">               addWorker(null, false);</span><br><span class="line">       &#125;</span><br><span class="line">       else if (!addWorker(command, false))</span><br><span class="line">           reject(command);</span><br><span class="line">   &#125;</span><br></pre></td></tr></table></figure><p>由此，即可看出Tomcat通过扩展的方式改变了线程池运行机制。</p><h2 id="Dubbo线程池源码分析"><a href="#Dubbo线程池源码分析" class="headerlink" title="Dubbo线程池源码分析"></a>Dubbo线程池源码分析</h2><p>dubbo中也采用了与Tomcat一样的思路去修改Java线程池，可以参考源码中的EagerThreadPool，也是同样的修改：</p><ul><li>自定义ThreadPoolExecutor，直接继承JDK的ThreadPoolExecutor，重写部分逻辑</li><li>实现TaskQueue，直接继承<code>LinkedBlockingQueue</code> ，重写 <code>offer</code> 方法。</li></ul><p><code>EagerThreadPoolExecutor</code>源码总共就这么一丢丢，重写的逻辑是：</p><ul><li>execute方法添加一次重试</li><li>加上有关<code>submittedTaskCount</code>这个变量的维护，该变量表示当前正在被执行的任务数量</li></ul><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br></pre></td><td class="code"><pre><span class="line">public class EagerThreadPoolExecutor extends ThreadPoolExecutor &#123;</span><br><span class="line"></span><br><span class="line">    /**</span><br><span class="line">     * task count</span><br><span class="line">     */</span><br><span class="line">    private final AtomicInteger submittedTaskCount = new AtomicInteger(0);</span><br><span class="line"></span><br><span class="line">    public EagerThreadPoolExecutor(int corePoolSize,</span><br><span class="line">                                   int maximumPoolSize,</span><br><span class="line">                                   long keepAliveTime,</span><br><span class="line">                                   TimeUnit unit, TaskQueue&lt;Runnable&gt; workQueue,</span><br><span class="line">                                   ThreadFactory threadFactory,</span><br><span class="line">                                   RejectedExecutionHandler handler) &#123;</span><br><span class="line">        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    /**</span><br><span class="line">     * @return current tasks which are executed</span><br><span class="line">     */</span><br><span class="line">    public int getSubmittedTaskCount() &#123;</span><br><span class="line">        return submittedTaskCount.get();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    @Override</span><br><span class="line">    protected void afterExecute(Runnable r, Throwable t) &#123;</span><br><span class="line">        submittedTaskCount.decrementAndGet();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    @Override</span><br><span class="line">    public void execute(Runnable command) &#123;</span><br><span class="line">        if (command == null) &#123;</span><br><span class="line">            throw new NullPointerException();</span><br><span class="line">        &#125;</span><br><span class="line">        // do not increment in method beforeExecute!</span><br><span class="line">        submittedTaskCount.incrementAndGet();</span><br><span class="line">        try &#123;</span><br><span class="line">            super.execute(command);</span><br><span class="line">        &#125; catch (RejectedExecutionException rx) &#123;</span><br><span class="line">            // retry to offer the task into queue.</span><br><span class="line">            final TaskQueue queue = (TaskQueue) super.getQueue();</span><br><span class="line">            try &#123;</span><br><span class="line">                if (!queue.retryOffer(command, 0, TimeUnit.MILLISECONDS)) &#123;</span><br><span class="line">                    submittedTaskCount.decrementAndGet();</span><br><span class="line">                    throw new RejectedExecutionException(&quot;Queue capacity is full.&quot;, rx);</span><br><span class="line">                &#125;</span><br><span class="line">            &#125; catch (InterruptedException x) &#123;</span><br><span class="line">                submittedTaskCount.decrementAndGet();</span><br><span class="line">                throw new RejectedExecutionException(x);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125; catch (Throwable t) &#123;</span><br><span class="line">            // decrease any way</span><br><span class="line">            submittedTaskCount.decrementAndGet();</span><br><span class="line">            throw t;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>TaskQueue</code>同样是继承自<code>LinkedBlockingQueue</code>，也只是改了一丢丢：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br></pre></td><td class="code"><pre><span class="line">public class TaskQueue&lt;R extends Runnable&gt; extends LinkedBlockingQueue&lt;Runnable&gt; &#123;</span><br><span class="line"></span><br><span class="line">    private static final long serialVersionUID = -2635853580887179627L;</span><br><span class="line"></span><br><span class="line">    private EagerThreadPoolExecutor executor;</span><br><span class="line"></span><br><span class="line">    public TaskQueue(int capacity) &#123;</span><br><span class="line">        super(capacity);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    public void setExecutor(EagerThreadPoolExecutor exec) &#123;</span><br><span class="line">        executor = exec;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    @Override</span><br><span class="line">    public boolean offer(Runnable runnable) &#123;</span><br><span class="line">        if (executor == null) &#123;</span><br><span class="line">            throw new RejectedExecutionException(&quot;The task queue does not have executor!&quot;);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        int currentPoolThreadSize = executor.getPoolSize();</span><br><span class="line">        // have free worker. put task into queue to let the worker deal with task.</span><br><span class="line">        if (executor.getSubmittedTaskCount() &lt; currentPoolThreadSize) &#123;</span><br><span class="line">            return super.offer(runnable);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        // return false to let executor create new worker.</span><br><span class="line">        if (currentPoolThreadSize &lt; executor.getMaximumPoolSize()) &#123;</span><br><span class="line">            return false;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        // currentPoolThreadSize &gt;= max</span><br><span class="line">        return super.offer(runnable);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    /**</span><br><span class="line">     * retry offer task</span><br><span class="line">     *</span><br><span class="line">     * @param o task</span><br><span class="line">     * @return offer success or not</span><br><span class="line">     * @throws RejectedExecutionException if executor is terminated.</span><br><span class="line">     */</span><br><span class="line">    public boolean retryOffer(Runnable o, long timeout, TimeUnit unit) throws InterruptedException &#123;</span><br><span class="line">        if (executor.isShutdown()) &#123;</span><br><span class="line">            throw new RejectedExecutionException(&quot;Executor is shutdown!&quot;);</span><br><span class="line">        &#125;</span><br><span class="line">        return super.offer(o, timeout, unit);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>重点还是在offer方法的实现，开发者都写了注释，我就不废话了 🙂 真的是和tomcat一毛一样的思路。</p><p>收工。</p><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><ul><li><a href="https://zhuanlan.zhihu.com/p/94679167">原生线程池这么强大，Tomcat 为何还需扩展线程池？</a></li></ul>]]>
    </content>
    <id>https://evasnowind.github.io/2020/08/17/2020-08-18_tomcat%E5%92%8Cdubbo%E5%AF%B9%E4%BA%8EJDK%E7%BA%BF%E7%A8%8B%E6%B1%A0%E7%9A%84%E4%BF%AE%E6%94%B9/</id>
    <link href="https://evasnowind.github.io/2020/08/17/2020-08-18_tomcat%E5%92%8Cdubbo%E5%AF%B9%E4%BA%8EJDK%E7%BA%BF%E7%A8%8B%E6%B1%A0%E7%9A%84%E4%BF%AE%E6%94%B9/"/>
    <published>2020-08-17T16:00:00.000Z</published>
    <summary>结合ThreadPoolExecutor的运行过程，可以知道ThreadPoolExecutor主要倾向于CPU密集型任务，但对于对于 io 密集型任务，如数据库查询，rpc 请求调用等，就不是很友好。</summary>
    <title>tomcat和dubbo对于JDK线程池的修改</title>
    <updated>2026-09-04T04:44:28.355Z</updated>
  </entry>
  <entry>
    <author>
      <name>Prayer</name>
    </author>
    <category term="工具" scheme="https://evasnowind.github.io/categories/%E5%B7%A5%E5%85%B7/"/>
    <category term="Docker" scheme="https://evasnowind.github.io/tags/Docker/"/>
    <content>
      <![CDATA[<p>按官方提示<a href="https://github.com/naver/pinpoint-docker">https://github.com/naver/pinpoint-docker</a> ，通过docker安装pinpoint时，遇到端口冲突问题</p><blockquote><table><thead><tr><th></th></tr></thead><tbody><tr><td>&#96;&#96;&#96;  docker-compose up -d</td></tr></tbody></table><p> Creating network “pinpoint-docker_pinpoint” with driver “bridge”<br> Creating pinpoint-docker_zoo3_1    … done<br> Creating pinpoint-docker_zoo2_1    … done<br> Creating pinpoint-flink-jobmanager … done<br> Creating pinpoint-docker_zoo1_1     … done<br> Creating pinpoint-mysql             … done<br> Creating pinpoint-hbase             … error<br> Creating pinpoint-flink-taskmanager … done                                                                                                                                                                       </p></blockquote><span id="more"></span><p> ERROR: for pinpoint-hbase  Cannot start service pinpoint-hbase: Ports are not available: listen tcp 0.0.0.0:2180: bind: An attempt was made to access a socket in a way forbidden by its access permissions.       </p><p> ERROR: for pinpoint-hbase  Cannot start service pinpoint-hbase: Ports are not available: listen tcp 0.0.0.0:2180: bind: An attempt was made to access a socket in a way forbidden by its access permissions.<br> Encountered errors while bringing up the project. &#96;&#96;&#96; |</p><p>根据官方提示，修改pinpoint-docker文件夹下的.env文件，修改hbase端口即可，比如我修改为了12180</p><blockquote><table><thead><tr><th></th></tr></thead><tbody><tr><td>&#96;&#96;&#96; ### Pinpoint-Hbase</td></tr></tbody></table></blockquote><p> PINPOINT_HBASE_NAME&#x3D;pinpoint-hbase<br> #config for hbase in external docker<br> EXTERNAL_HBASE_PORT&#x3D;12180 &#96;&#96;&#96; |</p>]]>
    </content>
    <id>https://evasnowind.github.io/2020/07/23/2020-07-24_%E8%A7%A3%E5%86%B3win_10%E9%80%9A%E8%BF%87docker%E5%AE%89%E8%A3%85pinpoint%E6%8A%A5%E7%AB%AF%E5%8F%A3%E9%94%99%E8%AF%AF/</id>
    <link href="https://evasnowind.github.io/2020/07/23/2020-07-24_%E8%A7%A3%E5%86%B3win_10%E9%80%9A%E8%BF%87docker%E5%AE%89%E8%A3%85pinpoint%E6%8A%A5%E7%AB%AF%E5%8F%A3%E9%94%99%E8%AF%AF/"/>
    <published>2020-07-23T16:00:00.000Z</published>
    <summary>--- docker-compose up -d Creating network &quot;pinpoint-dockerpinpoint&quot; with driver &quot;bridge&quot; Creatin。</summary>
    <title>解决win 10通过docker安装pinpoint报端口错误</title>
    <updated>2026-09-04T04:44:28.355Z</updated>
  </entry>
  <entry>
    <author>
      <name>Prayer</name>
    </author>
    <category term="随笔" scheme="https://evasnowind.github.io/categories/%E9%9A%8F%E7%AC%94/"/>
    <category term="成长思考" scheme="https://evasnowind.github.io/tags/%E6%88%90%E9%95%BF%E6%80%9D%E8%80%83/"/>
    <content>
      <![CDATA[<h3 id="明确目标"><a href="#明确目标" class="headerlink" title="明确目标"></a>明确目标</h3><p>进入互联网行业，不管是实习，还是正式工作，要记住，从入行这一刻起，就要不停的学习、不停的思考，最好是根据自己的优缺点、喜好，订个明确的目标，自己到底想走哪条路线，是纯技术路线、不想太关心业务？产品经理？程序员只是过渡、想走管理？还是在互联网公司捞几年钱然后就转行？</p><span id="more"></span><ul><li>对技术特别感兴趣：那毫无疑问，技术这条路，其实也没有上限，你可以去极客时间app上看看那些大牛们</li></ul><h3 id="制定计划、逐步实行"><a href="#制定计划、逐步实行" class="headerlink" title="制定计划、逐步实行"></a>制定计划、逐步实行</h3><p>听起来有点土、老生常谈了对吗？但我觉得这个当真是最朴实、最简单、也最实用的道理。</p><p>我目前在某央企、某互联网公司都做过程序员这个岗位。互联网行业，给我最大感触就是变化快、对于程序员的要求也越来越高，各种红利基本已经耗尽，前些年很多人看程序员赚钱、纷纷转行做程序员，这个恐怕后续会越来越难，从面试所考察内容就有直观感受，虽然每年大家都会说“互联网寒冬”、公司难，但从2019年年末开始，明显考察的内容越来越深、难。比如有如下一些题目（下面主要以java程序员举例，毕竟需求量大）：</p><ul><li>java实现单例模式有哪些？知道DCL（双重锁检查）吗？为何要这么写？是否需要加volatile？</li><li>java里synchronized关键字，锁膨胀的过程？</li><li>java 线程池的7个参数？为何阿里开发规范建议自己创建线程池、不要用JDK工具方法创建？</li><li>java AQS是什么？用AQS手写一个锁？</li><li>java里类加载的双亲委托模型？JDBC是如何打破双亲委托模型的？</li><li>JVM垃圾回收器有哪些？各自算法？怎么搭配？</li><li>JVM调优？</li><li>kafka为何速度那么快？</li><li>常见MQ有哪些？特点是什么？</li><li>spring cloud有哪些组件？spring cloud 和spring boot的关系？怎么实现高可用？</li><li>……</li></ul><p>上述还只是一部分，做一个合格的Java程序员实在太难了……目前就<strong>社招</strong>而言，想成为一个比较好的java程序员，能够脱颖而出、受大厂青睐的话，一般下面这几方面，至少得会，并且最好有一方面非常精通：</p><ul><li>java多线程、高并发相关</li><li>JVM，会调优</li><li>mysql</li><li>MQ</li><li>redis</li><li>zookeeper</li></ul><p>越是薪资高的岗位，要求越高。互联网公司，java程序员需求量很大，但已经不是找个培训班、上1个月学学spring mvc、会CRUD操作就可以胜任工作的时代了。可能实际工作里，确实用不到那么多，哪怕是阿里这种技术公司，同样如此。但“面试造火箭，进去拧螺丝”，那些受青睐的大厂们，就是因为能做那些岗位的人太多、他们需要筛选、优中选优，尽可能找到进来就能立刻干活儿的人，所以面试越来越难。</p><h3 id="对于校招生"><a href="#对于校招生" class="headerlink" title="对于校招生"></a>对于校招生</h3><p>我的建议就是：</p><ul><li><p>基础一定要扎实</p><ul><li><p>尤其数据结构、算法、操作系统、计算机网络等。校招生刚入职时一般不会安排太重要的工作（大牛学弟学妹们除外，我只是说多数情况），所以大家可能参加工作后的前两年以为这些课不重要。但越往后，越是想往程序员这条（不归）路上深入走，就会发现这些课实在太重要了。那些大部头的书（类似操作系统第6版、CSAPP、重构这种书），早晚还是得仔细看。我目前在翻的书大家可以体会一下：</p><ul><li>深入理解JVM 周志明，第三版（一块砖头……）</li><li>高性能Mysql 第三版（又一块砖头……）</li><li>redis设计与实现 （计划中、暂时还没时间看的一块砖头……）</li><li>java并发编程的艺术 方腾飞</li><li>zookeeper 分布式过程协同技术详解</li></ul></li></ul></li><li><p>一定要看源码</p><ul><li>作为校招生，应该试着去看JDK 源码（比如HashMap ConcurrentHashMap AQS等），spring 源码可以尝试一下，这两个都是设计比较好的，源码中往往有很多注释，可以让你了解作者为何这样&#x2F;那样实现，对于你自己的编程能力提高很有帮助。同时，这个对于校招有加分。</li><li>等到社招时，基本看源码是必问的，到时候会更深，比如，大家应该听说过netty，这玩意如果你看过源码、特别清楚原理，恐怕过面试so easy。只是举例子，netty这个估计对于在校生有些难，建议从JDK spring开始，养成读源码的习惯。</li></ul></li><li><p>想进大厂，务必刷刷算法题</p><ul><li><p>推荐极客时间的付费专栏《数据结构和算法之美》并尽量按作者提到的这个目录 <a href="https://github.com/wangzheng0822/algo">https://github.com/wangzheng0822/algo</a> 自己实现一下。</p></li><li><p>算法题，还是得刷刷leetcode，可以参与leetcode-cn每天刷题打卡的活动，持之以恒。</p></li><li><p>B站 花花酱 刷题视频（我没看，但B站上貌似看的人很多），或是覃超大魔王 五毒神掌刷题法（他的这个刷题法在B站有公开视频，我觉得不错，有按照他的思路在练习，有条件的可以参加下极客时间 算法训练营，不过网上有流出视频，可以找找）</p></li><li><p>labuladong 算法小抄，这个强烈推荐，有算法模板、习题分析 <a href="https://labuladong.gitbook.io/algo/di-ling-zhang-bi-du-xi-lie">https://labuladong.gitbook.io/algo/di-ling-zhang-bi-du-xi-lie</a> 有公众号<strong>labuladong</strong></p></li><li><p><strong>最重要的是务必自己去练手</strong>、不要以为看题解就理解了，要不断、反复练习，一道题刷几遍，简单到不能再简单（比如二叉树按层遍历），可以不用再练，但只要还有不理解、再次遇到时没法在5min内有思路的leetcode题目，都要隔段时间练习的。</p><ul><li><p>工具：anki, leetcode 探索中的学习任务</p></li><li><p>算法资料:</p><ul><li><a href="https://labuladong.gitbook.io/algo">https://labuladong.gitbook.io/algo</a></li><li>B站</li></ul></li></ul></li></ul></li><li><p>加入一些技术群&#x2F;刷题群，看大家是怎么学的</p><ul><li>学习是一件反人类的事，很容易懈怠。加群，多看看网上有多少人在一起学习，可以方便相互交流，同时相互激励、避免自己偷懒。</li></ul></li><li><p>持续学习，有输出</p><ul><li>如果是学编程语言、各种框架，必须自己手写demo、跑程序、看看效果。最好养成学东西时尽量写笔记&#x2F;博客的习惯，这样逐步积累，经年累月下来会非常可观。</li></ul></li><li><p>找项目练手</p><ul><li>网上，github上可以练手的项目很多，这个可以根据自身情况，自己找一下，一般培训机构都会选择网上商城，因为涉及的业务场景复杂、各种框架、中间件用的比较多，这里就不好推荐哪个了。</li></ul></li><li><p>极客时间</p><ul><li><p>极客时间的内容做的不错，每天听听卖桃者说，听听大牛们的观点，有条件的根据自己兴趣买些课学习，上面的课有不少质量相当高，以下仅推荐（我不是带货的，是真的不错的专栏哦），这些大佬们讲课往往是实际项目经验+理论知识，有些甚至能拿来直接用、解决工作中的问题：</p><ul><li>陈皓 《左耳听风》</li><li>王争 《数据结构和算法之美》</li><li>丁奇 《mysql 45讲》</li><li>李玥 《消息队列高手课》</li></ul></li></ul></li><li><p>谷歌评分卡</p><ul><li><p>技术的道路很长，可以参考谷歌评分卡，给自己打打分</p></li><li><p>以下翻译取自：<a href="https://www.jianshu.com/p/b1f57417320d">https://www.jianshu.com/p/b1f57417320d</a></p><blockquote><p>0 – you are unfamiliar with the subject area.(0 -你不熟悉主题领域。)</p><p>1 – you can read &#x2F; understand the most fundamental aspects of the subject area.(1 -你可以阅读&#x2F;了解主题领域最基本的方面。)</p><p>2 – ability to implement small changes, understand basic principles and able to figure out additional details with minimal help.(2 -能够实现小的变化，理解基本原理，并能在最小的帮助下找出更多的细节。)</p><p>3 – basic proficiency in a subject area without relying on help.(3 -在不依赖帮助的情况下，熟练掌握某一科目。)</p><p>4 – you are comfortable with the subject area and all routine work on it: (4 -你对主题领域和所有日常工作都很熟悉:) For software areas – ability to develop medium programs using all basic language features w&#x2F;o book, awareness of more esoteric features (with book).(对于软件领域来说，能够使用所有基本的语言来开发中等的程序，使用w&#x2F;o book，了解更深奥的特性(带书)。) For systems areas – understanding of many fundamentals of networking and systems administration, ability to run a small network of systems including recovery, debugging and nontrivial troubleshooting that relies on the knowledge of internals.(对于系统领域——了解网络和系统管理的许多基础知识，能够运行一个小型的系统网络，包括恢复、调试和依赖于内部知识的重要故障排除。)</p><p>5 – an even lower degree of reliance on reference materials. Deeper skills in a field or specific technology in the subject area.(5 -对参考资料的依赖程度更低。在某一领域或某一特定技术领域有较深的技能。)</p><p>6 – ability to develop large programs and systems from scratch. Understanding of low level details and internals. Ability to design &#x2F; deploy most large, distributed systems from scratch.(6 -能够从头开始开发大型程序和系统。了解低层次的细节和内部信息。能够设计&#x2F;部署大多数大型的分布式系统。)</p><p>7 – you understand and make use of most lesser known language features, technologies, and associated internals. Ability to automate significant amounts of systems administration.(7 -你理解并利用最不知名的语言特征、技术和相关的内部信息。能够自动化大量的系统管理。)</p><p>8 – deep understanding of corner cases, esoteric features, protocols and systems including “theory of operation”. Demonstrated ability to design, deploy and own very critical or large infrastructure, build accompanying automation.(8 -深刻理解角落案例，深奥的特点，协议和系统，包括“操作理论”。演示了设计、部署和拥有非常关键或大型基础设施的能力，并建立了相应的自动化。)</p><p>9 – could have written the book about the subject area but didn’t; works with standards committees on defining new standards and methodologies.(9 -本可以写关于主题领域的书，但没有;与标准委员会一起制定新的标准和方法。)</p><p>10 – wrote the book on the subject area (there actually has to be a book). Recognized industry expert in the field, might have invented it.(10 -写在主题领域的书(实际上必须有一本书)。业内公认的业内专家，可能已经发明了它。)</p></blockquote><blockquote><p><strong>Subject Areas:</strong></p><p>TCP&#x2F;IP Networking (OSI stack, DNS etc)</p><p>Unix&#x2F;Linux internals</p><p>Unix&#x2F;Linux Systems administration</p><p>Algorithms and Data Structures</p><p>C</p><p>C++</p><p>Python</p><p>Java</p><p>Perl</p><p>Go</p><p>Shell Scripting (sh, Bash, ksh, csh)</p><p>SQL and&#x2F;or Database Admin</p><p>Scripting language of your choice (not already mentioned) _</p><p>People Management</p><p>Project Management</p></blockquote></li></ul></li></ul>]]>
    </content>
    <id>https://evasnowind.github.io/2020/07/14/2020-07-15_%E5%86%99%E7%BB%99%E5%8D%B3%E5%B0%86%E6%88%96%E6%98%AF%E6%83%B3%E8%BF%9B%E5%85%A5%E4%BA%92%E8%81%94%E7%BD%91%E5%A4%A7%E5%8E%82%E5%B7%A5%E4%BD%9C%E7%9A%84%E5%90%8E%E6%B5%AA%E4%BB%AC/</id>
    <link href="https://evasnowind.github.io/2020/07/14/2020-07-15_%E5%86%99%E7%BB%99%E5%8D%B3%E5%B0%86%E6%88%96%E6%98%AF%E6%83%B3%E8%BF%9B%E5%85%A5%E4%BA%92%E8%81%94%E7%BD%91%E5%A4%A7%E5%8E%82%E5%B7%A5%E4%BD%9C%E7%9A%84%E5%90%8E%E6%B5%AA%E4%BB%AC/"/>
    <published>2020-07-14T16:00:00.000Z</published>
    <summary>进入互联网行业，不管是实习，还是正式工作，要记住，从入行这一刻起，就要不停的学习、不停的思考，最好是根据自己的优缺点、喜好，订个明确的目标，自己到底想走哪条路线，是纯技术路线、不想太关心业务？产品经理？</summary>
    <title>写给即将或是想进入互联网大厂工作的后浪们</title>
    <updated>2026-09-04T04:44:28.355Z</updated>
  </entry>
  <entry>
    <author>
      <name>Prayer</name>
    </author>
    <category term="Java" scheme="https://evasnowind.github.io/categories/Java/"/>
    <category term="并发" scheme="https://evasnowind.github.io/categories/Java/%E5%B9%B6%E5%8F%91/"/>
    <category term="Java" scheme="https://evasnowind.github.io/tags/Java/"/>
    <category term="并发" scheme="https://evasnowind.github.io/tags/%E5%B9%B6%E5%8F%91/"/>
    <content>
      <![CDATA[<p>从一道面试题讲起：<strong>采用DCL实现单例模式时，是否需要加volatile关键字？为什么？</strong></p><span id="more"></span><h2 id="有关单例模式"><a href="#有关单例模式" class="headerlink" title="有关单例模式"></a>有关单例模式</h2><p>我们在网上搜如何实现单例模式时，帖子往往给出多种实现：饿汉模式、懒汉模式、双重锁懒汉模式（双重锁检查，double check lock，经常简写做DCL）、静态内部类模式、枚举模式等。</p><p>此处可以参考 <a href="https://blog.csdn.net/mnb65482/article/details/80458571">深入理解单例模式：静态内部类单例原理</a> 这篇文章。</p><p>顺带提一句，《Java并发编程的艺术》（方腾飞）第3章有讨论过DCL、静态内部类这两种实现方式，建议去读一读。</p><p>先说结论：<strong>DCL实现单例，必须加volatile!</strong></p><p>至于为何，首先我们需要理解volatile的语义。</p><h2 id="volatile的语义"><a href="#volatile的语义" class="headerlink" title="volatile的语义"></a>volatile的语义</h2><p>目前我们正在使用的Java版本中，对于<code>volatile</code>语义的定义，主要采用 JSR-133中的定义。</p><blockquote><p>Oracle JSR-133文档：<a href="https://download.oracle.com/otndocs/jcp/memory_model-1.0-pfd-spec-oth-JSpec/">英文原版</a> <a href="http://ifeve.com/jsr133-cn/">中文翻译版</a></p></blockquote><p>JSR-133在对JLS原始规范的改变中，有两处最有可能要求JVM实现也做出相应的变动：</p><blockquote><p>加强了volatile变量的语义，需要有acquire和release语义。在原始的规范中，volatile变量的访问和非volatile变量的访问之间可以自由地重排序。<br>加强了final字段的语义，无需显式地同步，不可变对象也是线程安全的。这可能需要在给final字段赋值的那些构造器的末尾加上store-store屏障。</p></blockquote><h3 id="volatile的内存语义"><a href="#volatile的内存语义" class="headerlink" title="volatile的内存语义"></a>volatile的内存语义</h3><p>CPU缓存级别</p><blockquote><p>happens-before对volatile规则的定义 : volatile变量的写，先发生于后续对这个变量的读.<br>这句话的含义有两层:<br>volatile 的写操作, 需要将线程本地内存值,立马刷新到 主内存的共享变量中.<br>volatile 的读操作, 需要从主内存的共享变量中读取,更新本地内存变量的值.<br>由此引出 volatile 的内存语义.<br>当写一个volatile变量时，JMM会把该线程对应的本地内存中的共享变量值刷新到主内存.<br>当读一个volatile变量时，JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量,并更新本地内存的值.</p></blockquote><p>注：引自<a href="https://www.jianshu.com/p/9e467de97216">https://www.jianshu.com/p/9e467de97216</a></p><h3 id="通俗一点"><a href="#通俗一点" class="headerlink" title="通俗一点"></a>通俗一点</h3><p>简单来讲，目前<code>volatile</code>关键字主要的作用是：</p><ul><li><ol><li>在多线程访问，保证被volatile修饰变量的可见性</li><li>禁止指令重排序</li></ol></li></ul><h3 id="分析DCL代码"><a href="#分析DCL代码" class="headerlink" title="分析DCL代码"></a>分析DCL代码</h3><p>一个典型的DCL代码如下（没加volatile、有问题的版本）：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">public class SingleTon&#123;</span><br><span class="line">    //没加volatile、有问题的版本</span><br><span class="line">  private static SingleTon  INSTANCE = null;</span><br><span class="line">  private SingleTon()&#123;&#125;</span><br><span class="line">  public static SingleTon getInstance()&#123;</span><br><span class="line">      if(INSTANCE == null)&#123;</span><br><span class="line">          synchronized(SingleTon.class)&#123;</span><br><span class="line">             if(INSTANCE == null)&#123; </span><br><span class="line">               INSTANCE = new SingleTon();</span><br><span class="line">             &#125; </span><br><span class="line">          &#125;</span><br><span class="line">          return INSTANCE; </span><br><span class="line">    &#125; </span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>有问题地方是这里：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">INSTANCE = new SingleTon();</span><br></pre></td></tr></table></figure><p>这条语句编译成字节码指令后，实际分下面3个步骤：</p><ol><li>在堆内存开辟内存空间。</li><li>在堆内存中实例化SingleTon里面的各个参数。</li><li>把对象指向堆内存空间。</li></ol><p>由于JVM存在乱序执行功能，所以可能线程A在2还没执行时就先执行了3，如果此时再被切换到线程B上，由于执行了3，INSTANCE 已经非空了，会被直接拿出来用，这样的话，就会出现异常。这个就是著名的DCL失效问题。</p><p>在JSR-133（JDK 1.5中实现）之后，官方也发现了这个问题，增强了volatile，只要定义为<code>private volatile static SingleTon INSTANCE = null;</code>就可解决DCL失效问题。volatile确保INSTANCE每次均在主内存中读取，这样虽然会牺牲一点效率，但也无伤大雅。</p><h2 id="volatile如何保证可见性并禁止指令重排序？"><a href="#volatile如何保证可见性并禁止指令重排序？" class="headerlink" title="volatile如何保证可见性并禁止指令重排序？"></a>volatile如何保证可见性并禁止指令重排序？</h2><h3 id="基础知识"><a href="#基础知识" class="headerlink" title="基础知识"></a>基础知识</h3><p>Java程序执行：</p><p><img src="/images/Java%E7%A8%8B%E5%BA%8F%E6%89%A7%E8%A1%8C-300x46_d27ae705.png"></p><p>JVM执行字节码时，可能发生指令重排序，乱序执行。上面不加volatile的DCL重排就是一个例子。</p><p>需要分多个层次来理解：</p><h4 id="1-在Java字节码层面"><a href="#1-在Java字节码层面" class="headerlink" title="1. 在Java字节码层面"></a>1. 在Java字节码层面</h4><p>字节码文件结构</p><p><img src="/images/class%E6%96%87%E4%BB%B6%E7%BB%93%E6%9E%84-300x297_bb4aebf5.png"><img src="vscode-resource://file///d:/GitRepository/JavaKnowledgeTree/java/images/class%E6%96%87%E4%BB%B6%E7%BB%93%E6%9E%84.jpg"></p><p>在字段表中保存着类中各种字段的信息</p><p><img src="vscode-resource://file///d:/GitRepository/JavaKnowledgeTree/java/images/class%E6%96%87%E4%BB%B6%E4%B8%AD%E7%9A%84%E5%AD%97%E6%AE%B5%E8%A1%A8%E4%B8%8E%E5%AD%97%E6%AE%B5%E8%AE%BF%E9%97%AE%E6%A0%87%E5%BF%97.png"><img src="/images/class%E6%96%87%E4%BB%B6%E4%B8%AD%E7%9A%84%E8%AE%BF%E9%97%AE%E6%A0%87%E8%AE%B0-300x159_7c65ba27.png"></p><p><img src="/images/class%E6%96%87%E4%BB%B6%E4%B8%AD%E7%9A%84%E5%AD%97%E6%AE%B5%E8%A1%A8%E4%B8%8E%E5%AD%97%E6%AE%B5%E8%AE%BF%E9%97%AE%E6%A0%87%E5%BF%97-300x221_60021e35.png"></p><p>当字段被volatile修饰时，字段表中对应字段的访问标志将会加上<code>ACC_VOLATILE</code>。</p><h4 id="2-JVM的内存屏障"><a href="#2-JVM的内存屏障" class="headerlink" title="2. JVM的内存屏障"></a>2. JVM的内存屏障</h4><h4 id="as-if-serial与happen-before"><a href="#as-if-serial与happen-before" class="headerlink" title="as-if-serial与happen-before"></a>as-if-serial与happen-before</h4><p>虽然会乱序执行，但会遵循as-if-serial语义和happen-before原则。</p><ul><li>as-if-serial : 不管怎么重排序（编译器和处理器为了提高并行度），（单线程）程序的执行结果不会改变。</li><li>happen-before :<ul><li>与程序员密切相关的happens-before规则如下：<br>1、程序顺序规则：一个线程中的每个操作，happens-before于线程中的任意后续操作。<br>2、监视器锁规则：一个锁的解锁，happens-before于随后对这个锁的加锁。<br>3、volatile变量规则：对一个volatile域的写，happens-before于任意后续对这个volatile域的读。<br>4、传递性：如果A happens-before B，且B happens-before C，那么A happens-before C。</li></ul></li></ul><p>可以参考 <a href="https://blog.csdn.net/u010571316/article/details/64906481">happens-before规则和as-if-serial语义</a> 。不用记各种规则，理解即可。</p><p>编译器的重排序是指，在不改变单线程程序语义的前提下，可以重新安排语句的执行顺序来优化程序的性能.</p><p>编译器的重排序和CPU的重排序的原则一样，会遵守数据依赖性原则，编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序</p><p>编译器、处理器都必须遵守这个语义。JMM层面的内存屏障为了保证内存可见性，Java编译器在生成指令序列的适当位置会插入<strong>内存屏障</strong>来禁止特定类型的处理器的重排序。</p><p>JVM中内存屏障有以下4种：</p><ol><li>LoadLoad屏障：对于这样的语句Load1; LoadLoad; Load2，<br>在Load2及后续读取操作要读取的数据被访问前，保证Load1要读取的数据被读取完毕。</li><li>StoreStore屏障：对于这样的语句Store1; StoreStore; Store2，<br>在Store2及后续写入操作执行前，保证Store1的写入操作对其它处理器可见。</li><li>LoadStore屏障：对于这样的语句Load1; LoadStore; Store2，<br>在Store2及后续写入操作被刷出前，保证Load1要读取的数据被读取完毕。</li><li>StoreLoad屏障：对于这样的语句Store1; StoreLoad; Load2，<br>在Load2及后续所有读取操作执行前，保证Store1的写入对所有处理器可见。</li></ol><p>参考<a href="https://blog.csdn.net/weixin_42008012/article/details/105910949">JVM系列(三)[计算机硬件的内存模型,数据一致性问题,CPU指令乱序执行,合并写]</a></p><h4 id="3-Hotspot源码实现"><a href="#3-Hotspot源码实现" class="headerlink" title="3. Hotspot源码实现"></a>3. Hotspot源码实现</h4><p>JVM执行字节码的过程，最终会翻译成机器语言，在CPU上执行。</p><p>对于JVM内存屏障的实现，不同CPU有不同的实现。以X86为例，下面是个栗子：</p><p><img src="vscode-resource://file///d:/GitRepository/JavaKnowledgeTree/java/images/volatile%E5%AF%B9%E5%BA%94%E6%B1%87%E7%BC%96%E6%8C%87%E4%BB%A4.png"><img src="/images/volatile%E5%AF%B9%E5%BA%94%E6%B1%87%E7%BC%96%E6%8C%87%E4%BB%A4-300x90_9540b546.png"></p><p>有volatile变量修饰的共享变量进行写操作的时候会多第二行汇编代码，通过查IA-32架构软件开发者手册可知，lock前缀的指令在多核处理器下会引发了两件事情：</p><ul><li>将当前处理器缓存行的数据会写回到系统内存。</li><li>这个写回内存的操作会引起在其他CPU里缓存了该内存地址的数据无效。</li></ul><p>也就是说用<code>lock</code>指令来保证CPU中缓存的可见性。</p><blockquote><p>在多处理器环境中，LOCK# 信号确保在声言该信号期间，处理器可以独占使用任何共享内存。（因为它会锁住总线，导致其他CPU不能访问总线，不能访问总线就意味着不能访问系统内存），但是在最近的处理器里，LOCK＃信号一般不锁总线，而是锁缓存，毕竟锁总线开销比较大。</p><p>对于Intel486和Pentium处理器，在锁操作时，总是在总线上声言LOCK#信号。但在P6和最近的处理器中，如果访问的内存区域已经缓存在处理器内部，则不会声言LOCK#信号。相反地，它会锁定这块内存区域的缓存并回写到内存，并使用缓存一致性机制来确保修改的原子性，此操作被称为“缓存锁定”，缓存一致性机制会阻止同时修改被两个以上处理器缓存的内存区域数据 。</p><p>一个处理器的缓存回写到内存会导致其他处理器的缓存无效 。IA-32处理器和Intel 64处理器使用MESI（修改，独占，共享，无效）控制协议去维护内部缓存和其他处理器缓存的一致性。在多核处理器系统中进行操作的时候，IA-32 和Intel 64处理器能嗅探其他处理器访问系统内存和它们的内部缓存。它们使用嗅探技术保证它的内部缓存，系统内存和其他处理器的缓存的数据在总线上保持一致。例如在Pentium和P6 family处理器中，如果通过嗅探一个处理器来检测其他处理器打算写内存地址，而这个地址当前处理共享状态，那么正在嗅探的处理器将无效它的缓存行，在下次访问相同内存地址时，强制执行缓存行填充。</p></blockquote><p>上述内容参考自：<a href="https://www.cnblogs.com/awkflf11/p/9218414.html">并发之volatile底层原理</a></p><p>总结一下：<strong>LOCK 用于在多处理器中执行指令时对共享内存的独占使用。它的作用是能够将当前处理器对应缓存的内容刷新到内存，并使其他处理器对应的缓存失效。另外还提供了有序的指令无法越过这个内存屏障的作用。</strong></p><p>Hotspot C++源码分析可以参考这篇文章<a href="https://www.jianshu.com/p/506c1e38a922">面试必问的volatile，你了解多少？</a></p><h5 id="3-1-扩展1：缓存行（cache-line）"><a href="#3-1-扩展1：缓存行（cache-line）" class="headerlink" title="3.1 扩展1：缓存行（cache line）"></a>3.1 扩展1：缓存行（cache line）</h5><p>参考 <a href="https://www.cnblogs.com/awkflf11/p/9218414.html">并发之volatile底层原理</a></p><h5 id="3-2-扩展2：缓存一致性协议"><a href="#3-2-扩展2：缓存一致性协议" class="headerlink" title="3.2 扩展2：缓存一致性协议"></a>3.2 扩展2：缓存一致性协议</h5><p>参考这篇文章 <a href="https://www.cnblogs.com/z00377750/p/9180644.html">【并发编程】MESI–CPU缓存一致性协议</a></p><h1 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h1><ul><li><a href="https://blog.csdn.net/mnb65482/article/details/80458571">深入理解单例模式：静态内部类单例原理</a></li><li><a href="https://www.jianshu.com/p/9e467de97216">java并发(4)深入理解volatile</a></li><li><a href="https://www.cnblogs.com/awkflf11/p/9218414.html">并发之volatile底层原理</a></li><li><a href="https://www.jianshu.com/p/506c1e38a922">面试必问的volatile，你了解多少？</a></li><li><a href="https://www.jianshu.com/p/d2641fdf5d44">架构师五分钟带你读懂，Volatile的作用及原理</a></li><li><a href="https://blog.csdn.net/u010571316/article/details/64906481">happens-before规则和as-if-serial语义</a></li><li><a href="https://www.cnblogs.com/awkflf11/p/9218414.html">并发之volatile底层原理</a></li><li><a href="https://www.cnblogs.com/z00377750/p/9180644.html">【并发编程】MESI–CPU缓存一致性协议</a></li></ul>]]>
    </content>
    <id>https://evasnowind.github.io/2020/05/26/2020-05-27_Java%E5%B9%B6%E5%8F%91%E4%B9%8B%E6%B7%B1%E5%85%A5%E8%A7%A3%E6%9E%90volatile%E5%85%B3%E9%94%AE%E5%AD%97/</id>
    <link href="https://evasnowind.github.io/2020/05/26/2020-05-27_Java%E5%B9%B6%E5%8F%91%E4%B9%8B%E6%B7%B1%E5%85%A5%E8%A7%A3%E6%9E%90volatile%E5%85%B3%E9%94%AE%E5%AD%97/"/>
    <published>2020-05-26T16:00:00.000Z</published>
    <summary>从一道面试题讲起：采用DCL实现单例模式时，是否需要加volatile关键字？为什么？</summary>
    <title>Java并发之深入解析volatile关键字</title>
    <updated>2026-09-04T04:44:28.355Z</updated>
  </entry>
  <entry>
    <author>
      <name>Prayer</name>
    </author>
    <category term="Java" scheme="https://evasnowind.github.io/categories/Java/"/>
    <category term="Java" scheme="https://evasnowind.github.io/tags/Java/"/>
    <content>
      <![CDATA[<p><img src="vscode-resource://file///d:/GitRepository/JavaKnowledgeTree/java/images/JDK8%E4%BE%9D%E8%B5%96%E5%90%84%E4%B8%AA%E7%89%88%E6%9C%AC%E6%9B%B4%E6%96%B0%E5%86%85%E5%AE%B9.jpg">JDK 1.5</p><p>很重要的一个版本。</p><ul><li>更新了Java内存模型（JMM）<ul><li>增强volatile语义</li><li>增强final语义</li></ul></li><li>泛型</li><li>For-Each循环</li><li>注解</li><li>枚举</li><li>JUC包</li><li>lock接口</li></ul><span id="more"></span><h2 id="JDK-1-6"><a href="#JDK-1-6" class="headerlink" title="JDK 1.6"></a>JDK 1.6</h2><ul><li>Desktop类和SystemTray类</li><li>JAXB2实现对象与XML之间的映射</li><li>StAX</li><li>Compiler API</li><li>轻量级 Http Server API</li><li>插入式注解处理API（Pluggable Annotation Processing API）</li><li>用Console开发控制台程序</li><li>对脚本语言的支持（如：ruby，groovy，javascript）</li><li>Common Annotations</li></ul><p>并发相关：sychronized锁优化：偏向锁、轻量级锁、重量级锁</p><h2 id="JDK-1-7"><a href="#JDK-1-7" class="headerlink" title="JDK 1.7"></a>JDK 1.7</h2><ul><li>二进制字面量</li><li>数字字面量可以出现下划线</li><li>switch 语句可以用字符串</li><li>泛型实例的创建可以通过类型推断来简化</li><li>try-with-resources 语句</li><li>异常处理（捕获多个异常）</li><li>JSR203 NIO2.0（AIO）新IO的支持</li><li>JSR292与InvokeDynamic指令</li><li>Path接口、DirectoryStream、Files、WatchService（重要接口更新）</li><li>fork&#x2F;join framework</li></ul><p><strong>JDK 8之后的更新内容，强烈推荐这篇文章：</strong></p><p><a href="https://juejin.im/post/5d5950806fb9a06b0a277412">聊聊 Java8 以后各个版本的新特性</a></p><h2 id="JDK-1-8"><a href="#JDK-1-8" class="headerlink" title="JDK 1.8"></a>JDK 1.8</h2><ul><li>Lambda</li><li>Stream</li><li>Optional</li><li>Date&#x2F;Time API (JSR 310)</li><li>JVM的PermGen空间被移除：取代它的是Metaspace（JEP 122）。</li></ul><h2 id="JDK-9"><a href="#JDK-9" class="headerlink" title="JDK 9"></a>JDK 9</h2><ul><li>平台级modularity（原名：Jigsaw） 模块化系统</li><li>Java 的 REPL 工具： jShell 命令</li><li>多版本兼容 jar 包（这个在处理向下兼容方面，非常好用）</li><li>语法改进：接口的私有方法</li><li>语法改进：UnderScore(下划线)使用的限制</li><li>底层结构：<strong>String 存储结构变更（这个很重要）</strong></li><li>集合工厂方法：快速创建只读集合</li><li>增强的 Stream API</li><li>全新的 HTTP 客户端 API<ul><li>HTTP&#x2F;2, WebSocket</li></ul></li><li>其它特性</li></ul><p>JVM相关：<br>G1是JDK 9默认GC。</p><h2 id="JDK-10"><a href="#JDK-10" class="headerlink" title="JDK 10"></a>JDK 10</h2><ul><li>局部变量的类型推断 var关键字</li><li>GC改进和内存管理 并行全垃圾回收器 G1</li><li>垃圾回收器接口</li><li>线程-局部变量管控</li><li>合并 JDK 多个代码仓库到一个单独的储存库中</li><li>新增API：ByteArrayOutputStream</li><li>List、Map、Set新增API：copyOf(Collection）</li><li>新增API：java.util.Properties</li><li>新增API： Collectors收集器</li><li>其它特性</li></ul><p>JVM相关：</p><ul><li>The default value for <code>BiasedLockingStartupDelay</code> has been changed to 0. （原本默认值是4000，即4s后启动偏向锁。该值貌似一直有调整，在JDK 1.6.0 update 12中，是500，参见<a href="https://www.oracle.com/technetwork/java/javase/itanium6u12-137891.html">https://www.oracle.com/technetwork/java/javase/itanium6u12-137891.html</a> ） 即：JVM启动时延时启用偏向锁改为默认启动。参考<a href="https://www.oracle.com/java/technologies/javase/10-relnote-issues.html">JDK 10 Release Notes</a></li></ul><h2 id="JDK-11"><a href="#JDK-11" class="headerlink" title="JDK 11"></a>JDK 11</h2><p>2018年9月26日，Oracle 官方宣布 Java 11 正式发布。这是 Java 大版本周期变化后的第一个长期支持版本（LTS版本，Long-Term-Support，持续支持到2026年9月），非常值得关注。</p><ul><li>本地变量类型推断</li><li>字符串加强</li><li>集合加强</li><li>Stream 加强</li><li>Optional 加强</li><li>InputStream 加强</li><li>HTTP Client API</li><li>化繁为简，一个命令编译运行源代码</li></ul><p>JVM相关：<br>引入了实验性的<strong>ZGC</strong></p><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><ul><li><a href="https://blog.csdn.net/qq_27256783/article/details/98869743">java各个版本新特性介绍</a></li><li><a href="https://juejin.im/post/5d5950806fb9a06b0a277412">聊聊 Java8 以后各个版本的新特性</a></li></ul>]]>
    </content>
    <id>https://evasnowind.github.io/2020/05/17/2020-05-18_JDK%E5%90%84%E4%B8%AA%E7%89%88%E6%9C%AC%E7%89%B9%E6%80%A7%E9%80%9F%E8%A7%88/</id>
    <link href="https://evasnowind.github.io/2020/05/17/2020-05-18_JDK%E5%90%84%E4%B8%AA%E7%89%88%E6%9C%AC%E7%89%B9%E6%80%A7%E9%80%9F%E8%A7%88/"/>
    <published>2020-05-17T16:00:00.000Z</published>
    <summary>并发相关：sychronized锁优化：偏向锁、轻量级锁、重量级锁。</summary>
    <title>JDK各个版本特性速览</title>
    <updated>2026-09-04T04:44:28.355Z</updated>
  </entry>
</feed>
