<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>凝雨 - Yun</title>
  <icon>https://www.gravatar.com/avatar/34adc69e060ecff8415f82e36ab6a5a8</icon>
  <subtitle>快乐编程每一天 - Happy Coding Every Day</subtitle>
  <link href="/atom.xml" rel="self"/>
  
  <link href="https://ningyu1.github.io/"/>
  <updated>2024-04-23T01:48:15.039Z</updated>
  <id>https://ningyu1.github.io/</id>
  
  <author>
    <name>凝雨</name>
    <email>ningbe111@163.com</email>
  </author>
  
  <generator uri="http://hexo.io/">Hexo</generator>
  
  <entry>
    <title>解决安装Homebrew过程遇到的问题</title>
    <link href="https://ningyu1.github.io/20210118/homebrew-install.html"/>
    <id>https://ningyu1.github.io/20210118/homebrew-install.html</id>
    <published>2021-01-18T06:07:26.000Z</published>
    <updated>2024-04-23T01:48:15.039Z</updated>
    
    <content type="html"><![CDATA[<p>本篇主要讲解在国内安装Homebrew遇到的各种错误如何解决，让你可以轻松安装Homebrew，用mac的同学应该对Homebrew并不陌生，这里简单介绍一下Homebrew</p><h1 id="Homebrew简介"><a href="#Homebrew简介" class="headerlink" title="Homebrew简介"></a>Homebrew简介</h1><p>引用 <a href="https://brew.sh/" target="_blank" rel="noopener">官方</a> 的一句话：Homebrew是Mac OS 不可或缺的套件管理器。</p><p>Homebrew是一款Mac OS平台下的软件包管理工具，拥有安装、卸载、更新、查看、搜索等很多实用的功能。简单的一条指令，就可以实现包管理，而不用你关心各种依赖和文件路径的情况，十分方便快捷。</p><p>所以它是Mac必备神器Homebrew。</p><h1 id="安装时遇到的错误一"><a href="#安装时遇到的错误一" class="headerlink" title="安装时遇到的错误一"></a>安装时遇到的错误一</h1><p>安装的命令很简单如下<br><figure class="highlight shell"><figcaption><span>script</span></figcaption><table><tr><td class="code"><pre><span class="line">/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"</span><br></pre></td></tr></table></figure></p><p>当你在国内运行上面命令时会遇到下面的错误</p><figure class="highlight shell"><figcaption><span>script</span></figcaption><table><tr><td class="code"><pre><span class="line">curl: (7) Failed to connect to raw.githubusercontent.com port 443: Connection refused</span><br></pre></td></tr></table></figure><p>这是因为被墙了，如果你是科学上网的话，就使用你上网的神器来代理，这块我想说的就是使用代理收费的肯定比免费的要好一些，如果可以的话花钱买一个，毕竟天天都要使用。</p><p>那么如何代理？使用下面命令</p><p>假设我的代理http端口是：1087，socket是：869</p><figure class="highlight shell"><figcaption><span>script</span></figcaption><table><tr><td class="code"><pre><span class="line">export https_proxy=http://127.0.0.1:1087 http_proxy=http://127.0.0.1:1087 all_proxy=socks5://127.0.0.1:869</span><br></pre></td></tr></table></figure><p>ps. 将端口替换成你自己的代理端口，输入上面的命令代理只会在本次打开的<code>terminal</code>生效。</p><p>设置完代理之后再运行安装命令就不会看到无法连接raw.githubusercontent.com，紧接着提示需要你输入电脑密码，输入密码后它会提示你如下信息：</p><figure class="highlight shell"><figcaption><span>script</span></figcaption><table><tr><td class="code"><pre><span class="line">==&gt; This script will install:</span><br><span class="line">/usr/local/bin/brew</span><br><span class="line">/usr/local/share/doc/homebrew</span><br><span class="line">/usr/local/share/man/man1/brew.1</span><br><span class="line">/usr/local/share/zsh/site-functions/_brew</span><br><span class="line">/usr/local/etc/bash_completion.d/brew</span><br><span class="line">/usr/local/Homebrew</span><br><span class="line"></span><br><span class="line">Press RETURN to continue or any other key to abort</span><br></pre></td></tr></table></figure><p>输入回车直接安装即可。</p><h1 id="安装时遇到的错误二"><a href="#安装时遇到的错误二" class="headerlink" title="安装时遇到的错误二"></a>安装时遇到的错误二</h1><p>这里需要注意一个问题，它会去github上下载Homebrew代码进行安装。代码现在完后安装他会卡在某一个地方，比如说我这里卡在</p><figure class="highlight shell"><figcaption><span>script</span></figcaption><table><tr><td class="code"><pre><span class="line">==&gt; /usr/bin/sudo /usr/sbin/chown jiuye:admin /usr/local/Homebrew</span><br><span class="line">==&gt; Downloading and installing Homebrew...</span><br><span class="line">HEAD is now at 48e44870e Merge pull request #10334 from SeekingMeaning/rubocop-spec-descriptions</span><br></pre></td></tr></table></figure><p>长时间卡在这里不动，我直接<code>Control + C</code>退出，因为源码已经下载安装完成，其余的不知道为啥卡在这里，但是它已经安装成功了。</p><p>退出后运行<code>brew -v</code>测试一下是否可用，如果可以使用就证明安装成功。</p><h1 id="使用brew安装工具包遇到的错误三"><a href="#使用brew安装工具包遇到的错误三" class="headerlink" title="使用brew安装工具包遇到的错误三"></a>使用brew安装工具包遇到的错误三</h1><p>比如说我们安装wget的时候会遇到如下问题，它会长时间卡如下地方</p><figure class="highlight shell"><figcaption><span>script</span></figcaption><table><tr><td class="code"><pre><span class="line">Updating Homebrew...</span><br></pre></td></tr></table></figure><p>如果长时间卡在升级homebrew的话，我们可以尝试<code>Control + C</code>退出homebrew升级，这个时候它会跳过升级直接安装我们需要的包直到安装结束，如下所示：</p><figure class="highlight shell"><figcaption><span>script</span></figcaption><table><tr><td class="code"><pre><span class="line">MacBook-Pro:~ jiuye$ brew install wget</span><br><span class="line">==&gt; Downloading https://homebrew.bintray.com/bottles-portable-ruby/portable-ruby-2.6.3_2.yosemite.bottle.tar.gz</span><br><span class="line"><span class="meta">#</span>####################################################################### 100.0%</span><br><span class="line">==&gt; Pouring portable-ruby-2.6.3_2.yosemite.bottle.tar.gz</span><br><span class="line">Updating Homebrew...</span><br><span class="line"></span><br><span class="line">^C==&gt; Downloading https://homebrew.bintray.com/bottles/gettext-0.20.2_1.mojave.bot</span><br><span class="line">==&gt; Downloading from https://d29vzk4ow07wi7.cloudfront.net/52067198cab528f05fdc0</span><br><span class="line"><span class="meta">#</span>####################################################################### 100.0%</span><br><span class="line">==&gt; Downloading https://homebrew.bintray.com/bottles/libunistring-0.9.10.mojave.</span><br><span class="line">==&gt; Downloading from https://d29vzk4ow07wi7.cloudfront.net/1d0c8e266acddcebeef3d</span><br><span class="line"><span class="meta">#</span>####################################################################### 100.0%</span><br></pre></td></tr></table></figure><p>到目前为止我们就成功的安装和测试了Homebrew，本次主要收录我在使用过程中遇到的问题，如果后续安装其他包遇到其他问题的话，我也会持续的更新它收录在一起。</p><h1 id="特殊说明"><a href="#特殊说明" class="headerlink" title="特殊说明"></a>特殊说明</h1><p>到最后要说明一点，Homebrew在安装软件包的时候，它会将软件包安装到独立目录，并将其文件软链接至 /usr/local ，通过<code>ls -l</code>查看</p><figure class="highlight shell"><figcaption><span>script</span></figcaption><table><tr><td class="code"><pre><span class="line">ls -l /usr/local/bin/</span><br><span class="line"></span><br><span class="line">lrwxr-xr-x  1 xxx  xxx        32  1 18 13:33 wget -&gt; ../Cellar/wget/1.20.3_2/bin/wget</span><br><span class="line">-rwxr-xr-x  1 xxx  xxx       123  3  7  2019 wish</span><br><span class="line">-rwxr-xr-x  1 xxx  xxx       123  3  7  2019 wish8.6</span><br><span class="line">lrwxr-xr-x  1 xxx  xxx        39  1 18 13:33 xgettext -&gt; ../Cellar/gettext/0.20.2_1/bin/xgettext</span><br></pre></td></tr></table></figure>]]></content>
    
    <summary type="html">
    
      在国内安装Homebrew的时候经常会遇到各种错误，本篇就是解决所有问题，让你可以成功安装Homebrew
    
    </summary>
    
      <category term="tool" scheme="https://ningyu1.github.io/categories/tool/"/>
    
      <category term="Homebrew" scheme="https://ningyu1.github.io/categories/tool/Homebrew/"/>
    
    
      <category term="Homebrew" scheme="https://ningyu1.github.io/tags/Homebrew/"/>
    
      <category term="trouble shooting" scheme="https://ningyu1.github.io/tags/trouble-shooting/"/>
    
  </entry>
  
  <entry>
    <title>讲一讲加密数据如何进行模糊查询</title>
    <link href="https://ningyu1.github.io/20201230/encrypted-data-fuzzy-query.html"/>
    <id>https://ningyu1.github.io/20201230/encrypted-data-fuzzy-query.html</id>
    <published>2020-12-30T08:28:10.000Z</published>
    <updated>2024-04-23T01:48:15.039Z</updated>
    
    <content type="html"><![CDATA[<p>在上一篇<a href="https://ningyu1.github.io/20201229/datasource-security.html">讲一讲数据安全，如何有效预防脱库</a> 中我们提到了加密后的数据对模糊查询不是很友好，本篇就针对加密数据模糊查询这个问题来展开讲一讲实现的思路。</p><p>为了数据安全我们在开发过程中经常会对重要的数据进行加密存储，常见的有：密码、手机号、电话号码、详细地址、银行卡号、信用卡验证码等信息，这些信息对加解密的要求也不一样，比如说密码我们需要加密存储，一般使用的都是不可逆的慢<code>hash</code>算法，慢<code>hash</code>算法可以避免暴力破解（典型的用时间换安全性），在检索时我们既不需要解密也不需要模糊查找，直接使用密文完全匹配，但是手机号就不能这样做，因为手机号我们要查看原信息，并且对手机号还需要支持模糊查找，因此我们今天就针对可逆加解密的数据支持模糊查询来看看有哪些实现方式。</p><p>在网上随便搜索了一下，关于<a href="https://www.google.com/search?newwindow=1&amp;source=hp&amp;ei=YvXrX9LPHOeLr7wPydul0Ac&amp;q=%E5%8A%A0%E5%AF%86%E5%90%8E%E7%9A%84%E6%A8%A1%E7%B3%8A%E6%9F%A5%E8%AF%A2&amp;oq=%E5%8A%A0%E5%AF%86%E5%90%8E%E7%9A%84%E6%A8%A1%E7%B3%8A%E6%9F%A5%E8%AF%A2&amp;gs_lcp=CgZwc3ktYWIQAzoFCAAQsQM6AggAOggIABCxAxCDAToECAAQAzoECAAQDDoECAAQHjoGCAAQBRAeULQTWJQsYPAsaAFwAHgAgAHwAYgBuxqSAQYxLjIyLjGYAQCgAQGqAQdnd3Mtd2l6sAEA&amp;sclient=psy-ab&amp;ved=0ahUKEwjSiJeI4_TtAhXnxYsBHcltCXoQ4dUDCAc&amp;uact=5" target="_blank" rel="noopener">《加密后的模糊查询》</a> 的帖子很多，顺便整理了一下实现的方法，不得不说很多都是不靠谱的做法，甚至有一些沙雕做法，接下来我们就对这些做法来讲讲实现思路和优劣性。</p><h1 id="如何对加密后的数据进行模糊查询"><a href="#如何对加密后的数据进行模糊查询" class="headerlink" title="如何对加密后的数据进行模糊查询"></a>如何对加密后的数据进行模糊查询</h1><p>我整理了一下对加密的数据模糊查询大致分为三类做法，如下所示：</p><ul><li>沙雕做法（不动脑思考直男的思路，只管实现功能从不深入思考问题）</li><li>常规做法（思考了查询性能问题，也会使用一些存储空间换性能等做法）</li><li>超神做法（比较高端的做法从算法层面上思考）</li></ul><p>我们就对这三种实现方法一一来讲讲实现思路和优劣性，首先我们先看沙雕做法。</p><h2 id="沙雕做法"><a href="#沙雕做法" class="headerlink" title="沙雕做法"></a>沙雕做法</h2><ul><li>将所有数据加载到内存中进行解密，解密后通过程序算法来模糊匹配</li><li>将密文数据映射一份明文映射表，俗称<code>tag</code>表，然后模糊查询<code>tag</code>来关联密文数据</li></ul><h3 id="沙雕一"><a href="#沙雕一" class="headerlink" title="沙雕一"></a>沙雕一</h3><p>我们先来看看第一个做法，将所有数据加载到内存中进行解密，这个如果数据量小的话可以使用这个方式来做，这样做既简单又实惠，如果数据量大的话那就是灾难，我们来大致算一下。</p><p>一个英文字母(不分大小写)占一个字节的空间，一个中文汉字占两个字节的空间，用<code>DES</code>来举例，<code>13800138000</code>加密后的串<code>HE9T75xNx6c5yLmS5l4r6Q==</code>占<code>24</code>个字节。</p><table><thead><tr><th>条数</th><th>Bytes</th><th>MB</th></tr></thead><tbody><tr><td>100w</td><td>2400万</td><td>22.89</td></tr><tr><td>1000w</td><td>2.4亿</td><td>228.89</td></tr><tr><td>1亿</td><td>24亿</td><td>2288.89</td></tr></tbody></table><p>轻则上百兆，重则上千兆，这样分分钟给应用程序整成<code>Out of memory</code>，这样做如果数据少只有几百、几千、几万条时是完全可以这样做的，但是数据量大就强烈不建议了。</p><h3 id="沙雕二"><a href="#沙雕二" class="headerlink" title="沙雕二"></a>沙雕二</h3><p>我们再来看第二个做法，将密文数据映射一份明文映射表，然后模糊查询映射表来关联密文数据，<code>what？？？！！！</code>那我们为什么要对数据加密呢，直接不加密不是更好么！</p><p>我们既然对数据加密肯定是有安全诉求才会这样做，增加一个明文的映射表就违背了安全诉求，这样做既不安全也不方便完全是脱裤子放x，多此一举，强且不推荐。</p><h2 id="常规做法"><a href="#常规做法" class="headerlink" title="常规做法"></a>常规做法</h2><p>我们接下来看看常规的做法，也是最广泛使用的方法，此类方法及满足的数据安全性，又对查询友好。</p><ul><li>在数据库实现加密算法函数，在模糊查询的时候使用<code>decode(key) like &#39;%partial%</code></li><li>对密文数据进行分词组合，将分词组合的结果集分别进行加密，然后存储到扩展列，查询时通过<code>key like &#39;%partial%&#39;</code></li></ul><h3 id="常规一"><a href="#常规一" class="headerlink" title="常规一"></a>常规一</h3><p>在数据库中实现与程序一致的加解密算法，修改模糊查询条件，使用数据库加解密函数先解密再模糊查找，这样做的优点是实现成本低，开发使用成本低，只需要将以往的模糊查找稍微修改一下就可以实现，但是缺点也很明显，这样做无法利用数据库的索引来优化查询，甚至有一些数据库可能无法保证与程序实现一致的加解密算法，但是对于常规的加解密算法都可以保证与应用程序一致。</p><p>如果对查询性能要求不是特别高、对数据安全性要求一般，可以使用常见的加解密算法比如说<code>AES</code>、<code>DES</code>之类的也是一个不错的选择。</p><p>如果公司有自己的算法实现，并且没有提供多端的算法实现，要么找个算法好的人去研究吃透补全多端实现，要么放弃使用这个办法。</p><h3 id="常规二"><a href="#常规二" class="headerlink" title="常规二"></a>常规二</h3><p>对密文数据进行分词组合，将分词组合的结果集分别进行加密，然后存储到扩展列，查询时通过<code>key like &#39;%partial%&#39;</code>，这是一个比较划算的实现方法，我们先来分析一下它的实现思路。</p><p>先对字符进行固定长度的分组，将一个字段拆分为多个，比如说根据4位英文字符（半角），2个中文字符（全角）为一个检索条件，举个例子：</p><p><code>ningyu1</code>使用4个字符为一组的加密方式，第一组<code>ning</code> ，第二组<code>ingy</code> ，第三组<code>ngyu</code> ，第四组<code>gyu1</code> … 依次类推。</p><p>如果需要检索所有包含检索条件4个字符的数据比如：<code>ingy</code> ，加密字符后通过 <code>key like “%partial%”</code> 查库。 </p><p>我们都知道加密后长度会增长，增长的这部分长度存储就是我们要花费的额外成本，典型的使用成本来换取速度，密文增长的幅度随着算法不同而不同以<code>DES</code>举例，<code>13800138000</code>加密前占<code>11</code>个字节，加密后的串<code>HE9T75xNx6c5yLmS5l4r6Q==</code>占<code>24</code>个字节，增长是<code>2.18</code>倍，所以一个优秀的算法是多么的重要，能为公司节省不少成本，但是话又说回来算法工程师的工资也不低，所以我也不知道是节省成本还是增加成本，哈哈哈…你们自己算吧。</p><p>回到主题，这个方法虽然可以实现加密数据的模糊查询，但是对模糊查询的字符长度是有要求的，以我上面举的例子模糊查询字符原文长度必须大于等于4个英文/数字，或者2个汉字，再短的长度不建议支持，因为分词组合会增多从而导致存储的成本增加，反而安全性降低。</p><p>大家是否都对接过 淘宝、拼多多、JD他们的api，他们对平台订单数据中的用户敏感数据就是加密的同时支持模糊查询，使用就是这个方法，下面我整理了几家电商平台的密文字段检索方案的说明，感兴趣的可以查看下面链接。</p><p><a href="https://open.taobao.com/docV3.htm?docId=106213&amp;docType=1" target="_blank" rel="noopener">淘宝密文字段检索方案</a><br><a href="https://jaq-doc.alibaba.com/docs/doc.htm?treeId=1&amp;articleId=106213&amp;docType=1" target="_blank" rel="noopener">阿里巴巴文字段检索方案</a><br><a href="https://open.pinduoduo.com/application/document/browse?idStr=3407B605226E77F2" target="_blank" rel="noopener">拼多多密文字段检索方案</a><br><a href="https://jos.jd.com/commondoc?listId=345" target="_blank" rel="noopener">京东密文字段检索方案</a></p><p>ps. 基本上都是一样的，果然都是互相抄袭，连加密后的数据格式都一致。</p><p>这个方法优点就是实现起来不算复杂，使用起来也较为简单，算是一个折中的做法，因为会有扩展字段存储成本会有升高，但是可利用数据库索引优化查询速度，推荐使用这个方法。</p><h2 id="超神做法"><a href="#超神做法" class="headerlink" title="超神做法"></a>超神做法</h2><p>我们接下来看看优秀的做法，此类做法难度较高，都是从算法层面来考虑，有些甚至会设计一个新算法，虽然已有一些现成的算法参考，但是大多都是半成品无法拿来直接使用，所以还是要有人去深入研究和整合到自己的应用中去。</p><ul><li>从算法层面思考，甚至会设计一个新算法来支持模糊查找</li></ul><p>这个层面大多是专业算法工程师的研究领域，想要设计一个有序的、非不可逆的、密文长度不能增长过快的算法不是一件简单的事情，大致的思路是这样的，使用译码的方式进行加解密，保留密文和原文一样的顺序，从而支持密文模糊匹配，说的比较笼统因为我也不是这方面的专家没有更深一步的研究过，所以我从网上找了一些资料可以参考一下。</p><ul><li><a href="https://www.jiamisoft.com/blog/6542-zifushujumohupipeijiamifangfa.html" target="_blank" rel="noopener">数据库中字符数据的模糊匹配加密方法</a></li></ul><p>这里提到的<code>Hill</code>密码处理和模糊匹配加密方法<code>FMES</code>可以重点看看.</p><ul><li><p><a href="http://kzyjc.cnjournals.com/html/2019/1/20190112.htm" target="_blank" rel="noopener">一种基于BloomFilter的改进型加密文本模糊搜索机制研究</a></p></li><li><p><a href="https://www.jiamisoft.com/blog/5961-kuaisuchaxunshujukujiami.html" target="_blank" rel="noopener">支持快速查询的数据库如何加密</a></p></li><li><p><a href="https://www.cnblogs.com/arthurqin/p/6307153.html" target="_blank" rel="noopener">基于Lucene的云端搜索与密文基础上的模糊查询</a></p></li></ul><p>基于Lucene的思路就跟我们上面介绍的<a href="#常规二">常规做法二</a>类似，对字符进行等长度分词，将分词后的结果集加密后存储，只不过存储的<code>db</code>不一样，一个是关系型数据库，一个是<code>es</code>搜索引擎。</p><ul><li><a href="http://jeit.ie.ac.cn/fileDZYXXXB/journal/article/dzyxxxb/2017/7/PDF/160971.pdf" target="_blank" rel="noopener">云存储中一种支持可验证的模糊查询加密方案</a></li></ul><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>我们到这里对加密数据的检索方案全部介绍完了，我们首先提到的是网上搜索随处可见的沙雕做法，在这里也讲了不推荐使用这些沙雕做法，尽量使用常规做法，如果公司有专业算法方向人才的话不妨可以考虑基于算法层面的超神做法。</p><p>总的来说从投入、产出比、及实现、使用成本来算的话<a href="#常规二">常规做法二</a>是非常推荐的。</p><p>最后送上那句老话，Keep Real，Love and peace！</p>]]></content>
    
    <summary type="html">
    
      讲一讲加密数据如何进行模糊查询
    
    </summary>
    
      <category term="security" scheme="https://ningyu1.github.io/categories/security/"/>
    
    
      <category term="encrypt" scheme="https://ningyu1.github.io/tags/encrypt/"/>
    
      <category term="security" scheme="https://ningyu1.github.io/tags/security/"/>
    
  </entry>
  
  <entry>
    <title>讲一讲数据安全，如何有效预防脱库</title>
    <link href="https://ningyu1.github.io/20201229/datasource-security.html"/>
    <id>https://ningyu1.github.io/20201229/datasource-security.html</id>
    <published>2020-12-29T10:49:30.000Z</published>
    <updated>2024-04-23T01:48:15.039Z</updated>
    
    <content type="html"><![CDATA[<p>今天讲一讲数据的安全问题，我们本篇不从DBA、网络架构层面来讲述数据安全，这部分有很专业的架构和云上产品来解决，本篇重点从开发人员角度讲述如何避免数据安全的漏洞。</p><p>我相信大部分人都看到过这样的新闻，某某论坛泄漏了用户密码，某某物流公司泄漏了用户的手机号等等，我一直坚信大部分数据泄漏都是内部管理出现了问题，大部分都是内部团队有意或无意泄漏了数据，如果要从外界通过漏洞攻克不是没有可能但是成本是巨大的，所以人为的泄漏往往是需要更加关注的问题，那作为软件生产的主力军（程序员）如何来避免挖坑？那我们接下来就主要讲讲从开发角度如何避免数据泄漏的可能性。</p><p>从网上找了一些 <a href="https://zhuanlan.zhihu.com/p/26707158" target="_blank" rel="noopener">数据安全治理</a> 的相关资料，有兴趣可以参考一下。</p><p>我们从程序员角度来讲讲如何有效的预防数据安全问题。</p><h1 id="数据的访问控制"><a href="#数据的访问控制" class="headerlink" title="数据的访问控制"></a>数据的访问控制</h1><p>我们先来看看哪些是经常访问数据库的用户？</p><ol><li>软件程序（应用程序、数据库中间件）</li><li>人员：运维、开发、测试、产品、等</li></ol><p>那接下来我们就来看看从这几点如何来控制数据库的访问。</p><h2 id="软件程序层面"><a href="#软件程序层面" class="headerlink" title="软件程序层面"></a>软件程序层面</h2><p>这里说的软件程序包含：应用程序、数据库中间件等，作为数据库的第一用户我们如何有效的规避数据安全的问题呢？</p><p>我们先来说说现在的软件开发用到的一些框架，无论是<code>java、go、python</code>或其他，已经有很丰富的<code>orm</code>和<code>datasource</code>框架或工具，我下面罗列一些<code>java</code>中常用<code>jdbc</code>连接池和<code>orm</code>框架以及数据库中间件</p><table><thead><tr><th>名称</th><th>说明</th><th>是否有加解密策略</th></tr></thead><tbody><tr><td>druid</td><td>阿里巴巴开源的数据库连接吃</td><td>有</td></tr><tr><td>dbcp</td><td>Apache的开源数据库连接池</td><td>无</td></tr><tr><td>c3p0</td><td>一个开源的JDBC连接池</td><td>无</td></tr><tr><td>atomikos</td><td>一款分布式事务框架</td><td>无</td></tr><tr><td>mybatis</td><td>一款开源orm框架</td><td>无</td></tr><tr><td>hibernate</td><td>一款开源orm框架</td><td>无</td></tr><tr><td>mycat</td><td>开源分布式数据库中间件</td><td>无</td></tr><tr><td>shardingsphere</td><td>Apache的开源分布式数据库中间件</td><td>有</td></tr><tr><td>cobar</td><td>阿里巴巴开源分片数据库和表的代理</td><td>有</td></tr></tbody></table><p>我相信很多程序的数据库连接与密码都是通过配置文件来保存的，假如应用服务器被黑客利用软件漏洞拿下，我相信通过部署的软件可以翻出数据库连接的配置，那么针对这一点我们如何有效的避免呢？</p><h3 id="数据库连接密码加密"><a href="#数据库连接密码加密" class="headerlink" title="数据库连接密码加密"></a>数据库连接密码加密</h3><p>一般我们为了做数据库高可用都会给数据库集群中间做一层代理，通过域名的方式来暴漏连接，这样做的好处就是数据库<code>failover</code>的时候应用程序可以不需要重启，只用重新创建连接即可。因此这层代理可以有效的防止数据库真实部署的机器被暴漏出去，起到了一定的安全作用。</p><p>虽然连接层面有一层代理来杜绝真实服务器被暴漏，但是我们在通过<code>jdbc</code>连接的时候往往是有密码访问的，我相信很多数据库的密码是明文的存储在配置文件中，虽然现在都用配置中心（<code>configcenter</code>）来统一管理应用的配置，如果使用明文来保存密码始终是无法规避泄漏的风险，因为应用程序始终要进行连接，在连接的时候要读取配置，不管配置是从云端同步下来还是从本地读取，只要是明文存储密码的就会存在安全问题。</p><p>其实大多的数据库连接池都有对数据库访问密码加解密的功能，因此我们可以通过把数据库访问的密码进行加密来解决安全问题。</p><p>下来我使用<code>druid</code>举个例子，具体的看看如何使用，也可以查看<code>druid</code>官方 <a href="https://github.com/alibaba/druid/wiki/%E4%BD%BF%E7%94%A8ConfigFilter" target="_blank" rel="noopener">示例</a></p><p>通过使用<code>ConfigFilter</code>为数据库密码提供加密功能</p><figure class="highlight xml"><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">bean</span> <span class="attr">id</span>=<span class="string">"dataSource"</span> <span class="attr">class</span>=<span class="string">"com.alibaba.druid.pool.DruidDataSource"</span></span></span><br><span class="line"><span class="tag">        <span class="attr">destroy-method</span>=<span class="string">"close"</span>&gt;</span></span><br><span class="line">    .................</span><br><span class="line">    <span class="comment">&lt;!-- 如果fitlers走的配置中心，请去配置中心修改 --&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">"filters"</span> <span class="attr">value</span>=<span class="string">"$&#123;filters&#125;"</span> /&gt;</span></span><br><span class="line">    <span class="comment">&lt;!-- 如果没有配置中心请直接修改 --&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">"filters"</span> <span class="attr">value</span>=<span class="string">"stat,config"</span> /&gt;</span> </span><br><span class="line">    <span class="comment">&lt;!-- 以上两种filters配置2选一 --&gt;</span></span><br><span class="line">     </span><br><span class="line">    <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">"connectionProperties"</span> <span class="attr">value</span>=<span class="string">"config.decrypt=true;config.decrypt.key=$&#123;publickey&#125;"</span> /&gt;</span></span><br><span class="line">    .................</span><br><span class="line"><span class="tag">&lt;/<span class="name">bean</span>&gt;</span></span><br></pre></td></tr></table></figure><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">filters=stat</span><br><span class="line">改为</span><br><span class="line">filters=stat,config</span><br><span class="line"> </span><br><span class="line">jdbc.xxxx.password=123456</span><br><span class="line">改为</span><br><span class="line">jdbc.xxxx.password=加密后的值</span><br><span class="line"> </span><br><span class="line">增加</span><br><span class="line">publickey=公钥</span><br></pre></td></tr></table></figure><p>ps. 如果使用的是配置中心那么创建对应的配置项即可。</p><p>非对称秘钥对的生成方式有很多种，这里我给个<a href="http://web.chacuo.net/netrsakeypair" target="_blank" rel="noopener">在线生成的链接</a> 加密后的密文是一段比较长的字符，例如下面这段示例</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">jdbc.password=p9i+fChqlaYnfhI+NoJqmrGwTyWwlFZ1W7Vi7i2MGZ8agFkGxGr/kWU//yDvPyXZ6YwJwnMKQ4zXpTZnfxWaRjfqWIRG+JzxSdSYEMp/bRCiIvzF6y8FdVCqN/0m0eQeZFvMCdIf4wqhKF0QRCEOTysZ3oGg7t5o35CIMpV1A5Y=</span><br></pre></td></tr></table></figure><p>其他的<code>jdbc</code>连接池也都有类似的功能，但是不排除有一些没有这个功能的就需要我们自己动手开发来增强这部分功能。</p><p>首先我们需要了解数据库连接加解密的思路，只要有思路实现都是很简单的，其实数据库连接加解密思路很简单，在真正创建数据库连接的时候读取加密的密码进行解密后再进行数据库连接，那接下来我们给<code>dbcp</code>扩展这个功能。</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> java.sql.SQLFeatureNotSupportedException;</span><br><span class="line"><span class="keyword">import</span> java.util.Properties;</span><br><span class="line"><span class="keyword">import</span> java.util.logging.Logger;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> org.apache.commons.dbcp.BasicDataSource;</span><br><span class="line"><span class="keyword">import</span> org.slf4j.LoggerFactory;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">SecurityBasicDataSource</span> <span class="keyword">extends</span> <span class="title">BasicDataSource</span> </span>&#123;</span><br><span class="line"></span><br><span class="line"><span class="keyword">private</span> <span class="keyword">final</span> org.slf4j.Logger logger = LoggerFactory.getLogger(SecurityBasicDataSource<span class="class">.<span class="keyword">class</span>)</span>;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> Logger <span class="title">getParentLogger</span><span class="params">()</span> <span class="keyword">throws</span> SQLFeatureNotSupportedException </span>&#123;</span><br><span class="line"><span class="keyword">throw</span> <span class="keyword">new</span> SQLFeatureNotSupportedException();</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">setPassword</span><span class="params">(String password)</span> </span>&#123;</span><br><span class="line"><span class="keyword">try</span> &#123;</span><br><span class="line"><span class="comment">//这里可以从任意地方读取数据库配置</span></span><br><span class="line">Properties p = ConfigLoaderUtils.loadConfig(<span class="string">"jdbc.properties"</span>);</span><br><span class="line">String publickey = p.getProperty(<span class="string">"publickey"</span>);</span><br><span class="line"><span class="comment">//ConfigTools是实现私钥、公钥对加解密实现</span></span><br><span class="line">password = ConfigTools.decrypt(publickey, password);</span><br><span class="line"><span class="keyword">super</span>.setPassword(password);</span><br><span class="line">&#125; <span class="keyword">catch</span>(Exception e) &#123;</span><br><span class="line">logger.error(<span class="string">"解密password出错"</span>, e);</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>首先我们继承<code>dbcp</code>数据源<code>org.apache.commons.dbcp.BasicDataSource</code></li><li>重写<code>setPassword</code></li><li>设置密码的时候通过公钥和密文进行解密</li></ul><p>这样我们就给<code>dbcp</code>扩展了数据库连接加解密的功能，是不是很简单。</p><p>到这里我们就对数据库连接密码加密的方法介绍完毕，这样做的好处有什么呢？假设当应用服务器被坏人俘虏后，他想通过应用的配置信息轻松的获取数据库访问密码是不太可能，采用 <a href="https://zh.wikipedia.org/wiki/%E5%85%AC%E5%BC%80%E5%AF%86%E9%92%A5%E5%8A%A0%E5%AF%86" target="_blank" rel="noopener">公开密钥加密</a> 安全性还是很高的，它是一种非对称加密算法想要了解更多的可以点开维基百科的连接查看。</p><h3 id="敏感数据加解密"><a href="#敏感数据加解密" class="headerlink" title="敏感数据加解密"></a>敏感数据加解密</h3><p>前面我们介绍完了数据库连接上的安全问题以及如何解决，接下来我们继续介绍数据库中存储的敏感数据应该如何处理。</p><p>我相信很多人都接触过导出生产数据需要经过 <a href="https://baike.baidu.com/item/%E6%95%B0%E6%8D%AE%E8%84%B1%E6%95%8F" target="_blank" rel="noopener">数据脱敏</a> ，需要经过数据脱敏的大多都是存储的明文数据，比如说用户的手机号、详细地址、银行卡号、信用卡验证码、用户密码、等。</p><p>如果我们将这些敏感数据在存储入库的时候进行加密，数据库中存储的是密文数据，这样及时被脱库我相信也没有那么容易破解，有人可能说密码破解外界有 <a href="https://zh.wikipedia.org/wiki/%E5%BD%A9%E8%99%B9%E8%A1%A8" target="_blank" rel="noopener">彩虹表</a> ，彩虹表是一个用于加密散列函数逆运算的预先计算好的表，常用于破解加密过的密码散列，针对于用户详细地址、银行卡号我相信彩虹表是无能为力的，如果使用暴力破解时间上的成本也是难以想象的，可能需要xxxxxx亿年，哈哈哈。</p><p>我们对数据加解密使用对称加解密算法<code>AES</code>或<code>DES</code>，为什么不使用非对称的<a href="https://zh.wikipedia.org/wiki/%E5%85%AC%E5%BC%80%E5%AF%86%E9%92%A5%E5%8A%A0%E5%AF%86" target="_blank" rel="noopener">公开密钥加密</a> ？</p><p>虽然非对称加解密算法安全性高，但是非对称加解密算法加密后的值太长不利于存储，所以我们需要使用固定长度或者可控长度的加解密算法，刚好对称加解密算法符合要求，这里使用<code>DES</code>作为示例，当然可以替换成任意的加解密算法。</p><p>先说说数据落库时的加解密实现思路，假设我们需要存储用户详情，其中有姓名、电话、联系地址、银行卡号等信息，我们在持久化用户详情的时候对敏感字段进行加密计算出密文，再将密文存入数据库，当查询用户详情的时候，先从数据库查询出密文，通过对密文的解密和脱敏再返回给前台，这样我们就可以达到我们想要的效果，这里需要特殊说明一下，密文对模糊查询不是很友好，但是也可以实现模糊查询，具体的实现思路有很多种，这里我们就不多做介绍，感兴趣的话我后面可以单独出一篇支持模糊查询的加解密算法，回到主题我们就以这个案例进行实现。</p><p>首先我们需要对敏感字段进行打标记，<code>java</code>的<code>annotation</code>可以帮助我们实现打标</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> java.lang.annotation.ElementType;</span><br><span class="line"><span class="keyword">import</span> java.lang.annotation.Retention;</span><br><span class="line"><span class="keyword">import</span> java.lang.annotation.RetentionPolicy;</span><br><span class="line"><span class="keyword">import</span> java.lang.annotation.Target;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Retention</span>(RetentionPolicy.RUNTIME)</span><br><span class="line"><span class="meta">@Target</span>(ElementType.FIELD)</span><br><span class="line"><span class="keyword">public</span> <span class="meta">@interface</span> Cipher &#123;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>我们可以给用户对象的敏感字段：手机号、银行卡、详细地址 上标注<code>@Cipher</code>注释，说明这几个字段我们在保存、修改的时候需要加密，在查询的时候需要解密。</p><p>下来我们就要找一个公共的地方来统一的进行加解密处理，作为一名合格的程序员需要想尽一切办法来偷懒，并不是在所有<code>CRUD</code>的地方进行加解密调用这样会很傻很天真，作为被广泛使用的<code>orm</code>框架之一的<code>mybatis</code>这里我使用它作为示例讲解实现思路。</p><p><code>mybatis</code>提供拦截器机制，可以对执行的<code>CRUD</code>进行拦截处理操作，<a href="https://github.com/pagehelper/Mybatis-PageHelper" target="_blank" rel="noopener">pagehelper</a> 是一个分页的<code>mybatis</code>插件，就是利用拦截的机制来扩展分页功能。</p><p>我们刚才有说过我们需要对<code>insert</code>、<code>update</code>操作进行加密，对<code>select</code>操作进行解密，在<code>mybatis</code>的底层保存和修改都是<code>update</code>方法，查询都是<code>query</code>方法，刚好我们就对这两个方法进行拦截处理。</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> java.lang.reflect.Field;</span><br><span class="line"><span class="keyword">import</span> java.util.List;</span><br><span class="line"><span class="keyword">import</span> java.util.Properties;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> org.apache.commons.lang3.StringUtils;</span><br><span class="line"><span class="keyword">import</span> org.apache.ibatis.executor.Executor;</span><br><span class="line"><span class="keyword">import</span> org.apache.ibatis.mapping.MappedStatement;</span><br><span class="line"><span class="keyword">import</span> org.apache.ibatis.plugin.Interceptor;</span><br><span class="line"><span class="keyword">import</span> org.apache.ibatis.plugin.Intercepts;</span><br><span class="line"><span class="keyword">import</span> org.apache.ibatis.plugin.Invocation;</span><br><span class="line"><span class="keyword">import</span> org.apache.ibatis.plugin.Plugin;</span><br><span class="line"><span class="keyword">import</span> org.apache.ibatis.plugin.Signature;</span><br><span class="line"><span class="keyword">import</span> org.apache.ibatis.session.ResultHandler;</span><br><span class="line"><span class="keyword">import</span> org.apache.ibatis.session.RowBounds;</span><br><span class="line"><span class="keyword">import</span> org.apache.ibatis.session.defaults.DefaultSqlSession.StrictMap;</span><br><span class="line"><span class="keyword">import</span> org.slf4j.Logger;</span><br><span class="line"><span class="keyword">import</span> org.slf4j.LoggerFactory;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Intercepts</span>(&#123;</span><br><span class="line">        <span class="meta">@Signature</span>(type = Executor<span class="class">.<span class="keyword">class</span>, <span class="title">method</span> </span>= <span class="string">"update"</span>, args = &#123; MappedStatement<span class="class">.<span class="keyword">class</span>, <span class="title">Object</span>.<span class="title">class</span> &#125;),</span></span><br><span class="line"><span class="class">        @<span class="title">Signature</span>(<span class="title">type</span> </span>= Executor<span class="class">.<span class="keyword">class</span>, <span class="title">method</span> </span>= <span class="string">"query"</span>, args = &#123; MappedStatement<span class="class">.<span class="keyword">class</span>, <span class="title">Object</span>.<span class="title">class</span>,</span></span><br><span class="line"><span class="class">                <span class="title">RowBounds</span>.<span class="title">class</span>, <span class="title">ResultHandler</span>.<span class="title">class</span> &#125;) &#125;)</span></span><br><span class="line"><span class="class"><span class="title">public</span> <span class="title">class</span> <span class="title">CipherHelper</span> <span class="keyword">implements</span> <span class="title">Interceptor</span> </span>&#123;</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> Logger logger = LoggerFactory.getLogger(CipherHelper<span class="class">.<span class="keyword">class</span>)</span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 加密密钥&lt;/br&gt; 为null，使用默认密钥进行加解密&lt;/br&gt;</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> String secureKey = <span class="keyword">null</span>;</span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 是否允许宽容处理&lt;/br&gt; 宽容处理的话，使用原值，反之throw &#123;<span class="doctag">@link</span> CipherException&#125;&lt;/br&gt;</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">boolean</span> lenient = <span class="keyword">false</span>;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> Object <span class="title">intercept</span><span class="params">(Invocation invocation)</span> <span class="keyword">throws</span> Throwable </span>&#123;</span><br><span class="line">        String methodName = invocation.getMethod().getName();</span><br><span class="line">        <span class="keyword">if</span> (methodName.equals(<span class="string">"update"</span>) || methodName.equals(<span class="string">"query"</span>)) &#123;</span><br><span class="line">             Object parameter = invocation.getArgs()[<span class="number">1</span>];</span><br><span class="line">            <span class="keyword">if</span> (parameter <span class="keyword">instanceof</span> List) &#123;</span><br><span class="line">                List&lt;?&gt; list = (List&lt;?&gt;) parameter;</span><br><span class="line">                <span class="keyword">for</span> (Object obj : list) &#123;</span><br><span class="line">                    encrypt(obj);</span><br><span class="line">                &#125;</span><br><span class="line">            &#125; <span class="keyword">else</span> <span class="keyword">if</span>(parameter <span class="keyword">instanceof</span> StrictMap) &#123;</span><br><span class="line">                StrictMap&lt;?&gt; strictMap = (StrictMap&lt;?&gt;) parameter;</span><br><span class="line">                <span class="keyword">if</span> (strictMap.containsKey(<span class="string">"list"</span>)) &#123;</span><br><span class="line">                    List&lt;?&gt; list = (List&lt;?&gt;) strictMap.get(<span class="string">"list"</span>);</span><br><span class="line">                    <span class="keyword">for</span> (Object obj : list) &#123;</span><br><span class="line">                        encrypt(obj);</span><br><span class="line">                    &#125;</span><br><span class="line">                &#125; <span class="keyword">else</span> <span class="keyword">if</span> (strictMap.containsKey(<span class="string">"array"</span>)) &#123;</span><br><span class="line">                    Object[] objects = (Object[]) strictMap.get(<span class="string">"array"</span>);</span><br><span class="line">                    <span class="keyword">for</span> (Object obj : objects) &#123;</span><br><span class="line">                        encrypt(obj);</span><br><span class="line">                    &#125;</span><br><span class="line">                &#125;</span><br><span class="line">            &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                encrypt(parameter);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        Object returnValue = invocation.proceed();</span><br><span class="line">        <span class="keyword">if</span> (methodName.equals(<span class="string">"query"</span>)) &#123;</span><br><span class="line">            <span class="keyword">if</span> (returnValue <span class="keyword">instanceof</span> List) &#123;</span><br><span class="line">                List&lt;?&gt; list = (List&lt;?&gt;) returnValue;</span><br><span class="line">                <span class="keyword">for</span> (Object obj : list) &#123;</span><br><span class="line">                    decrypt(obj);</span><br><span class="line">                &#125;</span><br><span class="line">            &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                decrypt(returnValue);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> returnValue;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 加密处理</span></span><br><span class="line"><span class="comment">     * </span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> parameter</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@throws</span> IllegalAccessException</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">encrypt</span><span class="params">(Object parameter)</span> <span class="keyword">throws</span> IllegalAccessException </span>&#123;</span><br><span class="line">        <span class="keyword">if</span> (parameter == <span class="keyword">null</span>) <span class="keyword">return</span>;</span><br><span class="line">        Class&lt;?&gt; clazz = parameter.getClass();</span><br><span class="line">        <span class="keyword">if</span> (!clazz.getSimpleName().endsWith(<span class="string">"Entity"</span>)) &#123;</span><br><span class="line">            <span class="keyword">return</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">for</span> (; clazz != Object<span class="class">.<span class="keyword">class</span></span>; clazz = clazz.getSuperclass()) &#123;</span><br><span class="line">            Field[] fields = clazz.getDeclaredFields();</span><br><span class="line">            <span class="keyword">for</span> (<span class="keyword">int</span> i = <span class="number">0</span>; i &lt; fields.length; i++) &#123;</span><br><span class="line">                <span class="keyword">if</span> (!fields[i].isAnnotationPresent(Cipher<span class="class">.<span class="keyword">class</span>)) </span>&#123;</span><br><span class="line">                    <span class="keyword">continue</span>;</span><br><span class="line">                &#125;</span><br><span class="line">                <span class="keyword">if</span> (!fields[i].getType().equals(String<span class="class">.<span class="keyword">class</span>)) </span>&#123;</span><br><span class="line">                    logger.debug(<span class="string">"加密字段只支持String类型,当前类型非String,跳过!"</span>);</span><br><span class="line">                    <span class="keyword">continue</span>;</span><br><span class="line">                &#125;</span><br><span class="line">                fields[i].setAccessible(<span class="keyword">true</span>);</span><br><span class="line">                String v = (String) fields[i].get(parameter);</span><br><span class="line">                <span class="keyword">if</span> (StringUtils.isBlank(v)) &#123;</span><br><span class="line">                    logger.debug(<span class="string">"加密字段值为null,跳过!"</span>);</span><br><span class="line">                    <span class="keyword">continue</span>;</span><br><span class="line">                &#125;</span><br><span class="line">                <span class="keyword">try</span> &#123;</span><br><span class="line">                    String crypt = DESTools.encrypt(secureKey, v);</span><br><span class="line">                    fields[i].set(parameter, crypt);</span><br><span class="line">                    logger.debug(<span class="string">"加密处理字段,&#123;&#125;"</span>, fields[i].getName());</span><br><span class="line">                &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">                    <span class="keyword">if</span> (lenient) &#123;</span><br><span class="line">                        logger.warn(<span class="string">"加密处理失败,宽容处理使用原值"</span>);</span><br><span class="line">                    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                        <span class="keyword">throw</span> <span class="keyword">new</span> CipherException(<span class="string">"加密处理失败,不允许宽容处理["</span>+v+<span class="string">"]"</span>, e);</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">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 解密处理</span></span><br><span class="line"><span class="comment">     * </span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> obj</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@throws</span> IllegalAccessException</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@throws</span> Exception</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">decrypt</span><span class="params">(Object obj)</span> <span class="keyword">throws</span> IllegalAccessException, Exception </span>&#123;</span><br><span class="line">        <span class="keyword">if</span> (obj == <span class="keyword">null</span>) <span class="keyword">return</span>;</span><br><span class="line">        Class&lt;?&gt; clazz = obj.getClass();</span><br><span class="line">        <span class="keyword">if</span> (!clazz.getSimpleName().endsWith(<span class="string">"Entity"</span>)) &#123;</span><br><span class="line">            <span class="keyword">return</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">for</span> (; clazz != Object<span class="class">.<span class="keyword">class</span></span>; clazz = clazz.getSuperclass()) &#123;</span><br><span class="line">            Field[] fields = clazz.getDeclaredFields();</span><br><span class="line">            <span class="keyword">for</span> (<span class="keyword">int</span> i = <span class="number">0</span>; i &lt; fields.length; i++) &#123;</span><br><span class="line">                <span class="keyword">if</span> (!fields[i].isAnnotationPresent(Cipher<span class="class">.<span class="keyword">class</span>)) </span>&#123;</span><br><span class="line">                    <span class="keyword">continue</span>;</span><br><span class="line">                &#125;</span><br><span class="line">                <span class="keyword">if</span> (!fields[i].getType().equals(String<span class="class">.<span class="keyword">class</span>)) </span>&#123;</span><br><span class="line">                    logger.debug(<span class="string">"解密字段只支持String类型,当前类型非String,跳过!"</span>);</span><br><span class="line">                    <span class="keyword">continue</span>;</span><br><span class="line">                &#125;</span><br><span class="line">                fields[i].setAccessible(<span class="keyword">true</span>);</span><br><span class="line">                String v = (String) fields[i].get(obj);</span><br><span class="line">                <span class="keyword">if</span> (StringUtils.isBlank(v)) &#123;</span><br><span class="line">                    logger.debug(<span class="string">"解密字段值为null,跳过!"</span>);</span><br><span class="line">                    <span class="keyword">continue</span>;</span><br><span class="line">                &#125;</span><br><span class="line">                <span class="keyword">try</span> &#123;</span><br><span class="line">                    String crypt = DESTools.decrypt(secureKey, v);</span><br><span class="line">                    fields[i].set(obj, crypt);</span><br><span class="line">                    logger.info(<span class="string">"解密处理字段,&#123;&#125;"</span>, fields[i].getName());</span><br><span class="line">                &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">                    <span class="keyword">if</span> (lenient) &#123;</span><br><span class="line">                        logger.warn(<span class="string">"解密处理失败,宽容处理使用原值"</span>);</span><br><span class="line">                    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                        <span class="keyword">throw</span> <span class="keyword">new</span> CipherException(<span class="string">"解密处理失败,不允许宽容处理["</span>+v+<span class="string">"]"</span>, e);</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">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> Object <span class="title">plugin</span><span class="params">(Object target)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> Plugin.wrap(target, <span class="keyword">this</span>);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">setProperties</span><span class="params">(Properties properties)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">if</span> (properties != <span class="keyword">null</span> &amp;&amp; StringUtils.isNotBlank(properties.getProperty(<span class="string">"secureKey"</span>))) &#123;</span><br><span class="line">            <span class="keyword">this</span>.secureKey = properties.getProperty(<span class="string">"secureKey"</span>);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">if</span> (properties != <span class="keyword">null</span> &amp;&amp; StringUtils.isNoneBlank(properties.getProperty(<span class="string">"lenient"</span>))) &#123;</span><br><span class="line">            <span class="keyword">this</span>.lenient = Boolean.parseBoolean(properties.getProperty(<span class="string">"lenient"</span>));</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>通过<code>mybatis</code>的插件扩展机制在执行过程进行拦截处理，<code>plugin</code>方法是插件的装载方法，<code>setProperties</code>方法设置关键属性，比如说密钥串。</p><p>接下来我们对每个方法进行讲解：</p><p><code>encrypt</code>：是加密方法，这里加密方法需要注意的是，<code>mybatis</code>参数支持<a href="https://baike.baidu.com/item/POJO" target="_blank" rel="noopener"><code>Pojo</code></a> 、<code>Map</code>、<code>StrictMap</code>、<code>List</code>、<code>Array</code>，我们使用注解<code>@Cipher</code>是用在类上的所以只对<code>Pojo</code>生效，如果是<code>Map</code>它天生的<code>key，value</code>格式无法支持打标，我们这里对<code>Map</code>类型进行跳过不处理，如果非要处理<code>Map</code>也是有办法的，需要固定加解密的<code>key</code>值，对特定的<code>key</code>进行识别并加解密替换<code>value</code>，加密方法通过查找有注解<code>@Cipher</code>的字段进行加密并且回填值。</p><p><code>decrypt</code>：是解密方法，主要用在查询时的解密，这里需要注意的是查询有可能返回特定的<code>Pojo</code>也可能返回<code>List</code>，所以这里解密的时候需要根据类型来分别处理，如果是<code>List</code>需要进行很层次查找，如果是<code>Pojo</code>那就查找使用注解<code>@Cipher</code>的字段进行解密并且回填值。</p><p><code>intercept</code>：是拦截方法，在<code>update</code>、<code>query</code>前后进行拦截处理，在这个方法里需要进行如下步骤：</p><ul><li>识别当前执行的<code>method</code>是<code>update</code>还是<code>query</code><ul><li>如果是<code>update</code>那就进行加密</li><li>如果是<code>query</code>那就进行解密</li></ul></li><li>识别参数类型是<code>List</code>、<code>StrictMap</code>、<code>Pojo</code><ul><li>如果是<code>List</code>需要再深层次看一下<code>List</code>里是什么类型，这里建议使用递归方式<ul><li>如果<code>List</code>里是<code>Pojo</code>那就循环调用<code>encrypt</code>方法</li><li>如果<code>List</code>里是<code>Map</code>跳过处理，或者使用上面我们说的识别某些固定<code>key</code>进行加密处理</li></ul></li><li>如果是<code>StrictMap</code>需要再深层次看一下<code>StrictMap</code>里是什么类型，这里建议使用递归方式<ul><li>如果<code>StrictMap</code>里是<code>list</code>那就循环调用<code>encrypt</code>方法</li><li>如果<code>StrictMap</code>里是<code>array</code>那就循环调用<code>encrypt</code>方法</li></ul></li><li>如果是<code>Pojo</code>那就调用<code>encrypt</code>方法</li></ul></li><li>执行sql处理获取返回值</li><li>获取返回值并且执行的方法是<code>query</code>时，进行解密处理<ul><li>识别返回的类型是<code>List</code>还是<code>Pojo</code><ul><li>如果是<code>List</code>深层次查找内部类型，这里建议使用递归方式<ul><li>如果<code>List</code>里是<code>Pojo</code>那就循环调用<code>decrypt</code>方法</li><li>如果<code>List</code>里是<code>Map</code>跳过处理，或者使用上面我们说的识别某些固定<code>key</code>进行解密处理</li></ul></li><li>如果是<code>Pojo</code>那就调用<code>decrypt</code>方法</li></ul></li></ul></li></ul><p>到这里数据加解密的核心逻辑就介绍完了。</p><p>这里我们回顾一下，我们先是对数据库连接密码进行加解密，然后又对敏感数据落库和查询时进行加解密，第一步连接密码加密预防坏人即使攻击拿到了应用服务器的操作权限他也无法轻易的攻克我们的数据库访问密码，第二步敏感数据加解密预防坏人即使攻克了我们的数据库（俗称脱库）他也无法获取用户的隐私数据，这样就有效的保证了用户隐私数据的安全性。</p><p>到这里我们就对软件程序层面的数据安全防护手段介绍完毕，接下来我们再从人员访问控制方面来看看有什么有效的手段。</p><h2 id="人员层面"><a href="#人员层面" class="headerlink" title="人员层面"></a>人员层面</h2><p>前面我们说了绝大多数的数据泄密都不是技术问题而是人员管理问题，我们要对人员进行有效的管理与控制。</p><h3 id="开发或测试人员"><a href="#开发或测试人员" class="headerlink" title="开发或测试人员"></a>开发或测试人员</h3><p>这类人一般对数据是有<code>CRUD</code>的诉求，针对这类人员的控制有如下几个方面</p><ul><li>开发人员只能连接测试环境数据库，不允许连接生产数据库，即使连接vpn也不行。</li><li>开发人员申请数据库需要走运维工单流程，运维提供数据库连接密码时应直接提供密文，或者运维直接给配置到配置中心。</li><li>配置文件或配置中心禁止存储明文密码，需要对jdbc等其他敏感密码进行脱敏处理。</li><li>生产服务器需要通过跳板机访问，禁止开发使用<code>root</code>直接操作，如果看应用日志可以走日志平台，实在没有日志平台可以给跳板机开通<code>app</code>用户只给查看固定目录日志的权限，如果要发布走<code>devops</code>平台，如果没有可以提供给运维进行发布。</li><li>查询生产数据走<code>dms</code>平台，对敏感信息进行脱敏或隐藏，对上线的sql和日常的查询日志做到<code>dms</code>可管控。</li><li>提交到开放环境时需要注意以下几点<ul><li>提交到开放的仓库（<code>github</code>、<code>gitlab</code>、<code>gitee</code>等），需要对代码进行审核，避免有<code>hardcode</code>的公司服务器密码、ip、端口、密钥等。</li><li>提交到开放的论坛（<code>csdn</code>、<code>oschina</code>、<code>知乎</code>、<code>公众号</code>、<code>社区分享</code>等），需要对文章进行审核，避免有不允许公开的技术细节或敏感信息。</li></ul></li></ul><h3 id="运维或DBA人员"><a href="#运维或DBA人员" class="headerlink" title="运维或DBA人员"></a>运维或DBA人员</h3><p>这类人一般操作权限都很高，出问题概率最高的人员，有很多删库跑路或误操作<code>rm -rf</code>的例子哈哈哈！所以这类人更要重点管控。</p><ul><li>需要搭建和处理运维工单平台，用于开发提出的运维资源申请，尤其是数据库密码，直接提供加密后的密文和公钥。</li><li>需要搭建和处理数据库管理工具<code>dms</code>，用于开发日常生产数据查询和发版时<code>SQL</code>升级。</li><li>需要提供跳板机和给跳板机提供不同等级的用户，提供给特别需要的人访问生产环境机器。</li><li>需要提供<code>devops</code>平台或者自动化发版工具，避免手动操作失误带来问题，对开发提供升级发布的流水线。</li><li>对服务器密码需要进行加密存储，可以借助密码管理工具。</li><li>运维最好也不要使用<code>root</code>用户操作服务器，使用特定权限的用户操作。</li><li><code>dba</code>最好也不要使用<code>root</code>用户操作数据库，使用特定权限的用户操作。</li><li>制定责任人机制，对应的责任项必须到具体人，具体可以参考 <a href="https://baike.baidu.com/item/%E8%B4%A3%E4%BB%BB%E5%88%86%E9%85%8D%E7%9F%A9%E9%98%B5" target="_blank" rel="noopener">责任分配矩阵RAM</a> 。</li><li>关键重要的操作需要至少两个人在场，具体可以参考 <a href="https://baike.baidu.com/item/%E8%B4%A3%E4%BB%BB%E5%88%86%E9%85%8D%E7%9F%A9%E9%98%B5" target="_blank" rel="noopener">责任分配矩阵RAM</a> 。</li></ul><h3 id="产品或业务人员"><a href="#产品或业务人员" class="headerlink" title="产品或业务人员"></a>产品或业务人员</h3><p>这类人一般对数据有查询和分析的诉求，有分析诉求就需要导出数据，所以分析诉求统一走公司<code>BI</code>工具，也有少部分有修改的诉求。</p><ul><li>查询分析数据，统一接入<code>BI</code>工具，并且<code>BI</code>工具需要有功能和数据权限，并对敏感数据导出加以控制，导出走审批并脱敏。</li><li>提交数据变更，统一接入<code>dms</code>平台。</li><li>产品的分析文件（word、excel、ppt）应该进行加密，这种一般依赖公司引入文档安全的解决方案，要花钱的，如果不想花钱那就没啥好办法。</li></ul><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>到这里整篇也就差不多都介绍完了，我们现在回顾一下，首先我们介绍了数据安全的问题，并且说明了安全问题一般发生在两个方面，一个是软件程序，一个是人员管理。</p><p>我们在软件程序方面介绍了两种预防数据安全的手段，一个是数据库连接密码加解密，一个是数据加解密，数据库连接加密可以有效预防服务器被攻击后通过翻找程序来进一步攻击数据库，数据加解密可以有效预防数据库被攻击或脱库后泄漏用户及公司隐私数据。</p><p>我们在人员方面首先对人员进行了分类，针对每一类人的诉求谈了管控的手段，说了这么多的控制手段，并不是说对员工不信任，我们这里说的控制并不等于限制，说了这么多的终极目标是搭建一套有序的安全的管理机制，在安全的范围内给员工提供最大化的发挥空间，规避有心或无心的泄密，最后送上那句老话，Keep Real，Love and peace！</p>]]></content>
    
    <summary type="html">
    
      讲一讲数据安全，如何有效预防脱库
    
    </summary>
    
      <category term="security" scheme="https://ningyu1.github.io/categories/security/"/>
    
    
      <category term="security" scheme="https://ningyu1.github.io/tags/security/"/>
    
      <category term="datasource" scheme="https://ningyu1.github.io/tags/datasource/"/>
    
  </entry>
  
  <entry>
    <title>使用Travis CI部署Hexo</title>
    <link href="https://ningyu1.github.io/20200430/travis-ci-deploy-hexo.html"/>
    <id>https://ningyu1.github.io/20200430/travis-ci-deploy-hexo.html</id>
    <published>2020-04-30T04:19:26.000Z</published>
    <updated>2024-04-23T01:48:15.039Z</updated>
    
    <content type="html"><![CDATA[<p>今天说一下使用travis ci来部署hexo，在说这个之前呢要先提几个概念，CI/CD（是一种通过在应用开发阶段引入自动化来频繁向客户交付应用的方法）。</p><p>什么是CI？ 持续集成，Continuous Integration，简称 CI</p><p>什么是CD？ 持续交付，Continuous Deployment，简称CD</p><p>我们都知道软件开发写代码只是其中的一部分，写出代码后需要构建（build）、测试（test）、发布（deploy）。</p><p>这些都是比较按部就班的操作，所以有很多工具化的支持，为了提高软件开发效率，构建和测试的工具有很多，我们最熟知的是<a href="https://jenkins.io/" target="_blank" rel="noopener">jenkins</a>，相信很多人都用过，还有很多如<a href="https://about.gitlab.com/features/gitlab-ci-cd/" target="_blank" rel="noopener">gitlab-ci</a>、<a href="https://travis-ci.org/" target="_blank" rel="noopener">travis-ci</a>、等。</p><p>今天我们就说一下<a href="https://travis-ci.org/" target="_blank" rel="noopener">travis-ci</a>的简单使用，刚好我的blog每次都要编译然后提交到github，刚好用它做一个演示。</p><h3 id="Travis-CI的介绍"><a href="#Travis-CI的介绍" class="headerlink" title="Travis CI的介绍"></a>Travis CI的介绍</h3><p>Travis CI</p><p>是CI / CD生态系统中比较常见的名号之一，最初设定为开源项目，并在多年扩展之后转为闭源项目。它专注于CI工作，通过自动化测试和警报系统提高构建过程的效能。</p><h4 id="它有什么作用？"><a href="#它有什么作用？" class="headerlink" title="它有什么作用？"></a>它有什么作用？</h4><p>Travis-CI允许用户在部署代码时对代码进行快速测试。它支持代码大小变更，可识别构建与测试中发生的一切变更。检测到变更后，Travis CI可以提供有关变更是否成功的反馈。</p><p>开发人员可以使用Travis CI对运行时进行观察，并行运行多项测试，并将该工具与Slack、HipChat以及Email等集成，从而通过多种渠道获取问题或构建失败通知。</p><p>Travis CI支持容器构建，并支持Linux Ubuntu和OSX。您可以在不同的编程语言中使用它，例如Java，C＃，Clojure，GO，Haskell，Swift，Perl等等。其具备一份相对有限的第三方集成列表，但由于关注重点在于CI而非CD，因此其可能解决不了您的实际问题。</p><h1 id="使用Travsi-CI发布Hexo"><a href="#使用Travsi-CI发布Hexo" class="headerlink" title="使用Travsi CI发布Hexo"></a>使用Travsi CI发布Hexo</h1><p>我的blog使用的是<a href="https://hexo.io/" target="_blank" rel="noopener">Hexo</a>，它有丰富的模板可以选择，支持markdown写文章再构建生成静态html，所以当时就选用了它。</p><p>在做之前我们需要准备一些东西，如下：</p><h2 id="Step-1"><a href="#Step-1" class="headerlink" title="Step 1"></a>Step 1</h2><p>首先我们要在github上创建一个repository，给这个repository创建两个分支，我这里使用master作为静态html分支，blog-source作为博客源码分支。</p><p><em>ps. 分支名称都随意，有人喜欢用master作为源码分支，gh-pages作为静态html分支，都可以请随意。</em></p><h2 id="Step-2"><a href="#Step-2" class="headerlink" title="Step 2"></a>Step 2</h2><p>去github上获取accesstoken，可以去<a href="https://github.com/settings/tokens" target="_blank" rel="noopener">https://github.com/settings/tokens</a>获取，生成一个token（generate new token），token的scope范围看自己的需求选择，我这里选择的是repo下全部。</p><p>生成的token记得自己先保存起来，因为刷新页面后你就再也看不到它了，如果忘记了那就去重新生成一个（regenerate token）</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/travis-ci/4.png" alt="图四" title>                </div>                <div class="image-caption">图四</div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/travis-ci/5.png" alt="图五" title>                </div>                <div class="image-caption">图五</div>            </figure><h2 id="Step-3"><a href="#Step-3" class="headerlink" title="Step 3"></a>Step 3</h2><p>在blog库的blog-source分支中添加<code>.travis.yml</code></p><figure class="highlight yaml"><table><tr><td class="code"><pre><span class="line"><span class="attr">language:</span> <span class="string">node_js</span></span><br><span class="line"><span class="attr">node_js:</span> <span class="number">10.16</span><span class="number">.3</span></span><br><span class="line"><span class="attr">install:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">npm</span> <span class="string">install</span></span><br><span class="line"><span class="attr">script:</span> <span class="string">bash</span> <span class="string">./deploy.sh</span></span><br><span class="line"><span class="attr">branches:</span></span><br><span class="line">  <span class="attr">only:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">blog-source</span></span><br><span class="line"><span class="attr">notifications:</span></span><br><span class="line">  <span class="attr">email:</span> <span class="literal">false</span></span><br></pre></td></tr></table></figure><p>hexo采用nodejs开发，所以这里语言选择nodejs，我这里选择nodejs版本是10.16.3，因为高版本nodejs下hexo生成的静态文件有问题，如果是其他的程序可以使用stable。</p><p>install阶段执行npm安装依赖。</p><p>script阶段执行一个外部脚本，在同<code>.travis.yml</code>路径下创建shell脚本<code>deploy.sh</code></p><figure class="highlight shell"><table><tr><td class="code"><pre><span class="line"><span class="meta">#</span>!/usr/bin/env sh</span><br><span class="line"><span class="meta">#</span> 确保脚本抛出遇到的错误</span><br><span class="line">set -e</span><br><span class="line"><span class="meta">#</span> 生成静态文件</span><br><span class="line">hexo generate</span><br><span class="line"><span class="meta">#</span> 进入生成的文件夹</span><br><span class="line">cd ./public</span><br><span class="line"><span class="meta">#</span>创建.nojekyll 防止Github Pages build错误</span><br><span class="line">touch .nojekyll</span><br><span class="line">git init</span><br><span class="line">git add -A</span><br><span class="line">git commit -m "deploy"</span><br><span class="line">git push -f "https://$&#123;access_token&#125;@github.com/ningyu1/blog.git" master:master</span><br><span class="line">cd -</span><br></pre></td></tr></table></figure><p>脚本中先执行<code>hexo -g（等同于hexo generate）</code>生成静态html文件，文件生成在当前目录下的public文件夹。</p><p>进入到public文件夹下执行git命令，这里需要注意的是使用<code>git push -f</code>强制推送，如果没有设置<code>git user.name</code>和<code>git user.email</code>的话提交的用户是<code>Travis Ci User</code></p><p><em>ps. 这里特殊说明一下变量 <code>${access_token}</code>，这里使用travis ci的运行时环境变量，像这种相对重要的信息放到运行时环境变量会安全一些，避免别人拿到我的AccessToken做一些坏事情，哈哈。</em></p><h2 id="Step-4"><a href="#Step-4" class="headerlink" title="Step 4"></a>Step 4</h2><p>登录<a href="https://travis-ci.org/" target="_blank" rel="noopener">travis-ci</a>，可以使用github三方登录，添加一个github仓库，travis跟github集成的还是非常紧密和方便的，添加仓库可以直接读取github上的仓库，在列表里面找到blog打开使用travis，如图一。</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/travis-ci/1.png" alt="图一" title>                </div>                <div class="image-caption">图一</div>            </figure><p>进入dashbord查看刚才添加的库信息，选择More options-&gt;Settings，添加Environment Variables</p><p>创建一个access_token的环境变量，可以选择那些branch使用，DISPLAY VALUE IN BUILD LOG是控制构建日志中是否要输出变量的值，为了安全起见建议不用开启，如图二</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/travis-ci/2.png" alt="图二" title>                </div>                <div class="image-caption">图二</div>            </figure><h2 id="Step-5"><a href="#Step-5" class="headerlink" title="Step 5"></a>Step 5</h2><p><a href="https://travis-ci.org/" target="_blank" rel="noopener">travis-ci</a>接收到github的事件请求就会自动触发job。</p><p>一切准备就绪之后提交并推送代码到github仓库，这样travis会接收到github的push event事件，可以去More options-&gt;Requests中查看接收到的请求信息，如图三</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/travis-ci/3.png" alt="图三" title>                </div>                <div class="image-caption">图三</div>            </figure><p>如果没有啥特殊情况看构建日志就可以看到成功信息。</p><h2 id="Problem"><a href="#Problem" class="headerlink" title="Problem"></a>Problem</h2><p>这里我遇到了一个问题，我使用hexo只支持10.x一下的nodejs版本，刚开始使用的<code>node_js: stable</code>，查看job日志可以看到使用的版本号如下</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">$ node --version</span><br><span class="line">v14.0.0</span><br><span class="line">$ npm --version</span><br><span class="line">6.14.4</span><br><span class="line">$ nvm --version</span><br><span class="line">0.35.3</span><br></pre></td></tr></table></figure><p>v14.x 版本导致hexo生成的html文件都是0 byte，果断的降低了nodejs版本就好了。</p>]]></content>
    
    <summary type="html">
    
      使用Travis CI部署Hexo
    
    </summary>
    
      <category term="travis-ci" scheme="https://ningyu1.github.io/categories/travis-ci/"/>
    
    
      <category term="travis-ci" scheme="https://ningyu1.github.io/tags/travis-ci/"/>
    
      <category term="hexo" scheme="https://ningyu1.github.io/tags/hexo/"/>
    
  </entry>
  
  <entry>
    <title>一分钟弄清楚事务四要素：ACID</title>
    <link href="https://ningyu1.github.io/20200325/one-minute-transaction-ACID.html"/>
    <id>https://ningyu1.github.io/20200325/one-minute-transaction-ACID.html</id>
    <published>2020-03-24T18:19:26.000Z</published>
    <updated>2024-04-23T01:48:15.039Z</updated>
    
    <content type="html"><![CDATA[<p>在学习数据库时一定都听说过事务的四要素：ACID，但是大多数人对事务四要素的理解一直停留在课本概念层面并没有仔细的琢磨，当我们在实际工作中遇到问题时经常要去回顾这四要素的概念，甚至很多人说不清事务四要素具体是什么。</p><p>相信大家在实际工作中最常遇到的问题就是死锁（dead lock），说到死锁就不得不说先说事务，我们都知道死锁是并发事务时经常会遇到的问题，当然死锁的场景也非常多并且与数据库事务隔离级别有着很大的关系，这些不懂都没有关系，今天我们就花一分钟来理解消化事务的四要素。</p><h1 id="事务四要素：ACID"><a href="#事务四要素：ACID" class="headerlink" title="事务四要素：ACID"></a>事务四要素：ACID</h1><p>说到事务的四个要素，最容易理解的就是原子性，但是其他三个要素：一致性、隔离性、持久性也是非常重要，恰恰也是经常被人忽视的。</p><h2 id="原子性（Atomicity）"><a href="#原子性（Atomicity）" class="headerlink" title="原子性（Atomicity）"></a>原子性（Atomicity）</h2><p>原子性非常容易被人理解，直白点说就是一个事务中的所有操作（CRUD）就像是一个原子操作一样不可分割开来，要么全部成功，要么全部失败，不允许部分成功部分失败。</p><p>举个例子：</p><ol><li>往篮子里放入一个苹果</li><li>随后从篮子里拿出一个梨<br>这两个动作要么都成功，要么都失败，不存在只往篮子里放苹果而没拿梨，也不存在只从篮子里拿梨而没有放苹果。</li></ol><p>原子性的侧重点在于多个动作必须同时成功或者同时失败。</p><h2 id="一致性（Consistency）"><a href="#一致性（Consistency）" class="headerlink" title="一致性（Consistency）"></a>一致性（Consistency）</h2><p>网上争议最多的就是对一致性的理解，公说公有理婆说婆有理，我觉得还是仁者见仁智者见智。</p><p>说法一：事务开始和结束后，数据库的完整性约束没有被破坏，如外键约束。<br>说法二：一个事务单元需要提交之后才会被其他事务可见。</p><p>网上有很多用张三给李四转账的例子来说明一致性，但是看起来感觉跟原子性的概念雷同，这也是经常会有人傻傻分不清一致性和原子性的区别。</p><p>说法三：一致性和原子性的区别在于两者的侧重点不同，原子性关注的是状态，要么全部成功，要么全部失败，不存在部分成功的状态。而一致性关注的是数据的可见性，一个事务的中间状态的数据对外部不可见，只有最初状态和最终状态的数据对外可见。</p><p>其实这个说法是对的，但是理解起来不通俗易懂甚至有些模糊，很难让人很通俗易懂的明白其中的区别。</p><p>而且看到上面区别解释后又会有人提出挑战说，当事务的隔离级别是Read Uncommitted（读未提交）时，可以把事务的中间状态数据对其他事务可见，这个不就是违背了一致性么？</p><p>其实事务的隔离级别就是在事务的四要素和性能上面做平衡，有时候为了提高性能，会适度的破坏一致性原则。</p><p><a href="https://www.zhihu.com/question/31346392" target="_blank" rel="noopener">这篇文章</a>是我见过对一致性理解最靠谱的。</p><p>说回到主题一致性的理解上，<strong>我对一致性的理解是这样的，一致性关注的点是数据的操作结果是否与用户业务预期一致，这里有两点需要注意。</strong></p><ol><li><strong>一致性关注的是结果状态</strong>，什么是结果状态？<strong>它是一个要达到的预期效果并非是一个保证手段</strong>。而数据库的唯一建、外键、主键等这些约束建和AID原则：原子性（A）、隔离性（I）、持久性（D）以及提交、回滚等动作是保证结果能达到与预期效果一致的手段。</li><li><strong>一致性它是由用户的业务决定的并非数据库决定的</strong>，网上很多的例子解释一致性都是狭义的理解，为什么这样说的？比如说账户扣钱，如果业务约束账户不允许为负数，那当扣成负数的时候就会通过回滚来保持一致性，如果业务约束账户允许为负数，那当扣成负数也可以正常提交来保持一致性，那这两个最终的状态是完全不一样的，但都是满足用户预先设置好的约束规则，所以一致性是由用户来决定的，从结果来看只要符合业务预期约束就是满足一致性的。</li></ol><h2 id="隔离性（Isolation）"><a href="#隔离性（Isolation）" class="headerlink" title="隔离性（Isolation）"></a>隔离性（Isolation）</h2><p>并发事务之间不会互相影响，就像串行执行一样，也就是说并发事务之间都是互相隔离的，你不影响我，我也不影响你。<br>隔离性侧重点是并发事务之间的影响，说到并发事务就要提到数据库的隔离级别，有的时候会通过调整数据库的隔离级别来适度的破坏一致性和隔离性，从而提高数据库处理性能。</p><p><em>ps. 画外音，隔离性是我们需要重点关注的，因为不同的隔离级别，可能对应的加锁过程不一样，而正是因为引入了各种各样的隔离级别，才让锁问题变得格外复杂。解决和分析死锁问题，重点就是要搞清楚数据库的隔离级别。那么隔离级别是个什么东西呢？我们会在后面出单独的文章来重点说明。</em></p><h2 id="持久性（Durability）"><a href="#持久性（Durability）" class="headerlink" title="持久性（Durability）"></a>持久性（Durability）</h2><p>持久性就非常好理解了，事务提交后即持久化到磁盘不会丢失。<br>持久性侧重的是数据不丢失，这个跟网上讨论最多的 （<a href="http://fanyilun.me/2015/12/29/%E4%BA%8B%E5%8A%A1%E7%9A%84%E9%9A%94%E7%A6%BB%E7%BA%A7%E5%88%AB%E4%BB%A5%E5%8F%8AMysql%E4%BA%8B%E5%8A%A1%E7%9A%84%E4%BD%BF%E7%94%A8/" target="_blank" rel="noopener">“丢失更新”</a>、<a href="https://www.aneasystone.com/archives/2017/10/solving-dead-locks-one.html" target="_blank" rel="noopener">“提交覆盖”</a>、<a href="https://en.wikipedia.org/wiki/Read-modify-write" target="_blank" rel="noopener">“Read-Modify-Write问题”</a>）很容易混淆，看起来效果都是没有存入正确的数据，看起来好像数据丢失了一样。<br>实际上两者区别很大的，前者是说数据存到物理磁盘不会丢失，而后者则说的是并发事务中的相互影响导致最终的数据结果不同。</p><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>这一章我们对事务的ACID进行了快速的理解，这里容易混淆的有原子性和一致性。</p><ol><li>原子性的侧重点在于操作完整不可分隔，往篮子里放入一个苹果随后从篮子里拿出一个梨，不存在只往篮子里放入苹果没有拿梨，也不存在只从篮子里拿梨没有放入苹果。</li><li>一致性的侧重点在于结果状态是否与用户的预期效果一致。</li><li>隔离性的侧重点在于并发事务之间互不影响，切记不是你中有我我中有你。</li><li>持久性的侧重点在于数据落盘不丢，切记不是并发事务导致的最终数据结果丢失。</li></ol><p>下一章我们会从事务的隔离级别来进行快速学习和掌握，隔离级别会分两章来进行，先让大家搞清楚我们最常挂在嘴边的也是面试最常问道的：脏读、幻读、不可重复度、可重复度、丢失更新的定义和区别，我会尽量用通俗易懂的方式来让大家明白。</p><h1 id="附录"><a href="#附录" class="headerlink" title="附录"></a>附录</h1><p>这里从网上找了一些关于事务的文章，不用太纠结对错，仁者见仁，智者见智，有句俗话是这样说的：不要太认真，认真你就输了，哈哈。。。。</p><ol><li><a href="https://www.aneasystone.com/archives/2017/10/solving-dead-locks-one.html" target="_blank" rel="noopener">https://www.aneasystone.com/archives/2017/10/solving-dead-locks-one.html</a></li><li><a href="https://www.cnblogs.com/autointerface/p/11959711.html" target="_blank" rel="noopener">https://www.cnblogs.com/autointerface/p/11959711.html</a></li><li><a href="http://novoland.github.io/%E6%95%B0%E6%8D%AE%E5%BA%93/2014/07/26/MySQL%E6%80%BB%E7%BB%93.html" target="_blank" rel="noopener">http://novoland.github.io/%E6%95%B0%E6%8D%AE%E5%BA%93/2014/07/26/MySQL%E6%80%BB%E7%BB%93.html</a></li></ol><p><em>ps. 文章中有哪些错误的地方欢迎指正。</em></p><p>好像文章有点长阅读起来超过了一分钟，看来功力还不够没有压缩到一分钟，希望能帮助到需要的人。</p><p>Love and peace！</p>]]></content>
    
    <summary type="html">
    
      一分钟弄清楚事务四要素：ACID
    
    </summary>
    
      <category term="transaction" scheme="https://ningyu1.github.io/categories/transaction/"/>
    
    
      <category term="transaction" scheme="https://ningyu1.github.io/tags/transaction/"/>
    
      <category term="acid" scheme="https://ningyu1.github.io/tags/acid/"/>
    
  </entry>
  
  <entry>
    <title>[转]研究 Dubbo 网卡地址注册时的一点思考</title>
    <link href="https://ningyu1.github.io/20191016/dubbo-network-interfaces.md.html"/>
    <id>https://ningyu1.github.io/20191016/dubbo-network-interfaces.md.html</id>
    <published>2019-10-16T03:08:26.000Z</published>
    <updated>2024-04-23T01:48:15.039Z</updated>
    
    <content type="html"><![CDATA[<p>最近看到一篇Apache Dubbo官方Blog中对dubbo网卡实现的代码解读，觉得讲的非常好所以特别分享一下，话不多说直接看原文吧。</p><h1 id="研究-Dubbo-网卡地址注册时的一点思考"><a href="#研究-Dubbo-网卡地址注册时的一点思考" class="headerlink" title="研究 Dubbo 网卡地址注册时的一点思考"></a>研究 Dubbo 网卡地址注册时的一点思考</h1><h2 id="1-如何选择合适的网卡地址"><a href="#1-如何选择合适的网卡地址" class="headerlink" title="1 如何选择合适的网卡地址"></a>1 如何选择合适的网卡地址</h2><p>可能相当一部分人还不知道我这篇文章到底要讲什么，我说个场景，大家应该就明晰了。在分布式服务调用过程中，以 Dubbo 为例，服务提供者往往需要将自身的 IP 地址上报给注册中心，供消费者去发现。在大多数情况下 Dubbo 都可以正常工作，但如果你留意过 Dubbo 的 github issue，其实有不少人反馈：Dubbo Provider 注册了错误的 IP。如果你能立刻联想到：多网卡、内外网地址共存、VPN、虚拟网卡等关键词，那我建议你一定要继续将本文看下去，因为我也想到了这些，它们都是本文所要探讨的东西！那么“如何选择合适的网卡地址”呢，Dubbo 现有的逻辑到底算不算完备？我们不急着回答它，而是带着这些问题一起进行研究，相信到文末，其中答案，各位看官自有评说。</p><h2 id="2-Dubbo-是怎么做的"><a href="#2-Dubbo-是怎么做的" class="headerlink" title="2 Dubbo 是怎么做的"></a>2 Dubbo 是怎么做的</h2><p>Dubbo 获取网卡地址的逻辑在各个版本中也是千回百转，走过弯路，也做过优化，我们用最新的 2.7.2-SNAPSHOT 版本来介绍，在看以下源码时，大家可以怀着质疑的心态去阅读，在 dubbo github 的 master 分支可以获取源码。获取 localhost 的逻辑位于 <code>org.apache.dubbo.common.utils.NetUtils#getLocalAddress0()</code> 之中</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">static</span> InetAddress <span class="title">getLocalAddress0</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    InetAddress localAddress = <span class="keyword">null</span>;</span><br><span class="line">    <span class="comment">// 首先尝试获取 /etc/hosts 中 hostname 对应的 IP</span></span><br><span class="line">    localAddress = InetAddress.getLocalHost();</span><br><span class="line">    Optional&lt;InetAddress&gt; addressOp = toValidAddress(localAddress);</span><br><span class="line">    <span class="keyword">if</span> (addressOp.isPresent()) &#123;</span><br><span class="line">        <span class="keyword">return</span> addressOp.get();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 没有找到适合注册的 IP，则开始轮询网卡</span></span><br><span class="line">    Enumeration&lt;NetworkInterface&gt; interfaces = NetworkInterface.getNetworkInterfaces();</span><br><span class="line">    <span class="keyword">if</span> (<span class="keyword">null</span> == interfaces) &#123;</span><br><span class="line">        <span class="keyword">return</span> localAddress;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">while</span> (interfaces.hasMoreElements()) &#123;</span><br><span class="line">        NetworkInterface network = interfaces.nextElement();</span><br><span class="line">        Enumeration&lt;InetAddress&gt; addresses = network.getInetAddresses();</span><br><span class="line">        <span class="keyword">while</span> (addresses.hasMoreElements()) &#123;</span><br><span class="line">            <span class="comment">// 返回第一个匹配的适合注册的 IP</span></span><br><span class="line">            Optional&lt;InetAddress&gt; addressOp = toValidAddress(addresses.nextElement());</span><br><span class="line">            <span class="keyword">if</span> (addressOp.isPresent()) &#123;</span><br><span class="line">                <span class="keyword">return</span> addressOp.get();</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> localAddress;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>Dubbo 这段选取本地地址的逻辑大致分成了两步</p><ol><li>先去 /etc/hosts 文件中找 hostname 对应的 IP 地址，找到则返回；找不到则转 2</li><li>轮询网卡，寻找合适的 IP 地址，找到则返回；找不到返回 null，在 getLocalAddress0 外侧还有一段逻辑，如果返回 null，则注册 127.0.0.1 这个本地回环地址</li></ol><p>首先强调下，这段逻辑并没有太大的问题，先别急着挑刺，让我们来分析下其中的一些细节，并进行验证。</p><h3 id="2-1-尝试获取-hostname-映射-IP"><a href="#2-1-尝试获取-hostname-映射-IP" class="headerlink" title="2.1 尝试获取 hostname 映射 IP"></a>2.1 尝试获取 hostname 映射 IP</h3><p>Dubbo 首先选取的是 hostname 对应的 IP，在源码中对应的 <code>InetAddress.getLocalHost();</code>  在 <code>*nix</code> 系统实际部署 Dubbo 应用时，可以首先使用 <code>hostname</code> 命令获取主机名</p><figure class="highlight shell"><table><tr><td class="code"><pre><span class="line">xujingfengdeMacBook-Pro:~ xujingfeng$ hostname</span><br><span class="line">xujingfengdeMacBook-Pro.local</span><br></pre></td></tr></table></figure><p>紧接着在 <code>/etc/hosts</code> 配置 IP 映射，为了验证 Dubbo 的机制，我们随意为 hostname 配置一个 IP 地址</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">127.0.0.1localhost</span><br><span class="line">1.2.3.4 xujingfengdeMacBook-Pro.local</span><br></pre></td></tr></table></figure><p>接着调用 <code>NetUtils.getLocalAddress0()</code> 进行验证，控制台打印如下：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">xujingfengdeMacBook-Pro.local/1.2.3.4</span><br></pre></td></tr></table></figure><h3 id="2-2-判定有效的-IP-地址"><a href="#2-2-判定有效的-IP-地址" class="headerlink" title="2.2 判定有效的 IP 地址"></a>2.2 判定有效的 IP 地址</h3><p>在 toValidAddress 逻辑中，Dubbo 存在以下逻辑判定一个 IP 地址是否有效</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">static</span> Optional&lt;InetAddress&gt; <span class="title">toValidAddress</span><span class="params">(InetAddress address)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (address <span class="keyword">instanceof</span> Inet6Address) &#123;</span><br><span class="line">        Inet6Address v6Address = (Inet6Address) address;</span><br><span class="line">        <span class="keyword">if</span> (isValidV6Address(v6Address)) &#123;</span><br><span class="line">            <span class="keyword">return</span> Optional.ofNullable(normalizeV6Address(v6Address));</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> (isValidV4Address(address)) &#123;</span><br><span class="line">        <span class="keyword">return</span> Optional.of(address);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> Optional.empty();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>依次校验其符合 Ipv6 或者 Ipv4 的 IP 规范，对于 Ipv6 的地址，见如下代码：</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">static</span> <span class="keyword">boolean</span> <span class="title">isValidV6Address</span><span class="params">(Inet6Address address)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">boolean</span> preferIpv6 = Boolean.getBoolean(<span class="string">"java.net.preferIPv6Addresses"</span>);</span><br><span class="line">    <span class="keyword">if</span> (!preferIpv6) &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> address.isReachable(<span class="number">100</span>);</span><br><span class="line">    &#125; <span class="keyword">catch</span> (IOException e) &#123;</span><br><span class="line">        <span class="comment">// ignore</span></span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>首先获取 <code>java.net.preferIPv6Addresses</code> 参数，其默认值为 false，鉴于大多数应用并没有使用 Ipv6 地址作为理想的注册 IP，这问题不大，紧接着通过 isReachable 判断网卡的连通性。例如一些网卡可能是 VPN/虚拟网卡的地址，如果没有配置路由表，往往无法连通，可以将之过滤。</p><p>对于 Ipv4 的地址，见如下代码：</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">static</span> <span class="keyword">boolean</span> <span class="title">isValidV4Address</span><span class="params">(InetAddress address)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (address == <span class="keyword">null</span> || address.isLoopbackAddress()) &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    String name = address.getHostAddress();</span><br><span class="line">    <span class="keyword">boolean</span> result = (name != <span class="keyword">null</span></span><br><span class="line">            &amp;&amp; IP_PATTERN.matcher(name).matches()</span><br><span class="line">            &amp;&amp; !Constants.ANYHOST_VALUE.equals(name)</span><br><span class="line">            &amp;&amp; !Constants.LOCALHOST_VALUE.equals(name));</span><br><span class="line">    <span class="keyword">return</span> result;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>对比 Ipv6 的判断，这里我们已经发现前后不对称的情况了</p><ul><li>Ipv4 相比 Ipv6 的逻辑多了 Ipv4 格式的正则校验、本地回环地址校验、ANYHOST 校验</li><li>Ipv4 相比 Ipv6 的逻辑少了网卡连通性的校验</li></ul><p>大家都知道，Ipv4 将 127.0.0.1 定为本地回环地址， Ipv6 也存在回环地址：0:0:0:0:0:0:0:1 或者表示为 ::1。改进建议也很明显，我们放到文末统一总结。</p><h3 id="2-3-轮询网卡"><a href="#2-3-轮询网卡" class="headerlink" title="2.3 轮询网卡"></a>2.3 轮询网卡</h3><p>如果上述地址获取为 null 则进入轮询网卡的逻辑（例如 hosts 未指定 hostname 的映射或者 hostname 配置成了 127.0.0.1 之类的地址便会导致获取到空的网卡地址），轮询网卡对应的源码是 <code>NetworkInterface.getNetworkInterfaces()</code> ，这里面涉及的知识点就比较多了，支撑起了我写这篇文章的素材，Dubbo 的逻辑并不复杂，进行简单的校验，返回第一个可用的 IP 即可。</p><p>性子急的读者可能忍不住了，多网卡！合适的网卡可能不止一个，Dubbo 怎么应对呢？按道理说，我们也替 Dubbo 说句公道话，客官要不你自己指定下？我们首先得对多网卡的场景达成一致看法，才能继续把这篇文章完成下去：我们只能<strong>尽可能</strong>过滤那些“<strong>不对</strong>”的网卡。Dubbo 看样子对所有网卡是一视同仁了，那么是不是可以尝试优化一下其中的逻辑呢？</p><p>许多开源的服务治理框架在 stackoverflow 或者其 issue 中，注册错 IP 相关的问题都十分高频，大多数都是轮询网卡出了问题。既然事情发展到这儿，势必需要了解一些网络、网卡的知识，我们才能过滤掉那些明显不适合 RPC 服务注册的 IP 地址了。</p><h2 id="3-Ifconfig-介绍"><a href="#3-Ifconfig-介绍" class="headerlink" title="3 Ifconfig 介绍"></a>3 Ifconfig 介绍</h2><p>我并没有想要让大家对后续的内容望而却步，特地选择了这个大家最熟悉的 Linux 命令！对于那些吐槽：“天呐，都 2019 年了，你怎么还在用 net-tools/ifconfig，iproute2/ip 了解一下”的言论，请大家视而不见。无论你使用的是 mac，还是 linux，都可以使用它去 CRUD 你的网卡配置。</p><h3 id="3-1-常用指令"><a href="#3-1-常用指令" class="headerlink" title="3.1 常用指令"></a>3.1 常用指令</h3><p><strong>启动关闭指定网卡：</strong></p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">ifconfig eth0 up</span><br><span class="line">ifconfig eth0 down</span><br></pre></td></tr></table></figure><p><code>ifconfig eth0 up</code> 为启动网卡 eth0，<code>ifconfig eth0 down</code> 为关闭网卡 eth0。ssh 登陆 linux 服务器操作的用户要小心执行这个操作了，千万不要蠢哭自己。不然你下一步就需要去 google：“禁用 eth0 网卡后如何远程连接 Linux 服务器” 了。</p><p><strong>为网卡配置和删除IPv6地址：</strong></p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">ifconfig eth0 add 33ffe:3240:800:1005::2/64    #为网卡eth0配置IPv6地址</span><br><span class="line">ifconfig eth0 del 33ffe:3240:800:1005::2/64    #为网卡eth0删除IPv6地址</span><br></pre></td></tr></table></figure><p><strong>用 ifconfig 修改 MAC 地址：</strong></p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">ifconfig eth0 hw ether 00:AA:BB:CC:dd:EE</span><br></pre></td></tr></table></figure><p><strong>配置 IP 地址：</strong></p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">[root@localhost ~]# ifconfig eth0 192.168.2.10</span><br><span class="line">[root@localhost ~]# ifconfig eth0 192.168.2.10 netmask 255.255.255.0</span><br><span class="line">[root@localhost ~]# ifconfig eth0 192.168.2.10 netmask 255.255.255.0 broadcast 192.168.2.255</span><br></pre></td></tr></table></figure><p><strong>启用和关闭arp协议：</strong></p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">ifconfig eth0 arp    #开启网卡eth0 的arp协议</span><br><span class="line">ifconfig eth0 -arp   #关闭网卡eth0 的arp协议</span><br></pre></td></tr></table></figure><p><strong>设置最大传输单元：</strong></p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">ifconfig eth0 mtu 1500    #设置能通过的最大数据包大小为 1500 bytes</span><br></pre></td></tr></table></figure><h3 id="3-2-查看网卡信息"><a href="#3-2-查看网卡信息" class="headerlink" title="3.2 查看网卡信息"></a>3.2 查看网卡信息</h3><p>在一台 ubuntu 上执行 <code>ifconfig -a</code> </p><figure class="highlight shell"><table><tr><td class="code"><pre><span class="line">ubuntu@VM-30-130-ubuntu:~$ ifconfig -a</span><br><span class="line">eth0      Link encap:Ethernet  HWaddr 52:54:00:a9:5f:ae</span><br><span class="line">          inet addr:10.154.30.130  Bcast:10.154.63.255  Mask:255.255.192.0</span><br><span class="line">          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1</span><br><span class="line">          RX packets:149673 errors:0 dropped:0 overruns:0 frame:0</span><br><span class="line">          TX packets:152271 errors:0 dropped:0 overruns:0 carrier:0</span><br><span class="line">          collisions:0 txqueuelen:1000</span><br><span class="line">          RX bytes:15205083 (15.2 MB)  TX bytes:21386362 (21.3 MB)</span><br><span class="line"></span><br><span class="line">lo        Link encap:Local Loopback</span><br><span class="line">          inet addr:127.0.0.1  Mask:255.0.0.0</span><br><span class="line">          UP LOOPBACK RUNNING  MTU:65536  Metric:1</span><br><span class="line">          RX packets:0 errors:0 dropped:0 overruns:0 frame:0</span><br><span class="line">          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0</span><br><span class="line">          collisions:0 txqueuelen:1</span><br><span class="line">          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)</span><br><span class="line">          </span><br><span class="line">docker0   Link encap:Ethernet  HWaddr 02:42:58:45:c1:15</span><br><span class="line">          inet addr:172.17.0.1  Bcast:172.17.255.255  Mask:255.255.0.0</span><br><span class="line">          UP BROADCAST MULTICAST  MTU:1500  Metric:1</span><br><span class="line">          RX packets:0 errors:0 dropped:0 overruns:0 frame:0</span><br><span class="line">          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0</span><br><span class="line">          collisions:0 txqueuelen:0</span><br><span class="line">          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)</span><br><span class="line"></span><br><span class="line">tun0      Link encap:UNSPEC  HWaddr 00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00</span><br><span class="line">          UP POINTOPOINT NOARP MULTICAST  MTU:1500  Metric:1</span><br><span class="line">          RX packets:0 errors:0 dropped:0 overruns:0 frame:0</span><br><span class="line">          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0</span><br><span class="line">          collisions:0 txqueuelen:100</span><br><span class="line">          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)</span><br></pre></td></tr></table></figure><p>为了防止黑客对我的 Linux 发起攻击，我还是偷偷对 IP 做了一点“改造”，请不要为难一个趁着打折+组团购买廉价云服务器的小伙子。对于部分网卡的详细解读:</p><p>eth0 表示第一块网卡， 其中 HWaddr 表示网卡的物理地址，可以看到目前这个网卡的物理地址(MAC 地址）是 02:42:38:52:70:54</p><p>inet addr 用来表示网卡的 IP 地址，此网卡的 IP 地址是 10.154.30.130，广播地址， Bcast: 172.18.255.255，掩码地址 Mask:255.255.0.0 </p><p>lo 是表示主机的回环地址，这个一般是用来测试一个网络程序，但又不想让局域网或外网的用户能够查看，只能在此台主机上运行和查看所用的网络接口。比如把 HTTPD 服务器的指定到回坏地址，在浏览器输入 127.0.0.1 就能看到你所架构的 WEB 网站了。但只有你能看得到，局域网的其它主机或用户则无从知晓。</p><p>第一行：连接类型：Ethernet（以太网）HWaddr（硬件mac地址）</p><p>第二行：网卡的IP地址、子网、掩码</p><p>第三行：UP（代表网卡开启状态）RUNNING（代表网卡的网线被接上）MULTICAST（支持组播）MTU:1500（最大传输单元）：1500字节（ifconfig 不加 -a 则无法看到 DOWN 的网卡）</p><p>第四、五行：接收、发送数据包情况统计</p><p>第七行：接收、发送数据字节数统计信息。</p><p>紧接着的两个网卡 docker0，tun0 是怎么出来的呢？我在我的 ubuntu 上装了 docker 和 openvpn。这两个东西应该是日常干扰我们做服务注册时的罪魁祸首了，当然，也有可能存在 eth1 这样的第二块网卡。ifconfig -a 看到的东西就对应了 JDK 的 api ：<code>NetworkInterface.getNetworkInterfaces()</code> 。我们简单做个总结，大致有三个干扰因素</p><ul><li>以 docker 网桥为首的虚拟网卡地址，毕竟这东西这么火，怎么也得单独列出来吧？</li><li>以 TUN/TAP 为代表的虚拟网卡地址，多为 VPN 场景</li><li>以 eth1 为代表的多网卡场景，有钱就可以装多网卡了！</li></ul><p>我们后续的篇幅将针对这些场景做分别的介绍，力求让大家没吃过猪肉，起码看下猪怎么跑的。</p><h2 id="4-干扰因素一：Docker-网桥"><a href="#4-干扰因素一：Docker-网桥" class="headerlink" title="4 干扰因素一：Docker 网桥"></a>4 干扰因素一：Docker 网桥</h2><p>熟悉 docker 的朋友应该知道 docker 会默认创建一个 docker0 的网桥，供容器实例连接。如果嫌默认的网桥不够直观，我们可以使用 bridge 模式自定义创建一个新的网桥：</p><figure class="highlight shell"><table><tr><td class="code"><pre><span class="line">ubuntu@VM-30-130-ubuntu:~$ docker network create kirito-bridge</span><br><span class="line">a38696dbbe58aa916894c674052c4aa6ab32266dcf6d8111fb794b8a344aa0d9</span><br><span class="line">ubuntu@VM-30-130-ubuntu:~$ ifconfig -a</span><br><span class="line">br-a38696dbbe58 Link encap:Ethernet  HWaddr 02:42:6e:aa:fd:0c</span><br><span class="line">          inet addr:172.19.0.1  Bcast:172.19.255.255  Mask:255.255.0.0</span><br><span class="line">          UP BROADCAST MULTICAST  MTU:1500  Metric:1</span><br><span class="line">          RX packets:0 errors:0 dropped:0 overruns:0 frame:0</span><br><span class="line">          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0</span><br><span class="line">          collisions:0 txqueuelen:0</span><br><span class="line">          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)</span><br></pre></td></tr></table></figure><p>使用 docker network 指令创建网桥之后，自动创建了对应的网卡，我只给出了 <code>ifconfig -a</code> 的增量返回部分，可以看出多了一个 br-a38696dbbe58 的网卡。</p><p>我有意区分了“网桥”和“网卡”，可以使用 bridge-utils/brctl 来查看网桥信息：</p><figure class="highlight shell"><table><tr><td class="code"><pre><span class="line">ubuntu@VM-30-130-ubuntu:~$ sudo brctl show</span><br><span class="line">bridge namebridge idSTP enabledinterfaces</span><br><span class="line">br-a38696dbbe588000.02426eaafd0cno</span><br><span class="line">docker08000.02425845c215no</span><br></pre></td></tr></table></figure><p>网桥是一个虚拟设备，这个设备只有 brctl show 能看到，网桥创建之后，会自动创建一个同名的网卡，并将这个网卡加入网桥。</p><h2 id="5-干扰因素二：TUN-TAP-虚拟网络设备"><a href="#5-干扰因素二：TUN-TAP-虚拟网络设备" class="headerlink" title="5 干扰因素二：TUN/TAP 虚拟网络设备"></a>5 干扰因素二：TUN/TAP 虚拟网络设备</h2><p>平时我们所说的虚拟网卡、虚拟机，大致都跟 TUN/TAP 有关。我的读者大多数是 Java 从业者，相信我下面的内容并没有太超纲，不要被陌生的名词唬住。对于被唬住的读者，也可以直接跳过 5.1~5.3，直接看 5.4 的实战。</p><h3 id="5-1-真实网卡工作原理"><a href="#5-1-真实网卡工作原理" class="headerlink" title="5.1 真实网卡工作原理"></a>5.1 真实网卡工作原理</h3><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="http://kirito.iocoder.cn/1918847-496d0e96c237f25a.png" alt="1918847-496d0e96c237f25a" title>                </div>                <div class="image-caption">1918847-496d0e96c237f25a</div>            </figure><p>上图中的 <strong>eth0</strong> 表示我们主机已有的真实的网卡接口 (<strong>interface</strong>)。</p><p>网卡接口 <strong>eth0</strong> 所代表的真实网卡通过网线(<strong>wire</strong>)和外部网络相连，该物理网卡收到的数据包会经由接口 <strong>eth0</strong> 传递给内核的网络协议栈(<strong>Network Stack</strong>)。然后协议栈对这些数据包进行进一步的处理。</p><p>对于一些错误的数据包,协议栈可以选择丢弃；对于不属于本机的数据包，协议栈可以选择转发；而对于确实是传递给本机的数据包,而且该数据包确实被上层的应用所需要，协议栈会通过 <strong>Socket API</strong> 告知上层正在等待的应用程序。</p><h3 id="5-2-TUN-工作原理"><a href="#5-2-TUN-工作原理" class="headerlink" title="5.2 TUN 工作原理"></a>5.2 TUN 工作原理</h3><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="http://kirito.iocoder.cn/1918847-85ea08bc89d9427e.png" alt="1918847-85ea08bc89d9427e" title>                </div>                <div class="image-caption">1918847-85ea08bc89d9427e</div>            </figure><p>我们知道，普通的网卡是通过网线来收发数据包的话，而 <strong>TUN</strong> 设备比较特殊，它通过一个文件收发数据包。</p><p>如上图所示，<strong>tunX</strong> 和上面的 <strong>eth0</strong> 在逻辑上面是等价的， <strong>tunX</strong> 也代表了一个网络接口,虽然这个接口是系统通过软件所模拟出来的.</p><p>网卡接口 <strong>tunX 所代表的虚拟网卡通过文件 /dev/tunX 与我们的应用程序(App)相连</strong>，应用程序每次使用 <strong>write</strong> 之类的系统调用将数据写入该文件，这些数据会以网络层数据包的形式，通过该虚拟网卡，经由网络接口 <strong>tunX</strong> 传递给网络协议栈，同时该应用程序也可以通过 <strong>read</strong> 之类的系统调用，经由文件 <strong>/dev/tunX</strong> 读取到协议栈向 <strong>tunX</strong> 传递的<strong>所有</strong>数据包。</p><p>此外，协议栈可以像操纵普通网卡一样来操纵 <strong>tunX</strong> 所代表的虚拟网卡。比如说，给 <strong>tunX</strong> 设定 <strong>IP</strong> 地址，设置路由，总之，在协议栈看来，<strong>tunX</strong> 所代表的网卡和其他普通的网卡区别不大，当然，硬要说区别，那还是有的,那就是 <strong>tunX</strong> 设备不存在 <strong>MAC</strong> 地址，这个很好理解，<strong>tunX</strong> 只模拟到了网络层，要 <strong>MAC</strong>地址没有任何意义。当然，如果是 <strong>tapX</strong> 的话，在协议栈的眼中，<strong>tapX</strong> 和真实网卡没有任何区别。</p><p>是不是有些懵了？我是谁，为什么我要在这篇文章里面学习 TUN！因为我们常用的 VPN 基本就是基于 TUN/TAP 搭建的，如果我们使用 <strong>TUN</strong> 设备搭建一个基于 <strong>UDP</strong> 的 <strong>VPN</strong> ，那么整个处理过程可能是这幅样子：</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="http://kirito.iocoder.cn/1918847-ac4155ec7e9489b2.png" alt="1918847-ac4155ec7e9489b2" title>                </div>                <div class="image-caption">1918847-ac4155ec7e9489b2</div>            </figure><h3 id="5-3-TAP-工作原理"><a href="#5-3-TAP-工作原理" class="headerlink" title="5.3 TAP 工作原理"></a>5.3 TAP 工作原理</h3><p><strong>TAP</strong> 设备与 <strong>TUN</strong> 设备工作方式完全相同，区别在于：</p><ol><li><strong>TUN</strong> 设备是一个三层设备，它只模拟到了 <strong>IP</strong> 层，即网络层 我们可以通过 <strong>/dev/tunX</strong> 文件收发 <strong>IP</strong> 层数据包，它无法与物理网卡做 <strong>bridge</strong>，但是可以通过三层交换（如  <strong>ip_forward</strong>）与物理网卡连通。可以使用<code>ifconfig</code>之类的命令给该设备设定 <strong>IP</strong> 地址。</li><li><strong>TAP</strong> 设备是一个二层设备，它比 <strong>TUN</strong> 更加深入，通过 <strong>/dev/tapX</strong> 文件可以收发 <strong>MAC</strong> 层数据包，即数据链路层，拥有 <strong>MAC</strong> 层功能，可以与物理网卡做 <strong>bridge</strong>，支持 <strong>MAC</strong> 层广播。同样的，我们也可以通过<code>ifconfig</code>之类的命令给该设备设定 <strong>IP</strong> 地址，你如果愿意，我们可以给它设定 <strong>MAC</strong> 地址。</li></ol><p>关于文章中出现的二层，三层，我这里说明一下，第一层是物理层，第二层是数据链路层，第三层是网络层，第四层是传输层。</p><h3 id="5-4-openvpn-实战"><a href="#5-4-openvpn-实战" class="headerlink" title="5.4 openvpn 实战"></a>5.4 openvpn 实战</h3><p>openvpn 是 Linux 上一款开源的 vpn 工具，我们通过它来复现出影响我们做网卡选择的场景。</p><p>安装 openvpn</p><figure class="highlight shell"><table><tr><td class="code"><pre><span class="line">sudo apt-get install openvpn</span><br></pre></td></tr></table></figure><p>安装一个 TUN 设备：</p><figure class="highlight shell"><table><tr><td class="code"><pre><span class="line">ubuntu@VM-30-130-ubuntu:~$ sudo openvpn --mktun --dev tun0</span><br><span class="line">Mon Apr 29 22:23:31 2019 TUN/TAP device tun0 opened</span><br><span class="line">Mon Apr 29 22:23:31 2019 Persist state set to: ON</span><br></pre></td></tr></table></figure><p>安装一个 TAP 设备：</p><figure class="highlight shell"><table><tr><td class="code"><pre><span class="line">ubuntu@VM-30-130-ubuntu:~$ sudo openvpn --mktun --dev tap0</span><br><span class="line">Mon Apr 29 22:24:36 2019 TUN/TAP device tap0 opened</span><br><span class="line">Mon Apr 29 22:24:36 2019 Persist state set to: ON</span><br></pre></td></tr></table></figure><p>执行 <code>ifconfig -a</code> 查看网卡，只给出增量的部分：</p><figure class="highlight shell"><table><tr><td class="code"><pre><span class="line">tap0      Link encap:Ethernet  HWaddr 7a:a2:a8:f1:6b:df</span><br><span class="line">          BROADCAST MULTICAST  MTU:1500  Metric:1</span><br><span class="line">          RX packets:0 errors:0 dropped:0 overruns:0 frame:0</span><br><span class="line">          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0</span><br><span class="line">          collisions:0 txqueuelen:100</span><br><span class="line">          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)</span><br><span class="line"></span><br><span class="line">tun0      Link encap:UNSPEC  HWaddr 00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00</span><br><span class="line">          inet addr:10.154.30.131  P-t-P:10.154.30.131  Mask:255.255.255.255</span><br><span class="line">          UP POINTOPOINT NOARP MULTICAST  MTU:1500  Metric:1</span><br><span class="line">          RX packets:0 errors:0 dropped:0 overruns:0 frame:0</span><br><span class="line">          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0</span><br><span class="line">          collisions:0 txqueuelen:100</span><br><span class="line">          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)</span><br></pre></td></tr></table></figure><p>这样就解释了文章一开始为什么会有 tun0 这样的网卡了。这里读者可能会有疑惑，使用 ifconfig 不是也可以创建 tap 和 tun 网卡吗？当然啦，openvpn 是一个 vpn 工具，只能创建名为 tunX/tapX 的网卡，其遵守着一定的规范，ifconfig 可以随意创建，但没人认那些随意创建的网卡。</p><h2 id="6-干扰因素三：多网卡"><a href="#6-干扰因素三：多网卡" class="headerlink" title="6 干扰因素三：多网卡"></a>6 干扰因素三：多网卡</h2><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="http://kirito.iocoder.cn/image-20190429223515625.png" alt="image-20190429223515625" title>                </div>                <div class="image-caption">image-20190429223515625</div>            </figure><p>这个没有太多好说的，有多张真实的网卡，从普哥那儿搞到如上的 IP 信息。</p><h2 id="7-MAC-下的差异"><a href="#7-MAC-下的差异" class="headerlink" title="7 MAC 下的差异"></a>7 MAC 下的差异</h2><p>虽然 ifconfig 等指令是 <code>*nux</code> 通用的，但是其展示信息，网卡相关的属性和命名都有较大的差异。例如这是我 MAC 下执行 <code>ifconfig -a</code> 的返回：</p><figure class="highlight shell"><table><tr><td class="code"><pre><span class="line">xujingfengdeMacBook-Pro:dubbo-in-action xujingfeng$ ifconfig -a</span><br><span class="line">lo0: flags=8049&lt;UP,LOOPBACK,RUNNING,MULTICAST&gt; mtu 16384</span><br><span class="line">options=1203&lt;RXCSUM,TXCSUM,TXSTATUS,SW_TIMESTAMP&gt;</span><br><span class="line">inet 127.0.0.1 netmask 0xff000000</span><br><span class="line">inet6 ::1 prefixlen 128</span><br><span class="line">inet6 fe80::1%lo0 prefixlen 64 scopeid 0x1</span><br><span class="line">nd6 options=201&lt;PERFORMNUD,DAD&gt;</span><br><span class="line">gif0: flags=8010&lt;POINTOPOINT,MULTICAST&gt; mtu 1280</span><br><span class="line">stf0: flags=0&lt;&gt; mtu 1280</span><br><span class="line">XHC0: flags=0&lt;&gt; mtu 0</span><br><span class="line">XHC20: flags=0&lt;&gt; mtu 0</span><br><span class="line">en0: flags=8863&lt;UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST&gt; mtu 1500</span><br><span class="line">ether 88:e9:fe:88:a0:76</span><br><span class="line">inet6 fe80::1cab:f689:60d1:bacb%en0 prefixlen 64 secured scopeid 0x6</span><br><span class="line">inet 30.130.11.242 netmask 0xffffff80 broadcast 30.130.11.255</span><br><span class="line">nd6 options=201&lt;PERFORMNUD,DAD&gt;</span><br><span class="line">media: autoselect</span><br><span class="line">status: active</span><br><span class="line">p2p0: flags=8843&lt;UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST&gt; mtu 2304</span><br><span class="line">ether 0a:e9:fe:88:a0:76</span><br><span class="line">media: autoselect</span><br><span class="line">status: inactive</span><br><span class="line">awdl0: flags=8943&lt;UP,BROADCAST,RUNNING,PROMISC,SIMPLEX,MULTICAST&gt; mtu 1484</span><br><span class="line">ether 66:d2:8c:8c:dd:85</span><br><span class="line">inet6 fe80::64d2:8cff:fe8c:dd85%awdl0 prefixlen 64 scopeid 0x8</span><br><span class="line">nd6 options=201&lt;PERFORMNUD,DAD&gt;</span><br><span class="line">media: autoselect</span><br><span class="line">status: active</span><br><span class="line">en1: flags=8963&lt;UP,BROADCAST,SMART,RUNNING,PROMISC,SIMPLEX,MULTICAST&gt; mtu 1500</span><br><span class="line">options=60&lt;TSO4,TSO6&gt;</span><br><span class="line">ether aa:00:d0:13:0e:01</span><br><span class="line">media: autoselect &lt;full-duplex&gt;</span><br><span class="line">status: inactive</span><br><span class="line">en2: flags=8963&lt;UP,BROADCAST,SMART,RUNNING,PROMISC,SIMPLEX,MULTICAST&gt; mtu 1500</span><br><span class="line">options=60&lt;TSO4,TSO6&gt;</span><br><span class="line">ether aa:00:d0:13:0e:00</span><br><span class="line">media: autoselect &lt;full-duplex&gt;</span><br><span class="line">status: inactive</span><br><span class="line">bridge0: flags=8863&lt;UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST&gt; mtu 1500</span><br><span class="line">options=63&lt;RXCSUM,TXCSUM,TSO4,TSO6&gt;</span><br><span class="line">ether aa:00:d0:13:0e:01</span><br><span class="line">Configuration:</span><br><span class="line">id 0:0:0:0:0:0 priority 0 hellotime 0 fwddelay 0</span><br><span class="line">maxage 0 holdcnt 0 proto stp maxaddr 100 timeout 1200</span><br><span class="line">root id 0:0:0:0:0:0 priority 0 ifcost 0 port 0</span><br><span class="line">ipfilter disabled flags 0x2</span><br><span class="line">member: en1 flags=3&lt;LEARNING,DISCOVER&gt;</span><br><span class="line">        ifmaxaddr 0 port 9 priority 0 path cost 0</span><br><span class="line">member: en2 flags=3&lt;LEARNING,DISCOVER&gt;</span><br><span class="line">        ifmaxaddr 0 port 10 priority 0 path cost 0</span><br><span class="line">nd6 options=201&lt;PERFORMNUD,DAD&gt;</span><br><span class="line">media: &lt;unknown type&gt;</span><br><span class="line">status: inactive</span><br><span class="line">utun0: flags=8051&lt;UP,POINTOPOINT,RUNNING,MULTICAST&gt; mtu 2000</span><br><span class="line">inet6 fe80::3fe0:3e8b:384:9968%utun0 prefixlen 64 scopeid 0xc</span><br><span class="line">nd6 options=201&lt;PERFORMNUD,DAD&gt;</span><br><span class="line">utun1: flags=8051&lt;UP,POINTOPOINT,RUNNING,MULTICAST&gt; mtu 1380</span><br><span class="line">inet6 fe80::7894:3abc:5abd:457d%utun1 prefixlen 64 scopeid 0xd</span><br><span class="line">nd6 options=201&lt;PERFORMNUD,DAD&gt;</span><br></pre></td></tr></table></figure><p>内容很多，我挑几点差异简述下：</p><ul><li><p>内容展示形式不一样，没有 Linux 下的接收、发送数据字节数等统计信息</p></li><li><p>真实网卡的命名不一样：eth0 -&gt; en0</p></li><li>虚拟网卡的命名格式不一样：tun/tap -&gt; utun</li></ul><p>对于这些常见网卡命名的解读，我摘抄一部分来自 stackoverflow 的回答：</p><blockquote><p>In arbitrary order of my familarity / widespread relevance:</p><p><code>lo0</code> is loopback.</p><p><code>en0</code> at one point “ethernet”, now is WiFi (and I have no idea what extra <code>en1</code> or <code>en2</code> are used for).</p><p><code>fw0</code> is the FireWire network interface.</p><p><code>stf0</code> is an <a href="https://www.freebsd.org/cgi/man.cgi?gif(4" target="_blank" rel="noopener">IPv6 to IPv4 tunnel interface</a>) to support <a href="http://en.wikipedia.org/wiki/6to4" target="_blank" rel="noopener">the transition</a> from IPv4 to the IPv6 standard.</p><p><code>gif0</code> is a more <a href="https://www.freebsd.org/cgi/man.cgi?gif(4" target="_blank" rel="noopener">generic tunneling interface</a>) [46]-to-[46].</p><p><code>awdl0</code> is <a href="https://stackoverflow.com/questions/19587701/what-is-awdl-apple-wireless-direct-link-and-how-does-it-work" target="_blank" rel="noopener">Apple Wireless Direct Link</a></p><p><code>p2p0</code> is related to AWDL features. Either as an old version, or virtual interface with different semantics than <code>awdl</code>.</p><p>the “Network” panel in System Preferences to see what network devices “exist” or “can exist” with current configuration.</p><p>many VPNs will add additional devices, often “utun#” or “utap#” following <a href="https://en.wikipedia.org/wiki/TUN/TAP" target="_blank" rel="noopener">TUN/TAP (L3/L2)</a>virtual networking devices.</p><p>use <code>netstat -nr</code> to see how traffic is currently routed via network devices according to destination.</p><p>interface naming conventions started in BSD were retained in OS X / macOS, and now there also additions.</p></blockquote><h2 id="8-Dubbo-改进建议"><a href="#8-Dubbo-改进建议" class="headerlink" title="8 Dubbo 改进建议"></a>8 Dubbo 改进建议</h2><p>我们进行了以上探索，算是对网卡有一点了解了。回过头来看看 Dubbo 获取网卡的逻辑，是否可以做出改进呢？</p><p><strong>Dubbo Action 1:</strong> </p><p>保持 Ipv4 和 Ipv6 的一致性校验。为 Ipv4 增加连通性校验；为 Ipv6 增加 LoopBack 和 ANYHOST 等校验。</p><p><strong>Dubbo Action 2:</strong> </p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line">NetworkInterface network = interfaces.nextElement();</span><br><span class="line"><span class="keyword">if</span> (network.isLoopback() || network.isVirtual() || !network.isUp()) &#123;</span><br><span class="line">    <span class="keyword">continue</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>JDK 提供了以上的 API，我们可以利用起来，过滤一部分一定不正确的网卡。</p><p><strong>Dubbo Action 3:</strong> </p><p>我们本文花了较多的篇幅介绍了 docker 和 TUN/TAP 两种场景导致的虚拟网卡的问题，算是较为常见的一个影响因素，虽然他们的命名具有固定性，如 docker0、tunX、tapX，但我觉得通过网卡名称的判断方式去过滤注册 IP 有一些 hack，所以不建议 dubbo contributor 提出相应的 pr 去增加这些 hack 判断，尽管可能会对判断有所帮助。</p><p>对于真实多网卡、内外网 IP 共存的场景，不能仅仅是框架层在做努力，用户也需要做一些事，就像爱情一样，我可以主动一点，但你也得反馈，才能发展出故事。</p><p><strong>Dubbo User Action 1:</strong></p><p>可以配置 <code>/etc/hosts</code> 文件，将 hostname 对应的 IP 显式配置进去。</p><p><strong>Dubbo User Action 2:</strong></p><p>可以使用启动参数去显式指定注册的 IP：</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line">-DDUBBO_IP_TO_REGISTRY=<span class="number">1.2</span><span class="number">.3</span><span class="number">.4</span></span><br></pre></td></tr></table></figure><p>也可以指定 Dubbo 服务绑定在哪块网卡上：</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line">-DDUBBO_IP_TO_BIND=<span class="number">1.2</span><span class="number">.3</span><span class="number">.4</span></span><br></pre></td></tr></table></figure><h2 id="9-参考文章"><a href="#9-参考文章" class="headerlink" title="9 参考文章"></a>9 参考文章</h2><p><a href="https://www.jianshu.com/p/09f9375b7fa7" target="_blank" rel="noopener">TUN/TAP 设备浅析</a></p><p><a href="https://stackoverflow.com/questions/29958143/what-are-en0-en1-p2p-and-so-on-that-are-displayed-after-executing-ifconfig" target="_blank" rel="noopener">what-are-en0-en1-p2p-and-so-on-that-are-displayed-after-executing-ifconfig</a></p><p><a href="http://dubbo.apache.org/zh-cn/blog/dubbo-network-interfaces.html" target="_blank" rel="noopener">原文地址</a> | <a href="https://github.com/apache/dubbo-website/edit/asf-site/blog/zh-cn/dubbo-network-interfaces.md" target="_blank" rel="noopener">纠错</a></p>]]></content>
    
    <summary type="html">
    
      研究 Dubbo 网卡地址注册时的一点思考
    
    </summary>
    
      <category term="dubbo" scheme="https://ningyu1.github.io/categories/dubbo/"/>
    
    
      <category term="dubbo" scheme="https://ningyu1.github.io/tags/dubbo/"/>
    
      <category term="网卡地址注册" scheme="https://ningyu1.github.io/tags/%E7%BD%91%E5%8D%A1%E5%9C%B0%E5%9D%80%E6%B3%A8%E5%86%8C/"/>
    
  </entry>
  
  <entry>
    <title>Trouble Shooting —— CAS Server集群环境下TGC验证问题</title>
    <link href="https://ningyu1.github.io/20191015/118-cas-server-pit.html"/>
    <id>https://ningyu1.github.io/20191015/118-cas-server-pit.html</id>
    <published>2019-10-15T06:05:53.000Z</published>
    <updated>2024-04-23T01:48:15.031Z</updated>
    
    <content type="html"><![CDATA[<p>之前写了一篇cas server故障排查的文章<a href="https://ningyu1.github.io/blog/20180316/70-cas-server-pit.html">《Trouble Shooting —— CAS Server集群环境下TGC验证问题排查，需要开启会话保持》</a> ，之前这张文章写的是开启会话保持来解决这个故障，但是这个方式没有充分的发挥cas server集群的功能，因此这次讲一下另外一种解决方法，真正的解决cas server集群环境下的问题</p><p>我们可以点击上面的文章链接进去回顾一下具体的问题。</p><h1 id="问题分析"><a href="#问题分析" class="headerlink" title="问题分析"></a>问题分析</h1><p>错误是出在cas ticket cookie在cas server服务端验证时出现了问题，这里解释一下这个问题现象。</p><p>当我们cas server部署两台服务时，前端采用nginx做负载均衡，当我们访问cas server的时候nginx会随机选择一个服务端返回给前端，因此在第一次登陆的时候是由serverA生成的ticket，ticket中包含了客户端ip，当下次访问时路由到serverB时ticket验证的时候获取的客户端ip不一致导致的问题。</p><p>重点是在类：<code>org.apereo.cas.web.support.DefaultCasCookieValueManager</code>.<code>obtainCookieValue</code>方法</p><p>这里以cas server5.0.4版本为例看一下源码：</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> String <span class="title">obtainCookieValue</span><span class="params">(<span class="keyword">final</span> Cookie cookie, <span class="keyword">final</span> HttpServletRequest request)</span> </span>&#123;</span><br><span class="line">       <span class="keyword">final</span> String cookieValue = <span class="keyword">this</span>.cipherExecutor.decode(cookie.getValue());</span><br><span class="line">       LOGGER.debug(<span class="string">"Decoded cookie value is [&#123;&#125;]"</span>, cookieValue);</span><br><span class="line">       <span class="keyword">if</span> (StringUtils.isBlank(cookieValue)) &#123;</span><br><span class="line">           LOGGER.debug(<span class="string">"Retrieved decoded cookie value is blank. Failed to decode cookie [&#123;&#125;]"</span>, cookie.getName());</span><br><span class="line">           <span class="keyword">return</span> <span class="keyword">null</span>;</span><br><span class="line">       &#125;</span><br><span class="line"></span><br><span class="line">       <span class="keyword">final</span> String[] cookieParts = cookieValue.split(String.valueOf(COOKIE_FIELD_SEPARATOR));</span><br><span class="line">       <span class="keyword">if</span> (cookieParts.length != COOKIE_FIELDS_LENGTH) &#123;</span><br><span class="line">           <span class="keyword">throw</span> <span class="keyword">new</span> IllegalStateException(<span class="string">"Invalid cookie. Required fields are missing"</span>);</span><br><span class="line">       &#125;</span><br><span class="line">       <span class="keyword">final</span> String value = cookieParts[<span class="number">0</span>];</span><br><span class="line">       <span class="keyword">final</span> String remoteAddr = cookieParts[<span class="number">1</span>];</span><br><span class="line">       <span class="keyword">final</span> String userAgent = cookieParts[<span class="number">2</span>];</span><br><span class="line"></span><br><span class="line">       <span class="keyword">if</span> (StringUtils.isBlank(value) || StringUtils.isBlank(remoteAddr)</span><br><span class="line">               || StringUtils.isBlank(userAgent)) &#123;</span><br><span class="line">           <span class="keyword">throw</span> <span class="keyword">new</span> IllegalStateException(<span class="string">"Invalid cookie. Required fields are empty"</span>);</span><br><span class="line">       &#125;</span><br><span class="line"></span><br><span class="line">       <span class="keyword">if</span> (!remoteAddr.equals(request.getRemoteAddr())) &#123;</span><br><span class="line">           <span class="keyword">throw</span> <span class="keyword">new</span> IllegalStateException(<span class="string">"Invalid cookie. Required remote address does not match "</span></span><br><span class="line">                   + request.getRemoteAddr());</span><br><span class="line">       &#125;</span><br><span class="line"></span><br><span class="line">       <span class="keyword">final</span> String agent = WebUtils.getHttpServletRequestUserAgent(request);</span><br><span class="line">       <span class="keyword">if</span> (!userAgent.equals(agent)) &#123;</span><br><span class="line">           <span class="keyword">throw</span> <span class="keyword">new</span> IllegalStateException(<span class="string">"Invalid cookie. Required user-agent does not match "</span> + agent);</span><br><span class="line">       &#125;</span><br><span class="line">       <span class="keyword">return</span> value;</span><br><span class="line">   &#125;</span><br></pre></td></tr></table></figure><p>我们可以看出在ticket解决之后进行验证时获取的客户端ip是从：<code>request.getRemoteAddr()</code>获取的，这种方式获取在4、7层负载均衡的时候是无法获取真实的客户端ip。</p><p>接下来我们再看一下生成ticket的规则代码：</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> String <span class="title">buildCookieValue</span><span class="params">(<span class="keyword">final</span> String givenCookieValue, <span class="keyword">final</span> HttpServletRequest request)</span> </span>&#123;</span><br><span class="line">       <span class="keyword">final</span> StringBuilder builder = <span class="keyword">new</span> StringBuilder(givenCookieValue);</span><br><span class="line"></span><br><span class="line">       <span class="keyword">final</span> ClientInfo clientInfo = ClientInfoHolder.getClientInfo();</span><br><span class="line">       builder.append(COOKIE_FIELD_SEPARATOR);</span><br><span class="line">       builder.append(clientInfo.getClientIpAddress());</span><br><span class="line">       </span><br><span class="line">       <span class="keyword">final</span> String userAgent = WebUtils.getHttpServletRequestUserAgent(request);</span><br><span class="line">       <span class="keyword">if</span> (StringUtils.isBlank(userAgent)) &#123;</span><br><span class="line">           <span class="keyword">throw</span> <span class="keyword">new</span> IllegalStateException(<span class="string">"Request does not specify a user-agent"</span>);</span><br><span class="line">       &#125;</span><br><span class="line">       builder.append(COOKIE_FIELD_SEPARATOR);</span><br><span class="line">       builder.append(userAgent);</span><br><span class="line"></span><br><span class="line">       <span class="keyword">final</span> String res = builder.toString();</span><br><span class="line">       LOGGER.debug(<span class="string">"Encoding cookie value [&#123;&#125;]"</span>, res);</span><br><span class="line">       <span class="keyword">return</span> <span class="keyword">this</span>.cipherExecutor.encode(res);</span><br><span class="line">   &#125;</span><br></pre></td></tr></table></figure><p>ticket的生成是从<code>clientInfo.getClientIpAddress()</code>获取客户端ip</p><p>我们再看<code>org.apereo.inspektr.common.web.ClientInfo</code></p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="title">ClientInfo</span><span class="params">(<span class="keyword">final</span> HttpServletRequest request,</span></span></span><br><span class="line"><span class="function"><span class="params">                      <span class="keyword">final</span> String alternateServerAddrHeaderName,</span></span></span><br><span class="line"><span class="function"><span class="params">                      <span class="keyword">final</span> String alternateLocalAddrHeaderName,</span></span></span><br><span class="line"><span class="function"><span class="params">                      <span class="keyword">final</span> <span class="keyword">boolean</span> useServerHostAddress)</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            String serverIpAddress = request != <span class="keyword">null</span> ? request.getLocalAddr() : <span class="keyword">null</span>;</span><br><span class="line">            String clientIpAddress = request != <span class="keyword">null</span> ? request.getRemoteAddr() : <span class="keyword">null</span>;</span><br><span class="line"></span><br><span class="line">            <span class="keyword">if</span> (request != <span class="keyword">null</span>) &#123;</span><br><span class="line">                <span class="keyword">if</span> (useServerHostAddress) &#123;</span><br><span class="line">                    serverIpAddress = Inet4Address.getLocalHost().getHostAddress();</span><br><span class="line">                &#125; <span class="keyword">else</span> <span class="keyword">if</span> (alternateServerAddrHeaderName != <span class="keyword">null</span> &amp;&amp; !alternateServerAddrHeaderName.isEmpty()) &#123;</span><br><span class="line">                    serverIpAddress = request.getHeader(alternateServerAddrHeaderName) != <span class="keyword">null</span></span><br><span class="line">                            ? request.getHeader(alternateServerAddrHeaderName) : request.getLocalAddr();</span><br><span class="line">                &#125;</span><br><span class="line"></span><br><span class="line">                <span class="keyword">if</span> (alternateLocalAddrHeaderName != <span class="keyword">null</span> &amp;&amp; !alternateLocalAddrHeaderName.isEmpty()) &#123;</span><br><span class="line">                    clientIpAddress = request.getHeader(alternateLocalAddrHeaderName) != <span class="keyword">null</span> ? request.getHeader</span><br><span class="line">                            (alternateLocalAddrHeaderName) : request.getRemoteAddr();</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            <span class="keyword">this</span>.serverIpAddress = serverIpAddress == <span class="keyword">null</span> ? <span class="string">"unknown"</span> : serverIpAddress;</span><br><span class="line">            <span class="keyword">this</span>.clientIpAddress = clientIpAddress == <span class="keyword">null</span> ? <span class="string">"unknown"</span> : clientIpAddress;</span><br><span class="line"></span><br><span class="line">        &#125; <span class="keyword">catch</span> (<span class="keyword">final</span> Exception e) &#123;</span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> RuntimeException(e);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure><p>从中看出5.0.4版本支持了传入header的来自定义客户端ip获取</p><p>但是5.0.4依然有问题它没有改全，从上面的ticket生成逻辑(<code>org.apereo.cas.web.support.DefaultCasCookieValueManager</code>)中可以看出来，生成的时候是通过：<code>clientInfo.getClientIpAddress()</code>，但是验证的时候是通过：<code>request.getRemoteAddr()</code>获取验证的，所以只要加了4,7层负载的话就会存在这个问题。</p><p>以上就是整个问题的分析过程，接下来看我们怎么来解决这个问题。</p><h1 id="解决方案"><a href="#解决方案" class="headerlink" title="解决方案"></a>解决方案</h1><p>cas在tikcet生成与验证的时候都有配置项提供自定义。</p><p>只要我们关闭ticket加解密就可以规避这个问题，但是安全性上稍微低一些，如果不想关闭ticket加解密休需要修改配置和代码。</p><ol><li><p>如果开启cas.tgc.cipherEnabled=true</p><ol><li><p>需要同时多台server配置相同的cas.tgc.signingKey、cas.tgc.encryptionKey保证cookie加解密秘钥相同</p></li><li><p>修改代码让验证cookie获取客户端ip保持一致，如果是cas server 5.0.4版本可以修改<code>org.apereo.cas.audit.spi.config.CasCoreAuditConfiguration</code>类中的<code>org.apereo.inspektr.common.web.ClientInfoThreadLocalFilter</code>增加初始化参数来自定义客户端ip获取headerName</p></li><li><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">@Bean</span><br><span class="line">          public FilterRegistrationBean casClientInfoLoggingFilter() &#123;</span><br><span class="line">              final FilterRegistrationBean bean = new FilterRegistrationBean();</span><br><span class="line">              bean.setFilter(new ClientInfoThreadLocalFilter());</span><br><span class="line">              bean.setUrlPatterns(Collections.singleton(&quot;/*&quot;));</span><br><span class="line">              bean.setName(&quot;CAS Client Info Logging Filter&quot;);</span><br><span class="line">              bean.addInitParameter(ClientInfoThreadLocalFilter.CONST_IP_ADDRESS_HEADER,&quot;X-Forwarded-For&quot;);</span><br><span class="line">              return bean;</span><br><span class="line">          &#125;</span><br></pre></td></tr></table></figure></li><li><p>修改<code>org.apereo.cas.web.support.DefaultCasCookieValueManager</code>.<code>obtainCookieValue</code>代码，保持生成tikcet和验证ticket时获取客户端ip都使用<code>clientInfo.getClientIpAddress()</code></p></li></ol></li><li><p>关闭cas tgc的加解密：cas.tgc.cipherEnabled=false，牺牲安全性就可以规避这个问题</p></li></ol><p><span style="color:blue"><em>建议使用配置的方式来调整，这样可以充分的发挥集群的功能。</em></span></p><p>世界和平、Keep Real！</p>]]></content>
    
    <summary type="html">
    
      Trouble Shooting —— CAS Server集群环境下TGC验证问题
    
    </summary>
    
      <category term="CAS" scheme="https://ningyu1.github.io/categories/CAS/"/>
    
    
      <category term="trouble shooting" scheme="https://ningyu1.github.io/tags/trouble-shooting/"/>
    
      <category term="CAS" scheme="https://ningyu1.github.io/tags/CAS/"/>
    
      <category term="iphash" scheme="https://ningyu1.github.io/tags/iphash/"/>
    
      <category term="TomcatRedisSessionManager" scheme="https://ningyu1.github.io/tags/TomcatRedisSessionManager/"/>
    
      <category term="Invalid cookie. Required remote address does not match ip" scheme="https://ningyu1.github.io/tags/Invalid-cookie-Required-remote-address-does-not-match-ip/"/>
    
  </entry>
  
  <entry>
    <title>Tomcat对[RFC 3986]规范的支持，导致一些特殊请求报错</title>
    <link href="https://ningyu1.github.io/20190626/117-tomcat-RFC3986.html"/>
    <id>https://ningyu1.github.io/20190626/117-tomcat-RFC3986.html</id>
    <published>2019-06-26T09:46:21.000Z</published>
    <updated>2024-04-23T01:48:15.031Z</updated>
    
    <content type="html"><![CDATA[<h1 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h1><p>​    Tomcat在升级版本时遇到的一些问题，在升级tomcat版本后发现原有的功能莫名其妙的出现了错误，我们接下来看一下具体的问题以及分析一下具体的原因</p><h1 id="问题分析"><a href="#问题分析" class="headerlink" title="问题分析"></a>问题分析</h1><p>我们来具体看一下报错的请求报文</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line">http:<span class="comment">//localhost:8080/xx-rest/v1/report/salesOrder?</span></span><br><span class="line">salesStatusList[]=&amp;orderStatus=&amp;salesTypeList[]=&amp;itIsClose=&amp;salesNo=&amp;warehouseNo=&amp;orderNo=&amp;referenceNo=</span><br><span class="line">&amp;skuName=&amp;skuBarcode=&amp;shopNoList[]=&amp;creTimeBeg=<span class="number">2019</span>-<span class="number">06</span>-<span class="number">21</span>+<span class="number">00</span>:<span class="number">00</span>:<span class="number">00</span>&amp;creTimeEnd=<span class="number">2019</span>-<span class="number">06</span>-<span class="number">21</span>+<span class="number">23</span>:<span class="number">59</span>:<span class="number">59</span></span><br><span class="line">&amp;platformOrderTimeBeg=&amp;platformOrderTimeEnd=&amp;endFinishTimeBeg=&amp;endFinishTimeEnd=</span><br><span class="line">&amp;page=<span class="number">1</span>&amp;pageSize=<span class="number">50</span>&amp;customerNo=&amp;__preventCache=<span class="number">1561084397458</span></span><br></pre></td></tr></table></figure><p>调用后返回的是400的错误，并且进行了测试，当请求参数中存在[]时就会发生错误，去掉后可正常访问</p><p>我们查看到后台的错误具体如下：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">2019/6/21 上午11:48:052019-06-21 11:48:05.590 [http-apr-8080-exec-3] INFO com.xxx.doFilter(CORSFilter.java:75) - CORS filter &gt;&gt;&gt;&gt;&gt;&gt; Because host:xxx.domain.com and origin:https://xxx.domain.com so Access-Control-Allow-Origin:https://xxx.domain.com</span><br><span class="line">2019/6/21 上午11:48:05Jun 21, 2019 11:48:05 AM org.apache.coyote.http11.AbstractHttp11Processor process</span><br><span class="line">2019/6/21 上午11:48:05INFO: Error parsing HTTP request header</span><br><span class="line">2019/6/21 上午11:48:05 Note: further occurrences of HTTP header parsing errors will be logged at DEBUG level.</span><br><span class="line">2019/6/21 上午11:48:05java.lang.IllegalArgumentException: Invalid character found in the request target. The valid characters are defined in RFC 7230 and RFC 3986</span><br><span class="line">2019/6/21 上午11:48:05    at org.apache.coyote.http11.InternalAprInputBuffer.parseRequestLine(InternalAprInputBuffer.java:240)</span><br><span class="line">2019/6/21 上午11:48:05    at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1052)</span><br><span class="line">2019/6/21 上午11:48:05    at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:637)</span><br><span class="line">2019/6/21 上午11:48:05    at org.apache.tomcat.util.net.AprEndpoint$SocketWithOptionsProcessor.run(AprEndpoint.java:2492)</span><br><span class="line">2019/6/21 上午11:48:05    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)</span><br><span class="line">2019/6/21 上午11:48:05    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)</span><br><span class="line">2019/6/21 上午11:48:05    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)</span><br><span class="line">2019/6/21 上午11:48:05    at java.lang.Thread.run(Thread.java:748)</span><br></pre></td></tr></table></figure><p>非法参数：<code>Invalid character found in the request target. The valid characters are defined in RFC 7230 and RFC 3986</code></p><p>意思说的是请求中包含了无效的字符，具体去看RFC 7230 and RFC 3986规范中的字符定义。</p><p>这个问题很纳闷！因为以前是没有遇到过的，于是先去看tomcat对RFC 7230 and RFC 3986规范的支持情况。</p><p><span style="color:red"><strong>Tomcat从 7.0.73, 8.0.39, 8.5.7 版本后添加了对Url的限制，遵循的就是RFC 7230 and RFC 3986规范。那我们就先搞清楚规范中描述的是什么。</strong></span></p><h2 id="RFC-3986"><a href="#RFC-3986" class="headerlink" title="[RFC 3986]"></a>[RFC 3986]</h2><p><a href="https://www.ietf.org/rfc/rfc3986.txt" target="_blank" rel="noopener"><a href="https://www.ietf.org/rfc/rfc3986.txt" target="_blank" rel="noopener">https://www.ietf.org/rfc/rfc3986.txt</a></a></p><p>我们只需关注规范中字符定义这块，我截图的地方</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/rfc3986/1.png" alt title>                </div>                <div class="image-caption"></div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/rfc3986/2.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p>意思说的是规范中定义url中只允许包含英文字母(a-zA-Z)、数字(0-9)、-_.~四个特殊字符，保留字符用作特定领域分隔符，如果url中有用到需要进行转义</p><p>保留的字符有：<code>! * ’ ( ) ; : @ &amp; = + $ , / ? # [ ]</code></p><p> 我们知道了RFC3986规范中的定义和tomcat支持规范的版本后问题就很明了了，我们来看一下我们使用的tomcat版本具体是多少，经过查看具体如下：</p><table><thead><tr><th>环境</th><th>版本</th></tr></thead><tbody><tr><td>UAT</td><td>apache-tomcat-7.0.67</td></tr><tr><td>PRD</td><td>apache-tomcat-7.0.67</td></tr><tr><td>dev、qa jdk7</td><td>使用的镜像是：tomcat:7，具体的tomcat版本是： Apache Tomcat/7.0.70</td></tr><tr><td>dev、qa jdk8</td><td>使用的镜像是：tomcat:7-jre8，具体的tomcat版本是：Apache Tomcat/7.0.88</td></tr></tbody></table><p>问题就出在这里，我们使用的tomcat版本刚好是遵循RFC3986规范的tomcat版本。</p><h1 id="解决办法"><a href="#解决办法" class="headerlink" title="解决办法"></a>解决办法</h1><p>知道了原因之后，具体的解决办法就比较明确了，大概有以下几种：</p><ol><li>对请求编码解码。 encodeURI，decodeURI，但是有的时候参数名中出现保留字符是比较麻烦的，需要对参数名进行转义，后台再解析参数时需要先对参数名解码后再通过参数名获取具体参数值</li><li>tomcat降版本到7.0.73以下，这个是比较好操作的</li></ol>]]></content>
    
    <summary type="html">
    
      Tomcat对[RFC 3986]规范的支持，导致一些特殊请求报错
    
    </summary>
    
      <category term="Tomcat" scheme="https://ningyu1.github.io/categories/Tomcat/"/>
    
      <category term="RFC3986" scheme="https://ningyu1.github.io/categories/Tomcat/RFC3986/"/>
    
    
      <category term="Tomcat" scheme="https://ningyu1.github.io/tags/Tomcat/"/>
    
      <category term="RFC3986" scheme="https://ningyu1.github.io/tags/RFC3986/"/>
    
  </entry>
  
  <entry>
    <title>使用StopWatch优雅的输出执行耗时</title>
    <link href="https://ningyu1.github.io/20190505/116-stop-watch.html"/>
    <id>https://ningyu1.github.io/20190505/116-stop-watch.html</id>
    <published>2019-05-05T05:20:21.000Z</published>
    <updated>2024-04-23T01:48:15.031Z</updated>
    
    <content type="html"><![CDATA[<h1 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h1><p>​    有时我们在做开发的时候需要记录每个任务执行时间，或者记录一段代码执行时间，最简单的方法就是打印当前时间与执行完时间的差值，然后这样如果执行大量测试的话就很麻烦，并且不直观，如果想对执行的时间做进一步控制，则需要在程序中很多地方修改，目前spring-framework提供了一个StopWatch类可以做类似任务执行时间控制，也就是封装了一个对开始时间，结束时间记录工具</p><h1 id="示例"><a href="#示例" class="headerlink" title="示例"></a>示例</h1><p>我们来看几个示例</p><h2 id="统计输出总耗时"><a href="#统计输出总耗时" class="headerlink" title="统计输出总耗时"></a>统计输出总耗时</h2><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> org.springframework.util.StopWatch;</span><br><span class="line"> </span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">SpringStopWatchExample</span> </span>&#123;</span><br><span class="line"> </span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span> <span class="params">(String[] args)</span> <span class="keyword">throws</span> InterruptedException </span>&#123;</span><br><span class="line">        StopWatch sw = <span class="keyword">new</span> StopWatch();</span><br><span class="line">        sw.start();</span><br><span class="line">        <span class="comment">//long task simulation</span></span><br><span class="line">        Thread.sleep(<span class="number">1000</span>);</span><br><span class="line">        sw.stop();</span><br><span class="line">        System.out.println(sw.getTotalTimeMillis());</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>输出</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">1013</span><br></pre></td></tr></table></figure><h2 id="输出最后一个任务的耗时"><a href="#输出最后一个任务的耗时" class="headerlink" title="输出最后一个任务的耗时"></a>输出最后一个任务的耗时</h2><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">SpringStopWatchExample2</span> </span>&#123;</span><br><span class="line"> </span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span> <span class="params">(String[] args)</span> <span class="keyword">throws</span> InterruptedException </span>&#123;</span><br><span class="line">        StopWatch sw = <span class="keyword">new</span> StopWatch();</span><br><span class="line">        sw.start(<span class="string">"A"</span>);<span class="comment">//setting a task name</span></span><br><span class="line">        <span class="comment">//long task simulation</span></span><br><span class="line">        Thread.sleep(<span class="number">1000</span>);</span><br><span class="line">        sw.stop();</span><br><span class="line">        System.out.println(sw.getLastTaskTimeMillis());</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>输出</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">1009</span><br></pre></td></tr></table></figure><h2 id="以优雅的格式打出所有任务的耗时以及占比"><a href="#以优雅的格式打出所有任务的耗时以及占比" class="headerlink" title="以优雅的格式打出所有任务的耗时以及占比"></a>以优雅的格式打出所有任务的耗时以及占比</h2><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> org.springframework.util.StopWatch;</span><br><span class="line"> </span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">SpringStopWatchExample3</span> </span>&#123;</span><br><span class="line"> </span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span> <span class="params">(String[] args)</span> <span class="keyword">throws</span> InterruptedException </span>&#123;</span><br><span class="line">        StopWatch sw = <span class="keyword">new</span> StopWatch();</span><br><span class="line">        sw.start(<span class="string">"A"</span>);</span><br><span class="line">        Thread.sleep(<span class="number">500</span>);</span><br><span class="line">        sw.stop();</span><br><span class="line">        sw.start(<span class="string">"B"</span>);</span><br><span class="line">        Thread.sleep(<span class="number">300</span>);</span><br><span class="line">        sw.stop();</span><br><span class="line">        sw.start(<span class="string">"C"</span>);</span><br><span class="line">        Thread.sleep(<span class="number">200</span>);</span><br><span class="line">        sw.stop();</span><br><span class="line">        System.out.println(sw.prettyPrint());</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>输出</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">StopWatch &apos;&apos;: running time (millis) = 1031</span><br><span class="line">-----------------------------------------</span><br><span class="line">ms     %     Task name</span><br><span class="line">-----------------------------------------</span><br><span class="line">00514  050%  A</span><br><span class="line">00302  029%  B</span><br><span class="line">00215  021%  C</span><br></pre></td></tr></table></figure><h2 id="序列服务输出耗时信息"><a href="#序列服务输出耗时信息" class="headerlink" title="序列服务输出耗时信息"></a>序列服务输出耗时信息</h2><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">long</span> <span class="title">nextSeq</span><span class="params">(String name)</span> </span>&#123;</span><br><span class="line">    StopWatch watch = <span class="keyword">new</span> StopWatch();</span><br><span class="line">    watch.start(<span class="string">"单序列获取总消耗"</span>);</span><br><span class="line">    <span class="keyword">long</span> sequence = generator.generateId(name);</span><br><span class="line">    watch.stop();</span><br><span class="line">    logger.info(watch.prettyPrint());</span><br><span class="line">    <span class="keyword">return</span> sequence;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h1 id="更多用法"><a href="#更多用法" class="headerlink" title="更多用法"></a>更多用法</h1><p>不同的打印结果</p><ol><li>getTotalTimeSeconds()  获取总耗时秒，同时也有获取毫秒的方法</li><li>prettyPrint()  优雅的格式打印结果，表格形式</li><li>shortSummary()  返回简短的总耗时描述</li><li>getTaskCount()  返回统计时间任务的数量</li><li>getLastTaskInfo().getTaskName() 返回最后一个任务TaskInfo对象的名称<br>更多查看<a href="https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/util/StopWatch.html" target="_blank" rel="noopener">文档</a></li></ol><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>以后我们统计代码执行效率建议大家都使用这个工具来进行输出，不需要在starttime、endtime再相减计算，用优雅的方式来完成这件事情。</p>]]></content>
    
    <summary type="html">
    
      使用StopWatch优雅的输出执行耗时
    
    </summary>
    
      <category term="Java" scheme="https://ningyu1.github.io/categories/Java/"/>
    
      <category term="Spring" scheme="https://ningyu1.github.io/categories/Java/Spring/"/>
    
      <category term="StopWatch" scheme="https://ningyu1.github.io/categories/Java/Spring/StopWatch/"/>
    
    
      <category term="Java" scheme="https://ningyu1.github.io/tags/Java/"/>
    
      <category term="StopWatch" scheme="https://ningyu1.github.io/tags/StopWatch/"/>
    
      <category term="Spring" scheme="https://ningyu1.github.io/tags/Spring/"/>
    
  </entry>
  
  <entry>
    <title>PageHelper生成Count语句逻辑分析</title>
    <link href="https://ningyu1.github.io/20190422/115-mybatis-pagehelper-count.html"/>
    <id>https://ningyu1.github.io/20190422/115-mybatis-pagehelper-count.html</id>
    <published>2019-04-22T11:24:21.000Z</published>
    <updated>2024-04-23T01:48:15.031Z</updated>
    
    <content type="html"><![CDATA[<h1 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h1><p>在使用Mybatis分页插件时经常会遇到Mybatis生成的count语句效率很低，今天我们就看一下Mybatis中的countsql的生成逻辑是什么，为何会生成低效的countsql，怎么写sql可以避免低效countsql。</p><h1 id="源码分析"><a href="#源码分析" class="headerlink" title="源码分析"></a>源码分析</h1><p><code>com.github.pagehelper.parser.CountSqlParser</code></p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 将sql转换为count查询</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> select</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">sqlToCount</span><span class="params">(Select select)</span> </span>&#123;</span><br><span class="line">    SelectBody selectBody = select.getSelectBody();</span><br><span class="line">    <span class="comment">// 是否能简化count查询</span></span><br><span class="line">    <span class="keyword">if</span> (selectBody <span class="keyword">instanceof</span> PlainSelect &amp;&amp; isSimpleCount((PlainSelect) selectBody)) &#123;</span><br><span class="line">        ((PlainSelect) selectBody).setSelectItems(COUNT_ITEM);</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        PlainSelect plainSelect = <span class="keyword">new</span> PlainSelect();</span><br><span class="line">        SubSelect subSelect = <span class="keyword">new</span> SubSelect();</span><br><span class="line">        subSelect.setSelectBody(selectBody);</span><br><span class="line">        subSelect.setAlias(TABLE_ALIAS);</span><br><span class="line">        plainSelect.setFromItem(subSelect);</span><br><span class="line">        plainSelect.setSelectItems(COUNT_ITEM);</span><br><span class="line">        select.setSelectBody(plainSelect);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"> </span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 是否可以用简单的count查询方式</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> select</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@return</span></span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">boolean</span> <span class="title">isSimpleCount</span><span class="params">(PlainSelect select)</span> </span>&#123;</span><br><span class="line">    <span class="comment">//包含group by的时候不可以</span></span><br><span class="line">    <span class="keyword">if</span> (select.getGroupByColumnReferences() != <span class="keyword">null</span>) &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">//包含distinct的时候不可以</span></span><br><span class="line">    <span class="keyword">if</span> (select.getDistinct() != <span class="keyword">null</span>) &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">for</span> (SelectItem item : select.getSelectItems()) &#123;</span><br><span class="line">        <span class="comment">//select列中包含参数的时候不可以，否则会引起参数个数错误</span></span><br><span class="line">        <span class="keyword">if</span> (item.toString().contains(<span class="string">"?"</span>)) &#123;</span><br><span class="line">            <span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">//如果查询列中包含函数，也不可以，函数可能会聚合列</span></span><br><span class="line">        <span class="keyword">if</span> (item <span class="keyword">instanceof</span> SelectExpressionItem) &#123;</span><br><span class="line">            <span class="keyword">if</span> (((SelectExpressionItem) item).getExpression() <span class="keyword">instanceof</span> Function) &#123;</span><br><span class="line">                <span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">true</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>看代码比较清晰明了，话不多说，总结如下：</p><ol><li>sql包含group by的时候，会生成带子查询的低效countsql</li><li>sql包含distinct的时候，会生成带子查询的低效countsql</li><li>sql的列中包含参数的时候，会生成带子查询的低效countsql</li><li>sql的列中包含函数的时候，会生成带子查询的低效countsql</li></ol><p><a href="https://github.com/pagehelper/Mybatis-PageHelper/releases" target="_blank" rel="noopener">pagehelper官方最新版</a>更新到了：5.1.6</p><p>5.1.5版本有一个优化是对函数进行区分，如果是聚合函数生成带子查询的countsql，如果非聚合函数生成简单的countsql。</p><p>具体可以查看：<a href="https://github.com/pagehelper/Mybatis-PageHelper/releases" target="_blank" rel="noopener">V5.1.5 changelog</a></p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/mybatis-pagehelper-count/1.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p>看了一下4.2.1 —— 5.1.6之间的版本，除了5.1.5支持了函数区分的支持以外，其他版本没有countsql生成相关的优化，所以sql中包含group by、distinct、列中包含参数，聚合函数会生成低效的countsql。</p>]]></content>
    
    <summary type="html">
    
      PageHelper生成Count语句逻辑分析
    
    </summary>
    
      <category term="Java" scheme="https://ningyu1.github.io/categories/Java/"/>
    
      <category term="Mybatis" scheme="https://ningyu1.github.io/categories/Java/Mybatis/"/>
    
    
      <category term="Java" scheme="https://ningyu1.github.io/tags/Java/"/>
    
      <category term="Mybatis" scheme="https://ningyu1.github.io/tags/Mybatis/"/>
    
      <category term="PageHelper" scheme="https://ningyu1.github.io/tags/PageHelper/"/>
    
  </entry>
  
  <entry>
    <title>Mybatis PageHelper分页遇到的坑，莫名其妙的增加了limit ?,?</title>
    <link href="https://ningyu1.github.io/20190402/114-mybatis-pagehelper-pit.html"/>
    <id>https://ningyu1.github.io/20190402/114-mybatis-pagehelper-pit.html</id>
    <published>2019-04-02T07:36:21.000Z</published>
    <updated>2024-04-23T01:48:15.031Z</updated>
    
    <content type="html"><![CDATA[<h1 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h1><p>在使用Mybatis分页插件PageHelper的时候我相信或多或少都会遇到这样的问题，明明没有分页的语句执行后sql语句被自动添加了limit ？,?，看起来非常的莫名其妙，其实这个问题非常明确，就是Java基本功的问题，由于开发使用的是pagehelper.startPage方式，弄出这个问题就是对pagehelper的分页原理不理解而导致的。</p><p>首先我们先介绍一下Mybatis的分页用法。</p><h1 id="Mybatis分页用法"><a href="#Mybatis分页用法" class="headerlink" title="Mybatis分页用法"></a>Mybatis分页用法</h1><h2 id="RowBounds用法"><a href="#RowBounds用法" class="headerlink" title="RowBounds用法"></a>RowBounds用法</h2><p>显示的使用RowBounds参数，这种方法是最安全的，在经过Mybatis处理的时候会根据RowBounds参数来自动添加limit表达式，但是这种方法有个缺点，就是需要分页的方法都要增加RowBounds这个参数，其实也很正常，这也是最原始的用法，只是现在开发被惯叼了，又想少写代码又想使用最全的功能。</p><h2 id="PageHelper用法"><a href="#PageHelper用法" class="headerlink" title="PageHelper用法"></a>PageHelper用法</h2><p>使用pagehelper，这个是一个使用率最高的Mybatis分页插件，使用起来也比较方便，但是包装了很多高级的功能需要理解他的机制，要不然很容易写出bug，例如这次的问题，sql中明明没有使用分页，但是最后执行的sql语句中多了limit。</p><p>pagehelper的4.x以上的版本使用pageHelper.startPage方法来进行分页，这种方法是在需要执行的sql之前调用一次，例如：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">PageHelper.startPage(1, 10);</span><br><span class="line">list = countryMapper.selectIf(param1);</span><br></pre></td></tr></table></figure><h2 id="PageHelper优缺点"><a href="#PageHelper优缺点" class="headerlink" title="PageHelper优缺点"></a>PageHelper优缺点</h2><p>优点：</p><ol><li>使用简单</li><li>对sql无侵入</li></ol><p>缺点：</p><ol><li>不是及其安全的方式（至少跟采用RowBounds参数进行分页的方式来比较）</li></ol><h2 id="PageHelper分页原理"><a href="#PageHelper分页原理" class="headerlink" title="PageHelper分页原理"></a>PageHelper分页原理</h2><p>PageHelper采用ThreadLocal来进行分页标识设置，pagehelper保证的是当代码执行到Executor 方法时出现错误，它会在finally快中清理ThreadLocal中的分页标识，如果代码没有执行到Executor方法就出现异常，那就会造成ThreadLocal污染。<br>当我们执行<code>PageHelper.startPage(1, 10);</code>这一行的时候，其实是在当前线程的ThreadLocal中设置了分页的变量，当执行到<code>countryMapper.selectIf(param1);</code>的时候会通过Executor拦截，从ThreadLocal中获取分页标记，如果存在分页标记就在当前执行的sql语句中增加分页表达式，当Executor拦截执行的时候finally中会清理ThreadLocal中的分页变量。</p><h1 id="问题分析"><a href="#问题分析" class="headerlink" title="问题分析"></a>问题分析</h1><p>接下来让我们看一下出现的错误</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">2019-03-28 00:00:00.150 [DubboServerHandler-xx.xx.xx.xx:yyyy-thread-191] DEBUG org.apache.ibatis.logging.jdbc.BaseJdbcLogger.debug(BaseJdbcLogger.java:139) - ==&gt;  Preparing: SELECT id, job...skipping...</span><br><span class="line">### The error may involve xx.yy.zz.XXXXMapper.loadExpress-Inline</span><br><span class="line">### The error occurred while setting parameters</span><br><span class="line">### SQL: select     field as fieldName   from table where field= ?    order by id ASC limit 1 limit ?,?</span><br><span class="line">### Cause: com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near &apos;limit 0,50&apos; at line 5</span><br><span class="line">; bad SQL grammar []; nested exception is com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near &apos;limit 0,50&apos; at line 5, dubbo version: 2.5.3, current host: 10.24.232.204 #-# org.springframework.jdbc.BadSqlGrammarException:</span><br><span class="line">### Error querying database.  Cause: com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near &apos;limit 0,50&apos; at line 5</span><br><span class="line">### The error may exist in URL [jar:file:/usr/local/dubbo/xxx/lib/xxx-yyy-1.0.0.jar!/sqlMap/express/XXXXMapper.xml]</span><br><span class="line">### The error may involve xx.yy.zz.XXXXMapper.loadExpress-Inline</span><br><span class="line">### The error occurred while setting parameters</span><br><span class="line">### SQL: select     field as fieldName  from table where field= ?    order by id ASC limit 1 limit ?,?</span><br><span class="line">### Cause: com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near &apos;limit 0,50&apos; at line 5</span><br><span class="line">; bad SQL grammar []; nested exception is com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near &apos;limit 0,50&apos; at line 5</span><br></pre></td></tr></table></figure><p>我们来看一下方法实现代码</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">@Override</span><br><span class="line">public XXXVo loadExpress(param)&#123;</span><br><span class="line">    XXXEntity param = new XXXEntity();</span><br><span class="line">    param.setBusinessNo(businessNo);</span><br><span class="line">    XXXEntity  entity = XXXMapper.loadExpress(param);</span><br><span class="line">    //省略无关代码</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>我们看一下sqlmap</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">&lt;select id=&quot;loadExpress&quot; parameterType=&quot;xx.yy.zz.XXXEntity&quot;</span><br><span class="line">        resultType=&quot;xx.yy.zz.XXXEntity&quot;&gt;</span><br><span class="line">    select</span><br><span class="line">        field as fieldName </span><br><span class="line">    from table </span><br><span class="line">        where business_no= #&#123;businessNo&#125;</span><br><span class="line">        order by id ASC limit 1</span><br><span class="line">&lt;/select&gt;</span><br></pre></td></tr></table></figure><p>这个方法是唯一入口，并且没有执行pagehelper.startPage，但是语句在执行的时候自动加了<code>limit 0,50</code>，这个分页的数据非常像是页面的列表查询设置的分页变量。</p><p>其实问题所在是执行线程被污染，因为我们都是使用线程池的，当前执行所用的线程是会被放回池子中被反复使用的，如果某个执行污染了线程那就会影响下一个执行的代码。</p><p>我们来举个例子，看如下代码：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">public void method01() &#123; //@1</span><br><span class="line">    PageHelper.startPage(1, 10); //@5</span><br><span class="line">    List&lt;Country&gt; list;</span><br><span class="line">    if(param1.get() != null)&#123; //@2</span><br><span class="line">        list = countryMapper.selectIf(param1);</span><br><span class="line">    &#125; else &#123;</span><br><span class="line">        list = new ArrayList&lt;Country&gt;();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"> </span><br><span class="line">public List&lt;Country&gt; method02() &#123; //@3</span><br><span class="line">    List&lt;Country&gt; list = countryMapper.selectNotPage(param1); //@4</span><br><span class="line">    return list;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>假设我们一个请求进来调用method01()方法，随后另外的请求进来调用的是method02()方法，假定我们的线程池数量是1，让两个请求使用同一个线程。</p><ol><li>请求从<code>@1</code>处进入</li><li>执行到<code>@2</code>处发生了NullPointException异常</li><li>随后的请求从<code>@3</code>处进入</li><li>执行<code>@4</code>处时执行的sql会被自动添加limit ?,?。</li></ol><h2 id="错误原因"><a href="#错误原因" class="headerlink" title="错误原因"></a>错误原因</h2><p>因为前一个请求执行<code>@5</code>处时设置了分页标识到ThreadLocal中，当执行到<code>@2</code>处时触发了异常，<code>@5</code>处设置的分页变量没有被消费和清理，线程被污染，因此另一个请求进来复用了这个线程，当执行到<code>@4</code>处时PageHelper拦截器从ThreadLocal中获取到分页变量并自动增加了<code>limit ?,?</code>语句。</p><h1 id="解决方法"><a href="#解决方法" class="headerlink" title="解决方法"></a>解决方法</h1><h2 id="推荐使用的方式"><a href="#推荐使用的方式" class="headerlink" title="推荐使用的方式"></a>推荐使用的方式</h2><p>在执行<code>PageHelper.startPage(1, 10);</code>之后紧跟着执行<code>Executor</code>，避免这两行之间出现错误，将我们上图举例中的代码修改一下如下：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">public void method01() &#123;</span><br><span class="line">    List&lt;Country&gt; list;</span><br><span class="line">    if(param1.get() != null)&#123;</span><br><span class="line">        //两行紧挨着执行，避免出现异常</span><br><span class="line">        PageHelper.startPage(1, 10);</span><br><span class="line">        list = countryMapper.selectIf(param1);</span><br><span class="line">    &#125; else &#123;</span><br><span class="line">        list = new ArrayList&lt;Country&gt;();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><span style="color:blue"><em>ps. 要保证两行紧挨着执行，并且在执行了PageHelper.startPage之后与countryMapper.selectIf之前保证不会出错误。</em></span></p><h2 id="不推荐使用方式"><a href="#不推荐使用方式" class="headerlink" title="不推荐使用方式"></a>不推荐使用方式</h2><p>使用finally快进行清理，如下图：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">public void method01() &#123;</span><br><span class="line">    List&lt;Country&gt; list;</span><br><span class="line">    if(param1.get() != null)&#123;</span><br><span class="line">        //修改面太大，代码侵入太多</span><br><span class="line">        try &#123;</span><br><span class="line">            PageHelper.startPage(1, 10);</span><br><span class="line">            list = countryMapper.selectIf(param1);</span><br><span class="line">        &#125; finally &#123;</span><br><span class="line">            PageHelper.clearPage();</span><br><span class="line">        &#125;</span><br><span class="line">    &#125; else &#123;</span><br><span class="line">        list = new ArrayList&lt;Country&gt;();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><span style="color:blue"><em>ps. 参考pagehelper的<a href="https://github.com/pagehelper/Mybatis-PageHelper/blob/master/wikis/en/HowToUse.md" target="_blank" rel="noopener">安全使用指南</a></em></span></p><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>只有理解了pagehelper的分页机制之后才能别面写法带来的bug，我相信当我提到pagehelper采用ThreadLocal实现的分页标识传递时，应该有很多人已经明白了问题所在。任何通过ThreadLocal传递变量时都有可能出现线程污染的问题，尽量规避掉。</p><p>ThreadLocal传递变量是个非常好的方式，俗称为隐士传参，具有包装透明的效果，正因为这个特性更要注意安全清理的问题，需要全面思考代码执行过程中是否会出现错误，出现错误是否能友好的清理掉线程中透传的变量，如果处理不得当就会造成线程污染。</p>]]></content>
    
    <summary type="html">
    
      Mybatis PageHelper分页遇到的坑，莫名其妙的增加了limit ?,?
    
    </summary>
    
      <category term="Java" scheme="https://ningyu1.github.io/categories/Java/"/>
    
      <category term="Mybatis" scheme="https://ningyu1.github.io/categories/Java/Mybatis/"/>
    
    
      <category term="Java" scheme="https://ningyu1.github.io/tags/Java/"/>
    
      <category term="Mybatis" scheme="https://ningyu1.github.io/tags/Mybatis/"/>
    
      <category term="PageHelper" scheme="https://ningyu1.github.io/tags/PageHelper/"/>
    
  </entry>
  
  <entry>
    <title>Java对象复制类库性能对比</title>
    <link href="https://ningyu1.github.io/20190322/113-object-copy.html"/>
    <id>https://ningyu1.github.io/20190322/113-object-copy.html</id>
    <published>2019-03-22T08:03:21.000Z</published>
    <updated>2024-04-23T01:48:15.030Z</updated>
    
    <content type="html"><![CDATA[<h1 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h1><p>在开发中我们经常会用到对象之间的互相拷贝，<code>Java</code>中对象拷贝的类库也比较多，常见的有<code>Spring BeanUtils</code>，<code>Apache BeanUtils</code>，等并且在很多大厂公司对对象拷贝也有详尽的说明，避免大家踩坑。</p><h1 id="功能对比"><a href="#功能对比" class="headerlink" title="功能对比"></a>功能对比</h1><table><thead><tr><th style="text-align:left">耗时(毫秒)</th><th style="text-align:left">1000次</th><th style="text-align:left">10,000次</th><th style="text-align:left">100,100次</th></tr></thead><tbody><tr><td style="text-align:left"><code>Apache BeanUtils</code></td><td style="text-align:left">298</td><td style="text-align:left">983</td><td style="text-align:left">4211</td></tr><tr><td style="text-align:left"><code>Cglib BeanCopier</code></td><td style="text-align:left">89</td><td style="text-align:left">120</td><td style="text-align:left">203</td></tr><tr><td style="text-align:left"><code>Spring BeanUtils</code></td><td style="text-align:left">92</td><td style="text-align:left">160</td><td style="text-align:left">524</td></tr></tbody></table><h1 id="性能对比"><a href="#性能对比" class="headerlink" title="性能对比"></a>性能对比</h1><table><thead><tr><th style="text-align:left"></th><th style="text-align:left">Apache BeanUtils</th><th style="text-align:left">Cglib BeanCopier</th><th style="text-align:left">Spring BeanUtils</th></tr></thead><tbody><tr><td style="text-align:left">非public类</td><td style="text-align:left">不支持</td><td style="text-align:left">支持</td><td style="text-align:left">支持</td></tr><tr><td style="text-align:left">基本类型与装箱类型，int-&gt;Integer，Integer-&gt;int</td><td style="text-align:left">支持，可以copy</td><td style="text-align:left">不支持，不copy</td><td style="text-align:left">不支持，不copy</td></tr><tr><td style="text-align:left">int-&gt;long，long-&gt;int，int-&gt;Long，Integer-&gt;long</td><td style="text-align:left">不支持</td><td style="text-align:left">不支持</td><td style="text-align:left">不支持</td></tr><tr><td style="text-align:left">源对象相同属性无get方法</td><td style="text-align:left">不支持 不copy</td><td style="text-align:left">不支持 不copy</td><td style="text-align:left">不支持 不copy</td></tr><tr><td style="text-align:left">目标对象相同属性无get方法</td><td style="text-align:left">支持</td><td style="text-align:left">不支持</td><td style="text-align:left">支持</td></tr><tr><td style="text-align:left">目标对象相同属性无set方法</td><td style="text-align:left">不copy，不报错</td><td style="text-align:left">报错</td><td style="text-align:left">不copy，不报错</td></tr><tr><td style="text-align:left">源对象相同属性无set方法</td><td style="text-align:left">支持</td><td style="text-align:left">支持</td><td style="text-align:left">支持</td></tr><tr><td style="text-align:left">目标对象相同属性set方法返回非void</td><td style="text-align:left">不设置，其他正常属性可以copy</td><td style="text-align:left">不设置，导致其他属性都无法copy</td><td style="text-align:left">支持，能够copy</td></tr><tr><td style="text-align:left">目标对象多字段</td><td style="text-align:left">支持</td><td style="text-align:left">支持</td><td style="text-align:left">支持</td></tr><tr><td style="text-align:left">目标对象少字段</td><td style="text-align:left">支持</td><td style="text-align:left">支持</td><td style="text-align:left">支持</td></tr></tbody></table><h1 id="结论"><a href="#结论" class="headerlink" title="结论"></a>结论</h1><p>从性能对比来看：</p><ol><li><code>cglib</code>的<code>BeanCopier</code>最好， <code>Spring BeanUtils</code>稍微差点，但也还可以，<code>Apache BeanUtils</code>性能最差</li><li>从功能对比来看，<code>cglib</code> 在set方法返回非void时，会导致其他属性无法copy，目标没有set方法时，会报错，还存在并且有多项不支持的情况</li></ol>]]></content>
    
    <summary type="html">
    
      Java对象复制类库性能对比
    
    </summary>
    
      <category term="Java" scheme="https://ningyu1.github.io/categories/Java/"/>
    
    
      <category term="Java" scheme="https://ningyu1.github.io/tags/Java/"/>
    
      <category term="对象拷贝" scheme="https://ningyu1.github.io/tags/%E5%AF%B9%E8%B1%A1%E6%8B%B7%E8%B4%9D/"/>
    
  </entry>
  
  <entry>
    <title>谈一谈前后端分离的优劣势</title>
    <link href="https://ningyu1.github.io/20190311/112-front-end-and-back-end-separation.html"/>
    <id>https://ningyu1.github.io/20190311/112-front-end-and-back-end-separation.html</id>
    <published>2019-03-11T11:11:21.000Z</published>
    <updated>2024-04-23T01:48:15.030Z</updated>
    
    <content type="html"><![CDATA[<p>从公司内网转载一篇同事整理的资料，关于前后端分离的优劣方面，整理的比较全面，推荐不明白为什么要前后端分离的同学阅读。</p><p><span style="color:blue"><em>首先说明：前后端分离并非仅仅只是一种开发模式，而是一种架构模式（前后端分离架构）。</em></span></p><h1 id="开发模式对比"><a href="#开发模式对比" class="headerlink" title="开发模式对比"></a>开发模式对比</h1><table><thead><tr><th style="text-align:left">序号</th><th style="text-align:left">老的开发模式（后端以Java为例）</th><th style="text-align:left">新的开发模式（后端以Java为例）</th></tr></thead><tbody><tr><td style="text-align:left">1</td><td style="text-align:left">产品经理/领导/客户提出需求</td><td style="text-align:left">产品经理/领导/客户提出需求</td></tr><tr><td style="text-align:left">2</td><td style="text-align:left">UI做出设计图</td><td style="text-align:left">UI做出设计图</td></tr><tr><td style="text-align:left">3</td><td style="text-align:left">前端工程师做html页面</td><td style="text-align:left">前后端约定接口&amp;数据&amp;参数</td></tr><tr><td style="text-align:left">4</td><td style="text-align:left">后端工程师将html页面套成jsp页面</td><td style="text-align:left">前后端并行开发</td></tr><tr><td style="text-align:left">5</td><td style="text-align:left">集成出现问题 前端返工 后端返工</td><td style="text-align:left">前后端集成</td></tr><tr><td style="text-align:left">6</td><td style="text-align:left">二次集成 集成成功 交付</td><td style="text-align:left">前端页面调整 集成成功 交互</td></tr></tbody></table><h1 id="请求方式对比"><a href="#请求方式对比" class="headerlink" title="请求方式对比"></a>请求方式对比</h1><table><thead><tr><th style="text-align:left">序号</th><th style="text-align:left">老的开发模式（后端以Java为例）</th><th style="text-align:left">新的开发模式（后端以Java为例）</th></tr></thead><tbody><tr><td style="text-align:left">1</td><td style="text-align:left">客户端请求</td><td style="text-align:left">浏览器发送请求</td></tr><tr><td style="text-align:left">2</td><td style="text-align:left">服务端的servlet或controller接收请求</td><td style="text-align:left">直接到达html页面（前端控制路由与渲染页面，整个项目开发的权重前移）</td></tr><tr><td style="text-align:left">3</td><td style="text-align:left">调用service,dao代码完成业务逻辑</td><td style="text-align:left">html页面负责调用服务端接口产生数据</td></tr><tr><td style="text-align:left">4</td><td style="text-align:left">返回jsp jsp展现一些动态的代码</td><td style="text-align:left">填充html，展现动态效果，在页面上进行解析并操作DOM或数据</td></tr></tbody></table><h1 id="前后端分离的优势"><a href="#前后端分离的优势" class="headerlink" title="前后端分离的优势"></a>前后端分离的优势</h1><table><thead><tr><th style="text-align:left">序号</th><th style="text-align:left">优势</th></tr></thead><tbody><tr><td style="text-align:left">1</td><td style="text-align:left">可以实现真正的前后端解耦，前端服务器使用nginx，后端/应用服务器使用tomcat，加快整体响应速度</td></tr><tr><td style="text-align:left">2</td><td style="text-align:left">发现bug，可以快速定位是谁的问题，不会出现互相踢皮球的现象</td></tr><tr><td style="text-align:left">3</td><td style="text-align:left">减少后端服务器的并发/负载压力</td></tr><tr><td style="text-align:left">4</td><td style="text-align:left">即使后端服务暂时超时或者宕机了，前端页面也会正常访问，只不过数据刷不出来而已</td></tr><tr><td style="text-align:left">5</td><td style="text-align:left">多端应用</td></tr><tr><td style="text-align:left">6</td><td style="text-align:left">页面显示的东西再多也不怕，因为是异步加载</td></tr><tr><td style="text-align:left">7</td><td style="text-align:left">增加代码的维护性&amp;易读性</td></tr><tr><td style="text-align:left">8</td><td style="text-align:left">提升开发效率，因为可以前后端并行开发，而不是像以前的强依赖</td></tr><tr><td style="text-align:left">9</td><td style="text-align:left">前端大量的组件代码得以复用，组件化，提升开发效率，抽出来</td></tr><tr><td style="text-align:left">10</td><td style="text-align:left">在nginx中部署证书，外网使用https访问，并且只开放443和80端口，其他端口一律关闭（防止黑客端口扫描），内网使用http，性能和安全都有保障。</td></tr><tr><td style="text-align:left">11</td><td style="text-align:left">nginx支持页面热部署，不用重启服务器，前端升级更无缝。</td></tr><tr><td style="text-align:left">12</td><td style="text-align:left">前端项目中可以加入mock测试（构造虚拟测试对象来模拟后端，可以独立开发和测试）</td></tr></tbody></table><h1 id="前后端分离的劣势"><a href="#前后端分离的劣势" class="headerlink" title="前后端分离的劣势"></a>前后端分离的劣势</h1><p>有联调、沟通环节，这个过程非常花时间，也容易出bug，还很难维护。</p><h1 id="前后端分离-术业有专攻"><a href="#前后端分离-术业有专攻" class="headerlink" title="前后端分离-术业有专攻"></a>前后端分离-术业有专攻</h1><p>后端： 把精力放在 高并发，高可用，高性能，安全，存储，业务等等研究上</p><p>例如：设计模式，原理及源码，事务隔离与锁机制，http/tcp，多线程，分布式架构，弹性计算架构，微服务架构，性能优化，以及相关的项目管理等等</p><p>前端： 把精力放在页面表现，速度流畅，兼容性，用户体验等等</p><p>例如：html5，css3，jquery，angularjs，reactjs，vuejs，webpack，less/sass，gulp，nodejs，Google V8引擎，javascript多线程，模块化，面向切面编程，设计模式，浏览器兼容性，性能优化等等</p><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><table><thead><tr><th style="text-align:left">序号</th><th style="text-align:left">总结</th></tr></thead><tbody><tr><td style="text-align:left">1</td><td style="text-align:left">前后端分离并非仅仅只是一种开发模式，而是一种架构模式（前后端分离架构）。</td></tr><tr><td style="text-align:left">2</td><td style="text-align:left">千万不要以为只有在撸代码的时候把前端和后端分开就是前后端分离了，需要区分前后端项目。</td></tr><tr><td style="text-align:left">3</td><td style="text-align:left">前端项目与后端项目是两个项目，放在两个不同的服务器，需要独立部署，两个不同的工程，两个不同的代码库，不同的开发人员。</td></tr><tr><td style="text-align:left">4</td><td style="text-align:left">前后端工程师需要约定交互接口，实现并行开发，开发结束后需要进行独立部署，前端通过ajax来调用http请求调用后端的restful api。</td></tr><tr><td style="text-align:left">5</td><td style="text-align:left">前端只需要关注页面的样式与动态数据的解析&amp;渲染，而后端专注于具体业务逻辑。</td></tr></tbody></table>]]></content>
    
    <summary type="html">
    
      谈一谈前后端分离的优劣势
    
    </summary>
    
      <category term="前后端分离" scheme="https://ningyu1.github.io/categories/%E5%89%8D%E5%90%8E%E7%AB%AF%E5%88%86%E7%A6%BB/"/>
    
    
      <category term="前后端分离" scheme="https://ningyu1.github.io/tags/%E5%89%8D%E5%90%8E%E7%AB%AF%E5%88%86%E7%A6%BB/"/>
    
  </entry>
  
  <entry>
    <title>谈一谈开发团队代码质量如何管控与提升</title>
    <link href="https://ningyu1.github.io/20190307/111-code-quality-management.html"/>
    <id>https://ningyu1.github.io/20190307/111-code-quality-management.html</id>
    <published>2019-03-07T06:37:21.000Z</published>
    <updated>2024-04-23T01:48:15.030Z</updated>
    
    <content type="html"><![CDATA[<p>今天我们谈一下开发团队代码质量如何做到管控与提升，我相信很多公司都会面临这样的问题，开发团队大人员技术水平参差不齐，代码写的不够规范，代码扫描问题修改太过滞后，代码库管理每个团队都不一致，偶尔还会合并丢失一些代码，code review费人费时效率不高，开发任务的管理以及任务与代码的可追溯问题，等等之类的问题，我们能否制定一套从设计到开发再到交付一整套的管控方案来帮助开发团队管控代码的质量？下来我就针对这些问题展开来谈谈我的想法。</p><h1 id="举个例子"><a href="#举个例子" class="headerlink" title="举个例子"></a>举个例子</h1><p>比如说我们要增加代码和任务之间的可追溯性，我们可能考虑采用git+jira关联的方式对开发人员每笔提交在提交comment中增加jira编号，这是就是一个规范，但是规范落地如何检查？开发人员如果忘记在comment中添加就会造成关联失败，那我们就要采用工具的方式帮助开发人员在提交时检查comment是否符合规范。</p><p>比如说我们有制定编码规范，也采用了sonar去扫描代码的问题，但是这个方式的缺点是太过滞后，需要质量人员跟进去推动并且效果也不是很好，我们是否可以考虑前置检查点帮助开发人员在代码编写和提交时能主动的发现问题，在代码提交的时候发现规范问题可以直接进行解决再提交，我们可以考虑采用git加checkstyle、pmd、fingbug等工具插件，在代码提交的时候进行规范检测并且进行告警，这样就可以很好的帮助开发人员及时的发现问题，并不是开发已经提交了再去sonar上检查代码规范来发现问题再事后的安排人员去解决，开发人员都有一个习惯，当功能开发好没有问题后他们很少会去主动的修改与重构代码，这样就会导致迟迟不能推进，我们提前了检查点帮助开发人员及时发现问题就可以更好的推行规范的落地。</p><p>因此我们要考虑提供一整套代码质量管理的机制，应用在开发全生命周期中，并在关键的流程节点进行验证，从而把控与提升代码的质量。</p><h1 id="常见的问题及我的看法"><a href="#常见的问题及我的看法" class="headerlink" title="常见的问题及我的看法"></a>常见的问题及我的看法</h1><h2 id="静态代码扫描太滞后，推进吃力"><a href="#静态代码扫描太滞后，推进吃力" class="headerlink" title="静态代码扫描太滞后，推进吃力"></a>静态代码扫描太滞后，推进吃力</h2><p>我相信大多都会使用类似sonar这类的静态代码检查工具来检查代码，这里我们不说工具的好坏，我们只说检查问题的修复情况，我相信很多开发都会有一种习惯，在代码写完之后如果上线没有问题的话他们是很少会去主动的优化代码，即使你扫描结果告诉他他也会有各种理由推脱，当然我们可以通过管理的手段强制他们修改，比如说blocker、critical级别的必须全部改掉，其余的看情况修改，当然通过管理手段从上往下会有一定的效果，但是这些都是比较滞后的方式，我们能不能提前发现问题让开发在功能开发过程中就把发现的问题改掉？</p><p>这个当然是可以的，我们可以利用代码检查的机制，在代码开发中就让开发去扫描发现问题，在代码提交的时候去校验如果有严重的禁止代码提交。这样一来我们就可以提前来发现并解决问题，这样可能会带来的是开发人员的排斥，开发人员都觉得自己代码写的没有问题，所以这块我们需要把控这个检查规则的宽松度，我们可以结合公司的开发规范，整理不同级别的问题，通过先简后严的方式，先把开发的习惯培养起来后再逐渐的提升严格度，这样一来开发就有个适应期也比较好接受，比如说：我们通过checkstyle的规则模板定义，前期把一些无用导入包、命名不规范、导入包用*、system.out语句这类接受度高的作为error级别来推动开发适应从而培养这种良好的习惯。</p><h2 id="团队Code-Review没有跑起来或跑的太费事费力"><a href="#团队Code-Review没有跑起来或跑的太费事费力" class="headerlink" title="团队Code Review没有跑起来或跑的太费事费力"></a>团队Code Review没有跑起来或跑的太费事费力</h2><p>在技术行业做了一定时间的人应该都知道code review是多么的重要，一可以促进团队人员之间互相交流，二可以提升整体团队的技术水平，学习优秀人员写的代码，帮助初级人员提升代码编写能力，所以code review还是强烈必须要做的，至于怎么做code review？我谈一下我的想法和建议</p><p>比较常见的方式是定期团队内组织全体人员进行集中式的code review，我比较推荐利用工具在线的操作方式来做code review，现在开源非常的火也可以参考学习开源团队code review的方式，比如说github有pull request，gitlab有merge request，可以在这个合并代码的节点上进行code review，这样做的好处是第一比较开放，只要能看到合并代码请求的都可以进行review，第二可以留下review记录，互相的想法沟通和建议可以很好的留存下来并且可以通过UI的方式友好的展示出来，从而提升code review效率。</p><p>这个当然需要结合git flow的机制来协作完成。</p><h2 id="代码库分支、版本管理不规范，合并丢代码"><a href="#代码库分支、版本管理不规范，合并丢代码" class="headerlink" title="代码库分支、版本管理不规范，合并丢代码"></a>代码库分支、版本管理不规范，合并丢代码</h2><p>团队多了或团队大了，每个人或多或少对git的管理与使用理解不一致，这样就造成了分支、版本管理的混乱，这样在版本代码合并时就会产生很多冲突，我们可以指定一套规范性的东西，指导开发团队进行分支、版本的管理，这里说到的是指导不是限制，要让开发在可控的范围内自由发挥。</p><p>可以参考git flow、github flow等，当然我们要统一一个工作流程推广给开发团队中。</p><p>前面我们说了用代码合并来进行code review，这样我们就要让开发人员在每开发完一个任务的时候就要进行一次代码合并，git是一个优秀的分布式代码库管理工具，我们利用git的分布式特性，以及灵活的流程机制来规范大家的使用。</p><p>例如：</p><p>一次迭代冲刺或一个版本对应一个<code>develop-*</code>分支和<code>release-*</code>，并且控制分支的push与merge权限，固定一个master分支并且控制master分支的权限，让个人开发通过<code>feature-{username|功能名称}-*</code>分支来进行功能开发，当一个任务或者一个功能开发完成进行一次<code>develop-*</code>分支的合并，这样一来及可以code review也可以有序的管理分支上的代码，当开发人员提交合并请求时发生了冲突就需要开发人员自己解决完冲突后再进行代码合并请求，这样一来版本分支上代码是有序的。</p><table><thead><tr><th style="text-align:left">Name</th><th style="text-align:left">From</th><th style="text-align:left">Remark</th></tr></thead><tbody><tr><td style="text-align:left"><code>master</code></td><td style="text-align:left">-</td><td style="text-align:left">只能有一个并并且固定的</td></tr><tr><td style="text-align:left"><code>develop-*</code></td><td style="text-align:left">从master创建</td><td style="text-align:left">开发分支，可以结合jira的sprint，一个sprint对应一个，迭代开始时创建，’*’ 通常可以是一个发布周期或者一个冲刺命名</td></tr><tr><td style="text-align:left"><code>release-*</code></td><td style="text-align:left">从master创建</td><td style="text-align:left">预发布分支，可以结合jira的sprint，一个sprint对应一个，迭代开始时创建，’*’ 通常可以是一个发布周期或者一个冲刺命名</td></tr><tr><td style="text-align:left"><code>feature-{username or 功能名称}-*</code></td><td style="text-align:left">从<code>develop-*</code>创建</td><td style="text-align:left">开发人员分支，这个分支的声明周期很短，在这个功能开发完成通过Merge Request发起合并进行code review之后合并从而删除分支</td></tr></tbody></table><p>以上可以定位分支约定。</p><p>具体的操作可以参考下面描述：</p><ol><li>sprint开始时（需求确认后），从<code>master</code>创建<code>develop</code>分支，例如是<code>develop-V1.2.0</code></li><li>开发人员从对应的<code>develop</code>分支创建自己的<code>feature</code>分支进行开发</li><li>如果<code>master</code>分支发生变更，需要从<code>master</code>分支合并到对应的<code>develop</code>分支、可以考虑定期合并一次</li><li><code>feature</code>分支合并到对应的<code>develop</code>之前，需要从<code>develop</code>分支合并到<code>feature</code>分支（这个避免和其他人提交进行冲突，规范开发人员自己解决掉冲突后才能发起合并请求）</li><li><code>feature</code>分支合并到对应的<code>develop</code>之后，发布到测试环境进行测试（测试环境直接使用对应的<code>develop</code>分支）</li><li><code>develop</code>分支在测试环境测试通过之后，合并到对应的<code>release</code>分支并发布到预发布环境（UAT）进行测试</li><li><code>release</code>分支在预发布环境（UAT）验证通过后，合并到<code>master</code>分支并发布到生产环境进行验证</li><li>发布到生产环境后从<code>master</code>分支构建对应的版本tag</li></ol><p>可同时支持多个sprint的并行。</p><h2 id="代码提交备注写的很难懂甚至很随意"><a href="#代码提交备注写的很难懂甚至很随意" class="headerlink" title="代码提交备注写的很难懂甚至很随意"></a>代码提交备注写的很难懂甚至很随意</h2><p>代码的提交备注非常重要，尤其是在合并代码时产生冲突，第一时间肯定是根据提交日期去看本次提交做了什么修改，如果说备注随便填写，或者有些都没有填这样在回头来看的时候，及时是提交本人他也不能第一时间看出具体做了哪些修改，因此我觉得作为一个开发人员提交备注写的清晰明了是一件必备的职业素养，至于一些不按照规范的技术人员我们也可以要求他们按照规范必须填写。</p><p>那如何做到对备注填写的质量把控呢？我们可以通过版本管理工具在提交代码时进行提交备注检测，比如说对长度的限制，至少要15个字符，或者对格式做一些验证，必须包含任务编号之类，这样一来就可以有效的控制代码提交备注的质量以及可读性。</p><p>我们现在常用的git就有hook机制可以提供在代码提交前后做一些钩子，利用钩子来控制允许提交或者拒绝提交，比如说git的pre-commit和commit-msg</p><h2 id="开发人员的任务管理与提交代码没有关联，无法查看某个任务具体提交了哪些代码"><a href="#开发人员的任务管理与提交代码没有关联，无法查看某个任务具体提交了哪些代码" class="headerlink" title="开发人员的任务管理与提交代码没有关联，无法查看某个任务具体提交了哪些代码"></a>开发人员的任务管理与提交代码没有关联，无法查看某个任务具体提交了哪些代码</h2><p>优秀的开发人员主动性都是很好的，主动性对开发来说也是非常重要的职业素养，不要让人催促你来完成任务，自己要学会主动找任务去做主动想如何优化与提升，所以时间任务管理是非常重要的，我任务开发人员都应该具备自己的时间任务管理能力，无论用什么工具只要能管理跟踪好自己的任务就是不错的人员。</p><p>公司一般都有任务管理工具，有的用禅道、有的用jira，现在用jira的相对多一些，jira的功能丰富也可以促进团队进行敏捷的任务管理，我们可以通过打通任务管理工具和代码版本工具，让代码提交的时候通过任务编号产生关联，从而可以在任务中看到代码修改的片段。</p><p>这里我用jira+git举个例子，比如说我们利用jira做scrum的敏捷管理，在制定好epic、story、task、subtask后，可以通过scrum模型的管理手段，在开发过程中通过插件、图标的数据来分析是否有风险？那个人的任务delay？那个人的任务制定还可以再进行拆分？等，从而尽早的做出调整来控制整个迭代周期按时完成。利用git提交的备注写入jira编号，通过jira和git的插件打通任务与提交代码的关联，这样一来我们就可以很好的看到任务执行过程数据与具体改动了哪些代码，从而提升开发效率。</p><h2 id="统一管理校验规则版本，由简到严循序渐进的方式提升代码质量"><a href="#统一管理校验规则版本，由简到严循序渐进的方式提升代码质量" class="headerlink" title="统一管理校验规则版本，由简到严循序渐进的方式提升代码质量"></a>统一管理校验规则版本，由简到严循序渐进的方式提升代码质量</h2><p>我们上面说到的利用了checkstyle来验证代码风格，通过git hook来控制提交备注的规范，这些都需要自定义一些脚本，这些脚本也应该利用git进行有效的管理，我们能力能做到统一的调整了规则与脚本，开发过程中的应用立即使用最新的验证规则？还有git hooks的脚本是在开发机器本地运行的，这样就带来了一个问题如何让开发去安装脚本呢？叫他们手动安装？写个bat或shell脚本让开发执行一次？</p><p>我觉得更好的方式是对开发透明在他们不知觉的时候已经悄悄的安装，我们可以利用git对规则与脚本的版本进行管理，利用nginx可以通过http方式直接访问规则与脚本文件，通过自定义maven plugin在代码build的时候验证开发机器上是否已经安装，如果没有就给它自动安装与自动更新。</p><p>这样我们只要修改了规则与脚本后进行版本发布，开发机就会自动的更新下来从而可以立即生效。</p><h2 id="开发团队技术氛围低沉"><a href="#开发团队技术氛围低沉" class="headerlink" title="开发团队技术氛围低沉"></a>开发团队技术氛围低沉</h2><p>很多公司开发团队一味的满头苦干，很容易忽视团队内的技术分享，再加上团队内人员进进出出有一些正能量的人当然也有一些负能量的人，这都是常事，但是不管怎样我相信做技术的人都愿意提升自己的技术能力，不管是工作中实践学习还是说参加沙龙或者论坛，都是很好的学习渠道，人的精力也是比较有限不可能关注很多面，通过团队内的技术分享，把每个人擅长的部分分享给大家，互相学习来提升凝聚力和团队整体的技术水平，这样长期以来我相信团队内的技术氛围肯定不会差。</p><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>以上就是我对代码质量管理与提升方面的经验与思考，里面涉及到很多东西，有流程的制定、工具的协作、工具的打通、规范的制定等，因此这是一个系统性的方案，希望可以利用一整套代码质量管理的流程，在关键的流程节点来把控代码的质量，形成闭环，希望可以帮助有需要的人，如果有更好的建议也希望大家多提意见进行补充，没有完美的方式，只有找到适合的可落地的就是好的。</p>]]></content>
    
    <summary type="html">
    
      谈一谈开发团队代码质量如何管控与提升
    
    </summary>
    
      <category term="代码质量管理" scheme="https://ningyu1.github.io/categories/%E4%BB%A3%E7%A0%81%E8%B4%A8%E9%87%8F%E7%AE%A1%E7%90%86/"/>
    
    
      <category term="代码质量管理" scheme="https://ningyu1.github.io/tags/%E4%BB%A3%E7%A0%81%E8%B4%A8%E9%87%8F%E7%AE%A1%E7%90%86/"/>
    
      <category term="git flow" scheme="https://ningyu1.github.io/tags/git-flow/"/>
    
      <category term="jira git" scheme="https://ningyu1.github.io/tags/jira-git/"/>
    
      <category term="code review" scheme="https://ningyu1.github.io/tags/code-review/"/>
    
      <category term="git hook" scheme="https://ningyu1.github.io/tags/git-hook/"/>
    
      <category term="check style" scheme="https://ningyu1.github.io/tags/check-style/"/>
    
  </entry>
  
  <entry>
    <title>疑似Batch处理事务问题，保存了该回滚的数据</title>
    <link href="https://ningyu1.github.io/20190220/110-mybatis-batch.html"/>
    <id>https://ningyu1.github.io/20190220/110-mybatis-batch.html</id>
    <published>2019-02-20T06:34:21.000Z</published>
    <updated>2024-04-23T01:48:15.030Z</updated>
    
    <content type="html"><![CDATA[<p>这篇文章转自公司内网wiki中一篇不错的问题分析文章，</p><h1 id="问题描述"><a href="#问题描述" class="headerlink" title="问题描述"></a>问题描述</h1><ol><li>两个事物， 在第一个事务报错是则执行第二个事务</li><li>两个事物都是执行下面的批量操作</li><li>两个事务的批量操作是插入到相同的两张表中，如下代码</li><li>第一个事务预计在第一个表中插入3条记录， 第二个表中插入3条记录，但是第一个表的第一个记录就违反了约束，报错异常；</li><li>第一个事务失败后，执行第二个事务，第二个事务插入两个表中各一条记录。</li><li>实际结果：第一个表有一条记录（第二个事务中插入的），第二个表中有4条记录（除了第二个事务中的一条，还有第一个事务中的3条数据）</li></ol><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/mybatis-batch/1.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p>问题点是在第一个事务抛异常回滚了，第一个表成功回滚，但是第二个事务将第一个事务中的第二个表的数据提交了。</p><h1 id="问题原因"><a href="#问题原因" class="headerlink" title="问题原因"></a>问题原因</h1><ol><li>我们说明批量操作是指：如下的样例:insert into t(field) values(v1),(v2),(v3)</li><li>sqlSession.commit();实际上并不是事务的commit，而只是执行sql</li><li>2个事务绑定的是同一个connection。</li><li>在一个mybatis的sqlSession 批量中操作两张表，则会生成两个prepareStatement，</li><li>而prepareStatement对象在mybatis中有cache。</li><li>回滚时回滚到savepoint</li></ol><p>基于上面6点， 当第一个事务的第一个表执行是失败后（在第一个表的失败位置上设置一个savepoint，回滚时值回滚到这个savepoint，第二个preparestatement被缓存了）</p><h1 id="问题总结"><a href="#问题总结" class="headerlink" title="问题总结"></a>问题总结</h1><ol><li>本问题不设计到事务传播机制与隔离级别</li><li>本例为一个错误使用范例，<strong>即不能在一个mybatis的sqlSession批量中操作两张表</strong></li></ol><p><span style="color:blue"><em>注意：PreparedStatement确实适合执行相同sql的批处理，Statement适合执行不同sql的批处理</em></span></p><p>一些代码跟踪截图这里就不方便放出来请见谅。</p>]]></content>
    
    <summary type="html">
    
      疑似Batch处理事务问题，保存了该回滚的数据
    
    </summary>
    
      <category term="mybatis" scheme="https://ningyu1.github.io/categories/mybatis/"/>
    
    
      <category term="mybatis" scheme="https://ningyu1.github.io/tags/mybatis/"/>
    
      <category term="batch" scheme="https://ningyu1.github.io/tags/batch/"/>
    
  </entry>
  
  <entry>
    <title>RESTful开发日期类型字段如何正确传递</title>
    <link href="https://ningyu1.github.io/20190212/109-restful-date-convert.html"/>
    <id>https://ningyu1.github.io/20190212/109-restful-date-convert.html</id>
    <published>2019-02-12T07:30:21.000Z</published>
    <updated>2024-04-23T01:48:15.030Z</updated>
    
    <content type="html"><![CDATA[<p>RESTful开发时经常会遇到参数传入日期类型及返回的日期类型值，日期和时间戳如果没有适当和一致地处理，就会给人带来头痛的问题，我这里建议大家使用统一格式化的时间字符串<code>yyyy-MM-dd HH:mm:ss</code>，为什么建议这个呢？这样看起来比较直观，前后端联调起来比较高效。</p><p>下面我们就细说一下日期类型的参数将如何处理。</p><h1 id="GET方法时参数传入日期类型该如何处理"><a href="#GET方法时参数传入日期类型该如何处理" class="headerlink" title="GET方法时参数传入日期类型该如何处理"></a>GET方法时参数传入日期类型该如何处理</h1><p>举例</p><p>url如下：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">http://localhost:8081/test/time_get?time=2018-07-09 10:38:57</span><br></pre></td></tr></table></figure><p>Controller代码：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">import java.util.Date;</span><br><span class="line"></span><br><span class="line">@RequestMapping(value = &quot;/time_get&quot;, method = RequestMethod.GET)</span><br><span class="line">@ResponseBody</span><br><span class="line">public Response&lt;Date&gt; time_get(Date time) &#123;</span><br><span class="line">    logger.info(&quot;time:&#123;&#125;&quot;, time);</span><br><span class="line">    return Response.createResponse(time);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在这种情况下日期参数是无法成功的传入到controller方法里，会爆出如下的异常：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">org.springframework.core.convert.ConversionFailedException: Failed to convert from type java.lang.String to type java.util.Date for value &apos;2018-07-09 10:38:57&apos;; nested exception is java.lang.IllegalArgumentException</span><br><span class="line">at org.springframework.core.convert.support.ObjectToObjectConverter.convert(ObjectToObjectConverter.java:81) ~[spring-core-4.0.0.RELEASE.jar:4.0.0.RELEASE]</span><br><span class="line"> at org.springframework.core.convert.support.ConversionUtils.invokeConverter(ConversionUtils.java:35) ~[spring-core-4.0.0.RELEASE.jar:4.0.0.RELEASE]</span><br><span class="line"> at org.springframework.core.convert.support.GenericConversionService.convert(GenericConversionService.java:178) ~[spring-core-4.0.0.RELEASE.jar:4.0.0.RELEASE]</span><br><span class="line"> at org.springframework.beans.TypeConverterDelegate.convertIfNecessary(TypeConverterDelegate.java:161) ~[spring-beans-4.0.0.RELEASE.jar:4.0.0.RELEASE]</span><br><span class="line"> at org.springframework.beans.TypeConverterDelegate.convertIfNecessary(TypeConverterDelegate.java:93) ~[spring-beans-4.0.0.RELEASE.jar:4.0.0.RELEASE]</span><br><span class="line"> at org.springframework.beans.TypeConverterSupport.doConvert(TypeConverterSupport.java:64) ~[spring-beans-4.0.0.RELEASE.jar:4.0.0.RELEASE]</span><br><span class="line"> ... 43 common frames omitted</span><br><span class="line">Caused by: java.lang.IllegalArgumentException: null</span><br><span class="line"> at java.util.Date.parse(Date.java:615) ~[na:1.7.0_45]</span><br><span class="line"> at java.util.Date.&lt;init&gt;(Date.java:272) ~[na:1.7.0_45]</span><br><span class="line"> at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) ~[na:1.7.0_45]</span><br><span class="line"> at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:57) ~[na:1.7.0_45]</span><br><span class="line"> at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) ~[na:1.7.0_45]</span><br><span class="line"> at java.lang.reflect.Constructor.newInstance(Constructor.java:526) ~[na:1.7.0_45]</span><br><span class="line"> at org.springframework.core.convert.support.ObjectToObjectConverter.convert(ObjectToObjectConverter.java:76) ~[spring-core-4.0.0.RELEASE.jar:4.0.0.RELEASE]</span><br><span class="line"> ... 48 common frames omitted</span><br></pre></td></tr></table></figure><p>那如何解决上面的问题？使用<code>@DateTimeFormat(pattern = &quot;yyyy-MM-dd HH:mm:ss&quot;)</code>加到日期参数之前，像下面那样使用一样。</p><p>Controller代码：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">import java.util.Date;</span><br><span class="line"> </span><br><span class="line">@RequestMapping(value = &quot;/time_get&quot;, method = RequestMethod.GET)</span><br><span class="line">@ResponseBody</span><br><span class="line">public Response&lt;Date&gt; time_get(@DateTimeFormat(pattern = &quot;yyyy-MM-dd HH:mm:ss&quot;) Date time) &#123;</span><br><span class="line">    logger.info(&quot;time:&#123;&#125;&quot;, time);</span><br><span class="line">    return Response.createResponse(time);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><span style="color:blue"><em>提示：年月日：pattern=”yyyy-MM-dd”，年月日时分秒：pattern=”yyyy-MM-dd HH:mm:ss”</em></span></p><p>请求体：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">GET /test/time_get1?time=2018-07-09 11:31:00 HTTP/1.1</span><br><span class="line">Host: localhost:8081</span><br></pre></td></tr></table></figure><p>后端接收到的信息，debug截图：</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/restful-date-convert/1.png" alt title>                </div>                <div class="image-caption"></div>            </figure><h1 id="POST方法时参数传入日期类型该如何处理"><a href="#POST方法时参数传入日期类型该如何处理" class="headerlink" title="POST方法时参数传入日期类型该如何处理"></a>POST方法时参数传入日期类型该如何处理</h1><p>当使用<code>@RequestBody</code>接受一个<code>VO</code>对象时<code>@DateTimeFormat</code>就会失效，因为我们走的是<code>Json</code>序列化与反序列化，<code>@DateTimeFormat</code>只会生效与<code>object</code>序列化、反序列化。如果使用的<code>Spring</code>可以自定义<code>messageConvert</code>或者增强<code>MappingJackson2HttpMessageConverter</code>中的<code>ObjectMapper</code></p><p>代码如下：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">import java.text.SimpleDateFormat;</span><br><span class="line">import java.util.ArrayList;</span><br><span class="line">import java.util.List;</span><br><span class="line"></span><br><span class="line">import org.springframework.context.annotation.Bean;</span><br><span class="line">import org.springframework.context.annotation.Configuration;</span><br><span class="line">import org.springframework.http.converter.HttpMessageConverter;</span><br><span class="line">import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;</span><br><span class="line">import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;</span><br><span class="line">import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;</span><br><span class="line"></span><br><span class="line">import com.fasterxml.jackson.databind.DeserializationFeature;</span><br><span class="line">import com.fasterxml.jackson.databind.ObjectMapper;</span><br><span class="line"></span><br><span class="line">@Configuration</span><br><span class="line">public class WebConfig extends WebMvcConfigurationSupport &#123;</span><br><span class="line">    @Override</span><br><span class="line">    protected void configureMessageConverters(List&lt;HttpMessageConverter&lt;?&gt;&gt; converters) &#123;</span><br><span class="line">        List&lt;HttpMessageConverter&lt;?&gt;&gt; messageConverters = new ArrayList&lt;HttpMessageConverter&lt;?&gt;&gt;();</span><br><span class="line">        addDefaultHttpMessageConverters(messageConverters);</span><br><span class="line">        for (int i = 0; i &lt; messageConverters.size(); i++) &#123;</span><br><span class="line">            HttpMessageConverter&lt;?&gt; mc = messageConverters.get(i);</span><br><span class="line">            if (mc instanceof MappingJackson2HttpMessageConverter) &#123;</span><br><span class="line">                ObjectMapper objectMapper = new ObjectMapper();</span><br><span class="line">                //当json中属性在反序列化时，javabean中没有找到属性就忽略，如果FAIL_ON_UNKNOWN_PROPERTIES=true找不到属性会报错</span><br><span class="line">                objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);</span><br><span class="line">                //设置序列化、反序列化时日期类型的格式</span><br><span class="line">                objectMapper.setDateFormat(new SimpleDateFormat(&quot;yyyy-MM-dd HH:mm:ss&quot;));</span><br><span class="line">                ((MappingJackson2HttpMessageConverter) mc).setObjectMapper(objectMapper);</span><br><span class="line">            &#125;</span><br><span class="line">            converters.add(mc);</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><span style="color:blue"><em>注意：使用Jackson进行json序列化反序列化，默认可以处理<code>yyyy-MM-dd</code>这个格式，但是反序列化后的时间会差8小时</em></span></p><p>通过上面对json序列化反序列化的配置后日期参数处理就变的简单了，效果如下。</p><p>Controller代码：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line"></span><br><span class="line">@RequestMapping(value = &quot;/time_post&quot;, method = RequestMethod.POST)</span><br><span class="line">@ResponseBody</span><br><span class="line">public Response&lt;Date&gt; time_post(@RequestBody TestVo vo) &#123;</span><br><span class="line">    logger.info(&quot;time:&#123;&#125;&quot;, vo.getTime());</span><br><span class="line">    return Response.createResponse(vo.getTime());</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>VO代码：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">public class TestVo implements Serializable &#123;</span><br><span class="line">    private static final long serialVersionUID = 7435595656552442126L;</span><br><span class="line">    private Date time;</span><br><span class="line">    public Date getTime() &#123;</span><br><span class="line">        return time;</span><br><span class="line">    &#125;</span><br><span class="line">    public void setTime(Date time) &#123;</span><br><span class="line">        this.time = time;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><span style="color:blue"><em>提示：<code>VO</code>中无需使用<code>@DateTimeFormat</code>，就是一个普通的javabean即可</em></span></p><p>请求体：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">POST /test/time_post HTTP/1.1</span><br><span class="line">Host: localhost:8081</span><br><span class="line">Content-Type: application/json</span><br><span class="line">&#123;</span><br><span class="line">    &quot;time&quot;:&quot;2018-07-09 15:31:00&quot;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>后端接收到的信息，debug截图</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/restful-date-convert/2.png" alt title>                </div>                <div class="image-caption"></div>            </figure><h1 id="PUT方法时参数传入日期类型该如何处理"><a href="#PUT方法时参数传入日期类型该如何处理" class="headerlink" title="PUT方法时参数传入日期类型该如何处理"></a>PUT方法时参数传入日期类型该如何处理</h1><p>如果put传参方式与get一样在方法上直接传参（<code>url?time=2018-07-09 10:38:57</code>），那参考get请求参数处理方式即可</p><p>如果put传参方式与post一样使用<code>@RequestBody</code>传入json格式数据，那么参考post请求参数处理方式即可</p><p>请求体：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">PUT /test/time_put HTTP/1.1</span><br><span class="line">Host: localhost:8081</span><br><span class="line">Content-Type: application/json</span><br><span class="line">&#123;</span><br><span class="line">    &quot;time&quot;:&quot;2018-07-09 15:31:00&quot;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>后端接收到的信息，debug截图</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/restful-date-convert/3.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p>前面都说的是request时日期格式处理方式，那么我们继续说一下response时日期格式如何处理。</p><h1 id="Response中日期格式该如何处理"><a href="#Response中日期格式该如何处理" class="headerlink" title="Response中日期格式该如何处理"></a>Response中日期格式该如何处理</h1><p>SpringMVC使用<code>@ResponseBody</code>时，日期格式默认显示为时间戳，不管方法直接返回Date类型、或者VO类型时，时间格式都一样返回时间戳，例如这样。</p><p>请求体：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">POST /test/time_post1 HTTP/1.1</span><br><span class="line">Host: localhost:8081</span><br><span class="line">Content-Type: application/json</span><br><span class="line">&#123;</span><br><span class="line">    &quot;time&quot;:&quot;2018-07-09&quot;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>响应体：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line"></span><br><span class="line">&#123;</span><br><span class="line">    &quot;code&quot;: &quot;&quot;,</span><br><span class="line">    &quot;message&quot;: &quot;&quot;,</span><br><span class="line">    &quot;items&quot;: &#123;</span><br><span class="line">        &quot;time&quot;: 1531094400000</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>那如果我们要以字符串格式返回呢，那该如何处理？</p><p>方法一增加统一的<code>messageConvert</code>处理：</p><p>如果使用的spring可以自定义<code>messageConvert</code>或者增强<code>MappingJackson2HttpMessageConverter</code>中的<code>ObjectMapper</code></p><p>代码在<a href="#POST方法时参数传入日期类型该如何处理">POST方法时参数传入日期类型该如何处理</a>这个章节</p><p>方法二通过<code>@JsonFormat</code>注解处理：</p><p>请在<code>VO</code>对象的date字段上加上<code>@JsonFormat(pattern=&quot;yyyy-MM-dd HH:mm:ss&quot;,timezone = &quot;GMT+8&quot;)</code>,例如下面代码：</p><p>VO代码：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">public class TestVo implements Serializable &#123;</span><br><span class="line">    private static final long serialVersionUID = 7435595656552442126L;</span><br><span class="line"> </span><br><span class="line">    @JsonFormat(pattern=&quot;yyyy-MM-dd HH:mm:ss&quot;,timezone = &quot;GMT+8&quot;)</span><br><span class="line">    private Date time;</span><br><span class="line">    public Date getTime() &#123;</span><br><span class="line">        return time;</span><br><span class="line">    &#125;</span><br><span class="line">    public void setTime(Date time) &#123;</span><br><span class="line">        this.time = time;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><span style="color:blue"><em>注意：@JsonFormat(pattern=”yyyy-MM-dd HH:mm:ss”,timezone = “GMT+8”) ，即可将json返回的对象为指定的类型。</em></span></p><p>返回日期格式使用的是”yyyy-MM-dd HH:mm:ss”样式字符串示例：</p><p>请求体：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">POST /test/time_post1 HTTP/1.1</span><br><span class="line">Host: localhost:8081</span><br><span class="line">Content-Type: application/json</span><br><span class="line">&#123;</span><br><span class="line">    &quot;time&quot;:&quot;2018-07-09 15:31:00&quot;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>响应体：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">&#123;</span><br><span class="line">    &quot;code&quot;: &quot;&quot;,</span><br><span class="line">    &quot;message&quot;: &quot;&quot;,</span><br><span class="line">    &quot;items&quot;: &#123;</span><br><span class="line">        &quot;time&quot;: &quot;2018-07-09 15:31:00&quot;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h1 id="日期格式处理规范"><a href="#日期格式处理规范" class="headerlink" title="日期格式处理规范"></a>日期格式处理规范</h1><p>这里有的人喜欢使用时间戳，有的人喜欢使用统一格式化的时间字符串<code>yyyy-MM-dd HH:mm:ss</code>，我个人的建议使用后者，因为这样比较直观调试交流起来也比较方便。</p><p>使用哪种没有对错，其实就是一种规范，统一规范可以提升协作效率，因此我建议的规范如下：</p><ul><li>前端传入日期格式：yyyy-MM-dd HH:mm:ss，例如：2018-07-09 12:55:12（不使用时间戳，前后双方联调时查看起来更加友好）</li><li>后端返回日期格式：yyyy-MM-dd HH:mm:ss，例如：2018-07-09 12:55:12（不使用时间戳，前后双方联调时查看起来更加友好）</li><li>后端json序列化、反序列化框架：定义任何一种高效的json工具都行，例如：Fastjson、Gson、Jackson lib</li><li>序列化、反序列化日期格式规范：pattern=”yyyy-MM-dd HH:mm:ss”,timezone = “GMT+8”，除特出场景可以使用pattern=”yyyy-MM-dd”,timezone = “GMT+8”</li><li>不要随意自定义convert处理日期格式以及其他特殊格式转换，应该交由基础框架组或架构组统一在框架层面解决，对业务开发透明，或者在有些小公司由Leader或高开来担当。</li></ul><p>这些都是我个人摸爬滚打多年码出来的心得体会，说的不好还请见谅，希望可以帮助有需要的人。</p>]]></content>
    
    <summary type="html">
    
      RESTful开发日期类型字段如何正确传递
    
    </summary>
    
      <category term="restful" scheme="https://ningyu1.github.io/categories/restful/"/>
    
    
      <category term="restful" scheme="https://ningyu1.github.io/tags/restful/"/>
    
      <category term="date-convert" scheme="https://ningyu1.github.io/tags/date-convert/"/>
    
  </entry>
  
  <entry>
    <title>谈一谈自定义字段实现的几种方式</title>
    <link href="https://ningyu1.github.io/20190103/108-custom-field.html"/>
    <id>https://ningyu1.github.io/20190103/108-custom-field.html</id>
    <published>2019-01-03T07:50:21.000Z</published>
    <updated>2024-04-23T01:48:15.030Z</updated>
    
    <content type="html"><![CDATA[<p>我们经常会遇到项目中很多对表单进行自定义，比如说saas应用针对租户自定义表单字段名称，自定义列表名称。<br>还有更高级自定义，比如说自定义的模块，表单、字段、字段类型、流程等自定义。</p><p>提供自定义也是一个系统扩展性的体现，自定义功能的强大自然能适应更多的用户场景。</p><p>接下来我们就看看自定义的实现方案通常都有哪些方式。</p><p>常见的自定义字段的实现方式分为三种由简到繁，扩展性、复杂性也是逐渐增强的，每个方式各有优劣解决的场景也有所不同，具体往下看。</p><h1 id="列式存储自定义字段（扩展字段-ext-field）"><a href="#列式存储自定义字段（扩展字段-ext-field）" class="headerlink" title="列式存储自定义字段（扩展字段 ext field）"></a>列式存储自定义字段（扩展字段 ext field）</h1><p>模型如下：</p><table><thead><tr><th style="text-align:left">ID</th><th style="text-align:left">Name</th><th style="text-align:left">Ext1(性别)</th><th style="text-align:left">Ext2(地区)</th><th style="text-align:left">Ext3(QQ)</th><th style="text-align:left">Ext4(WECHAT)</th></tr></thead><tbody><tr><td style="text-align:left">1</td><td style="text-align:left">韩梅梅</td><td style="text-align:left">女</td><td style="text-align:left">Shanghai</td><td style="text-align:left">10000</td><td style="text-align:left"></td></tr><tr><td style="text-align:left">2</td><td style="text-align:left">李磊</td><td style="text-align:left">男</td><td style="text-align:left">Beijing</td><td style="text-align:left"></td><td style="text-align:left">abc001</td></tr></tbody></table><p>优点：</p><ol><li>实现成本最低</li><li>可以直接表连接进行检索</li></ol><p>缺点：</p><ol><li>扩展能力一般，有上限</li><li>浪费资源，比如说有20个扩展字段，一行只用到2个，其余的18个都要存储null来浪费空间。</li><li>能解决的场景比较有限。</li></ol><h1 id="EAV模型-Entity-Attribute-Value（实体、属性、值）"><a href="#EAV模型-Entity-Attribute-Value（实体、属性、值）" class="headerlink" title="EAV模型 Entity-Attribute-Value（实体、属性、值）"></a>EAV模型 Entity-Attribute-Value（实体、属性、值）</h1><p>对象属性存储在一个有三列的表中：实体，属性和值（entity，attribute，value)。实体（entiry）表示所描述的数据项，例如一个产品或汽车。属性（attribute）表示描述实体的数据，例如一个产品将有价格，重量和许多其他属性。值（value）是属性的值，例如产品可能有一个9.99英镑的价格属性。此外值可以基于数据类型进行分割，所以可将EAV表分为字符串、整数、日期和长文本（long text）表。依据数据类型分割是为了支持索引,使得数据库执行可能的类型检查验证。</p><p>EAV表模型带来了数据的灵活性，是的增加对象的属性不需要用增加数据库的字段，有很高的灵活性。但是EAV表也有较大的性能问题。通常，EAV表带来的一个问题是当查找多个字段时，需要进行关联查询join,这样的查询效率比较低。为了提高查询效率，我们可以对商品属性表进行矩阵转积处理(pivoting)。</p><p>一种方式是在代码中读出后存入cache中,当修改attributes表后触发更新cache或用cron定期更新;另一种方法是将关联信息组成一张大的临时表，数据的更新可以用数据库的触发器触发更新。由于大量数据在代码中进行处理会带来了DB的额外IO和服务器性能问题。当使用EAV表模型时，InnoDB比MYISAM的性能要好不少。</p><p>ps. 我们常用的行模型（纵向）存储就是EAV模型实现的一种方式。</p><p>模型如下：</p><p>人员表（Entity）</p><table><thead><tr><th style="text-align:left">ID</th><th style="text-align:left">Name</th></tr></thead><tbody><tr><td style="text-align:left">1</td><td style="text-align:left">韩梅梅</td></tr><tr><td style="text-align:left">2</td><td style="text-align:left">李磊</td></tr></tbody></table><p>扩展映射（Entities）</p><table><thead><tr><th style="text-align:left">Entity</th><th style="text-align:left">Attribute</th><th style="text-align:left">Value</th></tr></thead><tbody><tr><td style="text-align:left">1</td><td style="text-align:left">sex（性别）</td><td style="text-align:left">女</td></tr><tr><td style="text-align:left">2</td><td style="text-align:left">sex（性别）</td><td style="text-align:left">男</td></tr><tr><td style="text-align:left">1</td><td style="text-align:left">region（地区）</td><td style="text-align:left">Shanghai</td></tr><tr><td style="text-align:left">2</td><td style="text-align:left">region（地区）</td><td style="text-align:left">Beijing</td></tr><tr><td style="text-align:left">1</td><td style="text-align:left">QQ</td><td style="text-align:left">10000</td></tr><tr><td style="text-align:left">2</td><td style="text-align:left">WECHAT</td><td style="text-align:left">abc001</td></tr></tbody></table><p>优点：</p><ol><li>扩展能力较强</li><li>理论上无上限</li><li>可以支持几乎所有的自定义字段的需求</li></ol><p>缺点：</p><ol><li>关联查询效率低下</li><li>需要维护自定义字段与值的关系表</li></ol><h1 id="Json格式存储自定义字段"><a href="#Json格式存储自定义字段" class="headerlink" title="Json格式存储自定义字段"></a>Json格式存储自定义字段</h1><p>json格式非常丰富，在描述自定义字段的这方面比较适合，可以把一行多列的数据压缩到一个json text内，也比较节省空间，json格式可以无限扩展，还可支持多个自定义字段有不同的格式。</p><p>模型如下：</p><table><thead><tr><th style="text-align:left">ID</th><th style="text-align:left">Name</th><th style="text-align:left">Content</th></tr></thead><tbody><tr><td style="text-align:left">1</td><td style="text-align:left">韩梅梅</td><td style="text-align:left">{“sex”:”女”,”region”:”Shanghai”,”QQ”:”10000”}</td></tr><tr><td style="text-align:left">2</td><td style="text-align:left">李磊</td><td style="text-align:left">{“sex”:”女”,”region”:”Beijing”,”WECHAT”:”abc001”}</td></tr></tbody></table><p>ps. 支持以上的两种不同的自定义格式并存</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/custom-field/1.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p>优点：</p><ol><li>扩展能力强</li><li>理论上无上限</li><li>可以支持几乎所有的自定义字段的需求</li><li>无需维护自定义字段与值关系</li></ol><p>缺点：</p><ol><li>数据库需要支持json type，不建议使用text类型</li><li>不支持关联查询（mongodb除外）</li><li>自定义字段检索需要通过其他方式，例如搜索引擎。（mongodb除外）</li></ol><h2 id="数据库对Json格式支持情况"><a href="#数据库对Json格式支持情况" class="headerlink" title="数据库对Json格式支持情况"></a>数据库对Json格式支持情况</h2><p>数据库对Json类型的支持：</p><ol><li>Mysq5.7（<a href="http://www.lnmp.cn/mysql-57-new-features-json.html" target="_blank" rel="noopener">CRUD参考</a>）</li><li>PostgreSQL（<a href="https://www.enterprisedb.com/blog/crud-json-postgresql" target="_blank" rel="noopener">CRUD参考</a>，<a href="https://blog.csdn.net/qwdafedv/article/details/68066802" target="_blank" rel="noopener">json与jsonb区别</a>）</li><li>MongoDB（<a href="https://docs.mongodb.com/manual/crud/" target="_blank" rel="noopener">CRUD参考</a>）</li></ol><p>数据库对json类型的检索支持：</p><ol><li>Mysql5.7： 支持索引：通过虚拟列的功能可以对JSON中部分的数据进行索引。（相比PG和MongoDB弱一些，通过json_extract()函数做一些简单查询）</li><li>PostgreSQL：支持检索，可以复杂查询</li><li>MongoDB：支持检索，可以复杂查询，支持map reduce</li></ol><p>ORM框架对Json类型的支持：</p><ol><li>Mybatis支持json格式字段映射到POJO，方便json格式的bean与数据库映射。</li><li>Hibernate支持json格式字段映射到POJO，方便json格式的bean与数据库映射。</li></ol><p>Mysql5.7.x json操作官方文档：</p><ol><li><a href="https://dev.mysql.com/doc/refman/5.7/en/json-creation-functions.html" target="_blank" rel="noopener">json-creation-functions</a></li><li><a href="https://dev.mysql.com/doc/refman/5.7/en/json-search-functions.html" target="_blank" rel="noopener">json-search-functions</a></li><li><a href="https://dev.mysql.com/doc/refman/5.7/en/json-modification-functions.html" target="_blank" rel="noopener">json-modification-functions</a></li></ol><p>Mysql5.7.x 注意事项：</p><ol><li>JSON_UNQUOTE 、-&gt;、-&gt;&gt; 之间的区别<ul><li>下面三个表达式返回相同的值<ul><li>JSON_UNQUOTE( JSON_EXTRACT(column, path) )</li><li>JSON_UNQUOTE(column -&gt; path) </li><li>column-&gt;&gt;path</li></ul></li></ul></li><li>JSON_CONTAINS_PATH 参数说明<ul><li>第二个参数为’one’或’all’的区别<ul><li>‘one’：至少存在一个路径返回1，反之返回0</li><li>‘all’：全部路径存在返回1，反之返回0</li></ul></li></ul></li><li>JSON_CONTAINS 参数说明<ul><li>第二个参数是不接受整数的，无论 json 元素是整型还是字符串，否则会出现这个错误</li></ul></li><li>5.7.x不同版本支持的程度：<ul><li>MySQL 5.7.13<ul><li>支持操作符  -&gt;&gt; </li></ul></li><li>MySQL 5.7.9 <ul><li>支持操作符 -&gt; （JSON_EXTRACT()函数别名）</li><li>重命名函数JSON_APPEND()为JSON_ARRAY_APPEND()，函数作用：将值追加到JSON文档中指定数组的末尾并返回结果，未来会删除’JSON_APPEND()’</li></ul></li><li>MySQL 5.7.22<ul><li>支持JSON_ARRAYAGG()返回json数组形式结果集，JSON_OBJECTAGG()返回kson对象形式结果集</li><li>添加JSON_MERGE_PATCH()，作用：合并结果（相同path）</li><li>添加JSON_MERGE_PRESERVE()，作用：合并数据（不同path）</li><li>弃用JSON_MERGE()，使用JSON_MERGE_PRESERVE() / JSON_MERGE_PATCH()，未来会删除’JSON_MERGE()’ </li></ul></li></ul></li></ol><p>实现方式不局限于上面说到的方式，有更好的方式欢迎留言进行沟通。</p>]]></content>
    
    <summary type="html">
    
      谈一谈自定义字段实现的几种方式
    
    </summary>
    
      <category term="custom-field" scheme="https://ningyu1.github.io/categories/custom-field/"/>
    
    
      <category term="custom-field" scheme="https://ningyu1.github.io/tags/custom-field/"/>
    
  </entry>
  
  <entry>
    <title>自1996年起的(Best Paper)计算机科学最佳论文奖收录</title>
    <link href="https://ningyu1.github.io/20181228/107-best-paper.html"/>
    <id>https://ningyu1.github.io/20181228/107-best-paper.html</id>
    <published>2018-12-28T09:38:21.000Z</published>
    <updated>2024-04-23T01:48:15.030Z</updated>
    
    <content type="html"><![CDATA[<p>原文地址： <a href="https://jeffhuang.com/best_paper_awards.html" target="_blank" rel="noopener">https://jeffhuang.com/best_paper_awards.html</a></p><p>我擦好牛逼了，自1996年起的计算机科学最佳论文奖全收录，不说了戳开看吧。</p>]]></content>
    
    <summary type="html">
    
      自1996年起的(Best Paper)计算机科学最佳论文奖全收录
    
    </summary>
    
      <category term="best-paper" scheme="https://ningyu1.github.io/categories/best-paper/"/>
    
    
      <category term="best-paper" scheme="https://ningyu1.github.io/tags/best-paper/"/>
    
  </entry>
  
  <entry>
    <title>[Enhancement]Enumeration type support, Dubbo Plugin for Apache JMeter - V1.3.8</title>
    <link href="https://ningyu1.github.io/20181218/106-jmeter-plugin-dubbo-1.3.8.html"/>
    <id>https://ningyu1.github.io/20181218/106-jmeter-plugin-dubbo-1.3.8.html</id>
    <published>2018-12-18T05:57:21.000Z</published>
    <updated>2024-04-23T01:48:15.030Z</updated>
    
    <content type="html"><![CDATA[<h1 id="项目地址"><a href="#项目地址" class="headerlink" title="项目地址"></a>项目地址</h1><p><a href="https://github.com/dubbo/jmeter-plugins-dubbo" target="_blank" rel="noopener">github: jmeter-plugin-dubbo</a> </p><p><a href="https://gitee.com/ningyu/jmeter-plugins-dubbo" target="_blank" rel="noopener">码云: jmeter-plugin-dubbo</a></p><h1 id="V1-3-8"><a href="#V1-3-8" class="headerlink" title="V1.3.8"></a><a href="https://github.com/dubbo/jmeter-plugins-dubbo/releases/tag/V1.3.8" target="_blank" rel="noopener">V1.3.8</a></h1><h1 id="What-is-new"><a href="#What-is-new" class="headerlink" title="What is new:"></a>What is new:</h1><ol><li>Enumeration type support. <a href="https://github.com/dubbo/jmeter-plugins-dubbo/issues/34" target="_blank" rel="noopener">#34</a></li><li>Support group to zookeeper,redis registration center. <a href="https://github.com/dubbo/jmeter-plugins-dubbo/issues/33" target="_blank" rel="noopener">#33</a></li></ol><h1 id="新版改进："><a href="#新版改进：" class="headerlink" title="新版改进："></a>新版改进：</h1><ol><li>支持枚举类型参数。<a href="https://github.com/dubbo/jmeter-plugins-dubbo/issues/34" target="_blank" rel="noopener">#34</a></li><li>zookeeper、redis作为注册中心时增加group支持。 <a href="https://github.com/dubbo/jmeter-plugins-dubbo/issues/33" target="_blank" rel="noopener">#33</a></li></ol><p><span style="color:blue"><em>ps. 参数类型支持：枚举类型以及参数对象内属性为枚举类型</em></span></p><h1 id="截图"><a href="#截图" class="headerlink" title="截图"></a>截图</h1><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/jmeter-plugins-dubbo-1-3-8/1.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p><span style="color:blue"><em>ps. dubbo:registry group: 服务注册分组，跨组的服务不会相互影响，也无法相互调用，适用于环境隔离。</em></span></p><p><span style="color:blue"><em>具体查看<a href="http://dubbo.apache.org/zh-cn/docs/user/references/xml/dubbo-registry.html" target="_blank" rel="noopener">dubbo文档</a></em></span></p>]]></content>
    
    <summary type="html">
    
      [Enhancement]Enumeration type support, Dubbo Plugin for Apache JMeter - V1.3.8
    
    </summary>
    
      <category term="test" scheme="https://ningyu1.github.io/categories/test/"/>
    
    
      <category term="dubbo" scheme="https://ningyu1.github.io/tags/dubbo/"/>
    
      <category term="jmeter" scheme="https://ningyu1.github.io/tags/jmeter/"/>
    
      <category term="test" scheme="https://ningyu1.github.io/tags/test/"/>
    
      <category term="Dubbo可视化测试工具" scheme="https://ningyu1.github.io/tags/Dubbo%E5%8F%AF%E8%A7%86%E5%8C%96%E6%B5%8B%E8%AF%95%E5%B7%A5%E5%85%B7/"/>
    
      <category term="Jmeter对Dubbo接口进行可视化压力测试" scheme="https://ningyu1.github.io/tags/Jmeter%E5%AF%B9Dubbo%E6%8E%A5%E5%8F%A3%E8%BF%9B%E8%A1%8C%E5%8F%AF%E8%A7%86%E5%8C%96%E5%8E%8B%E5%8A%9B%E6%B5%8B%E8%AF%95/"/>
    
      <category term="Dubbo Jmeter插件" scheme="https://ningyu1.github.io/tags/Dubbo-Jmeter%E6%8F%92%E4%BB%B6/"/>
    
  </entry>
  
  <entry>
    <title>雪花算法-记录</title>
    <link href="https://ningyu1.github.io/20181207/105-snowflake.html"/>
    <id>https://ningyu1.github.io/20181207/105-snowflake.html</id>
    <published>2018-12-07T03:51:21.000Z</published>
    <updated>2024-04-23T01:48:15.030Z</updated>
    
    <content type="html"><![CDATA[<p>最近看到了一篇分析雪花算法的文章还不错，然后整理了一下分享出来。</p><p>先来科普一下SnowFlake算法</p><h1 id="算法原理"><a href="#算法原理" class="headerlink" title="算法原理"></a>算法原理</h1><p><a href="https://github.com/twitter/snowflake" target="_blank" rel="noopener">Twitter Snowflake</a> 生成的 unique ID 的组成 (由高位到低位):</p><p>41 bits: Timestamp (毫秒级)<br>10 bits: 节点 ID (datacenter ID 5 bits + worker ID 5 bits)<br>12 bits: sequence number<br>一共 63 bits (最高位是 0).</p><hr><h2 id="0-最高位预留-时间戳-41位-机器ID-10位-自增序列-12位"><a href="#0-最高位预留-时间戳-41位-机器ID-10位-自增序列-12位" class="headerlink" title="| 0(最高位预留) | 时间戳(41位) | 机器ID(10位) | 自增序列(12位) |"></a>| 0(最高位预留) | 时间戳(41位) | 机器ID(10位) | 自增序列(12位) |</h2><p>unique ID 生成过程:</p><ul><li>10 bits 的机器号, 在 ID 分配 Worker 启动的时候，从一个 Zookeeper 集群获取 (保证所有的 Worker 不会有重复的机器号)；</li><li>41 bits 的 Timestamp: 每次要生成一个新 ID 的时候，都会获取一下当前的 Timestamp, 然后分两种情况生成 sequence number；</li><li>如果当前的 Timestamp 和前一个已生成 ID 的 Timestamp 相同 (在同一毫秒中)，就用前一个 ID 的 sequence number + 1 作为新的 sequence number (12 bits);<br>如果本毫秒内的所有 ID 用完，等到下一毫秒继续 (这个等待过程中, 不能分配出新的 ID)；</li><li>如果当前的 Timestamp 比前一个 ID 的 Timestamp 大, 随机生成一个初始 sequence number (12bits) 作为本毫秒内的第一个 sequence number；</li></ul><p>41-bit的时间可以表示（1L&lt;&lt;41）/(1000L x 3600 x 24 x 365)=69年的时间，10-bit机器可以分别表示1024台机器。如果我们对IDC划分有需求，还可以将10-bit分5-bit给IDC，分5-bit给工作机器。这样就可以表示32个IDC，每个IDC下可以有32台机器，可以根据自身需求定义。12个自增序列号可以表示2^12个ID，理论上snowflake方案的QPS约为409.6w/s，这种分配方式可以保证在任何一个IDC的任何一台机器在任意毫秒内生成的ID都是不同的。</p><p>优缺点这里就不赘述了。</p><p>那我们继续看一个经典的Java版本的实现，这个在网上一搜一大把，官方原版的<a href="https://github.com/twitter-archive/snowflake/blob/snowflake-2010/src/main/scala/com/twitter/service/snowflake/IdWorker.scala" target="_blank" rel="noopener">Scala版本</a></p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">public class Snowflake &#123;</span><br><span class="line"></span><br><span class="line">private static final Logger logger = LoggerFactory.getLogger(Snowflake.class);</span><br><span class="line"></span><br><span class="line">/**</span><br><span class="line"> * 机器ID</span><br><span class="line"> */</span><br><span class="line">private final long workerId;</span><br><span class="line">/**</span><br><span class="line"> * 时间起始标记点，作为基准，一般取系统的最近时间，默认2017-01-01</span><br><span class="line"> */</span><br><span class="line">private final long epoch = 1483200000000L;</span><br><span class="line">/**</span><br><span class="line"> * 机器id所占的位数（源设计为5位，这里取消dataCenterId，采用10位，既1024台）</span><br><span class="line"> */</span><br><span class="line">private final long workerIdBits = 10L;</span><br><span class="line">/**</span><br><span class="line"> * 机器ID最大值: 1023 (从0开始)</span><br><span class="line"> */</span><br><span class="line">private final long maxWorkerId = -1L ^ -1L &lt;&lt; this.workerIdBits;</span><br><span class="line">/**</span><br><span class="line"> * 机器ID向左移12位</span><br><span class="line"> */</span><br><span class="line">private final long workerIdShift = this.sequenceBits;</span><br><span class="line">/** </span><br><span class="line"> * 时间戳向左移22位(5+5+12)</span><br><span class="line"> */</span><br><span class="line">private final long timestampLeftShift = this.sequenceBits + this.workerIdBits;</span><br><span class="line">/** </span><br><span class="line"> * 序列在id中占的位数</span><br><span class="line"> */</span><br><span class="line">private final long sequenceBits = 12L;</span><br><span class="line">/** </span><br><span class="line"> * 生成序列的掩码，这里为4095 (0b111111111111=0xfff=4095)，12位</span><br><span class="line"> */</span><br><span class="line">private final long sequenceMask = -1L ^ -1L &lt;&lt; this.sequenceBits;</span><br><span class="line">/**</span><br><span class="line"> * 并发控制，毫秒内序列(0~4095)</span><br><span class="line"> */</span><br><span class="line">private long sequence = 0L;</span><br><span class="line">/** </span><br><span class="line"> * 上次生成ID的时间戳 </span><br><span class="line"> */</span><br><span class="line">private long lastTimestamp = -1L;</span><br><span class="line"></span><br><span class="line">private final int HUNDRED_K = 100_000;</span><br><span class="line"></span><br><span class="line">/**</span><br><span class="line"> * @param workerId 机器Id</span><br><span class="line"> */</span><br><span class="line">private Snowflake(long workerId) &#123;</span><br><span class="line">if (workerId &gt; this.maxWorkerId || workerId &lt; 0) &#123;</span><br><span class="line">String message = String.format(&quot;worker Id can&apos;t be greater than %d or less than 0&quot;, this.maxWorkerId);</span><br><span class="line">throw new IllegalArgumentException(message);</span><br><span class="line">&#125;</span><br><span class="line">this.workerId = workerId;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">/**</span><br><span class="line"> * Snowflake Builder</span><br><span class="line"> * @param workerId workerId</span><br><span class="line"> * @return Snowflake Instance</span><br><span class="line"> */</span><br><span class="line">public static Snowflake create(long workerId) &#123;</span><br><span class="line">return new Snowflake(workerId);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">/**</span><br><span class="line"> * 批量获取ID</span><br><span class="line"> * @param size 获取大小，最多10万个</span><br><span class="line"> * @return SnowflakeId</span><br><span class="line"> */</span><br><span class="line">public long[] nextId(int size) &#123;</span><br><span class="line">if (size &lt;= 0 || size &gt; HUNDRED_K) &#123;</span><br><span class="line">String message = String.format(&quot;Size can&apos;t be greater than %d or less than 0&quot;, HUNDRED_K);</span><br><span class="line">throw new IllegalArgumentException(message);</span><br><span class="line">&#125;</span><br><span class="line">long[] ids = new long[size];</span><br><span class="line">for (int i = 0; i &lt; size; i++) &#123;</span><br><span class="line">ids[i] = nextId();</span><br><span class="line">&#125;</span><br><span class="line">return ids;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">/**</span><br><span class="line"> * 获得ID</span><br><span class="line"> * @return SnowflakeId</span><br><span class="line"> */</span><br><span class="line">public synchronized long nextId() &#123;</span><br><span class="line">long timestamp = timeGen();</span><br><span class="line"></span><br><span class="line">// 如果上一个timestamp与新产生的相等，则sequence加一(0-4095循环);</span><br><span class="line">if (this.lastTimestamp == timestamp) &#123;</span><br><span class="line">// 对新的timestamp，sequence从0开始</span><br><span class="line">this.sequence = this.sequence + 1 &amp; this.sequenceMask;</span><br><span class="line">// 毫秒内序列溢出</span><br><span class="line">if (this.sequence == 0) &#123;</span><br><span class="line">// 阻塞到下一个毫秒,获得新的时间戳</span><br><span class="line">timestamp = this.tilNextMillis(this.lastTimestamp);</span><br><span class="line">&#125;</span><br><span class="line">&#125; else &#123;</span><br><span class="line">// 时间戳改变，毫秒内序列重置</span><br><span class="line">this.sequence = 0;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">// 如果当前时间小于上一次ID生成的时间戳，说明系统时钟回退过这个时候应当抛出异常</span><br><span class="line">if (timestamp &lt; this.lastTimestamp) &#123;</span><br><span class="line">String message = String.format(&quot;Clock moved backwards. Refusing to generate id for %d milliseconds.&quot;, (this.lastTimestamp - timestamp));</span><br><span class="line">logger.error(message);</span><br><span class="line">throw new RuntimeException(message);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">this.lastTimestamp = timestamp;</span><br><span class="line">// 移位并通过或运算拼到一起组成64位的ID</span><br><span class="line">return timestamp - this.epoch &lt;&lt; this.timestampLeftShift | this.workerId &lt;&lt; this.workerIdShift | this.sequence;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">/**</span><br><span class="line"> * 等待下一个毫秒的到来, 保证返回的毫秒数在参数lastTimestamp之后</span><br><span class="line"> * @param lastTimestamp 上次生成ID的时间戳 </span><br><span class="line"> * @return</span><br><span class="line"> */</span><br><span class="line">private long tilNextMillis(long lastTimestamp) &#123;</span><br><span class="line">long timestamp = timeGen();</span><br><span class="line">while (timestamp &lt;= lastTimestamp) &#123;</span><br><span class="line">timestamp = timeGen();</span><br><span class="line">&#125;</span><br><span class="line">return timestamp;</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"> */</span><br><span class="line">private long timeGen() &#123;</span><br><span class="line">return System.currentTimeMillis();</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>那让我们看一下代码来理解一下算法的细节。</p><h1 id="代码理解"><a href="#代码理解" class="headerlink" title="代码理解"></a>代码理解</h1><p>我们从关键的代码段来理解，如下：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">this.sequence = this.sequence + 1 &amp; this.sequenceMask;</span><br><span class="line"></span><br><span class="line">private final long maxWorkerId = -1L ^ -1L &lt;&lt; this.workerIdBits;</span><br><span class="line"></span><br><span class="line">return ((timestamp - this.epoch) &lt;&lt; this.timestampLeftShift)</span><br><span class="line"> | (this.workerId &lt;&lt; this.workerIdShift)</span><br><span class="line"> | this.sequence;</span><br></pre></td></tr></table></figure><p><span style="color:blue"><em>ps. 我这里取消了datacenterId，将datacenterId和workerid合并到workerIdBits</em></span></p><h2 id="负数的二进制表示"><a href="#负数的二进制表示" class="headerlink" title="负数的二进制表示"></a>负数的二进制表示</h2><p>在计算机中，负数的二进制是用补码来表示的。<br>假设我是用Java中的int类型来存储数字的，<br>int类型的大小是32个二进制位（bit），即4个字节（byte）。（1 byte = 8 bit）<br>那么十进制数字3在二进制中的表示应该是这样的：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">00000000 00000000 00000000 00000011</span><br><span class="line">// 3的二进制表示，就是原码</span><br></pre></td></tr></table></figure><p>那数字-3在二进制中应该如何表示？<br>我们可以反过来想想，因为-3+3=0，<br>在二进制运算中把-3的二进制看成未知数x来求解，<br>求解算式的二进制表示如下：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">   00000000 00000000 00000000 00000011 //3，原码</span><br><span class="line">+  xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx //-3，补码</span><br><span class="line">-----------------------------------------------</span><br><span class="line">   00000000 00000000 00000000 00000000</span><br></pre></td></tr></table></figure><p>反推x的值，3的二进制加上什么值才使结果变成00000000 00000000 00000000 00000000？：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">   00000000 00000000 00000000 00000011 //3，原码                         </span><br><span class="line">+  11111111 11111111 11111111 11111101 //-3，补码</span><br><span class="line">-----------------------------------------------</span><br><span class="line"> 1 00000000 00000000 00000000 00000000</span><br></pre></td></tr></table></figure><p>反推的思路是3的二进制数从最低位开始逐位加1，使溢出的1不断向高位溢出，直到溢出到第33位。然后由于int类型最多只能保存32个二进制位，所以最高位的1溢出了，剩下的32位就成了（十进制的）0。</p><p>补码的意义就是可以拿补码和原码（3的二进制）相加，最终加出一个“溢出的0”</p><p>以上是理解的过程，实际中记住公式就很容易算出来：</p><p>补码 = 反码 + 1<br>补码 = （原码 - 1）再取反码<br>因此-1的二进制应该这样算：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">00000000 00000000 00000000 00000001 //原码：1的二进制</span><br><span class="line">11111111 11111111 11111111 11111110 //取反码：1的二进制的反码</span><br><span class="line">11111111 11111111 11111111 11111111 //加1：-1的二进制表示（补码）</span><br></pre></td></tr></table></figure><p>具体对位运算以及二进制的计算理解可以看看这篇文章<a href="https://blog.csdn.net/cj2580/article/details/80980459" target="_blank" rel="noopener">https://blog.csdn.net/cj2580/article/details/80980459</a></p>]]></content>
    
    <summary type="html">
    
      雪花算法-记录
    
    </summary>
    
      <category term="snowflake" scheme="https://ningyu1.github.io/categories/snowflake/"/>
    
    
      <category term="snowflake" scheme="https://ningyu1.github.io/tags/snowflake/"/>
    
  </entry>
  
  <entry>
    <title>[Enhancement]Support to select provider from zookeeper, Dubbo Plugin for Apache JMeter - V1.3.7</title>
    <link href="https://ningyu1.github.io/20181113/104-jmeter-plugin-dubbo-1.3.7.html"/>
    <id>https://ningyu1.github.io/20181113/104-jmeter-plugin-dubbo-1.3.7.html</id>
    <published>2018-11-13T10:51:21.000Z</published>
    <updated>2024-04-23T01:48:15.030Z</updated>
    
    <content type="html"><![CDATA[<h1 id="项目地址"><a href="#项目地址" class="headerlink" title="项目地址"></a>项目地址</h1><p><a href="https://github.com/dubbo/jmeter-plugins-dubbo" target="_blank" rel="noopener">github: jmeter-plugin-dubbo</a> </p><p><a href="https://gitee.com/ningyu/jmeter-plugins-dubbo" target="_blank" rel="noopener">码云: jmeter-plugin-dubbo</a></p><h1 id="V1-3-7"><a href="#V1-3-7" class="headerlink" title="V1.3.7"></a><a href="https://github.com/dubbo/jmeter-plugins-dubbo/releases/tag/V1.3.7" target="_blank" rel="noopener">V1.3.7</a></h1><h1 id="What-is-new"><a href="#What-is-new" class="headerlink" title="What is new:"></a>What is new:</h1><ol><li>Support to select provider from zookeeper. issue: #31 </li><li>Upgrade dubbo version to v2.6.4.</li></ol><h1 id="新版改进："><a href="#新版改进：" class="headerlink" title="新版改进："></a>新版改进：</h1><ol><li>支持从zookeeper选择服务提供者，降低手动输入出错概率，issue: #31 </li><li>升级dubbo版本到v2.6.4</li></ol><h1 id="截图"><a href="#截图" class="headerlink" title="截图"></a>截图</h1><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/jmeter-plugins-dubbo-1-3-7/1.png" alt title>                </div>                <div class="image-caption"></div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/jmeter-plugins-dubbo-1-3-7/2.png" alt title>                </div>                <div class="image-caption"></div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/jmeter-plugins-dubbo-1-3-7/3.png" alt title>                </div>                <div class="image-caption"></div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/jmeter-plugins-dubbo-1-3-7/4.png" alt title>                </div>                <div class="image-caption"></div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/jmeter-plugins-dubbo-1-3-7/5.png" alt title>                </div>                <div class="image-caption"></div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/jmeter-plugins-dubbo-1-3-7/6.png" alt title>                </div>                <div class="image-caption"></div>            </figure>]]></content>
    
    <summary type="html">
    
      [Enhancement]Support to select provider from zookeeper, Dubbo Plugin for Apache JMeter - V1.3.7
    
    </summary>
    
      <category term="test" scheme="https://ningyu1.github.io/categories/test/"/>
    
    
      <category term="dubbo" scheme="https://ningyu1.github.io/tags/dubbo/"/>
    
      <category term="jmeter" scheme="https://ningyu1.github.io/tags/jmeter/"/>
    
      <category term="test" scheme="https://ningyu1.github.io/tags/test/"/>
    
      <category term="Dubbo可视化测试工具" scheme="https://ningyu1.github.io/tags/Dubbo%E5%8F%AF%E8%A7%86%E5%8C%96%E6%B5%8B%E8%AF%95%E5%B7%A5%E5%85%B7/"/>
    
      <category term="Jmeter对Dubbo接口进行可视化压力测试" scheme="https://ningyu1.github.io/tags/Jmeter%E5%AF%B9Dubbo%E6%8E%A5%E5%8F%A3%E8%BF%9B%E8%A1%8C%E5%8F%AF%E8%A7%86%E5%8C%96%E5%8E%8B%E5%8A%9B%E6%B5%8B%E8%AF%95/"/>
    
      <category term="Dubbo Jmeter插件" scheme="https://ningyu1.github.io/tags/Dubbo-Jmeter%E6%8F%92%E4%BB%B6/"/>
    
  </entry>
  
  <entry>
    <title>Trouble Shooting —— jms:listener-container配置queue的concurrency数量与预期不一致</title>
    <link href="https://ningyu1.github.io/20181030/103-activemq-listener-concurrency.html"/>
    <id>https://ningyu1.github.io/20181030/103-activemq-listener-concurrency.html</id>
    <published>2018-10-30T10:40:00.000Z</published>
    <updated>2024-04-23T01:48:15.030Z</updated>
    
    <content type="html"><![CDATA[<ul><li><a href="#103-desc">问题描述</a><ul><li><a href="#103-1">现象一</a></li><li><a href="#103-2">现象二</a></li></ul></li><li><a href="#103-test">测试消费者</a></li><li><a href="#103-solution">测试后结论</a></li></ul><h1 id="问题描述"><a href="#问题描述" class="headerlink" title="问题描述"></a><span id="103-desc">问题描述</span></h1><p>测试程序时发现queue的consumer数量配置与预期不一致，具体如何不一致看下面的测试。</p><h2 id="现象一"><a href="#现象一" class="headerlink" title="现象一"></a><span id="103-1">现象一</span></h2><p>当我们使用下面配置，listener使用同一个task-executor并且监听三个queue时，consumer使用20-20，只会有一个queue能达到20个consumer，其余两个queue的consumer=0</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">&lt;bean id=&quot;queueMessageExecutor1&quot; class=&quot;org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor&quot;&gt;</span><br><span class="line">   &lt;property name=&quot;corePoolSize&quot; value=&quot;20&quot; /&gt;</span><br><span class="line">   &lt;property name=&quot;maxPoolSize&quot; value=&quot;20&quot; /&gt;</span><br><span class="line">   &lt;property name=&quot;daemon&quot; value=&quot;true&quot; /&gt;</span><br><span class="line">   &lt;property name=&quot;keepAliveSeconds&quot; value=&quot;120&quot; /&gt;</span><br><span class="line">&lt;/bean&gt;</span><br><span class="line">&lt;jms:listener-container task-executor=&quot;queueMessageExecutor1&quot; destination-type=&quot;queue&quot; container-type=&quot;default&quot; connection-factory=&quot;pooledConnectionFactory&quot;</span><br><span class="line">                  concurrency=&quot;20-20&quot; acknowledge=&quot;auto&quot; receive-timeout=&quot;60000&quot;&gt;</span><br><span class="line">   &lt;jms:listener destination=&quot;QUEUE.EMAIL&quot; ref=&quot;mailMessageListener&quot; /&gt;</span><br><span class="line">   &lt;jms:listener destination=&quot;QUEUE.SMS&quot; ref=&quot;smsMessageListener&quot; /&gt;</span><br><span class="line">   &lt;jms:listener destination=&quot;QUEUE.WECHAT&quot; ref=&quot;wechatMessageListener&quot; /&gt;</span><br><span class="line">&lt;/jms:listener-container&gt;</span><br></pre></td></tr></table></figure><p>效果如下图：</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/activemq-listener-concurrency/1.png" alt title>                </div>                <div class="image-caption"></div>            </figure><h2 id="现象二"><a href="#现象二" class="headerlink" title="现象二"></a><span id="103-2">现象二</span></h2><p>当我们去掉listener-container的<code>receive-timeout=&quot;60000&quot;</code>的配置，三个queue的consumer都等于20。</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">&lt;bean id=&quot;queueMessageExecutor1&quot; class=&quot;org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor&quot;&gt;</span><br><span class="line">   &lt;property name=&quot;corePoolSize&quot; value=&quot;20&quot; /&gt;</span><br><span class="line">   &lt;property name=&quot;maxPoolSize&quot; value=&quot;20&quot; /&gt;</span><br><span class="line">   &lt;property name=&quot;daemon&quot; value=&quot;true&quot; /&gt;</span><br><span class="line">   &lt;property name=&quot;keepAliveSeconds&quot; value=&quot;120&quot; /&gt;</span><br><span class="line">&lt;/bean&gt;</span><br><span class="line">&lt;jms:listener-container task-executor=&quot;queueMessageExecutor1&quot; destination-type=&quot;queue&quot; container-type=&quot;default&quot; connection-factory=&quot;pooledConnectionFactory&quot;</span><br><span class="line">                  concurrency=&quot;20-20&quot; acknowledge=&quot;auto&quot;&gt;</span><br><span class="line">   &lt;jms:listener destination=&quot;QUEUE.EMAIL&quot; ref=&quot;mailMessageListener&quot; /&gt;</span><br><span class="line">   &lt;jms:listener destination=&quot;QUEUE.SMS&quot; ref=&quot;smsMessageListener&quot; /&gt;</span><br><span class="line">   &lt;jms:listener destination=&quot;QUEUE.WECHAT&quot; ref=&quot;wechatMessageListener&quot; /&gt;</span><br><span class="line">&lt;/jms:listener-container&gt;</span><br></pre></td></tr></table></figure><p>效果如下图：</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/activemq-listener-concurrency/2.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p>这两种现象之间的差异就在<code>receive-timeout=&quot;60000&quot;</code>这个属性上，接下来让我们看一下“现象一”、“现象二”的jvm启动的consumer线程的具体信息，如下图：</p><p>现象一的线程信息：</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/activemq-listener-concurrency/3.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p>现象二的线程信息：</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/activemq-listener-concurrency/4.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p>从线程的信息上看，线程的数量与线程池的配置信息吻合，具体开多少个线程取决于线程池的大小，这个与预期一致，拿为什么两种现象锁显示的queue的consumer数量不同呢？</p><p>同样是20个线程，但是在现象二中三个queue的consumer分别都是20个，那总数就是60个完全超过了线程的数量，从这点能看的出来consumer的数量是逻辑数量，也就是说20个线程来承接60个逻辑消费者，每个线程会随机的去拿某一个queue里的消息。</p><h1 id="测试消费者"><a href="#测试消费者" class="headerlink" title="测试消费者"></a><span id="103-test">测试消费者</span></h1><p>当我们在“现象一”中只有一个queue有consumer，其他queue没有consumer，我们往没有consumer的q中写消息，看些消息的这个q是否有会consumer出现？</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/activemq-listener-concurrency/5.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p>当消息积压到一定的时间（测试下来时间为：14:18分积压消息，14:27分增加了20个consumer消费掉了积压消息）</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/activemq-listener-concurrency/6.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p>我们再往wechat中发送积压消息，看看wechat的consumer是否会增加？</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/activemq-listener-concurrency/7.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p>当消息积压到一定的时间（测试下来时间为：14:34分积压消息，14:38分增加了20个consumer消费掉了积压消息）</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/activemq-listener-concurrency/8.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p>一旦增加上来了consumer目前看下来不会自动消失</p><h1 id="测试后结论"><a href="#测试后结论" class="headerlink" title="测试后结论"></a><span id="103-solution">测试后结论</span></h1><p>当listener-container使用同一个<code>task-executor</code>并且监听多个q时：</p><ul><li>listener-container设置了<code>receive-timeout=&quot;60000</code>（接受超时时间），线程数会优先处理配置中第一个q上，其他q不会有consumer数量，当其他q有消息积压时会自动增加consumer数量，但是增加的时间不太规律。</li><li>listener-container没有设置<code>receive-timeout=&quot;60000</code>（接受超时时间），线程数会处理多个q的消息接收，随机接收某个q的消息，或者是那个q的消息积压的多会优先接受那个q的消息。</li></ul><p>ps. 同一个listener-container监听多个q，线程会接收多个q的消息（多个q共享接收消息线程），只不过q的consumer数量初始化的时间不同，如果不配置<code>receive-timeout=&quot;60000</code>（接受超时时间）这个参数，q的consumer数量在启动时就会初始化。</p><p>当listener-container使用不同的<code>task-executor</code>并且只监听一个q时：</p><ul><li>设不设置<code>receive-timeout=&quot;60000</code>（接受超时时间）没有区别，一个线程池中的线程只会处理一个q的消息接收，对于消息量大存在积压的情况下，可以独立配置线程池和监听器让这个q的处理线程资源独享。</li></ul>]]></content>
    
    <summary type="html">
    
      Trouble Shooting —— jms:listener-container配置queue的concurrency数量与预期不一致
    
    </summary>
    
      <category term="activemq" scheme="https://ningyu1.github.io/categories/activemq/"/>
    
      <category term="trouble shooting" scheme="https://ningyu1.github.io/categories/activemq/trouble-shooting/"/>
    
    
      <category term="activemq" scheme="https://ningyu1.github.io/tags/activemq/"/>
    
  </entry>
  
  <entry>
    <title>Trouble Shooting —— 莫名其妙的java.lang.NoClassDefFoundError: org.springframework.beans.FatalBeanException异常</title>
    <link href="https://ningyu1.github.io/20180929/102-NoClassDefFoundError.html"/>
    <id>https://ningyu1.github.io/20180929/102-NoClassDefFoundError.html</id>
    <published>2018-09-29T07:30:00.000Z</published>
    <updated>2024-04-23T01:48:15.029Z</updated>
    
    <content type="html"><![CDATA[<ul><li><a href="#desc">问题描述</a></li><li><a href="#analyze">问题分析</a><ul><li><a href="#retry1">尝试一</a></li><li><a href="#retry2">尝试二</a></li><li><a href="#retry3">尝试三</a></li><li><a href="#retry4">尝试四</a></li></ul></li><li><a href="#solution">解决方法</a></li></ul><h1 id="问题描述"><a href="#问题描述" class="headerlink" title="问题描述"></a><span id="desc">问题描述</span></h1><p>最近运维在部署应用的时候偶尔会碰到下面的异常：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">Exception in thread &quot;main&quot; java.lang.NoClassDefFoundError: org.springframework.beans.FatalBeanException</span><br><span class="line">    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:547)</span><br><span class="line">    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:475)</span><br><span class="line">    at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:304)</span><br><span class="line">    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:228)</span><br><span class="line">    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:300)</span><br><span class="line">    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:195)</span><br><span class="line">    at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:700)</span><br><span class="line">    at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:760)</span><br><span class="line">    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:482)</span><br><span class="line">    at org.springframework.context.support.ClassPathXmlApplicationContext.&lt;init&gt;(ClassPathXmlApplicationContext.java:139)</span><br><span class="line">    at org.springframework.context.support.ClassPathXmlApplicationContext.&lt;init&gt;(ClassPathXmlApplicationContext.java:93)</span><br><span class="line">    at com.alibaba.dubbo.container.spring.SpringContainer.start(SpringContainer.java:50)</span><br><span class="line">    at com.alibaba.dubbo.container.Main.main(Main.java:80)</span><br></pre></td></tr></table></figure><p>这个异常看上去是<code>org.springframework.beans.FatalBeanException</code>在运行时找不到class，但是调试起来很懵逼。</p><h1 id="问题分析"><a href="#问题分析" class="headerlink" title="问题分析"></a><span id="analyze">问题分析</span></h1><h2 id="尝试一"><a href="#尝试一" class="headerlink" title="尝试一"></a><span id="retry1">尝试一</span></h2><p>怀疑这个类<code>org.springframework.beans.FatalBeanException</code>在<code>classloader</code>的时候无法找到。</p><p>这个类<code>org.springframework.beans.FatalBeanException</code>在<code>spring-beans</code>包下，查看打包的<code>lib</code>下存在<code>spring-beans</code>包，查看运行<code>jar</code>中的<code>META-INF</code>下的<code>MANIFEST.MF</code>文件中也有<code>lib/spring-beans-4.0.0.RELEASE.jar</code></p><p>因此排除了这个怀疑。</p><p>ps.这里要区分一下<code>NoClassDefFoundError</code>和<code>ClassNotFoundException</code>异常看这篇<a href="https://blog.csdn.net/muskter/article/details/72236192" target="_blank" rel="noopener">文章</a></p><h2 id="尝试二"><a href="#尝试二" class="headerlink" title="尝试二"></a><span id="retry2">尝试二</span></h2><p>这个类在<code>spring-beans</code>包中，那是不是这个jar包损坏无法读取？</p><p>查看了<code>jar</code>包信息以及打开与解压也排除了jar包损坏的可能性。</p><h2 id="尝试三"><a href="#尝试三" class="headerlink" title="尝试三"></a><span id="retry3">尝试三</span></h2><p>修改<code>log</code>级别改为<code>debug</code>看会不会有更多的日志输出。</p><p>通过日志级别的调整为<code>debug</code>后，除了都了一些<code>debug</code>的常规日志以外，错误相关的日志还是跟上面的输出一样，因此也是无济于事。</p><h2 id="尝试四"><a href="#尝试四" class="headerlink" title="尝试四"></a><span id="retry4">尝试四</span></h2><p>通过<code>arthas</code>观察<code>org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory</code>这个类的<code>doCreateBean</code>这个方法异常的输出。</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">arthas $&#123;pid&#125;</span><br><span class="line"> </span><br><span class="line">watch org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory doCreateBean &quot;&#123;params, throwExp&#125;&quot; -e -x 2</span><br></pre></td></tr></table></figure><p>发现如下更多的日志：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">ts=2018-09-25 18:06:37;result=@ArrayList[</span><br><span class="line">    @Object[][</span><br><span class="line">        @String[xxxMapper],</span><br><span class="line">        @RootBeanDefinition[Root bean: class [org.mybatis.spring.mapper.MapperFa</span><br><span class="line">ctoryBean]; scope=singleton; abstract=false; lazyInit=false; autowireMode=2; dep</span><br><span class="line">endencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=null; fac</span><br><span class="line">toryMethodName=null; initMethodName=null; destroyMethodName=null; defined in URL</span><br><span class="line"> [jar:file:/E:/user/desktop/ningyu/Desktop/xxx-main-1.0.0-SNAPSHOT-201809251509/</span><br><span class="line">lib/xxx-service-JD-1.0.0-SNAPSHOT.jar!/com/xxx/xxx/order/mapper/xxxMapper.class]],</span><br><span class="line">        null,</span><br><span class="line">    ],</span><br><span class="line">    java.lang.NoClassDefFoundError: org.springframework.beans.FatalBeanException</span><br><span class="line">        at org.springframework.beans.factory.support.AbstractAutowireCapableBean</span><br><span class="line">Factory.doCreateBean(AbstractAutowireCapableBeanFactory.java:547)</span><br><span class="line">        at org.springframework.beans.factory.support.AbstractAutowireCapableBean</span><br><span class="line">Factory.createBean(AbstractAutowireCapableBeanFactory.java:475)</span><br><span class="line">        at org.springframework.beans.factory.support.AbstractBeanFactory$1.getOb</span><br><span class="line">ject(AbstractBeanFactory.java:304)</span><br><span class="line">        at org.springframework.beans.factory.support.DefaultSingletonBeanRegistr</span><br><span class="line">y.getSingleton(DefaultSingletonBeanRegistry.java:228)</span><br><span class="line">        at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBe</span><br><span class="line">an(AbstractBeanFactory.java:300)</span><br><span class="line">        at org.springframework.beans.factory.support.AbstractBeanFactory.getType</span><br><span class="line">ForFactoryBean(AbstractBeanFactory.java:1420)</span><br><span class="line">        at org.springframework.beans.factory.support.AbstractAutowireCapableBean</span><br><span class="line">Factory.getTypeForFactoryBean(AbstractAutowireCapableBeanFactory.java:788)</span><br><span class="line">        at org.springframework.beans.factory.support.AbstractBeanFactory.isTypeM</span><br><span class="line">atch(AbstractBeanFactory.java:543)</span><br><span class="line">        at org.springframework.beans.factory.support.DefaultListableBeanFactory.</span><br><span class="line">doGetBeanNamesForType(DefaultListableBeanFactory.java:384)</span><br><span class="line">        at org.springframework.beans.factory.support.DefaultListableBeanFactory.</span><br><span class="line">getBeanNamesForType(DefaultListableBeanFactory.java:361)</span><br><span class="line">        at org.springframework.beans.factory.BeanFactoryUtils.beanNamesForTypeIn</span><br><span class="line">cludingAncestors(BeanFactoryUtils.java:187)</span><br><span class="line">        at org.springframework.beans.factory.support.DefaultListableBeanFactory.</span><br><span class="line">findAutowireCandidates(DefaultListableBeanFactory.java:999)</span><br><span class="line">        at org.springframework.beans.factory.support.DefaultListableBeanFactory.</span><br><span class="line">doResolveDependency(DefaultListableBeanFactory.java:957)</span><br><span class="line">        at org.springframework.beans.factory.support.DefaultListableBeanFactory.</span><br><span class="line">resolveDependency(DefaultListableBeanFactory.java:855)</span><br><span class="line">        at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanP</span><br><span class="line">ostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.j</span><br><span class="line">ava:480)</span><br><span class="line">        at org.springframework.beans.factory.annotation.InjectionMetadata.inject</span><br><span class="line">(InjectionMetadata.java:87)</span><br><span class="line">        at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanP</span><br><span class="line">ostProcessor.postProcessPropertyValues(AutowiredAnnotationBeanPostProcessor.java</span><br><span class="line">:289)</span><br><span class="line">        at org.springframework.beans.factory.support.AbstractAutowireCapableBean</span><br><span class="line">Factory.populateBean(AbstractAutowireCapableBeanFactory.java:1185)</span><br><span class="line">        at org.springframework.beans.factory.support.AbstractAutowireCapableBean</span><br><span class="line">Factory.doCreateBean(AbstractAutowireCapableBeanFactory.java:537)</span><br><span class="line">        at org.springframework.beans.factory.support.AbstractAutowireCapableBean</span><br><span class="line">Factory.createBean(AbstractAutowireCapableBeanFactory.java:475)</span><br><span class="line">        at org.springframework.beans.factory.support.AbstractBeanFactory$1.getOb</span><br><span class="line">ject(AbstractBeanFactory.java:304)</span><br><span class="line">        at org.springframework.beans.factory.support.DefaultSingletonBeanRegistr</span><br><span class="line">y.getSingleton(DefaultSingletonBeanRegistry.java:228)</span><br><span class="line">        at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBe</span><br><span class="line">an(AbstractBeanFactory.java:300)</span><br><span class="line">        at org.springframework.beans.factory.support.AbstractBeanFactory.getBean</span><br><span class="line">(AbstractBeanFactory.java:195)</span><br><span class="line">        at org.springframework.beans.factory.support.DefaultListableBeanFactory.</span><br><span class="line">preInstantiateSingletons(DefaultListableBeanFactory.java:700)</span><br><span class="line">        at org.springframework.context.support.AbstractApplicationContext.finish</span><br><span class="line">BeanFactoryInitialization(AbstractApplicationContext.java:760)</span><br><span class="line">        at org.springframework.context.support.AbstractApplicationContext.refres</span><br><span class="line">h(AbstractApplicationContext.java:482)</span><br><span class="line">        at org.springframework.context.support.ClassPathXmlApplicationContext.&lt;i</span><br><span class="line">nit&gt;(ClassPathXmlApplicationContext.java:139)</span><br><span class="line">        at org.springframework.context.support.ClassPathXmlApplicationContext.&lt;i</span><br><span class="line">nit&gt;(ClassPathXmlApplicationContext.java:93)</span><br><span class="line">        at com.alibaba.dubbo.container.spring.SpringContainer.start(SpringContai</span><br><span class="line">ner.java:50)</span><br><span class="line">        at com.alibaba.dubbo.container.Main.main(Main.java:80)</span><br><span class="line">,</span><br><span class="line">]</span><br></pre></td></tr></table></figure><p>而且这个信息不停的打，并且看到的全是<code>xxxMapper</code>？</p><p>难道是<code>mybatis</code>的<code>mapper</code>代理类的创建出现了问题？</p><p>尝试本地通过代码的方式启动服务，没有任何问题。</p><p>又尝试本地通过打出的<code>zip</code>包，通过<code>java -jar</code>的方式启动，也没有任何问题。</p><p>这个时候就很头疼了，定位不到问题，而且问题不能重现。</p><p>网上能搜索到关于<code>mybatis</code>启动报<code>Stack overflow</code>的错误，难道我们这个问题跟他也有关系？于是尝试看一下<code>mybatis</code>的<code>mapper</code>代理自动创建的相关资料。</p><p>通过这篇<a href="https://blog.csdn.net/hongxingxiaonan/article/details/50354195" target="_blank" rel="noopener">文章</a></p><p>当<code>MapperFactoryBean</code>实例生成之后，<code>Spring</code>给它注入<code>SqlSessionTemplate</code>。而注入<code>SqlSessionTemplate</code>的过程中会向容器获取所有的<code>Dao</code>，对于已经在容器中的<code>Dao</code>所对应的<code>bean</code>可以直接获取返回，若还没有创建<code>bean</code>，则<code>Spring</code>又会先创建这个<code>Dao</code>的<code>MapperFactoryBean</code>。创建<code>MapperFactoryBean</code>的时候会再次注入<code>SqlSessionTemplate</code>。就这样一直循环下去，直到所有的<code>Dao</code>都已经创建完毕，这个过程才算结束。</p><p>看来跟<code>mybatis</code>的关系应该很大，网上有有说<code>mybatis Mapper</code>有导致过<code>stack overflow</code>的错误，新想如果是<code>stack overflow</code>肯定应该是有明确的异常抛出，于是也是抱着尝试调整一下<code>jvm</code>的参数看看是否有惊喜。</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">-Xms1024m -Xmx1024m -XX:PermSize=256m -XX:MaxPermSize=256m -Xss256k</span><br></pre></td></tr></table></figure><p><code>stack overflow</code>应该调整<code>Xss</code>参数大小（<code>-Xss512k</code>）调整后重启，竟然成功了！竟然成功了！竟然成功了！</p><p>太不可思议了难道是<code>stack overflow</code>异常被吃掉了？而且<code>mapper</code>在创建的时候是递归，递归的层次越深越消耗stack大小，然后具体搜索mybatis导致<code>stack</code>异常的信息看到了这篇<a href="http://fantaxy025025.iteye.com/blog/2223217" target="_blank" rel="noopener">文章</a>，上面就是说<code>mybatis-spring</code>工具包有问题将异常吃掉了，具体<code>mybatis-spring</code>中的那段代码我还在定位，定位好了在更新文章</p><h1 id="解决方法"><a href="#解决方法" class="headerlink" title="解决方法"></a><span id="solution">解决方法</span></h1><p>调整xss参数，从xss256k调整为xss512k</p>]]></content>
    
    <summary type="html">
    
      Trouble Shooting —— 莫名其妙的java.lang.NoClassDefFoundError: org.springframework.beans.FatalBeanException异常
    
    </summary>
    
      <category term="mybatis" scheme="https://ningyu1.github.io/categories/mybatis/"/>
    
      <category term="trouble shooting" scheme="https://ningyu1.github.io/categories/mybatis/trouble-shooting/"/>
    
    
      <category term="mybatis" scheme="https://ningyu1.github.io/tags/mybatis/"/>
    
  </entry>
  
  <entry>
    <title>zookeeper数据迁移及恢复</title>
    <link href="https://ningyu1.github.io/20180928/101-zookeeper-data-migrate.html"/>
    <id>https://ningyu1.github.io/20180928/101-zookeeper-data-migrate.html</id>
    <published>2018-09-28T03:20:00.000Z</published>
    <updated>2024-04-23T01:48:15.029Z</updated>
    
    <content type="html"><![CDATA[<p>在做环境迁移的时候经常会遇到中间件的数据迁移，今天我们说一下zookeeper的数据如何迁移与恢复。</p><p>比如说我们使用prd环境数据迁移到st环境为例来叙述一下具体的步骤。</p><p>第一步：从prd环境zookeeper服务器的数据目录下复制最新的日志和快照文件。</p><p>先去zookeeper的安装目录下找到zookeeper的conf文件，例如：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">$&gt; cd /usr/local/zookeeper/conf</span><br><span class="line">$&gt; cat zoo.cfg</span><br></pre></td></tr></table></figure><p>打开zoo.cfg文件找到具体配置的zookeeper的data目录，例如：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line"># the directory where the snapshot is stored.</span><br><span class="line"># do not use /tmp for storage, /tmp here is just</span><br><span class="line"># example sakes.</span><br><span class="line">dataDir=/var/zookeeper</span><br></pre></td></tr></table></figure><p>进入到dataDir下的version-2文件夹，version-2文件夹下存放的是zookeeper的日志和镜像文件，我们找到最新的日志和镜像文件，例如：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">$&gt; cd /var/zookeeper/version-2</span><br><span class="line">$&gt; ls -ah</span><br><span class="line">-rw-r--r-- 1 zookeeper zookeeper 67108880 Sep 27 17:20 log.909e2d252</span><br><span class="line">-rw-r--r-- 1 zookeeper zookeeper 10408329 Sep 27 17:01 snapshot.909e2d250</span><br></pre></td></tr></table></figure><p>找到最新的日志和快照文件，例如上面的：log.909e2d252和snapshot.909e2d250</p><p>日志文件存放zookeeper全部数据记录 ，快照文件则是内存增量文件。</p><p><span style="color:red"><strong>ps.这里要注意找最新的日志和快照文件</strong></span></p><p>zookeeper的日志和镜像文件的清理可以看这篇文章：<a href="https://ningyu1.github.io/site/post/89-zookeeper-cleanlog/">Zookeeper事务日志和snapshot清理方式</a></p><p>第二步：传输日志和快照文件</p><p>如果st和prd网络是通的话可以通过scp的方式复制过去，如果网络不通通过中转站来过渡。</p><p>第三步：停掉需要恢复数据的zk服务，删除数据目录下的文件，复制刚才的两个文件到数据目录下</p><p>假设需要恢复数据的服务器上zookeeper数据目录也是在/var/zookeeper下</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">$&gt; rm -fr /var/zookeeper/*</span><br><span class="line">$&gt; cp log.909e2d252 snapshot.909e2d250 /var/zookeeper</span><br><span class="line">$&gt; cd /usr/local/zookeeper/bin</span><br><span class="line">$&gt; ./zkServer.sh start</span><br></pre></td></tr></table></figure><p>如果是三台需要全部服务停掉，恢复其中的一台，然后等数据恢复完成后，再启动其余的两台服务让zk自己同步数据过去</p><p>第四步：验证数据是否真的恢复了</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">$&gt; cd /usr/local/zookeeper/bin</span><br><span class="line">$&gt; ./zkCli.sh</span><br><span class="line">$&gt; ls /</span><br></pre></td></tr></table></figure><p>ls查看zk中的数据.</p><p>Zookeeper日志与镜像文件的分析可以参考这篇文章：<a href="https://www.cnblogs.com/felixzh/p/8462740.html" target="_blank" rel="noopener">ZooKeeper日志与快照文件简单分析</a></p>]]></content>
    
    <summary type="html">
    
      zookeeper数据迁移及恢复
    
    </summary>
    
      <category term="zookeeper" scheme="https://ningyu1.github.io/categories/zookeeper/"/>
    
    
      <category term="zookeeper" scheme="https://ningyu1.github.io/tags/zookeeper/"/>
    
  </entry>
  
  <entry>
    <title>Git常用开发流程 —— 中心化的工作流</title>
    <link href="https://ningyu1.github.io/20180926/100-git-centralized-workflow.html"/>
    <id>https://ningyu1.github.io/20180926/100-git-centralized-workflow.html</id>
    <published>2018-09-26T12:50:00.000Z</published>
    <updated>2024-04-23T01:48:15.029Z</updated>
    
    <content type="html"><![CDATA[<ul><li><a href="#svn-style">一、中心化的工作流</a><ul><li><a href="#conflict">为什么会有冲突，冲突的原因</a></li><li><a href="#example">举例说明</a><ul><li><a href="#rebase">rebase实操记录</a></li><li><a href="#conclusion">示例总结以及注意事项</a></li></ul></li></ul></li></ul><h1 id="一、中心化的工作流"><a href="#一、中心化的工作流" class="headerlink" title="一、中心化的工作流"></a><span id="svn-style">一、中心化的工作流</span></h1><p>中心化的工作流又叫做<code>SVN-style</code>,适用于熟悉svn的管理流程来过渡到git（分布式版本控制系统），如果你的开发团队成员已经很熟悉svn，集中式工作流让你无需去适应一个全新流程就可以体验Git带来的收益。这个工作流也可以作为向更Git风格工作流迁移的友好过渡，入下图所示：</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/git-centralized-workflow/1.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p>像SVN一样，集中式工作流以中央仓库作为项目所有修改的单点实体。相比SVN缺省的开发分支trunk，Git叫做master，所有修改提交到这个分支上。该工作流只用到master这一个分支。<br>开发者开始先克隆中央仓库。在自己的项目拷贝中，像SVN一样的编辑文件和提交修改；但修改是存在本地的，和中央仓库是完全隔离的。开发者可以把和上游的同步延后到一个方便时间点。<br>要发布修改到正式项目中时，开发者要把本地master分支的修改『推（push）』到中央仓库中。这相当于svn commit操作，但push操作会把所有还不在中央仓库的本地提交都推上去，下图所示：</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/git-centralized-workflow/2.png" alt title>                </div>                <div class="image-caption"></div>            </figure><h2 id="为什么会有冲突，冲突的原因"><a href="#为什么会有冲突，冲突的原因" class="headerlink" title="为什么会有冲突，冲突的原因"></a><span id="conflict">为什么会有冲突，冲突的原因</span></h2><p>使用svn-style的方式避免不了会遇到冲突，冲突的解决尤为重要，中央仓库代表了正式项目（git里是master，svn里是trunk），所以提交历史应该被尊重且是稳定不变的。如果开发者本地的提交历史和中央仓库有分歧，Git会拒绝push提交否则会覆盖已经在中央库的正式提交。</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/git-centralized-workflow/3.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p>在开发者提交自己功能修改到中央库前，需要先fetch在中央库的新增提交，rebase自己提交到中央库提交历史之上。这样做的意思是在说，我要把自己的修改加到别人已经完成的修改上，最终的结果是一个完美的线性历史，就像以前的SVN的工作流中一样。如果本地修改和上游提交有冲突，Git会暂停rebase过程，给你手动解决冲突的机会。</p><h2 id="举例说明"><a href="#举例说明" class="headerlink" title="举例说明"></a><span id="example">举例说明</span></h2><p>让我们举一个例子来理解一下<code>中心化工作流-svn-style</code></p><p>比如说：wms项目组有两个开发人员：小明、小健，看他们是如何开发自己的功能并提交到中央仓库上的。</p><p>第一步：小明、小健从中央仓库克隆代码</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">git clone http://gitlab.xxx.com/demo/gitflow-demo.git</span><br></pre></td></tr></table></figure><p><span style="color:blue"><em>ps.克隆仓库时Git会自动添加远程别名origin指向中央仓库，不动请参考：<code>git clone --help</code></em></span></p><p>克隆代码入下图示例：</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/git-centralized-workflow/4.png" alt title>                </div>                <div class="image-caption"></div>            </figure><hr style=" height:2px;border:none;border-top:2px dotted #185598;"><p>小明开发新功能：</p><p>小明使用标准的Git过程开发功能：编辑、暂存（Stage）和提交，这里注意不进行push操作，只做本地commit提交到本地仓库</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">git status # 查看本地仓库的修改状态</span><br><span class="line">git add # 暂存文件</span><br><span class="line">git commit # 提交文件</span><br></pre></td></tr></table></figure><p>这些操作都是本地的提交操作，小明可以反复的按照需求来进行代码修改，不需要担心中央仓库的改变和小健自己本地的改变。</p><p>小明开发功能都在本地上进行就如下图示例：</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/git-centralized-workflow/5.png" alt title>                </div>                <div class="image-caption"></div>            </figure><hr style=" height:2px;border:none;border-top:2px dotted #185598;"><p>小健开发新功能</p><p>小健也是一样使用标准的Git过程开发功能，编辑、暂存、提交，他和小明一样不需要关系中央仓库的改变和小明自己本地的改变，所有的提交都是私有的，都是在自己的本地仓库中。</p><p>小健开发功能都在本地上进行就如下图所示：</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/git-centralized-workflow/6.png" alt title>                </div>                <div class="image-caption"></div>            </figure><hr style=" height:2px;border:none;border-top:2px dotted #185598;"><p>小明开发好了功能想发布了</p><p>小明把他的本地仓库修改的代码push到中央仓库，使用下面命令</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">git push origin master</span><br></pre></td></tr></table></figure><p><span style="color:blue"><em>ps. origin是在小明克隆仓库时Git创建的远程中央仓库别名。master参数告诉Git推送的分支</em></span></p><p><span style="color:blue"><em>ps. 我们这里假设团队中只有两个人（小明、小健），由于中央仓库自从小明克隆以来还没有被更新过，所以push操作不会有冲突，成功完成。</em></span></p><p>小明把他自己的本地代码push到中央仓库就如下图所示：</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/git-centralized-workflow/7.png" alt title>                </div>                <div class="image-caption"></div>            </figure><hr style=" height:2px;border:none;border-top:2px dotted #185598;"><p>小健开发好了功能也想发布了</p><p>小健也是使用git push命令来推送自己本地仓库的改动到中央仓库，使用下面命令</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">git push origin master</span><br></pre></td></tr></table></figure><p>但是此时origin已经由于小明在之前推送了小明本地的代码上去，因此已经和小健本地的代码产生了分歧，推送会被拒绝，入下图所示：</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/git-centralized-workflow/8.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p>拒绝的信息如下：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">$ git push origin master</span><br><span class="line">To http://gitlab.xxx.com/demo/gitflow-demo.git</span><br><span class="line"> ! [rejected]        master -&gt; master (fetch first)</span><br><span class="line">error: failed to push some refs to &apos;http://gitlab.xxx.com/demo/gitflow-demo.git&apos;</span><br><span class="line">hint: Updates were rejected because the remote contains work that you do</span><br><span class="line">hint: not have locally. This is usually caused by another repository pushing</span><br><span class="line">hint: to the same ref. You may want to first integrate the remote changes</span><br><span class="line">hint: (e.g., &apos;git pull ...&apos;) before pushing again.</span><br><span class="line">hint: See the &apos;Note about fast-forwards&apos; in &apos;git push --help&apos; for details.</span><br></pre></td></tr></table></figure><p>遇到这种问题我们该怎么解决了？</p><p>小健可以使用pull操作合并上游的修改到自己的仓库中，git的pull操作类似于svn的update操作，拉取所有上游小明提交命令到小健的本地仓库，但是要加上–rebase参数，例如下面命令：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">git pull --rebase origin master</span><br></pre></td></tr></table></figure><p><span style="color:red"><strong>这里特别解释一下上面命令的实际操作原理</strong></span></p><p><span style="color:red"><strong>–rebase选项告诉git把小健的提交移到（同步了中央仓库修改后的master分支）的顶部（head），也就是说它会先把小健本地分支的本地提交先移除掉，移动到一旁，然后把小健的本地分支同步中央仓库的最新版本（小明提交的记录），然后把刚刚移除了（小健本地的修改）再提交回小健的本地分支（已同步了最新中央仓库的代码，也就是说小明的代码）。</strong></span></p><p>不加rebase的话git会在xiaoming和xiaojian的提交后再进行一次merge操作从而就会多了一个merge的提交记录，加了rebase的话xiaojian的提交已经包含了与xiaoming代码的冲突，因此不会多一个merge操作。 </p><p>rebase（没有冲突）操作的过程例如下图所示：</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/git-centralized-workflow/9.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p>rebase（存在冲突）操作的过程例如下图所示：</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/git-centralized-workflow/10.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p><span style="color:blue"><em>ps. git会暂定rebase操作直到你去解决了冲突之后执行<code>git rebase --continue</code>来继续进行操作.</em></span></p><h3 id="rebase实操记录"><a href="#rebase实操记录" class="headerlink" title="rebase实操记录"></a><span id="rebase">rebase实操记录</span></h3><p>下面是rebase的操作实践，xiaojian执行<code>git pull --rebase origin master</code>，比如说xiaoming和xiaojian冲突到了同一个文件上会显示出下面的信息，例如：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">$ git pull --rebase origin master</span><br><span class="line">remote: Counting objects: 3, done.</span><br><span class="line">remote: Compressing objects: 100% (2/2), done.</span><br><span class="line">remote: Total 3 (delta 0), reused 0 (delta 0)</span><br><span class="line">Unpacking objects: 100% (3/3), done.</span><br><span class="line">From http://gitlab.xxx.com/demo/gitflow-demo</span><br><span class="line"> * branch            master     -&gt; FETCH_HEAD</span><br><span class="line">   788c7f3..9c7c9d2  master     -&gt; origin/master</span><br><span class="line">First, rewinding head to replay your work on top of it...</span><br><span class="line">Applying: xiaojian 提交</span><br><span class="line">error: Failed to merge in the changes.</span><br><span class="line">Using index info to reconstruct a base tree...</span><br><span class="line">M       README.md</span><br><span class="line">Falling back to patching base and 3-way merge...</span><br><span class="line">Auto-merging README.md</span><br><span class="line">CONFLICT (content): Merge conflict in README.md</span><br><span class="line">Patch failed at 0001 xiaojian 提交</span><br><span class="line">The copy of the patch that failed is found in: .git/rebase-apply/patch</span><br><span class="line">Resolve all conflicts manually, mark them as resolved with</span><br><span class="line">&quot;git add/rm &lt;conflicted_files&gt;&quot;, then run &quot;git rebase --continue&quot;.</span><br><span class="line">You can instead skip this commit: run &quot;git rebase --skip&quot;.</span><br><span class="line">To abort and get back to the state before &quot;git rebase&quot;, run &quot;git rebase --abort&quot;.</span><br></pre></td></tr></table></figure><p>如果装了小乌龟或者sourcetree目录下文件会显示冲突警告图标</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/git-centralized-workflow/11.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p>根据上面的警告我们需要手动的解决冲突，解决完冲突使用<code>git add/rm &lt;conflicted_files&gt;</code>命令标记解决冲突完毕，再执行<code>git rebase --continue</code>继续下一步操作。</p><p><span style="color:blue"><em>ps. 如果这个时候后悔执行了<code>git pull --rebase origin master</code>想撤销怎么办？可以执行<code>git rebase --abort</code>撤销rebase操作。</em></span></p><p>接下来是xiaojian执行push到中央仓库并且解决冲突的脚本记录如下：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">手动解决冲突</span><br><span class="line">$ git add README.md</span><br><span class="line">$ git rebase --continue</span><br><span class="line">Applying: xiaojian 提交</span><br><span class="line">$ git push origin master</span><br><span class="line">Counting objects: 3, done.</span><br><span class="line">Delta compression using up to 4 threads.</span><br><span class="line">Compressing objects: 100% (2/2), done.</span><br><span class="line">Writing objects: 100% (3/3), 305 bytes | 305.00 KiB/s, done.</span><br><span class="line">Total 3 (delta 1), reused 0 (delta 0)</span><br><span class="line">To http://gitlab.xxx.com/demo/gitflow-demo.git</span><br><span class="line">   9c7c9d2..87aed2d  master -&gt; master</span><br></pre></td></tr></table></figure><p>然后我们去gitlab上看我们的提交记录是个什么样子的，例如下图：</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/git-centralized-workflow/12.png" alt title>                </div>                <div class="image-caption"></div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/git-centralized-workflow/13.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p><span style="color:blue"><em>ps.提交记录非常的清晰明了而且是按照push仓库的顺序来显示的提交记录，这个样子也是我们希望看到的。</em></span></p><p>但是往往并没有这么顺利，理想很好现实确实各种问题。</p><p>比如说<code>git pull</code>的时候忘记添加<code>--rebase</code>参数了怎么办？</p><p>如果忘加了<code>--rebase</code>这个选项，pull操作仍然可以完成，但每次pull操作在同步中央仓库中别人的修改时，需要提交合并代码的记录从而导致提交历史中会多一个『合并提交』的记录。</p><p>例如下面所示：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">$ git pull origin master</span><br><span class="line">From http://gitlab.xxx.com/demo/gitflow-demo</span><br><span class="line"> * branch            master     -&gt; FETCH_HEAD</span><br><span class="line">Auto-merging README.md</span><br><span class="line">CONFLICT (content): Merge conflict in README.md</span><br><span class="line">Automatic merge failed; fix conflicts and then commit the result.</span><br><span class="line">手动解决完冲突</span><br><span class="line">$ git add .</span><br><span class="line">$ git commit -m &quot;合并冲突&quot;</span><br><span class="line">[master 9fab0c8] 合并冲突</span><br><span class="line">$ git push origin master</span><br><span class="line">Counting objects: 6, done.</span><br><span class="line">Delta compression using up to 4 threads.</span><br><span class="line">Compressing objects: 100% (4/4), done.</span><br><span class="line">Writing objects: 100% (6/6), 581 bytes | 290.00 KiB/s, done.</span><br><span class="line">Total 6 (delta 2), reused 0 (delta 0)</span><br><span class="line">To http://gitlab.xxx.com/demo/gitflow-demo.git</span><br><span class="line">   f862f27..9fab0c8  master -&gt; master</span><br></pre></td></tr></table></figure><p>然后我们去gitlab上看我们的提交记录是个什么样子的，例如下图：</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/git-centralized-workflow/14.png" alt title>                </div>                <div class="image-caption"></div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/git-centralized-workflow/15.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p><span style="color:blue"><em>ps. 会多出一个合并的提交，而且查看tree型图会发现不是一个线性的轨迹</em></span></p><p>如果使用的是小乌龟sourcetree这种工具合并冲突会是什么样子？让我们演示一下：</p><p>首先</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/git-centralized-workflow/16.png" alt title>                </div>                <div class="image-caption"></div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/git-centralized-workflow/17.png" alt title>                </div>                <div class="image-caption"></div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/git-centralized-workflow/18.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p>手动解决完冲突，选择标记为已经解决</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/git-centralized-workflow/19.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p>我们需要把解决的冲突提交上去</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/git-centralized-workflow/20.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p>自动生成的comment是<code>Merge branch</code>master<code>xxxxxxx</code></p><p>提交完成后右键菜单选择git push</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/git-centralized-workflow/21.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p>这个时候我们去gitlab上看我们的提交记录是个什么样子的，例如下图：</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/git-centralized-workflow/22.png" alt title>                </div>                <div class="image-caption"></div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/git-centralized-workflow/23.png" alt title>                </div>                <div class="image-caption"></div>            </figure><h3 id="示例总结以及注意事项"><a href="#示例总结以及注意事项" class="headerlink" title="示例总结以及注意事项"></a><span id="conclusion">示例总结以及注意事项</span></h3><p><span style="color:red"><strong>所以我们建议对于集中式工作流，最好是使用rebase，而不是使用merge生成一个合并提交</strong></span></p>]]></content>
    
    <summary type="html">
    
      Git常用开发流程 —— 中心化的工作流
    
    </summary>
    
      <category term="git" scheme="https://ningyu1.github.io/categories/git/"/>
    
    
      <category term="git-svn-style" scheme="https://ningyu1.github.io/tags/git-svn-style/"/>
    
  </entry>
  
  <entry>
    <title>如何编写高性能的 RPC 框架</title>
    <link href="https://ningyu1.github.io/20180919/99-rpc-benchmark1.html"/>
    <id>https://ningyu1.github.io/20180919/99-rpc-benchmark1.html</id>
    <published>2018-09-19T04:01:00.000Z</published>
    <updated>2024-04-23T01:48:15.039Z</updated>
    
    <content type="html"><![CDATA[<p>最近看相关rpc-benchmark相关的东西发现这篇文章挺好的，所以转载出来，下面是文章出处。</p><p>作者：鲁小憨<br>链接：<a href="https://www.jianshu.com/p/7182b8751e75" target="_blank" rel="noopener">https://www.jianshu.com/p/7182b8751e75</a><br>來源：简书<br>简书著作权归作者所有，任何形式的转载都请联系作者获得授权并注明出处。</p><p>在 <a href="https://www.jianshu.com/p/18c95649b1a4" target="_blank" rel="noopener">RPC Benchmark Round 1</a> 中，<a href="https://github.com/hank-whu/turbo-rpc" target="_blank" rel="noopener">Turbo</a> 性能炸裂表现强悍，并且在 listUser 这一项目中，取得了 10x dubbo 性能的好成绩。本文将介绍 <a href="https://github.com/hank-whu/turbo-rpc" target="_blank" rel="noopener">Turbo</a> 强悍性能背后的原理，并探讨如何编写高性能的 RPC 框架。</p><h1 id="过早的优化是万恶之源？"><a href="#过早的优化是万恶之源？" class="headerlink" title="过早的优化是万恶之源？"></a>过早的优化是万恶之源？</h1><p>这句话是 The Art of Computer Programming 作者，图领奖得主 Donald Knuth 大神说的。不过对于框架设计者而言，这句话并不正确。在设计一款高性能的基础框架时，必须始终重视性能优化，并将性能测试贯穿于整个设计开发过程中。这方面做到极致的类库有 <a href="https://github.com/LMAX-Exchange/disruptor" target="_blank" rel="noopener">Disruptor</a> <a href="https://github.com/JCTools/JCTools" target="_blank" rel="noopener">JCTools</a> <a href="https://github.com/real-logic/agrona" target="_blank" rel="noopener">Agrona</a> <a href="https://github.com/ngs-doo/dsl-json" target="_blank" rel="noopener">DSL-JSON</a> 等等，这几个高性能类库都坚持一个原则：不了解性能的外部类库坚决不用，如果现有的类库不能满足性能要求，那就重新设计一个。作为 <a href="https://github.com/hank-whu/turbo-rpc" target="_blank" rel="noopener">Turbo</a> 的设计者，我也尽量坚持这一原则，努力做到 Benchmark 驱动开发。</p><h1 id="JMH-让-Benchmark-驱动开发成为可能"><a href="#JMH-让-Benchmark-驱动开发成为可能" class="headerlink" title="JMH 让 Benchmark 驱动开发成为可能"></a>JMH 让 Benchmark 驱动开发成为可能</h1><p>在 JMH 出现之前，要对某个类库进行微基准性能测试是一件非常困难的事情。很难保证公平的测试条件，预热次数难以确定，预热效果也不好观察。JMH 的出现让性能测试变得 标准化 简单化，也让 Benchmark 驱动开发成为可能。Turbo 在开发过程中用 JMH 进行了充分的 Benchmark，以确定核心环节的性能开销，选择合适的实现方案。更多关于 JMH 的介绍请参考下面的链接：</p><ul><li><a href="http://openjdk.java.net/projects/code-tools/jmh/" target="_blank" rel="noopener">OpenJDK: jmh</a></li><li><a href="http://tutorials.jenkov.com/java-performance/jmh.html" target="_blank" rel="noopener">JMH - Java Microbenchmark Harness</a></li><li><a href="http://www.importnew.com/12548.html" target="_blank" rel="noopener">ImportNew JMH简介</a></li></ul><h1 id="RPC-的主要流程"><a href="#RPC-的主要流程" class="headerlink" title="RPC 的主要流程"></a>RPC 的主要流程</h1><ol><li>客户端 获取到 UserService 接口的 Refer: userServiceRefer</li><li>客户端 调用 userServiceRefer.verifyUser(email, pwd)</li><li>客户端 获取到 请求方法 和 请求数据</li><li>客户端 把 请求方法 和 请求数据 序列化为 传输数据</li><li>进行网络传输</li><li>服务端 获取到 传输数据</li><li>服务端 反序列化获取到 请求方法 和 请求数据</li><li>服务端 获取到 UserService 的 Invoker: userServiceInvoker</li><li>服务端 userServiceInvoker 调用 userServiceImpl.verifyUser(email, pwd) 获取到 响应结果</li><li>服务端 把 响应结果 序列化为 传输数据</li><li>进行网络传输</li><li>客户端 接收到 传输数据</li><li>客户端 反序列化获取到 响应结果</li><li>客户端 userServiceRefer.verifyUser(email, pwd) 返回 响应结果</li></ol><p>整个流程中对性能影响比较大的环节有：序列化[4, 7, 10, 13]，方法调用[2, 3, 8, 9, 14]，网络传输[5, 6, 11, 12]。本文后续内容将着重介绍这3个部分。</p><h1 id="序列化方案"><a href="#序列化方案" class="headerlink" title="序列化方案"></a>序列化方案</h1><p>Java 世界最常用的几款高性能序列化方案有 <a href="https://github.com/EsotericSoftware/kryo" target="_blank" rel="noopener">Kryo</a> <a href="https://github.com/protostuff/protostuff" target="_blank" rel="noopener">Protostuff</a> <a href="https://github.com/RuedigerMoeller/fast-serialization" target="_blank" rel="noopener">FST</a> <a href="https://github.com/FasterXML/jackson" target="_blank" rel="noopener">Jackson</a> <a href="https://github.com/alibaba/fastjson" target="_blank" rel="noopener">Fastjson</a>。只需要进行一次 Benchmark，然后从这5种序列化方案中选出性能最高的那个就行了。<a href="https://github.com/ngs-doo/dsl-json" target="_blank" rel="noopener">DSL-JSON</a> 使用起来过于繁琐，不在考虑之列。<a href="https://github.com/pascaldekloe/colfer" target="_blank" rel="noopener">Colfer</a> <a href="https://developers.google.com/protocol-buffers/" target="_blank" rel="noopener">Protocol</a> <a href="https://thrift.apache.org/" target="_blank" rel="noopener">Thrift</a> 因为必须预先定义描述文件，使用起来太麻烦，所以不在考虑之列。至于 Java 自带的序列化方案，早就因为性能问题被大家所抛弃，所以也不考虑。下面的表格列出了在考虑之列的5种序列化方案的性能。</p><ul><li><a href="https://github.com/hank-whu/rpc-benchmark/blob/master/benchmark-base/src/main/java/benchmark/bean/User.java" target="_blank" rel="noopener">User</a> 序列化+反序列化 性能</li></ul><table><thead><tr><th>framework</th><th>thrpt (ops/ms)</th><th>size</th></tr></thead><tbody><tr><td>protostuff</td><td>1654</td><td>240</td></tr><tr><td>kryo</td><td>1288</td><td>296</td></tr><tr><td>fst</td><td>1101</td><td>263</td></tr><tr><td>jackson</td><td>959</td><td>385</td></tr><tr><td>fastjson</td><td>603</td><td>378</td></tr></tbody></table><ul><li>包含15个 <a href="https://github.com/hank-whu/rpc-benchmark/blob/master/benchmark-base/src/main/java/benchmark/bean/User.java" target="_blank" rel="noopener">User</a> 的 <a href="https://github.com/hank-whu/rpc-benchmark/blob/master/benchmark-base/src/main/java/benchmark/bean/Page.java" target="_blank" rel="noopener">Page</a> 序列化+反序列化 性能</li></ul><table><thead><tr><th>framework</th><th>thrpt (ops/ms)</th><th>size</th></tr></thead><tbody><tr><td>kryo</td><td>143</td><td>2080</td></tr><tr><td>fst</td><td>118</td><td>3495</td></tr><tr><td>protostuff</td><td>98</td><td>3920</td></tr><tr><td>jackson</td><td>71</td><td>5711</td></tr><tr><td>fastjson</td><td>40</td><td>5606</td></tr></tbody></table><p>从这个 benchmark 中可以得出明确的结论：二进制协议的 protostuff kryo fst 要比文本协议的 jackson fastjson 有明显优势；文本协议中，jackson(开启了afterburner) 要比 fastjson 有明显的优势。</p><p>无法确定的是：3个二进制协议到底哪个更好一些，毕竟 速度 和 size 对于 RPC 都很重要。直观上 kryo 或许是最佳选择，而且 kryo 也广受各大型系统的青睐。不过最终还是决定把这3个类库都留作备选，通过集成传输模块后的 Benchmark 来决定选用哪个。</p><table><thead><tr><th>framework</th><th>exist op/ms</th><th>create op/ms</th><th>get op/ms</th><th>list op/ms</th></tr></thead><tbody><tr><td>proto</td><td>103.92</td><td>89.50</td><td>83.33</td><td>21.17</td></tr><tr><td>kryo</td><td>99.23</td><td>76.71</td><td>73.89</td><td>25.68</td></tr><tr><td>fst</td><td>102.33</td><td>76.24</td><td>78.81</td><td>23.30</td></tr></tbody></table><p>最终的结果也还是各有千秋难以抉择，所以 <a href="https://github.com/hank-whu/turbo-rpc" target="_blank" rel="noopener">Turbo</a> 保留了 protostuff 和 kryo 的实现，并允许用户自行替换为自己的实现。</p><h1 id="方法调用"><a href="#方法调用" class="headerlink" title="方法调用"></a>方法调用</h1><p>可用的 动态方法调用 方案有：Reflection ClassGeneration MethodHandle。Reflection 是最古老的技术，据说性能不佳。ClassGeneration 动态类生成，从原理上说应该是跟直接调用一样的性能。MethodHandle 是从 Java 7 开始出现的技术，据说能达到跟直接调用一样的性能。实际结果如下：</p><table><thead><tr><th>type</th><th>thrpt (ops/us)</th></tr></thead><tbody><tr><td>direct</td><td>1062</td></tr><tr><td>javassist</td><td>920</td></tr><tr><td>methodHandle</td><td>430</td></tr><tr><td>reflection</td><td>337</td></tr></tbody></table><p>结论非常明显：使用类生成技术的 <a href="http://jboss-javassist.github.io/javassist/" target="_blank" rel="noopener">javassist</a> 跟直接调用几乎一样的性能，就用 <a href="http://jboss-javassist.github.io/javassist/" target="_blank" rel="noopener">javassist</a> 了。</p><p>MethodHandle 表现并没有宣传的那么好，怎么回事？原来 MethodHandle 只有在明确知道调用 参数数量 参数类型 的情况下才能调用高性能的 invokeExact(Object… args)，所以它并不适合作为动态调用的方案。</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">As is usual with virtual methods, source-level calls to invokeExact and invoke compile to an invokevirtual instruction. More unusually, the compiler must record the actual argument types, and may not perform method invocation conversions on the arguments. Instead, it must push them on the stack according to their own unconverted types. The method handle object itself is pushed on the stack before the arguments. The compiler then calls the method handle with a symbolic type descriptor which describes the argument and return types.</span><br><span class="line">refer: https://docs.oracle.com/javase/7/docs/api/java/lang/invoke/MethodHandle.html</span><br></pre></td></tr></table></figure><h1 id="网络传输"><a href="#网络传输" class="headerlink" title="网络传输"></a>网络传输</h1><p><a href="https://github.com/netty/netty" target="_blank" rel="noopener">Netty</a> 已经成为事实上的标准，所有主流的项目现在使用的都是 <a href="https://github.com/netty/netty" target="_blank" rel="noopener">Netty</a>。<a href="http://mina.apache.org/" target="_blank" rel="noopener">Mina</a> <a href="https://github.com/javaee/grizzly" target="_blank" rel="noopener">Grizzly</a> 已经失去市场，所以也就不用考虑了。还好也不至于这么无聊，<a href="https://github.com/real-logic/aeron" target="_blank" rel="noopener">Aeron</a> 的闪亮登场让 Netty 多了一个有力的竞争对手。Aeron 是一个可靠高效的 UDP 单播 UDP 多播和 IPC 消息传递工具。性能是消息传递中的关键。Aeron 的设计旨在达到 高吞吐量 低开销 和 低延迟。实际效果到底如何呢？很遗憾，在 <a href="https://www.jianshu.com/p/18c95649b1a4" target="_blank" rel="noopener">RPC Benchmark Round 1</a> 中的表现一般。跟他们开发团队沟通后，最终确认其<a href="https://github.com/real-logic/aeron/issues/432" target="_blank" rel="noopener">无法对超过 64k 的消息进行 zero-copy 处理</a>，我觉得这可能是 Aeron 表现不佳的一个原因。Aeron 或许更适合 微小消息 极端低延迟 的场景，而不适用于更加通用的 RPC 场景。所以暂时还没有出现能够跟 Netty 一争高下的通用网络传输框架，现阶段 Netty 依然是 RPC 系统的最佳选择。</p><ul><li>existUser 判断某个 email 是否存在</li></ul><table><thead><tr><th>framework</th><th>thrpt (ops/ms)</th><th>avgt (ms)</th><th>p90 (ms)</th><th>p99 (ms)</th></tr></thead><tbody><tr><td>turbo-rpc</td><td>107.05</td><td>0.28</td><td>0.40</td><td>0.87</td></tr><tr><td>netty</td><td>99.81</td><td>0.32</td><td>0.40</td><td>0.52</td></tr><tr><td>jupiter</td><td>73.07</td><td>0.44</td><td>0.66</td><td>1.49</td></tr><tr><td>undertow</td><td>70.38</td><td>0.45</td><td>1.16</td><td>2.17</td></tr><tr><td>turbo-rest</td><td>68.49</td><td>0.44</td><td>1.17</td><td>2.15</td></tr><tr><td>undertow-async</td><td>62.65</td><td>0.49</td><td>1.14</td><td>2.41</td></tr><tr><td>dubbo-kryo</td><td>57.35</td><td>0.53</td><td>0.67</td><td>1.02</td></tr><tr><td>rapidoid</td><td>52.96</td><td>0.61</td><td>1.32</td><td>2.51</td></tr><tr><td>dubbo</td><td>52.12</td><td>0.54</td><td>0.67</td><td>0.92</td></tr><tr><td>motan</td><td>44.96</td><td>0.71</td><td>1.15</td><td>2.47</td></tr><tr><td>aeron</td><td>43.46</td><td>0.90</td><td>1.32</td><td>5.10</td></tr><tr><td>grpc</td><td>38.97</td><td>0.84</td><td>1.07</td><td>1.31</td></tr><tr><td>thrift</td><td>27.25</td><td>1.59</td><td>0.16</td><td>64.87</td></tr><tr><td>hprose</td><td>26.24</td><td>1.26</td><td>1.53</td><td>2.01</td></tr><tr><td>springwebflux</td><td>22.39</td><td>1.42</td><td>2.27</td><td>3.19</td></tr><tr><td>springboot</td><td>12.54</td><td>1.68</td><td>2.38</td><td>13.63</td></tr></tbody></table><h1 id="消息格式"><a href="#消息格式" class="headerlink" title="消息格式"></a>消息格式</h1><p>我们先来看一下 Dubbo 的消息格式</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">public class RpcInvocation implements Invocation, Serializable &#123;</span><br><span class="line">    private String methodName;</span><br><span class="line">    private Class&lt;?&gt;[] parameterTypes;</span><br><span class="line">    private Object[] arguments;</span><br><span class="line">    ...</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>可以说是非常经典的设计，Client 必须告知 Server 要调用的 方法名称 参数类型 参数。Server 获取到这3个参数后，通过 方法名称 com.alibaba.service.auth.UserService.verifyUser 和<br>参数类型 (String, String) 获取到 Invoker，然后通过 Invoker 实际调用 userServiceImpl 的 verifyUser(String, String) 方法。其他的众多 RPC 框架也都采取了这一经典设计。</p><p>但是，这是正确的做法吗？当然不是，这种做法非常浪费空间，每次请求消息体的大概内存布局应该是下面的样子：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">public boolean verifyUser(String email, String pwd);</span><br><span class="line"></span><br><span class="line">|com.alibaba.service.auth.UserService.verifyUser|java.lang.String,java.lang.String|实际的参数|</span><br></pre></td></tr></table></figure><p>啰里啰嗦的，浪费了 80 byte 来定义 方法 和 参数，并没有比 http+json 的方式高效多少。实际的 <a href="https://www.jianshu.com/p/18c95649b1a4" target="_blank" rel="noopener">性能测试</a> 也证明了这一点，undertow+jackson 要比 dubbo motan 的成绩都要好。</p><p>那什么才是正确的做法？<a href="https://github.com/hank-whu/turbo-rpc" target="_blank" rel="noopener">Turbo</a> 在消息格式上做出了非常大的改变。</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">public class Request implements Serializable &#123;</span><br><span class="line">    private int requestId;</span><br><span class="line">    private int serviceId;</span><br><span class="line">    private MethodParam methodParam;</span><br><span class="line">    ...</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>大致的内存布局：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">public boolean verifyUser(String email, String pwd);</span><br><span class="line">|int|int|实际的参数|</span><br></pre></td></tr></table></figure><p>高效多了，只用了 4 byte 就做到了 方法 和 参数 的定义。大大减小了 传输数据 的 size，同时 int 类型的 serviceId 也降低了 Invoker 的查找开销。</p><p>看到这里，有同学可能会问：那岂不是要为每个方法定义一个唯一 id ？<br>答案是不需要的，<a href="https://github.com/hank-whu/turbo-rpc" target="_blank" rel="noopener">Turbo</a> 解决了这一问题，详情参考 <a href="https://github.com/hank-whu/turbo-rpc/blob/master/turbo-rpc/src/main/java/rpc/turbo/common/TurboConnectService.java" target="_blank" rel="noopener">TurboConnectService</a> 。</p><h1 id="MethodParam-简介"><a href="#MethodParam-简介" class="headerlink" title="MethodParam 简介"></a>MethodParam 简介</h1><p><a href="https://github.com/hank-whu/turbo-rpc/blob/master/turbo-rpc/src/main/java/rpc/turbo/param/MethodParamClassFactory.java" target="_blank" rel="noopener">MethodParam</a> 才是 <a href="https://github.com/hank-whu/turbo-rpc" target="_blank" rel="noopener">Turbo</a> 性能炸裂的真正原因。其基本原理是利用 ClassGeneration 对每个 Method 都生成一个 <a href="https://github.com/hank-whu/turbo-rpc/blob/master/turbo-rpc/src/main/java/rpc/turbo/param/MethodParamClassFactory.java" target="_blank" rel="noopener">MethodParam</a> 类，用于对方法参数的封装。这样做的好处有：</p><ol><li>减少基本数据类型的 装箱 拆箱 开销</li><li>序列化时可以省略掉很多类型描述，大大减小 传输消息 的 size</li><li>使 Invoker 可以高效调用 被代理类 的方法</li><li>统一 RPC 和 REST 的数据模型，简化 序列化 反序列化 实现</li><li>大大加快 json 格式数据 反序列化 速度</li></ol><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">//方法 test(long id, int value) 将会生成下面的 MethodParam 类:     </span><br><span class="line">public class TestService_test_2_MethodParam implements MethodParam &#123;</span><br><span class="line">    private long id;</span><br><span class="line">    private int value;</span><br><span class="line">     </span><br><span class="line">    public long $param0() &#123; return this.id; &#125;</span><br><span class="line">    public int $param1() &#123; return this.value; &#125;</span><br><span class="line"></span><br><span class="line">    //... getters and setters</span><br><span class="line">     </span><br><span class="line">    public TestService_test_2_MethodParam(long id, int value) &#123;</span><br><span class="line">        this.id = id;</span><br><span class="line">        this.value= value;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h1 id="序列化的进一步优化"><a href="#序列化的进一步优化" class="headerlink" title="序列化的进一步优化"></a>序列化的进一步优化</h1><p>大部分 RPC 框架的 序列化 反序列化 过程都需要一个中间的 bytes</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">序列化过程：User &gt; bytes &gt; ByteBuf</span><br><span class="line">反序列化过程：ByteBuf &gt; bytes &gt; User</span><br></pre></td></tr></table></figure><p>而 <a href="https://github.com/hank-whu/turbo-rpc" target="_blank" rel="noopener">Turbo</a> 砍掉了中间的 bytes，直接操作 ByteBuf，实现了 序列化 反序列化 的 zero-copy，大大减少了 内存分配 内存复制 的开销。具体实现请参考 <a href="https://github.com/hank-whu/turbo-rpc/blob/master/turbo-protostuff/src/main/java/rpc/turbo/serialization/protostuff/ProtostuffSerializer.java" target="_blank" rel="noopener">ProtostuffSerializer</a> 和 <a href="https://github.com/hank-whu/turbo-rpc/tree/master/turbo-rpc/src/main/java/rpc/turbo/transport/server/rpc/codec" target="_blank" rel="noopener">Codec</a>。</p><p>对于已知类型和已知字段，<a href="https://github.com/hank-whu/turbo-rpc" target="_blank" rel="noopener">Turbo</a> 都尽量采用 手工序列化 手工反序列化 的方式来处理，以进一步减少性能开销。</p><h1 id="ObjectPool"><a href="#ObjectPool" class="headerlink" title="ObjectPool"></a>ObjectPool</h1><p>常见的几个 ObjectPool 实现性能都很差，反而很容易成为性能瓶颈。Stormpot 性能强悍，不过存在偶尔死锁的问题，而且作者也停止维护了。HikariCP 性能不错，不过其本身是一款数据库连接池，用作 ObjectPool 并不称手。我的建议是尽量避免使用 ObjectPool，转而使用替代技术。更重要的是 Netty 的 Channel 是线程安全的，并不需要使用 ObjectPool 来管理。只需要一个简单的容器来存储 Channel，用的时候使用 负载均衡策略 选出一个 Channel 出来就行了。</p><table><thead><tr><th>framework</th><th>thrpt (ops/us)</th></tr></thead><tbody><tr><td>ThreadLocal</td><td>685.418</td></tr><tr><td>Stormpot</td><td>272.934</td></tr><tr><td>HikariCP</td><td>139.126</td></tr><tr><td>SegmentLock</td><td>19.415</td></tr><tr><td>Vibur</td><td>4.668</td></tr><tr><td>CommonsPool2</td><td>1.107</td></tr><tr><td>CommonsPool</td><td>0.276</td></tr></tbody></table><h1 id="基础类库优化"><a href="#基础类库优化" class="headerlink" title="基础类库优化"></a>基础类库优化</h1><p>除了上述的关键流程优化，<a href="https://github.com/hank-whu/turbo-rpc" target="_blank" rel="noopener">Turbo</a> 还做了大量基础类库的优化</p><ul><li>AtomicMuiltInteger 多个 int 的原子性操作</li><li>ConcurrentArrayList 无锁并发 List 实现，比 CopyOnWriteArrayList 的写入开销低，O(1) vs O(n)</li><li>ConcurrentIntToObjectArrayMap 以 int 数组为底层实现的无锁并发 Map，读多写少情况下接近直接访问字段的性能，读多写多情况下是 ConcurrentHashMap 性能的 5x</li><li>ConcurrentIntegerSequencer 快速序号生成器，并发环境下是 AtomicInteger 性能的10x</li><li>ObjectId 全局唯一 id 生成器，是 Java 自带 UUID 性能的 200x</li><li>HexUtils 查表 + 批量操作，是 Netty 和 Guava 实现的 2x~5x</li><li>URLEncodeUtils 基于 HexUtils 实现，是 Java 和 Commons 实现的 2x，Guava 实现的 1.1x (Guava 只有 urlEncode 实现，无 urlDecode 实现)</li><li>ByteBufUtils 实现了高效的 ZigZag 写入操作，最高可达通常实现的 4x</li></ul><p>上面的内容仅介绍了作者认为重要的东西，更多内容请直接查看 <a href="https://github.com/hank-whu/turbo-rpc/" target="_blank" rel="noopener">Turbo 源码</a></p><h1 id="不足之处"><a href="#不足之处" class="headerlink" title="不足之处"></a>不足之处</h1><ul><li>有很多优化是毫无价值的，Donald Knuth 大神说得很对</li><li>强制必须使用 CompletableFuture 作为返回值导致了一些性能开销</li><li>滥用 ClassGeneration，而且并没有考虑类的卸载，这方面需要改进</li><li>实现了 <a href="https://github.com/hank-whu/turbo-rpc/blob/master/turbo-utils/src/main/java/rpc/turbo/util/UnsafeStringUtils.java" target="_blank" rel="noopener">UnsafeStringUtils</a>，这是个危险的黑魔法实现，需要重新思考下</li><li>对性能的追求有点走火入魔，导致了很多地方的设计过于复杂</li></ul>]]></content>
    
    <summary type="html">
    
      如何编写高性能的 RPC 框架
    
    </summary>
    
      <category term="rpc" scheme="https://ningyu1.github.io/categories/rpc/"/>
    
    
      <category term="rpc-benchmark" scheme="https://ningyu1.github.io/tags/rpc-benchmark/"/>
    
  </entry>
  
  <entry>
    <title>怎样对RPC进行有效的性能测试</title>
    <link href="https://ningyu1.github.io/20180918/98-rpc-benchmark.html"/>
    <id>https://ningyu1.github.io/20180918/98-rpc-benchmark.html</id>
    <published>2018-09-18T10:16:00.000Z</published>
    <updated>2024-04-23T01:48:15.039Z</updated>
    
    <content type="html"><![CDATA[<p>最近看相关rpc-benchmark相关的东西发现这篇文章挺好的，所以转载出来，下面是文章出处。</p><p>作者：鲁小憨<br>链接：<a href="https://www.jianshu.com/p/cbcdf05eaa5c" target="_blank" rel="noopener">https://www.jianshu.com/p/cbcdf05eaa5c</a><br>來源：简书<br>简书著作权归作者所有，任何形式的转载都请联系作者获得授权并注明出处。</p><p>在 <a href="https://www.jianshu.com/p/18c95649b1a4" target="_blank" rel="noopener">RPC Benchmark Round 1</a> 中 <a href="https://link.jianshu.com/?t=https%3A%2F%2Fgithub.com%2Fhank-whu%2Fturbo-rpc" target="_blank" rel="noopener">turbo</a> 的成绩一骑绝尘，实力碾压众 rpc 框架。对此，很多人表示不服气，认为作者既是运动员又是裁判员有失公平。所以我认为有必要解释一下 <a href="https://link.jianshu.com/?t=https%3A%2F%2Fgithub.com%2Fhank-whu%2Frpc-benchmark" target="_blank" rel="noopener">rpc-benchmark</a> 的公正性，以及为什么 <a href="https://link.jianshu.com/?t=https%3A%2F%2Fgithub.com%2Fhank-whu%2Fturbo-rpc" target="_blank" rel="noopener">turbo</a> 能够如此强悍。</p><h1 id="参考对象"><a href="#参考对象" class="headerlink" title="参考对象"></a>参考对象</h1><p><a href="https://link.jianshu.com/?t=https%3A%2F%2Fgithub.com%2Fhank-whu%2Frpc-benchmark" target="_blank" rel="noopener">rpc-benchmark</a> 灵感源自 <a href="https://link.jianshu.com/?t=https%3A%2F%2Fwww.techempower.com%2Fbenchmarks%2F" target="_blank" rel="noopener">techempower-benchmarks</a>，为了能够评测众多服务器框架，<a href="https://link.jianshu.com/?t=https%3A%2F%2Fwww.techempower.com%2Fbenchmarks%2F" target="_blank" rel="noopener">techempower-benchmarks</a> 提供了6个测试用例：</p><ul><li>JSON serialization</li></ul><p>This test exercises the framework fundamentals including keep-alive support, request routing, request header parsing, object instantiation, JSON serialization, response header generation, and request count throughput.</p><ul><li>Single database query</li></ul><p>This test exercises the framework’s object-relational mapper (ORM), random number generator, database driver, and database connection pool.</p><ul><li>Multiple database queries</li></ul><p>This test is a variation of Test #2 and also uses the World table. Multiple rows are fetched to more dramatically punish the database driver and connection pool. At the highest queries-per-request tested (20), this test demonstrates all frameworks’ convergence toward zero requests-per-second as database activity increases.</p><ul><li>Fortunes</li></ul><p>This test exercises the ORM, database connectivity, dynamic-size collections, sorting, server-side templates, XSS countermeasures, and character encoding.</p><ul><li>Database updates</li></ul><p>This test is a variation of Test #3 that exercises the ORM’s persistence of objects and the database driver’s performance at running UPDATE statements or similar. The spirit of this test is to exercise a variable number of read-then-write style database operations.</p><ul><li>Plaintext</li></ul><p>This test is an exercise of the request-routing fundamentals only, designed to demonstrate the capacity of high-performance platforms in particular. Requests will be sent using HTTP pipelining. The response payload is still small, meaning good performance is still necessary in order to saturate the gigabit Ethernet of the test environment.</p><p><a href="https://link.jianshu.com/?t=https%3A%2F%2Fwww.techempower.com%2Fbenchmarks%2F" target="_blank" rel="noopener">techempower-benchmarks</a> 规则都是公开的，代码都是开放的。任何人觉得xx框架写得不好，配置有问题，都可以来提交自己的 Pull Request 。一句话，不服气的话就来提交代码。</p><h1 id="测试用例"><a href="#测试用例" class="headerlink" title="测试用例"></a>测试用例</h1><p>不过 <a href="https://link.jianshu.com/?t=https%3A%2F%2Fwww.techempower.com%2Fbenchmarks%2F" target="_blank" rel="noopener">techempower-benchmarks</a> 对比的都是服务器框架，并不能用来测试 rpc 的性能，作为学习模仿者，我创建了 <a href="https://link.jianshu.com/?t=https%3A%2F%2Fgithub.com%2Fhank-whu%2Frpc-benchmark" target="_blank" rel="noopener">rpc-benchmark</a> 这个项目。 <a href="https://link.jianshu.com/?t=https%3A%2F%2Fgithub.com%2Fhank-whu%2Frpc-benchmark" target="_blank" rel="noopener">rpc-benchmark</a> 提供了4个测试用例：</p><ul><li><p>boolean existUser(String email), 判断某个 email 是否存在<br>输入是很短的字符串，输出是 bool 值，这个测试用例用于衡量小 Request 小<br>Response 的性能。</p></li><li><p>boolean createUser(User user), 添加一个 用户<br>输入是一个 User 的对象，输出是 bool 值，这个测试用例用于衡量大 Request 小 Response 的性能。</p></li><li><p>User getUser(long id), 根据 id 获取一个用户<br>输入是一个 long 类型的值，输出是 User 对象，这个测试用例用于衡量小 Request 大 Response 的性能。</p></li><li><p>Page<user> listUser(int pageNo), 获取用户列表<br>输入是 int 类型的值，输出是一个包含15个 User 的列表，这个测试用例用于衡量小 Request 超大 Response 的性能。</user></p></li></ul><p>这4个测试用例构成了一个基本的业务逻辑： 用户注册管理。非常具有代表性，并且没有脱离现实使用场景。有些测试用例可能会注重衡量字符串的传输速度，从4字节 64字节 … 64k字节 依次测起，这样的测试用例就过于脱离现实，没有太多的实际意义。毕竟作为 rpc 框架，除了传输速度，序列化速度其实也是非常重要的。而仅仅用字符串来测试仅能测试出框架的传输速度，并不能有效衡量序列化的性能，也不能衡量整体的 rpc 性能。</p><h1 id="测试工具"><a href="#测试工具" class="headerlink" title="测试工具"></a>测试工具</h1><p>因为每个 rpc 框架都有自己的 序列化协议 传输协议，所以 <a href="https://link.jianshu.com/?t=https%3A%2F%2Fgithub.com%2Fhank-whu%2Frpc-benchmark" target="_blank" rel="noopener">rpc-benchmark</a> 不能像 <a href="https://link.jianshu.com/?t=https%3A%2F%2Fwww.techempower.com%2Fbenchmarks%2F" target="_blank" rel="noopener">techempower-benchmarks</a> 一样直接使用 wrk 作为测试工具，只能每个框架都编写测试用的 客户端实现。</p><p>客户端实现 使用的工具是JMH，这个工具 Java 开发团队自己也在使用。正确的性能测试在之前并不是一件简单的事情，JMH 的出现让性能测试真正的 标准化 简单化。更多关于 JMH 的介绍可以参考下面的链接。</p><ul><li><a href="https://link.jianshu.com/?t=http%3A%2F%2Ftutorials.jenkov.com%2Fjava-performance%2Fjmh.html" target="_blank" rel="noopener">JMH - Java Microbenchmark Harness</a></li><li><a href="https://link.jianshu.com/?t=http%3A%2F%2Fwww.importnew.com%2F12548.html" target="_blank" rel="noopener">ImportNew JMH简介</a></li></ul><h1 id="测试方法"><a href="#测试方法" class="headerlink" title="测试方法"></a>测试方法</h1><p>测试的过程是先进行10次预热，然后才开始真正的3次测试（JMH的“每次”执行实际上是执行很多次，更好的翻译其实应该是“每轮”）。刚开始使用的是5次预热，但是后来发现 http 传输协议的 undertow grpc 等框架都比较慢热，需要更多的预热次数。完整的测试要跑起来依然有点费劲，需要配置很多环境。不过如果你只是想研究下某个框架的代码实现的话，完全可以更简单一些。拉下代码来直接导入到 Eclipse/IDEA ，配置好hosts，启动 Server，然后启动相应的 Client 就好了。</p><h1 id="为什么把-undertow-springboot-netty-也作为了测试对象"><a href="#为什么把-undertow-springboot-netty-也作为了测试对象" class="headerlink" title="为什么把 undertow springboot netty 也作为了测试对象"></a>为什么把 undertow springboot netty 也作为了测试对象</h1><p>按照 wiki 的<a href="https://link.jianshu.com/?t=https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FRemote_procedure_call" target="_blank" rel="noopener">定义</a>，这三个确实不能认为是 rpc ，不过简单封装之后他们都可以作为 rpc 使用。加入这几个更多的是为给 rpc 框架的实现者提供一个参考，作为基础的协议层性能是怎么样的？作为springcloud 的底层实现，springboot 其实代表了springcloud 的性能。undertow 证明了 http+json 并不比 tcp+binary 慢太多，其速度甚至比 dubbo motan 还要快不少。同时也是为了告诉喷子们，并不是说你用了高性能的 netty+protopuff 就能比 turbo 快，turbo 能碾压众框架并不只是靠简单的拼积木就能做到的。</p><h1 id="不足之处"><a href="#不足之处" class="headerlink" title="不足之处"></a>不足之处</h1><p>仅1个客户端32个线程其实是非常不严谨的，正确的做法应该是从1个线程一直到32k个线程逐步增加，从1台客户端机器到1000台客户端机器逐步增加（客户端数量 线程数量 应该是一个笛卡尔积）。不过每轮测试实在都太耗费时间了，而且阿里云的服务器也不便宜，所以只能作罢。后续如果有云服务器厂商赞助的话，可以考虑把这块给做起来。</p><h1 id="turbo为什么如此强悍"><a href="#turbo为什么如此强悍" class="headerlink" title="turbo为什么如此强悍"></a>turbo为什么如此强悍</h1><p>篇幅有限写不开了，下篇再说吧。</p>]]></content>
    
    <summary type="html">
    
      怎样对RPC进行有效的性能测试
    
    </summary>
    
      <category term="rpc" scheme="https://ningyu1.github.io/categories/rpc/"/>
    
    
      <category term="rpc-benchmark" scheme="https://ningyu1.github.io/tags/rpc-benchmark/"/>
    
  </entry>
  
  <entry>
    <title>Trouble Shooting —— MyBatis的PropertyTokenizer抛NPE异常</title>
    <link href="https://ningyu1.github.io/20180820/97-mybatis-npe.html"/>
    <id>https://ningyu1.github.io/20180820/97-mybatis-npe.html</id>
    <published>2018-08-20T09:48:00.000Z</published>
    <updated>2024-04-23T01:48:15.039Z</updated>
    
    <content type="html"><![CDATA[<p>这个文章转自公司内网WIKI，同事调试的问题以及问题分析过程，我觉得挺好的所以转载出来。</p><h1 id="问题描述"><a href="#问题描述" class="headerlink" title="问题描述"></a>问题描述</h1><p>多任务同时处理时会报出如下NPE异常，堆栈信息如下：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">2018-08-10 18:16:10.938 [xxxExecutor-2] ERROR c.j.bmc.mq.listener.xxxResultListener </span><br><span class="line">org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.exceptions.PersistenceException:</span><br><span class="line">### Error querying database.  Cause: java.lang.NullPointerException</span><br><span class="line">### Cause: java.lang.NullPointerException</span><br><span class="line">    at org.mybatis.spring.MyBatisExceptionTranslator.translateExceptionIfPossible(MyBatisExceptionTranslator.java:75) ~[mybatis-spring-1.2.2.jar:1.2.2]</span><br><span class="line">    at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:371) ~[mybatis-spring-1.2.2.jar:1.2.2]</span><br><span class="line">    at com.sun.proxy.$Proxy21.selectList(Unknown Source) ~[na:na]</span><br><span class="line">    at org.mybatis.spring.SqlSessionTemplate.selectList(SqlSessionTemplate.java:198) ~[mybatis-spring-1.2.2.jar:1.2.2]</span><br><span class="line">    at org.apache.ibatis.binding.MapperMethod.executeForMany(MapperMethod.java:119) ~[mybatis-3.2.7.jar:3.2.7]</span><br><span class="line">    at org.apache.ibatis.binding.MapperMethod.execute(MapperMethod.java:63) ~[mybatis-3.2.7.jar:3.2.7]</span><br><span class="line">    at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:52) ~[mybatis-3.2.7.jar:3.2.7]</span><br><span class="line">    at com.sun.proxy.$Proxy49.findBillBillingTask(Unknown Source) ~[na:na]</span><br><span class="line">    at com.xxx.service.impl.XXXServiceImpl.findBillBillingTask(XXXServiceImpl.java:118) ~[bmc-service-0.0.1-SNAPSHOT.jar:na]</span><br><span class="line">    at com.xxx.service.impl.XXXServiceImpl$$FastClassByCGLIB$$7d4463f0.invoke(&lt;generated&gt;) ~[spring-core-4.0.0.RELEASE.jar:na]</span><br><span class="line">    at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204) ~[spring-core-4.0.0.RELEASE.jar:4.0.0.RELEASE]</span><br><span class="line">    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:713) ~[spring-aop-4.0.0.RELEASE.jar:4.0.0.RELEASE]</span><br><span class="line">    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157) ~[spring-aop-4.0.0.RELEASE.jar:4.0.0.RELEASE]</span><br><span class="line">    at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:98) ~[spring-tx-4.0.0.RELEASE.jar:4.0.0.RELEASE]</span><br><span class="line">    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:262) ~[spring-tx-4.0.0.RELEASE.jar:4.0.0.RELEASE]</span><br><span class="line">    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:95) ~[spring-tx-4.0.0.RELEASE.jar:4.0.0.RELEASE]</span><br><span class="line">    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.0.0.RELEASE.jar:4.0.0.RELEASE]</span><br><span class="line">    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:646) ~[spring-aop-4.0.0.RELEASE.jar:4.0.0.RELEASE]</span><br><span class="line">    at com.xxx.service.impl.XXXServiceImpl$$EnhancerByCGLIB$$32d6287d.findBillBillingTask(&lt;generated&gt;) ~[spring-core-4.0.0.RELEASE.jar:na]</span><br><span class="line">    at com.xxx.service.impl.XXXResultServiceImpl.saveBillBillingTask(XXXResultServiceImpl.java:213) ~[bmc-service-0.0.1-SNAPSHOT.jar:na]</span><br><span class="line">    at com.xxx.service.impl.XXXResultServiceImpl.disposeBillBillingResult(XXXResultServiceImpl.java:193) ~[bmc-service-0.0.1-SNAPSHOT.jar:na]</span><br><span class="line">    at com.xxx.service.impl.XXXResultServiceImpl$$FastClassByCGLIB$$5e8db258.invoke(&lt;generated&gt;) ~[spring-core-4.0.0.RELEASE.jar:na]</span><br><span class="line">    at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204) ~[spring-core-4.0.0.RELEASE.jar:4.0.0.RELEASE]</span><br><span class="line">    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:713) ~[spring-aop-4.0.0.RELEASE.jar:4.0.0.RELEASE]</span><br><span class="line">    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157) ~[spring-aop-4.0.0.RELEASE.jar:4.0.0.RELEASE]</span><br><span class="line">    at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:98) ~[spring-tx-4.0.0.RELEASE.jar:4.0.0.RELEASE]</span><br><span class="line">    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:262) ~[spring-tx-4.0.0.RELEASE.jar:4.0.0.RELEASE]</span><br><span class="line">    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:95) ~[spring-tx-4.0.0.RELEASE.jar:4.0.0.RELEASE]</span><br><span class="line">    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.0.0.RELEASE.jar:4.0.0.RELEASE]</span><br><span class="line">    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:646) ~[spring-aop-4.0.0.RELEASE.jar:4.0.0.RELEASE]</span><br><span class="line">    at com.xxx.service.impl.XXXResultServiceImpl$$EnhancerByCGLIB$$8d251e5.disposeBillBillingResult(&lt;generated&gt;) ~[spring-core-4.0.0.RELEASE.jar:na]</span><br><span class="line">    at com.xxx.XXXListener.receiveMessage(BillBillingResultListener.java:92) ~[bmc-main-0.0.1-SNAPSHOT.jar:na]</span><br><span class="line">    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.7.0_79]</span><br><span class="line">    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57) ~[na:1.7.0_79]</span><br><span class="line">    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.7.0_79]</span><br><span class="line">    at java.lang.reflect.Method.invoke(Method.java:606) ~[na:1.7.0_79]</span><br><span class="line">    at org.springframework.util.MethodInvoker.invoke(MethodInvoker.java:273) [spring-core-4.0.0.RELEASE.jar:4.0.0.RELEASE]</span><br><span class="line">    at org.springframework.jms.listener.adapter.MessageListenerAdapter.invokeListenerMethod(MessageListenerAdapter.java:466) [spring-jms-4.0.0.RELEASE.jar:4.0.0.RELEASE]</span><br><span class="line">    at org.springframework.jms.listener.adapter.MessageListenerAdapter.onMessage(MessageListenerAdapter.java:357) [spring-jms-4.0.0.RELEASE.jar:4.0.0.RELEASE]</span><br><span class="line">    at org.springframework.jms.listener.adapter.MessageListenerAdapter.onMessage(MessageListenerAdapter.java:332) [spring-jms-4.0.0.RELEASE.jar:4.0.0.RELEASE]</span><br><span class="line">    at org.springframework.jms.listener.AbstractMessageListenerContainer.doInvokeListener(AbstractMessageListenerContainer.java:537) [spring-jms-4.0.0.RELEASE.jar:4.0.0.RELEASE]</span><br><span class="line">    at org.springframework.jms.listener.AbstractMessageListenerContainer.invokeListener(AbstractMessageListenerContainer.java:497) [spring-jms-4.0.0.RELEASE.jar:4.0.0.RELEASE]</span><br><span class="line">    at org.springframework.jms.listener.AbstractMessageListenerContainer.doExecuteListener(AbstractMessageListenerContainer.java:468) [spring-jms-4.0.0.RELEASE.jar:4.0.0.RELEASE]</span><br><span class="line">    at org.springframework.jms.listener.AbstractPollingMessageListenerContainer.doReceiveAndExecute(AbstractPollingMessageListenerContainer.java:325) [spring-jms-4.0.0.RELEASE.jar:4.0.0.RELEASE]</span><br><span class="line">    at org.springframework.jms.listener.AbstractPollingMessageListenerContainer.receiveAndExecute(AbstractPollingMessageListenerContainer.java:263) [spring-jms-4.0.0.RELEASE.jar:4.0.0.RELEASE]</span><br><span class="line">    at org.springframework.jms.listener.DefaultMessageListenerContainer$AsyncMessageListenerInvoker.invokeListener(DefaultMessageListenerContainer.java:1104) [spring-jms-4.0.0.RELEASE.jar:4.0.0.RELEASE]</span><br><span class="line">    at org.springframework.jms.listener.DefaultMessageListenerContainer$AsyncMessageListenerInvoker.run(DefaultMessageListenerContainer.java:998) [spring-jms-4.0.0.RELEASE.jar:4.0.0.RELEASE]</span><br><span class="line">    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145) [na:1.7.0_79]</span><br><span class="line">    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615) [na:1.7.0_79]</span><br><span class="line">    at java.lang.Thread.run(Thread.java:745) [na:1.7.0_79]</span><br><span class="line">Caused by: org.apache.ibatis.exceptions.PersistenceException:</span><br><span class="line">### Error querying database.  Cause: java.lang.NullPointerException</span><br><span class="line">### Cause: java.lang.NullPointerException</span><br><span class="line">    at org.apache.ibatis.exceptions.ExceptionFactory.wrapException(ExceptionFactory.java:26) ~[mybatis-3.2.7.jar:3.2.7]</span><br><span class="line">    at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:111) ~[mybatis-3.2.7.jar:3.2.7]</span><br><span class="line">    at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:102) ~[mybatis-3.2.7.jar:3.2.7]</span><br><span class="line">    at sun.reflect.GeneratedMethodAccessor203.invoke(Unknown Source) ~[na:na]</span><br><span class="line">    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.7.0_79]</span><br><span class="line">    at java.lang.reflect.Method.invoke(Method.java:606) ~[na:1.7.0_79]</span><br><span class="line">    at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:358) ~[mybatis-spring-1.2.2.jar:1.2.2]</span><br><span class="line">    ... 48 common frames omitted</span><br><span class="line">Caused by: java.lang.NullPointerException: null</span><br><span class="line">    at org.apache.ibatis.reflection.property.PropertyTokenizer.&lt;init&gt;(PropertyTokenizer.java:30) ~[mybatis-3.2.7.jar:3.2.7]</span><br><span class="line">    at org.apache.ibatis.reflection.MetaObject.getValue(MetaObject.java:107) ~[mybatis-3.2.7.jar:3.2.7]</span><br><span class="line">    at org.apache.ibatis.scripting.xmltags.DynamicContext$ContextMap.get(DynamicContext.java:97) ~[mybatis-3.2.7.jar:3.2.7]</span><br><span class="line">    at org.apache.ibatis.scripting.xmltags.DynamicContext$ContextAccessor.getProperty(DynamicContext.java:116) ~[mybatis-3.2.7.jar:3.2.7]</span><br><span class="line">    at org.apache.ibatis.ognl.OgnlRuntime.getProperty(OgnlRuntime.java:1657) ~[mybatis-3.2.7.jar:3.2.7]</span><br><span class="line">    at org.apache.ibatis.ognl.ASTProperty.getValueBody(ASTProperty.java:92) ~[mybatis-3.2.7.jar:3.2.7]</span><br><span class="line">    at org.apache.ibatis.ognl.SimpleNode.evaluateGetValueBody(SimpleNode.java:170) ~[mybatis-3.2.7.jar:3.2.7]</span><br><span class="line">    at org.apache.ibatis.ognl.SimpleNode.getValue(SimpleNode.java:210) ~[mybatis-3.2.7.jar:3.2.7]</span><br><span class="line">    at org.apache.ibatis.ognl.ASTNotEq.getValueBody(ASTNotEq.java:49) ~[mybatis-3.2.7.jar:3.2.7]</span><br><span class="line">    at org.apache.ibatis.ognl.SimpleNode.evaluateGetValueBody(SimpleNode.java:170) ~[mybatis-3.2.7.jar:3.2.7]</span><br><span class="line">    at org.apache.ibatis.ognl.SimpleNode.getValue(SimpleNode.java:210) ~[mybatis-3.2.7.jar:3.2.7]</span><br><span class="line">    at org.apache.ibatis.ognl.Ognl.getValue(Ognl.java:333) ~[mybatis-3.2.7.jar:3.2.7]</span><br><span class="line">    at org.apache.ibatis.ognl.Ognl.getValue(Ognl.java:413) ~[mybatis-3.2.7.jar:3.2.7]</span><br><span class="line">    at org.apache.ibatis.ognl.Ognl.getValue(Ognl.java:395) ~[mybatis-3.2.7.jar:3.2.7]</span><br><span class="line">    at org.apache.ibatis.scripting.xmltags.OgnlCache.getValue(OgnlCache.java:48) ~[mybatis-3.2.7.jar:3.2.7]</span><br><span class="line">    at org.apache.ibatis.scripting.xmltags.ExpressionEvaluator.evaluateBoolean(ExpressionEvaluator.java:32) ~[mybatis-3.2.7.jar:3.2.7]</span><br><span class="line">    at org.apache.ibatis.scripting.xmltags.IfSqlNode.apply(IfSqlNode.java:33) ~[mybatis-3.2.7.jar:3.2.7]</span><br><span class="line">    at org.apache.ibatis.scripting.xmltags.MixedSqlNode.apply(MixedSqlNode.java:32) ~[mybatis-3.2.7.jar:3.2.7]</span><br><span class="line">    at org.apache.ibatis.scripting.xmltags.DynamicSqlSource.getBoundSql(DynamicSqlSource.java:40) ~[mybatis-3.2.7.jar:3.2.7]</span><br><span class="line">    at org.apache.ibatis.mapping.MappedStatement.getBoundSql(MappedStatement.java:278) ~[mybatis-3.2.7.jar:3.2.7]</span><br><span class="line">    at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:75) ~[mybatis-3.2.7.jar:3.2.7]</span><br><span class="line">    at sun.reflect.GeneratedMethodAccessor205.invoke(Unknown Source) ~[na:na]</span><br><span class="line">    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.7.0_79]</span><br><span class="line">    at java.lang.reflect.Method.invoke(Method.java:606) ~[na:1.7.0_79]</span><br><span class="line">    at org.apache.ibatis.plugin.Invocation.proceed(Invocation.java:49) ~[mybatis-3.2.7.jar:3.2.7]</span><br><span class="line">    at com.github.pagehelper.util.SqlUtil.doIntercept(SqlUtil.java:175) ~[pagehelper-4.2.1.jar:na]</span><br><span class="line">    at com.github.pagehelper.util.SqlUtil.intercept(SqlUtil.java:84) ~[pagehelper-4.2.1.jar:na]</span><br><span class="line">    at com.github.pagehelper.PageHelper.intercept(PageHelper.java:50) ~[pagehelper-4.2.1.jar:na]</span><br><span class="line">    at org.apache.ibatis.plugin.Plugin.invoke(Plugin.java:60) ~[mybatis-3.2.7.jar:3.2.7]</span><br><span class="line">    at com.sun.proxy.$Proxy53.query(Unknown Source) ~[na:na]</span><br><span class="line">    at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:108) ~[mybatis-3.2.7.jar:3.2.7]</span><br><span class="line">    ... 53 common frames omitted</span><br></pre></td></tr></table></figure><h1 id="分析过程"><a href="#分析过程" class="headerlink" title="分析过程"></a>分析过程</h1><p>通常，有堆栈数据的时候就很容分析出问题的原因。但是经过查看相关代码后发现触发点操作逻辑非常简单，不太会出现该异常。</p><p>于是就考虑从日志的细节分析问题。</p><p>先查看业务代码。该代码使用一个非空的VO对象作为查询条件，提交给Mapper。</p><p>Mapper中，判断各个参数是否为null或者空，然后拼接到SQL中。整个过程非常简单，而且大部分是成功执行的。</p><p>通过以上判断，可以认为不是业务代码的问题，极有可能是mybatis的问题。于是上网进行搜索，得到一些关于偶发NPE问题的描述。</p><p><a href="https://github.com/mybatis/mybatis-3/issues/313" target="_blank" rel="noopener">mybatis-3/issues/313</a></p><p><a href="https://github.com/mybatis/mybatis-3/issues/199" target="_blank" rel="noopener">mybatis-3/issues/199</a></p><p>以下问题提及了偶发返回null的情况。</p><p><a href="https://issues.apache.org/jira/browse/OGNL-121" target="_blank" rel="noopener">issues-OGNL-121</a></p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/mybatis-npe/1.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p>于是我们又观察反编译代码和日志执行情况，可以看到在SimpleNode.java中的确有非安全的逻辑操作。</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/mybatis-npe/2.png" alt title>                </div>                <div class="image-caption"></div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/mybatis-npe/3.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p>日志也有相关的执行过程。</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/mybatis-npe/4.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p>由于没有源代码，所以无法有效的进行Debug，模拟并发操作。因此该问题只能怀疑是这个原因导致的，具体可以在后续</p><p>有条件的情况下进行模拟测试。</p><h1 id="深入并发测试"><a href="#深入并发测试" class="headerlink" title="深入并发测试"></a>深入并发测试</h1><p>在Idea中可以反编译代码，且还原度较高，因此我们做了一次测试。</p><h2 id="测试环境"><a href="#测试环境" class="headerlink" title="测试环境"></a>测试环境</h2><p>两个Consumer调用Provider，Provider只做数据库查询，且查询中带条件判断。</p><p>这里使用Spring test进行测试，以下是Consumer端调用</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/mybatis-npe/5.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p>Provider端定义</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/mybatis-npe/6.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p>查询条件判断</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/mybatis-npe/7.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p>准备好测试环境后，就可以进行测试了。此处还需要注意如何在IntellJ Idea中Debug多线程，具体设置方法请找度娘。</p><h2 id="测试过程"><a href="#测试过程" class="headerlink" title="测试过程"></a>测试过程</h2><p>同时启动两个Consumer，然后在Provider中的SimpleNode.java中设置断点。</p><p>根据之前分析，如果要出现null，则说明getProperty会返回null。</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/mybatis-npe/8.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p>而getValue方法实际调用的逻辑是以下代码：</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/mybatis-npe/9.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p>说明以下的代码返回了null</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/mybatis-npe/10.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p>从代码上及Debug分析，如果要返回null，则很有可能在hasConstantValue=true且constantValue为null。</p><p>当然此处的数据已经是我们模拟出并发问题后的结果，也验证了是有可能的。</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/mybatis-npe/11.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p>如果没有出问题的情况时，正常的结果应该是constantValue=id，hasConstantValue=true。</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/mybatis-npe/12.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p>测试过程中，我们发现多个线程调用的对象实际是同一个，如下图中的ASTConst@6075。</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/mybatis-npe/13.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p>根据多线程常见问题处理经验来看，如果多线程操作同一个对象，则要注意其是否存在成员变量。如果有，那还要注意是否做了并发可见性处理。</p><p>于是我们看下代码</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/mybatis-npe/14.png" alt title>                </div>                <div class="image-caption"></div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/mybatis-npe/15.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p>那我们指导了这里有并发问题，那就好容易模拟了。我们只要在第一个线程中保持一种状态，然后暂停操作。再在另外一个线程中去特定的操作</p><p>步骤中正常变更数据。最后再放开第一个线程继续往下执行。由于第一个线程的成员变量已经发生了变化，所以后续的结果就不再是预想的那样</p><p>了。</p><p>于是就有了如下结果</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/mybatis-npe/16.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p>模拟的关键在于：</p><ol><li>多线程操作同一个服务</li><li>第一个线程在判断语句处等待第二个线程变更条件值，this.constantValueCalculated变量初始化为false，等第二个线程变更后变为true</li><li>第一个线程继续往下执行</li><li>第二个线程变更了成员变量的值，this.hasConstantValue变量初始化为false，但是被变更为true，然后等待第一个线程执行</li><li>第一个线程用刚更新的值去判断，返回了null值，也就导致了后续的NPE异常</li></ol><p><span style="color:blue"><em>注意：以上说的“等待”只是模拟说法，实际情况会由CPU控制，执行顺序不定。恰巧出现了以上执行流程，则会出现NPE问题。</em><span></span></span></p><h1 id="升级版处理逻辑"><a href="#升级版处理逻辑" class="headerlink" title="升级版处理逻辑"></a>升级版处理逻辑</h1><p>在Mybatis的3.4.5版本中，程序采用了volatile修饰符来定义变量，并且在使用上面也注意了赋值的先后顺序。</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/mybatis-npe/17.png" alt title>                </div>                <div class="image-caption"></div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/mybatis-npe/18.png" alt title>                </div>                <div class="image-caption"></div>            </figure><h1 id="结论"><a href="#结论" class="headerlink" title="结论"></a>结论</h1><p>建议升级mybatis，版本是3.3.0+，提及到新的ognl处理逻辑修复此问题，但是我们要考虑在经过充分测试的前提下进行升级。</p>]]></content>
    
    <summary type="html">
    
      Trouble Shooting —— MyBatis的PropertyTokenizer抛NPE异常
    
    </summary>
    
      <category term="mybatis" scheme="https://ningyu1.github.io/categories/mybatis/"/>
    
      <category term="trouble shooting" scheme="https://ningyu1.github.io/categories/mybatis/trouble-shooting/"/>
    
    
      <category term="mybatis" scheme="https://ningyu1.github.io/tags/mybatis/"/>
    
  </entry>
  
  <entry>
    <title>为什么手机浏览器打开word、excel文件部分文件能预览，部分文件不能预览？</title>
    <link href="https://ningyu1.github.io/20180809/96-word-xml.html"/>
    <id>https://ningyu1.github.io/20180809/96-word-xml.html</id>
    <published>2018-08-09T01:48:00.000Z</published>
    <updated>2024-04-23T01:48:15.038Z</updated>
    
    <content type="html"><![CDATA[<p>最近公司合同项目中有很多附件是excel和word的格式，这些文件有用户直接导入的，也有程序自动生成的，合同项目中有结合钉钉来做工作流，所以会有pc端和钉钉移动端的互动。</p><h2 id="问题现象"><a href="#问题现象" class="headerlink" title="问题现象"></a>问题现象</h2><p>pc端的附件列表可以正常的下载word、excel文件，并且可以成功的打开，但是当流程流转到钉钉时，在钉钉审批的时候可以通过连接跳转h5来显示附件列表，项目的功能设计初衷是可以在手机端打开预览word、excel文件。</p><p>但是发现了奇怪的问题，部分word可以在钉钉中显示，部分word无法显示，例如下图所示：</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/word-xml/1.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p>我们的期望效果如下图所示：</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/word-xml/2.png" alt title>                </div>                <div class="image-caption"></div>            </figure><h2 id="问题分析"><a href="#问题分析" class="headerlink" title="问题分析"></a>问题分析</h2><p>我们分别使用手机浏览器（safari）、postman、微信内嵌浏览器、qq内嵌浏览器分别测试无法正常预览的word链接</p><p>手机浏览器、微信内嵌浏览器、qq内嵌浏览器均无法打开</p><p>使用postman下载在移动端无法打开的word链接，返回的是一段xml，如下：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line"></span><br><span class="line">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;yes&quot;?&gt;</span><br><span class="line">&lt;?mso-application progid=&quot;Word.Document&quot;?&gt;</span><br><span class="line">&lt;pkg:package xmlns:pkg=&quot;http://schemas.microsoft.com/office/2006/xmlPackage&quot;&gt;&lt;pkg:part pkg:name=&quot;/_rels/.rels&quot;</span><br><span class="line">....</span><br></pre></td></tr></table></figure><p>这是个什么鬼？是word的xml格式，问题原因就在这里手机浏览器无法识别word的xml格式，因此再次尝试excel</p><p>excel使用的是poi生成直接写的是二级制格式，没有使用xml格式，因此excel是可以在移动端打开预览的。</p><p>询问开发word是如何生成的？</p><p>生成过程是这样的：使用word编辑好模版，然后另存为xml格式，导入到系统中去，通过FreeMarker替换内容，再将xml写到fastdfs中去后缀给成 ‘.doc’ ,这样下载下来使用office word可以直接打开xml格式的来进行无损渲染。</p><h2 id="解决方案"><a href="#解决方案" class="headerlink" title="解决方案"></a>解决方案</h2><p>询问业务是否必须要使用word格式文件？我的理解合同项目大多都是给用户只读的文件，建议使用pdf来做，使用jasper生成word模版，通过jasper的java api直接生成pdf，合同后期还要考虑添加水印，pdf更加方便一些。</p><p>建议使用pdf来替换word，如果非要使用word，建议生成word二进制格式来替换xml格式，除非不考虑移动端渲染可以使用xml格式的word。</p><p>目前java生成word的方式有如下六种方式：</p><ol><li>Jacob是Java-COM Bridge的缩写，它在Java与微软的COM组件之间构建一座桥梁。使用Jacob自带的DLL动态链接库，并通过JNI的方式实现了在Java平台上对COM程序的调用。DLL动态链接库的生成需要windows平台的支持。该方案只能在windows平台实现，是其局限性。</li><li>Apache POI包括一系列的API，它们可以操作基于MicroSoft OLE 2 Compound Document Format的各种格式文件，可以通过这些API在Java中读写Excel、Word等文件。他的excel处理很强大，对于word还局限于读取，目前只能实现一些简单文件的操作，不能设置样式。</li><li>Java2word是一个在java程序中调用 MS Office Word 文档的组件(类库)。该组件提供了一组简单的接口，以便java程序调用他的服务操作Word 文档。 这些服务包括： 打开文档、新建文档、查找文字、替换文字，插入文字、插入图片、插入表格，在书签处插入文字、插入图片、插入表格等。填充数据到表格中读取表格数据 ，1.1版增强的功能： 指定文本样式，指定表格样式。如此，则可动态排版word文档。是一种不错的解决方案。</li><li>iText是著名的开放源码的站点sourceforge一个项目，是用于生成PDF文档的一个java类库。通过iText不仅可以生成PDF或rtf的文档，而且可以将XML、Html文件转化为PDF文件。功能强大。</li><li>JSP输出样式，该方案实现简单，但是处理样式有点缺陷，简单的导出可以使用。</li><li>用XML做就很简单了。Word从2003开始支持XML格式，大致的思路是先用office2003或者2007编辑好word的样式，然后另存为xml，将xml翻译为FreeMarker模板，最后用java来解析FreeMarker模板并输出Doc。经测试这样方式生成的word文档完全符合office标准，样式、内容控制非常便利，打印也不会变形，生成的文档和office中编辑文档完全一样。</li></ol><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>伴随着手机的兴起，不管是传统行业还是互联网行业对系统都有在移动端使用的要求，不管是从用户体验上还是从移动系统兼容性以及浏览器兼容性上都会遇到各种问题，当然也有工具可以解决这些问题，例如：RN、flutter都可以很好的解决系统兼容问题，vue.js、angularjs都可以很好的解决浏览器兼容问题，而且这些都有大厂的支持，关于以上的问题这种解决方法并不是最好的，但是可以做为一种参考，重点是对问题的总结，只要解决问题的方法符合自己的业务场景我个人认为就是正（有）确（效）的方法，如果有更好的方式可以在下放留言一起讨论。</p>]]></content>
    
    <summary type="html">
    
      为什么手机浏览器打开word、excel文件部分文件能预览，部分文件不能预览？
    
    </summary>
    
      <category term="case-analysis" scheme="https://ningyu1.github.io/categories/case-analysis/"/>
    
    
      <category term="word-xml" scheme="https://ningyu1.github.io/tags/word-xml/"/>
    
  </entry>
  
  <entry>
    <title>使用downloadjs下载并且重命名文件名称引发的跨域问题</title>
    <link href="https://ningyu1.github.io/20180802/95-downloadjs-cors.html"/>
    <id>https://ningyu1.github.io/20180802/95-downloadjs-cors.html</id>
    <published>2018-08-02T06:50:00.000Z</published>
    <updated>2024-04-23T01:48:15.038Z</updated>
    
    <content type="html"><![CDATA[<p>我们有一部分静态资源放在fastdfs文件服务器上，并且文件名称是生成的随机数，直接浏览器下载是可以正常下载文件的，但是我们需要修改下载文件的名称，直接a标签href是无法修改下载文件名称的。</p><p>使用a标签的download属性又有浏览器兼容问题，而且download属性有一个弊端，只有点击右键另存为才会生效，直接点击是不生效的。</p><p>因此我们这里借助了一个组件<a href="https://github.com/rndme/download" target="_blank" rel="noopener">downloadjs</a>来进行文件下载，它可以修改下载文件的名称，并且也没有浏览器兼容问题，原理呢很简单那，使用ajax请求去下载文件，在发起请求时构造请求header来重命名下载文件名。</p><p>但是这里会存在一个问题？我们的fastdfs和应用程序是独立的两个域，因此存在跨域的问题，直接使用a标签的href是不存在跨域的问题，按关于这个跨域的问题我们如何解决？</p><p>先来看一下使用downloadjs下载fastdfs的文件时报出的跨域错误信息如下</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">Failed to load http://192.168.0.48:8079/group1/M00/03/35/wKgAMFtgB2SAFjibAAX3egrfUI8922.doc: No &apos;Access-Control-Allow-Origin&apos; header is present on the requested resource. Origin &apos;http://localhost:8080&apos; is therefore not allowed access.</span><br></pre></td></tr></table></figure><p>本地使用是通过vue的proxyTable绕过跨域的问题，其实就是前端的proxy方式虚拟一个context然后pass转发，虽然这样可以解决目前的问题，但是我们在uat和prd环境又要增加相同的context path的映射，这不是我们想要的，我们想直接访问下载地址来进行下载，因此我们需要修改fastdfs的nginx模块配置。</p><p>跨域的配置这里就不多说了，其实就是添加一系列的Access-Control-Allow-X的header即可，不会的可以参考我以前的文章<a href="https://ningyu1.github.io/site/post/92-cors-ajax/">跨域踩坑经验总结》</a>，唯一需要注意的是，当使用<code>Access-Control-Allow-Credentials=true</code>时<code>Access-Control-Allow-Origin</code>不允许使用<code>*</code> 必须使用具体的域名多个可以使用<code>,</code>分割。</p><p>修改后我们可以直接的请求地址下载文件即可。</p>]]></content>
    
    <summary type="html">
    
      使用downloadjs下载并且重命名文件名称引发的跨域问题
    
    </summary>
    
      <category term="js" scheme="https://ningyu1.github.io/categories/js/"/>
    
      <category term="cors" scheme="https://ningyu1.github.io/categories/js/cors/"/>
    
    
      <category term="js" scheme="https://ningyu1.github.io/tags/js/"/>
    
      <category term="downloadjs" scheme="https://ningyu1.github.io/tags/downloadjs/"/>
    
      <category term="cors" scheme="https://ningyu1.github.io/tags/cors/"/>
    
  </entry>
  
  <entry>
    <title>使用Embedded RedisServer写UT</title>
    <link href="https://ningyu1.github.io/20180717/94-embedded-redisServer.html"/>
    <id>https://ningyu1.github.io/20180717/94-embedded-redisServer.html</id>
    <published>2018-07-17T10:11:00.000Z</published>
    <updated>2024-04-23T01:48:15.038Z</updated>
    
    <content type="html"><![CDATA[<p>当我们在进行开发的时候经常会用到<code>Redis</code>，但是在写<code>junit</code>的时候往往引用了<code>Redis</code>造成<code>test case</code>很难写，我们需要<code>mock</code>一个<code>localhost</code>的<code>Redis server</code>来进行测试，因此我们可以借助<code>embedded redisServer</code>来实现，下面我们就看一下具体使用的示例</p><h2 id="代码示例"><a href="#代码示例" class="headerlink" title="代码示例"></a>代码示例</h2><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">@Before</span><br><span class="line">public void setUp() throws IOException &#123;</span><br><span class="line">    initMocks(this);</span><br><span class="line">    final Random random = new SecureRandom();</span><br><span class="line">    redisServer = new RedisServer();</span><br><span class="line">    redisServer.start();</span><br><span class="line"> </span><br><span class="line">    pool = new JedisPool();</span><br><span class="line">    repository = new RedisKeyRepository(pool);</span><br><span class="line">    manager = new RedisKeyManager(random, pool, repository);</span><br><span class="line">    manager.setMaxActiveKeys(3);</span><br><span class="line"> </span><br><span class="line">    clearData();</span><br><span class="line">    manager.initialiseNewRepository();</span><br><span class="line"> </span><br><span class="line">    resource = new ProtectedResource(repository, random);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><span style="color:blue"><em>这是一个非常简单的使用示例，我们还可以更改配置以及增加密码</em></span></p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">@Before</span><br><span class="line">public void setUpRedis() throws IOException, SchedulerConfigException &#123;</span><br><span class="line">    port = getPort();</span><br><span class="line">    logger.debug(&quot;Attempting to start embedded Redis server on port &quot; + port);</span><br><span class="line">    redisServer = RedisServer.builder()</span><br><span class="line">            .port(port)</span><br><span class="line">            .build();</span><br><span class="line">    redisServer.start();</span><br><span class="line">    final short database = 1;</span><br><span class="line">    JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();</span><br><span class="line">    jedisPoolConfig.setTestOnBorrow(true);</span><br><span class="line">    jedisPool = new JedisPool(jedisPoolConfig, host, port, Protocol.DEFAULT_TIMEOUT, null, database);</span><br><span class="line"> </span><br><span class="line">    jobStore = new RedisJobStore();</span><br><span class="line">    jobStore.setHost(host);</span><br><span class="line">    jobStore.setLockTimeout(2000);</span><br><span class="line">    jobStore.setPort(port);</span><br><span class="line">    jobStore.setInstanceId(&quot;testJobStore1&quot;);</span><br><span class="line">    jobStore.setDatabase(database);</span><br><span class="line">    mockScheduleSignaler = mock(SchedulerSignaler.class);</span><br><span class="line">    jobStore.initialize(null, mockScheduleSignaler);</span><br><span class="line">    schema = new RedisJobStoreSchema();</span><br><span class="line"> </span><br><span class="line">    jedis = jedisPool.getResource();</span><br><span class="line">    jedis.flushDB();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><span style="color:blue"><em>使用<code>RedisServerBuilder</code>构建<code>Redis server</code>，并且指定port</em></span></p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">@Test</span><br><span class="line">//Note the try/finally is to ensure that the server is shutdown so other tests do not have to</span><br><span class="line">//provide auth information</span><br><span class="line">public void testAuth() throws Exception &#123;</span><br><span class="line"> </span><br><span class="line">  RedisServer server = RedisServer.builder().port(6381).setting(&quot;requirepass foobar&quot;).build();</span><br><span class="line">  server.start();</span><br><span class="line">  RedisOptions job = new RedisOptions()</span><br><span class="line">    .setHost(&quot;localhost&quot;)</span><br><span class="line">    .setPort(6381);</span><br><span class="line">  RedisClient rdx = RedisClient.create(vertx, job);</span><br><span class="line"> </span><br><span class="line">  rdx.auth(&quot;barfoo&quot;, reply -&gt; &#123;</span><br><span class="line">    assertFalse(reply.succeeded());</span><br><span class="line">    rdx.auth(&quot;foobar&quot;, reply2 -&gt; &#123;</span><br><span class="line">      assertTrue(reply2.succeeded());</span><br><span class="line">      try &#123;</span><br><span class="line">        server.stop();</span><br><span class="line">      &#125; catch (Exception ignore) &#123;</span><br><span class="line">      &#125;</span><br><span class="line">      testComplete();</span><br><span class="line">    &#125;);</span><br><span class="line">  &#125;);</span><br><span class="line">  await();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><span style="color:blue"><em>设置一个需要密码访问的Redis server，setting可以设置redis conf中的所有属性</em></span></p><h2 id="更多用法"><a href="#更多用法" class="headerlink" title="更多用法"></a>更多用法</h2><p>还有很多用法，具体查看下面的代码示例</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">@Test</span><br><span class="line">public void testDebugSegfault() throws Exception &#123;</span><br><span class="line"> </span><br><span class="line">  RedisServer server = RedisServer.builder().port(6381).build();</span><br><span class="line">  server.start();</span><br><span class="line">  RedisOptions job = new RedisOptions()</span><br><span class="line">    .setHost(&quot;localhost&quot;)</span><br><span class="line">    .setPort(6381);</span><br><span class="line">  RedisClient rdx = RedisClient.create(vertx, job);</span><br><span class="line"> </span><br><span class="line">  rdx.debugSegfault(reply -&gt; &#123;</span><br><span class="line">    // this should fail, since we crashed the server on purpose</span><br><span class="line">    assertTrue(reply.failed());</span><br><span class="line">    rdx.info(reply2 -&gt; &#123;</span><br><span class="line">      assertFalse(reply2.succeeded());</span><br><span class="line">      server.stop();</span><br><span class="line">      testComplete();</span><br><span class="line">    &#125;);</span><br><span class="line">  &#125;);</span><br><span class="line">  await();</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">public RedisServerResource(int port, String password) &#123;</span><br><span class="line">    this.port = port;</span><br><span class="line">    try &#123;</span><br><span class="line">        RedisExecProvider redisExecProvider = RedisExecProvider.defaultProvider();</span><br><span class="line">        this.redisServer = RedisServer</span><br><span class="line">                .builder()</span><br><span class="line">                .redisExecProvider(redisExecProvider)</span><br><span class="line">                .port(port)</span><br><span class="line">                .setting(&quot;requirepass &quot; + password)</span><br><span class="line">                .build();</span><br><span class="line">    &#125; catch (Throwable error) &#123;</span><br><span class="line">        String message = String.format(&quot;failed creating Redis server (port=%d)&quot;, port);</span><br><span class="line">        throw new RuntimeException(message, error);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">@Before</span><br><span class="line">public void before() throws Exception &#123;</span><br><span class="line">  mockTracer.reset();</span><br><span class="line"> </span><br><span class="line">  redisServer = RedisServer.builder().setting(&quot;bind 127.0.0.1&quot;).build();</span><br><span class="line">  redisServer.start();</span><br><span class="line">&#125; </span><br><span class="line"></span><br><span class="line">private void startServer(TestContext testContext) &#123;</span><br><span class="line">    EmbeddedRedis embeddedRedis = AnnotationUtils.findAnnotation(testContext.getTestClass(), EmbeddedRedis.class);</span><br><span class="line">    int port = embeddedRedis.port();</span><br><span class="line"> </span><br><span class="line">    try &#123;</span><br><span class="line">        server = new RedisServer(port);</span><br><span class="line">        server.start();</span><br><span class="line">    &#125; catch (IOException e) &#123;</span><br><span class="line">        if (logger.isErrorEnabled()) &#123;</span><br><span class="line">            logger.error(e.getMessage(), e);</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">private RedisServer createRedisServer() &#123;</span><br><span class="line">    final RedisServerBuilder redisServerBuilder = RedisServer.builder()</span><br><span class="line">            .port(redisPort)</span><br><span class="line">            .setting(&quot;appendonly yes&quot;)</span><br><span class="line">            .setting(&quot;appendfsync everysec&quot;);</span><br><span class="line">    settings.stream().forEach(s -&gt; redisServerBuilder.setting(s));</span><br><span class="line"> </span><br><span class="line">    final RedisServer redisServer = redisServerBuilder.build();</span><br><span class="line">    return redisServer;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="有可能我们启动会遇到下面的错误："><a href="#有可能我们启动会遇到下面的错误：" class="headerlink" title="有可能我们启动会遇到下面的错误："></a>有可能我们启动会遇到下面的错误：</h2><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">java.lang.RuntimeException: Can&apos;t start redis server. Check logs for details.</span><br><span class="line">    at redis.embedded.AbstractRedisInstance.awaitRedisServerReady(AbstractRedisInstance.java:66)</span><br><span class="line">    at redis.embedded.AbstractRedisInstance.start(AbstractRedisInstance.java:37)</span><br><span class="line">    at redis.embedded.RedisServer.start(RedisServer.java:11)</span><br><span class="line">    at com.bignibou.configuration.session.EmbeddedRedisConfiguration$RedisServerBean.afterPropertiesSet(EmbeddedRedisConfiguration.java:26)</span><br><span class="line">    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1633)</span><br><span class="line">    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1570)</span><br><span class="line">    ... 15 more</span><br></pre></td></tr></table></figure><p>这个是什么原因呢？我们进一步debug输出redis server的log看是什么问题，redis log如下：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">The windows version of redis allocates a large memory mapped file for sharing the heap with the forked process used in persistence operations. This file will be created in the current working directory or the directory specified by the &apos;heapdir&apos; directive in the</span><br><span class="line">.conf file. Windows is reporting that there is insufficient disk space available for this file (Windows error 0x70).</span><br><span class="line">You may fix this probilem by either reducing the size of the Redis heap with the --maxheap flag, or by moving the heap file to a local drive with sufficient space.</span><br><span class="line">Please see the documentation included with the binary distributions for more details on the --maxheap and --heapdir flags.</span><br><span class="line">Redis can not continue, Exiting.</span><br></pre></td></tr></table></figure><p>这里的原因是我们启动的时候<code>heap</code>不够，redis server默认的<code>maxheap:1024000000</code>，创建<code>.conf</code>文件时硬盘不够，那如何解决这个错误呢？</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">@Test</span><br><span class="line">public void testAuth() throws Exception &#123;</span><br><span class="line">  RedisServer server = RedisServer.builder().port(6381).setting(&quot;maxheap 51200000&quot;).build();</span><br><span class="line">  server.start();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>关于<code>redis maxheap</code>的详细描述如下：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line"># The Redis heap must be larger than the value specified by the maxmemory</span><br><span class="line"># flag, as the heap allocator has its own memory requirements and</span><br><span class="line"># fragmentation of the heap is inevitable. If only the maxmemory flag is</span><br><span class="line"># specified, maxheap will be set at 1.5*maxmemory. If the maxheap flag is</span><br><span class="line"># specified along with maxmemory, the maxheap flag will be automatically</span><br><span class="line"># increased if it is smaller than 1.5*maxmemory.</span><br><span class="line"># </span><br><span class="line"># maxheap &lt;bytes&gt;</span><br><span class="line">maxheap 51200000</span><br></pre></td></tr></table></figure><p><span style="color:blue"><em>注意：修改时需要考虑可用量，常规情况都无需修改这个参数</em></span></p><p><a href="https://www.programcreek.com/java-api-examples/index.php?api=redis.embedded.RedisServer" target="_blank" rel="noopener">更多查看官方文档</a></p>]]></content>
    
    <summary type="html">
    
      使用Embedded RedisServer写UT
    
    </summary>
    
      <category term="test" scheme="https://ningyu1.github.io/categories/test/"/>
    
      <category term="redis" scheme="https://ningyu1.github.io/categories/test/redis/"/>
    
    
      <category term="test" scheme="https://ningyu1.github.io/tags/test/"/>
    
      <category term="redis" scheme="https://ningyu1.github.io/tags/redis/"/>
    
      <category term="Can&#39;t start redis server. Check logs for details." scheme="https://ningyu1.github.io/tags/Can-t-start-redis-server-Check-logs-for-details/"/>
    
  </entry>
  
  <entry>
    <title>通过对Maven的依赖分析剔除无用的jar引用</title>
    <link href="https://ningyu1.github.io/20180711/93-maven-depenpency-analyze.html"/>
    <id>https://ningyu1.github.io/20180711/93-maven-depenpency-analyze.html</id>
    <published>2018-07-11T06:11:00.000Z</published>
    <updated>2024-04-23T01:48:15.038Z</updated>
    
    <content type="html"><![CDATA[<p>当项目开发维护了一段时间时，经常会有项目打包速度慢，jar依赖多，依赖关系错综复杂，这种问题是项目维护最常见的问题，由于开发人员在bugfix或者feature开发时往往只是往项目中添加jar依赖，那我们如何分析出项目中哪些依赖是用到的，哪些依赖是不用的？</p><p>使用<a href="http://maven.apache.org/plugins/maven-dependency-plugin/analyze-mojo.html" target="_blank" rel="noopener">Maven analyze</a>来进行分析</p><p>使用如下命令：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">mvn dependency:analyze</span><br></pre></td></tr></table></figure><p>会输出如下的日志：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">[INFO] --- maven-dependency-plugin:2.8:analyze (default-cli) @ xxxproject ---</span><br><span class="line">[WARNING] Used undeclared dependencies found:</span><br><span class="line">[WARNING]    org.springframework:spring-beans:jar:4.0.0.RELEASE:compile</span><br><span class="line">[WARNING]    org.springframework:spring-context:jar:4.0.0.RELEASE:compile</span><br><span class="line">[WARNING] Unused declared dependencies found:</span><br><span class="line">[WARNING]    com.alibaba:dubbo:jar:2.5.3:compile</span><br><span class="line">[WARNING]    com.baidu.disconf:disconf-client:jar:2.6.32:compile</span><br><span class="line">[WARNING]    org.mybatis:mybatis:jar:3.2.7:compile</span><br><span class="line">[WARNING]    org.mybatis:mybatis-spring:jar:1.2.2:compile</span><br><span class="line">[WARNING]    mysql:mysql-connector-java:jar:5.1.41:compile</span><br><span class="line">[WARNING]    com.alibaba:druid:jar:1.0.9:compile</span><br><span class="line">[WARNING]    com.github.sgroschupf:zkclient:jar:0.1:compile</span><br><span class="line">[WARNING]    org.apache.zookeeper:zookeeper:jar:3.4.6:compile</span><br><span class="line">[WARNING]    org.springframework:spring-jdbc:jar:4.0.0.RELEASE:compile</span><br><span class="line">[WARNING]    org.slf4j:log4j-over-slf4j:jar:1.7.5:compile</span><br><span class="line">[WARNING]    org.slf4j:jcl-over-slf4j:jar:1.7.5:runtime</span><br><span class="line">[WARNING]    ch.qos.logback:logback-classic:jar:1.0.13:compile</span><br></pre></td></tr></table></figure><p>我们就来说一下日志中的<code>Used undeclared dependencies found</code>和<code>Unused declared dependencies found</code></p><h2 id="Used-undeclared-dependencies-found"><a href="#Used-undeclared-dependencies-found" class="headerlink" title="Used undeclared dependencies found"></a>Used undeclared dependencies found</h2><p>这个是指某些依赖的包在代码中有用到它的代码，但是它并不是直接的依赖（就是说没有在pom中直接声明），是通过引入传递下来的包。</p><p>举个例子：</p><p><code>project</code>在<code>pom</code>中声明了<code>A.ja</code>r的依赖（没有声明<code>B.jar</code>的依赖）<br><code>A.jar</code>的依赖关系：<code>A.jar</code> -&gt; <code>B.jar</code><br>通过<code>mvn dependency:analyze</code>出现<br><code>[WARNING] Used undeclared dependencies found: B.jar</code><br>就说明<code>project</code>中的代码用到了<code>B.jar</code>的代码<br>这个时候你就可以把<code>B.jar</code>直接声明在pom中</p><h2 id="Unused-declared-dependencies-found"><a href="#Unused-declared-dependencies-found" class="headerlink" title="Unused declared dependencies found"></a>Unused declared dependencies found</h2><p>这个是指我们在pom中声明了依赖，但是在实际代码中并没有用到这个包！也就是多余的包。<br>这个时候我们就可以把这个依赖从pom中剔除。</p><p><span style="color:blue"><br><em>但是这里我们需要注意：<br>这里说的实际代码没有用到，指的是在main/java和test里没有用的，但是并不是意味着真的没有用到这些包，有可能配置文件中引用或者其他扩展点自动加载这些包，所以我们在删除依赖的时候一定要小心，做好备份，因为这类引用maven是分析不出来的。</em><br></span></p>]]></content>
    
    <summary type="html">
    
      通过对Maven的依赖分析剔除无用的jar引用
    
    </summary>
    
      <category term="maven" scheme="https://ningyu1.github.io/categories/maven/"/>
    
    
      <category term="maven" scheme="https://ningyu1.github.io/tags/maven/"/>
    
      <category term="dependency:analyze" scheme="https://ningyu1.github.io/tags/dependency-analyze/"/>
    
  </entry>
  
  <entry>
    <title>跨域踩坑经验总结（内涵：跨域知识科普）</title>
    <link href="https://ningyu1.github.io/20180627/92-CORS-ajax.html"/>
    <id>https://ningyu1.github.io/20180627/92-CORS-ajax.html</id>
    <published>2018-06-27T04:20:00.000Z</published>
    <updated>2024-04-23T01:48:15.038Z</updated>
    
    <content type="html"><![CDATA[<p>跨域问题是我们非常常见的问题，尤其在跨系统页面间的调用经常会遇到，解决的方式在网上一搜一大把，这里整理出我遇到跨域问题解决的方式以及思路，如何安全的解决跨域调用请继续往下看。</p><ul><li><a href="#what&#39;s-Cross-domain">什么是跨域？</a></li><li><a href="#scene">跨域使用的场景？</a></li><li><a href="#solution">解决跨域的方式？</a></li><li><a href="#solution1">前端、后端如何配合处理跨域？</a><ul><li><a href="#errors">跨域常见错误</a></li><li><a href="#options">突如其来的OPTIONS请求？</a></li><li><a href="#response-header">后端需要返回的Header有哪些？</a></li><li><a href="#ajax-crossDomain">前端如何配合发起请求？</a></li><li><a href="#ajax-compatibility">Ajax跨域请求跨平台兼容性问题</a></li></ul></li></ul><h1 id="什么是跨域？"><a href="#什么是跨域？" class="headerlink" title="什么是跨域？"></a><span id="what's-Cross-domain">什么是跨域？</span></h1><p><a href="https://en.wikipedia.org/wiki/Cross-origin_resource_sharing" target="_blank" rel="noopener">什么是Cross-origin_resource_sharing?</a><br>跨域请求存在的原因：由于浏览器的同源策略，即属于不同域的页面之间不能相互访问各自的页面内容。</p><h1 id="跨域使用的场景？"><a href="#跨域使用的场景？" class="headerlink" title="跨域使用的场景？"></a><span id="scene">跨域使用的场景？</span></h1><ol><li>域名不同<ul><li><code>www.jiuyescm.com</code>和<code>www.jiuye.com</code>即为不同的域名</li></ul></li><li>二级域名相同，子域名不同<ul><li><code>a.jiuyescm.com</code>和<code>b.jiuyescm.com</code>为子域不同</li></ul></li><li>端口不同，协议不同 <ul><li><code>http://www.jiuyescm.com</code>和<code>https://www.jiuyescm.com</code></li><li><code>www.jiuyescm.com:8888</code>和<code>www.jiuyescm.com:8080</code></li></ul></li></ol><h1 id="解决跨域的方式？"><a href="#解决跨域的方式？" class="headerlink" title="解决跨域的方式？"></a><span id="solution">解决跨域的方式？</span></h1><ol><li>jsonp<ul><li>安全性差，已经不推荐</li></ul></li><li>CORS（W3C标准，跨域资源共享 - Cross-origin resource sharing）<ul><li>服务端设置，安全性高，推荐使用</li></ul></li><li>websocke<ul><li>特殊场景时使用，不属于常规跨域操作</li></ul></li><li>代理服务（nginx）<ul><li>可作为服务端cors配置的一种方式，推荐使用</li></ul></li></ol><h1 id="前端、后端如何配合处理跨域？"><a href="#前端、后端如何配合处理跨域？" class="headerlink" title="前端、后端如何配合处理跨域？"></a><span id="solution1">前端、后端如何配合处理跨域？</span></h1><p><span style="color:blue"><em>ps. 我们这里只介绍：CORS处理方式。</em></span></p><h2 id="跨域常见错误"><a href="#跨域常见错误" class="headerlink" title="跨域常见错误"></a><span id="errors">跨域常见错误</span></h2><p>首先让我们看一下前端报出的跨域错误信息</p><p>第一种：<code>No &#39;Access-Control-Allow-Origin&#39; header is present on the requested resource</code>，并且<code>The response had HTTP status code 404</code></p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">XMLHttpRequest cannot load http://b.domain.com, Response to preflinght request doesn&apos;t pass access control check: No &apos;Access-Control-Allow-Origin&apos; header is present on the requested resource. Origin &apos;http://a.domain.com&apos; is therefore not allowed access. The Response had HTTP status code 404.</span><br></pre></td></tr></table></figure><p><span style="color:blue"><em>ps.并且The response had HTTP status code 404</em></span></p><p>问题原因：服务器端后台没有允许OPTIONS请求</p><p>第二种：<code>No &#39;Access-Control-Allow-Origin&#39; header is present on the requested resource</code>，并且<code>The response had HTTP status code 405</code></p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">XMLHttpRequest cannot load http://b.domain.com, Response to preflinght request doesn&apos;t pass access control check: No &apos;Access-Control-Allow-Origin&apos; header is present on the requested resource. Origin &apos;http://a.domain.com&apos; is therefore not allowed access. The Response had HTTP status code 405.</span><br></pre></td></tr></table></figure><p><span style="color:blue"><em>ps.并且The response had HTTP status code 405</em></span></p><p>问题原因：服务器端后台允许了OPTIONS请求，但是某些安全配置阻止了OPTIONS请求</p><p>第三种：<code>No &#39;Access-Control-Allow-Origin&#39; header is present on the requested resource</code>，并且<code>The response had HTTP status code 200</code></p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">XMLHttpRequest cannot load http://b.domain.com, Response to preflinght request doesn&apos;t pass access control check: No &apos;Access-Control-Allow-Origin&apos; header is present on the requested resource. Origin &apos;http://a.domain.com&apos; is therefore not allowed access.</span><br></pre></td></tr></table></figure><p><span style="color:blue"><em>ps.并且The response had HTTP status code 200</em></span></p><p>问题原因：服务器端后台允许了OPTIONS请求，并且OPTIONS请求没有被阻止，但是头部不匹配。</p><p>第四种：<code>heade contains multiple values &#39;*,*&#39;</code>，并且<code>The response had HTTP status code 200</code></p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">XMLHttpRequestcannot load http://b.domain.com. The &apos;Access-Control-Allow-Origin&apos; header contains multiple values&apos;*, *&apos;, but only one is allowed. Origin &apos;http://a.domain.com&apos; is therefore notallowed access.</span><br></pre></td></tr></table></figure><p><span style="color:blue"><em>ps.并且The response had HTTP status code 200</em></span></p><p>问题原因：设置多次Access-Control-Allow-Origin=*，可能是配置的人对CORS实现原理和机制不了解导致。</p><h2 id="突如其来的OPTIONS请求？"><a href="#突如其来的OPTIONS请求？" class="headerlink" title="突如其来的OPTIONS请求？"></a><span id="options">突如其来的OPTIONS请求？</span></h2><p>有时你会发现明明请求的是POST、GET、PUT、DELETE，但是浏览器中看到的确实OPTION，，为什么会变成OPTION？</p><p>原因：因为本次Ajax请求是“非简单请求”,所以请求前会发送一次预检请求(OPTIONS)，这个操作由浏览器自己进行。如果服务器端后台接口没有允许OPTIONS请求,将会导致无法找到对应接口地址，因此需要服务端提供相应的信息到response header中，继续往下看。</p><h2 id="后端需要返回的Header有哪些？"><a href="#后端需要返回的Header有哪些？" class="headerlink" title="后端需要返回的Header有哪些？"></a><span id="response-header">后端需要返回的Header有哪些？</span></h2><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line"># 服务端允许访问的域名</span><br><span class="line">Access-Control-Allow-Origin=https://idss-uat.jiuyescm.com</span><br><span class="line"># 服务端允许访问Http Method</span><br><span class="line">Access-Control-Allow-Methods=GET, POST, PUT, DELETE, PATCH, OPTIONS</span><br><span class="line"># 服务端接受跨域带过来的Cookie,当为true时,origin必须是明确的域名不能使用*</span><br><span class="line">Access-Control-Allow-Credentials=true</span><br><span class="line"># Access-Control-Allow-Headers 表明它允许跨域请求包含content-type头，我们这里不设置，有需要的可以设置</span><br><span class="line">#Access-Control-Allow-Headers=Content-Type,Accept</span><br><span class="line"># 跨域请求中预检请求(Http Method为Option)的有效期,20天,单位秒</span><br><span class="line">Access-Control-Max-Age=1728000</span><br></pre></td></tr></table></figure><p><span style="color:blue"><em>ps. 如果跨域需要携带cookie去请求，<code>Access-Control-Allow-Credentials</code>必须为true，但是需要注意当<code>Access-Control-Allow-Credentials=true</code>时，<code>Access-Control-Allow-Origin</code>就不能为” * “ ，必须是明确的域名，当然可以多个域名使用 “,” 分割</em></span></p><h2 id="前端如何配合发起请求？"><a href="#前端如何配合发起请求？" class="headerlink" title="前端如何配合发起请求？"></a><span id="ajax-crossDomain">前端如何配合发起请求？</span></h2><p>如果是浏览器直接访问跨域请求url，只要服务端返回 “Access-Control-Allow-X” 系列header在response中即可成功访问。</p><p>如果是ajax发起的请求该如何处理？</p><p>第一种：请求不需要携带cookie</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">$.ajax(&#123;</span><br><span class="line">    url : &apos;url&apos;,</span><br><span class="line">    data : data,</span><br><span class="line">    dataType: &apos;json&apos;,</span><br><span class="line">    type : &apos;POST&apos;,</span><br><span class="line">    crossDomain: true,</span><br><span class="line">    contentType: &quot;application/json&quot;,</span><br><span class="line">    success: function (data) &#123;</span><br><span class="line">        var a=JSON.stringify(data);</span><br><span class="line">        if(data.result==true)&#123;</span><br><span class="line">        　　...........</span><br><span class="line">    　　 &#125;else&#123;</span><br><span class="line">　　　　 　　...........</span><br><span class="line">　　　　 &#125;</span><br><span class="line">    &#125;,</span><br><span class="line">    error:function (data) &#123;</span><br><span class="line">        var a=JSON.stringify(data);</span><br><span class="line">        alert(a);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p><span style="color:blue"><em>ps. 增加crossDomain=true</em></span></p><p>第二种：请求需要携带cookie</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">$.ajax(&#123;</span><br><span class="line">url : &apos;url&apos;,</span><br><span class="line">data : data,</span><br><span class="line">dataType: &apos;json&apos;,</span><br><span class="line">type : &apos;POST&apos;,</span><br><span class="line">xhrFields: &#123;</span><br><span class="line">    withCredentials: true</span><br><span class="line">&#125;,</span><br><span class="line">crossDomain: true,</span><br><span class="line">contentType: &quot;application/json&quot;,</span><br><span class="line">success: function (data) &#123;</span><br><span class="line">        var a=JSON.stringify(data);</span><br><span class="line">        if(data.result==true)&#123;</span><br><span class="line">        　　...........</span><br><span class="line">    　　 &#125;else&#123;</span><br><span class="line">　　　　 　　...........</span><br><span class="line">　　　　 &#125;</span><br><span class="line">    &#125;,</span><br><span class="line">    error:function (data) &#123;</span><br><span class="line">        var a=JSON.stringify(data);</span><br><span class="line">        alert(a);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p><span style="color:blue"><em>ps. 增加crossDomain与xhr.withCredentials，发送Ajax时，Request header中便会带上 Cookie 信息。</em></span></p><p>到这里你以为跨域的相关都介绍完毕了？太天真</p><p>最后还有一个终极boss问题，是什么问题呢？</p><p>上面的第二种携带cookie的跨域请求调用方式在IOS下可以正常工作，但是在Android下无法正常工作并且还报错，额。。。。。</p><h2 id="Ajax跨域请求跨平台兼容性问题"><a href="#Ajax跨域请求跨平台兼容性问题" class="headerlink" title="Ajax跨域请求跨平台兼容性问题"></a><span id="ajax-compatibility">Ajax跨域请求跨平台兼容性问题</span></h2><p>问题原因：因为Android下的webview不兼容这个写法，使用标准的 <a href="http://www.w3school.com.cn/jquery/ajax_ajax.asp#beforeSend(XHR" target="_blank" rel="noopener">beforeSend(XHR)</a>) 替换</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">xhrFields: &#123;</span><br><span class="line">withCredentials: true</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><span style="color:blue"><em>ps. webview不兼容的写法，firefox下也不兼容</em></span></p><p>标准的写法：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">$.ajax(&#123;</span><br><span class="line">    type: &quot;POST&quot;,</span><br><span class="line">    url: &quot;url&quot;,</span><br><span class="line">    data:datatosend,</span><br><span class="line">    dataType:&quot;json&quot;,</span><br><span class="line">    beforeSend: function(xhr) &#123;</span><br><span class="line">        xhr.withCredentials = true;</span><br><span class="line">    &#125;</span><br><span class="line">    crossDomain:true,</span><br><span class="line">    success: function (data) &#123;</span><br><span class="line">        var a=JSON.stringify(data);</span><br><span class="line">        if(data.result==true)&#123;</span><br><span class="line">        　　...........</span><br><span class="line">    　　 &#125;else&#123;</span><br><span class="line">　　　　 　　...........</span><br><span class="line">　　　　 &#125;</span><br><span class="line">    &#125;,</span><br><span class="line">    error:function (data) &#123;</span><br><span class="line">        var a=JSON.stringify(data);</span><br><span class="line">        alert(a);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>到这跨域的相关使用就介绍完毕，这次是真的结束了。Keep Real!</p>]]></content>
    
    <summary type="html">
    
      跨域踩坑经验总结（内涵：跨域知识科普）
    
    </summary>
    
      <category term="cors" scheme="https://ningyu1.github.io/categories/cors/"/>
    
    
      <category term="cors" scheme="https://ningyu1.github.io/tags/cors/"/>
    
      <category term="xhr.withCredentials" scheme="https://ningyu1.github.io/tags/xhr-withCredentials/"/>
    
      <category term="crossDomain" scheme="https://ningyu1.github.io/tags/crossDomain/"/>
    
      <category term="Ajax跨域" scheme="https://ningyu1.github.io/tags/Ajax%E8%B7%A8%E5%9F%9F/"/>
    
  </entry>
  
  <entry>
    <title>Docker启动的容器如何清理日志？看这里</title>
    <link href="https://ningyu1.github.io/20180619/90-docker-container-cleanlog.html"/>
    <id>https://ningyu1.github.io/20180619/90-docker-container-cleanlog.html</id>
    <published>2018-06-19T07:10:00.000Z</published>
    <updated>2024-04-23T01:48:15.038Z</updated>
    
    <content type="html"><![CDATA[<p>Docker run起来的容器随着时间久了，容器内的服务输出的日志也在日积月累，需要定期的进行日志清理。</p><p>如果公司使用DevOps的话更加需要对容器内的日志进行定期清理，业务的镜像服务或许还好一些，因为开发同学每天都在用、每天都会upgrade服务，在upgrade的时候会删除老的容器，再重新run一个新容器去替换掉老的，但是有一些长期run的服务就很少有人关注了，比如说rancher、还有一些基础服务，可能很长时间也不会去做upgrade操作，因此容器内的日志就越来越多，如果不清理总有一天会撑爆服务器硬盘，到那个时候再去清理恢复服务的话，有可能会有磁盘文件损坏的风险。</p><p>因此我们需要定期的对Docker容器内的日志进行清理。</p><p>如何查看Docker内容器的日志？可以参考文章：<a href="https://ningyu1.github.io/site/post/48-docker/">《如何直接操作Docker容器？》</a></p><p>在清理容器日志前，我们首先要知道Docker将容器的日志放在那里？</p><p>Docker将容器的日志放在<code>/var/lib/docker/containers/containerid/containerid-json.log</code></p><p><span style="color:blue"><em>ps. containerid是容器id一般是<code>82bbc....</code>这个风格，64位字符</em></span></p><p>当然找不到的话也可以使用文件搜索的方式去查找Docker的容器日志放在那里，查找的时候按照上面的名称风格去查找，例如：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">find / -type f -name &quot;*-json.log&quot;</span><br></pre></td></tr></table></figure><p>容器的id怎么查看呢？</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">docker ps</span><br></pre></td></tr></table></figure><p>通过ps找到容器id，也找到日志所在的位置后，接下来就是清理日志的操作了，日志文件不能直接删除，直接删除会影响正在运行的容器，可以通过清空文件内容的方式来处理。</p><p>清空文件的方式有很多种如下：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">$ : &gt; filename </span><br><span class="line">$ &gt; filename </span><br><span class="line">$ echo &quot;&quot; &gt; filename </span><br><span class="line">$ echo &gt; filename </span><br><span class="line">$ cat /dev/null &gt; filename</span><br></pre></td></tr></table></figure><p>选一种即可</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">cat /dev/null &gt;/var/lib/docker/containers/containerid/containerid-json.log</span><br></pre></td></tr></table></figure>]]></content>
    
    <summary type="html">
    
      Docker启动的容器如何清理日志？看这里
    
    </summary>
    
      <category term="docker" scheme="https://ningyu1.github.io/categories/docker/"/>
    
    
      <category term="docker" scheme="https://ningyu1.github.io/tags/docker/"/>
    
  </entry>
  
  <entry>
    <title>Zookeeper事务日志和snapshot清理方式</title>
    <link href="https://ningyu1.github.io/20180615/89-zookeeper-cleanlog.html"/>
    <id>https://ningyu1.github.io/20180615/89-zookeeper-cleanlog.html</id>
    <published>2018-06-15T09:15:00.000Z</published>
    <updated>2024-04-23T01:48:15.038Z</updated>
    
    <content type="html"><![CDATA[<p>Zookeeper运行过程会产生大量的事务日志和snapshot镜像文件，文件的目录是通过<code>zoo.conf</code>的<code>datadir</code>参数指定的，下面我们就说一下如何清理事务日志和snapshot。</p><p>清理的方式有如下三种：</p><ul><li><a href="#zkConf">一、zookeeper配置自动清理</a></li><li><a href="#shell">二、使用自定义清理脚本</a></li><li><a href="#zkCleanup">三、使用zkCleanup.sh清理</a></li></ul><p>下面我们一一介绍每种清理方式是如何使用的。</p><h1 id="zookeeper配置自动清理"><a href="#zookeeper配置自动清理" class="headerlink" title="zookeeper配置自动清理"></a><span id="zkConf">zookeeper配置自动清理</span></h1><p>zookeeper在3.4.0版本以后提供了自动清理snapshot和事务日志的功能通过配置 autopurge.snapRetainCount 和 autopurge.purgeInterval 这两个参数能够实现定时清理了。这两个参数都是在zoo.cfg中配置的：</p><p>我们使用的zk版本是：3.4.6，因此可以使用自带的清理功能</p><p>autopurge.purgeInterval  这个参数指定了清理频率，单位是小时，需要填写一个1或更大的整数，默认是0，表示不开启自己清理功能。</p><p>autopurge.snapRetainCount 这个参数和上面的参数搭配使用，这个参数指定了需要保留的文件数目。默认是保留3个。</p><p>示例：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">autopurge.snapRetainCount=60 </span><br><span class="line">autopurge.purgeInterval=48</span><br></pre></td></tr></table></figure><p>保留48小时内的日志，并且保留60个文件</p><p><span style="color:red"><em>ps.但是修改conf需要重启服务，生产可能不会考虑重启服务因此使用其他方法。</em></span></p><h1 id="使用自定义清理脚本"><a href="#使用自定义清理脚本" class="headerlink" title="使用自定义清理脚本"></a><span id="shell">使用自定义清理脚本</span></h1><p>clean_zook_log.sh脚本内容如下</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">#!/bin/bash</span><br><span class="line">            </span><br><span class="line">#snapshot file dir</span><br><span class="line">dataDir=/var/zookeeper/version-2</span><br><span class="line">#tran log dir</span><br><span class="line">dataLogDir=/var/zookeeper/version-2</span><br><span class="line">logDir=/usr/local/zookeeper/logs</span><br><span class="line">#Leave 60 files</span><br><span class="line">count=60</span><br><span class="line">count=$[$count+1]</span><br><span class="line">ls -t $dataLogDir/log.* | tail -n +$count | xargs rm -f</span><br><span class="line">ls -t $dataDir/snapshot.* | tail -n +$count | xargs rm -f</span><br><span class="line">ls -t $logDir/zookeeper.log.* | tail -n +$count | xargs rm -f</span><br></pre></td></tr></table></figure><p>这个脚本保留最新的60个文件，可以将他写到 将这个脚本添加到crontab中，设置为每天凌晨2点？或者其他时间执行即可。</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">crontab -e 2 2 * * * /bin/bash /usr/local/zookeeper/bin/clean_zook_log.sh &gt; /dev/null 2&gt;&amp;1</span><br></pre></td></tr></table></figure><p><span style="color:red"><em>ps.不用修改配置，不需要重启zk集群，推荐使用</em></span></p><h1 id="使用zkCleanup-sh清理"><a href="#使用zkCleanup-sh清理" class="headerlink" title="使用zkCleanup.sh清理"></a><span id="zkCleanup">使用zkCleanup.sh清理</span></h1><p>这个脚本是使用的zookeeper.jar里的<code>org.apache.zookeeper.server.PurgeTxnLog</code>这个class的main函数清理的，因此需要启动一个java进程，比shell清理要重一些。</p><p><a href="http://zookeeper.apache.org/doc/r3.4.3/api/index.html" target="_blank" rel="noopener"><code>org.apache.zookeeper.server.PurgeTxnLog</code>文档</a></p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">sh /usr/local/zookeeper/bin/zkCleanup.sh 数据目录 -n 20</span><br></pre></td></tr></table></figure><p>参数说明</p><p>数据目录： /var/zookeeper<br>20:  保留快照日志的数量</p><p><span style="color:red"><em>ps.因为zookeeper从3.4.0版本之后提供了对历史事务日志和快照文件的自动清理，所以这个脚本很少使用，另外在生产环境中我们一般采取自动脚本来定点定量清除指定日期的日志文件</em></span></p><p>到这里三种清理方式都介绍完毕了，根据自己的实际情况选择一种使用就可以了。祝大家周末愉快以及端午节快乐，ok 收工 回家。</p>]]></content>
    
    <summary type="html">
    
      Zookeeper事务日志和snapshot清理方式
    
    </summary>
    
      <category term="zookeeper" scheme="https://ningyu1.github.io/categories/zookeeper/"/>
    
    
      <category term="zookeeper" scheme="https://ningyu1.github.io/tags/zookeeper/"/>
    
  </entry>
  
  <entry>
    <title>如何扩展个人微信号来实现群组管理的功能？</title>
    <link href="https://ningyu1.github.io/20180608/85-wechat.html"/>
    <id>https://ningyu1.github.io/20180608/85-wechat.html</id>
    <published>2018-06-08T05:38:00.000Z</published>
    <updated>2024-04-23T01:48:15.038Z</updated>
    
    <content type="html"><![CDATA[<p>最近在思考一个问题，如何能在系统中集成微信群组管理的功能，比如说邀请好友进群、对群组进行管理、创建群组、删除群组之类的操作，说白了就是将微信的功能嵌入到自己的程序里面去。这样就可以有效的管理多个群组来扩展一些客服的功能。</p><p>于是查找关于微信API的资料，第一反应就是先去官方的开放文档中查看是否有类似API开放。</p><p>翻了一圈资料，看了微信服务号、订阅号、小程序、以及企业微信的开放文档后整理出来目前官方开放的API的功能现状如下：</p><p>官方是没有提供任何个人群管理接口API，只有一些类似外挂类的工具可以对个人群组进行管理。</p><p>但是外挂类的工具又怕有风险，说不定哪天就over了。</p><p>在资料翻阅的过程，发现了可以通过微友助手在群组里添加机器人，但是这个方式可能不是我们想要的。</p><p>官方开放的客服API可以与客服系统进行对接，它是将微信端作为客服的入口与客服系统对接，生成客诉工单或者是跟客服对话，这也不是我们想要的方式。</p><p><span style="color:blue"><strong>在没有官方API可用的情况下我们想使用这方面的功能该如何操作呢？</strong></span></p><h1 id="发现曙光"><a href="#发现曙光" class="headerlink" title="发现曙光"></a>发现曙光</h1><p>微信目前官方提供的终端，除了手机端以外还有电脑端和WEB网页端。</p><p>咦，有WEB网页端那不就有API可以操作么？只是可能我们要写类似于外挂一样的东西模拟官方的微信网页端操作。</p><p>于是开始搜索这方面的资料，很幸运找到了<a href="https://github.com/littlecodersh/ItChat" target="_blank" rel="noopener"><code>ItChat</code></a>这个类库。</p><p>这个类库的实现方式就是我们刚才说到的模拟网页版本的rest请求去扩展的一些接口。</p><p>那已经有人做好了轮子那我们就可以直接使用了。</p><h1 id="ItChat的实现方式"><a href="#ItChat的实现方式" class="headerlink" title="ItChat的实现方式"></a>ItChat的实现方式</h1><p>第一部分，分析微信协议，开始获取并模拟扩展个人微信号所需要的协议。<br>第二部分，利用上面分析出来的协议<br>第三部分，包装成扩展功能API</p><p>网页端微信协议分析思路可以查看：<a href="http://python.jobbole.com/84918/" target="_blank" rel="noopener">手把手教你扩展个人微信号（1）</a></p><p>接口的使用可以查看：<a href="http://python.jobbole.com/86532/" target="_blank" rel="noopener">手把手教你扩展个人微信号（2）</a></p><p>有兴趣的可以进去看一下。</p><p>Github链接：<a href="https://github.com/littlecodersh/ItChat" target="_blank" rel="noopener">ItChat</a></p><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>这个库的实现方式还是很有趣的，使用网页版微信调用的rest接口，跟常规外挂一样模拟网页微信的操作，只要网页版本微信不关闭应该都能用，只是可能需要紧跟着网页版本的微信rest接口持续升级</p><p>github上有1w多star，很明显说明了这个扩展功能还是很多人迫切想使用的，后面我会尝试一下然后把遇到的问题和使用经验会再分享出来。Keep Real！</p>]]></content>
    
    <summary type="html">
    
      如何扩展个人微信号来实现群组管理的功能？
    
    </summary>
    
      <category term="wechat" scheme="https://ningyu1.github.io/categories/wechat/"/>
    
    
      <category term="wechat" scheme="https://ningyu1.github.io/tags/wechat/"/>
    
  </entry>
  
  <entry>
    <title>如何使用抓包调试工具 —— Charles</title>
    <link href="https://ningyu1.github.io/20180604/84-charles.html"/>
    <id>https://ningyu1.github.io/20180604/84-charles.html</id>
    <published>2018-06-04T09:28:00.000Z</published>
    <updated>2024-04-23T01:48:15.038Z</updated>
    
    <content type="html"><![CDATA[<p>以下信息转自公司内网资料，觉得很实用就转载出来提供参考。</p><h1 id="一、Charles是什么？"><a href="#一、Charles是什么？" class="headerlink" title="一、Charles是什么？"></a>一、Charles是什么？</h1><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/charles/1.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p>Charles是在 Mac或Windows下常用的http协议网络包截取工具，在平常的测试与调式过程中，掌握此工具就基本可以不用其他抓包工具了。</p><h1 id="二、为什么是Charles？"><a href="#二、为什么是Charles？" class="headerlink" title="二、为什么是Charles？"></a>二、为什么是Charles？</h1><p>为什么要用抓包工具？大家在平常移动App调试测试中是如何进行抓包的？</p><p><strong>主要特点如下：</strong></p><ol><li>支持SSL代理，可以截取分析SSL的请求</li><li>支持流量控制。可以模拟慢速网络(2G,3G)，以及等待时间较长的请求。</li><li>支持AJAX调试。可以自动把JSON或者XML数据格式化，方便查看。</li><li>支持重发网络请求，方便后端调试。</li><li>支持修改网络请求参数。</li><li>支持网络请求的截取和动态修改。</li><li>最重要的一个优点就是有不同平台的版本（Mac，Windows、Linux）即学一个打遍天下。</li></ol><h1 id="三、Charles基本工作原理"><a href="#三、Charles基本工作原理" class="headerlink" title="三、Charles基本工作原理"></a>三、Charles基本工作原理</h1><p>charles是通过网络代理来进行抓包的，下面先了解一下http代理的原理：</p><ul><li>普通http请求过程</li></ul><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/charles/2.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p><span style="color:blue"><em>一般情况下的HTTP请求与响应</em></span></p><ul><li>加入了Charles的HTTP代理的请求与响应过程</li></ul><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/charles/3.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p><span style="color:blue"><em>中间的代理服务器就是Charles</em></span></p><h1 id="四、Charles的下载与安装过程"><a href="#四、Charles的下载与安装过程" class="headerlink" title="四、Charles的下载与安装过程"></a>四、Charles的下载与安装过程</h1><ul><li>官网下载地址：<a href="http://www.charlesproxy.com/download/" target="_blank" rel="noopener">http://www.charlesproxy.com/download/</a></li><li>Mac下安装</li></ul><p>是一个安装包是一个dmg后缀的文件。打开后将Charles拖到Application目录下即完成安装。</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/charles/4.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p>在Mac下你打开Launchpad即可看到一个像花瓶一样的Charles程序图标</p><ul><li>Windows下安装</li></ul><p>下载后直接双击根据安装向导一步一步安装即可</p><h1 id="五、Http抓包操作步骤"><a href="#五、Http抓包操作步骤" class="headerlink" title="五、Http抓包操作步骤"></a>五、Http抓包操作步骤</h1><h2 id="Step-1-开启Charleshttp代理"><a href="#Step-1-开启Charleshttp代理" class="headerlink" title="Step 1: 开启Charleshttp代理"></a>Step 1: 开启Charleshttp代理</h2><ul><li>设置Charles代理</li></ul><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/charles/5.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p>第一次启动默认会开启本机的系统代理，因为我们只是监控移动端的所以将此选去除（去掉选项前面的小钩）</p><ul><li>激活http代理功能</li></ul><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/charles/6.png" alt title>                </div>                <div class="image-caption"></div>            </figure><h2 id="Step-2-手机端Wifi添加代理"><a href="#Step-2-手机端Wifi添加代理" class="headerlink" title="Step 2: 手机端Wifi添加代理"></a>Step 2: 手机端Wifi添加代理</h2><h3 id="Android端"><a href="#Android端" class="headerlink" title="Android端"></a>Android端</h3><ul><li>在手机端打开你的Wifi设置然后长按已经连接的Wifi在弹出来的菜单中选择【修改网络】</li><li>沟上[显示高级]选项–【手动】</li><li>输入代理服务器的IP与端口，IP即安装了Charles的电脑IP地址，端口就是前面一步设置Charles时所设置的端口。</li></ul><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/charles/7.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p><span style="color:blue"><em>注意：手机所连接Wifi要与电脑在同一个LAN(局域网)</em></span></p><h3 id="iOS端"><a href="#iOS端" class="headerlink" title="iOS端"></a>iOS端</h3><ul><li>点击你所连接的wifi</li><li>输入代理服务器的IP与端口，</li></ul><p>IP即安装了Charles的电脑IP地址，端口就是前面一步设置Charles时所设置的端口。</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/charles/8.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p><span style="color:blue"><em>注意：手机所连接Wifi要与电脑在同一个LAN(局域网)</em></span></p><h2 id="Step-3-开启Charles录制功能"><a href="#Step-3-开启Charles录制功能" class="headerlink" title="Step 3:开启Charles录制功能"></a>Step 3:开启Charles录制功能</h2><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/charles/9.png" alt title>                </div>                <div class="image-caption"></div>            </figure><ol><li>当手机连接上代理后Charles会弹出相应的提示框，点击Allow即可</li><li>点击工具栏上的开始录制按钮，即启动了Charles的抓包功能了。</li></ol><h2 id="Step-4：启动应用开始抓包"><a href="#Step-4：启动应用开始抓包" class="headerlink" title="Step 4：启动应用开始抓包"></a>Step 4：启动应用开始抓包</h2><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/charles/10.png" alt title>                </div>                <div class="image-caption"></div>            </figure><ol><li>在手机上操作相应的App进行抓包。</li><li>在Charles的主界面上就可看到相应的请求内容。</li></ol><h2 id="Step-5：分析抓取的数据包"><a href="#Step-5：分析抓取的数据包" class="headerlink" title="Step 5：分析抓取的数据包"></a>Step 5：分析抓取的数据包</h2><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/charles/11.png" alt title>                </div>                <div class="image-caption"></div>            </figure><ol><li>Charles 主要提供两种查看封包的视图，分别名为 “Structure”和 “Sequence”：<ul><li>Structure 视图将网络请求按访问的域名分类；</li><li>Sequence 视图将网络请求按访问的时间排序。</li></ul></li><li>大家可以根据具体的需要在这两种视图之前来回切换。请求多了有些时候会看不过来，Charles提供了一个简单的Filter功能，可以输入关键字来快速筛选出URL 中带指定关键字的网络请求。</li><li>对于某一个具体的网络请求，你可以查看其详细的请求内容和响应内容。如果请求内容是POST 的表单，Charles 会自动帮你将表单进行分项显示。如果响应内容是 JSON 格式的，那么 Charles可以自动帮你将JSON 内容格式化，方便你查看。如果响应内容是图片，那么 Charles可以显示出图片的预览。</li></ol><h1 id="六、Https抓包操作步骤"><a href="#六、Https抓包操作步骤" class="headerlink" title="六、Https抓包操作步骤"></a>六、Https抓包操作步骤</h1><h2 id="Step-1：了解一下https的基本原理；"><a href="#Step-1：了解一下https的基本原理；" class="headerlink" title="Step 1：了解一下https的基本原理；"></a>Step 1：了解一下https的基本原理；</h2><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/charles/12.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p>HTTPS其实是有两部分组成：HTTP+ SSL / TLS，也就是在HTTP上又加了一层处理加密信息的模块。服务端和客户端的信息传输都会通过TLS进行加密，所以传输的数据都是加密后的数据。具体是如何进行加密，解密，验证的，且看图,下面这个图的解说<br>详细说明，请参考：<a href="http://blog.csdn.net/clh604/article/details/22179907" target="_blank" rel="noopener">http://blog.csdn.net/clh604/article/details/22179907</a></p><h2 id="Step-2：在手机端安装SSL证书"><a href="#Step-2：在手机端安装SSL证书" class="headerlink" title="Step 2：在手机端安装SSL证书"></a>Step 2：在手机端安装SSL证书</h2><ul><li>将证书文件从Charles导出</li><li>然后通过adb或者其他工具将其复制到手机的SD卡中。</li></ul><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/charles/13.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p><span style="color:blue"><em>从Charles导出证书文件</em></span></p><ul><li>将证书文件导入Android手机</li></ul><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/charles/14.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p><span style="color:blue"><em>在手机的设置界面找到【安全】—》【从内部存储设备或SD卡安装】—-》选择SD卡上的证书—》弹出设置证书名对话框，输入一个易记的名字，然后根据提示进行导入即可</em></span></p><ul><li>将证书文件导入iOS手机</li></ul><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/charles/15.png" alt title>                </div>                <div class="image-caption"></div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/charles/16.jpg" alt title>                </div>                <div class="image-caption"></div>            </figure><ol><li>在iPhone手机上打开Safari浏览器，然后在地址栏中输入<a href="http://www.charlesproxy.com/getssl。" target="_blank" rel="noopener">www.charlesproxy.com/getssl。</a></li><li>稍后会弹出安装描述文件提示，点击右上角的【安装】按钮进行证书安装即可。</li><li>在iOS 10.3之后,需要手动打开开关以信任证书，设置-&gt;通用-&gt;关于本机-&gt;证书信任设置-&gt; 找到charles proxy custom root certificate然后信任该证书即可.</li></ol><h2 id="Step-3：激活Charles的SSL代理"><a href="#Step-3：激活Charles的SSL代理" class="headerlink" title="Step 3：激活Charles的SSL代理"></a>Step 3：激活Charles的SSL代理</h2><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/charles/17.jpg" alt title>                </div>                <div class="image-caption"></div>            </figure><ol><li>选择【Proxy】—&gt;【SSL Proxying Settings..】设置。</li><li>在弹出来的对话框中沟选【Enable SSL Proxying】。</li></ol><h2 id="Step-4：将指定的URL请求开启SSL代理功能"><a href="#Step-4：将指定的URL请求开启SSL代理功能" class="headerlink" title="Step 4：将指定的URL请求开启SSL代理功能"></a>Step 4：将指定的URL请求开启SSL代理功能</h2><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/charles/18.jpg" alt title>                </div>                <div class="image-caption"></div>            </figure><ol><li>选择抓取的https链接，然后右键选择【Enable SSL Proxying】。</li><li>如果不激活SSL代理，所以https请求都是乱码无法查看。</li></ol><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/charles/19.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p><span style="color:blue"><em>再次请求这个Https时，其请求内容已经一目了然了。</em></span></p><h1 id="七、Charles进阶—修改请求也响应的内容"><a href="#七、Charles进阶—修改请求也响应的内容" class="headerlink" title="七、Charles进阶—修改请求也响应的内容"></a>七、Charles进阶—修改请求也响应的内容</h1><h2 id="Step-1：设置Charless断点"><a href="#Step-1：设置Charless断点" class="headerlink" title="Step 1：设置Charless断点"></a>Step 1：设置Charless断点</h2><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/charles/20.jpg" alt title>                </div>                <div class="image-caption"></div>            </figure><p><span style="color:blue"><em>选择【Breakpoint Settings…】—&gt;勾选【Enable Breakpoints】来激活断点功能</em></span></p><h2 id="Step-2：对指定的URL开启断点功能"><a href="#Step-2：对指定的URL开启断点功能" class="headerlink" title="Step 2：对指定的URL开启断点功能"></a>Step 2：对指定的URL开启断点功能</h2><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/charles/21.png" alt title>                </div>                <div class="image-caption"></div>            </figure><ol><li>选择一个URL链接-à右键开启菜单—》选择【Breakpoints】即可开启此请求的断点。</li><li>这样Charles会遇到此请求时会弹出中断对话框。</li></ol><h2 id="Step-3：编辑请求与响应的内容。"><a href="#Step-3：编辑请求与响应的内容。" class="headerlink" title="Step 3：编辑请求与响应的内容。"></a>Step 3：编辑请求与响应的内容。</h2><ul><li>编辑请求内容，在中断对话框中，用户可以点击Edit Request来编辑请求的内容，编辑完成后然后点击【Execute】发出去这个请求给服务端</li></ul><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/charles/22.png" alt title>                </div>                <div class="image-caption"></div>            </figure><ul><li>编辑服务器响应的内容，在【Edit Request】对话中点击【Execute】发出请求后，服务端返回来数据后，用户点击【Edit Response】可对响应内容进行编辑完成后然后点击【Execute】发出去这个数据给客户端。</li></ul><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/charles/23.png" alt title>                </div>                <div class="image-caption"></div>            </figure><h1 id="八、Charles进阶—弱网模拟"><a href="#八、Charles进阶—弱网模拟" class="headerlink" title="八、Charles进阶—弱网模拟"></a>八、Charles进阶—弱网模拟</h1><ol><li>菜单中选择【Proxy】—&gt;【Throttle Settings..】-à激活【Enable Throttling】。</li><li>在Throttle Configuration设置弱网的参数。</li><li>以下是各种网制式的速率参考文档：</li></ol><p><a href="https://link.jianshu.com/?t=http://wenku.baidu.com/link?url=buoPWkwmfW3B216gtRzIBbLZST3EEqxnAHNEabaVu2tXlGlkCMUl_E4tor_408BRG4eRSd4p5VQd_k4xiq14VXvJIrrfZq7l9CJhU8ht7Nq" target="_blank" rel="noopener">移动网络制式与网速的参考文档</a></p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/charles/24.jpg" alt title>                </div>                <div class="image-caption"></div>            </figure><p><span style="color:blue"><em>弱网模拟设置</em></span></p>]]></content>
    
    <summary type="html">
    
      如何使用抓包调试工具 —— Charles
    
    </summary>
    
      <category term="Charles" scheme="https://ningyu1.github.io/categories/Charles/"/>
    
    
      <category term="Charles" scheme="https://ningyu1.github.io/tags/Charles/"/>
    
      <category term="移动端测试" scheme="https://ningyu1.github.io/tags/%E7%A7%BB%E5%8A%A8%E7%AB%AF%E6%B5%8B%E8%AF%95/"/>
    
  </entry>
  
  <entry>
    <title>Trouble Shooting —— Docker Pull Image : error pulling image configuration: unexpected EOF错误</title>
    <link href="https://ningyu1.github.io/20180529/83-docker-pull-error.html"/>
    <id>https://ningyu1.github.io/20180529/83-docker-pull-error.html</id>
    <published>2018-05-29T04:09:00.000Z</published>
    <updated>2024-04-23T01:48:15.038Z</updated>
    
    <content type="html"><![CDATA[<h1 id="问题现象"><a href="#问题现象" class="headerlink" title="问题现象"></a>问题现象</h1><p>执行docker pull命令报错：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">docker@rancher-192:~$ docker pull 192.168.0.34:5000/imageName:latest</span><br><span class="line">latest: Pulling from imageName</span><br><span class="line">75a822cd7888: Pulling fs layer</span><br><span class="line">046e44ee6057: Download complete</span><br><span class="line">8c47541cb10b: Waiting</span><br><span class="line">e17edf9a1bd4: Waiting</span><br><span class="line">error pulling image configuration: unexpected EOF</span><br></pre></td></tr></table></figure><p>查看日志错误如下：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">docker@rancher-192:~$ journalctl -u docker.service</span><br><span class="line">-- Logs begin at Mon 2018-05-14 04:14:07 CST, end at Tue 2018-05-29 11:31:02 CST. --</span><br><span class="line">May 29 11:28:22 rancher-192.168.0.83 docker[993]: time=&quot;2018-05-29T11:28:22.601383366+08:00&quot; level=error msg=&quot;Not continuing with pull after error: error pulling image configuration: unexpected EOF&quot;</span><br><span class="line">May 29 11:30:36 rancher-192.168.0.83 docker[993]: time=&quot;2018-05-29T11:30:36.987345560+08:00&quot; level=error msg=&quot;Not continuing with pull after error: error pulling image configuration: unexpected EOF&quot;</span><br></pre></td></tr></table></figure><p>随便找一台其他机器上进行pull操作，一样报错，但是pull其他镜像确实正常的，查看其他机器上日志如下：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">docker@devserver1:~/messer/public$ journalctl -u docker.service</span><br><span class="line">-- Logs begin at Fri 2018-05-25 23:29:13 CST, end at Tue 2018-05-29 11:34:24 CST. --</span><br><span class="line">May 29 11:34:24 devserver1 docker[893]: time=&quot;2018-05-29T11:34:24.167053102+08:00&quot; level=error msg=&quot;Not continuing with pull after error: error pulling image configuration: unexpected EOF&quot;</span><br><span class="line">May 29 11:34:24 devserver1 docker[893]: time=&quot;2018-05-29T11:34:24.212480193+08:00&quot; level=info msg=&quot;Layer sha256:3fc67fe0621339e8f025cb429eecee5db64025673f3eafb02d12b512f07bbba5 cleaned up&quot;</span><br></pre></td></tr></table></figure><p>这个问题让我们想到了之前我写过一篇文章<a href="https://ningyu1.github.io/site/post/79-docker-registry-pull-filesystem-layer/">《Trouble Shooting —— Docker Pull Image : Filesystem layer verification failed for digest sha256错误》</a>也是<code>docker pull</code>的时候报错：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">8b7054...: Verifying Checksum</span><br><span class="line">Filesystem layer verification failed for digest sha256: 8b7054.....</span><br></pre></td></tr></table></figure><p>通过使用之前文章的解决方案依然可以解决这个问题。</p><p>这类问题可以使用绕过校验重新build后push刷新digest值后，恢复原始build参数再重新push恢复默认操作来进行解决。</p>]]></content>
    
    <summary type="html">
    
      Trouble Shooting —— Docker Pull Image : error pulling image configuration: unexpected EOF错误
    
    </summary>
    
      <category term="docker" scheme="https://ningyu1.github.io/categories/docker/"/>
    
    
      <category term="docker" scheme="https://ningyu1.github.io/tags/docker/"/>
    
      <category term="docker pull" scheme="https://ningyu1.github.io/tags/docker-pull/"/>
    
      <category term="error pulling image configuration: unexpected EOF" scheme="https://ningyu1.github.io/tags/error-pulling-image-configuration-unexpected-EOF/"/>
    
  </entry>
  
  <entry>
    <title>如何免费的让你的网站变得更加安全 - HTTPS</title>
    <link href="https://ningyu1.github.io/20180528/82-ssl-lets-encrypt.html"/>
    <id>https://ningyu1.github.io/20180528/82-ssl-lets-encrypt.html</id>
    <published>2018-05-28T07:57:00.000Z</published>
    <updated>2024-04-23T01:48:15.038Z</updated>
    
    <content type="html"><![CDATA[<p>在这个数据不安全的世界里，很有可能你早上买了个东西下午就会有类似的推销电话打过来骚扰你，这些数据信息从哪里来的呢？当然很多时候是人为的贩卖信息造成的，但是数据来源很大一部分是来自于互联网。因此站点使用https已经是最基本的防护，当我去访问一个站点它如果不是https的我可能都不想访问它更别提输入一些个人信息了。那怎么才能让我们提供的网站安全的服务你的用户呢？当然是使用证书来保护网站来往的数据。</p><p>如果不差钱的话还是使用收费的证书去给你的网站开启https。当然国内也有很多免费的证书，去谷哥或者度娘能检索到一大把的免费证书信息，各大云服务商上面也有免费的证书可以申请使用，我下面就介绍一个免费的使用方式。</p><p><a href="https://letsencrypt.org/" target="_blank" rel="noopener">Let’s Encrypt</a></p><p><code>Let&#39;s Encrypt</code>是一个于2015年三季度推出的<a href="https://zh.wikipedia.org/wiki/%E6%95%B0%E5%AD%97%E8%AF%81%E4%B9%A6%E8%AE%A4%E8%AF%81%E6%9C%BA%E6%9E%84" target="_blank" rel="noopener">数字证书认证机构</a>，旨在以自动化流程消除手动创建和安装证书的复杂流程，并推广使<a href="https://zh.wikipedia.org/wiki/%E8%90%AC%E7%B6%AD%E7%B6%B2" target="_blank" rel="noopener">万维网</a>服务器的加密连接无所不在，为安全网站提供免费的<a href="https://zh.wikipedia.org/wiki/SSL" target="_blank" rel="noopener">SSL</a>/<a href="https://zh.wikipedia.org/wiki/TLS" target="_blank" rel="noopener">TLS</a>证书。</p><p><code>Let&#39;s Encrypt由</code><a href="https://zh.wikipedia.org/w/index.php?title=%E4%BA%92%E8%81%94%E7%BD%91%E5%AE%89%E5%85%A8%E7%A0%94%E7%A9%B6%E5%B0%8F%E7%BB%84&amp;action=edit&amp;redlink=1" target="_blank" rel="noopener">互联网安全研究小组</a>（缩写ISRG）提供服务。主要赞助商包括<a href="https://zh.wikipedia.org/wiki/%E7%94%B5%E5%AD%90%E5%89%8D%E5%93%A8%E5%9F%BA%E9%87%91%E4%BC%9A" target="_blank" rel="noopener">电子前哨基金会</a>、<a href="https://zh.wikipedia.org/wiki/Mozilla%E5%9F%BA%E9%87%91%E4%BC%9A" target="_blank" rel="noopener">Mozilla基金会</a>、<a href="https://zh.wikipedia.org/wiki/Akamai" target="_blank" rel="noopener">Akamai</a>以及思科。2015年4月9日，ISRG与Linux基金会宣布合作。</p><p>通过官网我们能看到赞助商还是蛮多的，<a href="https://letsencrypt.org/sponsors/" target="_blank" rel="noopener">赞助商列表</a></p><p>上述来自于维基百科，<a href="https://zh.wikipedia.org/wiki/Let%27s_Encrypt" target="_blank" rel="noopener">查看原文</a></p><p>从介绍中能了解到它是为了解决，<span style="color:blue"><strong><em>以自动化流程消除手动创建和安装证书的复杂流程</em></strong></span>，让证书使用更加简单。</p><p>我们通过<code>Let&#39;s Encrypt</code>官网的<a href="https://letsencrypt.org/getting-started/" target="_blank" rel="noopener">Getting Started</a>中可以查看具体的使用说明</p><p>下面我们简单介绍一下使用步骤：</p><h1 id="使用步骤"><a href="#使用步骤" class="headerlink" title="使用步骤"></a>使用步骤</h1><p>安装证书非常简单，只需要使用<a href="https://certbot.eff.org/" target="_blank" rel="noopener">Certbot</a>，就可以完成。</p><ul><li>打开<a href="https://certbot.eff.org/" target="_blank" rel="noopener">Certbot</a>，选择你的网站使用的应用服务器和操作系统。如下图：</li></ul><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/certbot/1.png" alt title>                </div>                <div class="image-caption"></div>            </figure><ul><li>选择完后会生成安装教程，不用想太多Step by step就好了，如下图：</li></ul><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/certbot/2.png" alt title>                </div>                <div class="image-caption"></div>            </figure><h2 id="安装基础环境"><a href="#安装基础环境" class="headerlink" title="安装基础环境"></a>安装基础环境</h2><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">$ sudo apt-get update</span><br><span class="line">$ sudo apt-get install software-properties-common</span><br><span class="line">$ sudo add-apt-repository ppa:certbot/certbot</span><br><span class="line">$ sudo apt-get update</span><br><span class="line">$ sudo apt-get install python-certbot-nginx</span><br></pre></td></tr></table></figure><h2 id="安装证书"><a href="#安装证书" class="headerlink" title="安装证书"></a>安装证书</h2><p>安装完之后直接运行<code>sudo certbot --nginx</code>即可</p><p><span style="color:blue"><code>certbot</code> 会自动修改nginx配置文件(<code>nginx.conf</code>)并且列出你的虚拟站点让你选择是否开启HTTPS，当然你只用选择是否开启即可，选择完后它会自动下载证书并且修改nginx配置文件</span></p><p>修改后的nginx.conf是什么样的？让我们看一下</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">listen 443 ssl; # managed by Certbot</span><br><span class="line">ssl_certificate /etc/letsencrypt/live/your.domain/fullchain.pem; # managed by Certbot</span><br><span class="line">ssl_certificate_key /etc/letsencrypt/live/your.domain/privkey.pem; # managed by Certbot</span><br><span class="line">include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot</span><br></pre></td></tr></table></figure><p>还会很贴心的帮你生成http跳转到https的配置，如下：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line"># Redirect non-https traffic to https</span><br><span class="line">if ($scheme != &quot;https&quot;) &#123;</span><br><span class="line">  return 301 https://$host$request_uri;</span><br><span class="line">&#125; # managed by Certbot</span><br></pre></td></tr></table></figure><p>到这里就完成了证书安装，是不是很简单。当然我们之前也说过证书是有有效期的，那过期了之后我们如何操作？再根据上面的操作执行一次？</p><p>当然不是了，我们可以使用自动检测的方式来进行自动的更新证书与nginx配置。具体看下面操作：</p><h2 id="证书自动更新"><a href="#证书自动更新" class="headerlink" title="证书自动更新"></a>证书自动更新</h2><p>下面是官方对于证书续订的说明：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">Automating renewal</span><br><span class="line">The Certbot packages on your system come with a cron job that will renew your certificates automatically before they expire. </span><br><span class="line">Since Let&apos;s Encrypt certificates last for 90 days, it&apos;s highly advisable to take advantage of this feature. </span><br><span class="line">You can test automatic renewal for your certificates by running this command:</span><br><span class="line"></span><br><span class="line">$ sudo certbot renew --dry-run</span><br></pre></td></tr></table></figure><p>首先<code>Let’s Encrypt</code> 的证书只有90天的有效期，所以我们可以使用<code>crontab</code>来进行定时自动更新。</p><p><code>crontab</code>如何使用这里就不多做介绍了，可以查看<a href="https://ningyu1.github.io/linux-command/c/crontab.html">crontab使用说明</a></p><p>使用下面的表达式让其在每个月的一号强制更新证书，但是证书的强制更新不能太频繁，太频繁会提前进入证书授权限制。</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">0 0 1 * * /usr/bin/certbot renew --force-renewal</span><br><span class="line">10 0 1 * * /usr/sbin/service nginx restart</span><br></pre></td></tr></table></figure><p><a href="https://certbot.eff.org/docs/using.html#renewal" target="_blank" rel="noopener">renew的使用说明</a></p><p>到这里证书安装以及自动更新就介绍完毕了，当然我们的站点中有很多静态资源或超链接的地方，在启用https后可能也要进行一轮的检查由http修改为https，主要就是那些hard code的地方需要找出来进行修改掉。</p><p>为了保护你服务的用户信息安全，我强烈建议开启HTTPS，只要是个站点服务就应该开启HTTPS这才是负责任的的体现，Keep Real。</p>]]></content>
    
    <summary type="html">
    
      如何免费的让你的网站变得更加安全 - HTTPS
    
    </summary>
    
      <category term="https" scheme="https://ningyu1.github.io/categories/https/"/>
    
    
      <category term="https" scheme="https://ningyu1.github.io/tags/https/"/>
    
      <category term="lets-encrypt" scheme="https://ningyu1.github.io/tags/lets-encrypt/"/>
    
  </entry>
  
  <entry>
    <title>Mysql数据库字符集utf8mb4使用问题</title>
    <link href="https://ningyu1.github.io/20180514/81-mysql-utf8mb4.html"/>
    <id>https://ningyu1.github.io/20180514/81-mysql-utf8mb4.html</id>
    <published>2018-05-14T06:38:00.000Z</published>
    <updated>2024-04-23T01:48:15.038Z</updated>
    
    <content type="html"><![CDATA[<p>问题发生在<figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/mysql-utf8mf4/3.png" alt title>                </div>                <div class="image-caption"></div>            </figure>这个字上，首先先让我们看这个字的字符信息</p><h1 id="utf8字符集信息"><a href="#utf8字符集信息" class="headerlink" title="utf8字符集信息"></a>utf8字符集信息</h1><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/mysql-utf8mf4/3.png" alt title>                </div>                <div class="image-caption"></div>            </figure> Utf-8 Character<br><br><strong>Symbol information table</strong><br><br>|Name: |Utf-8 Character|<br>|:–:|:–:|<br>|Unicode Subset:|<a href="https://www.utf8icons.com/subsets/cjk-extension-b" target="_blank" rel="noopener">CJK Extension B</a>|<br>|Unicode HEX:|U+20046|<br>|ASCII value:|131142|<br>|HTML:|&#131142;|<br>|CSS:|\20046|<br><br>它属于utf8的字符集，具体可参考：<a href="https://www.utf8icons.com/character/131142/utf-8-character" target="_blank" rel="noopener">传送门</a><br><br>既然属于utf8的字符集那为什么数据库保存这个字会出现非法字符的错误呢？错误如下：<br><br><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">### Cause: java.sql.SQLException: Incorrect string value: &apos;\xF0\xA5\x8A\x8D&apos; for column &apos;DESCRIPTION&apos; at row 1</span><br><span class="line">; uncategorized SQLException for SQL []; SQL state [HY000]; error code [1366]; Incorrect string value: &apos;\xF0\xA5\x8A\x8D&apos; for column &apos;DESCRIPTION&apos; at row 1; nested exception is java.sql.SQLException: Incorrect string value: &apos;\xF0\xA5\x8A\x8D&apos; for column &apos;DESCRIPTION&apos; at row 1</span><br><span class="line">    at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:84)</span><br><span class="line">    at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:81)</span><br><span class="line">    at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:81)</span><br><span class="line">    at org.mybatis.spring.MyBatisExceptionTranslator.translateExceptionIfPossible(MyBatisExceptionTranslator.java:73)</span><br></pre></td></tr></table></figure><p>让我们先了解一下utf8的编码，UTF-8编码是U+2528D，属于CJK Unified Ideographs Extension B（中日韩统一表意文字扩充B）字符集的字符，处于第二辅助平面（SIP，表意文字补充平面），不属于我们通常所见的基本多文种平面（BMP，即Unicode编码范围在0000-FFFF之内）的字符。保存一个字；相比之下，在BMP范围之内的字符只需要占用3 Bytes。仅仅就因为字符保存位数不同，就让程序开发出现了难题。</p><p>来自wikipedia的Unicode字符平面映射</p><p>目前的<a href="https://zh.wikipedia.org/wiki/Unicode" target="_blank" rel="noopener">Unicode</a>字符分为17组编排，每组称为平面（Plane），而每平面拥有65536（即216）个代码点。然而目前只用了少数平面。</p><table><thead><tr><th style="text-align:center">平面</th><th style="text-align:center">始末字符值</th><th style="text-align:center">中文名称</th><th style="text-align:center">英文名称</th></tr></thead><tbody><tr><td style="text-align:center">0号平面</td><td style="text-align:center">U+0000 - U+FFFF</td><td style="text-align:center"><strong>基本多文种平面</strong></td><td style="text-align:center">Basic Multilingual Plane，简称<strong>BMP</strong></td></tr><tr><td style="text-align:center">1号平面</td><td style="text-align:center">U+10000 - U+1FFFF</td><td style="text-align:center"><strong>多文种补充平面</strong></td><td style="text-align:center">Supplementary Multilingual Plane，简称<strong>SMP</strong></td></tr><tr><td style="text-align:center">2号平面</td><td style="text-align:center">U+20000 - U+2FFFF</td><td style="text-align:center"><strong>表意文字补充平面</strong></td><td style="text-align:center">Supplementary Ideographic Plane，简称<strong>SIP</strong></td></tr><tr><td style="text-align:center">3号平面</td><td style="text-align:center">U+30000 - U+3FFFF</td><td style="text-align:center"><strong>表意文字第三平面</strong></td><td style="text-align:center">（未正式使用[1]）    Tertiary Ideographic Plane，简称<strong>TIP</strong></td></tr><tr><td style="text-align:center">4号平面 至 13号平面</td><td style="text-align:center">U+40000 - U+DFFFF</td><td style="text-align:center">（尚未使用）</td></tr><tr><td style="text-align:center">14号平面</td><td style="text-align:center">U+E0000 - U+EFFFF</td><td style="text-align:center"><strong>特别用途补充平面</strong></td><td style="text-align:center">Supplementary Special-purpose Plane，简称<strong>SSP</strong></td></tr><tr><td style="text-align:center">15号平面</td><td style="text-align:center">U+F0000 - U+FFFFF</td><td style="text-align:center">保留作为<strong>私人使用区（A区）</strong><a href="https://zh.wikipedia.org/wiki/Unicode%E5%AD%97%E7%AC%A6%E5%B9%B3%E9%9D%A2%E6%98%A0%E5%B0%84#cite_note-PUA-2" target="_blank" rel="noopener">[2]</a></td><td style="text-align:center">Private Use Area-A，简称<strong>PUA-A</strong></td></tr><tr><td style="text-align:center">16号平面</td><td style="text-align:center">U+100000 - U+10FFFF</td><td style="text-align:center">保留作为<strong>私人使用区（B区）</strong><a href="https://zh.wikipedia.org/wiki/Unicode%E5%AD%97%E7%AC%A6%E5%B9%B3%E9%9D%A2%E6%98%A0%E5%B0%84#cite_note-PUA-2" target="_blank" rel="noopener">[2]</a></td><td style="text-align:center">Private Use Area-B，简称<strong>PUA-B</strong></td></tr></tbody></table><p>具体可查看：<a href="https://zh.wikipedia.org/wiki/Unicode%E5%AD%97%E7%AC%A6%E5%B9%B3%E9%9D%A2%E6%98%A0%E5%B0%84" target="_blank" rel="noopener">传送门</a></p><p>究其原因就是这个字保存需要占4 Bytes的字节，mysql在<span style="color:red"><strong>MySQL 5.6+</strong></span>版本之后支持4Bytes字节（utf8mb4）的存储，首先先看一下mysql编码如何设置？</p><h1 id="数据库字符集设置"><a href="#数据库字符集设置" class="headerlink" title="数据库字符集设置"></a>数据库字符集设置</h1><p>建库语句中需要指定字符集编码为：utf8mb4</p><p>建库语句中需要指定字符集校对规则为：utf8mb4_general_ci</p><p>建表语句中需要指定字符集编码为：utf8mb4</p><p>表字段需要指定字符集编码为：utf8mb4</p><p>表字段collation需要指定字符集校验规则：utf8mb4_general_ci</p><p>如下图：</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/mysql-utf8mf4/1.png" alt title>                </div>                <div class="image-caption"></div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/mysql-utf8mf4/2.png" alt title>                </div>                <div class="image-caption"></div>            </figure><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">CREATE TABLE `t_application` (</span><br><span class="line">   `ID` bigint(20) NOT NULL AUTO_INCREMENT COMMENT &apos;ID&apos;,</span><br><span class="line">   `APP_ID` bigint(20) NOT NULL COMMENT &apos;应用编号&apos;,</span><br><span class="line">   `NAME` varchar(100) NOT NULL COMMENT &apos;应用名称&apos;,</span><br><span class="line">   `DESCRIPTION` varchar(200) DEFAULT NULL COMMENT &apos;应用描述&apos;,</span><br><span class="line">   PRIMARY KEY (`ID`)</span><br><span class="line"> ) ENGINE=InnoDB AUTO_INCREMENT=31 DEFAULT CHARSET=utf8mb4 CHECKSUM=1 DELAY_KEY_WRITE=1 ROW_FORMAT=DYNAMIC</span><br></pre></td></tr></table></figure><h1 id="utf8mb4不生效问题分析"><a href="#utf8mb4不生效问题分析" class="headerlink" title="utf8mb4不生效问题分析"></a>utf8mb4不生效问题分析</h1><p>完成上面的字符集设置之后我们使用程序保存<figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/mysql-utf8mf4/3.png" alt title>                </div>                <div class="image-caption"></div>            </figure>字符来进行测试，还是提示非法的字符集错误，那这又是为什么呢？</p><p>让我们来看回数据库charset设置，<code>SHOW VARIABLES LIKE &#39;CHARACTER%&#39;</code>，关注character_set_server这个字符设置</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/mysql-utf8mf4/4.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p>character_set_server在默认情况下为latin1</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">/*</span><br><span class="line">character_set_server: 服务器安装时指定的默认字符集设定。</span><br><span class="line">character_set_database: 数据库服务器中某个库使用的字符集设定，如果建库时没有指明，将使用服务器安装时指定的字符集设置。</span><br><span class="line">  </span><br><span class="line">建表时候，字段字符集的选取方式如下：</span><br><span class="line"> </span><br><span class="line">1. * if 字段指定的字符集</span><br><span class="line">2. * else if 表指定的字符集</span><br><span class="line">3. * else if @@character_set_database</span><br><span class="line">4. * else @@character_set_server (如果没有设定，这个值为latin1)</span><br><span class="line">  </span><br><span class="line">*/</span><br></pre></td></tr></table></figure><p>按照上面的说法，如果character_set_server和character_set_database变量的值不同，则新建数据库的字符集以character_set_server为准，而不是按照character_set_database。</p><p><span style="color:red"><strong>我的理解是character_set_database指定了utf8mb4后目标库应该按照设置的字符集格式走才对，只有没有设置的库才会走默认的，但是现在测试下来，character_set_server必须修改为utf8mb4,否则保存这种字符依然提示非法字符集，不太清楚具体什么原因。</strong></span></p><p>如果数据库按照上面设置了以后还是无法保存的话，应该就是mysql驱动的问题和数据库连接串字符集的问题。下面让我们看一下从mysql server到mysql驱动到数据源再到应用所有的utf8mb4设置。</p><h1 id="完整的正确设置"><a href="#完整的正确设置" class="headerlink" title="完整的正确设置"></a>完整的正确设置</h1><h2 id="Mysql服务端配置"><a href="#Mysql服务端配置" class="headerlink" title="Mysql服务端配置"></a>Mysql服务端配置</h2><p>建库语句中需要指定字符集编码为：utf8mb4</p><p>建库语句中需要指定字符集校对规则为：utf8mb4_general_ci</p><p>建表语句中需要指定字符集编码为：utf8mb4</p><p>表字段需要指定字符集编码为：utf8mb4</p><p>表字段collation需要指定字符集校验规则：utf8mb4_general_ci</p><p>mysql ini配置文件指定character_set_server</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">[client]</span><br><span class="line">default-character-set=utf8mb4</span><br><span class="line">[mysql]</span><br><span class="line">default-character-set=utf8mb4</span><br><span class="line">[mysqld]</span><br><span class="line">character-set-server=utf8mb4</span><br><span class="line">collation-server=utf8mb4_general_ci</span><br></pre></td></tr></table></figure><p><span style="color:red"><strong>ps. character_set_server设置成utf8或使用默认latin1，在直接使用sql插入特殊字符和emoji表情时数据库显示？？？？？乱码，使用程序（mybatis）插入特殊字符和emoji表情时报错：java.sql.SQLException: Incorrect string value: ‘\xF0\xA5\x8A\x8D’ for column ‘某某列’ at row 1</strong></span></p><h2 id="java连接mysql驱动版本"><a href="#java连接mysql驱动版本" class="headerlink" title="java连接mysql驱动版本"></a>java连接mysql驱动版本</h2><p>mysql-connector-java版本在5.1.13+才支持utf8mb4，因此在选用连接驱动时应注意这个问题，我们使用的驱动是mysql-connector-java-5.1.41。</p><h2 id="数据源配置"><a href="#数据源配置" class="headerlink" title="数据源配置"></a>数据源配置</h2><p>我们使用的druid数据源配置上需要加上<code>connectionInitSqls</code>属性配置，具体如下：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">&lt;bean id=&quot;dataSource&quot; class=&quot;com.alibaba.druid.pool.DruidDataSource&quot;</span><br><span class="line">        destroy-method=&quot;close&quot;&gt;</span><br><span class="line">    ....此处省略其他属性.....</span><br><span class="line">    &lt;property name=&quot;connectionInitSqls&quot; value=&quot;set names utf8mb4;&quot; /&gt;</span><br><span class="line">&lt;/bean&gt;</span><br></pre></td></tr></table></figure><p><span style="color:red"><strong>ps.但是我实际测试下来，这个参数不加也没有什么问题，可以正常保存这种特殊字符和emoji表情，官方建议配置。</strong></span></p><h2 id="数据库连接串配置"><a href="#数据库连接串配置" class="headerlink" title="数据库连接串配置"></a>数据库连接串配置</h2><p>我们的配置如下：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">jdbc.url=jdbc:mysql://ip:port/databaseName?useUnicode=true&amp;characterEncoding=UTF-8&amp;noAccessToProcedureBodies=true&amp;autoReconnect=true</span><br></pre></td></tr></table></figure><p>建议去掉useUnicode和characterEncoding的配置，使用如下配置：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">jdbc.url=jdbc:mysql://ip:port/databaseName?noAccessToProcedureBodies=true&amp;autoReconnect=true</span><br></pre></td></tr></table></figure><p><span style="color:red"><strong>ps.但是我实际测试下来，连接串上增加useUnicode=true&amp;characterEncoding=utf8也没有什么问题，可以正常保存这种特殊字符和emoji表情。</strong></span></p><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p><span><strong>最主要的是mysql服务端的character_set_server需要和character_set_database保持一致修改为utf8mb4，因此我们先不考虑数据库连接串和druid数据源配置修改，但是mysql-connector-java版本必须使用5.1.13+</strong></span></p>]]></content>
    
    <summary type="html">
    
      Mysql数据库字符集utf8mb4使用问题
    
    </summary>
    
      <category term="trouble shooting" scheme="https://ningyu1.github.io/categories/trouble-shooting/"/>
    
    
      <category term="mysql" scheme="https://ningyu1.github.io/tags/mysql/"/>
    
      <category term="utf8mb4" scheme="https://ningyu1.github.io/tags/utf8mb4/"/>
    
  </entry>
  
  <entry>
    <title>ActiveMQ消息消费慢问题排查</title>
    <link href="https://ningyu1.github.io/20180509/80-activemq-consumer-slow-speed.html"/>
    <id>https://ningyu1.github.io/20180509/80-activemq-consumer-slow-speed.html</id>
    <published>2018-05-09T07:38:00.000Z</published>
    <updated>2024-04-23T01:48:15.037Z</updated>
    
    <content type="html"><![CDATA[<h1 id="问题现象"><a href="#问题现象" class="headerlink" title="问题现象"></a>问题现象</h1><p>有的时候会发现<code>ActiveMQ</code>中某个个队列的消息在写入后，不是立刻就被调度消费，而是需要等待一小会才能被调度消费（大概时间是1分钟），而且还伴随着这样的现象，当消息写入速度很快时消费很快，当消息写入消息速度很慢时反而消费很慢，我们的理解就是当写入慢的时候很多消费者都是闲置的那为什么消费反而会变慢？</p><h1 id="问题原因"><a href="#问题原因" class="headerlink" title="问题原因"></a>问题原因</h1><p>跟了一下代码发现了跟我们的设置有很大关系，因为我们设置的<code>receiveTimeout=6000</code>（1分钟）接受阻塞时间为1分钟。</p><p><code>ActiveMQ</code>在消费时每个<code>consumer</code>会独占一个<code>Thread</code>，<code>Thead</code>中通过<code>consumer.receive()</code>去阻塞，只有当<code>consumer</code>消费了<code>maxMessagesPerTask</code>个消息后，才会退出线程，由<code>taskExecutor</code>重新调度，<code>maxMessagesPerTask</code>这个值默认为10，可以通过下面代码得知：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">@Override</span><br><span class="line">public void initialize() &#123;</span><br><span class="line">    // Adapt default cache level.</span><br><span class="line">    if (this.cacheLevel == CACHE_AUTO) &#123;</span><br><span class="line">        this.cacheLevel = (getTransactionManager() != null ? CACHE_NONE : CACHE_CONSUMER);</span><br><span class="line">    &#125;</span><br><span class="line">    // Prepare taskExecutor and maxMessagesPerTask.</span><br><span class="line">    synchronized (this.lifecycleMonitor) &#123;</span><br><span class="line">        if (this.taskExecutor == null) &#123;</span><br><span class="line">            this.taskExecutor = createDefaultTaskExecutor();</span><br><span class="line">        &#125;</span><br><span class="line">        else if (this.taskExecutor instanceof SchedulingTaskExecutor &amp;&amp;</span><br><span class="line">                ((SchedulingTaskExecutor) this.taskExecutor).prefersShortLivedTasks() &amp;&amp;</span><br><span class="line">                this.maxMessagesPerTask == Integer.MIN_VALUE) &#123;</span><br><span class="line">            // TaskExecutor indicated a preference for short-lived tasks. According to</span><br><span class="line">            // setMaxMessagesPerTask javadoc, we&apos;ll use 10 message per task in this case</span><br><span class="line">            // unless the user specified a custom value.</span><br><span class="line">            this.maxMessagesPerTask = 10;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    // Proceed with actual listener initialization.</span><br><span class="line">    super.initialize();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><span style="color:red"><strong>ps. 我们使用的taskExecutor为：org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor，因此上面代码走到else if中设置this.maxMessagesPerTask = 10;</strong></span></p><p><span style="color:blue"><strong>如果消息写入很快的时候，你会发现消费的很快，只有当消息写入很慢的时候（比如说：1分钟写入不到10条）的时候，才会发现消息消费的有些慢</strong></span></p><h1 id="解决方案"><a href="#解决方案" class="headerlink" title="解决方案"></a>解决方案</h1><p>如果有这类情况，可以调整<code>receiveTime</code>这个参数，具体参数设置多少合理自己去结合业务场景去权衡，可以根据消息写入的速度和写入量来设置该参数（<strong><code>maxMessagesPerTask</code></strong> 和 <strong><code>receiveTimeout</code></strong>），调整这个参数有两种方式：</p><h2 id="第一种使用JMS消费消息："><a href="#第一种使用JMS消费消息：" class="headerlink" title="第一种使用JMS消费消息："></a>第一种使用JMS消费消息：</h2><p>使用JMS消费消息时调整：<code>jmsTemplate</code>的<code>receiveTimeout</code>参数（以毫秒为单位，0表示阻塞接收不超时，默认值为0毫秒表示阻塞接受没有超时）</p><h2 id="第二种使用listener-container消费消息："><a href="#第二种使用listener-container消费消息：" class="headerlink" title="第二种使用listener-container消费消息："></a>第二种使用listener-container消费消息：</h2><p>使用<code>jms:listener-container</code>消费消息时调整：<code>receive-timeout</code>参数（以毫秒为单位， 默认值为1000毫秒（1秒）; -1指示器根本没有超时。）</p>]]></content>
    
    <summary type="html">
    
      ActiveMQ消息消费慢问题排查
    
    </summary>
    
      <category term="trouble shooting" scheme="https://ningyu1.github.io/categories/trouble-shooting/"/>
    
    
      <category term="activemq" scheme="https://ningyu1.github.io/tags/activemq/"/>
    
      <category term="activemq slow speed" scheme="https://ningyu1.github.io/tags/activemq-slow-speed/"/>
    
  </entry>
  
  <entry>
    <title>Trouble Shooting —— Docker Pull Image : Filesystem layer verification failed for digest sha256错误</title>
    <link href="https://ningyu1.github.io/20180427/79-docker-registry-pull-filesystem-layer.html"/>
    <id>https://ningyu1.github.io/20180427/79-docker-registry-pull-filesystem-layer.html</id>
    <published>2018-04-27T09:46:00.000Z</published>
    <updated>2024-04-23T01:48:15.037Z</updated>
    
    <content type="html"><![CDATA[<h1 id="问题现象"><a href="#问题现象" class="headerlink" title="问题现象"></a>问题现象</h1><p>除了打包镜像的服务器上可以执行<code>docker pull 192.168.0.34:5000/sample:latest</code>以外，其它任何服务器执行此命令时，都会出现以下错误信息：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">8b7054...: Verifying Checksum</span><br><span class="line">Filesystem layer verification failed for digest sha256: 8b7054.....</span><br></pre></td></tr></table></figure><p>这使得无法正常使用最新的<code>sample</code>镜像文件。</p><p>如果是按分析过程中的方式把<code>8b7054</code>文件夹迁移的话，<code>docker</code>会不断重试去拉取此文件信息，大概结果如下：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">8b7054...: (..Retry 10 seconds)</span><br><span class="line">Filesystem layer verification failed for digest sha256: 8b7054.....</span><br></pre></td></tr></table></figure><h1 id="分析过程"><a href="#分析过程" class="headerlink" title="分析过程"></a>分析过程</h1><p>尝试在服务器上找日志，结果没有可用的日志。</p><p>在<code>/var/lib/registry</code>下找该<code>sha256</code>的数据，能够找到，尝试移走该文件夹数据。结果执行<code>docker pull</code>命令时，依旧是报错。只好迁移回文件夹。</p><p>尝试在网络上寻找解决方案，有的说与源有关系，有的说与<code>docker</code>版本有关系，需要升级版本，大多都没有很好的解决。如果实在搞不定，估计</p><p>需要考虑这些方案了。</p><p>尝试删除所有<code>sample</code>开发版本相关的<code>image</code>，并重新打包镜像，结果问题依旧。</p><h1 id="解决方案"><a href="#解决方案" class="headerlink" title="解决方案"></a>解决方案</h1><p><code>docker build</code>的过程中有很多选项可以使用，尝试将缓存关闭（默认否）、签名关闭（默认否）、清理过程文件（默认是）。</p><p>因此切换到<code>jenkins</code>的<code>workspace</code>下，找到<code>sample</code>文件夹，执行以下命令:</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">docker build --rm=true --no-cache --disable-content-trust=true -t sample .</span><br><span class="line">docker tag sample 192.168.0.34:5000/sample</span><br><span class="line">docker push --disable-content-trust=true 192.168.0.34:5000/sample</span><br></pre></td></tr></table></figure><p>编译打包过程没有任何错误，可以正常发布镜像到<code>registry</code>上。</p><p>于是，切换到其他服务器上去执行<code>docker pull</code>，结果一切正常。</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/docker-registry/8.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p>没有<code>checksum</code>？ 且没有原来失败的<code>sha256 digest</code>。</p><p>看了下其他镜像成功过的pull日志，也是没有<code>checksum</code>。看来只有出现异常的时候，才会去<code>checksum</code>（待考证）</p><p>既然已经成功过，那还是用正常的方式去打包编译及下载。于是删除现有镜像文件，在<code>jenkins</code>上进行工程打包（原始逻辑）。</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">docker build -t sample:latest .</span><br><span class="line">docker tag sample:latest 192.168.0.34:5000/sample:latest</span><br><span class="line">docker push 192.168.0.34:5000/sample:latest</span><br></pre></td></tr></table></figure><p>打包好后，在其它服务器上执行<code>docker pull</code>，一样可以正常使用了。</p><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>问题最终通过<code>docker</code>创建镜像时增加了关闭缓存、关闭校验的参数（<code>--rm=true --no-cache --disable-content-trust=true</code>），然后构建出来的镜像<code>push</code>到<code>registry</code>去重写这个镜像的最新<code>digest</code>值，再重新去掉这些参数再次构建镜像（恢复成正常构建镜像命令）后重新<code>push</code>到<code>registry</code>。这个问题看样子是由于新构建的镜像无法修改<code>registry</code>上的<code>digest</code>值导致<code>pull</code>的时候报错。</p>]]></content>
    
    <summary type="html">
    
      Trouble Shooting —— Docker Pull Image : Filesystem layer verification failed for digest sha256错误
    
    </summary>
    
      <category term="docker" scheme="https://ningyu1.github.io/categories/docker/"/>
    
    
      <category term="docker" scheme="https://ningyu1.github.io/tags/docker/"/>
    
      <category term="docker pull" scheme="https://ningyu1.github.io/tags/docker-pull/"/>
    
      <category term="Filesystem layer verification failed for digest sha256" scheme="https://ningyu1.github.io/tags/Filesystem-layer-verification-failed-for-digest-sha256/"/>
    
  </entry>
  
  <entry>
    <title>Dubbo使用jsr303框架hibernate-validator遇到的问题</title>
    <link href="https://ningyu1.github.io/20180423/78-dubbo-validation-jsr303-pit.html"/>
    <id>https://ningyu1.github.io/20180423/78-dubbo-validation-jsr303-pit.html</id>
    <published>2018-04-23T05:25:00.000Z</published>
    <updated>2024-04-23T01:48:15.037Z</updated>
    
    <content type="html"><![CDATA[<p><code>Dubbo</code>可以集成jsr303标准规范的验证框架，作为验证框架不二人选的<code>hibernate-validator</code>是大家都会经常在项目中使用的，但是在<code>Dubbo</code>使用是会发生下面这个问题。</p><h1 id="问题描述"><a href="#问题描述" class="headerlink" title="问题描述"></a>问题描述</h1><p>背景：使用<code>springmvc</code>做<code>restful</code>，使用<code>dubbo</code>做rpc，<code>restful</code>中调用大量的rpc，数据验证会在这两个地方，一个是<code>restful</code>层面，一个是rpc层面，<code>restful</code>层面使用<code>springmvc</code>默认的集成<code>hibernate-validator</code>来实现，参数开启验证只需要加入<code>@Validated param</code>。</p><p>rpc层面也使用<code>hibernate-validator</code>实现，dubbo中开启validation也有两个方式，一个是在<code>consumer</code>端，一个是在<code>provider</code>端。</p><h2 id="当我们在consumer端开启验证时"><a href="#当我们在consumer端开启验证时" class="headerlink" title="当我们在consumer端开启验证时:"></a>当我们在consumer端开启验证时:</h2><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">&lt;dubbo:reference id=&quot;serviceName&quot; interface=&quot;com.domain.package.TestService&quot; registry=&quot;registry&quot; validation=&quot;true&quot;/&gt;</span><br></pre></td></tr></table></figure><p>没有任何问题，可以拿到所有的数据校验失败数据。</p><h2 id="当我们在provider端开启验证时："><a href="#当我们在provider端开启验证时：" class="headerlink" title="当我们在provider端开启验证时："></a>当我们在provider端开启验证时：</h2><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">&lt;dubbo:service interface=&quot;com.domain.package.TestService&quot; ref=&quot;serviceName&quot; validation=&quot;true&quot; /&gt;</span><br></pre></td></tr></table></figure><p>会发生如下异常：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">com.alibaba.dubbo.rpc.RpcException: Failed to invoke remote method: sayHello, provider: </span><br><span class="line"></span><br><span class="line">dubbo://127.0.0.1:20831/com.domain.package.TestService?application=dubbo-test-</span><br><span class="line"></span><br><span class="line">rest&amp;default.check=false&amp;default.cluster=failfast&amp;default.retries=0&amp;default.timeout=1200000&amp;default.version=1.0</span><br><span class="line"></span><br><span class="line">.0&amp;dubbo=2.6.1&amp;interface=com.domain.package.TestService&amp;methods=sayHello&amp;pid=29268&amp;register.ip=192.</span><br><span class="line"></span><br><span class="line">168.6.47&amp;side=consumer&amp;timestamp=1524453157718, cause: com.alibaba.com.caucho.hessian.io.HessianFieldException: </span><br><span class="line"></span><br><span class="line">org.hibernate.validator.internal.engine.ConstraintViolationImpl.constraintDescriptor: </span><br><span class="line"></span><br><span class="line">&apos;org.hibernate.validator.internal.metadata.descriptor.ConstraintDescriptorImpl&apos; could not be instantiated</span><br><span class="line">com.alibaba.com.caucho.hessian.io.HessianFieldException: </span><br><span class="line"></span><br><span class="line">org.hibernate.validator.internal.engine.ConstraintViolationImpl.constraintDescriptor: </span><br><span class="line"></span><br><span class="line">&apos;org.hibernate.validator.internal.metadata.descriptor.ConstraintDescriptorImpl&apos; could not be instantiated</span><br><span class="line">at com.alibaba.com.caucho.hessian.io.JavaDeserializer.logDeserializeError(JavaDeserializer.java:167)</span><br><span class="line">at com.alibaba.com.caucho.hessian.io.JavaDeserializer$ObjectFieldDeserializer.deserialize</span><br><span class="line"></span><br><span class="line">(JavaDeserializer.java:408)</span><br><span class="line">at com.alibaba.com.caucho.hessian.io.JavaDeserializer.readObject(JavaDeserializer.java:273)</span><br><span class="line">at com.alibaba.com.caucho.hessian.io.JavaDeserializer.readObject(JavaDeserializer.java:200)</span><br><span class="line">at com.alibaba.com.caucho.hessian.io.SerializerFactory.readObject(SerializerFactory.java:525)</span><br><span class="line">at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObjectInstance(Hessian2Input.java:2791)</span><br><span class="line">at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2731)</span><br><span class="line">at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2260)</span><br><span class="line">at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2705)</span><br><span class="line">at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2260)</span><br><span class="line">at com.alibaba.com.caucho.hessian.io.CollectionDeserializer.readLengthList</span><br><span class="line"></span><br><span class="line">(CollectionDeserializer.java:119)</span><br><span class="line">at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2186)</span><br><span class="line">at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2057)</span><br><span class="line">at com.alibaba.com.caucho.hessian.io.JavaDeserializer$ObjectFieldDeserializer.deserialize</span><br><span class="line"></span><br><span class="line">(JavaDeserializer.java:404)</span><br><span class="line">at com.alibaba.com.caucho.hessian.io.JavaDeserializer.readObject(JavaDeserializer.java:273)</span><br><span class="line">at com.alibaba.com.caucho.hessian.io.JavaDeserializer.readObject(JavaDeserializer.java:200)</span><br><span class="line">at com.alibaba.com.caucho.hessian.io.SerializerFactory.readObject(SerializerFactory.java:525)</span><br><span class="line">at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObjectInstance(Hessian2Input.java:2791)</span><br><span class="line">at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2731)</span><br><span class="line">at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2260)</span><br><span class="line">at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2705)</span><br><span class="line">at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2260)</span><br><span class="line">at com.alibaba.dubbo.common.serialize.hessian2.Hessian2ObjectInput.readObject</span><br><span class="line"></span><br><span class="line">(Hessian2ObjectInput.java:74)</span><br><span class="line">at com.alibaba.dubbo.rpc.protocol.dubbo.DecodeableRpcResult.decode(DecodeableRpcResult.java:90)</span><br><span class="line">at com.alibaba.dubbo.rpc.protocol.dubbo.DecodeableRpcResult.decode(DecodeableRpcResult.java:110)</span><br><span class="line">at com.alibaba.dubbo.rpc.protocol.dubbo.DubboCodec.decodeBody(DubboCodec.java:88)</span><br><span class="line">at com.alibaba.dubbo.remoting.exchange.codec.ExchangeCodec.decode(ExchangeCodec.java:121)</span><br><span class="line">at com.alibaba.dubbo.remoting.exchange.codec.ExchangeCodec.decode(ExchangeCodec.java:82)</span><br><span class="line">at com.alibaba.dubbo.rpc.protocol.dubbo.DubboCountCodec.decode(DubboCountCodec.java:44)</span><br><span class="line">at com.alibaba.dubbo.remoting.transport.netty.NettyCodecAdapter$InternalDecoder.messageReceived</span><br><span class="line"></span><br><span class="line">(NettyCodecAdapter.java:133)</span><br><span class="line">at org.jboss.netty.channel.SimpleChannelUpstreamHandler.handleUpstream</span><br><span class="line"></span><br><span class="line">(SimpleChannelUpstreamHandler.java:70)</span><br><span class="line">at org.jboss.netty.channel.DefaultChannelPipeline.sendUpstream(DefaultChannelPipeline.java:564)</span><br><span class="line">at org.jboss.netty.channel.DefaultChannelPipeline.sendUpstream(DefaultChannelPipeline.java:559)</span><br><span class="line">at org.jboss.netty.channel.Channels.fireMessageReceived(Channels.java:268)</span><br><span class="line">at org.jboss.netty.channel.Channels.fireMessageReceived(Channels.java:255)</span><br><span class="line">at org.jboss.netty.channel.socket.nio.NioWorker.read(NioWorker.java:88)</span><br><span class="line">at org.jboss.netty.channel.socket.nio.AbstractNioWorker.process(AbstractNioWorker.java:109)</span><br><span class="line">at org.jboss.netty.channel.socket.nio.AbstractNioSelector.run(AbstractNioSelector.java:312)</span><br><span class="line">at org.jboss.netty.channel.socket.nio.AbstractNioWorker.run(AbstractNioWorker.java:90)</span><br><span class="line">at org.jboss.netty.channel.socket.nio.NioWorker.run(NioWorker.java:178)</span><br><span class="line">at org.jboss.netty.util.ThreadRenamingRunnable.run(ThreadRenamingRunnable.java:108)</span><br><span class="line">at org.jboss.netty.util.internal.DeadLockProofWorker$1.run(DeadLockProofWorker.java:42)</span><br><span class="line">at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)</span><br><span class="line">at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)</span><br><span class="line">at java.lang.Thread.run(Thread.java:744)</span><br><span class="line">Caused by: com.alibaba.com.caucho.hessian.io.HessianProtocolException: </span><br><span class="line"></span><br><span class="line">&apos;org.hibernate.validator.internal.metadata.descriptor.ConstraintDescriptorImpl&apos; could not be instantiated</span><br><span class="line">at com.alibaba.com.caucho.hessian.io.JavaDeserializer.instantiate(JavaDeserializer.java:313)</span><br><span class="line">at com.alibaba.com.caucho.hessian.io.JavaDeserializer.readObject(JavaDeserializer.java:198)</span><br><span class="line">at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObjectInstance(Hessian2Input.java:2789)</span><br><span class="line">at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2128)</span><br><span class="line">at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2057)</span><br><span class="line">at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2101)</span><br><span class="line">at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2057)</span><br><span class="line">at com.alibaba.com.caucho.hessian.io.JavaDeserializer$ObjectFieldDeserializer.deserialize</span><br><span class="line"></span><br><span class="line">(JavaDeserializer.java:404)</span><br><span class="line">... 43 more</span><br><span class="line">Caused by: java.lang.reflect.InvocationTargetException</span><br><span class="line">at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)</span><br><span class="line">at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:57)</span><br><span class="line">at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)</span><br><span class="line">at java.lang.reflect.Constructor.newInstance(Constructor.java:526)</span><br><span class="line">at com.alibaba.com.caucho.hessian.io.JavaDeserializer.instantiate(JavaDeserializer.java:309)</span><br><span class="line">... 50 more</span><br><span class="line">Caused by: java.lang.NullPointerException</span><br><span class="line">at org.hibernate.validator.internal.metadata.descriptor.ConstraintDescriptorImpl.&lt;init&gt;</span><br><span class="line"></span><br><span class="line">(ConstraintDescriptorImpl.java:158)</span><br><span class="line">at org.hibernate.validator.internal.metadata.descriptor.ConstraintDescriptorImpl.&lt;init&gt;</span><br><span class="line"></span><br><span class="line">(ConstraintDescriptorImpl.java:211)</span><br><span class="line">... 55 more</span><br></pre></td></tr></table></figure><h1 id="问题分析"><a href="#问题分析" class="headerlink" title="问题分析"></a>问题分析</h1><p>上面的问题从异常面来看已经很直观了，<code>&#39;org.hibernate.validator.internal.metadata.descriptor.ConstraintDescriptorImpl&#39; could not be instantiated</code>，这个类无法实例化，那是什么原因导致它无法实例化呢？</p><p><code>Dubbo</code>的序列化协议，默认是<code>hessian</code>，如果没有进行其他协议配置的话，默认使用的就是<code>hessian</code>，<code>hessian</code>在反序列化时有个特点需要注意一下，它会在反序列化时取<span style="color:red"><strong>参数最少的构造器来创建对象</strong></span>，有的时候会有很多重载的构造器，因此会有一些参数直接给<code>null</code>，因此可能就会造成一些莫名其妙的问题，就像我们这个问题一样。</p><p>那这个问题如何解决呢？接着往下看</p><h1 id="解决方案"><a href="#解决方案" class="headerlink" title="解决方案"></a>解决方案</h1><p>由于这个是<code>Hessian</code>反序列化问题，因此与<code>Dubbo</code>的版本关系不大，为了验证这个我还专门使用<code>apache dubbo</code> <code>2.6.1</code>版本测试了一下，问题依旧存在。</p><h2 id="方法一：使用无参构造方法来创建对象"><a href="#方法一：使用无参构造方法来创建对象" class="headerlink" title="方法一：使用无参构造方法来创建对象"></a>方法一：使用无参构造方法来创建对象</h2><p>既然是<code>hessian</code>反序列化问题，而且它在反序列化时根据构造函数参数个数优先级来取参数最少的，那我们就可以增加一个<span style="color:red"><strong>无参的构造方法</strong></span>来解决这个问题。</p><p>但是有的时候我们使用的是第三方的包，不太好增加无参的构造方法，那怎么办的，我们能不能使用其他方法，继续往下看。</p><h2 id="方法二：替换jsr303实现框架"><a href="#方法二：替换jsr303实现框架" class="headerlink" title="方法二：替换jsr303实现框架"></a>方法二：替换jsr303实现框架</h2><p>既然<code>hibernate-validator</code>的<code>org.hibernate.validator.internal.metadata.descriptor.ConstraintDescriptorImpl</code>这个类在使用hessian反序列化存在问题，那我们使用其他jsr303的框架来试试。</p><p>jsr303的实现框架有哪些？</p><ul><li>org.hibernate : hibernate-validator : 5.2.4.Final</li><li>org.apache.bval : bval-jsr303 : 0.5</li><li>jersery</li></ul><p>bval是apache的一个bean validator的实现，jersery是一个restful的框架为了满足自身的数据验证功能因此增加了jsr303的实现。</p><p>由于我们使用的springmvc构建restful因此这里就不考虑jersery，我们就从bval下手来试一试。</p><p>在进行了一番配置后（都有哪些配置？）</p><ul><li>增加bval包，现在版本是：0.5</li></ul><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">&lt;dependency&gt;</span><br><span class="line">&lt;groupId&gt;org.apache.bval&lt;/groupId&gt;</span><br><span class="line">&lt;artifactId&gt;bval-jsr303&lt;/artifactId&gt;</span><br><span class="line">&lt;version&gt;0.5&lt;/version&gt;</span><br><span class="line">&lt;/dependency&gt;</span><br></pre></td></tr></table></figure><ul><li>将bval集成到spring框架中，作为spring的验证框架</li></ul><p>这里有两种方式，一种xml配置，一种java config</p><p>xml方式：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">&lt;mvc:annotation-driven validator=&quot;validator&quot;/&gt;  </span><br><span class="line">  </span><br><span class="line">&lt;!-- 数据验证 Validator bean --&gt;  </span><br><span class="line">&lt;bean id=&quot;validator&quot; class=&quot;org.springframework.validation.beanvalidation.LocalValidatorFactoryBean&quot;&gt;  </span><br><span class="line">    &lt;property name=&quot;providerClass&quot; value=&quot;org.apache.bval.jsr.ApacheValidationProvider&quot; /&gt;  </span><br><span class="line">&lt;/bean&gt;</span><br></pre></td></tr></table></figure><p>java config方式:<br>重写mvcValidator方法</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">@Override</span><br><span class="line">public Validator mvcValidator() &#123;</span><br><span class="line">    Validator validator = super.mvcValidator();</span><br><span class="line">    if (validator instanceof LocalValidatorFactoryBean) &#123;</span><br><span class="line">        LocalValidatorFactoryBean lvfb = (LocalValidatorFactoryBean) validator;</span><br><span class="line">        try &#123;</span><br><span class="line">            String className = &quot;org.apache.bval.jsr303.ApacheValidationProvider&quot;;</span><br><span class="line">            Class&lt;?&gt; clazz = ClassUtils.forName(className, WebMvcConfigurationSupport.class.getClassLoader());</span><br><span class="line">            lvfb.setProviderClass(clazz);</span><br><span class="line">        &#125;</span><br><span class="line">        catch (ClassNotFoundException e) &#123;</span><br><span class="line">            //没有找到bval验证框架，走spring默认整合的验证框架：hibernate-validator</span><br><span class="line">            //这里异常没有必要跑出去，直接吃掉</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    return validator;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>启动后验证功能</p><p>但是不好的事情发生了，无法启动报错，错误如下：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">java.lang.AbstractMethodError: org.apache.bval.jsr303.ConfigurationImpl.getDefaultParameterNameProvider()</span><br><span class="line"></span><br><span class="line">Ljavax/validation/ParameterNameProvider;</span><br></pre></td></tr></table></figure><p>经过对<code>spring</code>的资料查找，发现<code>spring</code>从4.0版本往后不在支持集成其他jsr303的框架了，只能使用<code>hibernate-validator</code>，我擦这个有点暴力了。即使自己实现一个jsr303框架也无法再<code>spring</code>中使用，除非不使用<code>spring</code> <code>validator</code>功能，直接使用自己的验证框架来进行验证，这样就无法使用<code>@Validated param</code>方式。</p><p>那这种方法只能放弃了。</p><h2 id="方法三：修改hibernate-validator的原声类，修改Dubbo-ValidationFilter，这也是我最终采用的方法"><a href="#方法三：修改hibernate-validator的原声类，修改Dubbo-ValidationFilter，这也是我最终采用的方法" class="headerlink" title="方法三：修改hibernate-validator的原声类，修改Dubbo ValidationFilter，这也是我最终采用的方法"></a>方法三：修改hibernate-validator的原声类，修改Dubbo ValidationFilter，这也是我最终采用的方法</h2><p>其实替换jsr303框架不能成功，替换序列化协议应该也可以避免这个问题，只不过替换协议这个一般在维护的项目中不太会选择这样的方式来动刀子，现在开发很多都是分布式服务，序列化反序列化已经无处不在了，因此我建议编写代码时都增加一个无参数的构造方法，养成这样的一个好习惯可以避免很多序列化反序列化框架的坑。而且还有那些有匿名内部类的这种在序列化反序列化也需要注意，不是所有的序列化反序列化框架都支持有匿名类，gson是支持的这个为测试过，我前面也写过一篇博文里面就主要说这个问题，可以查看：<a href="https://ningyu1.github.io/site/post/65-java-inner-class/">《Java中内部类使用注意事项，内部类对序列化与反序列化的影响》</a></p><p>有兴趣的可以看一下我们常用的序列化反序列化类库的一些使用中的注意事项，可以参考这篇文章：<a href="https://blog.fliaping.com/the-attention-of-json-serialization-and-deserialization-in-java/" target="_blank" rel="noopener">《java常用JSON库注意事项总结》</a></p><p>回归话题，上面的问题我们如何解决，最终我们采用重写<code>javax.validation.ConstraintViolation&lt;T&gt;</code>的实现类，替换掉<code>hibernate-validation</code>的<code>org.hibernate.validator.internal.engine.ConstraintViolationImpl</code>，因为<code>ConstraintViolationImpl</code>中有部分对象无法通过hessian反序列化。</p><p>我们最终的目标是不管是validation开启在provider端还是consumer端，调用方接收到的参数校验异常数据是一致的。</p><p>修改的代码已经提交到<code>apache dubbo</code>，具体查看Pull request：<a href="https://github.com/apache/incubator-dubbo/pull/1708" target="_blank" rel="noopener">https://github.com/apache/incubator-dubbo/pull/1708</a></p><p>大概的代码如下：</p><h3 id="增加类：DubboConstraintViolation实现javax-validation-ConstraintViolation接口"><a href="#增加类：DubboConstraintViolation实现javax-validation-ConstraintViolation接口" class="headerlink" title="增加类：DubboConstraintViolation实现javax.validation.ConstraintViolation接口"></a>增加类：DubboConstraintViolation实现<code>javax.validation.ConstraintViolation</code>接口</h3><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">import java.io.Serializable;</span><br><span class="line">import javax.validation.ConstraintViolation;</span><br><span class="line">import javax.validation.Path;</span><br><span class="line">import javax.validation.ValidationException;</span><br><span class="line">import javax.validation.metadata.ConstraintDescriptor;</span><br><span class="line">import com.alibaba.dubbo.common.logger.Logger;</span><br><span class="line">import com.alibaba.dubbo.common.logger.LoggerFactory;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">public class DubboConstraintViolation&lt;T&gt; implements ConstraintViolation&lt;T&gt;, Serializable &#123;</span><br><span class="line">    </span><br><span class="line">    static final Logger logger = LoggerFactory.getLogger(DubboConstraintViolation.class.getName());</span><br><span class="line"></span><br><span class="line">private static final long serialVersionUID = -8901791810611051795L;</span><br><span class="line"></span><br><span class="line">private String interpolatedMessage;</span><br><span class="line">    private Object value;</span><br><span class="line">    private Path propertyPath;</span><br><span class="line">    private String messageTemplate;</span><br><span class="line">    private Object[] executableParameters;</span><br><span class="line">    private Object executableReturnValue;</span><br><span class="line">    private int hashCode;</span><br><span class="line"></span><br><span class="line">    public DubboConstraintViolation() &#123;</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    public DubboConstraintViolation(ConstraintViolation&lt;T&gt; violation) &#123;</span><br><span class="line">        this(violation.getMessageTemplate(), violation.getMessage(), violation.getInvalidValue(), violation.getPropertyPath(),</span><br><span class="line">                violation.getExecutableParameters(), violation.getExecutableReturnValue());</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    public DubboConstraintViolation(String messageTemplate,</span><br><span class="line">            String interpolatedMessage,</span><br><span class="line">            Object value,</span><br><span class="line">            Path propertyPath,</span><br><span class="line">            Object[] executableParameters,</span><br><span class="line">            Object executableReturnValue) &#123;</span><br><span class="line">        this.messageTemplate = messageTemplate;</span><br><span class="line">        this.interpolatedMessage = interpolatedMessage;</span><br><span class="line">        this.value = value;</span><br><span class="line">        this.propertyPath = propertyPath;</span><br><span class="line">        this.executableParameters = executableParameters;</span><br><span class="line">        this.executableReturnValue = executableReturnValue;</span><br><span class="line">        // pre-calculate hash code, the class is immutable and hashCode is needed often</span><br><span class="line">        this.hashCode = createHashCode();</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    @Override</span><br><span class="line">    public final String getMessage() &#123;</span><br><span class="line">        return interpolatedMessage;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    @Override</span><br><span class="line">    public final String getMessageTemplate() &#123;</span><br><span class="line">        return messageTemplate;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    @Override</span><br><span class="line">    public final T getRootBean() &#123;</span><br><span class="line">        return null;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    @Override</span><br><span class="line">    public final Class&lt;T&gt; getRootBeanClass() &#123;</span><br><span class="line">        return null;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    @Override</span><br><span class="line">    public final Object getLeafBean() &#123;</span><br><span class="line">        return null;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    @Override</span><br><span class="line">    public final Object getInvalidValue() &#123;</span><br><span class="line">        return value;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    @Override</span><br><span class="line">    public final Path getPropertyPath() &#123;</span><br><span class="line">        return propertyPath;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    @Override</span><br><span class="line">    public final ConstraintDescriptor&lt;?&gt; getConstraintDescriptor() &#123;</span><br><span class="line">        return null;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    @Override</span><br><span class="line">    public &lt;C&gt; C unwrap(Class&lt;C&gt; type) &#123;</span><br><span class="line">        if ( type.isAssignableFrom( ConstraintViolation.class ) ) &#123;</span><br><span class="line">            return type.cast( this );</span><br><span class="line">        &#125;</span><br><span class="line">        throw new ValidationException(&quot;Type &quot; + type.toString() + &quot; not supported for unwrapping.&quot;);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    @Override</span><br><span class="line">    public Object[] getExecutableParameters() &#123;</span><br><span class="line">        return executableParameters;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    @Override</span><br><span class="line">    public Object getExecutableReturnValue() &#123;</span><br><span class="line">        return executableReturnValue;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    @Override</span><br><span class="line">    // IMPORTANT - some behaviour of Validator depends on the correct implementation of this equals method! (HF)</span><br><span class="line"></span><br><span class="line">    // Do not take expressionVariables into account here. If everything else matches, the two CV should be considered</span><br><span class="line">    // equals (and because of the scary comment above). After all, expressionVariables is just a hint about how we got</span><br><span class="line">    // to the actual CV. (NF)</span><br><span class="line">    public boolean equals(Object o) &#123;</span><br><span class="line">        if ( this == o ) &#123;</span><br><span class="line">            return true;</span><br><span class="line">        &#125;</span><br><span class="line">        if ( o == null || getClass() != o.getClass() ) &#123;</span><br><span class="line">            return false;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        DubboConstraintViolation&lt;?&gt; that = (DubboConstraintViolation&lt;?&gt;) o;</span><br><span class="line"></span><br><span class="line">        if ( interpolatedMessage != null ? !interpolatedMessage.equals( that.interpolatedMessage ) : that.interpolatedMessage != null ) &#123;</span><br><span class="line">            return false;</span><br><span class="line">        &#125;</span><br><span class="line">        if ( propertyPath != null ? !propertyPath.equals( that.propertyPath ) : that.propertyPath != null ) &#123;</span><br><span class="line">            return false;</span><br><span class="line">        &#125;</span><br><span class="line">        if ( messageTemplate != null ? !messageTemplate.equals( that.messageTemplate ) : that.messageTemplate != null ) &#123;</span><br><span class="line">            return false;</span><br><span class="line">        &#125;</span><br><span class="line">        if ( value != null ? !value.equals( that.value ) : that.value != null ) &#123;</span><br><span class="line">            return false;</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><span class="line"></span><br><span class="line">    @Override</span><br><span class="line">    public int hashCode() &#123;</span><br><span class="line">        return hashCode;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    @Override</span><br><span class="line">    public String toString() &#123;</span><br><span class="line">        final StringBuilder sb = new StringBuilder();</span><br><span class="line">        sb.append( &quot;DubboConstraintViolation&quot; );</span><br><span class="line">        sb.append( &quot;&#123;interpolatedMessage=&apos;&quot; ).append( interpolatedMessage ).append( &apos;\&apos;&apos; );</span><br><span class="line">        sb.append( &quot;, propertyPath=&quot; ).append( propertyPath );</span><br><span class="line">        sb.append( &quot;, messageTemplate=&apos;&quot; ).append( messageTemplate ).append( &apos;\&apos;&apos; );</span><br><span class="line">        sb.append( &quot;, value=&apos;&quot; ).append( value ).append( &apos;\&apos;&apos; );</span><br><span class="line">        sb.append( &apos;&#125;&apos; );</span><br><span class="line">        return sb.toString();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    // Same as for equals, do not take expressionVariables into account here.</span><br><span class="line">    private int createHashCode() &#123;</span><br><span class="line">        int result = interpolatedMessage != null ? interpolatedMessage.hashCode() : 0;</span><br><span class="line">        result = 31 * result + ( propertyPath != null ? propertyPath.hashCode() : 0 );</span><br><span class="line">        result = 31 * result + ( value != null ? value.hashCode() : 0 );</span><br><span class="line">        result = 31 * result + ( messageTemplate != null ? messageTemplate.hashCode() : 0 );</span><br><span class="line">        return result;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="修改com-alibaba-dubbo-validation-filter-ValidationFilter异常处理的部分"><a href="#修改com-alibaba-dubbo-validation-filter-ValidationFilter异常处理的部分" class="headerlink" title="修改com.alibaba.dubbo.validation.filter.ValidationFilter异常处理的部分"></a>修改<code>com.alibaba.dubbo.validation.filter.ValidationFilter</code>异常处理的部分</h3><p>这里的变更为捕捉<code>javax.validation.ConstraintViolationException</code>异常，对异常中的<code>Set&lt;ConstraintViolation&lt;String&gt;&gt;</code>数据进行转换，去掉无法反序列化的对象,具体代码如下：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">public Result invoke(Invoker&lt;?&gt; invoker, Invocation invocation) throws RpcException &#123;</span><br><span class="line">    if (validation != null &amp;&amp; !invocation.getMethodName().startsWith(&quot;$&quot;)</span><br><span class="line">            &amp;&amp; ConfigUtils.isNotEmpty(invoker.getUrl().getMethodParameter(invocation.getMethodName(), Constants.VALIDATION_KEY))) &#123;</span><br><span class="line">        try &#123;</span><br><span class="line">            Validator validator = validation.getValidator(invoker.getUrl());</span><br><span class="line">            if (validator != null) &#123;</span><br><span class="line">                validator.validate(invocation.getMethodName(), invocation.getParameterTypes(), invocation.getArguments());</span><br><span class="line">            &#125;</span><br><span class="line">        &#125; catch (ConstraintViolationException e) &#123;</span><br><span class="line">            Set&lt;ConstraintViolation&lt;?&gt;&gt; set = null;</span><br><span class="line">            //验证set中如果是hibernate-validation实现的类就处理，其他的实现类放过</span><br><span class="line">            Set&lt;ConstraintViolation&lt;?&gt;&gt; constraintViolations = e.getConstraintViolations();</span><br><span class="line">            for (ConstraintViolation&lt;?&gt; v : constraintViolations) &#123;</span><br><span class="line">                if (!v.getClass().getName().equals(&quot;org.hibernate.validator.internal.engine.ConstraintViolationImpl&quot;)) &#123;</span><br><span class="line">                    return new RpcResult(e);</span><br><span class="line">                &#125; else &#123;</span><br><span class="line">                    if (set == null) set = new HashSet&lt;ConstraintViolation&lt;?&gt;&gt;();</span><br><span class="line">                    set.add(new DubboConstraintViolation&lt;&gt;(v));</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">            return new RpcResult(new ConstraintViolationException(e.getMessage(), set));</span><br><span class="line">        &#125; catch (RpcException e) &#123;</span><br><span class="line">            throw e;</span><br><span class="line">        &#125; catch (Throwable t) &#123;</span><br><span class="line">            return new RpcResult(t);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    return invoker.invoke(invocation);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>使用这个方法后，在<code>provider</code>端设置<code>validation=true</code>，<code>consumer</code>端可以正常拿到所有校验数据的异常信息。</p><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>我觉得这个方法并不是完美的方法，虽然这个问题是<code>hibernate-validator</code>框架的问题，<code>hibernate-validator</code>出生的年代分布式还不是特别的完善因此没有充分的考虑序列化反序列化问题也很正常，但是作为<code>Dubbo</code>框架在集成<code>jsr303</code>的时候也需要考虑这些问题。具体可以查看<code>Apache Dubbo</code>的<code>Pull Request</code>：<a href="https://github.com/apache/incubator-dubbo/pull/1708" target="_blank" rel="noopener">https://github.com/apache/incubator-dubbo/pull/1708</a></p>]]></content>
    
    <summary type="html">
    
      Dubbo使用jsr303框架hibernate-validator遇到的问题
    
    </summary>
    
      <category term="dubbo" scheme="https://ningyu1.github.io/categories/dubbo/"/>
    
    
      <category term="dubbo" scheme="https://ningyu1.github.io/tags/dubbo/"/>
    
      <category term="jsr303" scheme="https://ningyu1.github.io/tags/jsr303/"/>
    
      <category term="hibernate-validator" scheme="https://ningyu1.github.io/tags/hibernate-validator/"/>
    
  </entry>
  
  <entry>
    <title>单元测试以及代码覆盖率——Jenkins集成SonarQube、JaCoCo、Junit使用问题汇总</title>
    <link href="https://ningyu1.github.io/20180412/77-jenkins-sonarqube-jacoco-junit.html"/>
    <id>https://ningyu1.github.io/20180412/77-jenkins-sonarqube-jacoco-junit.html</id>
    <published>2018-04-12T09:03:00.000Z</published>
    <updated>2024-04-23T01:48:15.037Z</updated>
    
    <content type="html"><![CDATA[<p>当我们使用持续集成<code>Jenkins</code>的时候经常会结合一系列的插件使用，这里就说一下<code>Jenkins</code>集成<code>Sonar</code>做代码质量管理以及<code>Junit（testng）</code>、<code>JaCoCo</code>做单元测试和覆盖率的时候遇到的问题。</p><h1 id="前提"><a href="#前提" class="headerlink" title="前提"></a>前提</h1><p>首先我们的工程使用<code>maven</code>构建，单元测试使用<code>testng</code>编写，在使用<code>jenkins</code>之前我们应该在本地使用maven调通所有的单元测试以及<code>test coverage</code>的问题。</p><p>我们使用<code>maven-surefire-plugin</code>来生成单元测试报告，使用<code>jacoco-maven-plugin</code>来生成<code>test coverage</code>报告。下面我给出以下我使用的标准配置</p><h1 id="maven工程调通单元测试以及测试覆盖率报告生成"><a href="#maven工程调通单元测试以及测试覆盖率报告生成" class="headerlink" title="maven工程调通单元测试以及测试覆盖率报告生成"></a>maven工程调通单元测试以及测试覆盖率报告生成</h1><p>pom.xml的标准配置</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">&lt;dependencies&gt;</span><br><span class="line">&lt;dependency&gt;</span><br><span class="line">&lt;groupId&gt;org.slf4j&lt;/groupId&gt;</span><br><span class="line">&lt;artifactId&gt;slf4j-api&lt;/artifactId&gt;</span><br><span class="line">&lt;/dependency&gt;</span><br><span class="line">&lt;dependency&gt;</span><br><span class="line">&lt;groupId&gt;org.testng&lt;/groupId&gt;</span><br><span class="line">&lt;artifactId&gt;testng&lt;/artifactId&gt;</span><br><span class="line">&lt;version&gt;6.4&lt;/version&gt;</span><br><span class="line">&lt;scope&gt;test&lt;/scope&gt;</span><br><span class="line">&lt;optional&gt;true&lt;/optional&gt;</span><br><span class="line">&lt;/dependency&gt;</span><br><span class="line">&lt;dependency&gt;</span><br><span class="line">&lt;groupId&gt;junit&lt;/groupId&gt;</span><br><span class="line">&lt;artifactId&gt;junit&lt;/artifactId&gt;</span><br><span class="line">&lt;scope&gt;test&lt;/scope&gt;</span><br><span class="line">&lt;optional&gt;true&lt;/optional&gt;</span><br><span class="line">&lt;/dependency&gt;</span><br><span class="line">&lt;dependency&gt;</span><br><span class="line">&lt;groupId&gt;org.jacoco&lt;/groupId&gt;</span><br><span class="line">&lt;artifactId&gt;jacoco-maven-plugin&lt;/artifactId&gt;</span><br><span class="line">&lt;version&gt;0.8.1&lt;/version&gt;</span><br><span class="line">&lt;/dependency&gt;</span><br><span class="line">&lt;/dependencies&gt;</span><br><span class="line"></span><br><span class="line">&lt;build&gt;</span><br><span class="line">&lt;plugins&gt;</span><br><span class="line">&lt;plugin&gt;</span><br><span class="line">&lt;groupId&gt;org.apache.maven.plugins&lt;/groupId&gt;</span><br><span class="line">&lt;artifactId&gt;maven-surefire-plugin&lt;/artifactId&gt;</span><br><span class="line">&lt;version&gt;2.5&lt;/version&gt;</span><br><span class="line">&lt;configuration&gt;</span><br><span class="line">&lt;skipTests&gt;false&lt;/skipTests&gt;</span><br><span class="line">&lt;argLine&gt;$&#123;argLine&#125; -Dfile.encoding=UTF-8&lt;/argLine&gt;</span><br><span class="line">&lt;/configuration&gt;</span><br><span class="line">&lt;/plugin&gt;</span><br><span class="line">&lt;plugin&gt;</span><br><span class="line">&lt;groupId&gt;org.apache.maven.plugins&lt;/groupId&gt;</span><br><span class="line">&lt;artifactId&gt;maven-deploy-plugin&lt;/artifactId&gt;</span><br><span class="line">&lt;configuration&gt;</span><br><span class="line">&lt;skip&gt;false&lt;/skip&gt;</span><br><span class="line">&lt;/configuration&gt;</span><br><span class="line">&lt;/plugin&gt;</span><br><span class="line">&lt;plugin&gt;</span><br><span class="line">&lt;groupId&gt;org.jacoco&lt;/groupId&gt;</span><br><span class="line">&lt;artifactId&gt;jacoco-maven-plugin&lt;/artifactId&gt;</span><br><span class="line">&lt;version&gt;0.8.1&lt;/version&gt;</span><br><span class="line">&lt;configuration&gt;</span><br><span class="line">&lt;skip&gt;false&lt;/skip&gt;</span><br><span class="line">&lt;/configuration&gt;</span><br><span class="line">&lt;executions&gt;</span><br><span class="line">&lt;execution&gt;</span><br><span class="line">&lt;goals&gt;</span><br><span class="line">&lt;goal&gt;prepare-agent&lt;/goal&gt;</span><br><span class="line">&lt;/goals&gt;</span><br><span class="line">&lt;/execution&gt;</span><br><span class="line">&lt;execution&gt;</span><br><span class="line">&lt;configuration&gt;</span><br><span class="line">&lt;outputDirectory&gt;$&#123;basedir&#125;/target/coverage-reports&lt;/outputDirectory&gt;</span><br><span class="line">&lt;/configuration&gt;</span><br><span class="line">&lt;id&gt;report&lt;/id&gt;</span><br><span class="line">&lt;phase&gt;test&lt;/phase&gt;</span><br><span class="line">&lt;goals&gt;</span><br><span class="line">&lt;goal&gt;report&lt;/goal&gt;</span><br><span class="line">&lt;/goals&gt;</span><br><span class="line">&lt;/execution&gt;</span><br><span class="line">&lt;/executions&gt;</span><br><span class="line">&lt;/plugin&gt;</span><br><span class="line">&lt;/plugins&gt;</span><br><span class="line">&lt;/build&gt;</span><br></pre></td></tr></table></figure><p>根据上面配置执行下来的报告生成的目录结构如下:</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/jenkins-sonar-jacoco/8.png" alt title>                </div>                <div class="image-caption"></div>            </figure><ul><li><code>classes</code>是源代码编译生成的字节码目录</li><li><code>coverage-reports</code>是单元测试覆盖率报告生成目录</li><li><code>surefire-reports</code>是单元测试报告生成目录</li><li><code>test-classes</code>是单元测试代码编译生成的字节码目录</li><li><code>jacoco.exec</code>是用于生成单元测试可执行文件</li></ul><p>下面我说一下我们会遇到的常规问题</p><h1 id="上步操作会遇到的常规问题"><a href="#上步操作会遇到的常规问题" class="headerlink" title="上步操作会遇到的常规问题"></a>上步操作会遇到的常规问题</h1><h2 id="问题一：Tests-are-skipped"><a href="#问题一：Tests-are-skipped" class="headerlink" title="问题一：Tests are skipped."></a>问题一：Tests are skipped.</h2><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">[INFO] --- maven-surefire-plugin:2.5:test (default-test) @ tools ---</span><br><span class="line">[INFO] Tests are skipped.</span><br></pre></td></tr></table></figure><p>单元测试被跳过，这个可以通过<code>maven-surefire-plugin</code>插件的<code>configuration</code>来配置不跳过，如下配置：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">&lt;plugin&gt;</span><br><span class="line">&lt;groupId&gt;org.apache.maven.plugins&lt;/groupId&gt;</span><br><span class="line">&lt;artifactId&gt;maven-surefire-plugin&lt;/artifactId&gt;</span><br><span class="line">&lt;version&gt;2.5&lt;/version&gt;</span><br><span class="line">&lt;configuration&gt;</span><br><span class="line">&lt;skipTests&gt;false&lt;/skipTests&gt;</span><br><span class="line">&lt;/configuration&gt;</span><br><span class="line">&lt;/plugin&gt;</span><br></pre></td></tr></table></figure><p><span style="color:blue">配置<code>skipTests</code>属性而不是<code>skip</code>属性这里需要注意一下，有很多人配置的<code>skip</code>属性</span></p><h2 id="问题二：单元测试输出乱码"><a href="#问题二：单元测试输出乱码" class="headerlink" title="问题二：单元测试输出乱码"></a>问题二：单元测试输出乱码</h2><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">-------------------------------------------------------</span><br><span class="line"> T E S T S</span><br><span class="line">-------------------------------------------------------</span><br><span class="line">Running TestSuite</span><br><span class="line">SLF4J: Failed to load class &quot;org.slf4j.impl.StaticLoggerBinder&quot;.</span><br><span class="line">SLF4J: Defaulting to no-operation (NOP) logger implementation</span><br><span class="line">SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.</span><br><span class="line">=====��һ��===============</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><p>单元测试输出信息乱码，这个可以通过<code>maven-surefire-plugin</code>插件的<code>configuration</code>来配置字符编码，如下配置：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">&lt;plugin&gt;</span><br><span class="line">&lt;groupId&gt;org.apache.maven.plugins&lt;/groupId&gt;</span><br><span class="line">&lt;artifactId&gt;maven-surefire-plugin&lt;/artifactId&gt;</span><br><span class="line">&lt;version&gt;2.5&lt;/version&gt;</span><br><span class="line">&lt;configuration&gt;</span><br><span class="line">&lt;skipTests&gt;false&lt;/skipTests&gt;</span><br><span class="line">&lt;argLine&gt;-Dfile.encoding=UTF-8&lt;/argLine&gt;</span><br><span class="line">&lt;/configuration&gt;</span><br><span class="line">&lt;/plugin&gt;</span><br></pre></td></tr></table></figure><p><span style="color:blue">到这里我们就可以去<code>taget/surefire-reports</code>目录下查看单元测试报告。</span></p><h2 id="问题三：Skipping-JaCoCo-execution-due-to-missing-execution-data-file"><a href="#问题三：Skipping-JaCoCo-execution-due-to-missing-execution-data-file" class="headerlink" title="问题三：Skipping JaCoCo execution due to missing execution data file."></a>问题三：Skipping JaCoCo execution due to missing execution data file.</h2><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">[INFO] --- jacoco-maven-plugin:0.8.1:report (report) @ tools ---</span><br><span class="line">[INFO] Skipping JaCoCo execution due to missing execution data file.</span><br></pre></td></tr></table></figure><p><code>jacoco</code>执行被跳过，原因是没有找到<code>jacoco</code>可执行文件<code>jacoco.exec</code>。</p><p>这个时候我们去<code>target</code>目录下是看不到<code>jacoco.exec</code>文件的，有的版本名字叫<code>jacoco-junit.exec</code>。</p><p>理论上执行的时候会自动生成exec文件，但是为什么没有生成？我们看一下执行日志</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">[INFO] --- jacoco-maven-plugin:0.8.1:prepare-agent (default) @ tools ---</span><br><span class="line">[INFO] argLine set to -javaagent:D:\\javatools\\mvnrepository\\org\\jacoco\\org.jacoco.agent\\0.8.1\\org.jacoco.agent-0.8.1-runtime.jar=destfile=D:\\javatools\\workspace\\framework\\tools\\target\\jacoco.exec</span><br></pre></td></tr></table></figure><p><code>jacoco.exec</code>的生成是根据<code>-javaagent</code>的方式来生成的，我们有可以看到<code>jacoco-maven-plugin</code>指定了<code>argLine</code>参数，但是为什么没有生效？</p><p>原因是我们上面指定过单元测试编码，使用的就是<code>argLine</code>参数，因此这个问题应该是上面的编码参数指定后没有带入插件添加的<code>-javaagent</code>参数，那如何解决？查看下面配置：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">&lt;plugin&gt;</span><br><span class="line">&lt;groupId&gt;org.apache.maven.plugins&lt;/groupId&gt;</span><br><span class="line">&lt;artifactId&gt;maven-surefire-plugin&lt;/artifactId&gt;</span><br><span class="line">&lt;version&gt;2.5&lt;/version&gt;</span><br><span class="line">&lt;configuration&gt;</span><br><span class="line">&lt;skipTests&gt;false&lt;/skipTests&gt;</span><br><span class="line">&lt;argLine&gt;$&#123;argLine&#125; -Dfile.encoding=UTF-8&lt;/argLine&gt;</span><br><span class="line">&lt;/configuration&gt;</span><br><span class="line">&lt;/plugin&gt;</span><br></pre></td></tr></table></figure><p><span style="color:blue">在<code>argLine</code>中增加变量<code>${argLine}</code>后面再增加自动以的参数</span></p><p>如果通过配置手动的指定<code>jacoco.exec</code>文件的生成路径也需要注意也可能会出现这个问题，生成exec的路径指定在哪里，report执行的时候就需要通过<code>dataFile</code>来指定exec的路径，让程序知道正确的exec路径，比如说：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">&lt;plugin&gt;</span><br><span class="line">&lt;groupId&gt;org.jacoco&lt;/groupId&gt;</span><br><span class="line">&lt;artifactId&gt;jacoco-maven-plugin&lt;/artifactId&gt;</span><br><span class="line">&lt;version&gt;0.8.1&lt;/version&gt;</span><br><span class="line">&lt;configuration&gt;</span><br><span class="line">&lt;skip&gt;false&lt;/skip&gt;</span><br><span class="line">&lt;destFile&gt;$&#123;basedir&#125;/target/coverage-reports/jacoco.exec&lt;/destFile&gt;</span><br><span class="line">&lt;/configuration&gt;</span><br><span class="line">&lt;executions&gt;</span><br><span class="line">&lt;execution&gt;</span><br><span class="line">&lt;goals&gt;</span><br><span class="line">&lt;goal&gt;prepare-agent&lt;/goal&gt;</span><br><span class="line">&lt;/goals&gt;</span><br><span class="line">&lt;/execution&gt;</span><br><span class="line">&lt;execution&gt;</span><br><span class="line">&lt;configuration&gt;</span><br><span class="line">&lt;dataFile&gt;$&#123;basedir&#125;/target/coverage-reports/jacoco.exec&lt;/dataFile&gt;</span><br><span class="line">&lt;outputDirectory&gt;$&#123;basedir&#125;/target/coverage-reports&lt;/outputDirectory&gt;</span><br><span class="line">&lt;/configuration&gt;</span><br><span class="line">&lt;id&gt;report&lt;/id&gt;</span><br><span class="line">&lt;phase&gt;test&lt;/phase&gt;</span><br><span class="line">&lt;goals&gt;</span><br><span class="line">&lt;goal&gt;report&lt;/goal&gt;</span><br><span class="line">&lt;/goals&gt;</span><br><span class="line">&lt;/execution&gt;</span><br><span class="line">&lt;/executions&gt;</span><br><span class="line">&lt;/plugin&gt;</span><br></pre></td></tr></table></figure><p>上面通过<code>configuration</code>的<code>destFile</code>来自定义<code>jacoco.exec</code>的生成路径，下面在<code>report</code>的时候需要通过<code>dataFile</code>来指定对应的<code>jacoco.exec</code>的路径。</p><h1 id="Jenkins使用JaCoCo-plugin插件"><a href="#Jenkins使用JaCoCo-plugin插件" class="headerlink" title="Jenkins使用JaCoCo plugin插件"></a>Jenkins使用JaCoCo plugin插件</h1><p>首先去<code>Jenkins</code>上安装<code>JaCoCo plugin</code>插件，插件的安装就跳过了，插件安装好后，在job中如何配置?</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/jenkins-sonar-jacoco/1.png" alt title>                </div>                <div class="image-caption"></div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/jenkins-sonar-jacoco/2.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p><span style="color:red">这里需要注意的配置</span></p><ul><li>Path to exec files: **/jacoco.exec 可执行文件路径</li><li>Path to class directories: 这个配置的是源代码编译后的字节码目录，也就是<code>classes</code>目录不是<code>test-classes</code>目录，如果有多个可以指定多个</li><li>Path to source directories: 这个配置的是源代码的目录，也就是<code>src/main/java</code>目录，如果有多个可以指定多个。</li></ul><p>配置好之后执行job会看到如下的日志：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">INFO: ------------------------------------------------------------------------</span><br><span class="line">Injecting SonarQube environment variables using the configuration: SonarQube</span><br><span class="line">[JaCoCo plugin] Collecting JaCoCo coverage data...</span><br><span class="line">[JaCoCo plugin] **/jacoco.exec;**/classes;src/main/java; locations are configured</span><br><span class="line">Injecting SonarQube environment variables using the configuration: SonarQube</span><br><span class="line">Injecting SonarQube environment variables using the configuration: SonarQube</span><br><span class="line">[JaCoCo plugin] Number of found exec files for pattern **/jacoco.exec: 1</span><br><span class="line">[JaCoCo plugin] Saving matched execfiles:  /var/lib/jenkins/workspace/cc-framework-tools/target/coverage-reports/jacoco.exec</span><br><span class="line">[JaCoCo plugin] Saving matched class directories for class-pattern: **/classes: </span><br><span class="line">[JaCoCo plugin]  - /var/lib/jenkins/workspace/cc-framework-tools/target/classes 5 files</span><br><span class="line">[JaCoCo plugin] Saving matched source directories for source-pattern: src/main/java: </span><br><span class="line">[JaCoCo plugin] - /var/lib/jenkins/workspace/cc-framework-tools/src/main/java 5 files</span><br><span class="line">[JaCoCo plugin] Loading inclusions files..</span><br><span class="line">[JaCoCo plugin] inclusions: []</span><br><span class="line">[JaCoCo plugin] exclusions: []</span><br><span class="line">[JaCoCo plugin] Thresholds: JacocoHealthReportThresholds [minClass=0, maxClass=0, minMethod=0, maxMethod=0, minLine=0, maxLine=0, minBranch=0, maxBranch=0, minInstruction=0, maxInstruction=0, minComplexity=0, maxComplexity=0]</span><br><span class="line">[JaCoCo plugin] Publishing the results..</span><br><span class="line">[JaCoCo plugin] Loading packages..</span><br><span class="line">[JaCoCo plugin] Done.</span><br><span class="line">[JaCoCo plugin] Overall coverage: class: 50, method: 54, line: 48, branch: 40, instruction: 55</span><br><span class="line">Finished: SUCCESS</span><br></pre></td></tr></table></figure><p>出现上面日志就证明配置成功并且可以看到报告，如果出现下面的日志就证明配置的目录没有扫到classes，需要修改<code>Path to class directories</code>目录的配置</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">Overall coverage: class: 0, method: 0, line: 0, branch: 0, instruction: 0</span><br></pre></td></tr></table></figure><p>最终结果如下图：</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/jenkins-sonar-jacoco/3.png" alt title>                </div>                <div class="image-caption"></div>            </figure><h1 id="Jenkins使用Sonarqube-plugin插件"><a href="#Jenkins使用Sonarqube-plugin插件" class="headerlink" title="Jenkins使用Sonarqube plugin插件"></a>Jenkins使用Sonarqube plugin插件</h1><p>首先去<code>Jenkins上</code>安装<code>SonarQube plugin</code>插件，插件的安装就跳过了，插件安装好后，在<code>jenkins</code>的系统配置中配置<code>sonar</code>服务器信息，如下</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/jenkins-sonar-jacoco/4.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p>配置好后在job的配置中增加<code>SonarQube</code>的支持，如下</p><ul><li>在构建环境下添加<code>Prepare SonarQube Scanner environment</code></li></ul><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/jenkins-sonar-jacoco/5.png" alt title>                </div>                <div class="image-caption"></div>            </figure><ul><li>在构建下添加<code>Execute SonarQube Scanner</code></li></ul><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/jenkins-sonar-jacoco/6.png" alt title>                </div>                <div class="image-caption"></div>            </figure><ul><li>在<code>Execute SonarQube Scanner</code>中增加<code>Analysis properties</code></li></ul><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line"># required metadata</span><br><span class="line"># 项目key</span><br><span class="line">sonar.projectKey=com.domian.package:projectName</span><br><span class="line"># 项目名称</span><br><span class="line">sonar.projectName=tools</span><br><span class="line"># 项目版本，可以写死，也可以引用变量</span><br><span class="line">sonar.projectVersion=$&#123;VER&#125;</span><br><span class="line"># 源文件编码</span><br><span class="line">sonar.sourceEncoding=UTF-8</span><br><span class="line"># 源文件语言</span><br><span class="line">sonar.language=java</span><br><span class="line"># path to source directories (required)</span><br><span class="line"># 源代码目录，如果多个使用&quot;,&quot;分割 例如：mode1/src/main,mode2/src/main</span><br><span class="line">sonar.sources=src/main</span><br><span class="line"># 单元测试目录，如果多个使用&quot;,&quot;分割 例如：mode1/src/test,mode2/src/test</span><br><span class="line">sonar.tests=src/test</span><br><span class="line"># Exclude the test source</span><br><span class="line"># 忽略的目录</span><br><span class="line">#sonar.exclusions=*/src/test/**/*</span><br><span class="line"># 单元测试报告目录</span><br><span class="line">sonar.junit.reportsPath=target/surefire-reports</span><br><span class="line"># 代码覆盖率插件</span><br><span class="line">sonar.java.coveragePlugin=jacoco</span><br><span class="line"># jacoco.exec文件路径</span><br><span class="line">sonar.jacoco.reportPath=target/coverage-reports/jacoco.exec</span><br><span class="line"># 这个没搞懂，官方示例是配置成jacoco.exec文件路径</span><br><span class="line">sonar.jacoco.itReportPath=target/coverage-reports/jacoco.exec</span><br></pre></td></tr></table></figure><p><span style="color:blue">具体的参数可以查看官方文档:<a href="https://docs.sonarqube.org/display/SONAR/Analysis+Parameters" target="_blank" rel="noopener">《Analysis Parameters》</a></span></p><p>配置好之后执行job后去<code>Sonar</code>上只看到了单元测试的信息，没有看到单元测试覆盖率的信息，关于这个问题我们分析job执行的日志，如下:</p><h2 id="问题一：No-JaCoCo-analysis-of-project-coverage-can-be-done-since-there-is-no-class-files"><a href="#问题一：No-JaCoCo-analysis-of-project-coverage-can-be-done-since-there-is-no-class-files" class="headerlink" title="问题一：No JaCoCo analysis of project coverage can be done since there is no class files."></a>问题一：No JaCoCo analysis of project coverage can be done since there is no class files.</h2><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">16:01:17.455 INFO  - Sensor JaCoCoOverallSensor</span><br><span class="line">16:01:17.470 INFO  - Analysing /var/lib/jenkins/workspace/cc-framework-tools/target/coverage-reports/jacoco.exec</span><br><span class="line">16:01:17.481 INFO  - No JaCoCo analysis of project coverage can be done since there is no class files.</span><br><span class="line">16:01:17.481 INFO  - Sensor JaCoCoOverallSensor (done) | time=26ms</span><br><span class="line">16:01:17.482 INFO  - Sensor JaCoCoSensor</span><br><span class="line">16:01:17.482 INFO  - No JaCoCo analysis of project coverage can be done since there is no class files.</span><br><span class="line">16:01:17.482 INFO  - Sensor JaCoCoSensor (done) | time=0ms</span><br><span class="line">16:01:17.482 INFO  - Sensor Code Colorizer Sensor</span><br></pre></td></tr></table></figure><p>说的是没找到class文件所以<code>jacoco</code>不能进行分析，问题很明显是没有找到class类，难道它不是去<code>maven</code>标准的<code>target/classes</code>下找文件么？</p><p>但是找到了这篇文章：<a href="https://dzone.com/articles/integration-jenkins-jacoco-and-sonarqube" target="_blank" rel="noopener">《Jenkins, JaCoCo, and SonarQube Integration With Maven》</a>，看到里面在pom.xml中配置了一些参数给我了启发，发现有个参数<code>sonar.binaries</code>指定的是classes目录，可以插件的有些参数不兼容maven，在官方的配置中可以看到这样的字样: <code>Not compatible with Mave</code>和<code>Compatible with Maven</code>，能看到有写参数兼容maven默认路径有些不兼容。</p><p>随后再官方文档中也找到了与jenkins继承的properties配置说明：<a href="https://docs.sonarqube.org/display/PLUG/Triggering+Analysis+on+Hudson+Job" target="_blank" rel="noopener">《Triggering Analysis on Hudson Job》</a></p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line"># path to project binaries (optional), for example directory of Java bytecode</span><br><span class="line"># java字节码目录</span><br><span class="line">sonar.binaries=binDir</span><br></pre></td></tr></table></figure><p>最终给出<code>Execute SonarQube Scanner</code>中的<code>Analysis properties</code>完成配置参数如下：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line"># required metadata</span><br><span class="line"># 项目key</span><br><span class="line">sonar.projectKey=com.domian.package:projectName</span><br><span class="line"># 项目名称</span><br><span class="line">sonar.projectName=tools</span><br><span class="line"># 项目版本，可以写死，也可以引用变量</span><br><span class="line">sonar.projectVersion=$&#123;VER&#125;</span><br><span class="line"># 源文件编码</span><br><span class="line">sonar.sourceEncoding=UTF-8</span><br><span class="line"># 源文件语言</span><br><span class="line">sonar.language=java</span><br><span class="line"># path to source directories (required)</span><br><span class="line"># 源代码目录，如果多个使用&quot;,&quot;分割 例如：mode1/src/main,mode2/src/main</span><br><span class="line">sonar.sources=src/main/java</span><br><span class="line"># 单元测试目录，如果多个使用&quot;,&quot;分割 例如：mode1/src/test,mode2/src/test</span><br><span class="line">sonar.tests=src/test/java</span><br><span class="line"># java字节码目录</span><br><span class="line">sonar.binaries=target/classes</span><br><span class="line"># 单元测试报告目录</span><br><span class="line">sonar.junit.reportsPath=target/surefire-reports</span><br><span class="line"># 代码覆盖率插件</span><br><span class="line">sonar.java.coveragePlugin=jacoco</span><br><span class="line"># jacoco插件版本</span><br><span class="line">jacoco.version=0.8.1</span><br><span class="line"># jacoco.exec文件路径</span><br><span class="line">sonar.jacoco.reportPath=target/coverage-reports/jacoco.exec</span><br></pre></td></tr></table></figure><p>全部配置修改完后执行job后去<code>Sonar</code>上查看具体的信息如下：</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/jenkins-sonar-jacoco/7.png" alt title>                </div>                <div class="image-caption"></div>            </figure>]]></content>
    
    <summary type="html">
    
      单元测试以及代码覆盖率——Jenkins集成SonarQube、JaCoCo、Junit使用问题汇总
    
    </summary>
    
      <category term="test" scheme="https://ningyu1.github.io/categories/test/"/>
    
      <category term="jenkins" scheme="https://ningyu1.github.io/categories/test/jenkins/"/>
    
    
      <category term="jenkins" scheme="https://ningyu1.github.io/tags/jenkins/"/>
    
      <category term="sonar" scheme="https://ningyu1.github.io/tags/sonar/"/>
    
      <category term="sonarqube" scheme="https://ningyu1.github.io/tags/sonarqube/"/>
    
      <category term="jacoco" scheme="https://ningyu1.github.io/tags/jacoco/"/>
    
      <category term="junit" scheme="https://ningyu1.github.io/tags/junit/"/>
    
      <category term="testng" scheme="https://ningyu1.github.io/tags/testng/"/>
    
  </entry>
  
  <entry>
    <title>TiDB使用笔记 —— 测试环境集群部署</title>
    <link href="https://ningyu1.github.io/20180410/76-tidb-notes.html"/>
    <id>https://ningyu1.github.io/20180410/76-tidb-notes.html</id>
    <published>2018-04-10T12:13:00.000Z</published>
    <updated>2024-04-23T01:48:15.037Z</updated>
    
    <content type="html"><![CDATA[<p>TiDB是一个NewSql的分布式数据库，具体介绍我们引用官方的简介</p><h2 id="简介"><a href="#简介" class="headerlink" title="简介"></a>简介</h2><p>TiDB 是 PingCAP 公司受 Google <a href="http://research.google.com/archive/spanner.html" target="_blank" rel="noopener">Spanner</a> / <a href="http://research.google.com/pubs/pub41344.html" target="_blank" rel="noopener">F1</a> 论文启发而设计的开源分布式 NewSQL 数据库。</p><p>TiDB 具备如下 NewSQL 核心特性：</p><p>SQL支持（TiDB 是 MySQL 兼容的）<br>水平弹性扩展（吞吐可线性扩展）<br>分布式事务<br>跨数据中心数据强一致性保证<br>故障自恢复的高可用<br>海量数据高并发实时写入与实时查询（HTAP 混合负载）<br>TiDB 的设计目标是 100% 的 OLTP 场景和 80% 的 OLAP 场景，更复杂的 OLAP 分析可以通过 TiSpark 项目来完成。</p><p>TiDB 对业务没有任何侵入性，能优雅的替换传统的数据库中间件、数据库分库分表等 Sharding 方案。同时它也让开发运维人员不用关注数据库 Scale 的细节问题，专注于业务开发，极大的提升研发的生产力。</p><p>我们来看一下TiDB的架构图</p><h2 id="架构图"><a href="#架构图" class="headerlink" title="架构图"></a>架构图</h2><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/tidb/1.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p>从架构图中可以看出TiDB的三大组件都支持水平扩展而且内部通信使用的是gRPC，关于TiDB和gRPC的那些事可以查看InfoQ的文章：<a href="http://www.infoq.com/cn/articles/tidb-and-grpc" target="_blank" rel="noopener">《TiDB与gRPC的那点事》</a></p><p>TiDB使用的TiKV作为存储，官方建议至少TiKV使用ssd硬盘，如果条件好pd模块最好也使用ssd硬盘。</p><p>下来我们具体看一下三大组件分别都是干什么的</p><h2 id="TiDB-Server"><a href="#TiDB-Server" class="headerlink" title="TiDB Server"></a>TiDB Server</h2><p>TiDB Server 负责接收 SQL 请求，处理 SQL 相关的逻辑，并通过 PD 找到存储计算所需数据的 TiKV 地址，与 TiKV 交互获取数据，最终返回结果。 TiDB Server 是无状态的，其本身并不存储数据，只负责计算，可以无限水平扩展，可以通过负载均衡组件（如LVS、HAProxy 或 F5）对外提供统一的接入地址。</p><h2 id="PD-Server"><a href="#PD-Server" class="headerlink" title="PD Server"></a>PD Server</h2><p>Placement Driver (简称 PD) 是整个集群的管理模块，其主要工作有三个： 一是存储集群的元信息（某个 Key 存储在哪个 TiKV 节点）；二是对 TiKV 集群进行调度和负载均衡（如数据的迁移、Raft group leader 的迁移等）；三是分配全局唯一且递增的事务 ID。</p><p>PD 是一个集群，需要部署奇数个节点，一般线上推荐至少部署 3 个节点。</p><h2 id="TiKV-Server"><a href="#TiKV-Server" class="headerlink" title="TiKV Server"></a>TiKV Server</h2><p>TiKV Server 负责存储数据，从外部看 TiKV 是一个分布式的提供事务的 Key-Value 存储引擎。存储数据的基本单位是 Region，每个 Region 负责存储一个 Key Range （从 StartKey 到 EndKey 的左闭右开区间）的数据，每个 TiKV 节点会负责多个 Region 。TiKV 使用 Raft 协议做复制，保持数据的一致性和容灾。副本以 Region 为单位进行管理，不同节点上的多个 Region 构成一个 Raft Group，互为副本。数据在多个 TiKV 之间的负载均衡由 PD 调度，这里也是以 Region 为单位进行调度。</p><h2 id="特性"><a href="#特性" class="headerlink" title="特性"></a>特性</h2><p>可以无限水平扩展而且三大组件都是高可用，TiDB/TiKV/PD 这三个组件都能容忍部分实例失效，不影响整个集群的可用性。关于三大组件出现问题后如何恢复可以查看：<a href="https://pingcap.com/docs-cn/overview/#tidb-%E6%95%B4%E4%BD%93%E6%9E%B6%E6%9E%84" target="_blank" rel="noopener">《tidb-整体架构中的高可用章节》</a></p><h2 id="官方的部署建议"><a href="#官方的部署建议" class="headerlink" title="官方的部署建议"></a>官方的部署建议</h2><p>TiDB使用的TiKV作为存储，官方建议至少TiKV使用ssd硬盘，如果条件好pd模块最好也使用ssd硬盘。</p><p>建议 4 台及以上，TiKV 至少 3 实例，且与 TiDB、PD 模块不位于同一主机。</p><table><thead><tr><th style="text-align:center">组件</th><th style="text-align:center">CPU</th><th style="text-align:center">内存</th><th style="text-align:center">本地存储</th><th style="text-align:center">网络</th><th style="text-align:center">实例数量(最低要求)</th></tr></thead><tbody><tr><td style="text-align:center">TiDB</td><td style="text-align:center">8核+</td><td style="text-align:center">16 GB+</td><td style="text-align:center">SAS, 200 GB+</td><td style="text-align:center">千兆网卡</td><td style="text-align:center">1（可与 PD 同机器）</td></tr><tr><td style="text-align:center">PD</td><td style="text-align:center">8核+</td><td style="text-align:center">16 GB+</td><td style="text-align:center">SAS, 200 GB+</td><td style="text-align:center">千兆网卡</td><td style="text-align:center">1（可与 TiDB 同机器）</td></tr><tr><td style="text-align:center">TiKV</td><td style="text-align:center">8核+</td><td style="text-align:center">32 GB+</td><td style="text-align:center">SSD, 200 GB+</td><td style="text-align:center">千兆网卡</td><td style="text-align:center">3</td></tr><tr><td style="text-align:center">-</td><td style="text-align:center">-</td><td style="text-align:center">-</td><td style="text-align:center">-</td><td style="text-align:center">服务器总计</td><td style="text-align:center">4</td></tr></tbody></table><p>个人觉得这个使用的成本还是蛮高的。具体可以看<a href="https://pingcap.com/docs-cn/op-guide/recommendation/" target="_blank" rel="noopener">《软、硬件环境要求》</a></p><h2 id="测试部署"><a href="#测试部署" class="headerlink" title="测试部署"></a>测试部署</h2><p>TiDB的部署方式还是蛮丰富的，可以使用Ansible在线以及离线的部署集群，<a href="https://github.com/pingcap/tidb-ansible" target="_blank" rel="noopener">TiDB-Ansible</a> 是 PingCAP 基于 Ansible playbook 功能编写的集群部署工具。使用 TiDB-Ansible 可以快速部署一个完整的 TiDB 集群（包括 PD、TiDB、TiKV 和集群监控模块)。</p><p>TiDB同时也支持Docker部署方案，由于我们公司内网使用docker容器的方式管理所有服务，所以我这里使用docker方式部署。</p><p>我们使用Rancher来做企业级的容器管理平台，没有使用k8s、mesos来进行编排管理，使用的是Rancher自带的Cattle，Cattle不光有编排管理还包含了应用、服务、卷、负载均衡、健康检查、服务升级、dns服务、等功能，有兴趣的可以查看：<a href="https://rancher.com/docs/rancher/v1.6/zh/cattle/stacks/" target="_blank" rel="noopener">《Rancher官方文档-Cattle》</a></p><p>在进行部署之前需要先去Docker官方镜像库中拉TiDB集群所需要的三大组件的镜像： <a href="https://hub.docker.com/u/pingcap/" target="_blank" rel="noopener">Docker 官方镜像仓库</a></p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">docker pull pingcap/tidb:latest</span><br><span class="line">docker pull pingcap/tikv:latest</span><br><span class="line">docker pull pingcap/pd:latest</span><br></pre></td></tr></table></figure><p>这三个组件的镜像都不大，TiKV只有54MB，PD只有21MB，TiDB只有17MB</p><p><span style="color:blue"><em>这个我需要说一下他们这块做的还是很不错的，将镜像压缩的都比较小，去除了很多无用的东西。</em></span></p><p>我们需要创建7个容器来部署一个TiDB集群：</p><table><thead><tr><th style="text-align:center">容器</th><th style="text-align:center">容器IP</th><th style="text-align:center">宿主机IP</th><th style="text-align:center">部署服务</th><th style="text-align:center">数据盘挂载</th></tr></thead><tbody><tr><td style="text-align:center">PD1</td><td style="text-align:center">10.42.59.28</td><td style="text-align:center">192.168.18.108</td><td style="text-align:center">PD1</td><td style="text-align:center">/home/docker/TiDB</td></tr><tr><td style="text-align:center">PD2</td><td style="text-align:center">10.42.202.152</td><td style="text-align:center">192.168.18.108</td><td style="text-align:center">PD2</td><td style="text-align:center">/home/docker/TiDB</td></tr><tr><td style="text-align:center">PD3</td><td style="text-align:center">10.42.214.245</td><td style="text-align:center">192.168.18.108</td><td style="text-align:center">PD3</td><td style="text-align:center">/home/docker/TiDB</td></tr><tr><td style="text-align:center">TiDB</td><td style="text-align:center">10.42.188.35</td><td style="text-align:center">192.168.18.109</td><td style="text-align:center">TiDB</td><td style="text-align:center">/home/docker/TiDB</td></tr><tr><td style="text-align:center">TiKV1</td><td style="text-align:center">10.42.106.167</td><td style="text-align:center">192.168.18.109</td><td style="text-align:center">TiKV1</td><td style="text-align:center">/home/docker/TiDB</td></tr><tr><td style="text-align:center">TiKV2</td><td style="text-align:center">10.42.34.97</td><td style="text-align:center">192.168.18.109</td><td style="text-align:center">TiKV2</td><td style="text-align:center">/home/docker/TiDB</td></tr><tr><td style="text-align:center">TiKV3</td><td style="text-align:center">10.42.170.152</td><td style="text-align:center">192.168.18.109</td><td style="text-align:center">TiKV3</td><td style="text-align:center">/home/docker/TiDB</td></tr></tbody></table><p><span style="color:blue"><em>用docker的好处就是资源可以压缩到最小，我6个容器可以放在一到两台虚机上</em></span></p><p>查看pd集群信息</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">http://192.168.18.108:2379/v2/members</span><br><span class="line">http://192.168.18.108:2479/v2/members</span><br><span class="line">http://192.168.18.108:2579/v2/members</span><br></pre></td></tr></table></figure><p>返回信息以json格式，三台pd返回集群信息都是一样的</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">&#123;&quot;members&quot;:[&#123;&quot;id&quot;:&quot;969b7171b723b804&quot;,&quot;name&quot;:&quot;pd3&quot;,&quot;peerURLs&quot;:[&quot;http://192.168.18.108:2580&quot;],&quot;clientURLs&quot;:[&quot;http://192.168.18.108:2579&quot;]&#125;,&#123;&quot;id&quot;:&quot;d141f07798663b47&quot;,&quot;name&quot;:&quot;pd2&quot;,&quot;peerURLs&quot;:[&quot;http://192.168.18.108:2480&quot;],&quot;clientURLs&quot;:[&quot;http://192.168.18.108:2479&quot;]&#125;,&#123;&quot;id&quot;:&quot;e5e987f33a60e672&quot;,&quot;name&quot;:&quot;pd1&quot;,&quot;peerURLs&quot;:[&quot;http://192.168.18.108:2380&quot;],&quot;clientURLs&quot;:[&quot;http://192.168.18.108:2379&quot;]&#125;]&#125;</span><br></pre></td></tr></table></figure><p>具体的docker容器创建命令可以参考官方文档：<a href="https://pingcap.com/docs-cn/op-guide/docker-deployment/" target="_blank" rel="noopener">《Docker部署方案》</a></p><p>TiDB支持mysql协议可以使用任意mysql客户端连接，默认安装好的集群使用mysql登录，端口：4000，用户名：root，密码为空，修改密码跟mysql修改密码方式完全一样。</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">SET PASSWORD FOR &apos;root&apos;@&apos;%&apos; = &apos;xxx&apos;;</span><br></pre></td></tr></table></figure><p>下面说几个我们必须要关心的东西。</p><p>事务隔离级别可以查看：<a href="https://pingcap.com/docs-cn/sql/transaction-isolation/" target="_blank" rel="noopener">《TiDB 事务隔离级别》</a></p><p>SQL语法没有什么变化，具体可以查看：<a href="https://pingcap.com/docs-cn/sql/ddl/" target="_blank" rel="noopener">《SQL语句语法》</a></p><p>SQL执行计划什么的都有跟使用mysql几乎一样，还增加了json的支持，可以设置字段列存储类型为json格式。</p><p>具体与MySQL有什么差异可以查看：<a href="https://pingcap.com/docs-cn/sql/mysql-compatibility/" target="_blank" rel="noopener">《与MySQL兼容性对比》</a></p><p>历史数据回溯问题可以查看：<a href="https://pingcap.com/docs-cn/op-guide/history-read/" target="_blank" rel="noopener">《TiDB 历史数据回溯》</a></p><p>Binlog可以使用：<a href="https://pingcap.com/docs-cn/tools/tidb-binlog-kafka/" target="_blank" rel="noopener">《TiDB-Binlog 部署方案》</a></p><p>还有<a href="https://pingcap.com/docs-cn/op-guide/backup-restore/" target="_blank" rel="noopener">《备份与恢复》</a> 和 <a href="https://pingcap.com/docs-cn/op-guide/migration-overview/" target="_blank" rel="noopener">《数据迁移》</a>。</p><p>好了今天的大致介绍和测试环境集群搭建都到这里，后面会总结使用中遇到的问题。</p>]]></content>
    
    <summary type="html">
    
      TiDB使用笔记 —— 测试环境集群部署
    
    </summary>
    
      <category term="TiDB" scheme="https://ningyu1.github.io/categories/TiDB/"/>
    
    
      <category term="TiDB" scheme="https://ningyu1.github.io/tags/TiDB/"/>
    
      <category term="TiKV" scheme="https://ningyu1.github.io/tags/TiKV/"/>
    
      <category term="pd" scheme="https://ningyu1.github.io/tags/pd/"/>
    
  </entry>
  
  <entry>
    <title>MySql Lock wait timeout exceeded该如何处理？</title>
    <link href="https://ningyu1.github.io/20180408/75-mysql-lock-wait-timeout-exceeded.html"/>
    <id>https://ningyu1.github.io/20180408/75-mysql-lock-wait-timeout-exceeded.html</id>
    <published>2018-04-08T10:02:00.000Z</published>
    <updated>2024-04-23T01:48:15.037Z</updated>
    
    <content type="html"><![CDATA[<p>这个问题我相信大家对它并不陌生，但是有很多人对它产生的原因以及处理吃的不是特别透，很多情况都是交给DBA去定位和处理问题，接下来我们就针对这个问题来展开讨论。</p><p>Mysql造成锁的情况有很多，下面我们就列举一些情况：</p><ol><li>执行DML操作没有commit，再执行删除操作就会锁表。</li><li>在同一事务内先后对同一条数据进行插入和更新操作。</li><li>表索引设计不当，导致数据库出现死锁。</li><li>长事物，阻塞DDL，继而阻塞所有同表的后续操作。</li></ol><p>但是要区分的是<code>Lock wait timeout exceeded</code>与<code>Dead Lock</code>是不一样。</p><ul><li><code>Lock wait timeout exceeded</code>：后提交的事务等待前面处理的事务释放锁，但是在等待的时候超过了mysql的锁等待时间，就会引发这个异常。</li><li><code>Dead Lock</code>：两个事务互相等待对方释放相同资源的锁，从而造成的死循环，就会引发这个异常。</li></ul><p>还有一个要注意的是<code>innodb_lock_wait_timeout</code>与<code>lock_wait_timeout</code>也是不一样的。</p><ul><li><code>innodb_lock_wait_timeout</code>：innodb的dml操作的行级锁的等待时间 </li><li><code>lock_wait_timeout</code>：数据结构ddl操作的锁的等待时间</li></ul><p>如何查看innodb_lock_wait_timeout的具体值？</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">SHOW VARIABLES LIKE &apos;innodb_lock_wait_timeout&apos;</span><br></pre></td></tr></table></figure><p>如何修改innode lock wait timeout的值？</p><p>参数修改的范围有Session和Global，并且支持动态修改，可以有两种方法修改：</p><p>方法一：</p><p>通过下面语句修改</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">set innodb_lock_wait_timeout=100;</span><br><span class="line">set global innodb_lock_wait_timeout=100;</span><br></pre></td></tr></table></figure><p><span style="color:red"><em>ps. 注意global的修改对当前线程是不生效的，只有建立新的连接才生效。</em></span></p><p>方法二：</p><p>修改参数文件<code>/etc/my.cnf</code><br><code>innodb_lock_wait_timeout = 50</code></p><p><span style="color:red"><em>ps. <code>innodb_lock_wait_timeout</code>指的是事务等待获取资源等待的最长时间，超过这个时间还未分配到资源则会返回应用失败； 当锁等待超过设置时间的时候，就会报如下的错误；<code>ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction</code>。其参数的时间单位是秒，最小可设置为1s(一般不会设置得这么小)，最大可设置1073741824秒，默认安装时这个值是50s(默认参数设置)。</em></span></p><p>下面介绍在遇到这类问题该如何处理</p><h1 id="问题现象"><a href="#问题现象" class="headerlink" title="问题现象"></a>问题现象</h1><ul><li>数据更新或新增后数据经常自动回滚。</li><li>表操作总报 <code>Lock wait timeout exceeded</code> 并长时间无反应</li></ul><h1 id="解决方法"><a href="#解决方法" class="headerlink" title="解决方法"></a>解决方法</h1><ul><li>应急方法：<code>show full processlist;</code> <code>kill</code>掉出现问题的进程。 <span style="color:blue"><em>ps.有的时候通过processlist是看不出哪里有锁等待的，当两个事务都在commit阶段是无法体现在processlist上</em></span></li><li>根治方法：<code>select * from innodb_trx;</code>查看有是哪些事务占据了表资源。 <span style="color:blue"><em>ps.通过这个办法就需要对innodb有一些了解才好处理</em></span></li></ul><p><span style="color:blue">说起来很简单找到它杀掉它就搞定了，但是实际上并没有想象的这么简单，当问题出现要分析问题的原因，通过原因定位业务代码可能某些地方实现的有问题，从而来避免今后遇到同样的问题。</span></p><h1 id="innodb-表的解释"><a href="#innodb-表的解释" class="headerlink" title="innodb_*表的解释"></a>innodb_*表的解释</h1><p><code>Mysql</code>的<code>InnoDB</code>存储引擎是支持事务的，事务开启后没有被主动<code>Commit</code>。导致该资源被长期占用，其他事务在抢占该资源时，因上一个事务的锁而导致抢占失败！因此出现 <code>Lock wait timeout exceeded</code></p><p>下面几张表是innodb的事务和锁的信息表，理解这些表就能很好的定位问题。</p><p><code>innodb_trx</code> ## 当前运行的所有事务<br><code>innodb_locks</code> ## 当前出现的锁<br><code>innodb_lock_waits</code> ## 锁等待的对应关系</p><p>下面对 <code>innodb_trx</code> 表的每个字段进行解释：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">trx_id：事务ID。</span><br><span class="line">trx_state：事务状态，有以下几种状态：RUNNING、LOCK WAIT、ROLLING BACK 和 COMMITTING。</span><br><span class="line">trx_started：事务开始时间。</span><br><span class="line">trx_requested_lock_id：事务当前正在等待锁的标识，可以和 INNODB_LOCKS 表 JOIN 以得到更多详细信息。</span><br><span class="line">trx_wait_started：事务开始等待的时间。</span><br><span class="line">trx_weight：事务的权重。</span><br><span class="line">trx_mysql_thread_id：事务线程 ID，可以和 PROCESSLIST 表 JOIN。</span><br><span class="line">trx_query：事务正在执行的 SQL 语句。</span><br><span class="line">trx_operation_state：事务当前操作状态。</span><br><span class="line">trx_tables_in_use：当前事务执行的 SQL 中使用的表的个数。</span><br><span class="line">trx_tables_locked：当前执行 SQL 的行锁数量。</span><br><span class="line">trx_lock_structs：事务保留的锁数量。</span><br><span class="line">trx_lock_memory_bytes：事务锁住的内存大小，单位为 BYTES。</span><br><span class="line">trx_rows_locked：事务锁住的记录数。包含标记为 DELETED，并且已经保存到磁盘但对事务不可见的行。</span><br><span class="line">trx_rows_modified：事务更改的行数。</span><br><span class="line">trx_concurrency_tickets：事务并发票数。</span><br><span class="line">trx_isolation_level：当前事务的隔离级别。</span><br><span class="line">trx_unique_checks：是否打开唯一性检查的标识。</span><br><span class="line">trx_foreign_key_checks：是否打开外键检查的标识。</span><br><span class="line">trx_last_foreign_key_error：最后一次的外键错误信息。</span><br><span class="line">trx_adaptive_hash_latched：自适应散列索引是否被当前事务锁住的标识。</span><br><span class="line">trx_adaptive_hash_timeout：是否立刻放弃为自适应散列索引搜索 LATCH 的标识。</span><br></pre></td></tr></table></figure><p>下面对 <code>innodb_locks</code> 表的每个字段进行解释：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">lock_id：锁 ID。</span><br><span class="line">lock_trx_id：拥有锁的事务 ID。可以和 INNODB_TRX 表 JOIN 得到事务的详细信息。</span><br><span class="line">lock_mode：锁的模式。有如下锁类型：行级锁包括：S、X、IS、IX，分别代表：共享锁、排它锁、意向共享锁、意向排它锁。表级锁包括：S_GAP、X_GAP、IS_GAP、IX_GAP 和 AUTO_INC，分别代表共享间隙锁、排它间隙锁、意向共享间隙锁、意向排它间隙锁和自动递增锁。</span><br><span class="line">lock_type：锁的类型。RECORD 代表行级锁，TABLE 代表表级锁。</span><br><span class="line">lock_table：被锁定的或者包含锁定记录的表的名称。</span><br><span class="line">lock_index：当 LOCK_TYPE=’RECORD’ 时，表示索引的名称；否则为 NULL。</span><br><span class="line">lock_space：当 LOCK_TYPE=’RECORD’ 时，表示锁定行的表空间 ID；否则为 NULL。</span><br><span class="line">lock_page：当 LOCK_TYPE=’RECORD’ 时，表示锁定行的页号；否则为 NULL。</span><br><span class="line">lock_rec：当 LOCK_TYPE=’RECORD’ 时，表示一堆页面中锁定行的数量，亦即被锁定的记录号；否则为 NULL。</span><br><span class="line">lock_data：当 LOCK_TYPE=’RECORD’ 时，表示锁定行的主键；否则为NULL。</span><br></pre></td></tr></table></figure><p>下面对 innodb_lock_waits 表的每个字段进行解释：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">requesting_trx_id：请求事务的 ID。</span><br><span class="line">requested_lock_id：事务所等待的锁定的 ID。可以和 INNODB_LOCKS 表 JOIN。</span><br><span class="line">blocking_trx_id：阻塞事务的 ID。</span><br><span class="line">blocking_lock_id：某一事务的锁的 ID，该事务阻塞了另一事务的运行。可以和 INNODB_LOCKS 表 JOIN。</span><br></pre></td></tr></table></figure><h1 id="锁等待的处理步骤"><a href="#锁等待的处理步骤" class="headerlink" title="锁等待的处理步骤"></a>锁等待的处理步骤</h1><ul><li>直接查看 innodb_lock_waits 表</li></ul><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">SELECT * FROM innodb_lock_waits;</span><br></pre></td></tr></table></figure><ul><li>innodb_locks 表和 innodb_lock_waits 表结合：</li></ul><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">SELECT * FROM innodb_locks WHERE lock_trx_id IN (SELECT blocking_trx_id FROM innodb_lock_waits);</span><br></pre></td></tr></table></figure><ul><li>innodb_locks 表 JOIN innodb_lock_waits 表:</li></ul><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">SELECT innodb_locks.* FROM innodb_locks JOIN innodb_lock_waits ON (innodb_locks.lock_trx_id = innodb_lock_waits.blocking_trx_id);</span><br></pre></td></tr></table></figure><ul><li>查询 innodb_trx 表:</li></ul><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">SELECT trx_id, trx_requested_lock_id, trx_mysql_thread_id, trx_query FROM innodb_trx WHERE trx_state = &apos;LOCK WAIT&apos;;</span><br></pre></td></tr></table></figure><ul><li>trx_mysql_thread_id 即kill掉事务线程 ID</li></ul><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">SHOW ENGINE INNODB STATUS ;</span><br><span class="line">SHOW PROCESSLIST ;</span><br></pre></td></tr></table></figure><p>从上述方法中得到了相关信息，我们可以得到发生锁等待的线程 ID，然后将其 KILL 掉。<br>KILL 掉发生锁等待的线程。</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">kill ID;</span><br></pre></td></tr></table></figure>]]></content>
    
    <summary type="html">
    
      MySql Lock wait timeout exceeded该如何处理？
    
    </summary>
    
      <category term="mysql" scheme="https://ningyu1.github.io/categories/mysql/"/>
    
    
      <category term="mysql" scheme="https://ningyu1.github.io/tags/mysql/"/>
    
      <category term="Lock wait timeout exceeded" scheme="https://ningyu1.github.io/tags/Lock-wait-timeout-exceeded/"/>
    
  </entry>
  
  <entry>
    <title>RediSearch基于Redis的高性能全文搜索引擎，资料整理</title>
    <link href="https://ningyu1.github.io/20180330/74-redisearch.html"/>
    <id>https://ningyu1.github.io/20180330/74-redisearch.html</id>
    <published>2018-03-30T04:24:00.000Z</published>
    <updated>2024-04-23T01:48:15.037Z</updated>
    
    <content type="html"><![CDATA[<p>最近在参考<a href="http://www.cnblogs.com/netfocus/p/4055346.html" target="_blank" rel="noopener">CQRS DDD架构</a>来进行公司的库存中心重构设计，在<code>CQRS</code>架构中需要一个<code>in-memory</code>的方式快速修改库存在通过消息驱动异步更新到<code>DB</code>，也就是说内存的数据是最新的，<code>DB</code>的数据是异步持久化的，在某一个时刻内存和<code>DB</code>的数据是存在不一致的，但是满足最终一致性。</p><p>这样我们就需要内存当作前置<code>DB</code>在使用，因此不单纯的只满足修改数据，还需要满足<code>Query</code>的要求，内存结构的数据<code>Query</code>是比较麻烦的，它不像<code>DB</code>那样已经实现好了索引检索，需要我们自己来设计<code>Key</code>的机构和搜索索引的构建。</p><p>当然行业里也有这样的做法，对数据修改的时候双写到内存(<code>Redis</code>)和<code>ElasticSearch</code>再异步到<code>DB</code>，这样<code>Query</code>全部走向<code>ElasticSearch</code>，但是我觉得这样做的复杂度会增加很多，所以就在看如何基于<code>Redis</code>来设计一个搜索引擎。</p><p>看到了<code>RedisLabs</code>团队开发的基于<code>Redis</code>的搜索引擎：<code>RediSearch</code></p><h1 id="RediSearch"><a href="#RediSearch" class="headerlink" title="RediSearch"></a>RediSearch</h1><p><a href="https://github.com/RedisLabsModules/RediSearch" target="_blank" rel="noopener">Github: RediSearch</a></p><p><a href="http://redisearch.io" target="_blank" rel="noopener">官方站点</a></p><p>官方给出的描述</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">Redisearch implements a search engine on top of redis, but unlike other redis search libraries, it does not use internal data structures like sorted sets.</span><br><span class="line">Inverted indexes are stored as a special compressed data type that allows for fast indexing and search speed, and low memory footprint.</span><br><span class="line">This also enables more advanced features, like exact phrase matching and numeric filtering for text queries, that are not possible or efficient with traditional redis search approaches.</span><br></pre></td></tr></table></figure><h2 id="主要特点"><a href="#主要特点" class="headerlink" title="主要特点"></a>主要特点</h2><p>高性能的全文搜索引擎（Faster, in-memory, highly available full text search），可作为Redis Module运行在Redis上。但是它与其他Redis搜索库不同的是，它不使用Redis内部数据结构，例如：集合、排序集（ps.后面会写一篇基于Redis的数据结构来设计搜索引擎），Redis原声的搜索还是有很大的局限性，简单的分词搜索是可以满足，但是应用到复杂的场景就不太适合。</p><ul><li>Full-Text indexing of multiple fields in documents.</li><li>Incremental indexing without performance loss.</li><li>Document ranking (provided manually by the user at index time).</li><li>Field weights.</li><li>Complex boolean queries with AND, OR, NOT operators between sub-queries.</li><li>Prefix matching in full-text queries.</li><li>Auto-complete suggestions (with fuzzy prefix suggestions)</li><li>Exact Phrase Search.</li><li>Stemming based query expansion in many languages (using Snowball).</li><li>Support for logographic (Chinese, etc.) tokenization and querying (using Friso)</li><li>Limiting searches to specific document fields (up to 128 fields supported).</li><li>Numeric filters and ranges.</li><li>Geographical search utilizing redis’ own GEO commands.</li><li>Supports any utf-8 encoded text.</li><li>Retrieve full document content or just ids.</li><li>Automatically index existing HASH keys as documents.</li><li>Document Deletion (Update can be done by deletion and then re-insertion).</li><li>Sortable properties (i.e. sorting users by age or name).</li></ul><p>下面是中文版本</p><ul><li>多个字段的文档的全文索引。</li><li>没有性能损失增量索引。</li><li>文档排名(由用户提供手动指数时间)。</li><li>字段权重。</li><li>在子查询之间使用AND，OR，NOT运算符进行复杂的布尔查询。</li><li>前缀匹配全文查询。</li><li>自动完成建议以模糊前缀(建议)</li><li>准确短语搜索。</li><li>阻止基于查询扩展多种语言(使用Snowball)。</li><li>支持语标的(中国等)标记和查询(使用Friso)</li><li>将搜索限制在特定的文档字段(128字段支持)。</li><li>数字过滤器和范围。</li><li>利用redis自己的GEO命令进行地理搜索。</li><li>支持任何utf-8编码的文本。</li><li>获取完整的文档内容或者只是id。</li><li>自动索引现有HASH keys文件。</li><li>文档删除(更新可以通过删除然后re-insertion)。</li><li>可排序属性（即按年龄或名称对用户进行排序）。</li></ul><h2 id="集群"><a href="#集群" class="headerlink" title="集群"></a>集群</h2><p><span style="color:blue"><em>当然还支持分布式集群，只不过集群还是试验阶段还不建议正式应用到企业级应用上。</em></span></p><h2 id="暂不支持"><a href="#暂不支持" class="headerlink" title="暂不支持"></a>暂不支持</h2><ul><li>Spelling correction（拼写更正）</li><li>Aggregations（集合）</li></ul><h2 id="支持的Client类库"><a href="#支持的Client类库" class="headerlink" title="支持的Client类库"></a>支持的Client类库</h2><p>Official (Redis Labs) and community Clients:</p><table><thead><tr><th style="text-align:center">Language</th><th style="text-align:center">Library</th><th style="text-align:center">Author</th><th style="text-align:center">License</th><th style="text-align:center">Comments</th></tr></thead><tbody><tr><td style="text-align:center">Python</td><td style="text-align:center">redisearch-py</td><td style="text-align:center">Redis Labs</td><td style="text-align:center">BSD</td><td style="text-align:center">Usually the most up-to-date client library</td></tr><tr><td style="text-align:center">Java</td><td style="text-align:center">JRediSearch</td><td style="text-align:center">Redis Labs</td><td style="text-align:center">BSD</td><td style="text-align:center">-</td></tr><tr><td style="text-align:center">Go</td><td style="text-align:center">redisearch-go</td><td style="text-align:center">Redis Labs</td><td style="text-align:center">BSD</td><td style="text-align:center">Incomplete API</td></tr><tr><td style="text-align:center">JavaScript</td><td style="text-align:center">RedRediSearch</td><td style="text-align:center">Kyle J. Davis</td><td style="text-align:center">MIT</td><td style="text-align:center">Partial API, compatible with Reds</td></tr><tr><td style="text-align:center">C#</td><td style="text-align:center">NRediSearch</td><td style="text-align:center">Marc Gravell</td><td style="text-align:center">MIT</td><td style="text-align:center">Part of StackExchange.Redis</td></tr><tr><td style="text-align:center">PHP</td><td style="text-align:center">redisearch-php</td><td style="text-align:center">Ethan Hann</td><td style="text-align:center">MIT</td><td style="text-align:center">-</td></tr><tr><td style="text-align:center">Ruby on Rails</td><td style="text-align:center">redi_search_rails</td><td style="text-align:center">Dmitry Polyakovsky</td><td style="text-align:center">MIT</td><td style="text-align:center">-</td></tr><tr><td style="text-align:center">Ruby</td><td style="text-align:center">redisearch-rb</td><td style="text-align:center">Victor Ruiz</td><td style="text-align:center">MIT</td><td style="text-align:center">-</td></tr></tbody></table><p>类库支持的还算丰富，可以尝试使用一下。</p><h2 id="性能"><a href="#性能" class="headerlink" title="性能"></a>性能</h2><p>性能对比是以ElasticSearch、Solr来进行对比，官方的benchmark数据，<a href="https://github.com/RedisLabs/RediSearchBenchmark" target="_blank" rel="noopener">benchmark程序地址</a></p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/redisearch/1.jpg" alt title>                </div>                <div class="image-caption"></div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/redisearch/2.jpg" alt title>                </div>                <div class="image-caption"></div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/redisearch/3.jpg" alt title>                </div>                <div class="image-caption"></div>            </figure><h2 id="总结："><a href="#总结：" class="headerlink" title="总结："></a>总结：</h2><p>从数据上看，使用RediSearch的吞吐量高、延迟低，但是相比于ElasticSearch和Solr支持的特性上还有些欠缺比如：中文的模糊搜索支持的不是很好，但是其性能很高在某些场景是可以作为搜索引擎的替代方案来试用。</p><h2 id="案例资料："><a href="#案例资料：" class="headerlink" title="案例资料："></a>案例资料：</h2><ol><li><a href="https://www.csdn.net/article/a/2011-11-11/307327?bsh_bid=36806832" target="_blank" rel="noopener">利用RediSearch构建高效实时搜索案例</a></li><li><a href="https://segmentfault.com/a/1190000009821423" target="_blank" rel="noopener">一步步实现 Redis 搜索引擎</a></li><li><a href="https://www.v2ex.com/t/334928" target="_blank" rel="noopener">我们做了一个支持全文搜索和关系查询的 Redis</a></li></ol><p>上述就是关于RediSearch的资料整理，后面会尝试使用它来构建搜索引擎，会记录使用过程经历。</p>]]></content>
    
    <summary type="html">
    
      RediSearch基于Redis的高性能全文搜索引擎，资料整理
    
    </summary>
    
      <category term="redisearch" scheme="https://ningyu1.github.io/categories/redisearch/"/>
    
    
      <category term="redisearch" scheme="https://ningyu1.github.io/tags/redisearch/"/>
    
      <category term="redis" scheme="https://ningyu1.github.io/tags/redis/"/>
    
      <category term="in-memory" scheme="https://ningyu1.github.io/tags/in-memory/"/>
    
      <category term="highly available full text search" scheme="https://ningyu1.github.io/tags/highly-available-full-text-search/"/>
    
  </entry>
  
  <entry>
    <title>Trouble Shooting —— CAS Server集群环境下报错：Server redirected too many  times (20)</title>
    <link href="https://ningyu1.github.io/20180323/73-cas-server-pit1.html"/>
    <id>https://ningyu1.github.io/20180323/73-cas-server-pit1.html</id>
    <published>2018-03-23T08:01:00.000Z</published>
    <updated>2024-04-23T01:48:15.037Z</updated>
    
    <content type="html"><![CDATA[<p>当我们使用<code>cas</code>做单点登录的时候往往会使用集群方式部署，不管是<code>cas server</code>或者是接入的<code>app server</code>都会采用集群的方式部署。</p><p>在对<code>cas server</code>做集群实现无状态化，需要注意一下几点，也是我上一篇<code>cas</code>遇到的<code>TGC</code>验证问题中总结出来的：</p><ol><li><code>cas</code>的<code>ticket</code>需要做到集中存储，可以使用<code>redis</code>、<code>jpa</code>、或者其他方式，这个官方文章上有详细介绍：<a href="https://apereo.github.io/cas/5.0.x/installation/Configuration-Properties.html#ticket-registry" target="_blank" rel="noopener">ticket-registry</a></li><li><code>cas</code>的<code>session</code>信息需要做到集中存储，如果使用的是<code>tomcat</code>可以使用<a href="https://github.com/ran-jit/tomcat-cluster-redis-session-manager" target="_blank" rel="noopener">TomcatRedisSessionMananger</a>插件来通过<code>redis</code>做<code>session</code>集中存储。</li><li>还有一个就是上面遇到的问题，客户端<code>cookie</code>信息：<code>TGC</code>，<code>TGC</code>采用<code>cookie</code>方式存在客户端，因此需要开启会话保持，使得相同客户端每次都会被路由到同一个<code>cas server</code>上去做<code>TGC</code>验证。</li><li>最后一个就是需要接入<code>sso</code>的<code>client</code>应用端的<code>session</code>信息也需要做集中存储，因此<code>cas server</code>会和<code>client</code>进行通信去验证<code>ticket</code>，验证完后会生成信息并存储到<code>sesson</code>中，因此也需要使用<a href="https://github.com/ran-jit/tomcat-cluster-redis-session-manager" target="_blank" rel="noopener">TomcatRedisSessionMananger</a>插件来通过<code>redis</code>做<code>session</code>集中存储。</li><li>cas server端和接入的app服务端需要保证网络通畅。</li></ol><h1 id="cas使用总结博文目录"><a href="#cas使用总结博文目录" class="headerlink" title="cas使用总结博文目录"></a>cas使用总结博文目录</h1><p>最近<code>cas</code>遇到的问题我都总结到了blog中，这里整理一下目录如下：</p><ul><li><a href="https://ningyu1.github.io/site/post/54-cas-server/">《CAS使用经验总结，纯干货》</a></li><li><a href="https://ningyu1.github.io/site/post/57-cas-server1/">《CAS Server强制踢人功能实现方式》</a></li><li><a href="https://ningyu1.github.io/site/post/70-cas-server-pit/">《Trouble Shooting —— CAS Server集群环境下TGC验证问题排查，需要开启会话保持》</a></li></ul><p>接下来我们就说一下这次遇到的问题。</p><h1 id="问题现象"><a href="#问题现象" class="headerlink" title="问题现象"></a>问题现象</h1><p>通过上面的方式可以将<code>cas server</code>做到集群无状态化，但是避免不了其他的问题，下面就是最近与到的问题，现象是这样的，一部分人可以正常登陆，一部分人登陆时报错，错误如下：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">2018-03-23 10:33:22.768 [http-nio-7051-exec-1] ERROR org.jasig.cas.client.util.CommonUtils - Server redirected too many  times (20)</span><br><span class="line">java.net.ProtocolException: Server redirected too many  times (20)</span><br><span class="line">at sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1637) ~[na:1.7.0_79]</span><br><span class="line">at sun.net.www.protocol.https.HttpsURLConnectionImpl.getInputStream(HttpsURLConnectionImpl.java:254) ~[na:1.7.0_79]</span><br><span class="line">at org.jasig.cas.client.util.CommonUtils.getResponseFromServer(CommonUtils.java:393) ~[cas-client-core-3.3.3.jar:3.3.3]</span><br><span class="line">at org.jasig.cas.client.validation.AbstractCasProtocolUrlBasedTicketValidator.retrieveResponseFromServer(AbstractCasProtocolUrlBasedTicketValidator.java:45) [cas-client-core-3.3.3.jar:3.3.3]</span><br><span class="line">at org.jasig.cas.client.validation.AbstractUrlBasedTicketValidator.validate(AbstractUrlBasedTicketValidator.java:200) [cas-client-core-3.3.3.jar:3.3.3]</span><br><span class="line">at org.springframework.security.cas.authentication.CasAuthenticationProvider.authenticateNow(CasAuthenticationProvider.java:140) [spring-security-cas-3.1.7.RELEASE.jar:3.1.7.RELEASE]</span><br><span class="line">at org.springframework.security.cas.authentication.CasAuthenticationProvider.authenticate(CasAuthenticationProvider.java:126) [spring-security-cas-3.1.7.RELEASE.jar:3.1.7.RELEASE]</span><br><span class="line">at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:156) [spring-security-core-3.1.7.RELEASE.jar:3.1.7.RELEASE]</span><br><span class="line">at org.springframework.security.cas.web.CasAuthenticationFilter.attemptAuthentication(CasAuthenticationFilter.java:242) [spring-security-cas-3.1.7.RELEASE.jar:3.1.7.RELEASE]</span><br><span class="line">at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:195) [spring-security-web-3.1.7.RELEASE.jar:3.1.7.RELEASE]</span><br><span class="line">at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342) [spring-security-web-3.1.7.RELEASE.jar:3.1.7.RELEASE]</span><br><span class="line">at org.jasig.cas.client.session.SingleSignOutFilter.doFilter(SingleSignOutFilter.java:100) [cas-client-core-3.3.3.jar:3.3.3]</span><br><span class="line">at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342) [spring-security-web-3.1.7.RELEASE.jar:3.1.7.RELEASE]</span><br><span class="line">at com.bstek.bdf2.core.security.filter.PreAuthenticatedProcessingFilter.doFilter(PreAuthenticatedProcessingFilter.java:41) [scm-bdf2-core-1.1.0-SNAPSHOT.jar:na]</span><br><span class="line">at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342) [spring-security-web-3.1.7.RELEASE.jar:3.1.7.RELEASE]</span><br><span class="line">at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:105) [spring-security-web-3.1.7.RELEASE.jar:3.1.7.RELEASE]</span><br><span class="line">at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342) [spring-security-web-3.1.7.RELEASE.jar:3.1.7.RELEASE]</span><br><span class="line">at org.springframework.security.web.session.ConcurrentSessionFilter.doFilter(ConcurrentSessionFilter.java:125) [spring-security-web-3.1.7.RELEASE.jar:3.1.7.RELEASE]</span><br><span class="line">at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342) [spring-security-web-3.1.7.RELEASE.jar:3.1.7.RELEASE]</span><br><span class="line">at com.bstek.bdf2.core.security.filter.ContextFilter.doFilter(ContextFilter.java:36) [scm-bdf2-core-1.1.0-SNAPSHOT.jar:na]</span><br><span class="line">at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342) [spring-security-web-3.1.7.RELEASE.jar:3.1.7.RELEASE]</span><br><span class="line">at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:87) [spring-security-web-3.1.7.RELEASE.jar:3.1.7.RELEASE]</span><br><span class="line">at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342) [spring-security-web-3.1.7.RELEASE.jar:3.1.7.RELEASE]</span><br><span class="line">at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:192) [spring-security-web-3.1.7.RELEASE.jar:3.1.7.RELEASE]</span><br><span class="line">at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:160) [spring-security-web-3.1.7.RELEASE.jar:3.1.7.RELEASE]</span><br><span class="line">at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:344) [spring-web-4.0.0.RELEASE.jar:4.0.0.RELEASE]</span><br><span class="line">at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:261) [spring-web-4.0.0.RELEASE.jar:4.0.0.RELEASE]</span><br></pre></td></tr></table></figure><p>从异常的描述来看是服务器端多次<code>redirected</code>超过了20次导致的问题，什么原因会造成这个问题？</p><h1 id="cas单点登录过程剖析"><a href="#cas单点登录过程剖析" class="headerlink" title="cas单点登录过程剖析"></a>cas单点登录过程剖析</h1><p><code>cas</code>的单点登录的过程大致是这样的。</p><p>第一步：访问app地址，例如：<code>https://app.domain.com</code>，app端的<code>cas-client-core</code>会判断是否已经登录，如果没有登录会重定向到如下地址：<code>https://login.domain.com/login?service=https%3A%2F%2Fapp.domain.com%2Fcas_security_check_</code></p><p>第二步：当重定向到<code>cas</code>登录页面后，我们输入用户名密码，<code>cas server</code>端会进行如下操作</p><ul><li>先进行<code>AUTHENTICATION</code>过程，这个过程是验证我们的用户名密码是否正确，会输出如下日志：</li></ul><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">2018-03-23 14:58:01,429 INFO [org.apereo.inspektr.audit.support.Slf4jLoggingAuditTrailManager] - &lt;Audit trail record BEGIN</span><br><span class="line">=============================================================</span><br><span class="line">WHO: admin</span><br><span class="line">WHAT: Supplied credentials: [admin]</span><br><span class="line">ACTION: AUTHENTICATION_SUCCESS</span><br><span class="line">APPLICATION: CAS</span><br><span class="line">WHEN: Fri Mar 23 14:58:01 HKT 2018</span><br><span class="line">CLIENT IP ADDRESS: xx.xx.xx.xx</span><br><span class="line">SERVER IP ADDRESS: xx.xx.xx.xx</span><br><span class="line">=============================================================</span><br></pre></td></tr></table></figure><ul><li>当<code>AUTHENTICATION</code>通过以后会生成TGT（TICKET_GRANTING_TICKET），这个是换取服务票据的预授票据，并且将TGT保存起来，我这里使用的是jpa方式保存到db，会输出如下日志：</li></ul><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">=============================================================</span><br><span class="line">WHO: admin</span><br><span class="line">WHAT: TGT-***********************************************1VX72iaQBZ-077adac8d80f</span><br><span class="line">ACTION: TICKET_GRANTING_TICKET_CREATED</span><br><span class="line">APPLICATION: CAS</span><br><span class="line">WHEN: Fri Mar 23 14:58:01 HKT 2018</span><br><span class="line">CLIENT IP ADDRESS: 10.42.37.135</span><br><span class="line">SERVER IP ADDRESS: 10.42.185.88</span><br><span class="line">=============================================================</span><br><span class="line"></span><br><span class="line">&gt;</span><br><span class="line">Hibernate: insert into TICKETGRANTINGTICKET (NUMBER_OF_TIMES_USED, CREATION_TIME, EXPIRATION_POLICY, LAST_TIME_USED, PREVIOUS_LAST_TIME_USED, AUTHENTICATION, EXPIRED, PROXIED_BY, SERVICES_GRANTED_ACCESS_TO, ticketGrantingTicket_ID, TYPE, ID) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, &apos;TGT&apos;, ?)</span><br></pre></td></tr></table></figure><ul><li>当<code>TGT</code>生成完后会生成ST（SERVICE_TICKET），这个是服务票据，是授权这个服务的票据，并且会将ST保存起来和更新TGT信息，我这里使用的是jpa方式保存到db，会输出如下日志：</li></ul><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">2018-03-23 14:58:01,504 INFO [org.apereo.inspektr.audit.support.Slf4jLoggingAuditTrailManager] - &lt;Audit trail record BEGIN</span><br><span class="line">=============================================================</span><br><span class="line">WHO: admin</span><br><span class="line">WHAT: ST-153-RfpK0ACJHtPsSdnbYhVf-077adac8d80f for https://app.domain.com/cas_security_check_</span><br><span class="line">ACTION: SERVICE_TICKET_CREATED</span><br><span class="line">APPLICATION: CAS</span><br><span class="line">WHEN: Fri Mar 23 14:58:01 HKT 2018</span><br><span class="line">CLIENT IP ADDRESS: xx.xx.xx.xx</span><br><span class="line">SERVER IP ADDRESS: xx.xx.xx.xx</span><br><span class="line">=============================================================</span><br><span class="line"></span><br><span class="line">&gt;</span><br><span class="line">Hibernate: insert into SERVICETICKET (NUMBER_OF_TIMES_USED, CREATION_TIME, EXPIRATION_POLICY, LAST_TIME_USED, PREVIOUS_LAST_TIME_USED, FROM_NEW_LOGIN, TICKET_ALREADY_GRANTED, SERVICE, ticketGrantingTicket_ID, TYPE, ID) values (?, ?, ?, ?, ?, ?, ?, ?, ?, &apos;ST&apos;, ?)</span><br><span class="line">Hibernate: update TICKETGRANTINGTICKET set NUMBER_OF_TIMES_USED=?, CREATION_TIME=?, EXPIRATION_POLICY=?, LAST_TIME_USED=?, PREVIOUS_LAST_TIME_USED=?, AUTHENTICATION=?, EXPIRED=?, PROXIED_BY=?, SERVICES_GRANTED_ACCESS_TO=?, ticketGrantingTicket_ID=? where ID=?</span><br></pre></td></tr></table></figure><p>这个时候服务端生成的票据就完成了，会将ST信息生成TGC（TICKET_GRANTING_COOKIE）返回给app端。</p><p>第三步：app端接收到cas server端的返回，TGC会直接写入到浏览器cookie中，app端会再发起一次ST验证，这个过程是在app的后端发起请求的，url如下：</p><p><code>https://login.domain.com/serviceValidate?ticket=ST-153-RfpK0ACJHtPsSdnbYhVf-077adac8d80f&amp;service=https%3A%2F%2Fapp.domain.com%2Fcas_security_check_</code></p><p>第四步：cas server端收到service validate请求后会验证ST和TGC是否合法，并且验证TGC的时候cas server需要开启会话保持，让请求发送到生成TGC的机器上去，因为TGC中保存生成的服务端地址，具体问题我前面分析过查看：<a href="https://ningyu1.github.io/site/post/70-cas-server-pit/">《Trouble Shooting —— CAS Server集群环境下TGC验证问题排查，需要开启会话保持》</a>，cas server验证成功后会输出如下的日志：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">2018-03-23 14:58:01,578 INFO [org.apereo.inspektr.audit.support.Slf4jLoggingAuditTrailManager] - &lt;Audit trail record BEGIN</span><br><span class="line">=============================================================</span><br><span class="line">WHO: admin</span><br><span class="line">WHAT: ST-153-RfpK0ACJHtPsSdnbYhVf-077adac8d80f</span><br><span class="line">ACTION: SERVICE_TICKET_VALIDATED</span><br><span class="line">APPLICATION: CAS</span><br><span class="line">WHEN: Fri Mar 23 14:58:01 HKT 2018</span><br><span class="line">CLIENT IP ADDRESS: xx.xx.xx.xx</span><br><span class="line">SERVER IP ADDRESS: xx.xx.xx.xx</span><br><span class="line">=============================================================</span><br></pre></td></tr></table></figure><p><span style="color:red"><em>ps.出现下面日志表示验证失败</em></span></p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">2018-03-23 14:58:01,580 INFO [org.apereo.inspektr.audit.support.Slf4jLoggingAuditTrailManager] - &lt;Audit trail record BEGIN</span><br><span class="line">=============================================================</span><br><span class="line">WHO: audit:unknown</span><br><span class="line">WHAT: ST-154-YA6KibaqHpOMGXbluz7V-077adac8d80f</span><br><span class="line">ACTION: SERVICE_TICKET_VALIDATE_FAILED</span><br><span class="line">APPLICATION: CAS</span><br><span class="line">WHEN: Fri Mar 23 14:58:01 HKT 2018</span><br><span class="line">CLIENT IP ADDRESS: xx.xx.xx.xx</span><br><span class="line">SERVER IP ADDRESS: xx.xx.xx.xx</span><br><span class="line">=============================================================</span><br></pre></td></tr></table></figure><p>第五步：app后端接收到cas server端service验证成功的返回后，会生成session并且与TG进行关系绑定，绑定信息会保存起来，<span style="color:blue"><em>这里需要注意的是如果是集群环境需要保存到redis或者其他统一存储的地方。</em></span>，app后端接收验证成功后的输出日志如下：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">2018-03-23 14:58:01.531 [http-apr-8080-exec-1] DEBUG o.j.c.c.validation.Cas20ServiceTicketValidator - Constructing validation url: https://login.domain.com/serviceValidate?ticket=ST-153-RfpK0ACJHtPsSdnbYhVf-077adac8d80f&amp;service=https%3A%2F%2Fapp.domain.com%2Fcas_security_check_</span><br><span class="line">2018-03-23 14:58:01.531 [http-apr-8080-exec-1] DEBUG o.j.c.c.validation.Cas20ServiceTicketValidator - Retrieving response from server.</span><br><span class="line">2018-03-23 14:58:01.602 [http-apr-8080-exec-1] DEBUG o.j.c.c.validation.Cas20ServiceTicketValidator - Server response: &lt;cas:serviceResponse xmlns:cas=&apos;http://www.yale.edu/tp/cas&apos;&gt;</span><br><span class="line">    &lt;cas:authenticationSuccess&gt;</span><br><span class="line">        &lt;cas:user&gt;admin&lt;/cas:user&gt;</span><br><span class="line">    &lt;/cas:authenticationSuccess&gt;</span><br><span class="line">&lt;/cas:serviceResponse&gt;</span><br></pre></td></tr></table></figure><p><span style="color:blue"><em>输出以上信息就是验证成功。到这里cas server端的所有验证都完成了。</em></span></p><p><span style="color:red"><em>ps.出现下面日志表示app后端接收到的是验证失败返回信息</em></span></p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">2018-03-23 14:58:02.295 [http-bio-7051-exec-6] DEBUG o.j.c.c.validation.Cas20ServiceTicketValidator - Constructing validation url: https://login.domain.com/serviceValidate?ticket=ST-154-YA6KibaqHpOMGXbluz7V-077adac8d80f&amp;service=https%3A%2F%2Fapp.domain.com%2Fcas_security_check_</span><br><span class="line">2018-03-23 14:58:02.295 [http-bio-7051-exec-6] DEBUG o.j.c.c.validation.Cas20ServiceTicketValidator - Retrieving response from server.</span><br><span class="line">2018-03-23 14:58:02.830 [http-bio-7051-exec-6] DEBUG o.j.c.c.validation.Cas20ServiceTicketValidator - Server response: &lt;cas:serviceResponse xmlns:cas=&apos;http://www.yale.edu/tp/cas&apos;&gt;</span><br><span class="line">    &lt;cas:authenticationFailure code=&quot;INVALID_TICKET&quot;&gt;Ticket &amp;#39;ST-154-YA6KibaqHpOMGXbluz7V-077adac8d80f&amp;#39; not recognized&lt;/cas:authenticationFailure&gt;</span><br><span class="line">&lt;/cas:serviceResponse&gt;</span><br></pre></td></tr></table></figure><p>第六步：app端登录成功进入主页面。</p><p>根据这个流程我们再来分析上面的异常是那个环节出现了问题。</p><h1 id="问题分析"><a href="#问题分析" class="headerlink" title="问题分析"></a>问题分析</h1><p>首先上面的异常是app的后端出现的异常，app后端发起请求是在cas server生成完ticket之后才发起的，并且发起的是service validate验证请求，这个请求导致重定向超过20次。</p><p>而且还有一个重要的信息就是，一部分人可以正常登录，一部分人不能登录，我们部署的结构是2台cas server，2台app服务。</p><p>通过日志排查，2台app服务，其中一台没有出现过一场，另外一台爆出异常。这个时候问题已经有些明朗了，当负载均衡路由到出错的这台服务时，后台服务发起service validate验证时出现了问题，那接下来就让我们对比两台服务器上的配置。</p><p>我们采用的是阿里云的SLB映射到后台的nginx，app的后台服务要和cas server通信那首先网络需要是通的，理论上网络应该是没问题的，但是为了验证问题，我们就从网络这块开始排查。</p><p>因为我们使用的是阿里云而且app服务没有开通外网，app后天和cas服务通信走的是内网的SLB，接下来我们就ping一下登录地址看一下返回的slb地址是否相同。</p><p>两台机器上ping login.domain.com ，果然返回的ip不一致，其中报错的那台机器返回的是本机ip，奥这就是问题的根源，<code>cat /etc/hosts</code>果然域名映射的ip不一致，应该是运维配置失误导致的问题。</p><p>通过修改host配置之后再次验证错误解决。</p><h1 id="问题总结"><a href="#问题总结" class="headerlink" title="问题总结"></a>问题总结</h1><p>最终定位的到的问题感觉很白痴的问题，是因为运维配置失误导致，但是值得回味的是，通过这个问题我们对cas的单点登录机制理解的更加深刻，这就是一种收获，往往通过繁琐的分析后定位到的问题都很easy，所以当我们分析问题、定位问题的时候一定要先理解其中的原理，再结合现象去一步一步分析，这是仔细和关注度是否全面的一种考验。好了问题就说到这里，希望能够帮助到需要的人。</p><p>世界和平、Keep Real！</p>]]></content>
    
    <summary type="html">
    
      Trouble Shooting —— CAS Server集群环境下报错：Server redirected too many  times (20)
    
    </summary>
    
      <category term="CAS" scheme="https://ningyu1.github.io/categories/CAS/"/>
    
    
      <category term="trouble shooting" scheme="https://ningyu1.github.io/tags/trouble-shooting/"/>
    
      <category term="CAS" scheme="https://ningyu1.github.io/tags/CAS/"/>
    
      <category term="iphash" scheme="https://ningyu1.github.io/tags/iphash/"/>
    
      <category term="TomcatRedisSessionManager" scheme="https://ningyu1.github.io/tags/TomcatRedisSessionManager/"/>
    
      <category term="Server redirected too many  times (20)" scheme="https://ningyu1.github.io/tags/Server-redirected-too-many-times-20/"/>
    
  </entry>
  
  <entry>
    <title>Subversion库如何全文检索代码？</title>
    <link href="https://ningyu1.github.io/20180323/72-svn-query.html"/>
    <id>https://ningyu1.github.io/20180323/72-svn-query.html</id>
    <published>2018-03-23T02:44:53.000Z</published>
    <updated>2024-04-23T01:48:15.037Z</updated>
    
    <content type="html"><![CDATA[<p>现在是<code>Git</code>流行的年代，在<code>Git</code>的套件里想要全文检索代码也有很多方案，<code>Git</code>也支持命令直接检索代码，但是当使用<code>svn</code>的用户代码检索应该如何处理呢？</p><p>在回答前面问题之前我们还要搞清楚另外一个问题，我们为什么要检索代码？</p><p>有的时候我们想从所有的代码库去寻找使用相同方法的代码，常规做法就是<code>checkout</code>下来所有的项目，然后通过<code>IDE</code>工具去关联检索使用到某个方法的代码，但是这样做比较耗费时间而且当项目过多<code>IDE</code>不一定能扛得住。还有的时候我们想从规范角度去<code>check</code>开发人员写的代码是否有违规的或者有问题的，就可以通过检索去寻找，当然规范的<code>check</code>有更好的工具，可以使用<code>scm</code>工具<code>sonar</code>去<code>check</code>代码它整合了很多<code>check</code>模版。</p><p>鉴于上面种种的原因对代码做检索还是很有必要的，接下来我们就说一下使用<code>svn</code>时如何全文检索代码。</p><p>我们可以先说一个思路，把代码灌入<code>elasticsearch</code>、<code>lucene</code>、<code>solr</code>，然后通过ui去搜索这是一条可行的路子。</p><p>这两天发现了一个工具<code>svnquery</code>很好用，它使用<code>ASP.net</code>开发，采用<code>Lucene</code>生成索引，提供<code>GUI</code>和<code>WEB</code>工具通过索引文件来检索代码。</p><p><a href="http://svnquery.tigris.org/servlets/ProjectProcess?pageID=o0dpdE" target="_blank" rel="noopener">svnquery官网</a></p><p>它提供三个程序，一个<code>svnindex</code>用于通过<code>svn</code>库生成索引目录</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">SvnIndex.exe %aciton% %index_path% %svn_path% -u 用户名 -p 密码</span><br></pre></td></tr></table></figure><p><span style="color:blue"><em>ps. <code>action</code>包括<code>create</code>、<code>update</code>，更新和修改</em></span></p><p>执行后会生成一个索引目录，可以通过<code>svnfind</code>工具可以选择索引目录来进行代码搜索，<code>svnfind</code>是一个<code>GUI</code>工具。</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/svnquery/1.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p>还可以通过<code>SvnWebQuery</code>来进行代码搜索，<code>SvnWebQuery</code>是一个<code>.NET</code>的<code>web</code>程序需要放入<code>IIS</code>服务器来使用</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/svnquery/2.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p><span style="color:blue"><em>引用官网的两张图</em></span></p><p>唯一的缺点就是需要一个库一个库的生成索引，没有批量生成<code>svn</code>路径下所有有权限的库，如果有这个功能我个人觉得就完美了。</p><p>好了工具介绍到这里，如果有用<code>svn</code>的想对代码进行检索的可以使用这个工具。</p>]]></content>
    
    <summary type="html">
    
      Subversion库如何全文检索代码？
    
    </summary>
    
      <category term="svn" scheme="https://ningyu1.github.io/categories/svn/"/>
    
    
      <category term="svn" scheme="https://ningyu1.github.io/tags/svn/"/>
    
      <category term="subversion" scheme="https://ningyu1.github.io/tags/subversion/"/>
    
      <category term="svnquery" scheme="https://ningyu1.github.io/tags/svnquery/"/>
    
  </entry>
  
  <entry>
    <title>Zookeeper常用命令与注意事项</title>
    <link href="https://ningyu1.github.io/20180321/71-zookeeper-considerations.html"/>
    <id>https://ningyu1.github.io/20180321/71-zookeeper-considerations.html</id>
    <published>2018-03-21T07:26:53.000Z</published>
    <updated>2024-04-23T01:48:15.037Z</updated>
    
    <content type="html"><![CDATA[<p><code>Zookeeper</code>在互联网行业和分布式环境下是最常用的集群协调工具，那我们今天就对<code>Zookeeper</code>的常用命令和使用注意事项进一步说明，在这之前我们先看一下<code>Zookeeper</code>是什么，它能做什么？</p><h1 id="Zookeeper是什么？"><a href="#Zookeeper是什么？" class="headerlink" title="Zookeeper是什么？"></a>Zookeeper是什么？</h1><p><code>ZooKeeper</code>是一个开源的分布式应用程序协调服务，是<code>Google</code>的<code>Chubby</code>一个开源的实现，是<code>Hadoop</code>和Hbase的重要组件。它是一个为分布式应用提供一致性服务的软件，提供的功能包括：配置维护、域名服务、分布式同步、组服务等。</p><p>它的这些特性可以让我们在很多场景下使用它，可以用它做注册中心、分布式锁、选举、队列等。</p><h1 id="Zookeeper的原理"><a href="#Zookeeper的原理" class="headerlink" title="Zookeeper的原理"></a>Zookeeper的原理</h1><p><code>ZooKeeper</code>是以<code>Fast Paxos</code>算法为基础的，<a href="https://baike.baidu.com/item/Paxos%20%E7%AE%97%E6%B3%95" target="_blank" rel="noopener"><code>Paxos</code> 算法</a>存在活锁的问题，即当有多个<code>proposer</code>交错提交时，有可能互相排斥导致没有一个<code>proposer</code>能提交成功，而<code>Fast Paxos</code>作了一些优化，通过选举产生一个<code>leader</code> (领导者)，只有<code>leader</code>才能提交<code>proposer</code>，具体算法可见<code>Fast Paxos</code>。因此，要想弄懂<code>ZooKeeper</code>首先得对<code>Fast Paxos</code>有所了解</p><p><code>ZooKeeper</code>的基本运转流程：</p><ol><li>选举<code>Leader</code>。</li><li>同步数据。</li><li>选举<code>Leader</code>过程中算法有很多，但要达到的选举标准是一致的。</li><li><code>Leader</code>要具有最高的执行<code>ID</code>，类似<code>root</code>权限。</li><li>集群中大多数的机器得到响应并接受选出的<code>Leader</code>。</li></ol><h1 id="Zookeeper数据结构"><a href="#Zookeeper数据结构" class="headerlink" title="Zookeeper数据结构"></a>Zookeeper数据结构</h1><p>与普通的文件系统极其类似，如下：</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/zookeeper/1.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p>其中每个节点称为一个znode. 每个znode由3部分组成:</p><ul><li>stat. 此为状态信息, 描述该znode的版本, 权限等信息.</li><li>data. 与该znode关联的数据.</li><li>children. 该znode下的子节点.</li></ul><h1 id="Zookeeper节点类型"><a href="#Zookeeper节点类型" class="headerlink" title="Zookeeper节点类型"></a>Zookeeper节点类型</h1><ul><li><code>persistent</code>： <code>persistent</code>节点不和特定的<code>session</code>绑定, 不会随着创建该节点的<code>session</code>的结束而消失, 而是一直存在, 除非该节点被显式删除.</li><li><code>ephemeral</code>： <code>ephemeral</code>节点是临时性的, 如果创建该节点的<code>session</code>结束了, 该节点就会被自动删除.     <code>ephemeral</code>节点不能拥有子节点. 虽然<code>ephemeral</code>节点与创建它的<code>session</code>绑定, 但只要该该节点没有被删除, 其他<code>session</code>就可以读写该节点中关联的数据. 使用<code>-e</code>参数指定创建<code>ephemeral</code>节点.</li><li><code>sequence</code>： 严格的说, <code>sequence</code>并非节点类型中的一种. <code>sequence</code>节点既可以是<code>ephemeral</code>的, 也可以是<code>persistent</code>的. 创建<code>sequence</code>节点时, <code>ZooKeeper server</code>会在指定的节点名称后加上一个数字序列, 该数字序列是递增的. 因此可以多次创建相同的<code>sequence</code>节点, 而得到不同的节点. 使用-s参数指定创建<code>sequence</code>节点.</li></ul><h1 id="Zookeeper常用命令"><a href="#Zookeeper常用命令" class="headerlink" title="Zookeeper常用命令"></a>Zookeeper常用命令</h1><h2 id="启动服务"><a href="#启动服务" class="headerlink" title="启动服务"></a>启动服务</h2><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">[app@iZbp1dijzcfg8m0bcqfv9yZ zookeeper]$ ./bin/zkServer.sh start</span><br><span class="line">ZooKeeper JMX enabled by default</span><br><span class="line">Using config: /usr/local/servers/zookeeper/zookeeper/bin/../conf/zoo.cfg</span><br><span class="line">Starting zookeeper ... STARTED</span><br></pre></td></tr></table></figure><h2 id="查看当前zk节点状态"><a href="#查看当前zk节点状态" class="headerlink" title="查看当前zk节点状态"></a>查看当前zk节点状态</h2><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">[zk@iZbp1dijzcfg8m0bcqfv9yZ bin]$ ./zkServer.sh status</span><br><span class="line">JMX enabled by default</span><br><span class="line">Using config: /usr/local/servers/zookeeper/zookeeper/bin/../conf/zoo.cfg</span><br><span class="line">Mode: standalone</span><br></pre></td></tr></table></figure><p><span style="color:blue">ps. <code>standalone</code>代表单机模式，</span></p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">[zk@iZ23np2fk60Z bin]$ ./zkServer.sh status</span><br><span class="line">JMX enabled by default</span><br><span class="line">Using config: /usr/local/zookeeper/bin/../conf/zoo.cfg</span><br><span class="line">Mode: leader</span><br></pre></td></tr></table></figure><p><span style="color:blue">ps. 集群模式下会显示的状态，<code>leader</code>节点，集群中其他机器会从leader节点同步数据</span></p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">[zk@iZ237ydkhyiZ bin]$ ./zkServer.sh status</span><br><span class="line">JMX enabled by default</span><br><span class="line">Using config: /usr/local/zookeeper/bin/../conf/zoo.cfg</span><br><span class="line">Mode: follower</span><br></pre></td></tr></table></figure><p><span style="color:blue">ps. 集群模式下会显示的状态，<code>follower</code>节点在启动过程中会从leader节点同步所有数据</span></p><h2 id="连接服务"><a href="#连接服务" class="headerlink" title="连接服务"></a>连接服务</h2><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">[app@iZbp1dijzcfg8m0bcqfv9yZ zookeeper]$ ./bin/zkCli.sh -server ip:port</span><br></pre></td></tr></table></figure><p><span style="color:blue">ps. 不写ip端口默认连接本机服务.</span></p><h2 id="查看节点信息"><a href="#查看节点信息" class="headerlink" title="查看节点信息"></a>查看节点信息</h2><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">[zk: localhost:2181(CONNECTED) 0] ls /</span><br><span class="line">[seq, dubbo, disconf, otter, pinpoint-cluster, zookeeper]</span><br></pre></td></tr></table></figure><h2 id="查看指定node的子node"><a href="#查看指定node的子node" class="headerlink" title="查看指定node的子node"></a>查看指定node的子node</h2><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">[zk: localhost:2181(CONNECTED) 3] ls /zookeeper</span><br><span class="line">[quota]</span><br></pre></td></tr></table></figure><h2 id="创建一个普通节点"><a href="#创建一个普通节点" class="headerlink" title="创建一个普通节点"></a>创建一个普通节点</h2><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">[zk: localhost:2181(CONNECTED) 6] create /hello world</span><br><span class="line">Created /hello</span><br></pre></td></tr></table></figure><h2 id="获取hello节点的数据与状态"><a href="#获取hello节点的数据与状态" class="headerlink" title="获取hello节点的数据与状态"></a>获取hello节点的数据与状态</h2><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">[zk: localhost:2181(CONNECTED) 8] get /hello</span><br><span class="line">world</span><br><span class="line">cZxid = 0x262ea76</span><br><span class="line">ctime = Wed Mar 21 14:39:12 CST 2018</span><br><span class="line">mZxid = 0x262ea76</span><br><span class="line">mtime = Wed Mar 21 14:39:12 CST 2018</span><br><span class="line">pZxid = 0x262ea76</span><br><span class="line">cversion = 0</span><br><span class="line">dataVersion = 0</span><br><span class="line">aclVersion = 0</span><br><span class="line">ephemeralOwner = 0x0</span><br><span class="line">dataLength = 5</span><br><span class="line">numChildren = 0</span><br></pre></td></tr></table></figure><h2 id="删除hello节点"><a href="#删除hello节点" class="headerlink" title="删除hello节点"></a>删除hello节点</h2><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">[zk: localhost:2181(CONNECTED) 9] delete /hello</span><br><span class="line">[zk: localhost:2181(CONNECTED) 10] get /hello</span><br><span class="line">Node does not exist: /hello</span><br></pre></td></tr></table></figure><p><span style="color:blue">ps. 使用delete命令可以删除指定znode. 当该znode拥有子znode时, 必须先删除其所有子znode, 否则操作将失败. rmr命令可用于代替delete命令, rmr是一个递归删除命令, 如果发生指定节点拥有子节点时, rmr命令会首先删除子节点.</span></p><h2 id="znode节点的状态信息"><a href="#znode节点的状态信息" class="headerlink" title="znode节点的状态信息"></a>znode节点的状态信息</h2><p>使用get命令获取指定节点的数据时, 同时也将返回该节点的状态信息, 称为Stat. 其包含如下字段:</p><ul><li>czxid. 节点创建时的zxid.</li><li>mzxid. 节点最新一次更新发生时的zxid.</li><li>ctime. 节点创建时的时间戳.</li><li>mtime. 节点最新一次更新发生时的时间戳.</li><li>dataVersion. 节点数据的更新次数.</li><li>cversion. 其子节点的更新次数.</li><li>aclVersion. 节点ACL(授权信息)的更新次数.</li><li>ephemeralOwner. 如果该节点为ephemeral节点, ephemeralOwner值表示与该节点绑定的session id. 如果该节点不是ephemeral节点, ephemeralOwner值为0. 至于什么是ephemeral节点, 请看后面的讲述.</li><li>dataLength. 节点数据的字节数.</li><li>numChildren. 子节点个数.</li></ul><h2 id="zxid"><a href="#zxid" class="headerlink" title="zxid"></a>zxid</h2><p>znode节点的状态信息中包含czxid和mzxid, 那么什么是zxid呢?<br>ZooKeeper状态的每一次改变, 都对应着一个递增的Transaction id, 该id称为zxid. 由于zxid的递增性质, 如果zxid1小于zxid2, 那么zxid1肯定先于zxid2发生. 创建任意节点, 或者更新任意节点的数据, 或者删除任意节点, 都会导致Zookeeper状态发生改变, 从而导致zxid的值增加.</p><h2 id="session"><a href="#session" class="headerlink" title="session"></a>session</h2><p>在client和server通信之前, 首先需要建立连接, 该连接称为session. 连接建立后, 如果发生连接超时, 授权失败, 或者显式关闭连接, 连接便处于CLOSED状态, 此时session结束.</p><h2 id="创建不同类型的节点"><a href="#创建不同类型的节点" class="headerlink" title="创建不同类型的节点"></a>创建不同类型的节点</h2><p>节点的类型前面已经讲过。</p><p>创建一个临时节点</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">[zk: localhost:2181(CONNECTED) 12] create -e /hello world   </span><br><span class="line">Created /hello</span><br><span class="line">[zk: localhost:2181(CONNECTED) 13] get /hello</span><br><span class="line">world</span><br><span class="line">cZxid = 0x262ea78</span><br><span class="line">ctime = Wed Mar 21 14:45:23 CST 2018</span><br><span class="line">mZxid = 0x262ea78</span><br><span class="line">mtime = Wed Mar 21 14:45:23 CST 2018</span><br><span class="line">pZxid = 0x262ea78</span><br><span class="line">cversion = 0</span><br><span class="line">dataVersion = 0</span><br><span class="line">aclVersion = 0</span><br><span class="line">ephemeralOwner = 0x15c150a650f066c</span><br><span class="line">dataLength = 5</span><br><span class="line">numChildren = 0</span><br></pre></td></tr></table></figure><p>创建一个序列节点</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">[zk: localhost:2181(CONNECTED) 14] create -s /hello1 world</span><br><span class="line">Created /hello10000000007</span><br><span class="line">[zk: localhost:2181(CONNECTED) 15] create -s /hello1 world</span><br><span class="line">Created /hello10000000008</span><br><span class="line">[zk: localhost:2181(CONNECTED) 16] ls /</span><br><span class="line">[hello, dubbo, otter, zookeeper, seq, disconf, hello10000000007, hello10000000008, pinpoint-cluster]</span><br><span class="line">[zk: localhost:2181(CONNECTED) 17] get /hello10000000007</span><br><span class="line">world</span><br><span class="line">cZxid = 0x262ea7e</span><br><span class="line">ctime = Wed Mar 21 14:47:51 CST 2018</span><br><span class="line">mZxid = 0x262ea7e</span><br><span class="line">mtime = Wed Mar 21 14:47:51 CST 2018</span><br><span class="line">pZxid = 0x262ea7e</span><br><span class="line">cversion = 0</span><br><span class="line">dataVersion = 0</span><br><span class="line">aclVersion = 0</span><br><span class="line">ephemeralOwner = 0x0</span><br><span class="line">dataLength = 5</span><br><span class="line">numChildren = 0</span><br></pre></td></tr></table></figure><h2 id="watch"><a href="#watch" class="headerlink" title="watch"></a>watch</h2><p>watch的意思是监听感兴趣的事件. 在命令行中, 以下几个命令可以指定是否监听相应的事件.</p><h3 id="ls命令"><a href="#ls命令" class="headerlink" title="ls命令"></a>ls命令</h3><p>ls命令. ls命令的第一个参数指定znode, 第二个参数如果为true, 则说明监听该znode的子节点的增减, 以及该znode本身的删除事件.</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">[zk: localhost:2181(CONNECTED) 27] create /hello world</span><br><span class="line">Created /hello</span><br><span class="line">[zk: localhost:2181(CONNECTED) 28] ls /hello true</span><br><span class="line">[]</span><br><span class="line">[zk: localhost:2181(CONNECTED) 29] create /hello/test item001</span><br><span class="line"></span><br><span class="line">WATCHER::</span><br><span class="line"></span><br><span class="line">WatchedEvent state:SyncConnected type:NodeChildrenChanged path:/hello</span><br><span class="line">Created /hello/test</span><br></pre></td></tr></table></figure><h3 id="get命令"><a href="#get命令" class="headerlink" title="get命令"></a>get命令</h3><p>get命令. get命令的第一个参数指定znode, 第二个参数如果为true, 则说明监听该znode的更新和删除事件.</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">[zk: localhost:2181(CONNECTED) 30] get /hello true</span><br><span class="line">world</span><br><span class="line">cZxid = 0x262ef5d</span><br><span class="line">ctime = Wed Mar 21 14:52:16 CST 2018</span><br><span class="line">mZxid = 0x262ef5d</span><br><span class="line">mtime = Wed Mar 21 14:52:16 CST 2018</span><br><span class="line">pZxid = 0x262ef5e</span><br><span class="line">cversion = 1</span><br><span class="line">dataVersion = 0</span><br><span class="line">aclVersion = 0</span><br><span class="line">ephemeralOwner = 0x0</span><br><span class="line">dataLength = 5</span><br><span class="line">numChildren = 1</span><br><span class="line">[zk: localhost:2181(CONNECTED) 31] create /hello/test1 item001</span><br><span class="line">Created /hello/test1</span><br><span class="line">[zk: localhost:2181(CONNECTED) 32] rmr /hello</span><br><span class="line"></span><br><span class="line">WATCHER::</span><br><span class="line"></span><br><span class="line">WatchedEvent state:SyncConnected type:NodeDeleted path:/hello</span><br></pre></td></tr></table></figure><h3 id="stat命令"><a href="#stat命令" class="headerlink" title="stat命令"></a>stat命令</h3><p>stat命令. stat命令用于获取znode的状态信息. 第一个参数指定znode, 如果第二个参数为true.</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">[zk: localhost:2181(CONNECTED) 35] create /hello world</span><br><span class="line"></span><br><span class="line">WATCHER::</span><br><span class="line"></span><br><span class="line">WatchedEvent state:SyncConnected type:NodeCreated path:/hello</span><br><span class="line">Created /hello</span><br><span class="line">[zk: localhost:2181(CONNECTED) 36] stat /hello true</span><br><span class="line">cZxid = 0x262f0f0</span><br><span class="line">ctime = Wed Mar 21 14:56:31 CST 2018</span><br><span class="line">mZxid = 0x262f0f0</span><br><span class="line">mtime = Wed Mar 21 14:56:31 CST 2018</span><br><span class="line">pZxid = 0x262f0f0</span><br><span class="line">cversion = 0</span><br><span class="line">dataVersion = 0</span><br><span class="line">aclVersion = 0</span><br><span class="line">ephemeralOwner = 0x0</span><br><span class="line">dataLength = 5</span><br><span class="line">numChildren = 0</span><br></pre></td></tr></table></figure>]]></content>
    
    <summary type="html">
    
      Zookeeper常用命令与注意事项
    
    </summary>
    
      <category term="zookeeper" scheme="https://ningyu1.github.io/categories/zookeeper/"/>
    
    
      <category term="zookeeper" scheme="https://ningyu1.github.io/tags/zookeeper/"/>
    
  </entry>
  
  <entry>
    <title>Trouble Shooting —— CAS Server集群环境下TGC验证问题排查，需要开启会话保持</title>
    <link href="https://ningyu1.github.io/20180316/70-cas-server-pit.html"/>
    <id>https://ningyu1.github.io/20180316/70-cas-server-pit.html</id>
    <published>2018-03-16T04:02:53.000Z</published>
    <updated>2024-04-23T01:48:15.036Z</updated>
    
    <content type="html"><![CDATA[<h1 id="问题现象"><a href="#问题现象" class="headerlink" title="问题现象"></a>问题现象</h1><p><code>CAS</code>部署结构：</p><p>两台<code>cas server</code>通过<code>nginx</code>做负载均衡，两个<code>cas server</code>的<code>ticket registry</code>配置的<code>jpa</code>方式，指向同一个库。两个<code>cas server</code>的<code>tomcat</code>做了<code>TomcatRedisSessionManager</code>，使用<code>redis</code>集中存储<code>session</code>。</p><p>目前的现象：</p><p>页面上请求<code>cas</code>登录地址，登录过后频繁刷新登录页面，有时返回已登录，有时返回未登录，当返回未登录时去后台查看日志发现有如下错误，验证<code>cookie</code>发现请求的源<code>IP</code>与第一次访问的源<code>IP</code>不一致。这个很明显是<code>cas</code>集群环境下的问题。</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">2018-03-16 10:02:44,418 DEBUG [org.apereo.cas.web.support.TGCCookieRetrievingCookieGenerator] - &lt;Invalid cookie. Required remote address does not match $&#123;ip&#125;&gt;</span><br><span class="line">java.lang.IllegalStateException: Invalid cookie. Required remote address does not match $&#123;ip&#125;</span><br><span class="line">at org.apereo.cas.web.support.DefaultCasCookieValueManager.obtainCookieValue(DefaultCasCookieValueManager.java:84) ~[cas-server-support-cookie-5.0.4.jar:5.0.4]</span><br><span class="line">at org.apereo.cas.web.support.CookieRetrievingCookieGenerator.retrieveCookieValue(CookieRetrievingCookieGenerator.java:93) ~[cas-server-support-cookie-5.0.4.jar:5.0.4]</span><br><span class="line">at org.apereo.cas.web.support.CookieRetrievingCookieGenerator$$FastClassBySpringCGLIB$$25dba342.invoke(&lt;generated&gt;) ~[cas-server-support-cookie-5.0.4.jar:5.0.4]</span><br><span class="line">at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204) ~[spring-core-4.3.4.RELEASE.jar:4.3.4.RELEASE]</span><br><span class="line">at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:720) ~[spring-aop-4.3.4.RELEASE.jar:4.3.4.RELEASE]</span><br><span class="line">at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157) ~[spring-aop-4.3.4.RELEASE.jar:4.3.4.RELEASE]</span><br><span class="line">at org.springframework.aop.support.DelegatingIntroductionInterceptor.doProceed(DelegatingIntroductionInterceptor.java:133) ~[spring-aop-4.3.4.RELEASE.jar:4.3.4.RELEASE]</span><br><span class="line">at org.springframework.aop.support.DelegatingIntroductionInterceptor.invoke(DelegatingIntroductionInterceptor.java:121) ~[spring-aop-4.3.4.RELEASE.jar:4.3.4.RELEASE]</span><br><span class="line">at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.3.4.RELEASE.jar:4.3.4.RELEASE]</span><br><span class="line">at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:655) ~[spring-aop-4.3.4.RELEASE.jar:4.3.4.RELEASE]</span><br><span class="line">at org.apereo.cas.web.support.CookieRetrievingCookieGenerator$$EnhancerBySpringCGLIB$$10d36968.retrieveCookieValue(&lt;generated&gt;) ~[cas-server-support-cookie-5.0.4.jar:5.0.4]</span><br><span class="line">at org.apereo.cas.logging.web.ThreadContextMDCServletFilter.doFilter(ThreadContextMDCServletFilter.java:83) ~[cas-server-core-logging-5.0.4.jar:5.0.4]</span><br><span class="line">at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:241) ~[catalina.jar:7.0.85]</span><br><span class="line">at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208) ~[catalina.jar:7.0.85]</span><br><span class="line">at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:99) ~[spring-web-4.3.4.RELEASE.jar:4.3.4.RELEASE]</span><br><span class="line">at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-4.3.4.RELEASE.jar:4.3.4.RELEASE]</span><br><span class="line">at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:241) ~[catalina.jar:7.0.85]</span><br><span class="line">at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208) ~[catalina.jar:7.0.85]</span><br><span class="line">at org.springframework.web.filter.HttpPutFormContentFilter.doFilterInternal(HttpPutFormContentFilter.java:89) ~[spring-web-4.3.4.RELEASE.jar:4.3.4.RELEASE]</span><br><span class="line">at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-4.3.4.RELEASE.jar:4.3.4.RELEASE]</span><br><span class="line">at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:241) ~[catalina.jar:7.0.85]</span><br><span class="line">at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208) ~[catalina.jar:7.0.85]</span><br><span class="line">at org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:77) ~[spring-web-4.3.4.RELEASE.jar:4.3.4.RELEASE]</span><br><span class="line">at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-4.3.4.RELEASE.jar:4.3.4.RELEASE]</span><br><span class="line">at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:241) ~[catalina.jar:7.0.85]</span><br><span class="line">at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208) ~[catalina.jar:7.0.85]</span><br><span class="line">at org.springframework.boot.actuate.autoconfigure.MetricsFilter.doFilterInternal(MetricsFilter.java:107) ~[spring-boot-actuator-1.4.2.RELEASE.jar:1.4.2.RELEASE]</span><br><span class="line">at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-4.3.4.RELEASE.jar:4.3.4.RELEASE]</span><br><span class="line">at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:241) ~[catalina.jar:7.0.85]</span><br><span class="line">at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208) ~[catalina.jar:7.0.85]</span><br><span class="line">at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:197) ~[spring-web-4.3.4.RELEASE.jar:4.3.4.RELEASE]</span><br><span class="line">at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-4.3.4.RELEASE.jar:4.3.4.RELEASE]</span><br><span class="line">at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:241) ~[catalina.jar:7.0.85]</span><br><span class="line">at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208) ~[catalina.jar:7.0.85]</span><br><span class="line">at org.springframework.boot.web.support.ErrorPageFilter.doFilter(ErrorPageFilter.java:117) ~[spring-boot-1.4.2.RELEASE.jar:1.4.2.RELEASE]</span><br><span class="line">at org.springframework.boot.web.support.ErrorPageFilter.access$000(ErrorPageFilter.java:61) ~[spring-boot-1.4.2.RELEASE.jar:1.4.2.RELEASE]</span><br><span class="line">at org.springframework.boot.web.support.ErrorPageFilter$1.doFilterInternal(ErrorPageFilter.java:92) ~[spring-boot-1.4.2.RELEASE.jar:1.4.2.RELEASE]</span><br><span class="line">at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-4.3.4.RELEASE.jar:4.3.4.RELEASE]</span><br><span class="line">at org.springframework.boot.web.support.ErrorPageFilter.doFilter(ErrorPageFilter.java:110) ~[spring-boot-1.4.2.RELEASE.jar:1.4.2.RELEASE]</span><br><span class="line">at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:241) ~[catalina.jar:7.0.85]</span><br><span class="line">at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208) ~[catalina.jar:7.0.85]</span><br><span class="line">at org.apache.logging.log4j.web.Log4jServletFilter.doFilter(Log4jServletFilter.java:71) ~[log4j-web-2.6.2.jar:2.6.2]</span><br><span class="line">at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:241) ~[catalina.jar:7.0.85]</span><br><span class="line">at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208) ~[catalina.jar:7.0.85]</span><br><span class="line">at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:219) ~[catalina.jar:7.0.85]</span><br><span class="line">at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:110) ~[catalina.jar:7.0.85]</span><br><span class="line">at com.r.tomcat.session.management.RequestSessionHandlerValve.invoke(RequestSessionHandlerValve.java:30) ~[TomcatRedisSessionManager-1.0.jar:?]</span><br><span class="line">at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:169) ~[catalina.jar:7.0.85]</span><br><span class="line">at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:103) ~[catalina.jar:7.0.85]</span><br><span class="line">at org.apache.catalina.valves.AccessLogValve.invoke(AccessLogValve.java:962) ~[catalina.jar:7.0.85]</span><br><span class="line">at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:116) ~[catalina.jar:7.0.85]</span><br><span class="line">at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:445) ~[catalina.jar:7.0.85]</span><br><span class="line">at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1115) ~[tomcat-coyote.jar:7.0.85]</span><br><span class="line">at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:637) ~[tomcat-coyote.jar:7.0.85]</span><br><span class="line">at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1775) ~[tomcat-coyote.jar:7.0.85]</span><br><span class="line">at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.run(NioEndpoint.java:1734) ~[tomcat-coyote.jar:7.0.85]</span><br><span class="line">at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) [?:1.8.0_162]</span><br><span class="line">at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) [?:1.8.0_162]</span><br><span class="line">at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) ~[tomcat-coyote.jar:7.0.85]</span><br><span class="line">at java.lang.Thread.run(Thread.java:748) [?:1.8.0_162]</span><br></pre></td></tr></table></figure><p>网上查询资料：<a href="https://groups.google.com/a/apereo.org/forum/#!topic/cas-user/R4WVT4Tq0g8" target="_blank" rel="noopener">google group</a>，相同的问题，但是没有看到具体的解决方法。</p><p>看到的<a href="https://tomcat.apache.org/tomcat-7.0-doc/api/org/apache/catalina/valves/RemoteIpValve.html" target="_blank" rel="noopener">tomcat RemoteIpValue</a>也只是<code>tomcat</code>请求<code>ip</code>限制的方法，跟我们要的不匹配</p><p>根据异常查看<code>CAS</code>代码，如下：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">public String obtainCookieValue(Cookie cookie, HttpServletRequest request)</span><br><span class="line"> &#123;</span><br><span class="line">   String cookieValue = (String)this.cipherExecutor.decode(cookie.getValue());</span><br><span class="line">   LOGGER.debug(&quot;Decoded cookie value is [&#123;&#125;]&quot;, cookieValue);</span><br><span class="line">   if (StringUtils.isBlank(cookieValue))</span><br><span class="line">   &#123;</span><br><span class="line">     LOGGER.debug(&quot;Retrieved decoded cookie value is blank. Failed to decode cookie [&#123;&#125;]&quot;, cookie.getName());</span><br><span class="line">     return null;</span><br><span class="line">   &#125;</span><br><span class="line">   String[] cookieParts = cookieValue.split(String.valueOf(&apos;@&apos;));</span><br><span class="line">   if (cookieParts.length != 3) &#123;</span><br><span class="line">     throw new IllegalStateException(&quot;Invalid cookie. Required fields are missing&quot;);</span><br><span class="line">   &#125;</span><br><span class="line">   String value = cookieParts[0];</span><br><span class="line">   String remoteAddr = cookieParts[1];</span><br><span class="line">   String userAgent = cookieParts[2];</span><br><span class="line">   if ((StringUtils.isBlank(value)) || (StringUtils.isBlank(remoteAddr)) || </span><br><span class="line">     (StringUtils.isBlank(userAgent))) &#123;</span><br><span class="line">     throw new IllegalStateException(&quot;Invalid cookie. Required fields are empty&quot;);</span><br><span class="line">   &#125;</span><br><span class="line">   if (!remoteAddr.equals(request.getRemoteAddr())) &#123;</span><br><span class="line">     throw new IllegalStateException(&quot;Invalid cookie. Required remote address does not match &quot; + request.getRemoteAddr());</span><br><span class="line">   &#125;</span><br><span class="line">   String agent = WebUtils.getHttpServletRequestUserAgent(request);</span><br><span class="line">   if (!userAgent.equals(agent)) &#123;</span><br><span class="line">     throw new IllegalStateException(&quot;Invalid cookie. Required user-agent does not match &quot; + agent);</span><br><span class="line">   &#125;</span><br><span class="line">   return value;</span><br><span class="line"> &#125;</span><br></pre></td></tr></table></figure><p><code>TGC</code>中包含了<code>user-agent</code>信息，会根据<code>request</code>的<code>user-agent</code>去跟<code>decode</code>后的<code>cookie</code>中的<code>user-agent</code>对比，而且这个验证是在<code>cas 4.1</code>版本就已经加了这个验证信息了，如果我们修改源码去掉这个<code>user-agent</code>验证可能还会引发其他问题。</p><h1 id="解决方案"><a href="#解决方案" class="headerlink" title="解决方案"></a>解决方案</h1><p>采用负载均衡的粘性配置，nginx中可以是ip_hash或者sticky。</p><ol><li>如果使用的是阿里云的<code>SLB</code>需要开启会话保持的选项。</li><li>如果使用<code>nginx</code>需要在<code>upstream</code>中增加<code>ip_hash</code>保持会话。</li></ol><p><span style="color:blue"><em>这样就可以让相同的客户端ip将会话永远路由到相同的一台后端<code>cas server</code>上去。</em></span></p><p>现在不建议使用上面的方法解决，这个会丢失集群的特性，建议采用配置来关闭cas.tgc的加解密或者修改cas源代码解决问题，查看这篇文章<a href="https://ningyu1.github.io/blog/20191015/118-cas-server-pit.htmll">《Trouble Shooting —— CAS Server集群环境下TGC验证问题》</a>中的解决办法</p><p>经过验证解决了上述的问题。</p><p>所以这里需要说明一下，在对<code>cas server</code>做集群实现无状态化，需要注意一下几点：</p><ol><li><code>cas</code>的<code>ticket</code>需要做到集中存储，可以使用<code>redis</code>、<code>jpa</code>、或者其他方式，这个官方文章上有详细介绍：<a href="https://apereo.github.io/cas/5.0.x/installation/Configuration-Properties.html#ticket-registry" target="_blank" rel="noopener">ticket-registry</a></li><li><code>cas</code>的<code>session</code>信息需要做到集中存储，如果使用的是<code>tomcat</code>可以使用<a href="https://github.com/ran-jit/tomcat-cluster-redis-session-manager" target="_blank" rel="noopener">TomcatRedisSessionMananger</a>插件来通过redis做session集中存储。</li><li>需解决集群环境和springwebflow框架下CAS登录流程数据加密秘钥统一（或去除登录流程数据加密）-&gt; cas.webflow.encryption.key，cas.webflow.signing.key</li><li>需解决集群环境和springwebflow框架下CAS登录票据加密秘钥统一（或去除票据数据加密）-&gt; cas.tgc.cipherEnabled，cas.tgc.signingKey，cas.tgc.encryptionKey</li><li>最后一个就是需要接入<code>sso</code>的<code>client</code>应用端的<code>session</code>信息也需要做集中存储，因此<code>cas server</code>会和<code>client</code>进行通信去验证<code>ticket</code>，验证完后会生成信息并存储到<code>sesson</code>中，因此也需要使用<a href="https://github.com/ran-jit/tomcat-cluster-redis-session-manager" target="_blank" rel="noopener">TomcatRedisSessionMananger</a>插件来通过<code>redis</code>做<code>session</code>集中存储。</li></ol><p>世界和平、Keep Real！</p>]]></content>
    
    <summary type="html">
    
      Trouble Shooting —— CAS Server集群环境下TGC验证问题排查，需要开启会话保持
    
    </summary>
    
      <category term="CAS" scheme="https://ningyu1.github.io/categories/CAS/"/>
    
    
      <category term="trouble shooting" scheme="https://ningyu1.github.io/tags/trouble-shooting/"/>
    
      <category term="CAS" scheme="https://ningyu1.github.io/tags/CAS/"/>
    
      <category term="iphash" scheme="https://ningyu1.github.io/tags/iphash/"/>
    
      <category term="TomcatRedisSessionManager" scheme="https://ningyu1.github.io/tags/TomcatRedisSessionManager/"/>
    
      <category term="Invalid cookie. Required remote address does not match ip" scheme="https://ningyu1.github.io/tags/Invalid-cookie-Required-remote-address-does-not-match-ip/"/>
    
  </entry>
  
  <entry>
    <title>Json序列化、反序列化支持泛型，Dubbo对泛型参数方法进行反射调用</title>
    <link href="https://ningyu1.github.io/20180313/69-java-reflect.html"/>
    <id>https://ningyu1.github.io/20180313/69-java-reflect.html</id>
    <published>2018-03-13T07:31:53.000Z</published>
    <updated>2024-04-23T01:48:15.036Z</updated>
    
    <content type="html"><![CDATA[<p>最近在对Dubbo接口进行反射调用时，遇到了参数类型较为复杂的情况下，使用反射方式无法调用的问题。</p><p>由于Dubbo使用了proxy代理对象，因此在反射上调用是存在一定的问题，从反射对象上获取的方法和参数类型可能会导致无法正常的调用。</p><p>首先先让我们看一个复杂参数的接口定义</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">public String testMethod(Map&lt;String,ResourceVo&gt; map, List&lt;Map&lt;String,ResourceVo&gt;&gt; list) throws BizException;</span><br></pre></td></tr></table></figure><h2 id="Gson反序列化复杂类型"><a href="#Gson反序列化复杂类型" class="headerlink" title="Gson反序列化复杂类型"></a>Gson反序列化复杂类型</h2><p>在对参数进行反序列化时，内部的类型容易丢失，我们可以使用gson的Type进行反序列化得到正确的参数值，让我们看一下gson反序列化的两个方法</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line"> /**</span><br><span class="line">  * This method deserializes the specified Json into an object of the specified class. It is not</span><br><span class="line">  * suitable to use if the specified class is a generic type since it will not have the generic</span><br><span class="line">  * type information because of the Type Erasure feature of Java. Therefore, this method should not</span><br><span class="line">  * be used if the desired type is a generic type. Note that this method works fine if the any of</span><br><span class="line">  * the fields of the specified object are generics, just the object itself should not be a</span><br><span class="line">  * generic type. For the cases when the object is of generic type, invoke</span><br><span class="line">  * &#123;@link #fromJson(String, Type)&#125;. If you have the Json in a &#123;@link Reader&#125; instead of</span><br><span class="line">  * a String, use &#123;@link #fromJson(Reader, Class)&#125; instead.</span><br><span class="line">  *</span><br><span class="line">  * @param &lt;T&gt; the type of the desired object</span><br><span class="line">  * @param json the string from which the object is to be deserialized</span><br><span class="line">  * @param classOfT the class of T</span><br><span class="line">  * @return an object of type T from the string. Returns &#123;@code null&#125; if &#123;@code json&#125; is &#123;@code null&#125;.</span><br><span class="line">  * @throws JsonSyntaxException if json is not a valid representation for an object of type</span><br><span class="line">  * classOfT</span><br><span class="line">  */</span><br><span class="line"> public &lt;T&gt; T fromJson(String json, Class&lt;T&gt; classOfT) throws JsonSyntaxException &#123;</span><br><span class="line">   Object object = fromJson(json, (Type) classOfT);</span><br><span class="line">   return Primitives.wrap(classOfT).cast(object);</span><br><span class="line"> &#125;</span><br><span class="line"></span><br><span class="line">/**</span><br><span class="line">  * This method deserializes the specified Json into an object of the specified type. This method</span><br><span class="line">  * is useful if the specified object is a generic type. For non-generic objects, use</span><br><span class="line">  * &#123;@link #fromJson(String, Class)&#125; instead. If you have the Json in a &#123;@link Reader&#125; instead of</span><br><span class="line">  * a String, use &#123;@link #fromJson(Reader, Type)&#125; instead.</span><br><span class="line">  *</span><br><span class="line">  * @param &lt;T&gt; the type of the desired object</span><br><span class="line">  * @param json the string from which the object is to be deserialized</span><br><span class="line">  * @param typeOfT The specific genericized type of src. You can obtain this type by using the</span><br><span class="line">  * &#123;@link com.google.gson.reflect.TypeToken&#125; class. For example, to get the type for</span><br><span class="line">  * &#123;@code Collection&lt;Foo&gt;&#125;, you should use:</span><br><span class="line">  * &lt;pre&gt;</span><br><span class="line">  * Type typeOfT = new TypeToken&amp;lt;Collection&amp;lt;Foo&amp;gt;&amp;gt;()&#123;&#125;.getType();</span><br><span class="line">  * &lt;/pre&gt;</span><br><span class="line">  * @return an object of type T from the string. Returns &#123;@code null&#125; if &#123;@code json&#125; is &#123;@code null&#125;.</span><br><span class="line">  * @throws JsonParseException if json is not a valid representation for an object of type typeOfT</span><br><span class="line">  * @throws JsonSyntaxException if json is not a valid representation for an object of type</span><br><span class="line">  */</span><br><span class="line"> @SuppressWarnings(&quot;unchecked&quot;)</span><br><span class="line"> public &lt;T&gt; T fromJson(String json, Type typeOfT) throws JsonSyntaxException &#123;</span><br><span class="line">   if (json == null) &#123;</span><br><span class="line">     return null;</span><br><span class="line">   &#125;</span><br><span class="line">   StringReader reader = new StringReader(json);</span><br><span class="line">   T target = (T) fromJson(reader, typeOfT);</span><br><span class="line">   return target;</span><br><span class="line"> &#125;</span><br></pre></td></tr></table></figure><p>让我们测试一下复杂接口参数在使用这两个方法反序列化会有什么不同</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">String json = &quot;&#123;\&quot;name\&quot;:\&quot;name\&quot;,\&quot;value\&quot;:&#123;\&quot;service\&quot;:\&quot;test1\&quot;,\&quot;url\&quot;:\&quot;test\&quot;,\&quot;action\&quot;:\&quot;GET\&quot;,\&quot;enabled\&quot;:true,\&quot;isPublic\&quot;:false,\&quot;appId\&quot;:8,\&quot;menuId\&quot;:30001&#125;&#125;&quot;;</span><br><span class="line">Class clazz = Map.class;</span><br><span class="line">Map map = gson.fromJson(json, clazz);</span><br></pre></td></tr></table></figure><p>上面代码反序列化后的map对象实际是<code>com.google.gson.internal.LinkedTreeMap&lt;K, V&gt;</code>，这个是gson中自定义的Map实现类，而且内部的对象也都是<code>LinkedTreeMap</code>，当我们换成HashMap时，返回的结果都是<code>HashMap</code>，但是我们的方法上使用的是<code>Map&lt;String,ResourceVo&gt;</code>，如何才能反序列化得到这个类型的对象呢？让我们看一下使用Type后的情况。</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">String json = &quot;&#123;\&quot;name\&quot;:\&quot;name\&quot;,\&quot;value\&quot;:&#123;\&quot;service\&quot;:\&quot;test1\&quot;,\&quot;url\&quot;:\&quot;test\&quot;,\&quot;action\&quot;:\&quot;GET\&quot;,\&quot;enabled\&quot;:true,\&quot;isPublic\&quot;:false,\&quot;appId\&quot;:8,\&quot;menuId\&quot;:30001&#125;&#125;&quot;;</span><br><span class="line">Type type = new TypeToken&lt;ResourceVo&gt;()&#123;&#125;.getType();</span><br><span class="line">Map&lt;ResourceVo&gt; map = gson.fromJson(json, type);</span><br></pre></td></tr></table></figure><p>通过使用TypeToken生成的Type对象可以得到<code>Map&lt;String,ResourceVo&gt;</code>这个类型的实例，但是当我们在反射调用方法时，由于不知道参数是什么类型，也不能够import自定义的对象来使用<code>TypeToken</code>来获取type对象，那我们应该怎么做呢？接着往下看</p><p><span style="color:blue">ps.类型：List&#60;ResourceVo&gt;和List&#60;Map&#60;Object,ResourceVo&gt;&gt;这样的类型一样使用Type来进行反序列化</span></p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">String json = &quot;&#123;\&quot;name\&quot;:\&quot;name\&quot;,\&quot;value\&quot;:&#123;\&quot;service\&quot;:\&quot;test1\&quot;,\&quot;url\&quot;:\&quot;test\&quot;,\&quot;action\&quot;:\&quot;GET\&quot;,\&quot;enabled\&quot;:true,\&quot;isPublic\&quot;:false,\&quot;appId\&quot;:8,\&quot;menuId\&quot;:30001&#125;&#125;&quot;;</span><br><span class="line">Class clazz = Class.forName(&quot;com.package.JavaBean&quot;);</span><br><span class="line">String methodName = &quot;testMethod&quot;;</span><br><span class="line">Method[] methods = clazz.getMethods();</span><br><span class="line">for (Method m : methods) &#123;</span><br><span class="line">if (m.getName().equals(methodName)) &#123;</span><br><span class="line">Type[] paramTypes = m.getGenericParameterTypes();</span><br><span class="line">for (int j = 0; j &lt; paramTypes.length; j++) &#123;</span><br><span class="line">gson.fromJson(json, paramTypes[j]);</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>method.getGenericParameterTypes()</code>获取参数的<code>Type</code>对象。</p><p><span style="color:blue">但是需要注意的是，当使用<code>Proxy</code>代理对象通过上面的方式获取的<code>Type</code>对象全都是<code>java.lang.Class</code></span></p><p>那如何解决代理对象获取的<code>Type</code>不正确的问题呢？</p><p>正确的做法就是放弃通过<code>Proxy</code>对象来进行反射，使用<code>Class.forName</code>获取<code>Class</code>对象进行反射。</p><p>可以通过<code>Class.forName</code>的方式获取<code>Class</code>对象，再获取<code>Method</code>对象，最后通过<code>Method.getGenericParameterTypes()</code>获取正确的<code>Type</code>对象，这个步骤是构造方法的参数类型和参数值。但是通过这个方式构造出来的参数类型和参数值，无法通过<code>proxy</code>对象来进行<code>method.invoke</code>，其原因就是原始接口的方法参数定义和代理对象的方法参数定义不同导致。这让我们如何是好。</p><p>继续往下看。</p><h2 id="Dubbo泛化调用"><a href="#Dubbo泛化调用" class="headerlink" title="Dubbo泛化调用"></a>Dubbo泛化调用</h2><p>通过<code>Dubbo</code>的官网文档找到<code>Dubbo</code>支持<code>GenericService</code>泛化调用，什么是泛化调用？</p><p>泛化接口调用方式主要用于客户端没有 <code>API</code> 接口及模型类元的情况，参数及返回值中的所有 <code>POJO</code> 均用 <code>Map</code> 表示，通常用于框架集成，比如：实现一个通用的服务测试框架，可通过 <code>GenericService</code> 调用所有服务实现。</p><p><span style="color:blue">ps. <code>GenericService</code>实际上是<code>Dubbo</code>提供的通用接口，解决使用通用接口调用任何服务方法</span></p><p>这样我们就可以使用前面说到的参数反序列化方式来获取方法的参数类型和参数值，传入<code>GenericService</code>通用接口来对目标方法进行调用。</p><p>首先先让我们看一下<code>Dubbo</code>的泛化调用如何使用。</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">import com.alibaba.dubbo.rpc.config.ApplicationConfig;</span><br><span class="line">import com.alibaba.dubbo.rpc.config.RegistryConfig;</span><br><span class="line">import com.alibaba.dubbo.rpc.config.ConsumerConfig;</span><br><span class="line">import com.alibaba.dubbo.rpc.config.ReferenceConfig;</span><br><span class="line"></span><br><span class="line">Class clazz = Class.forName(&quot;com.package.JavaBean&quot;);</span><br><span class="line">String method = &quot;testMethod&quot;</span><br><span class="line">// 当前应用配置</span><br><span class="line">ApplicationConfig application = new ApplicationConfig();</span><br><span class="line">application.setName(&quot;yyy&quot;);</span><br><span class="line">// 连接注册中心配置</span><br><span class="line">RegistryConfig registry = new RegistryConfig();</span><br><span class="line">registry.setAddress(&quot;10.20.130.230:9090&quot;);</span><br><span class="line">// 注意：ReferenceConfig为重对象，内部封装了与注册中心的连接，以及与服务提供方的连接</span><br><span class="line">// 引用远程服务</span><br><span class="line">ReferenceConfig reference = new ReferenceConfig(); // 此实例很重，封装了与注册中心的连接以及与提供者的连接，请自行缓存，否则可能造成内存和连接泄漏</span><br><span class="line">reference.setApplication(application);</span><br><span class="line">reference.setRegistry(registry); // 多个注册中心可以用setRegistries()</span><br><span class="line">reference.setInterface(clazz);</span><br><span class="line">reference.setVersion(&quot;1.0.0&quot;);</span><br><span class="line">reference.setRetries(0);</span><br><span class="line">reference.setCluster(&quot;failfast&quot;);</span><br><span class="line">reference.setTimeout(12001);</span><br><span class="line">reference.setGeneric(true);</span><br><span class="line">GenericService genericService = (GenericService) reference.get();</span><br><span class="line">Object result = genericService.$invoke(method, parameterTypes, parameterValues);</span><br></pre></td></tr></table></figure><p>只要给<code>reference</code>设置<code>generic</code>为<code>true</code>就可以使用<code>GenericService</code>通用接口来进行方法调用。</p><p>这样我们就可以顺利的完成任何参数类型方法的反射调用。</p><p><span style="color:blue">从而避免了通过<code>Proxy</code>代理类获取到不正确的参数<code>Type</code>导致反序列化参数失败，这个原因前面也说了是因为原始接口的方法参数定义和代理对象的方法参数定义不同导致。</span></p><p>接下来让我们看一下具体的实现</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">@SuppressWarnings(&#123;&quot;unchecked&quot;, &quot;rawtypes&quot;, &quot;static-access&quot;&#125;)</span><br><span class="line">private Object callDubbo(SampleResult res) &#123;</span><br><span class="line">    ApplicationConfig application = new ApplicationConfig();</span><br><span class="line">    application.setName(&quot;DubboSample&quot;);</span><br><span class="line">    </span><br><span class="line">    // 此实例很重，封装了与注册中心的连接以及与提供者的连接，请自行缓存，否则可能造成内存和连接泄漏</span><br><span class="line">    ReferenceConfig reference = new ReferenceConfig();</span><br><span class="line">    // 引用远程服务</span><br><span class="line">    reference.setApplication(application);</span><br><span class="line">    RegistryConfig registry = null;</span><br><span class="line">    </span><br><span class="line">    String protocol = getProtocol();</span><br><span class="line">    if (&quot;zookeeper&quot;.equals(protocol)) &#123;</span><br><span class="line">        // 连接注册中心配置</span><br><span class="line">        registry = new RegistryConfig();</span><br><span class="line">        registry.setProtocol(&quot;zookeeper&quot;);</span><br><span class="line">        registry.setAddress(getAddress());</span><br><span class="line">        reference.setRegistry(registry); // 多个注册中心可以用setRegistries()</span><br><span class="line">    &#125; else &#123;</span><br><span class="line">        StringBuffer sb = new StringBuffer();</span><br><span class="line">        sb.append(protocol).append(&quot;://&quot;).append(getAddress()).append(&quot;/&quot;).append(getInterface());</span><br><span class="line">        log.info(&quot;rpc invoker url : &quot; + sb.toString());</span><br><span class="line">        reference.setUrl(sb.toString());</span><br><span class="line">    &#125;</span><br><span class="line">    try &#123;</span><br><span class="line">        Class clazz = Class.forName(getInterface());</span><br><span class="line">        reference.setInterface(clazz);</span><br><span class="line">        reference.setRetries(Integer.valueOf(getRetries()));</span><br><span class="line">        reference.setCluster(getCluster());</span><br><span class="line">        reference.setVersion(getVersion());</span><br><span class="line">        reference.setTimeout(Integer.valueOf(getTimeout()));</span><br><span class="line">        reference.setGeneric(true);</span><br><span class="line">        GenericService genericService = (GenericService) reference.get();</span><br><span class="line">        Method method = null;</span><br><span class="line">        String[] parameterTypes = null;</span><br><span class="line">        Object[] parameterValues = null;</span><br><span class="line">        List&lt;MethodArgument&gt; args = getMethodArgs();</span><br><span class="line">        List&lt;String&gt; paramterTypeList = null;</span><br><span class="line">        List&lt;Object&gt; parameterValuesList = null;</span><br><span class="line">        Method[] methods = clazz.getMethods();</span><br><span class="line">for (int i = 0; i &lt; methods.length; i++) &#123;</span><br><span class="line">Method m = methods[i];</span><br><span class="line">Type[] paramTypes = m.getGenericParameterTypes();</span><br><span class="line">paramterTypeList = new ArrayList&lt;String&gt;();</span><br><span class="line">parameterValuesList = new ArrayList&lt;Object&gt;();</span><br><span class="line">log.info(&quot;paramTypes.length=&quot;+paramTypes.length+&quot;|args.size()=&quot;+args.size());</span><br><span class="line">if (m.getName().equals(getMethod()) &amp;&amp; paramTypes.length == args.size()) &#123;</span><br><span class="line">//名称与参数数量匹配，进行参数类型转换</span><br><span class="line">for (int j = 0; j &lt; paramTypes.length; j++) &#123;</span><br><span class="line">paramterTypeList.add(args.get(j).getParamType());</span><br><span class="line">ClassUtils.parseParameter(paramTypes[j], parameterValuesList, args.get(j));</span><br><span class="line">&#125;</span><br><span class="line">if (parameterValuesList.size() == paramTypes.length) &#123;</span><br><span class="line">//没有转换错误，数量应该一致</span><br><span class="line">method = m;</span><br><span class="line">break;</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line">        if (method == null) &#123;</span><br><span class="line">            res.setSuccessful(false);</span><br><span class="line">            return &quot;Method[&quot;+getMethod()+&quot;] Not found!&quot;;</span><br><span class="line">        &#125;</span><br><span class="line">        //发起调用</span><br><span class="line">        parameterTypes = paramterTypeList.toArray(new String[paramterTypeList.size()]);</span><br><span class="line">        parameterValues = parameterValuesList.toArray(new Object[parameterValuesList.size()]);</span><br><span class="line">        Object result = null;</span><br><span class="line">try &#123;</span><br><span class="line">result = genericService.$invoke(getMethod(), parameterTypes, parameterValues);</span><br><span class="line">res.setSuccessful(true);</span><br><span class="line">&#125; catch (Throwable e) &#123;</span><br><span class="line">log.error(&quot;接口返回异常：&quot;, e);</span><br><span class="line">res.setSuccessful(false);</span><br><span class="line">result = e;</span><br><span class="line">&#125;</span><br><span class="line">        return result;</span><br><span class="line">    &#125; catch (Exception e) &#123;</span><br><span class="line">        log.error(&quot;调用dubbo接口出错：&quot;, e);</span><br><span class="line">        res.setSuccessful(false);</span><br><span class="line">        return e;</span><br><span class="line">    &#125; finally &#123;</span><br><span class="line">        if (registry != null) &#123;</span><br><span class="line">            registry.destroyAll();</span><br><span class="line">        &#125;</span><br><span class="line">        reference.destroy();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">## ClassUtils.parseParameter方法代码</span><br><span class="line"></span><br><span class="line">public static void parseParameter(Type type,</span><br><span class="line">List&lt;Object&gt; parameterValuesList, MethodArgument arg)</span><br><span class="line">throws ClassNotFoundException &#123;</span><br><span class="line">String className = getClassName(type);</span><br><span class="line">if (className.equals(&quot;int&quot;)) &#123;</span><br><span class="line">parameterValuesList.add(Integer.parseInt(arg.getParamValue()));</span><br><span class="line">&#125; else if (className.equals(&quot;double&quot;)) &#123;</span><br><span class="line">parameterValuesList.add(Double.parseDouble(arg.getParamValue()));</span><br><span class="line">&#125; else if (className.equals(&quot;short&quot;)) &#123;</span><br><span class="line">parameterValuesList.add(Short.parseShort(arg.getParamValue()));</span><br><span class="line">&#125; else if (className.equals(&quot;float&quot;)) &#123;</span><br><span class="line">parameterValuesList.add(Float.parseFloat(arg.getParamValue()));</span><br><span class="line">&#125; else if (className.equals(&quot;long&quot;)) &#123;</span><br><span class="line">parameterValuesList.add(Long.parseLong(arg.getParamValue()));</span><br><span class="line">&#125; else if (className.equals(&quot;byte&quot;)) &#123;</span><br><span class="line">parameterValuesList.add(Byte.parseByte(arg.getParamValue()));</span><br><span class="line">&#125; else if (className.equals(&quot;boolean&quot;)) &#123;</span><br><span class="line">parameterValuesList.add(Boolean.parseBoolean(arg.getParamValue()));</span><br><span class="line">&#125; else if (className.equals(&quot;char&quot;)) &#123;</span><br><span class="line">parameterValuesList.add(arg.getParamValue().charAt(0));</span><br><span class="line">&#125; else if (className.equals(&quot;java.lang.String&quot;)</span><br><span class="line">|| className.equals(&quot;String&quot;) || className.equals(&quot;string&quot;)) &#123;</span><br><span class="line">parameterValuesList.add(String.valueOf(arg.getParamValue()));</span><br><span class="line">&#125; else if (className.equals(&quot;java.lang.Integer&quot;)</span><br><span class="line">|| className.equals(&quot;Integer&quot;) || className.equals(&quot;integer&quot;)) &#123;</span><br><span class="line">parameterValuesList.add(Integer.valueOf(arg.getParamValue()));</span><br><span class="line">&#125; else if (className.equals(&quot;java.lang.Double&quot;)</span><br><span class="line">|| className.equals(&quot;Double&quot;)) &#123;</span><br><span class="line">parameterValuesList.add(Double.valueOf(arg.getParamValue()));</span><br><span class="line">&#125; else if (className.equals(&quot;java.lang.Short&quot;)</span><br><span class="line">|| className.equals(&quot;Short&quot;)) &#123;</span><br><span class="line">parameterValuesList.add(Short.valueOf(arg.getParamValue()));</span><br><span class="line">&#125; else if (className.equals(&quot;java.lang.Long&quot;)</span><br><span class="line">|| className.equals(&quot;Long&quot;)) &#123;</span><br><span class="line">parameterValuesList.add(Long.valueOf(arg.getParamValue()));</span><br><span class="line">&#125; else if (className.equals(&quot;java.lang.Float&quot;)</span><br><span class="line">|| className.equals(&quot;Float&quot;)) &#123;</span><br><span class="line">parameterValuesList.add(Float.valueOf(arg.getParamValue()));</span><br><span class="line">&#125; else if (className.equals(&quot;java.lang.Byte&quot;)</span><br><span class="line">|| className.equals(&quot;Byte&quot;)) &#123;</span><br><span class="line">parameterValuesList.add(Byte.valueOf(arg.getParamValue()));</span><br><span class="line">&#125; else if (className.equals(&quot;java.lang.Boolean&quot;)</span><br><span class="line">|| className.equals(&quot;Boolean&quot;)) &#123;</span><br><span class="line">parameterValuesList.add(Boolean.valueOf(arg.getParamValue()));</span><br><span class="line">&#125; else &#123;</span><br><span class="line">parameterValuesList.add(JsonUtils.formJson(arg.getParamValue(),</span><br><span class="line">type));</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">## JsonUtils.formJson方法代码</span><br><span class="line"></span><br><span class="line">public static &lt;T&gt; T formJson(String json, Type type) &#123;</span><br><span class="line">try &#123;</span><br><span class="line">return gson.fromJson(json, type);</span><br><span class="line">&#125; catch (JsonSyntaxException e) &#123;</span><br><span class="line">logger.error(&quot;json to class[&quot; + type.getClass().getName()</span><br><span class="line">+ &quot;] is error!&quot;, e);</span><br><span class="line">&#125;</span><br><span class="line">return null;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><ol><li>复杂参数类型：<code>Map&lt;Object, ResourceVo&gt;</code>、<code>List&lt;ResourceVo&gt;</code>、<code>List&lt;Map&lt;Object,ResourceVo&gt;&gt;</code>使用<code>gson.fromJson(json, classOfT)</code>反序列化会丢失内部的类型。通过使用<code>gson.fromJson(json, type)</code>方式可以得到正确的类型。</li><li>通过<code>Proxy</code>对象的<code>method.getGenericParameterTypes()</code>获取的<code>Type</code>值全部为<code>java.lang.Class</code>，我们需要的是<code>java.util.Map&lt;com.package.ResourceVo&gt;</code>。</li><li>使用<code>Class.forName</code>得到<code>Class</code>，再获取Method，再通过<code>method.getGenericParameterTypes()</code>获取我们想要的参数<code>Type</code>是：<code>java.util.Map&lt;com.package.ResourceVo&gt;</code></li><li>通过<code>Class.forName</code>得到<code>Class</code>，再获取Method，再通过<code>method.getGenericParameterTypes()</code>构造出来的参数类型和参数值，无法通过<code>Proxy</code>代理对象来进行<code>method.invoke</code>，其原因是：原始接口的方法参数定义和代理对象的方法参数定义不同导致。</li><li>放弃通过<code>Proxy</code>对象的<code>method.invoke</code>方式调用接口，通过<code>Dubbo</code>的通用服务接口（<code>GenericService</code>）来调用任何服务接口方法：<code>GenericService.$invoke(method, parameterTypes, args)</code></li></ol><p>参数对照参考表如下</p><table><thead><tr><th style="text-align:left">Java类型</th><th style="text-align:left">paramType</th><th style="text-align:left">paramValue</th></tr></thead><tbody><tr><td style="text-align:left">int</td><td style="text-align:left">int</td><td style="text-align:left">1</td></tr><tr><td style="text-align:left">double</td><td style="text-align:left">double</td><td style="text-align:left">1.2</td></tr><tr><td style="text-align:left">short</td><td style="text-align:left">short</td><td style="text-align:left">1</td></tr><tr><td style="text-align:left">float</td><td style="text-align:left">float</td><td style="text-align:left">1.2</td></tr><tr><td style="text-align:left">long</td><td style="text-align:left">long</td><td style="text-align:left">1</td></tr><tr><td style="text-align:left">byte</td><td style="text-align:left">byte</td><td style="text-align:left">字节</td></tr><tr><td style="text-align:left">boolean</td><td style="text-align:left">boolean</td><td style="text-align:left">true或false</td></tr><tr><td style="text-align:left">char</td><td style="text-align:left">char</td><td style="text-align:left">A，如果字符过长取值为：”STR”.charAt(0)</td></tr><tr><td style="text-align:left">java.lang.String</td><td style="text-align:left">java.lang.String或String或string</td><td style="text-align:left">字符串</td></tr><tr><td style="text-align:left">java.lang.Integer</td><td style="text-align:left">java.lang.Integer或Integer或integer</td><td style="text-align:left">1</td></tr><tr><td style="text-align:left">java.lang.Double</td><td style="text-align:left">java.lang.Double或Double</td><td style="text-align:left">1.2</td></tr><tr><td style="text-align:left">java.lang.Short</td><td style="text-align:left">java.lang.Short或Short</td><td style="text-align:left">1</td></tr><tr><td style="text-align:left">java.lang.Long</td><td style="text-align:left">java.lang.Long或Long</td><td style="text-align:left">1</td></tr><tr><td style="text-align:left">java.lang.Float</td><td style="text-align:left">java.lang.Float或Float</td><td style="text-align:left">1.2</td></tr><tr><td style="text-align:left">java.lang.Byte</td><td style="text-align:left">java.lang.Byte或Byte</td><td style="text-align:left">字节</td></tr><tr><td style="text-align:left">java.lang.Boolean</td><td style="text-align:left">java.lang.Boolean或Boolean</td><td style="text-align:left">true或false</td></tr><tr><td style="text-align:left">JavaBean</td><td style="text-align:left">com.package.Bean</td><td style="text-align:left">{“service”:”test1”,”url”:”test”,”action”:”GET”,”enabled”:true,”isPublic”:false,”appId”:8,”menuId”:30001}</td></tr><tr><td style="text-align:left">java.util.Map以及子类</td><td style="text-align:left">java.util.Map以及子类</td><td style="text-align:left">{“service”:”test1”,”url”:”test”,”action”:”GET”,”enabled”:true,”isPublic”:false,”appId”:8,”menuId”:30001}</td></tr><tr><td style="text-align:left">java.util.Map&#60;String,JavaBean&gt;</td><td style="text-align:left">java.util.Map</td><td style="text-align:left">{“name”:{“service”:”test1”,”url”:”test”,”action”:”GET”,”enabled”:true,”isPublic”:false,”appId”:8,”menuId”:30001},”value”:{“service”:”test1”,”url”:”test”,”action”:”GET”,”enabled”:true,”isPublic”:false,”appId”:8,”menuId”:30001}}</td></tr><tr><td style="text-align:left">java.util.HashMap&#60;Object,Object&gt;</td><td style="text-align:left">java.util.HashMap</td><td style="text-align:left">{“name”:{“service”:”test1”,”url”:”test”,”action”:”GET”,”enabled”:true,”isPublic”:false,”appId”:8,”menuId”:30001},”value”:{“service”:”test1”,”url”:”test”,”action”:”GET”,”enabled”:true,”isPublic”:false,”appId”:8,”menuId”:30001}}</td></tr><tr><td style="text-align:left">java.util.Collection以及子类</td><td style="text-align:left">java.util.Collection以及子类</td><td style="text-align:left">[“a”,”b”]</td></tr><tr><td style="text-align:left">java.util.List&#60;String&gt;</td><td style="text-align:left">java.util.List</td><td style="text-align:left">[“a”,”b”]</td></tr><tr><td style="text-align:left">java.util.List&#60;JavaBean&gt;</td><td style="text-align:left">java.util.List</td><td style="text-align:left">[{“service”:”test1”,”url”:”test”,”action”:”GET”,”enabled”:true,”isPublic”:false,”appId”:8,”menuId”:30001},{“service”:”test1”,”url”:”test”,”action”:”GET”,”enabled”:true,”isPublic”:false,”appId”:8,”menuId”:30001}]</td></tr><tr><td style="text-align:left">java.util.List&#60;Map&#60;Object, JavaBean&gt;&gt;</td><td style="text-align:left">java.util.List</td><td style="text-align:left">[{“name”:{“service”:”test1”,”url”:”test”,”action”:”GET”,”enabled”:true,”isPublic”:false,”appId”:8,”menuId”:30001},”value”:{“service”:”test1”,”url”:”test”,”action”:”GET”,”enabled”:true,”isPublic”:false,”appId”:8,”menuId”:30001}},{“name”:{“service”:”test1”,”url”:”test”,”action”:”GET”,”enabled”:true,”isPublic”:false,”appId”:8,”menuId”:30001},”value”:{“service”:”test1”,”url”:”test”,”action”:”GET”,”enabled”:true,”isPublic”:false,”appId”:8,”menuId”:30001}}]</td></tr><tr><td style="text-align:left">java.util.List&#60;Long&gt;</td><td style="text-align:left">java.util.List</td><td style="text-align:left">[1,2,3]</td></tr><tr><td style="text-align:left">java.util.ArrayList&#60;Object&gt;</td><td style="text-align:left">java.util.ArrayList</td><td style="text-align:left">[“ny”,1,true]</td></tr></tbody></table>]]></content>
    
    <summary type="html">
    
      Json序列化、反序列化支持泛型，Dubbo对泛型参数方法进行反射调用
    
    </summary>
    
      <category term="dubbo" scheme="https://ningyu1.github.io/categories/dubbo/"/>
    
      <category term="json" scheme="https://ningyu1.github.io/categories/dubbo/json/"/>
    
    
      <category term="dubbo" scheme="https://ningyu1.github.io/tags/dubbo/"/>
    
      <category term="gson" scheme="https://ningyu1.github.io/tags/gson/"/>
    
      <category term="json" scheme="https://ningyu1.github.io/tags/json/"/>
    
      <category term="GenericService" scheme="https://ningyu1.github.io/tags/GenericService/"/>
    
  </entry>
  
  <entry>
    <title>New Version V1.2.0, Dubbo Plugin for Apache JMeter</title>
    <link href="https://ningyu1.github.io/20180313/68-jmeter-plugin-dubbo-1.2.0.html"/>
    <id>https://ningyu1.github.io/20180313/68-jmeter-plugin-dubbo-1.2.0.html</id>
    <published>2018-03-13T05:18:21.000Z</published>
    <updated>2024-04-23T01:48:15.036Z</updated>
    
    <content type="html"><![CDATA[<h1 id="项目地址"><a href="#项目地址" class="headerlink" title="项目地址"></a>项目地址</h1><p>jmeter-plugin-dubbo项目已经transfer到dubbo group下</p><p><a href="https://github.com/dubbo/jmeter-plugins-dubbo" target="_blank" rel="noopener">github: jmeter-plugin-dubbo</a> </p><p><a href="https://gitee.com/ningyu/jmeter-plugins-dubbo" target="_blank" rel="noopener">码云: jmeter-plugin-dubbo</a></p><h1 id="V1-2-0"><a href="#V1-2-0" class="headerlink" title="V1.2.0"></a>V1.2.0</h1><ol><li>使用gson进行json序列化、反序列化</li><li>使用dubbo泛化调用方式重构反射调用方式</li><li>支持复杂类型、支持泛型，例如：”java.lang.List<resourcevo>,Map&lt;String,ResourceVo&gt; map,List&lt;Map&lt;String, ResourceVo&gt;&gt; list”</resourcevo></li></ol><p>本次版本主要对反射参数类型进行了增强，支持复杂类型、支持参数泛型，可以参考如下的参数对照表：</p><table><thead><tr><th style="text-align:left">Java类型</th><th style="text-align:left">paramType</th><th style="text-align:left">paramValue</th></tr></thead><tbody><tr><td style="text-align:left">int</td><td style="text-align:left">int</td><td style="text-align:left">1</td></tr><tr><td style="text-align:left">double</td><td style="text-align:left">double</td><td style="text-align:left">1.2</td></tr><tr><td style="text-align:left">short</td><td style="text-align:left">short</td><td style="text-align:left">1</td></tr><tr><td style="text-align:left">float</td><td style="text-align:left">float</td><td style="text-align:left">1.2</td></tr><tr><td style="text-align:left">long</td><td style="text-align:left">long</td><td style="text-align:left">1</td></tr><tr><td style="text-align:left">byte</td><td style="text-align:left">byte</td><td style="text-align:left">字节</td></tr><tr><td style="text-align:left">boolean</td><td style="text-align:left">boolean</td><td style="text-align:left">true或false</td></tr><tr><td style="text-align:left">char</td><td style="text-align:left">char</td><td style="text-align:left">A，如果字符过长取值为：”STR”.charAt(0)</td></tr><tr><td style="text-align:left">java.lang.String</td><td style="text-align:left">java.lang.String或String或string</td><td style="text-align:left">字符串</td></tr><tr><td style="text-align:left">java.lang.Integer</td><td style="text-align:left">java.lang.Integer或Integer或integer</td><td style="text-align:left">1</td></tr><tr><td style="text-align:left">java.lang.Double</td><td style="text-align:left">java.lang.Double或Double</td><td style="text-align:left">1.2</td></tr><tr><td style="text-align:left">java.lang.Short</td><td style="text-align:left">java.lang.Short或Short</td><td style="text-align:left">1</td></tr><tr><td style="text-align:left">java.lang.Long</td><td style="text-align:left">java.lang.Long或Long</td><td style="text-align:left">1</td></tr><tr><td style="text-align:left">java.lang.Float</td><td style="text-align:left">java.lang.Float或Float</td><td style="text-align:left">1.2</td></tr><tr><td style="text-align:left">java.lang.Byte</td><td style="text-align:left">java.lang.Byte或Byte</td><td style="text-align:left">字节</td></tr><tr><td style="text-align:left">java.lang.Boolean</td><td style="text-align:left">java.lang.Boolean或Boolean</td><td style="text-align:left">true或false</td></tr><tr><td style="text-align:left">JavaBean</td><td style="text-align:left">com.package.Bean</td><td style="text-align:left">{“service”:”test1”,”url”:”test-${__RandomString(5,12345,ids)}”,”action”:”GET”,”enabled”:true,”isPublic”:false,”appId”:8,”menuId”:30001}</td></tr><tr><td style="text-align:left">java.util.Map以及子类</td><td style="text-align:left">java.util.Map以及子类</td><td style="text-align:left">{“service”:”test1”,”url”:”test-${__RandomString(5,12345,ids)}”,”action”:”GET”,”enabled”:true,”isPublic”:false,”appId”:8,”menuId”:30001}</td></tr><tr><td style="text-align:left">java.util.Map&#60;String,JavaBean&gt;</td><td style="text-align:left">java.util.Map</td><td style="text-align:left">{“name”:{“service”:”test1”,”url”:”test-${__RandomString(5,12345,ids)}”,”action”:”GET”,”enabled”:true,”isPublic”:false,”appId”:8,”menuId”:30001},”value”:{“service”:”test1”,”url”:”test”,”action”:”GET”,”enabled”:true,”isPublic”:false,”appId”:8,”menuId”:30001}}</td></tr><tr><td style="text-align:left">java.util.HashMap&#60;Object,Object&gt;</td><td style="text-align:left">java.util.HashMap</td><td style="text-align:left">{“name”:{“service”:”test1”,”url”:”test-${__RandomString(5,12345,ids)}”,”action”:”GET”,”enabled”:true,”isPublic”:false,”appId”:8,”menuId”:30001},”value”:{“service”:”test1”,”url”:”test”,”action”:”GET”,”enabled”:true,”isPublic”:false,”appId”:8,”menuId”:30001}}</td></tr><tr><td style="text-align:left">java.util.Collection以及子类</td><td style="text-align:left">java.util.Collection以及子类</td><td style="text-align:left">[“a”,”b”]</td></tr><tr><td style="text-align:left">java.util.List&#60;String&gt;</td><td style="text-align:left">java.util.List</td><td style="text-align:left">[“a”,”b”]</td></tr><tr><td style="text-align:left">java.util.List&#60;JavaBean&gt;</td><td style="text-align:left">java.util.List</td><td style="text-align:left">[{“service”:”test1”,”url”:”test-${<strong>RandomString(5,12345,ids)}”,”action”:”GET”,”enabled”:true,”isPublic”:false,”appId”:8,”menuId”:30001},{“service”:”test1”,”url”:”test-${</strong>RandomString(5,12345,ids)}”,”action”:”GET”,”enabled”:true,”isPublic”:false,”appId”:8,”menuId”:30001}]</td></tr><tr><td style="text-align:left">java.util.List&#60;Map&#60;Object, JavaBean&gt;&gt;</td><td style="text-align:left">java.util.List</td><td style="text-align:left">[{“name”:{“service”:”test1”,”url”:”test-${<strong>RandomString(5,12345,ids)}”,”action”:”GET”,”enabled”:true,”isPublic”:false,”appId”:8,”menuId”:30001},”value”:{“service”:”test1”,”url”:”test”,”action”:”GET”,”enabled”:true,”isPublic”:false,”appId”:8,”menuId”:30001}},{“name”:{“service”:”test1”,”url”:”test-${</strong>RandomString(5,12345,ids)}”,”action”:”GET”,”enabled”:true,”isPublic”:false,”appId”:8,”menuId”:30001},”value”:{“service”:”test1”,”url”:”test”,”action”:”GET”,”enabled”:true,”isPublic”:false,”appId”:8,”menuId”:30001}}]</td></tr><tr><td style="text-align:left">java.util.List&#60;Long&gt;</td><td style="text-align:left">java.util.List</td><td style="text-align:left">[1,2,3]</td></tr><tr><td style="text-align:left">java.util.ArrayList&#60;Object&gt;</td><td style="text-align:left">java.util.ArrayList</td><td style="text-align:left">[“ny”,1,true]</td></tr></tbody></table>]]></content>
    
    <summary type="html">
    
      New Version V1.2.0, Dubbo Plugin for Apache JMeter
    
    </summary>
    
      <category term="test" scheme="https://ningyu1.github.io/categories/test/"/>
    
    
      <category term="dubbo" scheme="https://ningyu1.github.io/tags/dubbo/"/>
    
      <category term="jmeter" scheme="https://ningyu1.github.io/tags/jmeter/"/>
    
      <category term="test" scheme="https://ningyu1.github.io/tags/test/"/>
    
      <category term="Dubbo可视化测试工具" scheme="https://ningyu1.github.io/tags/Dubbo%E5%8F%AF%E8%A7%86%E5%8C%96%E6%B5%8B%E8%AF%95%E5%B7%A5%E5%85%B7/"/>
    
      <category term="Jmeter对Dubbo接口进行可视化压力测试" scheme="https://ningyu1.github.io/tags/Jmeter%E5%AF%B9Dubbo%E6%8E%A5%E5%8F%A3%E8%BF%9B%E8%A1%8C%E5%8F%AF%E8%A7%86%E5%8C%96%E5%8E%8B%E5%8A%9B%E6%B5%8B%E8%AF%95/"/>
    
      <category term="Dubbo Jmeter插件" scheme="https://ningyu1.github.io/tags/Dubbo-Jmeter%E6%8F%92%E4%BB%B6/"/>
    
  </entry>
  
  <entry>
    <title>Python项目生成requirements.txt的多种方式，用于类库迁移必备</title>
    <link href="https://ningyu1.github.io/20180309/67-python-requirements.html"/>
    <id>https://ningyu1.github.io/20180309/67-python-requirements.html</id>
    <published>2018-03-09T06:19:54.000Z</published>
    <updated>2024-04-23T01:48:15.036Z</updated>
    
    <content type="html"><![CDATA[<p>我相信任何软件程序都会有依赖的类库，尤其现在开源如此的火爆，很多轮子可以拿来直接使用不再需要自己再去开发（拿来主义者），这样大大的提高开发效率。<code>NPM</code>就是轮子最多的地方，哈哈！开个玩笑！</p><p>我们做开发时为何需要对依赖库进行管理？当依赖类库过多时，如何管理类库的版本？</p><p>我相信大家都知道怎么回答这个问题，为了更加规范管理项目结构，提高开发效率所以我们需要对依赖库进行管理，不管使用任何开发语言，如今都有依赖库的管理工具。</p><p>例如：<code>Java</code>有<code>Maven</code>、<code>Gradle</code>，<code>JS</code>有<code>NPM</code>，<code>Python</code>有<code>pip</code>、<code>easy_install</code>，<code>Linux</code>有<code>apt-get</code>、<code>yun</code> 等。</p><p>我们这里就对<code>Python</code>的依赖库管理来进一步说一说。</p><p><code>Python</code>提供通过<code>requirements.txt</code>文件来进行项目中依赖的三方库进行整体安装导入。</p><p>那首先让我们看一下<code>requirements.txt</code>的格式</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">requests==1.2.0</span><br><span class="line">Flask==0.10.1</span><br></pre></td></tr></table></figure><p><code>Python</code>安装依赖库使用<code>pip</code>可以很方便的安装，如果我们需要迁移一个项目，那我们就需要导出项目中依赖的所有三方类库的版本、名称等信息。</p><p>接下来就看<code>Python</code>项目如何根据<code>requirements.txt</code>文件来安装三方类库</p><h1 id="方法一：pip-freeze"><a href="#方法一：pip-freeze" class="headerlink" title="方法一：pip freeze"></a>方法一：pip freeze</h1><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">pip freeze &gt; requirements.txt</span><br></pre></td></tr></table></figure><p><code>pip freeze</code>命令输出的格式和<code>requirements.txt</code>文件内容格式完全一样，因此我们可以将<code>pip freeze</code>的内容输出到文件<code>requirements.txt</code>中。在其他机器上可以根据导出的<code>requirements.txt</code>进行包安装。</p><p>如果要安装<code>requirements.txt</code>中的类库内容，那么你可以执行</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">pip install -r requirements.txt</span><br></pre></td></tr></table></figure><p><span style="color:blue"><em>注意：<code>pip freeze</code>输出的是本地环境中所有三方包信息，但是会比<code>pip list</code>少几个包，因为<code>pip</code>，<code>wheel</code>，<code>setuptools</code>等包，是自带的而无法(<code>un</code>)<code>install</code>的，如果要显示所有包可以加上参数<code>-all</code>，即<code>pip freeze -all</code></em></span></p><h1 id="方法二：pipreqs"><a href="#方法二：pipreqs" class="headerlink" title="方法二：pipreqs"></a>方法二：pipreqs</h1><p>使用<code>pipreqs</code>生成<code>requirements.txt</code></p><p>首先先安装<code>pipreqs</code></p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">pip install pipreqs</span><br></pre></td></tr></table></figure><p>使用<code>pipreqs</code>生成<code>requirements.txt</code></p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">pipreqs requirements.txt</span><br></pre></td></tr></table></figure><p><span style="color:blue"><em>注意：<code>pipreqs</code>生成指定目录下的依赖类库</em></span></p><h1 id="上面两个方法的区别？"><a href="#上面两个方法的区别？" class="headerlink" title="上面两个方法的区别？"></a>上面两个方法的区别？</h1><p>使用<code>pip freeze</code>保存的是当前<code>Python</code>环境下所有的类库，如果你没有用<code>virtualenv</code>来对<code>Python</code>环境做虚拟化的话，类库就会很杂很多，在对项目进行迁移的时候我们只需关注项目中使用的类库，没有必要导出所有安装过的类库，因此我们一般迁移项目不会使用<code>pipreqs</code>，<code>pip freeze</code>更加适合迁移整个<code>python</code>环境下安装过的类库时使用。</p><p><span style="color:blue"><em>不知道<code>virtualenv</code>是什么或者不会使用它的可以查看：<a href="https://ningyu1.github.io/site/post/63-python-virtualenv/">《构建Python多个虚拟环境来进行不同版本开发之神器-virtualenv》</a></em></span></p><p>使用<code>pipreqs</code>它会根据当前目录下的项目的依赖来导出三方类库，因此常用与项目的迁移中。</p><p>这就是<code>pip freeze</code>、<code>pipreqs</code>的区别，前者是导出<code>Python</code>环境下所有安装的类库，后者导出项目中使用的类库。</p>]]></content>
    
    <summary type="html">
    
      Python项目生成requirements.txt的多种方式，用于类库迁移必备
    
    </summary>
    
      <category term="python" scheme="https://ningyu1.github.io/categories/python/"/>
    
    
      <category term="python" scheme="https://ningyu1.github.io/tags/python/"/>
    
      <category term="pip" scheme="https://ningyu1.github.io/tags/pip/"/>
    
      <category term="freeze" scheme="https://ningyu1.github.io/tags/freeze/"/>
    
      <category term="pipreqs" scheme="https://ningyu1.github.io/tags/pipreqs/"/>
    
  </entry>
  
  <entry>
    <title>Bug Fix Version V1.1.0, Dubbo Plugin for Apache JMeter</title>
    <link href="https://ningyu1.github.io/20180307/66-jmeter-plugin-dubbo-bugfix.html"/>
    <id>https://ningyu1.github.io/20180307/66-jmeter-plugin-dubbo-bugfix.html</id>
    <published>2018-03-07T10:00:54.000Z</published>
    <updated>2024-04-23T01:48:15.036Z</updated>
    
    <content type="html"><![CDATA[<p>首先先感谢网友 @流浪的云 提的bug，让我感觉到写这个工具没有白费还有点价值，非常感谢，</p><p>他在使用<code>jmeter-plugin-dubbo</code>插件时发现<code>GUI</code>中输入的信息无法使用<code>Jmeter</code>变量<code>${var}</code>与函数来进行参数化，以下是我修复这个问题的记录。</p><h1 id="项目地址"><a href="#项目地址" class="headerlink" title="项目地址"></a>项目地址</h1><p>jmeter-plugin-dubbo项目已经transfer到dubbo group下</p><p><a href="https://github.com/dubbo/jmeter-plugins-dubbo" target="_blank" rel="noopener">github: jmeter-plugin-dubbo</a> </p><p><a href="https://gitee.com/ningyu/jmeter-plugins-dubbo" target="_blank" rel="noopener">码云: jmeter-plugin-dubbo</a></p><h1 id="问题描述"><a href="#问题描述" class="headerlink" title="问题描述"></a>问题描述</h1><ol><li><code>jmeter-plugin-dubbo</code>插件<code>GUI</code>输入的信息无法使用<code>${var}</code>变量来进行参数化</li></ol><h1 id="问题修复"><a href="#问题修复" class="headerlink" title="问题修复"></a>问题修复</h1><p><code>Jmeter</code>的输出要想使用用户自定义变量、<code>CSV</code>变量、<code>BeanShell</code>、函数来进行参数化，必须将输入的参数通过<code>JMeterProperty</code>的子类<code>add</code>到<code>Jmeter</code>管理。如果使用的是<code>Swing</code>的<code>Bean</code>绑定机制可以很好的支持变量与函数参数化，如果是手写的GUI与Sample就需要注意这一点，可能写出来的插件不能使用变量<code>${var}</code>参数化。</p><p>我之前在处理参数值在GUI和Sample之间传递时，没有使用<code>org.apache.jmeter.testelement.property.JMeterProperty</code>系列子类来处理参数，因此变量无法支持，让我们来看一下区别。</p><p>先让我们看一下<code>org.apache.jmeter.testelement.property.JMeterProperty</code>都有哪些子类。</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/jmeter-plugins-dubbo/7.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p>我们之前使用的参数赋值是这样的：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">public String getVersion() &#123;</span><br><span class="line">    return this.getPropertyAsString(FIELD_DUBBO_VERSION, DEFAULT_VERSION);</span><br><span class="line">&#125;</span><br><span class="line">public void setVersion(String version) &#123;</span><br><span class="line">    this.setProperty(FIELD_DUBBO_VERSION, version);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这种方式是无法支持使用<code>${var}</code>变量来参数化赋值的（也就是动态赋值）。</p><p>我们应该给<code>setProperty</code>传入<code>JMeterProperty</code>的子类来支持变量参数化，如下：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">public String getVersion() &#123;</span><br><span class="line">    return this.getPropertyAsString(FIELD_DUBBO_VERSION, DEFAULT_VERSION);</span><br><span class="line">&#125;</span><br><span class="line">public void setVersion(String version) &#123;</span><br><span class="line">    this.setProperty(new StringProperty(FIELD_DUBBO_VERSION, version));</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><span style="color:blue"><em>ps.注意setProperty的使用不一样，这里使用的是new StringProperty</em></span></p><p>上面的参数还相对简单的普通字符串参数，当我们遇到集合或更加复杂的参数类型时如何处理？</p><p>我本以为使用<code>JMeterProperty</code>的子类<code>CollectionProperty</code>是可以让集合参数支持变量参数化的，结果测试下来没有任何用，传入的<code>${var}</code>变量，在运行的时候还是变量没有变成相应的值。</p><p>于是又换成<code>MapProperty</code>和<code>ObjectProperty</code>一样无法支持变量参数化。</p><p>查看<code>Jmeter Plugins</code>的<code>Http Sample</code>源码，看他是如何处理的。</p><h2 id="org-apache-jmeter-protocol-http-util-HTTPArgument源码"><a href="#org-apache-jmeter-protocol-http-util-HTTPArgument源码" class="headerlink" title="org.apache.jmeter.protocol.http.util.HTTPArgument源码"></a>org.apache.jmeter.protocol.http.util.HTTPArgument源码</h2><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">package org.apache.jmeter.protocol.http.util;</span><br><span class="line"></span><br><span class="line">import java.io.Serializable;</span><br><span class="line">import java.io.UnsupportedEncodingException;</span><br><span class="line">import java.net.URLDecoder;</span><br><span class="line">import java.util.LinkedList;</span><br><span class="line">import java.util.List;</span><br><span class="line">import org.apache.jmeter.config.Argument;</span><br><span class="line">import org.apache.jmeter.config.Arguments;</span><br><span class="line">import org.apache.jmeter.testelement.property.BooleanProperty;</span><br><span class="line">import org.apache.jmeter.testelement.property.JMeterProperty;</span><br><span class="line">import org.apache.jorphan.logging.LoggingManager;</span><br><span class="line">import org.apache.log.Logger;</span><br><span class="line"></span><br><span class="line">public class HTTPArgument</span><br><span class="line">  extends Argument</span><br><span class="line">  implements Serializable</span><br><span class="line">&#123;</span><br><span class="line">  private static final Logger log = ;</span><br><span class="line">  private static final long serialVersionUID = 240L;</span><br><span class="line">  private static final String ALWAYS_ENCODE = &quot;HTTPArgument.always_encode&quot;;</span><br><span class="line">  private static final String USE_EQUALS = &quot;HTTPArgument.use_equals&quot;;</span><br><span class="line">  private static final EncoderCache cache = new EncoderCache(1000);</span><br><span class="line">  </span><br><span class="line">  public HTTPArgument(String name, String value, String metadata)</span><br><span class="line">  &#123;</span><br><span class="line">    this(name, value, false);</span><br><span class="line">    setMetaData(metadata);</span><br><span class="line">  &#125;</span><br><span class="line">  </span><br><span class="line">  public void setUseEquals(boolean ue)</span><br><span class="line">  &#123;</span><br><span class="line">    if (ue) &#123;</span><br><span class="line">      setMetaData(&quot;=&quot;);</span><br><span class="line">    &#125; else &#123;</span><br><span class="line">      setMetaData(&quot;&quot;);</span><br><span class="line">    &#125;</span><br><span class="line">    setProperty(new BooleanProperty(&quot;HTTPArgument.use_equals&quot;, ue));</span><br><span class="line">  &#125;</span><br><span class="line">  </span><br><span class="line">  public boolean isUseEquals()</span><br><span class="line">  &#123;</span><br><span class="line">    boolean eq = getPropertyAsBoolean(&quot;HTTPArgument.use_equals&quot;);</span><br><span class="line">    if ((getMetaData().equals(&quot;=&quot;)) || ((getValue() != null) &amp;&amp; (getValue().length() &gt; 0)))</span><br><span class="line">    &#123;</span><br><span class="line">      setUseEquals(true);</span><br><span class="line">      return true;</span><br><span class="line">    &#125;</span><br><span class="line">    return eq;</span><br><span class="line">  &#125;</span><br><span class="line">  </span><br><span class="line">  public void setAlwaysEncoded(boolean ae)</span><br><span class="line">  &#123;</span><br><span class="line">    setProperty(new BooleanProperty(&quot;HTTPArgument.always_encode&quot;, ae));</span><br><span class="line">  &#125;</span><br><span class="line">  </span><br><span class="line">  public boolean isAlwaysEncoded()</span><br><span class="line">  &#123;</span><br><span class="line">    return getPropertyAsBoolean(&quot;HTTPArgument.always_encode&quot;);</span><br><span class="line">  &#125;</span><br><span class="line">  </span><br><span class="line">  public HTTPArgument(String name, String value)</span><br><span class="line">  &#123;</span><br><span class="line">    this(name, value, false);</span><br><span class="line">  &#125;</span><br><span class="line">  </span><br><span class="line">  public HTTPArgument(String name, String value, boolean alreadyEncoded)</span><br><span class="line">  &#123;</span><br><span class="line">    this(name, value, alreadyEncoded, &quot;UTF-8&quot;);</span><br><span class="line">  &#125;</span><br><span class="line">  </span><br><span class="line">  public HTTPArgument(String name, String value, boolean alreadyEncoded, String contentEncoding)</span><br><span class="line">  &#123;</span><br><span class="line">    setAlwaysEncoded(true);</span><br><span class="line">    if (alreadyEncoded) &#123;</span><br><span class="line">      try</span><br><span class="line">      &#123;</span><br><span class="line">        if (log.isDebugEnabled()) &#123;</span><br><span class="line">          log.debug(&quot;Decoding name, calling URLDecoder.decode with &apos;&quot; + name + &quot;&apos; and contentEncoding:&quot; + &quot;UTF-8&quot;);</span><br><span class="line">        &#125;</span><br><span class="line">        name = URLDecoder.decode(name, &quot;UTF-8&quot;);</span><br><span class="line">        if (log.isDebugEnabled()) &#123;</span><br><span class="line">          log.debug(&quot;Decoding value, calling URLDecoder.decode with &apos;&quot; + value + &quot;&apos; and contentEncoding:&quot; + contentEncoding);</span><br><span class="line">        &#125;</span><br><span class="line">        value = URLDecoder.decode(value, contentEncoding);</span><br><span class="line">      &#125;</span><br><span class="line">      catch (UnsupportedEncodingException e)</span><br><span class="line">      &#123;</span><br><span class="line">        log.error(contentEncoding + &quot; encoding not supported!&quot;);</span><br><span class="line">        throw new Error(e.toString(), e);</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    setName(name);</span><br><span class="line">    setValue(value);</span><br><span class="line">    setMetaData(&quot;=&quot;);</span><br><span class="line">  &#125;</span><br><span class="line">  </span><br><span class="line">  public HTTPArgument(String name, String value, String metaData, boolean alreadyEncoded)</span><br><span class="line">  &#123;</span><br><span class="line">    this(name, value, metaData, alreadyEncoded, &quot;UTF-8&quot;);</span><br><span class="line">  &#125;</span><br><span class="line">  </span><br><span class="line">  public HTTPArgument(String name, String value, String metaData, boolean alreadyEncoded, String contentEncoding)</span><br><span class="line">  &#123;</span><br><span class="line">    this(name, value, alreadyEncoded, contentEncoding);</span><br><span class="line">    setMetaData(metaData);</span><br><span class="line">  &#125;</span><br><span class="line">  </span><br><span class="line">  public HTTPArgument(Argument arg)</span><br><span class="line">  &#123;</span><br><span class="line">    this(arg.getName(), arg.getValue(), arg.getMetaData());</span><br><span class="line">  &#125;</span><br><span class="line">  </span><br><span class="line">  public HTTPArgument() &#123;&#125;</span><br><span class="line">  </span><br><span class="line">  public void setName(String newName)</span><br><span class="line">  &#123;</span><br><span class="line">    if ((newName == null) || (!newName.equals(getName()))) &#123;</span><br><span class="line">      super.setName(newName);</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">  </span><br><span class="line">  public String getEncodedValue()</span><br><span class="line">  &#123;</span><br><span class="line">    try</span><br><span class="line">    &#123;</span><br><span class="line">      return getEncodedValue(&quot;UTF-8&quot;);</span><br><span class="line">    &#125;</span><br><span class="line">    catch (UnsupportedEncodingException e)</span><br><span class="line">    &#123;</span><br><span class="line">      throw new Error(&quot;Should not happen: &quot; + e.toString());</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">  </span><br><span class="line">  public String getEncodedValue(String contentEncoding)</span><br><span class="line">    throws UnsupportedEncodingException</span><br><span class="line">  &#123;</span><br><span class="line">    if (isAlwaysEncoded()) &#123;</span><br><span class="line">      return cache.getEncoded(getValue(), contentEncoding);</span><br><span class="line">    &#125;</span><br><span class="line">    return getValue();</span><br><span class="line">  &#125;</span><br><span class="line">  </span><br><span class="line">  public String getEncodedName()</span><br><span class="line">  &#123;</span><br><span class="line">    if (isAlwaysEncoded()) &#123;</span><br><span class="line">      return cache.getEncoded(getName());</span><br><span class="line">    &#125;</span><br><span class="line">    return getName();</span><br><span class="line">  &#125;</span><br><span class="line">  </span><br><span class="line">  public static void convertArgumentsToHTTP(Arguments args)</span><br><span class="line">  &#123;</span><br><span class="line">    List&lt;Argument&gt; newArguments = new LinkedList();</span><br><span class="line">    for (JMeterProperty jMeterProperty : args.getArguments())</span><br><span class="line">    &#123;</span><br><span class="line">      Argument arg = (Argument)jMeterProperty.getObjectValue();</span><br><span class="line">      if (!(arg instanceof HTTPArgument)) &#123;</span><br><span class="line">        newArguments.add(new HTTPArgument(arg));</span><br><span class="line">      &#125; else &#123;</span><br><span class="line">        newArguments.add(arg);</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    args.removeAllArguments();</span><br><span class="line">    args.setArguments(newArguments);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="org-apache-jmeter-protocol-http-gui-HTTPArgumentsPanel源码"><a href="#org-apache-jmeter-protocol-http-gui-HTTPArgumentsPanel源码" class="headerlink" title="org.apache.jmeter.protocol.http.gui.HTTPArgumentsPanel源码"></a>org.apache.jmeter.protocol.http.gui.HTTPArgumentsPanel源码</h2><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">package org.apache.jmeter.protocol.http.gui;</span><br><span class="line"></span><br><span class="line">import java.awt.event.ActionEvent;</span><br><span class="line">import java.awt.event.ActionListener;</span><br><span class="line">import java.util.Iterator;</span><br><span class="line">import javax.swing.JMenuItem;</span><br><span class="line">import javax.swing.JPopupMenu;</span><br><span class="line">import javax.swing.JTable;</span><br><span class="line">import org.apache.commons.lang3.BooleanUtils;</span><br><span class="line">import org.apache.commons.lang3.StringUtils;</span><br><span class="line">import org.apache.jmeter.config.Argument;</span><br><span class="line">import org.apache.jmeter.config.Arguments;</span><br><span class="line">import org.apache.jmeter.config.gui.ArgumentsPanel;</span><br><span class="line">import org.apache.jmeter.protocol.http.util.HTTPArgument;</span><br><span class="line">import org.apache.jmeter.testelement.TestElement;</span><br><span class="line">import org.apache.jmeter.testelement.property.JMeterProperty;</span><br><span class="line">import org.apache.jmeter.util.JMeterUtils;</span><br><span class="line">import org.apache.jorphan.gui.GuiUtils;</span><br><span class="line">import org.apache.jorphan.gui.ObjectTableModel;</span><br><span class="line">import org.apache.jorphan.reflect.Functor;</span><br><span class="line"></span><br><span class="line">public class HTTPArgumentsPanel</span><br><span class="line">  extends ArgumentsPanel</span><br><span class="line">&#123;</span><br><span class="line">  private static final long serialVersionUID = 240L;</span><br><span class="line">  private static final String ENCODE_OR_NOT = &quot;encode?&quot;;</span><br><span class="line">  private static final String INCLUDE_EQUALS = &quot;include_equals&quot;;</span><br><span class="line">  </span><br><span class="line">  protected void initializeTableModel()</span><br><span class="line">  &#123;</span><br><span class="line">    this.tableModel = new ObjectTableModel(new String[] &#123; &quot;name&quot;, &quot;value&quot;, &quot;encode?&quot;, &quot;include_equals&quot; &#125;, HTTPArgument.class, new Functor[] &#123; new Functor(&quot;getName&quot;), new Functor(&quot;getValue&quot;), new Functor(&quot;isAlwaysEncoded&quot;), new Functor(&quot;isUseEquals&quot;) &#125;, new Functor[] &#123; new Functor(&quot;setName&quot;), new Functor(&quot;setValue&quot;), new Functor(&quot;setAlwaysEncoded&quot;), new Functor(&quot;setUseEquals&quot;) &#125;, new Class[] &#123; String.class, String.class, Boolean.class, Boolean.class &#125;);</span><br><span class="line">  &#125;</span><br><span class="line">  </span><br><span class="line">  public static boolean testFunctors()</span><br><span class="line">  &#123;</span><br><span class="line">    HTTPArgumentsPanel instance = new HTTPArgumentsPanel();</span><br><span class="line">    instance.initializeTableModel();</span><br><span class="line">    return instance.tableModel.checkFunctors(null, instance.getClass());</span><br><span class="line">  &#125;</span><br><span class="line">  </span><br><span class="line">  protected void sizeColumns(JTable table)</span><br><span class="line">  &#123;</span><br><span class="line">    GuiUtils.fixSize(table.getColumn(&quot;include_equals&quot;), table);</span><br><span class="line">    GuiUtils.fixSize(table.getColumn(&quot;encode?&quot;), table);</span><br><span class="line">  &#125;</span><br><span class="line">  </span><br><span class="line">  protected HTTPArgument makeNewArgument()</span><br><span class="line">  &#123;</span><br><span class="line">    HTTPArgument arg = new HTTPArgument(&quot;&quot;, &quot;&quot;);</span><br><span class="line">    arg.setAlwaysEncoded(false);</span><br><span class="line">    arg.setUseEquals(true);</span><br><span class="line">    return arg;</span><br><span class="line">  &#125;</span><br><span class="line">  </span><br><span class="line">  public HTTPArgumentsPanel()</span><br><span class="line">  &#123;</span><br><span class="line">    super(JMeterUtils.getResString(&quot;paramtable&quot;));</span><br><span class="line">    init();</span><br><span class="line">  &#125;</span><br><span class="line">  </span><br><span class="line">  public TestElement createTestElement()</span><br><span class="line">  &#123;</span><br><span class="line">    Arguments args = getUnclonedParameters();</span><br><span class="line">    super.configureTestElement(args);</span><br><span class="line">    return (TestElement)args.clone();</span><br><span class="line">  &#125;</span><br><span class="line">  </span><br><span class="line">  public Arguments getParameters()</span><br><span class="line">  &#123;</span><br><span class="line">    Arguments args = getUnclonedParameters();</span><br><span class="line">    return (Arguments)args.clone();</span><br><span class="line">  &#125;</span><br><span class="line">  </span><br><span class="line">  private Arguments getUnclonedParameters()</span><br><span class="line">  &#123;</span><br><span class="line">    stopTableEditing();</span><br><span class="line">    </span><br><span class="line">    Iterator&lt;HTTPArgument&gt; modelData = this.tableModel.iterator();</span><br><span class="line">    Arguments args = new Arguments();</span><br><span class="line">    while (modelData.hasNext())</span><br><span class="line">    &#123;</span><br><span class="line">      HTTPArgument arg = (HTTPArgument)modelData.next();</span><br><span class="line">      args.addArgument(arg);</span><br><span class="line">    &#125;</span><br><span class="line">    return args;</span><br><span class="line">  &#125;</span><br><span class="line">  </span><br><span class="line">  public void configure(TestElement el)</span><br><span class="line">  &#123;</span><br><span class="line">    super.configure(el);</span><br><span class="line">    if ((el instanceof Arguments))</span><br><span class="line">    &#123;</span><br><span class="line">      this.tableModel.clearData();</span><br><span class="line">      HTTPArgument.convertArgumentsToHTTP((Arguments)el);</span><br><span class="line">      for (JMeterProperty jMeterProperty : ((Arguments)el).getArguments())</span><br><span class="line">      &#123;</span><br><span class="line">        HTTPArgument arg = (HTTPArgument)jMeterProperty.getObjectValue();</span><br><span class="line">        this.tableModel.addRow(arg);</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    checkButtonsStatus();</span><br><span class="line">  &#125;</span><br><span class="line">  </span><br><span class="line">  protected boolean isMetaDataNormal(HTTPArgument arg)</span><br><span class="line">  &#123;</span><br><span class="line">    return (arg.getMetaData() == null) || (arg.getMetaData().equals(&quot;=&quot;)) || ((arg.getValue() != null) &amp;&amp; (arg.getValue().length() &gt; 0));</span><br><span class="line">  &#125;</span><br><span class="line">  </span><br><span class="line">  protected Argument createArgumentFromClipboard(String[] clipboardCols)</span><br><span class="line">  &#123;</span><br><span class="line">    HTTPArgument argument = makeNewArgument();</span><br><span class="line">    argument.setName(clipboardCols[0]);</span><br><span class="line">    if (clipboardCols.length &gt; 1)</span><br><span class="line">    &#123;</span><br><span class="line">      argument.setValue(clipboardCols[1]);</span><br><span class="line">      if (clipboardCols.length &gt; 2)</span><br><span class="line">      &#123;</span><br><span class="line">        argument.setAlwaysEncoded(Boolean.parseBoolean(clipboardCols[2]));</span><br><span class="line">        if (clipboardCols.length &gt; 3)</span><br><span class="line">        &#123;</span><br><span class="line">          Boolean useEqual = BooleanUtils.toBooleanObject(clipboardCols[3]);</span><br><span class="line">          </span><br><span class="line">          argument.setUseEquals(useEqual != null ? useEqual.booleanValue() : true);</span><br><span class="line">        &#125;</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    return argument;</span><br><span class="line">  &#125;</span><br><span class="line">  </span><br><span class="line">  private void init()</span><br><span class="line">  &#123;</span><br><span class="line">    JTable table = getTable();</span><br><span class="line">    JPopupMenu popupMenu = new JPopupMenu();</span><br><span class="line">    JMenuItem variabilizeItem = new JMenuItem(JMeterUtils.getResString(&quot;transform_into_variable&quot;));</span><br><span class="line">    variabilizeItem.addActionListener(new ActionListener()</span><br><span class="line">    &#123;</span><br><span class="line">      public void actionPerformed(ActionEvent e)</span><br><span class="line">      &#123;</span><br><span class="line">        HTTPArgumentsPanel.this.transformNameIntoVariable();</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;);</span><br><span class="line">    popupMenu.add(variabilizeItem);</span><br><span class="line">    table.setComponentPopupMenu(popupMenu);</span><br><span class="line">  &#125;</span><br><span class="line">  </span><br><span class="line">  private void transformNameIntoVariable()</span><br><span class="line">  &#123;</span><br><span class="line">    int[] rowsSelected = getTable().getSelectedRows();</span><br><span class="line">    for (int selectedRow : rowsSelected)</span><br><span class="line">    &#123;</span><br><span class="line">      String name = (String)this.tableModel.getValueAt(selectedRow, 0);</span><br><span class="line">      if (StringUtils.isNotBlank(name))</span><br><span class="line">      &#123;</span><br><span class="line">        name = name.trim();</span><br><span class="line">        name = name.replaceAll(&quot;\\$&quot;, &quot;_&quot;);</span><br><span class="line">        name = name.replaceAll(&quot;\\&#123;&quot;, &quot;_&quot;);</span><br><span class="line">        name = name.replaceAll(&quot;\\&#125;&quot;, &quot;_&quot;);</span><br><span class="line">        this.tableModel.setValueAt(&quot;$&#123;&quot; + name + &quot;&#125;&quot;, selectedRow, 1);</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>能发现它使用的是继承<code>Argument</code>来处理和<code>GUI</code>之间的参数传递，使用继承<code>ArgumentsPanel</code>来处理<code>GUI</code>页面，这个就是我们上面说的，通过<code>Swing</code>的<code>Bean</code>绑定机制来进行开发，很遗憾我们没有使用这种方式，如果要改成这种方式，整个代码结构都要修改，成本太大。</p><p>但是我们发现像<code>String</code>，<code>Integer</code>等这种普通类型的参数通过使用<code>JMeterProperty</code>的子类可以很好的支持变量参数化，那我们能不能将集合参数拉平来直接使用普通类型的参数来处理，我承认这种方式有点恶心。</p><h1 id="解决方式"><a href="#解决方式" class="headerlink" title="解决方式"></a>解决方式</h1><p>首先我们的集合参数有索引下标和总行数，每一行有两列，那就修改集合参数的赋值，代码如下：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">//标记集合参数前缀</span><br><span class="line">public static String FIELD_DUBBO_METHOD_ARGS = &quot;FIELD_DUBBO_METHOD_ARGS&quot;;</span><br><span class="line">//集合参数总数</span><br><span class="line">public static String FIELD_DUBBO_METHOD_ARGS_SIZE = &quot;FIELD_DUBBO_METHOD_ARGS_SIZE&quot;;</span><br><span class="line"></span><br><span class="line">public List&lt;MethodArgument&gt; getMethodArgs() &#123;</span><br><span class="line">int paramsSize = this.getPropertyAsInt(FIELD_DUBBO_METHOD_ARGS_SIZE, 0);</span><br><span class="line">List&lt;MethodArgument&gt; list = new ArrayList&lt;MethodArgument&gt;();</span><br><span class="line">for (int i = 1; i &lt;= paramsSize; i++) &#123;</span><br><span class="line">String paramType = this.getPropertyAsString(FIELD_DUBBO_METHOD_ARGS + &quot;_PARAM_TYPE&quot; + i);</span><br><span class="line">String paramValue = this.getPropertyAsString(FIELD_DUBBO_METHOD_ARGS + &quot;_PARAM_VALUE&quot; + i);</span><br><span class="line">MethodArgument args = new MethodArgument(paramType, paramValue);</span><br><span class="line">list.add(args);</span><br><span class="line">&#125;</span><br><span class="line">return list;</span><br><span class="line">&#125;</span><br><span class="line">public void setMethodArgs(List&lt;MethodArgument&gt; methodArgs) &#123;</span><br><span class="line">int size = methodArgs == null ? 0 : methodArgs.size();</span><br><span class="line">this.setProperty(new IntegerProperty(FIELD_DUBBO_METHOD_ARGS_SIZE, size));</span><br><span class="line">if (size &gt; 0) &#123;</span><br><span class="line">for (int i = 1; i &lt;= methodArgs.size(); i++) &#123;</span><br><span class="line">this.setProperty(new StringProperty(FIELD_DUBBO_METHOD_ARGS + &quot;_PARAM_TYPE&quot; + i, methodArgs.get(i-1).getParamType()));</span><br><span class="line">this.setProperty(new StringProperty(FIELD_DUBBO_METHOD_ARGS + &quot;_PARAM_VALUE&quot; + i, methodArgs.get(i-1).getParamValue()));</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 plain"><table><tr><td class="code"><pre><span class="line">FIELD_DUBBO_METHOD_ARGS_SIZE = 2</span><br><span class="line">FIELD_DUBBO_METHOD_ARGS_SIZE_PARAM_TYPE_1 = xx$&#123;var1&#125;xx </span><br><span class="line">FIELD_DUBBO_METHOD_ARGS_SIZE__PARAM_VALUE_1 = xx$&#123;var2&#125;xx </span><br><span class="line">FIELD_DUBBO_METHOD_ARGS_SIZE_PARAM_TYPE_2 = xx$&#123;var3&#125;xx </span><br><span class="line">FIELD_DUBBO_METHOD_ARGS_SIZE__PARAM_VALUE_2 = xx$&#123;var4&#125;xx</span><br></pre></td></tr></table></figure><p>让我们测试一下是否可用。</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/jmeter-plugins-dubbo/8.png" alt title>                </div>                <div class="image-caption"></div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/jmeter-plugins-dubbo/9.png" alt title>                </div>                <div class="image-caption"></div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/jmeter-plugins-dubbo/10.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p>测试结果<code>GUI</code>上所有的输入框均可以支持<code>Jmeter</code>变量<code>${var}</code>参数化.</p><p>我觉得应该还是更加完美的解决办法只不过我没有找到，有空了再细致研究一下<code>Jmeter</code>的插件开发的细节看看能否找到突破口。</p><p>再次感谢网友 @流浪的云 提的bug，非常感谢！感谢使用插件的朋友多提rp和bug，让我们来一起完善起来，感谢这个开放的世界，最后还是一句老话：世界和平，Keep Real!</p>]]></content>
    
    <summary type="html">
    
      Bug Fix Version V1.1.0, Dubbo Plugin for Apache JMeter
    
    </summary>
    
      <category term="test" scheme="https://ningyu1.github.io/categories/test/"/>
    
    
      <category term="dubbo" scheme="https://ningyu1.github.io/tags/dubbo/"/>
    
      <category term="jmeter" scheme="https://ningyu1.github.io/tags/jmeter/"/>
    
      <category term="test" scheme="https://ningyu1.github.io/tags/test/"/>
    
      <category term="Dubbo可视化测试工具" scheme="https://ningyu1.github.io/tags/Dubbo%E5%8F%AF%E8%A7%86%E5%8C%96%E6%B5%8B%E8%AF%95%E5%B7%A5%E5%85%B7/"/>
    
      <category term="Jmeter对Dubbo接口进行可视化压力测试" scheme="https://ningyu1.github.io/tags/Jmeter%E5%AF%B9Dubbo%E6%8E%A5%E5%8F%A3%E8%BF%9B%E8%A1%8C%E5%8F%AF%E8%A7%86%E5%8C%96%E5%8E%8B%E5%8A%9B%E6%B5%8B%E8%AF%95/"/>
    
      <category term="Dubbo Jmeter插件" scheme="https://ningyu1.github.io/tags/Dubbo-Jmeter%E6%8F%92%E4%BB%B6/"/>
    
  </entry>
  
  <entry>
    <title>Java中内部类使用注意事项，内部类对序列化与反序列化的影响</title>
    <link href="https://ningyu1.github.io/20180306/65-java-inner-class.html"/>
    <id>https://ningyu1.github.io/20180306/65-java-inner-class.html</id>
    <published>2018-03-06T08:50:17.000Z</published>
    <updated>2024-04-23T01:48:15.036Z</updated>
    
    <content type="html"><![CDATA[<p>现在很多服务架构都是微服务、分布式架构，开发模式也都是模块化开发，在分布式的开发方式下服务之间的调用不管是<code>RPC</code>还是<code>RESTful</code>或是其他<code>SOA</code>方案，均离不开序列化与反序列化，尤其是使用<code>Java</code>开发，<code>Bean</code>实现序列化接口几乎已经是必备的要求，而且这个要求已经纳入到很多大厂公司的开发规范中，开发规范中强制要求实现序列化接口和重写<code>toString</code>、<code>hashCode</code>方法。</p><p>前面提到了序列化与反序列化，那序列化与反序列化的对象就是开发人员写的<code>java bean</code>，不同的<code>java bean</code>会给序列化反序列化带来什么问题呢？接下来就让我们看一下内部类对序列化反序列化的影响。</p><p>在这之前我们先看一下常用的序列化工具：</p><ul><li>JavaSerialize </li><li>fastjson</li><li>dubbo json</li><li>google gson</li><li>google protoBuf</li><li>hessian</li><li>kryo</li><li>Avro</li><li>fast-serialization</li><li>jboss-serialization</li><li>jboss-marshalling-river</li><li>protostuff</li><li>msgpack-databind</li><li>json/jackson/databind </li><li>json/jackson/db-afterburner</li><li>xml/xstream+c</li><li>xml/jackson/databind-aalto</li></ul><p>工具太多了这里就不列了，让我们先做一个测试。</p><h1 id="测试"><a href="#测试" class="headerlink" title="测试"></a>测试</h1><h1 id="常规java-bean"><a href="#常规java-bean" class="headerlink" title="常规java bean"></a>常规java bean</h1><p>测试类：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">import java.io.Serializable;</span><br><span class="line"></span><br><span class="line">public class Test implements Serializable &#123;</span><br><span class="line">private static final long serialVersionUID = 2010307013874058143L;</span><br><span class="line">private String name;</span><br><span class="line"></span><br><span class="line">public String getName() &#123;</span><br><span class="line">return name;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">public void setName(String name) &#123;</span><br><span class="line">this.name = name;</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>调用序列化与反序列化:</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">public static String toJson(Object obj) &#123;</span><br><span class="line">    try &#123;</span><br><span class="line">        return JSON.json(obj);</span><br><span class="line">    &#125; catch (IOException e) &#123;</span><br><span class="line">        log.error(&quot;class to json is error!&quot;, e);</span><br><span class="line">    &#125;</span><br><span class="line">    return null;</span><br><span class="line">&#125;</span><br><span class="line">public static &lt;T&gt; T formJson(String json, Class&lt;T&gt; classOfT) &#123;</span><br><span class="line">    try &#123;</span><br><span class="line">        return JSON.parse(json, classOfT);</span><br><span class="line">    &#125; catch (ParseException e) &#123;</span><br><span class="line">        log.error(&quot;json to class is error! &quot;+classOfT.getName(), e);</span><br><span class="line">    &#125;</span><br><span class="line">    return null;</span><br><span class="line">&#125;</span><br><span class="line">public static void main(String[] args) &#123;</span><br><span class="line">Test test = new Test();</span><br><span class="line">System.out.println(toJson(test));</span><br><span class="line">String json = &quot;&#123;\&quot;name\&quot;:\&quot;test\&quot;&#125;&quot;;</span><br><span class="line">test = formJson(json, Test.class);</span><br><span class="line">System.out.println(test.getName());</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>输出：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">&#123;&quot;name&quot;:null&#125;</span><br><span class="line">test</span><br></pre></td></tr></table></figure><p>我们能看到不管是序列化还是反序列化都没有任何问题，我们这里测试使用了常用的<code>fastjson</code>、<code>dubbo json</code>做了测试。</p><h2 id="有内部类的java-bean"><a href="#有内部类的java-bean" class="headerlink" title="有内部类的java bean"></a>有内部类的java bean</h2><p>测试类：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">import java.io.Serializable;</span><br><span class="line"></span><br><span class="line">public class Test implements Serializable &#123;</span><br><span class="line">private static final long serialVersionUID = 2010307013874058143L;</span><br><span class="line">private String name;</span><br><span class="line">public String getName() &#123;</span><br><span class="line">return name;</span><br><span class="line">&#125;</span><br><span class="line">public void setName(String name) &#123;</span><br><span class="line">this.name = name;</span><br><span class="line">&#125;</span><br><span class="line">public C1 c1;</span><br><span class="line">public C1 getC1() &#123;</span><br><span class="line">return c1;</span><br><span class="line">&#125;</span><br><span class="line">public void setC1(C1 c1) &#123;</span><br><span class="line">this.c1 = c1;</span><br><span class="line">&#125;</span><br><span class="line">public class C1 &#123;</span><br><span class="line">public String name;</span><br><span class="line">public C1() &#123;</span><br><span class="line">&#125;</span><br><span class="line">public String getName() &#123;</span><br><span class="line">return name;</span><br><span class="line">&#125;</span><br><span class="line">public void setName(String name) &#123;</span><br><span class="line">this.name = name;</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 plain"><table><tr><td class="code"><pre><span class="line">public static String toJson(Object obj) &#123;</span><br><span class="line">    try &#123;</span><br><span class="line">        return JSON.json(obj);</span><br><span class="line">    &#125; catch (IOException e) &#123;</span><br><span class="line">        log.error(&quot;class to json is error!&quot;, e);</span><br><span class="line">    &#125;</span><br><span class="line">    return null;</span><br><span class="line">&#125;</span><br><span class="line">public static &lt;T&gt; T formJson(String json, Class&lt;T&gt; classOfT) &#123;</span><br><span class="line">    try &#123;</span><br><span class="line">        return JSON.parse(json, classOfT);</span><br><span class="line">    &#125; catch (ParseException e) &#123;</span><br><span class="line">        log.error(&quot;json to class is error! &quot;+classOfT.getName(), e);</span><br><span class="line">    &#125;</span><br><span class="line">    return null;</span><br><span class="line">&#125;</span><br><span class="line">public static void main(String[] args) &#123;</span><br><span class="line">Test test = new Test();</span><br><span class="line">System.out.println(toJson(test));</span><br><span class="line">String json = &quot;&#123;\&quot;name\&quot;:\&quot;test\&quot;,\&quot;c1\&quot;:&#123;\&quot;name\&quot;:\&quot;c1\&quot;&#125;&#125;&quot;;</span><br><span class="line">test = formJson(json, Test.class);</span><br><span class="line">System.out.println(test.getC1().getName());</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>输出：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">&#123;&quot;c1&quot;:null,&quot;name&quot;:null,&quot;C1&quot;:null&#125;</span><br><span class="line">Exception in thread &quot;main&quot; java.lang.NullPointerException</span><br><span class="line">ERROR   2018-03-06 15:19:05.418 [xxx] (): json to class is error! Test</span><br><span class="line">com.alibaba.dubbo.common.json.ParseException: java.lang.InstantiationException: Test$C1</span><br><span class="line">java.lang.InstantiationException: Test$C1</span><br><span class="line">at java.lang.Class.newInstance(Class.java:359)</span><br><span class="line">at com.alibaba.dubbo.common.json.J2oVisitor.objectBegin(J2oVisitor.java:119)</span><br><span class="line">at com.alibaba.dubbo.common.json.JSON.parse(JSON.java:745)</span><br><span class="line">at com.alibaba.dubbo.common.json.JSON.parse(JSON.java:227)</span><br><span class="line">at com.alibaba.dubbo.common.json.JSON.parse(JSON.java:210)</span><br></pre></td></tr></table></figure><p>可以成功序列化，但是反序列化报错了：无法创建实例<code>Test$C1</code>，这是什么问题？为什么会有这个错误？接下来我们分析一下</p><h1 id="错误分析（java-lang-InstantiationException-Test-C1）"><a href="#错误分析（java-lang-InstantiationException-Test-C1）" class="headerlink" title="错误分析（java.lang.InstantiationException: Test$C1）"></a>错误分析（java.lang.InstantiationException: Test$C1）</h1><p>通过使用fastjson和dubbo json的错误代码跟踪，找到了<code>J2oVisitor.objectBegin(J2oVisitor.java:119)</code>这个地方，代码如下：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">//下面是com.alibaba.dubbo.common.json.J2oVisitor的方法</span><br><span class="line">public void objectBegin() throws ParseException</span><br><span class="line">&#123;</span><br><span class="line">mStack.push(mValue);</span><br><span class="line">mStack.push(mType);</span><br><span class="line">mStack.push(mWrapper);</span><br><span class="line"></span><br><span class="line">if( mType == Object.class || Map.class.isAssignableFrom(mType) )</span><br><span class="line">&#123;</span><br><span class="line">if (! mType.isInterface() &amp;&amp; mType != Object.class) &#123;</span><br><span class="line">try &#123;</span><br><span class="line">mValue = mType.newInstance();</span><br><span class="line">&#125; catch (Exception e) &#123;</span><br><span class="line">throw new IllegalStateException(e.getMessage(), e);</span><br><span class="line">&#125;</span><br><span class="line">&#125; else if (mType == ConcurrentMap.class) &#123;</span><br><span class="line">mValue = new ConcurrentHashMap&lt;String, Object&gt;();</span><br><span class="line">&#125; else &#123;</span><br><span class="line">mValue = new HashMap&lt;String, Object&gt;();</span><br><span class="line">&#125;</span><br><span class="line">mWrapper = null;</span><br><span class="line">&#125; else &#123;</span><br><span class="line">try &#123;</span><br><span class="line">mValue = mType.newInstance();</span><br><span class="line">mWrapper = Wrapper.getWrapper(mType);</span><br><span class="line">&#125; catch(IllegalAccessException e)&#123; </span><br><span class="line">throw new ParseException(StringUtils.toString(e)); </span><br><span class="line">&#125; catch(InstantiationException e)&#123; </span><br><span class="line">throw new ParseException(StringUtils.toString(e)); </span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line">//下面是Class的方法</span><br><span class="line">public T newInstance()</span><br><span class="line">        throws InstantiationException, IllegalAccessException</span><br><span class="line">&#123;</span><br><span class="line">    if (System.getSecurityManager() != null) &#123;</span><br><span class="line">        checkMemberAccess(Member.PUBLIC, Reflection.getCallerClass(), false);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    // NOTE: the following code may not be strictly correct under</span><br><span class="line">    // the current Java memory model.</span><br><span class="line"></span><br><span class="line">    // Constructor lookup</span><br><span class="line">    if (cachedConstructor == null) &#123;</span><br><span class="line">        if (this == Class.class) &#123;</span><br><span class="line">            throw new IllegalAccessException(</span><br><span class="line">                &quot;Can not call newInstance() on the Class for java.lang.Class&quot;</span><br><span class="line">            );</span><br><span class="line">        &#125;</span><br><span class="line">        try &#123;</span><br><span class="line">            Class&lt;?&gt;[] empty = &#123;&#125;;</span><br><span class="line">            final Constructor&lt;T&gt; c = getConstructor0(empty, Member.DECLARED);</span><br><span class="line">            // Disable accessibility checks on the constructor</span><br><span class="line">            // since we have to do the security check here anyway</span><br><span class="line">            // (the stack depth is wrong for the Constructor&apos;s</span><br><span class="line">            // security check to work)</span><br><span class="line">            java.security.AccessController.doPrivileged(</span><br><span class="line">                new java.security.PrivilegedAction&lt;Void&gt;() &#123;</span><br><span class="line">                    public Void run() &#123;</span><br><span class="line">                            c.setAccessible(true);</span><br><span class="line">                            return null;</span><br><span class="line">                        &#125;</span><br><span class="line">                    &#125;);</span><br><span class="line">            cachedConstructor = c;</span><br><span class="line">        &#125; catch (NoSuchMethodException e) &#123;</span><br><span class="line">            throw new InstantiationException(getName());</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    Constructor&lt;T&gt; tmpConstructor = cachedConstructor;</span><br><span class="line">    // Security check (same as in java.lang.reflect.Constructor)</span><br><span class="line">    int modifiers = tmpConstructor.getModifiers();</span><br><span class="line">    if (!Reflection.quickCheckMemberAccess(this, modifiers)) &#123;</span><br><span class="line">        Class&lt;?&gt; caller = Reflection.getCallerClass();</span><br><span class="line">        if (newInstanceCallerCache != caller) &#123;</span><br><span class="line">            Reflection.ensureMemberAccess(caller, this, null, modifiers);</span><br><span class="line">            newInstanceCallerCache = caller;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    // Run constructor</span><br><span class="line">    try &#123;</span><br><span class="line">        return tmpConstructor.newInstance((Object[])null);</span><br><span class="line">    &#125; catch (InvocationTargetException e) &#123;</span><br><span class="line">        Unsafe.getUnsafe().throwException(e.getTargetException());</span><br><span class="line">        // Not reached</span><br><span class="line">        return null;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>代码中使用的是<code>tmpConstructor.newInstance((Object[])null)</code>不带参数的构造器，查看我们的原类，我们的内部类也是无参数的构造器，那为什么无法实例化呢？</p><p>我们来看一下我们的java源代码中内部类生成的class字节码文件，通过反编译工具查看如下：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">public class Test$C1</span><br><span class="line">&#123;</span><br><span class="line">  public String name;</span><br><span class="line">  </span><br><span class="line">  public Test$C1(Test paramTest) &#123;&#125;</span><br><span class="line">  </span><br><span class="line">  public String getName()</span><br><span class="line">  &#123;</span><br><span class="line">    return this.name;</span><br><span class="line">  &#125;</span><br><span class="line">  </span><br><span class="line">  public void setName(String name)</span><br><span class="line">  &#123;</span><br><span class="line">    this.name = name;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>我们是空构造器为什么生成的确是带参数的构造器而且参数<code>paramTest</code>的类型是<code>Test</code>，这是为什么呢？</p><p>我们来看一下JDK doc关于<code>Constructor.newInstance</code>它的解释</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">Uses the constructor represented by this Constructor object to create and initialize a new instance of the constructor&apos;s declaring class, with the specified initialization parameters. Individual parameters are automatically unwrapped to match primitive formal parameters, and both primitive and reference parameters are subject to method invocation conversions as necessary. </span><br><span class="line">If the number of formal parameters required by the underlying constructor is 0, the supplied initargs array may be of length 0 or null. </span><br><span class="line"></span><br><span class="line">If the constructor&apos;s declaring class is an inner class in a non-static context, the first argument to the constructor needs to be the enclosing instance; see The Java Language Specification, section 15.9.3. </span><br><span class="line"></span><br><span class="line">If the required access and argument checks succeed and the instantiation will proceed, the constructor&apos;s declaring class is initialized if it has not already been initialized. </span><br><span class="line"></span><br><span class="line">If the constructor completes normally, returns the newly created and initialized instance.</span><br></pre></td></tr></table></figure><p><span style="color:blue"><em>具体关注这句：If the constructor’s declaring class is an inner class in a non-static context, the first argument to the constructor needs to be the enclosing instance; see The Java Language Specification, section 15.9.3</em></span></p><p>意思是说：如果构造函数的声明类是一个非静态（non-static）上下文中的内部类，则构造函数的第一个参数需要是封闭实例;参见Java语言规范，第15.9.3节。</p><p><a href="https://docs.oracle.com/javase/specs/jls/se7/html/jls-15.html" target="_blank" rel="noopener">15.9.3节</a>具体看：15.9.3. Choosing the Constructor and its Arguments的说明</p><p>到这里我们应该清楚内部类在没有修饰符<code>static</code>和有修饰符<code>static</code>的区别了吧，就是<code>non-static</code>的内部类在生成的时候构造器第一个参数是<code>parent</code>实例，用来共享<code>parent</code>的属性访问的，那让我们将内部类修改为<code>static</code>再做一次测试验证。</p><h1 id="验证"><a href="#验证" class="headerlink" title="验证"></a>验证</h1><p>测试类：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">import java.io.Serializable;</span><br><span class="line"></span><br><span class="line">public class Test implements Serializable &#123;</span><br><span class="line">private static final long serialVersionUID = 2010307013874058143L;</span><br><span class="line">private String name;</span><br><span class="line">public String getName() &#123;</span><br><span class="line">return name;</span><br><span class="line">&#125;</span><br><span class="line">public void setName(String name) &#123;</span><br><span class="line">this.name = name;</span><br><span class="line">&#125;</span><br><span class="line">public C1 c1;</span><br><span class="line">public C1 getC1() &#123;</span><br><span class="line">return c1;</span><br><span class="line">&#125;</span><br><span class="line">public void setC1(C1 c1) &#123;</span><br><span class="line">this.c1 = c1;</span><br><span class="line">&#125;</span><br><span class="line">public static class C1 &#123;</span><br><span class="line">public String name;</span><br><span class="line">public C1() &#123;</span><br><span class="line">&#125;</span><br><span class="line">public String getName() &#123;</span><br><span class="line">return name;</span><br><span class="line">&#125;</span><br><span class="line">public void setName(String name) &#123;</span><br><span class="line">this.name = name;</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><span style="color:blue"><em>ps.内部类C1增加了static修饰符</em></span></p><p>调用序列化与反序列化:</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">public static String toJson(Object obj) &#123;</span><br><span class="line">    try &#123;</span><br><span class="line">        return JSON.json(obj);</span><br><span class="line">    &#125; catch (IOException e) &#123;</span><br><span class="line">        log.error(&quot;class to json is error!&quot;, e);</span><br><span class="line">    &#125;</span><br><span class="line">    return null;</span><br><span class="line">&#125;</span><br><span class="line">public static &lt;T&gt; T formJson(String json, Class&lt;T&gt; classOfT) &#123;</span><br><span class="line">    try &#123;</span><br><span class="line">        return JSON.parse(json, classOfT);</span><br><span class="line">    &#125; catch (ParseException e) &#123;</span><br><span class="line">        log.error(&quot;json to class is error! &quot;+classOfT.getName(), e);</span><br><span class="line">    &#125;</span><br><span class="line">    return null;</span><br><span class="line">&#125;</span><br><span class="line">public static void main(String[] args) &#123;</span><br><span class="line">Test test = new Test();</span><br><span class="line">System.out.println(toJson(test));</span><br><span class="line">String json = &quot;&#123;\&quot;name\&quot;:\&quot;test\&quot;,\&quot;c1\&quot;:&#123;\&quot;name\&quot;:\&quot;c1\&quot;&#125;&#125;&quot;;</span><br><span class="line">test = formJson(json, Test.class);</span><br><span class="line">System.out.println(test.getC1().getName());</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>输出：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">&#123;&quot;c1&quot;:null,&quot;name&quot;:null,&quot;C1&quot;:null&#125;</span><br><span class="line">c1</span><br></pre></td></tr></table></figure><p>结果可以正常的序列化了，以上测试使用的是<code>fastjson</code>与<code>dubbo json</code>进行测试。</p><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>按照规范内部类是不太推荐使用的，如果要用尽量使用<code>static</code>修饰符修饰内部类，这个问题其实就是<code>Java</code>的基本功，尽量一个<code>Java</code>文件中只保留一个类，这样在大多数序列化与反序列化工具中都不会出现问题，也比较符合当下模块化开发的规范，内部类改为<code>static</code>修饰符修饰还可以有效的避免内存泄漏，很多大厂的性能建议文档与<code>Java</code>开发规范文档都可以看到对内部类使用的注意事项，有空多看看大厂的经验总结。</p><p>使用<code>Google</code>的<code>gson</code>进行测试，<code>non-static</code>的内部类可以正常序列化，<code>Google</code>出的工具包就是强大兼容了各种使用方式，从<code>gson</code>的<code>api</code>还发现可以通过参数来<code>disable</code>或<code>enable</code>对<code>inner class</code>序列化的支持，具体查看如下代码：</p><p>测试类：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">import java.io.Serializable;</span><br><span class="line"></span><br><span class="line">public class Test implements Serializable &#123;</span><br><span class="line">private static final long serialVersionUID = 2010307013874058143L;</span><br><span class="line">private String name;</span><br><span class="line">public String getName() &#123;</span><br><span class="line">return name;</span><br><span class="line">&#125;</span><br><span class="line">public void setName(String name) &#123;</span><br><span class="line">this.name = name;</span><br><span class="line">&#125;</span><br><span class="line">public C1 c1;</span><br><span class="line">public C1 getC1() &#123;</span><br><span class="line">return c1;</span><br><span class="line">&#125;</span><br><span class="line">public void setC1(C1 c1) &#123;</span><br><span class="line">this.c1 = c1;</span><br><span class="line">&#125;</span><br><span class="line">public class C1 &#123;</span><br><span class="line">public String name;</span><br><span class="line">public C1() &#123;</span><br><span class="line">&#125;</span><br><span class="line">public String getName() &#123;</span><br><span class="line">return name;</span><br><span class="line">&#125;</span><br><span class="line">public void setName(String name) &#123;</span><br><span class="line">this.name = name;</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><span style="color:blue"><em>ps.注意我这里的内部类C1是non-static的</em></span></p><h2 id="gson开启内部类序列化"><a href="#gson开启内部类序列化" class="headerlink" title="gson开启内部类序列化"></a>gson开启内部类序列化</h2><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">public static void main(String[] args) &#123;</span><br><span class="line">Gson gson = new GsonBuilder().serializeNulls().create();</span><br><span class="line">Test test = new Test();</span><br><span class="line">test.setName(&quot;序列化参数name&quot;);</span><br><span class="line">System.out.println(gson.toJson(test));</span><br><span class="line">String json = &quot;&#123;\&quot;name\&quot;:\&quot;test\&quot;,\&quot;c1\&quot;:&#123;\&quot;name\&quot;:\&quot;c1\&quot;&#125;&#125;&quot;;</span><br><span class="line">test = gson.fromJson(json, Test.class);</span><br><span class="line">System.out.println(test.getC1() == null ? &quot;null&quot; : test.getC1().getName());</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><span style="color:blue"><em>ps.默认InnerClassSerialization就是开启的</em></span></p><p>输出：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">&#123;&quot;name&quot;:&quot;序列化参数name&quot;,&quot;c1&quot;:null&#125;</span><br><span class="line">c1</span><br></pre></td></tr></table></figure><h2 id="gson禁用内部类序列化"><a href="#gson禁用内部类序列化" class="headerlink" title="gson禁用内部类序列化"></a>gson禁用内部类序列化</h2><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">public static void main(String[] args) &#123;</span><br><span class="line">Gson gson = new GsonBuilder().serializeNulls().disableInnerClassSerialization().create();</span><br><span class="line">Test test = new Test();</span><br><span class="line">test.setName(&quot;序列化参数name&quot;);</span><br><span class="line">System.out.println(gson.toJson(test));</span><br><span class="line">String json = &quot;&#123;\&quot;name\&quot;:\&quot;test\&quot;,\&quot;c1\&quot;:&#123;\&quot;name\&quot;:\&quot;c1\&quot;&#125;&#125;&quot;;</span><br><span class="line">test = gson.fromJson(json, Test.class);</span><br><span class="line">System.out.println(test.getC1() == null ? &quot;null&quot; : test.getC1().getName());</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><span style="color:blue"><em>ps.调用GsonBuilder.disableInnerClassSerialization()禁用InnerClassSerialization</em></span></p><p>输出：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">&#123;&quot;name&quot;:&quot;序列化参数name&quot;&#125;</span><br><span class="line">null</span><br></pre></td></tr></table></figure><p>从而能看出<code>Google</code>出的工具包就是强大兼容各种使用方式，<code>Google</code>出的都是精品，从<code>guava</code>就可以看出。</p><p>好了到这里整个文章就介绍完了，最后还是一句老话：世界和平、Keep Real！</p>]]></content>
    
    <summary type="html">
    
      Java中内部类使用注意事项，内部类对序列化与反序列化的影响
    
    </summary>
    
      <category term="java" scheme="https://ningyu1.github.io/categories/java/"/>
    
    
      <category term="inner class" scheme="https://ningyu1.github.io/tags/inner-class/"/>
    
      <category term="static" scheme="https://ningyu1.github.io/tags/static/"/>
    
      <category term="non-static" scheme="https://ningyu1.github.io/tags/non-static/"/>
    
      <category term="innerClassSerialization" scheme="https://ningyu1.github.io/tags/innerClassSerialization/"/>
    
      <category term="fastjson" scheme="https://ningyu1.github.io/tags/fastjson/"/>
    
      <category term="gson" scheme="https://ningyu1.github.io/tags/gson/"/>
    
      <category term="dubbo json" scheme="https://ningyu1.github.io/tags/dubbo-json/"/>
    
  </entry>
  
  <entry>
    <title>Trouble Shooting —— Docker rancher/agent-instance cannot start automatically</title>
    <link href="https://ningyu1.github.io/20180305/64-rancher-agent-instance.html"/>
    <id>https://ningyu1.github.io/20180305/64-rancher-agent-instance.html</id>
    <published>2018-03-05T09:23:11.000Z</published>
    <updated>2024-04-23T01:48:15.036Z</updated>
    
    <content type="html"><![CDATA[<p>今天发现一个<code>docker</code>机器莫名其妙的无工作了，于是进入宿主机查看信息如下：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">docker@xxx:~$ docker ps</span><br><span class="line">be4238200956        rancher/agent:v1.0.2                          &quot;/run.sh run&quot;            5 months ago        Up 34 minutes                                                              rancher-agent</span><br></pre></td></tr></table></figure><p>发现只有一个<code>rancher/agent</code>容器是启动的，其余的都没有启动，查看<code>rancher</code>控制台，服务都在转圈圈<code>Restaring</code>状态，而且长时间一直这个状态没有变化。</p><p>这是什么问题呢？</p><p>查看机器上所有的容器</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">docker@xxx:~$ docker ps -a</span><br><span class="line">CONTAINER ID        IMAGE                                         COMMAND                  CREATED             STATUS                        PORTS               NAMES</span><br><span class="line">d9da7f16ef2d        192.168.0.34:5000/saas-erp:latest             &quot;./entrypoint.sh&quot;        4 days ago          Exited (0) 50 minutes ago                         r-erp_erp-dubbo_1</span><br><span class="line">79e8e475db19        192.168.0.34:5000/tms2job:latest              &quot;./entrypoint.sh&quot;        4 weeks ago         Exited (0) 50 minutes ago                         r-tms_tms2-job_1</span><br><span class="line">0995dabe324b        192.168.0.34:5000/customer-mq:latest          &quot;catalina.sh run&quot;        8 weeks ago         Exited (143) 7 weeks ago                          r-customer_customer-mq_1</span><br><span class="line">65492930b132        192.168.0.34:5000/saas-account:latest         &quot;./entrypoint.sh&quot;        9 weeks ago         Exited (0) 50 minutes ago                         r-account_account-dubbo_1</span><br><span class="line">248514cd635a        192.168.0.34:5000/saas-erp-http-main:latest   &quot;./entrypoint.sh&quot;        4 months ago        Exited (0) 50 minutes ago                         r-erp_erp-http-main_1</span><br><span class="line">94e51332cc40        192.168.0.34:5000/zookeeper:elevy             &quot;/entrypoint.sh zkSer&quot;   5 months ago        Exited (0) 50 minutes ago                         db61a2f2-9b47-4d97-97a3-b6e0764208ca</span><br><span class="line">d72c359c2d5e        192.168.0.34:5000/mysql:5.6.30                &quot;docker-entrypoint.sh&quot;   5 months ago        Exited (0) 50 minutes ago                         c7638fa0-f263-45bd-85d7-2e3b7407ad2f</span><br><span class="line">0c8d3edbc53d        rancher/agent-instance:v0.8.3                 &quot;/etc/init.d/agent-in&quot;   5 months ago        Exited (128) 50 minutes ago                       e505b911-a391-4d1c-8ef2-7bbb306df8eb</span><br><span class="line">be4238200956        rancher/agent:v1.0.2                          &quot;/run.sh run&quot;            5 months ago        Up 11 minutes                                     rancher-agent</span><br></pre></td></tr></table></figure><p>发现服务全都是<code>Exited</code>状态，<code>Rancher</code>控制台上<code>Network Agent</code>容器也是一直转圈圈<code>Restarting</code>状态。</p><p>因此断定应该是<code>Network Agent</code>服务没有启动导致的所有服务无法恢复自动启动。</p><p>那为什么会出现这个问题？这个问题是什么原因导致的呢？</p><p>在解决这个问题之前先看一下<code>Rancher</code>的网络+负载均衡 实现与说明</p><h1 id="Rancher网络-负载均衡的实现与说明"><a href="#Rancher网络-负载均衡的实现与说明" class="headerlink" title="Rancher网络+负载均衡的实现与说明"></a>Rancher网络+负载均衡的实现与说明</h1><p>依赖镜像：<code>rancher/agent-instance:v0.8.3</code></p><p><code>Rancher</code>网络是采用SDN技术所建容器为虚拟ip地址，各host之间容器采用ipsec隧道实现跨主机通信，使用的是udp的500和4500端口。</p><p>启动任务时，在各个host部署容器之前会起一个<code>Network  Agent</code>容器，负责组建网络环境。</p><p>网络全都靠<code>agent-instance</code>容器实现，网络没有准备好其余的容器当然也不会自动恢复。</p><p>那我们的这个问题就是<code>agent-instance</code>容器没有起来导致的，那让我们启动<code>agent-instance</code>容器。</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">docker@xxx:~$ docker start 0c8d3edbc53d</span><br><span class="line">Error response from daemon: rpc error: code = 2 desc = &quot;oci runtime error: exec format error&quot;</span><br><span class="line">Error: failed to start containers: 0c8d3edbc53d</span><br></pre></td></tr></table></figure><p>很遗憾提示错误无法启动，那让我们看一下日志中的错误是什么？</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">docker@xxx:~$ docker logs --tail=200 -f 0c8d3edbc53d</span><br><span class="line">.......省略其他的</span><br><span class="line">INFO: Sending agent-instance-startup applied 3-0f669dbfe83bbb7389a0c2129247f633575904e41d665e311051de2ce1b85737</span><br><span class="line">Starting monit daemon with http interface at [localhost:2812]</span><br><span class="line">The system is going down NOW!</span><br><span class="line">Sent SIGTERM to all processes</span><br><span class="line">Sent SIGKILL to all processes</span><br><span class="line">Requesting system reboot</span><br><span class="line">INFO: Downloading agent http://192.168.0.34:8080/v1/configcontent/configscripts</span><br></pre></td></tr></table></figure><p>发现<code>The system is going down NOW!</code>这个错误，什么情况？无法启动要求重启系统。</p><p>于是查看<code>rancher</code>官方相关这个问题的issues，也没看出个所以然来，跟我的系统版本和agent、agent-instance版本都一致也有很多人无法启动或者启动报错。</p><ul><li><a href="https://github.com/rancher/rancher/issues/5951" target="_blank" rel="noopener">agent-instance cannot start automatically on Ubuntu 16.04.X #5951</a></li><li><a href="https://github.com/rancher/rancher/issues/4237" target="_blank" rel="noopener">Rancher network agent stuck in restart loop - DNS lookup issue #4237</a></li></ul><p>最终无解尝试暴力做法，删除以前的agent-instance容器，然后重新创建重启</p><p>删除rancher/agent-instance:v0.8.3容器</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">docker@xxx:~$ docker rm 0c8d3edbc53d</span><br><span class="line">0c8d3edbc53d</span><br></pre></td></tr></table></figure><p>查看有没有rancher/agent-instance:v0.8.3这个镜像</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">docker@xxx:~$ docker images</span><br><span class="line">REPOSITORY                             TAG                 IMAGE ID            CREATED             SIZE</span><br><span class="line">192.168.0.34:5000/saas-erp             latest              0ad78488245a        4 days ago          275.4 MB</span><br><span class="line">192.168.0.34:5000/tms2job              latest              caa888ff603f        4 weeks ago         236.8 MB</span><br><span class="line">192.168.0.34:5000/customer-mq          latest              db319e29bd7f        8 weeks ago         431.8 MB</span><br><span class="line">192.168.0.34:5000/saas-account         latest              004999746d2c        9 weeks ago         181.9 MB</span><br><span class="line">192.168.0.34:5000/saas-erp-http-main   latest              9a5f8be5ef8d        4 months ago        200.8 MB</span><br><span class="line">192.168.0.34:5000/messer               1.0                 74e9ec4742cc        7 months ago        184.8 MB</span><br><span class="line">192.168.0.34:5000/tomcat               7                   830387a4274c        19 months ago       357.8 MB</span><br><span class="line">rancher/agent-instance                 v0.8.3              b6b013f2aa85        20 months ago       331 MB</span><br><span class="line">192.168.0.34:5000/rancher/agent        v1.0.2              860ed2b2e8e3        20 months ago       454.3 MB</span><br><span class="line">rancher/agent                          v1.0.2              860ed2b2e8e3        20 months ago       454.3 MB</span><br><span class="line">192.168.0.34:5000/mysql                5.6.30              2c0964ec182a        21 months ago       329 MB</span><br><span class="line">192.168.0.34:5000/zookeeper            elevy               d2805d0326a9        2 years ago         131.8 MB</span><br></pre></td></tr></table></figure><p>有镜像，根据镜像重新创建rancher/agent-instance:v0.8.3容器</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">docker@xxx:~$ docker run -d b6b013f2aa85</span><br><span class="line">0060edfa2594</span><br></pre></td></tr></table></figure><p><span style="color:blue"><em>ps.-d, –detach                    Run container in background and print container ID，后台运行容器并且打印出容器ID</em></span></p><p>OK创建好了，再ps查看一下其余的容器是否都自动恢复了</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">docker@xxx:~$ docker ps</span><br><span class="line">CONTAINER ID        IMAGE                                         COMMAND                  CREATED             STATUS              PORTS                                                  NAMES</span><br><span class="line">854fa1039e76        192.168.0.34:5000/zookeeper:elevy             &quot;/entrypoint.sh zkSer&quot;   33 minutes ago      Up 33 minutes       2888/tcp, 3888/tcp, 0.0.0.0:2181-&gt;2181/tcp, 9010/tcp   r-zookeeper_zookeeper-2_1</span><br><span class="line">47c189dbd5c6        b6b013f2aa85                                  &quot;/etc/init.d/agent-in&quot;   37 minutes ago      Up 37 minutes                                                              drunk_tesla</span><br><span class="line">0060edfa2594        rancher/agent-instance:v0.8.3                 &quot;/etc/init.d/agent-in&quot;   37 minutes ago      Up 37 minutes       0.0.0.0:500-&gt;500/udp, 0.0.0.0:4500-&gt;4500/udp           e505b911-a391-4d1c-8ef2-7bbb306df8eb</span><br><span class="line">d9da7f16ef2d        192.168.0.34:5000/saas-erp:latest             &quot;./entrypoint.sh&quot;        4 days ago          Up 37 minutes       0.0.0.0:20833-&gt;20833/tcp                               r-erp_erp-dubbo_1</span><br><span class="line">79e8e475db19        192.168.0.34:5000/tms2job:latest              &quot;./entrypoint.sh&quot;        4 weeks ago         Up 37 minutes       0.0.0.0:50831-&gt;50831/tcp                               r-tms_tms2-job_1</span><br><span class="line">65492930b132        192.168.0.34:5000/saas-account:latest         &quot;./entrypoint.sh&quot;        9 weeks ago         Up 37 minutes       0.0.0.0:20834-&gt;20834/tcp                               r-account_account-dubbo_1</span><br><span class="line">248514cd635a        192.168.0.34:5000/saas-erp-http-main:latest   &quot;./entrypoint.sh&quot;        4 months ago        Up 37 minutes       0.0.0.0:20902-&gt;20902/tcp                               r-erp_erp-http-main_1</span><br><span class="line">d72c359c2d5e        192.168.0.34:5000/mysql:5.6.30                &quot;docker-entrypoint.sh&quot;   5 months ago        Up 37 minutes       0.0.0.0:3306-&gt;3306/tcp                                 c7638fa0-f263-45bd-85d7-2e3b7407ad2f</span><br><span class="line">be4238200956        rancher/agent:v1.0.2                          &quot;/run.sh run&quot;            5 months ago        Up About an hour                                                           rancher-agent</span><br></pre></td></tr></table></figure><p>很好全都恢复了，Status全都是Up。早知道删除重建就不需要这么麻烦去Issues中找答案，以后记住了只要Network  Agent容器（rancher/agent-instance:v0.8.3）出问题先尝试start，如果无法start就删除了重新创建容器。</p>]]></content>
    
    <summary type="html">
    
      Trouble Shooting —— Docker rancher/agent-instance cannot start automatically
    
    </summary>
    
      <category term="rancher" scheme="https://ningyu1.github.io/categories/rancher/"/>
    
      <category term="docker" scheme="https://ningyu1.github.io/categories/rancher/docker/"/>
    
    
      <category term="docker" scheme="https://ningyu1.github.io/tags/docker/"/>
    
      <category term="rancher" scheme="https://ningyu1.github.io/tags/rancher/"/>
    
      <category term="trouble shooting" scheme="https://ningyu1.github.io/tags/trouble-shooting/"/>
    
      <category term="rancher/agent:v1.0.2" scheme="https://ningyu1.github.io/tags/rancher-agent-v1-0-2/"/>
    
      <category term="rancher/agent-instance:v0.8.3" scheme="https://ningyu1.github.io/tags/rancher-agent-instance-v0-8-3/"/>
    
  </entry>
  
  <entry>
    <title>构建Python多个虚拟环境来进行不同版本开发之神器-virtualenv</title>
    <link href="https://ningyu1.github.io/20180302/63-python-virtualenv.html"/>
    <id>https://ningyu1.github.io/20180302/63-python-virtualenv.html</id>
    <published>2018-03-02T03:22:28.000Z</published>
    <updated>2024-04-23T01:48:15.036Z</updated>
    
    <content type="html"><![CDATA[<p>我们都知道<code>Python</code>的类库很多，但是大多支持的版本还是<code>Python2.x</code>系列，<code>Python3</code>支持的类库相对较少，因此我们在开发的时候经常还使用的<code>Python2</code>系列的版本，<code>Python3</code>对语法进行了比较大的重构，<code>Python3</code>中将一些<code>Python2</code>的模块名称做了修改，虽然兼容<code>Python2</code>但还是需要我们做一些处理来保证代码在不同<code>Python</code>版本中能够正常运作，如果我们想同时使用<code>Python2</code> 和 <code>Python3</code>，这个时候大家最常用的做法就是机器上配置多个版本，虽然可以解决问题但是配合多个项目的各种杂乱的包依赖情况，问题就变的非常复杂了，可能升级某一个第三方依赖库会对很多项目产生影响。</p><p>我们都知道在安装<code>Python</code>类库的时候它默认会安装到<code>Python</code>的目录下，有编程洁癖的人都会因此苦恼，因为它污染了<code>Python</code>的目录，并且在开发的时候不同的项目使用的类库差异也蛮大，为了使多个项目之间互相不影响，我们能不能根据项目来区分开<code>Python</code>环境目录？</p><p>当然可以，<code>virtualenv</code>就能帮助我们解决上面的苦恼，它是一个可以创建多个隔绝<code>Python</code>环境的工具，<code>virtualenv</code>可以创建一个包含所有必要的可执行的文件夹，用来使用<code>Python</code>工程所需要的包，同时还不污染<code>Python</code>的原安装目录。</p><p>这个工具简直就是给有开发洁癖的人送福音的。<scpan style="color:blue"><em>画外音：专业送快递</em></scpan></p><p>上面大致说了一下我们使用<code>virtualenv</code>的初衷，接下来让我们看一下<code>virtualenv</code>如何使用，在使用之前先正式的了解一下<code>virtualenv</code></p><h1 id="什么是virtualenv"><a href="#什么是virtualenv" class="headerlink" title="什么是virtualenv?"></a>什么是virtualenv?</h1><p><code>Virtualenv</code>是一个用来创建独立的<code>Python</code>环境的工具</p><h1 id="为什么我们需要一个独立的Python环境？"><a href="#为什么我们需要一个独立的Python环境？" class="headerlink" title="为什么我们需要一个独立的Python环境？"></a>为什么我们需要一个独立的Python环境？</h1><p>引用<code>virtualenv</code>的文档</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">virtualenv is a tool to create isolated Python environments.</span><br><span class="line">The basic problem being addressed is one of dependencies and versions, and indirectly permissions. Imagine you have an application that needs version 1 of LibFoo, but another application requires version 2. How can you use both these applications? If you install everything into /usr/lib/python2.7/site-packages (or whatever your platform’s standard location is), it’s easy to end up in a situation where you unintentionally upgrade an application that shouldn’t be upgraded.</span><br><span class="line">Or more generally, what if you want to install an application and leave it be? If an application works, any change in its libraries or the versions of those libraries can break the application.</span><br><span class="line">Also, what if you can’t install packages into the global site-packages directory? For instance, on a shared host.</span><br><span class="line">In all these cases, virtualenv can help you. It creates an environment that has its own installation directories, that doesn’t share libraries with other virtualenv environments (and optionally doesn’t access the globally installed libraries either).</span><br></pre></td></tr></table></figure><p>上面这段话的意思大致是这样的，我们需要处理的基本问题是包的依赖、版本和间接权限问题。想象一下，你有两个应用，一个应用需要<code>libfoo</code>的版本1，而另一应用需要版本2。如何才能同时使用这些应用程序？如果您安装到的<code>/usr/lib/python2.7/site-packages</code>（或任何平台的标准位置）的一切，在这种情况下，您可能会不小心升级不应该升级的应用程序。或者更广泛地说，如果您想要安装一个应用程序并离开它呢?如果应用程序工作，其库中的任何更改或这些库的版本都可以破坏应用程序。另外，如果您不能将包安装到全局站点包目录中，该怎么办?例如，在共享主机上。在所有这些情况下，<code>virtualenv</code>可以帮助您。它创建了一个有自己的安装目录的环境，它不与其他<code>virtualenv</code>环境共享库(也不可能访问全局安装的库)。</p><p>简单地说，你可以为每个项目建立不同的/独立的Python环境，你将为每个项目安装所有需要的软件包到它们各自独立的环境中。</p><p>到这里我相信我们已经很清晰的知道了<code>virtualenv</code>是什么，能做什么，那接下来就让我们来用一用它。</p><h1 id="使用virtualenv"><a href="#使用virtualenv" class="headerlink" title="使用virtualenv"></a>使用virtualenv</h1><h2 id="安装"><a href="#安装" class="headerlink" title="安装"></a>安装</h2><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">pip install virtualenv</span><br></pre></td></tr></table></figure><p><scpan style="color:blue"><em>画外音：pip安装非常简单，简直就是傻瓜式的</em></scpan></p><h2 id="使用"><a href="#使用" class="headerlink" title="使用"></a>使用</h2><p><code>virtualenv</code>安装完毕后，可以通过运行下面的命令来为你的项目创建独立的<code>Python</code>环境：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">mkdir my_project_dir</span><br><span class="line">cd my_project_dir</span><br><span class="line">virtualenv --distribute my_venv</span><br><span class="line"># my_venv为虚拟环境目录名，目录名自定义</span><br></pre></td></tr></table></figure><p>OK，执行成功，上面发生了什么？</p><p>它会在<code>my_project_dir</code>目录中创建一个文件夹（my_venv），包含了<code>Python</code>可执行文件，以及 <code>pip</code> 库的一份拷贝，这样就能安装其他包了。虚拟环境的名字（my_venv）可以是任意的；不写名字会使用当前目录创建。</p><p>我们再来看看输出：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">1 New python executable in my_venv/bin/python2.7</span><br><span class="line">2 Also creating executable in my_venv/bin/python</span><br><span class="line">3 Installing Setuptools......done.</span><br><span class="line">4 Installing Pip...........done.</span><br></pre></td></tr></table></figure><p><code>--distribute</code> 选项使<code>virtualenv</code>使用新的基于发行版的包管理系统而不是 <code>setuptools</code> 获得的包。 你现在需要知道的就是 <code>--distribute</code> 选项会自动在新的虚拟环境中安装 <code>pip</code> ，这样就不需要手动安装了。 当你成为一个更有经验的<code>Python</code>开发者，你就会明白其中细节。</p><p>当然还有很多参数配置，例如：-p参数指定Python解释器程序路径，这里就过多介绍了，通过help去查看。</p><p>到这里这个虚拟环境就创建好了，但是要真正使用还需要激活，通过如下命令激活。</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">my_project_dir\my_venv\Scripts\activate</span><br></pre></td></tr></table></figure><p>激活后输出如下：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line"># window下</span><br><span class="line">(my_venv) yourpath\venv\Scripts&gt;</span><br><span class="line"></span><br><span class="line"># linux下</span><br><span class="line">(my_venv)[root@docker-x my_venv]#</span><br></pre></td></tr></table></figure><p>从现在起，任何你使用pip安装的包将会放在 my_venv文件夹中，与全局安装的<code>Python</code>隔绝开，是不是很赞，想怎么装怎么装。</p><p>就像平常一样安装包，例如：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">pip install flask</span><br></pre></td></tr></table></figure><p>上面启用激活，有激活那就有停用，如果你在当前虚拟环境中暂时完成了工作，可以使用如下命令停用它：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">my_project_dir\my_venv\Scripts\deactivate</span><br></pre></td></tr></table></figure><p>这将会回到系统默认的<code>Python</code>解释器，包括已安装的库也会回到默认的。要删除一个虚拟环境，只需删除它的文件夹。（执行 <code>rm -rf venv</code> ）。</p><h2 id="思考"><a href="#思考" class="headerlink" title="思考"></a>思考</h2><p>让我们看看激活与停用<code>virtualenv</code>，调用python/pip命令有什么不一样。先停用<code>virtualenv</code>，如下：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">[root@docker-x ~]# which python</span><br><span class="line">/usr/bin/python</span><br><span class="line">[root@docker-x ~]# which pip</span><br><span class="line">/usr/local/bin/pip</span><br></pre></td></tr></table></figure><p>让我们激活<code>virtualenv</code>后，再来一次！看看有什么不同。如下：<br><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">[root@docker-x ~]# which python</span><br><span class="line">/usr/local/my_venv/bin/python</span><br><span class="line"></span><br><span class="line">[root@docker-x ~]# which pip</span><br><span class="line">/usr/local/my_venv/bin/pip</span><br></pre></td></tr></table></figure></p><p><code>virtualenv</code>拷贝了<code>Python</code>可执行文件的副本，并创建一些有用的脚本和安装了项目需要的软件包，你可以在项目的整个生命周期中安装/升级/删除这些包。 它也修改了一些搜索路径，例如<code>PYTHONPATH</code>，以确保：</p><ol><li>当安装包时，它们被安装在当前活动的<code>virtualenv</code>里，而不是系统范围内的<code>Python</code>路径。</li><li>当import代码时，<code>virtualenv</code>将优先采取本环境中安装的包，而不是系统<code>Python</code>目录中安装的包。</li></ol><p>还有一点比较重要，在默认情况下，所有安装在系统范围内的包对于<code>virtualenv</code>是可见的。这意味着如果你将<code>simplejson</code>安装在您的系统<code>Python</code>目录中，它会自动提供给所有的<code>virtualenvs</code>使用。这种行为可以被更改，在创建<code>virtualenv</code>时增加 <code>--no-site-packages</code> 选项,<code>virtualenv</code>就不会读取系统包，如下：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">virtualenv my_venv --no-site-packages</span><br></pre></td></tr></table></figure><h1 id="virtualenvwrapper"><a href="#virtualenvwrapper" class="headerlink" title="virtualenvwrapper"></a>virtualenvwrapper</h1><p>有的时候<code>virtualenv</code>也会带来一些问题，由于<code>virtualenv</code>的启动、停止脚本都在特定文件夹，可能一段时间后，你可能会有很多个虚拟环境散落在系统各处，你可能忘记它们的名字甚至忘记它的位置。怎么来管理<code>virtualenv</code>?</p><p>鉴于<code>virtualenv</code>不便于对虚拟环境集中管理，所以推荐直接使用<code>virtualenvwrapper</code>。 </p><p><code>virtualenvwrapper</code>提供了一系列命令使得和虚拟环境工作变得便利。它把你所有的虚拟环境都放在一个地方。</p><h2 id="安装-1"><a href="#安装-1" class="headerlink" title="安装"></a>安装</h2><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">pip install virtualenvwrapper</span><br><span class="line">pip install virtualenvwrapper-win　　#Windows使用该命令</span><br></pre></td></tr></table></figure><p><span style="color:blue"><br><em>注意：安装virtualenvwrapper之前首先确保virtualenv已安装</em><br></span></p><p>安装完成后，在~/.bashrc写入以下内容</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">export WORKON_HOME=~/Envs</span><br><span class="line">source /usr/local/bin/virtualenvwrapper.sh</span><br><span class="line"></span><br><span class="line">#读入配置文件，立即生效</span><br><span class="line">source ~/.bashrc</span><br></pre></td></tr></table></figure><p><span style="color:blue"><br><em>说明：第一行：virtualenvwrapper存放虚拟环境目录，第二行：virtrualenvwrapper会安装到python的bin目录下，所以该路径是python安装目录下bin/virtualenvwrapper.sh</em></span></p><h2 id="使用-1"><a href="#使用-1" class="headerlink" title="使用"></a>使用</h2><p>使用如下命令创建虚拟环境:</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">mkvirtualenv my_venv_py3</span><br></pre></td></tr></table></figure><p>这样会在<code>WORKON_HOME</code>变量指定的目录下新建名为 my_venv_py3 的虚拟环境。</p><p>若想指定<code>Python</code>版本，可通过<code>--python</code>指定<code>Python</code>解释器</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">mkvirtualenv --python=/usr/local/python3.5.3/bin/python my_venv_py3</span><br></pre></td></tr></table></figure><p>查看当前的虚拟环境目录</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">[root@docker-x ~]# workon</span><br><span class="line">my_venv_py2</span><br><span class="line">my_venv_py3</span><br></pre></td></tr></table></figure><p>切换到虚拟环境</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">[root@docker-x ~]# workon my_venv_py3</span><br><span class="line">(my_venv_py3) [root@docker-x ~]#</span><br></pre></td></tr></table></figure><p>退出虚拟环境</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">(my_venv_py3) [root@docker-x ~]# deactivate</span><br><span class="line">[root@docker-x ~]#</span><br></pre></td></tr></table></figure><p>删除虚拟环境</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">rmvirtualenv my_venv_py3</span><br></pre></td></tr></table></figure><p>到这里 <code>virtualenvs</code> 和 <code>virtualenvwrapper</code> 就讲完了，是不是 so easy！跟着步骤来，一切都是顺理成章的。而且功能也很强大，使<code>Python</code>的开发环境配置起来变得非常简单。尤其是扩展工具<code>virtualenvwrapper</code> 使得构建出来的虚拟环境可以更好的管理起来。感谢这个世界，世界和平，Keep Real！</p>]]></content>
    
    <summary type="html">
    
      构建Python多个虚拟环境来进行不同版本开发之神器-virtualenv
    
    </summary>
    
      <category term="python" scheme="https://ningyu1.github.io/categories/python/"/>
    
    
      <category term="python" scheme="https://ningyu1.github.io/tags/python/"/>
    
      <category term="virtualenv" scheme="https://ningyu1.github.io/tags/virtualenv/"/>
    
      <category term="virtualenvwrapper" scheme="https://ningyu1.github.io/tags/virtualenvwrapper/"/>
    
  </entry>
  
  <entry>
    <title>RESTful访问权限管理实现思路，采用路径匹配神器之AntPathMatcher</title>
    <link href="https://ningyu1.github.io/20180227/62-ant-path-matcher.html"/>
    <id>https://ningyu1.github.io/20180227/62-ant-path-matcher.html</id>
    <published>2018-02-27T08:15:49.000Z</published>
    <updated>2024-04-23T01:48:15.036Z</updated>
    
    <content type="html"><![CDATA[<p>我们经常在写程序时需要对路径进行匹配，比如说：资源的拦截与加载、<code>RESTful</code>访问控制、审计日志采集、等，伟大的<code>SpringMVC</code>在匹配<code>Controller</code>路径时是如何实现的？全都归功于ant匹配规则。</p><p><code>Spring</code>源码之<code>AntPathMatcher</code>，这个工具类匹配很强大，采用的是ant匹配规则。</p><p>什么是ant匹配规则？</p><table><thead><tr><th style="text-align:left">字符wildcard</th><th style="text-align:left">描述</th></tr></thead><tbody><tr><td style="text-align:left">?</td><td style="text-align:left">匹配一个字符（matches one character）</td></tr><tr><td style="text-align:left">*</td><td style="text-align:left">匹配0个及以上字符（matches zero or more characters ）</td></tr><tr><td style="text-align:left">**</td><td style="text-align:left">匹配0个及以上目录directories（matches zero or more ‘directories’ in a path ）</td></tr></tbody></table><p>这个匹配规则很简单，采用简洁明了的方式来进行匹配解析，简化版本的正则。</p><p>结合官方的示例来理解一下</p><table><thead><tr><th style="text-align:left">Pattern</th><th style="text-align:left">匹配说明</th></tr></thead><tbody><tr><td style="text-align:left">com/t?st.jsp</td><td style="text-align:left">匹配: com/test.jsp  ,  com/tast.jsp  ,  com/txst.jsp</td></tr><tr><td style="text-align:left">com/*.jsp</td><td style="text-align:left">匹配: com文件夹下的全部.jsp文件</td></tr><tr><td style="text-align:left">com/**/test.jsp</td><td style="text-align:left">匹配: com文件夹和子文件夹下的全部.jsp文件</td></tr><tr><td style="text-align:left">org/springframework/*<em>/</em>.jsp</td><td style="text-align:left">匹配: org/springframework文件夹和子文件夹下的全部.jsp文件</td></tr><tr><td style="text-align:left">org/**/servlet/bla.jsp</td><td style="text-align:left">匹配: org/springframework/servlet/bla.jsp  , org/springframework/testing/servlet/bla.jsp  ,  org/servlet/bla.jsp</td></tr></tbody></table><h2 id="如何实现RESTful访问权限管理？"><a href="#如何实现RESTful访问权限管理？" class="headerlink" title="如何实现RESTful访问权限管理？"></a>如何实现RESTful访问权限管理？</h2><p>在微服务和前后端分离的开发模式下，往往会使用<code>RESTful</code>来开发后端服务，那服务的访问权限控制就是一个问题，那下来我们就说一下如何实现<code>RESTful</code>访问权限管理。</p><h2 id="权限资源类型"><a href="#权限资源类型" class="headerlink" title="权限资源类型"></a>权限资源类型</h2><p>资源分为如下两种类型：</p><ul><li><code>public</code>（公有）：<code>public</code>为不控制访问的资源</li><li><code>private</code>（私有）：<code>private</code>为需要被控制访问的资源</li></ul><p><span style="color:blue"><em>ps.这种方式资源管理的相对严格一些，如果想管理的粗矿一些，可以不需要public，只要在private中未找到的资源就是不控制访问的资源即可，实现时可以根据自己的业务场景来调整。</em></span></p><h2 id="匹配原则"><a href="#匹配原则" class="headerlink" title="匹配原则"></a>匹配原则</h2><p>基础匹配规则：使用ant匹配规则</p><p>在<code>SpringMVC</code>的路径匹配原则中有一个原则是：最长匹配原则(has more characters)</p><p>什么是最长匹配原则(has more characters)？</p><p>最长匹配原则(has more characters)简单的理解就是目标<code>URL</code>有多个<code>pattern</code>都可以匹配上就取最长的那个<code>pattern</code></p><p>例如：请求的<code>URL</code>为<code>/app/dir/file.jsp</code>，有两个<code>pattern</code> <code>/**/*.jsp</code>和<code>/app/dir/*.jsp</code>都可以匹配成功，那么会根据<code>pattern</code>的长度来控制是否采用哪一个，这里使用<code>/app/dir/*.jsp</code>来匹配。</p><p>为什么要使用最长匹配原则？我的理解是长的<code>pattern</code>更符合目标<code>URL</code>格式，短的<code>pattern</code>往往是范围较广的，匹配取最适合的<code>pattern</code>也是比较符合预期的。</p><h2 id="根据服务名分类"><a href="#根据服务名分类" class="headerlink" title="根据服务名分类"></a>根据服务名分类</h2><p>在做资源访问权限时往往会有多个服务可能会出现相同的资源路径，因此增加一级服务名来对资源进行分类。</p><p>例如：<code>GET /v1/service1/product/1</code> 和 <code>GET /v1/service2/product/1</code>，根据二级目录<code>service</code>名称来对服务进行模块化分割。<code>/v1</code>为RESTful版本号</p><p><span style="color:blue"><em>ps.服务名就是为了做资源分类</em></span></p><h2 id="权限验证逻辑"><a href="#权限验证逻辑" class="headerlink" title="权限验证逻辑"></a>权限验证逻辑</h2><ul><li>验证<code>public</code>资源<ul><li>去除末尾<code>&quot;/&quot;</code></li><li>验证<code>service</code>服务名，服务名为空返回没有权限</li><li>获取服务名下<code>enabled=true</code>的资源表，结果进行<code>cache</code>，结果为空没有权限</li><li>根据<code>pattern</code>长度倒序</li><li>匹配<code>method</code>，匹配成功进行下一步匹配</li><li>匹配请求的<code>url</code>，匹配成功返回有权限，反之返回没有权限</li></ul></li><li>验证<code>private</code>资源<ul><li>去除末尾<code>&quot;/&quot;</code></li><li>验证<code>service</code>服务名，服务名为空返回没有权限</li><li>获取服务名下用户角色对应的资源列表聚合结果，结果进行<code>cache</code>，结果为空返回没有权限</li><li>根据<code>pattern</code>长度倒序</li><li>匹配<code>method</code>，匹配成功进行下一步匹配，反之<code>continue</code></li><li>匹配请求的<code>url</code>，匹配成功进行下一步匹配，反之<code>continue</code></li><li>检查匹配成功的<code>url</code>是否为禁用状态，如果禁用返回无权限，反之进行下一步匹配</li><li>匹配成功的<code>url</code>对应的角色列表进行登录用户的角色匹配</li><li>角色匹配成功返回有权限，反之返回没有权限</li></ul></li></ul><p><span style="color:blue"><em>ps.method是<code>GET</code>、<code>POST</code>、<code>PUT</code>、<code>PATCH</code>、<code>DELETE</code>，<code>service</code>是服务模块名</em></span></p><h2 id="缓存结构"><a href="#缓存结构" class="headerlink" title="缓存结构"></a>缓存结构</h2><ul><li>private资源数据<ul><li>结构：<code>hash</code></li><li><code>cache key=${APPNAME}.METADATA.RESOURCE</code>，<code>field=${RESOURCE_ID}</code>，<code>value=Resource</code>对象</li></ul></li><li>public资源数据<ul><li>结构：<code>hash</code></li><li><code>cache key=${APPNAME}.METADATA.RESOURCE.PUBLIC</code>，<code>field=${SERVICE}</code>，<code>value=List&lt;Resource&gt;</code></li></ul></li><li>用户关联角色数据<ul><li>结构：<code>hash</code></li><li><code>cache key=${APPNAME}.METADATA.ROLE</code>，<code>field=${USER_ID}</code>，<code>value=List&lt;ROLE_ID&gt;</code></li></ul></li><li>角色关联的资源数据<ul><li>结构：<code>hash</code></li><li><code>cache key=${APPNAME}.METADATA.MAPPING</code>，<code>field=${SERVICE}</code>，<code>value=List&lt;Metadata&lt;Resource,List&lt;ROLE_ID&gt;&gt;&gt;</code></li><li>这里存储的数据结构是反向的，获取服务下的资源列表，每个资源数据中会有拥有这个资源的角色列表。</li></ul></li></ul><p><span style="color:blue"><em>ps.缓存可以使用分布式的<code>redis</code>、<code>redisson</code>、如果单机可以使用<code>jvm cache</code>。</em></span></p><h2 id="缓存控制"><a href="#缓存控制" class="headerlink" title="缓存控制"></a>缓存控制</h2><ul><li>private资源数据发生变更时<ul><li>调用<code>MetadataCache.invalidResources()</code>，失效<code>cache key=${APPNAME}.METADATA.RESOURCE</code>下所有数据</li></ul></li><li>public资源数据发生变更时<ul><li>调用<code>MetadataCache.invalidPublicResource(service)</code>失效服务名下的<code>public</code>资源集合，失效<code>cache key=${APPNAME}.METADATA.RESOURCE.PUBLIC</code>下的某个<code>${SERVICE}</code>数据</li><li>调用<code>MetadataCache.invalidPublicResource()</code>失效所有服务名下的<code>public</code>资源集合，失效<code>cache key=${APPNAME}.METADATA.RESOURCE.PUBLIC</code>下所有数据</li></ul></li><li>用户关联角色数据发生变更时<ul><li>调用<code>MetadataCache.invalidUserRoles(userId)</code>失效用户下的角色集合，失效<code>cache key=${APPNAME}.METADATA.ROLE</code>下所有数据</li></ul></li><li>角色关联的资源数据发生变更时<ul><li>调用<code>MetadataCache.invalidMetadata(service)</code>失效服务名下的资源角色聚合对象，失效<code>cache key=${APPNAME}.METADATA.MAPPING</code>下的某个<code>${SERVICE}</code>数据</li><li>调用<code>MetadataCache.invalidMetadata()</code>失效所有服务名下的资源角色聚合对象，失效<code>cache key=${APPNAME}.METADATA.MAPPING</code>下所有数据</li></ul></li></ul><p><span style="color:blue"><em>ps.在以上触发点上对缓存数据进行更新，这里采用失效再加载方式</em></span></p><h2 id="缓存加载"><a href="#缓存加载" class="headerlink" title="缓存加载"></a>缓存加载</h2><ul><li><code>private</code>资源数据，在系统启动加载，加载所有私有资源，如果失效了，会在<code>private</code>匹配的时再进行加载</li><li><code>public</code>资源数据，在<code>public</code>匹配时加载，通过服务名加载，如果失效了，会在<code>public</code>匹配时再进行加载</li><li>用户关联角色数据，在<code>private</code>匹配时加载，如果失效了，会在<code>private</code>匹配时再进行加载</li><li>角色关联的资源数据，在<code>private</code>匹配时加载，如果失效了，会在<code>private</code>匹配时再进行加载</li></ul><p><span style="color:blue"><em>ps.资源数据加载触发点</em></span></p><h2 id="pattern配置建议"><a href="#pattern配置建议" class="headerlink" title="pattern配置建议"></a>pattern配置建议</h2><ul><li>配置资源时，将不需要配置权限的url配置为<code>public</code>资源</li><li>每个服务名下建议配置一个<code>**</code>（双星）通配符给超级管理员使用，例如：<code>/v1/products/**</code></li><li>每个<code>url</code>的第二级目录要与服务名一致，例如：<code>/v1/products/{pid}</code>，服务名为<code>products</code></li><li><code>url</code>的目录结构必须大于两级目录，例如：<code>/v1/products/{pid}</code>，不允许为：<code>/v1/{pid}</code></li><li><code>url</code>与权限通配符映射关系，前面<code>url</code>，后面<code>pattern</code><ul><li>例如：<code>/v1/products/{pid}</code> -&gt; <code>/v1/products/*</code></li><li>例如：<code>/v1/products/{pid}/skus/{sid}</code> -&gt; <code>/v1/products/*/skus/*</code></li><li>例如：<code>/v1/products/enabled</code> -&gt; <code>/v1/products/enabled</code></li><li>例如：<code>/v1/products/**</code>，匹配<code>products</code>目录下所有目录</li></ul></li></ul><p><strong>以上就是一种RESTful资源管理的实现思路，能控制到RESTful的方法级别，在前后端分离的项目可以使用这种方式来控制访问权限。</strong></p>]]></content>
    
    <summary type="html">
    
      RESTful访问权限管理实现思路，采用路径匹配神器之AntPathMatcher
    
    </summary>
    
      <category term="restful" scheme="https://ningyu1.github.io/categories/restful/"/>
    
    
      <category term="restful" scheme="https://ningyu1.github.io/tags/restful/"/>
    
      <category term="antPathMatcher" scheme="https://ningyu1.github.io/tags/antPathMatcher/"/>
    
  </entry>
  
  <entry>
    <title>扩展Disconf支持Global共享配置，简化业务应用参数配置</title>
    <link href="https://ningyu1.github.io/20180211/61-disconf-ext.html"/>
    <id>https://ningyu1.github.io/20180211/61-disconf-ext.html</id>
    <published>2018-02-11T03:26:49.000Z</published>
    <updated>2024-04-23T01:48:15.036Z</updated>
    
    <content type="html"><![CDATA[<p>当我们使用统一配置中心（<code>UCM</code>）后或许都会出现这种烦恼，项目中的配置项目多，当项目引用到基础中间件时都要增加基础中间件的配置，例如：zk参数、redis参数、rpc参数、loadbalance参数、mq参数、等。这些配置都是基础的中间件配置，应该做成共享的方式让所有APP都共享，而并不是在用的时候再去APP中添加，Global的配置基础中间件团队维护即可。</p><h3 id="为什么要有公共共享的配置？"><a href="#为什么要有公共共享的配置？" class="headerlink" title="为什么要有公共共享的配置？"></a>为什么要有公共共享的配置？</h3><p>因为在APP配置中有很多是公共的配置，如果没有Global就需要在自己的APP中配置这些配置信息，导致APP中配置信息过多不好维护，公共的配置信息修改需要通知各业务APP修改自己APP中的配置，没有达到一处修改，各处使用的目标。</p><h3 id="这时候有朋友就会问我了如果做成全局共享配置，那不同项目需要修改全局某个参数怎么办呢？"><a href="#这时候有朋友就会问我了如果做成全局共享配置，那不同项目需要修改全局某个参数怎么办呢？" class="headerlink" title="这时候有朋友就会问我了如果做成全局共享配置，那不同项目需要修改全局某个参数怎么办呢？"></a>这时候有朋友就会问我了如果做成全局共享配置，那不同项目需要修改全局某个参数怎么办呢？</h3><p>这个需求也很正常，比如loadbalance参数确实需要根据不同项目的具体情况去配置参数，对于这种问题其实很好解决，我们可以使用APP中的配置去覆盖Global配置，也就是说当APP中的配置项与Global配置项相同的情况下，以APP的配置为主即可。</p><p>这样一来APP的配置生效的优先级为：Local conf &gt; Project conf &gt; Global conf，当出现相同配置项以APP自身的配置为主去覆盖。</p><p>增加了Global的支持后，APP中的配置减少了，避免了一些由于配置导致的错误，也可以通过Global的方式去规范APP的配置，让业务开发不关心公共配置的细节，在使用的时候直接使用无需维护。</p><p><code>Disconf</code>作为一个比较老牌的<code>UCM</code>在这方面支持的并不好，它并没有共享配置这个概念，这样一来公共的配置就需要在每个APP中都要配置一份，操作起来很烦人。</p><h1 id="那我们如何来解决这个问题？我们能否扩展Disconf让其支持Global共享配置呢？"><a href="#那我们如何来解决这个问题？我们能否扩展Disconf让其支持Global共享配置呢？" class="headerlink" title="那我们如何来解决这个问题？我们能否扩展Disconf让其支持Global共享配置呢？"></a>那我们如何来解决这个问题？我们能否扩展<code>Disconf</code>让其支持<code>Global</code>共享配置呢？</h1><h2 id="扩展思路"><a href="#扩展思路" class="headerlink" title="扩展思路"></a>扩展思路</h2><p>在加载properties的时候，也就是ReloadablePropertiesFactoryBean的locations，给前面默认加一个GlobalProp项目的索引项：global（使用disconf的新建配置项，而不是配置文件），这个索引项的值是所有global配置文件的名称，使用”,”分隔，例如：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">global-dubbo.properties,global-redis.properties,global-zookeeper.properties,global-sso.properties,global-mq.properties,global-fastdfs.properties,global-elasticsearch.properties</span><br></pre></td></tr></table></figure><p>让disconf下载配置文件的时候优先下载global的配置文件，在properties加载的时候优先加载global的配置，这样当发生重复项时后加载的会覆盖前面的信息，从而达到了我们上面的需求，当APP中修改了某个global配置应该以APP的配置项为主。</p><p>接下来就让我们看一下具体扩展了哪些类？</p><p>Disconf的扩展点做的不是那么的好，因此扩展起来有些麻烦，我使用的是比较暴力的方式，直接使用原包的类在名称后加Ext然后修改代码，使用的时候使用Ext的类替代即可，这种方式的弊端是升级<code>Disconf</code>的时候很麻烦。</p><h2 id="Disconf扫描管理"><a href="#Disconf扫描管理" class="headerlink" title="Disconf扫描管理"></a>Disconf扫描管理</h2><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">com.baidu.disconf.client.DisconfMgrBean</span><br><span class="line">扩展一个</span><br><span class="line">com.baidu.disconf.client.DisconfMgrBeanExt</span><br><span class="line"></span><br><span class="line">com.baidu.disconf.client.DisconfMgrBeanSecond</span><br><span class="line">扩展一个</span><br><span class="line">com.baidu.disconf.client.DisconfMgrBeanSecondExt</span><br></pre></td></tr></table></figure><h2 id="Reloadable-Properties"><a href="#Reloadable-Properties" class="headerlink" title="Reloadable Properties"></a>Reloadable Properties</h2><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">com.baidu.disconf.client.addons.properties.ReloadablePropertiesFactoryBean</span><br><span class="line">扩展一个</span><br><span class="line">com.baidu.disconf.client.addons.properties.ReloadablePropertiesFactoryBeanExt</span><br></pre></td></tr></table></figure><p>可以增加一个开关从而支持启用global的自由度，默认是开启的。</p><p>下面来看一下扩展后的具体使用方法如下</p><h2 id="项目地址"><a href="#项目地址" class="headerlink" title="项目地址"></a>项目地址</h2><p><a href="https://github.com/ningyu1/disconf-client-ext" target="_blank" rel="noopener">disconf-client-ext</a> </p><p><a href="https://github.com/ningyu1/disconf-client-ext/releases" target="_blank" rel="noopener"><img src="https://img.shields.io/github/release/ningyu1/disconf-client-ext.svg?style=social&amp;label=Release"></a>&nbsp;<a href="https://github.com/ningyu1/disconf-client-ext/stargazers" target="_blank" rel="noopener"><img src="https://img.shields.io/github/stars/ningyu1/disconf-client-ext.svg?style=social&amp;label=Star"></a>&nbsp;<a href="https://github.com/ningyu1/disconf-client-ext/fork" target="_blank" rel="noopener"><img src="https://img.shields.io/github/forks/ningyu1/disconf-client-ext.svg?style=social&amp;label=Fork"></a>&nbsp;<a href="https://github.com/ningyu1/disconf-client-ext/watchers" target="_blank" rel="noopener"><img src="https://img.shields.io/github/watchers/ningyu1/disconf-client-ext.svg?style=social&amp;label=Watch"></a> <a href="http://www.gnu.org/licenses/gpl-3.0.html" target="_blank" rel="noopener"><img src="https://img.shields.io/badge/license-GPLv3-blue.svg"></a></p><h2 id="disconf-client-ext的使用"><a href="#disconf-client-ext的使用" class="headerlink" title="disconf-client-ext的使用"></a>disconf-client-ext的使用</h2><ul><li>依赖disconf版本：2.6.32</li><li>pom中引入disconf-client-ext依赖</li><li>修改disconf配置<ul><li>替换<code>com.baidu.disconf.client.DisconfMgrBean</code> –&gt; <code>com.baidu.disconf.client.DisconfMgrBeanExt</code></li><li>替换<code>com.baidu.disconf.client.DisconfMgrBeanSecond</code> –&gt; <code>com.baidu.disconf.client.DisconfMgrBeanSecondExt</code></li><li>替换<code>com.baidu.disconf.client.addons.properties.ReloadablePropertiesFactoryBean</code> –&gt; <code>com.baidu.disconf.client.addons.properties.ReloadablePropertiesFactoryBeanExt</code></li><li>修改locations中配置文件，只保留项目自己的配置文件，例如</li></ul></li></ul><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">&lt;bean id=&quot;disconfNotReloadablePropertiesFactoryBean&quot; class=&quot;com.baidu.disconf.client.addons.properties.ReloadablePropertiesFactoryBeanExt&quot;&gt;</span><br><span class="line">&lt;property name=&quot;locations&quot;&gt;</span><br><span class="line">&lt;list&gt;</span><br><span class="line">&lt;value&gt;classpath:/jdbc.properties&lt;/value&gt;</span><br><span class="line">&lt;/list&gt;</span><br><span class="line">&lt;/property&gt;</span><br><span class="line">&lt;/bean&gt;</span><br></pre></td></tr></table></figure><ul><li><span style="color:red">关闭global共享配置（默认是开启的）</span></li></ul><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">&lt;bean id=&quot;disconfNotReloadablePropertiesFactoryBean&quot; class=&quot;com.baidu.disconf.client.addons.properties.ReloadablePropertiesFactoryBeanExt&quot;&gt;</span><br><span class="line">&lt;property name=&quot;locations&quot;&gt;</span><br><span class="line">&lt;list&gt;</span><br><span class="line">&lt;value&gt;classpath:/jdbc.properties&lt;/value&gt;</span><br><span class="line">&lt;/list&gt;</span><br><span class="line">&lt;/property&gt;</span><br><span class="line">&lt;property name=&quot;globalShareEnable&quot; value=&quot;false&quot; /&gt;</span><br><span class="line">&lt;/bean&gt;</span><br></pre></td></tr></table></figure><ul><li>最后一步添加global项目到<code>Disconf</code></li></ul><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/disconf-ext/1.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p>世界和平，Keep Real!</p>]]></content>
    
    <summary type="html">
    
      扩展Disconf支持Global共享配置，简化业务应用参数配置
    
    </summary>
    
      <category term="ucm" scheme="https://ningyu1.github.io/categories/ucm/"/>
    
    
      <category term="disconf" scheme="https://ningyu1.github.io/tags/disconf/"/>
    
      <category term="ucm" scheme="https://ningyu1.github.io/tags/ucm/"/>
    
  </entry>
  
  <entry>
    <title>Dubbo接口如何在Jmeter中测试，自研Dubbo Plugin for Apache JMeter</title>
    <link href="https://ningyu1.github.io/20180209/60-jmeter-plugins-dubbo-support.html"/>
    <id>https://ningyu1.github.io/20180209/60-jmeter-plugins-dubbo-support.html</id>
    <published>2018-02-09T07:39:49.000Z</published>
    <updated>2024-04-23T01:48:15.035Z</updated>
    
    <content type="html"><![CDATA[<p>最近公司测试需要对<code>Dubbo</code>的<code>RPC</code>接口进行测试，测试工具使用的是<code>Jmeter</code>，按照常规的做法需要包装一个<code>Java</code>请求，再配合<code>Jmeter</code>的<code>Java Sample</code>去做测试，这种做法是最简单最普遍的，但是这个方法不够灵活和方便，那我们能不能写一个<code>Jmeter Plugin</code>来解决这个问题？让<code>Dubbo RPC</code>接口测试更为方便一些？</p><p>那我们先了解一下Jmeter的插件机制</p><h1 id="Jmeter-Plugin"><a href="#Jmeter-Plugin" class="headerlink" title="Jmeter Plugin"></a>Jmeter Plugin</h1><p>先来看一下<code>Jmeter</code>的核心组件</p><ol><li><code>Sample</code> 取样器，这个是最主要的组件，测试的内容主要是靠Sample来实现，我们常见的Sample有，<code>HttpSample</code>、<code>FTPSample</code>、<code>JavaSample</code>、<code>SMTPSample</code>、<code>LDAPSample</code>等。</li><li><code>Timer</code> 定时器，主要用于配置sample之间的等待时间，可以查看：<code>org.apache.jmeter.timers.RandomTimer</code></li><li><code>ConfigElement</code> 配置组件，主要用于定义前置配置。如数据库连接，csv输入数据集等。主要功能是将配置转换为变量设置到JMeter context中。</li><li><code>Assertion</code> 验证Sampler的结果是否符合预期</li><li><code>PostProcessor</code> 一般用于对Sampler结果进行二次加工</li><li><code>Visualizer</code> 将sampler的结果进行可视化展示。</li><li><code>Controller</code> 对sampler进行逻辑控制。</li><li><code>SampleListener</code> 负责处理监听，基于事件机制。一般用于保存sampler的结果等耗费时间的操作。</li></ol><p>Jmeter的插件机制比较简单，Jmeter提供了扩展类来支持自定义插件的开发。<br>继承<code>org.apache.jmeter.samplers.gui.AbstractSamplerGui</code>和<code>org.apache.jmeter.samplers.AbstractSampler</code>就可以完成一个插件开发。</p><h1 id="JMeter的GUI机制"><a href="#JMeter的GUI机制" class="headerlink" title="JMeter的GUI机制"></a>JMeter的GUI机制</h1><p>由于<code>Jmeter</code>是一个基于<code>Swing</code>的GUI工具,所以开发插件需要对<code>Java Swing GUI</code>框架有一定了解。 <code>JMeter</code>内部有两种GUI的实现方式。</p><h2 id="第一种方式："><a href="#第一种方式：" class="headerlink" title="第一种方式："></a>第一种方式：</h2><p>直接继承JMeterGUIComponent接口的抽象实现类:</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">org.apache.jmeter.config.gui.AbstractConfigGui</span><br><span class="line">org.apache.jmeter.assertions.gui.AbstractAssertionGui</span><br><span class="line">org.apache.jmeter.control.gui.AbstractControllerGui</span><br><span class="line">org.apache.jmeter.timers.gui.AbstractTimerGui</span><br><span class="line">org.apache.jmeter.visualizers.gui.AbstractVisualizer</span><br><span class="line">org.apache.jmeter.samplers.gui.AbstractSamplerGui</span><br></pre></td></tr></table></figure><h2 id="通过Swing的Bean绑定机制"><a href="#通过Swing的Bean绑定机制" class="headerlink" title="通过Swing的Bean绑定机制"></a>通过Swing的Bean绑定机制</h2><p>前者的好处是自由度高，可定制性强，但需要开发者关心GUI控件布局,以及从控件到Model的转换。后者基本不需要开发者接触到GUI层的东西，定义好<code>Bean</code>以及<code>BeanInfo</code>即可。但<code>SampleListener</code>不支持<code>BeanInfo</code>方式定义。</p><p><strong>ps.如果java swing比较熟悉的话推荐使用第一种方式，自由度高。</strong></p><p>下面是我写的插件DubboSample，主要用于Dubbo RPC接口测试。</p><h2 id="Dubbo-Plugin-for-Apache-JMeter"><a href="#Dubbo-Plugin-for-Apache-JMeter" class="headerlink" title="Dubbo Plugin for Apache JMeter"></a>Dubbo Plugin for Apache JMeter</h2><p>jmeter-plugin-dubbo项目已经transfer到dubbo group下</p><p><a href="https://github.com/dubbo/jmeter-plugins-dubbo" target="_blank" rel="noopener">github: jmeter-plugin-dubbo</a> </p><p><a href="https://gitee.com/ningyu/jmeter-plugins-dubbo" target="_blank" rel="noopener">码云: jmeter-plugin-dubbo</a></p><h2 id="DubboSample使用"><a href="#DubboSample使用" class="headerlink" title="DubboSample使用"></a>DubboSample使用</h2><h3 id="支持Jmeter版本"><a href="#支持Jmeter版本" class="headerlink" title="支持Jmeter版本"></a>支持Jmeter版本</h3><p>Jmeter版本：3.0</p><h3 id="插件安装"><a href="#插件安装" class="headerlink" title="插件安装"></a>插件安装</h3><p>插件包可以去<code>github</code>上下载。将插件包放入Jmeter的lib的ext下。</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">$&#123;Path&#125;\apache-jmeter-3.0\lib\ext</span><br></pre></td></tr></table></figure><p>如果使用的是:<code>jmeter-plugins-dubbo-1.0.0-SNAPSHOT-jar-with-dependencies.jar</code>包含所有依赖。</p><p>如果使用的是：<code>jmeter-plugins-dubbo-1.0.0-SNAPSHOT.jar</code>需要自定添加插件的依赖包，推荐使用上面的包，依赖包版本如下：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">dubbo-2.5.3.jar</span><br><span class="line">javassist-3.15.0-GA.jar</span><br><span class="line">zookeeper-3.4.6.jar</span><br><span class="line">zkclient-0.1.jar</span><br><span class="line">jline-0.9.94.jar</span><br><span class="line">netty-3.7.0-Final.jar</span><br><span class="line">slf4j-api-1.7.5.jar</span><br><span class="line">log4j-over-slf4j-1.7.5.jar</span><br></pre></td></tr></table></figure><h3 id="插件使用"><a href="#插件使用" class="headerlink" title="插件使用"></a>插件使用</h3><p>启动<code>Jmeter</code>添加<code>DubboSample</code>如下图：</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/jmeter-plugins-dubbo/1.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p>添加后能看到<code>DubboSample</code>的具体操作页面，如下图：</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/jmeter-plugins-dubbo/2.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p>根据上图提示传入值即可。</p><p>接口以及接口依赖包请添加到<code>classpath</code>下，可以放在<code>apache-jmeter-3.0\lib\ext</code>下，也可以通过下图方式添加：</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/jmeter-plugins-dubbo/3.png" alt title>                </div>                <div class="image-caption"></div>            </figure><h3 id="运行结果"><a href="#运行结果" class="headerlink" title="运行结果"></a>运行结果</h3><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/jmeter-plugins-dubbo/4.png" alt title>                </div>                <div class="image-caption"></div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/jmeter-plugins-dubbo/5.png" alt title>                </div>                <div class="image-caption"></div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/jmeter-plugins-dubbo/6.png" alt title>                </div>                <div class="image-caption"></div>            </figure><h3 id="注意事项"><a href="#注意事项" class="headerlink" title="注意事项"></a>注意事项</h3><ol><li>当使用zk，address填入zk地址（集群地址使用”,”分隔）,使用dubbo直连，address填写直连地址和服务端口</li><li><code>timeout</code>：服务方法调用超时时间(毫秒)</li><li><code>version</code>：服务版本，与服务提供者的版本一致</li><li><code>retries</code>：远程服务调用重试次数，不包括第一次调用，不需要重试请设为0</li><li><code>cluster</code>：集群方式，可选：failover/failfast/failsafe/failback/forking</li><li>接口需要填写类型完全名称，含包名</li><li>参数支持任何类型，包装类直接使用<code>java.lang</code>下的包装类，小类型使用：<code>int、float、shot、double、long、byte、boolean、char</code>，自定义类使用类完全名称。</li><li>参数值，基础包装类和基础小类型直接使用值，例如：int为1，boolean为true等，自定义类与<code>List</code>或者<code>Map</code>等使用json格式数据。</li><li>更多dubbo参数查看官方文档：<a href="http://dubbo.io/books/dubbo-user-book/references/xml/dubbo-reference.html" target="_blank" rel="noopener">http://dubbo.io/books/dubbo-user-book/references/xml/dubbo-reference.html</a></li></ol><p>到这里插件的就介绍完了。世界和平、keep real！</p>]]></content>
    
    <summary type="html">
    
      Dubbo接口如何在Jmeter中测试，自研Dubbo Plugin for Apache JMeter
    
    </summary>
    
      <category term="test" scheme="https://ningyu1.github.io/categories/test/"/>
    
    
      <category term="dubbo" scheme="https://ningyu1.github.io/tags/dubbo/"/>
    
      <category term="jmeter" scheme="https://ningyu1.github.io/tags/jmeter/"/>
    
      <category term="test" scheme="https://ningyu1.github.io/tags/test/"/>
    
      <category term="Dubbo可视化测试工具" scheme="https://ningyu1.github.io/tags/Dubbo%E5%8F%AF%E8%A7%86%E5%8C%96%E6%B5%8B%E8%AF%95%E5%B7%A5%E5%85%B7/"/>
    
      <category term="Jmeter对Dubbo接口进行可视化压力测试" scheme="https://ningyu1.github.io/tags/Jmeter%E5%AF%B9Dubbo%E6%8E%A5%E5%8F%A3%E8%BF%9B%E8%A1%8C%E5%8F%AF%E8%A7%86%E5%8C%96%E5%8E%8B%E5%8A%9B%E6%B5%8B%E8%AF%95/"/>
    
      <category term="Dubbo Jmeter插件" scheme="https://ningyu1.github.io/tags/Dubbo-Jmeter%E6%8F%92%E4%BB%B6/"/>
    
  </entry>
  
  <entry>
    <title>如何将Python脚本打包成可执行文件？</title>
    <link href="https://ningyu1.github.io/20180207/59-py2exe-pyinstaller.html"/>
    <id>https://ningyu1.github.io/20180207/59-py2exe-pyinstaller.html</id>
    <published>2018-02-07T03:57:49.000Z</published>
    <updated>2024-04-23T01:48:15.035Z</updated>
    
    <content type="html"><![CDATA[<p>我们有时候经常会使用<code>python</code>写一些小工具，在<code>Linux</code>环境下可以很方便运行，因为<code>Linux</code>默认都会有<code>python</code>环境，我们只需要添加<code>python</code>脚本依赖的类库即可执行。但是有的时候我们需要把小工具给到一些麻瓜去用的时候就会出现一些问题，他们大多是在<code>Windows</code>上运行工具，那就必须要先准备<code>python</code>的可运行环境才行，这就给麻瓜们带来了使用成本，我们能否将<code>python</code>脚本打包成windows下可执行文件呢？</p><p>接下来让我们先了解一下<code>python</code>有哪些类库可以帮助我们解决这个问题。</p><p>这是一个来自<strong>Freezing Your Code</strong>的统计</p><table><thead><tr><th style="text-align:center">Solution</th><th style="text-align:center">Windows</th><th style="text-align:center">Linux</th><th style="text-align:center">OS X</th><th style="text-align:center">Python 3</th><th style="text-align:center">License</th><th style="text-align:center">One-file mode</th><th style="text-align:center">Zipfile import</th><th style="text-align:center">Eggs</th><th style="text-align:center">pkg_resources support</th></tr></thead><tbody><tr><td style="text-align:center">bbFreeze</td><td style="text-align:center">yes</td><td style="text-align:center">yes</td><td style="text-align:center">yes</td><td style="text-align:center">no</td><td style="text-align:center">MIT</td><td style="text-align:center">no</td><td style="text-align:center">yes</td><td style="text-align:center">yes</td><td style="text-align:center">yes</td></tr><tr><td style="text-align:center">py2exe</td><td style="text-align:center">yes</td><td style="text-align:center">no</td><td style="text-align:center">no</td><td style="text-align:center">yes</td><td style="text-align:center">MIT</td><td style="text-align:center">yes</td><td style="text-align:center">yes</td><td style="text-align:center">no</td><td style="text-align:center">no</td></tr><tr><td style="text-align:center">pyInstaller</td><td style="text-align:center">yes</td><td style="text-align:center">yes</td><td style="text-align:center">yes</td><td style="text-align:center">yes</td><td style="text-align:center">GPL</td><td style="text-align:center">yes</td><td style="text-align:center">no</td><td style="text-align:center">yes</td><td style="text-align:center">no</td></tr><tr><td style="text-align:center">cx_Freeze</td><td style="text-align:center">yes</td><td style="text-align:center">yes</td><td style="text-align:center">yes</td><td style="text-align:center">yes</td><td style="text-align:center">PSF</td><td style="text-align:center">no</td><td style="text-align:center">yes</td><td style="text-align:center">yes</td><td style="text-align:center">no</td></tr><tr><td style="text-align:center">py2app</td><td style="text-align:center">no</td><td style="text-align:center">no</td><td style="text-align:center">yes</td><td style="text-align:center">yes</td><td style="text-align:center">MIT</td><td style="text-align:center">no</td><td style="text-align:center">yes</td><td style="text-align:center">yes</td><td style="text-align:center">yes</td></tr></tbody></table><p>我们能看到有很多类库都可以解决我们的问题，其中<code>pyInstaller</code>、<code>cx_Freeze</code>、<code>bbFreeze</code>都不错，pkg_resources新版的<code>pyInstaller</code>貌似是支持的。</p><p>我们这里选用<code>pyInstaller</code>尝试一下，因为它各方面支持的是最好的。</p><h1 id="PyInstaller原理介绍"><a href="#PyInstaller原理介绍" class="headerlink" title="PyInstaller原理介绍"></a>PyInstaller原理介绍</h1><p><code>PyInstaller</code>其实就是把<code>python</code>解析器和脚本以及脚本的依赖库打包成一个可执行的文件，这和编译成真正的机器码是两回事，所以通过<code>PyInstaller</code>打包成一个可执行文件可能不会提高运行效率，相反可能会降低运行效率，但是它带来的好处就是在运行者的机器上不用安装<code>python</code>和你的脚本依赖的库。在<code>Linux</code>操作系统下，它主要用的<code>binutil</code>工具包里面的<code>ldd</code>和<code>objdump</code>命令。</p><p><code>PyInstaller</code>输入你指定的的脚本，首先分析脚本所依赖的其他脚本，然后去查找，复制，把所有相关的脚本收集起来，包括<code>Python</code>解析器，然后把这些文件放在一个目录下，再打包进一个可执行文件里。</p><p>这样就可以直接发布输出整个文件夹里面的文件，或者生成可执行文件。你只需要告诉用户，你的App是自我包含的，不需要安装其他包，或某个版本的<code>Python</code>，就可以直接运行。</p><p>但是需要注意的是，<code>PyInstaller</code>打包的执行文件，只能在和打包机器系统同样的环境下运行。它不具备可移植性，若需要在不同系统上运行，就必须针对不同平台进行打包。</p><h1 id="安装PyInstaller"><a href="#安装PyInstaller" class="headerlink" title="安装PyInstaller"></a>安装PyInstaller</h1><p>网络情况可以的话使用<code>pip</code>安装还是很方便的。</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">pip install pyinstaller</span><br></pre></td></tr></table></figure><p>如果网络不稳定，尤其在天朝访问墙外站点是很痛苦的，我们还可以通过下载源码包来安装。</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line"># 在源码包的根目录下执行</span><br><span class="line">python setup.py install</span><br></pre></td></tr></table></figure><p>安装完成后，检查安装版本。</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">pyinstaller --version</span><br></pre></td></tr></table></figure><h1 id="使用PyInstaller进行打包"><a href="#使用PyInstaller进行打包" class="headerlink" title="使用PyInstaller进行打包"></a>使用PyInstaller进行打包</h1><p><code>pyinstaller</code>的语法</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">pyinstaller [options] script [script ...] | specfile</span><br></pre></td></tr></table></figure><p>具体命令如何使用可以通过help或<a href="http://pythonhosted.org/PyInstaller/usage.html" target="_blank" rel="noopener">官方文档</a>去查询具体的用法。</p><p>我这里只说几个注意的点。</p><p><code>-F, --onefile    Create a one-file bundled executable.</code>创建一个可执行文件</p><p><code>-w, --windowed, --noconsole</code>去除黑框</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line"># A path to search for imports (like using PYTHONPATH). Multiple paths are allowed, separated by ‘:’, or use this option multiple times</span><br><span class="line"></span><br><span class="line">-p DIR, --paths DIR</span><br></pre></td></tr></table></figure><p>设置一个可搜索的入口路径，怎么理解呢？如果不指定这个参数打包出来的文件只能在生成它的目录下运行，如果打包时指定参数为<code>-p .</code>打包出来的文件可以放在任意路径下运行，如下示例：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">pyinstaller -w -F -p . your.py</span><br></pre></td></tr></table></figure><h1 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h1><ul><li><a href="http://docs.python-guide.org/en/latest/shipping/freezing/#comparison-of-freezing-tools" target="_blank" rel="noopener">Freezing Your Code</a></li><li><a href="http://pythonhosted.org/PyInstaller/usage.html" target="_blank" rel="noopener">PyInstaller官方WIKI</a></li><li><a href="https://github.com/pyinstaller/pyinstaller" target="_blank" rel="noopener">PyInstaller Github</a></li></ul><p>到这里<code>PyInstaller</code>就简单介绍完毕，感兴趣的可以试一试，我以前使用的是<code>py2exe</code>，其实<code>py2exe</code>也蛮好只不过它需要创建一个<code>py</code>脚本来把需要打包的脚本包含进去，用起来没有<code>PyInstaller</code>方便，希望这个简单的入门可以帮助到需要的朋友。</p><p>Keep Real！</p>]]></content>
    
    <summary type="html">
    
      如何将Python脚本打包成可执行文件
    
    </summary>
    
      <category term="python" scheme="https://ningyu1.github.io/categories/python/"/>
    
    
      <category term="python" scheme="https://ningyu1.github.io/tags/python/"/>
    
      <category term="py2exe" scheme="https://ningyu1.github.io/tags/py2exe/"/>
    
      <category term="pyinstaller" scheme="https://ningyu1.github.io/tags/pyinstaller/"/>
    
      <category term="bbFreeze" scheme="https://ningyu1.github.io/tags/bbFreeze/"/>
    
      <category term="cx_Freeze" scheme="https://ningyu1.github.io/tags/cx-Freeze/"/>
    
      <category term="py2app" scheme="https://ningyu1.github.io/tags/py2app/"/>
    
  </entry>
  
  <entry>
    <title>Git SSH Key settings and passphrase reset</title>
    <link href="https://ningyu1.github.io/20180130/58-git-ssh.html"/>
    <id>https://ningyu1.github.io/20180130/58-git-ssh.html</id>
    <published>2018-01-30T08:10:20.000Z</published>
    <updated>2024-04-23T01:48:15.035Z</updated>
    
    <content type="html"><![CDATA[<p>在使用github仓库的时候我们经常会看到<code>clone</code>有两种方式:<code>https</code>、<code>ssh</code>，<code>https</code>的方式使用起来非常简单但是每次在<code>pull</code>、<code>push</code>的时候需要输入密码，一两次还可以忍受但是作为常态是有点崩溃的，这个时候我们可以使用<code>ssh</code>的方式，<code>ssh</code>的好处就是在<code>pull</code>、<code>push</code>的时候可以使用密码也可以不使用密码，但是前提是要设置好<code>ssh key</code>，如果你是<code>Repository</code>的管理员那很好设置，如果不是管理员那就老老实实的使用<code>https</code>的方式，下来我们就说一下使用<code>ssh</code>遇到的问题。</p><h1 id="修改用户主目录（home）"><a href="#修改用户主目录（home）" class="headerlink" title="修改用户主目录（home）"></a>修改用户主目录（home）</h1><p>当出现下图问题时：</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/git-ssh/1.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p>是说明你的.ssh目录设置的有问题，关于用户主目录（home）的问题，一般windows机器安装完git后home都会是<code>C:\Users\用户名</code>这种目录，但是打开Git bash时它无法识别home目录使用到了其他莫名其妙的目录（有的时候会是不存在的目录或是网络盘符），在这个时候就需要变更home目录，变更的方法如下：</p><p>环境：windows</p><h2 id="Git-version-1-x系列"><a href="#Git-version-1-x系列" class="headerlink" title="Git version 1.x系列"></a>Git version 1.x系列</h2><p>如果是Git version 1.x系列，打开profile文件，文件位置：<code>$\Git\etc\profile</code>（$替换成你的盘符）。<br>在profile中找到：<code>HOME=&quot;$(cd &quot;$HOME&quot; ; pwd)&quot;</code>这个位置，在前面增加你想变成的home目录，例如：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line"># normalize HOME to unix path</span><br><span class="line">HOME=&quot;C:\Users\用户名&quot;</span><br><span class="line">HOME=&quot;$(cd &quot;$HOME&quot; ; pwd)&quot;</span><br><span class="line"></span><br><span class="line">export PATH=&quot;$HOME/bin:$PATH&quot;</span><br><span class="line"></span><br><span class="line">export GNUPGHOME=~/.gnupg</span><br></pre></td></tr></table></figure><p>当修改好之后，重启Git bash即可，输入<code>cd ~/.ssh</code>，会进入你设置好的目录，在这个目录下生成相关的配置文件，如：.ssh、.gnupg、.bash_history、.gitconfig等，如果以前已经有这些文件可以copy到这个目录下直接使用。</p><h2 id="Git-version-2-x系列"><a href="#Git-version-2-x系列" class="headerlink" title="Git version 2.x系列"></a>Git version 2.x系列</h2><p>如果是Git version 2.x系列，请设置环境变量，增加HOME的环境变量，目录为：<code>C:\Users\用户名</code>（你想设置的目录），随后重启Git bash即可，输入<code>cd ~/.ssh</code>，会进入你设置好的目录。</p><p>按照上面步骤修改好之后，出现下图所示就证明修改完成了。如：</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/git-ssh/2.png" alt title>                </div>                <div class="image-caption"></div>            </figure><h1 id="ssh-key设置"><a href="#ssh-key设置" class="headerlink" title="ssh key设置"></a>ssh key设置</h1><p>输入<code>cd ~/.ssh</code>进入home目录使用如下如下方法生成ssh key</p><ul><li>可以在Git bash中使用<code>ssh-keygen</code>生成ssh key</li><li>还可以使用eclipse的ssh2工具生成，操作如下：Window -&gt; Preferences -&gt; General -&gt; Network Connections -&gt; SSH -&gt; Key Management -&gt; Generate RSA Key</li><li>还可以使用TortoiseGit的PuTTY Key Generator工具生成。</li></ul><p>方法有很多，生成好的private key用文本编辑器打开复制出来，粘贴到git hub的settings中即可，操作如下：github -&gt; Settings -&gt; SSH and GPG keys -&gt; New SSH key，起个名字粘贴key然后保存即可。</p><h1 id="每次都输入passphrase问题"><a href="#每次都输入passphrase问题" class="headerlink" title="每次都输入passphrase问题"></a>每次都输入passphrase问题</h1><p>当我们在第一次<code>git clone</code>的时候会提示<code>Enter passphrase</code>，这个时候如果输入了密码，那以后<code>pull</code>、<code>push</code>都需要输入这个密码，就像我下图这样：</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/git-ssh/3.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p>我们使用ssh就是图个方便不想输入密码，出现这个问题怎么办呢？</p><p>在第一次<code>git clone</code>的时候提示<code>Enter passphrase</code>的时候不要输入密码，直接回车即可，如果输入了密码那就需要重置密码才能解决这个问题。</p><h2 id="重置passphrase"><a href="#重置passphrase" class="headerlink" title="重置passphrase"></a>重置passphrase</h2><p>打开Git bash使用如下命令重置密码</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">ssh-keygen -p</span><br></pre></td></tr></table></figure><p>输入后根据下图提示操作：</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/git-ssh/4.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p>这样就完成了重置密码为空的操作了，后面再<code>pull</code>、<code>push</code>的时候都不会再提示输入密码。</p>]]></content>
    
    <summary type="html">
    
      Git SSH Key settings and passphrase reset
    
    </summary>
    
      <category term="git" scheme="https://ningyu1.github.io/categories/git/"/>
    
    
      <category term="git" scheme="https://ningyu1.github.io/tags/git/"/>
    
      <category term="github" scheme="https://ningyu1.github.io/tags/github/"/>
    
      <category term="ssh key" scheme="https://ningyu1.github.io/tags/ssh-key/"/>
    
      <category term="passphrase" scheme="https://ningyu1.github.io/tags/passphrase/"/>
    
  </entry>
  
  <entry>
    <title>CAS Server强制踢人功能实现方式</title>
    <link href="https://ningyu1.github.io/20180126/57-cas-server1.html"/>
    <id>https://ningyu1.github.io/20180126/57-cas-server1.html</id>
    <published>2018-01-26T07:07:36.000Z</published>
    <updated>2024-04-23T01:48:15.035Z</updated>
    
    <content type="html"><![CDATA[<p>前面写过一篇关于CAS Server使用的经验总结，主要总结了CAS Server在使用的时候遇到的一些常见问题，比如说：证书、SLO、集群session处理、自定义用户认证、Ticket持久化等问题，传送门：<a href="https://ningyu1.github.io/site/post/54-cas-server/">CAS使用经验总结，纯干货</a>，这次在基础上又增加了一个很常见很普通的问题，那就是踢人功能。</p><p>在管理系统这个领域里面踢人功能并不陌生，为了更好的管理用户串用账号，安全等方面考虑，接下来我们就细说一下CAS如何实现踢人的功能。</p><p>先说一下踢人功能的场景：</p><p>用户A在机器A上登录了APP1，用户A在机器B上登录APP1，在这种情况下后者登录需要踢掉前者的登录状态。</p><p>用户A在机器A上登录了APP1，用户B在机器B上登录了APP1，在这种情况下不存在踢人操作。</p><p>用户A在机器A上登录了APP1，用户A在机器B上登录了APP2，在这种情况下要分情况了，可以踢也可以不踢，这个就根据产品情况来选择，我们本次测试不能解决这个场景，如何解决我还在摸索中。</p><p>要做踢人功能之前先了解一下CAS的认证授权机制是如何完成的？</p><p>我这里直接引用官网的架构图：</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/cas/1.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p>CAS Server与应用的Session交互图：</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/cas/2.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p>其实CAS就是生成维护Ticket信息和应用session做绑定，当然它的Ticket实现还是比较复杂的，有树形关系以及和Service关联关系，从Ticket的源码能看的出来它有root的判断和Service的映射列表。</p><p>根据上面对CAS的理解，接下来我们说CAS怎么操作踢人功能？</p><h1 id="踢人功能实现思路"><a href="#踢人功能实现思路" class="headerlink" title="踢人功能实现思路"></a>踢人功能实现思路</h1><p>在登录认证的时候记录一下，在下次登录获取到登录的人员列表，然后去匹配找出是否存在相同的用户，如果存在相同的用户，就注销掉这个用户的登录信息，这个是常规的思路和做法，但是在CAS里如何去找到切入点来进行判断操作呢？</p><p>我们在上一篇中提到了自定义认证逻辑，那么我们就可以继续在认证的这个切入点去进一步分析。</p><p>这里要先搞清楚一个概念：<code>Authentication</code>和<code>Authorization</code>这两者是不同的。</p><p><code>Authentication</code>：字面意思认证，怎么理解这个认证呢？举个例子：我们每个人都有身份证，比如你去买火车票，买火车票需要出示身份证，那这个身份证就是证明你是你自己的凭证，那这个证明的过程就是认证。</p><p><code>Authorization</code>：字面意思授权，怎么理解这个授权呢？举个例子：继续拿买火车票来说，你刚才出示了身份证证明了你自己，然后给了钱买了一张火车票，铁道部给了你一张票，这个票授权了你可以乘坐X车次X座位的权限其他车次你无权乘坐，那么这张票就是证明你确实买了X车次X座位的凭证，这就是授权。</p><p>换回系统的角度来说，认证就是验证用户名密码，授权就是验证你能不能操作某个功能的权限。</p><p>理解完认证和授权的区别，我们就开始从认证这块的切入点去看如何操作，CAS提供了这个类<code>TicketRegistry</code>它是管理所有<code>Ticket</code>的接口，通过调用<code>TicketRegistry.getTickets()</code>方法可以获取到所有认证用户的凭证。</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">/**</span><br><span class="line"> * Retrieve all tickets from the registry.</span><br><span class="line"> *</span><br><span class="line"> * @return collection of tickets currently stored in the registry. Tickets</span><br><span class="line"> * might or might not be valid i.e. expired.</span><br><span class="line"> */</span><br><span class="line">Collection&lt;Ticket&gt; getTickets();</span><br></pre></td></tr></table></figure><p>那有了凭证信息就好更进一步操作。</p><p>CAS提供了<code>TicketGrantingTicket</code>，这个类是<code>Ticket</code>接口的一个实现类，可以通过<code>TicketGrantingTicket.getAuthentication().getPrincipal().getId()</code>来获取用户的身份。</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">/**</span><br><span class="line"> * @return the unique id for the Principal</span><br><span class="line"> */</span><br><span class="line">String getId();</span><br></pre></td></tr></table></figure><p><code>getId()</code>返回的是登录的用户名，那拿到了用户名就要考虑如何注销的事情了。</p><p>刚才说到了它<code>TicketGrantingTicket</code>是<code>Ticket</code>接口的实现类，它的<code>t.markTicketExpired()</code>方法就是标记<code>Ticket</code>过期的动作。</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">/**</span><br><span class="line"> * Mark a ticket as expired.</span><br><span class="line"> */</span><br><span class="line">void markTicketExpired();</span><br></pre></td></tr></table></figure><p>光标记过期还不能完成注销操作，还需要通过<code>ticketRegistry.deleteTicket(t.getId())</code>来删除<code>Ticket</code>信息。</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">/**</span><br><span class="line"> * Remove a specific ticket from the registry.</span><br><span class="line"> * If ticket to delete is TGT then related service tickets are removed as well.</span><br><span class="line"> *</span><br><span class="line"> * @param ticketId The id of the ticket to delete.</span><br><span class="line"> * @return the number of tickets deleted including children.</span><br><span class="line"> */</span><br><span class="line">int deleteTicket(String ticketId);</span><br></pre></td></tr></table></figure><p>上面的分析过程看上去是可行的，那我们就来测试一下是否可以达到踢人功能的目的。</p><h1 id="踢人功能实现过程"><a href="#踢人功能实现过程" class="headerlink" title="踢人功能实现过程"></a>踢人功能实现过程</h1><p>话不多说直接帖实现代码</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">/**</span><br><span class="line"> * 登录成功，踢掉前一个相同登录的人</span><br><span class="line"> * </span><br><span class="line"> * @param username</span><br><span class="line"> */</span><br><span class="line">public void forceLogout(final String username) &#123;</span><br><span class="line">TicketRegistry ticketRegistry = (TicketRegistry) ApplicationContextProvider.getApplicationContext().getBean(&quot;ticketRegistry&quot;);</span><br><span class="line">final Collection&lt;Ticket&gt; ticketsInCache = ticketRegistry.getTickets();</span><br><span class="line">for (final Ticket ticket : ticketsInCache) &#123;</span><br><span class="line">TicketGrantingTicket t = null;</span><br><span class="line">try &#123;</span><br><span class="line">log.info(&quot;cast TicketGrantingTicketImpl&quot;);</span><br><span class="line">t = (TicketGrantingTicketImpl) ticket;</span><br><span class="line">&#125; catch (Exception e) &#123;</span><br><span class="line">log.error(&quot;cast TicketGrantingTicketImpl is error:&quot;, e);</span><br><span class="line">t = ((ServiceTicketImpl) ticket).getGrantingTicket();</span><br><span class="line">&#125;</span><br><span class="line">if (t.getAuthentication().getPrincipal().getId().equals(username) &amp;&amp; t.getId() != null) &#123;</span><br><span class="line">/***</span><br><span class="line"> * 注销方法一 涉及到cookie的删除，但是无法获取response 该方法有待考究 未测试</span><br><span class="line"> */</span><br><span class="line">// centralAuthenticationService.destroyTicketGrantingTicket(t.getId());</span><br><span class="line">/***</span><br><span class="line"> * 注销方法二</span><br><span class="line"> */</span><br><span class="line">// t.expire();</span><br><span class="line">t.markTicketExpired();</span><br><span class="line">ticketRegistry.deleteTicket(t.getId());</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><ol><li>项目：<code>cas-site</code></li><li>类：<code>org.apereo.cas.adaptors.jdbc.QueryAndEncodeDatabaseAuthenticationHandler</code></li><li>方法：<code>authenticateUsernamePasswordInternal()</code>的<code>createHandlerResult()</code>之前调用。</li></ol><p>代码如下：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">@Override</span><br><span class="line">protected HandlerResult authenticateUsernamePasswordInternal(final UsernamePasswordCredential transformedCredential)</span><br><span class="line">        throws GeneralSecurityException, PreventedException &#123;</span><br><span class="line"></span><br><span class="line">    if (StringUtils.isBlank(this.sql) || StringUtils.isBlank(this.algorithmName) || getJdbcTemplate() == null) &#123;</span><br><span class="line">        throw new GeneralSecurityException(&quot;Authentication handler is not configured correctly&quot;);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    final String username = transformedCredential.getUsername();</span><br><span class="line">    try &#123;</span><br><span class="line">        // Get password and salt</span><br><span class="line">        final Map&lt;String, Object&gt; rows = getJdbcTemplate().queryForMap(this.sql, username);</span><br><span class="line">        final String encodedPassword = rows.get(&quot;password&quot;).toString();</span><br><span class="line">        final String dbSalt = rows.get(&quot;salt&quot;).toString();</span><br><span class="line">        SaltPasswordEncoder passwordEncoder = new SaltPasswordEncoder();</span><br><span class="line">        passwordEncoder.setSalt(dbSalt);</span><br><span class="line">        if (!passwordEncoder.matches(transformedCredential.getPassword(), encodedPassword)) &#123;</span><br><span class="line">            throw new FailedLoginException(&quot;Password does not match value on record.&quot;);</span><br><span class="line">        &#125;</span><br><span class="line">// 登录成功，踢掉前一个相同登录的人</span><br><span class="line">        forceLogout(username);</span><br><span class="line">        return createHandlerResult(transformedCredential, this.principalFactory.createPrincipal(username), null);</span><br><span class="line"></span><br><span class="line">    &#125; catch (final IncorrectResultSizeDataAccessException e) &#123;</span><br><span class="line">        if (e.getActualSize() == 0) &#123;</span><br><span class="line">            throw new AccountNotFoundException(username + &quot; not found with SQL query&quot;);</span><br><span class="line">        &#125; else &#123;</span><br><span class="line">            throw new FailedLoginException(&quot;Multiple records found for &quot; + username);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125; catch (final DataAccessException e) &#123;</span><br><span class="line">        throw new PreventedException(&quot;SQL exception while executing query for &quot; + username, e);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>cas-site项目我已经放入到了github，在这篇<a href="https://ningyu1.github.io/site/post/54-cas-server/">《CAS使用经验总结，纯干货》</a>博文中可以找到。</p><p>万事俱备只欠东风了，接下来就是启动程序来验证它。</p><p>理想很美好，现实很骨感，出现了如下错误：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">javax.persistence.TransactionRequiredException: No EntityManager with actual transaction available for current thread - cannot reliably process &apos;remove&apos; call</span><br><span class="line">at org.springframework.orm.jpa.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler.invoke(SharedEntityManagerCreator.java:282) ~[spring-orm-4.3.4.RELEASE.jar:4.3.4.RELEASE]</span><br><span class="line">at com.sun.proxy.$Proxy175.remove(Unknown Source) ~[?:?]</span><br><span class="line">at org.apereo.cas.ticket.registry.JpaTicketRegistry.removeTicket(JpaTicketRegistry.java:72) ~[cas-server-support-jpa-ticket-registry-5.0.4.jar:5.0.4]</span><br><span class="line">at org.apereo.cas.ticket.registry.JpaTicketRegistry.deleteTicketsFromResultList(JpaTicketRegistry.java:214) ~[cas-server-support-jpa-ticket-registry-5.0.4.jar:5.0.4]</span><br><span class="line">at org.apereo.cas.ticket.registry.JpaTicketRegistry.deleteTicketGrantingTickets(JpaTicketRegistry.java:244) ~[cas-server-support-jpa-ticket-registry-5.0.4.jar:5.0.4]</span><br><span class="line">at org.apereo.cas.ticket.registry.JpaTicketRegistry.deleteSingleTicket(JpaTicketRegistry.java:158) ~[cas-server-support-jpa-ticket-registry-5.0.4.jar:5.0.4]</span><br><span class="line">at org.apereo.cas.ticket.registry.AbstractTicketRegistry.deleteTicket(AbstractTicketRegistry.java:125) ~[cas-server-core-tickets-5.0.4.jar:5.0.4]</span><br><span class="line">at org.apereo.cas.ticket.registry.AbstractTicketRegistry$$FastClassBySpringCGLIB$$d3c67a11.invoke(&lt;generated&gt;) ~[cas-server-core-tickets-5.0.4.jar:5.0.4]</span><br><span class="line">at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204) ~[spring-core-4.3.4.RELEASE.jar:4.3.4.RELEASE]</span><br><span class="line">at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:651) ~[spring-aop-4.3.4.RELEASE.jar:4.3.4.RELEASE]</span><br><span class="line">at org.apereo.cas.ticket.registry.JpaTicketRegistry$$EnhancerBySpringCGLIB$$b6d104b8.deleteTicket(&lt;generated&gt;) ~[cas-server-support-jpa-ticket-registry-5.0.4.jar:5.0.4]</span><br><span class="line">at org.apereo.cas.ticket.registry.AbstractTicketRegistry$$FastClassBySpringCGLIB$$d3c67a11.invoke(&lt;generated&gt;) ~[cas-server-core-tickets-5.0.4.jar:5.0.4]</span><br><span class="line">at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204) ~[spring-core-4.3.4.RELEASE.jar:4.3.4.RELEASE]</span><br><span class="line">at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:651) ~[spring-aop-4.3.4.RELEASE.jar:4.3.4.RELEASE]</span><br><span class="line">at org.apereo.cas.ticket.registry.JpaTicketRegistry$$EnhancerBySpringCGLIB$$ef44b76a.deleteTicket(&lt;generated&gt;) ~[cas-server-support-jpa-ticket-registry-5.0.4.jar:5.0.4]</span><br><span class="line">at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[?:1.8.0_31]</span><br><span class="line">at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[?:1.8.0_31]</span><br><span class="line">at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:1.8.0_31]</span><br><span class="line">at java.lang.reflect.Method.invoke(Method.java:483) ~[?:1.8.0_31]</span><br></pre></td></tr></table></figure><p><strong>ps.异常堆栈很长我只截了一部分展示出来。</strong></p><p>这个错误是个什么鬼？从异常字面理解：在当前的线程中没有找到可用的事务，无法处理“删除”调用。</p><p>这个错误是<code>JPA</code>的错误，因为我的<code>Ticket Registry</code>配置的是<code>JPA</code>的方式，我猜测换成其他方式也会有类似的错误，我去掉<code>JPA</code>采用<code>InMemroy</code>的方式处理<code>Ticket Registry</code>，再次进行测试。</p><p>果然出现了类似的错误，如下：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">javax.persistence.TransactionRequiredException: no transaction is in progress</span><br><span class="line">at org.hibernate.internal.SessionImpl.checkTransactionNeeded(SessionImpl.java:3428) ~[hibernate-core-5.2.2.Final.jar:5.2.2.Final]</span><br><span class="line">at org.hibernate.internal.SessionImpl.find(SessionImpl.java:3362) ~[hibernate-core-5.2.2.Final.jar:5.2.2.Final]</span><br><span class="line">at org.hibernate.internal.SessionImpl.find(SessionImpl.java:3342) ~[hibernate-core-5.2.2.Final.jar:5.2.2.Final]</span><br><span class="line">at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[?:1.8.0_31]</span><br><span class="line">at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[?:1.8.0_31]</span><br><span class="line">at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:1.8.0_31]</span><br><span class="line">at java.lang.reflect.Method.invoke(Method.java:483) ~[?:1.8.0_31]</span><br><span class="line">at org.springframework.orm.jpa.ExtendedEntityManagerCreator$ExtendedEntityManagerInvocationHandler.invoke(ExtendedEntityManagerCreator.java:347) ~[spring-orm-4.3.4.RELEASE.jar:4.3.4.RELEASE]</span><br><span class="line">at com.sun.proxy.$Proxy175.find(Unknown Source) ~[?:?]</span><br><span class="line">at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[?:1.8.0_31]</span><br><span class="line">at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[?:1.8.0_31]</span><br><span class="line">at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:1.8.0_31]</span><br><span class="line">at java.lang.reflect.Method.invoke(Method.java:483) ~[?:1.8.0_31]</span><br><span class="line">at org.springframework.orm.jpa.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler.invoke(SharedEntityManagerCreator.java:298) ~[spring-orm-4.3.4.RELEASE.jar:4.3.4.RELEASE]</span><br><span class="line">at com.sun.proxy.$Proxy175.find(Unknown Source) ~[?:?]</span><br></pre></td></tr></table></figure><p>说白了就是没有开启事务被禁止操作了。</p><p>这个怎么解决？cas-site采用的是<code>overlays</code>的方式构建，要看具体功能就要翻CAS的源码来看它是如何控制事务的。</p><p>于是去翻CAS的源码，翻源码也要讲究技巧的，要不然翻一天都翻不到关键点。</p><p>我们这里需要找如何开启事务的代码，还好CAS使用的是<code>Spring</code>来管理事务的，<code>Spring</code>的事务开启无非就这两种：一种是<code>AOP</code>方式，一种是手动方式。</p><p>那么<code>AOP</code>的方式可以使用注解（<code>Annotation</code>）也可以使用<code>XML</code>的配置去做。</p><p>CAS v5.0.4使用的<code>Spring Boot</code>的方式构建，说白了就是使用编程（<code>Java Config</code>）的方式替换<code>XML</code>的配置方式。而且我们使用的<code>Ticket Registry</code>是<code>JPA</code>，<code>JPA</code>的操作肯定要处理事务的，因此我们就锁定到注解（<code>Annotation</code>）的方式和<code>JPA</code>的实现上去找。</p><p>最终目标定位到了<code>cas-server-support-jpq-ticket-registry-5.0.4.jar</code>这个包上。</p><p>查看这个包的<code>org.apereo.cas.ticket.registry.JpaTicketRegistry</code>类代码</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">/**</span><br><span class="line"> * JPA implementation of a CAS &#123;@link TicketRegistry&#125;. This implementation of</span><br><span class="line"> * ticket registry is suitable for HA environments.</span><br><span class="line"> *</span><br><span class="line"> * @author Scott Battaglia</span><br><span class="line"> * @author Marvin S. Addison</span><br><span class="line"> * @since 3.2.1</span><br><span class="line"> */</span><br><span class="line">@EnableTransactionManagement(proxyTargetClass = true)</span><br><span class="line">@Transactional(transactionManager = &quot;ticketTransactionManager&quot;, readOnly = false)</span><br><span class="line">public class JpaTicketRegistry extends AbstractTicketRegistry &#123;</span><br><span class="line">.....................其余的省略..............................</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>很明显就是我们说的注解（<code>Annotation</code>）的使用方式，我们再次修改代码。</p><h2 id="踢人功能代码重构"><a href="#踢人功能代码重构" class="headerlink" title="踢人功能代码重构"></a>踢人功能代码重构</h2><p><code>@EnableTransactionManagement(proxyTargetClass = true)</code>开启代理的方式，那我们就要抽一个接口和一个实现类来做，这里的具体原因就不多说了做多了都明白。</p><p><code>@Transactional(transactionManager = &quot;ticketTransactionManager&quot;, readOnly = false)</code>在实现类上直接使用这个注解方式。</p><p>直接贴重构后的代码：</p><p>新建接口<code>ForceLogoutManager</code></p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">public interface ForceLogoutManager &#123;</span><br><span class="line"></span><br><span class="line">public void doLogout(final String username);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>新建实现类<code>ForceLogoutManagerImpl</code></p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">import java.util.Collection;</span><br><span class="line">import org.apereo.cas.ticket.ServiceTicketImpl;</span><br><span class="line">import org.apereo.cas.ticket.Ticket;</span><br><span class="line">import org.apereo.cas.ticket.TicketGrantingTicket;</span><br><span class="line">import org.apereo.cas.ticket.TicketGrantingTicketImpl;</span><br><span class="line">import org.apereo.cas.ticket.registry.TicketRegistry;</span><br><span class="line">import org.apereo.cas.util.ApplicationContextProvider;</span><br><span class="line">import org.slf4j.Logger;</span><br><span class="line">import org.slf4j.LoggerFactory;</span><br><span class="line">import org.springframework.stereotype.Component;</span><br><span class="line">import org.springframework.transaction.annotation.EnableTransactionManagement;</span><br><span class="line">import org.springframework.transaction.annotation.Transactional;</span><br><span class="line"></span><br><span class="line">@EnableTransactionManagement(proxyTargetClass = true)</span><br><span class="line">@Transactional(transactionManager = &quot;ticketTransactionManager&quot;, readOnly = false)</span><br><span class="line">@Component(&quot;forceLogoutManager&quot;)</span><br><span class="line">public class ForceLogoutManagerImpl implements ForceLogoutManager &#123;</span><br><span class="line"></span><br><span class="line">private final Logger log = LoggerFactory.getLogger(this.getClass());</span><br><span class="line"></span><br><span class="line">/**</span><br><span class="line"> * 登录成功，踢掉前一个相同登录的人</span><br><span class="line"> * </span><br><span class="line"> * @param username</span><br><span class="line"> */</span><br><span class="line">public void doLogout(final String username) &#123;</span><br><span class="line">TicketRegistry ticketRegistry = (TicketRegistry) ApplicationContextProvider.getApplicationContext()</span><br><span class="line">.getBean(&quot;ticketRegistry&quot;);</span><br><span class="line">final Collection&lt;Ticket&gt; ticketsInCache = ticketRegistry.getTickets();</span><br><span class="line">for (final Ticket ticket : ticketsInCache) &#123;</span><br><span class="line">TicketGrantingTicket t = null;</span><br><span class="line">try &#123;</span><br><span class="line">log.info(&quot;cast TicketGrantingTicketImpl&quot;);</span><br><span class="line">t = (TicketGrantingTicketImpl) ticket;</span><br><span class="line">&#125; catch (Exception e) &#123;</span><br><span class="line">log.error(&quot;cast TicketGrantingTicketImpl is error:&quot;, e);</span><br><span class="line">t = ((ServiceTicketImpl) ticket).getGrantingTicket();</span><br><span class="line">&#125;</span><br><span class="line">if (t.getAuthentication().getPrincipal().getId().equals(username) &amp;&amp; t.getId() != null) &#123;</span><br><span class="line">/***</span><br><span class="line"> * 注销方法一 涉及到cookie的删除，但是无法获取response 该方法有待考究 未测试</span><br><span class="line"> */</span><br><span class="line">// centralAuthenticationService.destroyTicketGrantingTicket(t.getId());</span><br><span class="line">/***</span><br><span class="line"> * 注销方法二</span><br><span class="line"> */</span><br><span class="line">// t.expire();</span><br><span class="line">t.markTicketExpired();</span><br><span class="line">ticketRegistry.deleteTicket(t.getId());</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>修改<code>org.apereo.cas.adaptors.jdbc.QueryAndEncodeDatabaseAuthenticationHandler</code>类<code>authenticateUsernamePasswordInternal</code>方法</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">public ForceLogoutManager getForceLogoutManager() &#123;</span><br><span class="line">return (ForceLogoutManager) ApplicationContextProvider.getApplicationContext().getBean(&quot;forceLogoutManager&quot;);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">@Override</span><br><span class="line">protected HandlerResult authenticateUsernamePasswordInternal(final UsernamePasswordCredential transformedCredential)</span><br><span class="line">        throws GeneralSecurityException, PreventedException &#123;</span><br><span class="line"></span><br><span class="line">    if (StringUtils.isBlank(this.sql) || StringUtils.isBlank(this.algorithmName) || getJdbcTemplate() == null) &#123;</span><br><span class="line">        throw new GeneralSecurityException(&quot;Authentication handler is not configured correctly&quot;);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    final String username = transformedCredential.getUsername();</span><br><span class="line">    try &#123;</span><br><span class="line">        // Get password and salt</span><br><span class="line">        final Map&lt;String, Object&gt; rows = getJdbcTemplate().queryForMap(this.sql, username);</span><br><span class="line">        final String encodedPassword = rows.get(&quot;password&quot;).toString();</span><br><span class="line">        final String dbSalt = rows.get(&quot;salt&quot;).toString();</span><br><span class="line">        SaltPasswordEncoder passwordEncoder = new SaltPasswordEncoder();</span><br><span class="line">        passwordEncoder.setSalt(dbSalt);</span><br><span class="line">        if (!passwordEncoder.matches(transformedCredential.getPassword(), encodedPassword)) &#123;</span><br><span class="line">            throw new FailedLoginException(&quot;Password does not match value on record.&quot;);</span><br><span class="line">        &#125;</span><br><span class="line">// 登录成功，踢掉前一个相同登录的人</span><br><span class="line">        getForceLogoutManager().doLogout(username);</span><br><span class="line">        return createHandlerResult(transformedCredential, this.principalFactory.createPrincipal(username), null);</span><br><span class="line"></span><br><span class="line">    &#125; catch (final IncorrectResultSizeDataAccessException e) &#123;</span><br><span class="line">        if (e.getActualSize() == 0) &#123;</span><br><span class="line">            throw new AccountNotFoundException(username + &quot; not found with SQL query&quot;);</span><br><span class="line">        &#125; else &#123;</span><br><span class="line">            throw new FailedLoginException(&quot;Multiple records found for &quot; + username);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125; catch (final DataAccessException e) &#123;</span><br><span class="line">        throw new PreventedException(&quot;SQL exception while executing query for &quot; + username, e);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>再次启动测试。</p><p>很顺利调用没有任何问题，到这里基于CAS v5.0.4的踢人功能的处理过程就整理完毕了。</p><p>最后还有一句话，我的愿望是：世界和平，快乐编程每一天，keep real！</p>]]></content>
    
    <summary type="html">
    
      CAS Server强制踢人功能实现方式
    
    </summary>
    
      <category term="CAS" scheme="https://ningyu1.github.io/categories/CAS/"/>
    
    
      <category term="CAS" scheme="https://ningyu1.github.io/tags/CAS/"/>
    
      <category term="SLO" scheme="https://ningyu1.github.io/tags/SLO/"/>
    
      <category term="Single Logout" scheme="https://ningyu1.github.io/tags/Single-Logout/"/>
    
      <category term="Ticket" scheme="https://ningyu1.github.io/tags/Ticket/"/>
    
      <category term="Ticket Registry" scheme="https://ningyu1.github.io/tags/Ticket-Registry/"/>
    
      <category term="Session Centralized Storage" scheme="https://ningyu1.github.io/tags/Session-Centralized-Storage/"/>
    
      <category term="Cookie" scheme="https://ningyu1.github.io/tags/Cookie/"/>
    
      <category term="CAS Cluster" scheme="https://ningyu1.github.io/tags/CAS-Cluster/"/>
    
      <category term="CAS Server" scheme="https://ningyu1.github.io/tags/CAS-Server/"/>
    
      <category term="ForceLogout" scheme="https://ningyu1.github.io/tags/ForceLogout/"/>
    
  </entry>
  
  <entry>
    <title>Trouble Shooting —— HTTPS(SSL)站点使用WebSocket(ws)出现SecurityError问题</title>
    <link href="https://ningyu1.github.io/20180125/56-websocket-ssl.html"/>
    <id>https://ningyu1.github.io/20180125/56-websocket-ssl.html</id>
    <published>2018-01-25T09:04:36.000Z</published>
    <updated>2024-04-23T01:48:15.035Z</updated>
    
    <content type="html"><![CDATA[<p>最近发生了一个问题我觉得挺有意思的，所以针对这个问题总结一下。</p><p>最近公司服务上了https(SSL)，在https(SSL)的环境下呢本因为可以愉快的玩耍，但是后来发现程序有使用websocket（ws://domain.com），这里就有朋友想了使用ws跟ssl有什么关系？我可以很明确的告诉你当然有关系。</p><p>当你的站点使用的是http的时候，使用ws可以很愉快的玩耍。当换成了https(SSL)那么问题来了。</p><p>在chrome下是测试没有问题可以正常使用，但是在ie下就出现了问题，报SecurityError的错误，那这个错误是什么原因呢?</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">WebSocket connection to &apos;ws://domain.com/websocket&apos; failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED</span><br></pre></td></tr></table></figure><p>应该是每个浏览器对websocket的支持不一样或者说每个浏览器的安全沙箱不太一样，禁止了一些用法，各大浏览器对websocket的支持情况请看：<a href="https://caniuse.com/#search=websocket" target="_blank" rel="noopener">https://caniuse.com/#search=websocket</a></p><p>无意中看到了mozilla的websocket支持详细说明如下：</p><p><span style="color:red"><br><strong>Security considerations</strong><br><br>WebSockets should not be used in a mixed content environment; that is, you shouldn’t open a non-secure WebSocket connection from a page loaded using HTTPS or vice-versa. In fact, some browsers explicitly forbid this, including Firefox 8 and later.<br></span></p><p>具体地址：<a href="https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_client_applications" target="_blank" rel="noopener">https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_client_applications</a></p><p>意思呢就是，ws与http对应，wss与https对应，如果站点使用的是https那就必须使用wss来做websocket请求不能使用ws来请求，不允许混合的方式使用。</p><p>看到这个就更加明确了问题所在：安全机制问题，最好不要混合使用避免奇怪的问题。</p><p>于是就开启了wss服务的使用路程。</p><p>如果你的wss服务是使用ip方式访问的，那么需要制作一个对应这个ip的证书，可以使用openssl生成自签名证书，但是不推荐使用ip的方式访问WebSocket。</p><p>如果你的wss服务是使用域名方式访问的，那么需要制作一个对应这个域名证书（最好是通配符域名证书），这样在构建wss服务的时候将证书配置进去。</p><p>构建wss服务有很多种方式，我这里提供一种比较简单的方式。</p><h1 id="使用nginx提供ssl代理"><a href="#使用nginx提供ssl代理" class="headerlink" title="使用nginx提供ssl代理"></a>使用nginx提供ssl代理</h1><p>保留以前的ws服务提供方式不做任何变更，增加一个nginx开启ssl代理，配置跟常规的ssl配置有一些细微的变化，那就是header会有一些变化，websocket需要指定<code>header：Upgrade</code>和<code>http version：1.1</code> ，因此我这里给出配置详情：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">server &#123;</span><br><span class="line">    listen       443 ssl;</span><br><span class="line">server_name  your.domain.com;#你的域名，如果没有域名就去掉</span><br><span class="line">ssl on;</span><br><span class="line">#ssl_certificate     127.0.0.1.crt;</span><br><span class="line">    #ssl_certificate_key 127.0.0.1.key;</span><br><span class="line">ssl_certificate     your.domain.com.pem;#这里可以使用pem文件和crt文件</span><br><span class="line">    ssl_certificate_key your.domain.com.key;</span><br><span class="line">ssl_session_timeout 5m;</span><br><span class="line">ssl_session_cache shared:SSL:50m;</span><br><span class="line">ssl_protocols SSLv3 SSLv2 TLSv1 TLSv1.1 TLSv1.2;</span><br><span class="line">ssl_ciphers ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP;</span><br><span class="line"></span><br><span class="line">location / &#123;</span><br><span class="line">proxy_pass http://127.0.0.1:19808;# 这里换成你想转发的ws服务地址即可</span><br><span class="line">proxy_http_version 1.1;</span><br><span class="line">proxy_set_header Upgrade $http_upgrade;</span><br><span class="line">proxy_set_header Connection &quot;Upgrade&quot;;</span><br><span class="line">proxy_set_header X-Real-IP $remote_addr;</span><br><span class="line">        &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>将证书文件放到conf同级目录即可，如果证书放在其他目录，需要修改ssl_certificate、ssl_certificate_key指定的位置。</p><p>这样就可以不用修改以前的ws服务来提供wss服务。</p><h1 id="修改ws的请求方式为wss"><a href="#修改ws的请求方式为wss" class="headerlink" title="修改ws的请求方式为wss"></a>修改ws的请求方式为wss</h1><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">wss://your.domain.com</span><br></pre></td></tr></table></figure><p>ws服务这里也简单的说一下，有很多服务都可以构建ws服务，nginx、Workerman都可以，或者自己写程序开启ws服务。方式很多看个人喜好和公司的项目背景。</p><h1 id="附录"><a href="#附录" class="headerlink" title="附录"></a>附录</h1><ol><li><a href="http://nginx.org/en/docs/" target="_blank" rel="noopener">nginx官方文档</a></li><li><a href="https://ningyu1.github.io/site/post/51-ssl-cert/">Openssl生成自签名证书，简单步骤</a></li><li><a href="https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_client_applications" target="_blank" rel="noopener">mozilla的websocket支持说明</a></li><li><a href="https://caniuse.com/#search=websocket" target="_blank" rel="noopener">各大浏览器对websocket的支持情况</a></li></ol><h1 id="常见错误"><a href="#常见错误" class="headerlink" title="常见错误"></a>常见错误</h1><p>如果在ie下报如下错误：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">IE Network Error 12038, 证书中的主名称无效或不相符</span><br></pre></td></tr></table></figure><p>那是因为证书与请求地址不匹配导致的错误，在chrome测试它的https验证不会这么严格，在ie下https验证很严格（坑爹的ie）。</p>]]></content>
    
    <summary type="html">
    
      Trouble Shooting —— HTTPS(SSL)站点使用WebSocket(ws)出现SecurityError问题
    
    </summary>
    
      <category term="WebSocket" scheme="https://ningyu1.github.io/categories/WebSocket/"/>
    
    
      <category term="ssl" scheme="https://ningyu1.github.io/tags/ssl/"/>
    
      <category term="WebSocket" scheme="https://ningyu1.github.io/tags/WebSocket/"/>
    
      <category term="wss" scheme="https://ningyu1.github.io/tags/wss/"/>
    
      <category term="ws" scheme="https://ningyu1.github.io/tags/ws/"/>
    
      <category term="SecurityError" scheme="https://ningyu1.github.io/tags/SecurityError/"/>
    
  </entry>
  
  <entry>
    <title>生产环境如何快速跟踪、分析、定位问题-Java</title>
    <link href="https://ningyu1.github.io/20180123/55-java-jvm-analysis.html"/>
    <id>https://ningyu1.github.io/20180123/55-java-jvm-analysis.html</id>
    <published>2018-01-23T06:36:36.000Z</published>
    <updated>2024-04-23T01:48:15.035Z</updated>
    
    <content type="html"><![CDATA[<p>我相信做技术的都会遇到过这样的问题，生产环境服务遇到宕机的情况下如何去分析问题？比如说JVM内存爆掉、CPU持续高位运行、线程被夯住或线程deadlocks，面对这样的问题，如何在生产环境第一时间跟踪分析与定位问题很关键。下来让我们看看通过如下步骤在第一时间分析问题。</p><h1 id="CPU占用较高场景"><a href="#CPU占用较高场景" class="headerlink" title="CPU占用较高场景"></a>CPU占用较高场景</h1><p>收集当前CPU占用较高的线程信息，执行如下命令：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">top -H -p PID -b -d 1 -n 1 &gt; top.log</span><br><span class="line">或</span><br><span class="line">top -H -p PID</span><br></pre></td></tr></table></figure><p>结果如下：</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/jvm-analysis/1.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p>上图显示的都是某一个进程内的线程信息，找到cpu消耗最高的线程id，再配合jstack来分析耗cpu的代码位置，那如何分析呢？</p><p>先执行jstack获取线程信息</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">jstack -l PID &gt; jstackl.log</span><br></pre></td></tr></table></figure><p>将PID（29978）转成16进制：0x751a，16进制转换工具很多可以在线随便搜索一个或者基本功好的自己计算。</p><p>打开jstackl.log，查找nid=0x751a的信息，这样就定位到了具体的代码位置，这里由于是安全原因我就不贴图了。</p><p>通过上面的步骤就可以轻松的定位那个线程导致cpu过高，当然也可以通过其他方式来定位，下面介绍一个快捷的方式</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">#线程cpu占用</span><br><span class="line">#!/bin/bash</span><br><span class="line"></span><br><span class="line">[ $# -ne 1 ] &amp;&amp; exit 1</span><br><span class="line"></span><br><span class="line">jstack $1 &gt;/tmp/jstack.log</span><br><span class="line"></span><br><span class="line">for cpu_tid in `ps -mp $1 -o THREAD,tid,time|sort -k2nr| sed -n &apos;2,15p&apos; |awk &apos;&#123;print$2&quot;_&quot;$(NF-1)&#125;&apos;`;do</span><br><span class="line"></span><br><span class="line">cpu=`echo $cpu_tid | cut -d_ -f1`</span><br><span class="line"></span><br><span class="line">tid=`echo $cpu_tid | cut -d_ -f2`</span><br><span class="line"></span><br><span class="line">xtid=`printf &quot;%x\n&quot; $tid`</span><br><span class="line"></span><br><span class="line">echo -e &quot;\033[31m========================$xtid $cpu%\033[0m&quot;</span><br><span class="line"></span><br><span class="line">cat /tmp/jstack.log | sed -n -e &quot;/0x$xtid/,/^$/ p&quot;</span><br><span class="line"></span><br><span class="line">#cat /tmp/jstack.log | grep &quot;$xtid&quot; -A15</span><br><span class="line"></span><br><span class="line">done</span><br><span class="line"></span><br><span class="line">rm /tmp/jstack.log</span><br></pre></td></tr></table></figure><p>上述命令会以百分比的方式来显示每个线程的cpu消耗百分比，这里我就不贴图了，谁用谁知道。</p><h1 id="内存消耗过高场景"><a href="#内存消耗过高场景" class="headerlink" title="内存消耗过高场景"></a>内存消耗过高场景</h1><p>收集当前活跃对象数据量信息，执行以下命令获取</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">jmap -histo:live pid &gt; jmaplive.log</span><br></pre></td></tr></table></figure><p><span style="color:red"><strong>ps. jmap -histo:live 数据可以多进行几次，比如说间隔几分钟输出一次，然后对比两个文件的差异可以看出gc回收的对象，如果多次结果没有差异并且gc频繁执行，证明剩余对象在引用无法gc回收，这时就需要对服务进行限流给服务喘气的机会。</strong></span></p><p>或者收集dump信息，通常这种获取方式需要较长时间执行，并产生大容量的dump文件，我们会考虑逐步废掉通过这个文件来分析。执行以下命令获取</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">jmap -dump:file=./dump.mdump pid</span><br></pre></td></tr></table></figure><p>dump文件通过MAT工具来进行内存泄漏分析。</p><h1 id="线程、内存分析工具"><a href="#线程、内存分析工具" class="headerlink" title="线程、内存分析工具"></a>线程、内存分析工具</h1><p>上面说过通过jstack生成的线程文件是可以通过工具来直接打开可视化分析的，这里我推荐使用：tda（Thread Dump Analyzer）这个工具可以自行搜索下载。</p><p>通过jmap -dump生成的dump文件也是可以通过工具来进行可视化分析的，这里我推荐使用MAT（Memory Analysis Tools）它可以通过eclipse plugin的方式使用或者独立的下载安装包使用。</p>]]></content>
    
    <summary type="html">
    
      生产环境如何快速跟踪、分析、定位问题-Java
    
    </summary>
    
      <category term="Java" scheme="https://ningyu1.github.io/categories/Java/"/>
    
    
      <category term="Java" scheme="https://ningyu1.github.io/tags/Java/"/>
    
      <category term="JVM" scheme="https://ningyu1.github.io/tags/JVM/"/>
    
      <category term="CPU TOP" scheme="https://ningyu1.github.io/tags/CPU-TOP/"/>
    
      <category term="Jstack" scheme="https://ningyu1.github.io/tags/Jstack/"/>
    
      <category term="Jmap" scheme="https://ningyu1.github.io/tags/Jmap/"/>
    
      <category term="JVM dump" scheme="https://ningyu1.github.io/tags/JVM-dump/"/>
    
  </entry>
  
  <entry>
    <title>CAS使用经验总结，纯干货</title>
    <link href="https://ningyu1.github.io/20180119/54-cas-server.html"/>
    <id>https://ningyu1.github.io/20180119/54-cas-server.html</id>
    <published>2018-01-19T08:25:36.000Z</published>
    <updated>2024-04-23T01:48:15.035Z</updated>
    
    <content type="html"><![CDATA[<p>最近在处理公司项目对接到CAS server，在使用CAS发生了很多问题，下面整理一下遇到的问题与解决方式，希望可以帮助到需要的工程师们</p><p>CAS它是什么？它能做什么？这些我就不概述了，自行去搜索了解，<a href="https://baike.baidu.com/item/CAS/1329561" target="_blank" rel="noopener">https://baike.baidu.com/item/CAS/1329561</a></p><p>我们在使用CAS的时候基本都会遇到如下的几种问题：</p><ol><li>证书问题</li><li>Client接入配置</li><li>SLO（Single Logout）</li><li>CAS callback回调问题</li><li>Cookie问题</li><li>用户数据源以及认证问题</li><li>CAS Server Ticket持久化问题</li><li>Client Server集群模式下session问题</li></ol><p>还有一些是公司内部项目框架集成问题这里就不多说了。</p><p><strong>以下总结都是基于CAS v5.0.4版本测试</strong></p><p>我用的CAS Server是通过overlays改造后的项目，为什么需要修改原有的CAS Server呢？</p><p>我相信每个公司都有一些特殊的需求比如说：</p><ol><li>对登录页面的修改</li><li>自有的密码加密验证方式</li><li>新老项目架构参差不齐</li><li>使用公司自有用户数据源</li></ol><p>等等很多问题都需要对CAS Server进行改造</p><p>这里我将改造的CAS Server放到github上：</p><p>项目地址：<a href="https://github.com/ningyu1/cas-site" target="_blank" rel="noopener">cas-site</a></p><p><a href="https://github.com/ningyu1/cas-site/releases" target="_blank" rel="noopener"><img src="https://img.shields.io/github/release/ningyu1/cas-site.svg?style=social&amp;label=Release"></a>&nbsp;<a href="https://github.com/ningyu1/cas-site/stargazers" target="_blank" rel="noopener"><img src="https://img.shields.io/github/stars/ningyu1/cas-site.svg?style=social&amp;label=Star"></a>&nbsp;<a href="https://github.com/ningyu1/cas-site/fork" target="_blank" rel="noopener"><img src="https://img.shields.io/github/forks/ningyu1/cas-site.svg?style=social&amp;label=Fork"></a>&nbsp;<a href="https://github.com/ningyu1/cas-site/watchers" target="_blank" rel="noopener"><img src="https://img.shields.io/github/watchers/ningyu1/cas-site.svg?style=social&amp;label=Watch"></a> <a href="http://www.gnu.org/licenses/gpl-3.0.html" target="_blank" rel="noopener"><img src="https://img.shields.io/badge/license-GPLv3-blue.svg"></a></p><p>下面具体说一下上述的问题将如何来分析并解决</p><h1 id="证书问题"><a href="#证书问题" class="headerlink" title="证书问题"></a>证书问题</h1><p>如果你的服务不打算使用SSL那请跳过这段说明。</p><p>一般公司项目会有很多域名大概都是子域名的方式，例如：account.xxxx.com,login.xxxx.com，那么最好使用通配符证书，为什么呢？这样你的cas server上配置一个通配符证书即可，如果没有使用通配符证书那cas server上要配置所有授信域名的证书，这样就很麻烦，除非一些历史问题没办法才会导入多个证书，一般使用通配符证书。</p><p>我使用的是自签名的通配符证书，具体自签名证书如何生成可以查看我之前写的文章：</p><p><a href="https://ningyu1.github.io/site/post/51-ssl-cert/">《Openssl生成自签名证书，简单步骤》</a>中讲述了如何生成自签名证书。</p><p><a href="https://ningyu1.github.io/site/post/52-ssl-cert-2/">《使用自签名证书，简单步骤》</a>中讲述了如何使用自签名证书。</p><p><a href="https://ningyu1.github.io/site/post/51-ssl-cert-3/">《Java访问SSL地址，使用证书方式和免验证证书方式》</a>中讲述了Java访问ssl使用证书方式和免验证证书方式。</p><p><span style="color:red"><strong>ps.这里需要注意的是在制作单域名证书和通配符域名证书的区别是在：Common Name输入的时候，例如：</strong></span></p><p>单域名证书：<code>Common Name：account.xxxx.ccom</code><br>通配符域名证书：<code>Common Name：*.xxxx.com</code></p><p>将制作好的证书文件通过keytool导入到jdk下即可，或使用InstallCert来生成文件copy到jdk下，具体可以参考文章：<a href="https://ningyu1.github.io/site/post/52-ssl-cert-2/">《使用自签名证书，简单步骤》</a></p><p>证书放在：<code>%JAVA_HOME%\jre\lib\security</code></p><p>我们cas server使用的jdk1.8，client服务大多是jdk1.7，因此在证书处理上要注意这个细节，上面文章中有明确说明</p><p><span style="color:red"><strong>如果需要使用Docker构建，可以参考我写好的Dockerfile，在cas-site项目下Dockerfile文件</strong></span></p><h1 id="Client接入配置"><a href="#Client接入配置" class="headerlink" title="Client接入配置"></a>Client接入配置</h1><p>接入cas的client端配置非常简单，可以使用spring framework对接cas方式，也可以使用spring security对接cas方式，或者其他支持cas的第三方框架，自己对接配置非常简单只需要配置<code>SingleSignOutFilter</code>和<code>SingleSignOutHttpSessionListener</code></p><ul><li>org.jasig.cas.client.session.SingleSignOutFilter：解决Logout清空TGC和session信息</li><li>org.jasig.cas.client.session.SingleSignOutHttpSessionListener：session监听</li></ul><p>这里在对接方面就不做过多的介绍了。</p><h1 id="SLO（Single-Logout）"><a href="#SLO（Single-Logout）" class="headerlink" title="SLO（Single Logout）"></a>SLO（Single Logout）</h1><p>SLO是个什么？</p><p>通俗点讲就是：浏览器多个tab页开启不同的APP（使用同一个用户登录），在某一个APP里进行登出操作，其余APP应该一起登出</p><p>CAS Server默认是开启SLO功能，如果想要关闭这个功能可以通过设置<code>application.properties</code>文件中的参数来关闭，具体如下：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line"># 是否禁用SLO功能，true为禁用SLO功能</span><br><span class="line">cas.slo.disabled=true</span><br><span class="line"># 使用采用异步方式进行callback</span><br><span class="line">cas.slo.asynchronous=true</span><br></pre></td></tr></table></figure><p><span style="color:red"><strong>这里需要注意Logout时服务重定向需要开启：</strong></span></p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line"># Logout时服务重定向</span><br><span class="line">cas.logout.followServiceRedirects=true</span><br></pre></td></tr></table></figure><p>CAS Server在进行异步回调时会忽略所有的错误来保证所有APP都能接收到Server发出的logout请求，因此在遇到错误时不开启trace级别日志是看不到错误信息的。</p><p>如果你的client端能看到接下来的章节（CAS callback回调问题） 说到的日志信息那就证明回调是没有问题的。</p><h1 id="CAS-callback回调问题"><a href="#CAS-callback回调问题" class="headerlink" title="CAS callback回调问题"></a>CAS callback回调问题</h1><p>CAS认证过程需要server端和client端来回调用，如果发现callback回调有问题多半是第一步证书问题导致，可以开启日志trace级别查看cas的日志来排除问题。</p><p>cas回调有三种情况:</p><p>一个是授权的时候进行回调信息如下</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">2018-01-19 11:44:28.419 [http-apr-8080-exec-9] TRACE org.jasig.cas.client.session.SingleSignOutHandler - Received a token request</span><br><span class="line">2018-01-19 11:44:28.419 [http-apr-8080-exec-9] DEBUG org.jasig.cas.client.session.SingleSignOutHandler - Recording session for token ST-250-AouhaxqAjvmh5sfaP3Yz-8ec54e266608</span><br><span class="line">2018-01-19 11:44:28.419 [http-apr-8080-exec-9] DEBUG c.j.f.c.s.storage.RedisBackedSessionMappingStorage - Attempting to remove Session=[8F24552DD446F669B7A522B1A8A0C86D]</span><br><span class="line">2018-01-19 11:44:28.419 [http-apr-8080-exec-9] DEBUG c.j.f.c.s.storage.RedisBackedSessionMappingStorage - No mapping for session found.  Ignoring.</span><br><span class="line">2018-01-19 11:44:28.420 [http-apr-8080-exec-9] DEBUG o.j.c.c.validation.Cas20ServiceTicketValidator - Placing URL parameters in map.</span><br><span class="line">2018-01-19 11:44:28.420 [http-apr-8080-exec-9] DEBUG o.j.c.c.validation.Cas20ServiceTicketValidator - Calling template URL attribute map.</span><br><span class="line"></span><br><span class="line">2018-01-19 11:44:28.420 [http-apr-8080-exec-9] DEBUG o.j.c.c.validation.Cas20ServiceTicketValidator - Loading custom parameters from configuration.</span><br><span class="line">2018-01-19 11:44:28.420 [http-apr-8080-exec-9] DEBUG o.j.c.c.validation.Cas20ServiceTicketValidator - Constructing validation url: https://login.dev.xxx.com.cn/serviceValidate?ticket=ST-250-AouhaxqAjvmh5sfaP3Yz-8ec54e266608&amp;service=https%3A%2F%2Faccount.dev.xxx.com.cn%2Fcas_security_check_</span><br><span class="line">2018-01-19 11:44:28.420 [http-apr-8080-exec-9] DEBUG o.j.c.c.validation.Cas20ServiceTicketValidator - Retrieving response from server.</span><br><span class="line">2018-01-19 11:44:28.460 [http-apr-8080-exec-9] DEBUG o.j.c.c.validation.Cas20ServiceTicketValidator - Server response: &lt;cas:serviceResponse xmlns:cas=&apos;http://www.yale.edu/tp/cas&apos;&gt;</span><br><span class="line">    &lt;cas:authenticationSuccess&gt;</span><br><span class="line">        &lt;cas:user&gt;admin&lt;/cas:user&gt;</span><br><span class="line">        &lt;/cas:authenticationSuccess&gt;</span><br><span class="line">&lt;/cas:serviceResponse&gt;</span><br></pre></td></tr></table></figure><p>一个是SLO时清理session的回调信息如下</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">2018-01-19 11:44:45.484 [http-apr-8080-exec-5] TRACE org.jasig.cas.client.session.SingleSignOutHandler - Received a back channel logout request</span><br><span class="line">2018-01-19 11:44:45.484 [http-apr-8080-exec-5] DEBUG org.jasig.cas.client.util.CommonUtils - safeGetParameter called on a POST HttpServletRequest for Restricted Parameters.  Cannot complete check safely.  Reverting to standard behavior for this Parameter</span><br><span class="line">2018-01-19 11:44:45.485 [http-apr-8080-exec-5] TRACE org.jasig.cas.client.session.SingleSignOutHandler - Logout request:</span><br><span class="line">&lt;samlp:LogoutRequest xmlns:samlp=&quot;urn:oasis:names:tc:SAML:2.0:protocol&quot; ID=&quot;LR-79-M3OyvVsRH7Ft1gRVaBfeuBCAj4K1JEDnndt&quot; Version=&quot;2.0&quot; IssueInstant=&quot;2018-01-19T11:44:45Z&quot;&gt;&lt;saml:NameID xmlns:saml=&quot;urn:oasis:names:tc:SAML:2.0:assertion&quot;&gt;@NOT_USED@&lt;/saml:NameID&gt;&lt;samlp:SessionIndex&gt;ST-250-AouhaxqAjvmh5sfaP3Yz-8ec54e266608&lt;/samlp:SessionIndex&gt;&lt;/samlp:LogoutRequest&gt;</span><br><span class="line">2018-01-19 11:44:45.485 [http-apr-8080-exec-5] DEBUG c.j.f.c.s.storage.RedisBackedSessionMappingStorage - Attempting to remove Session=[8F24552DD446F669B7A522B1A8A0C86D]</span><br><span class="line">2018-01-19 11:44:45.485 [http-apr-8080-exec-5] DEBUG c.j.f.c.s.storage.RedisBackedSessionMappingStorage - Found mapping for session.  Session Removed.</span><br><span class="line">2018-01-19 11:44:45.486 [http-apr-8080-exec-5] DEBUG org.jasig.cas.client.session.SingleSignOutHandler - Invalidating session [8F24552DD446F669B7A522B1A8A0C86D] for token [ST-250-AouhaxqAjvmh5sfaP3Yz-8ec54e266608]</span><br></pre></td></tr></table></figure><p>还有一种也是SLO时清理session的回调和上面的有什么区别呢？</p><p>上面的SLO是back channel logout方式，还有一种方式：front channel logout，后者是cas新版本提供的新方式，我这里没有使用，具体可以参考官方说明：<a href="https://apereo.github.io/cas/5.0.x/installation/Logout-Single-Signout.html#turning-off-single-logout" target="_blank" rel="noopener">https://apereo.github.io/cas/5.0.x/installation/Logout-Single-Signout.html#turning-off-single-logout</a></p><p>开启trace日志查看回调是否发生错误来解决回调不生效问题</p><h1 id="Cookie问题"><a href="#Cookie问题" class="headerlink" title="Cookie问题"></a>Cookie问题</h1><p>当使用单个域名时会出现Cookie清理问题从而导致SLO失效，因为CAS Server生成TGC时如果不设置cookie domain它会写在对接的service所在的域名下，最好的方式是让Cookie写在根域名的根Path（/）下，在CAS server端配置TGC的domain以及其他cookie参数，具体参考：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">cas.tgc.path=/</span><br><span class="line">cas.tgc.maxAge=-1</span><br><span class="line">cas.tgc.domain=your.domain.com</span><br><span class="line">#cas.tgc.signingKey=</span><br><span class="line">cas.tgc.name=TGC</span><br><span class="line">#cas.tgc.encryptionKey=</span><br><span class="line">cas.tgc.secure=true</span><br><span class="line">cas.tgc.httpOnly=true</span><br><span class="line">cas.tgc.rememberMeMaxAge=1209600</span><br><span class="line">cas.tgc.cipherEnabled=true</span><br></pre></td></tr></table></figure><p>具体说明查看官方文档：<a href="https://apereo.github.io/cas/5.0.x/installation/Configuration-Properties.html#ticket-granting-cookie" target="_blank" rel="noopener">https://apereo.github.io/cas/5.0.x/installation/Configuration-Properties.html#ticket-granting-cookie</a></p><h2 id="举个例子理解一下"><a href="#举个例子理解一下" class="headerlink" title="举个例子理解一下"></a>举个例子理解一下</h2><p>我有三个APP域名分别为：<br><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">https://account.domain.com</span><br><span class="line">https://login.domain.com</span><br><span class="line">https://app.domain.com</span><br></pre></td></tr></table></figure></p><p>我生成的通配符证书域名为：<code>*.domain.com</code></p><p>我三个APP在部署时jdk下放通配符域名证书</p><p>这样修改tgc配置为：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line"># cookie写的路径 / 为根域名下</span><br><span class="line">cas.tgc.path=/</span><br><span class="line"># cookie有效期，-1 为关闭浏览器自动清空</span><br><span class="line">cas.tgc.maxAge=-1</span><br><span class="line"># cookie写在那个域名下</span><br><span class="line">cas.tgc.domain=domain.com</span><br><span class="line"># cookie的名称</span><br><span class="line">cas.tgc.name=TGC</span><br><span class="line"># cookie开启器安全模式ssl</span><br><span class="line">cas.tgc.secure=true</span><br><span class="line"># cookie禁止js调用</span><br><span class="line">cas.tgc.httpOnly=true</span><br><span class="line"># 这两个采用默认配置即可</span><br><span class="line">cas.tgc.rememberMeMaxAge=1209600</span><br><span class="line">cas.tgc.cipherEnabled=true</span><br></pre></td></tr></table></figure><h1 id="用户数据源以及认证问题"><a href="#用户数据源以及认证问题" class="headerlink" title="用户数据源以及认证问题"></a>用户数据源以及认证问题</h1><p>CAS在这方面留了很多扩展的地方，而且很方便的配置就可以支持自定义</p><p>数据源支持的方式也有很多种（jdbc、mongodb、RestStorage、GIT、等）这里就不一一介绍了<br>认证方式支持的方式也很多种（Basic、OAuth2.0|1.0、Google Authenticator、LDAP、REST、OpenID、SPNEGO、等）这里就不一一介绍了</p><p>具体可以查看官方说明对应的配置：<a href="https://apereo.github.io/cas/5.0.x/installation/Configuration-Properties.html" target="_blank" rel="noopener">https://apereo.github.io/cas/5.0.x/installation/Configuration-Properties.html</a></p><p>我使用的是jdbc方式</p><p>具体可以去github上查看cas-site源码：<a href="https://github.com/ningyu1/cas-site" title="项目地址" target="_blank" rel="noopener">cas-site</a> </p><h1 id="CAS-Server-Ticket持久化问题"><a href="#CAS-Server-Ticket持久化问题" class="headerlink" title="CAS Server Ticket持久化问题"></a>CAS Server Ticket持久化问题</h1><p>Ticket持久化方式也有很多中（JPA、Couchbase、Hazelcast、Infinispan、InMemory、Ehcache、Ignite、Memcached），默认方式（inMemory基于内存的），下面我给出JAP方式的配置参数：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">cas.ticket.registry.jpa.jpaLockingTimeout=3600</span><br><span class="line">cas.ticket.registry.jpa.healthQuery=SELECT 1</span><br><span class="line">cas.ticket.registry.jpa.isolateInternalQueries=false</span><br><span class="line">cas.ticket.registry.jpa.url=jdbc:mysql://127.0.0.1:3306/cas?useUnicode=true&amp;characterEncoding=UTF-8&amp;noAccessToProcedureBodies=true</span><br><span class="line">cas.ticket.registry.jpa.failFast=true</span><br><span class="line">cas.ticket.registry.jpa.dialect=org.hibernate.dialect.MySQL5Dialect</span><br><span class="line">cas.ticket.registry.jpa.leakThreshold=10</span><br><span class="line">cas.ticket.registry.jpa.jpaLockingTgtEnabled=false</span><br><span class="line">cas.ticket.registry.jpa.batchSize=1</span><br><span class="line">#cas.ticket.registry.jpa.defaultCatalog=</span><br><span class="line">cas.ticket.registry.jpa.defaultSchema=cas</span><br><span class="line">cas.ticket.registry.jpa.user=root</span><br><span class="line">cas.ticket.registry.jpa.ddlAuto=validate</span><br><span class="line">cas.ticket.registry.jpa.password=root@123456</span><br><span class="line">cas.ticket.registry.jpa.autocommit=true</span><br><span class="line">cas.ticket.registry.jpa.driverClass=com.mysql.jdbc.Driver</span><br><span class="line">cas.ticket.registry.jpa.idleTimeout=5000</span><br><span class="line"></span><br><span class="line"># 下面的参数根据实际情况选择使用</span><br><span class="line"># 连接池</span><br><span class="line"># cas.ticket.registry.jpa.pool.suspension=false</span><br><span class="line"># cas.ticket.registry.jpa.pool.minSize=6</span><br><span class="line"># cas.ticket.registry.jpa.pool.maxSize=18</span><br><span class="line"># cas.ticket.registry.jpa.pool.maxWait=2000</span><br><span class="line"># 签名与数据加解密密钥和算法</span><br><span class="line"># cas.ticket.registry.jpa.crypto.signing.key=</span><br><span class="line"># cas.ticket.registry.jpa.crypto.signing.keySize=512</span><br><span class="line"># cas.ticket.registry.jpa.crypto.encryption.key=</span><br><span class="line"># cas.ticket.registry.jpa.crypto.encryption.keySize=16</span><br><span class="line"># cas.ticket.registry.jpa.crypto.alg=AES</span><br></pre></td></tr></table></figure><p>这里需要注意的是，以上给出的配置参数是建议值，ddlauto默认值是create-drop，可选值有（create、create-drop、validate、update），具体含义可以查看官方文档：<a href="https://apereo.github.io/cas/5.0.x/installation/JPA-Ticket-Registry.html" target="_blank" rel="noopener">https://apereo.github.io/cas/5.0.x/installation/JPA-Ticket-Registry.html</a>，建议使用validate的方式，使用validate需要自己创建表，一共四张表下面贴出建表语句：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">CREATE TABLE `locks` (</span><br><span class="line">`application_id` varchar(255) NOT NULL,</span><br><span class="line">`expiration_date` datetime DEFAULT NULL,</span><br><span class="line">`unique_id` varchar(255) DEFAULT NULL,</span><br><span class="line">`lockVer` int(11) NOT NULL DEFAULT &apos;0&apos;,</span><br><span class="line">PRIMARY KEY (`application_id`)</span><br><span class="line">) ENGINE=InnoDB DEFAULT CHARSET=utf8</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">CREATE TABLE `oauth_tokens` (</span><br><span class="line">`TYPE` varchar(31) NOT NULL,</span><br><span class="line">`ID` varchar(255) NOT NULL,</span><br><span class="line">`NUMBER_OF_TIMES_USED` int(11) DEFAULT NULL,</span><br><span class="line">`CREATION_TIME` datetime DEFAULT NULL,</span><br><span class="line">`EXPIRATION_POLICY` longblob NOT NULL,</span><br><span class="line">`LAST_TIME_USED` datetime DEFAULT NULL,</span><br><span class="line">`PREVIOUS_LAST_TIME_USED` datetime DEFAULT NULL,</span><br><span class="line">`AUTHENTICATION` longblob NOT NULL,</span><br><span class="line">`SERVICE` longblob NOT NULL,</span><br><span class="line">PRIMARY KEY (`ID`)</span><br><span class="line">) ENGINE=InnoDB DEFAULT CHARSET=utf8</span><br><span class="line"></span><br><span class="line">CREATE TABLE `serviceticket` (</span><br><span class="line">`TYPE` varchar(31) NOT NULL,</span><br><span class="line">`ID` varchar(255) NOT NULL,</span><br><span class="line">`NUMBER_OF_TIMES_USED` int(11) DEFAULT NULL,</span><br><span class="line">`CREATION_TIME` datetime DEFAULT NULL,</span><br><span class="line">`EXPIRATION_POLICY` longblob NOT NULL,</span><br><span class="line">`LAST_TIME_USED` datetime DEFAULT NULL,</span><br><span class="line">`PREVIOUS_LAST_TIME_USED` datetime DEFAULT NULL,</span><br><span class="line">`FROM_NEW_LOGIN` bit(1) NOT NULL,</span><br><span class="line">`TICKET_ALREADY_GRANTED` bit(1) NOT NULL,</span><br><span class="line">`SERVICE` longblob NOT NULL,</span><br><span class="line">`ticketGrantingTicket_ID` varchar(255) DEFAULT NULL,</span><br><span class="line">PRIMARY KEY (`ID`),</span><br><span class="line">KEY `FK60oigifivx01ts3n8vboyqs38` (`ticketGrantingTicket_ID`),</span><br><span class="line">CONSTRAINT `FK60oigifivx01ts3n8vboyqs38` FOREIGN KEY (`ticketGrantingTicket_ID`) REFERENCES `ticketgrantingticket` (`ID`)</span><br><span class="line">) ENGINE=InnoDB DEFAULT CHARSET=utf8</span><br><span class="line"></span><br><span class="line">CREATE TABLE `ticketgrantingticket` (</span><br><span class="line">`TYPE` varchar(31) NOT NULL,</span><br><span class="line">`ID` varchar(255) NOT NULL,</span><br><span class="line">`NUMBER_OF_TIMES_USED` int(11) DEFAULT NULL,</span><br><span class="line">`CREATION_TIME` datetime DEFAULT NULL,</span><br><span class="line">`EXPIRATION_POLICY` longblob NOT NULL,</span><br><span class="line">`LAST_TIME_USED` datetime DEFAULT NULL,</span><br><span class="line">`PREVIOUS_LAST_TIME_USED` datetime DEFAULT NULL,</span><br><span class="line">`AUTHENTICATION` longblob NOT NULL,</span><br><span class="line">`EXPIRED` bit(1) NOT NULL,</span><br><span class="line">`PROXIED_BY` longblob,</span><br><span class="line">`SERVICES_GRANTED_ACCESS_TO` longblob NOT NULL,</span><br><span class="line">`ticketGrantingTicket_ID` varchar(255) DEFAULT NULL,</span><br><span class="line">PRIMARY KEY (`ID`),</span><br><span class="line">KEY `FKiqyu3qw2fxf5qaqin02mox8r4` (`ticketGrantingTicket_ID`),</span><br><span class="line">CONSTRAINT `FKiqyu3qw2fxf5qaqin02mox8r4` FOREIGN KEY (`ticketGrantingTicket_ID`) REFERENCES `ticketgrantingticket` (`ID`)</span><br><span class="line">) ENGINE=InnoDB DEFAULT CHARSET=utf8</span><br></pre></td></tr></table></figure><p>其他参数含义可以查看官方配置说明：<a href="https://apereo.github.io/cas/5.0.x/installation/JPA-Ticket-Registry.html" target="_blank" rel="noopener">https://apereo.github.io/cas/5.0.x/installation/JPA-Ticket-Registry.html</a></p><h1 id="Client-Server集群模式下session问题"><a href="#Client-Server集群模式下session问题" class="headerlink" title="Client Server集群模式下session问题"></a>Client Server集群模式下session问题</h1><p>当我们CAS Server准备好后，就要处理Client接入的问题，如果我们的Client服务是单机模式那没有任何问题，一旦放到集群环境下就会发生如下有意思的事情。</p><p>我前面说了CAS在授权回调时会做几件事，第一TG保存到Cookie，第二个保存ticketid对应的session关系以及session对象。</p><p>那么如果我们的Client服务是集群的会发生什么？</p><p>举个例子：</p><p>我的APP服务部署了2台服务（S1、S2）采用loadbalance映射一个域名出去访问，当CAS授权回调时被loadbalance路由到S1上，SingleSignOutFilter以及SingleSignOutHandler进行了TGC和SessionMappingStorage，默认的持久化方式是hash的方式，也就是说本地map方式，这样在下次访问到APP时被loadbalance路由到S2上就会发生什么有意思的事情呢？我相信做过分布式服务的应该都能猜出来什么问题。</p><p>APP：我没找到cas认证信息，跳转到cas login页面</p><p>CAS：我找到了你APP已经做过认证了，跳转到APP并且给你上次认证的ticlet</p><p>APP：我真没找到你的认证信息，跳转到cas login页面</p><p>CAS：你真的已经做过认证了，跳转到APP并且给你上次认证的ticlet</p><p>这样就会发生无线跳转死循环问题。</p><p>那如何解决上面的问题呢？</p><p>在分布式的环境下几乎服务都是集群的，甚至有很多公司会做异地多活等等。那么在集群环境下如何解决cas授权持久化的问题呢？很简单重新实现一个cas-client的SessionMappingStorage，这里可以使用很多方式，比如说：放到db、nosql的存储上（mongodb、redis）、memcache、分布式文件存储都可以。</p><p>我这里采用的是redis，而且我们dev和qa环境采用单机模式，stage和prod环境使用集群模式，因此我还做了集群和本地都兼容的方式，话不多说直接贴出实现代码</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">import java.util.HashMap;</span><br><span class="line">import java.util.Map;</span><br><span class="line">import javax.servlet.http.HttpSession;</span><br><span class="line">import org.jasig.cas.client.session.SessionMappingStorage;</span><br><span class="line">import org.slf4j.Logger;</span><br><span class="line">import org.slf4j.LoggerFactory;</span><br><span class="line">import xxxxxxxx.framework.redis.client.IRedisClient;</span><br><span class="line"></span><br><span class="line">public class RedisBackedSessionMappingStorage implements SessionMappingStorage &#123;</span><br><span class="line">    </span><br><span class="line">    private final Logger logger = LoggerFactory.getLogger(getClass());</span><br><span class="line">    </span><br><span class="line">    /**</span><br><span class="line">     * Maps the ID from the CAS server to the Session.</span><br><span class="line">     */</span><br><span class="line">    private final Map&lt;String, HttpSession&gt; MANAGED_SESSIONS = new HashMap&lt;String, HttpSession&gt;();</span><br><span class="line"></span><br><span class="line">    /**</span><br><span class="line">     * Maps the Session ID to the key from the CAS Server.</span><br><span class="line">     */</span><br><span class="line">    private final Map&lt;String, String&gt; ID_TO_SESSION_KEY_MAPPING = new HashMap&lt;String, String&gt;();</span><br><span class="line">    </span><br><span class="line">    private final static String NAME_SPACE = &quot;CAS&quot;;</span><br><span class="line">    </span><br><span class="line">    private IRedisClient redisClient;</span><br><span class="line">    </span><br><span class="line">    /**</span><br><span class="line">     * 在dev和qa环境使用单机模式：hash</span><br><span class="line">     * 在stage和prod环境使用集群模式：redis</span><br><span class="line">     */</span><br><span class="line">    private String storageMode = &quot;hash&quot;;</span><br><span class="line"></span><br><span class="line">    /**</span><br><span class="line">     * 获取 redisClient</span><br><span class="line">     * @return the redisClient</span><br><span class="line">     */</span><br><span class="line">    public IRedisClient getRedisClient() &#123;</span><br><span class="line">        return redisClient;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    /**</span><br><span class="line">     * 设置 redisClient</span><br><span class="line">     * @param redisClient the redisClient to set</span><br><span class="line">     */</span><br><span class="line">    public void setRedisClient(IRedisClient redisClient) &#123;</span><br><span class="line">        this.redisClient = redisClient;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    /**</span><br><span class="line">     * 获取 storageMode</span><br><span class="line">     * @return the storageMode</span><br><span class="line">     */</span><br><span class="line">    public String getStorageMode() &#123;</span><br><span class="line">        return storageMode;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    /**</span><br><span class="line">     * 设置 storageMode</span><br><span class="line">     * @param storageMode the storageMode to set</span><br><span class="line">     */</span><br><span class="line">    public void setStorageMode(String storageMode) &#123;</span><br><span class="line">        this.storageMode = storageMode;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    @Override</span><br><span class="line">    public HttpSession removeSessionByMappingId(String mappingId) &#123;</span><br><span class="line">        HttpSession session = null;</span><br><span class="line">        if (storageMode.equals(&quot;hash&quot;)) &#123;</span><br><span class="line">            session = MANAGED_SESSIONS.get(mappingId);</span><br><span class="line">        &#125; else &#123;</span><br><span class="line">            session = redisClient.get(mappingId, NAME_SPACE, HttpSession.class, null);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        if (session != null) &#123;</span><br><span class="line">            removeBySessionById(session.getId());</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        return session;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    @Override</span><br><span class="line">    public void removeBySessionById(String sessionId) &#123;</span><br><span class="line">        logger.debug(&quot;Attempting to remove Session=[&#123;&#125;]&quot;, sessionId);</span><br><span class="line">        String key = null;</span><br><span class="line">        if (storageMode.equals(&quot;hash&quot;)) &#123;</span><br><span class="line">            key = ID_TO_SESSION_KEY_MAPPING.get(sessionId);</span><br><span class="line">        &#125; else &#123;</span><br><span class="line">            key = redisClient.get(sessionId, NAME_SPACE, null);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        if (logger.isDebugEnabled()) &#123;</span><br><span class="line">            if (key != null) &#123;</span><br><span class="line">                logger.debug(&quot;Found mapping for session.  Session Removed.&quot;);</span><br><span class="line">            &#125; else &#123;</span><br><span class="line">                logger.debug(&quot;No mapping for session found.  Ignoring.&quot;);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        </span><br><span class="line">        if (storageMode.equals(&quot;hash&quot;)) &#123;</span><br><span class="line">            MANAGED_SESSIONS.remove(key);</span><br><span class="line">            ID_TO_SESSION_KEY_MAPPING.remove(sessionId);</span><br><span class="line">        &#125; else &#123;</span><br><span class="line">            redisClient.del(key, NAME_SPACE);</span><br><span class="line">            redisClient.del(sessionId, NAME_SPACE);</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 addSessionById(String mappingId, HttpSession session) &#123;</span><br><span class="line">        if (storageMode.equals(&quot;hash&quot;)) &#123;</span><br><span class="line">            ID_TO_SESSION_KEY_MAPPING.put(session.getId(), mappingId);</span><br><span class="line">            MANAGED_SESSIONS.put(mappingId, session);</span><br><span class="line">        &#125; else &#123;</span><br><span class="line">            redisClient.set(session.getId(), NAME_SPACE, mappingId, -1);</span><br><span class="line">            redisClient.set(mappingId, NAME_SPACE, session, -1);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里使用的redis-client是我自己封装，使用文档在：<a href="https://ningyu1.github.io/site/post/22-redis-client/">《RedisClient使用说明》</a>，支持redis集群模式：<a href="https://ningyu1.github.io/site/post/28-redis-client-sentinel/">《RedisClient升级支持Sentinel使用说明》</a>，代码已经放到了github上：</p><p>项目地址：<a href="https://github.com/ningyu1/redis-client" target="_blank" rel="noopener">redis-client</a></p><p><a href="https://github.com/ningyu1/redis-client/releases" target="_blank" rel="noopener"><img src="https://img.shields.io/github/release/ningyu1/redis-client.svg?style=social&amp;label=Release"></a>&nbsp;<a href="https://github.com/ningyu1/redis-client/stargazers" target="_blank" rel="noopener"><img src="https://img.shields.io/github/stars/ningyu1/redis-client.svg?style=social&amp;label=Star"></a>&nbsp;<a href="https://github.com/ningyu1/redis-client/fork" target="_blank" rel="noopener"><img src="https://img.shields.io/github/forks/ningyu1/redis-client.svg?style=social&amp;label=Fork"></a>&nbsp;<a href="https://github.com/ningyu1/redis-client/watchers" target="_blank" rel="noopener"><img src="https://img.shields.io/github/watchers/ningyu1/redis-client.svg?style=social&amp;label=Watch"></a> <a href="http://www.gnu.org/licenses/gpl-3.0.html" target="_blank" rel="noopener"><img src="https://img.shields.io/badge/license-GPLv3-blue.svg"></a></p><p>把上面的<code>RedisBackedSessionMappingStorage</code>类注入到<code>org.jasig.cas.client.session.SingleSignOutFilter</code>中即可</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">&lt;bean id=&quot;singleLogoutFilter&quot; class=&quot;org.jasig.cas.client.session.SingleSignOutFilter&quot;&gt;</span><br><span class="line">&lt;property name=&quot;sessionMappingStorage&quot; ref=&quot;redisBackedSessionMappingStorage&quot;&gt;&lt;/property&gt;</span><br><span class="line">&lt;/bean&gt;</span><br><span class="line">&lt;bean id=&quot;redisBackedSessionMappingStorage&quot; class=&quot;xxxxxxx.cas.session.storage.RedisBackedSessionMappingStorage&quot;&gt;</span><br><span class="line">&lt;property name=&quot;redisClient&quot; ref=&quot;redisClient&quot;&gt;&lt;/property&gt;</span><br><span class="line">&lt;property name=&quot;storageMode&quot; value=&quot;$&#123;cas.session.storage.mode&#125;&quot;&gt;&lt;/property&gt;</span><br><span class="line">&lt;/bean&gt;</span><br></pre></td></tr></table></figure><p><span style="color:red"><strong>ps.参数cas.session.storage.mode，值：hash（本地map）、redis（集中存储）</strong></span></p><h2 id="WEB服务端session集中存储处理"><a href="#WEB服务端session集中存储处理" class="headerlink" title="WEB服务端session集中存储处理"></a>WEB服务端session集中存储处理</h2><p>WEB服务端session集中存储处理方案也有很多种，使用tomcat可以使用TomcatRedisSessionManager来解决session集中存储问题，github地址：<a href="https://github.com/ran-jit/tomcat-cluster-redis-session-manager" target="_blank" rel="noopener">https://github.com/ran-jit/tomcat-cluster-redis-session-manager</a></p><p>如果要自己实现也很简单，我这里大致说一下思路，需要包装一个可序列话的session，说白了就是包装一下session实现序列化接口：<code>java.io.Serializable</code>接口生成一个version id，包装一个获取器，在生成session的时候序列化写入集中存储返回id，在用的使用通过id获取，id可以使用jsessionid或者自己生成一个uuid都行。这个id可以放入浏览器cookie，也可以放入url每次带入,在登录成功后将session序列化存储到redis或其他cache、nosql、db等，在登出时清空即可，就看自己喜好来实现了。</p><p>到这里基本上对cas的使用经验就总结完了，我相信大家在使用cas时都会遇到上面的问题，希望这篇总结可以帮助到需要的人，感谢看到最后。</p><p>最后我的愿望是：世界和平，快乐编程每一天，keep real</p>]]></content>
    
    <summary type="html">
    
      CAS使用经验总结，纯干货
    
    </summary>
    
      <category term="CAS" scheme="https://ningyu1.github.io/categories/CAS/"/>
    
    
      <category term="CAS" scheme="https://ningyu1.github.io/tags/CAS/"/>
    
      <category term="SLO" scheme="https://ningyu1.github.io/tags/SLO/"/>
    
      <category term="Single Logout" scheme="https://ningyu1.github.io/tags/Single-Logout/"/>
    
      <category term="Ticket" scheme="https://ningyu1.github.io/tags/Ticket/"/>
    
      <category term="Ticket Registry" scheme="https://ningyu1.github.io/tags/Ticket-Registry/"/>
    
      <category term="Session Centralized Storage" scheme="https://ningyu1.github.io/tags/Session-Centralized-Storage/"/>
    
      <category term="Cookie" scheme="https://ningyu1.github.io/tags/Cookie/"/>
    
      <category term="CAS Cluster" scheme="https://ningyu1.github.io/tags/CAS-Cluster/"/>
    
      <category term="CAS Server" scheme="https://ningyu1.github.io/tags/CAS-Server/"/>
    
      <category term="SSL" scheme="https://ningyu1.github.io/tags/SSL/"/>
    
      <category term="Cert" scheme="https://ningyu1.github.io/tags/Cert/"/>
    
  </entry>
  
  <entry>
    <title>Java访问SSL地址，使用证书方式和免验证证书方式</title>
    <link href="https://ningyu1.github.io/20180115/53-ssl-cert-3.html"/>
    <id>https://ningyu1.github.io/20180115/53-ssl-cert-3.html</id>
    <published>2018-01-15T06:08:36.000Z</published>
    <updated>2024-04-23T01:48:15.035Z</updated>
    
    <content type="html"><![CDATA[<h1 id="前文回顾"><a href="#前文回顾" class="headerlink" title="前文回顾"></a>前文回顾</h1><p><a href="https://ningyu1.github.io/site/post/51-ssl-cert/">《Openssl生成自签名证书，简单步骤》</a>中讲述了如何生成自签名证书。</p><p><a href="https://ningyu1.github.io/site/post/52-ssl-cert-2/">《使用自签名证书，简单步骤》</a>中讲述了如何使用自签名证书。</p><p>下面讲述在Java中如何访问SSL地址，使用证书访问和免验证证书访问。</p><h1 id="Java安装证书访问SSL地址"><a href="#Java安装证书访问SSL地址" class="headerlink" title="Java安装证书访问SSL地址"></a>Java安装证书访问SSL地址</h1><h2 id="使用InstallCert安装证书"><a href="#使用InstallCert安装证书" class="headerlink" title="使用InstallCert安装证书"></a>使用InstallCert安装证书</h2><p><a href="https://ningyu1.github.io/site/post/52-ssl-cert-2/">《使用自签名证书，简单步骤》</a>这篇文章中介绍的InstallCert生成jssecacerts文件。<br>将ssecacerts文件放入%JAVA_HOME%\jre\lib\security 下即可。</p><h2 id="使用keytool工具导入证书"><a href="#使用keytool工具导入证书" class="headerlink" title="使用keytool工具导入证书"></a>使用keytool工具导入证书</h2><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">keytool -import -alias xstore -keystore &quot;cacerts_path&quot; -file a.cer</span><br></pre></td></tr></table></figure><ul><li><code>cacerts_path</code>: 你的cacerts文件路径，一般在%JAVA_HOME%jre\lib\security\cacerts</li><li><code>a.cer</code>: 你需要导入的cer文件路径，可以是InstallCert生成的文件</li><li>密码使用jdk默认密码：<code>changeit</code>，或者在上面命令后增加<code>-storepass changeit</code>设置密码参数</li></ul><p>通过上面两种方式可以将证书安装到jdk下，接下来就是java中如何访问ssl地址，不多说直接上代码。</p><h2 id="自定义javax-net-ssl-X509TrustManager实现类"><a href="#自定义javax-net-ssl-X509TrustManager实现类" class="headerlink" title="自定义javax.net.ssl.X509TrustManager实现类"></a>自定义javax.net.ssl.X509TrustManager实现类</h2><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">import java.security.cert.CertificateException;</span><br><span class="line">import java.security.cert.X509Certificate;</span><br><span class="line">import javax.net.ssl.X509TrustManager;</span><br><span class="line"></span><br><span class="line">public class MyX509TrustManager implements X509TrustManager &#123;</span><br><span class="line"></span><br><span class="line">    @Override</span><br><span class="line">    public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException &#123;</span><br><span class="line"></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    @Override</span><br><span class="line">    public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException &#123;</span><br><span class="line"></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    @Override</span><br><span class="line">    public X509Certificate[] getAcceptedIssuers() &#123;</span><br><span class="line">        return null;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="包装HttpsDemo类"><a href="#包装HttpsDemo类" class="headerlink" title="包装HttpsDemo类"></a>包装HttpsDemo类</h2><p><code>HttpsDemo</code>类中包装两个方法，<code>sendHttps</code>发起ssl地址请求，<code>sendHttp</code>发起普通地址请求</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">import java.io.BufferedReader;</span><br><span class="line">import java.io.ByteArrayOutputStream;</span><br><span class="line">import java.io.InputStream;</span><br><span class="line">import java.io.InputStreamReader;</span><br><span class="line">import java.io.OutputStream;</span><br><span class="line">import java.net.HttpURLConnection;</span><br><span class="line">import java.net.URL;</span><br><span class="line">import javax.net.ssl.HttpsURLConnection;</span><br><span class="line">import javax.net.ssl.SSLContext;</span><br><span class="line">import javax.net.ssl.SSLSocketFactory;</span><br><span class="line">import javax.net.ssl.TrustManager;</span><br><span class="line">import org.apache.commons.io.IOUtils;</span><br><span class="line">import org.slf4j.Logger;</span><br><span class="line">import org.slf4j.LoggerFactory;</span><br><span class="line"></span><br><span class="line">public class HttpsDemo &#123;</span><br><span class="line"></span><br><span class="line">    private static final Logger logger = LoggerFactory.getLogger(HttpsDemo.class.getName());</span><br><span class="line"></span><br><span class="line">    public static void sendHttps(String path, String outputStr) &#123;</span><br><span class="line">        InputStream inputStream = null;</span><br><span class="line">        OutputStream outputStream = null;</span><br><span class="line">        HttpsURLConnection httpUrlConn = null;</span><br><span class="line">        BufferedReader bufferedReader = null;</span><br><span class="line">        InputStreamReader inputStreamReader = null;</span><br><span class="line">        StringBuffer buffer = new StringBuffer();</span><br><span class="line">        try &#123;</span><br><span class="line">            // 创建SSLContext对象，并使用我们指定的信任管理器初始化</span><br><span class="line">            TrustManager[] tm = &#123; new MyX509TrustManager() &#125;;</span><br><span class="line">            SSLContext sslContext = SSLContext.getInstance(&quot;SSL&quot;, &quot;SunJSSE&quot;);</span><br><span class="line">            sslContext.init(null, tm, new java.security.SecureRandom());</span><br><span class="line">            // 从上述SSLContext对象中得到SSLSocketFactory对象</span><br><span class="line">            SSLSocketFactory ssf = sslContext.getSocketFactory();</span><br><span class="line"></span><br><span class="line">            URL url = new URL(path);</span><br><span class="line">            httpUrlConn = (HttpsURLConnection) url.openConnection();</span><br><span class="line">            httpUrlConn.setSSLSocketFactory(ssf);</span><br><span class="line">            httpUrlConn.setDoOutput(true);</span><br><span class="line">            httpUrlConn.setDoInput(true);</span><br><span class="line">            httpUrlConn.setUseCaches(false);</span><br><span class="line">            httpUrlConn.setRequestMethod(&quot;GET&quot;);</span><br><span class="line">            httpUrlConn.connect();</span><br><span class="line"></span><br><span class="line">            // 当有数据需要提交时</span><br><span class="line">            if (null != outputStr) &#123;</span><br><span class="line">                outputStream = httpUrlConn.getOutputStream();</span><br><span class="line">                // 注意编码格式，防止中文乱码</span><br><span class="line">                outputStream.write(outputStr.getBytes(&quot;UTF-8&quot;));</span><br><span class="line">                outputStream.close();</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            // 将返回的输入流转换成字符串</span><br><span class="line">            inputStream = httpUrlConn.getInputStream();</span><br><span class="line">            inputStreamReader = new InputStreamReader(inputStream, &quot;utf-8&quot;);</span><br><span class="line">            bufferedReader = new BufferedReader(inputStreamReader);</span><br><span class="line"></span><br><span class="line">            String str = null;</span><br><span class="line">            while ((str = bufferedReader.readLine()) != null) &#123;</span><br><span class="line">                buffer.append(str);</span><br><span class="line">            &#125;</span><br><span class="line">            logger.info(&quot;地址:&#123;&#125;, success, result:&#123;&#125;&quot;, path, buffer.toString());</span><br><span class="line">        &#125; catch (Exception e) &#123;</span><br><span class="line">            logger.error(&quot;地址:&#123;&#125;, error, exception:&#123;&#125;&quot;, path, e);</span><br><span class="line">        &#125; finally &#123;</span><br><span class="line">            if (bufferedReader != null) &#123;</span><br><span class="line">                IOUtils.closeQuietly(bufferedReader);</span><br><span class="line">            &#125;</span><br><span class="line">            if (inputStreamReader != null) &#123;</span><br><span class="line">                IOUtils.closeQuietly(inputStreamReader);</span><br><span class="line">            &#125;</span><br><span class="line">            if (inputStream != null) &#123;</span><br><span class="line">                IOUtils.closeQuietly(inputStream);</span><br><span class="line">            &#125;</span><br><span class="line">            if (httpUrlConn != null) &#123;</span><br><span class="line">                httpUrlConn.disconnect();</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">    public static void sendHttp(String path) &#123;</span><br><span class="line">        InputStream inputStream = null;</span><br><span class="line">        ByteArrayOutputStream outputStream = null;</span><br><span class="line">        HttpURLConnection urlConnection = null;</span><br><span class="line">        try &#123;</span><br><span class="line">            URL url = new URL(path);</span><br><span class="line">            urlConnection = (HttpURLConnection) url.openConnection();</span><br><span class="line">            urlConnection.setRequestMethod(&quot;GET&quot;);</span><br><span class="line">            urlConnection.setUseCaches(false);</span><br><span class="line">            inputStream = urlConnection.getInputStream();</span><br><span class="line">            outputStream = new ByteArrayOutputStream();</span><br><span class="line">            byte[] buffer = new byte[1024];</span><br><span class="line">            int n = 0;</span><br><span class="line">            while (-1 != (n = inputStream.read(buffer))) &#123;</span><br><span class="line">                outputStream.write(buffer, 0, n);</span><br><span class="line">            &#125;</span><br><span class="line">            logger.info(&quot;地址:&#123;&#125;, success, result:&#123;&#125;&quot;, path, outputStream.toString());</span><br><span class="line">        &#125; catch (Exception e) &#123;</span><br><span class="line">            logger.error(&quot;地址:&#123;&#125;, error, exception:&#123;&#125;&quot;, path, e);</span><br><span class="line">        &#125; finally &#123;</span><br><span class="line">            if (outputStream != null) &#123;</span><br><span class="line">                IOUtils.closeQuietly(inputStream);</span><br><span class="line">            &#125;</span><br><span class="line">            if (outputStream != null) &#123;</span><br><span class="line">                IOUtils.closeQuietly(outputStream);</span><br><span class="line">            &#125;</span><br><span class="line">            if (urlConnection != null) &#123;</span><br><span class="line">                urlConnection.disconnect();</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">public static void main(String[] args) &#123;</span><br><span class="line">        sendHttps(&quot;https://xxx.com&quot;, null);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>上面访问ssl地址如果报错<code>java.security.cert.CertificateException: No name matching localhost found</code>那就是证书没有安装好，检查前面证书安装过程。</p><p>Java访问ssl其实是可以绕过证书验证的，可以不需要证书直接发起ssl地址请求，下面介绍一下。</p><h1 id="Java绕过证书验证访问SSL地址，达到免验证证书效果"><a href="#Java绕过证书验证访问SSL地址，达到免验证证书效果" class="headerlink" title="Java绕过证书验证访问SSL地址，达到免验证证书效果"></a>Java绕过证书验证访问SSL地址，达到免验证证书效果</h1><p>这种方式是采用重写HostnameVerifier的verify方法配合X509TrustManager来处理授信所有host，下面直接上代码</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">import java.net.HttpURLConnection;</span><br><span class="line">import java.net.URL;</span><br><span class="line">import java.security.cert.X509Certificate;</span><br><span class="line">import javax.net.ssl.HostnameVerifier;</span><br><span class="line">import javax.net.ssl.HttpsURLConnection;</span><br><span class="line">import javax.net.ssl.SSLContext;</span><br><span class="line">import javax.net.ssl.SSLSession;</span><br><span class="line">import javax.net.ssl.TrustManager;</span><br><span class="line">import javax.net.ssl.X509TrustManager;</span><br><span class="line">import org.slf4j.Logger;</span><br><span class="line">import org.slf4j.LoggerFactory;</span><br><span class="line"></span><br><span class="line">public class HttpDemo &#123;</span><br><span class="line"></span><br><span class="line">    private static final Logger logger = LoggerFactory.getLogger(HttpDemo.class.getName());</span><br><span class="line"></span><br><span class="line">    final static HostnameVerifier DO_NOT_VERIFY = new HostnameVerifier() &#123;</span><br><span class="line"></span><br><span class="line">        public boolean verify(String hostname, SSLSession session) &#123;</span><br><span class="line">            return true;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;;</span><br><span class="line"></span><br><span class="line">    public static void httpGet(String path) &#123;</span><br><span class="line">        StringBuffer tempStr = new StringBuffer();</span><br><span class="line">        String responseContent = &quot;&quot;;</span><br><span class="line">        HttpURLConnection conn = null;</span><br><span class="line">        try &#123;</span><br><span class="line">            // Create a trust manager that does not validate certificate chains</span><br><span class="line">            trustAllHosts();</span><br><span class="line">            URL url = new URL(path);</span><br><span class="line">            HttpsURLConnection https = (HttpsURLConnection) url.openConnection();</span><br><span class="line">            if (url.getProtocol().toLowerCase().equals(&quot;https&quot;)) &#123;</span><br><span class="line">                https.setHostnameVerifier(DO_NOT_VERIFY);</span><br><span class="line">                conn = https;</span><br><span class="line">            &#125; else &#123;</span><br><span class="line">                conn = (HttpURLConnection) url.openConnection();</span><br><span class="line">            &#125;</span><br><span class="line">            conn.connect();</span><br><span class="line">            logger.info(&quot;地址:&#123;&#125;, success, result:&#123;&#125;&quot;, path, conn.getResponseCode() + &quot; &quot; + conn.getResponseMessage());</span><br><span class="line">            // HttpURLConnection conn = (HttpURLConnection)</span><br><span class="line">            // url.openConnection();</span><br><span class="line"></span><br><span class="line">            // conn.setConnectTimeout(5000);</span><br><span class="line">            // conn.setReadTimeout(5000);</span><br><span class="line">            // conn.setDoOutput(true);</span><br><span class="line">            //</span><br><span class="line">            // InputStream in = conn.getInputStream();</span><br><span class="line">            // conn.setReadTimeout(10*1000);</span><br><span class="line">            // BufferedReader rd = new BufferedReader(new InputStreamReader(in,</span><br><span class="line">            // &quot;UTF-8&quot;));</span><br><span class="line">            // String tempLine;</span><br><span class="line">            // while ((tempLine = rd.readLine()) != null) &#123;</span><br><span class="line">            // tempStr.append(tempLine);</span><br><span class="line">            // &#125;</span><br><span class="line">            // responseContent = tempStr.toString();</span><br><span class="line">            // System.out.println(responseContent);</span><br><span class="line">            // rd.close();</span><br><span class="line">            // in.close();</span><br><span class="line">        &#125; catch (Exception e) &#123;</span><br><span class="line">            logger.error(&quot;地址:&#123;&#125;, is error&quot;, e);</span><br><span class="line">        &#125; finally &#123;</span><br><span class="line">            if (conn != null) &#123;</span><br><span class="line">                conn.disconnect();</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">     * Trust every server - dont check for any certificate</span><br><span class="line">     */</span><br><span class="line">    private static void trustAllHosts() &#123;</span><br><span class="line"></span><br><span class="line">        // Create a trust manager that does not validate certificate chains</span><br><span class="line">        TrustManager[] trustAllCerts = new TrustManager[] &#123; new X509TrustManager() &#123;</span><br><span class="line"></span><br><span class="line">            public java.security.cert.X509Certificate[] getAcceptedIssuers() &#123;</span><br><span class="line">                return new java.security.cert.X509Certificate[] &#123;&#125;;</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            public void checkClientTrusted(X509Certificate[] chain, String authType) &#123;</span><br><span class="line"></span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            public void checkServerTrusted(X509Certificate[] chain, String authType) &#123;</span><br><span class="line"></span><br><span class="line">            &#125;</span><br><span class="line">        &#125; &#125;;</span><br><span class="line"></span><br><span class="line">        // Install the all-trusting trust manager</span><br><span class="line">        // 忽略HTTPS请求的SSL证书，必须在openConnection之前调用</span><br><span class="line">        try &#123;</span><br><span class="line">            SSLContext sc = SSLContext.getInstance(&quot;TLS&quot;);</span><br><span class="line">            sc.init(null, trustAllCerts, new java.security.SecureRandom());</span><br><span class="line">            HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());</span><br><span class="line">        &#125; catch (Exception e) &#123;</span><br><span class="line">            logger.error(&quot;trustAllHosts is error&quot;, e);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">public static void main(String[] args) &#123;</span><br><span class="line">        httpGet(&quot;https://xxx.com&quot;);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>以上代码需要注意一点：<span style="color:red"><strong>忽略HTTPS请求的SSL证书，必须在openConnection之前调用。</strong></span></p><h1 id="常见错误"><a href="#常见错误" class="headerlink" title="常见错误"></a>常见错误</h1><h2 id="错误一"><a href="#错误一" class="headerlink" title="错误一"></a>错误一</h2><p>如果发生如下错误，请添加vm参数：<code>-Dhttps.protocols=TLSv1.1,TLSv1.2 -Djava.net.preferIPv4Stack=true</code>，一般是jdk1.7会发生这个错误，具体原因在<a href="https://ningyu1.github.io/site/post/52-ssl-cert-2/">《使用自签名证书，简单步骤》</a>这篇文章中已经解释。</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">javax.net.ssl.SSLHandshakeException: Remote host closed connection during handshake</span><br><span class="line">at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:946) ~[na:1.7.0_45]</span><br><span class="line">at sun.security.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1312) ~[na:1.7.0_45]</span><br><span class="line">at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1339) ~[na:1.7.0_45]</span><br><span class="line">at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1323) ~[na:1.7.0_45]</span><br><span class="line">at sun.net.www.protocol.https.HttpsClient.afterConnect(HttpsClient.java:563) ~[na:1.7.0_45]</span><br><span class="line">at sun.net.www.protocol.https.AbstractDelegateHttpsURLConnection.connect(AbstractDelegateHttpsURLConnection.java:185) ~[na:1.7.0_45]</span><br><span class="line">at sun.net.www.protocol.https.HttpsURLConnectionImpl.connect(HttpsURLConnectionImpl.java:153) ~[na:1.7.0_45]</span><br><span class="line">at HttpDemo.httpGet(HttpDemo.java:59) [classes/:na]</span><br><span class="line">at HttpDemo.main(HttpDemo.java:122) [classes/:na]</span><br><span class="line">Caused by: java.io.EOFException: SSL peer shut down incorrectly</span><br><span class="line">at sun.security.ssl.InputRecord.read(InputRecord.java:482) ~[na:1.7.0_45]</span><br><span class="line">at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:927) ~[na:1.7.0_45]</span><br><span class="line">... 8 common frames omitted</span><br></pre></td></tr></table></figure><h2 id="错误二"><a href="#错误二" class="headerlink" title="错误二"></a>错误二</h2><p>如果发生如下错误，是因为没有找到匹配的证书。<br>如果使用证书的方式访问，请检查证书安装是否错误。<br>如果是免验证证书访问，请检查代码没有跳过证书验证。</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">javax.net.ssl.SSLHandshakeException: java.security.cert.CertificateException: No name matching xxxxxxx.com found</span><br><span class="line">at sun.security.ssl.Alerts.getSSLException(Alerts.java:192) ~[na:1.7.0_45]</span><br><span class="line">at sun.security.ssl.SSLSocketImpl.fatal(SSLSocketImpl.java:1884) ~[na:1.7.0_45]</span><br><span class="line">at sun.security.ssl.Handshaker.fatalSE(Handshaker.java:276) ~[na:1.7.0_45]</span><br><span class="line">at sun.security.ssl.Handshaker.fatalSE(Handshaker.java:270) ~[na:1.7.0_45]</span><br><span class="line">at sun.security.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:1341) ~[na:1.7.0_45]</span><br><span class="line">at sun.security.ssl.ClientHandshaker.processMessage(ClientHandshaker.java:153) ~[na:1.7.0_45]</span><br><span class="line">at sun.security.ssl.Handshaker.processLoop(Handshaker.java:868) ~[na:1.7.0_45]</span><br><span class="line">at sun.security.ssl.Handshaker.process_record(Handshaker.java:804) ~[na:1.7.0_45]</span><br><span class="line">at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:1016) ~[na:1.7.0_45]</span><br><span class="line">at sun.security.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1312) ~[na:1.7.0_45]</span><br><span class="line">at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1339) ~[na:1.7.0_45]</span><br><span class="line">at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1323) ~[na:1.7.0_45]</span><br><span class="line">at sun.net.www.protocol.https.HttpsClient.afterConnect(HttpsClient.java:563) ~[na:1.7.0_45]</span><br><span class="line">at sun.net.www.protocol.https.AbstractDelegateHttpsURLConnection.connect(AbstractDelegateHttpsURLConnection.java:185) ~[na:1.7.0_45]</span><br><span class="line">at sun.net.www.protocol.https.HttpsURLConnectionImpl.connect(HttpsURLConnectionImpl.java:153) ~[na:1.7.0_45]</span><br><span class="line">at HttpsDemo.sendHttps(HttpsDemo.java:62) [classes/:na]</span><br><span class="line">at HttpsDemo.main(HttpsDemo.java:133) [classes/:na]</span><br><span class="line">Caused by: java.security.cert.CertificateException: No name matching xxxxxxx.com found</span><br><span class="line">at sun.security.util.HostnameChecker.matchDNS(HostnameChecker.java:208) ~[na:1.7.0_45]</span><br><span class="line">at sun.security.util.HostnameChecker.match(HostnameChecker.java:93) ~[na:1.7.0_45]</span><br><span class="line">at sun.security.ssl.X509TrustManagerImpl.checkIdentity(X509TrustManagerImpl.java:347) ~[na:1.7.0_45]</span><br><span class="line">at sun.security.ssl.AbstractTrustManagerWrapper.checkAdditionalTrust(SSLContextImpl.java:847) ~[na:1.7.0_45]</span><br><span class="line">at sun.security.ssl.AbstractTrustManagerWrapper.checkServerTrusted(SSLContextImpl.java:814) ~[na:1.7.0_45]</span><br><span class="line">at sun.security.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:1323) ~[na:1.7.0_45]</span><br><span class="line">... 12 common frames omitted</span><br></pre></td></tr></table></figure>]]></content>
    
    <summary type="html">
    
      Java访问SSL地址，使用证书方式和免验证证书方式
    
    </summary>
    
      <category term="ssl" scheme="https://ningyu1.github.io/categories/ssl/"/>
    
    
      <category term="ssl" scheme="https://ningyu1.github.io/tags/ssl/"/>
    
      <category term="openssl" scheme="https://ningyu1.github.io/tags/openssl/"/>
    
  </entry>
  
  <entry>
    <title>使用自签名证书，简单步骤</title>
    <link href="https://ningyu1.github.io/20180112/52-ssl-cert-2.html"/>
    <id>https://ningyu1.github.io/20180112/52-ssl-cert-2.html</id>
    <published>2018-01-12T11:13:36.000Z</published>
    <updated>2024-04-23T01:48:15.035Z</updated>
    
    <content type="html"><![CDATA[<p>在前文<a href="https://ningyu1.github.io/site/post/51-ssl-cert/">《Openssl生成自签名证书，简单步骤》</a>中讲述了如何生成自签名证书，接下来整理证书使用遇到的问题。</p><p>证书使用的方式也有很多中，可以使用keytool生成或导入导出证书，这里对keytool不做过多描述，可以通过–help查看使用方法。</p><p>证书文件可以放到应用服务器、负载均衡、jvm中使用，如：IIS、tomcat、nginx或者loadbalance、jdk等等。</p><p>这里介绍一个简单的工具：<code>InstallCert</code>安装证书文件到jdk下，这个在本地调试连接ssl服务器代码的时候很有用。</p><p>如果我们的服务端使用的是jdk1.8（比如说：cas服务），访问的客户端（业务系统）也是jdk1.8，那么直接使用<code>InstallCert</code>安装即可.</p><p>如果我们的服务端使用的是jdk1.8，但是客户端使用jdk1.7会遇到什么问题？</p><p>我们都知道jdk1.7默认的TLS版本是1.0但是支持1.1和1.2，如何查看jdk支持的TLS版本呢？</p><p>可以使用jdk自带的<code>jcp（java control panel）</code>工具</p><p><code>jcp（java control panel）</code>路径：<code>%JAVA_HOME%\jre\bin</code></p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/ssl-cert/2.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p>点击高级，勾选TLS1.1 TSL1.2开启支持。</p><p>如果使用客户端程序（jdk1.7开发的）访问服务端程序（jdk1.8开发的），在使用<code>InstallCert</code>安装证书时会出现如下错误：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">javax.net.ssl.SSLHandshakeException: Remote host closed connection during handshake</span><br><span class="line">    at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:946) ~[na:1.7.0_45]</span><br><span class="line">    at sun.security.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1312) ~[na:1.7.0_45]</span><br></pre></td></tr></table></figure><p>上面错误的意思就是服务器把你拒绝了！把你拒绝了！把你拒绝了！拒绝你的理由就是TLS版本不对。</p><p>下面我主要讲在客户端程序（jdk1.7开发的）访问服务端程序（jdk1.8开发的）的场景下安装证书如何解决上面的错误。</p><h1 id="通过InstallCert源码安装证书"><a href="#通过InstallCert源码安装证书" class="headerlink" title="通过InstallCert源码安装证书"></a>通过InstallCert源码安装证书</h1><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">/*</span><br><span class="line"> * Copyright 2006 Sun Microsystems, Inc.  All Rights Reserved.</span><br><span class="line"> *</span><br><span class="line"> * Redistribution and use in source and binary forms, with or without</span><br><span class="line"> * modification, are permitted provided that the following conditions</span><br><span class="line"> * are met:</span><br><span class="line"> *</span><br><span class="line"> *   - Redistributions of source code must retain the above copyright</span><br><span class="line"> *     notice, this list of conditions and the following disclaimer.</span><br><span class="line"> *</span><br><span class="line"> *   - Redistributions in binary form must reproduce the above copyright</span><br><span class="line"> *     notice, this list of conditions and the following disclaimer in the</span><br><span class="line"> *     documentation and/or other materials provided with the distribution.</span><br><span class="line"> *</span><br><span class="line"> *   - Neither the name of Sun Microsystems nor the names of its</span><br><span class="line"> *     contributors may be used to endorse or promote products derived</span><br><span class="line"> *     from this software without specific prior written permission.</span><br><span class="line"> *</span><br><span class="line"> * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS &quot;AS</span><br><span class="line"> * IS&quot; AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,</span><br><span class="line"> * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR</span><br><span class="line"> * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR</span><br><span class="line"> * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,</span><br><span class="line"> * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,</span><br><span class="line"> * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR</span><br><span class="line"> * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF</span><br><span class="line"> * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING</span><br><span class="line"> * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS</span><br><span class="line"> * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.</span><br><span class="line"> */</span><br><span class="line"></span><br><span class="line">import java.io.*;</span><br><span class="line">import java.net.URL;</span><br><span class="line"></span><br><span class="line">import java.security.*;</span><br><span class="line">import java.security.cert.*;</span><br><span class="line"></span><br><span class="line">import javax.net.ssl.*;</span><br><span class="line"></span><br><span class="line">public class InstallCert &#123;</span><br><span class="line"></span><br><span class="line">    public static void main(String[] args) throws Exception &#123;</span><br><span class="line">    String host;</span><br><span class="line">    int port;</span><br><span class="line">    char[] passphrase;</span><br><span class="line">    if ((args.length == 1) || (args.length == 2)) &#123;</span><br><span class="line">        String[] c = args[0].split(&quot;:&quot;);</span><br><span class="line">        host = c[0];</span><br><span class="line">        port = (c.length == 1) ? 443 : Integer.parseInt(c[1]);</span><br><span class="line">        String p = (args.length == 1) ? &quot;changeit&quot; : args[1];</span><br><span class="line">        passphrase = p.toCharArray();</span><br><span class="line">    &#125; else &#123;</span><br><span class="line">        System.out.println(&quot;Usage: java InstallCert &lt;host&gt;[:port] [passphrase]&quot;);</span><br><span class="line">        return;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    File file = new File(&quot;jssecacerts&quot;);</span><br><span class="line">    if (file.isFile() == false) &#123;</span><br><span class="line">        char SEP = File.separatorChar;</span><br><span class="line">        File dir = new File(System.getProperty(&quot;java.home&quot;) + SEP</span><br><span class="line">            + &quot;lib&quot; + SEP + &quot;security&quot;);</span><br><span class="line">        file = new File(dir, &quot;jssecacerts&quot;);</span><br><span class="line">        if (file.isFile() == false) &#123;</span><br><span class="line">        file = new File(dir, &quot;cacerts&quot;);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    System.out.println(&quot;Loading KeyStore &quot; + file + &quot;...&quot;);</span><br><span class="line">    InputStream in = new FileInputStream(file);</span><br><span class="line">    KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());</span><br><span class="line">    ks.load(in, passphrase);</span><br><span class="line">    in.close();</span><br><span class="line"></span><br><span class="line">    SSLContext context = SSLContext.getInstance(&quot;TLSv1.2&quot;);</span><br><span class="line">    TrustManagerFactory tmf =</span><br><span class="line">        TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());</span><br><span class="line">    tmf.init(ks);</span><br><span class="line">    X509TrustManager defaultTrustManager = (X509TrustManager)tmf.getTrustManagers()[0];</span><br><span class="line">    SavingTrustManager tm = new SavingTrustManager(defaultTrustManager);</span><br><span class="line">    context.init(null, new TrustManager[] &#123;tm&#125;, null);</span><br><span class="line">    SSLSocketFactory factory = context.getSocketFactory();</span><br><span class="line"></span><br><span class="line">    System.out.println(&quot;Opening connection to &quot; + host + &quot;:&quot; + port + &quot;...&quot;);</span><br><span class="line">    SSLSocket socket = (SSLSocket)factory.createSocket(host, port);</span><br><span class="line">    socket.setSoTimeout(10000);</span><br><span class="line">    try &#123;</span><br><span class="line">        System.out.println(&quot;Starting SSL handshake...&quot;);</span><br><span class="line">        socket.startHandshake();</span><br><span class="line">        socket.close();</span><br><span class="line">        System.out.println();</span><br><span class="line">        System.out.println(&quot;No errors, certificate is already trusted&quot;);</span><br><span class="line">    &#125; catch (SSLException e) &#123;</span><br><span class="line">        System.out.println();</span><br><span class="line">        e.printStackTrace(System.out);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    X509Certificate[] chain = tm.chain;</span><br><span class="line">    if (chain == null) &#123;</span><br><span class="line">        System.out.println(&quot;Could not obtain server certificate chain&quot;);</span><br><span class="line">        return;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    BufferedReader reader =</span><br><span class="line">        new BufferedReader(new InputStreamReader(System.in));</span><br><span class="line"></span><br><span class="line">    System.out.println();</span><br><span class="line">    System.out.println(&quot;Server sent &quot; + chain.length + &quot; certificate(s):&quot;);</span><br><span class="line">    System.out.println();</span><br><span class="line">    MessageDigest sha1 = MessageDigest.getInstance(&quot;SHA1&quot;);</span><br><span class="line">    MessageDigest md5 = MessageDigest.getInstance(&quot;MD5&quot;);</span><br><span class="line">    for (int i = 0; i &lt; chain.length; i++) &#123;</span><br><span class="line">        X509Certificate cert = chain[i];</span><br><span class="line">        System.out.println</span><br><span class="line">            (&quot; &quot; + (i + 1) + &quot; Subject &quot; + cert.getSubjectDN());</span><br><span class="line">        System.out.println(&quot;   Issuer  &quot; + cert.getIssuerDN());</span><br><span class="line">        sha1.update(cert.getEncoded());</span><br><span class="line">        System.out.println(&quot;   sha1    &quot; + toHexString(sha1.digest()));</span><br><span class="line">        md5.update(cert.getEncoded());</span><br><span class="line">        System.out.println(&quot;   md5     &quot; + toHexString(md5.digest()));</span><br><span class="line">        System.out.println();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    System.out.println(&quot;Enter certificate to add to trusted keystore or &apos;q&apos; to quit: [1]&quot;);</span><br><span class="line">    String line = reader.readLine().trim();</span><br><span class="line">    int k;</span><br><span class="line">    try &#123;</span><br><span class="line">        k = (line.length() == 0) ? 0 : Integer.parseInt(line) - 1;</span><br><span class="line">    &#125; catch (NumberFormatException e) &#123;</span><br><span class="line">        System.out.println(&quot;KeyStore not changed&quot;);</span><br><span class="line">        return;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    X509Certificate cert = chain[k];</span><br><span class="line">    String alias = host + &quot;-&quot; + (k + 1);</span><br><span class="line">    ks.setCertificateEntry(alias, cert);</span><br><span class="line"></span><br><span class="line">    OutputStream out = new FileOutputStream(&quot;jssecacerts&quot;);</span><br><span class="line">    ks.store(out, passphrase);</span><br><span class="line">    out.close();</span><br><span class="line"></span><br><span class="line">    System.out.println();</span><br><span class="line">    System.out.println(cert);</span><br><span class="line">    System.out.println();</span><br><span class="line">    System.out.println</span><br><span class="line">        (&quot;Added certificate to keystore &apos;jssecacerts&apos; using alias &apos;&quot;</span><br><span class="line">        + alias + &quot;&apos;&quot;);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    private static final char[] HEXDIGITS = &quot;0123456789abcdef&quot;.toCharArray();</span><br><span class="line"></span><br><span class="line">    private static String toHexString(byte[] bytes) &#123;</span><br><span class="line">    StringBuilder sb = new StringBuilder(bytes.length * 3);</span><br><span class="line">    for (int b : bytes) &#123;</span><br><span class="line">        b &amp;= 0xff;</span><br><span class="line">        sb.append(HEXDIGITS[b &gt;&gt; 4]);</span><br><span class="line">        sb.append(HEXDIGITS[b &amp; 15]);</span><br><span class="line">        sb.append(&apos; &apos;);</span><br><span class="line">    &#125;</span><br><span class="line">    return sb.toString();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    private static class SavingTrustManager implements X509TrustManager &#123;</span><br><span class="line"></span><br><span class="line">    private final X509TrustManager tm;</span><br><span class="line">    private X509Certificate[] chain;</span><br><span class="line"></span><br><span class="line">    SavingTrustManager(X509TrustManager tm) &#123;</span><br><span class="line">        this.tm = tm;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    public X509Certificate[] getAcceptedIssuers() &#123;</span><br><span class="line">//        throw new UnsupportedOperationException();</span><br><span class="line">        return new X509Certificate[0];</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    public void checkClientTrusted(X509Certificate[] chain, String authType)</span><br><span class="line">        throws CertificateException &#123;</span><br><span class="line">        throw new UnsupportedOperationException();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    public void checkServerTrusted(X509Certificate[] chain, String authType)</span><br><span class="line">        throws CertificateException &#123;</span><br><span class="line">        this.chain = chain;</span><br><span class="line">        tm.checkServerTrusted(chain, authType);</span><br><span class="line">    &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>上面源码我修改了<code>SSLContext context = SSLContext.getInstance(&quot;TLSv1.2&quot;);</code>，原本是TLS，这样在jdk1.7下会报错，尽管加了vm参数：<code>-Dhttps.protocols=TLSv1.1,TLSv1.2 -Djava.net.preferIPv4Stack=true</code>，依然会报错。</p><p>修改为TLSv1.2后，直接运行代码，参数为：你需要签名的域名</p><p>运行日志会出现如下错误（不用紧张，这个错误没有关系）：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">Opening connection to login.xxxxx.com.cn:443...</span><br><span class="line">Starting SSL handshake...</span><br><span class="line"></span><br><span class="line">javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target</span><br><span class="line">at sun.security.ssl.Alerts.getSSLException(Alerts.java:192)</span><br><span class="line">at sun.security.ssl.SSLSocketImpl.fatal(SSLSocketImpl.java:1884)</span><br><span class="line">at sun.security.ssl.Handshaker.fatalSE(Handshaker.java:276)</span><br><span class="line">at sun.security.ssl.Handshaker.fatalSE(Handshaker.java:270)</span><br><span class="line">at sun.security.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:1341)</span><br><span class="line">at sun.security.ssl.ClientHandshaker.processMessage(ClientHandshaker.java:153)</span><br><span class="line">at sun.security.ssl.Handshaker.processLoop(Handshaker.java:868)</span><br><span class="line">at sun.security.ssl.Handshaker.process_record(Handshaker.java:804)</span><br><span class="line">at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:1016)</span><br><span class="line">at sun.security.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1312)</span><br><span class="line">at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1339)</span><br><span class="line">at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1323)</span><br><span class="line">at InstallCert.main(InstallCert.java:99)</span><br><span class="line">Caused by: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target</span><br><span class="line">at sun.security.validator.PKIXValidator.doBuild(PKIXValidator.java:385)</span><br><span class="line">at sun.security.validator.PKIXValidator.engineValidate(PKIXValidator.java:292)</span><br><span class="line">at sun.security.validator.Validator.validate(Validator.java:260)</span><br><span class="line">at sun.security.ssl.X509TrustManagerImpl.validate(X509TrustManagerImpl.java:326)</span><br><span class="line">at sun.security.ssl.X509TrustManagerImpl.checkTrusted(X509TrustManagerImpl.java:231)</span><br><span class="line">at sun.security.ssl.X509TrustManagerImpl.checkServerTrusted(X509TrustManagerImpl.java:107)</span><br><span class="line">at InstallCert$SavingTrustManager.checkServerTrusted(InstallCert.java:195)</span><br><span class="line">at sun.security.ssl.AbstractTrustManagerWrapper.checkServerTrusted(SSLContextImpl.java:813)</span><br><span class="line">at sun.security.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:1323)</span><br><span class="line">... 8 more</span><br><span class="line">Caused by: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target</span><br><span class="line">at sun.security.provider.certpath.SunCertPathBuilder.engineBuild(SunCertPathBuilder.java:196)</span><br><span class="line">at java.security.cert.CertPathBuilder.build(CertPathBuilder.java:268)</span><br><span class="line">at sun.security.validator.PKIXValidator.doBuild(PKIXValidator.java:380)</span><br><span class="line">... 16 more</span><br><span class="line"></span><br><span class="line">Server sent 1 certificate(s):</span><br><span class="line"></span><br><span class="line"> 1 Subject EMAILADDRESS=ningyu@xxxxxx.com, CN=login.xxxxxxx.com, OU=JY, O=JY, L=Shanghai, ST=Shanghai, C=CN</span><br><span class="line">   Issuer  EMAILADDRESS=ningyu@xxxxxx.com, CN=login.xxxxxxx.com, OU=JY, O=JY, L=Shanghai, ST=Shanghai, C=CN</span><br><span class="line">   sha1    18 fe a4 26 de 9f ef 9f d0 12 f9 1b da e8 f4 6e 46 a3 ca e2 </span><br><span class="line">   md5     53 02 53 bc 1f 5d e3 0f c2 ce a5 fa 43 7b 53 83 </span><br><span class="line"></span><br><span class="line">Enter certificate to add to trusted keystore or &apos;q&apos; to quit: [1]</span><br></pre></td></tr></table></figure><p>出现上面错误没关系，在命令行输入：1，生成文件，会在执行目录下生成：jssecacerts，并且会输出下面的日志：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">Enter certificate to add to trusted keystore or &apos;q&apos; to quit: [1]</span><br><span class="line">1</span><br><span class="line"></span><br><span class="line">[</span><br><span class="line">[</span><br><span class="line">  Version: V1</span><br><span class="line">  Subject: EMAILADDRESS=ningyu@xxxxx.com, CN=login.xxxxxxx.com, OU=JY, O=JY, L=Shanghai, ST=Shanghai, C=CN</span><br><span class="line">  Signature Algorithm: SHA256withRSA, OID = 1.2.840.113549.1.1.11</span><br><span class="line"></span><br><span class="line">  Key:  Sun RSA public key, 1024 bits</span><br><span class="line">  modulus: 150111273197244637724411949927732292545940427223472330318676441758610292860528090849280500452765059055376192276098938042951946335160244351904122898746077164287399465663417510841977938344538423662939325238497292924898237072606839002269269847753256718676717424760603548961942760492908854629736493402902120207483</span><br><span class="line">  public exponent: 65537</span><br><span class="line">  Validity: [From: Fri Jan 12 15:15:03 CST 2018,</span><br><span class="line">               To: Mon Jan 10 15:15:03 CST 2028]</span><br><span class="line">  Issuer: EMAILADDRESS=ningyu@xxxxxx.com, CN=login.xxxxxx.com, OU=JY, O=JY, L=Shanghai, ST=Shanghai, C=CN</span><br><span class="line">  SerialNumber: [    b9c6224c 0cf5ee1a]</span><br><span class="line"></span><br><span class="line">]</span><br><span class="line">  Algorithm: [SHA256withRSA]</span><br><span class="line">  Signature:</span><br><span class="line">0000: B7 F8 1B FB 3C 7E 46 31   9C 56 31 47 F5 79 2C AA  ....&lt;.F1.V1G.y,.</span><br><span class="line">0010: B0 E3 FB EA CF 6C 15 72   53 8B A9 36 1D 43 E0 AB  .....l.rS..6.C..</span><br><span class="line">0020: 21 3C BD 65 51 11 B3 D6   5B 42 40 DB 07 9C 35 5C  !&lt;.eQ...[B@...5\</span><br><span class="line">0030: 84 9B B7 B8 02 5A E0 96   5D 5F 9E 5D B3 5F 85 A8  .....Z..]_.]._..</span><br><span class="line">0040: 50 64 63 E7 12 B0 DF CA   48 DD 28 B7 B2 8D 42 33  Pdc.....H.(...B3</span><br><span class="line">0050: A5 C1 E8 E1 41 08 F8 39   21 DD 6C BE 6E F1 CD EE  ....A..9!.l.n...</span><br><span class="line">0060: F9 C0 DC 2F 1E 99 D2 DC   A3 2C C7 C2 64 ED 94 5E  .../.....,..d..^</span><br><span class="line">0070: 32 6F CC B4 3D 93 B7 F8   09 8D F9 4E 39 CA 5E 53  2o..=......N9.^S</span><br><span class="line"></span><br><span class="line">]</span><br><span class="line"></span><br><span class="line">Added certificate to keystore &apos;jssecacerts&apos; using alias &apos;login.xxxxxx.com-1&apos;</span><br></pre></td></tr></table></figure><p>这个时候再运行一遍<code>InstallCert</code>就不会报错，因为已经有jssecacerts文件，直接copy jssecacerts文件到%JAVA_HOME%\jre\lib\security下，就可以愉快的玩耍了。</p><p>这个在我们本地调试连接ssl服务器的代码时很有用，如果不把证书放入jdk下你会被无限的拒绝。</p>]]></content>
    
    <summary type="html">
    
      使用自签名证书，简单步骤
    
    </summary>
    
      <category term="ssl" scheme="https://ningyu1.github.io/categories/ssl/"/>
    
    
      <category term="ssl" scheme="https://ningyu1.github.io/tags/ssl/"/>
    
      <category term="openssl" scheme="https://ningyu1.github.io/tags/openssl/"/>
    
      <category term="InstallCert" scheme="https://ningyu1.github.io/tags/InstallCert/"/>
    
  </entry>
  
  <entry>
    <title>Openssl生成自签名证书，简单步骤</title>
    <link href="https://ningyu1.github.io/20180112/51-ssl-cert.html"/>
    <id>https://ningyu1.github.io/20180112/51-ssl-cert.html</id>
    <published>2018-01-12T09:06:36.000Z</published>
    <updated>2024-04-23T01:48:15.035Z</updated>
    
    <content type="html"><![CDATA[<p>最近在调试服务时需要使用证书，因此对证书的生成和使用做了一些整理，网上关于这部分资料也很多，但是很杂乱，我整理出以下简单的步骤生成自签名证书，具体让我们来看一看吧。</p><h1 id="第一种方式"><a href="#第一种方式" class="headerlink" title="第一种方式"></a>第一种方式</h1><p>通过openssl生成私钥</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">openssl genrsa -out server.key 1024</span><br></pre></td></tr></table></figure><p>使用私钥生成自签名的cert证书文件，以下是通过参数只定证书需要的信息</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">openssl req -new -x509 -days 3650 -key server.key -out server.crt -subj &quot;/C=CN/ST=mykey/L=mykey/O=mykey/OU=mykey/CN=domain1/CN=domain2/CN=domain3&quot;</span><br></pre></td></tr></table></figure><p>如果对上面参数具体的说明不太了解的，可以使用不带参数的方式，通过命令行步骤生成，参考第二种方式。</p><h1 id="第二种方式"><a href="#第二种方式" class="headerlink" title="第二种方式"></a>第二种方式</h1><p>通过openssl生成私钥</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">openssl genrsa -out server.key 1024</span><br></pre></td></tr></table></figure><p>根据私钥生成证书申请文件csr</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">openssl req -new -key server.key -out server.csr</span><br></pre></td></tr></table></figure><p>这里根据命令行向导来进行信息输入：</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/ssl-cert/1.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p><span style="color:red"><strong>ps.Common Name可以输入：*.yourdomain.com，这种方式生成通配符域名证书</strong></span></p><p>使用私钥对证书申请进行签名从而生成证书</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">openssl x509 -req -in server.csr -out server.crt -signkey server.key -days 3650</span><br></pre></td></tr></table></figure><p>这样就生成了有效期为：10年的证书文件，对于自己内网服务使用足够。</p><h1 id="第三种方式"><a href="#第三种方式" class="headerlink" title="第三种方式"></a>第三种方式</h1><p>直接生成证书文件</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">openssl req -new -x509 -keyout server.key -out server.crt -config openssl.cnf</span><br></pre></td></tr></table></figure><p><span style="color:red"><strong>ps.以上生成得到的server.crt证书，格式都是pem的。</strong></span></p><p>我个人比较推荐使用第二种方式，如果不在乎其他参数可以使用第三种直接一步生成。</p>]]></content>
    
    <summary type="html">
    
      Openssl生成自签名证书，简单步骤
    
    </summary>
    
      <category term="ssl" scheme="https://ningyu1.github.io/categories/ssl/"/>
    
    
      <category term="ssl" scheme="https://ningyu1.github.io/tags/ssl/"/>
    
      <category term="openssl" scheme="https://ningyu1.github.io/tags/openssl/"/>
    
  </entry>
  
  <entry>
    <title>MySQL Gap Lock问题</title>
    <link href="https://ningyu1.github.io/20180111/50-mysql-gap-lock.html"/>
    <id>https://ningyu1.github.io/20180111/50-mysql-gap-lock.html</id>
    <published>2018-01-11T09:10:36.000Z</published>
    <updated>2024-04-23T01:48:15.035Z</updated>
    
    <content type="html"><![CDATA[<p>文章来源：<a href="http://blog.chinaunix.net/uid-20726500-id-5749804.html" target="_blank" rel="noopener">http://blog.chinaunix.net/uid-20726500-id-5749804.html</a><br>作者：@小桥河西</p><p>初识MySQL的gap，觉得这个设计比较独特，和其他数据库的做法不太一样，所以整理一个简单的memo（虽然关于gap锁，相关资料已经很多了）</p><h1 id="一、什么是gap"><a href="#一、什么是gap" class="headerlink" title="一、什么是gap"></a>一、什么是gap</h1><p><code>A place in an InnoDB index data structure where new values could be inserted.</code></p><p>说白了gap就是索引树中插入新记录的空隙。相应的gap lock就是加在gap上的锁，还有一个next-key锁，是记录+记录前面的gap的组合的锁。</p><h1 id="二、gap锁或next-key锁的作用"><a href="#二、gap锁或next-key锁的作用" class="headerlink" title="二、gap锁或next-key锁的作用"></a>二、gap锁或next-key锁的作用</h1><p><a href="http://dev.mysql.com/doc/refman/5.7/en/innodb-next-key-locking.html" target="_blank" rel="noopener">http://dev.mysql.com/doc/refman/5.7/en/innodb-next-key-locking.html</a></p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">To prevent phantoms, InnoDB uses an algorithm called next-key locking that combines index-row </span><br><span class="line">locking with gap locking. InnoDB performs row-level locking in such a way that when it searches</span><br><span class="line"> or scans a table index, it sets shared or exclusive locks on the index records it encounters. </span><br><span class="line">Thus, the row-level locks are actually index-record locks. In addition, a next-key lock on </span><br><span class="line">an index record also affects the “gap” before that index record. That is, a next-key lock is </span><br><span class="line">an index-record lock plus a gap lock on the gap preceding the index record. If one session has </span><br><span class="line">a shared or exclusive lock on record R in an index, another session cannot insert a new index </span><br><span class="line">record in the gap immediately before R in the index order.</span><br></pre></td></tr></table></figure><p>简单讲就是防止幻读。通过锁阻止特定条件的新记录的插入，因为插入时也要获取gap锁(Insert Intention Locks)。</p><h1 id="三、什么时候会取得gap-lock或nextkey-lock"><a href="#三、什么时候会取得gap-lock或nextkey-lock" class="headerlink" title="三、什么时候会取得gap lock或nextkey lock"></a>三、什么时候会取得gap lock或nextkey lock</h1><p>这和隔离级别有关,只在REPEATABLE READ或以上的隔离级别下的特定操作才会取得gap lock或nextkey lock。</p><p><a href="http://dev.mysql.com/doc/refman/5.7/en/innodb-transaction-isolation-levels.html" target="_blank" rel="noopener">http://dev.mysql.com/doc/refman/5.7/en/innodb-transaction-isolation-levels.html</a></p><h2 id="REPEATABLE-READ"><a href="#REPEATABLE-READ" class="headerlink" title="REPEATABLE READ"></a>REPEATABLE READ</h2><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">... For consistent reads, there is an important difference from the READ COMMITTED isolation level:</span><br><span class="line"> All consistent reads within the same transaction read the snapshot established by the first read. ...</span><br><span class="line"></span><br><span class="line">For locking reads (SELECT with FOR UPDATE or LOCK IN SHARE MODE), UPDATE, and DELETE statements, </span><br><span class="line">locking depends on whether the statement uses a unique index with a unique search condition, </span><br><span class="line">or a range-type search condition. For a unique index with a unique search condition, </span><br><span class="line">InnoDB locks only the index record found, not the gap before it. For other search conditions, </span><br><span class="line">InnoDB locks the index range scanned, using gap locks or next-key locks to block insertions </span><br><span class="line">by other sessions into the gaps covered by the range.</span><br></pre></td></tr></table></figure><p>locking reads，UPDATE和DELETE时，除了对唯一索引的唯一搜索外都会获取gap锁或next-key锁。即锁住其扫描的范围。</p><p>下面对非唯一索引做个测试。</p><p>表定义如下:</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">mysql&gt; show create table tb2;</span><br><span class="line">+-------+------------------------------------------------------------------------------------------------------------------------------------------------+</span><br><span class="line">| Table | Create Table                                                                                                                                   |</span><br><span class="line">+-------+------------------------------------------------------------------------------------------------------------------------------------------------+</span><br><span class="line">| tb2   | CREATE TABLE `tb2` (</span><br><span class="line">  `id` int(11) DEFAULT NULL,</span><br><span class="line">  `c1` int(11) DEFAULT NULL,</span><br><span class="line">  KEY `tb2_idx1` (`id`)</span><br><span class="line">) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 |</span><br><span class="line">+-------+------------------------------------------------------------------------------------------------------------------------------------------------+</span><br><span class="line">1 row in set (0.00 sec)</span><br></pre></td></tr></table></figure><p>表中有3条记录： 10,20,30。</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">mysql&gt; select * from tb2;</span><br><span class="line">+------+------+</span><br><span class="line">| id   | c1   |</span><br><span class="line">+------+------+</span><br><span class="line">|   10 |    0 |</span><br><span class="line">|   20 |    0 |</span><br><span class="line">|   30 |    0 |</span><br><span class="line">+------+------+</span><br><span class="line">3 rows in set (0.01 sec)</span><br></pre></td></tr></table></figure><p>在REPEATABLE READ下，更新一条记录不提交，然后看看能阻塞另外的会话哪些操作。</p><h2 id="SESSION-1"><a href="#SESSION-1" class="headerlink" title="SESSION 1:"></a>SESSION 1:</h2><p>SESSION 1中更新id=20的记录</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">mysql&gt; begin;</span><br><span class="line">Query OK, 0 rows affected (0.00 sec)</span><br><span class="line"></span><br><span class="line">mysql&gt; update tb2 set c1=2 where id=20;</span><br><span class="line">Query OK, 1 row affected (0.04 sec)</span><br><span class="line">Rows matched: 1  Changed: 1  Warnings: 0</span><br></pre></td></tr></table></figure><h2 id="SESSION-2"><a href="#SESSION-2" class="headerlink" title="SESSION 2:"></a>SESSION 2:</h2><p>SESSION 2中，执行插入操作，发现[10,30)范围不能插入数据。</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">mysql&gt; begin;</span><br><span class="line">Query OK, 0 rows affected (0.00 sec)</span><br><span class="line">mysql&gt; insert into tb2 values(9,4);</span><br><span class="line">Query OK, 1 row affected (0.00 sec)</span><br><span class="line"></span><br><span class="line">mysql&gt; insert into tb2 values(10,4);</span><br><span class="line">ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction</span><br><span class="line">mysql&gt; insert into tb2 values(19,4);</span><br><span class="line">ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction</span><br><span class="line">mysql&gt; insert into tb2 values(20,4);</span><br><span class="line">ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction</span><br><span class="line">mysql&gt; insert into tb2 values(21,4);</span><br><span class="line">ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction</span><br><span class="line">mysql&gt; insert into tb2 values(29,4);</span><br><span class="line">ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction</span><br><span class="line">mysql&gt; insert into tb2 values(30,4);</span><br><span class="line">Query OK, 1 row affected (0.01 sec)</span><br></pre></td></tr></table></figure><p>对于更新操作，仅20这条记录不能更新，因为更新操作不会去获取gap锁。</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">mysql&gt; begin;</span><br><span class="line">Query OK, 0 rows affected (0.00 sec)</span><br><span class="line">mysql&gt; update tb2 set c1=4 where id=10;</span><br><span class="line">Query OK, 0 rows affected (0.00 sec)</span><br><span class="line">Rows matched: 1  Changed: 0  Warnings: 0</span><br><span class="line"></span><br><span class="line">mysql&gt; update tb2 set c1=4 where id=20;</span><br><span class="line">ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction</span><br><span class="line">mysql&gt; update tb2 set c1=4 where id=30;</span><br><span class="line">Query OK, 0 rows affected (0.00 sec)</span><br><span class="line">Rows matched: 2  Changed: 0  Warnings: 0</span><br></pre></td></tr></table></figure><p>如果SESSION 1的表扫描没有用到索引，那么gap或next-key锁住的范围是整个表，即任何值都不能插入。</p><h2 id="READ-COMMITTED"><a href="#READ-COMMITTED" class="headerlink" title="READ COMMITTED"></a>READ COMMITTED</h2><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">For locking reads (SELECT with FOR UPDATE or LOCK IN SHARE MODE), UPDATE statements, </span><br><span class="line">and DELETE statements, InnoDB locks only index records, not the gaps before them,</span><br><span class="line"> and thus permits the free insertion of new records next to locked records.</span><br></pre></td></tr></table></figure><p>只会锁住已有记录，不会加gap锁。</p><h2 id="SERIALIZABLE"><a href="#SERIALIZABLE" class="headerlink" title="SERIALIZABLE"></a>SERIALIZABLE</h2><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">This level is like REPEATABLE READ, but InnoDB implicitly converts all plain </span><br><span class="line">SELECT statements to SELECT ... LOCK IN SHARE MODE if autocommit is disabled.</span><br></pre></td></tr></table></figure><p>和REPEATABLE READ的主要区别在于把普通的SELECT变成SELECT … LOCK IN SHARE MODE，即对普通的select都会获取gap锁或next-key锁。</p><h1 id="REPEATABLE-READ和幻读"><a href="#REPEATABLE-READ和幻读" class="headerlink" title="REPEATABLE READ和幻读"></a>REPEATABLE READ和幻读</h1><p>在“consistent-read”时，REPEATABLE READ下看到是事务开始时的快照，即使其它事务插入了新行通常也是看不到的，所以在常见的场合可以避免幻读。 但是，”locking read”或更新，删除时是会看到已提交的修改的，包括新插入的行。</p><p><a href="http://dev.mysql.com/doc/refman/5.7/en/innodb-consistent-read.html" target="_blank" rel="noopener">http://dev.mysql.com/doc/refman/5.7/en/innodb-consistent-read.html</a></p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">If you want to see the “freshest” state of the database, use either the READ COMMITTED </span><br><span class="line">isolation level or a locking read:</span><br></pre></td></tr></table></figure><p>下面看一个例子</p><h2 id="SESSION-1-1"><a href="#SESSION-1-1" class="headerlink" title="SESSION 1:"></a>SESSION 1:</h2><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">mysql&gt; START TRANSACTION;</span><br><span class="line">Query OK, 0 rows affected (0.00 sec)</span><br><span class="line">mysql&gt; select id,c1 from tb1 where id=1;</span><br><span class="line">+----+------+</span><br><span class="line">| id | c1   |</span><br><span class="line">+----+------+</span><br><span class="line">|  1 |  100 |</span><br><span class="line">+----+------+</span><br><span class="line">1 row in set (0.00 sec)</span><br></pre></td></tr></table></figure><h2 id="SESSION-2-1"><a href="#SESSION-2-1" class="headerlink" title="SESSION 2:"></a>SESSION 2:</h2><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">mysql&gt; update tb1 set c1=101 where id =1;</span><br><span class="line">Query OK, 1 row affected (0.03 sec)</span><br><span class="line">Rows matched: 1  Changed: 1  Warnings: 0</span><br></pre></td></tr></table></figure><h2 id="SESSION-1-2"><a href="#SESSION-1-2" class="headerlink" title="SESSION 1:"></a>SESSION 1:</h2><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">mysql&gt; select id,c1 from tb1 where id=1 LOCK IN SHARE MODE;</span><br><span class="line">+----+------+</span><br><span class="line">| id | c1   |</span><br><span class="line">+----+------+</span><br><span class="line">|  1 |  101 |</span><br><span class="line">+----+------+</span><br><span class="line">1 row in set (0.00 sec)</span><br><span class="line"></span><br><span class="line">mysql&gt; select id,c1 from tb1 where id=1;</span><br><span class="line">+----+------+</span><br><span class="line">| id | c1   |</span><br><span class="line">+----+------+</span><br><span class="line">|  1 |  100 |</span><br><span class="line">+----+------+</span><br><span class="line">1 row in set (0.00 sec)</span><br><span class="line"></span><br><span class="line">mysql&gt; update tb1 set c1=c1+1000 where id=1;</span><br><span class="line">Query OK, 1 row affected (0.02 sec)</span><br><span class="line">Rows matched: 1  Changed: 1  Warnings: 0</span><br><span class="line"></span><br><span class="line">mysql&gt; select id,c1 from tb1 where id=1;</span><br><span class="line">+----+------+</span><br><span class="line">| id | c1   |</span><br><span class="line">+----+------+</span><br><span class="line">|  1 | 1101 |</span><br><span class="line">+----+------+</span><br><span class="line">1 row in set (0.00 sec)</span><br></pre></td></tr></table></figure><p>上面update的行为违反了REPEATABLE READ的承诺，看到了事务开始后其它事务的并发更新。这对应用开发需要特别注意，这种情况下其它数据库通常都是报错的。</p><h1 id="其它"><a href="#其它" class="headerlink" title="其它"></a>其它</h1><p>RR和RC相比还有一个重要的区别，RC下，扫描过但不匹配的记录不会加锁，或者是先加锁再释放，即semi-consistent read。但RR下扫描过记录都要加锁。这个差别对有全表扫描的更新的场景影响极大。详细参考<a href="http://hedengcheng.com/?p=771，关于MySQL的加锁处理，这篇文章讲得很透彻！" target="_blank" rel="noopener">http://hedengcheng.com/?p=771，关于MySQL的加锁处理，这篇文章讲得很透彻！</a></p><h1 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h1><ul><li><a href="http://hedengcheng.com/?p=771" target="_blank" rel="noopener">http://hedengcheng.com/?p=771</a></li><li><a href="http://dev.mysql.com/doc/refman/5.7/en/innodb-consistent-read.html" target="_blank" rel="noopener">http://dev.mysql.com/doc/refman/5.7/en/innodb-consistent-read.html</a></li><li><a href="http://dev.mysql.com/doc/refman/5.7/en/innodb-transaction-isolation-levels.html" target="_blank" rel="noopener">http://dev.mysql.com/doc/refman/5.7/en/innodb-transaction-isolation-levels.html</a></li><li><a href="http://dev.mysql.com/doc/refman/5.7/en/innodb-next-key-locking.html" target="_blank" rel="noopener">http://dev.mysql.com/doc/refman/5.7/en/innodb-next-key-locking.html</a></li><li><a href="http://blog.chinaunix.net/uid-20726500-id-3902528.html" target="_blank" rel="noopener">http://blog.chinaunix.net/uid-20726500-id-3902528.html</a></li><li><a href="http://blog.itpub.net/22664653/viewspace-750824/" target="_blank" rel="noopener">http://blog.itpub.net/22664653/viewspace-750824/</a></li><li><a href="http://www.bitscn.com/pdb/mysql/201405/227973.html" target="_blank" rel="noopener">http://www.bitscn.com/pdb/mysql/201405/227973.html</a></li></ul>]]></content>
    
    <summary type="html">
    
      MySQL Gap Lock问题
    
    </summary>
    
      <category term="mysql" scheme="https://ningyu1.github.io/categories/mysql/"/>
    
    
      <category term="mysql gap lock" scheme="https://ningyu1.github.io/tags/mysql-gap-lock/"/>
    
  </entry>
  
  <entry>
    <title>推荐一个性能测试工具包（适用于单元测试）</title>
    <link href="https://ningyu1.github.io/20180111/49-java-test.html"/>
    <id>https://ningyu1.github.io/20180111/49-java-test.html</id>
    <published>2018-01-11T08:52:36.000Z</published>
    <updated>2024-04-23T01:48:15.034Z</updated>
    
    <content type="html"><![CDATA[<p>给大家推荐一个做单元测试非常好用的性能测试工具包，contiperf，很方便的进行并发压力测试</p><ul><li>pom引用</li></ul><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">&lt;!-- 单元测试 --&gt;</span><br><span class="line">&lt;dependency&gt;</span><br><span class="line">    &lt;groupId&gt;junit&lt;/groupId&gt;</span><br><span class="line">    &lt;artifactId&gt;junit&lt;/artifactId&gt;</span><br><span class="line">    &lt;version&gt;4.7&lt;/version&gt;</span><br><span class="line">    &lt;scope&gt;test&lt;/scope&gt;</span><br><span class="line">&lt;/dependency&gt;</span><br><span class="line">&lt;!-- 性能测试 --&gt;</span><br><span class="line">&lt;dependency&gt;</span><br><span class="line">    &lt;groupId&gt;org.databene&lt;/groupId&gt;</span><br><span class="line">    &lt;artifactId&gt;contiperf&lt;/artifactId&gt;</span><br><span class="line">    &lt;version&gt;2.1.0&lt;/version&gt;</span><br><span class="line">    &lt;scope&gt;test&lt;/scope&gt;</span><br><span class="line">&lt;/dependency&gt;</span><br></pre></td></tr></table></figure><ul><li>使用示例</li></ul><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">/**</span><br><span class="line"> * &lt;功能描述&gt;</span><br><span class="line"> *</span><br><span class="line"> * @author ningyu</span><br><span class="line"> * @date 2017年10月24日 下午2:40:58</span><br><span class="line"> */</span><br><span class="line">public class MyPerfTest &#123;</span><br><span class="line">     </span><br><span class="line">    private IRedisSequenceService sequenceService;</span><br><span class="line">     </span><br><span class="line">    @Rule</span><br><span class="line">    public ContiPerfRule i = new ContiPerfRule();</span><br><span class="line">    @Before</span><br><span class="line">    public void init() &#123;</span><br><span class="line">        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(&quot;test-spring-context.xml&quot;);</span><br><span class="line">        sequenceService = (IRedisSequenceService) context.getBean(&quot;redisSequenceService&quot;);</span><br><span class="line">    &#125;</span><br><span class="line">     </span><br><span class="line">    @Test</span><br><span class="line">    @PerfTest(threads=10, invocations=10000)//threads并发线程数量，invocations总调用次数,还有其他参数可以设置查看文档或者源码</span><br><span class="line">    public void test() &#123;</span><br><span class="line">        try &#123;</span><br><span class="line">            long res = sequenceService.nextSeq(&quot;TEST_NINGYU&quot;);</span><br><span class="line">        System.out.println(Thread.currentThread().getName()+&quot;:&quot;+res);</span><br><span class="line">        &#125; catch(Exception 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></pre></td></tr></table></figure>]]></content>
    
    <summary type="html">
    
      推荐一个性能测试工具包（适用于单元测试）
    
    </summary>
    
      <category term="java" scheme="https://ningyu1.github.io/categories/java/"/>
    
      <category term="test" scheme="https://ningyu1.github.io/categories/java/test/"/>
    
    
      <category term="test" scheme="https://ningyu1.github.io/tags/test/"/>
    
      <category term="java" scheme="https://ningyu1.github.io/tags/java/"/>
    
  </entry>
  
  <entry>
    <title>如何直接操作Docker容器？</title>
    <link href="https://ningyu1.github.io/20180111/48-docker.html"/>
    <id>https://ningyu1.github.io/20180111/48-docker.html</id>
    <published>2018-01-11T08:11:36.000Z</published>
    <updated>2024-04-23T01:48:15.034Z</updated>
    
    <content type="html"><![CDATA[<p>如果你想对Docker的容器进行操作，比如直接查看日志（<code>Rancher</code>无法看的时候），可以通过以下方式实现：</p><p>执行命令docker ps，找到该容器</p><p>第一种方式：</p><p>执行命令<code>docker exec -it [容器号前几位即可] /bin/bash</code>，进入容器内部（类似Linux环境），如：</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/docker/1.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p>如果/bin/bash不能执行，那就用/bin/sh。换一种shell。</p><p>进入容器后我们就可以做任何事情，建议只在容器内做只读操作，必要进行修改操作。如果不想进入容器内部操作也可以：</p><p>执行命令<code>docker exec -it [容器号前几位即可] tailf -n 100 /xxxx/xxxxx.log</code>，进入容器内部（类似Linux环境），如：</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/docker/2.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p>第二种方式：</p><p>执行命令<code>docker logs  [容器号前几位即可]</code>，查看日志</p><p><code>docker logs --tail=200 -f 容器id</code></p><p>ps:–tail=200 显示最近200行 ,all显示所有</p><p>这个可以用于不知道日志存放在哪里，如：</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/docker/3.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p>或者直接去宿主机器上查看容器日志文件，docker会在主机上面的<code>/var/lib/docker/containers/[容器id]/</code>生成每个容器的日志文件，以[容器id]-json.log命名，<span style="color:red"><strong>但是不推荐这种方式查看</strong></span>，如：</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/docker/4.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p>在<code>/var/lib/docker/containers</code>能看到很多关于容器的信息比如说hostname等。</p><p>docker还支持Log Driver可以将日志接入到日志分析工具，比如说：ELKB套件</p>]]></content>
    
    <summary type="html">
    
      如何直接操作Docker容器？
    
    </summary>
    
      <category term="docker" scheme="https://ningyu1.github.io/categories/docker/"/>
    
    
      <category term="docker" scheme="https://ningyu1.github.io/tags/docker/"/>
    
      <category term="rancher" scheme="https://ningyu1.github.io/tags/rancher/"/>
    
  </entry>
  
  <entry>
    <title>Jenkins、SVN、MAVEN打包时区问题解决方案</title>
    <link href="https://ningyu1.github.io/20180109/47-jenkins-svn-maven-timezone.html"/>
    <id>https://ningyu1.github.io/20180109/47-jenkins-svn-maven-timezone.html</id>
    <published>2018-01-09T10:30:36.000Z</published>
    <updated>2024-04-23T01:48:15.034Z</updated>
    
    <content type="html"><![CDATA[<h1 id="目录"><a href="#目录" class="headerlink" title="目录"></a>目录</h1><ol><li><a href="#jenkins">Jenkins时区设置问题</a></li><li><a href="#svn">SVN更新代码时区问题</a></li><li><a href="#maven">MAVEN打包时区问题</a></li></ol><h1 id="一、Jenkins时区设置问题"><a href="#一、Jenkins时区设置问题" class="headerlink" title="一、Jenkins时区设置问题"></a><a name="jenkins">一、Jenkins时区设置问题</a></h1><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">docker@jenkins:~$ cat /etc/default/jenkins|grep 2048</span><br><span class="line">JAVA_ARGS=&quot;-Xmx2048m -Xms2048m -XX:PermSize=512m -XX:MaxPermSize=512m  -Dorg.apache.commons.jelly.tags.fmt.timeZone=Asia/Shanghai  -Djava.awt.headless=true&quot;  # Allow graphs etc. to work even when an X server is present</span><br></pre></td></tr></table></figure><p>增加时区参数：<code>-Dorg.apache.commons.jelly.tags.fmt.timeZone=Asia/Shanghai</code></p><p>修改启动后查看jenkins系统参数：</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/jenkins-svn-maven-timezone/1.png" alt title>                </div>                <div class="image-caption"></div>            </figure><h1 id="二、SVN更新代码时区问题"><a href="#二、SVN更新代码时区问题" class="headerlink" title="二、SVN更新代码时区问题"></a><a name="svn">二、SVN更新代码时区问题</a></h1><p>svn时区依赖jenkins的时区设置</p><p>没有修改时区之前：</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/jenkins-svn-maven-timezone/2.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p>能看的出来revision时间是有问题的跟我们机器时间不一致少了8小时</p><p>修复这个问题有两个方法</p><ul><li>可以通过设置svn路径后增加@HEAD忽略掉revision来修复这个问题，具体设置如下</li></ul><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/jenkins-svn-maven-timezone/3.png" alt title>                </div>                <div class="image-caption"></div>            </figure><ul><li>修改jenkins时区，参考第一个问题<ul><li>jenkins时区设置完之后svn拉取代码会自动修改：revision，如图</li></ul></li></ul><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/jenkins-svn-maven-timezone/4.png" alt title>                </div>                <div class="image-caption"></div>            </figure><h1 id="三、MAVEN打包时区问题"><a href="#三、MAVEN打包时区问题" class="headerlink" title="三、MAVEN打包时区问题"></a><a name="maven">三、MAVEN打包时区问题</a></h1><p>我项目中使用的是maven自己的timestamp</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">&lt;timestamp&gt;$&#123;maven.build.timestamp&#125;&lt;/timestamp&gt;</span><br></pre></td></tr></table></figure><p>它的问题是：时区是UTC而且无法修改，如果要使用GMT+8，就需要插件提供支持</p><p>使用maven utc的timestamp构建出来的包名如下：</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/jenkins-svn-maven-timezone/5.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p>我使用插件：<code>build-helper-maven-plugin</code></p><p>在<code>pom</code>中增加<code>plugin</code> <code>build-helper-maven-plugin</code>来覆盖<code>maven</code>的<code>timestamp</code>变量：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">&lt;build&gt;</span><br><span class="line">    &lt;plugins&gt;</span><br><span class="line">        &lt;plugin&gt;</span><br><span class="line">            &lt;groupId&gt;org.codehaus.mojo&lt;/groupId&gt;</span><br><span class="line">            &lt;artifactId&gt;build-helper-maven-plugin&lt;/artifactId&gt;</span><br><span class="line">            &lt;version&gt;3.0.0&lt;/version&gt;</span><br><span class="line">            &lt;executions&gt;</span><br><span class="line">                &lt;execution&gt;</span><br><span class="line">                    &lt;id&gt;timestamp-property&lt;/id&gt;</span><br><span class="line">                    &lt;goals&gt;</span><br><span class="line">                        &lt;goal&gt;timestamp-property&lt;/goal&gt;</span><br><span class="line">                    &lt;/goals&gt;</span><br><span class="line">                    &lt;configuration&gt;</span><br><span class="line">                        &lt;name&gt;timestamp&lt;/name&gt;</span><br><span class="line">                        &lt;pattern&gt;yyyyMMddHHmm&lt;/pattern&gt;</span><br><span class="line">                        &lt;timeZone&gt;GMT+8&lt;/timeZone&gt;</span><br><span class="line">                    &lt;/configuration&gt;</span><br><span class="line">                &lt;/execution&gt;</span><br><span class="line">            &lt;/executions&gt;</span><br><span class="line">        &lt;/plugin&gt;</span><br><span class="line">    &lt;/plugins&gt;</span><br><span class="line">&lt;/build&gt;</span><br></pre></td></tr></table></figure><p>然后打包测试：</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/jenkins-svn-maven-timezone/6.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p>测试通过，plugin配置建议配置在parent pom中这样所有子集项目都可以继承</p>]]></content>
    
    <summary type="html">
    
      Jenkins、SVN、MAVEN打包时区问题解决方案
    
    </summary>
    
      <category term="jenkins" scheme="https://ningyu1.github.io/categories/jenkins/"/>
    
      <category term="svn" scheme="https://ningyu1.github.io/categories/jenkins/svn/"/>
    
      <category term="maven" scheme="https://ningyu1.github.io/categories/jenkins/svn/maven/"/>
    
    
      <category term="jenkins" scheme="https://ningyu1.github.io/tags/jenkins/"/>
    
      <category term="svn" scheme="https://ningyu1.github.io/tags/svn/"/>
    
      <category term="maven" scheme="https://ningyu1.github.io/tags/maven/"/>
    
  </entry>
  
  <entry>
    <title>Docker Registry镜像清理问题</title>
    <link href="https://ningyu1.github.io/20171229/46-docker-registry.html"/>
    <id>https://ningyu1.github.io/20171229/46-docker-registry.html</id>
    <published>2017-12-29T06:45:36.000Z</published>
    <updated>2024-04-23T01:48:15.034Z</updated>
    
    <content type="html"><![CDATA[<h1 id="目录"><a href="#目录" class="headerlink" title="目录"></a>目录</h1><ol><li><a href="#config">修改Docker Registry配置</a></li><li><a href="#restful">使用Registry V2 RestfulAPI 删除镜像</a></li><li><a href="#gc">Docker Registry GC回收空间</a></li><li><a href="#ui">使用UI管理Docker Registry</a></li></ol><h1 id="修改Docker-Registry配置"><a href="#修改Docker-Registry配置" class="headerlink" title="修改Docker Registry配置"></a><a name="config">修改Docker Registry配置</a></h1><p>配置开启删除功能:config.yml</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">version: 0.1</span><br><span class="line">log:</span><br><span class="line">  fields:</span><br><span class="line">    service: registry</span><br><span class="line">storage:</span><br><span class="line">    delete:</span><br><span class="line">        enabled: true</span><br><span class="line">    cache:</span><br><span class="line">        blobdescriptor: inmemory</span><br><span class="line">    filesystem:</span><br><span class="line">        rootdirectory: /var/lib/registry</span><br><span class="line">http:</span><br><span class="line">    addr: :5000</span><br><span class="line">    headers:</span><br><span class="line">        X-Content-Type-Options: [nosniff]</span><br><span class="line">health:</span><br><span class="line">  storagedriver:</span><br><span class="line">    enabled: true</span><br><span class="line">    interval: 10s</span><br><span class="line">    threshold: 3</span><br></pre></td></tr></table></figure><p><span style="color:red"><strong>主要在storage下增加delete开启状态 enabled:true</strong></span></p><p>具体配置参考官方配置详情：<a href="https://github.com/docker/distribution/blob/master/docs/configuration.md" target="_blank" rel="noopener">https://github.com/docker/distribution/blob/master/docs/configuration.md</a></p><h1 id="使用Registry-V2-RestfulAPI-删除镜像"><a href="#使用Registry-V2-RestfulAPI-删除镜像" class="headerlink" title="使用Registry V2 RestfulAPI 删除镜像"></a><a name="restful">使用Registry V2 RestfulAPI 删除镜像</a></h1><p>镜像删除之前需要获取镜像的digest值</p><ul><li>获取镜像digest值</li></ul><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">curl --cacert /etc/docker/certs.d/192.168.0.34\:5000/ca.crt -H &quot;Accept:application/vnd.docker.distribution.manifest.v2+json&quot; https://192.168.0.34:5000/v2/messer/manifests/1.0</span><br></pre></td></tr></table></figure><p>注意：</p><p>我们配置了证书，所以必须要添加证书 –cacert使用crt证书</p><p>在获取镜像digest值时必须要指定Header “Accept:application/vnd.docker.distribution.manifest.v2+json” 否则无法获取</p><p>RESTful API格式：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">/v2/&lt;镜像名称&gt;/manifests/&lt;tag&gt;</span><br></pre></td></tr></table></figure><p>具体Docker registry V2 RESTful API查看：<a href="https://docs.docker.com/registry/spec/api/" target="_blank" rel="noopener">https://docs.docker.com/registry/spec/api/</a></p><ul><li>通过上面获取到的具体返回信息</li></ul><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">&#123;</span><br><span class="line">   &quot;schemaVersion&quot;: 2,</span><br><span class="line">   &quot;mediaType&quot;: &quot;application/vnd.docker.distribution.manifest.v2+json&quot;,</span><br><span class="line">   &quot;config&quot;: &#123;</span><br><span class="line">      &quot;mediaType&quot;: &quot;application/octet-stream&quot;,</span><br><span class="line">      &quot;size&quot;: 4191,</span><br><span class="line">      &quot;digest&quot;: &quot;sha256:c8043677c5d750e0904298c29825d1da8389a1ea2e2564e076ed54a023ece056&quot;</span><br><span class="line">   &#125;,</span><br><span class="line">   &quot;layers&quot;: [</span><br><span class="line">      &#123;</span><br><span class="line">         &quot;mediaType&quot;: &quot;application/vnd.docker.image.rootfs.diff.tar.gzip&quot;,</span><br><span class="line">         &quot;size&quot;: 51363125,</span><br><span class="line">         &quot;digest&quot;: &quot;sha256:75a822cd7888e394c49828b951061402d31745f596b1f502758570f2d0ee79e2&quot;</span><br><span class="line">      &#125;,</span><br><span class="line">      &#123;</span><br><span class="line">         &quot;mediaType&quot;: &quot;application/vnd.docker.image.rootfs.diff.tar.gzip&quot;,</span><br><span class="line">         &quot;size&quot;: 20179224,</span><br><span class="line">         &quot;digest&quot;: &quot;sha256:0aefb9dc4a57d3de6a9cfa2e87e4502dfa8ce3876264bb20783b1610f8e44806&quot;</span><br><span class="line">      &#125;,</span><br><span class="line">      &#123;</span><br><span class="line">         &quot;mediaType&quot;: &quot;application/vnd.docker.image.rootfs.diff.tar.gzip&quot;,</span><br><span class="line">         &quot;size&quot;: 193,</span><br><span class="line">         &quot;digest&quot;: &quot;sha256:046e44ee6057f1264d00b0c54adcff2f2c44d30a29b50dfef928776f7aa45cc8&quot;</span><br><span class="line">      &#125;,</span><br><span class="line">      &#123;</span><br><span class="line">         &quot;mediaType&quot;: &quot;application/vnd.docker.image.rootfs.diff.tar.gzip&quot;,</span><br><span class="line">         &quot;size&quot;: 596,</span><br><span class="line">         &quot;digest&quot;: &quot;sha256:614a7b3525a1442775b9d1b52413024dc750b6a9169fcae8d4ef9cf98bda7f0f&quot;</span><br><span class="line">      &#125;,</span><br><span class="line">      &#123;</span><br><span class="line">         &quot;mediaType&quot;: &quot;application/vnd.docker.image.rootfs.diff.tar.gzip&quot;,</span><br><span class="line">         &quot;size&quot;: 1083978,</span><br><span class="line">         &quot;digest&quot;: &quot;sha256:5fe57df972ae5e10f02783cb372841e6feab67a296e2abc16f9a868e4322c33d&quot;</span><br><span class="line">      &#125;</span><br><span class="line">   ]</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>我们要的就是<code>&quot;digest&quot;: &quot;sha256:c8043677c5d750e0904298c29825d1da8389a1ea2e2564e076ed54a023ece056&quot;</code>这个值</p><ul><li>通过delete接口删除镜像</li></ul><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">curl --cacert /etc/docker/certs.d/192.168.0.34\:5000/ca.crt -X DELETE https://192.168.0.34:5000/v2/messer/manifests/sha256:c8043677c5d750e0904298c29825d1da8389a1ea2e2564e076ed54a023ece056</span><br></pre></td></tr></table></figure><p>返回不是404 就是删除了</p><p>具体Docker registry V2 RESTful API查看：<a href="https://docs.docker.com/registry/spec/api/" target="_blank" rel="noopener">https://docs.docker.com/registry/spec/api/</a></p><h1 id="Docker-Registry-GC回收空间"><a href="#Docker-Registry-GC回收空间" class="headerlink" title="Docker Registry GC回收空间"></a><a name="gc">Docker Registry GC回收空间</a></h1><p>但是实际上并没有删除，只是删除了 Registry 的索引。实际文件并没有删除。</p><p>最后还需要执行镜像的垃圾回收：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">registry garbage-collect /etc/docker/registry/config.yml</span><br></pre></td></tr></table></figure><p>上面需要进入到registry容器里面去执行，/etc/docker/registry/config.yml为配置文件路径</p><p>gc完后会看到被gc的信息例如：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">root@83d6f5acc9f5:/# /bin/registry garbage-collect /etc/docker/registry/config.yml</span><br><span class="line">INFO[0013] Deleting blob: /docker/registry/v2/blobs/sha256/c0/c0c9ad6136b5e7b142c48c7167eede3d15af54c538f7f3177c50693006cca242  go.version=go1.6.2 instance.id=73c88c92-c196-413e-9cdf-413760de2a62</span><br><span class="line">INFO[0013] Deleting blob: /docker/registry/v2/blobs/sha256/0c/0c1f3512513001c7e37c0dff11064a5c76ad9098507ee74189d6a810742173d7  go.version=go1.6.2 instance.id=73c88c92-c196-413e-9cdf-413760de2a62</span><br></pre></td></tr></table></figure><p>如果没有任何输出证明没有回收到任何东西。</p><h1 id="使用UI管理Docker-Registry"><a href="#使用UI管理Docker-Registry" class="headerlink" title="使用UI管理Docker Registry"></a><a name="ui">使用UI管理Docker Registry</a></h1><p>上面是通过Docker registry V2 RESTful API的方式删除，也可以通过UI工具删除，目前Docker registry UI工具也比较多这里介绍两个， docker-registry-frontend和hyper/docker-registry-web。</p><h2 id="docker-registry-frontend"><a href="#docker-registry-frontend" class="headerlink" title="docker-registry-frontend"></a>docker-registry-frontend</h2><p>我们使用的是 docker-registry-frontend但是他的功能比较弱没有删除的操作，只能浏览，虽然他的说明里面有说明添加了删除功能但是发布的版本中并没有合并删除功能的代码：</p><p>官方hub信息：<a href="https://hub.docker.com/r/konradkleine/docker-registry-frontend/" target="_blank" rel="noopener">https://hub.docker.com/r/konradkleine/docker-registry-frontend/</a></p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/docker-registry/1.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p>这个<code>MODE_BROWSE_ONLY=false</code>这个配置是完全没有效果的，今天可以查看docker-registry-frontend的github issue：<a href="https://github.com/kwk/docker-registry-frontend/issues/106" target="_blank" rel="noopener">https://github.com/kwk/docker-registry-frontend/issues/106</a></p><h2 id="hyper-docker-registry-web"><a href="#hyper-docker-registry-web" class="headerlink" title="hyper/docker-registry-web"></a>hyper/docker-registry-web</h2><p>这个UI虽然不是很好看，但是有删除功能</p><p>官方hub信息：<a href="https://hub.docker.com/r/hyper/docker-registry-web/" target="_blank" rel="noopener">https://hub.docker.com/r/hyper/docker-registry-web/</a></p><p>创建步骤根据官方hub上面的说明信息一步一步做就ok了，但是这个东西做的不太好速度有点慢。</p><p>界面预览：</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/docker-registry/2.png" alt title>                </div>                <div class="image-caption"></div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/docker-registry/3.png" alt title>                </div>                <div class="image-caption"></div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/docker-registry/4.png" alt title>                </div>                <div class="image-caption"></div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/docker-registry/5.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p>不管是通过RESTful API还是UI删除镜像，都需要去再registry里去gc一下才能真正释放空间，如下时候gc后的效果图</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/docker-registry/6.png" alt title>                </div>                <div class="image-caption"></div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/docker-registry/7.png" alt title>                </div>                <div class="image-caption"></div>            </figure>]]></content>
    
    <summary type="html">
    
      Docker Registry镜像清理问题
    
    </summary>
    
      <category term="docker" scheme="https://ningyu1.github.io/categories/docker/"/>
    
    
      <category term="docker" scheme="https://ningyu1.github.io/tags/docker/"/>
    
      <category term="docker registry" scheme="https://ningyu1.github.io/tags/docker-registry/"/>
    
      <category term="docker registry web" scheme="https://ningyu1.github.io/tags/docker-registry-web/"/>
    
      <category term="docker registry frontend" scheme="https://ningyu1.github.io/tags/docker-registry-frontend/"/>
    
  </entry>
  
  <entry>
    <title>Spring Cloud Netflix架构浅析</title>
    <link href="https://ningyu1.github.io/20171225/45-spring-cloud-netflix.html"/>
    <id>https://ningyu1.github.io/20171225/45-spring-cloud-netflix.html</id>
    <published>2017-12-25T05:58:36.000Z</published>
    <updated>2024-04-23T01:48:15.034Z</updated>
    
    <content type="html"><![CDATA[<h1 id="点评"><a href="#点评" class="headerlink" title="点评"></a>点评</h1><p>这篇文章比较适合入门，对于spring cloud生态的成员有一个大致的了解，其实spring cloud生态将netflix的产品进行了很好的整合，netflix早几年就在服务治理这块有很深入的研究，出品了很多服务治理的工具hystrix就是很有名的一个，具体可以查看：<a href="https://github.com/netflix" target="_blank" rel="noopener">https://github.com/netflix</a>，刚好在微服务盛行的年代服务治理是必不可少的一环，现在在微服务开发套件这块常用也就是下面这两种选择：</p><ol><li>spring cloud套件，成熟上手快</li><li>自建微服务架构<ol><li>UCM，统一配置管理（百度的disconf、阿里的diamond、点评的lion，等很多开源的）。</li><li>RPC，阿里的Dubbo、点评的Pigeon，当当改的DubboX，grpc，等等很多开源的，还有很多公司自研的。</li><li>服务治理，netflix的hystrix老牌的功能强大的服务治理工具，有熔断、降级等功能，很多公司会结合监控套件开发自己的服务治理工具。</li><li>开发框架（rpc、restful这个一般公司都有自研的开发框架）</li><li>注册中心（zookeeper、redis、Consul、SmartStack、Eureka，其中一些已经是spring cloud生态的一员了）。</li><li>网关，restful的使用nginx+lua，这也是openAPI网关常用的手段</li><li>负载均衡，这个结合选用的rpc框架来选择。一般rpc框架都有负载均衡的功能。</li><li>服务治理熔断，使用hystrix（也已经是spring cloud生态的一员了）</li><li>监控，使用pinpoint、点评的cat、等其他开源的APM工具</li><li>DevOPS，持续交付一般也是自己构架的，采用jenkins打包docker镜像，使用docker生态的工具构建容器化发布平台。</li></ol></li></ol><p>下面文章转自：<a href="https://my.oschina.net/u/3747963/blog/1592777" target="_blank" rel="noopener">https://my.oschina.net/u/3747963/blog/1592777</a><br>作者：@海岸线的曙光</p><h1 id="微服务框架Spring-Boot-Spring-Cloud"><a href="#微服务框架Spring-Boot-Spring-Cloud" class="headerlink" title="微服务框架Spring Boot+Spring Cloud"></a>微服务框架Spring Boot+Spring Cloud</h1><p>Spring Cloud是基于<a href="http://www.60kb.com/tags-11.html" target="_blank" rel="noopener">Spring Boot</a>的一整套实现<a href="http://www.60kb.com/tags-20.html" target="_blank" rel="noopener">微服务</a>的框架，可以说，Spring Boot作为框架，Spring Cloud作为微服务，一起构成了一种不可忽视的、新生的框架体系。它提供了微服务开发所需的配置管理、服务发现、断路器、智能路由、微代理、控制总线、全局锁、决策竞选、<a href="http://www.60kb.com/tags-16.html" target="_blank" rel="noopener">分布式</a>会话和集群状态管理等组件，方便易用。Spring Cloud包含了非常多的子框架，其中，Spring Cloud <a href="http://www.60kb.com/tags-29.html" target="_blank" rel="noopener">Netflix</a>是其中一套框架，它主要提供的模块包括：服务发现、断路器和监控、智能路由、客户端负载均衡等。</p><h1 id="Spring-Cloud-Netflix组件以及部署"><a href="#Spring-Cloud-Netflix组件以及部署" class="headerlink" title="Spring Cloud Netflix组件以及部署"></a>Spring Cloud Netflix组件以及部署</h1><ol><li>Eureka，服务注册和发现，它提供了一个服务注册中心、服务发现的客户端，还有一个方便的查看所有注册的服务的界面。 所有的服务使用Eureka的服务发现客户端来将自己注册到Eureka的服务器上。</li><li><a href="http://www.60kb.com/tags-63.html" target="_blank" rel="noopener">Zuul</a>，网关，所有的客户端请求通过这个网关访问后台的服务。他可以使用一定的路由配置来判断某一个URL由哪个服务来处理。并从Eureka获取注册的服务来转发请求。</li><li><a href="http://www.60kb.com/tags-55.html" target="_blank" rel="noopener">Ribbon</a>，即负载均衡，Zuul网关将一个请求发送给某一个服务的应用的时候，如果一个服务启动了多个实例，就会通过Ribbon来通过一定的负载均衡策略来发送给某一个服务实例。</li><li>Feign，服务客户端，服务之间如果需要相互访问，可以使用RestTemplate，也可以使用Feign客户端访问。它默认会使用Ribbon来实现负载均衡。</li><li>Hystrix，监控和断路器。我们只需要在服务接口上添加Hystrix标签，就可以实现对这个接口的监控和断路器功能。</li><li>Hystrix Dashboard，监控面板，他提供了一个界面，可以监控各个服务上的服务调用所消耗的时间等。</li><li>Turbine，监控聚合，使用Hystrix监控，我们需要打开每一个服务实例的监控信息来查看。而Turbine可以帮助我们把所有的服务实例的监控信息聚合到一个地方统一查看。</li></ol><h1 id="Spring-Cloud-Netflix组件开发"><a href="#Spring-Cloud-Netflix组件开发" class="headerlink" title="Spring Cloud Netflix组件开发"></a>Spring Cloud Netflix组件开发</h1><p>可以参考其中文文档：<a href="https://springcloud.cc/spring-cloud-netflix.html" target="_blank" rel="noopener">https://springcloud.cc/spring-cloud-netflix.html</a></p><ul><li>服务注册与监控中心：</li></ul><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">@SpringBootApplication</span><br><span class="line">@EnableEurekaServer</span><br><span class="line">@EnableHystrixDashboard</span><br><span class="line">public class ApplicationRegistry &#123;</span><br><span class="line">    public static void main(String[] args) &#123;</span><br><span class="line">        new SpringApplicationBuilder(Application.class).web(true).run(args);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里使用spring boot标签的 @SpringBootApplication 说明当前的应用是一个spring boot应用。这样我就可以直接用main函数在IDE里面启动这个应用，也可以打包后用命令行启动。当然也可以把打包的war包用tomcat之类的服务器启动。 使用标签 @EnableEurekaServer ，就能在启动过程中启动Eureka服务注册中心的组件。它会监听一个端口，默认是8761，来接收服务注册。并提供一个web页面，打开以后，可以看到注册的服务。 添加 @EnableHystrixDashboard 就会提供一个监控的页面，我们可以在上面输入要监控的服务的地址，就可以查看启用了Hystrix监控的接口的调用情况。 当然，为了使用上面的组件，我们需要在maven的POM文件里添加相应的依赖，比如使用 spring-boot-starter-parent ，依赖 spring-cloud-starter-eureka-server 和 spring-cloud-starter-hystrix-dashboard 等。</p><ul><li>服务间调用：</li></ul><p>两种方式可以进行服务调用，RestTemplate和FeignClient。不管是什么方式，他都是通过REST接口调用服务的http接口，参数和结果默认都是通过jackson序列化和反序列化。因为Spring MVC的RestController定义的接口，返回的数据都是通过jackson序列化成json数据。</p><p>第一种：RestTemplate，只需要定义一个RestTemplate的Bean，设置成 LoadBalanced 即可：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">@Configuration</span><br><span class="line">public class SomeCloudConfiguration &#123;</span><br><span class="line">    @LoadBalanced</span><br><span class="line">    @Bean</span><br><span class="line">    RestTemplate restTemplate() &#123;</span><br><span class="line">        return new RestTemplate();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这样我们就可以在需要用的地方注入这个bean使用：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">public class SomeServiceClass &#123;</span><br><span class="line">    @Autowired</span><br><span class="line">    private RestTemplate restTemplate;</span><br><span class="line">    public String getUserById(Long userId) &#123;</span><br><span class="line">        UserDTO results = restTemplate.getForObject(&quot;http://users/getUserDetail/&quot; + userId, UserDTO.class);</span><br><span class="line">        return results;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>其中， users 是服务ID，Ribbon会从服务实例列表获得这个服务的一个实例，发送请求，并获得结果。对象 UserDTO 需要序列号，它的反序列号会自动完成。</p><p>第二种：FeignClient</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">@FeignClient(value = &quot;users&quot;, path = &quot;/users&quot;)</span><br><span class="line">public interface UserCompositeService &#123;</span><br><span class="line">    @RequestMapping(value = &quot;/getUserDetail/&#123;id&#125;&quot;, method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)</span><br><span class="line">    UserDTO getUserById(@PathVariable Long id);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>我们只需要使用 @FeignClient 定义一个借口，Spring Cloud Feign会帮我们生成一个它的实现，从相应的users服务获取数据。 其中， @FeignClient(value = “users”, path = “/users/getUserDetail”) 里面的value是服务ID，path是这一组接口的path前缀。 在下面的方法定义里，就好像设置Spring MVC的接口一样，对于这个方法，它对应的URL是 /users/getUserDetail/{id} 。 然后，在使用它的时候，就像注入一个一般的服务一样注入后使用即可：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">public class SomeOtherServiceClass &#123;</span><br><span class="line">    @Autowired</span><br><span class="line">    private UserCompositeService userService;</span><br><span class="line">    public void doSomething() &#123;</span><br><span class="line">        // .....                    </span><br><span class="line">        UserDTO results = userService.getUserById(userId);</span><br><span class="line">        // other operation...                    </span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>断路器：</li></ul><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">//断路器：为了解决当某个方法调用失败的时候，调用后备方法来替代失败的方法，已达到容错／阻止级联错误的功能</span><br><span class="line">//fallbackMethod指定后备方法</span><br><span class="line">@HystrixCommand(fallbackMethod = &quot;doStudentFallback&quot;)</span><br><span class="line">@RequestMapping(value = &quot;dostudent&quot;,method = RequestMethod.GET)</span><br><span class="line">public String doStudent()&#123;</span><br><span class="line">   return &quot;your name:secret,your age:secret!&quot;;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">public String doStudentFallback()&#123;</span><br><span class="line">   return &quot;your name:FEIFEI,your age:26!&quot;;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>其中，使用@EnableCircuitBreaker来启用断路器支持，Spring Cloud提供了一个控制台来监控断路器的运行情况，通过@EnableHystrixDashboard注解开启。</p><p>以上是简单的一些对Spring Cloud Netflix组件的介绍。</p>]]></content>
    
    <summary type="html">
    
      Spring Cloud Netflix架构浅析
    
    </summary>
    
      <category term="spring cloud" scheme="https://ningyu1.github.io/categories/spring-cloud/"/>
    
      <category term="netflix" scheme="https://ningyu1.github.io/categories/spring-cloud/netflix/"/>
    
    
      <category term="rpc" scheme="https://ningyu1.github.io/tags/rpc/"/>
    
      <category term="ucm" scheme="https://ningyu1.github.io/tags/ucm/"/>
    
      <category term="spring cloud" scheme="https://ningyu1.github.io/tags/spring-cloud/"/>
    
      <category term="netflix" scheme="https://ningyu1.github.io/tags/netflix/"/>
    
      <category term="hystrix" scheme="https://ningyu1.github.io/tags/hystrix/"/>
    
      <category term="Eureka" scheme="https://ningyu1.github.io/tags/Eureka/"/>
    
      <category term="zuul" scheme="https://ningyu1.github.io/tags/zuul/"/>
    
      <category term="ribbon" scheme="https://ningyu1.github.io/tags/ribbon/"/>
    
      <category term="devops" scheme="https://ningyu1.github.io/tags/devops/"/>
    
      <category term="monitor" scheme="https://ningyu1.github.io/tags/monitor/"/>
    
  </entry>
  
  <entry>
    <title>JDK1.8新特性详解</title>
    <link href="https://ningyu1.github.io/20171215/44-jdk1.8-feature.html"/>
    <id>https://ningyu1.github.io/20171215/44-jdk1.8-feature.html</id>
    <published>2017-12-15T10:15:36.000Z</published>
    <updated>2024-04-23T01:48:15.034Z</updated>
    
    <content type="html"><![CDATA[<p>将Java8的新特新逐一列出，并将使用简单的代码示例来指导你如何使用默认接口方法，lambda表达式，方法引用以及多重Annotation，之后你将会学到最新的API上的改进，比如流，函数式接口，Map以及全新的日期API</p><h2 id="一、接口的默认方法"><a href="#一、接口的默认方法" class="headerlink" title="一、接口的默认方法"></a>一、接口的默认方法</h2><p>Java 8允许我们给接口添加一个非抽象的方法实现，只需要使用 default关键字即可，这个特征又叫做扩展方法，示例如下：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">interface Formula &#123;</span><br><span class="line">    double calculate(int a);</span><br><span class="line">    default double sqrt(int a) &#123;</span><br><span class="line">        return Math.sqrt(a);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>Formula接口在拥有calculate方法之外同时还定义了sqrt方法，实现了Formula接口的子类只需要实现一个calculate方法，默认方法sqrt将在子类上可以直接使用。</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">Formula formula = new Formula() &#123;</span><br><span class="line">    @Override</span><br><span class="line">    public double calculate(int a) &#123;</span><br><span class="line">        return sqrt(a * 100);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;;</span><br><span class="line">formula.calculate(100);     // 100.0</span><br><span class="line">formula.sqrt(16);           // 4.0</span><br></pre></td></tr></table></figure><p>文中的formula被实现为一个匿名类的实例，该代码非常容易理解，6行代码实现了计算 sqrt(a * 100)。在下一节中，我们将会看到实现单方法接口的更简单的做法。</p><h2 id="二、Lambda-表达式"><a href="#二、Lambda-表达式" class="headerlink" title="二、Lambda 表达式"></a>二、Lambda 表达式</h2><p>首先看看在老版本的Java中是如何排列字符串的：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">List&lt;String&gt; names = Arrays.asList(&quot;peter&quot;, &quot;anna&quot;, &quot;mike&quot;, &quot;xenia&quot;);</span><br><span class="line">Collections.sort(names, new Comparator&lt;String&gt;() &#123;</span><br><span class="line">    @Override</span><br><span class="line">    public int compare(String a, String b) &#123;</span><br><span class="line">        return b.compareTo(a);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>只需要给静态方法 Collections.sort 传入一个List对象以及一个比较器来按指定顺序排列。通常做法都是创建一个匿名的比较器对象然后将其传递给sort方法。</p><p>在Java 8 中你就没必要使用这种传统的匿名对象的方式了，Java 8提供了更简洁的语法，lambda表达式：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">Collections.sort(names, (String a, String b) -&gt; &#123;</span><br><span class="line">    return b.compareTo(a);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>看到了吧，代码变得更段且更具有可读性，但是实际上还可以写得更短：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">Collections.sort(names, (String a, String b) -&gt; b.compareTo(a));</span><br></pre></td></tr></table></figure><p>对于函数体只有一行代码的，你可以去掉大括号{}以及return关键字，但是你还可以写得更短点：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">Collections.sort(names, (a, b) -&gt; b.compareTo(a));</span><br></pre></td></tr></table></figure><p>Java编译器可以自动推导出参数类型，所以你可以不用再写一次类型。接下来我们看看lambda表达式还能作出什么更方便的东西来：</p><h2 id="三、函数式接口"><a href="#三、函数式接口" class="headerlink" title="三、函数式接口"></a>三、函数式接口</h2><p>Lambda表达式是如何在java的类型系统中表示的呢？每一个lambda表达式都对应一个类型，通常是接口类型。而“函数式接口”是指仅仅只包含一个抽象方法的接口，每一个该类型的lambda表达式都会被匹配到这个抽象方法。因为 默认方法 不算抽象方法，所以你也可以给你的函数式接口添加默认方法。</p><p>我们可以将lambda表达式当作任意只包含一个抽象方法的接口类型，确保你的接口一定达到这个要求，你只需要给你的接口添加 @FunctionalInterface 注解，编译器如果发现你标注了这个注解的接口有多于一个抽象方法的时候会报错的。</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">@FunctionalInterface</span><br><span class="line">interface Converter&lt;F, T&gt; &#123;</span><br><span class="line">    T convert(F from);</span><br><span class="line">&#125;</span><br><span class="line">Converter&lt;String, Integer&gt; converter = (from) -&gt; Integer.valueOf(from);</span><br><span class="line">Integer converted = converter.convert(&quot;123&quot;);</span><br><span class="line">System.out.println(converted);    // 123</span><br></pre></td></tr></table></figure><p>需要注意如果@FunctionalInterface如果没有指定，上面的代码也是对的。</p><p>将lambda表达式映射到一个单方法的接口上，这种做法在Java 8之前就有别的语言实现，比如Rhino JavaScript解释器，如果一个函数参数接收一个单方法的接口而你传递的是一个function，Rhino 解释器会自动做一个单接口的实例到function的适配器，典型的应用场景有 org.w3c.dom.events.EventTarget 的addEventListener 第二个参数 EventListener。</p><h2 id="四、方法与构造函数引用"><a href="#四、方法与构造函数引用" class="headerlink" title="四、方法与构造函数引用"></a>四、方法与构造函数引用</h2><p>前一节中的代码还可以通过静态方法引用来表示：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">Converter&lt;String, Integer&gt; converter = Integer::valueOf;</span><br><span class="line">Integer converted = converter.convert(&quot;123&quot;);</span><br><span class="line">System.out.println(converted);   // 123</span><br></pre></td></tr></table></figure><p>Java 8 允许你使用 :: 关键字来传递方法或者构造函数引用，上面的代码展示了如何引用一个静态方法，我们也可以引用一个对象的方法：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">converter = something::startsWith;</span><br><span class="line">String converted = converter.convert(&quot;Java&quot;);</span><br><span class="line">System.out.println(converted);    // &quot;J&quot;</span><br></pre></td></tr></table></figure><p>接下来看看构造函数是如何使用::关键字来引用的，首先我们定义一个包含多个构造函数的简单类：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">class Person &#123;</span><br><span class="line">    String firstName;</span><br><span class="line">    String lastName;</span><br><span class="line">    Person() &#123;&#125;</span><br><span class="line"></span><br><span class="line">    Person(String firstName, String lastName) &#123;</span><br><span class="line">        this.firstName = firstName;</span><br><span class="line">        this.lastName = lastName;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>接下来我们指定一个用来创建Person对象的对象工厂接口：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">interface PersonFactory&lt;P extends Person&gt; &#123;</span><br><span class="line">    P create(String firstName, String lastName);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里我们使用构造函数引用来将他们关联起来，而不是实现一个完整的工厂：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">PersonFactory&lt;Person&gt; personFactory = Person::new;</span><br><span class="line">Person person = personFactory.create(&quot;Peter&quot;, &quot;Parker&quot;);</span><br></pre></td></tr></table></figure><p>我们只需要使用 Person::new 来获取Person类构造函数的引用，Java编译器会自动根据PersonFactory.create方法的签名来选择合适的构造函数。</p><h2 id="五、Lambda-作用域"><a href="#五、Lambda-作用域" class="headerlink" title="五、Lambda 作用域"></a>五、Lambda 作用域</h2><p>在lambda表达式中访问外层作用域和老版本的匿名对象中的方式很相似。你可以直接访问标记了final的外层局部变量，或者实例的字段以及静态变量。</p><h2 id="六、访问局部变量"><a href="#六、访问局部变量" class="headerlink" title="六、访问局部变量"></a>六、访问局部变量</h2><p>我们可以直接在lambda表达式中访问外层的局部变量：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">final int num = 1;</span><br><span class="line">Converter&lt;Integer, String&gt; stringConverter =</span><br><span class="line">        (from) -&gt; String.valueOf(from + num);</span><br><span class="line">stringConverter.convert(2);     // 3</span><br></pre></td></tr></table></figure><p>但是和匿名对象不同的是，这里的变量num可以不用声明为final，该代码同样正确：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">int num = 1;</span><br><span class="line">Converter&lt;Integer, String&gt; stringConverter =</span><br><span class="line">        (from) -&gt; String.valueOf(from + num);</span><br><span class="line">stringConverter.convert(2);     // 3</span><br></pre></td></tr></table></figure><p>不过这里的num必须不可被后面的代码修改（即隐性的具有final的语义），例如下面的就无法编译：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">int num = 1;</span><br><span class="line">Converter&lt;Integer, String&gt; stringConverter =</span><br><span class="line">        (from) -&gt; String.valueOf(from + num);</span><br><span class="line">num = 3;</span><br></pre></td></tr></table></figure><p>在lambda表达式中试图修改num同样是不允许的。</p><h2 id="七、访问对象字段与静态变量"><a href="#七、访问对象字段与静态变量" class="headerlink" title="七、访问对象字段与静态变量"></a>七、访问对象字段与静态变量</h2><p>和本地变量不同的是，lambda内部对于实例的字段以及静态变量是即可读又可写。该行为和匿名对象是一致的：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">class Lambda4 &#123;</span><br><span class="line">    static int outerStaticNum;</span><br><span class="line">    int outerNum;</span><br><span class="line">    void testScopes() &#123;</span><br><span class="line">        Converter&lt;Integer, String&gt; stringConverter1 = (from) -&gt; &#123;</span><br><span class="line">            outerNum = 23;</span><br><span class="line">            return String.valueOf(from);</span><br><span class="line">        &#125;;</span><br><span class="line"></span><br><span class="line">        Converter&lt;Integer, String&gt; stringConverter2 = (from) -&gt; &#123;</span><br><span class="line">            outerStaticNum = 72;</span><br><span class="line">            return String.valueOf(from);</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><h2 id="八、访问接口的默认方法"><a href="#八、访问接口的默认方法" class="headerlink" title="八、访问接口的默认方法"></a>八、访问接口的默认方法</h2><p>还记得第一节中的formula例子么，接口Formula定义了一个默认方法sqrt可以直接被formula的实例包括匿名对象访问到，但是在lambda表达式中这个是不行的。<br>Lambda表达式中是无法访问到默认方法的，以下代码将无法编译：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">Formula formula = (a) -&gt; sqrt( a * 100);</span><br><span class="line">Built-in Functional Interfaces</span><br></pre></td></tr></table></figure><p>JDK 1.8 API包含了很多内建的函数式接口，在老Java中常用到的比如Comparator或者Runnable接口，这些接口都增加了@FunctionalInterface注解以便能用在lambda上。<br>Java 8 API同样还提供了很多全新的函数式接口来让工作更加方便，有一些接口是来自Google Guava库里的，即便你对这些很熟悉了，还是有必要看看这些是如何扩展到lambda上使用的。</p><h3 id="Predicate接口"><a href="#Predicate接口" class="headerlink" title="Predicate接口"></a>Predicate接口</h3><p>Predicate 接口只有一个参数，返回boolean类型。该接口包含多种默认方法来将Predicate组合成其他复杂的逻辑（比如：与，或，非）：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">Predicate&lt;String&gt; predicate = (s) -&gt; s.length() &gt; 0;</span><br><span class="line">predicate.test(&quot;foo&quot;);              // true</span><br><span class="line">predicate.negate().test(&quot;foo&quot;);     // false</span><br><span class="line"></span><br><span class="line">Predicate&lt;Boolean&gt; nonNull = Objects::nonNull;</span><br><span class="line">Predicate&lt;Boolean&gt; isNull = Objects::isNull;</span><br><span class="line"></span><br><span class="line">Predicate&lt;String&gt; isEmpty = String::isEmpty;</span><br><span class="line">Predicate&lt;String&gt; isNotEmpty = isEmpty.negate();</span><br></pre></td></tr></table></figure><h3 id="Function-接口"><a href="#Function-接口" class="headerlink" title="Function 接口"></a>Function 接口</h3><p>Function 接口有一个参数并且返回一个结果，并附带了一些可以和其他函数组合的默认方法（compose, andThen）：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">Function&lt;String, Integer&gt; toInteger = Integer::valueOf;</span><br><span class="line">Function&lt;String, String&gt; backToString = toInteger.andThen(String::valueOf);</span><br><span class="line">backToString.apply(&quot;123&quot;);     // &quot;123&quot;</span><br></pre></td></tr></table></figure><h3 id="Supplier-接口"><a href="#Supplier-接口" class="headerlink" title="Supplier 接口"></a>Supplier 接口</h3><p>Supplier 接口返回一个任意范型的值，和Function接口不同的是该接口没有任何参数</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">Supplier&lt;Person&gt; personSupplier = Person::new;</span><br><span class="line">personSupplier.get();   // new Person</span><br></pre></td></tr></table></figure><h3 id="Consumer-接口"><a href="#Consumer-接口" class="headerlink" title="Consumer 接口"></a>Consumer 接口</h3><p>Consumer 接口表示执行在单个参数上的操作。</p><p>Consumer<person> greeter = (p) -&gt; System.out.println(“Hello, “ + p.firstName);<br>greeter.accept(new Person(“Luke”, “Skywalker”));</person></p><h3 id="Comparator-接口"><a href="#Comparator-接口" class="headerlink" title="Comparator 接口"></a>Comparator 接口</h3><p>Comparator 是老Java中的经典接口， Java 8在此之上添加了多种默认方法：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">Comparator&lt;Person&gt; comparator = (p1, p2) -&gt; p1.firstName.compareTo(p2.firstName);</span><br><span class="line">Person p1 = new Person(&quot;John&quot;, &quot;Doe&quot;);</span><br><span class="line">Person p2 = new Person(&quot;Alice&quot;, &quot;Wonderland&quot;);</span><br><span class="line"></span><br><span class="line">comparator.compare(p1, p2);             // &gt; 0</span><br><span class="line">comparator.reversed().compare(p1, p2);  // &lt; 0</span><br></pre></td></tr></table></figure><h3 id="Optional-接口"><a href="#Optional-接口" class="headerlink" title="Optional 接口"></a>Optional 接口</h3><p>Optional 不是函数是接口，这是个用来防止NullPointerException异常的辅助类型，这是下一届中将要用到的重要概念，现在先简单的看看这个接口能干什么：</p><p>Optional 被定义为一个简单的容器，其值可能是null或者不是null。在Java 8之前一般某个函数应该返回非空对象但是偶尔却可能返回了null，而在Java 8中，不推荐你返回null而是返回Optional。</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">Optional&lt;String&gt; optional = Optional.of(&quot;bam&quot;);</span><br><span class="line">optional.isPresent();           // true</span><br><span class="line">optional.get();                 // &quot;bam&quot;</span><br><span class="line">optional.orElse(&quot;fallback&quot;);    // &quot;bam&quot;</span><br><span class="line"></span><br><span class="line">optional.ifPresent((s) -&gt; System.out.println(s.charAt(0)));     // &quot;b&quot;</span><br></pre></td></tr></table></figure><h3 id="Stream-接口"><a href="#Stream-接口" class="headerlink" title="Stream 接口"></a>Stream 接口</h3><p>java.util.Stream 表示能应用在一组元素上一次执行的操作序列。Stream 操作分为中间操作或者最终操作两种，最终操作返回一特定类型的计算结果，而中间操作返回Stream本身，这样你就可以将多个操作依次串起来。Stream 的创建需要指定一个数据源，比如 java.util.Collection的子类，List或者Set， Map不支持。Stream的操作可以串行执行或者并行执行。</p><p>首先看看Stream是怎么用，首先创建实例代码的用到的数据List：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">List&lt;String&gt; stringCollection = new ArrayList&lt;&gt;();</span><br><span class="line">stringCollection.add(&quot;ddd2&quot;);</span><br><span class="line">stringCollection.add(&quot;aaa2&quot;);</span><br><span class="line">stringCollection.add(&quot;bbb1&quot;);</span><br><span class="line">stringCollection.add(&quot;aaa1&quot;);</span><br><span class="line">stringCollection.add(&quot;bbb3&quot;);</span><br><span class="line">stringCollection.add(&quot;ccc&quot;);</span><br><span class="line">stringCollection.add(&quot;bbb2&quot;);</span><br><span class="line">stringCollection.add(&quot;ddd1&quot;);</span><br></pre></td></tr></table></figure><p>Java 8扩展了集合类，可以通过 Collection.stream() 或者 Collection.parallelStream() 来创建一个Stream。下面几节将详细解释常用的Stream操作：</p><h3 id="Filter-过滤"><a href="#Filter-过滤" class="headerlink" title="Filter 过滤"></a>Filter 过滤</h3><p>过滤通过一个predicate接口来过滤并只保留符合条件的元素，该操作属于中间操作，所以我们可以在过滤后的结果来应用其他Stream操作（比如forEach）。forEach需要一个函数来对过滤后的元素依次执行。forEach是一个最终操作，所以我们不能在forEach之后来执行其他Stream操作。</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">stringCollection</span><br><span class="line">    .stream()</span><br><span class="line">    .filter((s) -&gt; s.startsWith(&quot;a&quot;))</span><br><span class="line">    .forEach(System.out::println);</span><br><span class="line">// &quot;aaa2&quot;, &quot;aaa1&quot;</span><br></pre></td></tr></table></figure><h3 id="Sort-排序"><a href="#Sort-排序" class="headerlink" title="Sort 排序"></a>Sort 排序</h3><p>排序是一个中间操作，返回的是排序好后的Stream。如果你不指定一个自定义的Comparator则会使用默认排序。</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">stringCollection</span><br><span class="line">    .stream()</span><br><span class="line">    .sorted()</span><br><span class="line">    .filter((s) -&gt; s.startsWith(&quot;a&quot;))</span><br><span class="line">    .forEach(System.out::println);</span><br><span class="line">// &quot;aaa1&quot;, &quot;aaa2&quot;</span><br></pre></td></tr></table></figure><p>需要注意的是，排序只创建了一个排列好后的Stream，而不会影响原有的数据源，排序之后原数据stringCollection是不会被修改的：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">System.out.println(stringCollection);</span><br><span class="line">// ddd2, aaa2, bbb1, aaa1, bbb3, ccc, bbb2, ddd1</span><br></pre></td></tr></table></figure><h3 id="Map-映射"><a href="#Map-映射" class="headerlink" title="Map 映射"></a>Map 映射</h3><p>中间操作map会将元素根据指定的Function接口来依次将元素转成另外的对象，下面的示例展示了将字符串转换为大写字符串。你也可以通过map来讲对象转换成其他类型，map返回的Stream类型是根据你map传递进去的函数的返回值决定的。</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">stringCollection</span><br><span class="line">    .stream()</span><br><span class="line">    .map(String::toUpperCase)</span><br><span class="line">    .sorted((a, b) -&gt; b.compareTo(a))</span><br><span class="line">    .forEach(System.out::println);</span><br><span class="line">// &quot;DDD2&quot;, &quot;DDD1&quot;, &quot;CCC&quot;, &quot;BBB3&quot;, &quot;BBB2&quot;, &quot;AAA2&quot;, &quot;AAA1&quot;</span><br></pre></td></tr></table></figure><h3 id="Match-匹配"><a href="#Match-匹配" class="headerlink" title="Match 匹配"></a>Match 匹配</h3><p>Stream提供了多种匹配操作，允许检测指定的Predicate是否匹配整个Stream。所有的匹配操作都是最终操作，并返回一个boolean类型的值。</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">boolean anyStartsWithA = </span><br><span class="line">    stringCollection</span><br><span class="line">        .stream()</span><br><span class="line">        .anyMatch((s) -&gt; s.startsWith(&quot;a&quot;));</span><br><span class="line">System.out.println(anyStartsWithA);      // true</span><br><span class="line"></span><br><span class="line">boolean allStartsWithA = </span><br><span class="line">    stringCollection</span><br><span class="line">        .stream()</span><br><span class="line">        .allMatch((s) -&gt; s.startsWith(&quot;a&quot;));</span><br><span class="line"></span><br><span class="line">System.out.println(allStartsWithA);      // false</span><br><span class="line"></span><br><span class="line">boolean noneStartsWithZ = </span><br><span class="line">    stringCollection</span><br><span class="line">        .stream()</span><br><span class="line">        .noneMatch((s) -&gt; s.startsWith(&quot;z&quot;));</span><br><span class="line"></span><br><span class="line">System.out.println(noneStartsWithZ);      // true</span><br></pre></td></tr></table></figure><h3 id="Count-计数"><a href="#Count-计数" class="headerlink" title="Count 计数"></a>Count 计数</h3><p>计数是一个最终操作，返回Stream中元素的个数，返回值类型是long。</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">long startsWithB = </span><br><span class="line">    stringCollection</span><br><span class="line">        .stream()</span><br><span class="line">        .filter((s) -&gt; s.startsWith(&quot;b&quot;))</span><br><span class="line">        .count();</span><br><span class="line">System.out.println(startsWithB);    // 3</span><br></pre></td></tr></table></figure><h3 id="Reduce-规约"><a href="#Reduce-规约" class="headerlink" title="Reduce 规约"></a>Reduce 规约</h3><p>这是一个最终操作，允许通过指定的函数来讲stream中的多个元素规约为一个元素，规越后的结果是通过Optional接口表示的：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">Optional&lt;String&gt; reduced =</span><br><span class="line">    stringCollection</span><br><span class="line">        .stream()</span><br><span class="line">        .sorted()</span><br><span class="line">        .reduce((s1, s2) -&gt; s1 + &quot;#&quot; + s2);</span><br><span class="line">reduced.ifPresent(System.out::println);</span><br><span class="line">// &quot;aaa1#aaa2#bbb1#bbb2#bbb3#ccc#ddd1#ddd2&quot;</span><br></pre></td></tr></table></figure><h3 id="并行Streams"><a href="#并行Streams" class="headerlink" title="并行Streams"></a>并行Streams</h3><p>前面提到过Stream有串行和并行两种，串行Stream上的操作是在一个线程中依次完成，而并行Stream则是在多个线程上同时执行。</p><p>下面的例子展示了是如何通过并行Stream来提升性能：</p><p>首先我们创建一个没有重复元素的大表：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">int max = 1000000;</span><br><span class="line">List&lt;String&gt; values = new ArrayList&lt;&gt;(max);</span><br><span class="line">for (int i = 0; i &lt; max; i++) &#123;</span><br><span class="line">    UUID uuid = UUID.randomUUID();</span><br><span class="line">    values.add(uuid.toString());</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>然后我们计算一下排序这个Stream要耗时多久，<br>串行排序：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">long t0 = System.nanoTime();</span><br><span class="line">long count = values.stream().sorted().count();</span><br><span class="line">System.out.println(count);</span><br><span class="line"></span><br><span class="line">long t1 = System.nanoTime();</span><br><span class="line"></span><br><span class="line">long millis = TimeUnit.NANOSECONDS.toMillis(t1 - t0);</span><br><span class="line">System.out.println(String.format(&quot;sequential sort took: %d ms&quot;, millis));</span><br></pre></td></tr></table></figure><p>// 串行耗时: 899 ms<br>并行排序：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">long t0 = System.nanoTime();</span><br><span class="line">long count = values.parallelStream().sorted().count();</span><br><span class="line">System.out.println(count);</span><br><span class="line"></span><br><span class="line">long t1 = System.nanoTime();</span><br><span class="line"></span><br><span class="line">long millis = TimeUnit.NANOSECONDS.toMillis(t1 - t0);</span><br><span class="line">System.out.println(String.format(&quot;parallel sort took: %d ms&quot;, millis));</span><br></pre></td></tr></table></figure><p>// 并行排序耗时: 472 ms<br>上面两个代码几乎是一样的，但是并行版的快了50%之多，唯一需要做的改动就是将stream()改为parallelStream()。</p><h3 id="Map"><a href="#Map" class="headerlink" title="Map"></a>Map</h3><p>前面提到过，Map类型不支持stream，不过Map提供了一些新的有用的方法来处理一些日常任务。</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">Map&lt;Integer, String&gt; map = new HashMap&lt;&gt;();</span><br><span class="line">for (int i = 0; i &lt; 10; i++) &#123;</span><br><span class="line">    map.putIfAbsent(i, &quot;val&quot; + i);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>map.forEach((id, val) -&gt; System.out.println(val));<br>以上代码很容易理解， putIfAbsent 不需要我们做额外的存在性检查，而forEach则接收一个Consumer接口来对map里的每一个键值对进行操作。</p><p>下面的例子展示了map上的其他有用的函数：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">map.computeIfPresent(3, (num, val) -&gt; val + num);</span><br><span class="line">map.get(3);             // val33</span><br><span class="line">map.computeIfPresent(9, (num, val) -&gt; null);</span><br><span class="line">map.containsKey(9);     // false</span><br><span class="line"></span><br><span class="line">map.computeIfAbsent(23, num -&gt; &quot;val&quot; + num);</span><br><span class="line">map.containsKey(23);    // true</span><br><span class="line"></span><br><span class="line">map.computeIfAbsent(3, num -&gt; &quot;bam&quot;);</span><br><span class="line">map.get(3);             // val33</span><br></pre></td></tr></table></figure><p>接下来展示如何在Map里删除一个键值全都匹配的项：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">map.remove(3, &quot;val3&quot;);</span><br><span class="line">map.get(3);             // val33</span><br><span class="line">map.remove(3, &quot;val33&quot;);</span><br><span class="line">map.get(3);             // null</span><br></pre></td></tr></table></figure><p>另外一个有用的方法：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">map.getOrDefault(42, &quot;not found&quot;);  // not found</span><br></pre></td></tr></table></figure><p>对Map的元素做合并也变得很容易了：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">map.merge(9, &quot;val9&quot;, (value, newValue) -&gt; value.concat(newValue));</span><br><span class="line">map.get(9);             // val9</span><br><span class="line">map.merge(9, &quot;concat&quot;, (value, newValue) -&gt; value.concat(newValue));</span><br><span class="line">map.get(9);             // val9concat</span><br></pre></td></tr></table></figure><p>Merge做的事情是如果键名不存在则插入，否则则对原键对应的值做合并操作并重新插入到map中。</p><h2 id="九、Date-API"><a href="#九、Date-API" class="headerlink" title="九、Date API"></a>九、Date API</h2><p>Java 8 在包java.time下包含了一组全新的时间日期API。新的日期API和开源的Joda-Time库差不多，但又不完全一样，下面的例子展示了这组新API里最重要的一些部分：</p><h3 id="Clock-时钟"><a href="#Clock-时钟" class="headerlink" title="Clock 时钟"></a>Clock 时钟</h3><p>Clock类提供了访问当前日期和时间的方法，Clock是时区敏感的，可以用来取代 System.currentTimeMillis() 来获取当前的微秒数。某一个特定的时间点也可以使用Instant类来表示，Instant类也可以用来创建老的java.util.Date对象。</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">Clock clock = Clock.systemDefaultZone();</span><br><span class="line">long millis = clock.millis();</span><br><span class="line">Instant instant = clock.instant();</span><br><span class="line">Date legacyDate = Date.from(instant);   // legacy java.util.Date</span><br></pre></td></tr></table></figure><h3 id="Timezones-时区"><a href="#Timezones-时区" class="headerlink" title="Timezones 时区"></a>Timezones 时区</h3><p>在新API中时区使用ZoneId来表示。时区可以很方便的使用静态方法of来获取到。 时区定义了到UTS时间的时间差，在Instant时间点对象到本地日期对象之间转换的时候是极其重要的。</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">System.out.println(ZoneId.getAvailableZoneIds());</span><br><span class="line">// prints all available timezone ids</span><br><span class="line">ZoneId zone1 = ZoneId.of(&quot;Europe/Berlin&quot;);</span><br><span class="line">ZoneId zone2 = ZoneId.of(&quot;Brazil/East&quot;);</span><br><span class="line">System.out.println(zone1.getRules());</span><br><span class="line">System.out.println(zone2.getRules());</span><br><span class="line"></span><br><span class="line">// ZoneRules[currentStandardOffset=+01:00]</span><br><span class="line">// ZoneRules[currentStandardOffset=-03:00]</span><br></pre></td></tr></table></figure><h3 id="LocalTime-本地时间"><a href="#LocalTime-本地时间" class="headerlink" title="LocalTime 本地时间"></a>LocalTime 本地时间</h3><p>LocalTime 定义了一个没有时区信息的时间，例如 晚上10点，或者 17:30:15。下面的例子使用前面代码创建的时区创建了两个本地时间。之后比较时间并以小时和分钟为单位计算两个时间的时间差：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">LocalTime now1 = LocalTime.now(zone1);</span><br><span class="line">LocalTime now2 = LocalTime.now(zone2);</span><br><span class="line">System.out.println(now1.isBefore(now2));  // false</span><br><span class="line"></span><br><span class="line">long hoursBetween = ChronoUnit.HOURS.between(now1, now2);</span><br><span class="line">long minutesBetween = ChronoUnit.MINUTES.between(now1, now2);</span><br><span class="line"></span><br><span class="line">System.out.println(hoursBetween);       // -3</span><br><span class="line">System.out.println(minutesBetween);     // -239</span><br></pre></td></tr></table></figure><p>LocalTime 提供了多种工厂方法来简化对象的创建，包括解析时间字符串。</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">LocalTime late = LocalTime.of(23, 59, 59);</span><br><span class="line">System.out.println(late);       // 23:59:59</span><br><span class="line">DateTimeFormatter germanFormatter =</span><br><span class="line">    DateTimeFormatter</span><br><span class="line">        .ofLocalizedTime(FormatStyle.SHORT)</span><br><span class="line">        .withLocale(Locale.GERMAN);</span><br><span class="line"></span><br><span class="line">LocalTime leetTime = LocalTime.parse(&quot;13:37&quot;, germanFormatter);</span><br><span class="line">System.out.println(leetTime);   // 13:37</span><br></pre></td></tr></table></figure><h3 id="LocalDate-本地日期"><a href="#LocalDate-本地日期" class="headerlink" title="LocalDate 本地日期"></a>LocalDate 本地日期</h3><p>LocalDate 表示了一个确切的日期，比如 2014-03-11。该对象值是不可变的，用起来和LocalTime基本一致。下面的例子展示了如何给Date对象加减天/月/年。另外要注意的是这些对象是不可变的，操作返回的总是一个新实例。</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">LocalDate today = LocalDate.now();</span><br><span class="line">LocalDate tomorrow = today.plus(1, ChronoUnit.DAYS);</span><br><span class="line">LocalDate yesterday = tomorrow.minusDays(2);</span><br><span class="line">LocalDate independenceDay = LocalDate.of(2014, Month.JULY, 4);</span><br><span class="line">DayOfWeek dayOfWeek = independenceDay.getDayOfWeek();</span><br></pre></td></tr></table></figure><p>System.out.println(dayOfWeek);    // FRIDAY<br>从字符串解析一个LocalDate类型和解析LocalTime一样简单：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">DateTimeFormatter germanFormatter =</span><br><span class="line">    DateTimeFormatter</span><br><span class="line">        .ofLocalizedDate(FormatStyle.MEDIUM)</span><br><span class="line">        .withLocale(Locale.GERMAN);</span><br><span class="line">LocalDate xmas = LocalDate.parse(&quot;24.12.2014&quot;, germanFormatter);</span><br><span class="line">System.out.println(xmas);   // 2014-12-24</span><br></pre></td></tr></table></figure><h3 id="LocalDateTime-本地日期时间"><a href="#LocalDateTime-本地日期时间" class="headerlink" title="LocalDateTime 本地日期时间"></a>LocalDateTime 本地日期时间</h3><p>LocalDateTime 同时表示了时间和日期，相当于前两节内容合并到一个对象上了。LocalDateTime和LocalTime还有LocalDate一样，都是不可变的。LocalDateTime提供了一些能访问具体字段的方法。</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">LocalDateTime sylvester = LocalDateTime.of(2014, Month.DECEMBER, 31, 23, 59, 59);</span><br><span class="line">DayOfWeek dayOfWeek = sylvester.getDayOfWeek();</span><br><span class="line">System.out.println(dayOfWeek);      // WEDNESDAY</span><br><span class="line"></span><br><span class="line">Month month = sylvester.getMonth();</span><br><span class="line">System.out.println(month);          // DECEMBER</span><br><span class="line"></span><br><span class="line">long minuteOfDay = sylvester.getLong(ChronoField.MINUTE_OF_DAY);</span><br><span class="line">System.out.println(minuteOfDay);    // 1439</span><br></pre></td></tr></table></figure><p>只要附加上时区信息，就可以将其转换为一个时间点Instant对象，Instant时间点对象可以很容易的转换为老式的java.util.Date。</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">Instant instant = sylvester</span><br><span class="line">        .atZone(ZoneId.systemDefault())</span><br><span class="line">        .toInstant();</span><br><span class="line">Date legacyDate = Date.from(instant);</span><br><span class="line">System.out.println(legacyDate);     // Wed Dec 31 23:59:59 CET 2014</span><br></pre></td></tr></table></figure><p>格式化LocalDateTime和格式化时间和日期一样的，除了使用预定义好的格式外，我们也可以自己定义格式：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">DateTimeFormatter formatter =</span><br><span class="line">    DateTimeFormatter</span><br><span class="line">        .ofPattern(&quot;MMM dd, yyyy - HH:mm&quot;);</span><br><span class="line">LocalDateTime parsed = LocalDateTime.parse(&quot;Nov 03, 2014 - 07:13&quot;, formatter);</span><br><span class="line">String string = formatter.format(parsed);</span><br><span class="line">System.out.println(string);     // Nov 03, 2014 - 07:13</span><br></pre></td></tr></table></figure><p>和java.text.NumberFormat不一样的是新版的DateTimeFormatter是不可变的，所以它是线程安全的。<br>关于时间日期格式的详细信息：<a href="http://download.java.net/jdk8/docs/api/java/time/format/DateTimeFormatter.html" target="_blank" rel="noopener">http://download.java.net/jdk8/docs/api/java/time/format/DateTimeFormatter.html</a></p><h2 id="十、Annotation-注解"><a href="#十、Annotation-注解" class="headerlink" title="十、Annotation 注解"></a>十、Annotation 注解</h2><p>在Java 8中支持多重注解了，先看个例子来理解一下是什么意思。<br>首先定义一个包装类Hints注解用来放置一组具体的Hint注解：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">@interface Hints &#123;</span><br><span class="line">    Hint[] value();</span><br><span class="line">&#125;</span><br><span class="line">@Repeatable(Hints.class)</span><br><span class="line">@interface Hint &#123;</span><br><span class="line">    String value();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>Java 8允许我们把同一个类型的注解使用多次，只需要给该注解标注一下@Repeatable即可。<br>例 1: 使用包装类当容器来存多个注解（老方法）</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">@Hints(&#123;@Hint(&quot;hint1&quot;), @Hint(&quot;hint2&quot;)&#125;)</span><br><span class="line">class Person &#123;&#125;</span><br></pre></td></tr></table></figure><p>例 2：使用多重注解（新方法）</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">@Hint(&quot;hint1&quot;)</span><br><span class="line">@Hint(&quot;hint2&quot;)</span><br><span class="line">class Person &#123;&#125;</span><br></pre></td></tr></table></figure><p>第二个例子里java编译器会隐性的帮你定义好@Hints注解，了解这一点有助于你用反射来获取这些信息：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">Hint hint = Person.class.getAnnotation(Hint.class);</span><br><span class="line">System.out.println(hint);                   // null</span><br><span class="line">Hints hints1 = Person.class.getAnnotation(Hints.class);</span><br><span class="line">System.out.println(hints1.value().length);  // 2</span><br><span class="line"></span><br><span class="line">Hint[] hints2 = Person.class.getAnnotationsByType(Hint.class);</span><br><span class="line">System.out.println(hints2.length);          // 2</span><br></pre></td></tr></table></figure><p>即便我们没有在Person类上定义@Hints注解，我们还是可以通过 getAnnotation(Hints.class) 来获取 @Hints注解，更加方便的方法是使用 getAnnotationsByType 可以直接获取到所有的@Hint注解。<br>另外Java 8的注解还增加到两种新的target上了：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">@Target(&#123;ElementType.TYPE_PARAMETER, ElementType.TYPE_USE&#125;)</span><br><span class="line">@interface MyAnnotation &#123;&#125;</span><br></pre></td></tr></table></figure><p>关于Java 8的新特性就写到这了，肯定还有更多的特性等待发掘。JDK 1.8里还有很多很有用的东西，比如Arrays.parallelSort, StampedLock和CompletableFuture等等。</p>]]></content>
    
    <summary type="html">
    
      JDK1.8新特性详解
    
    </summary>
    
      <category term="java" scheme="https://ningyu1.github.io/categories/java/"/>
    
    
      <category term="jdk" scheme="https://ningyu1.github.io/tags/jdk/"/>
    
  </entry>
  
  <entry>
    <title>JDK1.7新特性详解</title>
    <link href="https://ningyu1.github.io/20171215/43-jdk1.7-feature.html"/>
    <id>https://ningyu1.github.io/20171215/43-jdk1.7-feature.html</id>
    <published>2017-12-15T10:00:36.000Z</published>
    <updated>2024-04-23T01:48:15.034Z</updated>
    
    <content type="html"><![CDATA[<p>JDK7对Java语法有少量更新，重点是在易用性和便捷性的改进。</p><h2 id="1-二进制字面量"><a href="#1-二进制字面量" class="headerlink" title="1.二进制字面量"></a>1.二进制字面量</h2><p>JDK7开始，终于可以用二进制来表示整数（byte,short,int和long）。使用二进制字面量的好处是，可以是代码更容易被理解。语法非常简单，只要在二进制数值前面加 0b或者0B</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">byte nByte = (byte)0b0001;  </span><br><span class="line">short nShort = (short)0B0010;  </span><br><span class="line">int nInt = 0b0011;  </span><br><span class="line">long nLong = 0b0100L;</span><br></pre></td></tr></table></figure><h2 id="2-数字字面量可以出现下划线"><a href="#2-数字字面量可以出现下划线" class="headerlink" title="2.数字字面量可以出现下划线"></a>2.数字字面量可以出现下划线</h2><p>对于一些比较大的数字，我们定义起来总是不方面，经常缺少或者增加位数。JDK7为我们提供了一种解决方案，下划线可以出现在数字字面量。</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">int a = 10_0000_0000;  </span><br><span class="line">long b = 0xffff_ffff_ffff_ffffl;  </span><br><span class="line">byte c = 0b0001_1000;</span><br></pre></td></tr></table></figure><p>注意：你只能将下划线置于数字之间，以下使用方法是错误的，</p><ol><li>数字的开头或者结尾</li><li>小数点的前后</li><li>‘F’或者‘f’的后缀</li><li>只能用数字的位置</li></ol><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">nt err1 = _11,err2=11_;  </span><br><span class="line">float err3=3._4,err4=3_.4;  </span><br><span class="line">long err5=0x888_f;</span><br></pre></td></tr></table></figure><h2 id="3-switch-语句可以用字符串了"><a href="#3-switch-语句可以用字符串了" class="headerlink" title="3.switch 语句可以用字符串了"></a>3.switch 语句可以用字符串了</h2><p>这个功能千呼万唤，终于出来了</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">private static void switchString(String str)&#123;  </span><br><span class="line">        switch(str)&#123;  </span><br><span class="line">            case &quot;one&quot;:  </span><br><span class="line">                System.err.println(&quot;1&quot;);  </span><br><span class="line">                break;  </span><br><span class="line">            case &quot;two&quot;:  </span><br><span class="line">                System.out.println(&quot;2&quot;);  </span><br><span class="line">                break;  </span><br><span class="line">            default :  </span><br><span class="line">                System.out.println(&quot;err&quot;);  </span><br><span class="line">        &#125;  </span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="4-泛型实例的创建可以通过类型推断来简化"><a href="#4-泛型实例的创建可以通过类型推断来简化" class="headerlink" title="4.泛型实例的创建可以通过类型推断来简化"></a>4.泛型实例的创建可以通过类型推断来简化</h2><p>以后你创建一个泛型实例，不需要再详细说明类型，只需用&lt;&gt;,编译器会自动帮你匹配</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">//例如   </span><br><span class="line">Map&lt;String, List&lt;String&gt;&gt; myMap = new HashMap&lt;String, List&lt;String&gt;&gt;();  </span><br><span class="line">//可以简化为  </span><br><span class="line">Map&lt;String, List&lt;String&gt;&gt; myMap = new HashMap&lt;&gt;();</span><br></pre></td></tr></table></figure><h2 id="5-在可变参数方法中传递非具体化参数（Non-Reifiable-Formal-Parameters）-改进编译警告和错误"><a href="#5-在可变参数方法中传递非具体化参数（Non-Reifiable-Formal-Parameters）-改进编译警告和错误" class="headerlink" title="5.在可变参数方法中传递非具体化参数（Non-Reifiable Formal Parameters）,改进编译警告和错误"></a>5.在可变参数方法中传递非具体化参数（Non-Reifiable Formal Parameters）,改进编译警告和错误</h2><p>有些参数类型，例如ArrayList<number> 和 List<string>,是非具体化的（non-reifiable）.在编译阶段，编译器会擦除该类型信息。</string></number></p><p>Heappollution 指一个变量被指向另外一个不是相同类型的变量。例如</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">List l = new ArrayList&lt;Number&gt;();  </span><br><span class="line">List&lt;String&gt; ls = l;       // unchecked warning  </span><br><span class="line">l.add(0, new Integer(42)); // another unchecked warning  </span><br><span class="line">String s = ls.get(0);      // ClassCastException is thrown</span><br></pre></td></tr></table></figure><p>回到我们的主题，在jdk7中，当你定义下面的函数时</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">public static &lt;T&gt; void addToList (List&lt;T&gt; listArg, T... elements) &#123;  </span><br><span class="line">    for (T x : elements) &#123;  </span><br><span class="line">      listArg.add(x);  </span><br><span class="line">    &#125;  </span><br><span class="line">  &#125;</span><br></pre></td></tr></table></figure><p>你会得到一个warning</p><p><code>warning: [varargs] Possible heap pollution from parameterized vararg type</code></p><p>在jdk7之前，当你调用一个含有非具体化参数的可变参数方法，你必须自行保证不会发生<code>heappollution</code>。这有一个问题，如果调用者对方法不熟悉，他根本无法判断。JDK7对此做了改进，在该方法被定义时久发出警告</p><p>要消除警告，可以有三种方式</p><ol><li>加 annotation @SafeVarargs</li><li>加 annotation @SuppressWarnings({“unchecked”, “varargs”})</li><li>使用编译器参数 –Xlint:varargs;</li></ol><h2 id="6-try-with-resources-语句"><a href="#6-try-with-resources-语句" class="headerlink" title="6.try-with-resources 语句"></a>6.try-with-resources 语句</h2><p>jdk7提供了try-with-resources,可以自动关闭相关的资源（只要该资源实现了AutoCloseable接口，jdk7为绝大部分资源对象都实现了这个接口）</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">static String readFirstLineFromFile(String path) throws IOException &#123;  </span><br><span class="line">  try (BufferedReader br = new BufferedReader(new FileReader(path))) &#123;  </span><br><span class="line">    return br.readLine();  </span><br><span class="line">  &#125;  </span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>try 语句块中还可以同时处理多个资源,可以跟普通的try语句一样catch异常，有finally语句块</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">try (  </span><br><span class="line">      java.util.zip.ZipFile zf = new java.util.zip.ZipFile(zipFileName);  </span><br><span class="line">      java.io.BufferedWriter writer = java.nio.file.Files.newBufferedWriter(outputFilePath, charset)  </span><br><span class="line">    ) &#123;  </span><br><span class="line">&#125;catch(…)&#123;  </span><br><span class="line">&#125;finally&#123;  </span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="7-Catch多个Exception，rethrow-exception-改进了类型检测"><a href="#7-Catch多个Exception，rethrow-exception-改进了类型检测" class="headerlink" title="7.Catch多个Exception，rethrow exception 改进了类型检测"></a>7.Catch多个Exception，rethrow exception 改进了类型检测</h2><p>很多时候，我们捕获了多个异常，却做了相同的事情，比如记日志，包装成新的异常，然后rethrow。这时，代码就不那么优雅了，例如</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">catch (IOException ex) &#123;  </span><br><span class="line">     logger.log(ex);  </span><br><span class="line">     throw ex;  </span><br><span class="line">catch (SQLException ex) &#123;  </span><br><span class="line">     logger.log(ex);  </span><br><span class="line">     throw ex;  </span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>Jdk7允许捕获多个异常</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">catch (IOException|SQLException ex) &#123;  </span><br><span class="line">    logger.log(ex);  </span><br><span class="line">    throw ex;  </span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>注意，catch后面的异常参数是final的，不能重新再复制</p><p><code>RethrowException</code>更具包容性的类型检测</p><p>当你重新抛出多个异常时，不再需要详细定义异常类型了，编译器已经知道你具体抛出的是哪个异常了。你只需在方法定义的时候声明需要抛出的异常即可</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">public void call() throws ReflectiveOperationException, IOException &#123;  </span><br><span class="line">    try &#123;  </span><br><span class="line">      callWithReflection(arg);  </span><br><span class="line">    &#125; catch (final Exception e) &#123;  </span><br><span class="line">      logger.trace(&quot;Exception in reflection&quot;, e);  </span><br><span class="line">      throw e;  </span><br><span class="line">    &#125;  </span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>参考资料</p><p>Jdk7官网 <a href="http://openjdk.java.net/projects/jdk7/" target="_blank" rel="noopener">http://openjdk.java.net/projects/jdk7/</a></p>]]></content>
    
    <summary type="html">
    
      JDK1.7新特性详解
    
    </summary>
    
      <category term="java" scheme="https://ningyu1.github.io/categories/java/"/>
    
    
      <category term="jdk" scheme="https://ningyu1.github.io/tags/jdk/"/>
    
  </entry>
  
  <entry>
    <title>Fastjson反序列化java.lang.VerifyError错误</title>
    <link href="https://ningyu1.github.io/20171215/42-fastjson.html"/>
    <id>https://ningyu1.github.io/20171215/42-fastjson.html</id>
    <published>2017-12-15T07:42:36.000Z</published>
    <updated>2024-04-23T01:48:15.034Z</updated>
    
    <content type="html"><![CDATA[<h1 id="现象"><a href="#现象" class="headerlink" title="现象"></a>现象</h1><p>当反序列化目标对象属性超过32个时会报如下错误：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">Exception in thread &quot;main&quot; java.lang.VerifyError: (class: com/alibaba/fastjson/parser/deserializer/FastjsonASMDeserializer_1_OmsMaterialStorageReconciliationEntity, method: deserialze signature: (Lcom/alibaba/fastjson/parser/DefaultJSONParser;Ljava/lang/reflect/Type;Ljava/lang/Object;I)Ljava/lang/Object;) Accessing value from uninitialized register 48</span><br><span class="line">    at java.lang.Class.getDeclaredConstructors0(Native Method)</span><br><span class="line">    at java.lang.Class.privateGetDeclaredConstructors(Class.java:2493)</span><br><span class="line">    at java.lang.Class.getConstructor0(Class.java:2803)</span><br><span class="line">    at java.lang.Class.getConstructor(Class.java:1718)</span><br><span class="line">    at com.alibaba.fastjson.parser.deserializer.ASMDeserializerFactory.createJavaBeanDeserializer(ASMDeserializerFactory.java:82)</span><br><span class="line">    at com.alibaba.fastjson.parser.ParserConfig.createJavaBeanDeserializer(ParserConfig.java:639)</span><br><span class="line">    at com.alibaba.fastjson.parser.ParserConfig.getDeserializer(ParserConfig.java:491)</span><br><span class="line">    at com.alibaba.fastjson.parser.ParserConfig.getDeserializer(ParserConfig.java:348)</span><br><span class="line">    at com.alibaba.fastjson.parser.DefaultJSONParser.parseObject(DefaultJSONParser.java:639)</span><br><span class="line">    at com.alibaba.fastjson.JSON.parseObject(JSON.java:350)</span><br><span class="line">    at com.alibaba.fastjson.JSON.parseObject(JSON.java:254)</span><br><span class="line">    at com.alibaba.fastjson.JSON.parseObject(JSON.java:467)</span><br><span class="line">    at com.jiuyescm.uam.main.Main.main(Main.java:29)</span><br></pre></td></tr></table></figure><p>查看我们使用的fastjson包版本：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">&lt;dependency&gt;</span><br><span class="line">    &lt;groupId&gt;com.alibaba&lt;/groupId&gt;</span><br><span class="line">    &lt;artifactId&gt;fastjson&lt;/artifactId&gt;</span><br><span class="line">    &lt;version&gt;1.2.28&lt;/version&gt;</span><br><span class="line">&lt;/dependency&gt;</span><br></pre></td></tr></table></figure><p>查看官方issues是否有同样的问题</p><p>找到问题：<a href="https://github.com/alibaba/fastjson/issues/1071" target="_blank" rel="noopener">https://github.com/alibaba/fastjson/issues/1071</a></p><p>是一个反序列化的bug，在1.2.29版本修复</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/fastjson/1.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p>升级我们使用的fastjson版本验证是否修复问题</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">&lt;dependency&gt;</span><br><span class="line">    &lt;groupId&gt;com.alibaba&lt;/groupId&gt;</span><br><span class="line">    &lt;artifactId&gt;fastjson&lt;/artifactId&gt;</span><br><span class="line">    &lt;version&gt;1.2.29&lt;/version&gt;</span><br><span class="line">&lt;/dependency&gt;</span><br></pre></td></tr></table></figure><p>测试代码：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">public static void main(String[] args) throws IOException &#123;</span><br><span class="line">    String a = &quot;&#123;\&quot;region\&quot;:\&quot;aaa\&quot;,\&quot;weight\&quot;:null,\&quot;outqty\&quot;:null,\&quot;inVolume\&quot;:null,\&quot;qtyMax\&quot;:null,\&quot;creTime\&quot;:null,\&quot;lastStock\&quot;:null,\&quot;inHeight\&quot;:null,\&quot;wallThickness\&quot;:null,\&quot;id\&quot;:null,\&quot;height\&quot;:null,\&quot;length\&quot;:null,\&quot;materialType\&quot;:null,\&quot;inqty\&quot;:null,\&quot;materialTypeName\&quot;:null,\&quot;materialName\&quot;:null,\&quot;supplierId\&quot;:null,\&quot;status\&quot;:null,\&quot;width\&quot;:null,\&quot;barcode\&quot;:null,\&quot;qtyMin\&quot;:null,\&quot;crePersonId\&quot;:null,\&quot;unit\&quot;:null,\&quot;changeDate\&quot;:null,\&quot;initStock\&quot;:null,\&quot;materialNo\&quot;:null,\&quot;crePerson\&quot;:null,\&quot;inLength\&quot;:null,\&quot;materialPrice\&quot;:null,\&quot;volume\&quot;:null,\&quot;inWidth\&quot;:null,\&quot;warehouseNo\&quot;:null&#125;&quot;;</span><br><span class="line">    OmsMaterialStorageReconciliationEntity t2 = JSON.parseObject(a, OmsMaterialStorageReconciliationEntity.class);</span><br><span class="line">    System.out.println(t2.getRegion());</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>OmsMaterialStorageReconciliationEntity 这个entity对象属性超过32个，运行测试结果：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">aaa</span><br></pre></td></tr></table></figure><p>运行结果符合预期，验证完毕</p><h1 id="结论"><a href="#结论" class="headerlink" title="结论"></a>结论</h1><ul><li>升级fastjson包版本 -&gt; 1.2.29</li></ul>]]></content>
    
    <summary type="html">
    
      Fastjson反序列化java.lang.VerifyError错误
    
    </summary>
    
      <category term="java" scheme="https://ningyu1.github.io/categories/java/"/>
    
    
      <category term="Fastjson" scheme="https://ningyu1.github.io/tags/Fastjson/"/>
    
  </entry>
  
  <entry>
    <title>Java开源APM概要</title>
    <link href="https://ningyu1.github.io/20171211/41-apm.html"/>
    <id>https://ningyu1.github.io/20171211/41-apm.html</id>
    <published>2017-12-11T02:00:36.000Z</published>
    <updated>2024-04-23T01:48:15.034Z</updated>
    
    <content type="html"><![CDATA[<h1 id="候选APM"><a href="#候选APM" class="headerlink" title="候选APM"></a>候选APM</h1><ul><li><a href="https://github.com/naver/pinpoint" target="_blank" rel="noopener">naver/pinpoint</a>(github上2148个star)</li></ul><p>韩国的一个公司开源的，有待评估使用情况，就是整体还不是JDK8，有些还是有点费劲，技术上采用agent的方式，对java友好</p><ul><li><a href="https://github.com/dianping/cat" target="_blank" rel="noopener">大众点评cat</a>(github上1725个star)</li></ul><p>看接入的公司还是挺多的，个人感觉是点评名气还可以，但是搭建起来有点费劲，很多东西都写死配置了，不灵活。整体设计的话，由于没有采用agent的方式，采用的是api手工埋点的方式，跟SNG的很像，好处的是跨语言，不好的地方就是对java来说用起来还需要包装一下</p><ul><li><a href="https://github.com/wu-sheng/sky-walking" target="_blank" rel="noopener">sky-walking</a>(github上374个star)</li></ul><p>开发团队加入了OneAPM,目前看使用的公司不多，整体技术采用agent方式，对java友好。提供了对dubbo等的支持，属于soa时代的产品</p><h1 id="技术架构"><a href="#技术架构" class="headerlink" title="技术架构"></a>技术架构</h1><h2 id="pinpoint"><a href="#pinpoint" class="headerlink" title="pinpoint"></a>pinpoint</h2><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/apm/1.png" alt title>                </div>                <div class="image-caption"></div>            </figure><h2 id="CAT"><a href="#CAT" class="headerlink" title="CAT"></a>CAT</h2><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/apm/2.png" alt title>                </div>                <div class="image-caption"></div>            </figure><h2 id="skywalking"><a href="#skywalking" class="headerlink" title="skywalking"></a>skywalking</h2><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/apm/3.png" alt title>                </div>                <div class="image-caption"></div>            </figure><h2 id="简要评价"><a href="#简要评价" class="headerlink" title="简要评价"></a>简要评价</h2><p>从技术架构上看，对于log的存储都使用了hbase，也都是自己实现了日志/监控数据的上报。pinpoint支持udp的方式，这个好一点。这类还是有点SOA时代的痕迹，更为符合大数据时代的做法是，监控数据丢给kafka，然后监控server来消费数据即可，这一点在cat中使用了consumer有点这个味道，但是没有彻底转型过来。</p><h2 id="展望"><a href="#展望" class="headerlink" title="展望"></a>展望</h2><p>APM整体的功能结构，主要是 1.日志追踪，2.监控报警 3.性能统计。对于日志追踪，已经有spirng cloud zipkin了，这个对spring cloud体系结合的很好，确的就是监控报警和性能统计，可以采用agent的方式进行无侵入的监控，或者采用log appender的方式到kafka，之后再进行error的监控报警，以及把performance的数据log到日志，发送到kafka来进行统计。</p><h2 id="docs"><a href="#docs" class="headerlink" title="docs"></a>docs</h2><ul><li><a href="https://github.com/naver/pinpoint" target="_blank" rel="noopener">pinpoint</a></li><li><a href="http://blog.csdn.net/szwandcj/article/details/51025669" target="_blank" rel="noopener">大众点评Cat–架构分析</a></li><li><a href="http://www.infoq.com/cn/articles/distributed-real-time-monitoring-and-control-system" target="_blank" rel="noopener">透过CAT，来看分布式实时监控系统的设计与实现</a></li><li><a href="https://github.com/wu-sheng/sky-walking" target="_blank" rel="noopener">sky-walking</a></li></ul><p>转自原文地址：<a href="https://segmentfault.com/a/1190000006817114" target="_blank" rel="noopener">https://segmentfault.com/a/1190000006817114</a></p>]]></content>
    
    <summary type="html">
    
      Java开源APM概要
    
    </summary>
    
      <category term="APM" scheme="https://ningyu1.github.io/categories/APM/"/>
    
    
      <category term="APM" scheme="https://ningyu1.github.io/tags/APM/"/>
    
      <category term="pinpoint" scheme="https://ningyu1.github.io/tags/pinpoint/"/>
    
      <category term="cat" scheme="https://ningyu1.github.io/tags/cat/"/>
    
      <category term="sky walking" scheme="https://ningyu1.github.io/tags/sky-walking/"/>
    
  </entry>
  
  <entry>
    <title>跨库分页-架构技术实践</title>
    <link href="https://ningyu1.github.io/20171124/40-distributed-db-paging.html"/>
    <id>https://ningyu1.github.io/20171124/40-distributed-db-paging.html</id>
    <published>2017-11-24T10:00:36.000Z</published>
    <updated>2024-04-23T01:48:15.034Z</updated>
    
    <content type="html"><![CDATA[<p>文章来源：<a href="http://gitbook.cn/books/58a98f512bd83c246b6b8866/index.html" target="_blank" rel="noopener">http://gitbook.cn/books/58a98f512bd83c246b6b8866/index.html</a><br>作者：@58沈剑<br>说明：文章转自沈老板的文章，分析的很不错</p><h1 id="一、需求缘起"><a href="#一、需求缘起" class="headerlink" title="一、需求缘起"></a>一、需求缘起</h1><h2 id="分页需求"><a href="#分页需求" class="headerlink" title="分页需求"></a>分页需求</h2><p>互联网很多业务都有分页拉取数据的需求，例如： </p><ul><li>微信消息过多时，拉取第N页消息。</li><li>京东下单过多时，拉取第N页订单。</li><li>浏览58同城，查看第N页帖子。<br>这些业务场景对应的消息表，订单表，帖子表分页拉取需求有这样一些特点： </li><li>有一个业务主键id，例如msg_id，order_id，tiezi_id</li><li>分页排序是按照非业务主键id来排序的，业务中经常按照时间time来排序order by</li></ul><p>在数据量不大时，可以通过在排序字段time上建立索引，利用SQL提供的offset/limit功能就能满足分页查询需求： </p><pre><code>select * from t_msg order by time offset 200 limit 100   select * from t_order order by time offset 200 limit 100   select * from t_tiezi order by time offset 200 limit 100</code></pre><p>此处假设一页数据为100条，均拉取第3页数据。</p><h2 id="分库需求"><a href="#分库需求" class="headerlink" title="分库需求"></a>分库需求</h2><p>高并发大流量的互联网架构，一般通过服务层来访问数据库，随着数据量的增大，数据库需要进行水平切分，分库后将数据分布到不同的数据库实例（甚至物理机器）上，以达到降低数据量，增加实例数的扩容目的。 </p><p>一旦涉及分库，逃不开“分库依据”patition key的概念，使用哪一个字段来水平切分数据库呢：大部分的业务场景，会使用业务主键id。 </p><p>确定了分库依据patition key后，接下来要确定的是分库算法：大部分的业务场景，会使用业务主键id取模的算法来分库，这样即能够保证每个库的数据分布是均匀的，又能够保证每个库的请求分布是均匀的，实在是简单实现负载均衡的好方法，此法在互联网架构中应用颇多。 </p><p>举一个更具体的例子： </p><p><img src="/img/rdb-paging/1.png" alt></p><p>用户库user，水平切分后变为两个库，分库依据patition key是uid，分库算法是uid取模：uid%2余0的数据会落到db0，uid%2余1的数据会落到db1。 </p><h2 id="问题的提出"><a href="#问题的提出" class="headerlink" title="问题的提出"></a>问题的提出</h2><p>仍然是上述用户库的例子，如果业务要查询“最近注册的第3页用户”，该如何实现呢？单库上，可以<code>select * from t_user order by time offset 200 limit 100</code>，变成两个库后，分库依据是uid，排序依据是time，数据库层失去了time排序的全局视野，数据分布在两个库上，此时该怎么办呢？ </p><p>如何满足“跨越多个水平切分数据库，且分库依据与排序依据为不同属性，并需要进行分页”的查询需求，实现<code>select*from T order by time offset X limit Y</code>的跨库分页SQL，是本文将要讨论的技术问题。 </p><h1 id="二、全局视野法"><a href="#二、全局视野法" class="headerlink" title="二、全局视野法"></a>二、全局视野法</h1><p><img src="/img/rdb-paging/2.png" alt></p><p>如上图所述，服务层通过uid取模将数据分布到两个库上去之后，每个数据库都失去了全局视野，数据按照time局部排序之后，不管哪个分库的第3页数据，都不一定是全局排序的第3页数据。 </p><p>那到底哪些数据才是全局排序的第3页数据呢，暂且分三种情况讨论。 </p><p>（1）极端情况，两个库的数据完全一样 </p><p><img src="/img/rdb-paging/3.png" alt></p><p>如果两个库的数据完全相同，只需要每个库offset一半，再取半页，就是最终想要的数据（如上图中粉色部分数据）。</p><p>（2）极端情况，结果数据来自一个库 </p><p><img src="/img/rdb-paging/4.png" alt></p><p>也可能两个库的数据分布及其不均衡，例如db0的所有数据的time都大于db1的所有数据的time，则可能出现：一个库的第3页数据，就是全局排序后的第3页数据（如上图中粉色部分数据）。 </p><p>（3）一般情况，每个库数据各包含一部分 </p><p><img src="/img/rdb-paging/5.png" alt></p><p>正常情况下，全局排序的第3页数据，每个库都会包含一部分（如上图中粉色部分数据）。 </p><p>由于不清楚到底是哪种情况，所以必须每个库都返回3页数据，所得到的6页数据在服务层进行内存排序，得到数据全局视野，再取第3页数据，便能够得到想要的全局分页数据。 </p><p>再总结一下这个方案的步骤： </p><ul><li>将<code>order by time offset X limit Y</code>，改写成<code>order by time offset 0 limit X+Y</code>。</li><li>服务层将改写后的SQL语句发往各个分库：即例子中的各取3页数据。</li><li>假设共分为N个库，服务层将得到N*(X+Y)条数据：即例子中的6页数据。</li><li>服务层对得到的N*(X+Y)条数据进行内存排序，内存排序后再取偏移量X后的Y条记录，就是全局视野所需的一页数据。</li></ul><p>方案优点：通过服务层修改SQL语句，扩大数据召回量，能够得到全局视野，业务无损，精准返回所需数据。 </p><p>方案缺点（显而易见）： </p><ul><li>每个分库需要返回更多的数据，增大了网络传输量（耗网络）；</li><li>除了数据库按照time进行排序，服务层还需要进行二次排序，增大了服务层的计算量（耗CPU）；</li><li>最致命的，这个算法随着页码的增大，性能会急剧下降，这是因为SQL改写后每个分库要返回X+Y行数据：返回第3页，offset中的X=200；假如要返回第100页，offset中的X=9900，即每个分库要返回100页数据，数据量和排序量都将大增，性能平方级下降。</li></ul><h1 id="三、业务折衷法"><a href="#三、业务折衷法" class="headerlink" title="三、业务折衷法"></a>三、业务折衷法</h1><p>“全局视野法”虽然性能较差， 但其业务无损，数据精准，不失为一种方案，有没有性能更优的方案呢？ </p><p>“任何脱离业务的架构设计都是耍流氓”，技术方案需要折衷，在技术难度较大的情况下，业务需求的折衷能够极大的简化技术方案。 </p><h2 id="业务折衷一：禁止跳页查询"><a href="#业务折衷一：禁止跳页查询" class="headerlink" title="业务折衷一：禁止跳页查询"></a>业务折衷一：禁止跳页查询</h2><p>在数据量很大，翻页数很多的时候，很多产品并不提供“直接跳到指定页面”的功能，而只提供“下一页”的功能，这一个小小的业务折衷，就能极大的降低技术方案的复杂度。 </p><p><img src="/img/rdb-paging/6.png" alt></p><p>如上图，不能够跳页，那么第一次只能够查询第一页： </p><ul><li>将查询<code>order by time offset 0 limit 100</code>，改写成<code>order by time where time&gt;0 limit 100</code>。</li><li>上述改写和offset 0 limit 100的效果相同，都是每个分库返回了一页数据（上图中粉色部分）。</li></ul><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/rdb-paging/7.png" alt title>                </div>                <div class="image-caption"></div>            </figure><ul><li>服务层得到2页数据，内存排序，取出前100条数据，作为最终的第一页数据，这个全局的第一页数据，一般来说每个分库都包含一部分数据（如上图粉色部分）。</li></ul><p>咦，这个方案也需要服务器内存排序，岂不是和“全局视野法”一样么？第一页数据的拉取确实一样，但每一次“下一页”拉取的方案就不一样了。 </p><p>点击“下一页”时，需要拉取第二页数据，在第一页数据的基础之上，能够找到第一页数据time的最大值： </p><p><img src="/img/rdb-paging/8.png" alt></p><p>这个上一页记录的time_max，会作为第二页数据拉取的查询条件： </p><ul><li>将查询<code>order by time offset 100 limit 100</code>，改写成<code>order by time where time&gt;$time_max limit 100</code>。</li></ul><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/rdb-paging/9.png" alt title>                </div>                <div class="image-caption"></div>            </figure><ul><li>这下不是返回2页数据了（“全局视野法，会改写成offset 0 limit 200”），每个分库还是返回一页数据（如上图中粉色部分）。</li></ul><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/rdb-paging/10.png" alt title>                </div>                <div class="image-caption"></div>            </figure><ul><li>服务层得到2页数据，内存排序，取出前100条数据，作为最终的第2页数据，这个全局的第2页数据，一般来说也是每个分库都包含一部分数据（如上图粉色部分）。</li></ul><p>如此往复，查询全局视野第100页数据时，不是将查询条件改写为offset 0 limit 9900+100（返回100页数据），而是改写为time&gt;$time_max99 limit 100（仍返回一页数据），以保证数据的传输量和排序的数据量不会随着不断翻页而导致性能下降。 </p><h2 id="业务折衷二：允许数据精度损失"><a href="#业务折衷二：允许数据精度损失" class="headerlink" title="业务折衷二：允许数据精度损失"></a>业务折衷二：允许数据精度损失</h2><p>“全局视野法”能够返回业务无损的精确数据，在查询页数较大，例如第100页时，会有性能问题，此时业务上是否能够接受，返回的100页不是精准的数据，而允许有一些数据偏差呢？ </p><p>数据库分库-数据均衡原理 </p><p>使用patition key进行分库，在数据量较大，数据分布足够随机的情况下，各分库所有非patition key属性，在各个分库上的数据分布，统计概率情况应该是一致的。 </p><p>例如，在uid随机的情况下，使用uid取模分两库，db0和db1： </p><ul><li>性别属性，如果db0库上的男性用户占比70%，则db1上男性用户占比也应为70%；</li><li>年龄属性，如果db0库上18-28岁少女用户比例占比15%，则db1上少女用户比例也应为15%；</li><li>时间属性，如果db0库上每天10:00之前登录的用户占比为20%，则db1上应该是相同的统计规律；</li><li>…</li></ul><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/rdb-paging/11.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p>利用这一原理，要查询全局100页数据，offset 9900 limit 100改写为offset 4950 limit 50，每个分库偏移4950（一半），获取50条数据（半页），得到的数据集的并集，基本能够认为，是全局数据的offset 9900 limit 100的数据，当然，这一页数据的精度，并不是精准的。 </p><p>根据实际业务经验，用户都要查询第100页网页、帖子、邮件的数据了，这一页数据的精准性损失，业务上往往是可以接受的，但此时技术方案的复杂度便大大降低列，既不需要返回更多的数据，也不需要进行服务内存排序了。 </p><h1 id="四、终极武器：二次查询法"><a href="#四、终极武器：二次查询法" class="headerlink" title="四、终极武器：二次查询法"></a>四、终极武器：二次查询法</h1><p>有没有一种技术方案，即能够满足业务的精确需要，无需业务折衷，又高性能的方法呢？这就是接下来要介绍的终极武器：“二次查询法”。 </p><p>为了方便举例，假设一页只有5条数据，查询第200页的SQL语句为<code>select*from T order by time offset 1000 limit 5</code>。 </p><h1 id="步骤一：查询改写"><a href="#步骤一：查询改写" class="headerlink" title="步骤一：查询改写"></a>步骤一：查询改写</h1><p>将<code>select*from T order by time offset 1000 limit 5</code>改写为<code>select*from T order by time offset 500 limit 5</code>并投递给所有的分库，注意，这个<code>offset</code>的500，来自于全局<code>offset</code>的总偏移量1000，除以水平切分数据库个数2。 </p><p>如果是3个分库，则可以改写为<code>select*from T order by time offset 333 limit 5</code>，假设这三个分库返回的数据(time, uid)如下： </p><p><img src="/img/rdb-paging/12.png" alt></p><p>可以看到，每个分库都是返回的按照time排序的一页数据。 </p><h2 id="步骤二：找到所返回3页全部数据的最小值"><a href="#步骤二：找到所返回3页全部数据的最小值" class="headerlink" title="步骤二：找到所返回3页全部数据的最小值"></a>步骤二：找到所返回3页全部数据的最小值</h2><ul><li>第一个库，5条数据的time最小值是1487501123</li><li>第二个库，5条数据的time最小值是1487501133</li><li>第三个库，5条数据的time最小值是1487501143</li></ul><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/rdb-paging/13.png" alt title>                </div>                <div class="image-caption"></div>            </figure><p>故，三页数据中，time最小值来自第一个库，time_min=1487501123，这个过程只需要比较各个分库第一条数据，时间复杂度很低。 </p><h2 id="步骤三：查询二次改写"><a href="#步骤三：查询二次改写" class="headerlink" title="步骤三：查询二次改写"></a>步骤三：查询二次改写</h2><p>第一次改写的SQL语句是<code>select*from T order by time offset 333 limit 5</code>。第二次要改写成一个between语句，between的起点是time_min，between的终点是原来每个分库各自返回数据的最大值： </p><p>第一个分库，第一次返回数据的最大值是1487501523；所以查询改写为<code>select*from T order by time where time between time_min and 1487501523</code>。 </p><p>第二个分库，第一次返回数据的最大值是1487501323；所以查询改写为<code>select*from T order by time where time between time_min and 1487501323</code>。 </p><p>第三个分库，第一次返回数据的最大值是1487501553；所以查询改写为<code>select*from T order by time where time between time_min and 1487501553</code>。 </p><p>相对第一次查询，第二次查询条件放宽了，故第二次查询会返回比第一次查询结果集更多的数据，假设这三个分库返回的数据(time, uid)如下： </p><p><img src="/img/rdb-paging/14.png" alt></p><p>可以看到： </p><ul><li>由于time_min来自原来的分库一，所以分库一的返回结果集和第一次查询相同（所以其实这次查询是可以省略的）；</li><li>分库二的结果集，比第一次多返回了1条数据，头部的1条记录（time最小的记录）是新的（上图中粉色记录）；</li><li>分库三的结果集，比第一次多返回了2条数据，头部的2条记录（time最小的2条记录）是新的（上图中粉色记录）。</li></ul><h2 id="步骤四：在每个结果集中虚拟一个time-min记录，找到time-min在全局的offset"><a href="#步骤四：在每个结果集中虚拟一个time-min记录，找到time-min在全局的offset" class="headerlink" title="步骤四：在每个结果集中虚拟一个time_min记录，找到time_min在全局的offset"></a>步骤四：在每个结果集中虚拟一个time_min记录，找到time_min在全局的offset</h2><p><img src="/img/rdb-paging/15.png" alt></p><ul><li>在第一个库中，time_min在第一个库的offset是333；</li><li>在第二个库中，(1487501133, uid_aa)的offset是333（根据第一次查询条件得出的），故虚拟time_min在第二个库的offset是331；</li><li>在第三个库中，(1487501143, uid_aaa)的offset是333（根据第一次查询条件得出的），故虚拟time_min在第三个库的offset是330。</li></ul><p>综上，time_min在全局的offset是333+331+330=994。 </p><p>步骤五：既然得到了time_min在全局的offset，就相当于有了全局视野，根据第二次的结果集，就能够得到全局offset 1000 limit 5的记录 </p><p><img src="/img/rdb-paging/16.png" alt></p><p>第二次查询在各个分库返回的结果集是有序的，又知道了time_min在全局的offset是994，一路排下来，容易知道全局offset 1000 limit 5的一页记录（上图中黄色记录）。 </p><p>是不是非常巧妙？这种方法的优点是：可以精确的返回业务所需数据，每次返回的数据量都非常小，不会随着翻页增加数据的返回量。 </p><p>不足是：需要进行两次数据库查询。 </p><h1 id="五、总结"><a href="#五、总结" class="headerlink" title="五、总结"></a>五、总结</h1><p>今天分享了解决“夸N库分页”这一技术难题的四种方法，稍作总结： </p><p>方法一：全局视野法 </p><ul><li>将<code>order by time offset X limit Y</code>，改写成<code>order by time offset 0 limit X+Y</code>。</li><li>服务层对得到的N*(X+Y)条数据进行内存排序，内存排序后再取偏移量X后的Y条记录。</li></ul><p>方法二：业务折衷法-禁止跳页查询 </p><ul><li>用正常的方法取得第一页数据，并得到第一页记录的time_max。</li><li>每次翻页，将<code>order by time offset X limit Y</code>，改写成<code>order by time where time&gt;$time_max limit Y</code>以保证每次只返回一页数据，性能为常量。</li></ul><p>方法三：业务折衷法-允许模糊数据</p><ul><li>将<code>order by time offset X limit Y</code>，改写成<code>order by time offset X/N limit Y/N</code>。</li></ul><p>方法四：二次查询法 </p><ul><li>将<code>order by time offset X limit Y</code>，改写成<code>order by time offset X/N limit Y</code>；</li><li>找到最小值time_min；</li><li>between二次查询，<code>order by time between $$time_min and $time_i_max</code>；</li><li>设置虚拟time_min，找到time_min在各个分库的offset，从而得到time_min在全局的offset；</li><li>得到了time_min在全局的offset，自然得到了全局的offset X limit Y。</li></ul>]]></content>
    
    <summary type="html">
    
      跨库分页-架构技术实践
    
    </summary>
    
      <category term="跨库分页" scheme="https://ningyu1.github.io/categories/%E8%B7%A8%E5%BA%93%E5%88%86%E9%A1%B5/"/>
    
    
      <category term="rdb-paging" scheme="https://ningyu1.github.io/tags/rdb-paging/"/>
    
  </entry>
  
  <entry>
    <title>BTrace使用笔记</title>
    <link href="https://ningyu1.github.io/20171115/39-btrace.html"/>
    <id>https://ningyu1.github.io/20171115/39-btrace.html</id>
    <published>2017-11-15T03:00:36.000Z</published>
    <updated>2024-04-23T01:48:15.034Z</updated>
    
    <content type="html"><![CDATA[<h2 id="BTrace是什么？"><a href="#BTrace是什么？" class="headerlink" title="BTrace是什么？"></a>BTrace是什么？</h2><p>Btrace是由sundararajan在2009年6月开发的一个开源项目，是一种动态跟踪分析一个运行中的Java应用程序的工具。<br>BTrace是一个为Java平台开发的安全、动态的追踪工具。BTrace动态地向目标应用程序的字节码注入追踪代码（字节码追踪），这些追踪字节码追踪代码使用Java语言表达，也就是BTrace的脚本。</p><h2 id="BTrace能做什么？"><a href="#BTrace能做什么？" class="headerlink" title="BTrace能做什么？"></a>BTrace能做什么？</h2><p>BTrace可以用来帮我们做运行时的JAVA程序分析，监控等等操作，BTrace也有一些使用上的限制，如：不能在脚本中新建类等。<br>Btrace是通过Attach API中提供的VirtualMachine.attach(PID)方法来获得要监控的JVM，然后使用VirtualMachine.loadAgent(“*.jar”)方法来加载jar文件。</p><h2 id="特别注意"><a href="#特别注意" class="headerlink" title="特别注意"></a>特别注意</h2><p><span style="color:red"><strong>BTrace植入过的代码，会一直在，直到应用重启为止。所以即使Btrace退出了，业务函数每次执行时都会执行Btrace植入的代码</strong></span></p><h2 id="Btrace术语"><a href="#Btrace术语" class="headerlink" title="Btrace术语"></a>Btrace术语</h2><p><strong>Probe Point(探测点)</strong><br>追踪语句（或者一组追踪语句）被触发执行的“位置”或“事件”。也就是我们想要执行一些追踪语句的“位置”或“事件”。<br><strong>Trace Actions or Actions（追踪动作）</strong><br>probe被触发时，执行的追踪语句。<br><strong>Action Methods（动作方法）</strong><br>我的理解是定义追踪动作的方法，当然根据官方的说明这个方法应该是静态的。<br>在静态方法中定义probe触发所调用的trace语句，那么这种定义了trace脚本的静态方法就是”动作方法”</p><h2 id="BTrace程序结构"><a href="#BTrace程序结构" class="headerlink" title="BTrace程序结构"></a>BTrace程序结构</h2><p>一个BTrace程序是其实就是一个普通的java类，特别之处就是由一个或者多个被(public static void)组合修饰的方法并且这些方法被BTrace对应的annotations注解。注解用来指出被追踪程序的位置（probe point）。追踪动作须书写在静态方法体中，也就是action方法（可以有多个action方法）。</p><h2 id="BTrace约束"><a href="#BTrace约束" class="headerlink" title="BTrace约束"></a>BTrace约束</h2><p>为了保证追踪动作是“只读”的（也就是这些动作不可以修改被追踪程序的状态）和有限度的（比如在固定时间里结束）。一个BTrace程序只允许完成一些指定的动作。下面是BTrace一些不可以完成的事情：</p><ul><li>不能创建新的对象</li><li>不能创建新的数组</li><li>不能抛出异常</li><li>不能捕获异常</li><li>不能进行任何的实例函数或者静态函数 – 只有com.sun.btrace.BTraceUtils类中的静态函数或者BTrace程序自己声明的函数才可以被BTrace调用</li><li>不可以在目标程序的类，或者对象的静态或者实例级别的field进行赋值。但是，BTrace自身的类是可以给它的静态field进行赋值的</li><li>不能有outer，inner,嵌套的或者本地类。</li><li>不能有同步代码块或者同步的函数</li><li>不能有循环语句（for,while, do..while）</li><li>不能继承其它类（父类只能是java.lang.Object）</li><li>不能实现接口</li><li>不能包含断言(assert)语句</li><li>不能使用类字面值</li></ul><p>这上面的种种限制可以通过一个配置改变：unsafe=true，在使用BTrace注解时修改该属性的默认值（false）为true，即@BTrace（unsafe=true）；也可以启动选项中显式声明-Dcom.sun.btrace.unsafe=true（响应也有-u参数）；现在你可以为所欲为了。<span style="color:red"><strong>BUT，这样做之前最好考虑好风险并再三检查脚本，请斟酌使用！</strong></span></p><h2 id="BTrace安装"><a href="#BTrace安装" class="headerlink" title="BTrace安装"></a>BTrace安装</h2><p><a href="https://github.com/btraceio/btrace" title="btrace-gitbub" target="_blank" rel="noopener">btrace</a> git下载地址<br>下载下来直接解压就可以使用</p><h2 id="基本语法"><a href="#基本语法" class="headerlink" title="基本语法"></a>基本语法</h2><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">btrace &lt;pid&gt; &lt;btrace-script&gt;脚本</span><br></pre></td></tr></table></figure><p>btrace命令行工具运行命令如下：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">btrace &lt;options&gt; &lt;pid&gt; &lt;btrace source or .class file&gt; &lt;btrace arguments&gt;</span><br><span class="line">常用选项：</span><br><span class="line">[-I &lt;include-path&gt;] [-p &lt;port&gt;] [-cp &lt;classpath&gt;]</span><br></pre></td></tr></table></figure><p>参数说明：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">where possible options include:</span><br><span class="line">  --version             Show the version</span><br><span class="line">  -v                    Run in verbose mode</span><br><span class="line">  -o &lt;file&gt;             The path to store the probe output (will disable showing the output in console)</span><br><span class="line">  -u                    Run in trusted mode</span><br><span class="line">  -d &lt;path&gt;             Dump the instrumented classes to the specified path</span><br><span class="line">  -pd &lt;path&gt;            The search path for the probe XML descriptors</span><br><span class="line">  -classpath &lt;path&gt;     Specify where to find user class files and annotation processors</span><br><span class="line">  -cp &lt;path&gt;            Specify where to find user class files and annotation processors</span><br><span class="line">  -I &lt;path&gt;             Specify where to find include files</span><br><span class="line">  -p &lt;port&gt;             Specify port to which the btrace agent listens for clients</span><br><span class="line">  -statsd &lt;host[:port]&gt; Specify the statsd server, if any</span><br></pre></td></tr></table></figure><ul><li><strong>include-path</strong> : 是一些用来查找头文件的目录。BTrace包含一个简单的预处理,支持# define,# + include和条件编译。它不像一个完整的C / c++预处理器–而是一个有用的子集。详见demo代码“ThreadBean.java”，如果没有显式的声明选项-I，Btrace跳过预处理程序调用步骤。</li><li><strong>port</strong>： BTrace代理程序所侦听的端口，这是可选的选项。默认是2020</li><li><strong>classpath</strong>: 是一些用来查找jar文件的目录。默认是当前目录”.”</li><li><strong>pid</strong>：是要追踪目标程序id</li><li><strong>btrace-script</strong>: 就是追踪程序本身。如果这是个java文件，那么提交前会进行编译。否则,它被认为已预编译(即它必须是一个类)并提交</li><li><strong>arguments</strong>: 这是传递给BTrace程序的参数。BTrace程序可以通过内置的符号来引用这些参数，length是这些参数的个数。</li></ul><p>在samples目录下有很多示例，并且有的跟踪很有用可直接使用，下来让我们编写一个脚本来看一下具体是怎么使用的</p><h2 id="BTrace的注解"><a href="#BTrace的注解" class="headerlink" title="BTrace的注解"></a>BTrace的注解</h2><h3 id="方法注解"><a href="#方法注解" class="headerlink" title="方法注解"></a>方法注解</h3><ul><li><a href="mailto:**@com.sun.btrace.annotations.OnMethod" target="_blank" rel="noopener">**@com.sun.btrace.annotations.OnMethod</a>** 该注解可用来指定目标类，目标方法，以及目标方法里的“位置”。加了该注解后的操作方法会在对应的方法运行到指定的“位置”时被执行。这该注解中，目标类用“clazz”属性来指定，而目标方法用“method”属性来指定。”clazz”可以是类的全路径(比如java.awt.Component或者用两个反斜杠中间的正则表达式，参考例子NewComponent.java和Classload.java来看它们的用法，正则表达式可以匹配0个或多个目标类，这个时候多个类都会被进行动态指令更换。如/java.awt.+/匹配java.awt包下的所有类)。方法名也可以用这样的正则表达式 来匹配零个或者多个多个方法。参考例子MultiClass.java来查看用法。 还有一种方法来指定追踪类和函数。被追踪的类和函数可以用注解来指定。比如，如果”clazz”属性是@javax.jws.Webservice.那么BTrace会会把所有注解是这个的函数都进行动态指令更换。类似地，方法级别的注解也可以用来执行方法。参看例子WebServiceTracker.java来了解如何使用。可以把正则表达式和注解放在一起用，比如@/com.acme..+/可以匹配任何类，只要这个类的注解能跟那段正则表达式匹配即可。可以通过指定父类来匹配多个类名，比如+java.lang.Runnable就可以匹配所有实现了java.lang.Runnable这个接口的类。参考例子SubtypeTracer.java来看它的用法。</li><li><a href="mailto:**@com.sun.btrace.annotations.OnTimer" target="_blank" rel="noopener">**@com.sun.btrace.annotations.OnTimer</a>** 该注解可以用来执行那些需要周期性（间隔是毫秒）的追踪操作。参考Histogram.java来看它的用法。</li><li><a href="mailto:**@com.sun.btrace.annotations.OnError" target="_blank" rel="noopener">**@com.sun.btrace.annotations.OnError</a>** 该注解可以用来指定当任何异常抛出时需要执行的操作。被该注解修饰后的BTrace函数会在同一个BTrace类的其他操作方法抛出异常时执行。</li><li><a href="mailto:**@com.sun.btrace.annotations.OnExit" target="_blank" rel="noopener">**@com.sun.btrace.annotations.OnExit</a>** 该注解用来执行党BTrace代码调用了exit(int)结束追踪会话后需要执行的操作。参考例子ProbeExit.java来了解如何使用。</li><li><a href="mailto:**@com.sun.btrace.annotations.OnEvent" target="_blank" rel="noopener">**@com.sun.btrace.annotations.OnEvent</a>** 该注解用来追踪函数与”外部”的事件关联起来。当BTrace客户端发送了一个“事件”后，该注解里的操作就会被执行。客户端发送的事件可能是由用户触发的（比如按下Ctrl-C）。事件的名字是个字符串，这样追踪操作就只会在对应的事件触发后被执行。到目标为止，BTrace命令行客户端会在用户按下Ctrl-C后发送事件，参考例子HistoOnEvent.java来了解用法。</li><li><a href="mailto:**@com.sun.btrace.annotations.OnLowMemory" target="_blank" rel="noopener">**@com.sun.btrace.annotations.OnLowMemory</a>** 该注解可以用来追踪特定内存阈值被用光的事件。参看例子MemAlerter.java了解用法。</li><li><a href="mailto:**@com.sun.btrace.annotations.OnProbe" target="_blank" rel="noopener">**@com.sun.btrace.annotations.OnProbe</a>** 该注解可以用来避免使用BTrace脚本的内部类。@OnProbe探测点被映射到一个或多个@OnMethod上。目前这个映射是通过一个XML探测描述文件类指定的（这个文件会被BTrace代理所使用）。参考例子SocketTracker1.java和对应的描述文件java.net.socket.xml.当运行这个例子时，xml文件需要放在目标JVM所有运行的目录下(或者修改btracer.bat中的probeDescPath选项来指向任意的xml文件)。</li><li><a href="mailto:**@com.sun.btrace.annotations.Location" target="_blank" rel="noopener">**@com.sun.btrace.annotations.Location</a>**：该注解在一个traced/probed方法中指定一个特定的“位置”</li><li><a href="mailto:**@com.sun.btrace.annotations.Simpled" target="_blank" rel="noopener">**@com.sun.btrace.annotations.Simpled</a>**：标记@OnMethod注解处理器采样。采样处理程序时并不是所有的事件将被追踪,只有一个统计样品与给定的意思。在默认情况下使用一种自适应采样。BTrace将增加或减少样品之间的调用数量保持平均时间窗口,因此减少整体的开销。</li></ul><h3 id="参数相关的注解"><a href="#参数相关的注解" class="headerlink" title="参数相关的注解"></a>参数相关的注解</h3><ul><li><a href="mailto:**@com.sun.btrace.annotations.Self" target="_blank" rel="noopener">**@com.sun.btrace.annotations.Self</a>**：该注解把一个参数标识为保留了目标函数所指向的this的值。参考例子AWTEventTracer.java和AllCalls1.java.</li><li><a href="mailto:**@com.sun.btrace.annotations.Return" target="_blank" rel="noopener">**@com.sun.btrace.annotations.Return</a>**：该注解说明这个参数保存目标函数的返回值。参考例子Classload.java</li><li><a href="mailto:**@com.sun.btrace.annotations.ProbeClassName" target="_blank" rel="noopener">**@com.sun.btrace.annotations.ProbeClassName</a>**：所修饰的参数保留了探测类的类名 。参看AllMethods.java（有多个探测类）</li><li><a href="mailto:**@com.sun.btrace.annotations.ProbeMethodName" target="_blank" rel="noopener">**@com.sun.btrace.annotations.ProbeMethodName</a>**：所修饰的参数保留了探测函数的函数名。参考WebServiceTracker.java（多个探测函数）</li><li><a href="mailto:**@com.sun.btrace.annotations.TargetInstance" target="_blank" rel="noopener">**@com.sun.btrace.annotations.TargetInstance</a>**：修饰的参数保留了被调用的实例。参考例子AllCall2.java.</li><li><a href="mailto:**@com.sun.btrace.annotations.TargetMethodOrField" target="_blank" rel="noopener">**@com.sun.btrace.annotations.TargetMethodOrField</a>**：该注解修饰的参数保存了调用的函数名。参考AllCalls1.java 和AllCall2.java</li><li><a href="mailto:**@com.sun.btrace.annotations.Duration" target="_blank" rel="noopener">**@com.sun.btrace.annotations.Duration</a>**：探测方法参数标记为持续时间值的接收者，即目标方法执行的时间，单位纳秒。只是用带Location属性的@OnMethod，并且需要配合Kind.ERROR或者Kind.RETURN使用</li></ul><h3 id="无注解的参数"><a href="#无注解的参数" class="headerlink" title="无注解的参数"></a>无注解的参数</h3><p>没有注解的<code>BTrace</code>探测函数参数是用来作签名匹配的，因为他们必须必须在固定的位置上出现。然而，它们可以和其他的注解的参数进行交换。如果一个参数的类型是<em>AnyType[]</em>，它就会“吃”掉所所有剩下的参数。没有注解的参数的具体含义与他们所在的位置有关：</p><table><thead><tr><th style="text-align:left">名称</th><th style="text-align:left">作用</th></tr></thead><tbody><tr><td style="text-align:left">Kind.ARRAY_GET</td><td style="text-align:left">数组元素加载</td></tr><tr><td style="text-align:left">Kind.ARRAY_SET</td><td style="text-align:left">数组元素存储</td></tr><tr><td style="text-align:left">Kind.CALL</td><td style="text-align:left">方法调用</td></tr><tr><td style="text-align:left">Kind.CATCH</td><td style="text-align:left">异常捕获</td></tr><tr><td style="text-align:left">Kind.CHECKCAST</td><td style="text-align:left">checkcast</td></tr><tr><td style="text-align:left">Kind.ENTRY</td><td style="text-align:left">方法进入。意指进入匹配probe点，跟你@Location设置的clazz和method没有任何关系</td></tr><tr><td style="text-align:left">Kind.ERROR</td><td style="text-align:left">错误，异常没有捕获，返回</td></tr><tr><td style="text-align:left">Kind.FIELD_GET</td><td style="text-align:left">field获取</td></tr><tr><td style="text-align:left">Kind.FIELD_SET</td><td style="text-align:left">field设置</td></tr><tr><td style="text-align:left">Kind.INSTANCEOF</td><td style="text-align:left">实例检测</td></tr><tr><td style="text-align:left">Kind.LINE</td><td style="text-align:left">源代码行号</td></tr><tr><td style="text-align:left">Kind.NEW</td><td style="text-align:left">创建新实例</td></tr><tr><td style="text-align:left">Kind.NEWARRAY</td><td style="text-align:left">新的数组对象被创建</td></tr><tr><td style="text-align:left">Kind.RETURN</td><td style="text-align:left">意指从某个匹配probe的方法中调用了匹配A class method的点，一定要和clazz,method配合使用。clazz和method的默认值为”“，所以不能被匹配</td></tr><tr><td style="text-align:left">Kind.SYNC_ENTRY</td><td style="text-align:left">进入一个同步方法锁</td></tr><tr><td style="text-align:left">Kind.SYNC_EXIT</td><td style="text-align:left">离开一个同步方法锁</td></tr><tr><td style="text-align:left">Kind.THROW</td><td style="text-align:left">抛出异常</td></tr></tbody></table><h3 id="字段相关的注解"><a href="#字段相关的注解" class="headerlink" title="字段相关的注解"></a>字段相关的注解</h3><ul><li><a href="mailto:**@com.sun.btrace.annotations.Export" target="_blank" rel="noopener">**@com.sun.btrace.annotations.Export</a>** BTrace字段使用该注解来说明它已经被映射到一个jvmstat计数器上。使用该注解，BTrace程序可以把追踪计数器暴露给外部的jvmstat客户端（比如jstat）。参考例子ThreadCounter.java</li><li><a href="mailto:**@com.sun.btrace.annotations.Property" target="_blank" rel="noopener">**@com.sun.btrace.annotations.Property</a>**该注解可以把一个字段标识为一个MBean属性。如果一个BTrace类至少有一个静态的字段使用了该注解。那么一个MBean就会被创建并且注册到平台MBean服务器上。JMX客户端比如VisualVM，jconsole可以访问这个字段来查看BTrace的MBean。在把BTrace附加到目标程序上后，你可以把VisualVM或者jconsole也附加到同一个目标程序上来查看刚创建好的MBean属性。通过VisualVM或者jconsole,你可以通过MBeans tab页来查看BTrace相关的域，然后查看它们的值。参考例子ThreadCounterBean.java 和HistogramBean.java来了解用法</li><li><a href="mailto:**@com.sun.btrace.annotations.TLS" target="_blank" rel="noopener">**@com.sun.btrace.annotations.TLS</a>** BTrace字段使用该注解来说明它自己是一个线程本地字段（thread local field）.注意你只能在@OnMethod注解后的函数里访问这样的字段。每个Java线程都有一个这个字段的拷贝。为了让这样的方式能够工作，这个字段的类型只能是immutable（比如原始类型） 或者是cloneable （实现了Cloneable接口并且覆盖了clone()函数）的。这些线程本地字段可以被BTrace程序用来识别它是否在同一个线程里执行了多个探测操作。参考例子OnThrow.java和WebServiceTracker.java</li></ul><h3 id="类相关的注解"><a href="#类相关的注解" class="headerlink" title="类相关的注解"></a>类相关的注解</h3><ul><li><a href="mailto:**@com.sun.btrace.annotations.DTrace" target="_blank" rel="noopener">**@com.sun.btrace.annotations.DTrace</a>**该注解用来把一小段D脚本（嵌在BTrace 的java类中）和BTrace程序关联起来。参考例子DTraceInline.java</li><li><a href="mailto:**@com.sun.btrace.annotations.DTraceRef" target="_blank" rel="noopener">**@com.sun.btrace.annotations.DTraceRef</a>** 和上个注解一样，不同的是D脚本是在独立的文件中，不是嵌在java类中。</li><li><a href="mailto:**@com.sun.btrace.annotations.BTrace" target="_blank" rel="noopener">**@com.sun.btrace.annotations.BTrace</a>**必须使用该注解来指定一个Java类是BTrace程序。BTrace编译器会强制查找该注解，BTrace代理也会检查这个是否有该注解。如果没有，则提示错误，并且不会执行。</li></ul><h2 id="脚本编写"><a href="#脚本编写" class="headerlink" title="脚本编写"></a>脚本编写</h2><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line"></span><br><span class="line">package btrace;</span><br><span class="line"></span><br><span class="line">import com.sun.btrace.BTraceUtils;</span><br><span class="line">import com.sun.btrace.annotations.*;</span><br><span class="line"></span><br><span class="line">@BTrace</span><br><span class="line">public class UniqueIdMgrBtrace &#123;</span><br><span class="line">    @OnMethod(clazz = &quot;com.atomikos.util.UniqueIdMgr&quot;, method = &quot;get&quot;, location = @Location(Kind.RETURN))</span><br><span class="line">    public static void onGet(@Return String result) &#123;</span><br><span class="line">        long millis = BTraceUtils.timeMillis();</span><br><span class="line">        String threadName = BTraceUtils.Threads.name(BTraceUtils.currentThread());</span><br><span class="line">        String str = BTraceUtils.strcat(BTraceUtils.str(millis), &quot; - [&quot;);</span><br><span class="line">        str = BTraceUtils.strcat(str, BTraceUtils.str(threadName));</span><br><span class="line">        str = BTraceUtils.strcat(str, &quot;] - com.atomikos.util.UniqueIdMgr.get()--&gt;&quot;);</span><br><span class="line">        str = BTraceUtils.strcat(str, BTraceUtils.str(result));</span><br><span class="line">        BTraceUtils.println(BTraceUtils.str(str));</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    @OnMethod(clazz = &quot;com.atomikos.icatch.imp.TransactionServiceImp&quot;, method = &quot;setTidToTx&quot;)</span><br><span class="line">    public static void onSetTidToTx(String tid) &#123;</span><br><span class="line">        long millis = BTraceUtils.timeMillis();</span><br><span class="line">        String threadName = BTraceUtils.Threads.name(BTraceUtils.currentThread());</span><br><span class="line">        String str = BTraceUtils.strcat(BTraceUtils.str(millis), &quot; - [&quot;);</span><br><span class="line">        str = BTraceUtils.strcat(str, BTraceUtils.str(threadName));</span><br><span class="line">        str = BTraceUtils.strcat(str, &quot;] - com.atomikos.icatch.imp.TransactionServiceImp.setTidToTx(&quot;);</span><br><span class="line">        str = BTraceUtils.strcat(str, BTraceUtils.str(tid));</span><br><span class="line">        str = BTraceUtils.strcat(str, &quot;)&quot;);</span><br><span class="line">        BTraceUtils.println(BTraceUtils.str(str));</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>上面代码意思是在<code>com.atomikos.util.UniqueIdMgr.get()</code>方法上面进行跟踪返回值，要跟踪赶回值必须要加<code>@Location(Kind.RETURN))</code>,才能使用参数的<code>@Return</code></p><p>如果要使用方法参数，可以在脚本方法上直接写跟踪的原始方法参数并且类型保持一样，例如：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">package com.btrace;</span><br><span class="line">//需要跟踪的类</span><br><span class="line">public class RemoteClass &#123;</span><br><span class="line"></span><br><span class="line">    public String f1(String a, int b) &#123;</span><br><span class="line">        System.out.println(a + &quot; &quot; + b);</span><br><span class="line">        return a;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">//btrace脚本</span><br><span class="line">@BTrace public class HelloBtrace &#123;</span><br><span class="line"></span><br><span class="line">  @OnMethod(</span><br><span class="line">    clazz=&quot;com.btrace.RemoteClass&quot;,</span><br><span class="line">    method=&quot;f1&quot;</span><br><span class="line">  ) </span><br><span class="line">  public static void onF1() &#123;</span><br><span class="line">    println(&quot;Hello BTrace&quot;);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  @OnMethod(</span><br><span class="line">    clazz=&quot;com.btrace.RemoteClass&quot;,</span><br><span class="line">    method=&quot;f1&quot;</span><br><span class="line">  ) </span><br><span class="line">  public static void onF2(String a,int b) &#123;</span><br><span class="line">    println(str(a));</span><br><span class="line">    println(str(b));</span><br><span class="line">    println(&quot;&quot;);</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><ol><li>脚本中方法参数需要跟原方法参数类型保持一致</li><li>脚本中不允许使用除btrace之外的类，拼接字符串使用<code>BTraceUtils.strcat()</code>,打印使用<code>BTraceUtils.println()</code>,获取线程使用<code>BTraceUtils.Threads</code></li><li><span style="color:red"><strong>BTrace植入过的代码，会一直在，直到应用重启为止。所以即使Btrace退出了，业务函数每次执行时都会执行Btrace植入的代码</strong></span></li></ol>]]></content>
    
    <summary type="html">
    
      BTrace使用笔记
    
    </summary>
    
      <category term="trace" scheme="https://ningyu1.github.io/categories/trace/"/>
    
      <category term="java" scheme="https://ningyu1.github.io/categories/trace/java/"/>
    
    
      <category term="btrace" scheme="https://ningyu1.github.io/tags/btrace/"/>
    
  </entry>
  
  <entry>
    <title>ActiveMQ发送速度慢问题排查</title>
    <link href="https://ningyu1.github.io/20171109/38-activemq-slow-speed.html"/>
    <id>https://ningyu1.github.io/20171109/38-activemq-slow-speed.html</id>
    <published>2017-11-09T09:00:36.000Z</published>
    <updated>2024-04-23T01:48:15.034Z</updated>
    
    <content type="html"><![CDATA[<h1 id="目录："><a href="#目录：" class="headerlink" title="目录："></a>目录：</h1><ol><li><a href="#sync-async-problem">关于使用发送消息给activemq的同步/异步发送问题需要注意</a></li><li><a href="#sync-async-scene">同步/异步发送使用场景</a></li><li><a href="#maxconnections">maxConnections配置问题注意事项</a></li><li><a href="#idletimeout">idleTimeout配置问题注意事项</a></li><li><a href="#failover">关于Failover的问题</a></li></ol><h2 id="关于使用发送消息给activemq的同步-异步发送问题需要注意"><a href="#关于使用发送消息给activemq的同步-异步发送问题需要注意" class="headerlink" title="关于使用发送消息给activemq的同步/异步发送问题需要注意"></a><a name="sync-async-problem">关于使用发送消息给activemq的同步/异步发送问题需要注意</a></h2><p>activemq发送异步参数：useAsyncSend与发送超时参数：sendTimeout是存在冲突的，</p><ol><li>当useAsyncSend=true，没有sendTimeout参数时（sendTimeout默认值0秒），走异步发送</li><li>当useAsyncSend=false，没有sendTimeout参数时（sendTimeout默认值0秒），走同步发送</li><li>当useAsyncSend=true，sendTimeout=1000，优先根据sendTimeout参数走同步发送</li></ol><h2 id="同步-异步发送使用场景"><a href="#同步-异步发送使用场景" class="headerlink" title="同步/异步发送使用场景"></a><a name="sync-async-scene">同步/异步发送使用场景</a></h2><p>场景一：业务可以容忍消息丢失（日志记录）这样的场景使用：<br>使用：异步发送<br>配置：useAsyncSend=true，sendTimeout不配置（sendTimeout默认值0秒）<br>注意：可以不需要补偿机制</p><p>场景二：业务不能容忍消息丢失，这样的场景使用：<br>使用1：异步发送<br>配置1：useAsyncSend=true，sendTimeout不配置（sendTimeout默认值0秒）<br>注意1：当异步发送消息失败或异常导致消息丢失时有补偿的做法（如：定时任务、重发消息、等）<br>使用2：同步发送<br>配置2：useAsyncSend=false（useAsyncSend默认值false），sendTimeout=2000（超时时间一定要配置）<br>注意2：可以不需要补偿机制</p><p>场景三：业务必须将消息发送和jdbc事务放在一个事务内，保证数据的强一致性，这样的场景使用：<br>使用：同步发送<br>配置：useAsyncSend=false（useAsyncSend默认值false），sendTimeout=2000（超时时间一定要配置）<br>注意：消息发送的超时时间（sendTimeout）&lt; jdbc事务超时时间</p><p>禁止使用的配置：<br>配置：useAsyncSend=false（useAsyncSend默认值false），sendTimeout不配置（sendTimeout默认值0秒）<br>注意：上面不配置超时时间的同步发送会造成请求阻塞在这里。</p><h2 id="maxConnections配置问题注意事项"><a href="#maxConnections配置问题注意事项" class="headerlink" title="maxConnections配置问题注意事项"></a><a name="maxconnections">maxConnections配置问题注意事项</a></h2><p>根据activemq的连接池实现代码，发现maxconnections不适合设置很大，除非并发非常高的情况下，因为现在activemq创建一个连接平均在1-2秒钟左右，根据activemq的连接实现发现</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">if (getConnectionsPool().getNumIdle(key) &lt; getMaxConnections()) &#123;</span><br><span class="line">            try &#123;</span><br><span class="line">                connectionsPool.addObject(key);</span><br><span class="line">                connection = mostRecentlyCreated.getAndSet(null);</span><br><span class="line">                connection.incrementReferenceCount();</span><br><span class="line">            &#125; catch (Exception e) &#123;</span><br><span class="line">                throw createJmsException(&quot;Error while attempting to add new Connection to the pool&quot;, e);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125; else &#123;</span><br><span class="line">            try &#123;</span><br><span class="line">                // We can race against other threads returning the connection when there is an</span><br><span class="line">                // expiration or idle timeout.  We keep pulling out ConnectionPool instances until</span><br><span class="line">                // we win and get a non-closed instance and then increment the reference count</span><br><span class="line">                // under lock to prevent another thread from triggering an expiration check and</span><br><span class="line">                // pulling the rug out from under us.</span><br><span class="line">                while (connection == null) &#123;</span><br><span class="line">                    connection = connectionsPool.borrowObject(key);</span><br><span class="line">                    synchronized (connection) &#123;</span><br><span class="line">                        if (connection.getConnection() != null) &#123;</span><br><span class="line">                            connection.incrementReferenceCount();</span><br><span class="line">                            break;</span><br><span class="line">                        &#125;</span><br><span class="line">                        // Return the bad one to the pool and let if get destroyed as normal.</span><br><span class="line">                        connectionsPool.returnObject(key, connection);</span><br><span class="line">                        connection = null;</span><br><span class="line">                    &#125;</span><br><span class="line">                &#125;</span><br><span class="line">            &#125; catch (Exception e) &#123;</span><br><span class="line">                throw createJmsException(&quot;Error while attempting to retrieve a connection from the pool&quot;, e);</span><br><span class="line">            &#125;</span><br><span class="line">            try &#123;</span><br><span class="line">                connectionsPool.returnObject(key, connection);</span><br><span class="line">            &#125; catch (Exception e) &#123;</span><br><span class="line">                throw createJmsException(&quot;Error when returning connection to the pool&quot;, e);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br></pre></td></tr></table></figure><p>当MaxConnections设置的很大的时候，会在发消息的时候一直判断池子中数量是否达到最大值，如果小于最大值再创建一个新的连接放入池子，这样就会前面发送消息的动作都会创建连接从而发送时间会增长。<br>比如：MaxConnections=20，发送消息50次，前20次都会去创建连接并且发送，后面30次会去复用连接池内的连接</p><h2 id="idleTimeout配置问题注意事项"><a href="#idleTimeout配置问题注意事项" class="headerlink" title="idleTimeout配置问题注意事项"></a><a name="idletimeout">idleTimeout配置问题注意事项</a></h2><p>空闲时间配置问题，activemq默认idleTimeout=30秒，activemq开启failover的话它的连接创建时间相对较长，因此建议这个时间设置大一些，尽量不要让超时清空掉，提高复用率</p><h2 id="关于Failover的问题"><a href="#关于Failover的问题" class="headerlink" title="关于Failover的问题"></a><a name="failover">关于Failover的问题</a></h2><p>activemq开启failover策略会根据配置的连接串中的tpc ip按顺序迭代去检测可用来创建连接，当可用的连接排在第一个的时候他的创建连接时间相比可用连接排在后面的时间短一些。<br>但是我们现在单个连接的时间耗时确实很高，这个问题不太清楚具体是什么问题，如下是创建连接耗时日志：<br>不开启failover的日志</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">耗时0：945ms</span><br><span class="line">耗时1：1040ms</span><br><span class="line">耗时2：595ms</span><br><span class="line">耗时3：853ms</span><br><span class="line">耗时4：716ms</span><br><span class="line">耗时5：0ms</span><br><span class="line">耗时6：0ms</span><br><span class="line">耗时7：0ms</span><br></pre></td></tr></table></figure><p>开启failover，可用连接排在第一位置，的日志</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">耗时0：2689ms</span><br><span class="line">2017-11-03 18:47:20.599 [ActiveMQ Task-1] INFO  o.a.activemq.transport.failover.FailoverTransport - Successfully connected to tcp://10.51.232.238:61616</span><br><span class="line">耗时1：1944ms</span><br><span class="line">2017-11-03 18:47:22.615 [ActiveMQ Task-1] INFO  o.a.activemq.transport.failover.FailoverTransport - Successfully connected to tcp://10.51.232.238:61616</span><br><span class="line">耗时2：1968ms</span><br><span class="line">2017-11-03 18:47:24.724 [ActiveMQ Task-1] INFO  o.a.activemq.transport.failover.FailoverTransport - Successfully connected to tcp://10.51.232.238:61616</span><br><span class="line">耗时3：2079ms</span><br><span class="line">2017-11-03 18:47:25.318 [ActiveMQ Task-1] INFO  o.a.activemq.transport.failover.FailoverTransport - Successfully connected to tcp://10.51.232.238:61616</span><br><span class="line">耗时4：608ms</span><br><span class="line">耗时5：0ms</span><br><span class="line">耗时6：0ms</span><br><span class="line">耗时7：0ms</span><br></pre></td></tr></table></figure><p>开启failover，可用连接排在最后的位置，的日志</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">耗时0：1960ms</span><br><span class="line">2017-11-03 18:49:14.991 [ActiveMQ Task-1] INFO  o.a.activemq.transport.failover.FailoverTransport - Successfully connected to tcp://10.51.232.238:61616</span><br><span class="line">耗时1：2084ms</span><br><span class="line">2017-11-03 18:49:16.661 [ActiveMQ Task-1] INFO  o.a.activemq.transport.failover.FailoverTransport - Successfully connected to tcp://10.51.232.238:61616</span><br><span class="line">耗时2：1775ms</span><br><span class="line">2017-11-03 18:49:17.397 [ActiveMQ Task-1] INFO  o.a.activemq.transport.failover.FailoverTransport - Successfully connected to tcp://10.51.232.238:61616</span><br><span class="line">耗时3：708ms</span><br><span class="line">2017-11-03 18:49:18.066 [ActiveMQ Task-1] INFO  o.a.activemq.transport.failover.FailoverTransport - Successfully connected to tcp://10.51.232.238:61616</span><br><span class="line">耗时4：864ms</span><br><span class="line">耗时5：3ms</span><br><span class="line">耗时6：0ms</span><br><span class="line">耗时7：0ms</span><br></pre></td></tr></table></figure><p>以上创建连接包括vpn加密的过程，可能会影响时间。<br><span style="color:red"><strong>ps.前五个是创建连接，因为我配置的5个连接数，后面都是连接复用，异步发送</strong></span></p>]]></content>
    
    <summary type="html">
    
      ActiveMQ发送速度慢问题排查
    
    </summary>
    
      <category term="trouble shooting" scheme="https://ningyu1.github.io/categories/trouble-shooting/"/>
    
    
      <category term="activemq" scheme="https://ningyu1.github.io/tags/activemq/"/>
    
      <category term="activemq slow speed" scheme="https://ningyu1.github.io/tags/activemq-slow-speed/"/>
    
  </entry>
  
  <entry>
    <title>并发与幂等性</title>
    <link href="https://ningyu1.github.io/20171106/37-concurrency-idempotent.html"/>
    <id>https://ningyu1.github.io/20171106/37-concurrency-idempotent.html</id>
    <published>2017-11-06T09:40:36.000Z</published>
    <updated>2024-04-23T01:48:15.033Z</updated>
    
    <content type="html"><![CDATA[<p>文章来源：<a href="https://my.oschina.net/wangen2009/blog/1560975" target="_blank" rel="noopener">https://my.oschina.net/wangen2009/blog/1560975</a><br>作者：@码代码的小司机</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/concurrency/1.jpg" alt title>                </div>                <div class="image-caption"></div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/concurrency/2.jpg" alt title>                </div>                <div class="image-caption"></div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/concurrency/3.jpg" alt title>                </div>                <div class="image-caption"></div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/concurrency/4.jpg" alt title>                </div>                <div class="image-caption"></div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/concurrency/5.jpg" alt title>                </div>                <div class="image-caption"></div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/concurrency/6.jpg" alt title>                </div>                <div class="image-caption"></div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/concurrency/7.jpg" alt title>                </div>                <div class="image-caption"></div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/concurrency/8.jpg" alt title>                </div>                <div class="image-caption"></div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/concurrency/9.jpg" alt title>                </div>                <div class="image-caption"></div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/concurrency/10.jpg" alt title>                </div>                <div class="image-caption"></div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/concurrency/11.jpg" alt title>                </div>                <div class="image-caption"></div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/concurrency/12.jpg" alt title>                </div>                <div class="image-caption"></div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/concurrency/13.jpg" alt title>                </div>                <div class="image-caption"></div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/concurrency/14.jpg" alt title>                </div>                <div class="image-caption"></div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/concurrency/15.jpg" alt title>                </div>                <div class="image-caption"></div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/concurrency/16.jpg" alt title>                </div>                <div class="image-caption"></div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/concurrency/17.jpg" alt title>                </div>                <div class="image-caption"></div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/concurrency/18.jpg" alt title>                </div>                <div class="image-caption"></div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/concurrency/19.jpg" alt title>                </div>                <div class="image-caption"></div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/concurrency/20.jpg" alt title>                </div>                <div class="image-caption"></div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/concurrency/21.jpg" alt title>                </div>                <div class="image-caption"></div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/concurrency/22.jpg" alt title>                </div>                <div class="image-caption"></div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/concurrency/23.jpg" alt title>                </div>                <div class="image-caption"></div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/concurrency/24.jpg" alt title>                </div>                <div class="image-caption"></div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/concurrency/25.jpg" alt title>                </div>                <div class="image-caption"></div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/concurrency/26.jpg" alt title>                </div>                <div class="image-caption"></div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/concurrency/27.jpg" alt title>                </div>                <div class="image-caption"></div>            </figure>]]></content>
    
    <summary type="html">
    
      并发与幂等性
    
    </summary>
    
      <category term="concurrency" scheme="https://ningyu1.github.io/categories/concurrency/"/>
    
      <category term="idempotent" scheme="https://ningyu1.github.io/categories/concurrency/idempotent/"/>
    
    
      <category term="concurrency" scheme="https://ningyu1.github.io/tags/concurrency/"/>
    
      <category term="idempotent" scheme="https://ningyu1.github.io/tags/idempotent/"/>
    
  </entry>
  
  <entry>
    <title>atomikos jta(xa) transaction问题：Already mapped: xxxx</title>
    <link href="https://ningyu1.github.io/20171102/36-atomikos-transactions-trouble.html"/>
    <id>https://ningyu1.github.io/20171102/36-atomikos-transactions-trouble.html</id>
    <published>2017-11-02T07:52:36.000Z</published>
    <updated>2024-04-23T01:48:15.033Z</updated>
    
    <content type="html"><![CDATA[<h1 id="目录："><a href="#目录：" class="headerlink" title="目录："></a>目录：</h1><ol><li><a href="#trouble">问题现象</a></li><li><a href="#troubleshooting">问题分析</a></li><li><a href="#validation">修改验证</a></li><li><a href="#solutions">解决方案</a></li><li><a href="#summed">总结</a></li></ol><h2 id="问题现象"><a href="#问题现象" class="headerlink" title="问题现象"></a><a name="trouble">问题现象</a></h2><p>库存中心在压测过程中会时不时的报错，错误如下：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">2017-11-02 11:38:37.620 [DubboServerHandler-10.27.69.168:20888-thread-156] ERROR xx.xx.inv.service.impl.OptionApiImpl - java.lang.IllegalStateException: Already mapped: 10.27.69.168.tm150959391756909559</span><br><span class="line">xx.xx.exception.BizException: java.lang.IllegalStateException: Already mapped: 10.27.69.168.tm150959391756909559</span><br><span class="line">        at xx.xx.inv.service.impl.OptionApiImpl.invWmsOption(OptionApiImpl.java:290) ~[inv-api-impl-1.0.1-SNAPSHOT.jar:na]</span><br><span class="line">        at com.alibaba.dubbo.common.bytecode.Wrapper1.invokeMethod(Wrapper1.java) [na:2.5.3]</span><br><span class="line">        at com.alibaba.dubbo.rpc.proxy.javassist.JavassistProxyFactory$1.doInvoke(JavassistProxyFactory.java:46) [dubbo-2.5.3.jar:2.5.3]</span><br><span class="line">        at com.alibaba.dubbo.rpc.proxy.AbstractProxyInvoker.invoke(AbstractProxyInvoker.java:72) [dubbo-2.5.3.jar:2.5.3]</span><br><span class="line">        at com.alibaba.dubbo.rpc.protocol.InvokerWrapper.invoke(InvokerWrapper.java:53) [dubbo-2.5.3.jar:2.5.3]</span><br><span class="line">        at com.alibaba.dubbo.rpc.filter.AccessLogFilter.invoke(AccessLogFilter.java:199) [dubbo-2.5.3.jar:2.5.3]</span><br><span class="line">        at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:91) [dubbo-2.5.3.jar:2.5.3]</span><br><span class="line">        at com.alibaba.dubbo.rpc.filter.ExceptionFilter.invoke(ExceptionFilter.java:64) [dubbo-2.5.3.jar:2.5.3]</span><br><span class="line">        at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:91) [dubbo-2.5.3.jar:2.5.3]</span><br><span class="line">        at com.alibaba.dubbo.rpc.filter.TimeoutFilter.invoke(TimeoutFilter.java:42) [dubbo-2.5.3.jar:2.5.3]</span><br><span class="line">        at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:91) [dubbo-2.5.3.jar:2.5.3]</span><br><span class="line">        at com.alibaba.dubbo.monitor.support.MonitorFilter.invoke(MonitorFilter.java:75) [dubbo-2.5.3.jar:2.5.3]</span><br><span class="line">        at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:91) [dubbo-2.5.3.jar:2.5.3]</span><br><span class="line">        at com.alibaba.dubbo.rpc.protocol.dubbo.filter.TraceFilter.invoke(TraceFilter.java:78) [dubbo-2.5.3.jar:2.5.3]</span><br><span class="line">        at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:91) [dubbo-2.5.3.jar:2.5.3]</span><br><span class="line">        at com.alibaba.dubbo.rpc.filter.ContextFilter.invoke(ContextFilter.java:60) [dubbo-2.5.3.jar:2.5.3]</span><br><span class="line">        at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:91) [dubbo-2.5.3.jar:2.5.3]</span><br><span class="line">        at com.alibaba.dubbo.rpc.filter.GenericFilter.invoke(GenericFilter.java:112) [dubbo-2.5.3.jar:2.5.3]</span><br><span class="line">        at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:91) [dubbo-2.5.3.jar:2.5.3]</span><br><span class="line">        at com.alibaba.dubbo.rpc.filter.ClassLoaderFilter.invoke(ClassLoaderFilter.java:38) [dubbo-2.5.3.jar:2.5.3]</span><br><span class="line">        at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:91) [dubbo-2.5.3.jar:2.5.3]</span><br><span class="line">        at com.alibaba.dubbo.rpc.filter.EchoFilter.invoke(EchoFilter.java:38) [dubbo-2.5.3.jar:2.5.3]</span><br><span class="line">        at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:91) [dubbo-2.5.3.jar:2.5.3]</span><br><span class="line">        at com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol$1.reply(DubboProtocol.java:108) [dubbo-2.5.3.jar:2.5.3]</span><br><span class="line">        at com.alibaba.dubbo.remoting.exchange.support.header.HeaderExchangeHandler.handleRequest(HeaderExchangeHandler.java:84) [dubbo-2.5.3.jar:2.5.3]</span><br><span class="line">        at com.alibaba.dubbo.remoting.exchange.support.header.HeaderExchangeHandler.received(HeaderExchangeHandler.java:170) [dubbo-2.5.3.jar:2.5.3]</span><br><span class="line">        at com.alibaba.dubbo.remoting.transport.DecodeHandler.received(DecodeHandler.java:52) [dubbo-2.5.3.jar:2.5.3]</span><br><span class="line">        at com.alibaba.dubbo.remoting.transport.dispatcher.ChannelEventRunnable.run(ChannelEventRunnable.java:82) [dubbo-2.5.3.jar:2.5.3]</span><br><span class="line">        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145) [na:1.7.0_79]</span><br><span class="line">        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615) [na:1.7.0_79]</span><br><span class="line">        at java.lang.Thread.run(Thread.java:745) [na:1.7.0_79]</span><br><span class="line">Caused by: java.lang.IllegalStateException: Already mapped: 10.27.69.168.tm150959391756909559</span><br><span class="line">        at com.atomikos.icatch.imp.TransactionServiceImp.setTidToTx(TransactionServiceImp.java:191) ~[transactions-4.0.0.jar:na]</span><br><span class="line">        at com.atomikos.icatch.imp.TransactionServiceImp.createCT(TransactionServiceImp.java:277) ~[transactions-4.0.0.jar:na]</span><br><span class="line">        at com.atomikos.icatch.imp.TransactionServiceImp.createCompositeTransaction(TransactionServiceImp.java:783) ~[transactions-4.0.0.jar:na]</span><br><span class="line">        at com.atomikos.icatch.imp.CompositeTransactionManagerImp.createCompositeTransaction(CompositeTransactionManagerImp.java:393) ~[transactions-4.0.0.jar:na]</span><br><span class="line">        at com.atomikos.icatch.jta.TransactionManagerImp.begin(TransactionManagerImp.java:271) ~[transactions-jta-4.0.0.jar:na]</span><br><span class="line">        at com.atomikos.icatch.jta.TransactionManagerImp.begin(TransactionManagerImp.java:249) ~[transactions-jta-4.0.0.jar:na]</span><br><span class="line">        at com.atomikos.icatch.jta.UserTransactionImp.begin(UserTransactionImp.java:72) ~[transactions-jta-4.0.0.jar:na]</span><br><span class="line">        at org.springframework.transaction.jta.JtaTransactionManager.doJtaBegin(JtaTransactionManager.java:874) ~[spring-tx-4.3.6.RELEASE.jar:4.3.6.RELEASE]</span><br><span class="line">        at org.springframework.transaction.jta.JtaTransactionManager.doBegin(JtaTransactionManager.java:831) ~[spring-tx-4.3.6.RELEASE.jar:4.3.6.RELEASE]</span><br><span class="line">        at org.springframework.transaction.support.AbstractPlatformTransactionManager.getTransaction(AbstractPlatformTransactionManager.java:373) ~[spring-tx-4.3.6.RELEASE.jar:4.3.6.RELEASE]</span><br><span class="line">        at org.springframework.transaction.interceptor.TransactionAspectSupport.createTransactionIfNecessary(TransactionAspectSupport.java:447) ~[spring-tx-4.3.6.RELEASE.jar:4.3.6.RELEASE]</span><br><span class="line">        at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:277) ~[spring-tx-4.3.6.RELEASE.jar:4.3.6.RELEASE]</span><br><span class="line">        at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:96) ~[spring-tx-4.3.6.RELEASE.jar:4.3.6.RELEASE]</span><br><span class="line">        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.3.6.RELEASE.jar:4.3.6.RELEASE]</span><br><span class="line">        at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:656) ~[spring-aop-4.3.6.RELEASE.jar:4.3.6.RELEASE]</span><br><span class="line">        at xx.xx.inv.service.impl.VoucherExecutor$$EnhancerBySpringCGLIB$$a5e2dd9c.doWms(&lt;generated&gt;) ~[inv-api-impl-1.0.1-SNAPSHOT.jar:na]</span><br><span class="line">        at xx.xx.inv.service.impl.OptionApiImpl.invWmsOption(OptionApiImpl.java:286) ~[inv-api-impl-1.0.1-SNAPSHOT.jar:na]</span><br><span class="line">        ... 30 common frames omitted</span><br></pre></td></tr></table></figure><h2 id="问题分析"><a href="#问题分析" class="headerlink" title="问题分析"></a><a name="troubleshooting">问题分析</a></h2><p>跟踪源码：com.atomikos.icatch.imp.TransactionServiceImp.setTidToTx()</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">private void setTidToTx ( String tid , CompositeTransaction ct )</span><br><span class="line">        throws IllegalStateException</span><br><span class="line">&#123;</span><br><span class="line">    synchronized ( tidToTransactionMap_ ) &#123;</span><br><span class="line">        if ( tidToTransactionMap_.containsKey ( tid.intern () ) )</span><br><span class="line">            throw new IllegalStateException ( &quot;Already mapped: &quot; + tid );</span><br><span class="line">        tidToTransactionMap_.put ( tid.intern (), ct );</span><br><span class="line">        ct.addSubTxAwareParticipant(this); // for GC purposes</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>发现在tidToTransactionMap_中存在tid重复的情况，这个方法判断如果出现重复报：Already mapped: ${tid}，继续跟踪找到tid生成的地方</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">public CompositeTransaction createCompositeTransaction ( long timeout ) throws SysException</span><br><span class="line">&#123;</span><br><span class="line">    if ( !initialized_ ) throw new IllegalStateException ( &quot;Not initialized&quot; );</span><br><span class="line">    if ( maxNumberOfActiveTransactions_ &gt;= 0 &amp;&amp;</span><br><span class="line">         tidToTransactionMap_.size () &gt;= maxNumberOfActiveTransactions_ ) &#123;</span><br><span class="line">        throw new IllegalStateException ( &quot;Max number of active transactions reached:&quot; + maxNumberOfActiveTransactions_ );</span><br><span class="line">    &#125;</span><br><span class="line">     </span><br><span class="line">    String tid = tidmgr_.get ();</span><br><span class="line">    Stack&lt;CompositeTransaction&gt; lineage = new Stack&lt;CompositeTransaction&gt;();</span><br><span class="line">    // create a CC with heuristic preference set to false,</span><br><span class="line">    // since it does not really matter anyway (since we are</span><br><span class="line">    // creating a root)</span><br><span class="line">    CoordinatorImp cc = createCC ( null, tid, true, false, timeout );</span><br><span class="line">    CompositeTransaction ct = createCT ( tid, cc, lineage, false );</span><br><span class="line">    return ct;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>tid是通过tidmgr_.get ();这个东西生成的，那我们进去看一下生成的代码具体是什么？</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">private final static int MAX_LENGTH_OF_NUMERIC_SUFFIX = 8 + 5;</span><br><span class="line">private final static int MAX_COUNTER_WITHIN_SAME_MILLIS = 32000;</span><br><span class="line"> </span><br><span class="line"> </span><br><span class="line">private final String commonPartOfId; //name of server</span><br><span class="line">private int lastcounter;</span><br><span class="line"> </span><br><span class="line">public String get()</span><br><span class="line">&#123;</span><br><span class="line">    incrementAndGet();</span><br><span class="line">    StringBuffer buffer = new StringBuffer();</span><br><span class="line">    return buffer.append(commonPartOfId).</span><br><span class="line">                  append(System.currentTimeMillis()).</span><br><span class="line">                  append(getCountWithLeadingZeroes ( lastcounter )).</span><br><span class="line">                  toString() ;</span><br><span class="line">&#125;</span><br><span class="line"> </span><br><span class="line">private synchronized void incrementAndGet() &#123;</span><br><span class="line">    lastcounter++;</span><br><span class="line">    if (lastcounter == MAX_COUNTER_WITHIN_SAME_MILLIS) lastcounter = 0;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>那极其有可能get的时候在极端的情况下生成的id是相同的，incrementAndGet方法是synchronized 理论上不会有并发问题，但是lastcounter这个属性不是支持并发的对象，在get方法中先调用同步方法incrementAndGet对属性lastcounter++，后面buffer在append的时候直接使用的是属性lastcounter属性的值，很有可能问题就出在这里，那让我们使用btrace验证一下。</p><p>通过btrace对get方法拦截验证发现确实在极端的情况下会有多个线程生成同一个tid，如下：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">[DubboServerHandler-10.27.69.168:20888-thread-177] - com.atomikos.util.UniqueIdMgr.get()--&gt;10.27.69.168.tm150959391749109556</span><br><span class="line">[DubboServerHandler-10.27.69.168:20888-thread-156] - com.atomikos.util.UniqueIdMgr.get()--&gt;10.27.69.168.tm150959391749509557</span><br><span class="line">[DubboServerHandler-10.27.69.168:20888-thread-156] - com.atomikos.util.UniqueIdMgr.get()--&gt;10.27.69.168.tm150959391756909559</span><br><span class="line">[DubboServerHandler-10.27.69.168:20888-thread-177] - com.atomikos.util.UniqueIdMgr.get()--&gt;10.27.69.168.tm150959391756909559</span><br><span class="line">[DubboServerHandler-10.27.69.168:20888-thread-155] - com.atomikos.util.UniqueIdMgr.get()--&gt;10.27.69.168.tm150959391780109560</span><br><span class="line">[DubboServerHandler-10.27.69.168:20888-thread-155] - com.atomikos.util.UniqueIdMgr.get()--&gt;10.27.69.168.tm150959391786909561</span><br><span class="line">[DubboServerHandler-10.27.69.168:20888-thread-112] - com.atomikos.util.UniqueIdMgr.get()--&gt;10.27.69.168.tm150959391791609562</span><br><span class="line">[DubboServerHandler-10.27.69.168:20888-thread-197] - com.atomikos.util.UniqueIdMgr.get()--&gt;10.27.69.168.tm150959391794109563</span><br></pre></td></tr></table></figure><p>出现了两个tm150959391756909559，那就能断定肯定是这块出问题，那如何解决呢?</p><p>首先查看我们使用的atomikos transaction的版本号 – &gt; 4.0.0</p><p>去maven官服上搜索transaction的版本信息:<a href="http://mvnrepository.com/artifact/com.atomikos/atomikos-util" title="maven repostory" target="_blank" rel="noopener">http://mvnrepository.com/artifact/com.atomikos/atomikos-util</a></p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/atomikos-transactions/1.png" alt="1.png" title>                </div>                <div class="image-caption">1.png</div>            </figure><p>看来有更高的版本，那我们下载一个版本看一下get的代码是否发生了变化，我们从4.0.1版本开始查看。</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">public String get()</span><br><span class="line">&#123;</span><br><span class="line">  StringBuffer buffer = new StringBuffer();</span><br><span class="line">  String id = this.commonPartOfId + System.currentTimeMillis() + getCountWithLeadingZeroes(incrementAndGet());</span><br><span class="line">  return id;</span><br><span class="line">&#125;</span><br><span class="line"> </span><br><span class="line">private synchronized int incrementAndGet()</span><br><span class="line">&#123;</span><br><span class="line">  this.lastcounter += 1;</span><br><span class="line">  if (this.lastcounter == 32000) &#123;</span><br><span class="line">    this.lastcounter = 0;</span><br><span class="line">  &#125;</span><br><span class="line">  return this.lastcounter;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>从上面代码可以发现跟4.0.0的代码是有变化的</p><p><span style="color:red"><strong>一、4.0.0版本在incrementAndGet方法同步的对lastcounter++之后，在拼接id的时候是直接使用属性lastcounter进行拼接</strong></span><br><span style="color:red"><strong>二、4.0.1版本在incrementAndGet方法同步的对lastcounter++之后直接将lastcounter值返回，在拼接的时候使用返回的lastcounter值来进行拼接</strong></span></p><p>从代码上看好像是为了解决这个问题，那我们还需要进一步验证</p><p>首先先找到官方的chang log看是否有明确的版本升级描述中fixed并发tid的bug，翻atomikos的<a href="https://www.atomikos.com/Blog/ExtremeTransactions4dot0dot1" target="_blank" rel="noopener">官网站点</a></p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/atomikos-transactions/2.png" alt="2.png" title>                </div>                <div class="image-caption">2.png</div>            </figure><p>功夫不负有心人找到了fixed记录，接下来就需要升级程序然后再进行实际压测过程去校验是否真的解决了这个问题</p><h2 id="修改验证"><a href="#修改验证" class="headerlink" title="修改验证"></a><a name="validation">修改验证</a></h2><p>升级atomikos transactions版本–&gt;4.0.1,打包程序发布进行压力测试<br>压测场景：<br>4个仓，一个仓10个线程，一个线程2000单，一单2个商品，一个商品6个sku<br><span style="color:red"><strong>压测后再没有Already mapped: xxxx的错误爆出，库存扣除也是正确的。</strong></span></p><h2 id="解决方案"><a href="#解决方案" class="headerlink" title="解决方案"></a><a name="solutions">解决方案</a></h2><p><span style="color:red"><strong>升级atomikos transactions版本–&gt;4.0.1</strong></span></p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a><a name="summed">总结</a></h2><p>在使用任何第三方框架都是存在风险，就看如何进行权衡，出现问题能否hold的住，当出现由于使用第三方框架带来的问题时。</p><ol><li>首先要彻底的分析出问题的原因</li><li>其次就去社区或者官网或者问作者是否bug已经fixed。</li><li>上面的都尝试之后如果还不能解决，要么寻找替换方案，要么修改源码。<br><span style="color:red"><strong>能使用官网升级的版本解决问题尽量升级版本解决，第三步的方法虽然不推荐，但是在特定的环境也是一个兜底的方案。</strong></span></li></ol>]]></content>
    
    <summary type="html">
    
      atomikos jta(xa) transaction问题：Already mapped: xxxx
    
    </summary>
    
      <category term="trouble shooting" scheme="https://ningyu1.github.io/categories/trouble-shooting/"/>
    
    
      <category term="transaction" scheme="https://ningyu1.github.io/tags/transaction/"/>
    
      <category term="atomikos" scheme="https://ningyu1.github.io/tags/atomikos/"/>
    
      <category term="jta xa" scheme="https://ningyu1.github.io/tags/jta-xa/"/>
    
      <category term="Already mapped" scheme="https://ningyu1.github.io/tags/Already-mapped/"/>
    
  </entry>
  
  <entry>
    <title>数据源连接泄漏问题分析</title>
    <link href="https://ningyu1.github.io/20171026/35-datasource-connection-leak.html"/>
    <id>https://ningyu1.github.io/20171026/35-datasource-connection-leak.html</id>
    <published>2017-10-26T05:29:36.000Z</published>
    <updated>2024-04-23T01:48:15.033Z</updated>
    
    <content type="html"><![CDATA[<h1 id="目录："><a href="#目录：" class="headerlink" title="目录："></a>目录：</h1><ol><li><a href="#trouble">问题现象</a></li><li><a href="#troubleshooting">问题分析</a></li><li><a href="#validation">修改验证</a></li><li><a href="#solutions">解决方案</a></li><li><a href="#summed">总结</a></li></ol><h2 id="问题现象"><a href="#问题现象" class="headerlink" title="问题现象"></a><a name="trouble">问题现象</a></h2><p>开启druid数据源的连接泄漏开关（removeAbandoned=true），设置强制回收非法连接的超时时间为120（removeAbandonedTimeout=120,2分钟，目的是调试方便，让非法连接快速close掉）。<br>启动程序，等待2分钟会有连接泄漏的异常爆出，具体日志如下：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">2017-10-25 17:19:52.858 [qtp365976330-72] WARN  org.jasig.cas.client.session.SingleSignOutHandler - Front Channel single sign out redirects are disabled when the &apos;casServerUrlPrefix&apos; value is not set.</span><br><span class="line">2017-10-25 17:21:56.531 [Druid-ConnectionPool-Destroy-678372234] ERROR com.alibaba.druid.pool.DruidDataSource - abandon connection, open stackTrace</span><br><span class="line">    at java.lang.Thread.getStackTrace(Thread.java:1588)</span><br><span class="line">    at com.alibaba.druid.pool.DruidDataSource.getConnectionDirect(DruidDataSource.java:995)</span><br><span class="line">    at com.alibaba.druid.filter.FilterChainImpl.dataSource_connect(FilterChainImpl.java:4544)</span><br><span class="line">    at com.alibaba.druid.filter.FilterAdapter.dataSource_getConnection(FilterAdapter.java:2723)</span><br><span class="line">    at com.alibaba.druid.filter.FilterChainImpl.dataSource_connect(FilterChainImpl.java:4540)</span><br><span class="line">    at com.alibaba.druid.filter.stat.StatFilter.dataSource_getConnection(StatFilter.java:661)</span><br><span class="line">    at com.alibaba.druid.filter.FilterChainImpl.dataSource_connect(FilterChainImpl.java:4540)</span><br><span class="line">    at com.alibaba.druid.pool.DruidDataSource.getConnection(DruidDataSource.java:919)</span><br><span class="line">    at com.alibaba.druid.pool.DruidDataSource.getConnection(DruidDataSource.java:911)</span><br><span class="line">    at com.alibaba.druid.pool.DruidDataSource.getConnection(DruidDataSource.java:98)</span><br><span class="line">    at com.github.pagehelper.PageHelper.initSqlUtil(PageHelper.java:165)</span><br><span class="line">    at com.github.pagehelper.PageHelper.intercept(PageHelper.java:148)</span><br><span class="line">    at org.apache.ibatis.plugin.Plugin.invoke(Plugin.java:60)</span><br><span class="line">    at com.sun.proxy.$Proxy64.query(Unknown Source)</span><br><span class="line">    at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:108)</span><br><span class="line">    at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:102)</span><br><span class="line">    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)</span><br><span class="line">    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)</span><br><span class="line">    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)</span><br><span class="line">    at java.lang.reflect.Method.invoke(Method.java:606)</span><br><span class="line">    at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:358)</span><br><span class="line">    at com.sun.proxy.$Proxy57.selectList(Unknown Source)</span><br><span class="line">    at org.mybatis.spring.SqlSessionTemplate.selectList(SqlSessionTemplate.java:198)</span><br><span class="line">    at com.xx.xx.xx.mybatis.MyBatisDao.selectList(MyBatisDao.java:391)</span><br><span class="line">    at com.xx.xx.xx.xx.xx.xx.XXDaoImpl.queryByDeliverCode(XXDaoImpl.java:158)</span><br><span class="line">    at com.xx.xx.xx.xx.xx.xx.XXServiceImpl.queryByDeliverCode(XXServiceImpl.java:159)</span><br><span class="line">    at com.xx.xx.xx.xx.xx.xx.XXServiceImpl$$FastClassByCGLIB$$41eff1cc.invoke(&lt;generated&gt;)</span><br><span class="line">    at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204)</span><br><span class="line">    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:642)</span><br><span class="line">    at com.xx.xx.xx.xx.xx.xx.XXServiceImpl$$EnhancerByCGLIB$$708c18f3.queryByDeliverCode(&lt;generated&gt;)</span><br><span class="line">    at com.xx.xx.xx.xx.xx.XXController.initId(XXController.java:168)</span><br><span class="line">    at com.xx.xx.xx.xx.xx.XXController.afterPropertiesSet(XXController.java:2080)</span><br><span class="line">    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1612)</span><br><span class="line">    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1549)</span><br><span class="line">    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:539)</span><br><span class="line">    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:475)</span><br><span class="line">    at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:304)</span><br><span class="line">    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:228)</span><br><span class="line">    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:300)</span><br><span class="line">    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:195)</span><br><span class="line">    at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:700)</span><br><span class="line">    at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:760)</span><br><span class="line">    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:482)</span><br><span class="line">    at org.springframework.web.context.ContextLoader.configureAndRefreshWebApplicationContext(ContextLoader.java:381)</span><br><span class="line">    at org.springframework.web.context.ContextLoader.initWebApplicationContext(ContextLoader.java:293)</span><br><span class="line">    at org.springframework.web.context.ContextLoaderListener.contextInitialized(ContextLoaderListener.java:106)</span><br><span class="line">    at com.bstek.dorado.web.servlet.SpringContextLoaderListener.contextInitialized(SpringContextLoaderListener.java:73)</span><br><span class="line">    at org.eclipse.jetty.server.handler.ContextHandler.callContextInitialized(ContextHandler.java:782)</span><br><span class="line">    at org.eclipse.jetty.servlet.ServletContextHandler.callContextInitialized(ServletContextHandler.java:424)</span><br><span class="line">    at org.eclipse.jetty.server.handler.ContextHandler.startContext(ContextHandler.java:774)</span><br><span class="line">    at org.eclipse.jetty.servlet.ServletContextHandler.startContext(ServletContextHandler.java:249)</span><br><span class="line">    at org.eclipse.jetty.webapp.WebAppContext.startContext(WebAppContext.java:1242)</span><br><span class="line">    at org.eclipse.jetty.server.handler.ContextHandler.doStart(ContextHandler.java:717)</span><br><span class="line">    at org.eclipse.jetty.webapp.WebAppContext.doStart(WebAppContext.java:494)</span><br><span class="line">    at org.eclipse.jetty.util.component.AbstractLifeCycle.start(AbstractLifeCycle.java:64)</span><br><span class="line">    at org.eclipse.jetty.server.handler.HandlerWrapper.doStart(HandlerWrapper.java:95)</span><br><span class="line">    at org.eclipse.jetty.server.Server.doStart(Server.java:282)</span><br><span class="line">    at org.eclipse.jetty.util.component.AbstractLifeCycle.start(AbstractLifeCycle.java:64)</span><br><span class="line">    at net.sourceforge.eclipsejetty.starter.embedded.JettyEmbeddedAdapter.start(JettyEmbeddedAdapter.java:67)</span><br><span class="line">    at net.sourceforge.eclipsejetty.starter.common.AbstractJettyLauncherMain.launch(AbstractJettyLauncherMain.java:84)</span><br><span class="line">    at net.sourceforge.eclipsejetty.starter.embedded.JettyEmbeddedLauncherMain.main(JettyEmbeddedLauncherMain.java:42)</span><br></pre></td></tr></table></figure><h2 id="问题分析"><a href="#问题分析" class="headerlink" title="问题分析"></a><a name="troubleshooting">问题分析</a></h2><p>断点调试com.alibaba.druid.pool.DruidDataSource与com.alibaba.druid.pool.DruidPooledConnection中的close方法均有调用，如果都有关闭的话那怎么还会有连接泄漏呢？肯定有地方不对劲，因此进一步查询，开启druid的管理页面查看连接数，如下</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/connection-leak/1.png" alt="1.png" title>                </div>                <div class="image-caption">1.png</div>            </figure><p>逻辑连接打开次数132，逻辑连接关闭次数131，发现问题有一个连接是没有放回连接池的，当到2分钟报了连接泄漏异常后再刷新查看，如下：</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/connection-leak/2.png" alt="2.png" title>                </div>                <div class="image-caption">2.png</div>            </figure><p>逻辑连接打开次数和关闭次数一致了。</p><p>于是从上面的错误日志跟踪代码，第一感觉就是自己的业务代码出现了问题，找到业务代码的地方</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">at com.xx.xx.xx.xx.xx.xx.XXDaoImpl.queryByDeliverCode(XXDaoImpl.java:158)</span><br><span class="line">at com.xx.xx.xx.xx.xx.xx.XXServiceImpl.queryByDeliverCode(XXServiceImpl.java:159)</span><br><span class="line">at com.xx.xx.xx.xx.xx.xx.XXServiceImpl$$FastClassByCGLIB$$41eff1cc.invoke(&lt;generated&gt;)</span><br></pre></td></tr></table></figure><p>打开：XXServiceImpl.queryByDeliverCode代码第159行，代码如下：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">@Override</span><br><span class="line">public DeliverEntity queryByDeliverCode(String code) &#123;</span><br><span class="line">    return deliverDao.queryByDeliverCode(code);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>代码非常简单调用dao的方法，代开dao的queryByDeliverCode方法，代码如下：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">@Override</span><br><span class="line">public DeliverEntity queryByDeliverCode(String deliverCode) &#123;</span><br><span class="line">    Map&lt;String,Object&gt; map=new HashMap&lt;String,Object&gt;();</span><br><span class="line">    map.put(&quot;deliverCode&quot;, deliverCode);</span><br><span class="line">    List&lt;DeliverEntity&gt; list = selectList(&quot;com.xx.xx.xx.xx.xx.XXMapper.queryByDeliverCode&quot;, map);</span><br><span class="line">    return list.size() &gt; 0 ? list.get(0) : null;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>代码也非常简单调用的是基类：MybatisDao的selectList方法，代码如下：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">public List&lt;E&gt; selectList(final String aStatement, final Map&lt;String, Object&gt; aCondition) &#123;</span><br><span class="line">    SqlSession session = getSqlSessionTemplate();</span><br><span class="line">    return session.selectList(aStatement, aCondition);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>就是调用sqlsession的selectList方法，这个没有问题，连接是可以正常回收的，如果不能回收那上面的数字不可能是只有1个连接泄漏，应该是逻辑打开的132个都没有关闭才对。因此排除了这个地方，那还有什么地方会有问题呢？<br>肯定是有地方getConnection之后没有close导致!<br>继续分析连接泄漏打出来的日志！日志中的代码逐个分析，最终找到PageHelper.initSqlUtil方法</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">at com.github.pagehelper.PageHelper.initSqlUtil(PageHelper.java:165)</span><br><span class="line">at com.github.pagehelper.PageHelper.intercept(PageHelper.java:148)</span><br></pre></td></tr></table></figure><p>打开PageHelper.initSqlUtil代码，如下：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">public synchronized void initSqlUtil(Invocation invocation) &#123;</span><br><span class="line">       if (sqlUtil == null) &#123;</span><br><span class="line">            String url = null;</span><br><span class="line">            try &#123;</span><br><span class="line">                MappedStatement ms = (MappedStatement) invocation.getArgs()[0];</span><br><span class="line">                MetaObject msObject = SystemMetaObject.forObject(ms);</span><br><span class="line">                DataSource dataSource = (DataSource) msObject.getValue(&quot;configuration.environment.dataSource&quot;);</span><br><span class="line">                url = dataSource.getConnection().getMetaData().getURL();</span><br><span class="line">            &#125; catch (SQLException e) &#123;</span><br><span class="line">                throw new RuntimeException(&quot;分页插件初始化异常:&quot; + e.getMessage());</span><br><span class="line">            &#125;</span><br><span class="line">            if (url == null || url.length() == 0) &#123;</span><br><span class="line">                throw new RuntimeException(&quot;无法自动获取jdbcUrl，请在分页插件中配置dialect参数!&quot;);</span><br><span class="line">            &#125;</span><br><span class="line">            String dialect = Dialect.fromJdbcUrl(url);</span><br><span class="line">            if (dialect == null) &#123;</span><br><span class="line">                throw new RuntimeException(&quot;无法自动获取数据库类型，请通过dialect参数指定!&quot;);</span><br><span class="line">            &#125;</span><br><span class="line">            sqlUtil = new SqlUtil(dialect);</span><br><span class="line">            sqlUtil.setProperties(properties);</span><br><span class="line">            properties = null;</span><br><span class="line">            autoDialect = false;</span><br><span class="line">       &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>貌似问题找到了，第8行代码：dataSource.getConnection()，但是没有在finally中对connection进行回收，罪魁祸首竟然是PageHelper</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">public Object intercept(Invocation invocation) throws Throwable &#123;</span><br><span class="line">    if (autoDialect) &#123;</span><br><span class="line">        initSqlUtil(invocation);</span><br><span class="line">    &#125;</span><br><span class="line">    return sqlUtil.processPage(invocation);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>根据代码逻辑发现当autoDialect=true时会调用initSqlUtil(invocation);，因此核对了我们的配置mybatis-config.xml</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">&lt;plugins&gt;</span><br><span class="line">        &lt;!-- com.github.pagehelper为PageHelper类所在包名 --&gt;</span><br><span class="line">        &lt;plugin interceptor=&quot;com.github.pagehelper.PageHelper&quot;&gt;</span><br><span class="line">&lt;!--             &lt;property name=&quot;dialect&quot; value=&quot;mysql&quot; /&gt; --&gt;</span><br><span class="line">             </span><br><span class="line">            &lt;property name=&quot;autoDialect&quot; value=&quot;true&quot; /&gt;</span><br><span class="line">             </span><br><span class="line">            &lt;!-- 该参数默认为false --&gt;</span><br><span class="line">            &lt;!-- 设置为true时，会将RowBounds第一个参数offset当成pageNum页码使用 --&gt;</span><br><span class="line">            &lt;!-- 和startPage中的pageNum效果一样 --&gt;</span><br><span class="line">            &lt;property name=&quot;offsetAsPageNum&quot; value=&quot;true&quot; /&gt;</span><br><span class="line">            &lt;!-- 该参数默认为false --&gt;</span><br><span class="line">            &lt;!-- 设置为true时，使用RowBounds分页会进行count查询 --&gt;</span><br><span class="line">            &lt;property name=&quot;rowBoundsWithCount&quot; value=&quot;true&quot; /&gt;</span><br><span class="line">            &lt;!-- 设置为true时，如果pageSize=0或者RowBounds.limit = 0就会查询出全部的结果 --&gt;</span><br><span class="line">            &lt;!-- （相当于没有执行分页查询，但是返回结果仍然是Page类型） --&gt;</span><br><span class="line">            &lt;property name=&quot;pageSizeZero&quot; value=&quot;true&quot; /&gt;</span><br><span class="line">            &lt;!-- 3.3.0版本可用 - 分页参数合理化，默认false禁用 --&gt;</span><br><span class="line">            &lt;!-- 启用合理化时，如果pageNum&lt;1会查询第一页，如果pageNum&gt;pages会查询最后一页 --&gt;</span><br><span class="line">            &lt;!-- 禁用合理化时，如果pageNum&lt;1或pageNum&gt;pages会返回空数据 --&gt;</span><br><span class="line">            &lt;property name=&quot;reasonable&quot; value=&quot;false&quot; /&gt;</span><br><span class="line">            &lt;!-- 3.5.0版本可用 - 为了支持startPage(Object params)方法 --&gt;</span><br><span class="line">            &lt;!-- 增加了一个`params`参数来配置参数映射，用于从Map或ServletRequest中取值 --&gt;</span><br><span class="line">            &lt;!-- 可以配置pageNum,pageSize,count,pageSizeZero,reasonable,不配置映射的用默认值 --&gt;</span><br><span class="line">            &lt;!-- 不理解该含义的前提下，不要随便复制该配置 --&gt;</span><br><span class="line">            &lt;property name=&quot;params&quot; value=&quot;pageNum=start;pageSize=limit;&quot; /&gt;</span><br><span class="line">        &lt;/plugin&gt;</span><br><span class="line">    &lt;/plugins&gt;</span><br></pre></td></tr></table></figure><p>我们果然配置的是：autoDialect=true，PageHelper在没有设置数据库方言的时候，他会主动的获取jdbc url来判断时那种数据库，因此会发生有一个连接是泄漏的，那这个问题如何解决呢？<br>我们打开PageHelper.setProperties方法，如下：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">public void setProperties(Properties p) &#123;</span><br><span class="line">    //MyBatis3.2.0版本校验</span><br><span class="line">    try &#123;</span><br><span class="line">        Class.forName(&quot;org.apache.ibatis.scripting.xmltags.SqlNode&quot;);//SqlNode是3.2.0之后新增的类</span><br><span class="line">    &#125; catch (ClassNotFoundException e) &#123;</span><br><span class="line">        throw new RuntimeException(&quot;您使用的MyBatis版本太低，MyBatis分页插件PageHelper支持MyBatis3.2.0及以上版本!&quot;);</span><br><span class="line">    &#125;</span><br><span class="line">    //数据库方言</span><br><span class="line">    String dialect = p.getProperty(&quot;dialect&quot;);</span><br><span class="line">    if (dialect == null || dialect.length() == 0) &#123;</span><br><span class="line">        autoDialect = true;</span><br><span class="line">        this.properties = p;</span><br><span class="line">    &#125; else &#123;</span><br><span class="line">        autoDialect = false;</span><br><span class="line">        sqlUtil = new SqlUtil(dialect);</span><br><span class="line">        sqlUtil.setProperties(p);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>只要我们在plugin配置的时候设置具体的方言就可以避免这个问题：dialect=mysql，如果有明确的dialect设置，autoDialect就会等于false，因此在intercept方法中就不会走initSqlUtil(invocation);方法，这就间接的避免了PageHelper的这个bug。<br>但是如果我们的数据源有不同的dialect怎么办呢？有两个办法解决</p><ol><li>构造SessionFactory的时候加载不同的mybatis-config.xml配置，如果有两种数据库类型就写两个mybatis-config.xml分别配置不同的dialect</li><li>查看PageHelper高版本是否修复了这个bug，升级PageHelper版本</li><li>修改PageHelper源码，在dataSource.getConnection()之后增加close调用</li></ol><p><strong>ps. 我们现在用的PageHelper版本–&gt;4.0.0，根据官方的chang log可以看出4.X的版本修复了这个问题，可以升级到4.x的final released version –&gt; 4.2.1解决这个问题，5.x版本变更比较大。</strong></p><h2 id="修改验证"><a href="#修改验证" class="headerlink" title="修改验证"></a><a name="validation">修改验证</a></h2><p>修改mybatis-config.xml的plugin中PageHelper的dialect的配置</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">&lt;plugin interceptor=&quot;com.github.pagehelper.PageHelper&quot;&gt;</span><br><span class="line">            &lt;property name=&quot;dialect&quot; value=&quot;mysql&quot; /&gt;</span><br><span class="line">&lt;!--             &lt;property name=&quot;autoDialect&quot; value=&quot;true&quot; /&gt; --&gt;</span><br><span class="line"> </span><br><span class="line">&lt;/plugin&gt;</span><br></pre></td></tr></table></figure><p>修改后启动程序，打开druid的管理页面和等待2分钟超时看是否还有泄漏的异常爆出，如下：</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/connection-leak/3.png" alt="3.png" title>                </div>                <div class="image-caption">3.png</div>            </figure><p>超过2分钟并没有泄漏异常爆出</p><h2 id="解决方案"><a href="#解决方案" class="headerlink" title="解决方案"></a><a name="solutions">解决方案</a></h2><p>升级pagehelper版本–&gt;4.2.1,升级jsqlparser版本–&gt;0.9.5,其余配置无需变更</p><p><span style="color: rgb(255,0,0);">如果升级了4.2.1，如果出现SqlUtil.java(120)行报NullPointerException，具体异常如下：</span></p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/connection-leak/4.png" alt="4.png" title>                </div>                <div class="image-caption">4.png</div>            </figure><p><span style="color: rgb(255,0,0);">遇到上面问题，请修改pagehelper的配置参数，参数修改有两种方式，如下：</span></p><ol><li><span style="color: rgb(255,0,0);">直接配置dialect=目标数据源类型</span><span style="color: rgb(255,204,0);"><strong>（适合使用场景：项目中只有一个固定的数据库类型，例如：mysql，无需开启自动发现dialect）</strong></span></li><li><span style="color: rgb(255,0,0);">配置autoRuntimeDialect=true走自动获取，这个属性是替换老属性（autoDialect），老的属性为了向下兼容在并发获取dialect时会有bug存在。</span><span style="color: rgb(255,204,0);"><strong>（适合使用场景：项目中有多个数据库类型，需要运行中自动发现时使用）</strong></span></li></ol><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a><a name="summed">总结</a></h2><p>这个问题告诉我们使用第三方的组件的风险很大。</p>]]></content>
    
    <summary type="html">
    
      数据源连接泄漏问题分析
    
    </summary>
    
      <category term="trouble shooting" scheme="https://ningyu1.github.io/categories/trouble-shooting/"/>
    
    
      <category term="abandon connection" scheme="https://ningyu1.github.io/tags/abandon-connection/"/>
    
      <category term="connection leak" scheme="https://ningyu1.github.io/tags/connection-leak/"/>
    
      <category term="datasource" scheme="https://ningyu1.github.io/tags/datasource/"/>
    
  </entry>
  
  <entry>
    <title>Redis RDB文件格式全解析</title>
    <link href="https://ningyu1.github.io/20171009/34-redis-rdb.html"/>
    <id>https://ningyu1.github.io/20171009/34-redis-rdb.html</id>
    <published>2017-10-09T06:30:36.000Z</published>
    <updated>2024-04-23T01:48:15.033Z</updated>
    
    <content type="html"><![CDATA[<h1 id="点评"><a href="#点评" class="headerlink" title="点评"></a>点评</h1><p>这篇文章作为对RDB理解的教程文章，对RDB文件的原理理解有助于进行Redis高阶应用的设计与开发。</p><p>文章转自：<a href="http://blog.nosqlfan.com/html/3734.html" target="_blank" rel="noopener">http://blog.nosqlfan.com/html/3734.html</a><br>作者：@nosqlfan</p><p>RDB文件是Redis持久化的一种方式，Redis通过制定好的策略，按期将内存中的数据以镜像的形式转存到RDB文件中。那么RDB文件内部格式是什么样的呢，Redis又做了哪些工作让RDB能够更快的dump和加载呢，下面我们深入RDB文件，来看一看其内部结构。<br>首先我们来看一个RDB文件的概况图：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">----------------------------# RDB文件是二进制的，所以并不存在回车换行来分隔一行一行.</span><br><span class="line">52 45 44 49 53              # 以字符串 &quot;REDIS&quot; 开头</span><br><span class="line">30 30 30 33                 # RDB 的版本号，大端存储，比如左边这个表示版本号为0003</span><br><span class="line">----------------------------</span><br><span class="line">FE 00                       # FE = FE表示数据库编号，Redis支持多个库，以数字编号，这里00表示第0个数据库</span><br><span class="line">----------------------------# Key-Value 对存储开始了</span><br><span class="line">FD $length-encoding         # FD 表示过期时间，过期时间是用 length encoding 编码存储的，后面会讲到</span><br><span class="line">$value-type                 # 1 个字节用于表示value的类型，比如set,hash,list,zset等</span><br><span class="line">$string-encoded-key         # Key 值，通过string encoding 编码，同样后面会讲到</span><br><span class="line">$encoded-value              # Value值，根据不同的Value类型采用不同的编码方式</span><br><span class="line">----------------------------</span><br><span class="line">FC $length-encoding         # FC 表示毫秒级的过期时间，后面的具体时间用length encoding编码存储</span><br><span class="line">$value-type                 # 同上，也是一个字节的value类型</span><br><span class="line">$string-encoded-key         # 同样是以 string encoding 编码的 Key值</span><br><span class="line">$encoded-value              # 同样是以对应的数据类型编码的 Value 值</span><br><span class="line">----------------------------</span><br><span class="line">$value-type                 # 下面是没有过期时间设置的 Key-Value对，为防止冲突，数据类型不会以 FD, FC, FE, FF 开头</span><br><span class="line">$string-encoded-key</span><br><span class="line">$encoded-value</span><br><span class="line">----------------------------</span><br><span class="line">FE $length-encoding         # 下一个库开始，库的编号用 length encoding 编码</span><br><span class="line">----------------------------</span><br><span class="line">...                         # 继续存储这个数据库的 Key-Value 对</span><br><span class="line">FF                          ## FF：RDB文件结束的标志</span><br></pre></td></tr></table></figure><p>下面我们对上面的内容进行详细讲解</p><h2 id="Magic-Number"><a href="#Magic-Number" class="headerlink" title="Magic Number"></a>Magic Number</h2><p>第一行就不用讲了，REDIS字符串用于标识是Redis的RDB文件</p><h2 id="版本号"><a href="#版本号" class="headerlink" title="版本号"></a>版本号</h2><p>用了4个字节存储版本号，以大端（big endian）方式存储和读取</p><h2 id="数据库编号"><a href="#数据库编号" class="headerlink" title="数据库编号"></a>数据库编号</h2><p>以一个字节的0xFE开头，后面存储数据库的具体编号，数据库的编号是一个数字，通过 “Length Encoding” 方式编码存储，“Length Encoding” 我们后面会讲到。</p><h2 id="Key-Value值对"><a href="#Key-Value值对" class="headerlink" title="Key-Value值对"></a>Key-Value值对</h2><p>值对包括下面四个部分</p><ol><li>Key 过期时间，这一项是可有可无的</li><li>一个字节表示value的类型</li><li>Key的值，Key都是字符串，通过 “Redis String Encoding” 来保存</li><li>Value的值，通过 “Redis Value Encoding” 来根据不同的数据类型做不同的存储</li></ol><h2 id="Key过期时间"><a href="#Key过期时间" class="headerlink" title="Key过期时间"></a>Key过期时间</h2><p>过期时间由 0xFD 或 0xFC开头用于标识，分别表示秒级的过期时间和毫秒级的过期时间，后面的具体时间是一个UNIX时间戳，秒级或毫秒级的。具体时间戳的值通过“Redis Length Encoding” 编码存储。在导入RDB文件的过程中，会通过过期时间判断是否已过期并需要忽略。</p><h2 id="Value类型"><a href="#Value类型" class="headerlink" title="Value类型"></a>Value类型</h2><p>Value类型用一个字节进行存储，目前包括以下一些值：</p><ul><li>0 = “String Encoding”</li><li>1 = “List Encoding”</li><li>2 = “Set Encoding”</li><li>3 = “Sorted Set Encoding”</li><li>4 = “Hash Encoding”</li><li>9 = “Zipmap Encoding”</li><li>10 = “Ziplist Encoding”</li><li>11 = “Intset Encoding”</li><li>12 = “Sorted Set in Ziplist Encoding”</li></ul><h2 id="Key"><a href="#Key" class="headerlink" title="Key"></a>Key</h2><p>Key值就是简单的 “String Encoding” 编码，具体可以看后面的描述</p><h2 id="Value"><a href="#Value" class="headerlink" title="Value"></a>Value</h2><p>上面列举了Value的9种类型，实际上可以分为三大类</p><ul><li>type = 0, 简单字符串</li><li>type 为  9, 10, 11 或 12, value字符串在读取出来后需要先解压</li><li>type 为 1, 2, 3 或 4, value是字符串序列，这一系列的字符串用于构建list，set，hash 和 zset 结构</li></ul><h2 id="Length-Encoding"><a href="#Length-Encoding" class="headerlink" title="Length Encoding"></a>Length Encoding</h2><p>上面说了很多 Length Encoding ，现在就为大家讲解。可能你会说，长度用一个int存储不就行了吗？但是，通常我们使用到的长度可能都并不大，一个int 4个字节是否有点浪费呢。所以Redis采用了变长编码的方法，将不同大小的数字编码成不同的长度。</p><ol><li>首先在读取长度时，会读一个字节的数据，其中前两位用于进行变长编码的判断</li><li>如果前两位是 0 0，那么下面剩下的 6位就表示具体长度</li><li>如果前两位是 0 1，那么会再读取一个字节的数据，加上前面剩下的6位，共14位用于表示具体长度</li><li>如果前两位是 1 0，那么剩下的 6位就被废弃了，取而代之的是再读取后面的4 个字节用于表示具体长度</li><li>如果前两位是 1 1，那么下面的应该是一个特殊编码，剩下的 6位用于标识特殊编码的种类。特殊编码主要用于将数字存成字符串，或者编码后的字符串。具体见 “String Encoding”</li></ol><p>这样做有什么好处呢，实际就是节约空间：</p><ol><li>0 – 63的数字只需要一个字节进行存储</li><li>而64 – 16383 的数字只需要两个字节进行存储</li><li>16383 - 2^32 -1 的数字只需要用5个字节（1个字节的标识加4个字节的值）进行存储</li></ol><h2 id="String-Encoding"><a href="#String-Encoding" class="headerlink" title="String Encoding"></a>String Encoding</h2><p>Redis的 String Encoding 是二进制安全的，也就是说他没有任何特殊分隔符用于分隔各个值，你可以在里面存储任何东西。它就是一串字节码。<br>下面是 String Encoding 的三种类型</p><ol><li>长度编码的字符串</li><li>数字替代字符串：8位，16位或者32位的数字</li><li>LZF 压缩的字符串</li></ol><h2 id="长度编码字符串"><a href="#长度编码字符串" class="headerlink" title="长度编码字符串"></a>长度编码字符串</h2><p>长度编码字符串是最简单的一种类型，它由两部分组成，一部分是用 “Length Encoding” 编码的字符串长度，第二部分是具体的字节码。</p><h2 id="数字替代字符串"><a href="#数字替代字符串" class="headerlink" title="数字替代字符串"></a>数字替代字符串</h2><p>上面说到过 Length Encoding 的特殊编码，就在这里用上了。所以数字替代字符串是以 1 1 开头的，然后读取这个字节剩下的6 位，根据不同的值标识不同的数字类型：</p><ul><li>0 表示下面是一个8 位的数字</li><li>1 表示下面是一个16 位的数字</li><li>2 表示下面是一个32 位的数字</li></ul><h2 id="LZF压缩字符串"><a href="#LZF压缩字符串" class="headerlink" title="LZF压缩字符串"></a>LZF压缩字符串</h2><p>和数据替代字符串一样，它也是以1 1 开头的，然后剩下的6 位如果值为4，那么就表示它是一个压缩字符串。压缩字符串解析规则如下：</p><ol><li>首先按 Length Encoding 规则读取压缩长度 clen</li><li>然后按 Length Encoding 规则读取非压缩长度</li><li>再读取第二个 clen</li><li>获取到上面的三个信息后，再通过LZF算法解码后面clen长度的字节码</li></ol><h2 id="List-Encoding"><a href="#List-Encoding" class="headerlink" title="List Encoding"></a>List Encoding</h2><p>Redis List 结构在RDB文件中的存储，是依次存储List中的各个元素的。其结构如下：</p><ol><li>首先按 Length Encoding 读取这个List 的长度 size</li><li>然后读取 size个 String Encoding的值</li><li>然后再用这些读到的 size 个值重新构建 List就完成了</li></ol><h2 id="Set-Encoding"><a href="#Set-Encoding" class="headerlink" title="Set Encoding"></a>Set Encoding</h2><p>Set结构和List结构一样，也是依次存储各个元素的</p><h2 id="Sorted-Set-Encoding"><a href="#Sorted-Set-Encoding" class="headerlink" title="Sorted Set Encoding"></a>Sorted Set Encoding</h2><p>todo</p><h2 id="Hash-Encoding"><a href="#Hash-Encoding" class="headerlink" title="Hash Encoding"></a>Hash Encoding</h2><ol><li>首先按 Length Encoding 读出hash 结构的大小 size</li><li>然后读取2×size 个 String Encoding的字符串（因为一个hash项包括key和value两项）</li><li>将上面读取到的2×size 个字符串解析为hash 和key 和 value</li><li>然后将上面的key value对存储到hash结构中</li></ol><h2 id="Zipmap-Encoding"><a href="#Zipmap-Encoding" class="headerlink" title="Zipmap Encoding"></a>Zipmap Encoding</h2><p>参见本站之前的文章：Redis zipmap内存布局分析</p>]]></content>
    
    <summary type="html">
    
      Redis RDB文件格式全解析
    
    </summary>
    
      <category term="Redis" scheme="https://ningyu1.github.io/categories/Redis/"/>
    
      <category term="技术" scheme="https://ningyu1.github.io/categories/Redis/%E6%8A%80%E6%9C%AF/"/>
    
    
      <category term="Redis" scheme="https://ningyu1.github.io/tags/Redis/"/>
    
      <category term="RDB" scheme="https://ningyu1.github.io/tags/RDB/"/>
    
  </entry>
  
  <entry>
    <title>Redis数据结构使用以及注意事项，运维问题总结</title>
    <link href="https://ningyu1.github.io/20171009/33-redis-considerations.html"/>
    <id>https://ningyu1.github.io/20171009/33-redis-considerations.html</id>
    <published>2017-10-09T04:00:36.000Z</published>
    <updated>2024-04-23T01:48:15.033Z</updated>
    
    <content type="html"><![CDATA[<p>文章转自：<a href="http://www.cnblogs.com/cnmenglang/p/6225987.html" target="_blank" rel="noopener">http://www.cnblogs.com/cnmenglang/p/6225987.html</a><br>作者：@江南白衣</p><h2 id="优缺点"><a href="#优缺点" class="headerlink" title="优缺点"></a>优缺点</h2><p>非常非常的快，有测评说比Memcached还快(当大家都是单CPU的时候)，而且是无短板的快，读写都一般的快，所有API都差不多快，也没有MySQL Cluster、MongoDB那样更新同一条记录如Counter时慢下去的毛病。</p><p>丰富的数据结构，超越了一般的Key-Value数据库而被认为是一个数据结构服务器。组合各种结构，限制Redis用途的是你自己的想象力，作者自己捉刀写的用途入门。</p><p>因为是个人作品，Redis目前只有2.3万行代码，Keep it simple的死硬做法，使得普通公司而不需淘宝那个级别的文艺公司也可以吃透它。</p><p>Redis宣言就是作者的自白，我最喜欢其中的”代码像首诗”，”设计是一场与复杂性的战斗”，”Coding是一件艰苦的事情，唯一的办法是享受它。如果它已不能带来快乐就停止它。为了防止这一天的出现，我们要尽量避免把Redis往乏味的路上带。</p><p>让人又爱又恨的单线程架构，使得代码不用处理平时最让人头痛的并发而大幅简化，也不用老是担心作者的并发有没有写对，但也带来CPU的瓶颈，而且单线程被慢操作所阻塞时，其他请求的延时变得不确定。</p><p>那Redis不是什么？</p><p>Redis 不是Big Data，数据都在内存中，无法以T为单位。</p><p>在Redis-Cluster发布并被稳定使用之前，Redis没有真正的平滑水平扩展能力。</p><p>Redis 不支持Ad-Hoc Query，提供的只是数据结构的API，没有SQL一样的查询能力。</p><h3 id="Feature速览"><a href="#Feature速览" class="headerlink" title="Feature速览"></a>Feature速览</h3><p>所有数据都在内存中。</p><p>五种数据结构：String / Hash / List / Set / Ordered Set。</p><p>数据过期时间支持。</p><p>不完全的事务支持。</p><p>服务端脚本：使用Lua Script编写，类似存储过程的作用。</p><p>PubSub：捞过界的消息一对多发布订阅功能，起码Redis-Sentinel使用了它。</p><p>持久化：支持定期导出内存的Snapshot 与 记录写操作日志的Append Only File两种模式。</p><p>Replication：Master-Slave模式，Master可连接多个只读Slave，暂无专门的Geographic Replication支持。</p><p>Fail-Over：Redis-Sentinel节点负责监控Master节点，在master失效时提升slave，独立的仲裁节点模式有效防止脑裂。</p><p>Sharding：开发中的Redis-Cluser。</p><p>动态配置：所有参数可用命令行动态配置不需重启，并重新写回配置文件中，对云上的大规模部署非常合适。</p><h3 id="八卦"><a href="#八卦" class="headerlink" title="八卦"></a>八卦</h3><p>作者是意大利的Salvatore Sanfilippo(antirez)，又是VMWare大善人聘请了他专心写Redis。</p><p>EMC与VMWare将旗下的开源产品如Redis和Spring都整合到了孙公司Pivotal公司。</p><p><a href="http://blog.gopivotal.com/pivotal-people/pivotal-people-salvatore-sanfilippo-inventor-of-redis" target="_blank" rel="noopener">Pivotal做的antirez访谈录</a>，内含一切八卦，比如他的爱好是举重、跑步和品红酒。</p><p>默认端口6379，是手机按键上MERZ对应的号码，意大利歌女Alessia Merz是antirez和朋友们认为愚蠢的代名词。</p><h2 id="数据结构"><a href="#数据结构" class="headerlink" title="数据结构"></a>数据结构</h2><h3 id="Key"><a href="#Key" class="headerlink" title="Key"></a>Key</h3><p>Key 不能太长，比如1024字节，但antirez也不喜欢太短如”u:1000:pwd”，要表达清楚意思才好。他私人建议用”:”分隔域，用”.”作为单词间的连接，如”comment:1234:reply.to”。</p><p><a href="http://redis.readthedocs.org/en/latest/key/keys.html" target="_blank" rel="noopener">Keys</a>，返回匹配的key，支持通配符如 “keys a*” 、 “keys a?c”，但不建议在生产环境大数据量下使用。</p><p><a href="http://redis.readthedocs.org/en/latest/key/sort.html" target="_blank" rel="noopener">Sort</a>，对集合按数字或字母顺序排序后返回或另存为list，还可以关联到外部key等。因为复杂度是最高的O(N+M*log(M))(N是集合大小，M 为返回元素的数量)，有时会安排到slave上执行。</p><p><a href="http://redis.readthedocs.org/en/latest/key/expire.html" target="_blank" rel="noopener">Expire</a>/<a href="http://redis.readthedocs.org/en/latest/key/expireat.html" target="_blank" rel="noopener">ExpireAt</a>/<a href="http://redis.readthedocs.org/en/latest/key/persist.html" target="_blank" rel="noopener">Persist</a>/<a href="http://redis.readthedocs.org/en/latest/key/ttl.html" target="_blank" rel="noopener">TTL</a>，关于Key超时的操作。默认以秒为单位，也有p字头的以毫秒为单位的版本， Redis的内部实现见2.9 过期数据清除。</p><h3 id="String"><a href="#String" class="headerlink" title="String"></a>String</h3><p>最普通的key-value类型，说是String，其实是任意的byte[]，比如图片，最大512M。 所有常用命令的复杂度都是O(1)，普通的Get/Set方法，可以用来做Cache，存Session，为了简化架构甚至可以替换掉Memcached。</p><p><a href="http://redis.readthedocs.org/en/latest/string/incr.html" target="_blank" rel="noopener">Incr</a>/<a href="http://redis.readthedocs.org/en/latest/string/incrby.html" target="_blank" rel="noopener">IncrBy</a>/<a href="http://redis.readthedocs.org/en/latest/string/incrbyfloat.html" target="_blank" rel="noopener">IncrByFloat</a>/<a href="http://redis.readthedocs.org/en/latest/string/decr.html" target="_blank" rel="noopener">Decr</a>/<a href="http://redis.readthedocs.org/en/latest/string/decrby.html" target="_blank" rel="noopener">DecrBy</a>，可以用来做计数器，做自增序列。key不存在时会创建并贴心的设原值为0。IncrByFloat专门针对float，没有对应的decrByFloat版本？用负数啊。</p><p>SetNx， 仅当key不存在时才Set。可以用来选举Master或做分布式锁：所有Client不断尝试使用SetNx master myName抢注Master，成功的那位不断使用Expire刷新它的过期时间。</p><p>如果Master倒掉了key就会失效，剩下的节点又会发生新一轮抢夺。</p><p>其他Set指令：</p><p><a href="http://redis.readthedocs.org/en/latest/string/setnx.html" target="_blank" rel="noopener">SetEx</a>， Set + Expire 的简便写法，p字头版本以毫秒为单位。</p><p><a href="http://redis.readthedocs.org/en/latest/string/getset.html" target="_blank" rel="noopener">GetSet</a>， 设置新值，返回旧值。比如一个按小时计算的计数器，可以用GetSet获取计数并重置为0。这种指令在服务端做起来是举手之劳，客户端便方便很多。</p><p><a href="http://redis.readthedocs.org/en/latest/string/mget.html" target="_blank" rel="noopener">MGet</a>/<a href="http://redis.readthedocs.org/en/latest/string/mset.html" target="_blank" rel="noopener">MSet</a>/<a href="http://redis.readthedocs.org/en/latest/string/msetnx.html" target="_blank" rel="noopener">MSetNx</a>， 一次get/set多个key。</p><p>2.6.12版开始，Set命令已融合了Set/SetNx/SetEx三者，SetNx与SetEx可能会被废弃，这对Master抢注非常有用，不用担心setNx成功后，来不及执行Expire就倒掉了。可惜有些懒惰的Client并没有快速支持这个新指令。</p><p><a href="http://redis.readthedocs.org/en/latest/string/getbit.html" target="_blank" rel="noopener">GetBit</a>/<a href="http://redis.readthedocs.org/en/latest/string/Setbit.html" target="_blank" rel="noopener">SetBit</a>/<a href="http://redis.readthedocs.org/en/latest/string/bitop.html" target="_blank" rel="noopener">BitOp,与或非</a>/<a href="http://redis.readthedocs.org/en/latest/string/bitcount.html" target="_blank" rel="noopener">BitCount</a>/<a href="http://blog.getspool.com/2011/11/29/fast-easy-realtime-metrics-using-redis-bitmaps/" target="_blank" rel="noopener">BitMap的玩法</a>，比如统计今天的独立访问用户数时，每个注册用户都有一个offset，他今天进来的话就把他那个位设为1，用BitCount就可以得出今天的总人树。</p><p><a href="http://redis.readthedocs.org/en/latest/string/append.html" target="_blank" rel="noopener">Append</a>/<a href="http://redis.readthedocs.org/en/latest/string/setrange.html" target="_blank" rel="noopener">SetRange</a>/<a href="http://redis.readthedocs.org/en/latest/string/getrange.html" target="_blank" rel="noopener">GetRange</a>/<a href="http://redis.readthedocs.org/en/latest/string/strlen.html" target="_blank" rel="noopener">StrLen</a>，对文本进行扩展、替换、截取和求长度，只对特定数据格式如字段定长的有用，json就没什么用。</p><h3 id="Hash"><a href="#Hash" class="headerlink" title="Hash"></a>Hash</h3><p>Key-HashMap结构，相比String类型将这整个对象持久化成JSON格式，Hash将对象的各个属性存入Map里，可以只读取/更新对象的某些属性。</p><p>这样有些属性超长就让它一边呆着不动，另外不同的模块可以只更新自己关心的属性而不会互相并发覆盖冲突。</p><p>另一个用法是土法建索引。比如User对象，除了id有时还要按name来查询。</p><p>可以有如下的数据记录:</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">(String) user:101 -&gt; &#123;&quot;id&quot;:101,&quot;name&quot;:&quot;calvin&quot;...&#125;</span><br><span class="line">(String) user:102 -&gt; &#123;&quot;id&quot;:102,&quot;name&quot;:&quot;kevin&quot;...&#125;</span><br><span class="line">(Hash) user:index-&gt; &quot;calvin&quot;-&gt;101, &quot;kevin&quot; -&gt; 102</span><br></pre></td></tr></table></figure><p>底层实现是<a href="http://www.redisbook.com/en/latest/internal-datastruct/dict.html" target="_blank" rel="noopener">hash table</a>，一般操作复杂度是O(1)，要同时操作多个field时就是O(N)，N是field的数量。</p><h3 id="List"><a href="#List" class="headerlink" title="List"></a>List</h3><p>List是一个<a href="http://www.redisbook.com/en/latest/internal-datastruct/adlist.html" target="_blank" rel="noopener">双向链表</a>，支持双向的Pop/Push，江湖规矩一般从左端Push，右端Pop——<a href="http://redis.readthedocs.org/en/latest/list/lpush.html" target="_blank" rel="noopener">LPush</a>/<a href="http://redis.readthedocs.org/en/latest/list/rpop.html" target="_blank" rel="noopener">RPop</a>，而且还有Blocking的版本<a href="http://redis.readthedocs.org/en/latest/list/blpop.html" target="_blank" rel="noopener">BLPop</a>/<a href="http://redis.readthedocs.org/en/latest/list/brpop.html" target="_blank" rel="noopener">BRPop</a>，客户端可以阻塞在那直到有消息到来，所有操作都是O(1)的好孩子，可以当Message Queue来用。</p><p>当多个Client并发阻塞等待，有消息入列时谁先被阻塞谁先被服务。任务队列系统<a href="http://github.com/defunkt/resque" target="_blank" rel="noopener">Resque</a>是其典型应用。</p><p>还有<a href="http://redis.readthedocs.org/en/latest/list/rpoplpush.html" target="_blank" rel="noopener">RPopLPush</a>/ <a href="http://redis.readthedocs.org/en/latest/list/brpoplpush" target="_blank" rel="noopener">BRPopLPush</a>，弹出来返回给client的同时，把自己又推入另一个list，<a href="http://redis.readthedocs.org/en/latest/list/llen.html" target="_blank" rel="noopener">LLen</a>获取列表的长度。</p><p>还有按值进行的操作：LRem(按值删除元素)、LInsert(插在某个值的元素的前后)，复杂度是O(N)，N是List长度，因为List的值不唯一，所以要遍历全部元素，而Set只要O(log(N))。</p><p>按下标进行的操作：下标从0开始，队列从左到右算，下标为负数时则从右到左。</p><p><a href="http://redis.readthedocs.org/en/latest/list/lset.html" target="_blank" rel="noopener">LSet</a> ，按下标设置元素值。</p><p><a href="http://redis.readthedocs.org/en/latest/list/lindex.html" target="_blank" rel="noopener">LIndex</a>，按下标返回元素。</p><p><a href="http://redis.readthedocs.org/en/latest/list/lrange.html" target="_blank" rel="noopener">LRange</a>，不同于POP直接弹走元素，只是返回列表内一段下标的元素，是分页的最爱。</p><p><a href="http://redis.readthedocs.org/en/latest/list/ltrim.html" target="_blank" rel="noopener">LTrim</a>，限制List的大小，比如只保留最新的20条消息。</p><p>复杂度也是O(N)，其中LSet的N是List长度，LIndex的N是下标的值，LRange的N是start的值+列出元素的个数，因为是链表而不是数组，所以按下标访问其实要遍历链表，除非下标正好是队头和队尾。LTrim的N是移除元素的个数。</p><p>在消息队列中，并没有JMS的ack机制，如果消费者把job给Pop走了又没处理完就死机了怎么办？</p><p>解决方法之一是加多一个sorted set，分发的时候同时发到list与sorted set，以分发时间为score，用户把job做完了之后要用ZREM消掉sorted set里的job，并且定时从sorted set中取出超时没有完成的任务，重新放回list。</p><p>另一个做法是为每个worker多加一个的list，弹出任务时改用RPopLPush，将job同时放到worker自己的list中，完成时用LREM消掉。</p><p>如果集群管理(如zookeeper)发现worker已经挂掉，就将worker的list内容重新放回主list。</p><h3 id="Set"><a href="#Set" class="headerlink" title="Set"></a>Set</h3><p>Set就是Set，可以将重复的元素随便放入而Set会自动去重，底层实现也是<a href="http://www.redisbook.com/en/latest/internal-datastruct/dict.html" target="_blank" rel="noopener">hash table</a>。</p><p><a href="http://redis.readthedocs.org/en/latest/set/sadd.html" target="_blank" rel="noopener">SAdd</a>/<a href="http://redis.readthedocs.org/en/latest/set/srem.html" target="_blank" rel="noopener">SRem</a>/<a href="http://redis.readthedocs.org/en/latest/set/sismember.html" target="_blank" rel="noopener">SISMember</a>/<a href="http://redis.readthedocs.org/en/latest/set/scard.html" target="_blank" rel="noopener">SCard</a>/<a href="http://redis.readthedocs.org/en/latest/set/smove.html" target="_blank" rel="noopener">SMove</a>/<a href="http://redis.readthedocs.org/en/latest/set/smembers.html" target="_blank" rel="noopener">SMembers</a>，各种标准操作。除了SMembers都是O(1)。</p><p><a href="http://redis.readthedocs.org/en/latest/set/sinter.html" target="_blank" rel="noopener">SInter</a>/<a href="http://redis.readthedocs.org/en/latest/set/sinterstore.html" target="_blank" rel="noopener">SInterStore</a>/<a href="http://redis.readthedocs.org/en/latest/set/sunion.html" target="_blank" rel="noopener">SUnion</a>/<a href="http://redis.readthedocs.org/en/latest/set/sunionstore.html" target="_blank" rel="noopener">SUnionStore</a>/<a href="http://redis.readthedocs.org/en/latest/set/sdiff.html" target="_blank" rel="noopener">SDiff</a>/<a href="http://redis.readthedocs.org/en/latest/set/sdiffstore.html" target="_blank" rel="noopener">SDiffStore</a>，各种集合操作。交集运算可以用来显示在线好友(在线用户 交集 好友列表)，共同关注(两个用户的关注列表的交集)。</p><p>O(N)，并集和差集的N是集合大小之和，交集的N是小的那个集合的大小*2。</p><h3 id="Sorted-Set"><a href="#Sorted-Set" class="headerlink" title="Sorted Set"></a>Sorted Set</h3><p>有序集，元素放入集合时还要提供该元素的分数。</p><p><a href="http://redis.readthedocs.org/en/latest/sorted_set/zrange.html" target="_blank" rel="noopener">ZRange</a>/<a href="http://redis.readthedocs.org/en/latest/sorted_set/zrevrange.html" target="_blank" rel="noopener">ZRevRange</a>，按排名的上下限返回元素，正数与倒数。</p><p><a href="http://redis.readthedocs.org/en/latest/sorted_set/zrangebyscore.html" target="_blank" rel="noopener">ZRangeByScore</a>/<a href="http://redis.readthedocs.org/en/latest/sorted_set/zrevrangebyscore.html" target="_blank" rel="noopener">ZRevRangeByScore</a>，按分数的上下限返回元素，正数与倒数。</p><p><a href="http://redis.readthedocs.org/en/latest/sorted_set/zremrangebyrank.html" target="_blank" rel="noopener">ZRemRangeByRank</a>/<a href="http://redis.readthedocs.org/en/latest/sorted_set/zremrangebyscore.html" target="_blank" rel="noopener">ZRemRangeByScore</a>，按排名/按分数的上下限删除元素。</p><p><a href="http://redis.readthedocs.org/en/latest/sorted_set/zcount.html" target="_blank" rel="noopener">ZCount</a>，统计分数上下限之间的元素个数。</p><p><a href="http://redis.readthedocs.org/en/latest/sorted_set/zrank.html" target="_blank" rel="noopener">ZRank</a>/<a href="http://redis.readthedocs.org/en/latest/sorted_set/zrevrank.html" target="_blank" rel="noopener">ZRevRank</a> ，显示某个元素的正倒序的排名。</p><p><a href="http://redis.readthedocs.org/en/latest/sorted_set/zscore.html" target="_blank" rel="noopener">ZScore</a>/<a href="http://redis.readthedocs.org/en/latest/sorted_set/zincrby.html" target="_blank" rel="noopener">ZIncrby</a>，显示元素的分数/增加元素的分数。</p><p><a href="http://redis.readthedocs.org/en/latest/sorted_set/zadd.html" target="_blank" rel="noopener">ZAdd</a>(Add)/<a href="http://redis.readthedocs.org/en/latest/sorted_set/zrem.html" target="_blank" rel="noopener">ZRem</a>(Remove)/<a href="http://redis.readthedocs.org/en/latest/sorted_set/zcard.html" target="_blank" rel="noopener">ZCard</a>(Count)，<a href="http://redis.readthedocs.org/en/latest/sorted_set/zinterstore.html" target="_blank" rel="noopener">ZInsertStore</a>(交集)/<a href="http://redis.readthedocs.org/en/latest/sorted_set/zunionstore.html" target="_blank" rel="noopener">ZUnionStore</a>(并集)，Set操作，与正牌Set相比，少了IsMember和差集运算。</p><p>Sorted Set的实现是hash table(element-&gt;score, 用于实现ZScore及判断element是否在集合内)，和skip list(score-&gt;element,按score排序)的混合体。</p><p><a href="http://www.redisbook.com/en/latest/internal-datastruct/skiplist.html" target="_blank" rel="noopener">skip list</a>有点像平衡二叉树那样，不同范围的score被分成一层一层，每层是一个按score排序的链表。</p><p>ZAdd/ZRem是O(log(N))，ZRangeByScore/ZRemRangeByScore是O(log(N)+M)，N是Set大小，M是结果/操作元素的个数。</p><p>可见，原本可能很大的N被很关键的Log了一下，1000万大小的Set，复杂度也只是几十不到。</p><p>当然，如果一次命中很多元素M很大那谁也没办法了。</p><h3 id="事务"><a href="#事务" class="headerlink" title="事务"></a>事务</h3><p>用<a href="http://redis.readthedocs.org/en/latest/transaction/multi.html" target="_blank" rel="noopener">Multi</a>(Start Transaction)、<a href="http://redis.readthedocs.org/en/latest/transaction/exec.html" target="_blank" rel="noopener">Exec</a>(Commit)、<a href="http://redis.readthedocs.org/en/latest/transaction/discard.html" target="_blank" rel="noopener">Discard</a>(Rollback)实现。</p><p>在事务提交前，不会执行任何指令，只会把它们存到一个队列里，不影响其他客户端的操作。在事务提交时，批量执行所有指令。<a href="http://www.redisbook.com/en/latest/feature/transaction.html" target="_blank" rel="noopener">《Redis设计与实现》</a>中的详述。</p><p>注意，Redis里的事务，与我们平时的事务概念很不一样：</p><p>它仅仅是保证事务里的操作会被连续独占的执行。因为是单线程架构，在执行完事务内所有指令前是不可能再去同时执行其他客户端的请求的。</p><p>它没有隔离级别的概念，因为事务提交前任何指令都不会被实际执行，也就不存在”事务内的查询要看到事务里的更新，在事务外查询不能看到”这个让人万分头痛的问题。</p><p>它不保证原子性——所有指令同时成功或同时失败，只有决定是否开始执行全部指令的能力，没有执行到一半进行回滚的能力。</p><p>在redis里失败分两种，一种是明显的指令错误，比如指令名拼错，指令参数个数不对，在2.6版中全部指令都不会执行。</p><p>另一种是隐含的，比如在事务里，第一句是SET foo bar， 第二句是LLEN foo，对第一句产生的String类型的key执行LLEN会失败，但这种错误只有在指令运行后才能发现，这时候第一句成功，第二句失败。</p><p>还有，如果事务执行到一半redis被KILL，已经执行的指令同样也不会被回滚。</p><p><a href="http://redis.readthedocs.org/en/latest/transaction/watch.html" target="_blank" rel="noopener">Watch</a>指令，类似乐观锁，事务提交时，如果Key的值已被别的客户端改变，比如某个list已被别的客户端push/pop过了，整个事务队列都不会被执行。</p><h3 id="Lua-Script"><a href="#Lua-Script" class="headerlink" title="Lua Script"></a>Lua Script</h3><p>Redis2.6内置的<a href="http://redis.readthedocs.org/en/latest/script/eval.html" target="_blank" rel="noopener">Lua Script</a>支持，可以在Redis的Server端一次过运行大量逻辑，就像存储过程一样，避免了海量中间数据在网路上的传输。</p><p><a href="http://www.lua.org/" target="_blank" rel="noopener">Lua</a>自称是在Script语言里关于快的标准，Redis选择了它而不是流行的JavaScript。</p><p>因为Redis的单线程架构，整个Script默认是在一个事务里的。</p><p>Script里涉及的所有Key尽量用变量，从外面传入，使Redis一开始就知道你要改变哪些key，为了日后做水平分区做准备。如果涉及的key在不同服务器……</p><p>Eval每次传输一整段Script比较费带宽，可以先用<a href="http://redis.readthedocs.org/en/latest/script/script_load.html" target="_blank" rel="noopener">Script Load</a>载入script，返回哈希值。然后用<a href="http://redis.readthedocs.org/en/latest/script/evalsha.html" target="_blank" rel="noopener">EvalHash</a>执行。因为就是SHA-1，所以任何时候执行返回的哈希值都是一样的。</p><p>内置的Lua库里还很贴心的带了CJSON，可以处理json字符串。</p><p>Script一旦执行则不容易中断，中断了也会有不可知后果，因此最好在开发环境充分测试了再上线。</p><p>一段用Redis做Timer的示例代码，下面的script被定期调用，从以触发时间为score的sorted set中取出已到期的Job，放到list中给Client们blocking popup。</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">-- KEYS: [1]job:sleeping, [2]job:ready</span><br><span class="line">-- ARGS: [1]currentTime</span><br><span class="line">-- Comments: result is the  job id</span><br><span class="line">local jobs=redis.call(&apos;zrangebyscore&apos;, KEYS[1], &apos;-inf&apos;, ARGV[1])</span><br><span class="line">local count = table.maxn(jobs)</span><br><span class="line"></span><br><span class="line">if count&gt;0  then</span><br><span class="line">  -- Comments: remove from Sleeping Job sorted set</span><br><span class="line">  redis.call(&apos;zremrangebyscore&apos;, KEYS[1], &apos;-inf&apos;, ARGV[1])</span><br><span class="line">  </span><br><span class="line">  -- Comments: add to the Ready Job list</span><br><span class="line">  -- Comments: can optimize to use lpush id1,id2,... for better performance</span><br><span class="line">  for i=1,count do </span><br><span class="line">    redis.call(&apos;lpush&apos;, KEYS[2], jobs[i])</span><br><span class="line">  end</span><br><span class="line">end</span><br></pre></td></tr></table></figure><h3 id="过期数据清除"><a href="#过期数据清除" class="headerlink" title="过期数据清除"></a>过期数据清除</h3><p><a href="http://redis.io/commands/expire" target="_blank" rel="noopener">官方文档</a> 与 <a href="http://www.redisbook.com/en/latest/internal/db.html#id15" target="_blank" rel="noopener">《Redis设计与实现》</a>中的详述，过期数据的清除从来不容易，为每一条key设置一个timer，到点立刻删除的消耗太大，每秒遍历所有数据消耗也大，Redis使用了一种相对务实的做法：</p><p>当client主动访问key会先对key进行超时判断，过时的key会立刻删除。</p><p>如果clien永远都不再get那条key呢？ 它会在Master的后台，每秒10次的执行如下操作： 随机选取100个key校验是否过期，如果有25个以上的key过期了，立刻额外随机选取下100个key(不计算在10次之内)。</p><p>可见，如果过期的key不多，它最多每秒回收200条左右，如果有超过25%的key过期了，它就会做得更多，但只要key不被主动get，它占用的内存什么时候最终被清理掉只有天知道。</p><h2 id="性能"><a href="#性能" class="headerlink" title="性能"></a>性能</h2><h3 id="测试结果"><a href="#测试结果" class="headerlink" title="测试结果"></a>测试结果</h3><p>测试环境： RHEL 6.3 / HP Gen8 Server/ 2 * Intel Xeon 2.00GHz(6 core) / 64G DDR3 memory / 300G RAID-1 SATA / 1 master(writ AOF), 1 slave(write AOF &amp; RDB)</p><p>数据准备： 预加载两千万条数据，占用10G内存。</p><p>测试工具：自带的redis-benchmark，默认只是基于一个很小的数据集进行测试，调整命令行参数如下，就可以开100条线程(默认50)，SET 1千万次(key在0-1千万间随机)，key长21字节，value长256字节的数据。</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">redis-benchmark -t SET -c 100 -n 10000000 -r 10000000 -d 256</span><br></pre></td></tr></table></figure><p>测试结果(TPS)： 1.SET：4.5万， 2.GET：6万 ，3.INCR：6万，4.真实混合场景: 2.5万SET &amp; 3万GET</p><p>单条客户端线程时6千TPS，50与100条客户端线程差别不大，200条时会略多。</p><p>Get/Set操作，经过了LAN，延时也只有1毫秒左右，可以反复放心调用，不用像调用REST接口和访问数据库那样，每多一次外部访问都心痛。</p><p>资源监控:</p><ol><li>CPU: 占了一个处理器的100%，总CPU是4%(因为总共有2CPU<em>6核</em>超线程 = 24个处理器)，可见单线程下单处理器的能力是瓶颈。 AOF rewrite时另一个处理器占用50-70%。</li><li>网卡：15-20 MB/s receive, 3Mb/s send(no slave) or 15-20 MB/s send (with slave) 。当把value长度加到4K时，receive 99MB/s，已经到达千兆网卡的瓶颈，TPS降到2万。</li><li>硬盘：15MB/s(AOF append), 100MB/s(AOF rewrite/AOF load，普通硬盘的瓶颈)</li></ol><h3 id="为什么快"><a href="#为什么快" class="headerlink" title="为什么快"></a>为什么快</h3><p>纯ANSI C编写。</p><p>不依赖第三方类库，没有像memcached那样使用libevent，因为libevent迎合通用性而造成代码庞大，所以作者用libevent中两个文件修改实现了自己的epoll event loop。微软的兼容Windows补丁也因为同样原因被拒了。</p><p>快，原因之一是Redis多样的数据结构，每种结构只做自己爱做的事，当然比数据库只有Table，MongogoDB只有JSON一种结构快了。</p><p>可惜单线程架构，虽然作者认为CPU不是瓶颈，内存与网络带宽才是。但实际测试时并非如此，见上。</p><h3 id="性能调优"><a href="#性能调优" class="headerlink" title="性能调优"></a>性能调优</h3><p><a href="http://www.redis.io/topics/latency" target="_blank" rel="noopener">官方文档关于各种产生Latency的原因的详细分析, 中文版</a></p><p>正视网络往返时间：</p><p>1.MSet/LPush/ZAdd等都支持一次输入多个Key。</p><p>2.<a href="http://www.redis.io/topics/pipelining" target="_blank" rel="noopener">PipeLining</a>模式 可以一次输入多个指令。</p><p>3.更快的是Lua Script模式，还可以包含逻辑，直接在服务端又get又set的，见2.8 Lua Script。</p><p><a href="http://redis.readthedocs.org/en/latest/server/slowlog.html" target="_blank" rel="noopener">发现执行缓慢的命令</a>，可配置执行超过多少时间的指令算是缓慢指令(默认10毫秒，不含IO时间)，可以用slowlog get 指令查看(默认只保留最后的128条)。</p><p>单线程的模型下，一个请求占掉10毫秒是件大事情，注意设置和显示的单位为微秒。</p><p>CPU永远是瓶颈，但top看到单个CPU 100%时，就是垂直扩展的时候了。</p><p>持久化对性能的影响很大，见5.1持久化。</p><p>要熟悉各指令的复杂度，不过只要不是O(N)一个超大集合，都不用太担心。</p><h2 id="容量"><a href="#容量" class="headerlink" title="容量"></a>容量</h2><h3 id="最大内存"><a href="#最大内存" class="headerlink" title="最大内存"></a>最大内存</h3><p>所有的数据都必须在内存中，原来2.0版的VM策略(将Value放到磁盘，Key仍然放在内存)，2.4版后嫌麻烦又不支持了。</p><p>一定要设置最大内存，否则物理内存用爆了就会大量使用Swap，写RDB文件时的速度慢得你想死。</p><p>多留一倍内存是最安全的。重写AOF文件和RDB文件的进程(即使不做持久化，复制到Slave的时候也要写RDB)会fork出一条新进程来，采用了操作系统的Copy-On-Write策略(子进程与父进程共享Page。</p><p>如果父进程的Page-每页4K有修改，父进程自己创建那个Page的副本，不会影响到子进程，父爱如山)。留意Console打出来的报告，如”RDB: 1215 MB of memory used by copy-on-write”。</p><p>在系统极度繁忙时，如果父进程的所有Page在子进程写RDB过程中都被修改过了，就需要两倍内存。</p><p>按照Redis启动时的提醒，设置 vm.overcommit_memory = 1 ，使得fork()一条10G的进程时，因为COW策略而不一定需要有10G的free memory。</p><p>其他需要考虑的内存包括：</p><p>1.AOF rewrite过程中对新写入命令的缓存(rewrite结束后会merge到新的aof文件)，留意”Background AOF buffer size: 80 MB”的字样。</p><p>2.负责与Slave同步的Client的缓存，默认设置master需要为每个slave预留不高于256M的缓存(见5.1持久化)。</p><p>当最大内存到达时，按照配置的Policy进行处理， 默认策略为volatile-lru，对设置了expire time的key进行LRU清除(不是按实际expire time)。</p><p>如果沒有数据设置了expire time或者policy为noeviction，则直接报错，但此时系统仍支持get之类的读操作。</p><p>另外还有几种policy，比如volatile-ttl按最接近expire time的，allkeys-lru对所有key都做LRU。</p><h3 id="内存占用"><a href="#内存占用" class="headerlink" title="内存占用"></a>内存占用</h3><p>测试表明，string类型需要90字节的额外代价，就是说key 1个字节，value 1个字节时，还是需要占用92字节的长度，而上面的benchmark的记录就占用了367个字节。</p><p>其他类型可根据文档自行计算或实际测试一下。</p><p>使用jemalloc分配内存，删除数据后，内存并不会乖乖还给操作系统而是被Redis截留下来重用到新的数据上，直到Redis重启。</p><p>因此进程实际占用内存是看INFO里返回的used_memory_peak_human。</p><p>Redis内部用了<a href="http://www.redisbook.com/en/latest/compress-datastruct/ziplist.html" target="_blank" rel="noopener">ziplist</a>/<a href="http://www.redisbook.com/en/latest/compress-datastruct/intset.html" target="_blank" rel="noopener">intset</a>这样的压缩结构来减少hash/list/set/zset的存储，默认当集合的元素少于512个且最长那个值不超过64字节时使用，可配置。</p><p>用make 32bit可以编译出32位的版本，每个指针占用的内存更小，但只支持最大4GB内存。</p><h3 id="水平分区，Sharding"><a href="#水平分区，Sharding" class="headerlink" title="水平分区，Sharding"></a>水平分区，Sharding</h3><p>其实，大内存加上垂直分区也够了，不一定非要沙丁一把。</p><p>Jedis支持在客户端做分区，局限是不能动态re-sharding， 有分区的master倒了，不能减少分区必须用slave顶上。要增加分区的话，呃…..</p><p>antire在博客里提到了<a href="http://www.antirez.com/news/44" target="_blank" rel="noopener">Twemproxy</a>，一个Twitter写的Proxy，但它在发现节点倒掉后，只会重新计算一致性哈希环，把数据存到别的master去，而不是集成Sentinel指向新由slave升级的master，像Memcached一样的做法也只适合做Cache的场景。</p><p>Redis-Cluster是今年工作重点，支持automatic re-sharding， 采用和Hazelcast类似的算法，总共有N个分区(eg.N=1024)，每台Server负责若干个分区。</p><p>在客户端先hash出key 属于哪个分区，随便发给一台server，server会告诉它真正哪个Server负责这个分区，缓存下来，下次还有该分区的请求就直接发到地儿了。</p><p>Re-sharding时，会将某些分区的数据移到新的Server上，完成后各Server周知分区<->Server映射的变化，因为分区数量有限，所以通讯量不大。</-></p><p>在迁移过程中，客户端缓存的依然是旧的分区映射信息，原server对于已经迁移走的数据的get请求，会返回一个临时转向的应答，客户端先不会更新Cache。</p><p>等迁移完成了，就会像前面那样返回一条永久转向信息，客户端更新Cache，以后就都去新server了。</p><h2 id="高可用性"><a href="#高可用性" class="headerlink" title="高可用性"></a>高可用性</h2><p>高可用性关乎系统出错时到底会丢失多少数据，多久不能服务。要综合考虑持久化，Master-Slave复制及Fail-Over配置，以及具体Crash情形，比如Master死了，但Slave没死。或者只是Redis死了，操作系统没死等等。</p><h3 id="持久化"><a href="#持久化" class="headerlink" title="持久化"></a>持久化</h3><p>综述： <a href="http://blog.nosqlfan.com/html/3813.html" target="_blank" rel="noopener">解密Redis持久化(中文概括版)</a>, <a href="http://redis.io/topics/persistence" target="_blank" rel="noopener">英文原版</a>，《Redis设计与实现》： <a href="http://www.redisbook.com/en/latest/internal/rdb.html" target="_blank" rel="noopener">RDB</a> 与 <a href="http://www.redisbook.com/en/latest/internal/aof.html" target="_blank" rel="noopener">AOF</a>。</p><p>很多人开始会想象两者是互相结合的，即dump出一个snapshot到RDB文件，然后在此基础上记录变化日志到AOF文件。</p><p>实际上两者毫无关系，完全独立运行，因为作者认为简单才不会出错。如果使用了AOF，重启时只会从AOF文件载入数据，不会再管RDB文件。</p><p>正确关闭服务器：redis-cli shutdown 或者 kill，都会graceful shutdown，保证写RDB文件以及将AOF文件fsync到磁盘，不会丢失数据。</p><p>如果是粗暴的Ctrl+C，或者kill -9 就可能丢失。</p><h3 id="RDB文件"><a href="#RDB文件" class="headerlink" title="RDB文件"></a>RDB文件</h3><p>RDB是整个内存的压缩过的Snapshot，<a href="http://blog.nosqlfan.com/html/3734.html" target="_blank" rel="noopener">RDB的数据结构</a>，可以配置复合的快照触发条件，默认是1分钟内改了1万次，或5分钟内改了10次，或15分钟内改了1次。</p><p>RDB写入时，会连内存一起Fork出一个新进程，遍历新进程内存中的数据写文件，这样就解决了些Snapshot过程中又有新的写入请求进来的问题。 Fork的细节见4.1最大内存。</p><p>RDB会先写到临时文件，完了再Rename成，这样外部程序对RDB文件的备份和传输过程是安全的。而且即使写新快照的过程中Server被强制关掉了，旧的RDB文件还在。</p><p>可配置是否进行压缩，压缩方法是字符串的LZF算法，以及将string形式的数字变回int形式存储。</p><p>动态所有停止RDB保存规则的方法：redis-cli config set save “”</p><h3 id="AOF文件"><a href="#AOF文件" class="headerlink" title="AOF文件"></a>AOF文件</h3><p>操作日志，记录所有有效的写操作，等于mysql的binlog，格式就是明文的<a href="http://redis.io/topics/protocol" target="_blank" rel="noopener">Redis协议</a>的纯文本文件。</p><p>一般配置成每秒调用一次fdatasync将kernel的文件缓存刷到磁盘。当操作系统非正常关机时，文件可能会丢失不超过2秒的数据(更严谨的定义见后)。 如果设为fsync always，性能只剩几百TPS，不用考虑。</p><p>如果设为no，靠操作系统自己的sync，Linux系统一般30秒一次。</p><p>AOF文件持续增长而过大时，会fork出一条新进程来将文件重写(也是先写临时文件，最后再rename，)， 遍历新进程的内存中数据，每条记录有一条的Set语句。</p><p>默认配置是当AOF文件大小是上次rewrite后大小的一倍，且文件大于64M时触发。</p><p><a href="http://redis.io/topics/protocol" target="_blank" rel="noopener">Redis协议</a>，如set mykey hello，将持久化成*3 $3 set $5 mykey $5 hello， 第一个数字代表这条语句有多少元，其他的数字代表后面字符串的长度。</p><p>这样的设计，使得即使在写文件过程中突然关机导致文件不完整，也能自我修复，执行redis-check-aof即可。</p><p>综上所述，RDB的数据不实时，同时使用两者时服务器重启也只会找AOF文件。</p><p>那要不要只使用AOF呢？作者建议不要，因为RDB更适合用于备份数据库(AOF在不断变化不好备份)，快速重启，而且不会有AOF可能潜在的bug，留着作为一个万一的手段。</p><h3 id="读写性能"><a href="#读写性能" class="headerlink" title="读写性能"></a>读写性能</h3><p>AOF重写和RDB写入都是在fork出新进程后，遍历新进程的内存顺序写的，既不阻塞主进程继续处理客户端请求，顺序写的速度也比随机写快。</p><p>测试把刚才benchmark的11G数据写成一个1.3的RDB文件，或者等大的AOF文件rewrite，需要80秒，在redis-cli info中可查看。启动时载入一个AOF或RDB文件的速度与上面写入时相同，在log中可查看。</p><p>Fork一个使用了大量内存的进程也要时间，大约10ms per GB的样子，但Xen在EC2上是让人郁闷的239ms (KVM和VMWare貌似没有这个毛病)，<a href="http://blog.nosqlfan.com/html/3903.html" target="_blank" rel="noopener">各种系统的对比</a>，Info指令里的latest_fork_usec显示上次花费的时间。</p><p>在bgrewriteaof过程中，所有新来的写入请求依然会被写入旧的AOF文件，同时放到buffer中，当rewrite完成后，会在主线程把这部分内容合并到临时文件中之后才rename成新的AOF文件。</p><p>所以rewrite过程中会不断打印”Background AOF buffer size: 80 MB， Background AOF buffer size: 180 MB”，计算系统容量时要留意这部分的内存消耗。</p><p>注意，这个合并的过程是阻塞的，如果你产生了280MB的buffer，在100MB/s的传统硬盘上，Redis就要阻塞2.8秒！！！</p><p>NFS或者Amazon上的EBS都不推荐，因为它们也要消耗带宽。</p><p>bgsave和bgaofrewrite不会被同时执行，如果bgsave正在执行，bgaofrewrite会自动延后。</p><p>2.4版以后，写入AOF时的fdatasync由另一条线程来执行，不会再阻塞主线程。</p><p>2.4版以后，lpush/zadd可以输入一次多个值了，使得AOF重写时可以将旧版本中的多个lpush/zadd指令合成一个，每64个key串一串。</p><h3 id="性能调整"><a href="#性能调整" class="headerlink" title="性能调整"></a>性能调整</h3><p>因为RDB文件只用作后备用途，建议只在Slave上持久化RDB文件，而且只要15分钟备份一次就够了，只保留save 900 1这条规则。</p><p>如果Enalbe AOF，好处是在最恶劣情况下也只会丢失不超过两秒数据，启动脚本较简单只load自己的AOF文件就可以了。</p><p>代价一是带来了持续的IO，二是AOF rewrite的最后将rewrite过程中产生的新数据写到新文件造成的阻塞几乎是不可避免的。</p><p>只要硬盘许可，应该尽量减少AOF rewrite的频率，AOF重写的基础大小默认值64M太小了，可以设到5G以上。</p><p>默认超过原大小100%大小时重写可以改到适当的数值，比如之前的benchmark每个小时会产生40G大小的AOF文件，如果硬盘能撑到半夜系统闲时才用cron调度bgaofrewrite就好了。</p><p>如果不Enable AOF，仅靠Master-Slave Replication 实现高可用性也可以。能省掉一大笔IO也减少了rewrite时带来的系统波动。</p><p>代价是如果Master/Slave同时倒掉，会丢失十几分钟的数据，启动脚本也要比较两个Master/Slave中的RDB文件，载入较新的那个。新浪微博就选用了这种架构，<a href="http://timyang.net/data/redis-misunderstanding/" target="_blank" rel="noopener">见Tim的博客</a></p><h3 id="Trouble-Shooting-——-Enable-AOF可能导致整个Redis被Block住，在2-6-12版之前"><a href="#Trouble-Shooting-——-Enable-AOF可能导致整个Redis被Block住，在2-6-12版之前" class="headerlink" title="Trouble Shooting —— Enable AOF可能导致整个Redis被Block住，在2.6.12版之前"></a>Trouble Shooting —— Enable AOF可能导致整个Redis被Block住，在2.6.12版之前</h3><h4 id="现象描述："><a href="#现象描述：" class="headerlink" title="现象描述："></a>现象描述：</h4><p>当AOF rewrite 15G大小的内存时，Redis整个死掉的样子，所有指令甚至包括slave发到master的ping，redis-cli info都不能被执行。</p><h4 id="原因分析："><a href="#原因分析：" class="headerlink" title="原因分析："></a>原因分析：</h4><p><a href="http://www.redis.io/topics/latency" target="_blank" rel="noopener">官方文档，由IO产生的Latency详细分析</a>, 已经预言了悲剧的发生，但一开始没留意。</p><p>Redis为求简单，采用了单请求处理线程结构。</p><p>打开AOF持久化功能后， Redis处理完每个事件后会调用write(2)将变化写入kernel的buffer，如果此时write(2)被阻塞，Redis就不能处理下一个事件。</p><p>Linux规定执行write(2)时，如果对同一个文件正在执行fdatasync(2)将kernel buffer写入物理磁盘，或者有system wide sync在执行，write(2)会被block住，整个Redis被block住。</p><p>如果系统IO繁忙，比如有别的应用在写盘，或者Redis自己在AOF rewrite或RDB snapshot(虽然此时写入的是另一个临时文件，虽然各自都在连续写，但两个文件间的切换使得磁盘磁头的寻道时间加长），就可能导致fdatasync(2)迟迟未能完成从而block住write(2)，block住整个Redis。</p><p>为了更清晰的看到fdatasync(2)的执行时长，可以使用”strace -p (pid of redis server) -T -e -f trace=fdatasync”，但会影响系统性能。</p><p>Redis提供了一个自救的方式，当发现文件有在执行fdatasync(2)时，就先不调用write(2)，只存在cache里，免得被block。但如果已经超过两秒都还是这个样子，则会硬着头皮执行write(2)，即使redis会被block住。</p><p>此时那句要命的log会打印：“Asynchronous AOF fsync is taking too long (disk is busy?). Writing the AOF buffer without waiting for fsync to complete, this may slow down Redis.” </p><p>之后用redis-cli INFO可以看到aof_delayed_fsync的值被加1。</p><p>因此，对于fsync设为everysec时丢失数据的可能性的最严谨说法是：如果有fdatasync在长时间的执行，此时redis意外关闭会造成文件里不多于两秒的数据丢失。</p><p>如果fdatasync运行正常，redis意外关闭没有影响，只有当操作系统crash时才会造成少于1秒的数据丢失。</p><h4 id="解决方法："><a href="#解决方法：" class="headerlink" title="解决方法："></a>解决方法：</h4><p>最后发现，原来是AOF rewrite时一直埋头的调用write(2)，由系统自己去触发sync。在RedHat Enterprise 6里，默认配置vm.dirty_background_ratio=10，也就是占用了10%的可用内存才会开始后台flush，而我的服务器有64G内存。</p><p>很明显一次flush太多数据会造成阻塞，所以最后果断设置了sysctl vm.dirty_bytes=33554432(32M)，问题解决。</p><p>然后提了个issue，<a href="https://github.com/antirez/redis/issues/1019" target="_blank" rel="noopener">AOF rewrite时定时也执行一下fdatasync嘛</a>， antirez三分钟后就回复了，新版中，AOF rewrite时32M就会重写主动调用fdatasync。</p><h3 id="Master-Slave复制"><a href="#Master-Slave复制" class="headerlink" title="Master-Slave复制"></a>Master-Slave复制</h3><h4 id="概述"><a href="#概述" class="headerlink" title="概述"></a>概述</h4><p>slave可以在配置文件、启动命令行参数、以及redis-cli执行SlaveOf指令来设置自己是奴隶。</p><p>测试表明同步延时非常小，指令一旦执行完毕就会立刻写AOF文件和向Slave转发，除非Slave自己被阻塞住了。</p><p>比较蠢的是，即使在配置文件里设了slavof，slave启动时依然会先从数据文件载入一堆没用的数据，再去执行slaveof。</p><p>“Slaveof no one”，立马变身master。</p><p>2.8版本将支持<a href="http://www.antirez.com/news/47" target="_blank" rel="noopener">PSYNC部分同步</a>，master会拨出一小段内存来存放要发给slave的指令，如果slave短暂的断开了，重连时会从内存中读取需要补读的指令，这样就不需要断开两秒也搞一次全同步了。</p><p>但如果断开时间较长，已经超过了内存中保存的数据，就还是要全同步。</p><p>Slave也可以接收Read-Only的请求。</p><h4 id="slaveof执行过程，完全重用已有功能，非常经济"><a href="#slaveof执行过程，完全重用已有功能，非常经济" class="headerlink" title="slaveof执行过程，完全重用已有功能，非常经济"></a>slaveof执行过程，完全重用已有功能，非常经济</h4><p>先执行一次全同步 – 请求master BgSave出自己的一个RDB Snapshot文件发给slave，slave接收完毕后，清除掉自己的旧数据，然后将RDB载入内存。</p><p>再进行增量同步 – master作为一个普通的client连入slave，将所有写操作转发给slave，没有特殊的同步协议。</p><h4 id="Trouble-Shooting-again"><a href="#Trouble-Shooting-again" class="headerlink" title="Trouble Shooting again"></a>Trouble Shooting again</h4><p>有时候明明master/slave都活得好好的，突然间就说要重新进行全同步了：</p><p>1）Slave显示：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line"># MASTER time out: no data nor PING received...</span><br></pre></td></tr></table></figure><p>slave会每隔repl-ping-slave-period(默认10秒)ping一次master，如果超过repl-timeout(默认60秒)都没有收到响应，就会认为Master挂了。</p><p>如果Master明明没挂但被阻塞住了也会报这个错。可以适当调大repl-timeout。</p><p>2）Master显示：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line"># Client addr=10.175.162.123:44670 flags=S oll=104654 omem=2147487792 events=rw cmd=sync scheduled to be closed ASAP for overcoming of output buffer limits.</span><br></pre></td></tr></table></figure><p>当slave没挂但被阻塞住了，比如正在loading Master发过来的RDB， Master的指令不能立刻发送给slave，就会放在output buffer中(见oll是命令数量，omem是大小)，</p><p>在配置文件中有如下配置：client-output-buffer-limit slave 256mb 64mb 60， 这是说负责发数据给slave的client，如果buffer超过256m或者连续60秒超过64m，就会被立刻强行关闭！！！ Traffic大的话一定要设大一点。</p><p>否则就会出现一个很悲剧的循环，Master传输一个大的RDB给Slave，Slave努力的装载，但还没装载完，Master对client的缓存满了，再来一次。</p><p>平时可以在master执行 <a href="http://redis.io/commands/client-list" target="_blank" rel="noopener">redis-cli client list</a> 找那个cmd=sync，flag=S的client，注意OMem的变化。</p><h3 id="Fail-Over"><a href="#Fail-Over" class="headerlink" title="Fail-Over"></a>Fail-Over</h3><p>Redis-sentinel是2.6版开始加入的另一组独立运行的节点，提供自动Fail Over的支持。</p><p><a href="http://redis.io/topics/sentinel" target="_blank" rel="noopener">官方文档</a> 与 <a href="http://www.wzxue.com/redis%E6%A0%B8%E5%BF%83%E8%A7%A3%E8%AF%BB-%E9%9B%86%E7%BE%A4%E7%AE%A1%E7%90%86%E5%B7%A5%E5%85%B7redis-sentinel/" target="_blank" rel="noopener">Redis核心解读–集群管理工具(Redis-sentinel)</a></p><p><a href="http://www.antirez.com/news/55" target="_blank" rel="noopener">antirez 对 Sentinel的反驳</a>，与<a href="http://www.antirez.com/news/56" target="_blank" rel="noopener">下篇</a></p><h4 id="主要执行过程"><a href="#主要执行过程" class="headerlink" title="主要执行过程"></a>主要执行过程</h4><p>Sentinel每秒钟对所有master，slave和其他sentinel执行Ping，redis-server节点要应答+PONG或-LOADING或-MASTERDOWN.</p><p>如果某一台Sentinel没有在30秒内(可配置得短一些哦)收到上述正确应答，它就会认为master处于sdown状态(主观Down)</p><p>它向其他sentinel询问是否也认为该master倒了（SENTINEL is-master-down-by-addr ）， 如果quonum台(默认是2)sentinel在5秒钟内都这样认为，就会认为master真是odown了(客观Down)。</p><p>此时会选出一台sentinel作为Leader执行fail-over, Leader会从slave中选出一个提升为master(执行slaveof no one)，然后让其他slave指向它(执行slaveof new master)。</p><h4 id="master-slave-及-其他sentinel的发现"><a href="#master-slave-及-其他sentinel的发现" class="headerlink" title="master/slave 及 其他sentinel的发现"></a>master/slave 及 其他sentinel的发现</h4><p>master地址在sentinel.conf里, sentinel会每10秒一次向master发送INFO，知道master的slave有哪些。 如果master已经变为slave，sentinel会分析INFO的应答指向新的master。</p><p>以前，sentinel重启时，如果master已经切换过了，但sentinel.conf里master的地址并没有变，很可能有悲剧发生。</p><p>另外master重启后如果没有切换成slave，也可能有悲剧发生。新版好像修复了一点这个问题，待研究。</p><p>另外，sentinel会在master上建一个pub/sub channel，名为”sentinel:hello”，通告各种信息，sentinel们也是通过接收pub/sub channel上的+sentinel的信息发现彼此，因为每台sentinel每5秒会发送一次自己的host信息，宣告自己的存在。</p><h4 id="自定义reconfig脚本"><a href="#自定义reconfig脚本" class="headerlink" title="自定义reconfig脚本"></a>自定义reconfig脚本</h4><p>sentinel在failover时还会执行配置文件里指定的用户自定义reconfig脚本，做用户自己想做的事情，比如让master变为slave并指向新的master。</p><p>脚本的将会在命令行按顺序传入如下参数： <master-name> &lt;role(leader/observer)&gt; &lt;state(上述三种情况)&gt; <from-ip> <from-port> <to-ip> <to-port></to-port></to-ip></from-port></from-ip></master-name></p><p>脚本返回0是正常，如果返回1会被重新执行，如果返回2或以上不会。 如果超过60秒没返回会被强制终止。</p><p>觉得Sentinel至少有两个可提升的地方:</p><p>一是如果master 主动shutdown，比如系统升级，有办法主动通知sentinel提升新的master，减少服务中断时间。</p><p>二是比起redis-server太原始了，要自己丑陋的以nohup sentinel &gt; logfile 2&gt;&amp;1 &amp; 启动，也不支持shutdown命令，要自己kill pid。</p><h3 id="Client的高可用性"><a href="#Client的高可用性" class="headerlink" title="Client的高可用性"></a>Client的高可用性</h3><p>基于Sentinel的方案，client需要执行语句SENTINEL get-master-addr-by-name mymaster 可获得当前master的地址。</p><p>Jedis正在集成sentinel，已经支持了sentinel的一些指令，但还没发布，但sentinel版的连接池则暂时完全没有，在公司的项目里我参考<a href="https://github.com/hamsterready/jedis-sentinel-pool" target="_blank" rel="noopener">网友的项目</a>自己写了一个。</p><p><a href="https://github.com/taobao/tedis" target="_blank" rel="noopener">淘宝的Tedis driver</a>，使用了完全不同的思路，不基于Sentinel，而是多写随机读， 一开始就同步写入到所有节点，读的话随便读一个还活着的节点就行了。</p><p>但有些节点成功有些节点失败如何处理? 节点死掉重新起来后怎么重新同步?什么时候可以重新Ready? 所以不是很敢用。</p><p>另外如Ruby写的<a href="https://github.com/ryanlecompte/redis_failover" target="_blank" rel="noopener">redis_failover</a>，也是抛开了Redis Sentinel，基于ZooKeeper的临时方案。</p><p>Redis作者也在博客里抱怨<a href="http://www.antirez.com/news/33" target="_blank" rel="noopener">怎么没有人做Dynamo-style 的client</a>。</p><h3 id="Geographic-Replication"><a href="#Geographic-Replication" class="headerlink" title="Geographic Replication"></a>Geographic Replication</h3><p>没有特别支持，依然用Master Slave复制，<a href="http://3scale.github.io/2012/07/25/fun-with-redis-replication/" target="_blank" rel="noopener">3Scale想出了诸如用压缩的SSH隧道降低传输量等方法</a>。</p><h2 id="运维"><a href="#运维" class="headerlink" title="运维"></a>运维</h2><h3 id="安装"><a href="#安装" class="headerlink" title="安装"></a>安装</h3><p>安装包制作：没有现成，需要自己编译，自己写rpm包的脚本，可参考utils中的install_server.sh与redis_init_script。</p><p>但RHEL下设定script runlevel的方式不一样，redis_init_script中要增加一句 “# chkconfig: 345 90 10” ，而install_server.sh可以删掉后面的那句<code>chkconfig --level 345 reis</code></p><p>云服务：<a href="http://redis-cloud.com/" target="_blank" rel="noopener">Redis Cloud</a>，在Amazon、Heroku、Windows Azure、App Frog上提供云服务，供同样部署在这些云上的应用使用。</p><p>其他的云服务有<a href="http://garantiadata.com/" target="_blank" rel="noopener">GarantiaData</a>，已被redis-cloud收购。另外还有<a href="http://redistogo.com/" target="_blank" rel="noopener">Redis To Go</a>, <a href="https://openredis.com/" target="_blank" rel="noopener">OpenRedis</a>, <a href="http://www.redisgreen.net/" target="_blank" rel="noopener">RedisGreen</a>。</p><p>CopperEgg统计自己的用户在AWS上的数据库部署：mysqld占了50%半壁江山, redis占了18%排第二, mongodb也有11%, cassandra是3%，Oracle只有可怜的2%。</p><p>Chef Recipes：<a href="https://github.com/brianbianco/redisio" target="_blank" rel="noopener">brianbianco/redisio</a>，活跃，同步更新版本。</p><h3 id="部署模型"><a href="#部署模型" class="headerlink" title="部署模型"></a>部署模型</h3><p>Redis只能使用单线程，为了提高CPU利用率，有提议在同一台服务器上启动多个Redis实例，但这会带来严重的IO争用，除非Redis不需要持久化，或者有某种方式保证多个实例不会在同一个时间重写AOF。</p><p>一组sentinel能同时监控多个Master。</p><p>有提议说环形的slave结构，即master只连一个slave，然后slave再连slave，此部署有两个前提，一是有大量的只读需求需要在slave完成，二是对slave传递时的数据不一致性不敏感。</p><h3 id="配置"><a href="#配置" class="headerlink" title="配置"></a>配置</h3><p>约30个配置项，全都有默认配置，对redif.conf默认配置的修改见附录1。</p><h4 id="三条路"><a href="#三条路" class="headerlink" title="三条路"></a>三条路</h4><p>可以配置文件中编写。</p><p>可以在启动时的命令行配置，redis-server –port 7777 –slaveof 127.0.0.1 8888。</p><p>云时代大规模部署，把配置文件满街传显然不是好的做法， 可以用redis-cli执行<a href="http://redis.readthedocs.org/en/latest/server/config_set.html" target="_blank" rel="noopener">Config Set</a>指令， 修改所有的参数，达到维护人员最爱的不重启服务而修改参数的效果，而且在新版本里还可以执行 <a href="http://antirez.com/news/54" target="_blank" rel="noopener">Config Rewrite</a> 将改动写回到文件中，不过全部默认值都会打印出来，可能会破坏掉原来的文件的排版，注释。</p><h4 id="安全保护"><a href="#安全保护" class="headerlink" title="安全保护"></a>安全保护</h4><p>在配置文件里设置密码：requirepass foobar。</p><p>禁止某些危险命令，比如残暴的FlushDB，将它rename成””：rename-command FLUSHDB “”。</p><h3 id="监控与维护"><a href="#监控与维护" class="headerlink" title="监控与维护"></a>监控与维护</h3><p>综述： <a href="http://blog.nosqlfan.com/html/4166.html" target="_blank" rel="noopener">Redis监控技巧</a></p><h4 id="监控指令"><a href="#监控指令" class="headerlink" title="监控指令"></a>监控指令</h4><p><a href="http://redis.readthedocs.org/en/latest/server/info.html" target="_blank" rel="noopener">Info</a>指令将返回非常丰富的信息。 着重监控检查内存使用，是否已接近上限，used_memory是Redis申请的内存，used_memory_rss是操作系统分配给Redis的物理内存，两者之间隔着碎片，隔着Swap。</p><p>还有重点监控 AOF与RDB文件的保存情况，以及master-slave的关系。Statistic 信息还包括key命中率，所有命令的执行次数，所有client连接数量等， <a href="http://redis.readthedocs.org/en/latest/server/config_resetstat.html" target="_blank" rel="noopener">CONFIG RESETSTAT</a> 可重置为0。</p><p><a href="http://redis.readthedocs.org/en/latest/server/monitor.html" target="_blank" rel="noopener">Monitor</a>指令可以显示Server收到的所有指令，主要用于debug，影响性能，生产环境慎用。</p><p>SlowLog 检查慢操作(见2.性能)。</p><h4 id="Trouble-Shooting支持"><a href="#Trouble-Shooting支持" class="headerlink" title="Trouble Shooting支持"></a>Trouble Shooting支持</h4><p>日志可以动态的设置成verbose/debug模式，但不见得有更多有用的log可看,verbose还会很烦的每5秒打印当前的key情况和client情况。指令为config set loglevel verbose。</p><p>最爱Redis的地方是代码只有2.3万行，而且编码优美，而且huangz同学还在原来的注释上再加上了中文注释——<a href="https://github.com/huangz1990/annotated_redis_source/" target="_blank" rel="noopener">Redis 2.6源码中文注释版</a> ，所以虽然是C写的代码，虽然有十年没看过C代码，但这几天trouble shooting毫无难度，一看就懂。</p><p>Trobule shotting的经历证明antirez处理issue的速度非常快(如果你的issue言之有物的话)，比Weblogic之类的商业支持还好。</p><h4 id="持久化文件维护"><a href="#持久化文件维护" class="headerlink" title="持久化文件维护"></a>持久化文件维护</h4><p>如果AOF文件在写入过程中crash，可以用redis-check-aof修复，见5.1.2</p><p>如果AOF rewrite和 RDB snapshot的过程中crash，会留下无用的临时文件，需要定期扫描删除。</p><h4 id="三方工具"><a href="#三方工具" class="headerlink" title="三方工具"></a>三方工具</h4><p>官网列出了如下工具，但暂时没发现会直接拿来用的：</p><p><a href="http://www.nkrode.com/article/real-time-dashboard-for-redis" target="_blank" rel="noopener">Redis Live</a>，基于Python的web应用，使用Info和Monitor获得系统情况和指令统计分析。 因为Monitor指令影响性能，所以建议用cron定期运行，每次偷偷采样两分钟的样子。</p><p><a href="https://github.com/ErikDubbelboer/phpRedisAdmin" target="_blank" rel="noopener">phpRedisAdmin</a>，基于php的Web应用，目标是MysqlAdmin那样的管理工具，可以管理每一条Key的情况，但它的界面应该只适用于Key的数量不太多的情况，Demo。</p><p><a href="https://github.com/Instagram/redis-faina" target="_blank" rel="noopener">Redis Faina</a>，基于Python的命令行，Instagram出品，用户自行获得Monitor的输出后发给它进行统计分析。由于Monitor输出的格式在Redis版本间不一样，要去github下最新版。</p><p><a href="https://github.com/sripathikrishnan/redis-rdb-tools" target="_blank" rel="noopener">Redis-rdb-tools</a> 基于Python的命令行，可以分析RDB文件每条Key对应value所占的大小，还可以将RDB dump成普通文本文件然后比较两个库是否一致，还可以将RDB输出成JSON格式，可能是最有用的一个了。</p><p><a href="https://github.com/antirez/redis-sampler" target="_blank" rel="noopener">Redis Sampler</a>，基于Ruby的命令行，antirez自己写的，统计数据分布情况。</p><h2 id="Java-Driver"><a href="#Java-Driver" class="headerlink" title="Java Driver"></a>Java Driver</h2><h3 id="Driver选择"><a href="#Driver选择" class="headerlink" title="Driver选择"></a>Driver选择</h3><p>各个Driver好像只有<a href="https://github.com/xetorthio/jedis/" target="_blank" rel="noopener">Jedis</a>比较活跃，但也5个月没提交了，也是Java里唯一的Redis官方推荐。</p><p><a href="http://www.springsource.org/spring-data/redis" target="_blank" rel="noopener">Spring Data Redis</a>的封装并不太必要，因为Jedis已足够简单，没有像Spring Data MongoDB对MongoDB java driver的封装那样大幅简化代码，顶多就是加强了一点点点pipeline和transaction状态下的coding，禁止了一些此状态下不能用的命令。</p><p>而所谓屏蔽各种底层driver的差异并不太吸引人，因为我就没打算选其他几种driver。有兴趣的可以翻翻<a href="https://github.com/SpringSource/spring-data-redis/blob/master/src/main/java/org/springframework/data/redis/connection/jedis/JedisConnection.java" target="_blank" rel="noopener">它的JedisConnection代码</a>。</p><p>所以，SpringSide直接在Jedis的基础上，按Spring的风格封装了一个JedisTemplate，负责从池中获取与归还Jedis实例，处理异常。</p><h3 id="Jedis的细节"><a href="#Jedis的细节" class="headerlink" title="Jedis的细节"></a>Jedis的细节</h3><p>Jedis基于Apache Commons Pool做的连接池，默认MaxActive最大连接数只有8，必须重新设置。而且MaxIdle也要相应增大，否则所有新建的连接用完即弃，然后会不停的重新连接。</p><p>另外Jedis设定了每30秒对所有连接执行一次ping，以发现失效的连接，这样每30秒会有一个拿不到连接的高峰。</p><p>但效果如何需要独立分析。比如系统高峰之后可能有一长段时间很闲，而且Redis Server那边做了Timeout控制会把连接断掉，这时候做idle checking是有意义的，但30秒一次也太过频繁了。否则关掉它更好。</p><p>Jedis的blocking pop函数，应用执行ExecutorService.shutdownNow()中断线程时并不能把它中断，见讨论组。</p><p>两个解决方法：</p><p>不要用不限时的blocking popup，传多一个超时时间参数，如5秒。</p><p>找地方将调用blocking popup的jedis保存起来，shutdown时主动调用它的close。</p><h3 id="Redis对Client端连接的处理"><a href="#Redis对Client端连接的处理" class="headerlink" title="Redis对Client端连接的处理"></a>Redis对Client端连接的处理</h3><p>Redis默认最大连接数是一万。</p><p>Redis默认不对Client做Timeout处理，可以用timeout 项配置，但即使配了也不会非常精确。</p><h2 id="Windows的版本"><a href="#Windows的版本" class="headerlink" title="Windows的版本"></a>Windows的版本</h2><p>Windows版本方便对应用的本地开发调试，但Redis并没有提供，好在微软提供了一个依赖LibUV实现兼容的补丁，<a href="https://github.com/MSOpenTech/redis%EF%BC%8C%E4%BD%86redis%E4%BD%9C%E8%80%85%E6%8B%92%E7%BB%9D%E5%90%88%E5%B9%B6%E5%88%B0master%E4%B8%AD%EF%BC%8C%E5%BE%AE%E8%BD%AF%E5%8F%AA%E5%A5%BD%E8%8B%A6%E6%86%8B%E7%9A%84%E6%97%B6%E6%97%B6%E4%BA%BA%E5%B7%A5%E5%90%8C%E6%AD%A5%E3%80%82" target="_blank" rel="noopener">https://github.com/MSOpenTech/redis，但redis作者拒绝合并到master中，微软只好苦憋的时时人工同步</a>。</p><p>目前的稳定版是2.6版本，支持Lua脚本。</p><p>因为github现在已经没有Download服务了，所以编译好的可执行文件藏在这里：</p><p><a href="https://github.com/MSOpenTech/redis/tree/2.6/bin/release" target="_blank" rel="noopener">https://github.com/MSOpenTech/redis/tree/2.6/bin/release</a></p><h2 id="成功案例"><a href="#成功案例" class="headerlink" title="成功案例"></a>成功案例</h2><p>注：下文中的链接都是网站的架构描述文档。</p><p><a href="http://www.infoq.com/presentations/Real-Time-Delivery-Twitter" target="_blank" rel="noopener">Twitter</a>和<a href="http://www.infoq.com/cn/presentations/tfl-sina-weibo-platform-redis-practice" target="_blank" rel="noopener">新浪微博</a>， 都属于将Redis各种数据结构用得出神入化的那种，如何发布大V如奥巴马的消息是它们最头痛的问题。</p><p><a href="http://highscalability.com/blog/2013/5/20/the-tumblr-architecture-yahoo-bought-for-a-cool-billion-doll.html" target="_blank" rel="noopener">Tumblr</a>： 11亿美刀卖给Yahoo的图片日志网站，22 台Redis server，每台运行8 - 32个实例，总共100多个Redis实例在跑。</p><p>有着Redis has been completely problem free and the community is great的崇高评价。Redis在里面扮演了八爪鱼多面手的角色：</p><p>Dashboard的海量通知的存储。</p><p>Dashboard的二级索引。</p><p>存储海量短链接的HBase前面的缓存。</p><p>Gearman Job Queue的存储。</p><p>正在替换另外30台memcached。</p><p><a href="http://highscalability.com/blog/2011/12/6/instagram-architecture-14-million-users-terabytes-of-photos.html" target="_blank" rel="noopener">Instagram</a> ，曾经，Redis powers their main feed, activity feed, sessions system, and <a href="http://highscalability.com/blog/2012/4/16/instagram-architecture-update-whats-new-with-instagram.html" target="_blank" rel="noopener">other services</a>。</p><p>但可惜目前<a href="http://planetcassandra.org/blog/post/instagram-making-the-switch-to-cassandra-from-redis-75-instasavings" target="_blank" rel="noopener">已迁往Cassandra</a>，说新架构只需1/4的硬件费用，是的，就是那个导致Digg CTO辞职的Canssandra。</p><p><a href="http://code.flickr.net/2011/10/11/talk-real-time-updates-on-the-cheap-for-fun-and-profit/" target="_blank" rel="noopener">Flickr</a> , 依然是asynchronous task system and rudimentary queueing system。之前Task system放在mysql innodb，根本，撑不住。</p><p>The Others：</p><p><a href="http://nosql.mypopescu.com/post/17658415847/polyglot-persistence-at-pinterest-redis-membase" target="_blank" rel="noopener">Pinterest</a>，混合使用MySQL、Membase与Redis作为存储。</p><p><a href="http://highscalability.com/blog/2012/4/2/youporn-targeting-200-million-views-a-day-and-beyond.html" target="_blank" rel="noopener">Youporn.com</a>，100%的Redis，MySQL只用于创建新需求用到的sorted set，300K QPS的大压力。</p><p><a href="http://tech.naver.jp/blog/?p=1420" target="_blank" rel="noopener">日本微信</a> ，Redis在前负责异步Job Queue和O(n)的数据，且作为O(n<em>t)数据的cache，HBase在后，负责O(n</em>t)数据， n是用户，t是时间。</p><p><a href="http://highscalability.com/blog/2011/3/3/stack-overflow-architecture-update-now-at-95-million-page-vi.html" target="_blank" rel="noopener">StackOverflow</a> ，2 Redis servers for distribute caching，好穷好轻量。</p><p><a href="https://github.com/blog/530-how-we-made-github-fast" target="_blank" rel="noopener">Github</a>，任务系统<a href="http://github.com/defunkt/resque" target="_blank" rel="noopener">Resque</a>的存储。</p><p><a href="https://github.com/discourse/discourse" target="_blank" rel="noopener">Discourge</a>，号称是为下一个十年打造的论坛系统， We use Redis for our job queue, rate limiting, as a cache and for transient data，刚好和我司的用法一样。</p><p>情色网站 YouPorn，使用 Redis 进行数据存储，Redis 服务器每秒处理30万个页面请求，每小时会记录8-15GB数据。</p><h2 id="In-SpringSide"><a href="#In-SpringSide" class="headerlink" title="In SpringSide"></a>In SpringSide</h2><p>extension modules项目封装了常用的函数与场景，showcase example的src/demo/redis目录里有各场景的benchmark测试。</p><h3 id="Jedis-Template"><a href="#Jedis-Template" class="headerlink" title="Jedis Template"></a>Jedis Template</h3><p>典型的Spring Template风格，和JdbcTemplate，HibernateTemplate一样，封装从JedisPool获取与归还Connecton的代码，有带返回值与无返回值两种返回接口。</p><p>同时，对最常用的Jedis调用，直接封装了一系列方法。</p><h3 id="Scheduler与Master-Elector"><a href="#Scheduler与Master-Elector" class="headerlink" title="Scheduler与Master Elector"></a>Scheduler与Master Elector</h3><p>Scheduler实现了基于Redis的高并发单次定时任务分发。具体选型见<a href="https://github.com/springside/springside4/wiki/Schedule" target="_blank" rel="noopener">Scheduler</a>章节。</p><p>Master Elector基于redis setNx()与expire()两个api实现，与基于Zookeeper，Hazelcast实现的效果类似。</p><h3 id="Showcase中的Demo"><a href="#Showcase中的Demo" class="headerlink" title="Showcase中的Demo"></a>Showcase中的Demo</h3><p>计有Session，Counter，Scheduler 与 Master Elector四款。</p><h2 id="附录"><a href="#附录" class="headerlink" title="附录"></a>附录</h2><h3 id="附录1：-对redis-conf默认配置的修改"><a href="#附录1：-对redis-conf默认配置的修改" class="headerlink" title="附录1： 对redis.conf默认配置的修改"></a>附录1： 对redis.conf默认配置的修改</h3><h4 id="Master主机"><a href="#Master主机" class="headerlink" title="Master主机"></a>Master主机</h4><p>daemonize no -&gt; yes ，启动daemonize模式，注意如果用daemon工具启动redis-server时设回false。<br>logfile stdout -&gt; /var/log/redis/redis.log ，指定日志文件<br>注释掉RDB的所有触发规则，在Master不保存RDB文件。<br>dir ./ -&gt; /var/data/redis，指定持久化文件及临时文件目录.<br>maxmemory，设置为可用内存/2.<br>(可选)appendonly no-&gt;yes，打开AOF文件.<br>auto-aof-rewrite-percentage 100, 综合考虑硬盘大小，可接受重启加载延时等尽量的大，减少AOF rewrite频率.<br>auto-aof-rewrite-min-size 64mb，同上，起码设为5G.<br>client-output-buffer-limit slave 256mb 64mb 60. 考虑Traffic及Slave同步是RDB加载所需时间，正确设置避免buffer撑爆client被关掉后又要重新进行全同步。<br>Master上的安全配置，可选。</p><h4 id="Slave主机"><a href="#Slave主机" class="headerlink" title="Slave主机"></a>Slave主机</h4><p>设置RDB保存频率，因为RDB只作为Backup工具，只保留15分钟的规则，设置为15分钟保存一次就够了save 900 1。</p><p>(可选)slaveof 设置master地址，也可动态设定。</p><p>repl-timeout 60, 适当加大比如120，避免master实际还没倒掉就认为master倒了。</p><h3 id="附录2：版本变更历史"><a href="#附录2：版本变更历史" class="headerlink" title="附录2：版本变更历史"></a>附录2：版本变更历史</h3><p>3.0.1版-3.0.3版 2013-8-1，在微博发布后反应良好，持续修改。</p><p>3.0版 2013-6-29，在公司Workshop后修订，提高wiki的可读性而不只是简单的记录知识点。</p><h3 id="附录3：其他参考资料"><a href="#附录3：其他参考资料" class="headerlink" title="附录3：其他参考资料"></a>附录3：其他参考资料</h3><p><a href="http://timyang.net/data/redis-misunderstanding/" target="_blank" rel="noopener">Redis的几个认识误区</a> by Tim yang。</p>]]></content>
    
    <summary type="html">
    
      Redis数据结构使用以及注意事项，运维问题总结
    
    </summary>
    
      <category term="Redis" scheme="https://ningyu1.github.io/categories/Redis/"/>
    
    
      <category term="Redis" scheme="https://ningyu1.github.io/tags/Redis/"/>
    
      <category term="AOF Block" scheme="https://ningyu1.github.io/tags/AOF-Block/"/>
    
      <category term="AOF" scheme="https://ningyu1.github.io/tags/AOF/"/>
    
      <category term="Asynchronous AOF fsync is taking too long (disk is busy?)Writing the AOF buffer without waiting for fsync to complete, this may slow down Redis" scheme="https://ningyu1.github.io/tags/Asynchronous-AOF-fsync-is-taking-too-long-disk-is-busy-Writing-the-AOF-buffer-without-waiting-for-fsync-to-complete-this-may-slow-down-Redis/"/>
    
  </entry>
  
  <entry>
    <title>Trouble Shooting —— Enable AOF可能导致整个Redis被Block住，在3.0.6版本仍然存在</title>
    <link href="https://ningyu1.github.io/20171009/32-redis-aof.html"/>
    <id>https://ningyu1.github.io/20171009/32-redis-aof.html</id>
    <published>2017-10-09T01:53:36.000Z</published>
    <updated>2024-04-23T01:48:15.033Z</updated>
    
    <content type="html"><![CDATA[<h2 id="Redis会有短暂的几秒Block，应用报：Jedis-connection-failed-retrying…"><a href="#Redis会有短暂的几秒Block，应用报：Jedis-connection-failed-retrying…" class="headerlink" title="Redis会有短暂的几秒Block，应用报：Jedis connection failed, retrying…"></a>Redis会有短暂的几秒Block，应用报：Jedis connection failed, retrying…</h2><p>这个问题现象是这样的，应用周期性的报：Jedis connection failed, retrying…，Redis开启AOF会被Block住导致无法连接，查看redis的日志</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">1486:M 09 Oct 09:33:18.072 * 10 changes in 300 seconds. Saving...</span><br><span class="line">1486:M 09 Oct 09:33:18.075 * Background saving started by pid 20706</span><br><span class="line">1486:M 09 Oct 09:33:34.011 * Asynchronous AOF fsync is taking too long (disk is busy?). Writing the AOF buffer without waiting for fsync to complete, this may slow down Redis.</span><br><span class="line">20706:C 09 Oct 09:33:42.629 * DB saved on disk</span><br><span class="line">20706:C 09 Oct 09:33:42.630 * RDB: 178 MB of memory used by copy-on-write</span><br><span class="line">1486:M 09 Oct 09:33:42.723 * Background saving terminated with success</span><br></pre></td></tr></table></figure><p>重点：<code>Asynchronous AOF fsync is taking too long (disk is busy?). Writing the AOF buffer without waiting for fsync to complete, this may slow down Redis.</code></p><p>为什么每次写入磁盘会有disk is busy？这个问题？</p><p>网上有人写到：当AOF rewrite 15G大小的内存时，Redis整个死掉的样子，所有指令甚至包括slave发到master的ping，redis-cli info都不能被执行。</p><h2 id="原因分析"><a href="#原因分析" class="headerlink" title="原因分析"></a>原因分析</h2><p><a href="http://www.redis.io/topics/latency" target="_blank" rel="noopener">官方文档，由IO产生的Latency详细分析</a>, 已经预言了悲剧的发生，但一开始没留意。</p><p>Redis为求简单，采用了单请求处理线程结构。</p><p>打开AOF持久化功能后， Redis处理完每个事件后会调用write(2)将变化写入kernel的buffer，如果此时write(2)被阻塞，Redis就不能处理下一个事件。</p><p>Linux规定执行write(2)时，如果对同一个文件正在执行fdatasync(2)将kernel buffer写入物理磁盘，或者有system wide sync在执行，write(2)会被Block住，整个Redis被Block住。</p><p>如果系统IO繁忙，比如有别的应用在写盘，或者Redis自己在AOF rewrite或RDB snapshot(虽然此时写入的是另一个临时文件，虽然各自都在连续写，但两个文件间的切换使得磁盘磁头的寻道时间加长），就可能导致fdatasync(2)迟迟未能完成从而Block住write(2)，Block住整个Redis。</p><p>为了更清晰的看到fdatasync(2)的执行时长，可以使用”strace -p (pid of redis server) -T -e -f trace=fdatasync”，但会影响系统性能。</p><p>Redis提供了一个自救的方式，当发现文件有在执行fdatasync(2)时，就先不调用write(2)，只存在cache里，免得被Block。但如果已经超过两秒都还是这个样子，则会硬着头皮执行write(2)，即使redis会被Block住。</p><p>此时那句要命的log会打印：“Asynchronous AOF fsync is taking too long (disk is busy?). Writing the AOF buffer without waiting for fsync to complete, this may slow down Redis.” </p><p>之后用redis-cli INFO可以看到aof_delayed_fsync的值被加1。</p><p>因此，对于fsync设为everysec时丢失数据的可能性的最严谨说法是：如果有fdatasync在长时间的执行，此时redis意外关闭会造成文件里不多于两秒的数据丢失。</p><p>如果fdatasync运行正常，redis意外关闭没有影响，只有当操作系统crash时才会造成少于1秒的数据丢失。</p><h2 id="影响版本"><a href="#影响版本" class="headerlink" title="影响版本"></a>影响版本</h2><p>网上有说是在2.6.12版之前，但是我们使用的版本：redis_version:3.0.6 任然存在这个问题</p><h2 id="解决方法"><a href="#解决方法" class="headerlink" title="解决方法"></a>解决方法</h2><p>最后发现，原来是AOF rewrite时一直埋头的调用write(2)，由系统自己去触发sync。在RedHat Enterprise 6里，默认配置vm.dirty_background_ratio=10，也就是占用了10%的可用内存才会开始后台flush，而我的服务器有8G内存。</p><p>很明显一次flush太多数据会造成阻塞，所以最后果断设置了sysctl vm.dirty_bytes=33554432(32M)，问题解决。</p><p>然后提了个issue，<a href="https://github.com/antirez/redis/issues/1019" target="_blank" rel="noopener">AOF rewrite时定时也执行一下fdatasync嘛</a>， antirez回复新版中，AOF rewrite时32M就会重写主动调用fdatasync。</p><ul><li>查看一下系统内核参数</li></ul><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">&gt;sysctl -a | grep dirty_background_ratio</span><br><span class="line">vm.dirty_background_ratio = 10</span><br><span class="line"></span><br><span class="line">&gt;sysctl -a | grep vm.dirty_bytes</span><br><span class="line">vm.dirty_bytes = 0</span><br></pre></td></tr></table></figure><p><strong>ps.尝试修改一下</strong></p><ul><li>编辑/etc/sysctl.conf</li></ul><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">&gt;vi /etc/sysctl.conf</span><br><span class="line"></span><br><span class="line">## 在最后面增加</span><br><span class="line"># 32M</span><br><span class="line">vm.dirty_bytes=33554432</span><br></pre></td></tr></table></figure><p><strong>ps.保存后下次启动会生效，下面是立即生效的修改方法：</strong></p><ul><li>立即生效的修改方法</li></ul><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">&gt;sysctl vm.dirty_bytes=33554432</span><br><span class="line">&gt;sysctl -p</span><br></pre></td></tr></table></figure><ul><li>验证修改是否成功</li></ul><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">&gt;sysctl -a | grep vm.dirty_bytes</span><br><span class="line">vm.dirty_bytes = 33554432</span><br></pre></td></tr></table></figure><ul><li>修改后redis下次RDB和AOF时的日志</li></ul><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">1486:M 09 Oct 10:05:02.043 * 10 changes in 300 seconds. Saving...</span><br><span class="line">1486:M 09 Oct 10:05:02.046 * Background saving started by pid 20987</span><br><span class="line">20987:C 09 Oct 10:05:17.188 * DB saved on disk</span><br><span class="line">20987:C 09 Oct 10:05:17.188 * RDB: 944 MB of memory used by copy-on-write</span><br><span class="line">1486:M 09 Oct 10:05:17.274 * Background saving terminated with success</span><br></pre></td></tr></table></figure><p>从redis的日志中发现已经没有了这句：<code>Asynchronous AOF fsync is taking too long (disk is busy?). Writing the AOF buffer without waiting for fsync to complete, this may slow down Redis.</code></p><p>应用日志中也看不到：<code>Jedis connection failed, retrying...</code>异常</p><p>这个问题解决</p>]]></content>
    
    <summary type="html">
    
      Redis Asynchronous AOF fsync is taking too long (disk is busy?)Writing the AOF buffer without waiting for fsync to complete, this may slow down Redis.
    
    </summary>
    
      <category term="Redis" scheme="https://ningyu1.github.io/categories/Redis/"/>
    
      <category term="trouble shooting" scheme="https://ningyu1.github.io/categories/Redis/trouble-shooting/"/>
    
    
      <category term="调优" scheme="https://ningyu1.github.io/tags/%E8%B0%83%E4%BC%98/"/>
    
      <category term="Redis" scheme="https://ningyu1.github.io/tags/Redis/"/>
    
      <category term="AOF Block" scheme="https://ningyu1.github.io/tags/AOF-Block/"/>
    
      <category term="AOF" scheme="https://ningyu1.github.io/tags/AOF/"/>
    
      <category term="Asynchronous AOF fsync is taking too long (disk is busy?)Writing the AOF buffer without waiting for fsync to complete, this may slow down Redis" scheme="https://ningyu1.github.io/tags/Asynchronous-AOF-fsync-is-taking-too-long-disk-is-busy-Writing-the-AOF-buffer-without-waiting-for-fsync-to-complete-this-may-slow-down-Redis/"/>
    
  </entry>
  
  <entry>
    <title>条形码处理类库 ZXing</title>
    <link href="https://ningyu1.github.io/20170930/31-zxing.html"/>
    <id>https://ningyu1.github.io/20170930/31-zxing.html</id>
    <published>2017-09-30T08:56:36.000Z</published>
    <updated>2024-04-23T01:48:15.033Z</updated>
    
    <content type="html"><![CDATA[<h2 id="ZXing-详细介绍"><a href="#ZXing-详细介绍" class="headerlink" title="ZXing 详细介绍"></a>ZXing 详细介绍</h2><p>ZXing是一个开源Java类库用于解析多种格式的1D/2D条形码。目标是能够对QR编码、Data Matrix、UPC的1D条形码进行解码。 其提供了多种平台下的客户端包括：J2ME、J2SE和Android。</p><h2 id="示例代码"><a href="#示例代码" class="headerlink" title="示例代码"></a>示例代码</h2><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">import com.google.zxing.BarcodeFormat;</span><br><span class="line">import com.google.zxing.EncodeHintType;</span><br><span class="line">import com.google.zxing.MultiFormatWriter;</span><br><span class="line">import com.google.zxing.WriterException;</span><br><span class="line">import com.google.zxing.client.j2se.MatrixToImageWriter;</span><br><span class="line">import com.google.zxing.common.BitMatrix;</span><br><span class="line">import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;</span><br><span class="line"></span><br><span class="line">import java.io.File;</span><br><span class="line">import java.io.IOException;</span><br><span class="line">import java.io.OutputStream;</span><br><span class="line">import java.util.HashMap;</span><br><span class="line">import java.util.Map;</span><br><span class="line"></span><br><span class="line">/**</span><br><span class="line">* 二维码工具类</span><br><span class="line">*/</span><br><span class="line">public class QRCodeUtil &#123;</span><br><span class="line">  private static final int width = 300;// 默认二维码宽度</span><br><span class="line">  private static final int height = 300;// 默认二维码高度</span><br><span class="line">  private static final String format = &quot;png&quot;;// 默认二维码文件格式</span><br><span class="line">  private static final Map&lt;EncodeHintType, Object&gt; hints = new HashMap();// 二维码参数</span><br><span class="line"></span><br><span class="line">  static &#123;</span><br><span class="line">      hints.put(EncodeHintType.CHARACTER_SET, &quot;utf-8&quot;);// 字符编码</span><br><span class="line">      hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H);// 容错等级 L、M、Q、H 其中 L 为最低, H 为最高</span><br><span class="line">      hints.put(EncodeHintType.MARGIN, 2);// 二维码与图片边距</span><br><span class="line">  &#125;</span><br><span class="line">  /**</span><br><span class="line">   * 返回一个 BufferedImage 对象</span><br><span class="line">   * @param content 二维码内容</span><br><span class="line">   * @param width   宽</span><br><span class="line">   * @param height  高</span><br><span class="line">   */</span><br><span class="line">  public static BufferedImage toBufferedImage(String content, int width, int height) throws WriterException, IOException &#123;</span><br><span class="line">      BitMatrix bitMatrix = new MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, width, height, hints);</span><br><span class="line">      return MatrixToImageWriter.toBufferedImage(bitMatrix);</span><br><span class="line">  &#125;</span><br><span class="line">  /**</span><br><span class="line">   * 将二维码图片输出到一个流中</span><br><span class="line">   * @param content 二维码内容</span><br><span class="line">   * @param stream  输出流</span><br><span class="line">   * @param width   宽</span><br><span class="line">   * @param height  高</span><br><span class="line">   */</span><br><span class="line">  public static void writeToStream(String content, OutputStream stream, int width, int height) throws WriterException, IOException &#123;</span><br><span class="line">      BitMatrix bitMatrix = new MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, width, height, hints);</span><br><span class="line">      MatrixToImageWriter.writeToStream(bitMatrix, format, stream);</span><br><span class="line">  &#125;</span><br><span class="line">  /**</span><br><span class="line">   * 生成二维码图片文件</span><br><span class="line">   * @param content 二维码内容</span><br><span class="line">   * @param path    文件保存路径</span><br><span class="line">   * @param width   宽</span><br><span class="line">   * @param height  高</span><br><span class="line">   */</span><br><span class="line">  public static void createQRCode(String content, String path, int width, int height) throws WriterException, IOException &#123;</span><br><span class="line">      BitMatrix bitMatrix = new MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, width, height, hints);</span><br><span class="line">      //toPath() 方法由 jdk1.7 及以上提供</span><br><span class="line">      MatrixToImageWriter.writeToPath(bitMatrix, format, new File(path).toPath());</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>]]></content>
    
    <summary type="html">
    
      条形码处理类库 ZXing
    
    </summary>
    
      <category term="google" scheme="https://ningyu1.github.io/categories/google/"/>
    
      <category term="zxing" scheme="https://ningyu1.github.io/categories/google/zxing/"/>
    
    
      <category term="google" scheme="https://ningyu1.github.io/tags/google/"/>
    
      <category term="zxing" scheme="https://ningyu1.github.io/tags/zxing/"/>
    
  </entry>
  
  <entry>
    <title>npm registry太慢？怎么办？使用nrm</title>
    <link href="https://ningyu1.github.io/20170929/30-npm-nrm.html"/>
    <id>https://ningyu1.github.io/20170929/30-npm-nrm.html</id>
    <published>2017-09-29T06:16:36.000Z</published>
    <updated>2024-04-23T01:48:15.033Z</updated>
    
    <content type="html"><![CDATA[<p>转载自：<a href="http://cnodejs.org/topic/5326e78c434e04172c006826" target="_blank" rel="noopener">http://cnodejs.org/topic/5326e78c434e04172c006826</a></p><p>开发的npm registry 管理工具 <a href="https://github.com/Pana/nrm" target="_blank" rel="noopener">nrm</a>,  能够查看和切换当前使用的registry, 最近NPM经常 down 掉, 这个还是很有用的哈哈</p><h2 id="Install"><a href="#Install" class="headerlink" title="Install"></a>Install</h2><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">$ npm install -g nrm</span><br></pre></td></tr></table></figure><h2 id="Example"><a href="#Example" class="headerlink" title="Example"></a>Example</h2><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">$ nrm ls</span><br><span class="line"></span><br><span class="line">* npm ---- https://registry.npmjs.org/</span><br><span class="line">  cnpm --- http://r.cnpmjs.org/</span><br><span class="line">  eu ----- http://registry.npmjs.eu/</span><br><span class="line">  au ----- http://registry.npmjs.org.au/</span><br><span class="line">  sl ----- http://npm.strongloop.com/</span><br><span class="line">  nj ----- https://registry.nodejitsu.com/</span><br></pre></td></tr></table></figure><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">$ nrm use cnpm //switch registry to cnpm</span><br><span class="line"></span><br><span class="line">    Registry has been set to: http://r.cnpmjs.org/</span><br></pre></td></tr></table></figure><h2 id="cmd"><a href="#cmd" class="headerlink" title="cmd"></a>cmd</h2><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">nrm help // show help</span><br><span class="line">nrm list // show all registries</span><br><span class="line">nrm use cnpm // switch to cnpm</span><br><span class="line">nrm home // go to a registry home page</span><br></pre></td></tr></table></figure><h2 id="Registries"><a href="#Registries" class="headerlink" title="Registries"></a>Registries</h2><ul><li><a href="https://www.npmjs.org/" target="_blank" rel="noopener">npm</a></li><li><a href="http://cnpmjs.org/" target="_blank" rel="noopener">cnpm</a></li><li><a href="http://strongloop.com/" target="_blank" rel="noopener">strongloop</a></li><li><a href="http://npmjs.eu/" target="_blank" rel="noopener">european</a></li><li><a href="http://npmjs.org.au/" target="_blank" rel="noopener">australia</a></li><li><a href="https://www.nodejitsu.com/" target="_blank" rel="noopener">nodejitsu</a></li></ul>]]></content>
    
    <summary type="html">
    
      npm registry太慢？怎么办？使用nrm
    
    </summary>
    
      <category term="npm" scheme="https://ningyu1.github.io/categories/npm/"/>
    
      <category term="nrm" scheme="https://ningyu1.github.io/categories/npm/nrm/"/>
    
    
      <category term="npm" scheme="https://ningyu1.github.io/tags/npm/"/>
    
      <category term="nrm" scheme="https://ningyu1.github.io/tags/nrm/"/>
    
  </entry>
  
  <entry>
    <title>分布式锁（Redis实现）使用说明</title>
    <link href="https://ningyu1.github.io/20170927/29-distributed-lock.html"/>
    <id>https://ningyu1.github.io/20170927/29-distributed-lock.html</id>
    <published>2017-09-27T08:43:36.000Z</published>
    <updated>2024-04-23T01:48:15.033Z</updated>
    
    <content type="html"><![CDATA[<h2 id="概述"><a href="#概述" class="headerlink" title="概述"></a>概述</h2><p>[<figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://github.com/ningyu1/distributed-lock/watchers" alt="GitHub release](https://img.shields.io/github/release/ningyu1/distributed-lock.svg?style=social&label=Release)](https://github.com/ningyu1/distributed-lock/releases)&nbsp;[![GitHub stars](https://img.shields.io/github/stars/ningyu1/distributed-lock.svg?style=social&label=Star)](https://github.com/ningyu1/distributed-lock/stargazers)&nbsp;[![GitHub forks](https://img.shields.io/github/forks/ningyu1/distributed-lock.svg?style=social&label=Fork)](https://github.com/ningyu1/distributed-lock/fork)&nbsp;[![GitHub watchers](https://img.shields.io/github/watchers/ningyu1/distributed-lock.svg?style=social&label=Watch)" title>                </div>                <div class="image-caption">GitHub release](https://img.shields.io/github/release/ningyu1/distributed-lock.svg?style=social&label=Release)](https://github.com/ningyu1/distributed-lock/releases)&nbsp;[![GitHub stars](https://img.shields.io/github/stars/ningyu1/distributed-lock.svg?style=social&label=Star)](https://github.com/ningyu1/distributed-lock/stargazers)&nbsp;[![GitHub forks](https://img.shields.io/github/forks/ningyu1/distributed-lock.svg?style=social&label=Fork)](https://github.com/ningyu1/distributed-lock/fork)&nbsp;[![GitHub watchers](https://img.shields.io/github/watchers/ningyu1/distributed-lock.svg?style=social&label=Watch)</div>            </figure></p><h2 id="项目地址"><a href="#项目地址" class="headerlink" title="项目地址"></a>项目地址</h2><p><a href="https://github.com/ningyu1/distributed-lock" title="项目地址" target="_blank" rel="noopener">distributed-lock</a> </p><p>[<figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="http://www.gnu.org/licenses/gpl-3.0.html" alt="License](https://img.shields.io/badge/license-GPLv3-blue.svg)" title>                </div>                <div class="image-caption">License](https://img.shields.io/badge/license-GPLv3-blue.svg)</div>            </figure></p><p>分布式锁，默认是redis实现，可扩展接口增加zk、等其他实现,这个分布式锁采用redis实现，根据CAP理论保证了可用性、分区容错性、和最终一致性。</p><h2 id="实现的分布式锁特性"><a href="#实现的分布式锁特性" class="headerlink" title="实现的分布式锁特性"></a>实现的分布式锁特性</h2><ol><li>这把锁是非阻塞锁，可以根据超时时间和重试频率来定义重试次数</li><li>这把锁支持失效时间，极端情况下解锁失败，到达时间之后锁会自动删除</li><li>这把锁是非重入锁，一个线程获得锁之后，在释放锁之前，其他线程无法再次获得锁，只能根据获取锁超时时间和重试策略进行多次尝试获取锁。</li><li>因为这把锁是非阻塞的，所以性能很好，支持高并发</li><li>使用方无需手动获取锁和释放锁，锁的控制完全由框架控制操作，避免使用方由于没有释放锁或释放锁失败导致死锁的问题</li></ol><h2 id="实现的分布式锁缺点"><a href="#实现的分布式锁缺点" class="headerlink" title="实现的分布式锁缺点"></a>实现的分布式锁缺点</h2><ol><li>通过超时时间来控制锁的失效时间其实并不完美，但是根据性能和CAP理论有做取舍</li><li>这把锁不支持阻塞，因为要达到高的性能阻塞的特性是要牺牲</li></ol><h2 id="使用步骤"><a href="#使用步骤" class="headerlink" title="使用步骤"></a>使用步骤</h2><h3 id="Maven中引入"><a href="#Maven中引入" class="headerlink" title="Maven中引入"></a>Maven中引入</h3><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">&lt;dependency&gt;</span><br><span class="line">    &lt;groupId&gt;cn.tsoft.framework&lt;/groupId&gt;</span><br><span class="line">    &lt;artifactId&gt;distributed-lock&lt;/artifactId&gt;</span><br><span class="line">    &lt;version&gt;1.1.0-SNAPSHOT&lt;/version&gt;</span><br><span class="line">&lt;/dependency&gt;</span><br></pre></td></tr></table></figure><h3 id="spring中引入配置"><a href="#spring中引入配置" class="headerlink" title="spring中引入配置"></a>spring中引入配置</h3><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">&lt;import resource=&quot;classpath:spring-lock.xml&quot; /&gt;</span><br></pre></td></tr></table></figure><h3 id="使用到了RedisClient"><a href="#使用到了RedisClient" class="headerlink" title="使用到了RedisClient"></a>使用到了RedisClient</h3><p>具体可以查看<a href="https://ningyu1.github.io/site/post/22-redis-client/">《RedisCliet使用说明》</a></p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">&lt;aop:aspectj-autoproxy /&gt;</span><br><span class="line">&lt;context:component-scan base-package=&quot;cn.tsoft.framework&quot; /&gt;</span><br><span class="line">&lt;context:property-placeholder location=&quot;classpath:redis.properties&quot;/&gt;</span><br><span class="line">&lt;import resource=&quot;classpath:spring-redis.xml&quot; /&gt;</span><br></pre></td></tr></table></figure><h3 id="代码中使用"><a href="#代码中使用" class="headerlink" title="代码中使用"></a>代码中使用</h3><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">import cn.tsoft.framework.lock.Lock;</span><br><span class="line">import  cn.tsoft.framework.lock.LockCallBack;</span><br><span class="line">import  cn.tsoft.framework.lock.DefaultLockCallBack;</span><br><span class="line"> </span><br><span class="line">@Autowired</span><br><span class="line">Lock lock;</span><br><span class="line"></span><br><span class="line">//方法一</span><br><span class="line">T t = lock.lock(&quot;Test_key_2&quot;,20,60,new LockCallBack&lt;T&gt;()&#123;</span><br><span class="line">    public T handleObtainLock()&#123;</span><br><span class="line">        dosomething();</span><br><span class="line">    &#125;</span><br><span class="line">    public T handleNotObtainLock() throws LockCantObtainException&#123;</span><br><span class="line">        return T;//throw new LockCantObtainException();</span><br><span class="line">    &#125;</span><br><span class="line">    public T handleException(LockInsideExecutedException e) throws LockInsideExecutedException&#123;</span><br><span class="line">        return T;//throw new e;</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">T t = lock.lock(&quot;Test_key_2&quot;,LockRetryFrequncy.VERY_QUICK,20,60,new DefaultLockCallBack&lt;T&gt;(T,T)&#123;</span><br><span class="line">    public T handleObtainLock()&#123;</span><br><span class="line">        dosomething();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><h3 id="锁重试策略说明"><a href="#锁重试策略说明" class="headerlink" title="锁重试策略说明"></a>锁重试策略说明</h3><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">/**</span><br><span class="line"> * 锁重试获取频率策略</span><br><span class="line"> * </span><br><span class="line"> * @author ningyu</span><br><span class="line"> *</span><br><span class="line"> */</span><br><span class="line">LockRetryFrequncy.VERY_QUICK;  //非常快</span><br><span class="line">LockRetryFrequncy.QUICK;       //快</span><br><span class="line">LockRetryFrequncy.NORMAL;      //中</span><br><span class="line">LockRetryFrequncy.SLOW;        //慢</span><br><span class="line">LockRetryFrequncy.VERYSLOW;    //很慢</span><br><span class="line">//例如：</span><br><span class="line">//以获取锁的超时时间为：1秒来计算</span><br><span class="line">//VERY_QUICK的重试次数为：100次</span><br><span class="line">//QUICK的重试次数为：20次</span><br><span class="line">//NORMAL的重试次数为：10次</span><br><span class="line">//SLOW的重试次数为：2次</span><br><span class="line">//QUICK的重试次数为：1次</span><br><span class="line">//这个重试策略根据自身业务来选择合适的重试策略</span><br></pre></td></tr></table></figure><h3 id="Example"><a href="#Example" class="headerlink" title="Example"></a>Example</h3><h4 id="第一种用法"><a href="#第一种用法" class="headerlink" title="第一种用法"></a>第一种用法</h4><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">//锁名称：Test_key_2</span><br><span class="line">//获取锁超时时间：20秒</span><br><span class="line">//锁最大过期时间：60秒</span><br><span class="line">//内部执行回调，包含（1.获取到锁回调，2.没有获取到锁回调，3.获取到锁内部执行业务代码报错）</span><br><span class="line">//默认策略：NORMAL</span><br><span class="line">lock.lock(&quot;Test_key_2&quot;,20,60,new LockCallBack&lt;String&gt;() &#123;</span><br><span class="line">   @Override</span><br><span class="line">   public String handleException(LockInsideExecutedException e) throws LockInsideExecutedException &#123;</span><br><span class="line">       logger.error(&quot;获取到锁，内部执行报错&quot;);</span><br><span class="line">       return &quot;Exception&quot;;         </span><br><span class="line">   &#125;</span><br><span class="line"> </span><br><span class="line">   @Override</span><br><span class="line">   public String handleNotObtainLock() throws LockCantObtainException &#123;</span><br><span class="line">          logger.error(&quot;没有获取到锁&quot;);</span><br><span class="line">       return &quot;NotObtainLock&quot;;</span><br><span class="line">   &#125;</span><br><span class="line"> </span><br><span class="line">   @Override</span><br><span class="line">   public String handleObtainLock() &#123;</span><br><span class="line">       logger.info(&quot;获取到锁&quot;);</span><br><span class="line">       dosomething();</span><br><span class="line">       return &quot;ok&quot;;</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><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">//锁名称：Test_key_2</span><br><span class="line">//获取锁超时时间：20秒</span><br><span class="line">//锁最大过期时间：60秒</span><br><span class="line">//内部执行回调，使用默认回调实现，只需要实现获取到锁后需要执行的方法，当遇到没有获取锁和获取锁内部执行错误时会返回构造函数中设置的值（支持泛型）</span><br><span class="line">//默认策略：NORMAL</span><br><span class="line">lock.lock(&quot;Test_key_2&quot;,20,60,new DefaultLockCallBack&lt;String&gt;(&quot;NotObtainLock&quot;, &quot;Exception&quot;) &#123;</span><br><span class="line">   @Override</span><br><span class="line">   public String handleObtainLock() &#123;</span><br><span class="line">       logger.info(&quot;获取到锁&quot;);</span><br><span class="line">       dosomething();</span><br><span class="line">       return &quot;ok&quot;;</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><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">//锁名称：Test_key_2</span><br><span class="line">//锁重试获取频率：VERY_QUICK 非常快</span><br><span class="line">//获取锁超时时间：20秒</span><br><span class="line">//锁最大过期时间：60秒</span><br><span class="line">//内部执行回调，包含（1.获取到锁回调，2.没有获取到锁回调，3.获取到锁内部执行业务代码报错）</span><br><span class="line">lock.lock(&quot;Test_key_2&quot;,LockRetryFrequncy.VERY_QUICK,20,60,new LockCallBack&lt;String&gt;() &#123;</span><br><span class="line">   @Override</span><br><span class="line">   public String handleException(LockInsideExecutedException e) throws LockInsideExecutedException &#123;</span><br><span class="line">       logger.error(&quot;获取到锁，内部执行报错&quot;);</span><br><span class="line">       return &quot;Exception&quot;;         </span><br><span class="line">   &#125;</span><br><span class="line"> </span><br><span class="line">   @Override</span><br><span class="line">   public String handleNotObtainLock() throws LockCantObtainException &#123;</span><br><span class="line">          logger.error(&quot;没有获取到锁&quot;);</span><br><span class="line">       return &quot;NotObtainLock&quot;;</span><br><span class="line">   &#125;</span><br><span class="line"> </span><br><span class="line">   @Override</span><br><span class="line">   public String handleObtainLock() &#123;</span><br><span class="line">       logger.info(&quot;获取到锁&quot;);</span><br><span class="line">       dosomething();</span><br><span class="line">       return &quot;ok&quot;;</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><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">//锁名称：Test_key_2</span><br><span class="line">//锁重试获取频率：VERY_QUICK 非常快</span><br><span class="line">//获取锁超时时间：20秒</span><br><span class="line">//锁最大过期时间：60秒</span><br><span class="line">//内部执行回调，使用默认回调实现，只需要实现获取到锁后需要执行的方法，当遇到没有获取锁和获取锁内部执行错误时会返回构造函数中设置的值（支持泛型）</span><br><span class="line">lock.lock(&quot;Test_key_2&quot;,LockRetryFrequncy.VERY_QUICK,20,60,new DefaultLockCallBack&lt;String&gt;(&quot;NotObtainLock&quot;, &quot;Exception&quot;) &#123;</span><br><span class="line">   @Override</span><br><span class="line">   public String handleObtainLock() &#123;</span><br><span class="line">       logger.info(&quot;获取到锁&quot;);</span><br><span class="line">       dosomething();</span><br><span class="line">       return &quot;ok&quot;;</span><br><span class="line">   &#125;</span><br><span class="line">);</span><br></pre></td></tr></table></figure><h3 id="注意事项"><a href="#注意事项" class="headerlink" title="注意事项"></a>注意事项</h3><ol><li>获取锁的超时时间和重试策略直接影响获取锁重试的次数，根据业务场景来定义适合的重试获取锁的频次，避免线程阻塞。</li><li>场景：<ol><li>快速响应给客户端的场景，超时时间尽量短，超时时间 &lt; 锁后执行时间，例如：秒杀、抢购</li><li>可以容忍响应速度的场景，锁后执行时间*2 &gt; 超时时间 &gt;=锁后执行时间</li></ol></li><li>根据业务场景来定义锁的最大过期时间，理论上业务执行越慢过期时间越大，因为是并发锁，为了杜绝因为获得锁而没有释放造成的问题</li><li>建议 锁后执行时间*1.5 &gt; 锁超时时间 &gt; 锁后执行时间，避免并发问题</li><li>获取锁后执行的代码块一定是小而快的，就像事务块使用原则一样，禁止重而长的逻辑包在里面造成其他线程获取锁失败率过高，如果逻辑很复杂需要分析那一块需要支持并发就把需要并发的代码包在里面。</li></ol>]]></content>
    
    <summary type="html">
    
      分布式锁（Redis实现）使用说明
    
    </summary>
    
      <category term="Redis" scheme="https://ningyu1.github.io/categories/Redis/"/>
    
      <category term="Lock" scheme="https://ningyu1.github.io/categories/Redis/Lock/"/>
    
      <category term="Distributed" scheme="https://ningyu1.github.io/categories/Redis/Lock/Distributed/"/>
    
    
      <category term="Redis" scheme="https://ningyu1.github.io/tags/Redis/"/>
    
      <category term="Lock" scheme="https://ningyu1.github.io/tags/Lock/"/>
    
      <category term="Distributed" scheme="https://ningyu1.github.io/tags/Distributed/"/>
    
  </entry>
  
  <entry>
    <title>RedisClient升级支持Sentinel使用说明</title>
    <link href="https://ningyu1.github.io/20170925/28-redis-client-sentinel.html"/>
    <id>https://ningyu1.github.io/20170925/28-redis-client-sentinel.html</id>
    <published>2017-09-25T05:29:36.000Z</published>
    <updated>2024-04-23T01:48:15.032Z</updated>
    
    <content type="html"><![CDATA[<h2 id="项目地址"><a href="#项目地址" class="headerlink" title="项目地址"></a>项目地址</h2><p><a href="https://github.com/ningyu1/redis-client" target="_blank" rel="noopener">redis-client</a> </p><p><a href="https://github.com/ningyu1/redis-client/releases" target="_blank" rel="noopener"><img src="https://img.shields.io/github/release/ningyu1/redis-client.svg?style=social&amp;label=Release"></a>&nbsp;<a href="https://github.com/ningyu1/redis-client/stargazers" target="_blank" rel="noopener"><img src="https://img.shields.io/github/stars/ningyu1/redis-client.svg?style=social&amp;label=Star"></a>&nbsp;<a href="https://github.com/ningyu1/redis-client/fork" target="_blank" rel="noopener"><img src="https://img.shields.io/github/forks/ningyu1/redis-client.svg?style=social&amp;label=Fork"></a>&nbsp;<a href="https://github.com/ningyu1/redis-client/watchers" target="_blank" rel="noopener"><img src="https://img.shields.io/github/watchers/ningyu1/redis-client.svg?style=social&amp;label=Watch"></a> <a href="http://www.gnu.org/licenses/gpl-3.0.html" target="_blank" rel="noopener"><img src="https://img.shields.io/badge/license-GPLv3-blue.svg"></a></p><p>RedisClient操作单点Redis使用文档：<a href="https://ningyu1.github.io/site/post/22-redis-client/">《RedisClient使用》</a><br>以下是支持Sentinel（哨兵）+Redis集群的RedisClient（架构封装的Java访问Redis的客户端程序）高级使用方式</p><p>Redis集群方式：Master-Slave（1 - n 为一套集群可以多套）<br>Sentinel集群方式：Sentinel（n台，n&gt;=3），投票人数：n-1（参与Master是否宕机以及下一任Master选举的投票人数）</p><h2 id="1-Maven中引用（目前预览版）"><a href="#1-Maven中引用（目前预览版）" class="headerlink" title="1. Maven中引用（目前预览版）"></a>1. Maven中引用（目前预览版）</h2><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">&lt;dependency&gt;</span><br><span class="line">  &lt;groupId&gt;cn.tsoft.framework&lt;/groupId&gt;</span><br><span class="line">  &lt;artifactId&gt;redis-client&lt;/artifactId&gt;</span><br><span class="line">  &lt;version&gt;1.2.0-SNAPSHOT&lt;/version&gt;</span><br><span class="line">&lt;/dependency&gt;</span><br></pre></td></tr></table></figure><h2 id="2-配置说明"><a href="#2-配置说明" class="headerlink" title="2. 配置说明"></a>2. 配置说明</h2><p>原始（基础）配置：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">redis.pool.maxTotal=1000</span><br><span class="line">redis.pool.maxIdle=50</span><br><span class="line">redis.pool.minIdle=10</span><br><span class="line">redis.pool.testOnBorrow=true</span><br><span class="line">redis.pool.testOnReturn=true</span><br><span class="line">redis.ip=192.168.0.65</span><br><span class="line">redis.port=6379</span><br><span class="line">redis.timeout=2000</span><br><span class="line">redis.password=123456</span><br></pre></td></tr></table></figure><p>sentinel新增配置</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line"># sentinel</span><br><span class="line">redis.mastername=mymaster</span><br><span class="line">redis.sentinels=127.0.0.1:26379,127.0.0.1:26380,127.0.0.1:26381</span><br></pre></td></tr></table></figure><p>redis.mastername指的是monitor master的名称<br>redis.sentinels指的是哨兵的ip：port集合（ip和port需要替换）</p><p>删除配置</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">#redis.ip=192.168.0.65</span><br><span class="line">#redis.port=6379</span><br></pre></td></tr></table></figure><p><strong>ps.由于使用了sentinel自动发现redis服务因此不需要此配置，注释或删除即可</strong></p><h2 id="3-spring配置说明"><a href="#3-spring配置说明" class="headerlink" title="3. spring配置说明"></a>3. spring配置说明</h2><p>xml配置跟以前<code>pool</code>的配置方式有所不同，单节点<code>redis</code>的<code>pool</code>配置使用的是：<code>redis.clients.jedis.JedisPoolConfig</code>和<code>redis.clients.jedis.JedisPool</code><br><code>sentinel</code>的配置替换为：<code>redis.clients.jedis.JedisPoolConfig</code>和<code>cn.tsoft.framework.redis.pool.JedisSentinelPoolFactory</code></p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;</span><br><span class="line">&lt;beans xmlns=&quot;http://www.springframework.org/schema/beans&quot;</span><br><span class="line">    xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;</span><br><span class="line">    xmlns:aop=&quot;http://www.springframework.org/schema/aop&quot;</span><br><span class="line">    xmlns:context=&quot;http://www.springframework.org/schema/context&quot;</span><br><span class="line">    xmlns:tx=&quot;http://www.springframework.org/schema/tx&quot;</span><br><span class="line">    xsi:schemaLocation=&quot;http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd</span><br><span class="line">    http://www.springframework.org/schema/context  http://www.springframework.org/schema/context/spring-context.xsd</span><br><span class="line">    http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd</span><br><span class="line">    http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd&quot;&gt;</span><br><span class="line">     </span><br><span class="line">    &lt;aop:aspectj-autoproxy /&gt;</span><br><span class="line">    &lt;context:component-scan base-package=&quot;cn.tsoft.framework.redis&quot; /&gt;</span><br><span class="line">     </span><br><span class="line">    &lt;bean id=&quot;redisClient&quot; class=&quot;cn.tsoft.framework.redis.client.impl.RedisClientImpl&quot;&gt;</span><br><span class="line">        &lt;property name=&quot;jedisSentinelPoolFactory&quot; ref=&quot;jedisSentinelPoolFactory&quot; /&gt;</span><br><span class="line">    &lt;/bean&gt;</span><br><span class="line">    &lt;bean id=&quot;jedisPoolConfig&quot; class=&quot;redis.clients.jedis.JedisPoolConfig&quot;&gt;</span><br><span class="line">        &lt;property name=&quot;maxTotal&quot; value=&quot;$&#123;redis.pool.maxTotal&#125;&quot; /&gt;</span><br><span class="line">        &lt;property name=&quot;maxIdle&quot; value=&quot;$&#123;redis.pool.maxIdle&#125;&quot; /&gt;</span><br><span class="line">        &lt;property name=&quot;minIdle&quot; value=&quot;$&#123;redis.pool.minIdle&#125;&quot; /&gt;</span><br><span class="line">        &lt;property name=&quot;testOnBorrow&quot; value=&quot;$&#123;redis.pool.testOnBorrow&#125;&quot; /&gt;</span><br><span class="line">        &lt;property name=&quot;testOnReturn&quot; value=&quot;$&#123;redis.pool.testOnReturn&#125;&quot; /&gt;</span><br><span class="line">    &lt;/bean&gt;</span><br><span class="line">     </span><br><span class="line">    &lt;bean id=&quot;jedisSentinelPoolFactory&quot; class=&quot;cn.tsoft.framework.redis.pool.JedisSentinelPoolFactory&quot;&gt;</span><br><span class="line">        &lt;property name=&quot;poolConfig&quot; ref=&quot;jedisPoolConfig&quot; /&gt;</span><br><span class="line">        &lt;property name=&quot;masterName&quot; value=&quot;$&#123;redis.mastername&#125;&quot; /&gt;</span><br><span class="line">        &lt;property name=&quot;sentinels&quot; value=&quot;$&#123;redis.sentinels&#125;&quot; /&gt;</span><br><span class="line">        &lt;property name=&quot;timeout&quot; value=&quot;$&#123;redis.timeout&#125;&quot; /&gt;</span><br><span class="line">        &lt;property name=&quot;password&quot; value=&quot;$&#123;redis.password&#125;&quot; /&gt;</span><br><span class="line">    &lt;/bean&gt;</span><br><span class="line">&lt;/beans&gt;</span><br></pre></td></tr></table></figure><p><strong>ps.以上配置在redis-client-1.2.0-SNAPSHOT.jar包的spring-redis-sentinel.xml文件中</strong></p><h2 id="4-项目中引用"><a href="#4-项目中引用" class="headerlink" title="4. 项目中引用"></a>4. 项目中引用</h2><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">&lt;!-- redis.properties加载方式采用UCM的统一配置加载，具体可以查看global中的配置，如需要替换global的配置只需要在项目自定义配置中配置相同的key来进行属性覆盖  --&gt;</span><br><span class="line">&lt;context:component-scan base-package=&quot;cn.tsoft.framework.redis&quot; /&gt;</span><br><span class="line">&lt;import resource=&quot;classpath:spring-redis-sentinel.xml&quot; /&gt;</span><br></pre></td></tr></table></figure><p><strong>ps.替换掉以前的：<code>&lt;import resource=&quot;classpath:spring-redis.xml&quot; /&gt;</code></strong></p><h2 id="5-注意事项"><a href="#5-注意事项" class="headerlink" title="5. 注意事项"></a>5. 注意事项</h2><p>5.1. <code>pool</code>使用只允许使用一种，要么使用<code>jedis pool</code>要么使用<code>jedis sentinel pool</code>，两者不允许共存，<code>redisclient</code>启动会检测<code>pool</code>的设置是否合法，不合法会throw出异常，可能遇见的异常如下：</p><table><thead><tr><th style="text-align:left">异常</th><th style="text-align:left">描述</th><th style="text-align:left">解决办法</th></tr></thead><tbody><tr><td style="text-align:left">RedisClientException(“There can only be one pool! Will not work.”)</td><td style="text-align:left">只能存在一个pool的设置</td><td style="text-align:left">检查xml配置，确定使用的pool，只允许保留一个pool设置，直接引用redis-client.jar中的（spring-redis.xml、spring-redis-sentinel.xml）可以解决这个问题</td></tr><tr><td style="text-align:left">RedisClientException(“No connection pool found! Will not work.”)</td><td style="text-align:left">没有找到pool的设置</td><td style="text-align:left">检查xml配置，是否有pool的设置，直接引用redis-client.jar中的（spring-redis.xml、spring-redis-sentinel.xml）可以解决这个问题</td></tr></tbody></table><p>5.2. API使用起来跟以前没有任何变化，只是配置发生了变化</p>]]></content>
    
    <summary type="html">
    
      RedisClient升级支持Sentinel使用说明
    
    </summary>
    
      <category term="Redis" scheme="https://ningyu1.github.io/categories/Redis/"/>
    
    
      <category term="Redis" scheme="https://ningyu1.github.io/tags/Redis/"/>
    
  </entry>
  
  <entry>
    <title>Webpack 打包优化之速度篇</title>
    <link href="https://ningyu1.github.io/20170920/27-webpack2.html"/>
    <id>https://ningyu1.github.io/20170920/27-webpack2.html</id>
    <published>2017-09-20T03:57:36.000Z</published>
    <updated>2024-04-23T01:48:15.032Z</updated>
    
    <content type="html"><![CDATA[<p>文章来源：<a href="https://jeffjade.com/2017/08/12/125-webpack-package-optimization-for-speed/" target="_blank" rel="noopener">https://jeffjade.com/2017/08/12/125-webpack-package-optimization-for-speed/</a><br>作者：@晚晴幽草轩轩主</p><p>在前文 <a href="https://ningyu1.github.io/site/post/26-webpack1/">Webpack 打包优化之体积</a>篇中，对如何减小 <code>Webpack</code> 打包体积，做了些探讨；当然，那些法子对于打包速度的提升，也是大有裨益。然而，打包速度之于<strong>开发体验</strong>和<strong>及时构建</strong>，相当重要；所以有必要对其做更为深入的研究，以便完善工作流，这就是本文存在的缘由。</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/webpack/1.png" alt="Webpack Package optimization" title>                </div>                <div class="image-caption">Webpack Package optimization</div>            </figure><center><em>Webpack Package optimization</em></center><h2 id="减小文件搜索范围"><a href="#减小文件搜索范围" class="headerlink" title="减小文件搜索范围"></a>减小文件搜索范围</h2><p>在使用实际项目开发中，为了提升开发效率，很明显你会使用很多成熟第三方库；即便自己写的代码，模块间相互引用，为了方便也会使用相对路劲，或者别名(<code>alias</code>)；这中间如果能使得 <code>Webpack</code> 更快寻找到目标，将对打包速度产生很是积极的影响。于此，我们需要做的即：减小文件搜索范围，从而提升速度；实现这一点，可以有如下两法：</p><h3 id="配置-resolve-modules"><a href="#配置-resolve-modules" class="headerlink" title="配置 resolve.modules"></a>配置 resolve.modules</h3><p><code>Webpack</code>的<code>resolve.modules</code>配置模块库（即 node_modules）所在的位置，在 js 里出现 <code>import &#39;vue&#39;</code> 这样不是相对、也不是绝对路径的写法时，会去 <code>node_modules</code> 目录下找。但是默认的配置，会采用向上递归搜索的方式去寻找，但通常项目目录里只有一个 <code>node_modules</code>，且是在项目根目录，为了减少搜索范围，可以直接写明 <code>node_modules</code> 的全路径；同样，对于别名(<code>alias</code>)的配置，亦当如此：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">function resolve (dir) &#123;</span><br><span class="line">  return path.join(__dirname, &apos;..&apos;, dir)</span><br><span class="line">&#125;</span><br><span class="line">module.exports = &#123;</span><br><span class="line">  resolve: &#123;</span><br><span class="line">    extensions: [&apos;.js&apos;, &apos;.vue&apos;, &apos;.json&apos;],</span><br><span class="line">    modules: [</span><br><span class="line">      resolve(&apos;src&apos;),</span><br><span class="line">      resolve(&apos;node_modules&apos;)</span><br><span class="line">    ],</span><br><span class="line">    alias: &#123;</span><br><span class="line">      &apos;vue$&apos;: &apos;vue/dist/vue.common.js&apos;,</span><br><span class="line">      &apos;src&apos;: resolve(&apos;src&apos;),</span><br><span class="line">      &apos;assets&apos;: resolve(&apos;src/assets&apos;),</span><br><span class="line">      &apos;components&apos;: resolve(&apos;src/components&apos;),</span><br><span class="line">      // ...</span><br><span class="line">      &apos;store&apos;: resolve(&apos;src/store&apos;)</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;,</span><br><span class="line">  ...</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>需要额外补充一点的是，这是 Webpack2.<em> 以上的写法。在 1.</em> 版本中，使用的是 <code>resolve.root</code>，如今已经被弃用为 <code>resolve.modules</code>；同时被弃用的还有<code>resolve.fallback</code>、<code>resolve.modulesDirectories</code>。</p><h3 id="设置-test-amp-include-amp-exclude"><a href="#设置-test-amp-include-amp-exclude" class="headerlink" title="设置 test &amp; include &amp; exclude"></a>设置 test &amp; include &amp; exclude</h3><p><code>Webpack</code> 的装载机(loaders)，允许每个子项都可以有以下属性：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">test：必须满足的条件（正则表达式，不要加引号，匹配要处理的文件）</span><br><span class="line">exclude：不能满足的条件（排除不处理的目录）</span><br><span class="line">include：导入的文件将由加载程序转换的路径或文件数组（把要处理的目录包括进来）</span><br><span class="line">loader：一串“！”分隔的装载机（2.0版本以上，”-loader”不可以省略）</span><br><span class="line">loaders：作为字符串的装载器阵列</span><br></pre></td></tr></table></figure><p>对于<code>include</code>，更精确指定要处理的目录，这可以减少不必要的遍历，从而减少性能损失。同样，对于已经明确知道的，不需要处理的目录，则应该予以排除，从而进一步提升性能。假设你有一个第三方组件的引用，它肯定位于<code>node_modules</code>，通常它将有一个 <code>src</code> 和一个 <code>dist</code> 目录。如果配置 <code>Webpack</code> 来排除 <code>node_modules</code>，那么它将从 <code>dist</code> 已经编译的目录中获取文件。否则会再次编译它们。故而，合理的设置 <code>include</code> &amp; <code>exclude</code>，将会极大地提升 <code>Webpack</code> 打包优化速度，比如像这样：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">module: &#123;</span><br><span class="line">  preLoaders: [</span><br><span class="line">    &#123;</span><br><span class="line">      test: /\.js$/,</span><br><span class="line">      loader: &apos;eslint&apos;,</span><br><span class="line">      include: [resolve(&apos;src&apos;)],</span><br><span class="line">      exclude: /node_modules/</span><br><span class="line">    &#125;,</span><br><span class="line">    &#123;</span><br><span class="line">      test: /\.svg$/,</span><br><span class="line">      loader: &apos;svgo?&apos; + JSON.stringify(svgoConfig)，</span><br><span class="line">      include: [resolve(&apos;src/assets/icons&apos;)],</span><br><span class="line">      exclude: /node_modules/</span><br><span class="line">    &#125;</span><br><span class="line">  ],</span><br><span class="line">  loaders: [</span><br><span class="line">    &#123;</span><br><span class="line">      test: /\.vue$/,</span><br><span class="line">      loader: &apos;vue-loader&apos;,</span><br><span class="line">      include: [resolve(&apos;src&apos;)],</span><br><span class="line">      exclude: /node_modules\/(?!(autotrack|dom-utils))|vendor\.dll\.js/</span><br><span class="line">    &#125;,</span><br><span class="line">    &#123;</span><br><span class="line">      test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,</span><br><span class="line">      loader: &apos;url&apos;,</span><br><span class="line">      exclude: /assets\/icons/,</span><br><span class="line">      query: &#123;</span><br><span class="line">        limit: 10000,</span><br><span class="line">        name: utils.assetsPath(&apos;img/[name].[hash:7].[ext]&apos;)</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line">  ]</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="增强代码代码压缩工具"><a href="#增强代码代码压缩工具" class="headerlink" title="增强代码代码压缩工具"></a>增强代码代码压缩工具</h2><p><code>Webpack</code> 默认提供的 <code>UglifyJS</code> 插件，由于采用单线程压缩，速度颇慢 ；推荐采用 <code>webpack-parallel-uglify-plugin</code> 插件，她可以并行运行 <code>UglifyJS</code> 插件，更加充分而合理的使用 <code>CPU</code> 资源，这可以大大减少的构建时间；当然，该插件应用于生产环境而非开发环境，其做法如下，</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">new webpack.optimize.UglifyJsPlugin(&#123;</span><br><span class="line">  compress: &#123;</span><br><span class="line">    warnings: false</span><br><span class="line">  &#125;,</span><br><span class="line">  sourceMap: true</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><p>替换如上自带的 <code>UglifyJsPlugin</code> 写法为如下配置即可：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">var ParallelUglifyPlugin = require(&apos;webpack-parallel-uglify-plugin&apos;);</span><br><span class="line">new ParallelUglifyPlugin(&#123;</span><br><span class="line">  cacheDir: &apos;.cache/&apos;,</span><br><span class="line">  uglifyJS:&#123;</span><br><span class="line">    output: &#123;</span><br><span class="line">      comments: false</span><br><span class="line">    &#125;,</span><br><span class="line">    compress: &#123;</span><br><span class="line">      warnings: false</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>当然也有其他同类型的插件，比如：<a href="https://www.npmjs.com/package/webpack-uglify-parallel" target="_blank" rel="noopener">webpack-uglify-parallel</a>，但根据自己实践效果来看，并没有 <code>webpack-parallel-uglify-plugin</code> 表现的那么卓越，有兴趣的朋友，可以更全面的做下对比，择优选用。需要额外说明的是，<code>webpack-parallel-uglify-plugin</code> 插件的运用，会相对 <code>UglifyJsPlugin</code> 打出的包，看起来略大那么一丢丢(其实可以忽略不计)；如果在你使用时也是如此，那么在打包速度跟包体积之间，你应该有自己的抉择。</p><h2 id="用-Happypack-来加速代码构建"><a href="#用-Happypack-来加速代码构建" class="headerlink" title="用 Happypack 来加速代码构建"></a>用 Happypack 来加速代码构建</h2><p>你知道，<code>Webpack</code> 中为了方便各种资源和类型的加载，设计了以 <code>loader</code> 加载器的形式读取资源，但是受限于 <code>nodejs</code> 的编程模型影响，所有的 <code>loader</code> 虽然以 <code>async</code> 的形式来并发调用，但是还是运行在单个 <code>node</code> 的进程，以及在同一个事件循环中，这就直接导致了些问题：当同时读取多个<code>loader</code>文件资源时，比如<code>babel-loader</code>需要 <code>transform</code> 各种<code>jsx</code>，<code>es6</code>的资源文件。在这种同步计算同时需要大量耗费 <code>cpu</code> 运算的过程中，<code>node</code>的单进程模型就无优势了，而 <code>Happypack</code> 就是针对解决此类问题而生的存在。</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/webpack/3.png" alt="Webpack-Happypack" title>                </div>                <div class="image-caption">Webpack-Happypack</div>            </figure><center><em>Webpack-Happypack</em></center><p><code>Happypack</code> 的处理思路是：将原有的 <code>webpack</code> 对 <code>loader</code> 的执行过程，从单一进程的形式扩展多进程模式，从而加速代码构建；原本的流程保持不变，这样可以在不修改原有配置的基础上，来完成对编译过程的优化，具体配置如下：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">var HappyPack = require(&apos;happypack&apos;);</span><br><span class="line">var happyThreadPool = HappyPack.ThreadPool(&#123; size: os.cpus().length &#125;);</span><br><span class="line">module: &#123;</span><br><span class="line">  loaders: [</span><br><span class="line">    &#123;</span><br><span class="line">      test: /\.js[x]?$/,</span><br><span class="line">      include: [resolve(&apos;src&apos;)],</span><br><span class="line">      exclude: /node_modules/,</span><br><span class="line">      loader: &apos;happypack/loader?id=happybabel&apos;</span><br><span class="line">    &#125;</span><br><span class="line">  ]</span><br><span class="line">&#125;,</span><br><span class="line">plugins: [</span><br><span class="line">  new HappyPack(&#123;</span><br><span class="line">    id: &apos;happybabel&apos;,</span><br><span class="line">    loaders: [&apos;babel-loader&apos;],</span><br><span class="line">    threadPool: happyThreadPool,</span><br><span class="line">    cache: true,</span><br><span class="line">    verbose: true</span><br><span class="line">  &#125;)</span><br><span class="line">]</span><br></pre></td></tr></table></figure><p>可以研究看到，通过在 <code>loader</code> 中配置直接指向 <code>happypack</code> 提供的 <code>loader</code>，对于文件实际匹配的处理 loader，则是通过配置在 plugin 属性来传递说明，这里 <code>happypack</code> 提供的 <code>loader</code> 与 <code>plugin</code> 的衔接匹配，则是通过<code>id=happybabel</code>来完成。配置完成后，<code>laoder</code>的工作模式就转变成了如下所示：</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/webpack/4.png" alt="Webpack-Happypack" title>                </div>                <div class="image-caption">Webpack-Happypack</div>            </figure><center><em>Webpack-Happypack</em></center><p><code>Happypack</code> 在编译过程中，除了利用多进程的模式加速编译，还同时开启了 <code>cache</code> 计算，能充分利用缓存读取构建文件，对构建的速度提升也是非常明显的；更多关于 <code>happyoack</code> 个中原理，可参见 @淘宝前端团队(FED) 的这篇：<a href="https://taobaofed.org/blog/2016/12/08/happypack-source-code-analysis/" target="_blank" rel="noopener">happypack 原理解析</a>。如果你使用的 <code>Vue.js</code> 框架来开发，也可参考 <a href="https://github.com/nicejade/vue-boilerplate-template/blob/master/build/webpack.base.conf.js" target="_blank" rel="noopener">vue-webpack-happypack</a> 相关配置。</p><h2 id="设置-babel-的-cacheDirectory-为true"><a href="#设置-babel-的-cacheDirectory-为true" class="headerlink" title="设置 babel 的 cacheDirectory 为true"></a>设置 babel 的 cacheDirectory 为true</h2><p><a href="https://github.com/babel/babel-loader" target="_blank" rel="noopener">babel-loader</a> is slow! 所以不仅要使用<code>exclude</code>、<code>include</code>，尽可能准确的指定要转化内容的范畴，而且要充分利用缓存，进一步提升性能。<code>babel-loader</code> 提供了 <code>cacheDirectory</code>特定选项（默认 <code>false</code>）：设置时，给定的目录将用于缓存加载器的结果。<br>未来的 <code>Webpack</code> 构建将尝试从缓存中读取，以避免在每次运行时运行潜在昂贵的 Babel 重新编译过程。如果值为空（<code>loader: ‘babel-loader?cacheDirectory’</code>）或<code>true（loader: babel-loader?cacheDirectory=true</code>），<code>node_modules/.cache/babel-loader</code> 则 <code>node_modules</code> 在任何根目录中找不到任何文件夹时，加载程序将使用默认缓存目录或回退到默认的OS临时文件目录。实际使用中，效果显著；配置示例如下：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">rules: [</span><br><span class="line">  &#123;</span><br><span class="line">    test: /\.js$/,</span><br><span class="line">    loader: &apos;babel-loader?cacheDirectory=true&apos;,</span><br><span class="line">    exclude: /node_modules/,</span><br><span class="line">    include: [resolve(&apos;src&apos;), resolve(&apos;test&apos;)]</span><br><span class="line">  &#125;,</span><br><span class="line">  ... ...</span><br><span class="line">]</span><br></pre></td></tr></table></figure><h2 id="设置-noParse"><a href="#设置-noParse" class="headerlink" title="设置 noParse"></a>设置 <a href="https://webpack.github.io/docs/configuration.html#module-noparse" target="_blank" rel="noopener">noParse</a></h2><p>如果你确定一个模块中，没有其它新的依赖，就可以配置这项， <code>Webpack</code> 将不再扫描这个文件中的依赖，这对于比较大型类库，将能促进性能表现，具体可以参见以下配置：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">module: &#123;</span><br><span class="line">  noParse: /node_modules\/(element-ui\.js)/,</span><br><span class="line">  rules: [</span><br><span class="line">    &#123;</span><br><span class="line">      ...</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><p>在前文 <a href="https://ningyu1.github.io/site/post/26-webpack1/">Webpack 打包优化之体积</a>篇中提到，引入 <code>DllPlugin</code> 和 <code>DllReferencePlugin</code> 来提前构建一些第三方库，来优化 <code>Webpack</code> 打包。而在生产环境时，就需要将提前构建好的包，同步到 <code>dist</code> 中；这里拷贝静态文件，你可以使用 <code>copy-webpack-plugin</code> 插件：把指定文件夹下的文件复制到指定的目录；其配置如下：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">var CopyWebpackPlugin = require(&apos;copy-webpack-plugin&apos;)</span><br><span class="line">plugins: [</span><br><span class="line">  ...</span><br><span class="line">  // copy custom static assets</span><br><span class="line">  new CopyWebpackPlugin([</span><br><span class="line">    &#123;</span><br><span class="line">      from: path.resolve(__dirname, &apos;../static&apos;),</span><br><span class="line">      to: config.build.assetsSubDirectory,</span><br><span class="line">      ignore: [&apos;.*&apos;]</span><br><span class="line">    &#125;</span><br><span class="line">  ])</span><br><span class="line">]</span><br></pre></td></tr></table></figure><p>当然，这种工作，实现的法子很多，比如可以借助 <code>shelljs</code>，可以参见这里的实现 <a href="https://github.com/nicejade/vue-boilerplate-template/blob/master/build/build.js#L17-L22" target="_blank" rel="noopener">vue-boilerplate-template</a>。</p>]]></content>
    
    <summary type="html">
    
      Webpack 打包优化之速度篇
    
    </summary>
    
      <category term="Webpack" scheme="https://ningyu1.github.io/categories/Webpack/"/>
    
      <category term="Vue" scheme="https://ningyu1.github.io/categories/Webpack/Vue/"/>
    
      <category term="React" scheme="https://ningyu1.github.io/categories/Webpack/Vue/React/"/>
    
      <category term="Angular" scheme="https://ningyu1.github.io/categories/Webpack/Vue/React/Angular/"/>
    
    
      <category term="Webpack" scheme="https://ningyu1.github.io/tags/Webpack/"/>
    
      <category term="Vue" scheme="https://ningyu1.github.io/tags/Vue/"/>
    
      <category term="React" scheme="https://ningyu1.github.io/tags/React/"/>
    
      <category term="Angular" scheme="https://ningyu1.github.io/tags/Angular/"/>
    
  </entry>
  
  <entry>
    <title>Webpack 打包优化之体积篇</title>
    <link href="https://ningyu1.github.io/20170920/26-webpack1.html"/>
    <id>https://ningyu1.github.io/20170920/26-webpack1.html</id>
    <published>2017-09-20T02:36:36.000Z</published>
    <updated>2024-04-23T01:48:15.032Z</updated>
    
    <content type="html"><![CDATA[<p>文章来源：<a href="https://jeffjade.com/2017/08/06/124-webpack-packge-optimization-for-volume/" target="_blank" rel="noopener">https://jeffjade.com/2017/08/06/124-webpack-packge-optimization-for-volume/</a><br>作者：@晚晴幽草轩轩主</p><p>谈及如今欣欣向荣的前端圈，不仅有各类框架百花齐放，如<code>Vue</code>， <code>React</code>， <code>Angular</code>等等，就打包工具而言，发展也是如火如荼，百家争鸣；从早期的王者<code>Browserify</code>, <code>Grun</code>t，到后来赢得宝座的 <code>Gulp</code>， 以及独树一帜的 <code>fis3</code>, 以及下一代打包神器 <code>Rollup</code> ；在 <code>browserify</code>,<code>grunt</code>,<code>gulp</code>,<code>rollup</code>,<code>webpack</code> 可以一窥其中部分对比。在本文要探究的是，当前打包工具绝对霸者 <code>Webpack</code>。</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/webpack/1.png" alt="Webpack Package optimization" title>                </div>                <div class="image-caption">Webpack Package optimization</div>            </figure><center><em>Webpack Package optimization</em></center><p><code>Webpack</code>，当前各大主流框架默认配备的打包方案，对其如何使用，已有较完备中英文文档；并且，各主流框架也有对应 <code>CLI</code> 予以基础配置，故不作为探讨范畴。从产品层来讲，如何使得构建的包体积小、运行快，这有必要不断摸索实践，提炼升级，使之臻于最佳。本文将从以下些许方面，对 <code>Webpack</code> 打包体积方面，做下优化探讨(备注： <code>Webpack</code>实践版本： 3.3.0)：</p><h2 id="定位-webpack-大的原因"><a href="#定位-webpack-大的原因" class="headerlink" title="定位 webpack 大的原因"></a>定位 webpack 大的原因</h2><p>这里推荐使用 <a href="https://www.npmjs.com/package/webpack-bundle-analyzer" target="_blank" rel="noopener">webpack-bundle-analyzer</a> —— Webpack 插件和 CLI 实用程序，她可以将内容束展示为方便交互的直观树状图，让你明白你所构建包中真正引入的内容；我们可以借助她，发现它大体有哪些模块组成，找到不合时宜的存在，然后优化它。我们可以在 项目的 package.json 文件中注入如下命令，以方便运行她(<code>npm run analyz</code>)，默认会打开 <a href="http://127.0.0.1:8888" target="_blank" rel="noopener">http://127.0.0.1:8888</a> 作为展示。</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">“analyz”: “NODE_ENV=production npm_config_report=true npm run build”</span><br></pre></td></tr></table></figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/webpack/2.gif" alt="webpack-bundle-analyzer" title>                </div>                <div class="image-caption">webpack-bundle-analyzer</div>            </figure><center><em>webpack-bundle-analyzer</em></center><p>当然，同类型的还有 <a href="http://alexkuz.github.io/webpack-chart/" target="_blank" rel="noopener">webpack-chart</a> 以及 <a href="http://webpack.github.io/analyse/" target="_blank" rel="noopener">webpack-analyse</a>，这两个站点也是以可视方式呈现构造的组件，可以让你清楚的看到模块的组成部分；不过稍显麻烦的是，你需要运行以下命令，生成工具分析所需要的 json 文件：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">webpack --profile --json &gt; stats.json</span><br><span class="line">// 如果，运行指定的 weboack 文件，可用此命令</span><br><span class="line">webpack --config build/webpack.prod.conf.js  --profile --json &gt; stats.json</span><br></pre></td></tr></table></figure><h2 id="引入-DllPlugin-和-DllReferencePlugin"><a href="#引入-DllPlugin-和-DllReferencePlugin" class="headerlink" title="引入 DllPlugin 和 DllReferencePlugin"></a>引入 DllPlugin 和 DllReferencePlugin</h2><p>DllPlugin 和 DllReferencePlugin 提供了以大幅度提高构建时间性能的方式拆分软件包的方法。其中原理是，将特定的第三方NPM包模块提前构建👌，然后通过页面引入。这不仅能够使得 vendor 文件可以大幅度减小，同时，也极大的提高了构件速度。鉴于篇幅，具体用法可参见：<code>webpack.dll.conf.js</code>。</p><h2 id="外部引入模块-CDN"><a href="#外部引入模块-CDN" class="headerlink" title="外部引入模块(CDN)"></a>外部引入模块(CDN)</h2><p>如今前端开发，自然是使用<code>ES6</code>甚至更高版本，撸将起来才更嗨。但由于浏览器兼容问题，仍得使用 babel 转换。而这 <code>babel-polyfill</code> 也得引入以确保兼容；还比如项目开发中常用到的 <code>moment</code>, <code>lodash</code>等，都是挺大的存在，如果必须引入的话，即考虑外部引入之，再借助 <code>externals</code> 予以指定， <code>webpack</code>可以处理使之不参与打包，而依旧可以在代码中通过<code>CMD</code>、<code>AMD</code>或者<code>window</code>/<code>global</code>全局的方式访问。</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">// webpack 中予以指定</span><br><span class="line">externals: &#123;</span><br><span class="line">  // &apos;vue&apos;: &apos;Vue&apos;,</span><br><span class="line">  // &apos;lodash&apos;: &apos;_&apos;,</span><br><span class="line">  &apos;babel-polyfill&apos;: &apos;window&apos;</span><br><span class="line">&#125;</span><br><span class="line">//</span><br><span class="line">&lt;script src=&quot;//cdn.bootcss.com/autotrack/2.4.1/autotrack.js&quot;&gt;&lt;/script&gt;</span><br><span class="line">&lt;script src=&quot;//cdn.bootcss.com/babel-polyfill/7.0.0-alpha.15/polyfill.min.js&quot;&gt;&lt;/script&gt;</span><br></pre></td></tr></table></figure><p>需要补充的是 <code>externals</code> 中：key 是 require 的包名，value 是全局的变量。</p><h2 id="让每个第三包“引有所值”"><a href="#让每个第三包“引有所值”" class="headerlink" title="让每个第三包“引有所值”"></a>让每个第三包“引有所值”</h2><h3 id="确定引入的必要性"><a href="#确定引入的必要性" class="headerlink" title="确定引入的必要性"></a>确定引入的必要性</h3><p>前端发展到如今时期，倘若项目采用了 <code>MVVM</code>模式框架，数据双向绑定，那么像 <code>jQuery</code> 这般类库，不能说没有丝毫引入的必要，至少可以说确实没有引入的必要。对此，如果还有些顾虑，完全可以参考下 <a href="http://youmightnotneedjquery.com/" target="_blank" rel="noopener">YOU MIGHT NOT NEED JQUERY</a>；用原生写几行代码就可以解决的事儿，实在不易引入这么个庞然大物，平添烦恼。</p><h3 id="避免类库引而不用"><a href="#避免类库引而不用" class="headerlink" title="避免类库引而不用"></a>避免类库引而不用</h3><p>倘若这类情况发生，对整个打包体积，不仅大而且亏。项目一旦大了，很难人为保证每个引入的类库，都被有用到，尤其是二次开发。所以工具的利用十分必要，强烈推荐类如 <code>Eslint</code> 这般工具，并且注入对应规则，对声明却未使用的代码，给予强制提醒；这不仅可以有效的规避类似情形发生(也适用于普通变量的检测)，而且还能使得团队代码风格，尽可能地保持相似；要知道代码足够遵守规则，也可让压缩工具更有效压缩代码，一举多得，何乐不为？</p><h3 id="尽量使用模块化引入"><a href="#尽量使用模块化引入" class="headerlink" title="尽量使用模块化引入"></a>尽量使用模块化引入</h3><p>如果说 <code>jQuery</code> 确实没有引入必要，很多人会同意；但对于 <code>lodash</code> 这类依赖的工具，并不是所有人都会去造一发轮子的。然而全包引入 400kb 的体量，可否有让你心肝一颤？幸好的是，<code>lodash</code> 提供了模块化的引入方式；可按需引入，快哉快哉：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">import &#123; debounce &#125; from &apos;lodash&apos;</span><br><span class="line">import &#123; throttle &#125; from &apos;lodash&apos;</span><br><span class="line">// 改成如下写法</span><br><span class="line">import debounce from &apos;lodash/debounce&apos;</span><br><span class="line">import throttle from &apos;lodash/throttle&apos;</span><br></pre></td></tr></table></figure><p>擅懒如你的优秀程序员，是否也发现这样写颇为麻烦？那么恭喜你，这个问题已经被解决；<a href="https://github.com/lodash/lodash-webpack-plugin" target="_blank" rel="noopener">lodash-webpack-plugin</a> 和 <a href="https://www.npmjs.com/package/babel-plugin-lodash" target="_blank" rel="noopener">babel-plugin-lodash</a> 的存在（组合使用），即是解决这问题的。它可将全路径引用的 <code>lodash</code>， 自动转变为模块化按使用引入（如下例示）；并且所需配置也十分简单，就不在此赘述(温馨提示：当涉及些特殊方法时，尚需些留意)。</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">// 引入组件，自动转换</span><br><span class="line">import _ from &apos;lodash&apos;</span><br><span class="line">_.debounce()</span><br><span class="line">_.throttle()</span><br></pre></td></tr></table></figure><p>额外补充的是，即便采用如上写法，还是不够快捷，每个用到的文件，都写一遍 <code>import</code>，实在多有不便。更可取的是，将项目所需的方法，统一引入，按需添加，组建出本地 <code>lodash</code> 类库，然后 <code>export</code> 给框架层（比如 <code>Vue.prototype</code>），以便全局使用；详情可参见：<a href="https://github.com/nicejade/vue-boilerplate-template" target="_blank" rel="noopener">vue-modular-import-lodash</a>。</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">// helper 文件夹下 lodash，统一引入你需要的方法</span><br><span class="line">import _ from &apos;lodash&apos;</span><br><span class="line">export default &#123;</span><br><span class="line">  cloneDeep: _.cloneDeep,</span><br><span class="line">  debounce: _.debounce,</span><br><span class="line">  throttle: _.throttle,</span><br><span class="line">  size: _.size,</span><br><span class="line">  pick: _.pick,</span><br><span class="line">  isEmpty: _.isEmpty</span><br><span class="line">&#125;</span><br><span class="line">// 注入到全局</span><br><span class="line">import _ from &apos;@helper/lodash.js&apos;</span><br><span class="line">Vue.prototype.$_ = _</span><br><span class="line">// vue 组件内运用</span><br><span class="line">this.$_.debounce()</span><br></pre></td></tr></table></figure><h3 id="尽可能引入更合适的包"><a href="#尽可能引入更合适的包" class="headerlink" title="尽可能引入更合适的包"></a>尽可能引入更合适的包</h3><p>作为前端开发的你，想必知道有 <a href="https://github.com/moment/moment" target="_blank" rel="noopener">momentjs</a> 的存在（Parse, validate, manipulate, and display dates in javascript.）；更多的是，你想必知道它很好用，然而它的体态却十分丰满(丰盈)，没念及此，是否有重新造轮子的冲动？<a href="https://github.com/smallwins/spacetime" target="_blank" rel="noopener">SpaceTime</a>: A lightweight way to manipulate, traverse, compare, and format dates and times across planet Earth。 具有与 <code>monent</code> 相似 api 的新类库，其体积又相对小很多（当然，据观察其灵活度略逊一筹）；<a href="https://github.com/date-fns/date-fns" target="_blank" rel="noopener">date-fns</a>：现代JavaScript日期实用程序库（ Modern JavaScript date utility library ），如 <code>lodash</code> 一样，可支持模块化；知道这些或者更多的你，会如何选择？</p><h2 id="按需异步加载模块"><a href="#按需异步加载模块" class="headerlink" title="按需异步加载模块"></a>按需异步加载模块</h2><p>关于前端开发优化，重要的一条是，尽可能合并请求及资源，如常用的请求数据合并，压缩合并 <code>js</code>，构造雪碧图诸此等等（当然得适当，注意体积，过大不宜）；但，同时也当因需制宜，根据需要去异步加载，避免无端就引入早成的浪费。<code>webpack</code> 也是内置对这方面的支持； 假如，你使用的是 <code>Vue</code>，将一个组件（以及其所有依赖）改为异步加载，所需要的只是把：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">import Foo from &apos;./Foo.vue&apos;</span><br></pre></td></tr></table></figure><p>改为如下写法:</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">const Foo = () =&gt; import(&apos;./Foo.vue&apos;)</span><br></pre></td></tr></table></figure><p>如此分割之时，该组件所依赖的其他组件或其他模块，都会自动被分割进对应的 <code>chunk</code> 里，实现异步加载，当然也支持把组件按组分块，将同组中组件，打包在同个异步 <code>chunk</code> 中。如此能够非常有效的抑制 <code>Javascript</code> 包过大，同时也使得资源的利用更加合理化。</p><h2 id="生产环境，压缩混淆并移除console"><a href="#生产环境，压缩混淆并移除console" class="headerlink" title="生产环境，压缩混淆并移除console"></a>生产环境，压缩混淆并移除console</h2><p>现代化中等规模以上的开发中，区分<code>开发环境</code>、<code>测试环境</code>和<code>生产环境</code>，并根据需要予以区别对待，已然成为行业共识；可能的话，还会有预发布环境。对待生产环境，压缩混淆可以很有效的减小包的体积；同时，如果能够移除使用比较频繁的 <code>console</code>，而不是简单的替换为空方法，也是精彩的一笔小优化。如果使用 <code>UglifyJsPlugin</code> 插件来压缩代码，加入如下配置，即可移除掉代码中的 <code>console</code>：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">new webpack.optimize.UglifyJsPlugin(&#123;</span><br><span class="line">  compress: &#123;</span><br><span class="line">    warnings: false,</span><br><span class="line">    drop_console: true,</span><br><span class="line">    pure_funcs: [&apos;console.log&apos;]</span><br><span class="line">  &#125;,</span><br><span class="line">  sourceMap: false</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><h2 id="Webpack3-新功能-Scope-Hoisting"><a href="#Webpack3-新功能-Scope-Hoisting" class="headerlink" title="Webpack3 新功能: Scope Hoisting"></a>Webpack3 新功能: Scope Hoisting</h2><p>截止目前(17-08-06), Webpack 最新版本是 3.4.1；Webpack在 3.0 版本，提供了一个新的功能：<code>Scope Hoisting</code>，又译作“作用域提升”。只需在配置文件中添加一个新的插件，就可以让 Webpack 打包出来的代码文件更小、运行的更快：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">module.exports = &#123;</span><br><span class="line">  plugins: [</span><br><span class="line">    new webpack.optimize.ModuleConcatenationPlugin()</span><br><span class="line">  ]</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>据悉这个 <code>Scope Hoisting</code> 与 <code>Tree Shaking</code>，最初都是由 <code>Rollup</code> 实现的。在个人中实践中，这个功能的注入，对打包体积虽有影响，却不甚明显，有兴趣的盆友可以试下；更对关于此功能讯息，可参见 <code>Webpack 3</code> 的新功能：<code>Scope Hoisting</code>。</p><p>下一篇 《<a href="https://ningyu1.github.io/site/post/27-webpack2/">Webpack 打包优化之速度篇</a>》</p>]]></content>
    
    <summary type="html">
    
      Webpack 打包优化之体积篇
    
    </summary>
    
      <category term="Webpack" scheme="https://ningyu1.github.io/categories/Webpack/"/>
    
      <category term="Vue" scheme="https://ningyu1.github.io/categories/Webpack/Vue/"/>
    
      <category term="React" scheme="https://ningyu1.github.io/categories/Webpack/Vue/React/"/>
    
      <category term="Angular" scheme="https://ningyu1.github.io/categories/Webpack/Vue/React/Angular/"/>
    
      <category term="技术" scheme="https://ningyu1.github.io/categories/Webpack/Vue/React/Angular/%E6%8A%80%E6%9C%AF/"/>
    
    
      <category term="Webpack" scheme="https://ningyu1.github.io/tags/Webpack/"/>
    
      <category term="Vue" scheme="https://ningyu1.github.io/tags/Vue/"/>
    
      <category term="React" scheme="https://ningyu1.github.io/tags/React/"/>
    
      <category term="Angular" scheme="https://ningyu1.github.io/tags/Angular/"/>
    
  </entry>
  
  <entry>
    <title>五种开源协议(GPL,LGPL,BSD,MIT,Apache)</title>
    <link href="https://ningyu1.github.io/20170919/25-licence.html"/>
    <id>https://ningyu1.github.io/20170919/25-licence.html</id>
    <published>2017-09-19T05:36:36.000Z</published>
    <updated>2024-04-23T01:48:15.032Z</updated>
    
    <content type="html"><![CDATA[<h2 id="什么是许可协议？"><a href="#什么是许可协议？" class="headerlink" title="什么是许可协议？"></a>什么是许可协议？</h2><p>什么是许可，当你为你的产品签发许可，你是在出让自己的权利，不过，你仍然拥有版权和专利（如果申请了的话），许可的目的是，向使用你产品的人提供 一定的权限。</p><p>不管产品是免费向公众分发，还是出售，制定一份许可协议非常有用，否则，对于前者，你相当于放弃了自己所有的权利，任何人都没有义务表明你的原始作 者身份，对于后者，你将不得不花费比开发更多的精力用来逐个处理用户的授权问题。</p><p>而<a href="http://en.wikipedia.org/wiki/Open-source_license" target="_blank" rel="noopener">开源许可协议</a>使这些事情变得简单，开发者很容易向一个项目贡献自己的代码，它还可以保护你原始作者的身份，使你 至少获得认可，开源许可协议还可以阻止其它人将某个产品据为己有。以下是开源界的 5 大许可协议。</p><h2 id="GNU-GPL"><a href="#GNU-GPL" class="headerlink" title="GNU GPL"></a>GNU GPL</h2><p><a href="http://www.opensource.org/licenses/gpl-2.0.php" target="_blank" rel="noopener">GNU General Public Licence</a> (GPL) 有可能是开源界最常用的许可模式。GPL 保证了所有开发者的权利，同时为使用者提供了足够的复制，分发，修改的权利：</p><ul><li>可自由复制<br>你可以将软件复制到你的电脑，你客户的电脑，或者任何地方。复制份数没有任何限制。</li><li>可自由分发<br>在你的网站提供下载，拷贝到U盘送人，或者将源代码打印出来从窗户扔出去（环保起见，请别这样做）。</li><li>可以用来盈利<br>你可以在分发软件的时候收费，但你必须在收费前向你的客户提供该软件的 GNU GPL 许可协议，以便让他们知道，他们可以从别的渠道免费得到这份软件，以及你收费的理由。</li><li>可自由修改<br>如果你想添加或删除某个功能，没问题，如果你想在别的项目中使用部分代码，也没问题，唯一的要求是，使用了这段代码的项目也必须使用 GPL 协议。</li></ul><p>需要注意的是，分发的时候，需要明确提供源代码和二进制文件，另外，用于某些程序的某些协议有一些问题和限制，你可以看一下 <a href="http://www.twitter.com/PierreJoye" target="_blank" rel="noopener">@PierreJoye</a> 写的 <a href="http://www.softwarefreedom.org/resources/2008/compliance-guide.html" target="_blank" rel="noopener">Practical Guide to GPL Compliance</a> 一文。使用 GPL 协议，你必须在源代码代码中包含相应信息，以及协议本身。</p><h2 id="GNU-LGPL"><a href="#GNU-LGPL" class="headerlink" title="GNU LGPL"></a>GNU LGPL</h2><p>GNU 还有另外一种协议，叫做 LGPL （<a href="http://www.opensource.org/licenses/lgpl-2.1.php" target="_blank" rel="noopener">Lesser General Public Licence</a>），它对产品所保留的权利比 GPL 少，总的来说，LGPL 适合那些用于非 GPL 或非开源产品的开源类库或框架。因为 GPL 要求，使用了 GPL 代码的产品必须也使用 GPL 协议，开发者不允许将 GPL 代码用于商业产品。LGPL 绕过了这一限制。</p><h2 id="BSD"><a href="#BSD" class="headerlink" title="BSD"></a>BSD</h2><p>BSD 在软件分发方面的限制比别的开源协议（如 GNU GPL）要少。该协议有多种版本，最主要的版本有两个，新 BSD 协议与简单 BSD 协议，这两种协议经过修正，都和 GPL 兼容，并为开源组织所认可。</p><p>新 BSD 协议（3条款协议）在软件分发方面，除需要包含一份版权提示和免责声明之外，没有任何限制。另外，该协议还禁止拿开发者的名义为衍生产品背书，但简单 BSD 协议删除了这一条款。</p><h2 id="MIT"><a href="#MIT" class="headerlink" title="MIT"></a>MIT</h2><p><a href="http://www.opensource.org/licenses/mit-license.php" target="_blank" rel="noopener">MIT 协议</a>可能是几大开源协议中最宽松的一个，核心条款是：</p><p>该软件及其相关文档对所有人免费，可以任意处置，包括使用，复制，修改，合并，发表，分发，再授权，或者销售。唯一的限制是，软件中必须包含上述版 权和许可提示。</p><p>这意味着：</p><ul><li>你可以自由使用，复制，修改，可以用于自己的项目。</li><li>可以免费分发或用来盈利。</li><li>唯一的限制是必须包含许可声明。</li></ul><p>MIT 协议是所有开源许可中最宽松的一个，除了必须包含许可声明外，再无任何限制。</p><h2 id="Apache"><a href="#Apache" class="headerlink" title="Apache"></a>Apache</h2><p>Apache 协议 2.0 和别的开源协议相比，除了为用户提供版权许可之外，还有专利许可，对于那些涉及专利内容的开发者而言，该协议最适合（<a href="http://www.howstuffworks.com/question492.htm" target="_blank" rel="noopener">这里有 一篇文章阐述这个问题</a>）。</p><p>Apache 协议还有以下需要说明的地方:</p><ul><li>永久权利<br>一旦被授权，永久拥有。</li><li>全球范围的权利<br>在一个国家获得授权，适用于所有国家。假如你在美国，许可是从印度授权的，也没有问题。</li><li>授权免费，且无版税<br>前期，后期均无任何费用。</li><li>授权无排他性<br>任何人都可以获得授权</li><li>授权不可撤消<br>一旦获得授权，没有任何人可以取消。比如，你基于该产品代码开发了衍生产品，你不用担心会在某一天被禁止使用该代码。</li></ul><p>分发代码方面包含一些要求，主要是，要在声明中对参与开发的人给予认可并包含一份许可协议原文。</p><h2 id="Creative-Commons"><a href="#Creative-Commons" class="headerlink" title="Creative Commons"></a>Creative Commons</h2><p>Creative Commons (CC) 并非严格意义上的开源许可，它主要用于设计。Creative Commons 有多种协议，每种都提供了相应授权模式，CC 协议主要包含 4 种基本形式：</p><ul><li>署名权<br>必须为原始作者署名，然后才可以修改，分发，复制。</li><li>保持一致<br>作品同样可以在 CC 协议基础上修改，分发，复制。</li><li>非商业<br>作品可以被修改，分发，复制，但不能用于商业用途。但商业的定义有些模糊，比如，有的人认为非商业用途指的是不能销售，有的认为是甚至不能放在有广告的网 站，也有人认为非商业的意思是非盈利。</li><li>不能衍生新作品<br>你可以复制，分发，但不能修改，也不能以此为基础创作自己的作品。</li></ul><p>这些许可形式可以结合起来用，其中最严厉的组合是“署名，非商用，不能衍生新作品”，意味着，你可以分享作品，但不能改动或以此盈利，而且必须为原 作者署名。在这种许可模式下，原始作者对作品还拥有完全的控制权，而最宽松的组合是“署名”，意味着，只要为原始作者署名了，就可以自由处置。</p><p>## </p><p><strong>因为在此之前，我用了国内的一些开源程序，但是呢这些程序都是需要商业授权的，不知道能不能免费的自己搭建起来给企业用。比如说 shopex，康盛的产品， PHPCMS等等。。。。<br>如果真用了，他们会找上门来问你要版权么？</strong></p>]]></content>
    
    <summary type="html">
    
      五种开源协议(GPL,LGPL,BSD,MIT,Apache)
    
    </summary>
    
      <category term="Licence" scheme="https://ningyu1.github.io/categories/Licence/"/>
    
    
      <category term="Licence" scheme="https://ningyu1.github.io/tags/Licence/"/>
    
      <category term="GPL" scheme="https://ningyu1.github.io/tags/GPL/"/>
    
      <category term="LGPL" scheme="https://ningyu1.github.io/tags/LGPL/"/>
    
      <category term="BSD" scheme="https://ningyu1.github.io/tags/BSD/"/>
    
      <category term="MIT" scheme="https://ningyu1.github.io/tags/MIT/"/>
    
      <category term="Apache" scheme="https://ningyu1.github.io/tags/Apache/"/>
    
  </entry>
  
  <entry>
    <title>利用Zipkin对Spring Cloud应用进行服务追踪分析</title>
    <link href="https://ningyu1.github.io/20170908/24-zipkin.html"/>
    <id>https://ningyu1.github.io/20170908/24-zipkin.html</id>
    <published>2017-09-08T05:36:36.000Z</published>
    <updated>2024-04-23T01:48:15.032Z</updated>
    
    <content type="html"><![CDATA[<p>文章转自：<a href="https://yq.aliyun.com/articles/60165" target="_blank" rel="noopener">https://yq.aliyun.com/articles/60165</a><br>作者：@libinjingshan</p><h2 id="摘要：-本文简单介绍了如何利用Zipkin对SpringCloud应用进行服务分析。在实际的应用场景中，Zipkin可以结合压力测试工具一起使用，分析系统在大压力下的可用性和性能。"><a href="#摘要：-本文简单介绍了如何利用Zipkin对SpringCloud应用进行服务分析。在实际的应用场景中，Zipkin可以结合压力测试工具一起使用，分析系统在大压力下的可用性和性能。" class="headerlink" title="摘要： 本文简单介绍了如何利用Zipkin对SpringCloud应用进行服务分析。在实际的应用场景中，Zipkin可以结合压力测试工具一起使用，分析系统在大压力下的可用性和性能。"></a>摘要： 本文简单介绍了如何利用Zipkin对SpringCloud应用进行服务分析。在实际的应用场景中，Zipkin可以结合压力测试工具一起使用，分析系统在大压力下的可用性和性能。</h2><p>设想这么一种情况，如果你的微服务数量逐渐增大，服务间的依赖关系越来越复杂，怎么分析它们之间的调用关系及相互的影响？</p><h2 id="服务追踪分析"><a href="#服务追踪分析" class="headerlink" title="服务追踪分析"></a>服务追踪分析</h2><p>一个由微服务构成的应用系统通过服务来划分问题域，通过REST请求服务API来连接服务来完成完整业务。对于入口的一个调用可能需要有多个后台服务协同完成，链路上任何一个调用超时或出错都可能造成前端请求的失败。服务的调用链也会越来越长，并形成一个树形的调用链。</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/zipkin/1.png" alt="1.png" title>                </div>                <div class="image-caption">1.png</div>            </figure><p>随着服务的增多，对调用链的分析也会越来越负责。设想你在负责下面这个系统，其中每个小点都是一个微服务，他们之间的调用关系形成了复杂的网络。</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/zipkin/2.png" alt="2.png" title>                </div>                <div class="image-caption">2.png</div>            </figure><p>有密集恐惧症的同学就忽略吧。</p><p>针对服务化应用全链路追踪的问题，Google发表了<a href="http://research.google.com/pubs/pub36356.html?spm=5176.100239.blogcont60165.12.zDVtxt" target="_blank" rel="noopener">Dapper论文</a>，介绍了他们如何进行服务追踪分析。其基本思路是在服务调用的请求和响应中加入ID，标明上下游请求的关系。利用这些信息，可以可视化地分析服务调用链路和服务间的依赖关系。</p><h2 id="Spring-Cloud-Sleuth和Zipkin"><a href="#Spring-Cloud-Sleuth和Zipkin" class="headerlink" title="Spring Cloud Sleuth和Zipkin"></a>Spring Cloud Sleuth和Zipkin</h2><p>对应Dpper的开源实现是<a href="http://zipkin.io/?spm=5176.100239.blogcont60165.13.zDVtxt" target="_blank" rel="noopener">Zipkin</a>，支持多种语言包括JavaScript，Python，Java, Scala, Ruby, C#, Go等。其中Java由多种不同的库来支持。</p><p>在这个示例中，我们准备开发两个基于Spring Cloud的应用，利用Spring Cloud Sleuth来和Zipkin进行集成。Spring Cloud Sleuth是对Zipkin的一个封装，对于Span、Trace等信息的生成、接入HTTP Request，以及向Zipkin Server发送采集信息等全部自动完成。</p><p>这是Spring Cloud Sleuth的概念图。</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/zipkin/3.png" alt="3.png" title>                </div>                <div class="image-caption">3.png</div>            </figure><h2 id="服务REST调用"><a href="#服务REST调用" class="headerlink" title="服务REST调用"></a>服务REST调用</h2><p>本次演示的服务有两个：tracedemo做为前端服务接收用户的请求，tracebackend为后端服务，tracedemo通过http协议调用后端服务。</p><p>利用RestTemplate进行HTTP请求调用<br>tracedemo应用通过restTemplate调用后端tracedemo服务，注意，URL中指明tracedemo的地址为<code>backend</code>。</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="meta">@RequestMapping</span>(<span class="string">"/"</span>)</span><br><span class="line"><span class="function"><span class="keyword">public</span> String <span class="title">callHome</span><span class="params">()</span></span>&#123;</span><br><span class="line">    LOG.log(Level.INFO, <span class="string">"calling trace demo backend"</span>);</span><br><span class="line">    <span class="keyword">return</span> restTemplate.getForObject(<span class="string">"http://backend:8090"</span>, String<span class="class">.<span class="keyword">class</span>)</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>后端服务响应HTTP请求，输出一行日志后返回经典的“hello world”。</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="meta">@RequestMapping</span>(<span class="string">"/"</span>)</span><br><span class="line"><span class="function"><span class="keyword">public</span> String <span class="title">home</span><span class="params">()</span></span>&#123;</span><br><span class="line">    LOG.log(Level.INFO, <span class="string">"trace demo backend is being called"</span>);</span><br><span class="line">    <span class="keyword">return</span> <span class="string">"Hello World."</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="引入Sleuth和Zipkin依赖包"><a href="#引入Sleuth和Zipkin依赖包" class="headerlink" title="引入Sleuth和Zipkin依赖包"></a>引入Sleuth和Zipkin依赖包</h2><p>可以看到，这是典型的两个spring应用通过RestTemplate进行访问的方式，哪在HTTP请求中注入追踪信息并把相关信息发送到Zipkin Server呢？答案在两个应用所加载的JAR包里。</p><p>本示例采用gradle来构建应用，在build.gradle中加载了sleuth和zipkin相关的JAR包：</p><figure class="highlight xml"><table><tr><td class="code"><pre><span class="line">dependencies &#123;</span><br><span class="line">    compile('org.springframework.cloud:spring-cloud-starter-sleuth')</span><br><span class="line">    compile('org.springframework.cloud:spring-cloud-sleuth-zipkin')</span><br><span class="line">    testCompile('org.springframework.boot:spring-boot-starter-test')</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>Spring应用在监测到Java依赖包中有sleuth和zipkin后，会自动在RestTemplate的调用过程中向HTTP请求注入追踪信息，并向Zipkin Server发送这些信息。</p><p>哪么Zipkin Server的地址又是在哪里指定的呢？答案是在application.properties中：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">spring.zipkin.base-url=http://zipkin-server:9411</span><br></pre></td></tr></table></figure><p>注意Zipkin Server的地址为<code>zipkin-server</code>。</p><h2 id="构建Docker镜像"><a href="#构建Docker镜像" class="headerlink" title="构建Docker镜像"></a>构建Docker镜像</h2><p>为这两个服务创建相同的Dockerfile，用于生成Docker镜像：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">FROM java:8-jre-alpine</span><br><span class="line">RUN sed -i &apos;s/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/&apos; /etc/apk/repositories</span><br><span class="line">VOLUME /tmp</span><br><span class="line">ADD build/libs/*.jar app.jar</span><br><span class="line">RUN sh -c &apos;touch /app.jar&apos;</span><br><span class="line">ENTRYPOINT [&quot;java&quot;,&quot;-Djava.security.egd=file:/dev/./urandom&quot;,&quot;-jar&quot;,&quot;/app.jar&quot;]</span><br></pre></td></tr></table></figure><p>构建容器镜像的步骤如下：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">cd tracedemo</span><br><span class="line">./gradlew build</span><br><span class="line">docker build -t zipkin-demo-frontend .</span><br><span class="line"></span><br><span class="line">cd ../tracebackend</span><br><span class="line">./gradlew build</span><br><span class="line">docker build -t zipkin-demo-backend .</span><br></pre></td></tr></table></figure><p>构建镜像完成后用<code>docker push</code>命令上传到你的镜像仓库。</p><h2 id="Zipkin-Server"><a href="#Zipkin-Server" class="headerlink" title="Zipkin Server"></a>Zipkin Server</h2><p>利用Annotation声明方式创建Zipkin<br>在build.gradle中引入Zipkin依赖包。</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">dependencies &#123;</span><br><span class="line">    compile(&apos;org.springframework.boot:spring-boot-starter&apos;)</span><br><span class="line">    compile(&apos;io.zipkin.java:zipkin-server&apos;)</span><br><span class="line">    runtime(&apos;io.zipkin.java:zipkin-autoconfigure-ui&apos;)</span><br><span class="line">    testCompile(&apos;org.springframework.boot:spring-boot-starter-test&apos;)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在主程序Class增加一个注解<code>@EnableZipkinServer</code></p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="meta">@SpringBootApplication</span></span><br><span class="line"><span class="meta">@EnableZipkinServer</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">ZipkinApplication</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> </span>&#123;</span><br><span class="line">        SpringApplication.run(ZipkinApplication<span class="class">.<span class="keyword">class</span>, <span class="title">args</span>)</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在<code>application.properties</code>将端口指定为9411。</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">server.port=9411</span><br></pre></td></tr></table></figure><h2 id="构建Docker镜像-1"><a href="#构建Docker镜像-1" class="headerlink" title="构建Docker镜像"></a>构建Docker镜像</h2><p>Dockerfile和前面的两个服务一样，这里就不重复了。</p><p>在阿里云容器服务上部署<br>创建docker-compose.yml文件，内容如下：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">version: &quot;2&quot;</span><br><span class="line">services:</span><br><span class="line">  zipkin-server:</span><br><span class="line">    image: registry.cn-hangzhou.aliyuncs.com/jingshanlb/zipkin-demo-server</span><br><span class="line">    labels:</span><br><span class="line">      aliyun.routing.port_9411: http://zipkin</span><br><span class="line">    restart: always</span><br><span class="line"></span><br><span class="line">  frontend:</span><br><span class="line">    image: registry.cn-hangzhou.aliyuncs.com/jingshanlb/zipkin-demo-frontend</span><br><span class="line">    labels:</span><br><span class="line">      aliyun.routing.port_8080: http://frontend</span><br><span class="line">    links:</span><br><span class="line">      - zipkin-server</span><br><span class="line">      - backend</span><br><span class="line">    restart: always</span><br><span class="line"></span><br><span class="line">  backend:</span><br><span class="line">    image: registry.cn-hangzhou.aliyuncs.com/jingshanlb/zipkin-demo-backend</span><br><span class="line">    links:</span><br><span class="line">      - zipkin-server</span><br><span class="line">    restart: always</span><br></pre></td></tr></table></figure><p>在阿里云容器服务上<code>使用编排模版</code>创建应用，访问zipkin端点，可以看到服务分析的效果。</p><p>访问前端应用3次，页面显示3次服务调用。</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/zipkin/4.png" alt="4.png" title>                </div>                <div class="image-caption">4.png</div>            </figure><p>点击其中任意一个trace，可以看到请求链路上不同span所花费的时间。</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/zipkin/5.png" alt="5.png" title>                </div>                <div class="image-caption">5.png</div>            </figure><p>进入Dependencies页面，还可以看到服务之间的依赖关系。</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/zipkin/6.png" alt="6.png" title>                </div>                <div class="image-caption">6.png</div>            </figure><p>从这个过程可以看出，Zipkin和Spring Cloud的集成做得很好。而且对服务追踪分析的可视化也很直观。</p><p>注意的是，在生产环境中还需要为Zipkin配置数据库，这里就不详细介绍了。</p><p>本文的示例代码在此：<a href="https://github.com/binblee/zipkin-demo?spm=5176.100239.blogcont60165.18.zDVtxt" target="_blank" rel="noopener">https://github.com/binblee/zipkin-demo</a></p><h2 id="小节"><a href="#小节" class="headerlink" title="小节"></a>小节</h2><p>本文简单介绍了如何利用Zipkin对SpringCloud应用进行服务分析。在实际的应用场景中，Zipkin可以结合压力测试工具一起使用，分析系统在大压力下的可用性和性能。这部分内容未来会在DevOps系列中继续介绍。</p>]]></content>
    
    <summary type="html">
    
      利用Zipkin对Spring Cloud应用进行服务追踪分析
    
    </summary>
    
      <category term="Spring Cloud" scheme="https://ningyu1.github.io/categories/Spring-Cloud/"/>
    
      <category term="技术" scheme="https://ningyu1.github.io/categories/Spring-Cloud/%E6%8A%80%E6%9C%AF/"/>
    
    
      <category term="Spring Cloud" scheme="https://ningyu1.github.io/tags/Spring-Cloud/"/>
    
      <category term="Zipkin" scheme="https://ningyu1.github.io/tags/Zipkin/"/>
    
  </entry>
  
  <entry>
    <title>Spring Cloud学习-Eureka、Ribbon和Feign</title>
    <link href="https://ningyu1.github.io/20170908/23-spring-cloud.html"/>
    <id>https://ningyu1.github.io/20170908/23-spring-cloud.html</id>
    <published>2017-09-08T01:09:36.000Z</published>
    <updated>2024-04-23T01:48:15.032Z</updated>
    
    <content type="html"><![CDATA[<h1 id="前沿"><a href="#前沿" class="headerlink" title="前沿"></a>前沿</h1><p>这篇文章比较适合入门，对于spring cloud生态的成员有一个大致的了解，其实spring cloud生态将netflix的产品进行了很好的整合，netflix早几年就在服务治理这块有很深入的研究，出品了很多服务治理的工具hystrix就是很有名的一个，具体可以查看：<a href="https://github.com/netflix" target="_blank" rel="noopener">https://github.com/netflix</a>，刚好在微服务盛行的年代服务治理是必不可少的一环，现在在微服务开发套件这块常用也就是下面这两种选择：</p><ol><li>spring cloud套件，成熟上手快</li><li>自建微服务架构<ol><li>UCM，统一配置管理（百度的disconf、阿里的diamond、点评的lion，等很多开源的）。</li><li>RPC，阿里的Dubbo、点评的Pigeon，当当改的DubboX，grpc，等等很多开源的，还有很多公司自研的。</li><li>服务治理，netflix的hystrix老牌的功能强大的服务治理工具，有熔断、降级等功能，很多公司会结合监控套件开发自己的服务治理工具。</li><li>开发框架（rpc、restful这个一般公司都有自研的开发框架）</li><li>注册中心（zookeeper、redis、Consul、SmartStack、Eureka，其中一些已经是spring cloud生态的一员了）。</li><li>网关，restful的使用nginx+lua，这也是openAPI网关常用的手段</li><li>负载均衡，这个结合选用的rpc框架来选择。一般rpc框架都有负载均衡的功能。</li><li>服务治理熔断，使用hystrix（也已经是spring cloud生态的一员了）</li><li>监控，使用pinpoint、点评的cat、等其他开源的APM工具</li><li>DevOPS，持续交付一般也是自己构架的，采用jenkins打包docker镜像，使用docker生态的工具构建容器化发布平台。</li></ol></li></ol><p>下面文章转自：<a href="https://www.jianshu.com/p/0aef3724e6bc" target="_blank" rel="noopener">https://www.jianshu.com/p/0aef3724e6bc</a><br>作者：@杜琪</p><h2 id="Talk-is-cheap，show-me-the-code-，-书上得来终觉浅，绝知此事要躬行。在自己真正实现的过程中，会遇到很多莫名其妙的问题，而正是在解决这些问题的过程中，你会发现自己之前思维的盲点。"><a href="#Talk-is-cheap，show-me-the-code-，-书上得来终觉浅，绝知此事要躬行。在自己真正实现的过程中，会遇到很多莫名其妙的问题，而正是在解决这些问题的过程中，你会发现自己之前思维的盲点。" class="headerlink" title="Talk is cheap，show me the code ， 书上得来终觉浅，绝知此事要躬行。在自己真正实现的过程中，会遇到很多莫名其妙的问题，而正是在解决这些问题的过程中，你会发现自己之前思维的盲点。"></a>Talk is cheap，show me the code ， 书上得来终觉浅，绝知此事要躬行。在自己真正实现的过程中，会遇到很多莫名其妙的问题，而正是在解决这些问题的过程中，你会发现自己之前思维的盲点。</h2><h2 id="引子"><a href="#引子" class="headerlink" title="引子"></a>引子</h2><p>看完《微服务设计》后，算是补上了自己在服务化这块的理论知识，在业界，一般有两种微服务的实践方法：基于dubbo的微服务架构、基于Spring Cloud的微服务架构。从概念上来讲，Dubbo和Spring Cloud并不能放在一起对比，因为Dubbo仅仅是一个RPC框架，实现Java程序的远程调用，实施服务化的中间件则需要自己开发；而Spring Cloud则是实施微服务的一系列套件，包括：服务注册与发现、断路器、服务状态监控、配置管理、智能路由、一次性令牌、全局锁、分布式会话管理、集群状态管理等。</p><p>在有赞，我们基于Dubbo实施服务化，刚开始是基于ZooKeeper进行服务注册与发现，现在已经转成使用Etcd。我这次学习Spring Cloud，则是想成体系得学习下微服务架构的实现，也许能够对基于Dubbo实施微服务架构有所借鉴。</p><p>Spring Cloud下有很多工程：</p><ul><li>Spring Cloud Config：依靠git仓库实现的中心化配置管理。配置资源可以映射到Spring的不同开发环境中，但是也可以使用在非Spring应用中。</li><li>Spring Cloud Netflix：不同的Netflix OSS组件的集合：Eureka、Hystrix、Zuul、Archaius等。</li><li>Spring Cloud Bus：事件总线，利用分布式消息将多个服务连接起来。非常适合在集群中传播状态的改变事件（例如：配置变更事件）</li><li>Spring Cloud Consul：服务发现和配置管理，由Hashicorp团队开发。</li></ul><p>我决定先从Spring Cloud Netflix看起，它提供了如下的功能特性：</p><ul><li>服务发现：Eureka-server实例作为服务提供者，可以注册到服务注册中心，Eureka客户端可以通过Spring管理的bean发现实例；</li><li>服务发现：嵌套式的Eureka服务可以通过声明式的Java配置文件创建；</li><li>断路器：利用注解，可以创建一个简单的Hystrix客户端；</li><li>断路器：通过Java配置文件可以创建内嵌的Hystrix控制面板；</li><li>声明式REST客户端：使用Feign可以创建声明式、模板化的HTTP客户端；</li><li>客户端负载均衡器：Ribbon</li><li>路由器和过滤器：Zuul可以在微服务架构中提供路由功能、身份验证、服务迁移、金丝雀发布等功能。</li></ul><p>本文计划利用Eureka实现一个简答的服务注册于发现的例子，需要创建三个角色：服务注册中心、服务提供者、服务消费者。</p><h2 id="实践"><a href="#实践" class="headerlink" title="实践"></a>实践</h2><h3 id="1-服务注册中心"><a href="#1-服务注册中心" class="headerlink" title="1. 服务注册中心"></a>1. 服务注册中心</h3><p>在IDEA中创建一个Spring Cloud工程，引入Eureka-Server包，pom文件整体如下：</p><figure class="highlight"><table><tr><td class="code"><pre><span class="line">&lt;?xml version=<span class="string">"1.0"</span> encoding=<span class="string">"UTF-8"</span>?&gt;</span><br><span class="line">&lt;project xmlns=<span class="string">"http://maven.apache.org/POM/4.0.0"</span></span><br><span class="line">         xmlns:xsi=<span class="string">"http://www.w3.org/2001/XMLSchema-instance"</span></span><br><span class="line">         xsi:schemaLocation=<span class="string">"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"</span>&gt;</span><br><span class="line">    &lt;modelVersion&gt;4.0.0&lt;/modelVersion&gt;</span><br><span class="line"></span><br><span class="line">    &lt;groupId&gt;org.example.springcloud&lt;/groupId&gt;</span><br><span class="line">    &lt;artifactId&gt;service-register&lt;/artifactId&gt;</span><br><span class="line">    &lt;version&gt;1.0-SNAPSHOT&lt;/version&gt;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    &lt;!-- spring boot的parent 配置文件，有大部分spring boot需要用的Jar包 --&gt;</span><br><span class="line">    &lt;parent&gt;</span><br><span class="line">        &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;</span><br><span class="line">        &lt;artifactId&gt;spring-boot-starter-parent&lt;/artifactId&gt;</span><br><span class="line">        &lt;version&gt;1.5.1.RELEASE&lt;/version&gt;</span><br><span class="line">    &lt;/parent&gt;</span><br><span class="line"></span><br><span class="line">    &lt;properties&gt;</span><br><span class="line">        &lt;project.build.sourceEncoding&gt;UTF-8&lt;/project.build.sourceEncoding&gt;</span><br><span class="line">        &lt;java.version&gt;1.8&lt;/java.version&gt;</span><br><span class="line">    &lt;/properties&gt;</span><br><span class="line"></span><br><span class="line">    &lt;!-- spring boot的maven打包插件 --&gt;</span><br><span class="line">    &lt;build&gt;</span><br><span class="line">        &lt;plugins&gt;</span><br><span class="line">            &lt;plugin&gt;</span><br><span class="line">                &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;</span><br><span class="line">                &lt;artifactId&gt;spring-boot-maven-plugin&lt;/artifactId&gt;</span><br><span class="line">            &lt;/plugin&gt;</span><br><span class="line">        &lt;/plugins&gt;</span><br><span class="line">    &lt;/build&gt;</span><br><span class="line"></span><br><span class="line">    &lt;dependencies&gt;</span><br><span class="line">        &lt;!-- eureka-server --&gt;</span><br><span class="line">        &lt;dependency&gt;</span><br><span class="line">            &lt;groupId&gt;org.springframework.cloud&lt;/groupId&gt;</span><br><span class="line">            &lt;artifactId&gt;spring-cloud-starter-eureka-server&lt;/artifactId&gt;</span><br><span class="line">        &lt;/dependency&gt;</span><br><span class="line"></span><br><span class="line">        &lt;!-- spring boot test--&gt;</span><br><span class="line">        &lt;dependency&gt;</span><br><span class="line">            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;</span><br><span class="line">            &lt;artifactId&gt;spring-boot-starter-test&lt;/artifactId&gt;</span><br><span class="line">            &lt;scope&gt;test&lt;/scope&gt;</span><br><span class="line">        &lt;/dependency&gt;</span><br><span class="line">    &lt;/dependencies&gt;</span><br><span class="line"></span><br><span class="line">    &lt;dependencyManagement&gt;</span><br><span class="line">        &lt;dependencies&gt;</span><br><span class="line">            &lt;dependency&gt;</span><br><span class="line">                &lt;groupId&gt;org.springframework.cloud&lt;/groupId&gt;</span><br><span class="line">                &lt;artifactId&gt;spring-cloud-dependencies&lt;/artifactId&gt;</span><br><span class="line">                &lt;version&gt;Camden.SR5&lt;/version&gt;</span><br><span class="line">                &lt;type&gt;pom&lt;/type&gt;</span><br><span class="line">                &lt;scope&gt;import&lt;/scope&gt;</span><br><span class="line">            &lt;/dependency&gt;</span><br><span class="line">        &lt;/dependencies&gt;</span><br><span class="line">    &lt;/dependencyManagement&gt;</span><br><span class="line">&lt;/project&gt;</span><br></pre></td></tr></table></figure><p>在src/main/java包下创建一个名为hello的包，然后创建<code>EurekaServiceRegisterApplication</code>类，并用@EnableEurekaServer和@SpringBootApplication两个注解修饰，后者是Spring Boot应用都需要用的，这里不作过多解释；@EnableEurekaServer注解的作用是触发Spring Boot的自动配置机制，由于我们之前在pom文件中导入了eureka-server，spring boot会在容器中创建对应的bean。EurekaServiceRegisterApplication的代码如下：</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">package</span> hello;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> org.springframework.boot.SpringApplication;</span><br><span class="line"><span class="keyword">import</span> org.springframework.boot.autoconfigure.SpringBootApplication;</span><br><span class="line"><span class="keyword">import</span> org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * Created by IntelliJ IDEA.</span></span><br><span class="line"><span class="comment"> * User: duqi</span></span><br><span class="line"><span class="comment"> * Date: 2017/3/2</span></span><br><span class="line"><span class="comment"> * Time: 20:02</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"></span><br><span class="line"><span class="meta">@EnableEurekaServer</span> <span class="comment">//通过@EnableEurekaServer启动一个服务注册中心给其他应用使用</span></span><br><span class="line"><span class="meta">@SpringBootApplication</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">EurekaServiceRegisterApplication</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> </span>&#123;</span><br><span class="line">        SpringApplication.run(EurekaServiceRegisterApplication<span class="class">.<span class="keyword">class</span>, <span class="title">args</span>)</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在application.properties中还需要增加如下配置，才能创建一个真正可以使用的服务注册中心。</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">#注册服务的端口号</span><br><span class="line">server.port=8761</span><br><span class="line"></span><br><span class="line">#是否需要注册到注册中心，因为该项目本身作为服务注册中心，所以为false</span><br><span class="line">eureka.client.register-with-eureka=false</span><br><span class="line">#是否需要从注册中心获取服务列表，原因同上，为false</span><br><span class="line">eureka.client.fetch-registry=false</span><br><span class="line">#注册服务器的地址：服务提供者和服务消费者都要依赖这个地址</span><br><span class="line">eureka.client.service-url.defaultZone=http://localhost:$&#123;server.port&#125;/eureka</span><br><span class="line"></span><br><span class="line">logging.level.com.netflix.eureka=OFF</span><br><span class="line">logging.level.com.netflix.discovery=OFF</span><br></pre></td></tr></table></figure><p>启动注册服务，并访问：<code>http://localhost:8761</code>，就可以看到如下界面。</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/spring-cloud/1.png" alt="eureka" title>                </div>                <div class="image-caption">eureka</div>            </figure><p>服务注册中心后台</p><h3 id="2-服务提供者"><a href="#2-服务提供者" class="headerlink" title="2. 服务提供者"></a>2. 服务提供者</h3><p>创建一个Spring Boot工程，代表服务提供者，该服务提供者会暴露一个加法服务，接受客户端传来的加数和被加数，并返回两者的和。</p><p>工程的pom文件内容如下：</p><figure class="highlight"><table><tr><td class="code"><pre><span class="line">&lt;?xml version=<span class="string">"1.0"</span> encoding=<span class="string">"UTF-8"</span>?&gt;</span><br><span class="line">&lt;project xmlns=<span class="string">"http://maven.apache.org/POM/4.0.0"</span></span><br><span class="line">         xmlns:xsi=<span class="string">"http://www.w3.org/2001/XMLSchema-instance"</span></span><br><span class="line">         xsi:schemaLocation=<span class="string">"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"</span>&gt;</span><br><span class="line">    &lt;modelVersion&gt;4.0.0&lt;/modelVersion&gt;</span><br><span class="line"></span><br><span class="line">    &lt;groupId&gt;org.example.springcloud&lt;/groupId&gt;</span><br><span class="line">    &lt;artifactId&gt;service-client&lt;/artifactId&gt;</span><br><span class="line">    &lt;version&gt;1.0-SNAPSHOT&lt;/version&gt;</span><br><span class="line"></span><br><span class="line">    &lt;parent&gt;</span><br><span class="line">        &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;</span><br><span class="line">        &lt;artifactId&gt;spring-boot-starter-parent&lt;/artifactId&gt;</span><br><span class="line">        &lt;version&gt;1.5.1.RELEASE&lt;/version&gt;</span><br><span class="line">        &lt;relativePath/&gt; </span><br><span class="line">    &lt;/parent&gt;</span><br><span class="line"></span><br><span class="line">    &lt;properties&gt;</span><br><span class="line">        &lt;project.build.sourceEncoding&gt;UTF-8&lt;/project.build.sourceEncoding&gt;</span><br><span class="line">        &lt;java.version&gt;1.8&lt;/java.version&gt;</span><br><span class="line">    &lt;/properties&gt;</span><br><span class="line"></span><br><span class="line">    &lt;build&gt;</span><br><span class="line">        &lt;plugins&gt;</span><br><span class="line">            &lt;plugin&gt;</span><br><span class="line">                &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;</span><br><span class="line">                &lt;artifactId&gt;spring-boot-maven-plugin&lt;/artifactId&gt;</span><br><span class="line">            &lt;/plugin&gt;</span><br><span class="line">        &lt;/plugins&gt;</span><br><span class="line">    &lt;/build&gt;</span><br><span class="line"></span><br><span class="line">    &lt;dependencies&gt;</span><br><span class="line">        &lt;dependency&gt;</span><br><span class="line">            &lt;groupId&gt;org.springframework.cloud&lt;/groupId&gt;</span><br><span class="line">            &lt;artifactId&gt;spring-cloud-starter-eureka&lt;/artifactId&gt;</span><br><span class="line">        &lt;/dependency&gt;</span><br><span class="line"></span><br><span class="line">        &lt;dependency&gt;</span><br><span class="line">            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;</span><br><span class="line">            &lt;artifactId&gt;spring-boot-starter-test&lt;/artifactId&gt;</span><br><span class="line">            &lt;scope&gt;test&lt;/scope&gt;</span><br><span class="line">        &lt;/dependency&gt;</span><br><span class="line">        &lt;dependency&gt;</span><br><span class="line">            &lt;groupId&gt;org.springframework.cloud&lt;/groupId&gt;</span><br><span class="line">            &lt;artifactId&gt;spring-cloud-starter-eureka-server&lt;/artifactId&gt;</span><br><span class="line">            &lt;scope&gt;test&lt;/scope&gt;</span><br><span class="line">        &lt;/dependency&gt;</span><br><span class="line">    &lt;/dependencies&gt;</span><br><span class="line"></span><br><span class="line">    &lt;dependencyManagement&gt;</span><br><span class="line">        &lt;dependencies&gt;</span><br><span class="line">            &lt;dependency&gt;</span><br><span class="line">                &lt;groupId&gt;org.springframework.cloud&lt;/groupId&gt;</span><br><span class="line">                &lt;artifactId&gt;spring-cloud-dependencies&lt;/artifactId&gt;</span><br><span class="line">                &lt;version&gt;Camden.SR5&lt;/version&gt;</span><br><span class="line">                &lt;type&gt;pom&lt;/type&gt;</span><br><span class="line">                &lt;scope&gt;import&lt;/scope&gt;</span><br><span class="line">            &lt;/dependency&gt;</span><br><span class="line">        &lt;/dependencies&gt;</span><br><span class="line">    &lt;/dependencyManagement&gt;</span><br><span class="line">&lt;/project&gt;</span><br></pre></td></tr></table></figure><p>其中的关键在于spring-cloud-starter-eureka这个Jar包，其中包含了eureka的客户端实现。</p><p>在src/main/java/hello下创建工程的主类EurekaServerProducerApplication，使用@EnableDiscoveryClient注解修饰，该注解在服务启动的时候，可以触发服务注册的过程，向配置文件中指定的服务注册中心（Eureka-Server）的地址注册自己提供的服务。EurekaServerProducerApplication的源码如下：</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">package</span> hello;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> org.springframework.boot.SpringApplication;</span><br><span class="line"><span class="keyword">import</span> org.springframework.boot.autoconfigure.SpringBootApplication;</span><br><span class="line"><span class="keyword">import</span> org.springframework.cloud.client.discovery.EnableDiscoveryClient;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * Created by IntelliJ IDEA.</span></span><br><span class="line"><span class="comment"> * User: duqi</span></span><br><span class="line"><span class="comment"> * Date: 2017/3/2</span></span><br><span class="line"><span class="comment"> * Time: 20:34</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="meta">@EnableDiscoveryClient</span></span><br><span class="line"><span class="meta">@SpringBootApplication</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">EurekaServerProducerApplication</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> </span>&#123;</span><br><span class="line">        SpringApplication.run(EurekaServerProducerApplication<span class="class">.<span class="keyword">class</span>, <span class="title">args</span>)</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>配置文件的内容如下：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">#服务提供者的名字</span><br><span class="line">spring.application.name=compute-service</span><br><span class="line"></span><br><span class="line">#服务提供者的端口号</span><br><span class="line">server.port=8888</span><br><span class="line"></span><br><span class="line">#服务注册中心的地址</span><br><span class="line">eureka.client.serviceUrl.defaultZone=http://localhost:8761/eureka/</span><br></pre></td></tr></table></figure><p>服务提供者的基本框架搭好后，需要实现服务的具体内容，在ServiceInstanceRestController类中实现，它的具体代码如下：</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">package</span> hello;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> org.slf4j.Logger;</span><br><span class="line"><span class="keyword">import</span> org.slf4j.LoggerFactory;</span><br><span class="line"><span class="keyword">import</span> org.springframework.beans.factory.annotation.Autowired;</span><br><span class="line"><span class="keyword">import</span> org.springframework.cloud.client.ServiceInstance;</span><br><span class="line"><span class="keyword">import</span> org.springframework.cloud.client.discovery.DiscoveryClient;</span><br><span class="line"><span class="keyword">import</span> org.springframework.web.bind.annotation.GetMapping;</span><br><span class="line"><span class="keyword">import</span> org.springframework.web.bind.annotation.RequestParam;</span><br><span class="line"><span class="keyword">import</span> org.springframework.web.bind.annotation.RestController;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * Created by IntelliJ IDEA.</span></span><br><span class="line"><span class="comment"> * User: duqi</span></span><br><span class="line"><span class="comment"> * Date: 2017/3/2</span></span><br><span class="line"><span class="comment"> * Time: 20:36</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="meta">@RestController</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">ServiceInstanceRestController</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> Logger logger = LoggerFactory.getLogger(ServiceInstanceRestController<span class="class">.<span class="keyword">class</span>)</span>;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> DiscoveryClient discoveryClient; <span class="comment">//服务发现客户端</span></span><br><span class="line"></span><br><span class="line">    <span class="meta">@GetMapping</span>(value = <span class="string">"/add"</span>)</span><br><span class="line">    <span class="function"><span class="keyword">public</span> Integer <span class="title">add</span><span class="params">(@RequestParam Integer a, @RequestParam Integer b)</span> </span>&#123;</span><br><span class="line">        ServiceInstance instance = discoveryClient.getLocalServiceInstance();</span><br><span class="line">        Integer r = a + b;</span><br><span class="line">        logger.info(<span class="string">"/add, host:"</span> + instance.getHost() + <span class="string">", service_id:"</span> + instance.getServiceId() + <span class="string">", result:"</span> + r);</span><br><span class="line">        <span class="keyword">return</span> r;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>先启动服务注册中心的工程，然后再启动服务提供者，在访问：<code>localhost:8761</code>，如下图所示，服务提供者已经注册到服务注册中心啦。</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/spring-cloud/2.png" alt="eureka2" title>                </div>                <div class="image-caption">eureka2</div>            </figure><p>服务提供者注册到服务注册中心</p><p>在Spring Cloud Netflix中，使用Ribbon实现客户端负载均衡，使用Feign实现声明式HTTP客户端调用——即写得像本地函数调用一样。</p><h3 id="3-服务消费者-Ribbon"><a href="#3-服务消费者-Ribbon" class="headerlink" title="3. 服务消费者-Ribbon"></a>3. 服务消费者-Ribbon</h3><p>创建一个Spring boot工程，引入ribbon和eureka，pom文件内容如下：</p><figure class="highlight"><table><tr><td class="code"><pre><span class="line">&lt;?xml version=<span class="string">"1.0"</span> encoding=<span class="string">"UTF-8"</span>?&gt;</span><br><span class="line">&lt;project xmlns=<span class="string">"http://maven.apache.org/POM/4.0.0"</span></span><br><span class="line">         xmlns:xsi=<span class="string">"http://www.w3.org/2001/XMLSchema-instance"</span></span><br><span class="line">         xsi:schemaLocation=<span class="string">"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"</span>&gt;</span><br><span class="line">    &lt;modelVersion&gt;4.0.0&lt;/modelVersion&gt;</span><br><span class="line"></span><br><span class="line">    &lt;groupId&gt;org.example.springcloud&lt;/groupId&gt;</span><br><span class="line">    &lt;artifactId&gt;serviceconsumer&lt;/artifactId&gt;</span><br><span class="line">    &lt;version&gt;1.0-SNAPSHOT&lt;/version&gt;</span><br><span class="line"></span><br><span class="line">    &lt;parent&gt;</span><br><span class="line">        &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;</span><br><span class="line">        &lt;artifactId&gt;spring-boot-starter-parent&lt;/artifactId&gt;</span><br><span class="line">        &lt;version&gt;1.5.1.RELEASE&lt;/version&gt;</span><br><span class="line">    &lt;/parent&gt;</span><br><span class="line"></span><br><span class="line">    &lt;properties&gt;</span><br><span class="line">        &lt;project.build.sourceEncoding&gt;UTF-8&lt;/project.build.sourceEncoding&gt;</span><br><span class="line">        &lt;java.version&gt;1.8&lt;/java.version&gt;</span><br><span class="line">    &lt;/properties&gt;</span><br><span class="line"></span><br><span class="line">    &lt;build&gt;</span><br><span class="line">        &lt;plugins&gt;</span><br><span class="line">            &lt;plugin&gt;</span><br><span class="line">                &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;</span><br><span class="line">                &lt;artifactId&gt;spring-boot-maven-plugin&lt;/artifactId&gt;</span><br><span class="line">            &lt;/plugin&gt;</span><br><span class="line">        &lt;/plugins&gt;</span><br><span class="line">    &lt;/build&gt;</span><br><span class="line"></span><br><span class="line">    &lt;dependencies&gt;</span><br><span class="line">        &lt;!-- 客户端负载均衡 --&gt;</span><br><span class="line">        &lt;dependency&gt;</span><br><span class="line">            &lt;groupId&gt;org.springframework.cloud&lt;/groupId&gt;</span><br><span class="line">            &lt;artifactId&gt;spring-cloud-starter-ribbon&lt;/artifactId&gt;</span><br><span class="line">        &lt;/dependency&gt;</span><br><span class="line"></span><br><span class="line">        &lt;!-- eureka客户端 --&gt;</span><br><span class="line">        &lt;dependency&gt;</span><br><span class="line">            &lt;groupId&gt;org.springframework.cloud&lt;/groupId&gt;</span><br><span class="line">            &lt;artifactId&gt;spring-cloud-starter-eureka&lt;/artifactId&gt;</span><br><span class="line">        &lt;/dependency&gt;</span><br><span class="line"></span><br><span class="line">        &lt;!-- spring boot实现Java Web服务--&gt;</span><br><span class="line">        &lt;dependency&gt;</span><br><span class="line">            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;</span><br><span class="line">            &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt;</span><br><span class="line">        &lt;/dependency&gt;</span><br><span class="line"></span><br><span class="line">        &lt;dependency&gt;</span><br><span class="line">            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;</span><br><span class="line">            &lt;artifactId&gt;spring-boot-starter-test&lt;/artifactId&gt;</span><br><span class="line">            &lt;scope&gt;test&lt;/scope&gt;</span><br><span class="line">        &lt;/dependency&gt;</span><br><span class="line">    &lt;/dependencies&gt;</span><br><span class="line"></span><br><span class="line">    &lt;dependencyManagement&gt;</span><br><span class="line">        &lt;dependencies&gt;</span><br><span class="line">            &lt;dependency&gt;</span><br><span class="line">                &lt;groupId&gt;org.springframework.cloud&lt;/groupId&gt;</span><br><span class="line">                &lt;artifactId&gt;spring-cloud-dependencies&lt;/artifactId&gt;</span><br><span class="line">                &lt;version&gt;Camden.SR5&lt;/version&gt;</span><br><span class="line">                &lt;type&gt;pom&lt;/type&gt;</span><br><span class="line">                &lt;scope&gt;import&lt;/scope&gt;</span><br><span class="line">            &lt;/dependency&gt;</span><br><span class="line">        &lt;/dependencies&gt;</span><br><span class="line">    &lt;/dependencyManagement&gt;</span><br><span class="line">&lt;/project&gt;</span><br></pre></td></tr></table></figure><p>创建EurekaConsumerApplication类，定义REST客户端实例，代码如下：</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">package</span> hello;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> org.springframework.boot.SpringApplication;</span><br><span class="line"><span class="keyword">import</span> org.springframework.boot.autoconfigure.SpringBootApplication;</span><br><span class="line"><span class="keyword">import</span> org.springframework.cloud.client.discovery.EnableDiscoveryClient;</span><br><span class="line"><span class="keyword">import</span> org.springframework.cloud.client.loadbalancer.LoadBalanced;</span><br><span class="line"><span class="keyword">import</span> org.springframework.context.annotation.Bean;</span><br><span class="line"><span class="keyword">import</span> org.springframework.web.client.RestTemplate;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * Created by IntelliJ IDEA.</span></span><br><span class="line"><span class="comment"> * User: duqi</span></span><br><span class="line"><span class="comment"> * Date: 2017/3/2</span></span><br><span class="line"><span class="comment"> * Time: 22:55</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="meta">@EnableDiscoveryClient</span> <span class="comment">//开启服务发现的能力</span></span><br><span class="line"><span class="meta">@SpringBootApplication</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">EurekaConsumerApplication</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Bean</span> <span class="comment">//定义REST客户端，RestTemplate实例</span></span><br><span class="line">    <span class="meta">@LoadBalanced</span> <span class="comment">//开启负债均衡的能力</span></span><br><span class="line">    <span class="function">RestTemplate <span class="title">restTemplate</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> RestTemplate();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> </span>&#123;</span><br><span class="line">        SpringApplication.run(EurekaConsumerApplication<span class="class">.<span class="keyword">class</span>, <span class="title">args</span>)</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>application.properties中定义了服务注册中心的地址、消费者服务的端口号、消费者服务的名称这些内容：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">#应用名称</span><br><span class="line">spring.application.name=ribbon-consumer</span><br><span class="line"></span><br><span class="line">#端口号</span><br><span class="line">server.port=9000</span><br><span class="line"></span><br><span class="line">#注册中心的地址</span><br><span class="line">eureka.client.serviceUrl.defaultZone=http://localhost:8761/eureka/</span><br></pre></td></tr></table></figure><p>消费者服务的入口为：ConsumerController，我们通过这个实例进行测试。消费者服务启动过程中，会从服务注册中心中拉最新的服务列表，当浏览器触发对应的请求，就会根据COMPUTE-SERVICE查找服务提供者的IP和端口号，然后发起调用。</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">package</span> hello;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> org.springframework.beans.factory.annotation.Autowired;</span><br><span class="line"><span class="keyword">import</span> org.springframework.web.bind.annotation.GetMapping;</span><br><span class="line"><span class="keyword">import</span> org.springframework.web.bind.annotation.RestController;</span><br><span class="line"><span class="keyword">import</span> org.springframework.web.client.RestTemplate;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * Created by IntelliJ IDEA.</span></span><br><span class="line"><span class="comment"> * User: duqi</span></span><br><span class="line"><span class="comment"> * Date: 2017/3/2</span></span><br><span class="line"><span class="comment"> * Time: 22:58</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="meta">@RestController</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">ConsumerController</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> RestTemplate restTemplate;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@GetMapping</span>(value = <span class="string">"/add"</span>)</span><br><span class="line">    <span class="function"><span class="keyword">public</span> String <span class="title">add</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> restTemplate.getForEntity(<span class="string">"http://COMPUTE-SERVICE/add?a=10&amp;b=20"</span>, String<span class="class">.<span class="keyword">class</span>).<span class="title">getBody</span>()</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>首先启动服务注册中心，第二分别启动两个服务提供者（IP相同、端口不同即可），然后启动服务消费者。</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/spring-cloud/3.png" alt="eureka3" title>                </div>                <div class="image-caption">eureka3</div>            </figure><p>两个服务提供者</p><p>在浏览器里访问<code>localhost:9000/add</code>两次，可以看到请求有时候会在8888端口的服务，有时候会到8889的服务。具体背后选择的原理，还有待后续研究。</p><h3 id="4-服务消费者-Feign"><a href="#4-服务消费者-Feign" class="headerlink" title="4. 服务消费者-Feign"></a>4. 服务消费者-Feign</h3><p>使用类似<code>restTemplate.getForEntity(&quot;http://COMPUTE-SERVICE/add?a=10&amp;b=20&quot;,String.class).getBody()</code>这样的语句进行服务间调用并非不可以，只是我们在服务化的过程中，希望跨服务调用能够看起来像本地调用，这也是我理解的Feign的使用场景。</p><p>创建一个spring boot工程，该工程的pom文件与上一节的类似，只是把ribbon的依赖换为feign的即可，代码如下：</p><figure class="highlight"><table><tr><td class="code"><pre><span class="line">&lt;?xml version=<span class="string">"1.0"</span> encoding=<span class="string">"UTF-8"</span>?&gt;</span><br><span class="line">&lt;project xmlns=<span class="string">"http://maven.apache.org/POM/4.0.0"</span></span><br><span class="line">         xmlns:xsi=<span class="string">"http://www.w3.org/2001/XMLSchema-instance"</span></span><br><span class="line">         xsi:schemaLocation=<span class="string">"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"</span>&gt;</span><br><span class="line">    &lt;modelVersion&gt;4.0.0&lt;/modelVersion&gt;</span><br><span class="line"></span><br><span class="line">    &lt;groupId&gt;org.example.springcloud&lt;/groupId&gt;</span><br><span class="line">    &lt;artifactId&gt;serviceconsumer&lt;/artifactId&gt;</span><br><span class="line">    &lt;version&gt;1.0-SNAPSHOT&lt;/version&gt;</span><br><span class="line"></span><br><span class="line">    &lt;parent&gt;</span><br><span class="line">        &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;</span><br><span class="line">        &lt;artifactId&gt;spring-boot-starter-parent&lt;/artifactId&gt;</span><br><span class="line">        &lt;version&gt;1.5.1.RELEASE&lt;/version&gt;</span><br><span class="line">    &lt;/parent&gt;</span><br><span class="line"></span><br><span class="line">    &lt;properties&gt;</span><br><span class="line">        &lt;project.build.sourceEncoding&gt;UTF-8&lt;/project.build.sourceEncoding&gt;</span><br><span class="line">        &lt;java.version&gt;1.8&lt;/java.version&gt;</span><br><span class="line">    &lt;/properties&gt;</span><br><span class="line"></span><br><span class="line">    &lt;build&gt;</span><br><span class="line">        &lt;plugins&gt;</span><br><span class="line">            &lt;plugin&gt;</span><br><span class="line">                &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;</span><br><span class="line">                &lt;artifactId&gt;spring-boot-maven-plugin&lt;/artifactId&gt;</span><br><span class="line">            &lt;/plugin&gt;</span><br><span class="line">        &lt;/plugins&gt;</span><br><span class="line">    &lt;/build&gt;</span><br><span class="line"></span><br><span class="line">    &lt;dependencies&gt;</span><br><span class="line">        &lt;!-- Feign实现声明式HTTP客户端 --&gt;</span><br><span class="line">        &lt;dependency&gt;</span><br><span class="line">            &lt;groupId&gt;org.springframework.cloud&lt;/groupId&gt;</span><br><span class="line">            &lt;artifactId&gt;spring-cloud-starter-feign&lt;/artifactId&gt;</span><br><span class="line">        &lt;/dependency&gt;</span><br><span class="line"></span><br><span class="line">        &lt;!-- eureka客户端 --&gt;</span><br><span class="line">        &lt;dependency&gt;</span><br><span class="line">            &lt;groupId&gt;org.springframework.cloud&lt;/groupId&gt;</span><br><span class="line">            &lt;artifactId&gt;spring-cloud-starter-eureka&lt;/artifactId&gt;</span><br><span class="line">        &lt;/dependency&gt;</span><br><span class="line"></span><br><span class="line">        &lt;!-- spring boot实现Java Web服务--&gt;</span><br><span class="line">        &lt;dependency&gt;</span><br><span class="line">            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;</span><br><span class="line">            &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt;</span><br><span class="line">        &lt;/dependency&gt;</span><br><span class="line"></span><br><span class="line">        &lt;dependency&gt;</span><br><span class="line">            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;</span><br><span class="line">            &lt;artifactId&gt;spring-boot-starter-test&lt;/artifactId&gt;</span><br><span class="line">            &lt;scope&gt;test&lt;/scope&gt;</span><br><span class="line">        &lt;/dependency&gt;</span><br><span class="line">    &lt;/dependencies&gt;</span><br><span class="line"></span><br><span class="line">    &lt;dependencyManagement&gt;</span><br><span class="line">        &lt;dependencies&gt;</span><br><span class="line">            &lt;dependency&gt;</span><br><span class="line">                &lt;groupId&gt;org.springframework.cloud&lt;/groupId&gt;</span><br><span class="line">                &lt;artifactId&gt;spring-cloud-dependencies&lt;/artifactId&gt;</span><br><span class="line">                &lt;version&gt;Camden.SR5&lt;/version&gt;</span><br><span class="line">                &lt;type&gt;pom&lt;/type&gt;</span><br><span class="line">                &lt;scope&gt;import&lt;/scope&gt;</span><br><span class="line">            &lt;/dependency&gt;</span><br><span class="line">        &lt;/dependencies&gt;</span><br><span class="line">    &lt;/dependencyManagement&gt;</span><br><span class="line">&lt;/project&gt;</span><br></pre></td></tr></table></figure><p>首先创建应用程序启动类：EurekaConsumerApplication，代码如下：</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">package</span> hello;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> org.springframework.boot.SpringApplication;</span><br><span class="line"><span class="keyword">import</span> org.springframework.boot.autoconfigure.SpringBootApplication;</span><br><span class="line"><span class="keyword">import</span> org.springframework.cloud.client.discovery.EnableDiscoveryClient;</span><br><span class="line"><span class="keyword">import</span> org.springframework.cloud.netflix.feign.EnableFeignClients;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * Created by IntelliJ IDEA.</span></span><br><span class="line"><span class="comment"> * User: duqi</span></span><br><span class="line"><span class="comment"> * Date: 2017/3/19</span></span><br><span class="line"><span class="comment"> * Time: 16:59</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="meta">@EnableDiscoveryClient</span> <span class="comment">//用于启动服务发现功能</span></span><br><span class="line"><span class="meta">@EnableFeignClients</span> <span class="comment">//用于启动Fegin功能</span></span><br><span class="line"><span class="meta">@SpringBootApplication</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">EurekaConsumerApplication</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> </span>&#123;</span><br><span class="line">        SpringApplication.run(EurekaConsumerApplication<span class="class">.<span class="keyword">class</span>)</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>然后定义远程调用的接口，在hello包下创建depend包，然后创建ComputeClient接口，使用@FeignClient(“COMPUTE-SERVICE”)注解修饰，COMPUTE-SERVICE就是服务提供者的名称，然后定义要使用的服务，代码如下：</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">package</span> hello.depend;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> org.springframework.cloud.netflix.feign.FeignClient;</span><br><span class="line"><span class="keyword">import</span> org.springframework.web.bind.annotation.RequestMapping;</span><br><span class="line"><span class="keyword">import</span> org.springframework.web.bind.annotation.RequestMethod;</span><br><span class="line"><span class="keyword">import</span> org.springframework.web.bind.annotation.RequestParam;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * Created by IntelliJ IDEA.</span></span><br><span class="line"><span class="comment"> * User: duqi</span></span><br><span class="line"><span class="comment"> * Date: 2017/3/19</span></span><br><span class="line"><span class="comment"> * Time: 17:02</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="meta">@FeignClient</span>(<span class="string">"COMPUTE-SERVICE"</span>)</span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">interface</span> <span class="title">ComputeClient</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@RequestMapping</span>(method = RequestMethod.GET, value = <span class="string">"/add"</span>)</span><br><span class="line">    <span class="function">Integer <span class="title">add</span><span class="params">(@RequestParam(value = <span class="string">"a"</span>)</span> Integer a, @<span class="title">RequestParam</span><span class="params">(value = <span class="string">"b"</span>)</span> Integer b)</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在ConsumerController中，像引入普通的spring bean一样引入ComputeClient对象，其他的和Ribbon的类似。</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">package</span> hello;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> org.springframework.beans.factory.annotation.Autowired;</span><br><span class="line"><span class="keyword">import</span> org.springframework.web.bind.annotation.RequestMapping;</span><br><span class="line"><span class="keyword">import</span> org.springframework.web.bind.annotation.RequestMethod;</span><br><span class="line"><span class="keyword">import</span> org.springframework.web.bind.annotation.RestController;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> hello.depend.ComputeClient;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * Created by IntelliJ IDEA.</span></span><br><span class="line"><span class="comment"> * User: duqi</span></span><br><span class="line"><span class="comment"> * Date: 2017/3/19</span></span><br><span class="line"><span class="comment"> * Time: 17:06</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="meta">@RestController</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">ConsumerController</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> ComputeClient computeClient;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@RequestMapping</span>(value = <span class="string">"/add"</span>, method = RequestMethod.GET)</span><br><span class="line">    <span class="function"><span class="keyword">public</span> Integer <span class="title">add</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> computeClient.add(<span class="number">10</span>, <span class="number">20</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>application.properties的内容如下：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">#应用名称</span><br><span class="line">spring.application.name=fegin-consumer</span><br><span class="line"></span><br><span class="line">#端口号</span><br><span class="line">server.port=9000</span><br><span class="line"></span><br><span class="line">#注册中心的地址</span><br><span class="line">eureka.client.serviceUrl.defaultZone=http://localhost:8761/eureka/</span><br></pre></td></tr></table></figure><p>启动fegin消费者，访问<code>localhost:9000/add</code>，也可以看到服务提供者已经收到了消费者发来的请求。</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/spring-cloud/4.png" alt="log1" title>                </div>                <div class="image-caption">log1</div>            </figure><p>请求到达服务提供者1</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/spring-cloud/5.png" alt="log2" title>                </div>                <div class="image-caption">log2</div>            </figure><p>请求到达服务提供者2</p><p>源码下载</p><ul><li><a href="https://github.com/duqicauc/eureka-register/tree/master" target="_blank" rel="noopener">服务注册中心(Eureka服务端)</a></li><li><a href="https://github.com/duqicauc/eureka-producer/tree/master" target="_blank" rel="noopener">服务提供者(Eureka客户端)</a></li><li><a href="https://github.com/duqicauc/eureka-consumer-ribbon/tree/master" target="_blank" rel="noopener">服务消费者-Ribbon(Eureka客户端)</a></li><li><a href="https://github.com/duqicauc/eureka-consumer-feign/tree/master" target="_blank" rel="noopener">服务消费者-Feign(Eureka客户端)</a></li></ul><p>参考资料</p><ul><li><a href="http://blog.didispace.com/springcloud2/" target="_blank" rel="noopener">Spring Cloud构建微服务架构（一）服务注册与发现</a></li><li><a href="http://www.cnblogs.com/skyblog/p/5133752.html" target="_blank" rel="noopener">Spring Cloud实现服务注册于发现</a></li><li><a href="http://cloud.spring.io/spring-cloud-netflix/" target="_blank" rel="noopener">spring-cloud-netflix官网</a></li><li><a href="http://projects.spring.io/spring-cloud/" target="_blank" rel="noopener">spring cloud 官网</a></li><li><a href="http://blog.csdn.net/neosmith/article/details/52449921" target="_blank" rel="noopener">使用Spring Cloud Feign作为HTTP客户端调用远程HTTP服务</a></li></ul><p>作者：杜琪<br>链接：<a href="http://www.jianshu.com/p/0aef3724e6bc" target="_blank" rel="noopener">http://www.jianshu.com/p/0aef3724e6bc</a><br>來源：简书<br>著作权归作者所有。商业转载请联系作者获得授权，非商业转载请注明出处。</p>]]></content>
    
    <summary type="html">
    
      Spring Cloud学习-Eureka、Ribbon和Feign
    
    </summary>
    
      <category term="Spring Cloud" scheme="https://ningyu1.github.io/categories/Spring-Cloud/"/>
    
      <category term="技术" scheme="https://ningyu1.github.io/categories/Spring-Cloud/%E6%8A%80%E6%9C%AF/"/>
    
    
      <category term="Spring Cloud" scheme="https://ningyu1.github.io/tags/Spring-Cloud/"/>
    
      <category term="Eureka" scheme="https://ningyu1.github.io/tags/Eureka/"/>
    
      <category term="Ribbon" scheme="https://ningyu1.github.io/tags/Ribbon/"/>
    
      <category term="Feign" scheme="https://ningyu1.github.io/tags/Feign/"/>
    
  </entry>
  
  <entry>
    <title>RedisClient使用说明</title>
    <link href="https://ningyu1.github.io/20170906/22-redis-client.html"/>
    <id>https://ningyu1.github.io/20170906/22-redis-client.html</id>
    <published>2017-09-06T03:17:36.000Z</published>
    <updated>2024-04-23T01:48:15.032Z</updated>
    
    <content type="html"><![CDATA[<h2 id="项目地址"><a href="#项目地址" class="headerlink" title="项目地址"></a>项目地址</h2><p><a href="https://github.com/ningyu1/redis-client" target="_blank" rel="noopener">redis-client</a> </p><p><a href="https://github.com/ningyu1/redis-client/releases" target="_blank" rel="noopener"><img src="https://img.shields.io/github/release/ningyu1/redis-client.svg?style=social&amp;label=Release"></a>&nbsp;<a href="https://github.com/ningyu1/redis-client/stargazers" target="_blank" rel="noopener"><img src="https://img.shields.io/github/stars/ningyu1/redis-client.svg?style=social&amp;label=Star"></a>&nbsp;<a href="https://github.com/ningyu1/redis-client/fork" target="_blank" rel="noopener"><img src="https://img.shields.io/github/forks/ningyu1/redis-client.svg?style=social&amp;label=Fork"></a>&nbsp;<a href="https://github.com/ningyu1/redis-client/watchers" target="_blank" rel="noopener"><img src="https://img.shields.io/github/watchers/ningyu1/redis-client.svg?style=social&amp;label=Watch"></a> <a href="http://www.gnu.org/licenses/gpl-3.0.html" target="_blank" rel="noopener"><img src="https://img.shields.io/badge/license-GPLv3-blue.svg"></a></p><h2 id="Maven引入"><a href="#Maven引入" class="headerlink" title="Maven引入"></a>Maven引入</h2><figure class="highlight xml"><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>cn.tsoft.framework<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>redis-client<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">version</span>&gt;</span>1.1.0-SNAPSHOT<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure><h2 id="Spring引入"><a href="#Spring引入" class="headerlink" title="Spring引入"></a>Spring引入</h2><figure class="highlight xml"><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">import</span> <span class="attr">resource</span>=<span class="string">"classpath:spring-redis.xml"</span> /&gt;</span></span><br></pre></td></tr></table></figure><h2 id="Api使用说明"><a href="#Api使用说明" class="headerlink" title="Api使用说明"></a>Api使用说明</h2><p>ps.本次版本增加了namespace、泛型的支持（存、取直接使用java对象），namespace可以有效的避免key名称冲突和对以后做sharding提供了基础，泛型则是提升使用友好度，本次版本包装了驱动（jedis）的95%的方法，有一些性能不好的方法没有开放，新增了一些使用上更加友好的方法。</p><p>常规操作的command实现：RedisClientImpl</p><p>二进制操作的command实现：BinaryRedisClientImpl</p><p>两者都支持直接存、取java对象，区别在于前者序列化为json以string的方式发送到redis服务器，后者序列化为byte[]以字节方式发送到redis服务，通过redis-cli工具前者可以很明确的看到存的值，后者看到的是二进制编码。</p><h2 id="接口方法"><a href="#接口方法" class="headerlink" title="接口方法"></a>接口方法</h2><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/redis-client/1.png" alt="1.png" title>                </div>                <div class="image-caption">1.png</div>            </figure><h2 id="回调接口"><a href="#回调接口" class="headerlink" title="回调接口"></a>回调接口</h2><p>cn.tsoft.framework.redis.callback.GetDataCallBack<r></r></p><p>接口提供两个方法</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * ttl时间,不是所有命令都支持ttl设置</span></span><br><span class="line"><span class="comment"> * */</span></span><br><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">getExpiredTime</span><span class="params">()</span></span>;</span><br><span class="line"> </span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 执行回调方法</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="function">R <span class="title">invoke</span><span class="params">()</span></span>;</span><br></pre></td></tr></table></figure><p><strong>ps.int getExpiredTime();这个方法并不是所有命令都支持（hget系列不支持，因为hash的attr是不支持ttl设置的，ttl必须设置在hash的key上并不是hash的attr上），因此不支持ttl的命令就采用默认的空实现。</strong><br><strong>在使用get<em>和hget</em>方法时，如果key返回为null，则通过该接口的<code>R invoke();</code>方法获取数据并放到redis中。</strong><br><strong>hgetAllObjects方法上的GetDataCallBack<t> gbs参数是无效的传入null即可。</t></strong><br><strong>如果在get方法获取不到值时不想走数据回调时传入null即可。</strong></p><p>示例：<br><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="comment">//不设置回调</span></span><br><span class="line">Metadata resule = redisClient.get(bizkey, nameSpace, Metadata<span class="class">.<span class="keyword">class</span>, <span class="title">null</span>)</span>;</span><br><span class="line"> </span><br><span class="line">List&lt;Metadata &gt; resule = redisClient.get(bizkey, nameSpace, <span class="keyword">new</span> TypeReference&lt;List&lt;Metadata&gt;&gt;() &#123;&#125;, <span class="keyword">null</span>);</span><br><span class="line"> </span><br><span class="line"><span class="comment">//设置回调</span></span><br><span class="line">List&lt;Long&gt; resule = redisClient.get(bizkey, nameSpace, <span class="keyword">new</span> TypeReference&lt;List&lt;Long&gt;&gt;() &#123;&#125;, <span class="keyword">new</span> GetDataCallBack&lt;List&lt;Long&gt;&gt;() &#123;</span><br><span class="line">            <span class="meta">@Override</span></span><br><span class="line">            <span class="function"><span class="keyword">public</span> <span class="keyword">int</span> <span class="title">getExpiredTime</span><span class="params">()</span> </span>&#123;</span><br><span class="line">                <span class="keyword">return</span> <span class="number">3600</span>;</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="meta">@Override</span></span><br><span class="line">            <span class="function"><span class="keyword">public</span> List&lt;Long&gt; <span class="title">invoke</span><span class="params">()</span> </span>&#123;</span><br><span class="line">                <span class="keyword">return</span> getMetadataSourceProvider().getUserRoles(uid);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;);</span><br><span class="line"> </span><br><span class="line">List&lt;Long&gt; resule = redisClient.hgetObject(bizkey, nameSpace, String.valueOf(uid), <span class="keyword">new</span> TypeReference&lt;List&lt;Long&gt;&gt;() &#123;&#125;, <span class="keyword">new</span> GetDataCallBack&lt;List&lt;Long&gt;&gt;() &#123;</span><br><span class="line">            <span class="meta">@Override</span></span><br><span class="line">            <span class="function"><span class="keyword">public</span> <span class="keyword">int</span> <span class="title">getExpiredTime</span><span class="params">()</span> </span>&#123;</span><br><span class="line">                <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="meta">@Override</span></span><br><span class="line">            <span class="function"><span class="keyword">public</span> List&lt;Long&gt; <span class="title">invoke</span><span class="params">()</span> </span>&#123;</span><br><span class="line">                <span class="keyword">return</span> getMetadataSourceProvider().getUserRoles(uid);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;);</span><br></pre></td></tr></table></figure></p><h2 id="参数说明"><a href="#参数说明" class="headerlink" title="参数说明"></a>参数说明</h2><p>get*方法的参数Class<t> value和TypeReference<t> type的区别，前者不支持嵌套泛型，后者支持嵌套泛型，举一个例子说明</t></t></p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line">Metadata value = redisClient.get(bizkey, nameSpace, Metadata<span class="class">.<span class="keyword">class</span>, <span class="title">null</span>)</span>;</span><br><span class="line"> </span><br><span class="line">List&lt;Metadata&gt; list = redisClient.get(bizkey, nameSpace, <span class="keyword">new</span> TypeReference&lt;List&lt;Metadata&gt;&gt;()&#123;&#125;, <span class="keyword">null</span>);</span><br></pre></td></tr></table></figure><h2 id="综合使用示例"><a href="#综合使用示例" class="headerlink" title="综合使用示例"></a>综合使用示例</h2><figure class="highlight java"><table><tr><td class="code"><pre><span class="line">redisClient.set(bizkey, namespace, <span class="keyword">new</span> Metadata(), <span class="number">60</span>);<span class="comment">//set并设置ttl60秒</span></span><br><span class="line">redisClient.set(bizkey, namespace, <span class="keyword">new</span> Metadata(), -<span class="number">1</span>);<span class="comment">//set不设置ttl</span></span><br><span class="line">redisClient.setnx(bizkey, namespace, <span class="string">"aaaa"</span>);<span class="comment">//key不存在时才设置值</span></span><br><span class="line">redisClient.setex(bizkey, namespace, <span class="number">60</span>, <span class="keyword">new</span> Metadata());<span class="comment">//set一个key并设置ttl60秒，等价于第一行的用法</span></span><br><span class="line"><span class="comment">//setbit和setrange用法不多做说明，参考redis.io上面的command说明</span></span><br><span class="line"> </span><br><span class="line">redisClient.get(bizkey, namespace, <span class="keyword">new</span> GetDataCallBack&lt;String&gt;()&#123;</span><br><span class="line">            <span class="meta">@Override</span></span><br><span class="line">            <span class="function"><span class="keyword">public</span> <span class="keyword">int</span> <span class="title">getExpiredTime</span><span class="params">()</span> </span>&#123;</span><br><span class="line">                <span class="keyword">return</span> <span class="number">60</span>;</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="meta">@Override</span></span><br><span class="line">            <span class="function"><span class="keyword">public</span> String <span class="title">invoke</span><span class="params">()</span> </span>&#123;</span><br><span class="line">                <span class="keyword">return</span> <span class="string">"aaaa"</span>;</span><br><span class="line">            &#125;</span><br><span class="line">&#125;);<span class="comment">//获取，找不到取数据并set进去</span></span><br><span class="line"> </span><br><span class="line">redisClient.get(bizkey, namespace, Metadata<span class="class">.<span class="keyword">class</span>, <span class="title">new</span> <span class="title">GetDataCallBack</span>&lt;<span class="title">Metadata</span>&gt;()</span>&#123;</span><br><span class="line">            <span class="meta">@Override</span></span><br><span class="line">            <span class="function"><span class="keyword">public</span> <span class="keyword">int</span> <span class="title">getExpiredTime</span><span class="params">()</span> </span>&#123;</span><br><span class="line">                <span class="keyword">return</span> <span class="number">60</span>;</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="meta">@Override</span></span><br><span class="line">            <span class="function"><span class="keyword">public</span> Metadata <span class="title">invoke</span><span class="params">()</span> </span>&#123;</span><br><span class="line">                <span class="keyword">return</span> <span class="keyword">new</span> Metadata();</span><br><span class="line">            &#125;</span><br><span class="line">&#125;);<span class="comment">//获取值，类型：Metadata</span></span><br><span class="line"> </span><br><span class="line">redisClient.get(bizkey, namespace, <span class="keyword">new</span> TypeReference&lt;List&lt;Metadata<span class="class">.<span class="keyword">class</span>&gt;&gt;()</span>&#123;&#125;, <span class="keyword">new</span> GetDataCallBack&lt;List&lt;Metadata&gt;&gt;()&#123;</span><br><span class="line">            <span class="meta">@Override</span></span><br><span class="line">            <span class="function"><span class="keyword">public</span> <span class="keyword">int</span> <span class="title">getExpiredTime</span><span class="params">()</span> </span>&#123;</span><br><span class="line">                <span class="keyword">return</span> <span class="number">60</span>;</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="meta">@Override</span></span><br><span class="line">            <span class="function"><span class="keyword">public</span> List&lt;Metadata&gt; <span class="title">invoke</span><span class="params">()</span> </span>&#123;</span><br><span class="line">                <span class="keyword">return</span> <span class="keyword">new</span> ArrayList&lt;Metadata&gt;;</span><br><span class="line">            &#125;</span><br><span class="line">&#125;);<span class="comment">//获取值，类型：List&lt;Metadata&gt;</span></span><br><span class="line"> </span><br><span class="line">redisClient.get(bizkey, namespace, <span class="keyword">new</span> TypeReference&lt;List&lt;Metadata<span class="class">.<span class="keyword">class</span>&gt;&gt;()</span>&#123;&#125;, <span class="keyword">null</span>);<span class="comment">//获取值，类型：List&lt;Metadata&gt;</span></span><br><span class="line"> </span><br><span class="line"><span class="comment">//getbit、getrange、getSet、hget、hgetAll、hgetObject、hgetAllObjects，用法不多做说明，参考redis.io上面的command说明</span></span><br><span class="line"> </span><br><span class="line"><span class="comment">//管道，批量发送多条命令，但是不支持namespace需要手动添加namespace</span></span><br><span class="line">Pipeline pipelined = redisClient.pipelined();</span><br><span class="line">pipelined.set(key, value);</span><br><span class="line">pipelined.get(key);</span><br><span class="line">pipelined.syncAndReturnAll(); <span class="comment">//发送命令并接受返回值</span></span><br><span class="line">pipelined.sync();<span class="comment">//发送命令不接受返回值</span></span><br><span class="line"> </span><br><span class="line"><span class="comment">//其他z*、incr、decr、h*、s*命令不做说明，参考redis.io上面的command说明</span></span><br></pre></td></tr></table></figure>]]></content>
    
    <summary type="html">
    
      RedisClient使用说明
    
    </summary>
    
      <category term="Redis" scheme="https://ningyu1.github.io/categories/Redis/"/>
    
    
      <category term="Redis" scheme="https://ningyu1.github.io/tags/Redis/"/>
    
  </entry>
  
  <entry>
    <title>FastDFS-Client使用说明</title>
    <link href="https://ningyu1.github.io/20170906/21-fastfds-client.html"/>
    <id>https://ningyu1.github.io/20170906/21-fastfds-client.html</id>
    <published>2017-09-06T02:17:36.000Z</published>
    <updated>2024-04-23T01:48:15.032Z</updated>
    
    <content type="html"><![CDATA[<h2 id="项目地址"><a href="#项目地址" class="headerlink" title="项目地址"></a>项目地址</h2><p><a href="https://github.com/ningyu1/fastdfs-client" target="_blank" rel="noopener">fastdfs-client</a> </p><p><a href="https://github.com/ningyu1/fastdfs-client/releases" target="_blank" rel="noopener"><img src="https://img.shields.io/github/release/ningyu1/fastdfs-client.svg?style=social&amp;label=Release"></a>&nbsp;<a href="https://github.com/ningyu1/fastdfs-client/stargazers" target="_blank" rel="noopener"><img src="https://img.shields.io/github/stars/ningyu1/fastdfs-client.svg?style=social&amp;label=Star"></a>&nbsp;<a href="https://github.com/ningyu1/fastdfs-client/fork" target="_blank" rel="noopener"><img src="https://img.shields.io/github/forks/ningyu1/fastdfs-client.svg?style=social&amp;label=Fork"></a>&nbsp;<a href="https://github.com/ningyu1/fastdfs-client/watchers" target="_blank" rel="noopener"><img src="https://img.shields.io/github/watchers/ningyu1/fastdfs-client.svg?style=social&amp;label=Watch"></a> <a href="http://www.gnu.org/licenses/gpl-3.0.html" target="_blank" rel="noopener"><img src="https://img.shields.io/badge/license-GPLv3-blue.svg"></a></p><h2 id="fastdfs-client是什么"><a href="#fastdfs-client是什么" class="headerlink" title="fastdfs-client是什么"></a>fastdfs-client是什么</h2><p>fastdfs-client是一个访问fastdfs的Java客户端框架，帮助开发人员快速使用分布式文件系统的工具，封装了TrackerClient操作来管理存储节点，封装了StorageClient操作来执行文件上传下载功能。</p><h2 id="change-log"><a href="#change-log" class="headerlink" title="change log"></a>change log</h2><p><a href="https://github.com/ningyu1/fastdfs-client/releases/tag/V1.1.0" target="_blank" rel="noopener">V1.1.0</a></p><ol><li>修改download文件receive时带入的inputStream对象，inputStream对象修改为克隆socket的inputstream，避免污染连接池中的socket对象，当业务回调不读取留时会影响下一次连接池中获取的socket对象。</li><li>在使用1.0.0版本进行download文件时，建议使用DownloadCallback的实现类：DownloadByteArray和DownloadFileWriter不要自己去实现，不要关闭receive方法传入的inputStream对象。</li><li>在使用1.1.0版本进行download文件时，receive传入的inputStream是克隆的，因此使用完后必须进行关闭操作。</li></ol><p><a href="https://github.com/ningyu1/fastdfs-client/releases/tag/V1.0.0" target="_blank" rel="noopener">V1.0.0</a></p><ol><li>包装Request和Response报文解析</li><li>包装Storage和Tracker操作命令</li><li>增加连接池提升使用性能</li></ol><h2 id="接口方法"><a href="#接口方法" class="headerlink" title="接口方法"></a>接口方法</h2><p>StorageClient</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/fastdfs-client/1.png" alt="1.png" title>                </div>                <div class="image-caption">1.png</div>            </figure><p>TrackerClient</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/fastdfs-client/2.png" alt="2.png" title>                </div>                <div class="image-caption">2.png</div>            </figure><h2 id="Maven引入"><a href="#Maven引入" class="headerlink" title="Maven引入"></a>Maven引入</h2><figure class="highlight xml"><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>cn.tsoft.framework<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>fastdfs-client<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">version</span>&gt;</span>1.0.0-SNAPSHOT<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure><h2 id="Spring引入"><a href="#Spring引入" class="headerlink" title="Spring引入"></a>Spring引入</h2><figure class="highlight xml"><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">import</span> <span class="attr">resource</span>=<span class="string">"classpath:spring-fastdfs.xml"</span>/&gt;</span></span><br></pre></td></tr></table></figure><h2 id="Client使用"><a href="#Client使用" class="headerlink" title="Client使用"></a>Client使用</h2><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="meta">@Autowired</span></span><br><span class="line"><span class="keyword">private</span> StorageClient storageClient;</span><br><span class="line"> </span><br><span class="line"><span class="comment">//上传</span></span><br><span class="line">String path = ClassLoader.getSystemResource(<span class="string">"123456.txt"</span>).getPath();</span><br><span class="line">File file = <span class="keyword">new</span> File(path);</span><br><span class="line">FileInputStream fileInputStream = FileUtils.openInputStream(file);</span><br><span class="line"><span class="comment">//方式1</span></span><br><span class="line">StorePath storePath = storageClient.uploadFile(<span class="string">"group1"</span>, fileInputStream, file.length(), <span class="string">"txt"</span>);</span><br><span class="line"><span class="comment">//方式2</span></span><br><span class="line">StorePath storePath = storageClient.uploadFile(fileInputStream, file.length(), <span class="string">"txt"</span>);</span><br><span class="line"><span class="comment">//方式3</span></span><br><span class="line">StorePath storePath = storageClient.uploadFile(path);</span><br><span class="line"><span class="comment">//方式4</span></span><br><span class="line">StorePath storePath = storageClient.uploadFile(path, <span class="string">"txt"</span>);</span><br><span class="line"><span class="comment">//方式5</span></span><br><span class="line">StorePath storePath = storageClient.uploadFile(<span class="string">"group1"</span>, path, <span class="string">"txt"</span>);</span><br><span class="line"><span class="comment">//方式6</span></span><br><span class="line">StorePath storePath = storageClient.uploadFile(fileInputStream, file.length(), <span class="string">"txt"</span>, metaDataSet);</span><br><span class="line"><span class="comment">//上传文件并增加元数据</span></span><br><span class="line">Set&lt;MateData&gt; metaDataSet = <span class="keyword">new</span> HashSet&lt;MateData&gt;();</span><br><span class="line">MateData mateData = <span class="keyword">new</span> MateData(<span class="string">"mateDataName"</span>,<span class="string">"mateDataValue"</span>);</span><br><span class="line">metaDataSet.add(mateData);</span><br><span class="line">StorePath storePath = storageClient.uploadFile(<span class="string">"group1"</span>, fileInputStream, file.length(), <span class="string">"txt"</span>, metaDataSet);</span><br><span class="line"> </span><br><span class="line"><span class="comment">//上传从文件，一个主文件可以挂多个从文件</span></span><br><span class="line"><span class="comment">//方式1</span></span><br><span class="line">String masterFileId = storePath.getFullPath();</span><br><span class="line">String[] parts = <span class="keyword">new</span> String[<span class="number">2</span>];</span><br><span class="line">splitFileId(masterFileId, parts);</span><br><span class="line">storePath = storageClient.uploadSlaveFile(parts[<span class="number">0</span>], parts[<span class="number">1</span>], fileInputStream, file.length(), <span class="string">"-1"</span>, <span class="string">"txt"</span>);</span><br><span class="line"><span class="comment">//方式2</span></span><br><span class="line">storePath = storageClient.uploadSlaveFile(masterFileId, fileInputStream, file.length(), <span class="string">"-1"</span>, <span class="string">"xlsx"</span>);</span><br><span class="line">         </span><br><span class="line">fileInputStream.close();</span><br><span class="line"> </span><br><span class="line"> </span><br><span class="line"><span class="comment">//下载</span></span><br><span class="line"><span class="comment">//方式1</span></span><br><span class="line">String path = ClassLoader.getSystemResource(<span class="string">"123456.txt"</span>).getPath();</span><br><span class="line">StorePath storePath = storageClient.uploadFile(<span class="string">"group1"</span>, path, <span class="string">"txt"</span>);</span><br><span class="line">addResultFileId(storePath.getFullPath());</span><br><span class="line">DownloadFileWriter downloadFileWriter = <span class="keyword">new</span> DownloadFileWriter(path.replaceAll(<span class="string">"123456.txt"</span>, <span class="string">"123456downlaod1.txt"</span>));</span><br><span class="line">String filePath = storageClient.downloadFile(storePath.getGroup(), storePath.getPath(), downloadFileWriter);</span><br><span class="line"><span class="comment">//方式2</span></span><br><span class="line">DownloadFileWriter downloadFileWriter = <span class="keyword">new</span> DownloadFileWriter(path.replaceAll(<span class="string">"123456.txt"</span>, <span class="string">"123456downlaod2.txt"</span>));</span><br><span class="line">String filePath = storageClient.downloadFile(storePath.getFullPath(), downloadFileWriter);</span><br><span class="line"><span class="comment">//方式3</span></span><br><span class="line">DownloadFileWriter downloadFileWriter = <span class="keyword">new</span> DownloadFileWriter(path.replaceAll(<span class="string">"123456.txt"</span>, <span class="string">"123456downlaod3.txt"</span>));</span><br><span class="line">String filePath = storageClient.downloadFile(storePath.getGroup(), storePath.getPath(), <span class="number">10</span>, <span class="number">0</span>, downloadFileWriter); </span><br><span class="line"><span class="comment">//方式4</span></span><br><span class="line">DownloadFileWriter downloadFileWriter = <span class="keyword">new</span> DownloadFileWriter(path.replaceAll(<span class="string">"123456.txt"</span>, <span class="string">"123456downlaod4.txt"</span>));</span><br><span class="line">String filePath = storageClient.downloadFile(storePath.getFullPath(), <span class="number">5</span>, <span class="number">0</span>, downloadFileWriter);</span><br><span class="line"> </span><br><span class="line"><span class="comment">//删除</span></span><br><span class="line"><span class="comment">//方式1</span></span><br><span class="line">String path = ClassLoader.getSystemResource(<span class="string">"123456.txt"</span>).getPath();</span><br><span class="line">StorePath storePath = storageClient.uploadFile(<span class="string">"group1"</span>, path, <span class="string">"txt"</span>);</span><br><span class="line"><span class="keyword">boolean</span> flag = storageClient.deleteFile(storePath.getGroup(), storePath.getPath());</span><br><span class="line"><span class="comment">//方式2</span></span><br><span class="line"><span class="keyword">boolean</span> flag = storageClient.deleteFile(storePath.getFullPath());</span><br><span class="line"> </span><br><span class="line"><span class="comment">//获取文件信息</span></span><br><span class="line"><span class="comment">//方式1</span></span><br><span class="line">String path = ClassLoader.getSystemResource(<span class="string">"123456.txt"</span>).getPath();</span><br><span class="line">StorePath storePath = storageClient.uploadFile(<span class="string">"group1"</span>, path, <span class="string">"txt"</span>);</span><br><span class="line">addResultFileId(storePath.getFullPath());</span><br><span class="line">String fileId = storePath.getFullPath();</span><br><span class="line">FileInfo fileInfo = storageClient.getFileInfo(storePath.getGroup(), storePath.getPath());</span><br><span class="line"><span class="comment">//方式2</span></span><br><span class="line">FileInfo fileInfo = storageClient.getFileInfo(fileId);</span><br><span class="line"> </span><br><span class="line"><span class="comment">//获取文件元数据</span></span><br><span class="line"><span class="comment">//方式1</span></span><br><span class="line">String masterFileId = storePath.getFullPath();</span><br><span class="line">String[] parts = <span class="keyword">new</span> String[<span class="number">2</span>];</span><br><span class="line">splitFileId(masterFileId, parts);</span><br><span class="line">Set&lt;MateData&gt; mateDataSet = storageClient.getMetadata(parts[<span class="number">0</span>], parts[<span class="number">1</span>]);</span><br><span class="line"><span class="comment">//方式2</span></span><br><span class="line">Set&lt;MateData&gt; mateDataSet = storageClient.getMetadata(masterFileId);</span><br><span class="line"> </span><br><span class="line"><span class="comment">//覆盖文件元数据</span></span><br><span class="line"><span class="comment">//方式1</span></span><br><span class="line">String[] parts = <span class="keyword">new</span> String[<span class="number">2</span>];</span><br><span class="line">splitFileId(masterFileId, parts);</span><br><span class="line">mateDataSet = <span class="keyword">new</span> HashSet&lt;MateData&gt;();</span><br><span class="line">mateDataSet.add(<span class="keyword">new</span> MateData(<span class="string">"key5"</span>, <span class="string">"value5"</span>));</span><br><span class="line">mateDataSet.add(<span class="keyword">new</span> MateData(<span class="string">"key6"</span>, <span class="string">"value6"</span>));</span><br><span class="line">mateDataSet.add(<span class="keyword">new</span> MateData(<span class="string">"key7"</span>, <span class="string">"value7"</span>));</span><br><span class="line"><span class="keyword">boolean</span> flag = storageClient.overwriteMetadata(parts[<span class="number">0</span>], parts[<span class="number">1</span>], mateDataSet);</span><br><span class="line"><span class="comment">//方式2</span></span><br><span class="line"><span class="keyword">boolean</span> flag = storageClient.overwriteMetadata(masterFileId, mateDataSet);</span><br><span class="line"> </span><br><span class="line"><span class="comment">//合并文件元数据</span></span><br><span class="line"><span class="comment">//方式1</span></span><br><span class="line">String[] parts = <span class="keyword">new</span> String[<span class="number">2</span>];</span><br><span class="line">splitFileId(masterFileId, parts);</span><br><span class="line">mateDataSet = <span class="keyword">new</span> HashSet&lt;MateData&gt;();</span><br><span class="line">mateDataSet.add(<span class="keyword">new</span> MateData(<span class="string">"key5"</span>, <span class="string">"value5"</span>));</span><br><span class="line">mateDataSet.add(<span class="keyword">new</span> MateData(<span class="string">"key6"</span>, <span class="string">"value6"</span>));</span><br><span class="line">mateDataSet.add(<span class="keyword">new</span> MateData(<span class="string">"key7"</span>, <span class="string">"value7"</span>));</span><br><span class="line"><span class="keyword">boolean</span> flag = storageClient.mergeMetadata(parts[<span class="number">0</span>], parts[<span class="number">1</span>], mateDataSet);</span><br><span class="line"><span class="comment">//方式2</span></span><br><span class="line"><span class="keyword">boolean</span> flag = storageClient.mergeMetadata(masterFileId, mateDataSet);</span><br><span class="line"> </span><br><span class="line"><span class="comment">//一下方法就不具体介绍</span></span><br><span class="line"><span class="comment">//续传文件</span></span><br><span class="line">appendFile</span><br><span class="line"><span class="comment">//修改续传文件</span></span><br><span class="line">modifyFile</span><br><span class="line"><span class="comment">//清除续传文件</span></span><br><span class="line">truncateFile</span><br></pre></td></tr></table></figure><p><strong>ps.TrackerClient的操作是配合StorageClient使用，我们在正常业务使用中一般不会用到它。</strong></p><h2 id="FastDFS-nginx-module使用"><a href="#FastDFS-nginx-module使用" class="headerlink" title="FastDFS-nginx-module使用"></a>FastDFS-nginx-module使用</h2><p>上传的文件可以通过nginx直接访问</p><p>例如：我们上传的文件获取的文件id：group1/M00/02/92/wKgAMFkekciAC8fhAAJjfD2dq-w10.xlsx</p><p>nginx访问路径：<a href="http://192.168.0.48:8079/group1/M00/02/92/wKgAMFkekciAC8fhAAJjfD2dq-w10.xlsx" target="_blank" rel="noopener">http://192.168.0.48:8079/group1/M00/02/92/wKgAMFkekciAC8fhAAJjfD2dq-w10.xlsx</a></p><p>目前nginx模块跟storage存储节点匹配，nginx会通过fastdfs-plugin跟tracker通信将文件的信息路由到不同的storage上去</p><h2 id="注意事项"><a href="#注意事项" class="headerlink" title="注意事项"></a>注意事项</h2><ol><li>上传文件后记录fileId，fastdfs不会自动删除文件，所以业务需要进行定期删除无用的文件，避免硬盘消耗过大</li><li>rpc之间调用时<ul><li>以前是rpc client端通过文件byte方式传入rpc server端，这样rpc的请求包过大会导致rpc调用性能急速下降</li><li>应修改为通过fastdfs做桥接，rpc client端upload文件到fastdfs，将返回的fileId做参数传入rpc server端 ，rpc server端通过fileid去fastdfs服务器上download文件文件</li></ul></li></ol><h2 id="FastDFS-client-api说明"><a href="#FastDFS-client-api说明" class="headerlink" title="FastDFS-client-api说明"></a>FastDFS-client-api说明</h2><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 存储服务(Storage)客户端接口</span></span><br><span class="line"><span class="comment"> * </span></span><br><span class="line"><span class="comment"> * <span class="doctag">@author</span> ningyu</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@date</span> 2017年5月18日 上午11:25:03</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">interface</span> <span class="title">StorageClient</span> </span>&#123;</span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 上传文件</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> groupName   组名称</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> inputStream 文件输入流</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> fileSize    文件大小</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> fileExtName 文件扩展名</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span> 文件存储路径</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="function">StorePath <span class="title">uploadFile</span><span class="params">(String groupName, InputStream inputStream, <span class="keyword">long</span> fileSize, String fileExtName)</span></span>;</span><br><span class="line">     </span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 文件上传</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> inputStream 文件输入流</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> fileSize    文件大小</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> fileExtName 文件扩展名</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span></span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="function">StorePath <span class="title">uploadFile</span><span class="params">(InputStream inputStream, <span class="keyword">long</span> fileSize, String fileExtName)</span></span>;</span><br><span class="line">     </span><br><span class="line">     </span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 文件上传</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> localFilePath 文件完全路径</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span></span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="function">StorePath <span class="title">uploadFile</span><span class="params">(String localFilePath)</span></span>;</span><br><span class="line">     </span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 文件上传</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> localFilePath 文件完全路径</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> fileExtName   文件后缀名</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span></span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="function">StorePath <span class="title">uploadFile</span><span class="params">(String localFilePath, String fileExtName)</span></span>;</span><br><span class="line">     </span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 文件上传</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> groupName     组名称</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> localFilePath 文件完全路径</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> fileExtName   文件后缀名</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span></span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="function">StorePath <span class="title">uploadFile</span><span class="params">(String groupName, String localFilePath, String fileExtName)</span></span>;</span><br><span class="line"> </span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 上传从文件</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> groupName      组名称</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> masterFilename 主文件路径(fastdfs返回的file_id 去掉前面的group)</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> inputStream    从文件输入流</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> fileSize       从文件大小</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> prefixName     从文件前缀</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> fileExtName    主文件扩展名</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span> 文件存储路径</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="function">StorePath <span class="title">uploadSlaveFile</span><span class="params">(String groupName, String masterFilename, InputStream inputStream, <span class="keyword">long</span> fileSize, String prefixName, String fileExtName)</span></span>;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 上传从文件</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> masterFileId 主文件路径（fastdfs返回的file_id，包含前面的group）</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> inputStream  从文件输入流</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> fileSize     从文件大小</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> prefixName   从文件前缀</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> fileExtName  主文件扩展名</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span></span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="function">StorePath <span class="title">uploadSlaveFile</span><span class="params">(String masterFileId, InputStream inputStream, <span class="keyword">long</span> fileSize, String prefixName, String fileExtName)</span></span>;</span><br><span class="line"> </span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 获取文件元信息</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> groupName 组名称</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> path      主文件路径</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span> 获取文件元信息集合，不存在返回空集合</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="function">Set&lt;MateData&gt; <span class="title">getMetadata</span><span class="params">(String groupName, String path)</span></span>;</span><br><span class="line">     </span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 获取文件元信息</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> fileId 文件路径（fastdfs返回的file_id，包含前面的group）</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span></span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="function">Set&lt;MateData&gt; <span class="title">getMetadata</span><span class="params">(String fileId)</span></span>;</span><br><span class="line"> </span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 修改文件元信息（覆盖）</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> groupName   组名称</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> path        主文件路径</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> metaDataSet 元信息集合</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="function"><span class="keyword">boolean</span> <span class="title">overwriteMetadata</span><span class="params">(String groupName, String path, Set&lt;MateData&gt; metaDataSet)</span></span>;</span><br><span class="line">     </span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 修改文件元信息（覆盖）</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> fileId        文件路径（fastdfs返回的file_id，包含前面的group）</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> metaDataSet   元信息集合</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span></span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="function"><span class="keyword">boolean</span> <span class="title">overwriteMetadata</span><span class="params">(String fileId, Set&lt;MateData&gt; metaDataSet)</span></span>;</span><br><span class="line"> </span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 修改文件元信息（合并）</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> groupName   组名称</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> path        主文件路径</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> metaDataSet 元信息集合</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="function"><span class="keyword">boolean</span> <span class="title">mergeMetadata</span><span class="params">(String groupName, String path, Set&lt;MateData&gt; metaDataSet)</span></span>;</span><br><span class="line">     </span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 修改文件元信息（合并）</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> fileId         文件路径（fastdfs返回的file_id，包含前面的group）</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> metaDataSet    元信息集合</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span></span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="function"><span class="keyword">boolean</span> <span class="title">mergeMetadata</span><span class="params">(String fileId, Set&lt;MateData&gt; metaDataSet)</span></span>;</span><br><span class="line"> </span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 获取文件的信息</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> groupName 组名称</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> path      主文件路径</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span> 文件信息(不存在返回null)</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="function">FileInfo <span class="title">getFileInfo</span><span class="params">(String groupName, String path)</span></span>;</span><br><span class="line">     </span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 获取文件信息</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> fileId  文件路径（fastdfs返回的file_id，包含前面的group）</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span></span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="function">FileInfo <span class="title">getFileInfo</span><span class="params">(String fileId)</span></span>;</span><br><span class="line"> </span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 删除文件</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> groupName 组名称</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> path      主文件路径</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="function"><span class="keyword">boolean</span> <span class="title">deleteFile</span><span class="params">(String groupName, String path)</span></span>;</span><br><span class="line">     </span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 删除文件</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> fileId 文件路径（fastdfs返回的file_id，包含前面的group）</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span></span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="function"><span class="keyword">boolean</span> <span class="title">deleteFile</span><span class="params">(String fileId)</span></span>;</span><br><span class="line"> </span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 下载整个文件</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> groupName 组名称</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> path      主文件路径</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> callback  下载回调接口</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span> 下载回调接口返回结果</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    &lt;T&gt; <span class="function">T <span class="title">downloadFile</span><span class="params">(String groupName, String path, DownloadCallback&lt;T&gt; callback)</span></span>;</span><br><span class="line">     </span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 下载整个文件</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> fileId   文件路径（fastdfs返回的file_id，包含前面的group）</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> callback 下载回调接口</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span></span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    &lt;T&gt; <span class="function">T <span class="title">downloadFile</span><span class="params">(String fileId, DownloadCallback&lt;T&gt; callback)</span></span>;</span><br><span class="line"> </span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 下载文件片段</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> groupName  组名称</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> path       主文件路径</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> fileOffset 开始位置</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> fileSize   文件大小(经过测试好像这个参数值只能是“0”)</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> callback   下载回调接口</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span> 下载回调接口返回结果</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    &lt;T&gt; <span class="function">T <span class="title">downloadFile</span><span class="params">(String groupName, String path, <span class="keyword">long</span> fileOffset, <span class="keyword">long</span> fileSize, DownloadCallback&lt;T&gt; callback)</span></span>;</span><br><span class="line">     </span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 下载文件片段</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> fileId     文件路径（fastdfs返回的file_id，包含前面的group）</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> fileOffset 开始位置</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> fileSize   文件大小(经过测试好像这个参数值只能是“0”)</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> callback   下载回调接口</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span></span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    &lt;T&gt; <span class="function">T <span class="title">downloadFile</span><span class="params">(String fileId, <span class="keyword">long</span> fileOffset, <span class="keyword">long</span> fileSize, DownloadCallback&lt;T&gt; callback)</span></span>;</span><br><span class="line"> </span><br><span class="line">    <span class="comment">// ----------------------------------------------------------------------------------------------------------------------------------------------------</span></span><br><span class="line"> </span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 上传文件， 并设置文件元数据</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> inputStream 文件输入流</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> fileSize    文件大小</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> fileExtName 文件扩展名</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> metaDataSet 元信息集合</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span> 文件存储路径</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="function">StorePath <span class="title">uploadFile</span><span class="params">(InputStream inputStream, <span class="keyword">long</span> fileSize, String fileExtName, Set&lt;MateData&gt; metaDataSet)</span></span>;</span><br><span class="line">     </span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 上传文件， 并设置文件元数据</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> groupName    组名</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> inputStream  文件输入流</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> fileSize     文件大小</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> fileExtName  文件扩展名</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> metaDataSet  元信息集合</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span></span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="function">StorePath <span class="title">uploadFile</span><span class="params">(String groupName, InputStream inputStream, <span class="keyword">long</span> fileSize, String fileExtName, Set&lt;MateData&gt; metaDataSet)</span></span>;</span><br><span class="line"> </span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 文件上传(支持续传追加内容)</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> groupName   组名称</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> inputStream 文件输入流(文件部分)</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> fileSize    文件大小</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> fileExtName 文件扩展名</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span> 文件存储路径</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="function">StorePath <span class="title">uploadAppenderFile</span><span class="params">(String groupName, InputStream inputStream, <span class="keyword">long</span> fileSize, String fileExtName)</span></span>;</span><br><span class="line"> </span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 续传文件(追加内容)&lt;/br&gt;</span></span><br><span class="line"><span class="comment">     * 从末尾追加内容&lt;/br&gt;</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> groupName   组名称</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> path        文件路径</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> inputStream 文件输入流(文件部分)</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> fileSize    文件大小</span></span><br><span class="line"><span class="comment">     * </span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="function"><span class="keyword">void</span> <span class="title">appendFile</span><span class="params">(String groupName, String path, InputStream inputStream, <span class="keyword">long</span> fileSize)</span></span>;</span><br><span class="line">     </span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 续传文件(追加内容)&lt;/br&gt;</span></span><br><span class="line"><span class="comment">     * 从末尾追加内容&lt;/br&gt;</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> fileId       文件路径（fastdfs返回的file_id，包含前面的group）</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> inputStream  文件输入流(文件部分)</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> fileSize     文件大小</span></span><br><span class="line"><span class="comment">     * </span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="function"><span class="keyword">void</span> <span class="title">appendFile</span><span class="params">(String fileId, InputStream inputStream, <span class="keyword">long</span> fileSize)</span></span>;</span><br><span class="line"> </span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 修改文件内容的内容&lt;/br&gt;</span></span><br><span class="line"><span class="comment">     * 从offset开始覆盖fileSize长度&lt;/br&gt;</span></span><br><span class="line"><span class="comment">     * 报22参数错误，请检查offset是否超过文件长度&lt;/br&gt;</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> groupName   组名称</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> path        文件路径</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> inputStream 文件输入流</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> fileSize    文件大小</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> fileOffset  开始位置</span></span><br><span class="line"><span class="comment">     * </span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="function"><span class="keyword">void</span> <span class="title">modifyFile</span><span class="params">(String groupName, String path, InputStream inputStream, <span class="keyword">long</span> fileSize, <span class="keyword">long</span> fileOffset)</span></span>;</span><br><span class="line">     </span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 修改文件内容的内容&lt;/br&gt;</span></span><br><span class="line"><span class="comment">     * 从offset开始覆盖fileSize长度&lt;/br&gt;</span></span><br><span class="line"><span class="comment">     * 报22参数错误，请检查offset是否超过文件长度&lt;/br&gt;</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> fileId      文件路径（fastdfs返回的file_id，包含前面的group）</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> inputStream 文件输入流</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> fileSize    文件大小</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> fileOffset  开始位置</span></span><br><span class="line"><span class="comment">     * </span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="function"><span class="keyword">void</span> <span class="title">modifyFile</span><span class="params">(String fileId, InputStream inputStream, <span class="keyword">long</span> fileSize, <span class="keyword">long</span> fileOffset)</span></span>;</span><br><span class="line"> </span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 清除文件的内容</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> groupName         组名称</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> path              文件路径</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> truncatedFileSize 截断文件大小</span></span><br><span class="line"><span class="comment">     * </span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="function"><span class="keyword">void</span> <span class="title">truncateFile</span><span class="params">(String groupName, String path, <span class="keyword">long</span> truncatedFileSize)</span></span>;</span><br><span class="line">     </span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 清除文件的内容</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> fileId            文件路径（fastdfs返回的file_id，包含前面的group）</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> truncatedFileSize 截断文件大小</span></span><br><span class="line"><span class="comment">     * </span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="function"><span class="keyword">void</span> <span class="title">truncateFile</span><span class="params">(String fileId, <span class="keyword">long</span> truncatedFileSize)</span></span>;</span><br><span class="line"> </span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 清除文件的内容</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> groupName 组名称</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> path      文件路径</span></span><br><span class="line"><span class="comment">     * </span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="function"><span class="keyword">void</span> <span class="title">truncateFile</span><span class="params">(String groupName, String path)</span></span>;</span><br><span class="line">     </span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 清除文件的内容</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> fileId 文件路径（fastdfs返回的file_id，包含前面的group）</span></span><br><span class="line"><span class="comment">     * </span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="function"><span class="keyword">void</span> <span class="title">truncateFile</span><span class="params">(String fileId)</span></span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>ps.api我只放出了StorageClient的说明，TrackerClient的使用常规开发时用不到的，架构在进行调优的时候可能会使用到，所以这里就不做过多的解释</strong></p>]]></content>
    
    <summary type="html">
    
      FastDFS-Client使用说明
    
    </summary>
    
      <category term="Fastdfs" scheme="https://ningyu1.github.io/categories/Fastdfs/"/>
    
    
      <category term="Fastdfs" scheme="https://ningyu1.github.io/tags/Fastdfs/"/>
    
  </entry>
  
  <entry>
    <title>MybatisSql获取工具类SqlHelper使用说明</title>
    <link href="https://ningyu1.github.io/20170905/20-mybatis-sqlhelper.html"/>
    <id>https://ningyu1.github.io/20170905/20-mybatis-sqlhelper.html</id>
    <published>2017-09-05T09:50:36.000Z</published>
    <updated>2024-04-23T01:48:15.032Z</updated>
    
    <content type="html"><![CDATA[<h2 id="项目地址"><a href="#项目地址" class="headerlink" title="项目地址"></a>项目地址</h2><p><a href="https://github.com/ningyu1/tsoft-parent/tree/master/tsoft-common" title="tsoft-common" target="_blank" rel="noopener">tsoft-common</a></p><p>[<figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="http://www.gnu.org/licenses/gpl-3.0.html" alt="License](https://img.shields.io/badge/license-GPLv3-blue.svg)" title>                </div>                <div class="image-caption">License](https://img.shields.io/badge/license-GPLv3-blue.svg)</div>            </figure></p><h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>有的时候我们想在代码中获取Mybatis方法的sql但是又不想去实际执行Mybatis的查询方法，可以使用该工具直接得到sql。</p><h2 id="Maven引入"><a href="#Maven引入" class="headerlink" title="Maven引入"></a>Maven引入</h2><figure class="highlight xml"><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>cn.tsoft.framework<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>tsoft-common<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">version</span>&gt;</span>1.0.0-SNAPSHOT<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure><h2 id="目标"><a href="#目标" class="headerlink" title="目标"></a>目标</h2><p>SqlHelper是获取Mybatis方法的sql工具包，支持mybatis mapper方式和sqlmap方式，支持参数：entity，map，array，list，这个工具不需要你实际去执行Mybatis的查询方法就能得到sql，方法主要分两大类，使用命名空间namespace调用或者使用Mapper接口方式调用。</p><h2 id="测试方法"><a href="#测试方法" class="headerlink" title="测试方法"></a>测试方法</h2><figure class="highlight java"><table><tr><td class="code"><pre><span class="line">String sql = <span class="keyword">null</span>;</span><br><span class="line">UserEntity entity = <span class="keyword">new</span> UserEntity();</span><br><span class="line">entity.setUserId(<span class="number">1L</span>);</span><br><span class="line">entity.setPassword(<span class="string">"sdflkjsldjf"</span>);</span><br><span class="line">entity.setPasswordExpire(<span class="keyword">new</span> Date());</span><br><span class="line">entity.setVersion(<span class="number">2L</span>);</span><br><span class="line">List&lt;Long&gt; list = <span class="keyword">new</span> ArrayList&lt;Long&gt;();</span><br><span class="line">list.add(<span class="number">1L</span>);</span><br><span class="line">list.add(<span class="number">2L</span>);</span><br><span class="line">Long[] ids = <span class="keyword">new</span> Long[]&#123;<span class="number">1L</span>,<span class="number">2L</span>&#125;;</span><br><span class="line"><span class="comment">//方式一</span></span><br><span class="line">sql = SqlHelper.getMapperSql(userMapper, <span class="string">"mobileIsExists"</span>, <span class="number">1L</span>, <span class="string">"13800138000"</span>);</span><br><span class="line">System.out.println(<span class="string">"方式一：参数为：@Param："</span>+sql);</span><br><span class="line">sql = SqlHelper.getMapperSql(userMapper, <span class="string">"mobileIsExists"</span>);</span><br><span class="line">System.out.println(<span class="string">"方式一：参数为：无参："</span>+sql);</span><br><span class="line">sql = SqlHelper.getMapperSql(userMapper, <span class="string">"modifyPassword"</span>, entity);</span><br><span class="line">System.out.println(<span class="string">"方式一：参数为：entity"</span>+sql);</span><br><span class="line">sql = SqlHelper.getMapperSql(userMapper, <span class="string">"blockedArrays"</span>, ids);</span><br><span class="line">System.out.println(<span class="string">"方式一：参数为：arrays"</span>+sql);</span><br><span class="line">sql = SqlHelper.getMapperSql(userMapper, <span class="string">"blockedList"</span>, list);</span><br><span class="line">System.out.println(<span class="string">"方式一：参数为：list"</span>+sql);</span><br><span class="line"> </span><br><span class="line">SqlSession sqlSession = mybatisSessionFactory.getObject().openSession();</span><br><span class="line"><span class="comment">//方式二</span></span><br><span class="line">sql = SqlHelper.getMapperSql(sqlSession, <span class="string">"cn.tsoft.account.mapper.UserMapper.mobileIsExists"</span>, <span class="number">1L</span>, <span class="string">"13800138000"</span>);</span><br><span class="line">System.out.println(<span class="string">"方式二：参数为：@Param："</span>+sql);</span><br><span class="line">sql = SqlHelper.getMapperSql(sqlSession, <span class="string">"cn.tsoft.account.mapper.UserMapper.mobileIsExists"</span>);</span><br><span class="line">System.out.println(<span class="string">"方式二：参数为：无参："</span>+sql);</span><br><span class="line">sql = SqlHelper.getMapperSql(sqlSession, <span class="string">"cn.tsoft.account.mapper.UserMapper.modifyPassword"</span>, entity);</span><br><span class="line">System.out.println(<span class="string">"方式二：参数为：entity"</span>+sql);</span><br><span class="line">sql = SqlHelper.getMapperSql(sqlSession, <span class="string">"cn.tsoft.account.mapper.UserMapper.blockedArrays"</span>, ids);</span><br><span class="line">System.out.println(<span class="string">"方式二：参数为：arrays"</span>+sql);</span><br><span class="line">sql = SqlHelper.getMapperSql(sqlSession, <span class="string">"cn.tsoft.account.mapper.UserMapper.blockedList"</span>, list);</span><br><span class="line">System.out.println(<span class="string">"方式二：参数为：list"</span>+sql);</span><br><span class="line"> </span><br><span class="line"><span class="comment">//方式三</span></span><br><span class="line">sql = SqlHelper.getMapperSql(sqlSession, UserMapper.class, "mobileIsExists", 1L, "13800138000");</span><br><span class="line">System.out.println(<span class="string">"方式三：参数为：@Param："</span>+sql);</span><br><span class="line">sql = SqlHelper.getMapperSql(sqlSession, UserMapper.class, "mobileIsExists");</span><br><span class="line">System.out.println(<span class="string">"方式三：参数为：无参："</span>+sql);</span><br><span class="line">sql = SqlHelper.getMapperSql(sqlSession, UserMapper.class, "modifyPassword", entity);</span><br><span class="line">System.out.println(<span class="string">"方式三：参数为：entity"</span>+sql);</span><br><span class="line">sql = SqlHelper.getMapperSql(sqlSession, UserMapper.class, "blockedArrays", ids);</span><br><span class="line">System.out.println(<span class="string">"方式三：参数为：arrays"</span>+sql);</span><br><span class="line">sql = SqlHelper.getMapperSql(sqlSession, UserMapper.class, "blockedList", list);</span><br><span class="line">System.out.println(<span class="string">"方式三：参数为：list"</span>+sql);</span><br></pre></td></tr></table></figure><h2 id="日志输出"><a href="#日志输出" class="headerlink" title="日志输出"></a>日志输出</h2><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">方式一：参数为：@Param：</span><br><span class="line">SELECT COUNT(t.`ID`) FROM t_user t </span><br><span class="line">WHERE t.`MOBILE` = &apos;13800138000&apos;</span><br><span class="line">          </span><br><span class="line">            AND t.`USER_ID` != &apos;1&apos;</span><br><span class="line">方式一：参数为：无参：</span><br><span class="line">SELECT COUNT(t.`ID`) FROM t_user t </span><br><span class="line">WHERE t.`MOBILE` = &apos;null&apos;</span><br><span class="line">方式一：参数为：entity：</span><br><span class="line">UPDATE t_user t </span><br><span class="line">        SET </span><br><span class="line">        t.`PASSWORD` = &apos;sdflkjsldjf&apos; , </span><br><span class="line">        t.`LAST_UPDATE_TIME` = CURRENT_TIMESTAMP , </span><br><span class="line">        t.`PASSWORD_MODIFY_TIME` = CURRENT_TIMESTAMP , </span><br><span class="line">        t.`PASSWORD_EXPIRE` = &apos;Fri Aug 25 19:36:00 CST 2017&apos; , </span><br><span class="line">        t.`VERSION` = t.`VERSION` + 1 </span><br><span class="line">        WHERE </span><br><span class="line">        t.`USER_ID` = 1 </span><br><span class="line">        AND t.`VERSION` = 2</span><br><span class="line">方式一：参数为：arrays：</span><br><span class="line">UPDATE t_user t</span><br><span class="line">        SET </span><br><span class="line">        t.`LAST_UPDATE_TIME` = CURRENT_TIMESTAMP , </span><br><span class="line">        t.`VERSION` = t.`VERSION` + 1 </span><br><span class="line">        WHERE </span><br><span class="line">        t.`USER_ID` in</span><br><span class="line">           </span><br><span class="line">            1</span><br><span class="line">         , </span><br><span class="line">            2</span><br><span class="line">方式一：参数为：list：</span><br><span class="line">UPDATE t_user t</span><br><span class="line">        SET </span><br><span class="line">        t.`LAST_UPDATE_TIME` = CURRENT_TIMESTAMP , </span><br><span class="line">        t.`VERSION` = t.`VERSION` + 1 </span><br><span class="line">        WHERE </span><br><span class="line">        t.`USER_ID` in</span><br><span class="line">           </span><br><span class="line">            1</span><br><span class="line">         , </span><br><span class="line">            2</span><br><span class="line">方式二：参数为：@Param：</span><br><span class="line">SELECT COUNT(t.`ID`) FROM t_user t </span><br><span class="line">        WHERE t.`MOBILE` = &apos;13800138000&apos;</span><br><span class="line">          </span><br><span class="line">            AND t.`USER_ID` != &apos;1&apos;</span><br><span class="line">方式二：参数为：无参：</span><br><span class="line">SELECT COUNT(t.`ID`) FROM t_user t </span><br><span class="line">        WHERE t.`MOBILE` = &apos;null&apos;</span><br><span class="line">方式二：参数为：entity：</span><br><span class="line">UPDATE t_user t </span><br><span class="line">        SET </span><br><span class="line">        t.`PASSWORD` = &apos;sdflkjsldjf&apos; , </span><br><span class="line">        t.`LAST_UPDATE_TIME` = CURRENT_TIMESTAMP , </span><br><span class="line">        t.`PASSWORD_MODIFY_TIME` = CURRENT_TIMESTAMP , </span><br><span class="line">        t.`PASSWORD_EXPIRE` = &apos;Fri Aug 25 19:36:00 CST 2017&apos; , </span><br><span class="line">        t.`VERSION` = t.`VERSION` + 1 </span><br><span class="line">        WHERE </span><br><span class="line">        t.`USER_ID` = 1 </span><br><span class="line">        AND t.`VERSION` = 2</span><br><span class="line">方式二：参数为：arrays：</span><br><span class="line">UPDATE t_user t</span><br><span class="line">        SET </span><br><span class="line">        t.`LAST_UPDATE_TIME` = CURRENT_TIMESTAMP , </span><br><span class="line">        t.`VERSION` = t.`VERSION` + 1 </span><br><span class="line">        WHERE </span><br><span class="line">        t.`USER_ID` in</span><br><span class="line">           </span><br><span class="line">            1</span><br><span class="line">         , </span><br><span class="line">            2</span><br><span class="line">方式二：参数为：list：</span><br><span class="line">UPDATE t_user t</span><br><span class="line">        SET </span><br><span class="line">        t.`LAST_UPDATE_TIME` = CURRENT_TIMESTAMP , </span><br><span class="line">        t.`VERSION` = t.`VERSION` + 1 </span><br><span class="line">        WHERE </span><br><span class="line">        t.`USER_ID` in</span><br><span class="line">           </span><br><span class="line">            1</span><br><span class="line">         , </span><br><span class="line">            2</span><br><span class="line">方式三：参数为：@Param：</span><br><span class="line">SELECT COUNT(t.`ID`) FROM t_user t </span><br><span class="line">        WHERE t.`MOBILE` = &apos;13800138000&apos;</span><br><span class="line">          </span><br><span class="line">            AND t.`USER_ID` != &apos;1&apos;</span><br><span class="line">方式三：参数为：无参：</span><br><span class="line">SELECT COUNT(t.`ID`) FROM t_user t </span><br><span class="line">        WHERE t.`MOBILE` = &apos;null&apos;</span><br><span class="line">方式三：参数为：entity：</span><br><span class="line">UPDATE t_user t </span><br><span class="line">        SET </span><br><span class="line">        t.`PASSWORD` = &apos;sdflkjsldjf&apos; , </span><br><span class="line">        t.`LAST_UPDATE_TIME` = CURRENT_TIMESTAMP , </span><br><span class="line">        t.`PASSWORD_MODIFY_TIME` = CURRENT_TIMESTAMP , </span><br><span class="line">        t.`PASSWORD_EXPIRE` = &apos;Fri Aug 25 19:36:00 CST 2017&apos; , </span><br><span class="line">        t.`VERSION` = t.`VERSION` + 1 </span><br><span class="line">        WHERE </span><br><span class="line">        t.`USER_ID` = 1 </span><br><span class="line">        AND t.`VERSION` = 2</span><br><span class="line">方式三：参数为：arrays：</span><br><span class="line">UPDATE t_user t</span><br><span class="line">        SET </span><br><span class="line">        t.`LAST_UPDATE_TIME` = CURRENT_TIMESTAMP , </span><br><span class="line">        t.`VERSION` = t.`VERSION` + 1 </span><br><span class="line">        WHERE </span><br><span class="line">        t.`USER_ID` in</span><br><span class="line">           </span><br><span class="line">            1</span><br><span class="line">         , </span><br><span class="line">            2</span><br><span class="line">方式三：参数为：list：</span><br><span class="line">UPDATE t_user t</span><br><span class="line">        SET </span><br><span class="line">        t.`LAST_UPDATE_TIME` = CURRENT_TIMESTAMP , </span><br><span class="line">        t.`VERSION` = t.`VERSION` + 1 </span><br><span class="line">        WHERE </span><br><span class="line">        t.`USER_ID` in</span><br><span class="line">           </span><br><span class="line">            1</span><br><span class="line">         , </span><br><span class="line">            2</span><br></pre></td></tr></table></figure>]]></content>
    
    <summary type="html">
    
      MybatisSql获取工具类SqlHelper使用说明
    
    </summary>
    
      <category term="Mybatis" scheme="https://ningyu1.github.io/categories/Mybatis/"/>
    
    
      <category term="Mybatis" scheme="https://ningyu1.github.io/tags/Mybatis/"/>
    
      <category term="Sqlhelper" scheme="https://ningyu1.github.io/tags/Sqlhelper/"/>
    
  </entry>
  
  <entry>
    <title>JMS实现参数的集中式管理</title>
    <link href="https://ningyu1.github.io/20170905/19-jms-ucm.html"/>
    <id>https://ningyu1.github.io/20170905/19-jms-ucm.html</id>
    <published>2017-09-05T09:12:36.000Z</published>
    <updated>2024-04-23T01:48:15.032Z</updated>
    
    <content type="html"><![CDATA[<h1 id="点评"><a href="#点评" class="headerlink" title="点评"></a>点评</h1><p>虽然现在开源的UCM套件很多，UCM统一配置管理（百度的disconf、阿里的diamond、点评的lion，等很多开源的）。但是很多人是知其然不知其所以然，刚好发现下面这篇文章可以作为原理的教程文章，使用JMS、Redis、Zookeeper简单的实现UCM基本功能，作为学习交流还是很不错的。</p><p>文章转自：<a href="https://my.oschina.net/OutOfMemory/blog/1510101" target="_blank" rel="noopener">https://my.oschina.net/OutOfMemory/blog/1510101</a><br>作者：@ksfzhaohui</p><h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>JMS的发布订阅机制也能实现类似的功能，集群节点通过订阅指定的节点，同时使用JMS对消息的过滤器功能，实现对指定参数的更新，本文将介绍通过JMS实现简单的参数集中式管理。</p><h2 id="Maven引入"><a href="#Maven引入" class="headerlink" title="Maven引入"></a>Maven引入</h2><p>Spring相关的jar引入参考上一篇文章</p><figure class="highlight xml"><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>javax.jms<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>jms<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">version</span>&gt;</span>1.1<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.apache.activemq<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>activemq-all<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">version</span>&gt;</span>5.10.0<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure><h2 id="目标"><a href="#目标" class="headerlink" title="目标"></a>目标</h2><ol><li>可以同时配置监听多个节点如/app1,/app2；</li><li>希望只需要配置如/app1，就能够监听其子节点如/app1/modual1以及子节点的子节点如/app1/modual1/xxx/…；</li><li>服务器启动能获取当前指定父节点下的所有子节点数据；</li><li>在添加节点或者在更新节点数据的时候能够动态通知，这样代码中就能够实时获取最新的数据；</li><li>spring配置中可以从Zookeeper中读取参数进行初始化。</li></ol><p>虽然在实现的方式上有点区别，但是最终达成的目标是一致的，同样列出了这5条目标</p><h2 id="实现"><a href="#实现" class="headerlink" title="实现"></a>实现</h2><p>MQWatcher主要用来和JMS建立连接，同时订阅指定节点，建立点对点连接，过滤出需要监听的数据，更新数据，初始化数据，存储数据等<br>InitConfServer主要作为点对点连接的服务器端用来初始化数据</p><p>1.同时配置监听多个节点<br>提供一个字符串数组给用户用来添加需要监听的节点：</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">private</span> String[] keyPatterns;</span><br></pre></td></tr></table></figure><p>2.能够监听其子节点以及子节点的子节点<br>使用了一种和Zookeeper不一样的方式，JMS的方式是将所有的数据变更都发送到订阅者，然后订阅者通过过滤出需要的数据进行更新</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="comment">/** MQ的过滤器 **/</span></span><br><span class="line"><span class="keyword">private</span> StringBuffer keyFilter = <span class="keyword">new</span> StringBuffer();</span><br><span class="line"> </span><br><span class="line"><span class="keyword">private</span> <span class="keyword">final</span> String TOPIC = <span class="string">"dynamicConfTopic"</span>;</span><br><span class="line"> </span><br><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">watcherPaths</span><span class="params">()</span> <span class="keyword">throws</span> JMSException </span>&#123;</span><br><span class="line">    Topic topic = session.createTopic(TOPIC);</span><br><span class="line">    MessageConsumer consumer = session.createConsumer(topic, keyFilter.toString());</span><br><span class="line">    consumer.setMessageListener(<span class="keyword">new</span> MessageListener() &#123;</span><br><span class="line"> </span><br><span class="line">        <span class="meta">@Override</span></span><br><span class="line">        <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">onMessage</span><span class="params">(Message message)</span> </span>&#123;</span><br><span class="line">            <span class="keyword">try</span> &#123;</span><br><span class="line">                String key = message.getStringProperty(IDENTIFIER);</span><br><span class="line">                TextMessage tm = (TextMessage) message;</span><br><span class="line">                keyValueMap.put(key, tm.getText());</span><br><span class="line">                LOGGER.info(<span class="string">"key = "</span> + key + <span class="string">",value = "</span> + tm.getText());</span><br><span class="line">            &#125; <span class="keyword">catch</span> (JMSException e) &#123;</span><br><span class="line">                LOGGER.error(<span class="string">"onMessage error"</span>, e);</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>对TOPIC进行了订阅，并且指定了过滤器keyFilter，keyFilter正是基于keyPatterns组装而成的</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">final</span> String IDENTIFIER = <span class="string">"confKey"</span>;</span><br><span class="line"> </span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment">* 生成接受过滤器</span></span><br><span class="line"><span class="comment">*/</span></span><br><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">generateKeyFilter</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    <span class="keyword">for</span> (<span class="keyword">int</span> i = <span class="number">0</span>; i &lt; keyPatterns.length; i++) &#123;</span><br><span class="line">        keyFilter.append(IDENTIFIER + <span class="string">" LIKE '"</span> + keyPatterns[i] + <span class="string">"%'"</span>);</span><br><span class="line">        <span class="keyword">if</span> (i &lt; keyPatterns.length - <span class="number">1</span>) &#123;</span><br><span class="line">            keyFilter.append(<span class="string">" OR "</span>);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    LOGGER.info(<span class="string">"keyFilter : "</span> + keyFilter.toString());</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>对指定的属性IDENTIFIER，通过LIKE和OR关键字进行过滤</p><p>3.服务器启动初始化节点数据<br>通过点对点的方式，在服务器启动时通过请求响应模式来获取初始化数据</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">final</span> String QUEUE = <span class="string">"dynamicConfQueue"</span>;</span><br><span class="line"> </span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 初始化key-value值</span></span><br><span class="line"><span class="comment"> * </span></span><br><span class="line"><span class="comment"> * <span class="doctag">@throws</span> JMSException</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">initKeyValues</span><span class="params">()</span> <span class="keyword">throws</span> JMSException </span>&#123;</span><br><span class="line">    TemporaryQueue responseQueue = <span class="keyword">null</span>;</span><br><span class="line">    MessageProducer producer = <span class="keyword">null</span>;</span><br><span class="line">    MessageConsumer consumer = <span class="keyword">null</span>;</span><br><span class="line">    Queue queue = queueSession.createQueue(QUEUE);</span><br><span class="line"> </span><br><span class="line">    TextMessage requestMessage = queueSession.createTextMessage();</span><br><span class="line">    requestMessage.setText(generateKeyString());</span><br><span class="line">    responseQueue = queueSession.createTemporaryQueue();</span><br><span class="line">    producer = queueSession.createProducer(queue);</span><br><span class="line">    consumer = queueSession.createConsumer(responseQueue);</span><br><span class="line">    requestMessage.setJMSReplyTo(responseQueue);</span><br><span class="line">    producer.send(requestMessage);</span><br><span class="line"> </span><br><span class="line">    MapMessage receiveMap = (MapMessage) consumer.receive();</span><br><span class="line">    <span class="meta">@SuppressWarnings</span>(<span class="string">"unchecked"</span>)</span><br><span class="line">    Enumeration&lt;String&gt; mapNames = receiveMap.getPropertyNames();</span><br><span class="line">    <span class="keyword">while</span> (mapNames.hasMoreElements()) &#123;</span><br><span class="line">        String key = mapNames.nextElement();</span><br><span class="line">        String value = receiveMap.getStringProperty(key);</span><br><span class="line">        keyValueMap.put(key, value);</span><br><span class="line">        LOGGER.info(<span class="string">"init key = "</span> + key + <span class="string">",value = "</span> + value);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>通过对指定QUEUE请求，同时建立一个临时的响应QUEUE，然后接受一个MapMessage，用来初始化keyValueMap</p><p>4.监听节点数据的变更<br>通过发布订阅模式，接受所有数据，然后进行过滤，目标2中已经有相关实现</p><p>5.spring配置中可以从Zookeeper中读取参数进行初始化</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">MQPropPlaceholderConfigurer</span> <span class="keyword">extends</span> <span class="title">PropertyPlaceholderConfigurer</span> </span>&#123;</span><br><span class="line"> </span><br><span class="line">    <span class="keyword">private</span> MQWatcher mqwatcher;</span><br><span class="line"> </span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">protected</span> Properties <span class="title">mergeProperties</span><span class="params">()</span> <span class="keyword">throws</span> IOException </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> loadPropFromMQ(<span class="keyword">super</span>.mergeProperties());</span><br><span class="line">    &#125;</span><br><span class="line"> </span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 从MQ中加载配置的常量</span></span><br><span class="line"><span class="comment">     * </span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> result</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span></span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="function"><span class="keyword">private</span> Properties <span class="title">loadPropFromMQ</span><span class="params">(Properties result)</span> </span>&#123;</span><br><span class="line">        mqwatcher.watcherKeys();</span><br><span class="line">        mqwatcher.fillProperties(result);</span><br><span class="line">        <span class="keyword">return</span> result;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>通过以上的处理，可以使用如下简单的配置来达到目标：</p><figure class="highlight xml"><table><tr><td class="code"><pre><span class="line"><span class="meta">&lt;?xml version="1.0" encoding="UTF-8"?&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">beans</span> <span class="attr">xmlns</span>=<span class="string">"http://www.springframework.org/schema/beans"</span></span></span><br><span class="line"><span class="tag">    <span class="attr">xmlns:xsi</span>=<span class="string">"http://www.w3.org/2001/XMLSchema-instance"</span> <span class="attr">xmlns:aop</span>=<span class="string">"http://www.springframework.org/schema/aop"</span></span></span><br><span class="line"><span class="tag">    <span class="attr">xmlns:tx</span>=<span class="string">"http://www.springframework.org/schema/tx"</span></span></span><br><span class="line"><span class="tag">    <span class="attr">xsi:schemaLocation</span>=<span class="string">"http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd</span></span></span><br><span class="line"><span class="tag"><span class="string">        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd</span></span></span><br><span class="line"><span class="tag"><span class="string">        http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd"</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">bean</span> <span class="attr">id</span>=<span class="string">"person"</span> <span class="attr">class</span>=<span class="string">"zh.maven.DynamicConf.Person"</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">"name"</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">value</span>&gt;</span>$&#123;/a2/m1&#125;<span class="tag">&lt;/<span class="name">value</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">property</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">"address"</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">value</span>&gt;</span>$&#123;/a3/m1/v2&#125;<span class="tag">&lt;/<span class="name">value</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">property</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">"company"</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">value</span>&gt;</span>$&#123;/a3/m1/v2/t2&#125;<span class="tag">&lt;/<span class="name">value</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">property</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">bean</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">bean</span> <span class="attr">id</span>=<span class="string">"mqwatcher"</span> <span class="attr">class</span>=<span class="string">"zh.maven.DynamicConf.mq.MQWatcher"</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">"keyPatterns"</span> <span class="attr">value</span>=<span class="string">"/a2,/a3"</span> /&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">bean</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">bean</span> <span class="attr">id</span>=<span class="string">"propertyConfigurer"</span> <span class="attr">class</span>=<span class="string">"zh.maven.DynamicConf.mq.MQPropPlaceholderConfigurer"</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">"mqwatcher"</span> <span class="attr">ref</span>=<span class="string">"mqwatcher"</span>&gt;</span><span class="tag">&lt;/<span class="name">property</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">bean</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">beans</span>&gt;</span></span><br></pre></td></tr></table></figure><h2 id="测试"><a href="#测试" class="headerlink" title="测试"></a>测试</h2><p>1.启动ActiveMQ</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">activemq.bat</span><br></pre></td></tr></table></figure><p>2.InitConfServer启动<br>用来监听集群节点的初始化请求，获取到集群节点发送来的keyPatterns，然后将符合其模式的数据封装成MapMessage发送给集群节点</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">onMessage</span><span class="params">(Message message)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">        TextMessage receiveMessage = (TextMessage) message;</span><br><span class="line">        String keys = receiveMessage.getText();</span><br><span class="line">        LOGGER.info(<span class="string">"keys = "</span> + keys);</span><br><span class="line">        MapMessage returnMess = session.createMapMessage();</span><br><span class="line">        returnMess.setStringProperty(<span class="string">"/a2/m1"</span>, <span class="string">"zhaohui"</span>);</span><br><span class="line">        returnMess.setStringProperty(<span class="string">"/a3/m1/v2"</span>, <span class="string">"nanjing"</span>);</span><br><span class="line">        returnMess.setStringProperty(<span class="string">"/a3/m1/v2/t2"</span>, <span class="string">"zhaohui"</span>);</span><br><span class="line"> </span><br><span class="line">        QueueSender sender = session.createSender((Queue) message.getJMSReplyTo());</span><br><span class="line">        sender.send(returnMess);</span><br><span class="line">    &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">        LOGGER.error(<span class="string">"onMessage error"</span>, e);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>以上代码只是进行了简单的模拟，提供了一个思路</p><p>3.启动Main类</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">Main</span> </span>&#123;</span><br><span class="line"> </span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">        ApplicationContext context = <span class="keyword">new</span> ClassPathXmlApplicationContext(<span class="keyword">new</span> String[] &#123; <span class="string">"spring-config.xml"</span> &#125;);</span><br><span class="line">        Person person = (Person) context.getBean(<span class="string">"person"</span>);</span><br><span class="line">        System.out.println(person.toString());</span><br><span class="line">        &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>4.启动TopicPublisher<br>定时发布数据，同时查看集群节点的Main类日志输出</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">TopicPublisher</span> </span>&#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> String TOPIC = <span class="string">"dynamicConfTopic"</span>;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> String IDENTIFIER = <span class="string">"confKey"</span>;</span><br><span class="line"> </span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> JMSException </span>&#123;</span><br><span class="line">        ActiveMQConnectionFactory factory = <span class="keyword">new</span> ActiveMQConnectionFactory(<span class="string">"tcp://localhost:61616"</span>);</span><br><span class="line">        Connection connection = factory.createConnection();</span><br><span class="line">        connection.start();</span><br><span class="line"> </span><br><span class="line">        Session session = connection.createSession(<span class="keyword">false</span>, Session.AUTO_ACKNOWLEDGE);</span><br><span class="line">        Topic topic = session.createTopic(TOPIC);</span><br><span class="line"> </span><br><span class="line">        MessageProducer producer = session.createProducer(topic);</span><br><span class="line">        producer.setDeliveryMode(DeliveryMode.NON_PERSISTENT);</span><br><span class="line"> </span><br><span class="line">        <span class="keyword">int</span> i=<span class="number">1</span>;</span><br><span class="line">        <span class="keyword">while</span> (<span class="keyword">true</span>) &#123;</span><br><span class="line">            TextMessage message = session.createTextMessage();</span><br><span class="line">            message.setStringProperty(IDENTIFIER, <span class="string">"/a2/"</span>+i);</span><br><span class="line">            message.setText(<span class="string">"message_"</span> + System.currentTimeMillis());</span><br><span class="line">            producer.send(message);</span><br><span class="line">            System.out.println(<span class="string">"Sent message: "</span> + message.getText());</span><br><span class="line">            <span class="keyword">try</span> &#123;</span><br><span class="line">                Thread.sleep(<span class="number">2000</span>);</span><br><span class="line">            &#125; <span class="keyword">catch</span> (InterruptedException e) &#123;</span><br><span class="line">                e.printStackTrace();</span><br><span class="line">            &#125;</span><br><span class="line">            i++;</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 plain"><table><tr><td class="code"><pre><span class="line">2017-08-14 21:52:23 - keyFilter : confKey LIKE &apos;/a2%&apos; OR confKey LIKE &apos;/a3%&apos;</span><br><span class="line">2017-08-14 21:52:24 - init key = /a3/m1/v2/t2,value = zhaohui</span><br><span class="line">2017-08-14 21:52:24 - init key = /a3/m1/v2,value = nanjing</span><br><span class="line">2017-08-14 21:52:24 - init key = /a2/m1,value = zhaohui</span><br><span class="line">2017-08-14 21:52:24 - Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@223dd567: defining beans [person,mqwatcher,propertyConfigurer]; root of factory hierarchy</span><br><span class="line">name = zhaohui,address = nanjing,company = zhaohui</span><br><span class="line">2017-08-14 21:52:33 - key = /a2/1,value = message_1502718753819</span><br><span class="line">2017-08-14 21:52:35 - key = /a2/2,value = message_1502718755832</span><br><span class="line">2017-08-14 21:52:37 - key = /a2/3,value = message_1502718757846</span><br><span class="line">2017-08-14 21:52:39 - key = /a2/4,value = message_1502718759860</span><br><span class="line">2017-08-14 21:52:41 - key = /a2/5,value = message_1502718761876</span><br></pre></td></tr></table></figure><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>通过JMS实现了一个简单的参数化平台系统，当然想在生产中使用还有很多需要优化的地方，本文在于提供一个思路；后续有时间准备对DynamicConf提供更加完善的方案。</p>]]></content>
    
    <summary type="html">
    
      JMS实现参数的集中式管理
    
    </summary>
    
      <category term="JMS" scheme="https://ningyu1.github.io/categories/JMS/"/>
    
      <category term="技术" scheme="https://ningyu1.github.io/categories/JMS/%E6%8A%80%E6%9C%AF/"/>
    
    
      <category term="UCM" scheme="https://ningyu1.github.io/tags/UCM/"/>
    
      <category term="JMS" scheme="https://ningyu1.github.io/tags/JMS/"/>
    
  </entry>
  
  <entry>
    <title>Zookeeper实现参数的集中式管理</title>
    <link href="https://ningyu1.github.io/20170905/18-zookeeper-ucm.html"/>
    <id>https://ningyu1.github.io/20170905/18-zookeeper-ucm.html</id>
    <published>2017-09-05T09:07:36.000Z</published>
    <updated>2024-04-23T01:48:15.032Z</updated>
    
    <content type="html"><![CDATA[<h1 id="点评"><a href="#点评" class="headerlink" title="点评"></a>点评</h1><p>虽然现在开源的UCM套件很多，UCM统一配置管理（百度的disconf、阿里的diamond、点评的lion，等很多开源的）。但是很多人是知其然不知其所以然，刚好发现下面这篇文章可以作为原理的教程文章，使用JMS、Redis、Zookeeper简单的实现UCM基本功能，作为学习交流还是很不错的。</p><p>文章转自：<a href="https://my.oschina.net/OutOfMemory/blog/1503392" target="_blank" rel="noopener">https://my.oschina.net/OutOfMemory/blog/1503392</a><br>作者：@ksfzhaohui</p><h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>应用项目中都会有一些参数，一般的做法通常可以选择将其存储在本地配置文件或者内存变量中；对于集群机器规模不大、配置变更不是特别频繁的情况下，这两种方式都能很好的解决；但是一旦集群机器规模变大，且配置信息越来越频繁，依靠这两种方式就越来越困难；我们希望能够快速的做到全局参数的变更，因此需要一种参数的集中式管理，下面利用Zookeeper的一些特性来实现简单的参数管理。</p><h2 id="准备"><a href="#准备" class="headerlink" title="准备"></a>准备</h2><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">jdk:1.7.0_80</span><br><span class="line">zookeeper:3.4.3</span><br><span class="line">curator:2.6.0</span><br><span class="line">spring:3.1.2</span><br></pre></td></tr></table></figure><h2 id="Maven引入"><a href="#Maven引入" class="headerlink" title="Maven引入"></a>Maven引入</h2><p>Spring相关的jar引入参考上一篇文章</p><figure class="highlight xml"><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.springframework<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-core<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">version</span>&gt;</span>3.1.2.RELEASE<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.springframework<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-context<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">version</span>&gt;</span>3.1.2.RELEASE<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.springframework<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-beans<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">version</span>&gt;</span>3.1.2.RELEASE<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.apache.zookeeper<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>zookeeper<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">version</span>&gt;</span>3.4.3<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">exclusions</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">exclusion</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>com.sun.jmx<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>jmxri<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">exclusion</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">exclusion</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>com.sun.jdmk<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>jmxtools<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">exclusion</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">exclusion</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>javax.jms<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>jms<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">exclusion</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">exclusions</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.apache.curator<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>curator-framework<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">version</span>&gt;</span>2.6.0<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.apache.curator<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>curator-recipes<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">version</span>&gt;</span>2.6.0<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure><h2 id="目标"><a href="#目标" class="headerlink" title="目标"></a>目标</h2><ol><li>可以同时配置监听多个节点如/app1,/app2；</li><li>希望只需要配置如/app1，就能够监听其子节点如/app1/modual1以及子节点的子节点如/app1/modual1/xxx/…；</li><li>服务器启动能获取当前指定父节点下的所有子节点数据；</li><li>在添加节点或者在更新节点数据的时候能够动态通知，这样代码中就能够实时获取最新的数据；</li><li>spring配置中可以从Zookeeper中读取参数进行初始化。</li></ol><h2 id="实现"><a href="#实现" class="headerlink" title="实现"></a>实现</h2><p>提供ZKWatcher类主要用来和Zookeeper建立连接，监听节点，初始化节点数据，更新节点数据，存储节点数据等</p><p>1.同时配置监听多个节点<br>提供一个字符串数组给用户用来添加需要监听的节点：</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">private</span> String[] keyPatterns;</span><br></pre></td></tr></table></figure><p>2.能够监听其子节点以及子节点的子节点<br>使用递归的方式用来获取指定监听节点的子节点：</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">private</span> List&lt;String&gt; <span class="title">listChildren</span><span class="params">(String path)</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">    List&lt;String&gt; pathList = <span class="keyword">new</span> ArrayList&lt;String&gt;();</span><br><span class="line">    pathList.add(path);</span><br><span class="line">    List&lt;String&gt; list = client.getChildren().forPath(path);</span><br><span class="line">    <span class="keyword">if</span> (list != <span class="keyword">null</span> &amp;&amp; list.size() &gt; <span class="number">0</span>) &#123;</span><br><span class="line">        <span class="keyword">for</span> (String cPath : list) &#123;</span><br><span class="line">            String temp = <span class="string">""</span>;</span><br><span class="line">            <span class="keyword">if</span> (<span class="string">"/"</span>.equals(path)) &#123;</span><br><span class="line">                temp = path + cPath;</span><br><span class="line">            &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                temp = path + <span class="string">"/"</span> + cPath;</span><br><span class="line">            &#125;</span><br><span class="line">            pathList.addAll(listChildren(temp));</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> pathList;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>3.服务器启动初始化节点数据<br>上面已经递归获取了所有的节点，所有可以遍历获取所有节点数据，并且存储在Map中：</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">private</span> Map&lt;String, String&gt; keyValueMap = <span class="keyword">new</span> ConcurrentHashMap&lt;String, String&gt;();</span><br><span class="line"> </span><br><span class="line"><span class="keyword">if</span> (pathList != <span class="keyword">null</span> &amp;&amp; pathList.size() &gt; <span class="number">0</span>) &#123;</span><br><span class="line">    <span class="keyword">for</span> (String path : pathList) &#123;</span><br><span class="line">        keyValueMap.put(path, readPath(path));</span><br><span class="line">        watcherPath(path);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"> </span><br><span class="line"><span class="function"><span class="keyword">private</span> String <span class="title">readPath</span><span class="params">(String path)</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">    <span class="keyword">byte</span>[] buffer = client.getData().forPath(path);</span><br><span class="line">    String value = <span class="keyword">new</span> String(buffer);</span><br><span class="line">    logger.info(<span class="string">"readPath:path = "</span> + path + <span class="string">",value = "</span> + value);</span><br><span class="line">    <span class="keyword">return</span> value;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>4.监听节点数据的变更<br>使用PathChildrenCache用来监听子节点的CHILD_ADDED，CHILD_UPDATED，CHILD_REMOVED事件：</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">watcherPath</span><span class="params">(String path)</span> </span>&#123;</span><br><span class="line">    PathChildrenCache cache = <span class="keyword">null</span>;</span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">        cache = <span class="keyword">new</span> PathChildrenCache(client, path, <span class="keyword">true</span>);</span><br><span class="line">        cache.start(StartMode.POST_INITIALIZED_EVENT);</span><br><span class="line">        cache.getListenable().addListener(<span class="keyword">new</span> PathChildrenCacheListener() &#123;</span><br><span class="line"> </span><br><span class="line">            <span class="meta">@Override</span></span><br><span class="line">            <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">childEvent</span><span class="params">(CuratorFramework client, PathChildrenCacheEvent event)</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">                <span class="keyword">switch</span> (event.getType()) &#123;</span><br><span class="line">                <span class="keyword">case</span> CHILD_ADDED:</span><br><span class="line">                    logger.info(<span class="string">"CHILD_ADDED,"</span> + event.getData().getPath());</span><br><span class="line">                    watcherPath(event.getData().getPath());</span><br><span class="line">                    keyValueMap.put(event.getData().getPath(), <span class="keyword">new</span> String(event.getData().getData()));</span><br><span class="line">                    <span class="keyword">break</span>;</span><br><span class="line">                <span class="keyword">case</span> CHILD_UPDATED:</span><br><span class="line">                    logger.info(<span class="string">"CHILD_UPDATED,"</span> + event.getData().getPath());</span><br><span class="line">                    keyValueMap.put(event.getData().getPath(), <span class="keyword">new</span> String(event.getData().getData()));</span><br><span class="line">                    <span class="keyword">break</span>;</span><br><span class="line">                <span class="keyword">case</span> CHILD_REMOVED:</span><br><span class="line">                    logger.info(<span class="string">"CHILD_REMOVED,"</span> + event.getData().getPath());</span><br><span class="line">                    <span class="keyword">break</span>;</span><br><span class="line">                <span class="keyword">default</span>:</span><br><span class="line">                    <span class="keyword">break</span>;</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 class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">        <span class="keyword">if</span> (cache != <span class="keyword">null</span>) &#123;</span><br><span class="line">            <span class="keyword">try</span> &#123;</span><br><span class="line">                cache.close();</span><br><span class="line">            &#125; <span class="keyword">catch</span> (IOException e1) &#123;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        logger.error(<span class="string">"watch path error"</span>, e);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>5.spring配置中可以从Zookeeper中读取参数进行初始化<br>实现自定义的PropertyPlaceholderConfigurer类ZKPropPlaceholderConfigurer：</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">ZKPropPlaceholderConfigurer</span> <span class="keyword">extends</span> <span class="title">PropertyPlaceholderConfigurer</span> </span>&#123;</span><br><span class="line"> </span><br><span class="line">    <span class="keyword">private</span> ZKWatcher zkwatcher;</span><br><span class="line"> </span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">protected</span> Properties <span class="title">mergeProperties</span><span class="params">()</span> <span class="keyword">throws</span> IOException </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> loadPropFromZK(<span class="keyword">super</span>.mergeProperties());</span><br><span class="line">    &#125;</span><br><span class="line"> </span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 从zk中加载配置的常量</span></span><br><span class="line"><span class="comment">     * </span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> result</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span></span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="function"><span class="keyword">private</span> Properties <span class="title">loadPropFromZK</span><span class="params">(Properties result)</span> </span>&#123;</span><br><span class="line">        zkwatcher.watcherKeys();</span><br><span class="line">        zkwatcher.fillProperties(result);</span><br><span class="line">        <span class="keyword">return</span> result;</span><br><span class="line">    &#125;</span><br><span class="line">    ......</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>通过以上的处理，可以使用如下简单的配置来达到目标：</p><figure class="highlight xml"><table><tr><td class="code"><pre><span class="line"><span class="meta">&lt;?xml version="1.0" encoding="UTF-8"?&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">beans</span> <span class="attr">xmlns</span>=<span class="string">"http://www.springframework.org/schema/beans"</span></span></span><br><span class="line"><span class="tag">    <span class="attr">xmlns:xsi</span>=<span class="string">"http://www.w3.org/2001/XMLSchema-instance"</span> <span class="attr">xmlns:aop</span>=<span class="string">"http://www.springframework.org/schema/aop"</span></span></span><br><span class="line"><span class="tag">    <span class="attr">xmlns:tx</span>=<span class="string">"http://www.springframework.org/schema/tx"</span></span></span><br><span class="line"><span class="tag">    <span class="attr">xsi:schemaLocation</span>=<span class="string">"http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd</span></span></span><br><span class="line"><span class="tag"><span class="string">        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd</span></span></span><br><span class="line"><span class="tag"><span class="string">        http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd"</span>&gt;</span></span><br><span class="line"> </span><br><span class="line">    <span class="tag">&lt;<span class="name">bean</span> <span class="attr">id</span>=<span class="string">"zkwatcher"</span> <span class="attr">class</span>=<span class="string">"zh.maven.DynamicConf.ZKWatcher"</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">"keyPatterns"</span> <span class="attr">value</span>=<span class="string">"/a2,/a3/m1"</span> /&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">bean</span>&gt;</span></span><br><span class="line"> </span><br><span class="line">    <span class="tag">&lt;<span class="name">bean</span> <span class="attr">id</span>=<span class="string">"propertyConfigurer"</span> <span class="attr">class</span>=<span class="string">"zh.maven.DynamicConf.ZKPropPlaceholderConfigurer"</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">"zkwatcher"</span> <span class="attr">ref</span>=<span class="string">"zkwatcher"</span>&gt;</span><span class="tag">&lt;/<span class="name">property</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">bean</span>&gt;</span></span><br><span class="line"> </span><br><span class="line">    <span class="tag">&lt;<span class="name">bean</span> <span class="attr">id</span>=<span class="string">"person"</span> <span class="attr">class</span>=<span class="string">"zh.maven.DynamicConf.Person"</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">"name"</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">value</span>&gt;</span>$&#123;/a2/m1&#125;<span class="tag">&lt;/<span class="name">value</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">property</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">"address"</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">value</span>&gt;</span>$&#123;/a3/m1/v2&#125;<span class="tag">&lt;/<span class="name">value</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">property</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">"company"</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">value</span>&gt;</span>$&#123;/a3/m1/v2/t2&#125;<span class="tag">&lt;/<span class="name">value</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">property</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">bean</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">beans</span>&gt;</span></span><br></pre></td></tr></table></figure><h2 id="测试"><a href="#测试" class="headerlink" title="测试"></a>测试</h2><p>1.首先启动Zookeeper</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">zkServer.cmd</span><br></pre></td></tr></table></figure><p>2.初始化需要使用的节点</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">Create_Node</span> </span>&#123;</span><br><span class="line"> </span><br><span class="line">    <span class="keyword">static</span> String path = <span class="string">"/a3/m1/v2/t2"</span>;</span><br><span class="line">    <span class="keyword">static</span> CuratorFramework client = CuratorFrameworkFactory.builder()</span><br><span class="line">            .connectString(<span class="string">"127.0.0.1:2181"</span>).sessionTimeoutMs(<span class="number">5000</span>)</span><br><span class="line">            .retryPolicy(<span class="keyword">new</span> ExponentialBackoffRetry(<span class="number">1000</span>, <span class="number">3</span>)).build();</span><br><span class="line"> </span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">        client.start();</span><br><span class="line">        client.create().creatingParentsIfNeeded()</span><br><span class="line">                .withMode(CreateMode.PERSISTENT)</span><br><span class="line">                .forPath(path, <span class="string">"init"</span>.getBytes());</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>创建需要的节点方便ZKWatcher来监听，这里根据以上的配置，分别初始化/a3/m1/v2/t2和/a2/m1/v1/t1</p><p>3.启动Main，分别验证配置文件中的初始化以及代码动态获取参数</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">Main</span> </span>&#123;</span><br><span class="line"> </span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">        ApplicationContext context = <span class="keyword">new</span> ClassPathXmlApplicationContext(<span class="keyword">new</span> String[] &#123; <span class="string">"spring-config.xml"</span> &#125;);</span><br><span class="line">        Person person = (Person) context.getBean(<span class="string">"person"</span>);</span><br><span class="line">        System.out.println(person.toString());</span><br><span class="line"> </span><br><span class="line">        ZKWatcher zkwatcher = (ZKWatcher) context.getBean(<span class="string">"zkwatcher"</span>);</span><br><span class="line">        <span class="keyword">while</span> (<span class="keyword">true</span>) &#123;</span><br><span class="line">            Person p = <span class="keyword">new</span> Person(zkwatcher.getKeyValue(<span class="string">"/a2/m1"</span>), zkwatcher.getKeyValue(<span class="string">"/a3/m1/v2"</span>),</span><br><span class="line">                    zkwatcher.getKeyValue(<span class="string">"/a3/m1/v2/t2"</span>));</span><br><span class="line">            System.out.println(p.toString());</span><br><span class="line"> </span><br><span class="line">            Thread.sleep(<span class="number">1000</span>);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>4.观察日志同时更新参数：</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">Set_Data</span> </span>&#123;</span><br><span class="line"> </span><br><span class="line">    <span class="keyword">static</span> String path = <span class="string">"/a3/m1/v2/t2"</span>;</span><br><span class="line">    <span class="keyword">static</span> CuratorFramework client = CuratorFrameworkFactory.builder().connectString(<span class="string">"127.0.0.1:2181"</span>)</span><br><span class="line">            .sessionTimeoutMs(<span class="number">5000</span>).retryPolicy(<span class="keyword">new</span> ExponentialBackoffRetry(<span class="number">1000</span>, <span class="number">3</span>)).build();</span><br><span class="line"> </span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">        client.start();</span><br><span class="line">        Stat stat = <span class="keyword">new</span> Stat();</span><br><span class="line">        System.out.println(stat.getVersion());</span><br><span class="line">        System.out.println(<span class="string">"Success set node for :"</span> + path + <span class="string">",new version:"</span></span><br><span class="line">                + client.setData().forPath(path, <span class="string">"codingo_v2"</span>.getBytes()).getVersion());</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>部分日志如下：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">2017-08-05 18:04:57 - watcher path : [/a2, /a2/m1, /a2/m1/v1, /a2/m1/v1/t2, /a3/m1, /a3/m1/v2, /a3/m1/v2/t2]</span><br><span class="line">2017-08-05 18:04:57 - readPath:path = /a2,value = </span><br><span class="line">2017-08-05 18:04:57 - readPath:path = /a2/m1,value = zhaohui</span><br><span class="line">2017-08-05 18:04:57 - readPath:path = /a2/m1/v1,value = </span><br><span class="line">2017-08-05 18:04:57 - readPath:path = /a2/m1/v1/t2,value = init</span><br><span class="line">2017-08-05 18:04:57 - readPath:path = /a3/m1,value = </span><br><span class="line">2017-08-05 18:04:57 - readPath:path = /a3/m1/v2,value = nanjing</span><br><span class="line">2017-08-05 18:04:57 - readPath:path = /a3/m1/v2/t2,value = codingo_v10</span><br><span class="line">2017-08-05 18:04:57 - Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@182f4aea: defining beans [zkwatcher,propertyConfigurer,person]; root of factory hierarchy</span><br><span class="line">name = zhaohui,address = nanjing,company = codingo_v10</span><br><span class="line">name = zhaohui,address = nanjing,company = codingo_v10</span><br><span class="line">2017-08-05 18:04:57 - CHILD_ADDED,/a2/m1</span><br><span class="line">2017-08-05 18:04:57 - CHILD_ADDED,/a3/m1/v2</span><br><span class="line">2017-08-05 18:04:57 - CHILD_ADDED,/a2/m1/v1</span><br><span class="line">2017-08-05 18:04:57 - CHILD_ADDED,/a2/m1/v1/t2</span><br><span class="line">2017-08-05 18:04:57 - CHILD_ADDED,/a3/m1/v2/t2</span><br><span class="line">name = zhaohui,address = nanjing,company = codingo_v10</span><br><span class="line">name = zhaohui,address = nanjing,company = codingo_v10</span><br><span class="line">name = zhaohui,address = nanjing,company = codingo_v10</span><br><span class="line">2017-08-05 18:05:04 - CHILD_UPDATED,/a3/m1/v2/t2</span><br><span class="line">name = zhaohui,address = nanjing,company = codingo_v11</span><br><span class="line">name = zhaohui,address = nanjing,company = codingo_v11</span><br></pre></td></tr></table></figure><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>通过Zookeeper实现了一个简单的参数化平台，当然想在生产中使用还有很多需要优化的地方，本文在于提供一个思路；当然除了Zookeeper还可以使用MQ，分布式缓存等来实现参数化平台。</p>]]></content>
    
    <summary type="html">
    
      Zookeeper实现参数的集中式管理
    
    </summary>
    
      <category term="Zookeeper" scheme="https://ningyu1.github.io/categories/Zookeeper/"/>
    
      <category term="技术" scheme="https://ningyu1.github.io/categories/Zookeeper/%E6%8A%80%E6%9C%AF/"/>
    
    
      <category term="UCM" scheme="https://ningyu1.github.io/tags/UCM/"/>
    
      <category term="Zookeeper" scheme="https://ningyu1.github.io/tags/Zookeeper/"/>
    
  </entry>
  
  <entry>
    <title>[转]Redis实现参数的集中式管理</title>
    <link href="https://ningyu1.github.io/20170905/17-redis-ucm.html"/>
    <id>https://ningyu1.github.io/20170905/17-redis-ucm.html</id>
    <published>2017-09-05T08:40:36.000Z</published>
    <updated>2024-04-23T01:48:15.031Z</updated>
    
    <content type="html"><![CDATA[<h1 id="点评"><a href="#点评" class="headerlink" title="点评"></a>点评</h1><p>虽然现在开源的UCM套件很多，UCM统一配置管理（百度的disconf、阿里的diamond、点评的lion，等很多开源的）。但是很多人是知其然不知其所以然，刚好发现下面这篇文章可以作为原理的教程文章，使用JMS、Redis、Zookeeper简单的实现UCM基本功能，作为学习交流还是很不错的。</p><p>文章转自：<a href="https://my.oschina.net/OutOfMemory/blog/1526063" target="_blank" rel="noopener">https://my.oschina.net/OutOfMemory/blog/1526063</a><br>作者：@ksfzhaohui</p><h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>利用的Redis的发布订阅功能实现对参数的集中式管理；分布式缓存Redis提供了类似的发布订阅功能，并且Redis本身提供了缓存和持久化的功能，本文将介绍通过Redis实现简单的参数集中式管理。</p><h2 id="Maven引入"><a href="#Maven引入" class="headerlink" title="Maven引入"></a>Maven引入</h2><p>Spring相关的jar引入参考上一篇文章</p><figure class="highlight xml"><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>redis.clients<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>jedis<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">version</span>&gt;</span>2.4.0<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure><h2 id="目标"><a href="#目标" class="headerlink" title="目标"></a>目标</h2><ol><li>可以同时配置监听多个节点如/app1,/app2；</li><li>希望只需要配置如/app1，就能够监听其子节点如/app1/modual1以及子节点的子节点如/app1/modual1/xxx/…；</li><li>服务器启动能获取当前指定父节点下的所有子节点数据；</li><li>在添加节点或者在更新节点数据的时候能够动态通知，这样代码中就能够实时获取最新的数据；</li><li>spring配置中可以从Zookeeper中读取参数进行初始化。</li></ol><p>虽然在实现的方式上有点区别，但是最终达成的目标是一致的，同样列出了这5条目标</p><h2 id="实现"><a href="#实现" class="headerlink" title="实现"></a>实现</h2><p>RedisWatcher主要用来和Redis进行连接，然后对监听的节点进行初始化，模糊订阅需要监听的节点，最后接受数据的变更，更新本地数据，存储数据等。</p><p>1.同时配置监听多个节点<br>提供一个字符串数组给用户用来添加需要监听的节点：</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">private</span> String[] keyPatterns;</span><br></pre></td></tr></table></figure><p>2.能够监听其子节点以及子节点的子节点<br>使用Redis提供的psubscribe命令，订阅一个或多个符合给定模式的频道，提供了模糊订阅的功能</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">watcherPaths</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    <span class="keyword">new</span> Thread(<span class="keyword">new</span> Runnable() &#123;</span><br><span class="line"> </span><br><span class="line">        <span class="meta">@Override</span></span><br><span class="line">        <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">run</span><span class="params">()</span> </span>&#123;</span><br><span class="line">            jedis.psubscribe(<span class="keyword">new</span> JedisPubSub() &#123;</span><br><span class="line"> </span><br><span class="line">                <span class="meta">@Override</span></span><br><span class="line">                <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">onMessage</span><span class="params">(String channel, String message)</span> </span>&#123;</span><br><span class="line">                    <span class="keyword">try</span> &#123;</span><br><span class="line">                        keyValueMap.put(channel, message);</span><br><span class="line">                        LOGGER.info(<span class="string">"key = "</span> + channel + <span class="string">",value = "</span> + message);</span><br><span class="line">                    &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">                        LOGGER.error(<span class="string">"onMessage error"</span>, e);</span><br><span class="line">                    &#125;</span><br><span class="line">                &#125;</span><br><span class="line"> </span><br><span class="line">                <span class="meta">@Override</span></span><br><span class="line">                <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">onPMessage</span><span class="params">(String arg0, String arg1, String arg2)</span> </span>&#123;</span><br><span class="line">                    System.out.println(<span class="string">"onPMessage=&gt;"</span> + arg0 + <span class="string">"="</span> + arg1 + <span class="string">"="</span></span><br><span class="line">                            + arg2);</span><br><span class="line">                &#125;</span><br><span class="line"> </span><br><span class="line">                <span class="meta">@Override</span></span><br><span class="line">                <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">onPSubscribe</span><span class="params">(String pattern, <span class="keyword">int</span> subscribedChannels)</span> </span>&#123;</span><br><span class="line">                    LOGGER.info(<span class="string">"onPSubscribe=&gt;"</span> + pattern + <span class="string">"="</span> + subscribedChannels);</span><br><span class="line">                &#125;</span><br><span class="line"> </span><br><span class="line">                <span class="meta">@Override</span></span><br><span class="line">                <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">onPUnsubscribe</span><span class="params">(String arg0, <span class="keyword">int</span> arg1)</span> </span>&#123;</span><br><span class="line">                &#125;</span><br><span class="line"> </span><br><span class="line">                <span class="meta">@Override</span></span><br><span class="line">                <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">onSubscribe</span><span class="params">(String arg0, <span class="keyword">int</span> arg1)</span> </span>&#123;</span><br><span class="line">                &#125;</span><br><span class="line"> </span><br><span class="line">                <span class="meta">@Override</span></span><br><span class="line">                <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">onUnsubscribe</span><span class="params">(String arg0, <span class="keyword">int</span> arg1)</span> </span>&#123;</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;, getSubKeyPatterns());</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;).start();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>3.服务器启动初始化节点数据<br>通过使用keys命令来获取匹配节点的数据（keys命令可能引发性能问题，根据实际情况使用）</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">initKeyValues</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    <span class="keyword">for</span> (String keyPattern : keyPatterns) &#123;</span><br><span class="line">        Set&lt;String&gt; keys = jedis.keys(keyPattern + <span class="string">"*"</span>);</span><br><span class="line">        <span class="keyword">for</span> (String key : keys) &#123;</span><br><span class="line">            String value = jedis.get(key);</span><br><span class="line">            keyValueMap.put(key, value);</span><br><span class="line">            LOGGER.info(<span class="string">"init key = "</span> + key + <span class="string">",value = "</span> + value);</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>4.监听节点数据的变更<br>目标2中通过psubscribe命令，使用模糊订阅来监听数据的变更，onMessage用来接受变更的数据</p><p>5.spring配置中可以从Redis中读取参数进行初始化</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">RedisPropPlaceholderConfigurer</span> <span class="keyword">extends</span> <span class="title">PropertyPlaceholderConfigurer</span> </span>&#123;</span><br><span class="line"> </span><br><span class="line">    <span class="keyword">private</span> RedisWatcher rediswatcher;</span><br><span class="line"> </span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">protected</span> Properties <span class="title">mergeProperties</span><span class="params">()</span> <span class="keyword">throws</span> IOException </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> loadPropFromRedis(<span class="keyword">super</span>.mergeProperties());</span><br><span class="line">    &#125;</span><br><span class="line"> </span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 从Redis中加载配置的常量</span></span><br><span class="line"><span class="comment">     * </span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> result</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span></span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="function"><span class="keyword">private</span> Properties <span class="title">loadPropFromRedis</span><span class="params">(Properties result)</span> </span>&#123;</span><br><span class="line">        rediswatcher.watcherKeys();</span><br><span class="line">        rediswatcher.fillProperties(result);</span><br><span class="line">        <span class="keyword">return</span> result;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>通过以上的处理，可以使用如下简单的配置来达到目标：</p><figure class="highlight xml"><table><tr><td class="code"><pre><span class="line"><span class="meta">&lt;?xml version="1.0" encoding="UTF-8"?&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">beans</span> <span class="attr">xmlns</span>=<span class="string">"http://www.springframework.org/schema/beans"</span></span></span><br><span class="line"><span class="tag">    <span class="attr">xmlns:xsi</span>=<span class="string">"http://www.w3.org/2001/XMLSchema-instance"</span> <span class="attr">xmlns:aop</span>=<span class="string">"http://www.springframework.org/schema/aop"</span></span></span><br><span class="line"><span class="tag">    <span class="attr">xmlns:tx</span>=<span class="string">"http://www.springframework.org/schema/tx"</span></span></span><br><span class="line"><span class="tag">    <span class="attr">xsi:schemaLocation</span>=<span class="string">"http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd</span></span></span><br><span class="line"><span class="tag"><span class="string">        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd</span></span></span><br><span class="line"><span class="tag"><span class="string">        http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd"</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">bean</span> <span class="attr">id</span>=<span class="string">"rediswatcher"</span> <span class="attr">class</span>=<span class="string">"zh.maven.DynamicConf.redis.RedisWatcher"</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">"keyPatterns"</span> <span class="attr">value</span>=<span class="string">"/a2,/a3"</span> /&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">bean</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">bean</span> <span class="attr">id</span>=<span class="string">"propertyConfigurer"</span> <span class="attr">class</span>=<span class="string">"zh.maven.DynamicConf.redis.RedisPropPlaceholderConfigurer"</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">"rediswatcher"</span> <span class="attr">ref</span>=<span class="string">"rediswatcher"</span>&gt;</span><span class="tag">&lt;/<span class="name">property</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">bean</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">bean</span> <span class="attr">id</span>=<span class="string">"person"</span> <span class="attr">class</span>=<span class="string">"zh.maven.DynamicConf.Person"</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">"name"</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">value</span>&gt;</span>$&#123;/a2/m1&#125;<span class="tag">&lt;/<span class="name">value</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">property</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">"address"</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">value</span>&gt;</span>$&#123;/a3/m1/v2&#125;<span class="tag">&lt;/<span class="name">value</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">property</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">"company"</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">value</span>&gt;</span>$&#123;/a3/m1/v2/t2&#125;<span class="tag">&lt;/<span class="name">value</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">property</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">bean</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">beans</span>&gt;</span></span><br></pre></td></tr></table></figure><h2 id="测试"><a href="#测试" class="headerlink" title="测试"></a>测试</h2><p>1.启动Redis服务器</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">redis-server.exe</span><br></pre></td></tr></table></figure><p>2.启动Redis客户端进行初始化数据</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">redis-cli.exe</span><br><span class="line">redis 127.0.0.1:6379&gt; set /a2/m1 zhaohui</span><br><span class="line">OK</span><br><span class="line">redis 127.0.0.1:6379&gt; set /a3/m1/v2 nanjing</span><br><span class="line">OK</span><br><span class="line">redis 127.0.0.1:6379&gt; set /a3/m1/v2/t2 codingo</span><br><span class="line">OK</span><br></pre></td></tr></table></figure><p>3.启动Main类</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">Main</span> </span>&#123;</span><br><span class="line">  </span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">        ApplicationContext context = <span class="keyword">new</span> ClassPathXmlApplicationContext(<span class="keyword">new</span> String[] &#123; <span class="string">"spring-config.xml"</span> &#125;);</span><br><span class="line">        Person person = (Person) context.getBean(<span class="string">"person"</span>);</span><br><span class="line">        System.out.println(person.toString());</span><br><span class="line">        &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>4.启动RedisPublish<br>定时发布数据，同时查看集群节点的Main类日志输出</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">RedisPublish</span> </span>&#123;</span><br><span class="line"> </span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> </span>&#123;</span><br><span class="line">        Jedis jedis = <span class="keyword">new</span> Jedis(<span class="string">"127.0.0.1"</span>, <span class="number">6379</span>);</span><br><span class="line">        <span class="keyword">int</span> i = <span class="number">0</span>;</span><br><span class="line">        <span class="keyword">while</span> (<span class="keyword">true</span>) &#123;</span><br><span class="line">            jedis.publish(<span class="string">"/a2/b4/c1"</span> + i, <span class="string">"message_"</span> + System.currentTimeMillis());</span><br><span class="line">            <span class="keyword">try</span> &#123;</span><br><span class="line">                Thread.sleep(<span class="number">2000</span>);</span><br><span class="line">            &#125; <span class="keyword">catch</span> (InterruptedException e) &#123;</span><br><span class="line">                e.printStackTrace();</span><br><span class="line">            &#125;</span><br><span class="line">            i++;</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 plain"><table><tr><td class="code"><pre><span class="line">2017-08-30 10:44:00 - init key = /a2/m1,value = zhaohui</span><br><span class="line">2017-08-30 10:44:00 - init key = /a3/m1/v2,value = nanjing</span><br><span class="line">2017-08-30 10:44:00 - init key = /a3/m1/v2/t2,value = codingo</span><br><span class="line">2017-08-30 10:44:00 - onPSubscribe=&gt;/a2*=1</span><br><span class="line">2017-08-30 10:44:00 - onPSubscribe=&gt;/a3*=2</span><br><span class="line">2017-08-30 10:44:00 - Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@4bad4a49: defining beans [rediswatcher,propertyConfigurer,person]; root of factory hierarchy</span><br><span class="line">name = zhaohui,address = nanjing,company = codingo</span><br><span class="line">onPMessage=&gt;/a2*=/a2/b4/c10=message_1504061045414</span><br><span class="line">onPMessage=&gt;/a2*=/a2/b4/c11=message_1504061047458</span><br><span class="line">onPMessage=&gt;/a2*=/a2/b4/c12=message_1504061049458</span><br><span class="line">onPMessage=&gt;/a2*=/a2/b4/c13=message_1504061051458</span><br></pre></td></tr></table></figure><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>关于参数的集中式管理一共写了三篇文章，分别利用Zookeeper，MQ以及Redis来实现了一个简单的参数的集中式管理，但更多的只是提供了一个思路<br>离生产还有很大距离，本片文章也是这个系列的最后一篇，综合来看Zookeeper更加适合做参数的集中式管理平台，MQ方式本身没有提供存储的功能<br>只能作为一个中间层存在；而Redis方式虽然提供了持久化功能，但是会因为选择不同的持久化方式会出现丢数据的可能，还有就是本身的集群方式<br>并不是很完善；虽然Zookeeper本身并不是一个存储系统，但是紧紧用来存储少量的参数应该足够了。</p>]]></content>
    
    <summary type="html">
    
      [转]Redis实现参数的集中式管理
    
    </summary>
    
      <category term="Redis" scheme="https://ningyu1.github.io/categories/Redis/"/>
    
      <category term="技术" scheme="https://ningyu1.github.io/categories/Redis/%E6%8A%80%E6%9C%AF/"/>
    
    
      <category term="Redis" scheme="https://ningyu1.github.io/tags/Redis/"/>
    
      <category term="UCM" scheme="https://ningyu1.github.io/tags/UCM/"/>
    
  </entry>
  
  <entry>
    <title>Spring框架-事务管理注意事项</title>
    <link href="https://ningyu1.github.io/20170826/16-spring-transaction.html"/>
    <id>https://ningyu1.github.io/20170826/16-spring-transaction.html</id>
    <published>2017-08-26T08:40:36.000Z</published>
    <updated>2024-04-23T01:48:15.031Z</updated>
    
    <content type="html"><![CDATA[<h2 id="常见事务问题"><a href="#常见事务问题" class="headerlink" title="常见事务问题"></a>常见事务问题</h2><ol><li>事务不起作用<ul><li>可能是配置不起效，如扫描问题</li></ul></li><li>事务自动提交了（批量操作中）<ul><li>可能是在没事务的情况下，利用了数据库的隐式提交</li></ul></li></ol><h2 id="事务配置说明"><a href="#事务配置说明" class="headerlink" title="事务配置说明"></a>事务配置说明</h2><p>通常情况下我们的<code>Spring Component</code>扫描分为两部分，一部分是<code>Spring Servlet(MVC)</code>，一部分是其他<code>Context Config</code>的内容。主要扫描<code>Annotation</code>定义，包括<code>@Controller</code>、<code>@Autowired</code>、<code>@Resource</code>、<code>@Service</code>、<code>@Component</code>、<code>@Repository</code>等。</p><p><code>Spring Servlet</code>部分的扫描配置可以通过<code>web.xml</code>中<code>DispatchServlet</code>的<code>init-param</code>节点配置确定。</p><p><code>Context Config</code>部分的扫描配置为非以上配置的其他<code>Spring</code>配置文件确定。</p><p>为了能够使用事务，需要防止因<code>Spring Servlet</code>的扫描导致<code>@Service</code>事务配置失效。可以调整<code>DispatchServlet</code>中的配置文件，排除对<code>@Service</code>的扫描。</p><p>配置如下：<br><figure class="highlight xml"><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">context:component-scan</span> <span class="attr">base-package</span>=<span class="string">"com.jiuyescm.xxx"</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">context:exclude-filter</span> <span class="attr">type</span>=<span class="string">"annotation"</span> <span class="attr">expression</span>=<span class="string">"org.springframework.stereotype.Service"</span> /&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">context:component-scan</span>&gt;</span></span><br></pre></td></tr></table></figure></p><h2 id="如何通过日志判断事务是否已经被Spring所管理？"><a href="#如何通过日志判断事务是否已经被Spring所管理？" class="headerlink" title="如何通过日志判断事务是否已经被Spring所管理？"></a>如何通过日志判断事务是否已经被Spring所管理？</h2><ol><li>在logback或者log4j中对org.springframework.aop、org.springframework.transaction、org.springframework.jdbc、org.mybatis.spring.transaction进行DEBUG级别日志跟踪（开发期）</li><li><p>查看日志中是否有事务管理、开启、提交、回滚等字符，如：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">DEBUG o.m.spring.transaction.SpringManagedTransaction - JDBC Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@28cfe912] will be managed by Spring</span><br></pre></td></tr></table></figure></li><li><p>没有被控制的时候，日志如下：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">DEBUG o.m.spring.transaction.SpringManagedTransaction - JDBC Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@28cfe912] will not be managed by Spring</span><br></pre></td></tr></table></figure></li></ol><h2 id="如何通过程序判断是否存在事务？"><a href="#如何通过程序判断是否存在事务？" class="headerlink" title="如何通过程序判断是否存在事务？"></a>如何通过程序判断是否存在事务？</h2><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">boolean</span> flag = TransactionSynchronizationManager.isActualTransactionActive();</span><br></pre></td></tr></table></figure><p>返回true，则在事务控制下，否则不在控制下</p><h2 id="什么时候做了隐式提交？"><a href="#什么时候做了隐式提交？" class="headerlink" title="什么时候做了隐式提交？"></a>什么时候做了隐式提交？</h2><p>在没有容器事务的情况下，系统会尝试隐时提交。</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/spring-transaction/1.png" alt="spring1" title>                </div>                <div class="image-caption">spring1</div>            </figure><h2 id="开发建议："><a href="#开发建议：" class="headerlink" title="开发建议："></a>开发建议：</h2><ol><li>所有Service代码中设置Class级别的@Transactional，并设置为只读，开发时可以很容易发现误数据库操作的动作。如：@Transactional(readOnly=true)。</li><li>所有Service代码中Public的方法设置@Transactional，并根据实际情况设置Propagation，可以设置为REQUIRED。</li><li>对于有异常产生可能的情况下，根据情况选择合适的rollbackFor，默认情况下可以设置对Exception.class或BizException.class进行控制。</li><li>尽可能减少嵌套的使用方法（Service call Service），采用传统的Controller-》Service-》Repository(DAO)的模型。</li></ol><p>如果需要深入了解Transaction的流程，请自行翻阅和跟踪Spring和Mybatis相关代码。</p><p>以下是嵌套事务的各种情况下的执行结果（前提数据库的AutoCommit为true）</p><table><thead><tr><th style="text-align:left">编号</th><th style="text-align:left">External（Service）</th><th style="text-align:left">Internal（Service）</th><th style="text-align:left">Result</th><th style="text-align:left">Memo</th></tr></thead><tbody><tr><td style="text-align:left">1</td><td style="text-align:left">No Transactional</td><td style="text-align:left">No Transactional</td><td style="text-align:left">All Committed</td><td style="text-align:left">Auto Commit = True</td></tr><tr><td style="text-align:left">2</td><td style="text-align:left">No Transactional</td><td style="text-align:left">Class Level ReadOnly Transactional</td><td style="text-align:left">External Committed Internal TransientDataAccessResourceException</td><td style="text-align:left">Can’t update table</td></tr><tr><td style="text-align:left">3</td><td style="text-align:left">No Transactional</td><td style="text-align:left">Transactional(REQUIRED)</td><td style="text-align:left">All Committed</td><td style="text-align:left"></td></tr><tr><td style="text-align:left">4</td><td style="text-align:left">No Transactional</td><td style="text-align:left">Transactional(REQUIRES_NEW)</td><td style="text-align:left">All Committed</td><td style="text-align:left"></td></tr><tr><td style="text-align:left">5</td><td style="text-align:left">No Transactional</td><td style="text-align:left">Transactional(SUPPORTS)</td><td style="text-align:left">All Committed</td><td style="text-align:left"></td></tr><tr><td style="text-align:left">6</td><td style="text-align:left">No Transactional</td><td style="text-align:left">Transactional(MANDATORY)</td><td style="text-align:left">External Committed Internal IllegalTransactionStateException</td><td style="text-align:left">Must under transaction</td></tr><tr><td style="text-align:left">7</td><td style="text-align:left">No Transactional</td><td style="text-align:left">Transactional(NOT_SUPPORTED)</td><td style="text-align:left">All Committed</td><td style="text-align:left"></td></tr><tr><td style="text-align:left">8</td><td style="text-align:left">No Transactional</td><td style="text-align:left">Transactional(NEVER)</td><td style="text-align:left">All Committed</td><td style="text-align:left"></td></tr><tr><td style="text-align:left">9</td><td style="text-align:left">No Transactional</td><td style="text-align:left">Transactional(NESTED)</td><td style="text-align:left">All Committed</td><td style="text-align:left"></td></tr><tr><td style="text-align:left">10</td><td style="text-align:left">No Transactional</td><td style="text-align:left">Transactional(REQUIRED) rollackFor=Exception.class IOException</td><td style="text-align:left">External Committed Internal Rollbacked</td><td style="text-align:left"></td></tr><tr><td style="text-align:left">11</td><td style="text-align:left">No Transactional</td><td style="text-align:left">Transactional(REQUIRED) rollbackFor=RuntimeException.class IOException</td><td style="text-align:left">All Committed</td><td style="text-align:left"></td></tr><tr><td style="text-align:left">12</td><td style="text-align:left">No Transactional</td><td style="text-align:left">Transactional(REQUIRED)</td><td style="text-align:left">rollbackFor=Exception.class RuntimeException</td><td style="text-align:left">External Committed Internal Rollbacked</td></tr><tr><td style="text-align:left">13</td><td style="text-align:left">No Transactional</td><td style="text-align:left">Transactional(REQUIRED)</td><td style="text-align:left">rollbackFor=RuntimeException.class RuntimeException</td><td style="text-align:left">External Committed Internal Rollbacked</td></tr><tr><td style="text-align:left">14</td><td style="text-align:left">Class Level ReadOnly Transactional</td><td style="text-align:left">No Transactional</td><td style="text-align:left">External TransientDataAccessResourceException</td><td style="text-align:left">Can’t update table</td></tr><tr><td style="text-align:left">15</td><td style="text-align:left">Class Level ReadOnly Transactional</td><td style="text-align:left">Class Level ReadOnly Transactional</td><td style="text-align:left">External TransientDataAccessResourceException</td><td style="text-align:left">Can’t update table</td></tr><tr><td style="text-align:left">16</td><td style="text-align:left">Transactional(REQUIRED)</td><td style="text-align:left">No Transactional</td><td style="text-align:left">All Committed</td><td style="text-align:left"></td></tr><tr><td style="text-align:left">17</td><td style="text-align:left">Transactional(REQUIRES_NEW)</td><td style="text-align:left">No Transactional</td><td style="text-align:left">All Committed</td><td style="text-align:left"></td></tr><tr><td style="text-align:left">18</td><td style="text-align:left">Transactional(SUPPORTS)</td><td style="text-align:left">No Transactional</td><td style="text-align:left">All Committed</td><td style="text-align:left"></td></tr><tr><td style="text-align:left">19</td><td style="text-align:left">Transactional(MANDATORY)</td><td style="text-align:left">No Transactional</td><td style="text-align:left">External IllegalTransactionStateException</td><td style="text-align:left">Must under transaction</td></tr><tr><td style="text-align:left">20</td><td style="text-align:left">Transactional(NOT_SUPPORTED)</td><td style="text-align:left">No Transactional</td><td style="text-align:left">All Committed</td><td style="text-align:left"></td></tr><tr><td style="text-align:left">21</td><td style="text-align:left">Transactional(NEVER)</td><td style="text-align:left">No Transactional</td><td style="text-align:left">All Committed</td><td style="text-align:left"></td></tr><tr><td style="text-align:left">22</td><td style="text-align:left">Transactional(NESTED)</td><td style="text-align:left">No Transactional</td><td style="text-align:left">All Committed</td><td style="text-align:left"></td></tr><tr><td style="text-align:left">23</td><td style="text-align:left">Transactional(REQUIRED)</td><td style="text-align:left">Transactional(REQUIRED)</td><td style="text-align:left">All Committed</td><td style="text-align:left"></td></tr><tr><td style="text-align:left">24</td><td style="text-align:left">Transactional(REQUIRED)</td><td style="text-align:left">Transactional(REQUIRES_NEW)</td><td style="text-align:left">All Committed</td><td style="text-align:left"></td></tr><tr><td style="text-align:left">25</td><td style="text-align:left">Transactional(REQUIRED)</td><td style="text-align:left">Transactional(SUPPORTS)</td><td style="text-align:left">All Committed</td><td style="text-align:left"></td></tr><tr><td style="text-align:left">26</td><td style="text-align:left">Transactional(REQUIRED)</td><td style="text-align:left">Transactional(MANDATORY)</td><td style="text-align:left">All Committed</td><td style="text-align:left"></td></tr><tr><td style="text-align:left">27</td><td style="text-align:left">Transactional(REQUIRED)</td><td style="text-align:left">Transactional(NOT_SUPPORTED)</td><td style="text-align:left">All Committed</td><td style="text-align:left"></td></tr><tr><td style="text-align:left">28</td><td style="text-align:left">Transactional(REQUIRED)</td><td style="text-align:left">Transactional(NEVER)</td><td style="text-align:left">External Rollbacked Internal IllegalTransactionStateException</td><td style="text-align:left">Must under transaction</td></tr><tr><td style="text-align:left">29</td><td style="text-align:left">Transactional(REQUIRED)</td><td style="text-align:left">Transactional(NESTED)</td><td style="text-align:left">All Committed</td><td style="text-align:left"></td></tr><tr><td style="text-align:left">30</td><td style="text-align:left">Transactional(REQUIRED)</td><td style="text-align:left">rollackFor=Exception.class Transactional(REQUIRED) rollackFor=Exception.class IOException</td><td style="text-align:left">All Rollbacked</td><td style="text-align:left"></td></tr><tr><td style="text-align:left">31</td><td style="text-align:left">Transactional(REQUIRED)</td><td style="text-align:left">rollackFor=Exception.class Transactional(REQUIRED) rollbackFor=RuntimeException.class IOException</td><td style="text-align:left">All Rollbacked</td><td style="text-align:left"></td></tr><tr><td style="text-align:left">32</td><td style="text-align:left">Transactional(REQUIRED)</td><td style="text-align:left">rollackFor=RuntimeException.class Transactional(REQUIRED) rollackFor=Exception.class IOException</td><td style="text-align:left">All Rollbacked UnexpectedRollbackException</td><td style="text-align:left"></td></tr><tr><td style="text-align:left">33</td><td style="text-align:left">Transactional(REQUIRED)</td><td style="text-align:left">rollackFor=RuntimeException.class Transactional(REQUIRED) rollbackFor=RuntimeException.class IOException</td><td style="text-align:left">All Committed</td><td style="text-align:left"></td></tr><tr><td style="text-align:left">34</td><td style="text-align:left">Transactional(REQUIRED)</td><td style="text-align:left">rollackFor=Exception.class Transactional(REQUIRED) rollackFor=Exception.class RuntimeException</td><td style="text-align:left">All Rollbacked</td><td style="text-align:left"></td></tr><tr><td style="text-align:left">35</td><td style="text-align:left">Transactional(REQUIRED)</td><td style="text-align:left">rollackFor=Exception.class Transactional(REQUIRED) rollbackFor=RuntimeException.class RuntimeException</td><td style="text-align:left">All Rollbacked</td><td style="text-align:left"></td></tr><tr><td style="text-align:left">36</td><td style="text-align:left">Transactional(REQUIRED)</td><td style="text-align:left">rollackFor=RuntimeException.class Transactional(REQUIRED) rollackFor=Exception.class RuntimeException</td><td style="text-align:left">All Rollbacked</td><td style="text-align:left"></td></tr><tr><td style="text-align:left">37</td><td style="text-align:left">Transactional(REQUIRED)</td><td style="text-align:left">rollackFor=RuntimeException.class Transactional(REQUIRED) rollbackFor=RuntimeException.class RuntimeException</td><td style="text-align:left">All Rollbacked</td><td style="text-align:left"></td></tr><tr><td style="text-align:left">38</td><td style="text-align:left">Transactional(REQUIRED)</td><td style="text-align:left">rollackFor=Exception.class Transactional(REQUIRED) rollackFor=Exception.class IOException Catch IOException</td><td style="text-align:left">All Committed</td><td style="text-align:left"></td></tr><tr><td style="text-align:left">39</td><td style="text-align:left">Transactional(REQUIRED)</td><td style="text-align:left">rollackFor=Exception.class Catch IOExceptio Transactional(REQUIRED) rollbackFor=Exception.class IOException</td><td style="text-align:left">All Rollbacked UnexpectedRollbackException</td><td style="text-align:left"></td></tr><tr><td style="text-align:left">40</td><td style="text-align:left">Transactional(REQUIRED)</td><td style="text-align:left">rollackFor=Exception.class Catch IOException Transactional(REQUIRED) rollbackFor=RuntimeException.class IOException</td><td style="text-align:left">All Committed</td></tr></tbody></table><p>其他情况按照事务是否开启和是否抛出（捕获）对应异常来判断结果。</p>]]></content>
    
    <summary type="html">
    
      Spring框架-事务管理注意事项
    
    </summary>
    
      <category term="Java" scheme="https://ningyu1.github.io/categories/Java/"/>
    
      <category term="Spring" scheme="https://ningyu1.github.io/categories/Java/Spring/"/>
    
    
      <category term="Spring" scheme="https://ningyu1.github.io/tags/Spring/"/>
    
      <category term="Transaction" scheme="https://ningyu1.github.io/tags/Transaction/"/>
    
  </entry>
  
  <entry>
    <title>NPE（java.lang.NullPointerException）防范</title>
    <link href="https://ningyu1.github.io/20170826/15-java-npe.html"/>
    <id>https://ningyu1.github.io/20170826/15-java-npe.html</id>
    <published>2017-08-26T08:01:36.000Z</published>
    <updated>2024-04-23T01:48:15.031Z</updated>
    
    <content type="html"><![CDATA[<p>我们程序中NPE还是比较多的，下面介绍良好的编码规范防止NPE的发生</p><p>NPE（java.lang.NullPointerException）: 空指针异常</p><h2 id="一、【推荐】防止-NPE，是程序员的基本修养，注意-NPE-产生的场景："><a href="#一、【推荐】防止-NPE，是程序员的基本修养，注意-NPE-产生的场景：" class="headerlink" title="一、【推荐】防止 NPE，是程序员的基本修养，注意 NPE 产生的场景："></a>一、【推荐】防止 NPE，是程序员的基本修养，注意 NPE 产生的场景：</h2><p>1） 返回类型为基本数据类型， return 包装数据类型的对象时，自动拆箱有可能产生 NPE。</p><p>反例： public int f() { return Integer 对象}， 如果为 null，自动解箱抛 NPE。</p><p>2） 数据库的查询结果可能为 null。</p><p>3） 集合里的元素即使 isNotEmpty，取出的数据元素也可能为 null。</p><p>4） 远程调用返回对象时，一律要求进行空指针判断，防止 NPE。</p><p>5） 对于 Session 中获取的数据，建议 NPE 检查，避免空指针。</p><p>6） 级联调用 obj.getA().getB().getC()； 一连串调用，易产生 NPE。</p><p>正例： 使用 JDK8 的 Optional 类来防止 NPE 问题。</p><p>ps.我们现在开发规范jdk版本jdk1.7.0_45，对于jdk8里面的optional可以了解学习，它是一种友好的解决方式。</p><h2 id="二、【强制】当某一列的值全是-NULL-时，-count-col-的返回结果为-0，但-sum-col-的返回结果为"><a href="#二、【强制】当某一列的值全是-NULL-时，-count-col-的返回结果为-0，但-sum-col-的返回结果为" class="headerlink" title="二、【强制】当某一列的值全是 NULL 时， count(col)的返回结果为 0，但 sum(col)的返回结果为"></a>二、【强制】当某一列的值全是 NULL 时， count(col)的返回结果为 0，但 sum(col)的返回结果为</h2><p>NULL，因此使用 sum()时需注意 NPE 问题。</p><p>正例： 可以使用如下方式来避免 sum 的 NPE 问题： SELECT IF(ISNULL(SUM(g)),0,SUM(g))</p><p>FROM table;</p><h2 id="三、【推荐】高度注意-Map-类集合-K-V-能不能存储-null-值的情况，如下表格："><a href="#三、【推荐】高度注意-Map-类集合-K-V-能不能存储-null-值的情况，如下表格：" class="headerlink" title="三、【推荐】高度注意 Map 类集合 K/V 能不能存储 null 值的情况，如下表格："></a>三、【推荐】高度注意 Map 类集合 K/V 能不能存储 null 值的情况，如下表格：</h2><table><thead><tr><th style="text-align:left">集合类</th><th style="text-align:left">Key</th><th style="text-align:left">Value</th><th style="text-align:left">Super</th><th style="text-align:left">说明</th></tr></thead><tbody><tr><td style="text-align:left">Hashtable</td><td style="text-align:left">不允许为null</td><td style="text-align:left">不允许为null</td><td style="text-align:left">Dictionary</td><td style="text-align:left">线程安全</td></tr><tr><td style="text-align:left">ConcurrentHashMap</td><td style="text-align:left">不允许为null</td><td style="text-align:left">不允许为null</td><td style="text-align:left">AbstractMap</td><td style="text-align:left">分段锁技术</td></tr><tr><td style="text-align:left">TreeMap</td><td style="text-align:left">不允许为null</td><td style="text-align:left">允许为null</td><td style="text-align:left">AbstractMap</td><td style="text-align:left">线程不安全</td></tr><tr><td style="text-align:left">HashMap</td><td style="text-align:left">允许为null</td><td style="text-align:left">允许为null</td><td style="text-align:left">AbstractMap</td><td style="text-align:left">线程不安全</td></tr></tbody></table><p>反例： 由于 HashMap 的干扰，很多人认为 ConcurrentHashMap 是可以置入 null 值，而事实上，</p><p>存储 null 值时会抛出 NPE 异常。</p><h2 id="四、【推荐】方法的返回值可以为-null，不强制返回空集合，或者空对象等，必须添加注释充分"><a href="#四、【推荐】方法的返回值可以为-null，不强制返回空集合，或者空对象等，必须添加注释充分" class="headerlink" title="四、【推荐】方法的返回值可以为 null，不强制返回空集合，或者空对象等，必须添加注释充分"></a>四、【推荐】方法的返回值可以为 null，不强制返回空集合，或者空对象等，必须添加注释充分</h2><p>说明什么情况下会返回 null 值。调用方需要进行 null 判断防止 NPE 问题。</p><p>说明： 明确防止 NPE 是调用者的责任。即使被调用方法返回空集合或者空对象，对调用</p><p>者来说，也并非高枕无忧，必须考虑到远程调用失败、 序列化失败、 运行时异常等场景返回</p><p>null 的情况。</p><h2 id="五、关于基本数据类型与包装数据类型的使用标准如下："><a href="#五、关于基本数据类型与包装数据类型的使用标准如下：" class="headerlink" title="五、关于基本数据类型与包装数据类型的使用标准如下："></a>五、关于基本数据类型与包装数据类型的使用标准如下：</h2><p>1） 【强制】 所有的 POJO 类属性必须使用包装数据类型。</p><p>2） 【强制】 RPC 方法的返回值和参数必须使用包装数据类型。</p><p>3） 【 推荐】 所有的局部变量使用基本数据类型。</p><p>说明： POJO 类属性没有初值是提醒使用者在需要使用时，必须自己显式地进行赋值，任何</p><p>NPE 问题，或者入库检查，都由使用者来保证。</p><p>正例： 数据库的查询结果可能是 null，因为自动拆箱，用基本数据类型接收有 NPE 风险。</p><p><span style="color:blue"><strong><em>以上内容摘自阿里巴巴Java开发手册v1.2.0.pdf</em></strong></span></p>]]></content>
    
    <summary type="html">
    
      NPE（java.lang.NullPointerException）防范
    
    </summary>
    
      <category term="Java" scheme="https://ningyu1.github.io/categories/Java/"/>
    
    
      <category term="NPE" scheme="https://ningyu1.github.io/tags/NPE/"/>
    
      <category term="NullPointException" scheme="https://ningyu1.github.io/tags/NullPointException/"/>
    
  </entry>
  
  <entry>
    <title>JVM调优总结 -Xms -Xmx -Xmn -Xss</title>
    <link href="https://ningyu1.github.io/20170826/15-java-jvm.html"/>
    <id>https://ningyu1.github.io/20170826/15-java-jvm.html</id>
    <published>2017-08-26T07:56:36.000Z</published>
    <updated>2024-04-23T01:48:15.031Z</updated>
    
    <content type="html"><![CDATA[<h2 id="堆大小设置"><a href="#堆大小设置" class="headerlink" title="堆大小设置"></a>堆大小设置</h2><p>JVM 中最大堆大小有三方面限制：相关操作系统的数据模型（32-bt还是64-bit）限制；系统的可用虚拟内存限制；系统的可用物理内存限制。32位系统下，一般限制在1.5G~2G；64为操作系统对内存无限制。我在Windows Server 2003 系统，3.5G物理内存，JDK5.0下测试，最大可设置为1478m。</p><h2 id="典型设置"><a href="#典型设置" class="headerlink" title="典型设置"></a>典型设置</h2><ul><li><p>java -Xmx3550m -Xms3550m -Xmn2g -Xss128k<br>-Xmx3550m：设置JVM最大可用内存为3550M。<br>-Xms3550m：设置JVM促使内存为3550m。此值可以设置与-Xmx相同，以避免每次垃圾回收完成后JVM重新分配内存。<br>-Xmn2g：设置年轻代大小为2G。整个JVM内存大小=年轻代大小 + 年老代大小 + 持久代大小。持久代一般固定大小为64m，所以增大年轻代后，将会减小年老代大小。此值对系统性能影响较大，Sun官方推荐配置为整个堆的3/8。<br>-Xss128k：设置每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1M，以前每个线程堆栈大小为256K。更具应用的线程所需内存大小进行调整。在相同物理内存下，减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的，不能无限生成，经验值在3000~5000左右。</p></li><li><p>java -Xmx3550m -Xms3550m -Xss128k -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxPermSize=16m -XX:MaxTenuringThreshold=0<br>-XX:NewRatio=4:设置年轻代（包括Eden和两个Survivor区）与年老代的比值（除去持久代）。设置为4，则年轻代与年老代所占比值为1：4，年轻代占整个堆栈的1/5<br>-XX:SurvivorRatio=4：设置年轻代中Eden区与Survivor区的大小比值。设置为4，则两个Survivor区与一个Eden区的比值为2:4，一个Survivor区占整个年轻代的1/6<br>-XX:MaxPermSize=16m:设置持久代大小为16m。<br>-XX:MaxTenuringThreshold=0：设置垃圾最大年龄。如果设置为0的话，则年轻代对象不经过Survivor区，直接进入年老代。对于年老代比较多的应用，可以提高效率。如果将此值设置为一个较大值，则年轻代对象会在Survivor区进行多次复制，这样可以增加对象再年轻代的存活时间，增加在年轻代即被回收的概论。</p></li></ul><h2 id="回收器选择"><a href="#回收器选择" class="headerlink" title="回收器选择"></a>回收器选择</h2><p>JVM给了三种选择：串行收集器、并行收集器、并发收集器，但是串行收集器只适用于小数据量的情况，所以这里的选择主要针对并行收集器和并发收集器。默认情况下，JDK5.0以前都是使用串行收集器，如果想使用其他收集器需要在启动时加入相应参数。JDK5.0以后，JVM会根据当前<a href="http://java.sun.com/j2se/1.5.0/docs/guide/vm/server-class.html" title="系统配置" target="_blank" rel="noopener">系统配置</a>进行判断。</p><ul><li><p>java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:ParallelGCThreads=20 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC<br>-XX:+UseConcMarkSweepGC：设置年老代为并发收集。测试中配置这个以后，-XX:NewRatio=4的配置失效了，原因不明。所以，此时年轻代大小最好用-Xmn设置。<br>-XX:+UseParNewGC:设置年轻代为并行收集。可与CMS收集同时使用。JDK5.0以上，JVM会根据系统配置自行设置，所以无需再设置此值。</p></li><li><p>java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseConcMarkSweepGC -XX:CMSFullGCsBeforeCompaction=5 -XX:+UseCMSCompactAtFullCollection<br>-XX:CMSFullGCsBeforeCompaction：由于并发收集器不对内存空间进行压缩、整理，所以运行一段时间以后会产生“碎片”，使得运行效率降低。此值设置运行多少次GC以后对内存空间进行压缩、整理。<br>-XX:+UseCMSCompactAtFullCollection：打开对年老代的压缩。可能会影响性能，但是可以消除碎片</p></li><li><p>java -Xmx3800m -Xms3800m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20<br>-XX:+UseParallelGC：选择垃圾收集器为并行收集器。此配置仅对年轻代有效。即上述配置下，年轻代使用并发收集，而年老代仍旧使用串行收集。<br>-XX:ParallelGCThreads=20：配置并行收集器的线程数，即：同时多少个线程一起进行垃圾回收。此值最好配置与处理器数目相等。</p></li><li><p>java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20 -XX:+UseParallelOldGC<br>-XX:+UseParallelOldGC：配置年老代垃圾收集方式为并行收集。JDK6.0支持对年老代并行收集。</p></li><li><p>java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC  -XX:MaxGCPauseMillis=100<br>-XX:MaxGCPauseMillis=100:设置每次年轻代垃圾回收的最长时间，如果无法满足此时间，JVM会自动调整年轻代大小，以满足此值。</p></li><li><p>java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC  -XX:MaxGCPauseMillis=100 -XX:+UseAdaptiveSizePolicy<br>-XX:+UseAdaptiveSizePolicy：设置此选项后，并行收集器会自动选择年轻代区大小和相应的Survivor区比例，以达到目标系统规定的最低相应时间或者收集频率等，此值建议使用并行收集器时，一直打开。</p><ol><li>吞吐量优先的并行收集器<br>如上文所述，并行收集器主要以到达一定的吞吐量为目标，适用于科学技术和后台处理等。</li><li>响应时间优先的并发收集器<br>如上文所述，并发收集器主要是保证系统的响应时间，减少垃圾收集时的停顿时间。适用于应用服务器、电信领域等。</li></ol></li></ul><h2 id="辅助信息"><a href="#辅助信息" class="headerlink" title="辅助信息"></a>辅助信息</h2><p>JVM提供了大量命令行参数，打印信息，供调试使用。主要有以下一些：</p><ul><li><p>-XX:+PrintGC<br>输出形式：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">[GC 118250K-&gt;113543K(130112K), 0.0094143 secs]</span><br><span class="line">[Full GC 121376K-&gt;10414K(130112K), 0.0650971 secs]</span><br></pre></td></tr></table></figure></li><li><p>-XX:+PrintGCDetails<br>输出形式：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">[GC [DefNew: 8614K-&gt;781K(9088K), 0.0123035 secs] 118250K-&gt;113543K(130112K), 0.0124633 secs]</span><br><span class="line">[GC [DefNew: 8614K-&gt;8614K(9088K), 0.0000665 secs][Tenured: 112761K-&gt;10414K(121024K), 0.0433488 secs] 121376K-&gt;10414K(130112K), 0.0436268 secs]</span><br></pre></td></tr></table></figure></li><li><p>-XX:+PrintGCTimeStamps -XX:+PrintGC：PrintGCTimeStamps可与上面两个混合使用<br>输出形式：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">[GC 98328K-&gt;93620K(130112K), 0.0082960 secs]</span><br></pre></td></tr></table></figure></li><li><p>-XX:+PrintGCApplicationConcurrentTime:打印每次垃圾回收前，程序未中断的执行时间。可与上面混合使用<br>输出形式：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">Application time: 0.5291524 seconds</span><br></pre></td></tr></table></figure></li><li><p>-XX:+PrintGCApplicationStoppedTime：打印垃圾回收期间程序暂停的时间。可与上面混合使用<br>输出形式：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">Total time for which application threads were stopped: 0.0468229 seconds</span><br></pre></td></tr></table></figure></li><li><p>-XX:PrintHeapAtGC:打印GC前后的详细堆栈信息<br>输出形式：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">[GC &#123;Heap before gc invocations=7:</span><br><span class="line"> def new generation   total 55296K, used 52568K [0x1ebd0000, 0x227d0000, 0x227d0000)</span><br><span class="line">eden space 49152K,  99% used [0x1ebd0000, 0x21bce430, 0x21bd0000)</span><br><span class="line">from space 6144K,  55% used [0x221d0000, 0x22527e10, 0x227d0000)</span><br><span class="line">  to   space 6144K,   0% used [0x21bd0000, 0x21bd0000, 0x221d0000)</span><br><span class="line"> tenured generation   total 69632K, used 2696K [0x227d0000, 0x26bd0000, 0x26bd0000)</span><br><span class="line">the space 69632K,   3% used [0x227d0000, 0x22a720f8, 0x22a72200, 0x26bd0000)</span><br><span class="line"> compacting perm gen  total 8192K, used 2898K [0x26bd0000, 0x273d0000, 0x2abd0000)</span><br><span class="line">   the space 8192K,  35% used [0x26bd0000, 0x26ea4ba8, 0x26ea4c00, 0x273d0000)</span><br><span class="line">    ro space 8192K,  66% used [0x2abd0000, 0x2b12bcc0, 0x2b12be00, 0x2b3d0000)</span><br><span class="line">    rw space 12288K,  46% used [0x2b3d0000, 0x2b972060, 0x2b972200, 0x2bfd0000)</span><br><span class="line">34.735: [DefNew: 52568K-&gt;3433K(55296K), 0.0072126 secs] 55264K-&gt;6615K(124928K)Heap after gc invocations=8:</span><br><span class="line"> def new generation   total 55296K, used 3433K [0x1ebd0000, 0x227d0000, 0x227d0000)</span><br><span class="line">eden space 49152K,   0% used [0x1ebd0000, 0x1ebd0000, 0x21bd0000)</span><br><span class="line">  from space 6144K,  55% used [0x21bd0000, 0x21f2a5e8, 0x221d0000)</span><br><span class="line">  to   space 6144K,   0% used [0x221d0000, 0x221d0000, 0x227d0000)</span><br><span class="line"> tenured generation   total 69632K, used 3182K [0x227d0000, 0x26bd0000, 0x26bd0000)</span><br><span class="line">the space 69632K,   4% used [0x227d0000, 0x22aeb958, 0x22aeba00, 0x26bd0000)</span><br><span class="line"> compacting perm gen  total 8192K, used 2898K [0x26bd0000, 0x273d0000, 0x2abd0000)</span><br><span class="line">   the space 8192K,  35% used [0x26bd0000, 0x26ea4ba8, 0x26ea4c00, 0x273d0000)</span><br><span class="line">    ro space 8192K,  66% used [0x2abd0000, 0x2b12bcc0, 0x2b12be00, 0x2b3d0000)</span><br><span class="line">    rw space 12288K,  46% used [0x2b3d0000, 0x2b972060, 0x2b972200, 0x2bfd0000)</span><br><span class="line">&#125;</span><br><span class="line">, 0.0757599 secs]</span><br></pre></td></tr></table></figure></li><li><p>-Xloggc:filename:与上面几个配合使用，把相关日志信息记录到文件以便分析。</p></li></ul><h2 id="常见配置汇总"><a href="#常见配置汇总" class="headerlink" title="常见配置汇总"></a>常见配置汇总</h2><ul><li>-XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。</li><li>-XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时，使用的CPU数。并行收集线程数。</li><li>-XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数。</li><li>-XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间</li><li>-XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)</li><li>-XX:+PrintGC</li><li>-XX:+PrintGCDetails</li><li>-XX:+PrintGCTimeStamps</li><li>-Xloggc:filename</li><li>-XX: +UseSerialGC:设置串行收集器</li><li>-XX: +UseParallelGC:设置并行收集器</li><li>-XX: +UseParalledlOldGC:设置并行年老代收集器</li><li>-XX: +UseConcMarkSweepGC:设置并发收集器</li><li>-Xms:初始堆大小</li><li>-Xmx:最大堆大小</li><li>-XX:NewSize=n:设置年轻代大小</li><li>-XX:NewRatio=n:设置年轻代和年老代的比值。如:为3，表示年轻代与年老代比值为1：3，年轻代占整个年轻代年老代和的1/4</li><li>-XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如：3，表示Eden：Survivor=3：2，一个Survivor区占整个年轻代的1/5</li><li>-XX:MaxPermSize=n:设置持久代大小<br>a. 堆设置<br>b. 收集器设置<br>c. 垃圾回收统计信息<br>d. 并行收集器设置<br>f. 并发收集器设置</li></ul><h2 id="调优总结"><a href="#调优总结" class="headerlink" title="调优总结"></a>调优总结</h2><p>年轻代大小选择</p><ul><li>响应时间优先的应用：尽可能设大，直到接近系统的最低响应时间限制（根据实际情况选择）。在此种情况下，年轻代收集发生的频率也是最小的。同时，减少到达年老代的对象。</li><li>吞吐量优先的应用：尽可能的设置大，可能到达Gbit的程度。因为对响应时间没有要求，垃圾收集可以并行进行，一般适合8CPU以上的应用。</li><li>年老代大小选择</li><li>并发垃圾收集信息</li><li>持久代并发收集次数</li><li>传统GC信息</li><li>花在年轻代和年老代回收上的时间比例</li><li>响应时间优先的应用：年老代使用并发收集器，所以其大小需要小心设置，一般要考虑并发会话率和会话持续时间等一些参数。如果堆设置小了，可以会造成内存碎片、高回收频率以及应用暂停而使用传统的标记清除方式；如果堆大了，则需要较长的收集时间。最优化的方案，一般需要参考以下数据获得：减少年轻代和年老代花费的时间，一般会提高应用的效率</li><li>吞吐量优先的应用：一般吞吐量优先的应用都有一个很大的年轻代和一个较小的年老代。原因是，这样可以尽可能回收掉大部分短期对象，减少中期的对象，而年老代尽存放长期存活对象。</li><li>较小堆引起的碎片问题<br>因为年老代的并发收集器使用标记、清除算法，所以不会对堆进行压缩。当收集器回收时，他会把相邻的空间进行合并，这样可以分配给较大的对象。但是，当堆空间较小时，运行一段时间以后，就会出现“碎片”，如果并发收集器找不到足够的空间，那么并发收集器将会停止，然后使用传统的标记、清除方式进行回收。如果出现“碎片”，可能需要进行如下配置：</li><li>-XX:+UseCMSCompactAtFullCollection：使用并发收集器时，开启对年老代的压缩。</li><li>-XX:CMSFullGCsBeforeCompaction=0：上面配置开启的情况下，这里设置多少次Full GC后，对年老代进行压缩</li></ul>]]></content>
    
    <summary type="html">
    
      JVM调优总结 -Xms -Xmx -Xmn -Xss
    
    </summary>
    
      <category term="Java" scheme="https://ningyu1.github.io/categories/Java/"/>
    
    
      <category term="调优" scheme="https://ningyu1.github.io/tags/%E8%B0%83%E4%BC%98/"/>
    
      <category term="JVM" scheme="https://ningyu1.github.io/tags/JVM/"/>
    
  </entry>
  
  <entry>
    <title>关于Axios的GET与DELETE用法注意事项</title>
    <link href="https://ningyu1.github.io/20170824/12-vue-axios.html"/>
    <id>https://ningyu1.github.io/20170824/12-vue-axios.html</id>
    <published>2017-08-24T02:51:30.000Z</published>
    <updated>2024-04-23T01:48:15.031Z</updated>
    
    <content type="html"><![CDATA[<h2 id="axios的接口定义如下"><a href="#axios的接口定义如下" class="headerlink" title="axios的接口定义如下"></a>axios的接口定义如下</h2><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/vue-axios/1.png" alt="vue1" title>                </div>                <div class="image-caption">vue1</div>            </figure><h2 id="config定义如下："><a href="#config定义如下：" class="headerlink" title="config定义如下："></a>config定义如下：</h2><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/vue-axios/2.png" alt="vue2" title>                </div>                <div class="image-caption">vue2</div>            </figure><p>因此，我们在使用get和delete时需要注意，这两个接口接收的第二个参数是config。用时，就需要区别对待，且需要与后台定义对应。</p><ol><li>如果想参数在Query Parameter里面，那就用{params: params}，后台那边会用RequestParam接收</li><li>如果想参数在Payload里面，那就用{data: params}，后台那边会用RequestBody接收</li></ol><p>如果后台不匹配，可能会抛ContentType错误的异常，如：</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/vue-axios/3.png" alt="vue3" title>                </div>                <div class="image-caption">vue3</div>            </figure>]]></content>
    
    <summary type="html">
    
      关于Axios的GET与DELETE用法注意事项
    
    </summary>
    
      <category term="Vue" scheme="https://ningyu1.github.io/categories/Vue/"/>
    
    
      <category term="Vue.js" scheme="https://ningyu1.github.io/tags/Vue-js/"/>
    
      <category term="Axios" scheme="https://ningyu1.github.io/tags/Axios/"/>
    
  </entry>
  
  <entry>
    <title>Trouble Shooting —— Redis AOF rewrite错误导致Redis被Block住</title>
    <link href="https://ningyu1.github.io/20170815/11-redis-aof-pit.html"/>
    <id>https://ningyu1.github.io/20170815/11-redis-aof-pit.html</id>
    <published>2017-08-15T02:30:34.000Z</published>
    <updated>2024-04-23T01:48:15.030Z</updated>
    
    <content type="html"><![CDATA[<h2 id="问题现状："><a href="#问题现状：" class="headerlink" title="问题现状："></a>问题现状：</h2><p>redis-cli 上去执行任何命令返回：connnection reset by peer</p><p>重启的应用无法连接到redis，已经建立连接的应用可以正常使用。</p><h2 id="分析过程："><a href="#分析过程：" class="headerlink" title="分析过程："></a>分析过程：</h2><p>第一反应查看redis 日志，如下：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">1838:M 16 Aug 01:07:39.319 # Error opening /setting AOF rewrite IPC pipes: Numerical result out of range</span><br><span class="line">1838:M 16 Aug 01:07:39.319 * Starting automatic rewriting of AOF on 110% growth</span><br><span class="line">1838:M 16 Aug 01:07:39.319 # Error opening /setting AOF rewrite IPC pipes: Numerical result out of range</span><br><span class="line">1838:M 16 Aug 01:07:39.419 * Starting automatic rewriting of AOF on 110% growth</span><br><span class="line">1838:M 16 Aug 01:07:39.419 # Error opening /setting AOF rewrite IPC pipes: Numerical result out of range</span><br><span class="line">1838:M 16 Aug 01:07:39.441 # Error registering fd event for the new client: Numerical result out of range (fd=10311)</span><br><span class="line">1838:M 16 Aug 01:07:39.457 # Error registering fd event for the new client: Numerical result out of range (fd=10311)</span><br><span class="line">1838:M 16 Aug 01:07:39.457 # Error registering fd event for the new client: Numerical result out of range (fd=10311)</span><br><span class="line">1838:M 16 Aug 01:07:39.461 # Error registering fd event for the new client: Numerical result out of range (fd=10311)</span><br><span class="line">1838:M 16 Aug 01:07:39.461 # Error registering fd event for the new client: Numerical result out of range (fd=10311)</span><br><span class="line">1838:M 16 Aug 01:07:39.462 # Error registering fd event for the new client: Numerical result out of range (fd=10311)</span><br></pre></td></tr></table></figure><p>上面有两种错误日志</p><ul><li>Error opening /setting AOF rewrite IPC pipes: Numerical result out of range<ul><li>写aof出错了，超限</li></ul></li><li>Error registering fd event for the new client: Numerical result out of range (fd=10311)<ul><li>创建连接没有成功，能看到fd已经是10311 过万了</li></ul></li></ul><p>出现这种问题第一个先去看一下redis现在有多少个连接数</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">&gt;netstat -anp|grep 6379</span><br><span class="line">&gt;499</span><br></pre></td></tr></table></figure><p>查看redis.conf中配置maxclients没有配置，redis默认为10000<br>这个时候有个疑问？为什么netstat查看的连接数只有499，但是redis日志中已经过万（ fd=10311）？这个问题值得思考？<br>我们通过查询进程的fd看一下具体打开了多少个连接（在linux中任何连接都是open file）</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">&gt;ls -al /proc/1838/fd | grep socket | wc -l</span><br><span class="line">&gt;499</span><br><span class="line"> </span><br><span class="line">&gt;ls -al /proc/1838/fd | wc -l</span><br><span class="line">&gt;10322</span><br></pre></td></tr></table></figure><p>为什么fd中socket的只有499，所有类型的确是10322呢？通过具体查看发现有9823个全都是pipe类型的连接</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">&gt;ls -al /proc/1838/fd | grep pipe | wc -l</span><br><span class="line">&gt;9823</span><br></pre></td></tr></table></figure><p>为什么redis进程会有那么多pipe的连接呢？<br>难道是我们redis client使用的pipeline导致的连接泄漏？</p><p>于是查看了Jedis的源码</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">/**</span><br><span class="line"> * Synchronize pipeline by reading all responses. This operation close the pipeline. In order to</span><br><span class="line"> * get return values from pipelined commands, capture the different Response&amp;lt;?&amp;gt; of the</span><br><span class="line"> * commands you execute.</span><br><span class="line"> */</span><br><span class="line">public void sync() &#123;</span><br><span class="line">  if (getPipelinedResponseLength() &gt; 0) &#123;</span><br><span class="line">    List&lt;Object&gt; unformatted = client.getAll();</span><br><span class="line">    for (Object o : unformatted) &#123;</span><br><span class="line">      generateResponse(o);</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>能看到注释中有描述调用这个方法会操作连接关闭：This operation close the pipeline</p><p>又询问了开发的同学我们目前没有使用到pipelined，因此排除了这个可能</p><p>那问题来了是什么原因导致的pipe连接过多？</p><p>网上兜了一圈没发现有价值的信息，没办法只能去扫redis源码，</p><p>accetpCommonHandler函数源码：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">static void acceptCommonHandler(int fd, int flags) &#123;  </span><br><span class="line">    redisClient *c;  </span><br><span class="line">    if ((c = createClient(fd)) == NULL) &#123;  </span><br><span class="line">        redisLog(REDIS_WARNING,  </span><br><span class="line">            &quot;Error registering fd event for the new client: %s (fd=%d)&quot;,  </span><br><span class="line">            strerror(errno),fd);  </span><br><span class="line">        close(fd); /* May be already closed, just ignore errors */  </span><br><span class="line">        return;  </span><br><span class="line">    &#125;  </span><br><span class="line">    /* If maxclient directive is set and this is one client more... close the </span><br><span class="line">     * connection. Note that we create the client instead to check before </span><br><span class="line">     * for this condition, since now the socket is already set in non-blocking </span><br><span class="line">     * mode and we can send an error for free using the Kernel I/O */  </span><br><span class="line">    if (listLength(server.clients) &gt; server.maxclients) &#123;  </span><br><span class="line">        char *err = &quot;-ERR max number of clients reached\r\n&quot;;  </span><br><span class="line">   </span><br><span class="line">        /* That&apos;s a best effort error message, don&apos;t check write errors */  </span><br><span class="line">        if (write(c-&gt;fd,err,strlen(err)) == -1) &#123;  </span><br><span class="line">            /* Nothing to do, Just to avoid the warning... */  </span><br><span class="line">        &#125;  </span><br><span class="line">        server.stat_rejected_conn++;  </span><br><span class="line">        freeClient(c);  </span><br><span class="line">        return;  </span><br><span class="line">    &#125;  </span><br><span class="line">    server.stat_numconnections++;  </span><br><span class="line">    c-&gt;flags |= flags;  </span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>ps.这个函数主要调用createClient初始化客户端相关数据结构以及对应的socket，初始化后会判断当前连接的客户端是否超过最大值，如果超过的话，会拒绝这次连接。否则，更新客户端连接数的计数。<br>数据结构redisClient用于表示一个客户端的连接，包括一个客户多次请求的状态，createClient函数主要是初始化这个数据结构。在createClient函数中，首先是创建redisClient，然后是设置socket的属性，然后添加该socket的读事件</p><p>createClient函数源码：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">if (fd != -1) &#123;  </span><br><span class="line">    anetNonBlock(NULL,fd);  </span><br><span class="line">    // &lt;MM&gt;  </span><br><span class="line">    // 关闭Nagle算法，提升响应速度  </span><br><span class="line">    // &lt;/MM&gt;  </span><br><span class="line">    anetEnableTcpNoDelay(NULL,fd);  </span><br><span class="line">    if (server.tcpkeepalive)  </span><br><span class="line">        anetKeepAlive(NULL,fd,server.tcpkeepalive);  </span><br><span class="line">    if (aeCreateFileEvent(server.el,fd,AE_READABLE,  </span><br><span class="line">        readQueryFromClient, c) == AE_ERR)  </span><br><span class="line">    &#123;  </span><br><span class="line">        close(fd);  </span><br><span class="line">        zfree(c);  </span><br><span class="line">        return NULL;  </span><br><span class="line">    &#125;  </span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>ps.将socket设置为非阻塞的并且no delay，关闭Nagle算法，提升响应速度。最后会注册socket的读事件，事件处理函数是readQueryFromClient，这个函数便是客户端请求的起点，之后会详细介绍。</p><p>createClient函数的最后部分，就是对redisClient的属性初始化，代码不再列出。</p><p>当从acceptTcpHandler返回后，客户端的连接就建立完毕，接下来就是等待客户端的请求。</p><p>以上就是这个错误涉及到的redis源码</p><p>在redis的github上发现了有类似的问题issue：<a href="https://github.com/antirez/redis/issues/2857" target="_blank" rel="noopener">https://github.com/antirez/redis/issues/2857</a></p><p>在源码aof.c文件中</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">/* Parent */    </span><br><span class="line">server.stat_fork_time = ustime()-start;    </span><br><span class="line">server.stat_fork_rate = (double) zmalloc_used_memory() * 1000000 / server.stat_fork_time / (1024*1024*1024); /* GB per second. */    </span><br><span class="line">latencyAddSampleIfNeeded(&quot;fork&quot;,server.stat_fork_time/1000);    </span><br><span class="line">if (childpid == -1) &#123;    </span><br><span class="line">serverLog(LL_WARNING,    </span><br><span class="line">&quot;Can&apos;t rewrite append only file in background: fork: %s&quot;,    </span><br><span class="line">strerror(errno));      </span><br><span class="line">return C_ERR;    </span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>源码发现在报出Can’t rewrite append only file in background: fork: %s这个错误的时候，没有关闭pipe连接</p><p>因此看到了redis官方的修复说明已经修复了这个问题，翻出github上的提交记录，如下</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/redis-pit/1.png" alt="redis1" title>                </div>                <div class="image-caption">redis1</div>            </figure><p>这个时候看到了希望</p><p>于是搜索日志寻找是否有上图的错误：Can’t rewrite append only file in background: fork</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">1838:M 15 Aug 13:52:01.101 # Can&apos;t rewrite append only file in background: fork: Cannot allocate memory</span><br><span class="line">1838:M 15 Aug 13:52:01.202 * Starting automatic rewriting of AOF on 100% growth</span><br><span class="line">1838:M 15 Aug 13:52:01.203 # Can&apos;t rewrite append only file in background: fork: Cannot allocate memory</span><br><span class="line">1838:M 15 Aug 13:52:01.303 * Starting automatic rewriting of AOF on 100% growth</span><br><span class="line">1838:M 15 Aug 13:52:01.304 # Can&apos;t rewrite append only file in background: fork: Cannot allocate memory</span><br></pre></td></tr></table></figure><p>有很多我这里截取了前面的，总共出现的次数</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">&gt;less redis.log.1 | grep <span class="string">"Can't rewrite append only file in background: fork"</span> | wc -l</span><br><span class="line">&gt;1644</span><br></pre></td></tr></table></figure><p>基本可以断定是这个问题引发的连锁反应，这个时候我们需要研究一下Redis AOF机制，最终确认是否是这个问题导致。</p><p>研究redis AOF机制</p><p>redis aof rewirte机制，自动触发bgrewritedaof的条件：</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">long long growth =(server.appendonly_current_size*100/base) - 100;</span><br><span class="line">if (growth &gt;=server.auto_aofrewrite_perc)</span><br></pre></td></tr></table></figure><p>我们的配置文件配置的auto-aof-rewrite-percentage 为100，也就是说当写入日志文件文件大小超过上次rewrite之后的文件大小的百分之100的时候就触发rewrite（也就是超过2倍）</p><p>ps.rewrite之后aof文件会保存keys的最后的状态，清除掉之前冗余的，来缩小这个文件。</p><h2 id="通过分析aof-rewrite发现rewrite出错就是导致Redis连接数超过最大值的罪魁祸首。"><a href="#通过分析aof-rewrite发现rewrite出错就是导致Redis连接数超过最大值的罪魁祸首。" class="headerlink" title="通过分析aof rewrite发现rewrite出错就是导致Redis连接数超过最大值的罪魁祸首。"></a>通过分析aof rewrite发现rewrite出错就是导致Redis连接数超过最大值的罪魁祸首。</h2><h2 id="分析总结："><a href="#分析总结：" class="headerlink" title="分析总结："></a>分析总结：</h2><p>基本可以定位到，这个错误是个连锁反应最终导致Redis服务出现问题</p><ul><li>首先redis在进行aof的rewrite的时候，会检查机器可以用的内存够不够支撑做aof rewrite，这个时候我们机器的可用内存太小，因此报了如下错误</li></ul><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">Can&apos;t rewrite append only file in background: fork: Cannot allocate memory</span><br></pre></td></tr></table></figure><ul><li>但是rewirte自动触发机制当达到2倍的时候会一直触发，他就会一直尝试aof rewrite</li><li>在aof rewrite尝试的过程中，已经创建的连接还是可以正常使用，这导致aof的auto_aofrewrite_perc一直在增长但是无法写入到aof文件中，因此又暴漏出另外一个错误，如下所示</li></ul><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">1838:M 16 Aug 01:07:39.319 * Starting automatic rewriting of AOF on 110% growth</span><br><span class="line">1838:M 16 Aug 01:07:39.319 # Error opening /setting AOF rewrite IPC pipes: Numerical result out of range</span><br></pre></td></tr></table></figure><ul><li>当aof rewirte出错时，从redis代码也能看到，他没有调用close pipes管道连接，这个就造成了服务器上有大量连接被占用（pipe类型）</li></ul><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">&gt;netstat -anp|grep 6379</span><br><span class="line">&gt;499</span><br><span class="line"></span><br><span class="line">&gt;ls -al /proc/1838/fd | grep socket | wc -l</span><br><span class="line">&gt;499</span><br><span class="line"></span><br><span class="line">&gt;ls -al /proc/1838/fd | grep pipe | wc -l</span><br><span class="line">&gt;9823</span><br><span class="line"></span><br><span class="line">&gt;ls -al /proc/1838/fd | wc -l</span><br><span class="line">&gt;10322</span><br></pre></td></tr></table></figure><ul><li>当连接到达maxclients 10000时就会拒绝新建连接，并且报如下错误</li></ul><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">Error registering fd event for the new client: Numerical result out of range (fd=10311)</span><br></pre></td></tr></table></figure><h2 id="本次分析的结论"><a href="#本次分析的结论" class="headerlink" title="本次分析的结论"></a>本次分析的结论</h2><p>这个问题未解决需要继续跟踪，可能需要升级redis的版本，目前看到3.2.9以上才修复了这个bug，我们用的3.0.6版本的跨度有点大兼容性也需要考虑，还要对redis的配置在进一步研究，通过timeout配置让自动关闭无用的连接着也是一个解决问题的思路，这次只是先定位问题，具体解决还需要进一步研究</p><p>这个问题的issue：<a href="https://github.com/antirez/redis/issues/2857" target="_blank" rel="noopener">#2857</a>，<a href="https://github.com/antirez/redis/issues/2883" target="_blank" rel="noopener">#2883</a></p><p>这个问题的提交记录：<a href="https://github.com/antirez/redis/commit/9b05aafb50348838f45bfddcd689e7d8d1d3c950" target="_blank" rel="noopener">fix #2883, #2857 pipe fds leak when fork() failed on bg aof rw</a></p><p>问题修改的文件：<a href="https://github.com/antirez/redis/blob/3.2.9/src/aof.c" target="_blank" rel="noopener">3.2.9分支 -&gt; aof.c文件</a></p>]]></content>
    
    <summary type="html">
    
      Redis AOF踩过的坑
    
    </summary>
    
      <category term="Redis" scheme="https://ningyu1.github.io/categories/Redis/"/>
    
      <category term="Case analysis" scheme="https://ningyu1.github.io/categories/Redis/Case-analysis/"/>
    
      <category term="trouble shooting" scheme="https://ningyu1.github.io/categories/Redis/Case-analysis/trouble-shooting/"/>
    
    
      <category term="调优" scheme="https://ningyu1.github.io/tags/%E8%B0%83%E4%BC%98/"/>
    
      <category term="Redis" scheme="https://ningyu1.github.io/tags/Redis/"/>
    
      <category term="AOF" scheme="https://ningyu1.github.io/tags/AOF/"/>
    
      <category term="AOF rewriting" scheme="https://ningyu1.github.io/tags/AOF-rewriting/"/>
    
      <category term="Connnection reset by peer" scheme="https://ningyu1.github.io/tags/Connnection-reset-by-peer/"/>
    
      <category term="Numerical result out of range" scheme="https://ningyu1.github.io/tags/Numerical-result-out-of-range/"/>
    
      <category term="Cannot allocate memory" scheme="https://ningyu1.github.io/tags/Cannot-allocate-memory/"/>
    
      <category term="Can&#39;t rewrite append only file in background" scheme="https://ningyu1.github.io/tags/Can-t-rewrite-append-only-file-in-background/"/>
    
  </entry>
  
  <entry>
    <title>Lombok使用说明</title>
    <link href="https://ningyu1.github.io/20170719/04-lombok-quick-start.html"/>
    <id>https://ningyu1.github.io/20170719/04-lombok-quick-start.html</id>
    <published>2017-07-19T07:22:56.000Z</published>
    <updated>2024-04-23T01:48:15.029Z</updated>
    
    <content type="html"><![CDATA[<h3 id="一、项目背景"><a href="#一、项目背景" class="headerlink" title="一、项目背景"></a>一、项目背景</h3><p>在写Java程序的时候经常会遇到如下情形： </p><p>新建了一个Class类，然后在其中设置了几个字段，最后还需要花费很多时间来建立getter和setter方法 </p><p>lombok项目的产生就是为了省去我们手动创建getter和setter方法的麻烦，它能够在我们编译源码的时候自动帮我们生成getter和setter方法。即它最终能够达到的效果是：在源码中没有getter和setter方法，但是在编译生成的字节码文件中有getter和setter方法 </p><p>比如源码文件： </p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> java.io.Serializable;</span><br><span class="line"><span class="keyword">import</span> lombok.AllArgsConstructor;</span><br><span class="line"><span class="keyword">import</span> lombok.Data;</span><br><span class="line"><span class="keyword">import</span> lombok.NoArgsConstructor;</span><br><span class="line"><span class="keyword">import</span> lombok.extern.slf4j.Slf4j;</span><br><span class="line"> </span><br><span class="line"><span class="meta">@Data</span></span><br><span class="line"><span class="meta">@Slf</span>4j</span><br><span class="line"><span class="meta">@NoArgsConstructor</span></span><br><span class="line"><span class="meta">@AllArgsConstructor</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">TestUserVo</span> <span class="keyword">implements</span> <span class="title">Serializable</span></span>&#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="keyword">long</span> serialVersionUID = -<span class="number">5648809805573016853L</span>;</span><br><span class="line">    <span class="keyword">private</span> Long id;</span><br><span class="line">    <span class="keyword">private</span> Long userId;</span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 获取 id</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span> the id</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> Long <span class="title">getId</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        System.out.println(<span class="string">"getId"</span>);</span><br><span class="line">        <span class="keyword">return</span> id;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 设置 id</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> id the id to set</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">setId</span><span class="params">(Long id)</span> </span>&#123;</span><br><span class="line">        System.out.println(<span class="string">"setId"</span>);</span><br><span class="line">        <span class="keyword">this</span>.id = id;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>以下是编译上述源码文件得到的字节码文件，对其反编译得到的结果<br><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> java.io.Serializable;</span><br><span class="line"><span class="keyword">import</span> java.beans.ConstructorProperties;</span><br><span class="line"><span class="keyword">import</span> java.io.PrintStream;</span><br><span class="line"><span class="keyword">import</span> org.slf4j.Logger;</span><br><span class="line"><span class="keyword">import</span> org.slf4j.LoggerFactory;</span><br><span class="line"> </span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">TestUserVo</span> <span class="keyword">implements</span> <span class="title">Serializable</span> </span>&#123;</span><br><span class="line">    <span class="function"><span class="keyword">public</span> String <span class="title">toString</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="string">"TestUserVo(id="</span> + getId() + <span class="string">", userId="</span> + getUserId() + <span class="string">")"</span>;</span><br><span class="line">    &#125;</span><br><span class="line"> </span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">int</span> <span class="title">hashCode</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">int</span> PRIME = <span class="number">59</span>;</span><br><span class="line">        <span class="keyword">int</span> result = <span class="number">1</span>;</span><br><span class="line">        Object $id = getId();</span><br><span class="line">        result = result * <span class="number">59</span> + ($id == <span class="keyword">null</span> ? <span class="number">43</span> : $id.hashCode());</span><br><span class="line">        Object $userId = getUserId();</span><br><span class="line">        result = result * <span class="number">59</span> + ($userId == <span class="keyword">null</span> ? <span class="number">43</span> : $userId.hashCode());</span><br><span class="line">        <span class="keyword">return</span> result;</span><br><span class="line">    &#125;</span><br><span class="line"> </span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">setUserId</span><span class="params">(Long userId)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">this</span>.userId = userId;</span><br><span class="line">    &#125;</span><br><span class="line"> </span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">boolean</span> <span class="title">equals</span><span class="params">(Object o)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">if</span> (o == <span class="keyword">this</span>) &#123;</span><br><span class="line">            <span class="keyword">return</span> <span class="keyword">true</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">if</span> (!(o <span class="keyword">instanceof</span> TestUserVo)) &#123;</span><br><span class="line">            <span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        TestUserVo other = (TestUserVo) o;</span><br><span class="line">        <span class="keyword">if</span> (!other.canEqual(<span class="keyword">this</span>)) &#123;</span><br><span class="line">            <span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        Object <span class="keyword">this</span>$id = getId();</span><br><span class="line">        Object other$id = other.getId();</span><br><span class="line">        <span class="keyword">if</span> (<span class="keyword">this</span>$id == <span class="keyword">null</span> ? other$id != <span class="keyword">null</span> : !<span class="keyword">this</span>$id.equals(other$id)) &#123;</span><br><span class="line">            <span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        Object <span class="keyword">this</span>$userId = getUserId();</span><br><span class="line">        Object other$userId = other.getUserId();</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">this</span>$userId == <span class="keyword">null</span> ? other$userId == <span class="keyword">null</span> : <span class="keyword">this</span>$userId.equals(other$userId);</span><br><span class="line">    &#125;</span><br><span class="line"> </span><br><span class="line">    <span class="function"><span class="keyword">protected</span> <span class="keyword">boolean</span> <span class="title">canEqual</span><span class="params">(Object other)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> other <span class="keyword">instanceof</span> TestUserVo;</span><br><span class="line">    &#125;</span><br><span class="line"> </span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> Logger log = LoggerFactory.getLogger(TestUserVo<span class="class">.<span class="keyword">class</span>)</span>;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="keyword">long</span> serialVersionUID = -<span class="number">5648809805573016853L</span>;</span><br><span class="line">    <span class="keyword">private</span> Long id;</span><br><span class="line">    <span class="keyword">private</span> Long userId;</span><br><span class="line"> </span><br><span class="line">    <span class="meta">@ConstructorProperties</span>(&#123; <span class="string">"id"</span>, <span class="string">"userId"</span> &#125;)</span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="title">TestUserVo</span><span class="params">(Long id, Long userId)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">this</span>.id = id;</span><br><span class="line">        <span class="keyword">this</span>.userId = userId;</span><br><span class="line">    &#125;</span><br><span class="line"> </span><br><span class="line">    <span class="function"><span class="keyword">public</span> Long <span class="title">getUserId</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">this</span>.userId;</span><br><span class="line">    &#125;</span><br><span class="line"> </span><br><span class="line">    <span class="function"><span class="keyword">public</span> Long <span class="title">getId</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        System.out.println(<span class="string">"getId"</span>);</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">this</span>.id;</span><br><span class="line">    &#125;</span><br><span class="line"> </span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">setId</span><span class="params">(Long id)</span> </span>&#123;</span><br><span class="line">        System.out.println(<span class="string">"setId"</span>);</span><br><span class="line">        <span class="keyword">this</span>.id = id;</span><br><span class="line">    &#125;</span><br><span class="line"> </span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="title">TestUserVo</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p>为什么推荐使用它呢，因为我们一般写一个<code>pojo</code>时很容易遗漏（<code>equals</code>、<code>toString</code>、<code>hashCode</code>、<code>canEqual</code>）这几个方法，使用<code>Lombok</code>不但可以在编译的时候自动生成<code>getter</code>、<code>setter</code>方法还会根据字段来生成（<code>equals</code>、<code>toString</code>、<code>hashCode</code>、<code>canEqual</code>）这几个方法.</p><p><code>Lombok</code>在生成<code>getter</code>、<code>setter</code>方法时不会覆盖我们源码中已经编写的<code>getter</code>、<code>setter</code>方法，所以可以大胆的使用。</p><p>下面介绍几个常用的 lombok 注解：</p><p><code>@Data</code>   ：注解在类上；提供类所有属性的 <code>getting</code> 和 <code>setting</code> 方法，此外还提供了<code>equals</code>、<code>canEqual</code>、<code>hashCode</code>、<code>toString</code> 方法</p><p><code>@Setter</code>：注解在属性上；为属性提供 <code>setting</code> 方法</p><p><code>@Getter</code>：注解在属性上；为属性提供 <code>getting</code> 方法</p><p><code>@Log4j</code> | <code>@Slf4j</code> | <code>@Log</code> ：注解在类上；为类提供一个 属性名为<code>log</code> 的 <code>log4j</code> | <code>SLF4j</code> | <code>Log(java logging)</code>日志对象</p><p><code>@NoArgsConstructor</code>：注解在类上；为类提供一个无参的构造方法</p><p><code>@AllArgsConstructor</code>：注解在类上；为类提供一个全参的构造方法</p><p>其他的查看官网的文档：<a href="https://projectlombok.org/features/all" title="https://projectlombok.org/features/all" target="_blank" rel="noopener">https://projectlombok.org/features/all</a></p><h3 id="二、使用方法"><a href="#二、使用方法" class="headerlink" title="二、使用方法"></a>二、使用方法</h3><p>Maven依赖：<br><figure class="highlight xml"><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.projectlombok<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>lombok<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">version</span>&gt;</span>lombok<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line">``` </span><br><span class="line">ps.版本为：1.16.18，并且`scope`为：`provided`，我们只在编译时使用。</span><br><span class="line"></span><br><span class="line">使用`lombok`项目的方法很简单，分为四个步骤： </span><br><span class="line"></span><br><span class="line">1)在需要自动生成类上，加上自动生成注解（`@Data`、`@Setter`、`@Getter`、`@Log4j`、`@Slf4j`、`@Log`、`@NoArgsConstructor`、`@AllArgsConstructor`，等等）</span><br><span class="line"></span><br><span class="line">2)在编译类路径中加入`lombok.jar`包 ，`maven`中添加依赖</span><br><span class="line"></span><br><span class="line">3)使用支持`lombok`的编译工具编译源代码（关于支持`lombok`的编译工具，见“四、支持`lombok`的编译工具”） </span><br><span class="line"></span><br><span class="line">4)编译得到的字节码文件中自动生成相应配置的代码</span><br><span class="line"></span><br><span class="line">### 三、原理分析 </span><br><span class="line">接下来进行`lombok`能够工作的原理分析，以`Oracle`的`javac`编译工具为例。 </span><br><span class="line"></span><br><span class="line">自从Java 6起，javac就支持“JSR 269 Pluggable Annotation Processing API”规范，只要程序实现了该API，就能在javac运行的时候得到调用。 </span><br><span class="line"></span><br><span class="line">举例来说，现在有一个实现了"JSR 269 API"的程序A,那么使用javac编译源码的时候具体流程如下： </span><br><span class="line"></span><br><span class="line">1)javac对源代码进行分析，生成一棵抽象语法树(AST) </span><br><span class="line"></span><br><span class="line">2)运行过程中调用实现了"JSR 269 API"的A程序 </span><br><span class="line"></span><br><span class="line">3)此时A程序就可以完成它自己的逻辑，包括修改第一步骤得到的抽象语法树(AST) </span><br><span class="line"></span><br><span class="line">4)javac使用修改后的抽象语法树(AST)生成字节码文件 </span><br><span class="line"></span><br><span class="line">详细的流程图如下： </span><br><span class="line">![流程图](/img/lombok/1.jpg)</span><br><span class="line">`lombok`本质上就是这样的一个实现了"JSR 269 API"的程序。在使用javac的过程中，它产生作用的具体流程如下：</span><br><span class="line"></span><br><span class="line">1)javac对源代码进行分析，生成一棵抽象语法树(AST) </span><br><span class="line"></span><br><span class="line">2)运行过程中调用实现了"JSR 269 API"的lombok程序 </span><br><span class="line"></span><br><span class="line">3)此时`lombok`就对第一步骤得到的AST进行处理，找到@Data注解所在类对应的语法树(AST)，然后修改该语法树(AST)，增加`getter`和`setter`方法定义的相应树节点 </span><br><span class="line"></span><br><span class="line">4)javac使用修改后的抽象语法树(AST)生成字节码文件 </span><br><span class="line"></span><br><span class="line">### 四、支持lombok的编译工具 </span><br><span class="line">1. 由“三、原理分析”可知，`Oracle` javac直接支持`lombok` </span><br><span class="line">2. 常用的项目管理工具`Maven`所使用的java编译工具来源于配置的第三方工具，如果我们配置这个第三方工具为`Oracle` javac的话，那么`Maven`也就直接支持`lombok`了 </span><br><span class="line">3. Intellij Idea中配置，可以下载安装Intellij Idea中的"Lombok plugin"。 </span><br><span class="line">4. Eclipse中配置lombok支持（或者使用官方的plugin：[https://projectlombok.org/setup/eclipse](https://projectlombok.org/setup/eclipse "https://projectlombok.org/setup/eclipse")）</span><br><span class="line">1. 去官网下载：[http://projectlombok.org/ ](http://projectlombok.org/  "http://projectlombok.org/ ")</span><br><span class="line">2. eclipse / myeclipse 手动安装 `lombok`</span><br><span class="line">3. 将 lombok.jar 复制到 myeclipse.ini / eclipse.ini 所在的文件夹目录下</span><br><span class="line">4. 打开 eclipse.ini / myeclipse.ini，在最后面插入以下两行并保存：</span><br><span class="line">5. ``` -Xbootclasspath/lombok.jar -javaagent:lombok.jar</span><br></pre></td></tr></table></figure></p><pre><code>6. 重启 eclipse / myeclipse</code></pre><p>如上配置后，在类以后用上无需书写<code>getter</code>、<code>setter</code>程序中也可以直接引用<code>getter</code>、<code>setter</code>方法<br>其他IDE支持，请去官网：<a href="https://projectlombok.org/" title="https://projectlombok.org/" target="_blank" rel="noopener">https://projectlombok.org/</a> 点击Install选择不同的IDE插件安装说明</p><h3 id="五、lombok的罪恶"><a href="#五、lombok的罪恶" class="headerlink" title="五、lombok的罪恶"></a>五、lombok的罪恶</h3><p>使用<code>lombok</code>虽然能够省去手动创建<code>setter</code>和<code>getter</code>方法的麻烦，但是却大大降低了源代码文件的可读性和完整性，降低了阅读源代码的舒适度。 </p>]]></content>
    
    <summary type="html">
    
      Lombok使用说明
    
    </summary>
    
      <category term="Lombok" scheme="https://ningyu1.github.io/categories/Lombok/"/>
    
      <category term="Java" scheme="https://ningyu1.github.io/categories/Lombok/Java/"/>
    
    
      <category term="Java" scheme="https://ningyu1.github.io/tags/Java/"/>
    
      <category term="Lombok" scheme="https://ningyu1.github.io/tags/Lombok/"/>
    
  </entry>
  
  <entry>
    <title>Fastdfs安装说明与常见问题解决</title>
    <link href="https://ningyu1.github.io/20170704/02-fastdfs-installer.html"/>
    <id>https://ningyu1.github.io/20170704/02-fastdfs-installer.html</id>
    <published>2017-07-04T00:00:00.000Z</published>
    <updated>2024-04-23T01:48:15.028Z</updated>
    
    <content type="html"><![CDATA[<h1 id="Fastdfs安装说明与常见问题解决"><a href="#Fastdfs安装说明与常见问题解决" class="headerlink" title="Fastdfs安装说明与常见问题解决"></a>Fastdfs安装说明与常见问题解决</h1><h2 id="docker中安装"><a href="#docker中安装" class="headerlink" title="docker中安装"></a>docker中安装</h2><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">docker pull season/fastdfs</span><br><span class="line">docker tag season/fastdfs 192.168.0.34:5000/season/fastdfs</span><br><span class="line">docker push 192.168.0.34:5000/season/fastdfs</span><br></pre></td></tr></table></figure><p>启动会获取tracker ip<br><code>192.168.0.54:22122</code></p><p>monitor检测<br><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">/usr/local/bin/fdfs_monitor /etc/fdfs/storage.conf</span><br></pre></td></tr></table></figure></p><p>storage<br>store_path0路径与base_path路径必须不同</p><h2 id="物理机安装"><a href="#物理机安装" class="headerlink" title="物理机安装"></a>物理机安装</h2><h3 id="1-安装git"><a href="#1-安装git" class="headerlink" title="1.安装git"></a>1.安装git</h3><p><code>yum install -y git</code></p><h3 id="2-下载fastdfs源码"><a href="#2-下载fastdfs源码" class="headerlink" title="2.下载fastdfs源码"></a>2.下载fastdfs源码</h3><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">git <span class="built_in">clone</span> https://github.com/happyfish100/fastdfs.git</span><br><span class="line">git <span class="built_in">clone</span> https://github.com/happyfish100/libfastcommon.git</span><br><span class="line">git <span class="built_in">clone</span> https://github.com/happyfish100/fastdfs-nginx-module.git</span><br></pre></td></tr></table></figure><h3 id="3-下载nginx"><a href="#3-下载nginx" class="headerlink" title="3.下载nginx"></a>3.下载nginx</h3><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">cp /home/jyftp/nginx-1.10.1.tar.gz ./</span><br><span class="line">tar -xvf nginx-1.10.1.tar.gz </span><br><span class="line">rm -rf nginx-1.10.1.tar.gz </span><br><span class="line">chown -R root.root nginx-1.10.1/</span><br><span class="line">mv nginx-1.10.1/ nginx</span><br></pre></td></tr></table></figure><h3 id="4-安装libfastcommon-fastdfs依赖的系统库）"><a href="#4-安装libfastcommon-fastdfs依赖的系统库）" class="headerlink" title="4.安装libfastcommon (fastdfs依赖的系统库）"></a>4.安装libfastcommon (fastdfs依赖的系统库）</h3><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="built_in">cd</span> /usr/<span class="built_in">local</span>/fastdfs/libfastcommon</span><br><span class="line">./make.sh</span><br><span class="line">./make.sh install</span><br></pre></td></tr></table></figure><h3 id="5-安装fastdfs"><a href="#5-安装fastdfs" class="headerlink" title="5.安装fastdfs"></a>5.安装fastdfs</h3><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="built_in">cd</span> /usr/<span class="built_in">local</span>/fastdfs/fastdfs</span><br><span class="line">./make.sh</span><br><span class="line">./make.sh install</span><br></pre></td></tr></table></figure><h3 id="6-安装nginx"><a href="#6-安装nginx" class="headerlink" title="6.安装nginx"></a>6.安装nginx</h3><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="built_in">cd</span> /usr/<span class="built_in">local</span>/nginx</span><br><span class="line">./configure --prefix=/usr/<span class="built_in">local</span>/nginx --conf-path=/usr/<span class="built_in">local</span>/nginx/nginx.conf --add-module=/usr/<span class="built_in">local</span>/fastdfs/fastdfs-nginx-module/src</span><br><span class="line">make </span><br><span class="line">make install</span><br></pre></td></tr></table></figure><p>安装nginx错误处理<br>错误信息：<br><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">./configure: error: the HTTP rewrite module requires the PCRE library.</span><br></pre></td></tr></table></figure></p><p>安装pcre-devel与openssl-devel解决问题，执行下面命令<br><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">yum -y install pcre-devel openssl openssl-devel</span><br></pre></td></tr></table></figure></p><p>错误信息：<br><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">/data/soft/fastdfs-nginx-module/src/ngx_http_fastdfs_module.c:894: 错误：‘struct fdfs_http_context’没有名为‘if_modified_since’的成员</span><br><span class="line">/data/soft/fastdfs-nginx-module/src/ngx_http_fastdfs_module.c:897: 错误：‘struct fdfs_http_context’没有名为‘if_modified_since’的成员</span><br><span class="line">/data/soft/fastdfs-nginx-module/src/ngx_http_fastdfs_module.c:927: 错误：‘struct fdfs_http_context’没有名为‘range’的成员</span><br><span class="line">/data/soft/fastdfs-nginx-module/src/ngx_http_fastdfs_module.c:933: 错误：‘struct fdfs_http_context’没有名为‘if_range’的成员</span><br><span class="line">/data/soft/fastdfs-nginx-module/src/ngx_http_fastdfs_module.c:933: 错误：‘true’未声明(在此函数内第一次使用)</span><br><span class="line">make[1]: *** [objs/addon/src/ngx_http_fastdfs_module.o] 错误 1</span><br><span class="line">make[1]: Leaving directory `/data/soft/nginx-1.8.0&apos;</span><br><span class="line">make: *** [build] 错误 2</span><br></pre></td></tr></table></figure></p><p>解决办法<br>执行以下2条命令，然后重新make<br><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">ln -sv /usr/include/fastcommon /usr/local/include/fastcommon</span><br><span class="line">ln -sv /usr/include/fastdfs /usr/local/include/fastdfs</span><br></pre></td></tr></table></figure></p><p>拷贝相关文件到/etc/fdfs目录下：<br><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">cp /usr/local/fastdfs/fastdfs-nginx-module/src/mod_fastdfs.conf /etc/fdfs/</span><br><span class="line">cp /usr/local/fastdfs/fastdfs/conf/mime.types /etc/fdfs/</span><br><span class="line">cp /usr/local/fastdfs/fastdfs/conf/http.conf /etc/fdfs/</span><br><span class="line">cp /usr/local/fastdfs/fastdfs/conf/anti-steal.jpg /etc/fdfs/</span><br></pre></td></tr></table></figure></p><p>如果是下面错误，需要安装fastdfs最新版，直接从github上下载源码安装<br><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">local/fastdfs-nginx-module/src/common.c:1245: 错误：‘FDFSHTTPParams’没有名为‘support_multi_range’的成员</span><br><span class="line">make[1]: *** [objs/addon/src/ngx_http_fastdfs_module.o] 错误 1</span><br><span class="line">make[1]: Leaving directory `/usr/local/nginx-1.10.1&apos;</span><br></pre></td></tr></table></figure></p><p>解决办法：github上下载最新FastDFS master源码，重新编译安装即可。</p><h3 id="7-配置fastdfs"><a href="#7-配置fastdfs" class="headerlink" title="7.配置fastdfs"></a>7.配置fastdfs</h3><h4 id="7-1-创建数据存放目录（用于存放数据）"><a href="#7-1-创建数据存放目录（用于存放数据）" class="headerlink" title="7.1 创建数据存放目录（用于存放数据）"></a>7.1 创建数据存放目录（用于存放数据）</h4><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="built_in">cd</span> /usr/<span class="built_in">local</span>/fastdfs</span><br><span class="line">mkdir fast_data</span><br><span class="line"><span class="built_in">cd</span> fast_data</span><br></pre></td></tr></table></figure><h5 id="tracker基础数据和日志"><a href="#tracker基础数据和日志" class="headerlink" title="tracker基础数据和日志"></a>tracker基础数据和日志</h5><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">mkdir tracker</span><br></pre></td></tr></table></figure><h5 id="storage基础数据和日志"><a href="#storage基础数据和日志" class="headerlink" title="storage基础数据和日志"></a>storage基础数据和日志</h5><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">mkdir storage</span><br></pre></td></tr></table></figure><h5 id="storage-数据存放目录"><a href="#storage-数据存放目录" class="headerlink" title="storage 数据存放目录"></a>storage 数据存放目录</h5><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">mkdir store_path</span><br></pre></td></tr></table></figure><h5 id="fast-nginx模块基础数据和日志"><a href="#fast-nginx模块基础数据和日志" class="headerlink" title="fast nginx模块基础数据和日志"></a>fast nginx模块基础数据和日志</h5><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">mkdir nginx_module</span><br></pre></td></tr></table></figure><h4 id="7-2-创建配置文件目录（用于存放使用的配置文件）"><a href="#7-2-创建配置文件目录（用于存放使用的配置文件）" class="headerlink" title="7.2 创建配置文件目录（用于存放使用的配置文件）"></a>7.2 创建配置文件目录（用于存放使用的配置文件）</h4><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">mkdir fast_conf</span><br><span class="line">cd /usr/local/fastdfs/fastdfs/conf</span><br><span class="line">cp ./* /usr/local/fastdfs/fast_conf/</span><br></pre></td></tr></table></figure><h4 id="7-3-配置tracker"><a href="#7-3-配置tracker" class="headerlink" title="7.3 配置tracker"></a>7.3 配置tracker</h4><h5 id="编辑basepath"><a href="#编辑basepath" class="headerlink" title="编辑basepath"></a>编辑basepath</h5><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">＃basepath(用于存放tracker的基本数据，包括日志）</span><br><span class="line">base_path=/usr/local/fastdfs/fast_data/tracker</span><br></pre></td></tr></table></figure><h4 id="7-4-配置storage"><a href="#7-4-配置storage" class="headerlink" title="7.4 配置storage"></a>7.4 配置storage</h4><h5 id="修改如下配置："><a href="#修改如下配置：" class="headerlink" title="修改如下配置："></a>修改如下配置：</h5><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">＃用于存储storage基本数据的目录（包括日志）</span><br><span class="line">base_path=/usr/local/fastdfs/fast_data/storage</span><br><span class="line">＃数据存放的目录</span><br><span class="line">store_path0=/usr/local/fastdfs/fast_data/store_path</span><br><span class="line">＃group的名字</span><br><span class="line">group_name=group1</span><br><span class="line"># tracker地址</span><br><span class="line">tracker_server=10.30.193.163:22122</span><br></pre></td></tr></table></figure><h5 id="这个是tracker的ip地址和端口号"><a href="#这个是tracker的ip地址和端口号" class="headerlink" title="这个是tracker的ip地址和端口号"></a>这个是tracker的ip地址和端口号</h5><p><code>tracker_server=192.168.0.48:22122</code></p><h4 id="7-5-修改nginx相关的fastdfs配置文件"><a href="#7-5-修改nginx相关的fastdfs配置文件" class="headerlink" title="7.5 修改nginx相关的fastdfs配置文件"></a>7.5 修改nginx相关的fastdfs配置文件</h4><p>将nginx module的配置文件拷贝到fastdfs的配置目录<br><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">cp /usr/local/fastdfs/fastdfs-nginx-module/src/mod_fastdfs.conf /usr/local/fastdfs/fast_conf</span><br></pre></td></tr></table></figure></p><p>修改mod_fastdfs.conf<br><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">#存放日志等文件</span><br><span class="line">base_path=/usr/local/fastdfs/fast_data/nginx_module</span><br><span class="line">#tracker的地址（这个是nginx中的plugin使用的）</span><br><span class="line">tracker_server=192.168.0.48:22122</span><br><span class="line">#本地对应的group名字（当前nginx对应的storage存储的group的名字）</span><br><span class="line">group_name=group1</span><br><span class="line">#这个配置用于说明nginx对应的storage存储文件的实际位置</span><br><span class="line">store_path0=/usr/local/fastdfs/fast_data/store_path</span><br><span class="line">#这个是url是否需要带groupname</span><br><span class="line">url_have_group_name = true</span><br></pre></td></tr></table></figure></p><h4 id="9-编写启动脚本"><a href="#9-编写启动脚本" class="headerlink" title="9. 编写启动脚本"></a>9. 编写启动脚本</h4><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">cd /usr/local/fastdfs</span><br></pre></td></tr></table></figure><h4 id="9-1-创建-启动文件目录"><a href="#9-1-创建-启动文件目录" class="headerlink" title="9.1 创建 启动文件目录"></a>9.1 创建 启动文件目录</h4><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">mkdir bin</span><br></pre></td></tr></table></figure><p>tracker的启动脚本 </p><h5 id="9-2-在bin目录下，创建tracker-sh"><a href="#9-2-在bin目录下，创建tracker-sh" class="headerlink" title="9.2 在bin目录下，创建tracker.sh"></a>9.2 在bin目录下，创建tracker.sh</h5><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="meta">#!/bin/sh</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">case</span> <span class="string">"<span class="variable">$1</span>"</span> <span class="keyword">in</span></span><br><span class="line"></span><br><span class="line">    start) </span><br><span class="line">/usr/<span class="built_in">local</span>/fastdfs/fastdfs/tracker/fdfs_trackerd /usr/<span class="built_in">local</span>/fastdfs/fast_conf/tracker.conf</span><br><span class="line">    ;;</span><br><span class="line">    stop) </span><br><span class="line">/usr/<span class="built_in">local</span>/fastdfs/fastdfs/tracker/fdfs_trackerd /usr/<span class="built_in">local</span>/fastdfs/fast_conf/tracker.conf stop</span><br><span class="line">    ;;</span><br><span class="line">    restart)</span><br><span class="line">/usr/<span class="built_in">local</span>/fastdfs/fastdfs/tracker/fdfs_trackerd /usr/<span class="built_in">local</span>/fastdfs/fast_conf/tracker.conf restart </span><br><span class="line">    ;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">esac</span>   </span><br><span class="line"></span><br><span class="line"><span class="comment"># tailf /usr/local/fastdfs/fast_data/tracker/logs/trackerd.log</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">exit</span> 0</span><br></pre></td></tr></table></figure><p>将文件变成可执行<br><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">chmod +x tracker.sh</span><br></pre></td></tr></table></figure></p><h5 id="9-3-在bin目录下，创建storage-sh"><a href="#9-3-在bin目录下，创建storage-sh" class="headerlink" title="9.3 在bin目录下，创建storage.sh"></a>9.3 在bin目录下，创建storage.sh</h5><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="meta">#!/bin/sh</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">case</span> <span class="string">"<span class="variable">$1</span>"</span> <span class="keyword">in</span></span><br><span class="line"></span><br><span class="line">    start)</span><br><span class="line">/usr/<span class="built_in">local</span>/fastdfs/fastdfs/storage/fdfs_storaged /usr/<span class="built_in">local</span>/fastdfs/fast_conf/storage.conf </span><br><span class="line">    ;;</span><br><span class="line">    stop)</span><br><span class="line">/usr/<span class="built_in">local</span>/fastdfs/fastdfs/storage/fdfs_storaged /usr/<span class="built_in">local</span>/fastdfs/fast_conf/storage.conf stop </span><br><span class="line">    ;;</span><br><span class="line">    restart)</span><br><span class="line">/usr/<span class="built_in">local</span>/fastdfs/fastdfs/storage/fdfs_storaged /usr/<span class="built_in">local</span>/fastdfs/fast_conf/storage.conf restart</span><br><span class="line">    ;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">esac</span>   </span><br><span class="line"></span><br><span class="line"><span class="comment"># tailf /usr/local/fastdfs/fast_data/storage/logs/storaged.log</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">exit</span> 0</span><br></pre></td></tr></table></figure><p>将文件变成可执行<br><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">chmod +x storage.sh</span><br></pre></td></tr></table></figure></p><h5 id="9-4-配置、启动nginx"><a href="#9-4-配置、启动nginx" class="headerlink" title="9.4 配置、启动nginx"></a>9.4 配置、启动nginx</h5><p>修改<code>mod_fastdfs.conf</code>配置文件<br><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">cd /usr/local/fastdfs/fast_conf/</span><br><span class="line">vi mod_fastdfs.conf</span><br></pre></td></tr></table></figure></p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># FastDFS tracker_server can ocur more than once, and tracker_server format is</span></span><br><span class="line"><span class="comment">#  "host:port", host can be hostname or ip address</span></span><br><span class="line"><span class="comment"># valid only when load_fdfs_parameters_from_tracker is true</span></span><br><span class="line">tracker_server=192.168.0.48:22122</span><br><span class="line"></span><br><span class="line"><span class="comment"># the port of the local storage server</span></span><br><span class="line"><span class="comment"># the default value is 23000</span></span><br><span class="line">storage_server_port=23000</span><br><span class="line"></span><br><span class="line"><span class="comment"># if the url / uri including the group name</span></span><br><span class="line"><span class="comment"># set to false when uri like /M00/00/00/xxx</span></span><br><span class="line"><span class="comment"># set to true when uri like $&#123;group_name&#125;/M00/00/00/xxx, such as group1/M00/xxx</span></span><br><span class="line"><span class="comment"># default value is false</span></span><br><span class="line">url_have_group_name = <span class="literal">true</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># store_path#, based 0, if store_path0 not exists, it's value is base_path</span></span><br><span class="line"><span class="comment"># the paths must be exist</span></span><br><span class="line"><span class="comment"># must same as storage.conf</span></span><br><span class="line">store_path0=/usr/<span class="built_in">local</span>/fastdfs/fast_data/store_path</span><br><span class="line"><span class="comment">#store_path1=/home/yuqing/fastdfs1</span></span><br></pre></td></tr></table></figure><p>copy配置文件到 <code>/etc/fdfs</code>下<br><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="built_in">cd</span> /usr/<span class="built_in">local</span>/fastdfs/fast_conf/</span><br><span class="line">cp anti-steal.jpg http.conf mime.types mod_fastdfs.conf /etc/fdfs/</span><br></pre></td></tr></table></figure></p><p>修改nginx的配置文件<br><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="built_in">cd</span> /usr/<span class="built_in">local</span>/nginx</span><br><span class="line">vi nginx.conf</span><br><span class="line">server &#123;</span><br><span class="line">listen 8079;</span><br><span class="line">location ~/M00 &#123;</span><br><span class="line">    root /usr/<span class="built_in">local</span>/fastdfs/fast_data/store_path/data;</span><br><span class="line">    ngx_fastdfs_module;</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p>创建软连接<br><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">ln -s /usr/local/fastdfs/fast_data/store_path/data /usr/local/fastdfs/fast_data/store_path/data/M00</span><br></pre></td></tr></table></figure></p><p>启动nginx之前先-t检查一下配置文件是否有错误<br><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">/usr/local/nginx/sbin/nginx -t</span><br></pre></td></tr></table></figure></p><p>输出一下信息表示正确<br><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">[root@localhost bin]# /usr/local/nginx/sbin/nginx -t</span><br><span class="line">ngx_http_fastdfs_set pid=125936</span><br><span class="line">nginx: the configuration file /usr/local/nginx/nginx.conf syntax is ok</span><br><span class="line">nginx: configuration file /usr/local/nginx/nginx.conf test is successful</span><br></pre></td></tr></table></figure></p><p>启动nginx<br><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">/usr/local/nginx/sbin/nginx</span><br></pre></td></tr></table></figure></p><p>启动 Nginx 后会打印出fastdfs模块的pid，看看日志是否报错，正常不会报错的<br><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">[root@localhost fdfs]<span class="comment"># /usr/local/nginx/sbin/nginx</span></span><br><span class="line">ngx_http_fastdfs_set pid=126276</span><br></pre></td></tr></table></figure></p><h4 id="遇到的错误"><a href="#遇到的错误" class="headerlink" title="遇到的错误"></a>遇到的错误</h4><p>错误：<br><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">ERROR - file: storage_ip_changed_dealer.c, line: 186, connect to tracker server 172.0.0.1:22122 fail, errno: 110, error info: Connection timed out</span><br></pre></td></tr></table></figure></p><p>防火墙中打开tracker服务器端口（ 默认为 22122）<br><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">vi /etc/sysconfig/iptables</span><br></pre></td></tr></table></figure></p><p>附加：若/etc/sysconfig 目录下没有iptables文件可随便写一条iptables命令配置个防火墙规则：如：<br><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">iptables -P OUTPUT ACCEPT</span><br></pre></td></tr></table></figure></p><p>然后用命令：service iptables save 进行保存，默认就保存到 /etc/sysconfig/iptables 文件里。这时既有了这个文件。防火墙也可以启动了。接下来要写策略，也可以直接写在/etc/sysconfig/iptables 里了。<br>添加如下端口行：<br><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">-A INPUT -m state --state NEW -m tcp -p tcp --dport 22122 -j ACCEPT </span><br><span class="line">-A INPUT -m state --state NEW -m tcp -p tcp --dport 23000 -j ACCEPT </span><br><span class="line">-A INPUT -m state --state NEW -m tcp -p tcp --dport 8079 -j ACCEPT </span><br><span class="line"><span class="comment"># 22122 tracker 端口</span></span><br><span class="line"><span class="comment"># 23000 storage 端口</span></span><br><span class="line"><span class="comment"># 8079 nginx listen端口</span></span><br></pre></td></tr></table></figure></p><p>重启防火墙<br><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">service iptables restart</span><br></pre></td></tr></table></figure></p><h4 id="Fastdfs-Client测试"><a href="#Fastdfs-Client测试" class="headerlink" title="Fastdfs Client测试"></a>Fastdfs Client测试</h4><p>执行命令<br><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">/usr/bin/fdfs_test /usr/local/fastdfs/fast_conf/client.conf  upload /usr/local/fastdfs/fast_conf/</span><br></pre></td></tr></table></figure></p><p>输出<br><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">This is FastDFS client <span class="built_in">test</span> program v5.11</span><br><span class="line"></span><br><span class="line">Copyright (C) 2008, Happy Fish / YuQing</span><br><span class="line"></span><br><span class="line">FastDFS may be copied only under the terms of the GNU General</span><br><span class="line">Public License V3, <span class="built_in">which</span> may be found <span class="keyword">in</span> the FastDFS <span class="built_in">source</span> kit.</span><br><span class="line">Please visit the FastDFS Home Page http://www.csource.org/ </span><br><span class="line"><span class="keyword">for</span> more detail.</span><br><span class="line"></span><br><span class="line">[2017-05-16 14:03:07] DEBUG - base_path=/usr/<span class="built_in">local</span>/fastdfs/fast_data, connect_timeout=30, network_timeout=60, tracker_server_count=1, anti_steal_token=0, anti_steal_secret_key length=0, use_connection_pool=0, g_connection_pool_max_idle_time=3600s, use_storage_id=0, storage server id count: 0</span><br><span class="line"></span><br><span class="line">tracker_query_storage_store_list_without_group: </span><br><span class="line">server 1. group_name=, ip_addr=192.168.0.48, port=23000</span><br><span class="line"></span><br><span class="line">group_name=group1, ip_addr=192.168.0.48, port=23000</span><br><span class="line">storage_upload_by_filename</span><br><span class="line">group_name=group1, remote_filename=M00/00/00/wKgAMFkalhuARwsaAAAFvLZ-36489.conf</span><br><span class="line"><span class="built_in">source</span> ip address: 192.168.0.48</span><br><span class="line">file timestamp=2017-05-16 14:03:07</span><br><span class="line">file size=1468</span><br><span class="line">file crc32=3061768110</span><br><span class="line">example file url: http://192.168.0.48/group1/M00/00/00/wKgAMFkalhuARwsaAAAFvLZ-36489.conf</span><br><span class="line">storage_upload_slave_by_filename</span><br><span class="line">group_name=group1, remote_filename=M00/00/00/wKgAMFkalhuARwsaAAAFvLZ-36489_big.conf</span><br><span class="line"><span class="built_in">source</span> ip address: 192.168.0.48</span><br><span class="line">file timestamp=2017-05-16 14:03:08</span><br><span class="line">file size=1468</span><br><span class="line">file crc32=3061768110</span><br><span class="line">example file url: http://192.168.0.48/group1/M00/00/00/wKgAMFkalhuARwsaAAAFvLZ-36489_big.conf</span><br></pre></td></tr></table></figure></p><h4 id="查看Fastdfs集群监控信息"><a href="#查看Fastdfs集群监控信息" class="headerlink" title="查看Fastdfs集群监控信息"></a>查看Fastdfs集群监控信息</h4><p>执行命令<br><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">/usr/bin/fdfs_monitor /usr/local/fastdfs/fast_conf/client.conf</span><br></pre></td></tr></table></figure></p><p>输出<br><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">[2017-05-16 14:17:38] DEBUG - base_path=/usr/<span class="built_in">local</span>/fastdfs/fast_data, connect_timeout=30, network_timeout=60, tracker_server_count=1, anti_steal_token=0, anti_steal_secret_key length=0, use_connection_pool=0, g_connection_pool_max_idle_time=3600s, use_storage_id=0, storage server id count: 0</span><br><span class="line"></span><br><span class="line">server_count=1, server_index=0</span><br><span class="line"></span><br><span class="line">tracker server is 192.168.0.48:22122</span><br><span class="line"></span><br><span class="line">group count: 1</span><br><span class="line"></span><br><span class="line">Group 1:</span><br><span class="line">group name = group1</span><br><span class="line">disk total space = 46161 MB</span><br><span class="line">disk free space = 33446 MB</span><br><span class="line">trunk free space = 0 MB</span><br><span class="line">storage server count = 1</span><br><span class="line">active server count = 1</span><br><span class="line">storage server port = 23000</span><br><span class="line">storage HTTP port = 8888</span><br><span class="line">store path count = 1</span><br><span class="line">subdir count per path = 256</span><br><span class="line">current write server index = 0</span><br><span class="line">current trunk file id = 0</span><br><span class="line"></span><br><span class="line">Storage 1:</span><br><span class="line">id = 192.168.0.48</span><br><span class="line">ip_addr = 192.168.0.48  ACTIVE</span><br><span class="line">http domain = </span><br><span class="line">version = 5.11</span><br><span class="line">join time = 2017-05-16 11:40:48</span><br><span class="line">up time = 2017-05-16 13:04:57</span><br><span class="line">total storage = 46161 MB</span><br><span class="line">free storage = 33446 MB</span><br><span class="line">upload priority = 10</span><br><span class="line">store_path_count = 1</span><br><span class="line">subdir_count_per_path = 256</span><br><span class="line">storage_port = 23000</span><br><span class="line">storage_http_port = 8888</span><br><span class="line">current_write_path = 0</span><br><span class="line"><span class="built_in">source</span> storage id = </span><br><span class="line">if_trunk_server = 0</span><br><span class="line">connection.alloc_count = 256</span><br><span class="line">connection.current_count = 0</span><br><span class="line">connection.max_count = 2</span><br><span class="line">total_upload_count = 3</span><br><span class="line">success_upload_count = 3</span><br><span class="line">total_append_count = 0</span><br><span class="line">success_append_count = 0</span><br><span class="line">total_modify_count = 0</span><br><span class="line">success_modify_count = 0</span><br><span class="line">total_truncate_count = 0</span><br><span class="line">success_truncate_count = 0</span><br><span class="line">total_set_meta_count = 3</span><br><span class="line">success_set_meta_count = 3</span><br><span class="line">total_delete_count = 0</span><br><span class="line">success_delete_count = 0</span><br><span class="line">total_download_count = 0</span><br><span class="line">success_download_count = 0</span><br><span class="line">total_get_meta_count = 0</span><br><span class="line">success_get_meta_count = 0</span><br><span class="line">total_create_link_count = 0</span><br><span class="line">success_create_link_count = 0</span><br><span class="line">total_delete_link_count = 0</span><br><span class="line">success_delete_link_count = 0</span><br><span class="line">total_upload_bytes = 4738</span><br><span class="line">success_upload_bytes = 4738</span><br><span class="line">total_append_bytes = 0</span><br><span class="line">success_append_bytes = 0</span><br><span class="line">total_modify_bytes = 0</span><br><span class="line">success_modify_bytes = 0</span><br><span class="line">stotal_download_bytes = 0</span><br><span class="line">success_download_bytes = 0</span><br><span class="line">total_sync_in_bytes = 0</span><br><span class="line">success_sync_in_bytes = 0</span><br><span class="line">total_sync_out_bytes = 0</span><br><span class="line">success_sync_out_bytes = 0</span><br><span class="line">total_file_open_count = 3</span><br><span class="line">success_file_open_count = 3</span><br><span class="line">total_file_read_count = 0</span><br><span class="line">success_file_read_count = 0</span><br><span class="line">total_file_write_count = 3</span><br><span class="line">success_file_write_count = 3</span><br><span class="line">last_heart_beat_time = 2017-05-16 14:17:31</span><br><span class="line">last_source_update = 2017-05-16 14:14:16</span><br><span class="line">last_sync_update = 1970-01-01 08:00:00</span><br><span class="line">last_synced_timestamp = 1970-01-01 08:00:00</span><br></pre></td></tr></table></figure></p>]]></content>
    
    <summary type="html">
    
      Fastdfs安装说明与常见问题解决
    
    </summary>
    
      <category term="fastdfs" scheme="https://ningyu1.github.io/categories/fastdfs/"/>
    
    
      <category term="fastdfs" scheme="https://ningyu1.github.io/tags/fastdfs/"/>
    
  </entry>
  
  <entry>
    <title>Nginx 502 Bad Gateway问题分析与踩过的坑</title>
    <link href="https://ningyu1.github.io/20170630/03-nginx-502-Bad-Gateway.html"/>
    <id>https://ningyu1.github.io/20170630/03-nginx-502-Bad-Gateway.html</id>
    <published>2017-06-30T10:36:44.000Z</published>
    <updated>2024-04-23T01:48:15.029Z</updated>
    
    <content type="html"><![CDATA[<h3 id="我相信使用Nginx的都会遇到过502-504-这种bad-gateway错误，下面我把碰到这个问题分析过程记录并分享出来。"><a href="#我相信使用Nginx的都会遇到过502-504-这种bad-gateway错误，下面我把碰到这个问题分析过程记录并分享出来。" class="headerlink" title="我相信使用Nginx的都会遇到过502 504 这种bad gateway错误，下面我把碰到这个问题分析过程记录并分享出来。"></a>我相信使用Nginx的都会遇到过502 504 这种bad gateway错误，下面我把碰到这个问题分析过程记录并分享出来。</h3><p>先让我们看一下具体的错误信息<br><figure class="highlight prolog"><table><tr><td class="code"><pre><span class="line"><span class="number">502</span> <span class="symbol">Bad</span> <span class="symbol">Gateway</span></span><br><span class="line"><span class="symbol">The</span> proxy server received an invalid response from an upstream server</span><br></pre></td></tr></table></figure></p><p>从字面上的意思理解，nginx从upstream没有接受到信息，第一感觉就是连接被close？还是超时了？超时的话一般错误信息是 timeout</p><p>下面是尝试解决这个问题尝试过的手段</p><h3 id="1-第一感觉是proxy返回超时，因此查找nginx官方文档，找到关于proxy的timeout设置"><a href="#1-第一感觉是proxy返回超时，因此查找nginx官方文档，找到关于proxy的timeout设置" class="headerlink" title="1. 第一感觉是proxy返回超时，因此查找nginx官方文档，找到关于proxy的timeout设置"></a>1. 第一感觉是proxy返回超时，因此查找nginx官方文档，找到关于proxy的timeout设置</h3><figure class="highlight nginx"><table><tr><td class="code"><pre><span class="line">Syntax:proxy_connect_timeout time;</span><br><span class="line">Default:</span><br><span class="line"><span class="attribute">proxy_connect_timeout</span> <span class="number">60s</span>;</span><br><span class="line">Context:http, server, location</span><br><span class="line"><span class="attribute">Defines</span> a timeout for establishing a connection with a proxied server. It should be noted that this timeout cannot usually exceed <span class="number">75</span> seconds.</span><br></pre></td></tr></table></figure><p>ps. 这个时间不能超过75秒<br><figure class="highlight nginx"><table><tr><td class="code"><pre><span class="line">Syntax:proxy_read_timeout time;</span><br><span class="line">Default:</span><br><span class="line"><span class="attribute">proxy_read_timeout</span> <span class="number">60s</span>;</span><br><span class="line">Context:http, server, location</span><br><span class="line"><span class="attribute">Defines</span> a timeout for reading a response from the proxied server. The timeout is set only between two successive read operations, not for the transmission of the whole response. If the proxied server does not transmit anything within this time, the connection is closed.</span><br></pre></td></tr></table></figure></p><p>ps. 两次read的超时时间，并不是整个的response的超时时间<br><figure class="highlight nginx"><table><tr><td class="code"><pre><span class="line">Syntax:proxy_send_timeout time;</span><br><span class="line">Default:</span><br><span class="line"><span class="attribute">proxy_send_timeout</span> <span class="number">60s</span>;</span><br><span class="line">Context:http, server, location</span><br><span class="line"><span class="attribute">Sets</span> a timeout for transmitting a request to the proxied server. The timeout is set only between two successive write operations, not for the transmission of the whole request. If the proxied server does not receive anything within this time, the connection is closed.</span><br></pre></td></tr></table></figure></p><p>ps. 两次write的超时时间，并不是整个request的超时时间</p><p>配置后重启nginx服务进行测试仍然有502错误爆出，继续分析</p><h3 id="2-于是想到了keepalive，分析我们的请求报文头，报文是有keep-alive的头信息"><a href="#2-于是想到了keepalive，分析我们的请求报文头，报文是有keep-alive的头信息" class="headerlink" title="2. 于是想到了keepalive，分析我们的请求报文头，报文是有keep-alive的头信息"></a>2. 于是想到了keepalive，分析我们的请求报文头，报文是有keep-alive的头信息</h3><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/nginx/2.jpg" alt="site Architecture" title>                </div>                <div class="image-caption">site Architecture</div>            </figure><p>那问题出在哪里？我们应该知道前端请求如果设置为长连接必须要服务端也支持长连接才行，难道是服务器上没有配置长连接导致的？</p><p>翻nginx官网找keepalive的相关配置<br><figure class="highlight nginx"><table><tr><td class="code"><pre><span class="line">Syntax:keepalive_timeout timeout [header_timeout];</span><br><span class="line">Default:</span><br><span class="line"><span class="attribute">keepalive_timeout</span> <span class="number">75s</span>;</span><br><span class="line">Context:http, server, location</span><br><span class="line"><span class="attribute">The</span> first parameter sets a timeout during which a keep-alive client connection will stay open <span class="literal">on</span> the server side. The zero value disables keep-alive client connections. The optional second parameter sets a value in the “Keep-Alive: timeout=time” response header field. Two parameters may differ.</span><br><span class="line"></span><br><span class="line">The “Keep-Alive: timeout=time” header field is recognized by Mozilla and Konqueror. MSIE closes keep-alive connections by itself in about <span class="number">60</span> seconds.</span><br></pre></td></tr></table></figure></p><p>ps.长连接保持的超时时间设置<br><figure class="highlight nginx"><table><tr><td class="code"><pre><span class="line">Syntax:keepalive connections;</span><br><span class="line">Default:—</span><br><span class="line">Context:upstream</span><br><span class="line"><span class="attribute">This</span> directive appeared in version <span class="number">1</span>.<span class="number">1</span>.<span class="number">4</span>.</span><br><span class="line">Activates the cache for connections to upstream servers.</span><br><span class="line"></span><br><span class="line">The connections parameter sets the maximum number of idle keepalive connections to upstream servers that are preserved in the cache of each worker process. When this number is exceeded, the least recently used connections are closed.</span><br></pre></td></tr></table></figure></p><p>ps. 设置upstream长连接的数量</p><p>查看tomcat的keepalive的设置</p><p>keepAliveTimeout：表示在下次请求过来之前，tomcat保持该连接多久。这就是说假如客户端不断有请求过来，且为超过过期时间，则该连接将一直保持。</p><p>maxKeepAliveRequests：表示该连接最大支持的请求数。超过该请求数的连接也将被关闭（此时就会返回一个Connection: close头给客户端）。</p><p>以上设置调整后重启服务进行测试，仍然有502错误爆出，继续分析</p><h3 id="3-在nginx的log中发现了请求都是使用的HTTP-1-0，大家应该知道HTTP-1-0是不支持长连接的，于是顺着这条线继续查下去，为什么请求进来都是HTTP1-0呢？"><a href="#3-在nginx的log中发现了请求都是使用的HTTP-1-0，大家应该知道HTTP-1-0是不支持长连接的，于是顺着这条线继续查下去，为什么请求进来都是HTTP1-0呢？" class="headerlink" title="3. 在nginx的log中发现了请求都是使用的HTTP 1.0，大家应该知道HTTP 1.0是不支持长连接的，于是顺着这条线继续查下去，为什么请求进来都是HTTP1.0呢？"></a>3. 在nginx的log中发现了请求都是使用的HTTP 1.0，大家应该知道HTTP 1.0是不支持长连接的，于是顺着这条线继续查下去，为什么请求进来都是HTTP1.0呢？</h3><p>查看nginx官网的文档，发现proxy是可以只定HTTP版本的<br><figure class="highlight nginx"><table><tr><td class="code"><pre><span class="line">Syntax:proxy_http_version 1.0 | 1.1;</span><br><span class="line">Default:</span><br><span class="line"><span class="attribute">proxy_http_version</span> <span class="number">1</span>.<span class="number">0</span>;</span><br><span class="line">Context:http, server, location</span><br><span class="line"><span class="attribute">This</span> directive appeared in version <span class="number">1</span>.<span class="number">1</span>.<span class="number">4</span>.</span><br><span class="line">Sets the HTTP protocol version for proxying. By default, version <span class="number">1</span>.<span class="number">0</span> is used. Version <span class="number">1</span>.<span class="number">1</span> is recommended for use with keepalive connections and NTLM authentication.</span><br></pre></td></tr></table></figure></p><p>ps. 1.1.4以后的版本nginx默认使用的是HTTP1.0</p><p>于是我们查看一下nginx的版本 nginx -v，我们用的是nginx version: nginx/1.10.1，理论上默认开启的http1.1，不过没关系我们配置一下proxy_http_version 1.1试一下，这个参数要结合上面说道的upstream中的keepalive一起使用才能有效果。</p><p>修改好之后重启服务再次进行测试，依然有502的错误爆出，无解！！！，继续分析，为什么版本不生效呢？</p><p>我们前端请求的报文：</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/nginx/3.jpg" alt="site Architecture" title>                </div>                <div class="image-caption">site Architecture</div>            </figure><p>请求的明明是HTTP 1.1为什么到nginx中成了HTTP 1.0？</p><p>于是想到我们使用了阿里云的SLB，会不会是SLB的问题，先测试一下不通过SLB直接访问，查看日志<br><figure class="highlight prolog"><table><tr><td class="code"><pre><span class="line"><span class="number">100.97</span><span class="number">.90</span><span class="number">.213</span> - - [<span class="number">30</span>/<span class="symbol">Jun</span>/<span class="number">2017</span>:<span class="number">10</span>:<span class="number">44</span>:<span class="number">09</span> +<span class="number">0800</span>] <span class="string">"GET /api/v1/saleorder?dataSource=&amp;salesStatus=01&amp;shopNo=&amp;carrierAssignStatus=&amp;creTimeBegin=</span></span><br><span class="line"><span class="string">2017-06-30+00:00:00&amp;creTimeEnd=2017-06-30+23:59:59&amp;salesNo=&amp;status=&amp;carrierStatus=&amp;warehouseStatus=&amp;dataTranslateStatus=&amp;buyerAccount=&amp;changeBuyer=-1</span></span><br><span class="line"><span class="string">&amp;changeSeller=-1&amp;platformOrderTime=&amp;platformOrderEndTime=&amp;confirmPayTime=&amp;confirmPayEndTime=&amp;receiverMobile=&amp;receiverProvince=&amp;receiverCity=&amp;</span></span><br><span class="line"><span class="string">receiverArea=&amp;referenceNo=&amp;receiverName=&amp;page=1&amp;pageSize=50&amp;__preventCache=1498790641974 HTTP/1.1"</span> <span class="number">200</span> <span class="number">105705</span> <span class="string">"https://erp-uat.jiuyescm.com/"</span> </span><br><span class="line"><span class="string">"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36"</span></span><br></pre></td></tr></table></figure></p><p>日志果然出现了HTTP/1.1，这个让我们找到了希望，但是还有个区别，直接访问走的是ip+port普通的http，slb访问走的是域名而且是ssl，这个会不会跟ssl有关系，于是查询了ssl的http版本支持情况排除了这个问题，那就是继续往SLB上怀疑，翻阿里云负载均衡的说明文档。</p><p>让我找到了说明，查看如下信息</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/nginx/1.jpg" alt="site Architecture" title>                </div>                <div class="image-caption">site Architecture</div>            </figure><p>找了半天原来是SLB强制转换了协议版本，具体查看阿里云负载均衡的<a href="https://help.aliyun.com/knowledge_detail/55201.html#httpversion" title="常见问题" target="_blank" rel="noopener">常见问题</a></p><p>问题没有解决，需要咨询阿里云工作人员看对于这类问题是否有好的解决方法，问题持续跟踪</p><h3 id="跟阿里云客服沟通后，官方人员建议使用长连接通过slb的tcp协议，我们当初为了ssl方便配置slb选择的是http和https，因此就需要修改部署的结构"><a href="#跟阿里云客服沟通后，官方人员建议使用长连接通过slb的tcp协议，我们当初为了ssl方便配置slb选择的是http和https，因此就需要修改部署的结构" class="headerlink" title="跟阿里云客服沟通后，官方人员建议使用长连接通过slb的tcp协议，我们当初为了ssl方便配置slb选择的是http和https，因此就需要修改部署的结构"></a>跟阿里云客服沟通后，官方人员建议使用长连接通过slb的tcp协议，我们当初为了ssl方便配置slb选择的是http和https，因此就需要修改部署的结构</h3><ol><li>删除原有的slb</li><li>增加一个新的slb，协议选择tcp，添加两个端口监听，80-xxxx，443-xxxx</li><li>域名绑定的ip切换到新创建的slb</li><li>nginx中添加ssl-module，添加ssl证书配置，添加http跳转到https，调整80-xxxx，443-xxxx</li><li>重启nginx进行测试<h4 id="测试通过"><a href="#测试通过" class="headerlink" title="测试通过"></a>测试通过</h4><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/nginx/4.jpg" alt="site Architecture" title>                </div>                <div class="image-caption">site Architecture</div>            </figure></li></ol><p>采用这种部署方式来解决nginx 502问题</p>]]></content>
    
    <summary type="html">
    
      Nginx 502 Bad Gateway问题分析与踩过的坑
    
    </summary>
    
      <category term="Nginx" scheme="https://ningyu1.github.io/categories/Nginx/"/>
    
      <category term="Case analysis" scheme="https://ningyu1.github.io/categories/Nginx/Case-analysis/"/>
    
      <category term="trouble shooting" scheme="https://ningyu1.github.io/categories/Nginx/Case-analysis/trouble-shooting/"/>
    
    
      <category term="Nginx" scheme="https://ningyu1.github.io/tags/Nginx/"/>
    
      <category term="Bad Gateway" scheme="https://ningyu1.github.io/tags/Bad-Gateway/"/>
    
      <category term="调优" scheme="https://ningyu1.github.io/tags/%E8%B0%83%E4%BC%98/"/>
    
  </entry>
  
  <entry>
    <title>Cache设计和使用上的套路</title>
    <link href="https://ningyu1.github.io/20170602/05-cache-design.html"/>
    <id>https://ningyu1.github.io/20170602/05-cache-design.html</id>
    <published>2017-06-02T06:06:34.000Z</published>
    <updated>2024-04-23T01:48:15.029Z</updated>
    
    <content type="html"><![CDATA[<h1 id="一、管道（pipeline）提升效率"><a href="#一、管道（pipeline）提升效率" class="headerlink" title="一、管道（pipeline）提升效率"></a>一、管道（pipeline）提升效率</h1><p>Redis是一个cs模式的tcp server，使用和http类似的请求响应协议。一个client可以通过一个socket连接发起多个请求命令。每个请求命令发出后client通常会阻塞并等待redis服务处理，redis处理完后请求命令后会将结果通过响应报文返回给client。每执行一个命令需要2个tcp报文才能完成，由于通信会有网络延迟,假如从client和server之间的包传输时间需要0.125秒，那么执行四个命令8个报文至少会需要1秒才能完成，这样即使redis每秒能处理100k命令，而我们的client也只能一秒钟发出四个命令。这显示没有充分利用 redis的处理能力。因此我们需要使用管道（pipeline）的方式从client打包多条命令一起发出，不需要等待单条命令的响应返回，而redis服务端会处理完多条命令后会将多条命令的处理结果打包到一起返回给客户端（它能够让（多条）执行命令简单的，更加快速的发送给服务器，但是没有任何原子性的保证）<a href="https://redis.io/topics/pipelining" title="https://redis.io/topics/pipelining" target="_blank" rel="noopener">官方资料</a></p><p>【反例】</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/cache/1.jpg" alt="cache1" title>                </div>                <div class="image-caption">cache1</div>            </figure><p>【正例】<br><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">//管道，批量发送多条命令，但是不支持namespace需要手动添加namespace</span><br><span class="line">Pipeline pipelined = redisClient.pipelined();</span><br><span class="line">pipelined.set(key, value);</span><br><span class="line">pipelined.get(key);</span><br><span class="line">pipelined.syncAndReturnAll(); //发送命令并接受返回值</span><br><span class="line">pipelined.sync();//发送命令不接受返回值</span><br></pre></td></tr></table></figure></p><p>使用管道注意事项：</p><ol><li>tcp报文过长会被拆分。</li><li>如果使用pipeline服务器会被迫使用内存队列来发送应答（服务器会在处理完命令前先缓存所有的命令处理结果）</li><li>打包的命令越多，缓存消耗内存也越多，所以并不是打包命令越多越好，需要结合测试找到合适我们业务场景的量（双刃剑）</li><li>不保证原子性，因此在Redis中没有数据需要走DB获取数据，Redis也支持事务（multi、watch）但是会影响性能（没有事务和有事务相差还是蛮大的），不是非要强一致的场景请不要使用。</li></ol><h1 id="二、连接池使用问题"><a href="#二、连接池使用问题" class="headerlink" title="二、连接池使用问题"></a>二、连接池使用问题</h1><p>jedis客户端2.4版本以上对连接池资源使用上进行了优化，提供了更优雅的资源回收方法并且支持broken处理，提供close方法替换原来的回收资源方法（returnBrokenResource 、returnResource）</p><p>【反例】</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/cache/2.jpg" alt="cache2" title>                </div>                <div class="image-caption">cache2</div>            </figure><p>【正例】</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/cache/3.jpg" alt="cache3" title>                </div>                <div class="image-caption">cache3</div>            </figure><h1 id="三、使用key值前缀来作命名空间"><a href="#三、使用key值前缀来作命名空间" class="headerlink" title="三、使用key值前缀来作命名空间"></a>三、使用key值前缀来作命名空间</h1><p>虽然说Redis支持多个数据库（默认32个，可以配置更多），但是除了默认的0号库以外，其它的都需要通过一个额外请求才能使用。所以用前缀作为命名空间可能会更明智一点。另外，在使用前缀作为命名空间区隔不同key的时候，最好在程序中使用全局配置来实现，直接在代码里写前缀的做法要严格避免，这样可维护性实在太差了。</p><p>命名分割符使用 “.” 分隔</p><p>【正例】</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/cache/4.jpg" alt="cache4" title>                </div>                <div class="image-caption">cache4</div>            </figure><h1 id="四、expire对于key过期时间来控制垃圾回收"><a href="#四、expire对于key过期时间来控制垃圾回收" class="headerlink" title="四、expire对于key过期时间来控制垃圾回收"></a>四、expire对于key过期时间来控制垃圾回收</h1><p>Redis是一个提供持久化功能的内存数据库，如果你不指定上面值的过期时间（TTL），并且也不进行定期的清理工作，那么你的Redis内存占用会越来越大，当有一天它超过了系统可用内存，那么swap上场，离性能陡降的时间就不远了。所以在Redis中保存数据时，一定要预先考虑好数据的生命周期，这有很多方法可以实现。</p><p>比如你可以采用Redis自带的过期时间（setEX）为你的数据设定过期时间。但是自动过期有一个问题，很有可能导致你还有大量内存可用时，就让key过期去释放内存，或者是内存已经不足了key还没有过期。</p><p>（LRU）如果你想更精准的控制你的数据过期，你可以用一个ZSET来维护你的数据更新程度，你可以用时间戳作为score值，每次更新操作时更新一下score，这样你就得到了一个按更新时间排序序列串，你可以轻松地找到最老的数据，并且从最老的数据开始进行删除，一直删除到你的空间足够为止。</p><p>【正例】<br><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">redisClient.setex(bizkey, 60, value);//set一个key并设置ttl60秒</span><br></pre></td></tr></table></figure></p><h1 id="五、乱用（不要有个锤子看哪都是钉子）"><a href="#五、乱用（不要有个锤子看哪都是钉子）" class="headerlink" title="五、乱用（不要有个锤子看哪都是钉子）"></a>五、乱用（不要有个锤子看哪都是钉子）</h1><p>当你使用Redis构建你的服务的时候，一定要记住，你只是找了一个合适的工具来实现你需要的功能。而不是说你在用Redis构建一个服务，这是很不同的，你把Redis当作你很多工具中的一个，只在合适使用的时候再使用它，在不合适的时候选择其它的方法。</p><p>我们对它的定位更多是Cache服务而非DB</p><h1 id="六、缓存设计的误区"><a href="#六、缓存设计的误区" class="headerlink" title="六、缓存设计的误区"></a>六、缓存设计的误区</h1><p>我们通常是这样设计的，应用程序先从cache取数据，没有得到，则从数据库中取数据，成功后，放到缓存中。</p><p>那试想一下，如果取出来的null，需不需要放入cache呢？答案当然是需要的。</p><p>我们试想一下如果取出为null不放入cache会有什么结果？很显然每次取cache没有走db返回null，很容易让攻击者利用这个漏洞搞垮你的服务器，利用洪水攻击让你的程序夯在这个地方导致你的正常流程抢不到资源。</p><h1 id="七、缓存更新的问题"><a href="#七、缓存更新的问题" class="headerlink" title="七、缓存更新的问题"></a>七、缓存更新的问题</h1><p><span style="color:blue"><strong><em>以下内容摘自<a href="https://coolshell.cn/articles/17416.html" target="_blank" rel="noopener">酷壳-COOLSHELL的文章《缓存更新的套路》</a></em></strong></span></p><p>很多人在写更新缓存数据代码时，先删除缓存，然后再更新数据库，而后续的操作会把数据再装载的缓存中。然而，这个是逻辑是错误的。试想，两个并发操作，一个是更新操作，另一个是查询操作，更新操作删除缓存后，查询操作没有命中缓存，先把老数据读出来后放到缓存中，然后更新操作更新了数据库。于是，在缓存中的数据还是老的数据，导致缓存中的数据是脏的，而且还一直这样脏下去了。</p><p>正确更新缓存的Design Pattern有四种：Cache aside, Read through, Write through, Write behind caching</p><p>Cache Aside Pattern</p><p>这是最常用最常用的pattern了。其具体逻辑如下：</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/cache/Cache-Aside-Design-Pattern-Flow-Diagram.jpg" alt="cache5" title>                </div>                <div class="image-caption">cache5</div>            </figure><p>失效：应用程序先从cache取数据，没有得到，则从数据库中取数据，成功后，放到缓存中。</p><p>命中：应用程序从cache中取数据，取到后返回。</p><p>更新：先把数据存到数据库中，成功后，再让缓存失效。</p><p>注意，我们的更新是先更新数据库，成功后，让缓存失效。那么，这种方式是否可以没有文章前面提到过的那个问题呢？我们可以脑补一下。</p><p>一个是查询操作，一个是更新操作的并发，首先，没有了删除cache数据的操作了，而是先更新了数据库中的数据，此时，缓存依然有效，所以，并发的查询操作拿的是没有更新的数据，但是，更新操作马上让缓存的失效了，后续的查询操作再把数据从数据库中拉出来。而不会像文章开头的那个逻辑产生的问题，后续的查询操作一直都在取老的数据。</p><p>这是标准的design pattern，包括Facebook的论文<a href="https://www.usenix.org/system/files/conference/nsdi13/nsdi13-final170_update.pdf" title="《Scaling Memcache at Facebook》" target="_blank" rel="noopener">《Scaling Memcache at Facebook》</a>也使用了这个策略。为什么不是写完数据库后更新缓存？你可以看一下Quora上的这个问答<a href="https://www.quora.com/Why-does-Facebook-use-delete-to-remove-the-key-value-pair-in-Memcached-instead-of-updating-the-Memcached-during-write-request-to-the-backend" title="《Why does Facebook use delete to remove the key-value pair in Memcached instead of updating the Memcached during write request to the backend?》" target="_blank" rel="noopener">《Why does Facebook use delete to remove the key-value pair in Memcached instead of updating the Memcached during write request to the backend?》</a>，主要是怕两个并发的写操作导致脏数据</p><p>那么，是不是Cache Aside这个就不会有并发问题了？不是的，比如，一个是读操作，但是没有命中缓存，然后就到数据库中取数据，此时来了一个写操作，写完数据库后，让缓存失效，然后，之前的那个读操作再把老的数据放进去，所以，会造成脏数据。</p><p>但，这个case理论上会出现，不过，实际上出现的概率可能非常低，因为这个条件需要发生在读缓存时缓存失效，而且并发着有一个写操作。而实际上数据库的写操作会比读操作慢得多，而且还要锁表，而读操作必需在写操作前进入数据库操作，而又要晚于写操作更新缓存，所有的这些条件都具备的概率基本并不大。</p><p>所以，这也就是Quora上的那个答案里说的，要么通过2PC或是Paxos协议保证一致性，要么就是拼命的降低并发时脏数据的概率，而Facebook使用了这个降低概率的玩法，因为2PC太慢，而Paxos太复杂。当然，最好还是为缓存设置上过期时间。</p><p>Read/Write Through Pattern</p><p>我们可以看到，在上面的Cache Aside套路中，我们的应用代码需要维护两个数据存储，一个是缓存（Cache），一个是数据库（Repository）。所以，应用程序比较啰嗦。而Read/Write Through套路是把更新数据库（Repository）的操作由缓存自己代理了，所以，对于应用层来说，就简单很多了。可以理解为，应用认为后端就是一个单一的存储，而存储自己维护自己的Cache。</p><p>Read Through</p><p>Read Through 套路就是在查询操作中更新缓存，也就是说，当缓存失效的时候（过期或LRU换出），Cache Aside是由调用方负责把数据加载入缓存，而Read Through则用缓存服务自己来加载，从而对应用方是透明的。</p><p>Write Through</p><p>Write Through 套路和Read Through相仿，不过是在更新数据时发生。当有数据更新的时候，如果没有命中缓存，直接更新数据库，然后返回。如果命中了缓存，则更新缓存，然后再由Cache自己更新数据库（这是一个同步操作）</p><p>下图自来Wikipedia的<a href="https://en.wikipedia.org/wiki/Cache_&#40;computing&#41;" title="Cache词条" target="_blank" rel="noopener">Cache词条</a>。其中的Memory你可以理解为就是我们例子里的数据库。</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/cache/Write-through_with_no-write-allocation.jpg" alt="cache6" title>                </div>                <div class="image-caption">cache6</div>            </figure><p>Write Behind Caching Pattern</p><p>Write Behind 又叫 Write Back。一些了解Linux操作系统内核的同学对write back应该非常熟悉，这不就是Linux文件系统的Page Cache的算法吗？是的，你看基础这玩意全都是相通的。所以，基础很重要，我已经不是一次说过基础很重要这事了。</p><p>Write Back套路，一句说就是，在更新数据的时候，只更新缓存，不更新数据库，而我们的缓存会异步地批量更新数据库。这个设计的好处就是让数据的I/O操作飞快无比（因为直接操作内存嘛 ），因为异步，write backg还可以合并对同一个数据的多次操作，所以性能的提高是相当可观的。</p><p>但是，其带来的问题是，数据不是强一致性的，而且可能会丢失（我们知道Unix/Linux非正常关机会导致数据丢失，就是因为这个事）。在软件设计上，我们基本上不可能做出一个没有缺陷的设计，就像算法设计中的时间换空间，空间换时间一个道理，有时候，强一致性和高性能，高可用和高性性是有冲突的。软件设计从来都是取舍Trade-Off。</p><p>另外，Write Back实现逻辑比较复杂，因为他需要track有哪数据是被更新了的，需要刷到持久层上。操作系统的write back会在仅当这个cache需要失效的时候，才会被真正持久起来，比如，内存不够了，或是进程退出了等情况，这又叫lazy write。</p><p>在wikipedia上有一张write back的流程图，基本逻辑如下：</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/cache/Write-back_with_write-allocation.jpg" alt="cache7" title>                </div>                <div class="image-caption">cache7</div>            </figure>]]></content>
    
    <summary type="html">
    
      Cache设计和使用上的套路，Redis使用注意事项
    
    </summary>
    
      <category term="Redis" scheme="https://ningyu1.github.io/categories/Redis/"/>
    
    
      <category term="Redis" scheme="https://ningyu1.github.io/tags/Redis/"/>
    
      <category term="Cache" scheme="https://ningyu1.github.io/tags/Cache/"/>
    
  </entry>
  
  <entry>
    <title>ActiveMQ使用经验分享，配置详解</title>
    <link href="https://ningyu1.github.io/20170511/06-activemq-settings.html"/>
    <id>https://ningyu1.github.io/20170511/06-activemq-settings.html</id>
    <published>2017-05-11T04:03:10.000Z</published>
    <updated>2024-04-23T01:48:15.029Z</updated>
    
    <content type="html"><![CDATA[<p>根据我们的使用场景抽取出来了一系列activemq公共配置参数mq.properties</p><h2 id="mq-properties"><a href="#mq-properties" class="headerlink" title="mq.properties"></a>mq.properties</h2><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">activemq.connnect.brokerurl=failover:(tcp://192.168.0.66:61616)</span><br><span class="line">activemq.connnect.useAsyncSend=true</span><br><span class="line"># object对象接受报名单,true不受限制,false需要设置白名单</span><br><span class="line">activemq.connnect.trustAllPackages=true</span><br><span class="line"> </span><br><span class="line"># 最大连接数</span><br><span class="line">activemq.pool.maxConnections=20</span><br><span class="line"># 空闲失效时间,毫秒</span><br><span class="line">activemq.pool.idleTimeout=60000</span><br><span class="line"> </span><br><span class="line"># 初始数量</span><br><span class="line">activemq.listener.pool.corePoolSize=5</span><br><span class="line">activemq.listener.pool.maxPoolSize=10</span><br><span class="line"># 启动守护进程</span><br><span class="line">activemq.listener.pool.daemon=true</span><br><span class="line"># 单位秒</span><br><span class="line">activemq.listener.pool.keepAliveSeconds=120</span><br><span class="line"></span><br><span class="line"># 由于jms:listener-container不支持propertyPlaceholder替换，因此这些参数值写在spring-mq.xml文件中，参考值</span><br><span class="line"># </span><br><span class="line"># 接收消息时的超时时间,单位毫秒</span><br><span class="line">activemq.consumer.receiveTimeout=60000</span><br><span class="line"># 监听目标类型</span><br><span class="line">activemq.listener.destinationtype=queue</span><br><span class="line"># 监听确认消息方式</span><br><span class="line">activemq.listener.acknowledge=auto</span><br><span class="line"># 监听数量</span><br><span class="line">activemq.listener.concurrency=2-10</span><br></pre></td></tr></table></figure><h2 id="spring-mq-xml"><a href="#spring-mq-xml" class="headerlink" title="spring-mq.xml"></a>spring-mq.xml</h2><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;</span><br><span class="line">&lt;beans xmlns=&quot;http://www.springframework.org/schema/beans&quot;</span><br><span class="line">    xmlns:context=&quot;http://www.springframework.org/schema/context&quot;</span><br><span class="line">    xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot; xmlns:amq=&quot;http://activemq.apache.org/schema/core&quot;</span><br><span class="line">    xmlns:jms=&quot;http://www.springframework.org/schema/jms&quot;</span><br><span class="line">    xsi:schemaLocation=&quot;http://www.springframework.org/schema/beans   </span><br><span class="line">        http://www.springframework.org/schema/beans/spring-beans-4.0.xsd   </span><br><span class="line">        http://www.springframework.org/schema/context   </span><br><span class="line">        http://www.springframework.org/schema/context/spring-context-4.0.xsd</span><br><span class="line">        http://www.springframework.org/schema/jms</span><br><span class="line">        http://www.springframework.org/schema/jms/spring-jms-4.0.xsd</span><br><span class="line">        http://activemq.apache.org/schema/core</span><br><span class="line">        http://activemq.apache.org/schema/core/activemq-core-5.8.0.xsd&quot;&gt;</span><br><span class="line"> </span><br><span class="line">    &lt;!-- 配置activeMQ连接 tcp://192.168.0.66:61616 --&gt;</span><br><span class="line">    &lt;bean id=&quot;targetConnectionFactory&quot; class=&quot;org.apache.activemq.ActiveMQConnectionFactory&quot;&gt;</span><br><span class="line">        &lt;property name=&quot;brokerURL&quot; value=&quot;$&#123;activemq.connnect.brokerurl&#125;&quot; /&gt;</span><br><span class="line">        &lt;!-- useAsyncSend 异步发送 --&gt;</span><br><span class="line">        &lt;property name=&quot;useAsyncSend&quot; value=&quot;$&#123;activemq.connnect.useAsyncSend&#125;&quot;&gt;&lt;/property&gt;</span><br><span class="line">        &lt;!-- 关闭对象传输有白名单限制 --&gt;</span><br><span class="line">        &lt;property name=&quot;trustAllPackages&quot; value=&quot;$&#123;activemq.connnect.trustAllPackages&#125;&quot;&gt;&lt;/property&gt;</span><br><span class="line">    &lt;/bean&gt;</span><br><span class="line"> </span><br><span class="line">    &lt;!-- 通过往PooledConnectionFactory注入一个ActiveMQConnectionFactory可以用来将Connection，Session和MessageProducer池化 </span><br><span class="line">        这样可以大大减少我们的资源消耗， --&gt;</span><br><span class="line">    &lt;bean id=&quot;pooledConnectionFactory&quot; class=&quot;org.apache.activemq.pool.PooledConnectionFactory&quot;&gt;</span><br><span class="line">        &lt;property name=&quot;connectionFactory&quot; ref=&quot;targetConnectionFactory&quot; /&gt;</span><br><span class="line">        &lt;property name=&quot;maxConnections&quot; value=&quot;$&#123;activemq.pool.maxConnections&#125;&quot; /&gt;</span><br><span class="line">        &lt;property name=&quot;idleTimeout&quot; value=&quot;$&#123;activemq.pool.idleTimeout&#125;&quot; /&gt;</span><br><span class="line">        &lt;!-- maximumActiveSessionPerConnection : 500  每个连接中使用的最大活动会话数 --&gt;</span><br><span class="line">        &lt;!-- idleTimeout : 30 * 1000 单位毫秒 --&gt;</span><br><span class="line">        &lt;!-- blockIfSessionPoolIsFull : true --&gt;</span><br><span class="line">        &lt;!-- blockIfSessionPoolIsFullTimeout : -1L --&gt;</span><br><span class="line">        &lt;!-- expiryTimeout : 0L --&gt;</span><br><span class="line">        &lt;!-- createConnectionOnStartup : true --&gt;</span><br><span class="line">        &lt;!-- useAnonymousProducers : true --&gt;</span><br><span class="line">        &lt;!-- reconnectOnException : true --&gt;</span><br><span class="line">        &lt;!-- maxConnections : 默认1 --&gt;</span><br><span class="line">        &lt;!-- timeBetweenExpirationCheckMillis : -1 --&gt;</span><br><span class="line">    &lt;/bean&gt;</span><br><span class="line"> </span><br><span class="line">    &lt;!-- 线程池配置 --&gt;</span><br><span class="line">    &lt;bean id=&quot;queueMessagee x e cutor&quot;</span><br><span class="line">        class=&quot;org.springframework.scheduling.concurrent.ThreadPoolTaske x e cutor&quot;&gt;</span><br><span class="line">        &lt;property name=&quot;corePoolSize&quot; value=&quot;$&#123;activemq.listener.pool.corePoolSize&#125;&quot; /&gt;</span><br><span class="line">        &lt;property name=&quot;maxPoolSize&quot; value=&quot;$&#123;activemq.listener.pool.maxPoolSize&#125;&quot; /&gt;</span><br><span class="line">        &lt;property name=&quot;daemon&quot; value=&quot;$&#123;activemq.listener.pool.daemon&#125;&quot; /&gt;</span><br><span class="line">        &lt;property name=&quot;keepAliveSeconds&quot; value=&quot;$&#123;activemq.listener.pool.keepAliveSeconds&#125;&quot; /&gt;</span><br><span class="line">    &lt;/bean&gt;</span><br><span class="line"> </span><br><span class="line">    &lt;!-- 定义JmsTemplate的Queue类型 --&gt;</span><br><span class="line">    &lt;bean id=&quot;jmsTemplate&quot; class=&quot;org.springframework.jms.core.JmsTemplate&quot;&gt;</span><br><span class="line">        &lt;constructor-arg ref=&quot;pooledConnectionFactory&quot; /&gt;</span><br><span class="line">        &lt;!-- deliveryMode : PERSISTENT 默认保存消息 --&gt;</span><br><span class="line">        &lt;!-- messageIdEnabled : true 默认有消息id --&gt;</span><br><span class="line">        &lt;!-- messageTimestampEnabled : true 默认有消息发送时间 --&gt;</span><br><span class="line">        &lt;!-- pubSubNoLocal : false,默认点对点(Queues) --&gt;</span><br><span class="line">        &lt;!-- receiveTimeout : 0 阻塞接收不超时,接收消息时的超时时间,单位毫秒  --&gt;</span><br><span class="line">        &lt;!-- deliveryDelay : 0  --&gt;</span><br><span class="line">        &lt;!-- explicitQosEnabled : false  --&gt;</span><br><span class="line">        &lt;!-- priority : 4  --&gt;</span><br><span class="line">        &lt;!-- timeToLive : 0  --&gt;</span><br><span class="line">        &lt;!-- pubSubDomain : false  --&gt;</span><br><span class="line">        &lt;!-- defaultDestination : 默认目标，默认null  --&gt;</span><br><span class="line">        &lt;!-- messageConverter : 消息转换器，默认SimpleMessageConverter  --&gt;</span><br><span class="line">        &lt;!-- sessionTransacted : 事务控制，默认false  --&gt;</span><br><span class="line">    &lt;/bean&gt;</span><br><span class="line"> </span><br><span class="line">    &lt;!-- 定义Queue监听器 --&gt;</span><br><span class="line">    &lt;!-- 由于jms:listener-container不支持propertyPlaceholder替换，因此这些参数值写在spring-mq.xml文件中，参考值：mq.properties文件中 --&gt;</span><br><span class="line">    &lt;jms:listener-container task-e x e cutor=&quot;queueMessagee x e cutor&quot; receive-timeout=&quot;60000&quot;</span><br><span class="line">        destination-type=&quot;queue&quot; container-type=&quot;default&quot; connection-factory=&quot;pooledConnectionFactory&quot;</span><br><span class="line">        acknowledge=&quot;auto&quot; concurrency=&quot;2-10&quot; &gt;</span><br><span class="line">        &lt;jms:listener destination=&quot;QUEUE.EMAIL&quot; ref=&quot;mailMessageListener&quot; /&gt;</span><br><span class="line">        &lt;jms:listener destination=&quot;QUEUE.SMS&quot; ref=&quot;smsMessageListener&quot; /&gt;</span><br><span class="line">    &lt;/jms:listener-container&gt;</span><br><span class="line"> </span><br><span class="line">    &lt;bean id=&quot;smsMessageListener&quot;</span><br><span class="line">        class=&quot;org.springframework.jms.listener.adapter.MessageListenerAdapter&quot;&gt;</span><br><span class="line">        &lt;!-- 默认调用方法handleMessage --&gt;</span><br><span class="line">        &lt;property name=&quot;delegate&quot;&gt;</span><br><span class="line">            &lt;bean class=&quot;com.domain.framework.message.sms.listener.SMSMessageListener&quot; /&gt;</span><br><span class="line">        &lt;/property&gt;</span><br><span class="line">        &lt;property name=&quot;defaultListenerMethod&quot; value=&quot;receiveMessage&quot;/&gt;</span><br><span class="line">    &lt;/bean&gt;</span><br><span class="line">     </span><br><span class="line">    &lt;bean id=&quot;mailMessageListener&quot;</span><br><span class="line">        class=&quot;org.springframework.jms.listener.adapter.MessageListenerAdapter&quot;&gt;</span><br><span class="line">        &lt;!-- 默认调用方法handleMessage --&gt;</span><br><span class="line">        &lt;property name=&quot;delegate&quot;&gt;</span><br><span class="line">            &lt;bean class=&quot;com.domain.framework.message.mail.listener.EmailMessageListener&quot; /&gt;</span><br><span class="line">        &lt;/property&gt;</span><br><span class="line">        &lt;property name=&quot;defaultListenerMethod&quot; value=&quot;receiveMessage&quot;/&gt;</span><br><span class="line">    &lt;/bean&gt;</span><br><span class="line"> </span><br><span class="line">&lt;/beans&gt;</span><br></pre></td></tr></table></figure><h2 id="配置说明"><a href="#配置说明" class="headerlink" title="配置说明"></a>配置说明</h2><ol><li>trustAllPackages<ol><li>等于false时，在做object序列化时会有Class Not Found Exception：This class is not trusted to be serialized as ObjectMessage payload异常抛出，是因为activemq服务器默认是不接受object序列化对象，需要配置白名单（接受的object对象class全名）</li><li>等于true时关闭验证</li><li>传输对象安全说明: <a href="http://activemq.apache.org/objectmessage.htm" title="http://activemq.apache.org/objectmessage.htm" target="_blank" rel="noopener">http://activemq.apache.org/objectmessage.htm</a></li></ol></li><li>useAsyncSend<ol><li>开启异步消息发送，主要是一个性能上的提升从而提升消息吞吐量，但是不能拿到消息发送后的回执消息，消息不会丢失</li><li>异步发送的说明：<a href="http://activemq.apache.org/async-sends.html" title="http://activemq.apache.org/async-sends.html" target="_blank" rel="noopener">http://activemq.apache.org/async-sends.html</a></li></ol></li><li><p>executor corePoolSize</p><ol><li><p>该值的配置需要结合listener的个数和concurrency的数量去灵活配置</p><p>案例分析</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">&lt;bean id=&quot;queueMessageExecutor&quot;</span><br><span class="line">    class=&quot;org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor&quot;&gt;</span><br><span class="line">    &lt;property name=&quot;corePoolSize&quot; value=&quot;2&quot; /&gt;</span><br><span class="line">    &lt;property name=&quot;maxPoolSize&quot; value=&quot;10&quot; /&gt;</span><br><span class="line">    &lt;property name=&quot;daemon&quot; value=&quot;true&quot; /&gt;</span><br><span class="line">    &lt;property name=&quot;keepAliveSeconds&quot; value=&quot;120&quot; /&gt;</span><br><span class="line">&lt;/bean&gt;</span><br><span class="line">&lt;jms:listener-container task-executor=&quot;queueMessageExecutor&quot; receive-timeout=&quot;60000&quot;</span><br><span class="line">    destination-type=&quot;queue&quot; container-type=&quot;default&quot; connection-factory=&quot;pooledConnectionFactory&quot;</span><br><span class="line">    acknowledge=&quot;auto&quot; concurrency=&quot;2-10&quot; &gt;</span><br><span class="line">    &lt;jms:listener destination=&quot;QUEUE.EMAIL&quot; ref=&quot;mailMessageListener&quot; /&gt;</span><br><span class="line">    &lt;jms:listener destination=&quot;QUEUE.SMS&quot; ref=&quot;smsMessageListener&quot; /&gt;</span><br><span class="line">&lt;/jms:listener-container&gt;</span><br></pre></td></tr></table></figure><p>项目中有2个listener并且项目希望启动初始每个listener启动2个consumer最大10个consumer，如果e x e cutor corePoolSize配置为2，那么启动后只会给一个listener分配2个consumer，因为e x e cutor pool的初始配置数量不够，见下图</p><p><img src="/img/activemq/1.jpg" alt="activemq1"><br>修改corePoolSize之后</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">&lt;property name=&quot;corePoolSize&quot; value=&quot;5&quot; /&gt;</span><br></pre></td></tr></table></figure><p><img src="/img/activemq/2.jpg" alt="activemq2"></p></li></ol></li><li>executor daemon<ol><li>是否创建守护线程</li><li>设置为true时，在应用程序在紧急关闭时，任然会执行没有完成的runtime线程 </li></ol></li><li>jms:listener-container<ol><li>由于不支持propertyPlaceholder替换，因此这些参数值写在spring-mq.xml文件中，参考值：mq.properties文件中</li><li>destination-type 目标类型（QUEUE, TOPIC, DURABLETOPIC）</li><li>acknowledge 消息确认方式（auto、client、dups-ok、transacted）</li><li>concurrency listener consumer个数</li></ol></li><li>message-converter <ol><li>消息转换器，我们这里不配置特殊的转换器，使用Spring提供的org.springframework.jms.support.converter.SimpleMessageConverter.SimpleMessageConverter()简单转换器，支持对象（String、byte[]、Map、Serializable）</li><li>结合org.springframework.jms.listener.adapter.MessageListenerAdapter做接受消息自动转换对象</li><li>结合org.springframework.jms.core.JmsTemplate使用convertAndSend系列方法对象转换并发送，实现发送消息自动转换。</li><li>我们为什么不使用json做消息转换，因为json转换在反序列话时需要明确序列化Class类型，丢失了消息转换器的通用性。</li></ol></li><li>Listener<ol><li>支持实现JMS接口的类javax.jms.MessageListener，它是一个来自JMS规范的标准化接口，但是你要处理线程。。</li><li>支持Spring SessionAwareMessageListener，这是一个Spring特定的接口，提供对JMS会话对象的访问。 这对于请求 - 响应消息传递非常有用。 只需要注意，你必须做自己的异常处理（即，重写handleListenerException方法，这样异常不会丢失）。</li><li>支持Spring MessageListenerAdapter，这是一个Spring特定接口，允许特定类型的消息处理。 使用此接口可避免代码中任何特定于JMS的依赖关系。</li></ol></li><li><p>MessageListenerAdapter</p><ol><li><p>可以代理任意POJO类，无需实现JMS接口，任意指定回调方法，并且消息转换内置实现，JMS会话默认封装<br>使用示例：<br>消息接收</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">&lt;bean id=&quot;mailMessageListener&quot;</span><br><span class="line">    class=&quot;org.springframework.jms.listener.adapter.MessageListenerAdapter&quot;&gt;</span><br><span class="line">    &lt;!-- 默认调用方法handleMessage --&gt;</span><br><span class="line">    &lt;property name=&quot;delegate&quot;&gt;</span><br><span class="line">        &lt;bean class=&quot;com.domain.framework.message.mail.listener.EmailMessageListener&quot; /&gt;</span><br><span class="line">    &lt;/property&gt;</span><br><span class="line">    &lt;property name=&quot;defaultListenerMethod&quot; value=&quot;receiveMessage&quot;/&gt;</span><br><span class="line">&lt;/bean&gt;</span><br><span class="line"></span><br><span class="line">public class EmailMessageListener &#123;</span><br><span class="line">    public void receiveMessage(EmailMessageVo message) &#123;</span><br><span class="line">        ...someing....</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>消息发送</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">&lt;bean id=&quot;jmsTemplate&quot; class=&quot;org.springframework.jms.core.JmsTemplate&quot;&gt;</span><br><span class="line">    &lt;constructor-arg ref=&quot;pooledConnectionFactory&quot; /&gt;</span><br><span class="line">&lt;/bean&gt;</span><br><span class="line"></span><br><span class="line">@Component(&quot;emailService&quot;)</span><br><span class="line">public class EmailServiceImpl implements IEmailService &#123;</span><br><span class="line">    @Autowired</span><br><span class="line">    private JmsTemplate jmsTemplate;</span><br><span class="line">     </span><br><span class="line">    @Override</span><br><span class="line">    public void sendEmailMessage(EmailMessageVo message) throws BizException &#123;</span><br><span class="line">        if(message != null) &#123;</span><br><span class="line">            jmsTemplate.convertAndSend(QueueNames.EMAIL, message);</span><br><span class="line">        &#125; else &#123;</span><br><span class="line">            logger.warn(&quot;sendEmailMessage() param[message] is null ,can&apos;t send message!&quot;);</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><strong>ps.上面的示例主要是org.springframework.jms.core.JmsTemplate与org.springframework.jms.listener.adapter.MessageListenerAdapter和业务的POJO做消费者的一个结合使用示例，无需关注序列化，发送与接受对象直接使用业务POJO</strong></p></li></ol></li><li>Q名称的命名规则<ol><li>名称我们采用大写字母，多个单词之间分隔符使用“.”,例如：QUEUE.XXX、TOPIC.XXX</li><li>根据产品线或项目名称增加namespace，例如：APP1.QUEUE.XXX、APP2.QUEUE.XXX</li></ol></li><li><p>Active MQ包使用说明</p><ol><li>不要使用activemq-all这个包，这个包打包了依赖（pool源码，spring源码，log4j源码，jms源码），会跟我们的日志框架产生冲突</li><li>我们使用activemq-pool、activemq-client、activemq-broker、spring-jms去替换上面的activemq-all包</li></ol><p><img src="/img/activemq/3.jpg" alt="activemq3"></p></li></ol><p><strong>Spring+Activemq使用配置非常灵活，我们不拘泥于一种形式，如果有更好的经验尽管提出来我们共同努力和进步。</strong></p>]]></content>
    
    <summary type="html">
    
      ActiveMQ使用经验分享，配置详解
    
    </summary>
    
      <category term="ActiveMQ" scheme="https://ningyu1.github.io/categories/ActiveMQ/"/>
    
    
      <category term="MQ" scheme="https://ningyu1.github.io/tags/MQ/"/>
    
      <category term="ActiveMQ" scheme="https://ningyu1.github.io/tags/ActiveMQ/"/>
    
  </entry>
  
  <entry>
    <title>Maven settings.xml详解</title>
    <link href="https://ningyu1.github.io/20170510/07-maven-settings.html"/>
    <id>https://ningyu1.github.io/20170510/07-maven-settings.html</id>
    <published>2017-05-10T02:05:37.000Z</published>
    <updated>2024-04-23T01:48:15.029Z</updated>
    
    <content type="html"><![CDATA[<h2 id="settings-xml有什么用"><a href="#settings-xml有什么用" class="headerlink" title="settings.xml有什么用"></a>settings.xml有什么用</h2><p>从settings.xml的文件名就可以看出，它是用来设置maven参数的配置文件。并且，settings.xml是maven的全局配置文件。而pom.xml文件是所在项目的局部配置。</p><p>Settings.xml中包含类似本地仓储位置、修改远程仓储服务器、认证信息等配置。</p><h2 id="settings-xml文件位置"><a href="#settings-xml文件位置" class="headerlink" title="settings.xml文件位置"></a>settings.xml文件位置</h2><p>全局配置: ${M2_HOME}/conf/settings.xml</p><p>用户配置: user.home/.m2/settings.xmlnote：用户配置优先于全局配置。user.home/.m2/settings.xmlnote：用户配置优先于全局配置。{user.home} 和和所有其他系统属性只能在3.0+版本上使用。请注意windows和Linux使用变量的区别。</p><h2 id="配置优先级"><a href="#配置优先级" class="headerlink" title="配置优先级"></a>配置优先级</h2><p>需要注意的是：局部配置优先于全局配置。</p><p>配置优先级从高到低：pom.xml&gt; user settings &gt; global settings</p><p>如果这些文件同时存在，在应用配置时，会合并它们的内容，如果有重复的配置，优先级高的配置会覆盖优先级低的。</p><p><strong>ps.修改了配置文件最好吧cmd和eclipse重开一下</strong></p><h2 id="settings-xml元素详解"><a href="#settings-xml元素详解" class="headerlink" title="settings.xml元素详解"></a>settings.xml元素详解</h2><h3 id="顶级元素概览"><a href="#顶级元素概览" class="headerlink" title="顶级元素概览"></a>顶级元素概览</h3><p>下面列举了<code>settings.xml</code>中的顶级元素<br><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">&lt;settings xmlns=&quot;http://maven.apache.org/SETTINGS/1.0.0&quot;      xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;      xsi:schemaLocation=&quot;http://maven.apache.org/SETTINGS/1.0.0                          https://maven.apache.org/xsd/settings-1.0.0.xsd&quot;&gt;</span><br><span class="line">  &lt;localRepository/&gt;</span><br><span class="line">  &lt;interactiveMode/&gt;</span><br><span class="line">  &lt;usePluginRegistry/&gt;</span><br><span class="line">  &lt;offline/&gt;</span><br><span class="line">  &lt;pluginGroups/&gt;</span><br><span class="line">  &lt;servers/&gt;</span><br><span class="line">  &lt;mirrors/&gt;</span><br><span class="line">  &lt;proxies/&gt;</span><br><span class="line">  &lt;profiles/&gt;</span><br><span class="line">  &lt;activeProfiles/&gt;</span><br><span class="line">&lt;/settings&gt;</span><br></pre></td></tr></table></figure></p><h3 id="LocalRepository"><a href="#LocalRepository" class="headerlink" title="LocalRepository"></a>LocalRepository</h3><p>作用：该值表示构建系统本地仓库的路径。</p><p>其默认值：~/.m2/repository。<br><code>&lt;localRepository&gt;${user.home}/.m2/repository&lt;/localRepository&gt;</code></p><h3 id="InteractiveMode"><a href="#InteractiveMode" class="headerlink" title="InteractiveMode"></a>InteractiveMode</h3><p>作用：表示maven是否需要和用户交互以获得输入。</p><p>如果maven需要和用户交互以获得输入，则设置成true，反之则应为false。默认为true。<br><code>&lt;interactiveMode&gt;true&lt;/interactiveMode&gt;</code></p><h3 id="UsePluginRegistry"><a href="#UsePluginRegistry" class="headerlink" title="UsePluginRegistry"></a>UsePluginRegistry</h3><p>作用：maven是否需要使用plugin-registry.xml文件来管理插件版本。</p><p>如果需要让maven使用文件~/.m2/plugin-registry.xml来管理插件版本，则设为true。默认为false。<br><code>&lt;usePluginRegistry&gt;false&lt;/usePluginRegistry&gt;</code></p><h3 id="Offline"><a href="#Offline" class="headerlink" title="Offline"></a>Offline</h3><p>作用：表示maven是否需要在离线模式下运行。</p><p>如果构建系统需要在离线模式下运行，则为true，默认为false。</p><p>当由于网络设置原因或者安全因素，构建服务器不能连接远程仓库的时候，该配置就十分有用。<br><code>&lt;offline&gt;false&lt;/offline&gt;</code></p><h3 id="PluginGroups"><a href="#PluginGroups" class="headerlink" title="PluginGroups"></a>PluginGroups</h3><p>作用：当插件的组织id（groupId）没有显式提供时，供搜寻插件组织Id（groupId）的列表。</p><p>该元素包含一个pluginGroup元素列表，每个子元素包含了一个组织Id（groupId）。</p><p>当我们使用某个插件，并且没有在命令行为其提供组织Id（groupId）的时候，Maven就会使用该列表。默认情况下该列表包含了org.apache.maven.plugins和org.codehaus.mojo。<br><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">&lt;settings xmlns=&quot;http://maven.apache.org/SETTINGS/1.0.0&quot; xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;</span><br><span class="line">  xsi:schemaLocation=&quot;http://maven.apache.org/SETTINGS/1.0.0</span><br><span class="line">                      https://maven.apache.org/xsd/settings-1.0.0.xsd&quot;&gt;</span><br><span class="line">  ...</span><br><span class="line">  &lt;pluginGroups&gt;</span><br><span class="line">    &lt;!--plugin的组织Id（groupId） --&gt;</span><br><span class="line">    &lt;pluginGroup&gt;org.codehaus.mojo&lt;/pluginGroup&gt;</span><br><span class="line">  &lt;/pluginGroups&gt;</span><br><span class="line">  ...</span><br><span class="line">&lt;/settings&gt;</span><br></pre></td></tr></table></figure></p><h3 id="Servers"><a href="#Servers" class="headerlink" title="Servers"></a>Servers</h3><p>作用：一般，仓库的下载和部署是在pom.xml文件中的repositories和distributionManagement元素中定义的。然而，一般类似用户名、密码（有些仓库访问是需要安全认证的）等信息不应该在pom.xml文件中配置，这些信息可以配置在settings.xml中。<br><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">&lt;settings xmlns=&quot;http://maven.apache.org/SETTINGS/1.0.0&quot; xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;</span><br><span class="line">  xsi:schemaLocation=&quot;http://maven.apache.org/SETTINGS/1.0.0</span><br><span class="line">                      https://maven.apache.org/xsd/settings-1.0.0.xsd&quot;&gt;</span><br><span class="line">  ...</span><br><span class="line">  &lt;!--配置服务端的一些设置。一些设置如安全证书不应该和pom.xml一起分发。这种类型的信息应该存在于构建服务器上的settings.xml文件中。 --&gt;</span><br><span class="line">  &lt;servers&gt;</span><br><span class="line">    &lt;!--服务器元素包含配置服务器时需要的信息 --&gt;</span><br><span class="line">    &lt;server&gt;</span><br><span class="line">      &lt;!--这是server的id（注意不是用户登陆的id），该id与distributionManagement中repository元素的id相匹配。 --&gt;</span><br><span class="line">      &lt;id&gt;server001&lt;/id&gt;</span><br><span class="line">      &lt;!--鉴权用户名。鉴权用户名和鉴权密码表示服务器认证所需要的登录名和密码。 --&gt;</span><br><span class="line">      &lt;username&gt;my_login&lt;/username&gt;</span><br><span class="line">      &lt;!--鉴权密码 。鉴权用户名和鉴权密码表示服务器认证所需要的登录名和密码。密码加密功能已被添加到2.1.0 +。详情请访问密码加密页面 --&gt;</span><br><span class="line">      &lt;password&gt;my_password&lt;/password&gt;</span><br><span class="line">      &lt;!--鉴权时使用的私钥位置。和前两个元素类似，私钥位置和私钥密码指定了一个私钥的路径（默认是$&#123;user.home&#125;/.ssh/id_dsa）以及如果需要的话，一个密语。将来passphrase和password元素可能会被提取到外部，但目前它们必须在settings.xml文件以纯文本的形式声明。 --&gt;</span><br><span class="line">      &lt;privateKey&gt;$&#123;usr.home&#125;/.ssh/id_dsa&lt;/privateKey&gt;</span><br><span class="line">      &lt;!--鉴权时使用的私钥密码。 --&gt;</span><br><span class="line">      &lt;passphrase&gt;some_passphrase&lt;/passphrase&gt;</span><br><span class="line">      &lt;!--文件被创建时的权限。如果在部署的时候会创建一个仓库文件或者目录，这时候就可以使用权限（permission）。这两个元素合法的值是一个三位数字，其对应了unix文件系统的权限，如664，或者775。 --&gt;</span><br><span class="line">      &lt;filePermissions&gt;664&lt;/filePermissions&gt;</span><br><span class="line">      &lt;!--目录被创建时的权限。 --&gt;</span><br><span class="line">      &lt;directoryPermissions&gt;775&lt;/directoryPermissions&gt;</span><br><span class="line">    &lt;/server&gt;</span><br><span class="line">  &lt;/servers&gt;</span><br><span class="line">  ...</span><br><span class="line">&lt;/settings&gt;</span><br></pre></td></tr></table></figure></p><h3 id="Mirrors"><a href="#Mirrors" class="headerlink" title="Mirrors"></a>Mirrors</h3><p>作用：为仓库列表配置的下载镜像列表。<br><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">&lt;settings xmlns=&quot;http://maven.apache.org/SETTINGS/1.0.0&quot; xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;</span><br><span class="line">  xsi:schemaLocation=&quot;http://maven.apache.org/SETTINGS/1.0.0</span><br><span class="line">                      https://maven.apache.org/xsd/settings-1.0.0.xsd&quot;&gt;</span><br><span class="line">  ...</span><br><span class="line">  &lt;mirrors&gt;</span><br><span class="line">    &lt;!-- 给定仓库的下载镜像。 --&gt;</span><br><span class="line">    &lt;mirror&gt;</span><br><span class="line">      &lt;!-- 该镜像的唯一标识符。id用来区分不同的mirror元素。 --&gt;</span><br><span class="line">      &lt;id&gt;planetmirror.com&lt;/id&gt;</span><br><span class="line">      &lt;!-- 镜像名称 --&gt;</span><br><span class="line">      &lt;name&gt;PlanetMirror Australia&lt;/name&gt;</span><br><span class="line">      &lt;!-- 该镜像的URL。构建系统会优先考虑使用该URL，而非使用默认的服务器URL。 --&gt;</span><br><span class="line">      &lt;url&gt;http://downloads.planetmirror.com/pub/maven2&lt;/url&gt;</span><br><span class="line">      &lt;!-- 被镜像的服务器的id。例如，如果我们要设置了一个Maven中央仓库（http://repo.maven.apache.org/maven2/）的镜像，就需要将该元素设置成central。这必须和中央仓库的id central完全一致。 --&gt;</span><br><span class="line">      &lt;mirrorOf&gt;central&lt;/mirrorOf&gt;</span><br><span class="line">    &lt;/mirror&gt;</span><br><span class="line">  &lt;/mirrors&gt;</span><br><span class="line">  ...</span><br><span class="line">&lt;/settings&gt;</span><br></pre></td></tr></table></figure></p><h3 id="Proxies"><a href="#Proxies" class="headerlink" title="Proxies"></a>Proxies</h3><p>作用：用来配置不同的代理。<br><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">&lt;settings xmlns=&quot;http://maven.apache.org/SETTINGS/1.0.0&quot; xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;</span><br><span class="line">  xsi:schemaLocation=&quot;http://maven.apache.org/SETTINGS/1.0.0</span><br><span class="line">                      https://maven.apache.org/xsd/settings-1.0.0.xsd&quot;&gt;</span><br><span class="line">  ...</span><br><span class="line">  &lt;proxies&gt;</span><br><span class="line">    &lt;!--代理元素包含配置代理时需要的信息 --&gt;</span><br><span class="line">    &lt;proxy&gt;</span><br><span class="line">      &lt;!--代理的唯一定义符，用来区分不同的代理元素。 --&gt;</span><br><span class="line">      &lt;id&gt;myproxy&lt;/id&gt;</span><br><span class="line">      &lt;!--该代理是否是激活的那个。true则激活代理。当我们声明了一组代理，而某个时候只需要激活一个代理的时候，该元素就可以派上用处。 --&gt;</span><br><span class="line">      &lt;active&gt;true&lt;/active&gt;</span><br><span class="line">      &lt;!--代理的协议。 协议://主机名:端口，分隔成离散的元素以方便配置。 --&gt;</span><br><span class="line">      &lt;protocol&gt;http&lt;/protocol&gt;</span><br><span class="line">      &lt;!--代理的主机名。协议://主机名:端口，分隔成离散的元素以方便配置。 --&gt;</span><br><span class="line">      &lt;host&gt;proxy.somewhere.com&lt;/host&gt;</span><br><span class="line">      &lt;!--代理的端口。协议://主机名:端口，分隔成离散的元素以方便配置。 --&gt;</span><br><span class="line">      &lt;port&gt;8080&lt;/port&gt;</span><br><span class="line">      &lt;!--代理的用户名，用户名和密码表示代理服务器认证的登录名和密码。 --&gt;</span><br><span class="line">      &lt;username&gt;proxyuser&lt;/username&gt;</span><br><span class="line">      &lt;!--代理的密码，用户名和密码表示代理服务器认证的登录名和密码。 --&gt;</span><br><span class="line">      &lt;password&gt;somepassword&lt;/password&gt;</span><br><span class="line">      &lt;!--不该被代理的主机名列表。该列表的分隔符由代理服务器指定；例子中使用了竖线分隔符，使用逗号分隔也很常见。 --&gt;</span><br><span class="line">      &lt;nonProxyHosts&gt;*.google.com|ibiblio.org&lt;/nonProxyHosts&gt;</span><br><span class="line">    &lt;/proxy&gt;</span><br><span class="line">  &lt;/proxies&gt;</span><br><span class="line">  ...</span><br><span class="line">&lt;/settings&gt;</span><br></pre></td></tr></table></figure></p><h3 id="Profiles"><a href="#Profiles" class="headerlink" title="Profiles"></a>Profiles</h3><p>作用：根据环境参数来调整构建配置的列表。</p><p>settings.xml中的profile元素是pom.xml中profile元素的裁剪版本。</p><p>它包含了id、activation、repositories、pluginRepositories和 properties元素。这里的profile元素只包含这五个子元素是因为这里只关心构建系统这个整体（这正是settings.xml文件的角色定位），而非单独的项目对象模型设置。如果一个settings.xml中的profile被激活，它的值会覆盖任何其它定义在pom.xml中带有相同id的profile。<br><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">&lt;settings xmlns=&quot;http://maven.apache.org/SETTINGS/1.0.0&quot; xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;</span><br><span class="line">  xsi:schemaLocation=&quot;http://maven.apache.org/SETTINGS/1.0.0</span><br><span class="line">                      https://maven.apache.org/xsd/settings-1.0.0.xsd&quot;&gt;</span><br><span class="line">  ...</span><br><span class="line">  &lt;profiles&gt;</span><br><span class="line">    &lt;profile&gt;</span><br><span class="line">      &lt;!-- profile的唯一标识 --&gt;</span><br><span class="line">      &lt;id&gt;test&lt;/id&gt;</span><br><span class="line">      &lt;!-- 自动触发profile的条件逻辑 --&gt;</span><br><span class="line">      &lt;activation /&gt;</span><br><span class="line">      &lt;!-- 扩展属性列表 --&gt;</span><br><span class="line">      &lt;properties /&gt;</span><br><span class="line">      &lt;!-- 远程仓库列表 --&gt;</span><br><span class="line">      &lt;repositories /&gt;</span><br><span class="line">      &lt;!-- 插件仓库列表 --&gt;</span><br><span class="line">      &lt;pluginRepositories /&gt;</span><br><span class="line">    &lt;/profile&gt;</span><br><span class="line">  &lt;/profiles&gt;</span><br><span class="line">  ...</span><br><span class="line">&lt;/settings&gt;</span><br></pre></td></tr></table></figure></p><h3 id="Activation"><a href="#Activation" class="headerlink" title="Activation"></a>Activation</h3><p>作用：自动触发profile的条件逻辑。</p><p>如pom.xml中的profile一样，profile的作用在于它能够在某些特定的环境中自动使用某些特定的值；这些环境通过activation元素指定。</p><p>activation元素并不是激活profile的唯一方式。settings.xml文件中的activeProfile元素可以包含profile的id。profile也可以通过在命令行，使用-P标记和逗号分隔的列表来显式的激活（如，-P test）。<br><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">&lt;activation&gt;</span><br><span class="line">  &lt;!--profile默认是否激活的标识 --&gt;</span><br><span class="line">  &lt;activeByDefault&gt;false&lt;/activeByDefault&gt;</span><br><span class="line">  &lt;!--当匹配的jdk被检测到，profile被激活。例如，1.4激活JDK1.4，1.4.0_2，而!1.4激活所有版本不是以1.4开头的JDK。 --&gt;</span><br><span class="line">  &lt;jdk&gt;1.5&lt;/jdk&gt;</span><br><span class="line">  &lt;!--当匹配的操作系统属性被检测到，profile被激活。os元素可以定义一些操作系统相关的属性。 --&gt;</span><br><span class="line">  &lt;os&gt;</span><br><span class="line">    &lt;!--激活profile的操作系统的名字 --&gt;</span><br><span class="line">    &lt;name&gt;Windows XP&lt;/name&gt;</span><br><span class="line">    &lt;!--激活profile的操作系统所属家族(如 &apos;windows&apos;) --&gt;</span><br><span class="line">    &lt;family&gt;Windows&lt;/family&gt;</span><br><span class="line">    &lt;!--激活profile的操作系统体系结构 --&gt;</span><br><span class="line">    &lt;arch&gt;x86&lt;/arch&gt;</span><br><span class="line">    &lt;!--激活profile的操作系统版本 --&gt;</span><br><span class="line">    &lt;version&gt;5.1.2600&lt;/version&gt;</span><br><span class="line">  &lt;/os&gt;</span><br><span class="line">  &lt;!--如果Maven检测到某一个属性（其值可以在POM中通过$&#123;name&#125;引用），其拥有对应的name = 值，Profile就会被激活。如果值字段是空的，那么存在属性名称字段就会激活profile，否则按区分大小写方式匹配属性值字段 --&gt;</span><br><span class="line">  &lt;property&gt;</span><br><span class="line">    &lt;!--激活profile的属性的名称 --&gt;</span><br><span class="line">    &lt;name&gt;mavenVersion&lt;/name&gt;</span><br><span class="line">    &lt;!--激活profile的属性的值 --&gt;</span><br><span class="line">    &lt;value&gt;2.0.3&lt;/value&gt;</span><br><span class="line">  &lt;/property&gt;</span><br><span class="line">  &lt;!--提供一个文件名，通过检测该文件的存在或不存在来激活profile。missing检查文件是否存在，如果不存在则激活profile。另一方面，exists则会检查文件是否存在，如果存在则激活profile。 --&gt;</span><br><span class="line">  &lt;file&gt;</span><br><span class="line">    &lt;!--如果指定的文件存在，则激活profile。 --&gt;</span><br><span class="line">    &lt;exists&gt;$&#123;basedir&#125;/file2.properties&lt;/exists&gt;</span><br><span class="line">    &lt;!--如果指定的文件不存在，则激活profile。 --&gt;</span><br><span class="line">    &lt;missing&gt;$&#123;basedir&#125;/file1.properties&lt;/missing&gt;</span><br><span class="line">  &lt;/file&gt;</span><br><span class="line">&lt;/activation&gt;</span><br></pre></td></tr></table></figure></p><p><strong>注：在maven工程的pom.xml所在目录下执行mvn help:active-profiles命令可以查看中央仓储的profile是否在工程中生效。</strong></p><h3 id="properties"><a href="#properties" class="headerlink" title="properties"></a>properties</h3><p>作用：对应profile的扩展属性列表。</p><p>maven属性和ant中的属性一样，可以用来存放一些值。这些值可以在pom.xml中的任何地方使用标记${X}来使用，这里X是指属性的名称。属性有五种不同的形式，并且都能在settings.xml文件中访问。<br><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">&lt;!-- </span><br><span class="line">  1. env.X: 在一个变量前加上&quot;env.&quot;的前缀，会返回一个shell环境变量。例如,&quot;env.PATH&quot;指代了$path环境变量（在Windows上是%PATH%）。 </span><br><span class="line">  2. project.x：指代了POM中对应的元素值。例如: &lt;project&gt;&lt;version&gt;1.0&lt;/version&gt;&lt;/project&gt;通过$&#123;project.version&#125;获得version的值。 </span><br><span class="line">  3. settings.x: 指代了settings.xml中对应元素的值。例如：&lt;settings&gt;&lt;offline&gt;false&lt;/offline&gt;&lt;/settings&gt;通过 $&#123;settings.offline&#125;获得offline的值。 </span><br><span class="line">  4. Java System Properties: 所有可通过java.lang.System.getProperties()访问的属性都能在POM中使用该形式访问，例如 $&#123;java.home&#125;。 </span><br><span class="line">  5. x: 在&lt;properties/&gt;元素中，或者外部文件中设置，以$&#123;someVar&#125;的形式使用。</span><br><span class="line"> --&gt;</span><br><span class="line">&lt;properties&gt;</span><br><span class="line">  &lt;user.install&gt;$&#123;user.home&#125;/our-project&lt;/user.install&gt;</span><br><span class="line">&lt;/properties&gt;</span><br></pre></td></tr></table></figure></p><p><strong>注：如果该profile被激活，则可以在pom.xml中使用${user.install}。</strong></p><h3 id="Repositories"><a href="#Repositories" class="headerlink" title="Repositories"></a>Repositories</h3><p>作用：远程仓库列表，它是maven用来填充构建系统本地仓库所使用的一组远程仓库。<br><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">&lt;repositories&gt;</span><br><span class="line">  &lt;!--包含需要连接到远程仓库的信息 --&gt;</span><br><span class="line">  &lt;repository&gt;</span><br><span class="line">    &lt;!--远程仓库唯一标识 --&gt;</span><br><span class="line">    &lt;id&gt;codehausSnapshots&lt;/id&gt;</span><br><span class="line">    &lt;!--远程仓库名称 --&gt;</span><br><span class="line">    &lt;name&gt;Codehaus Snapshots&lt;/name&gt;</span><br><span class="line">    &lt;!--如何处理远程仓库里发布版本的下载 --&gt;</span><br><span class="line">    &lt;releases&gt;</span><br><span class="line">      &lt;!--true或者false表示该仓库是否为下载某种类型构件（发布版，快照版）开启。 --&gt;</span><br><span class="line">      &lt;enabled&gt;false&lt;/enabled&gt;</span><br><span class="line">      &lt;!--该元素指定更新发生的频率。Maven会比较本地POM和远程POM的时间戳。这里的选项是：always（一直），daily（默认，每日），interval：X（这里X是以分钟为单位的时间间隔），或者never（从不）。 --&gt;</span><br><span class="line">      &lt;updatePolicy&gt;always&lt;/updatePolicy&gt;</span><br><span class="line">      &lt;!--当Maven验证构件校验文件失败时该怎么做-ignore（忽略），fail（失败），或者warn（警告）。 --&gt;</span><br><span class="line">      &lt;checksumPolicy&gt;warn&lt;/checksumPolicy&gt;</span><br><span class="line">    &lt;/releases&gt;</span><br><span class="line">    &lt;!--如何处理远程仓库里快照版本的下载。有了releases和snapshots这两组配置，POM就可以在每个单独的仓库中，为每种类型的构件采取不同的策略。例如，可能有人会决定只为开发目的开启对快照版本下载的支持。参见repositories/repository/releases元素 --&gt;</span><br><span class="line">    &lt;snapshots&gt;</span><br><span class="line">      &lt;enabled /&gt;</span><br><span class="line">      &lt;updatePolicy /&gt;</span><br><span class="line">      &lt;checksumPolicy /&gt;</span><br><span class="line">    &lt;/snapshots&gt;</span><br><span class="line">    &lt;!--远程仓库URL，按protocol://hostname/path形式 --&gt;</span><br><span class="line">    &lt;url&gt;http://snapshots.maven.codehaus.org/maven2&lt;/url&gt;</span><br><span class="line">    &lt;!--用于定位和排序构件的仓库布局类型-可以是default（默认）或者legacy（遗留）。Maven 2为其仓库提供了一个默认的布局；然而，Maven 1.x有一种不同的布局。我们可以使用该元素指定布局是default（默认）还是legacy（遗留）。 --&gt;</span><br><span class="line">    &lt;layout&gt;default&lt;/layout&gt;</span><br><span class="line">  &lt;/repository&gt;</span><br><span class="line">&lt;/repositories&gt;</span><br></pre></td></tr></table></figure></p><h3 id="pluginRepositories"><a href="#pluginRepositories" class="headerlink" title="pluginRepositories"></a>pluginRepositories</h3><p>作用：发现插件的远程仓库列表。</p><p>和repository类似，只是repository是管理jar包依赖的仓库，pluginRepositories则是管理插件的仓库。</p><p>maven插件是一种特殊类型的构件。由于这个原因，插件仓库独立于其它仓库。pluginRepositories元素的结构和repositories元素的结构类似。每个pluginRepository元素指定一个Maven可以用来寻找新插件的远程地址。<br><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">&lt;pluginRepositories&gt;</span><br><span class="line">  &lt;!-- 包含需要连接到远程插件仓库的信息.参见profiles/profile/repositories/repository元素的说明 --&gt;</span><br><span class="line">  &lt;pluginRepository&gt;</span><br><span class="line">    &lt;releases&gt;</span><br><span class="line">      &lt;enabled /&gt;</span><br><span class="line">      &lt;updatePolicy /&gt;</span><br><span class="line">      &lt;checksumPolicy /&gt;</span><br><span class="line">    &lt;/releases&gt;</span><br><span class="line">    &lt;snapshots&gt;</span><br><span class="line">      &lt;enabled /&gt;</span><br><span class="line">      &lt;updatePolicy /&gt;</span><br><span class="line">      &lt;checksumPolicy /&gt;</span><br><span class="line">    &lt;/snapshots&gt;</span><br><span class="line">    &lt;id /&gt;</span><br><span class="line">    &lt;name /&gt;</span><br><span class="line">    &lt;url /&gt;</span><br><span class="line">    &lt;layout /&gt;</span><br><span class="line">  &lt;/pluginRepository&gt;</span><br><span class="line">&lt;/pluginRepositories&gt;</span><br></pre></td></tr></table></figure></p><h3 id="ActiveProfiles"><a href="#ActiveProfiles" class="headerlink" title="ActiveProfiles"></a>ActiveProfiles</h3><p>作用：手动激活profiles的列表，按照profile被应用的顺序定义activeProfile。</p><p>该元素包含了一组activeProfile元素，每个activeProfile都含有一个profile id。任何在activeProfile中定义的profile id，不论环境设置如何，其对应的 profile都会被激活。如果没有匹配的profile，则什么都不会发生。</p><p>例如，env-test是一个activeProfile，则在pom.xml（或者profile.xml）中对应id的profile会被激活。如果运行过程中找不到这样一个profile，Maven则会像往常一样运行。<br><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">&lt;settings xmlns=&quot;http://maven.apache.org/SETTINGS/1.0.0&quot; xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;</span><br><span class="line">  xsi:schemaLocation=&quot;http://maven.apache.org/SETTINGS/1.0.0</span><br><span class="line">                      https://maven.apache.org/xsd/settings-1.0.0.xsd&quot;&gt;</span><br><span class="line">  ...</span><br><span class="line">  &lt;activeProfiles&gt;</span><br><span class="line">    &lt;!-- 要激活的profile id --&gt;</span><br><span class="line">    &lt;activeProfile&gt;env-test&lt;/activeProfile&gt;</span><br><span class="line">  &lt;/activeProfiles&gt;</span><br><span class="line">  ...</span><br><span class="line">&lt;/settings&gt;</span><br></pre></td></tr></table></figure></p>]]></content>
    
    <summary type="html">
    
      Maven settings.xml详解
    
    </summary>
    
      <category term="Maven" scheme="https://ningyu1.github.io/categories/Maven/"/>
    
    
      <category term="Maven" scheme="https://ningyu1.github.io/tags/Maven/"/>
    
      <category term="Settings" scheme="https://ningyu1.github.io/tags/Settings/"/>
    
  </entry>
  
  <entry>
    <title>RESTful设计规范</title>
    <link href="https://ningyu1.github.io/20170221/01-RESTful-design-specifications.html"/>
    <id>https://ningyu1.github.io/20170221/01-RESTful-design-specifications.html</id>
    <published>2017-02-21T03:58:19.000Z</published>
    <updated>2024-04-23T01:48:15.028Z</updated>
    
    <content type="html"><![CDATA[<h1 id="一、-摘要（Abstract）"><a href="#一、-摘要（Abstract）" class="headerlink" title="一、 摘要（Abstract）"></a>一、 摘要（Abstract）</h1><p>RESTful API 已经非常成熟，也得到了大家的认可。我们按照 <a href="https://martinfowler.com/articles/richardsonMaturityModel.html" title="Richardson Maturity Model" target="_blank" rel="noopener">Richardson Maturity Model</a> 对 REST 评价的模型，规范基于 level2 来设计</p><h1 id="二、版本（Versioning）"><a href="#二、版本（Versioning）" class="headerlink" title="二、版本（Versioning）"></a>二、版本（Versioning）</h1><p>API的版本号放入URL。例如：<br><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">https://api.jiuyescm.com/v1/</span><br><span class="line">https://api.jiuyescm.com/v1.2/</span><br></pre></td></tr></table></figure></p><h1 id="三、资源、路径（Endpoint）"><a href="#三、资源、路径（Endpoint）" class="headerlink" title="三、资源、路径（Endpoint）"></a>三、资源、路径（Endpoint）</h1><p>路径，API的具体地址。在REST中，每个地址都代表一个具体的资源（<code>Resource</code>）约定如下：</p><ul><li>路径仅表示资源的路径（位置），尽量不要有actions操作（一些特殊的<code>actions</code>操作除外）</li><li>路径以 复数（名词） 进行命名资源，不管返回单个或者多个资源。</li><li>使用 小写字母、数字以及下划线（“_”） 。（下划线是为了区分多个单词，如user_name）</li><li><p>资源的路径从父到子依次如：</p>  <figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">/&#123;resource&#125;/&#123;resource_id&#125;/&#123;sub_resource&#125;/&#123;sub_resource_id&#125;/&#123;sub_resource_property&#125;</span><br></pre></td></tr></table></figure></li><li><p>使用 <code>?</code> 来进行资源的过滤、搜索以及分页等</p></li><li>使用版本号，且版本号在资源路径之前</li><li>优先使用内容协商来区分表述格式，而不是使用后缀来区分表述格式</li><li>应该放在一个专用的域名下，如：<code>http：//api.jiuyescm.com</code></li><li>使用SSL</li></ul><p>综上，一个API路径可能会是<br><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">https://api.domain.com/v1/&#123;resource&#125;/&#123;resource_id&#125;/&#123;sub_resource&#125;/&#123;sub_resource_id&#125;/&#123;sub_resource_property&#125;</span><br><span class="line">https://api.domain.com /v1/&#123;resource&#125;?page=1&amp;page_size=10</span><br><span class="line">https://api.domain.com /v1/&#123;resource&#125;?name=xx&amp;sortby=name&amp;order=asc</span><br></pre></td></tr></table></figure></p><h1 id="四、操作（HTTP-Actions）"><a href="#四、操作（HTTP-Actions）" class="headerlink" title="四、操作（HTTP Actions）"></a>四、操作（HTTP Actions）</h1><p>用<code>HTTP</code>动词（方法）表示对资源的具体操作。常用的<code>HTTP</code>动词有：<br><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">GET（SELECT）：从服务器取出资源（一项或多项）</span><br><span class="line">POST（CREATE）：在服务器新建一个资源</span><br><span class="line">PUT（UPDATE）：在服务器更新资源（客户端提供改变后的完整资源）  </span><br><span class="line">PATCH（UPDATE）：在服务器更新资源（客户端提供改变的属性） </span><br><span class="line">DELETE（DELETE）：从服务器删除资源</span><br><span class="line">还有两个不常用的HTTP动词</span><br><span class="line">HEAD：获取资源的元数据</span><br><span class="line">OPTIONS：获取信息，关于资源的哪些属性是客户端可以改变的</span><br></pre></td></tr></table></figure></p><p>下面是一些例子</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">GET /users：列出所有用户  </span><br><span class="line">POST /users：新建一个用户  </span><br><span class="line">GET /users/&#123;user_id&#125;：获取某个指定用户的信息  </span><br><span class="line">PUT /users/&#123;user_id&#125;：更新某个指定用户的信息（提供该用户的全部信息）  </span><br><span class="line">PATCH /users/&#123;user_id&#125;：更新某个指定用户的信息（提供该用户的部分信息）  </span><br><span class="line">DELETE /users/&#123;user_id&#125;：删除某个用户  </span><br><span class="line">GET /users/&#123;user_id&#125;/resources：列出某个指定用户的所有权限资源  </span><br><span class="line">DELETE /users/&#123;user_id&#125;/resources/&#123;resources_id&#125;：删除某个指定用户的指定权限资源</span><br></pre></td></tr></table></figure><h1 id="五、数据（Data-Format）"><a href="#五、数据（Data-Format）" class="headerlink" title="五、数据（Data Format）"></a>五、数据（Data Format）</h1><p>数据是对资源的具体描述，分为请求数据和返回数据。约定如下：</p><ul><li>查询，过滤条件使用query string，例如user?name=xxx</li><li>Content body 仅仅用来传输数据</li><li>通过Content-Type指定请求与返回的数据格式。其中请求数据还要指定Accept。（我们暂时只使用Json）</li><li>数据应该拿来就能用，不应该还要进行转换操作</li><li>使用字符串（YYYY-MM-dd hh:mm:ss）格式表达时间字段，例如: 2017-02-20 16:00:00</li><li>数据采用UTF-8编码</li><li>返回的数据应该尽量简单，响应状态应该包含在响应头中</li><li>使用 小写字母、数字以及下划线（“_”） 描述字段，不使用大写描述字段（这个由于使用了一些开源的jar所以这个不强求，比如说pageinfo我们无法修改属性名称）</li><li>建议资源中的唯一标识命名为id（这个不强求，有的唯一标识名称确实比较复杂）</li><li>属性和字符串值必须使用双引号””（这个json转换默认规则）</li><li>建议对每个字段设置默认值（数组型可设置为[],字符串型可设置为””，数值可设置为0，对象可设置为{}）,这一条是为了方便前端/客户端进行判断字段存不存在操作（这样json转换会自动转成相应的字符）</li><li>POST操作应该返回新建的资源；PUT/PATCH操作返回更新后的完整的资源；DELETE返回一个空文档；GET返回资源数组或当个资源</li><li>为了方便以后的扩展兼容，如果返回的是数组，强烈建议用一个包含如items属性的对象进行包裹，如：</li></ul><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">&#123;&quot;items&quot;:[&#123;&#125;,&#123;&#125;]&#125;</span><br></pre></td></tr></table></figure><p>示例：<br><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">POST https://api.domain.com/v1/users</span><br><span class="line">Request</span><br><span class="line">    headers:</span><br><span class="line">        Accept: application/json</span><br><span class="line">        Content-Type: application/json;charset=UTF-8</span><br><span class="line">    body:</span><br><span class="line"> &#123;</span><br><span class="line">            &quot;user_name&quot;: &quot;ZhangSan&quot;,</span><br><span class="line">            &quot;address&quot;: &quot;ujfhysdfsdf&quot;,</span><br><span class="line">         &quot;nick&quot;: &quot;ZS&quot;</span><br><span class="line">     &#125;</span><br><span class="line"></span><br><span class="line">Response</span><br><span class="line">    status: 201 Created</span><br><span class="line">    headers:</span><br><span class="line">        Content-Type: application/json;charset=UTF-8</span><br><span class="line">    body:</span><br><span class="line">        &#123;</span><br><span class="line">           &quot;requestId&quot;: sdfsdflkjoiusdf,</span><br><span class="line">           &quot;code&quot;: &quot;&quot;,</span><br><span class="line">        &quot;message&quot;: &quot;&quot;,</span><br><span class="line">        &quot;items&quot;:</span><br><span class="line">          &#123;</span><br><span class="line">               &quot;id&quot;:&quot;111&quot;,</span><br><span class="line">               &quot;user_name&quot;: &quot;HingKwan&quot;,</span><br><span class="line">               &quot;address&quot;: &quot;ujfhysdfsdf&quot;,</span><br><span class="line">                &quot;nick&quot;: &quot;ZS&quot;</span><br><span class="line">          &#125;</span><br><span class="line">        &#125;</span><br></pre></td></tr></table></figure></p><h1 id="六、安全（Security）"><a href="#六、安全（Security）" class="headerlink" title="六、安全（Security）"></a>六、安全（Security）</h1><h2 id="调用限制"><a href="#调用限制" class="headerlink" title="调用限制"></a>调用限制</h2><p>为了避免请求泛滥，给API设置速度限制很重要。入速度设置之后，可以在HTTP返回头上对返回的信息进行说明，下面是几个必须的返回头（依照twitter的命名规则）<br><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">X-Rate-Limit-Limit :当前时间段允许的并发请求数</span><br><span class="line">X-Rate-Limit-Remaining:当前时间段保留的请求数</span><br><span class="line">X-Rate-Limit-Reset:当前时间段剩余秒数</span><br></pre></td></tr></table></figure></p><p><strong>这个我们一般会在getway中实现</strong></p><h2 id="授权校验"><a href="#授权校验" class="headerlink" title="授权校验"></a>授权校验</h2><p>RESTful API是无状态的也就是说用户请求的鉴权和cookie以及session无关，每一次请求都应该包含鉴权证明。<br>可以使用http请求头Authorization设置授权码; 必须使用User-Agent设置客户端信息, 无User-Agent请求头的请求应该被拒绝访问。具体的授权可以采用OAuth2，或者自己定义并实现相关的授权验证机制（基于token）。<br><strong>这个我们一般会在getway中实现</strong></p><h2 id="错误"><a href="#错误" class="headerlink" title="错误"></a>错误</h2><p>当API返回非2XX的HTTP响应时，应该采用统一的响应信息，格式如：<br><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">HTTP/1.1 400 Bad Request</span><br><span class="line">Content-Type: application/json;charset=UTF-8</span><br><span class="line">&#123;</span><br><span class="line">    &quot;code&quot;:&quot;INVALID_ARGUMENT&quot;,</span><br><span class="line">    &quot;message&quot;:&quot;&#123;error message&#125;&quot;,</span><br><span class="line">    &quot;request_id&quot;:&quot;sdfsdfo8lkjsdf&quot;,</span><br><span class="line">    &quot;items&quot;:[],</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><ul><li>HTTP Header Code：符合HTTP响应的状态码。详细见以下的“状态码”节</li><li>code：用来表示某类错误不是具体错误，比如缺少参数等。是对HTTP Header Code的补充，开发团队可以根据自己的需要自己定义</li><li>message：错误信息的摘要，应该是对用户处理错误有用的信息</li><li>request_id：请求的id，方便开发定位发生错误的请求（可选）</li><li>code的定义约定：<ul><li>采用 大写字母命名，字母与字母之间用下划线（”_”） 隔开</li><li>code应该用来定义错误类别，而非定义具体的某个错误。</li><li>缺少参数使用：MISSING_X</li><li>无效参数使用：INVALID_X</li><li>逻辑验证错误使用：VALIDATION_X</li><li>不存在使用：NO_FOUND_X</li></ul></li></ul><h1 id="七、状态码（Status-Codes）"><a href="#七、状态码（Status-Codes）" class="headerlink" title="七、状态码（Status Codes）"></a>七、状态码（Status Codes）</h1><p>服务器向用户返回的状态码和提示信息，常见的有以下一些（方括号中是该状态码对应的HTTP动词）。<br><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">200 OK - [GET/PUT/PATCH/DELETE]：服务器成功返回用户请求的数据，该操作是幂等的（Idempotent）。  </span><br><span class="line">201 Created - [POST/PUT/PATCH]：用户新建或修改数据成功。  </span><br><span class="line">202 Accepted - [*]：表示一个请求已经进入后台排队（异步任务）  </span><br><span class="line">204 No Content - [DELETE]：用户删除数据成功。  </span><br><span class="line">304 Not Modified   - HTTP缓存有效。</span><br><span class="line">400 Invalid Request - [POST/PUT/PATCH]：用户发出的请求有错误，服务器没有进行新建或修改数据的操作，该操作是幂等的。  </span><br><span class="line">401 Unauthorized - [*]：表示用户没有权限（令牌、用户名、密码错误）。  </span><br><span class="line">403 Forbidden - [*] 表示用户得到授权（与401错误相对），但是访问是被禁止的。  </span><br><span class="line">404 Not Found - [*]：用户发出的请求针对的是不存在的记录，服务器没有进行操作，该操作是幂等的。</span><br><span class="line">405 Method Not Allowed - [*]：该http方法不被允许。  </span><br><span class="line">406 Not Acceptable - [GET]：用户请求的格式不可得（比如用户请求JSON格式，但是只有XML格式）。  </span><br><span class="line">410 Gone -[GET]：用户请求的资源被永久删除，且不会再得到的。  </span><br><span class="line">415 Unsupported Media Type - [*]：请求类型错误。</span><br><span class="line">422 Unprocesable Entity - [POST/PUT/PATCH] 当创建一个对象时，发生一个验证错误。  </span><br><span class="line">429 Too Many Request - [*]：请求过多。</span><br><span class="line">500 Internal Server Error - [*]：服务器发生错误，用户将无法判断发出的请求是否成功。  </span><br><span class="line">503 Service Unavailable - [*]：服务当前无法处理请求。</span><br></pre></td></tr></table></figure></p><h1 id="八、异常规范（Exceptions）"><a href="#八、异常规范（Exceptions）" class="headerlink" title="八、异常规范（Exceptions）"></a>八、异常规范（Exceptions）</h1><ul><li>Controller中try catch住service的异常，再转换为restful中需要抛出的异常</li></ul><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">try &#123;</span><br><span class="line">    Long id = userService.save(vo);</span><br><span class="line">    vo.setId(id);</span><br><span class="line">&#125; catch(BizException e) &#123;</span><br><span class="line">    throw new UnprocesableEntityException(ErrorCode.USER_NAME_EXIST.getCode(), ErrorCode.USER_NAME_EXIST.getMessage());</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>Controller中抛出的异常必须使用spring-mvc-rest包中的异常类，不允许自定义异常，选择需要返回的httpStatus对应的异常</li></ul><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">#403 [*]：表示得到授权（与401错误相对），但是访问是被禁止的。</span><br><span class="line">com.jiuyescm.spring.mvc.rest.exception.ForbiddenException</span><br><span class="line"></span><br><span class="line">#401 [GET]：用户请求的资源被永久删除，且不会再得到的。</span><br><span class="line">com.jiuyescm.spring.mvc.rest.exception.GoneException</span><br><span class="line"></span><br><span class="line">#400 [POST/PUT/PATCH]：用户发出的请求有错误（常用在请求必要的参数错误上），服务器没有进行新建或修改数据的操作，该操作是幂等的。</span><br><span class="line">com.jiuyescm.spring.mvc.rest.exception.InvalidRequestException</span><br><span class="line"></span><br><span class="line">#406 [GET]：用户请求的格式不可得（比如用户请求JSON格式，但是只有XML格式）或（请求参数需要数字，用户传入字符串）</span><br><span class="line">com.jiuyescm.spring.mvc.rest.exception.NotAcceptableException</span><br><span class="line"></span><br><span class="line">#404 [*]：用户发出的请求针对的是不存在的记录，服务器没有进行操作，该操作是幂等的。</span><br><span class="line">com.jiuyescm.spring.mvc.rest.exception.NotFoundException</span><br><span class="line"></span><br><span class="line">#401 [*]：表示没有权限（令牌、用户名、密码错误，或任何资源没有权限）</span><br><span class="line">com.jiuyescm.spring.mvc.rest.exception.UnauthorizedException</span><br><span class="line"></span><br><span class="line">#422 [POST/PUT/PATCH] 当创建一个对象时，发生一个验证错误。</span><br><span class="line">com.jiuyescm.spring.mvc.rest.exception.UnprocesableEntityException</span><br></pre></td></tr></table></figure><ul><li>抛出的异常中需要传入异常编码和异常信息，异常编码定义遵循上面 《安全中错误编码规范》</li></ul><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">&quot;MESSING_ID&quot;, &quot;缺少参数：id&quot;</span><br><span class="line">&quot;MESSING_NAME&quot;, &quot;缺少参数：name&quot;</span><br><span class="line">&quot;MESSING_ADDRESS&quot;, &quot;缺少参数：address&quot;</span><br><span class="line">&quot;USER_NAME_EXIST&quot;, &quot;用户名已存在&quot;</span><br><span class="line">&quot;USER_NOT_FOUND&quot;, &quot;用户名不存在&quot;</span><br></pre></td></tr></table></figure><ul><li>常用的错误编码、异常、httpStatus对应关系</li></ul><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">&quot;MESSING_ID&quot;, &quot;缺少参数：id&quot;、InvalidRequestException、400</span><br><span class="line">&quot;MESSING_NAME&quot;, &quot;缺少参数：name&quot;、InvalidRequestException、400</span><br><span class="line">&quot;MESSING_ADDRESS&quot;, &quot;缺少参数：address&quot;、InvalidRequestException、400</span><br><span class="line">&quot;USER_NAME_EXIST&quot;, &quot;用户名已存在&quot;、UnprocesableEntityException、422</span><br><span class="line">&quot;USER_NOT_FOUND&quot;, &quot;用户名不存在&quot;、NotFoundException、404</span><br></pre></td></tr></table></figure><h1 id="九、示例（Example）"><a href="#九、示例（Example）" class="headerlink" title="九、示例（Example）"></a>九、示例（Example）</h1><p>采用user提供的示例代码</p><h2 id="POST-users"><a href="#POST-users" class="headerlink" title="POST /users"></a>POST /users</h2><p>Resource<br><code>POST /v1/users</code></p><p>POST Parameters<br>Endpoint requires：</p><table><thead><tr><th>Name</th><th style="text-align:center">Type</th><th style="text-align:right">Description</th></tr></thead><tbody><tr><td>name</td><td style="text-align:center">String</td><td style="text-align:right">用户名称</td></tr><tr><td>address</td><td style="text-align:center">String</td><td style="text-align:right">用户住址</td></tr></tbody></table><p>and accepts a few other parameters listed below.</p><table><thead><tr><th>Name</th><th style="text-align:center">Type</th><th style="text-align:right">Description</th></tr></thead><tbody><tr><td>remark</td><td style="text-align:center">String</td><td style="text-align:right">描述信息</td></tr></tbody></table><p>Example<br><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">&#123;</span><br><span class="line">&quot;name&quot;:&quot;tuyir&quot;,</span><br><span class="line">&quot;address&quot;:&quot;sdflkjsdf&quot;,</span><br><span class="line">&quot;remark&quot;:&quot;sdfoiu&quot;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p>Response<br>Status-Code: 201 Created<br><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  &quot;code&quot;: &quot;&quot;,</span><br><span class="line">  &quot;message&quot;: null,</span><br><span class="line">  &quot;items&quot;: &#123;</span><br><span class="line">    &quot;id&quot;: 27,</span><br><span class="line">    &quot;name&quot;: &quot;tuyir&quot;,</span><br><span class="line">    &quot;address&quot;: &quot;sdflkjsdf&quot;,</span><br><span class="line">    &quot;remark&quot;: &quot;sdfoiu&quot;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><table><thead><tr><th>Name</th><th style="text-align:center">Type</th><th style="text-align:right">Description</th></tr></thead><tbody><tr><td>code</td><td style="text-align:center">String</td><td style="text-align:right">错误编码</td></tr><tr><td>message</td><td style="text-align:center">String</td><td style="text-align:right">错误描述</td></tr><tr><td>Items</td><td style="text-align:center">Objec</td><td style="text-align:right">t    返回结果</td></tr><tr><td>id</td><td style="text-align:center">Long</td><td style="text-align:right">唯一标识</td></tr><tr><td>name</td><td style="text-align:center">String</td><td style="text-align:right">用户名称</td></tr><tr><td>address</td><td style="text-align:center">String</td><td style="text-align:right">家庭住址</td></tr><tr><td>remark</td><td style="text-align:center">String</td><td style="text-align:right">描述信息</td></tr></tbody></table><p>Error response<br>Status-Code: 400 Bad Request<br><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  &quot;code&quot;: &quot;MESSING_NAME&quot;,</span><br><span class="line">  &quot;message&quot;: “缺少参数：name”,</span><br><span class="line">  &quot;items&quot;: &#123;&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><table><thead><tr><th>Name</th><th style="text-align:center">Type</th><th style="text-align:right">Description</th></tr></thead><tbody><tr><td>code</td><td style="text-align:center">String</td><td style="text-align:right">错误编码</td></tr><tr><td>message</td><td style="text-align:center">String</td><td style="text-align:right">错误描述</td></tr><tr><td>Items</td><td style="text-align:center">Object</td><td style="text-align:right">返回结果</td></tr></tbody></table><p>HTTP Error Codes</p><table><thead><tr><th>HTTP Status</th><th style="text-align:center">Code</th><th style="text-align:right">Description</th></tr></thead><tbody><tr><td>400</td><td style="text-align:center">MESSING_NAME</td><td style="text-align:right">缺少参数：name</td></tr><tr><td>400</td><td style="text-align:center">MESSING_ADDRESS</td><td style="text-align:right">缺少参数：address</td></tr><tr><td>422</td><td style="text-align:center">USER_NAME_EXIST</td><td style="text-align:right">用户名已存在</td></tr><tr><td>500</td><td style="text-align:center">INTERNAL_SERVER_ERROR</td><td style="text-align:right">未知的错误</td></tr></tbody></table><h2 id="DELETE-users-user-id"><a href="#DELETE-users-user-id" class="headerlink" title="DELETE /users/{user_id}"></a>DELETE /users/{user_id}</h2><p>Resource<br><code>DELETE /v1/users/{user_id}</code></p><p>Path Parameters</p><table><thead><tr><th>Name</th><th style="text-align:center">Type</th><th style="text-align:right">Description</th></tr></thead><tbody><tr><td>user_id</td><td style="text-align:center">Long</td><td style="text-align:right">用户唯一标识</td></tr></tbody></table><p>Query Parameters<br>None</p><p>Example Request<br><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">curl –H ‘Content-Type: application/json’\</span><br><span class="line">-X DELETE \</span><br><span class="line">‘https://api.jiuyescm.com/v1/users/111’</span><br></pre></td></tr></table></figure></p><p>Response<br>Status-Code: 204 No Content</p><p>HTTP Error Codes</p><table><thead><tr><th>HTTP Status</th><th style="text-align:center">Code</th><th style="text-align:right">Description</th></tr></thead><tbody><tr><td>400</td><td style="text-align:center">MESSING_ID</td><td style="text-align:right">缺少参数：id</td></tr><tr><td>404</td><td style="text-align:center">USER_NOT_FOUND</td><td style="text-align:right">用户不存在</td></tr></tbody></table><h2 id="PUT-users"><a href="#PUT-users" class="headerlink" title="PUT /users"></a>PUT /users</h2><p>Resource<br><code>PUT /v1/users</code></p><p>PUT Body Parameters<br>Endpoint requires：</p><table><thead><tr><th>Name</th><th style="text-align:center">Type</th><th style="text-align:right">Description</th></tr></thead><tbody><tr><td>user_id</td><td style="text-align:center">Long</td><td style="text-align:right">用户唯一标识</td></tr><tr><td>user_name</td><td style="text-align:center">String</td><td style="text-align:right">用户名称</td></tr><tr><td>address</td><td style="text-align:center">String</td><td style="text-align:right">用户住址</td></tr></tbody></table><p>and accepts a few other parameters listed below..</p><table><thead><tr><th>Name</th><th style="text-align:center">Type</th><th style="text-align:right">Description</th></tr></thead><tbody><tr><td>remark</td><td style="text-align:center">String</td><td style="text-align:right">描述信息</td></tr></tbody></table><p>Example<br><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">&#123;</span><br><span class="line">    &quot;user_id&quot;: 12,</span><br><span class="line">&quot;name&quot;:&quot;tuyir&quot;,</span><br><span class="line">&quot;address&quot;:&quot;sdflkjsdf&quot;,</span><br><span class="line">&quot;remark&quot;:&quot;sdfoiu&quot;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p>Response<br>Status-Code: 200 OK<br><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  &quot;code&quot;: &quot;&quot;,</span><br><span class="line">  &quot;message&quot;: null,</span><br><span class="line">  &quot;items&quot;: &#123;</span><br><span class="line">    &quot;id&quot;: 12,</span><br><span class="line">    &quot;name&quot;: &quot;tuyir&quot;,</span><br><span class="line">    &quot;address&quot;: &quot;sdflkjsdf&quot;,</span><br><span class="line">    &quot;remark&quot;: &quot;sdfoiu&quot;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><table><thead><tr><th>Name</th><th style="text-align:center">Type</th><th style="text-align:right">Description</th></tr></thead><tbody><tr><td>code</td><td style="text-align:center">String</td><td style="text-align:right">错误编码</td></tr><tr><td>message</td><td style="text-align:center">String</td><td style="text-align:right">错误描述</td></tr><tr><td>Items</td><td style="text-align:center">Object</td><td style="text-align:right">返回结果</td></tr><tr><td>id</td><td style="text-align:center">Long</td><td style="text-align:right">唯一标识</td></tr><tr><td>name</td><td style="text-align:center">String</td><td style="text-align:right">用户名称</td></tr><tr><td>address</td><td style="text-align:center">String</td><td style="text-align:right">家庭住址</td></tr><tr><td>remark</td><td style="text-align:center">String</td><td style="text-align:right">描述信息</td></tr></tbody></table><p>Error response<br>Status-Code: 400 Bad Request<br><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  &quot;code&quot;: &quot;MESSING_NAME&quot;,</span><br><span class="line">  &quot;message&quot;: “缺少参数：name”,</span><br><span class="line">  &quot;items&quot;: &#123;&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><table><thead><tr><th>Name</th><th style="text-align:center">Type</th><th style="text-align:right">Description</th></tr></thead><tbody><tr><td>code</td><td style="text-align:center">String</td><td style="text-align:right">错误编码</td></tr><tr><td>message</td><td style="text-align:center">String</td><td style="text-align:right">错误描述</td></tr></tbody></table><p>HTTP Error Codes</p><table><thead><tr><th>HTTP Status</th><th style="text-align:center">Code</th><th style="text-align:right">Description</th></tr></thead><tbody><tr><td>code</td><td style="text-align:center">String</td><td style="text-align:right">错误编码</td></tr><tr><td>400</td><td style="text-align:center">MESSING_ID</td><td style="text-align:right">缺少参数：id</td></tr><tr><td>400</td><td style="text-align:center">MESSING_NAME</td><td style="text-align:right">缺少参数：name</td></tr><tr><td>400</td><td style="text-align:center">MESSING_ADDRESS</td><td style="text-align:right">缺少参数：address</td></tr><tr><td>422</td><td style="text-align:center">USER_NAME_EXIST</td><td style="text-align:right">用户名已存在</td></tr><tr><td>500</td><td style="text-align:center">INTERNAL_SERVER_ERROR</td><td style="text-align:right">未知的错误</td></tr></tbody></table><h2 id="GET-users-user-id"><a href="#GET-users-user-id" class="headerlink" title="GET /users/{user_id}"></a>GET /users/{user_id}</h2><p>Resource<br><code>GET /v1/users/{user_id}</code></p><p>Path Parameters</p><table><thead><tr><th>Name</th><th style="text-align:center">Type</th><th style="text-align:right">Description</th></tr></thead><tbody><tr><td>user_id</td><td style="text-align:center">Long</td><td style="text-align:right">用户唯一标识</td></tr></tbody></table><p>Example Request<br><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">Curl –H &apos;Content-Type: application/json&apos; \</span><br><span class="line">&apos;https://api.jiuyescm.com/v1/users/12&apos;</span><br></pre></td></tr></table></figure></p><p>Response<br>Status-Code: 200 OK<br><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  &quot;code&quot;: &quot;&quot;,</span><br><span class="line">  &quot;message&quot;: null,</span><br><span class="line">  &quot;items&quot;: &#123;</span><br><span class="line">    &quot;id&quot;: 12,</span><br><span class="line">    &quot;name&quot;: &quot;tuyir&quot;,</span><br><span class="line">    &quot;address&quot;: &quot;sdflkjsdf&quot;,</span><br><span class="line">    &quot;remark&quot;: &quot;sdfoiu&quot;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><table><thead><tr><th>Name</th><th style="text-align:center">Type</th><th style="text-align:right">Description</th></tr></thead><tbody><tr><td>code</td><td style="text-align:center">String</td><td style="text-align:right">错误编码</td></tr><tr><td>message</td><td style="text-align:center">String</td><td style="text-align:right">错误描述</td></tr><tr><td>Items</td><td style="text-align:center">Object</td><td style="text-align:right">返回结果</td></tr><tr><td>id</td><td style="text-align:center">String</td><td style="text-align:right">唯一标识</td></tr><tr><td>name</td><td style="text-align:center">String</td><td style="text-align:right">用户名称</td></tr><tr><td>address</td><td style="text-align:center">String</td><td style="text-align:right">家庭住址</td></tr><tr><td>remark</td><td style="text-align:center">String</td><td style="text-align:right">描述信息</td></tr></tbody></table><p>Error response<br>Status-Code: 404 Bad Request<br><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  &quot;code&quot;: &quot; USER_NOT_FOUND&quot;,</span><br><span class="line">  &quot;message&quot;: “用户不存在”,</span><br><span class="line">  &quot;items&quot;: &#123;&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><table><thead><tr><th>Name</th><th style="text-align:center">Type</th><th style="text-align:right">Description</th></tr></thead><tbody><tr><td>code</td><td style="text-align:center">String</td><td style="text-align:right">错误编码</td></tr><tr><td>message</td><td style="text-align:center">String</td><td style="text-align:right">错误描述</td></tr></tbody></table><p>HTTP Error Codes</p><table><thead><tr><th>HTTP Status</th><th style="text-align:center">Code</th><th style="text-align:right">Description</th></tr></thead><tbody><tr><td>400</td><td style="text-align:center">MESSING_ID</td><td style="text-align:right">缺少参数：id</td></tr><tr><td>404</td><td style="text-align:center">USER_NOT_FOUND</td><td style="text-align:right">用户不存在</td></tr></tbody></table><h2 id="GET-users"><a href="#GET-users" class="headerlink" title="GET /users"></a>GET /users</h2><p>Resource<br><code>GET /v1/users</code></p><p>Query Parameters</p><table><thead><tr><th>Name</th><th style="text-align:center">Type</th><th style="text-align:right">Description</th></tr></thead><tbody><tr><td>name</td><td style="text-align:center">String</td><td style="text-align:right">根据用户名称进行查询</td></tr><tr><td>page</td><td style="text-align:center">int</td><td style="text-align:right">第几页，不传入默认1</td></tr><tr><td>page_size</td><td style="text-align:center">int</td><td style="text-align:right">每页返回多少条结果，不传入默认20</td></tr></tbody></table><p>Example Request<br><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">Curl –H &apos;Content-Type: application/json&apos; \</span><br><span class="line">&apos;https://api.jiuyescm.com/v1/users?name=xxx&amp;page=1&amp;page_size=20&apos;</span><br></pre></td></tr></table></figure></p><p>Response<br>Status-Code: 200 Success<br><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  &quot;code&quot;: &quot;&quot;,</span><br><span class="line">  &quot;message&quot;: &quot;&quot;,</span><br><span class="line">  &quot;items&quot;: &#123;</span><br><span class="line">    &quot;pageNum&quot;: 1,</span><br><span class="line">    &quot;pageSize&quot;: 20,</span><br><span class="line">    &quot;size&quot;: 17,</span><br><span class="line">    &quot;startRow&quot;: 1,</span><br><span class="line">    &quot;endRow&quot;: 17,</span><br><span class="line">    &quot;total&quot;: 17,</span><br><span class="line">    &quot;pages&quot;: 1,</span><br><span class="line">    &quot;list&quot;: [</span><br><span class="line">      &#123;</span><br><span class="line">        &quot;id&quot;: 2,</span><br><span class="line">        &quot;name&quot;: &quot;ningyu1&quot;,</span><br><span class="line">        &quot;address&quot;: &quot;sdflkjsdf&quot;,</span><br><span class="line">        &quot;remark&quot;: &quot;sdfoiu&quot;</span><br><span class="line">      &#125;,</span><br><span class="line">      &#123;</span><br><span class="line">        &quot;id&quot;: 3,</span><br><span class="line">        &quot;name&quot;: &quot;ningyu2&quot;,</span><br><span class="line">        &quot;address&quot;: &quot;sdflkjsdf&quot;,</span><br><span class="line">        &quot;remark&quot;: &quot;sdfoiu&quot;</span><br><span class="line">      &#125;,</span><br><span class="line">      &#123;</span><br><span class="line">        &quot;id&quot;: 4,</span><br><span class="line">        &quot;name&quot;: &quot;ningyu3&quot;,</span><br><span class="line">        &quot;address&quot;: &quot;sdflkjsdf&quot;,</span><br><span class="line">        &quot;remark&quot;: &quot;sdfoiu&quot;</span><br><span class="line">      &#125;,</span><br><span class="line">      &#123;</span><br><span class="line">        &quot;id&quot;: 5,</span><br><span class="line">        &quot;name&quot;: &quot;ningyu4&quot;,</span><br><span class="line">        &quot;address&quot;: &quot;sdflkjsdf&quot;,</span><br><span class="line">        &quot;remark&quot;: &quot;sdfoiu&quot;</span><br><span class="line">      &#125;,</span><br><span class="line">      &#123;</span><br><span class="line">        &quot;id&quot;: 6,</span><br><span class="line">        &quot;name&quot;: &quot;ningyu5&quot;,</span><br><span class="line">        &quot;address&quot;: &quot;sdflkjsdf&quot;,</span><br><span class="line">        &quot;remark&quot;: &quot;sdfoiu&quot;</span><br><span class="line">      &#125;,</span><br><span class="line">      &#123;</span><br><span class="line">        &quot;id&quot;: 7,</span><br><span class="line">        &quot;name&quot;: &quot;8888&quot;,</span><br><span class="line">        &quot;address&quot;: &quot;8888&quot;,</span><br><span class="line">        &quot;remark&quot;: &quot;8888&quot;</span><br><span class="line">      &#125;,</span><br><span class="line">      &#123;</span><br><span class="line">        &quot;id&quot;: 8,</span><br><span class="line">        &quot;name&quot;: &quot;444&quot;,</span><br><span class="line">        &quot;address&quot;: &quot;444&quot;,</span><br><span class="line">        &quot;remark&quot;: &quot;444&quot;</span><br><span class="line">      &#125;,</span><br><span class="line">      &#123;</span><br><span class="line">        &quot;id&quot;: 9,</span><br><span class="line">        &quot;name&quot;: &quot;ningyu7&quot;,</span><br><span class="line">        &quot;address&quot;: &quot;sdflkjsdf&quot;,</span><br><span class="line">        &quot;remark&quot;: &quot;sdfoiu&quot;</span><br><span class="line">      &#125;,</span><br><span class="line">      &#123;</span><br><span class="line">        &quot;id&quot;: 12,</span><br><span class="line">        &quot;name&quot;: &quot;ningyu9&quot;,</span><br><span class="line">        &quot;address&quot;: &quot;sdflkjsdf&quot;,</span><br><span class="line">        &quot;remark&quot;: &quot;sdfoiu&quot;</span><br><span class="line">      &#125;,</span><br><span class="line">      &#123;</span><br><span class="line">        &quot;id&quot;: 13,</span><br><span class="line">        &quot;name&quot;: &quot;ningyu10&quot;,</span><br><span class="line">        &quot;address&quot;: &quot;sdflkjsdf&quot;,</span><br><span class="line">        &quot;remark&quot;: &quot;sdfoiu&quot;</span><br><span class="line">      &#125;,</span><br><span class="line">      &#123;</span><br><span class="line">        &quot;id&quot;: 17,</span><br><span class="line">        &quot;name&quot;: &quot;ningyu&quot;,</span><br><span class="line">        &quot;address&quot;: &quot;sdflkjsdf&quot;,</span><br><span class="line">        &quot;remark&quot;: null</span><br><span class="line">      &#125;,</span><br><span class="line">      &#123;</span><br><span class="line">        &quot;id&quot;: 20,</span><br><span class="line">        &quot;name&quot;: &quot;9999&quot;,</span><br><span class="line">        &quot;address&quot;: &quot;sdflkjsdf&quot;,</span><br><span class="line">        &quot;remark&quot;: null</span><br><span class="line">      &#125;,</span><br><span class="line">      &#123;</span><br><span class="line">        &quot;id&quot;: 23,</span><br><span class="line">        &quot;name&quot;: &quot;888&quot;,</span><br><span class="line">        &quot;address&quot;: &quot;sdflkjsdf&quot;,</span><br><span class="line">        &quot;remark&quot;: &quot;sdfoiu&quot;</span><br><span class="line">      &#125;,</span><br><span class="line">      &#123;</span><br><span class="line">        &quot;id&quot;: 24,</span><br><span class="line">        &quot;name&quot;: &quot;222&quot;,</span><br><span class="line">        &quot;address&quot;: &quot;sdflkjsdf&quot;,</span><br><span class="line">        &quot;remark&quot;: &quot;sdfoiu&quot;</span><br><span class="line">      &#125;,</span><br><span class="line">      &#123;</span><br><span class="line">        &quot;id&quot;: 25,</span><br><span class="line">        &quot;name&quot;: &quot;222444&quot;,</span><br><span class="line">        &quot;address&quot;: &quot;sdflkjsdf&quot;,</span><br><span class="line">        &quot;remark&quot;: &quot;sdfoiu&quot;</span><br><span class="line">      &#125;,</span><br><span class="line">      &#123;</span><br><span class="line">        &quot;id&quot;: 26,</span><br><span class="line">        &quot;name&quot;: &quot;222444sdf&quot;,</span><br><span class="line">        &quot;address&quot;: &quot;sdflkjsdf&quot;,</span><br><span class="line">        &quot;remark&quot;: &quot;sdfoiu&quot;</span><br><span class="line">      &#125;,</span><br><span class="line">      &#123;</span><br><span class="line">        &quot;id&quot;: 27,</span><br><span class="line">        &quot;name&quot;: &quot;tuyir&quot;,</span><br><span class="line">        &quot;address&quot;: &quot;sdflkjsdf&quot;,</span><br><span class="line">        &quot;remark&quot;: &quot;sdfoiu&quot;</span><br><span class="line">      &#125;</span><br><span class="line">    ],</span><br><span class="line">    &quot;firstPage&quot;: 1,</span><br><span class="line">    &quot;prePage&quot;: 0,</span><br><span class="line">    &quot;nextPage&quot;: 0,</span><br><span class="line">    &quot;lastPage&quot;: 1,</span><br><span class="line">    &quot;isFirstPage&quot;: true,</span><br><span class="line">    &quot;isLastPage&quot;: true,</span><br><span class="line">    &quot;hasPreviousPage&quot;: false,</span><br><span class="line">    &quot;hasNextPage&quot;: false,</span><br><span class="line">    &quot;navigatePages&quot;: 8,</span><br><span class="line">    &quot;navigatepageNums&quot;: [</span><br><span class="line">      1</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><table><thead><tr><th>Name</th><th style="text-align:center">Type</th><th style="text-align:right">Description</th></tr></thead><tbody><tr><td>code</td><td style="text-align:center">String</td><td style="text-align:right">错误编码</td></tr><tr><td>message</td><td style="text-align:center">String</td><td style="text-align:right">错误描述</td></tr><tr><td>Items</td><td style="text-align:center">Object</td><td style="text-align:right">返回结果</td></tr><tr><td>id</td><td style="text-align:center">Long</td><td style="text-align:right">唯一标识</td></tr><tr><td>name</td><td style="text-align:center">String</td><td style="text-align:right">用户名称</td></tr><tr><td>address</td><td style="text-align:center">String</td><td style="text-align:right">家庭住址</td></tr><tr><td>remark</td><td style="text-align:center">String</td><td style="text-align:right">描述信息</td></tr></tbody></table>]]></content>
    
    <summary type="html">
    
      RESTful设计规范
    
    </summary>
    
      <category term="RESTful" scheme="https://ningyu1.github.io/categories/RESTful/"/>
    
    
      <category term="RESTful" scheme="https://ningyu1.github.io/tags/RESTful/"/>
    
  </entry>
  
  <entry>
    <title>Dubbo本地调试最优方式，本地Server端调用本地Client端</title>
    <link href="https://ningyu1.github.io/20161220/09-dubbo-debug.html"/>
    <id>https://ningyu1.github.io/20161220/09-dubbo-debug.html</id>
    <published>2016-12-20T06:32:41.000Z</published>
    <updated>2024-04-23T01:48:15.029Z</updated>
    
    <content type="html"><![CDATA[<h1 id="分布式应用的调试总是比常规项目开发调试起来要麻烦很多。"><a href="#分布式应用的调试总是比常规项目开发调试起来要麻烦很多。" class="headerlink" title="分布式应用的调试总是比常规项目开发调试起来要麻烦很多。"></a>分布式应用的调试总是比常规项目开发调试起来要麻烦很多。</h1><h1 id="我们还在为搞不清自己请求的服务是本地服务还是服务器服务而苦恼吗？"><a href="#我们还在为搞不清自己请求的服务是本地服务还是服务器服务而苦恼吗？" class="headerlink" title="我们还在为搞不清自己请求的服务是本地服务还是服务器服务而苦恼吗？"></a>我们还在为搞不清自己请求的服务是本地服务还是服务器服务而苦恼吗？</h1><h1 id="我们还在为配置文件被修改导致服务器上版本服务不正常而苦恼吗？"><a href="#我们还在为配置文件被修改导致服务器上版本服务不正常而苦恼吗？" class="headerlink" title="我们还在为配置文件被修改导致服务器上版本服务不正常而苦恼吗？"></a>我们还在为配置文件被修改导致服务器上版本服务不正常而苦恼吗？</h1><p>接下来我介绍一个Dubbo在多环境调试的最优调试方式，在介绍之前先说一下我们现在的调试方式。</p><p>不好的方式（现在的方式）：<br>现在本地调试，需要修改DubboServer.xml和DubboClient.xml配置文件</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">将文件中的</span><br><span class="line">dubbo:registry protocol=&quot;zookeeper&quot; address=&quot;$&#123;dubbo.registry&#125;&quot; /&gt;</span><br><span class="line">修改为</span><br><span class="line">&lt;dubbo:registry address=&quot;N/A&quot; /&gt;</span><br></pre></td></tr></table></figure><p>这种方式的弊端：</p><ol><li>开发总是不注意将修改为address=”N/A”的文件提交到svn，在其他环境打包run起来，总是没有Export Service。</li><li>文件经常被改来改去容易冲突，冲突解决不好容易丢失配置。</li><li>无法很好的将本地调试和各环境的相互依赖分离开</li></ol><p>最优的方式：</p><ol><li>创建一个properties文件，名字可以随便命名，我命名为：dubbo-local.properties，这个文件可以放在任何地方。该文件不提交到svn，我建议不要放在工程目录里以避免自己提交了都不知道，建议放在用户目录下${user.home}(不知道用户目录的自己去 度娘、谷哥、必硬)</li><li><p>dubbo-local.properties文件内容如下：</p> <figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">&lt;!--注册中心变量 --&gt;</span><br><span class="line">dubbo.registry=N/A</span><br><span class="line"> </span><br><span class="line">&lt;!--以下是你们DubboServer.xml中配置的需要Export Service，这里我建议你有几个要Export Service都配置在这里，后面是请求本地的地址</span><br><span class="line">地址格式：dubbo://ip:port，这里需要注意的是，需要修改为自己dubbo服务的端口 --&gt;</span><br><span class="line">com.domain.imprest.api.IImprestRecordService=dubbo://localhost:20812</span><br><span class="line">com.domain.imprest.api.IImprestRequestService=dubbo://localhost:20812</span><br><span class="line">com.domain.imprest.api.IImprestTrackService=dubbo://localhost:20812</span><br><span class="line">com.domain.imprest.api.IImprestWriteoffService=dubbo://localhost:20812</span><br><span class="line">com.domain.imprest.api.IImprestIOCollectService=dubbo://localhost:20812</span><br><span class="line">com.domain.imprest.api.ISystemService=dubbo://localhost:20812</span><br><span class="line">com.domain.imprest.api.IImprestDeptService=dubbo://localhost:20812</span><br></pre></td></tr></table></figure></li><li><p>接下来启动你的Dubbo服务，在启动之前需要添加一下启动参数</p></li></ol><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/dubbo/1.png" alt="dubbo1" title>                </div>                <div class="image-caption">dubbo1</div>            </figure><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">参数：-Ddubbo.properties.file</span><br><span class="line">值：dubbo-local.properties文件的本地地址，绝对地址</span><br></pre></td></tr></table></figure><ol start="4"><li>接下来启动你的web服务，在启动之前需要添加一下启动参数</li></ol><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/dubbo/2.png" alt="dubbo2" title>                </div>                <div class="image-caption">dubbo2</div>            </figure><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">参数：-Ddubbo.resolve.file</span><br><span class="line">值：dubbo-local.properties文件的本地地址，绝对地址</span><br></pre></td></tr></table></figure><p><strong>ps.当你不想连接本地服务调试时，只需将启动参数去掉即可，无需修改配置文件，让配置文件一直保持清爽干净。<br>以后你就可以安心的本地调试你的程序了，再也不会因为服务没有Export出去、配置文件被修改而焦头烂额。</strong></p><h1 id="Dubbo-Plugin-for-Apache-JMeter"><a href="#Dubbo-Plugin-for-Apache-JMeter" class="headerlink" title="Dubbo Plugin for Apache JMeter"></a>Dubbo Plugin for Apache JMeter</h1><p>Dubbo Plugin for Apache JMeter是用来在Jmeter里更加方便的测试Dubbo接口而开发的插件，<a href="https://ningyu1.github.io/site/post/60-jmeter-plugins-dubbo-support">马上使用</a></p><h1 id="项目地址"><a href="#项目地址" class="headerlink" title="项目地址"></a>项目地址</h1><p><a href="https://github.com/ningyu1/jmeter-plugins-dubbo" target="_blank" rel="noopener">github: jmeter-plugin-dubbo</a> </p><p><a href="https://github.com/ningyu1/jmeter-plugins-dubbo/releases" target="_blank" rel="noopener"><img src="https://img.shields.io/github/release/ningyu1/jmeter-plugins-dubbo.svg?style=social&amp;label=Release"></a>&nbsp;<a href="https://github.com/ningyu1/jmeter-plugins-dubbo/stargazers" target="_blank" rel="noopener"><img src="https://img.shields.io/github/stars/ningyu1/jmeter-plugins-dubbo.svg?style=social&amp;label=Star"></a>&nbsp;<a href="https://github.com/ningyu1/jmeter-plugins-dubbo/fork" target="_blank" rel="noopener"><img src="https://img.shields.io/github/forks/ningyu1/jmeter-plugins-dubbo.svg?style=social&amp;label=Fork"></a>&nbsp;<a href="https://github.com/ningyu1/jmeter-plugins-dubbo/watchers" target="_blank" rel="noopener"><img src="https://img.shields.io/github/watchers/ningyu1/jmeter-plugins-dubbo.svg?style=social&amp;label=Watch"></a> <a href="https://opensource.org/licenses/MIT" target="_blank" rel="noopener"><img src="https://img.shields.io/badge/license-MIT-blue.svg"></a></p><p><a href="https://gitee.com/ningyu/jmeter-plugins-dubbo" target="_blank" rel="noopener">码云: jmeter-plugin-dubbo</a></p><table><thead><tr><th style="text-align:center">release</th><th style="text-align:center">star</th><th style="text-align:center">fork</th><th style="text-align:center">license</th></tr></thead><tbody><tr><td style="text-align:center"><a href="https://gitee.com/ningyu/jmeter-plugins-dubbo/releases/V1.2.0" target="_blank" rel="noopener">V1.2.2</a></td><td style="text-align:center">[<figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://opensource.org/licenses/MIT" alt="star](https://gitee.com/ningyu/jmeter-plugins-dubbo/badge/star.svg?theme=white)](https://gitee.com/ningyu/jmeter-plugins-dubbo/stargazers)|[![fork](https://gitee.com/ningyu/jmeter-plugins-dubbo/badge/fork.svg?theme=white)](https://gitee.com/ningyu/jmeter-plugins-dubbo/members)|[MIT" title>                </div>                <div class="image-caption">star](https://gitee.com/ningyu/jmeter-plugins-dubbo/badge/star.svg?theme=white)](https://gitee.com/ningyu/jmeter-plugins-dubbo/stargazers)|[![fork](https://gitee.com/ningyu/jmeter-plugins-dubbo/badge/fork.svg?theme=white)](https://gitee.com/ningyu/jmeter-plugins-dubbo/members)|[MIT</div>            </figure></td></tr></tbody></table><h1 id="相关博文"><a href="#相关博文" class="headerlink" title="相关博文"></a>相关博文</h1><ul><li><a href="https://ningyu1.github.io/site/post/60-jmeter-plugins-dubbo-support/">Dubbo接口如何在Jmeter中测试，自研Dubbo Plugin for Apache JMeter</a></li><li><a href="https://ningyu1.github.io/site/post/66-jmeter-plugin-dubbo-bugfix/">Bug Fix Version V1.1.0, Dubbo Plugin for Apache JMeter</a></li><li><a href="https://ningyu1.github.io/site/post/68-jmeter-plugin-dubbo-1.2.0/">New Version V1.2.0, Dubbo Plugin for Apache JMeter</a></li></ul>]]></content>
    
    <summary type="html">
    
      Dubbo本地调试最优方式，本地Server端调用本地Client端
    
    </summary>
    
      <category term="dubbo" scheme="https://ningyu1.github.io/categories/dubbo/"/>
    
    
      <category term="dubbo" scheme="https://ningyu1.github.io/tags/dubbo/"/>
    
      <category term="debug" scheme="https://ningyu1.github.io/tags/debug/"/>
    
      <category term="rpc" scheme="https://ningyu1.github.io/tags/rpc/"/>
    
  </entry>
  
  <entry>
    <title>分支(branche)开发，主干(trunk)发布</title>
    <link href="https://ningyu1.github.io/20161220/08-svn-trunk-branche.html"/>
    <id>https://ningyu1.github.io/20161220/08-svn-trunk-branche.html</id>
    <published>2016-12-20T06:32:41.000Z</published>
    <updated>2024-04-23T01:48:15.029Z</updated>
    
    <content type="html"><![CDATA[<p>主干，分支分开开发模式在使用的时候要注意，主干是不做任何代码修改，只负责merge，修改全在分支上，不管是新功能的开发分支，还是修复bug的分支，如果线上有紧急bug修复，要先容trunk上拉一个bugfix分支出来，修改提交然后在merge到主干上去 ，打包测试发包。</p><p>图示：</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/svn/1.jpg" alt="svn1" title>                </div>                <div class="image-caption">svn1</div>            </figure><p><strong>注意事项：</strong><br><strong>本地修改的代码不要藏在本地 不提交，如果发现没有地方可以提交，提交会影响版本发布，那就是主干、分支开发模式使用不当，请及时调整</strong></p>]]></content>
    
    <summary type="html">
    
      分支(branche)开发，主干(trunk)发布
    
    </summary>
    
      <category term="SVN" scheme="https://ningyu1.github.io/categories/SVN/"/>
    
    
      <category term="SVN" scheme="https://ningyu1.github.io/tags/SVN/"/>
    
      <category term="trunk" scheme="https://ningyu1.github.io/tags/trunk/"/>
    
      <category term="branche" scheme="https://ningyu1.github.io/tags/branche/"/>
    
  </entry>
  
  <entry>
    <title>SVN设置文件忽略的多种方法</title>
    <link href="https://ningyu1.github.io/20161126/10-svn-ignore.html"/>
    <id>https://ningyu1.github.io/20161126/10-svn-ignore.html</id>
    <published>2016-11-26T02:30:34.000Z</published>
    <updated>2024-04-23T01:48:15.029Z</updated>
    
    <content type="html"><![CDATA[<h2 id="方法一："><a href="#方法一：" class="headerlink" title="方法一："></a>方法一：</h2><p>在svn客户端（小乌龟），想设置忽略提交.class文件，通过 properties -&gt; New -&gt; Other 添加一个忽略的属性，，还是不行：部分屏蔽了，部分class还是在列表中</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/svn-ignore/1.jpg" alt="svn1" title>                </div>                <div class="image-caption">svn1</div>            </figure><h2 id="方法二："><a href="#方法二：" class="headerlink" title="方法二："></a>方法二：</h2><p>在svn客户端（小乌龟）：Settings -&gt; General -&gt; Global ignore pattern 添加了一个 *.class就行了</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/svn-ignore/2.jpg" alt="svn2" title>                </div>                <div class="image-caption">svn2</div>            </figure><h2 id="方法三："><a href="#方法三：" class="headerlink" title="方法三："></a>方法三：</h2><p>在 Eclipse 中点击菜单 window -&gt; Preferences -&gt; Team -&gt; Ignored Resources</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/svn-ignore/3.jpg" alt="svn3" title>                </div>                <div class="image-caption">svn3</div>            </figure><p>点击 Add Pattern… 按钮添加你要忽略的文件或目录</p><h2 id="方法四："><a href="#方法四：" class="headerlink" title="方法四："></a>方法四：</h2><p>在Eclipse的导航视图中，选中尚未加入版本控制的文件或目录，右键 -&gt; Team -&gt; 添加至SVN:ignore</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/svn-ignore/4.jpg" alt="svn4" title>                </div>                <div class="image-caption">svn4</div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/svn-ignore/5.jpg" alt="svn5" title>                </div>                <div class="image-caption">svn5</div>            </figure><h2 id="方法五："><a href="#方法五：" class="headerlink" title="方法五："></a>方法五：</h2><p>在资源管理器中，右键一个未加入版本控制文件或目录，并从弹出菜单选择TortoiseSVN -&gt; Add to Ignore List，会出现一个子菜单，允许你仅选择该文件或者所有具有相同后缀的文件。</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/svn-ignore/6.jpg" alt="svn6" title>                </div>                <div class="image-caption">svn6</div>            </figure><p>如果你想从忽略列表中移除一个或多个条目，右击这些条目，选择TortoiseSVN -&gt; 从忽略列表删除。</p>]]></content>
    
    <summary type="html">
    
      SVN设置文件忽略的多种方法
    
    </summary>
    
      <category term="SVN" scheme="https://ningyu1.github.io/categories/SVN/"/>
    
    
      <category term="SVN" scheme="https://ningyu1.github.io/tags/SVN/"/>
    
      <category term="ignore" scheme="https://ningyu1.github.io/tags/ignore/"/>
    
  </entry>
  
  <entry>
    <title>SLF4J和Logback日志框架详解</title>
    <link href="https://ningyu1.github.io/20150410/13-slf4j-logback.html"/>
    <id>https://ningyu1.github.io/20150410/13-slf4j-logback.html</id>
    <published>2015-04-10T02:15:03.000Z</published>
    <updated>2024-04-23T01:48:15.031Z</updated>
    
    <content type="html"><![CDATA[<h2 id="本文讲述SLF4J和Logback日志框架。"><a href="#本文讲述SLF4J和Logback日志框架。" class="headerlink" title="本文讲述SLF4J和Logback日志框架。"></a>本文讲述SLF4J和Logback日志框架。</h2><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="/img/logger/1.png" alt="logger1" title>                </div>                <div class="image-caption">logger1</div>            </figure><p>SLF4J是一套简单的日志外观模式的Java API，帮助在项目部署时对接各种日志实现。<br>LogBack在运行时使用JMX帮助修改日志配置，在生产状态下无需重启应用程序。</p><h2 id="SLF4J"><a href="#SLF4J" class="headerlink" title="SLF4J"></a>SLF4J</h2><p>SLF4J是简单的日志外观模式框架，抽象了各种日志框架例如Logback、Log4j、Commons-logging和JDK自带的logging实现接口。它使得用户可以在部署时使用自己想要的日志框架。SLF4J是轻量级的，在性能方面几乎是零消耗的。</p><p>SLF4J没有替代任何日志框架，它仅仅是标准日志框架的外观模式。如果在类路径下除了SLF4J再没有任何日志框架，那么默认状态是在控制台输出日志。</p><h2 id="Logback"><a href="#Logback" class="headerlink" title="Logback"></a>Logback</h2><p>Logback是Log4j的改进版本，而且原生支持SLF4J（因为是同一作者开发的），因此从其它日志框架如Log4j或JDK的logging迁移到Logback是完全可行的。</p><p>由于Logback原生支持SLF4J，因此Logback＋SLF4J的组合是日志框架的最佳选择，比SLF4J+其它日志框架的组合要快一些。而且Logback的配置可以是XML或Groovy代码。</p><p>注意一个重要的特性，Logback通过JMX修改日志配置（比如日志级别从Debug调整到INFO），可以从JMX控制台直接操作，无需重启应用程序。</p><p>此外，Logback的异常堆栈跟踪的信息，有助于调试。</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line">java.lang.NullPointerException: <span class="keyword">null</span>  </span><br><span class="line">at com.fimt.poc.LoggingSample.(LoggingSample.java:<span class="number">16</span>) [classes/:na]  </span><br><span class="line">at com.fimt.poc.LoggingSample.main(LoggingSample.java:<span class="number">23</span>) [fimt-logging-poc-<span class="number">1.0</span>.jar/:<span class="number">1.0</span></span><br></pre></td></tr></table></figure><h2 id="SLF4J-API用法"><a href="#SLF4J-API用法" class="headerlink" title="SLF4J API用法"></a>SLF4J API用法</h2><ul><li>从org.slf4j包导入Logger和LoggerFactory</li></ul><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> org.slf4j.Logger;  </span><br><span class="line"><span class="keyword">import</span> org.slf4j.LoggerFactory;</span><br></pre></td></tr></table></figure><ul><li>声明日志类</li></ul><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">final</span> Logger logger = LoggerFactory.getLogger(LoggingSample<span class="class">.<span class="keyword">class</span>)</span>;</span><br></pre></td></tr></table></figure><ul><li>使用debug、warn、info、error方法并跟踪适合的参数。</li></ul><p>所有的方法默认都使用字符串作为输入。<br><figure class="highlight java"><table><tr><td class="code"><pre><span class="line">logger.info(<span class="string">"This is sample info statement"</span>);</span><br></pre></td></tr></table></figure></p><h2 id="SLF4J结合Logback"><a href="#SLF4J结合Logback" class="headerlink" title="SLF4J结合Logback"></a>SLF4J结合Logback</h2><p>在pom.xml包含下面的依赖：它会自动包含所有的依赖包logback-core、slf4j-api……<br><figure class="highlight xml"><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span>  </span><br><span class="line">  <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>ch.qos.logback<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span>  </span><br><span class="line">  <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>logback-classic<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span>  </span><br><span class="line">  <span class="tag">&lt;<span class="name">version</span>&gt;</span>1.0.7<span class="tag">&lt;/<span class="name">version</span>&gt;</span>  </span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure></p><p>SLF4J能用于现有的日志框架如Log4j、Commons-logging、java.util.logging(JUL)。</p><h2 id="SLF4J结合Log4j"><a href="#SLF4J结合Log4j" class="headerlink" title="SLF4J结合Log4j"></a>SLF4J结合Log4j</h2><p>在pom.xml包含下面的依赖</p><figure class="highlight xml"><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span>  </span><br><span class="line">  <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.slf4j<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span>  </span><br><span class="line">  <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>slf4j-log4j12<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span>  </span><br><span class="line">  <span class="tag">&lt;<span class="name">version</span>&gt;</span>1.7.2<span class="tag">&lt;/<span class="name">version</span>&gt;</span>  </span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure><h2 id="SLF4J结合JUL-java-util-logging"><a href="#SLF4J结合JUL-java-util-logging" class="headerlink" title="SLF4J结合JUL (java.util.logging)"></a>SLF4J结合JUL (java.util.logging)</h2><p>在pom.xml包含下面的依赖</p><figure class="highlight xml"><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span>  </span><br><span class="line">  <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.slf4j<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span>  </span><br><span class="line">  <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>slf4j-jdk14<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span>  </span><br><span class="line">  <span class="tag">&lt;<span class="name">version</span>&gt;</span>1.7.2<span class="tag">&lt;/<span class="name">version</span>&gt;</span>  </span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure>]]></content>
    
    <summary type="html">
    
      SLF4J和Logback日志框架详解
    
    </summary>
    
      <category term="Java" scheme="https://ningyu1.github.io/categories/Java/"/>
    
      <category term="Logger" scheme="https://ningyu1.github.io/categories/Java/Logger/"/>
    
    
      <category term="SLF4J" scheme="https://ningyu1.github.io/tags/SLF4J/"/>
    
      <category term="Logback" scheme="https://ningyu1.github.io/tags/Logback/"/>
    
  </entry>
  
  <entry>
    <title>Google Guava官方教程（中文版）</title>
    <link href="https://ningyu1.github.io/20150309/14-guava.html"/>
    <id>https://ningyu1.github.io/20150309/14-guava.html</id>
    <published>2015-03-09T09:34:36.000Z</published>
    <updated>2024-04-23T01:48:15.031Z</updated>
    
    <content type="html"><![CDATA[<p>Guava 中文是石榴的意思，该项目是 Google 的一个开源项目，包含许多 Google 核心的 Java 常用库。</p><h2 id="引言"><a href="#引言" class="headerlink" title="引言"></a>引言</h2><p>Guava工程包含了若干被Google的 Java项目广泛依赖 的核心库，例如：集合 [collections] 、缓存 [caching] 、原生类型支持 [primitives support] 、并发库 [concurrency libraries] 、通用注解 [common annotations] 、字符串处理 [string processing] 、I/O 等等。 所有这些工具每天都在被Google的工程师应用在产品服务中。</p><p>查阅Javadoc并不一定是学习这些库最有效的方式。在此，我们希望通过此文档为Guava中最流行和最强大的功能，提供更具可读性和解释性的说明。</p><p>译文格式说明</p><p>Guava中的类被首次引用时，都会链接到Guava的API文档。如：Optional。<br>Guava和JDK中的方法被引用时，一般都会链接到Guava或JDK的API文档，一些人所共知的JDK方法除外。如：Optional.of(T), Map.get(key)。<br>译者对文档的额外说明以斜体显示，并且以“译者注：”开始。</p><h2 id="目录"><a href="#目录" class="headerlink" title="目录"></a>目录</h2><h3 id="1-基本工具-Basic-utilities"><a href="#1-基本工具-Basic-utilities" class="headerlink" title="1. 基本工具 [Basic utilities]"></a>1. 基本工具 [Basic utilities]</h3><p>让使用Java语言变得更舒适</p><ul><li>使用和避免null：null是模棱两可的，会引起令人困惑的错误，有些时候它让人很不舒服。很多Guava工具类用快速失败拒绝null值，而不是盲目地接受</li><li>前置条件: 让方法中的条件检查更简单</li><li>常见Object方法: 简化Object方法实现，如hashCode()和toString()</li><li>排序: Guava强大的”流畅风格比较器”</li><li>Throwables：简化了异常和错误的传播与检查</li></ul><h3 id="2-集合-Collections"><a href="#2-集合-Collections" class="headerlink" title="2. 集合[Collections]"></a>2. 集合[Collections]</h3><p>Guava对JDK集合的扩展，这是Guava最成熟和为人所知的部分</p><ul><li>不可变集合: 用不变的集合进行防御性编程和性能提升。</li><li>新集合类型: multisets, multimaps, tables, bidirectional maps等</li><li>强大的集合工具类: 提供java.util.Collections中没有的集合工具</li><li>扩展工具类：让实现和扩展集合类变得更容易，比如创建Collection的装饰器，或实现迭代器</li></ul><h3 id="3-缓存-Caches"><a href="#3-缓存-Caches" class="headerlink" title="3. 缓存[Caches]"></a>3. 缓存[Caches]</h3><p>Guava Cache：本地缓存实现，支持多种缓存过期策略</p><h3 id="4-函数式风格-Functional-idioms"><a href="#4-函数式风格-Functional-idioms" class="headerlink" title="4. 函数式风格[Functional idioms]"></a>4. 函数式风格[Functional idioms]</h3><p>Guava的函数式支持可以显著简化代码，但请谨慎使用它</p><h3 id="5-并发-Concurrency"><a href="#5-并发-Concurrency" class="headerlink" title="5. 并发[Concurrency]"></a>5. 并发[Concurrency]</h3><p>强大而简单的抽象，让编写正确的并发代码更简单</p><ul><li>ListenableFuture：完成后触发回调的Future</li><li>Service框架：抽象可开启和关闭的服务，帮助你维护服务的状态逻辑</li></ul><h3 id="6-字符串处理-Strings"><a href="#6-字符串处理-Strings" class="headerlink" title="6. 字符串处理[Strings]"></a>6. 字符串处理[Strings]</h3><p>非常有用的字符串工具，包括分割、连接、填充等操作</p><h3 id="7-原生类型-Primitives"><a href="#7-原生类型-Primitives" class="headerlink" title="7. 原生类型[Primitives]"></a>7. 原生类型[Primitives]</h3><p>扩展 JDK 未提供的原生类型（如int、char）操作， 包括某些类型的无符号形式</p><h3 id="8-区间-Ranges"><a href="#8-区间-Ranges" class="headerlink" title="8. 区间[Ranges]"></a>8. 区间[Ranges]</h3><p>可比较类型的区间API，包括连续和离散类型</p><h3 id="9-I-O"><a href="#9-I-O" class="headerlink" title="9. I/O"></a>9. I/O</h3><p>简化I/O尤其是I/O流和文件的操作，针对Java5和6版本</p><h3 id="10-散列-Hash"><a href="#10-散列-Hash" class="headerlink" title="10. 散列[Hash]"></a>10. 散列[Hash]</h3><p>提供比Object.hashCode()更复杂的散列实现，并提供布鲁姆过滤器的实现</p><h3 id="11-事件总线-EventBus"><a href="#11-事件总线-EventBus" class="headerlink" title="11. 事件总线[EventBus]"></a>11. 事件总线[EventBus]</h3><p>发布-订阅模式的组件通信，但组件不需要显式地注册到其他组件中</p><h3 id="12-数学运算-Math"><a href="#12-数学运算-Math" class="headerlink" title="12. 数学运算[Math]"></a>12. 数学运算[Math]</h3><p>优化的、充分测试的数学工具类</p><h3 id="13-反射-Reflection"><a href="#13-反射-Reflection" class="headerlink" title="13. 反射[Reflection]"></a>13. 反射[Reflection]</h3><p>Guava 的 Java 反射机制工具类</p><p>参考: <a href="http://ifeve.com/google-guava/" title="Google Guava官方教程（中文版）" target="_blank" rel="noopener">Google Guava官方教程（中文版）</a></p>]]></content>
    
    <summary type="html">
    
      Google Guava官方教程（中文版）
    
    </summary>
    
      <category term="Java" scheme="https://ningyu1.github.io/categories/Java/"/>
    
    
      <category term="Guava" scheme="https://ningyu1.github.io/tags/Guava/"/>
    
  </entry>
  
</feed>
