<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>零の轨迹</title>
  
  <subtitle>Sic Parvis Magna</subtitle>
  <link href="/atom.xml" rel="self"/>
  
  <link href="https://www.elietio.xyz/"/>
  <updated>2021-11-04T05:47:44.408Z</updated>
  <id>https://www.elietio.xyz/</id>
  
  <author>
    <name>Elietio</name>
    
  </author>
  
  <generator uri="http://hexo.io/">Hexo</generator>
  
  <entry>
    <title>Druid空闲连接检测 KeepAlive与MySql discard long time none received connection报警</title>
    <link href="https://www.elietio.xyz/posts/99b22c04.html"/>
    <id>https://www.elietio.xyz/posts/99b22c04.html</id>
    <published>2021-11-04T02:55:00.000Z</published>
    <updated>2021-11-04T05:47:44.408Z</updated>
    
    <content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/assets/css/APlayer.min.css"><script src="/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><script class="meting-secondary-script-marker" src="/assets/js/Meting.min.js"></script><p>之前我的两篇关于Druid连接池的文章讨论了一些关于连接保活和超时设置的问题，后来我又重新梳理了一Druid关于空闲连接检测以及<code>KeepAlive</code>执行的过程，本文其实已经写了很久了，当时是基于<code>1.2.4</code>版本，一直忘了发布上来。目前最新版是<code>1.2.8</code>版本，建议升级到最新版本，因为<code>1.2.4</code>版本存在一些连接检测异常被错误丢弃和<code>KeepAlive</code>的一些Bug，不过本文探讨的大致流程没有变化，因此还是基于<code>1.2.4</code>版本说明，但是请关注新版本的Releases Note。</p><a id="more"></a><h3 id="空闲连接检测"><a href="#空闲连接检测" class="headerlink" title="空闲连接检测"></a>空闲连接检测</h3><p> 这是一份简化的Druid配置</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="string">......</span></span><br><span class="line"><span class="attr">min-idle:</span> <span class="number">1</span></span><br><span class="line"><span class="attr">max-active:</span> <span class="number">20</span></span><br><span class="line"><span class="comment">#空闲检测时间</span></span><br><span class="line"><span class="attr">time-between-eviction-runs-millis:</span> <span class="number">60000</span></span><br><span class="line"><span class="comment">#空闲保活时间，超过时间需要保活，默认2*60s</span></span><br><span class="line"><span class="attr">keep-alive-between-time-millis:</span> <span class="number">120000</span></span><br><span class="line"><span class="comment">#最小空闲时间，即多余min-idle数目的连接空闲存活时间</span></span><br><span class="line"><span class="attr">min-evictable-idle-time-millis:</span> <span class="number">300000</span></span><br><span class="line"><span class="comment">#最大空闲时间，min-idle连接空闲存活时间</span></span><br><span class="line"><span class="attr">max-evictable-idle-time-millis:</span> <span class="number">3600000</span></span><br><span class="line"><span class="attr">validation-query:</span> <span class="string">SELECT</span> <span class="number">1</span></span><br><span class="line"><span class="attr">validation-query-timeout:</span> <span class="number">10</span></span><br><span class="line"><span class="attr">keep-alive:</span> <span class="literal">true</span></span><br><span class="line"><span class="attr">test-while-idle:</span> <span class="literal">true</span></span><br><span class="line"><span class="attr">test-on-borrow:</span> <span class="literal">false</span></span><br><span class="line"><span class="attr">test-on-return:</span> <span class="literal">false</span></span><br><span class="line"><span class="string">......</span></span><br></pre></td></tr></table></figure><p>在当前Druid 1.2.4版本，<code>DestroyTask</code>线程会按照<code>time-between-eviction-runs-millis</code>时间间隔检测空闲连接，当<code>idleMillis</code>（连接空闲时间=当前系统时间-<code>lastActiveTimeMillis</code>）&gt;  <code>min-evictable-idle-time-millis</code>,会驱逐多余超过<code>min-idle</code>数量的连接，直到<code>idleMillis</code> &gt; <code>max-evictable-idle-time-millis</code>,<code>min-idle</code>的连接也会被关闭重新建立。如果开启<code>keep-alive</code>，当<code>idleMillis</code>&gt;  <code>keep-alive-between-time-millis</code>，会对连接进行心跳保活,首先会执行连接检测，不同数据源的检测方式不同，MySQL连接检测有两种方式<code>ping</code>和<code>validation-query</code>，默认使用<code>ping</code>，只检测连接有效，不会刷新相关时间参数，检测之后刷新<code>lastKeepTimeMillis</code>。检测的超时时间都是取自<code>validation-query-timeout</code>，默认是1。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br></pre></td><td class="code"><pre><span class="line">   </span><br><span class="line"><span class="keyword">long</span> idleMillis = currentTimeMillis - connection.lastActiveTimeMillis;</span><br><span class="line"> <span class="comment">//空闲时间小，直接跳出</span></span><br><span class="line">   <span class="keyword">if</span> (idleMillis &lt; minEvictableIdleTimeMillis</span><br><span class="line">           &amp;&amp; idleMillis &lt; keepAliveBetweenTimeMillis</span><br><span class="line">   ) &#123;</span><br><span class="line">       <span class="keyword">break</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> (idleMillis &gt;= minEvictableIdleTimeMillis) &#123;</span><br><span class="line">       <span class="keyword">if</span> (checkTime &amp;&amp; i &lt; checkCount) &#123;</span><br><span class="line">           evictConnections[evictCount++] = connection;</span><br><span class="line">           <span class="keyword">continue</span>;</span><br><span class="line">       &#125; <span class="keyword">else</span> <span class="keyword">if</span> (idleMillis &gt; maxEvictableIdleTimeMillis) &#123;</span><br><span class="line">           evictConnections[evictCount++] = connection;</span><br><span class="line">           <span class="keyword">continue</span>;</span><br><span class="line">       &#125;</span><br><span class="line">   &#125;</span><br><span class="line"> <span class="comment">//是否需要keepAlive</span></span><br><span class="line">   <span class="keyword">if</span> (keepAlive &amp;&amp; idleMillis &gt;= keepAliveBetweenTimeMillis) &#123;</span><br><span class="line">       keepAliveConnections[keepAliveCount++] = connection;</span><br><span class="line">   &#125;</span><br><span class="line"></span><br><span class="line">......</span><br><span class="line"> <span class="comment">//进行keepAlive</span></span><br><span class="line">   <span class="keyword">if</span> (keepAliveCount &gt; <span class="number">0</span>) &#123;</span><br><span class="line">       <span class="comment">// keep order</span></span><br><span class="line">       <span class="keyword">for</span> (<span class="keyword">int</span> i = keepAliveCount - <span class="number">1</span>; i &gt;= <span class="number">0</span>; --i) &#123;</span><br><span class="line">          ......</span><br><span class="line"></span><br><span class="line">           <span class="keyword">boolean</span> validate = <span class="keyword">false</span>;</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">               <span class="keyword">this</span>.validateConnection(connection);</span><br><span class="line">               validate = <span class="keyword">true</span>;</span><br><span class="line">           &#125; <span class="keyword">catch</span> (Throwable error) &#123;</span><br><span class="line">               ......</span><br><span class="line">           &#125;</span><br><span class="line">           <span class="keyword">boolean</span> discard = !validate;</span><br><span class="line">           <span class="keyword">if</span> (validate) &#123;</span><br><span class="line">              <span class="comment">//刷新时间</span></span><br><span class="line">               holer.lastKeepTimeMillis = System.currentTimeMillis();</span><br><span class="line">               ......</span><br><span class="line">           &#125;</span><br><span class="line">       ......</span><br><span class="line"></span><br><span class="line">       <span class="keyword">this</span>.getDataSourceStat().addKeepAliveCheckCount(keepAliveCount);</span><br><span class="line">       Arrays.fill(keepAliveConnections, <span class="keyword">null</span>);</span><br><span class="line">   &#125;</span><br></pre></td></tr></table></figure><p>当开启了<code>test-while-idle</code>，获取连接后会检测空闲连接，空闲的判断逻辑大致为取<code>lastActiveTimeMillis</code>和<code>lastKeepTimeMillis</code>的最大值和当前系统时间对比，如果超过了<code>time-between-eviction-runs-millis</code>，就认为连接空闲，需要检测，检测的第一步和上面类似，默认使用<code>ping</code>，成功之后再次判断连接的空闲时间，此处是通过反射获取MySQL连接的<code>lastPacketReceivedTimeMs</code>，<code>ping</code>不会刷新这个时间，如果当前时间-<code>lastPacketReceivedTimeMs</code>&gt;<code>time-between-eviction-runs-millis</code>，则会认为连接已经超过空闲时间，于是抛弃这个连接，打印WARN日志<code>discard long time none received connection</code>.</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line">   <span class="keyword">if</span> (testWhileIdle) &#123;</span><br><span class="line">  ......</span><br><span class="line">       <span class="keyword">if</span> (lastKeepTimeMillis &gt; lastActiveTimeMillis) &#123;</span><br><span class="line">           lastActiveTimeMillis = lastKeepTimeMillis;</span><br><span class="line">       &#125;</span><br><span class="line"></span><br><span class="line">       <span class="keyword">long</span> idleMillis                    = currentTimeMillis - lastActiveTimeMillis;</span><br><span class="line"> ......</span><br><span class="line">       <span class="keyword">if</span> (idleMillis &gt;= timeBetweenEvictionRunsMillis</span><br><span class="line">               || idleMillis &lt; <span class="number">0</span> <span class="comment">// unexcepted branch</span></span><br><span class="line">               ) &#123;</span><br><span class="line">         <span class="comment">//空闲连接需要检测</span></span><br><span class="line">           <span class="keyword">boolean</span> validate = testConnectionInternal(poolableConnection.holder, poolableConnection.conn);</span><br><span class="line">           ......</span><br><span class="line">       &#125;</span><br><span class="line">   &#125;     </span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment">//此处检测连接</span></span><br><span class="line"><span class="keyword">boolean</span> valid = validConnectionChecker.isValidConnection(conn, validationQuery, validationQueryTimeout);</span><br><span class="line">   ......</span><br><span class="line">   <span class="keyword">if</span> (valid &amp;&amp; isMySql) &#123; <span class="comment">// unexcepted branch</span></span><br><span class="line">       <span class="keyword">long</span> lastPacketReceivedTimeMs = MySqlUtils.getLastPacketReceivedTimeMs(conn);</span><br><span class="line">       <span class="keyword">if</span> (lastPacketReceivedTimeMs &gt; <span class="number">0</span>) &#123;</span><br><span class="line">           <span class="keyword">long</span> mysqlIdleMillis = currentTimeMillis - lastPacketReceivedTimeMs;</span><br><span class="line">           <span class="keyword">if</span> (lastPacketReceivedTimeMs &gt; <span class="number">0</span> <span class="comment">//判断空闲连接空闲时间</span></span><br><span class="line">                   &amp;&amp; mysqlIdleMillis &gt;= timeBetweenEvictionRunsMillis) &#123;</span><br><span class="line">               discardConnection(holder);</span><br><span class="line">               String errorMsg = <span class="string">"discard long time none received connection. "</span></span><br><span class="line">                       + <span class="string">", jdbcUrl : "</span> + jdbcUrl</span><br><span class="line">                       + <span class="string">", version : "</span> + VERSION.getVersionNumber()</span><br><span class="line">                       + <span class="string">", lastPacketReceivedIdleMillis : "</span> + mysqlIdleMillis;</span><br><span class="line">               LOG.warn(errorMsg);</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></pre></td></tr></table></figure><p>根据当前的配置，每60s执行一次空闲检测，但是只有空闲超过120s才会执行<code>keepalive</code>，所以超过空闲超过60s的连接不做处理，如果此时获取该连接，空闲连接检测生效，就会丢掉该连接.如果将最小<code>keepalive</code>时间改为和空闲检测一致，每次空闲检测都会刷新<code>lastKeepTimeMillis</code>，这样再获取连接不会进行空闲检测.但是这种是理想情况，如果0s进行了keepalive，间隔20s之后，执行数据库操作，当60s时，空闲为40s，无需<code>keepalive</code>，当100s时，此时空闲时间为80s，如果此时获取连接，则又会进行连接空闲检测抛弃连接.（<code>1.2.6</code>版本已经要求<code>keepAliveBetweenTimeMillis</code>必须要大于<code>timeBetweenEvictionRunsMillis</code>）</p><p>Druid检测MySQL连接的方式是根据一个系统属性<code>druid.mysql.usePingMethod</code>,没有设置的情况如果有<code>ping method</code>下会使用<code>MySQL ping</code>进行连接检测</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br></pre></td><td class="code"><pre><span class="line">  <span class="function"><span class="keyword">public</span> <span class="title">MySqlValidConnectionChecker</span><span class="params">()</span></span>&#123;</span><br><span class="line">      <span class="keyword">try</span> &#123;</span><br><span class="line">          clazz = Utils.loadClass(<span class="string">"com.mysql.jdbc.MySQLConnection"</span>);</span><br><span class="line">          <span class="keyword">if</span> (clazz == <span class="keyword">null</span>) &#123;</span><br><span class="line">              clazz = Utils.loadClass(<span class="string">"com.mysql.cj.jdbc.ConnectionImpl"</span>);</span><br><span class="line">          &#125;</span><br><span class="line"></span><br><span class="line">          <span class="keyword">if</span> (clazz != <span class="keyword">null</span>) &#123;</span><br><span class="line">              ping = clazz.getMethod(<span class="string">"pingInternal"</span>, <span class="keyword">boolean</span>.class, <span class="keyword">int</span>.class);</span><br><span class="line">          &#125;</span><br><span class="line"></span><br><span class="line">          <span class="keyword">if</span> (ping != <span class="keyword">null</span>) &#123;</span><br><span class="line">              usePingMethod = <span class="keyword">true</span>;</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">          LOG.warn(<span class="string">"Cannot resolve com.mysql.jdbc.Connection.ping method.  Will use 'SELECT 1' instead."</span>, e);</span><br><span class="line">      &#125;</span><br><span class="line"></span><br><span class="line">      configFromProperties(System.getProperties());</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">configFromProperties</span><span class="params">(Properties properties)</span> </span>&#123;</span><br><span class="line">      String property = properties.getProperty(<span class="string">"druid.mysql.usePingMethod"</span>);</span><br><span class="line">      <span class="keyword">if</span> (<span class="string">"true"</span>.equals(property)) &#123;</span><br><span class="line">          setUsePingMethod(<span class="keyword">true</span>);</span><br><span class="line">      &#125; <span class="keyword">else</span> <span class="keyword">if</span> (<span class="string">"false"</span>.equals(property)) &#123;</span><br><span class="line">          setUsePingMethod(<span class="keyword">false</span>);</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">public</span> <span class="keyword">boolean</span> <span class="title">isValidConnection</span><span class="params">(Connection conn, String validateQuery, <span class="keyword">int</span> validationQueryTimeout)</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">   </span><br><span class="line">      <span class="keyword">if</span> (usePingMethod) &#123;</span><br><span class="line">          ......</span><br><span class="line"><span class="comment">//ping检测后返回成功</span></span><br><span class="line">          <span class="keyword">if</span> (clazz.isAssignableFrom(conn.getClass())) &#123;</span><br><span class="line">              <span class="keyword">if</span> (validationQueryTimeout &lt;= <span class="number">0</span>) &#123;</span><br><span class="line">                  validationQueryTimeout = DEFAULT_VALIDATION_QUERY_TIMEOUT;</span><br><span class="line">              &#125;</span><br><span class="line"></span><br><span class="line">              <span class="keyword">try</span> &#123;</span><br><span class="line">                  ping.invoke(conn, <span class="keyword">true</span>, validationQueryTimeout * <span class="number">1000</span>);</span><br><span class="line">              &#125; <span class="keyword">catch</span> (InvocationTargetException e) &#123;</span><br><span class="line">                  Throwable cause = e.getCause();</span><br><span class="line">                  <span class="keyword">if</span> (cause <span class="keyword">instanceof</span> SQLException) &#123;</span><br><span class="line">                      <span class="keyword">throw</span> (SQLException) cause;</span><br><span class="line">                  &#125;</span><br><span class="line">                  <span class="keyword">throw</span> e;</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><span class="line">      &#125;</span><br><span class="line"><span class="comment">//Select 检测</span></span><br><span class="line">      String query = validateQuery;</span><br><span class="line">      <span class="keyword">if</span> (validateQuery == <span class="keyword">null</span> || validateQuery.isEmpty()) &#123;</span><br><span class="line">          query = DEFAULT_VALIDATION_QUERY;</span><br><span class="line">      &#125;</span><br><span class="line"></span><br><span class="line">      Statement stmt = <span class="keyword">null</span>;</span><br><span class="line">      ResultSet rs = <span class="keyword">null</span>;</span><br><span class="line">      <span class="keyword">try</span> &#123;</span><br><span class="line">          stmt = conn.createStatement();</span><br><span class="line">          <span class="keyword">if</span> (validationQueryTimeout &gt; <span class="number">0</span>) &#123;</span><br><span class="line">              stmt.setQueryTimeout(validationQueryTimeout);</span><br><span class="line">          &#125;</span><br><span class="line">          rs = stmt.executeQuery(query);</span><br><span class="line">          <span class="keyword">return</span> <span class="keyword">true</span>;</span><br><span class="line">      &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">          JdbcUtils.close(rs);</span><br><span class="line">          JdbcUtils.close(stmt);</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="MySql空闲连接关闭Warn日志解决"><a href="#MySql空闲连接关闭Warn日志解决" class="headerlink" title="MySql空闲连接关闭Warn日志解决"></a>MySql空闲连接关闭Warn日志解决</h3><p>如果不想出现空闲连接被强制关闭并且出现这个Warn日志，也很好解决</p><p>只需要将<code>druid.mysql.usePingMethod</code>设置为<code>false</code>，这样每次连接检测都会执行<code>validation-query</code>语句，因此不会再丢弃空闲连接，由于该配置为系统属性，可以通过启动参数<code>-Ddruid.mysql.usePingMethod=false</code>或者代码配置</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@PostConstruct</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">()</span></span>&#123;</span><br><span class="line">    System.setProperty(<span class="string">"druid.mysql.usePingMethod"</span>,<span class="string">"false"</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;之前我的两篇关于Druid连接池的文章讨论了一些关于连接保活和超时设置的问题，后来我又重新梳理了一Druid关于空闲连接检测以及&lt;code&gt;KeepAlive&lt;/code&gt;执行的过程，本文其实已经写了很久了，当时是基于&lt;code&gt;1.2.4&lt;/code&gt;版本，一直忘了发布上来。目前最新版是&lt;code&gt;1.2.8&lt;/code&gt;版本，建议升级到最新版本，因为&lt;code&gt;1.2.4&lt;/code&gt;版本存在一些连接检测异常被错误丢弃和&lt;code&gt;KeepAlive&lt;/code&gt;的一些Bug，不过本文探讨的大致流程没有变化，因此还是基于&lt;code&gt;1.2.4&lt;/code&gt;版本说明，但是请关注新版本的Releases Note。&lt;/p&gt;
    
    </summary>
    
      <category term="Java" scheme="https://www.elietio.xyz/categories/Java/"/>
    
    
      <category term="Druid" scheme="https://www.elietio.xyz/tags/Druid/"/>
    
      <category term="keepAlived" scheme="https://www.elietio.xyz/tags/keepAlived/"/>
    
      <category term="MySql discard long time none received connection" scheme="https://www.elietio.xyz/tags/MySql-discard-long-time-none-received-connection/"/>
    
  </entry>
  
  <entry>
    <title>SimpleDateFormat转换两位年&#39;yy&#39;少了100年？和defaultCenturyStartYear的值有关</title>
    <link href="https://www.elietio.xyz/posts/b4d79db5.html"/>
    <id>https://www.elietio.xyz/posts/b4d79db5.html</id>
    <published>2021-09-11T02:12:00.000Z</published>
    <updated>2021-09-11T03:25:22.213Z</updated>
    
    <content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/assets/css/APlayer.min.css"><script src="/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><script class="meting-secondary-script-marker" src="/assets/js/Meting.min.js"></script><p>最近遇到了一个哭笑不得的事情，生产上面一个日期207x年变成了197x年，少了100年，排查下来原因也是让人大跌眼镜，某位同学使用了<code>SimpleDateForma</code>类将一个两位数年的日期格式’yy/MM/dd’转换为Date类型，然后再转成’yyyy-MM-dd’字符串，而就是这个转换过程中丢掉了100年。<br><a id="more"></a></p><h3 id="问题排查："><a href="#问题排查：" class="headerlink" title="问题排查："></a>问题排查：</h3><p>首先感觉比较奇怪的是这个场景肯定测试过，这么显眼的问题不可能没有发现，那么是否与年份有关系，于是试了一些两位数的日期年份，发现超过当前年20年之后就会变成19xx年，少了100年，而20年之内就是正常的20xx年，那么肯定是 <code>SimpleDateFormat</code> 类parse ‘yy’的过程中有相关设置，于是就去翻相关代码，发现了这样的逻辑：</p><p><code>SimpleDateFormat</code>类<code>initialize</code>的时候会执行<code>initializeDefaultCentury()</code>方法，方法源码如下:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/* Initialize the fields we use to disambiguate ambiguous years. Separate</span></span><br><span class="line"><span class="comment"> * so we can call it from readObject().</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">initializeDefaultCentury</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    calendar.setTimeInMillis(System.currentTimeMillis());</span><br><span class="line">  <span class="comment">//注意此处将当前年年份减去80年</span></span><br><span class="line">    calendar.add( Calendar.YEAR, -<span class="number">80</span> );</span><br><span class="line">    parseAmbiguousDatesAsAfter(calendar.getTime());</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/* Define one-century window into which to disambiguate dates using</span></span><br><span class="line"><span class="comment"> * two-digit years.</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">parseAmbiguousDatesAsAfter</span><span class="params">(Date startDate)</span> </span>&#123;</span><br><span class="line">    defaultCenturyStart = startDate;</span><br><span class="line">    calendar.setTime(startDate);</span><br><span class="line">    <span class="comment">//此处年份已经比当前少了80年</span></span><br><span class="line">    defaultCenturyStartYear = calendar.get(Calendar.YEAR);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>可以看到，为了消除两位数的年的时间模糊，会去定义一个默认的世纪开始年份，默认值为当前年份向前80年，然后当执行<code>parse</code>方法时，会调用<code>subParse</code>方法，源码大致如下,只保留了一下相关逻辑：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * Private member function that converts the parsed date strings into</span></span><br><span class="line"><span class="comment"> * timeFields. Returns -start (for ParsePosition) if failed.</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> text the time text to be parsed.</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> start where to start parsing.</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> patternCharIndex the index of the pattern character.</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> count the count of a pattern character.</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> obeyCount if true, then the next field directly abuts this one,</span></span><br><span class="line"><span class="comment"> * and we should use the count to know when to stop parsing.</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> ambiguousYear return parameter; upon return, if ambiguousYear[0]</span></span><br><span class="line"><span class="comment"> * is true, then a two-digit year was parsed and may need to be readjusted.</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> origPos origPos.errorIndex is used to return an error index</span></span><br><span class="line"><span class="comment"> * at which a parse error occurred, if matching failure occurs.</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@return</span> the new start position if matching succeeded; -1 indicating</span></span><br><span class="line"><span class="comment"> * matching failure, otherwise. In case matching failure occurred,</span></span><br><span class="line"><span class="comment"> * an error index is set to origPos.errorIndex.</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">int</span> <span class="title">subParse</span><span class="params">(String text, <span class="keyword">int</span> start, <span class="keyword">int</span> patternCharIndex, <span class="keyword">int</span> count,</span></span></span><br><span class="line"><span class="function"><span class="params">                     <span class="keyword">boolean</span> obeyCount, <span class="keyword">boolean</span>[] ambiguousYear,</span></span></span><br><span class="line"><span class="function"><span class="params">                     ParsePosition origPos,</span></span></span><br><span class="line"><span class="function"><span class="params">                     <span class="keyword">boolean</span> useFollowingMinusSignAsDelimiter, CalendarBuilder calb)</span> </span>&#123;</span><br><span class="line">    Number number;</span><br><span class="line">    <span class="keyword">int</span> value = <span class="number">0</span>;</span><br><span class="line">    ParsePosition pos = <span class="keyword">new</span> ParsePosition(<span class="number">0</span>);</span><br><span class="line">    pos.index = start;</span><br><span class="line">    ......</span><br><span class="line"></span><br><span class="line">  parsing:</span><br><span class="line">    &#123;</span><br><span class="line">        ......</span><br><span class="line">        <span class="keyword">case</span> PATTERN_YEAR:      <span class="comment">// 'y'</span></span><br><span class="line">            ......</span><br><span class="line"></span><br><span class="line">            <span class="comment">// If there are 3 or more YEAR pattern characters, this indicates</span></span><br><span class="line">            <span class="comment">// that the year value is to be treated literally, without any</span></span><br><span class="line">            <span class="comment">// two-digit year adjustments (e.g., from "01" to 2001).  Otherwise</span></span><br><span class="line">            <span class="comment">// we made adjustments to place the 2-digit year in the proper</span></span><br><span class="line">            <span class="comment">// century, for parsed strings from "00" to "99".  Any other string</span></span><br><span class="line">            <span class="comment">// is treated literally:  "2250", "-1", "1", "002".</span></span><br><span class="line">            <span class="keyword">if</span> (count &lt;= <span class="number">2</span> &amp;&amp; (pos.index - actualStart) == <span class="number">2</span></span><br><span class="line">                &amp;&amp; Character.isDigit(text.charAt(actualStart))</span><br><span class="line">                &amp;&amp; Character.isDigit(text.charAt(actualStart + <span class="number">1</span>))) &#123;</span><br><span class="line">                <span class="comment">// Assume for example that the defaultCenturyStart is 6/18/1903.</span></span><br><span class="line">                <span class="comment">// This means that two-digit years will be forced into the range</span></span><br><span class="line">                <span class="comment">// 6/18/1903 to 6/17/2003.  As a result, years 00, 01, and 02</span></span><br><span class="line">                <span class="comment">// correspond to 2000, 2001, and 2002.  Years 04, 05, etc. correspond</span></span><br><span class="line">                <span class="comment">// to 1904, 1905, etc.  If the year is 03, then it is 2003 if the</span></span><br><span class="line">                <span class="comment">// other fields specify a date before 6/18, or 1903 if they specify a</span></span><br><span class="line">                <span class="comment">// date afterwards.  As a result, 03 is an ambiguous year.  All other</span></span><br><span class="line">                <span class="comment">// two-digit years are unambiguous.</span></span><br><span class="line">                <span class="keyword">int</span> ambiguousTwoDigitYear = defaultCenturyStartYear % <span class="number">100</span>;</span><br><span class="line">                ambiguousYear[<span class="number">0</span>] = value == ambiguousTwoDigitYear;</span><br><span class="line">                value += (defaultCenturyStartYear/<span class="number">100</span>)*<span class="number">100</span> +</span><br><span class="line">                    (value &lt; ambiguousTwoDigitYear ? <span class="number">100</span> : <span class="number">0</span>);</span><br><span class="line">            &#125;</span><br><span class="line">            calb.set(field, value);</span><br><span class="line">            <span class="keyword">return</span> pos.index;</span><br><span class="line"></span><br><span class="line">       ......</span><br><span class="line">    <span class="comment">// Parsing failed.</span></span><br><span class="line">    origPos.errorIndex = pos.index;</span><br><span class="line">    <span class="keyword">return</span> -<span class="number">1</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>如果’yy’的值比当前年份减去80年的<code>defaultCenturyStartYear</code>后两位小，那么取<code>defaultCenturyStartYear</code>两位补齐’yy’，并且再加上100年，否则直接补齐不加100，那么对于’71’,当前<code>defaultCenturyStartYear</code>为1941,71大于41,所以最终就变成了1971，所以问题原因就在于<code>defaultCenturyStartYear</code>这个值默认是当前年份减去80的年，当然这个值也能修改</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * Sets the 100-year period 2-digit years will be interpreted as being in</span></span><br><span class="line"><span class="comment"> * to begin on the date the user specifies.</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> startDate During parsing, two digit years will be placed in the range</span></span><br><span class="line"><span class="comment"> * &lt;code&gt;startDate&lt;/code&gt; to &lt;code&gt;startDate + 100 years&lt;/code&gt;.</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@see</span> #get2DigitYearStart</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@since</span> 1.2</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">set2DigitYearStart</span><span class="params">(Date startDate)</span> </span>&#123;</span><br><span class="line">    parseAmbiguousDatesAsAfter(<span class="keyword">new</span> Date(startDate.getTime()));</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="结论："><a href="#结论：" class="headerlink" title="结论："></a>结论：</h3><p>所以，当解析两位年份的时候，<code>SimpleDateFormat</code>的<code>parse</code>方法会自动补齐前两位，补齐的规则是先初始化一个世纪开始年份，默认是当前日期减去80年的年份，然后补齐的年份会处于这个世纪开始年份的100年内，不能超过，因此就出现了超过当前20年的两位年份被补齐成过去的日期，少了100年，当然这个世纪开始年份也可以进行修改，设置成当前世纪的开始年份，这样日期都会补齐为当前世纪。</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;最近遇到了一个哭笑不得的事情，生产上面一个日期207x年变成了197x年，少了100年，排查下来原因也是让人大跌眼镜，某位同学使用了&lt;code&gt;SimpleDateForma&lt;/code&gt;类将一个两位数年的日期格式’yy/MM/dd’转换为Date类型，然后再转成’yyyy-MM-dd’字符串，而就是这个转换过程中丢掉了100年。&lt;br&gt;
    
    </summary>
    
      <category term="Java" scheme="https://www.elietio.xyz/categories/Java/"/>
    
    
      <category term="SimpleDateFormat" scheme="https://www.elietio.xyz/tags/SimpleDateFormat/"/>
    
      <category term="initializeDefaultCentury" scheme="https://www.elietio.xyz/tags/initializeDefaultCentury/"/>
    
      <category term="defaultCenturyStartYear" scheme="https://www.elietio.xyz/tags/defaultCenturyStartYear/"/>
    
      <category term="转换日期少了100年" scheme="https://www.elietio.xyz/tags/%E8%BD%AC%E6%8D%A2%E6%97%A5%E6%9C%9F%E5%B0%91%E4%BA%86100%E5%B9%B4/"/>
    
  </entry>
  
  <entry>
    <title>TreeSet使用中的两个Exception</title>
    <link href="https://www.elietio.xyz/posts/264bd07b.html"/>
    <id>https://www.elietio.xyz/posts/264bd07b.html</id>
    <published>2021-01-17T04:48:26.000Z</published>
    <updated>2021-09-11T03:11:48.420Z</updated>
    
    <content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/assets/css/APlayer.min.css"><script src="/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><script class="meting-secondary-script-marker" src="/assets/js/Meting.min.js"></script><p>最近的一个业务场景中需要在内存中换成一些数据,并且需要根据时间戳有序排列,因此使用了TreeSet,但是在使用过程中确出现了IllegalArgumentException和ConcurrentModificationException,因此记录一下这两个问题.</p><a id="more"></a><h2 id="IllegalArgumentException"><a href="#IllegalArgumentException" class="headerlink" title="IllegalArgumentException"></a>IllegalArgumentException</h2><p>首先是</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">java.lang.IllegalArgumentException: fromKey &gt; toKey</span><br></pre></td></tr></table></figure><p>我们的一个业务场景是需要对内存中的一些数据根据指定区间筛选出对应数据排列好之后返回给前端,因此我们选用了有序集合TreeSet,当前端传入一个区间范围[A,B]之后,可以使用<code>E ceiling(E e)</code>返回大于等于A的最小元素,使用<code>E floor(E e)</code>返回小于等于B的最大元素,这样就可以使用subSet就可以返回指定区间的set集合</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="function">NavigableSet&lt;E&gt; <span class="title">subSet</span><span class="params">(E fromElement, <span class="keyword">boolean</span> fromInclusive,</span></span></span><br><span class="line"><span class="function"><span class="params">                 E toElement,   <span class="keyword">boolean</span> toInclusive)</span></span></span><br></pre></td></tr></table></figure><p>这样看来,这个思路并没有什么问题.但是代码部署之后,测试环境缺偶尔抛出了<code>java.lang.IllegalArgumentException: fromKey &gt; toKey</code>异常,简而言之就是fromElement大于toElement,无法返回指定范围的set,由于入参的时候已经对于A、B大小已经进行了校验,那么只能是使用ceiling和floor方法导致返回的元素出现了问题,查看了原数据和传入的区间参数之后发现了这样一个问题,例如TreeSet存储以下元素</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">[<span class="number">1</span>,<span class="number">3</span>,<span class="number">7</span>,<span class="number">8</span>,<span class="number">9</span>]</span><br></pre></td></tr></table></figure><p>而传入的区间为[4,6],<code>ceiling(4)</code>返回7,而<code>floor(6)</code>返回3,那么<code>subSet(7,3)</code>会抛出IllegalArgumentException也就不奇怪了,因此,当使用subSet方法时一定要确保fromElement小于toElement,加上这个二次校验之后,这个问题就再也没出现过了.但是过了不久之后又出现了另外一个问题</p><h2 id="ConcurrentModificationException"><a href="#ConcurrentModificationException" class="headerlink" title="ConcurrentModificationException"></a>ConcurrentModificationException</h2><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">java.util.ConcurrentModificationException</span><br></pre></td></tr></table></figure><p>这个异常也很清晰,由于并发安全问题导致的,多线程同时修改或者同时读取和修改都会导致这个问题.</p><p>我们知道,iterator遍历元素时通过源列表直接删除元素会导致ConcurrentModificationException,必须使用iterator的remove方法才能安全删除元素,使用Iterator会返回集合自身的一个迭代器,这个机制与这两个字段有关</p><ul><li>expectedModCount 预期被修改的次数,属于迭代器<code>私有</code>,初始时和modCount相等</li><li>modCount 集合被结构性修改(新增或删除)的次数,它是属于集合的</li></ul><p>当进行遍历/删除时都会判断modCount和expectedModCount是否相等,不等就会抛出ConcurrentModificationException,而只有使用迭代器的remove方法才会更新expectedModCount值确保二者相等.以下相关源码</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">final</span> Entry&lt;K,V&gt; <span class="title">nextEntry</span><span class="params">()</span> </span>&#123;</span><br><span class="line"> Entry&lt;K,V&gt; e = next;</span><br><span class="line">     <span class="keyword">if</span> (e == <span class="keyword">null</span>)</span><br><span class="line">         <span class="keyword">throw</span> <span class="keyword">new</span> NoSuchElementException();</span><br><span class="line">     <span class="keyword">if</span> (modCount != expectedModCount)</span><br><span class="line">         <span class="keyword">throw</span> <span class="keyword">new</span> ConcurrentModificationException();</span><br><span class="line">     next = successor(e);</span><br><span class="line">     lastReturned = e;</span><br><span class="line">     <span class="keyword">return</span> e;</span><br><span class="line">           </span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">remove</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (lastReturned == <span class="keyword">null</span>)</span><br><span class="line">        <span class="keyword">throw</span> <span class="keyword">new</span> IllegalStateException();</span><br><span class="line">    <span class="keyword">if</span> (modCount != expectedModCount)</span><br><span class="line">        <span class="keyword">throw</span> <span class="keyword">new</span> ConcurrentModificationException();</span><br><span class="line">    <span class="comment">// deleted entries are replaced by their successors</span></span><br><span class="line">    <span class="keyword">if</span> (lastReturned.left != <span class="keyword">null</span> &amp;&amp; lastReturned.right != <span class="keyword">null</span>)</span><br><span class="line">        next = lastReturned;</span><br><span class="line">    deleteEntry(lastReturned);</span><br><span class="line">    expectedModCount = modCount;</span><br><span class="line">    lastReturned = <span class="keyword">null</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>所以必须要求Iterator迭代过程中必须使用Iterator的remove方法,但是这仅限于单线程,当多线程情况下,即使使用Iterator的remove方法仍然会有线程安全问题,因为迭代器是线程私有的,所以expectedModCount也是线程私有的,而modCount是线程共享的.如果有一个线程对集合进行了修改,那么modCount和此线程的expectedModCount会更新,但是其他线程的expectedModCount都不会更新,expectedModCount!=modCount,最终抛出ConcurrentModificationException.</p><p>我们正是犯了了这个问题,多线程同时读取和修改,导致产生了线程安全问题.所以需要将TreeSet换成线程安全的有序Set集合SynchronizedSortedSet.</p><p> java.util 包中的集合类都返回 fail-fast 迭代器,具有强一致性,当迭代器检测到迭代过程中元素进行了更改,就会抛出ConcurrentModificationException.而java.util.concurrent包中的集合类返回的是weakly consistent迭代器,即弱一致迭代器,当迭代开始,如果元素在迭代到达前被删除或者修改,这些更改会返回给调用者,但是对于插入元素则无法保证,并且不会抛出ConcurrentModificationException.</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;最近的一个业务场景中需要在内存中换成一些数据,并且需要根据时间戳有序排列,因此使用了TreeSet,但是在使用过程中确出现了IllegalArgumentException和ConcurrentModificationException,因此记录一下这两个问题.&lt;/p&gt;
    
    </summary>
    
      <category term="Java" scheme="https://www.elietio.xyz/categories/Java/"/>
    
    
      <category term="TreeSet" scheme="https://www.elietio.xyz/tags/TreeSet/"/>
    
      <category term="IllegalArgumentException" scheme="https://www.elietio.xyz/tags/IllegalArgumentException/"/>
    
      <category term="ConcurrentModificationException" scheme="https://www.elietio.xyz/tags/ConcurrentModificationException/"/>
    
  </entry>
  
  <entry>
    <title>解决PlayStation商店网页版无法连接到服务器18.xxx错误、PS App无法登录</title>
    <link href="https://www.elietio.xyz/posts/8f94cbbe.html"/>
    <id>https://www.elietio.xyz/posts/8f94cbbe.html</id>
    <published>2020-11-01T03:15:45.000Z</published>
    <updated>2020-11-01T05:49:57.543Z</updated>
    
    <content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/assets/css/APlayer.min.css"><script src="/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><script class="meting-secondary-script-marker" src="/assets/js/Meting.min.js"></script><p>临近PS5发售，索尼对PlayStation Store网页版和PS App都进行了大的更新，包含UI和功能，然而我已经大半年几乎无法正常登录网页版ps商店了，无论是win10的Chrome还是安卓的Chrome，每次登录都是提示无法连接到服务器，然后一串长长的(18.xxxx.xxxx.xxx)错误码，安卓版App由于登录也是调起网页进行登录，因此遇到同样的问题，然而我Mac的Chrome却一切正常，甚至win10和安卓的其他浏览器也是ok的，只有Chrome和PS的那几个App使用WebView让人崩溃。。。</p><a id="more"></a><p>这次更新之后，我试了下问题依旧，甚至PS App连登录页面都进不去，直接提示无法登录错误，而且似乎有不少人都遇到这个问题，于是尝试解决这俩问题</p><h2 id="PlayStation-Store网页版无法连接到服务器"><a href="#PlayStation-Store网页版无法连接到服务器" class="headerlink" title="PlayStation Store网页版无法连接到服务器"></a>PlayStation Store网页版无法连接到服务器</h2><p>先说一下，这个问题与cookie有关系，具体确切原因我也没有确定，但是能够解决问题</p><p><img src="/img/image-20201101115120601.jpg" alt="img" style="zoom:50%;"></p><p>只有chrome无法登录而其他浏览器正常，于是我尝试了基于Chromium的最新版Edge，结果也是正常的，于是打开控制台，对比二者的差别，于是发现了Chrome在登录的时候会有一个请求被403了，请求链接是ca.account.sony.com/api/v1/ssocookie</p><p><img src="/img/image-20201101121039423.png" alt="image-20201101121039423"></p><p>看样子似乎和cookie有什么关系，网上搜索了一下，也有其他人不同的浏览器遇到了同样问题，<a href="https://github.com/FriendsOfGalaxy/galaxy-integration-psn/issues/7#issuecomment-618926912" target="_blank" rel="noopener">有人这样解释</a></p><blockquote><p>Reproduced on older Chromium v74 while works in newer browser version. I’ve send request to Galaxy to update their inner browser version. It may help but not for sure.</p><p>Problem is because we’re rejected with 403 while requesting auth cookie</p><p> <a href="https://auth.api.sonyentertainmentnetwork.com/2.0/ssocookie" target="_blank" rel="noopener">https://auth.api.sonyentertainmentnetwork.com/2.0/ssocookie</a></p><p>Akamai server blocks requests from older browser for some reason maybe because of SameSiteCookie policy, or CORS, or maybe because Akamai’s anti-bot script does not like Galaxy browser.</p><p>Login works when user requests are handled by direct PSN server (nginx header)<br>Login does not work when requests are handled by Akamai’s load balancer.<br>This is why it happen sometimes, not always.</p><p>Known workarounds:</p><p>use VPN<br>wait some time (like a day) and try to login again when there is smaller traffic.</p></blockquote><p>似乎并不能解决我的问题，我的Chrome是最新版，并且网络也是ok的，也看到了另外一个描述，Chrome 80版本之后cookie 的 SameSite 属性默认值由 None 变为 Lax，造成了一些访问跨域 cookie 无法携带的问题，尝试修改了Chrome相关设置，依然无效，这时候一条微博引起我的注意</p><p><img src="/img/image-20201101115220601.png" alt="image-20201101115220601"></p><p>于是进行了尝试，EditThisCookie插件拷贝了一下Edge登录页面的cookie值复制到Chrome登录页面上(直接拷贝Edge登录成功的cookie应该也是ok的，下图为截取的登录进去的cookie)，于是再次登录，果然这次终于成功了😭️</p><p><img src="/img/image-20201101121306201.png" alt="image-20201101121306201"></p><p>问题已经知道了，PC可以很方便的修改cookie，但是安卓端并没有方便的浏览器修改cookie插件，怎么办？还是有方法的，安卓Chrome的cookie是写到本地目录里面，并且是sqlite数据库文件，存储路径为<em>/data/data/com.android.chrome/app_chrome/Defaule/Cookie</em>，我们可以用SQLite编辑器打开它进行修改，于是先要找到一个正常的cookie，Edge，决定就是你了，下载登录，啊咧</p><p><img src="/img/image-20201101122529176.png" alt="image-20201101122529176"></p><p>这尼玛，算了，咱换一个，换上学生时代经常折腾而现在许久没有使用的Firefox吧，嗯，这次正常登录了，Firefox的cookie路径是<em>/data/data/org.mozilla.firefox/files/mozilla/xxxx.default/cookies.sqlite</em>，直接标明是个sqlite文件，中间是路径xxxx是随机字符串，打开之后同样将psn登录所需的cookie内容拷贝复制到Chrome的cookie文件里，这里强烈建议在PC端就行操作，手机端太麻烦了，我开了俩sqlite应用，一个查看，一个修改，手忙脚乱，弄完之后才想起了为啥我不用PC搞这玩意😓️，弄好之后，把Chrome停止掉，重新打开，登录，OK，大功告成！</p><p><img src="/img/image-20201101123200655.png" alt="image-20201101123200655" style="zoom:50%;"></p><h2 id="安卓PS-App无法登录"><a href="#安卓PS-App无法登录" class="headerlink" title="安卓PS App无法登录"></a>安卓PS App无法登录</h2><p>先说一下新版PS App登录需要的条件</p><ul><li>具有Google服务并能正常使用</li><li>第一次登录需通过SafetyNet检测</li><li><del>第一次登录需科学上网</del>（不一定是必须条件，打不开登录页面可以试试）</li></ul><p>不然就会出现如下错误</p><p><img src="/img/image-20201101131626953.png" alt="image-20201101131626953" style="zoom:50%;"></p><p>我新旧两版App都是无法登录，只是问题不太一样，旧版问题其实和Chrome问题一样，App通过WebView调起登录页面，而WebView是Chrome实现的，但是cookie文件存储在App自己的安装目录，而新版打开直接提示无法登录退出，推测这个可能和我Root了有关，尝试Magisk Hide和随机包名并未解决，于是暂时放弃，先搞定旧版App。</p><p>按照同样的套路，找到PS App的Cookie文件<em>/data/data/com.scee.psxandroid/app_webview/Default/Cookie</em>，同样进行修改，但是这次却没有成功。于是我换了个思路，PC端使用安卓模拟器成功登录App，拷贝出cookie，顺带一并拷贝登录用户数据库文件<em>/data/data/com.scee.psxandroid/databases/signin_user.db</em>，覆盖之后，停止应用，重新打开，由于读取了登录用户数据，不需要登录就直接进去了，但是旧的App就快要停用了，能登录似乎也不长久，于是继续搜索新版App的问题。</p><p>最终在P9看到网友回复，新版PS App初次打开需要检测SafetyNet，我擦，Sony你在搞啥，忒缺德了吧</p><p><img src="/img/image-20201101125544021.png" alt="image-20201101125544021" style="zoom: 50%;"></p><p>好吧，解决掉这个试试，SafetyNet检测各个手机状况不同，可以自行查询，我是临时禁用了所有Magisk模块重启之后就OK了，然后重新下载新版PS App，终于弹出了熟悉的登录页面，而当我又以为会再次无法连接到服务器需要修改cookie的时候，这次竟然直接登录上了🙂️，好吧，省事了，废了这么大劲终于搞定了，这时候可以再把Magisk模块启用，因为登录上就不会再检测SafetyNet了。</p><div class="note info no-icon">            <p>最后要吐槽一句Sony这新的PlayStation Store网页版和PS App的UI设计真是一言难尽，功能也称不上好用，不知道产品经理是什么脑回路(* ￣︿￣)。</p>          </div><p><img src="/img/image-20201101131429774.png" alt="image-20201101131429774"></p><p><img src="/img/image-20201101131240536.png" alt="image-20201101131240536" style="zoom: 50%;"></p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;临近PS5发售，索尼对PlayStation Store网页版和PS App都进行了大的更新，包含UI和功能，然而我已经大半年几乎无法正常登录网页版ps商店了，无论是win10的Chrome还是安卓的Chrome，每次登录都是提示无法连接到服务器，然后一串长长的(18.xxxx.xxxx.xxx)错误码，安卓版App由于登录也是调起网页进行登录，因此遇到同样的问题，然而我Mac的Chrome却一切正常，甚至win10和安卓的其他浏览器也是ok的，只有Chrome和PS的那几个App使用WebView让人崩溃。。。&lt;/p&gt;
    
    </summary>
    
      <category term="PlayStation" scheme="https://www.elietio.xyz/categories/PlayStation/"/>
    
    
      <category term="PlayStation Store 网页版" scheme="https://www.elietio.xyz/tags/PlayStation-Store-%E7%BD%91%E9%A1%B5%E7%89%88/"/>
    
      <category term="PS App" scheme="https://www.elietio.xyz/tags/PS-App/"/>
    
      <category term="PS App无法登录" scheme="https://www.elietio.xyz/tags/PS-App%E6%97%A0%E6%B3%95%E7%99%BB%E5%BD%95/"/>
    
      <category term="PSN无法连接到服务器18.xxx错误" scheme="https://www.elietio.xyz/tags/PSN%E6%97%A0%E6%B3%95%E8%BF%9E%E6%8E%A5%E5%88%B0%E6%9C%8D%E5%8A%A1%E5%99%A818-xxx%E9%94%99%E8%AF%AF/"/>
    
  </entry>
  
  <entry>
    <title>一次druid连接池获取连接错误的排查分析</title>
    <link href="https://www.elietio.xyz/posts/ceaacf5f.html"/>
    <id>https://www.elietio.xyz/posts/ceaacf5f.html</id>
    <published>2020-03-19T09:02:26.000Z</published>
    <updated>2020-06-14T02:35:21.561Z</updated>
    
    <content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/assets/css/APlayer.min.css"><script src="/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><script class="meting-secondary-script-marker" src="/assets/js/Meting.min.js"></script><h3 id="问题描述"><a href="#问题描述" class="headerlink" title="问题描述"></a>问题描述</h3><p>之前的上一篇文章 <a href="https://www.elietio.xyz/posts/fd295ff1.html">keepAlive解决druid空闲连接socket timeout 15分钟</a>解决了防火墙导致的空闲连接socket timeout的问题，而这一次在另外一个没有防火墙策略的内部环境却又出现了另外一个情况，进程偶发数据库操作报错，并且个别进程一段时间后始终无法获取数据库连接一直处于宕机状态。<br><a id="more"></a></p><h3 id="问题分析"><a href="#问题分析" class="headerlink" title="问题分析"></a>问题分析</h3><p>通过对错误日志分析，发现前期数据库报错的日志四个进程出现的时间点基本一致，因此怀疑是外部因素网络或者数据库导致。部分进程维持着偶发报错的情况下工作，而有的进程则在一段时间后彻底无法工作，一直出现无法创建数据库连接。于是首先对无法工作的进程分析排查。<br>首先，查看进程与数据库连接状态，发现存在<code>ESTABLISHED</code>状态连接</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">netstat -anp|grep 1521</span><br><span class="line">(Not all processes could be identified, non-owned process info</span><br><span class="line"> will not be shown, you would have to be root to see it all.)            </span><br><span class="line">tcp6       0      0 172.16.72.25:18704      172.16.71.13:1521       ESTABLISHED 9229/java</span><br></pre></td></tr></table></figure><p> 而错误日志显示<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.exceptions.PersistenceException: </span><br><span class="line"> Error querying database.  Cause: org.springframework.jdbc.CannotGetJdbcConnectionException: Could not get JDBC Connection; nested exception is com.alibaba.druid.pool.GetConnectionTimeoutException: wait millis <span class="number">60000</span>, active <span class="number">0</span>, maxActive <span class="number">20</span>, creating <span class="number">1</span> </span><br><span class="line"> .......</span><br><span class="line"> Caused by: java.sql.SQLRecoverableException: IO Error: Connection reset</span><br><span class="line">        at oracle.jdbc.driver.T4CConnection.logon(T4CConnection.java:<span class="number">498</span>)</span><br><span class="line">        at oracle.jdbc.driver.PhysicalConnection.&lt;init&gt;(PhysicalConnection.java:<span class="number">553</span>)</span><br></pre></td></tr></table></figure></p><p>可以看到当前数据库连接池中没有可用连接，druid在创建新连接的时候出现异常。<br>查看druid创建连接相关源码</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (maxWait &gt; <span class="number">0</span>) &#123;</span><br><span class="line">    holder = pollLast(nanos); </span><br><span class="line">&#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    holder = takeLast();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">private</span> DruidConnectionHolder <span class="title">pollLast</span><span class="params">(<span class="keyword">long</span> nanos)</span> <span class="keyword">throws</span> InterruptedException, SQLException </span>&#123;</span><br><span class="line">    <span class="keyword">long</span> estimate = nanos;</span><br><span class="line">    <span class="keyword">for</span> (;;) &#123;</span><br><span class="line">        <span class="keyword">if</span> (poolingCount == <span class="number">0</span>) &#123;</span><br><span class="line">            emptySignal(); <span class="comment">// send signal to CreateThread create connection</span></span><br><span class="line"></span><br><span class="line">            <span class="keyword">if</span> (failFast &amp;&amp; failContinuous.get()) &#123;</span><br><span class="line">                <span class="keyword">throw</span> <span class="keyword">new</span> DataSourceNotAvailableException(createError);</span><br><span class="line">            &#125;</span><br><span class="line">            ......</span><br><span class="line">            <span class="keyword">try</span> &#123;</span><br><span class="line">                <span class="keyword">long</span> startEstimate = estimate;</span><br><span class="line">                estimate = notEmpty.awaitNanos(estimate); <span class="comment">// signal by</span></span><br><span class="line">                                                          <span class="comment">// recycle or</span></span><br><span class="line">                                                          <span class="comment">// creator</span></span><br><span class="line">              ......  </span><br><span class="line">               <span class="keyword">return</span> last;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure><p>可以知道druid的连接获取是通过<code>notEmpty</code>和<code>empty</code>两个变量协调线程的同步，执行<code>pollLast</code>方法发现没可用连接时，就会<code>notEmpty.awaitNanos()</code>,同时<code>empty.signal()</code>去唤醒<code>CreateConnectionThread</code>这个线程去创建连接。</p><p>因此，查看当前进程的堆栈信息，找到<code>CreateConnectionThread</code>线程。<br>发现如下</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">"Druid-ConnectionPool-Create-523528914" #83 daemon prio=5 os_prio=0 tid=0x00007f6d1d8d4800 nid=0x26fb runnable [0x00007f6db3ffd000]</span><br><span class="line">   java.lang.Thread.State: RUNNABLE</span><br><span class="line">        at java.net.SocketInputStream.socketRead0(Native Method)</span><br><span class="line">        at java.net.SocketInputStream.socketRead(SocketInputStream.java:<span class="number">116</span>)</span><br><span class="line">        at java.net.SocketInputStream.read(SocketInputStream.java:<span class="number">170</span>)</span><br><span class="line">        at java.net.SocketInputStream.read(SocketInputStream.java:<span class="number">141</span>)</span><br><span class="line">   ......</span><br><span class="line">        at oracle.jdbc.driver.T4CSocketInputStreamWrapper.readNextPacket(T4CSocketInputStreamWrapper.java:<span class="number">124</span>)</span><br><span class="line">        at oracle.jdbc.driver.T4CSocketInputStreamWrapper.read(T4CSocketInputStreamWrapper.java:<span class="number">80</span>)</span><br><span class="line">        at oracle.jdbc.driver.T4CMAREngine.unmarshalUB1(T4CMAREngine.java:<span class="number">1137</span>)</span><br><span class="line">    ....</span><br><span class="line">        at com.alibaba.druid.filter.FilterChainImpl.connection_connect(FilterChainImpl.java:<span class="number">150</span>)</span><br><span class="line">        at com.alibaba.druid.pool.DruidAbstractDataSource.createPhysicalConnection(DruidAbstractDataSource.java:<span class="number">1560</span>)</span><br><span class="line">        at com.alibaba.druid.pool.DruidAbstractDataSource.createPhysicalConnection(DruidAbstractDataSource.java:<span class="number">1623</span>)</span><br><span class="line">        at com.alibaba.druid.pool.DruidDataSource$CreateConnectionThread.run(DruidDataSource.java:<span class="number">2468</span>)</span><br></pre></td></tr></table></figure><p> 而当前工作线程</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">"http-nio-8212-exec-7" #72 daemon prio=5 os_prio=0 tid=0x00007f6d46caa800 nid=0x26ea waiting on condition [0x00007f6db90ea000]</span><br><span class="line">   java.lang.Thread.State: TIMED_WAITING (parking)</span><br><span class="line">        at sun.misc.Unsafe.park(Native Method)</span><br><span class="line">        - parking to wait <span class="keyword">for</span>  &lt;<span class="number">0x00000006c23e65c8</span>&gt; (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)</span><br><span class="line">        at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:<span class="number">215</span>)</span><br><span class="line">        at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos(AbstractQueuedSynchronizer.java:<span class="number">2078</span>)</span><br><span class="line">        at com.alibaba.druid.pool.DruidDataSource.pollLast(DruidDataSource.java:<span class="number">1946</span>)</span><br><span class="line">        at com.alibaba.druid.pool.DruidDataSource.getConnectionInternal(DruidDataSource.java:<span class="number">1458</span>)</span><br><span class="line">        at com.alibaba.druid.pool.DruidDataSource.getConnectionDirect(DruidDataSource.java:<span class="number">1255</span>)</span><br></pre></td></tr></table></figure><p>可以看到，工作线程执行<code>pollLast</code>方法去唤醒<code>Druid-ConnectionPool-Create</code>线程，<code>Druid-ConnectionPool-Create</code>线程开始尝试连接数据库，但是线程一直出现了   socketRead0阻塞，导致无法创建连接，因此工作线程会一直waiting直到获取连接超时报错。而 <code>Druid-ConnectionPool-Create</code>线程通过jdbc连接数据库是使用Socket通信的，Socket没有办法探测到网络错误，因此应用也无法主动发现连接错误，它的超时是由Socket Timeout控制的，如果没有设置Socket Timeout在没有返回的情况下会一直等待下去,所以当数据库或者网络突然出现故障，就可能会发生socket阻塞，而如果没有socket timeout设置，那么阻塞将一直持续下去。这样始终无法创建连接，所以进程不可用。（一般来说Linux服务器会有系统级别的socket timeout，由于没有权限查看不了，从上面信息推测设置的可能较长）</p><p>那么为什么有的进程只是偶发故障，而后恢复呢？通过日志查看，发现这种情况的情况在报错的时候，连接池当前还存在着一个连接，即使连接池创建新的连接报错，但是并不会影响该连接，所以网络恢复后，该连接仍然可用，只是无法再创建新的连接而已，所以进程体现在偶发报错，但是仍然可用。而当这个连接空闲很长时间或者因为其他原因死亡后，那么连接池将再无连接可用，也无法创建新的连接，进程变成了不可用状态。<br>以上就是基于日志分析、进程堆栈和连接信息排查做出的推测。</p><h3 id="问题解决"><a href="#问题解决" class="headerlink" title="问题解决"></a>问题解决</h3><p>基于上面推测，问题主要出现在socket阻塞，因此解决方案就是尽量避免socket阻塞过长时间，导致长时间不可用。</p><h4 id="临时应急操作"><a href="#临时应急操作" class="headerlink" title="临时应急操作"></a>临时应急操作</h4><p>如果发生阻塞，只需要重启进程即可临时解决问题，如果不想要进程重启，那么我们可以通过杀掉阻塞的socket来重连数据库。</p><p>首先通过<code>lsof</code> 命令找到进程的所有文件描述符，并且找到阻塞的socket的连接，然后<code>gdb</code>连接进程，<code>call close</code>掉这个socket连接，这样<code>Druid-ConnectionPool-Create</code>线程可以再重新创建连接。</p><h4 id="设置jdbc级别的timeout防止长时间阻塞"><a href="#设置jdbc级别的timeout防止长时间阻塞" class="headerlink" title="设置jdbc级别的timeout防止长时间阻塞"></a>设置jdbc级别的timeout防止长时间阻塞</h4><p>长久的解决思路肯定是设置一个合适的socket timeout来避免socket阻塞，一般会有系统环境相关设置，为了预防系统socket timeout过长或者没有设置，还是有必要设置一个jdbc级别的timeout。</p><p>上一篇文章中是通过开启<code>keepAlive</code>来解决问题的，其中我也提到不建议通过修改 <code>socket timeout</code> 去解决。而这次的这个问题就必须要通过设置一个合理的 <code>socket timeout</code>  参数来保证当出现异常网路情况下服务不会宕机太久，但是这个时间又不能过小，否则会导致一些耗时较长的数据库操作被超时终止，配置参考如下。</p><p>oracle：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">connectionProperties:</span> <span class="string">oracle.net.CONNECT_TIMEOUT=60000;oracle.jdbc.ReadTimeout=300000</span></span><br></pre></td></tr></table></figure><p>mysql：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">url:</span> <span class="attr">jdbc:mysql://ip:port/db?connectTimeout=60000&amp;socketTimeout=300000</span></span><br></pre></td></tr></table></figure>]]></content>
    
    <summary type="html">
    
      &lt;h3 id=&quot;问题描述&quot;&gt;&lt;a href=&quot;#问题描述&quot; class=&quot;headerlink&quot; title=&quot;问题描述&quot;&gt;&lt;/a&gt;问题描述&lt;/h3&gt;&lt;p&gt;之前的上一篇文章 &lt;a href=&quot;https://www.elietio.xyz/posts/fd295ff1.html&quot;&gt;keepAlive解决druid空闲连接socket timeout 15分钟&lt;/a&gt;解决了防火墙导致的空闲连接socket timeout的问题，而这一次在另外一个没有防火墙策略的内部环境却又出现了另外一个情况，进程偶发数据库操作报错，并且个别进程一段时间后始终无法获取数据库连接一直处于宕机状态。&lt;br&gt;
    
    </summary>
    
      <category term="Java" scheme="https://www.elietio.xyz/categories/Java/"/>
    
    
      <category term="druid" scheme="https://www.elietio.xyz/tags/druid/"/>
    
      <category term="socket timeout" scheme="https://www.elietio.xyz/tags/socket-timeout/"/>
    
  </entry>
  
  <entry>
    <title>Logback totalSizeCap不生效的两个bug</title>
    <link href="https://www.elietio.xyz/posts/f6b1711c.html"/>
    <id>https://www.elietio.xyz/posts/f6b1711c.html</id>
    <published>2020-02-26T11:11:44.000Z</published>
    <updated>2020-06-13T08:18:25.218Z</updated>
    
    <content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/assets/css/APlayer.min.css"><script src="/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><script class="meting-secondary-script-marker" src="/assets/js/Meting.min.js"></script><h3 id="问题描述："><a href="#问题描述：" class="headerlink" title="问题描述："></a>问题描述：</h3><p>线上的一个数据接收服务最近数据量比较大，日志也打的很频繁，但是却出现了totalSizeCap配置不生效，无法删除日志文件的问题，每天日志一度回滚累计到几千个 。<br><a id="more"></a></p><p>日志设置maxHistory 30,maxFileSize 20MB,totalSizeCap 5GB</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">rollingPolicy</span> <span class="attr">class</span>=<span class="string">"ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">fileNamePattern</span>&gt;</span></span><br><span class="line">        $&#123;logger.path&#125;/all.%d&#123;yy-MM-dd&#125;.%i.log</span><br><span class="line">    <span class="tag">&lt;/<span class="name">fileNamePattern</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">maxHistory</span>&gt;</span>30<span class="tag">&lt;/<span class="name">maxHistory</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">maxFileSize</span>&gt;</span>20MB<span class="tag">&lt;/<span class="name">maxFileSize</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">totalSizeCap</span>&gt;</span>5GB<span class="tag">&lt;/<span class="name">totalSizeCap</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">rollingPolicy</span>&gt;</span></span><br></pre></td></tr></table></figure><h3 id="问题排查："><a href="#问题排查：" class="headerlink" title="问题排查："></a>问题排查：</h3><p>面向Google查询相关问题，看到一个logback无法处理totalSizeCap 超过2GB的bug</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">capTotalSize</span><span class="params">(Date now)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">int</span> totalSize = <span class="number">0</span>;</span><br><span class="line">    <span class="keyword">int</span> totalRemoved = <span class="number">0</span>;</span><br><span class="line">    <span class="keyword">for</span> (<span class="keyword">int</span> offset = <span class="number">0</span>; offset &lt; maxHistory; offset++) &#123;</span><br><span class="line">        Date date = rc.getEndOfNextNthPeriod(now, -offset);</span><br><span class="line">        File[] matchingFileArray = getFilesInPeriod(date);</span><br><span class="line">        descendingSortByLastModified(matchingFileArray);</span><br><span class="line">        <span class="keyword">for</span> (File f : matchingFileArray) &#123;</span><br><span class="line">            <span class="keyword">long</span> size = f.length();</span><br><span class="line">            <span class="keyword">if</span> (totalSize + size &gt; totalSizeCap) &#123;</span><br><span class="line">                addInfo(<span class="string">"Deleting ["</span> + f + <span class="string">"]"</span> + <span class="string">" of size "</span> + <span class="keyword">new</span> FileSize(size));</span><br><span class="line">                totalRemoved += size;</span><br><span class="line">                f.delete();</span><br><span class="line">            &#125;</span><br><span class="line">            totalSize += size;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    addInfo(<span class="string">"Removed  "</span> + <span class="keyword">new</span> FileSize(totalRemoved) + <span class="string">" of files"</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这段代码定义了totalSize来累加当前日志文件大小，但是数据类型是int，我们知道int类型最大值是 2147483647，而如果totalSize超过了2GB <code>totalSize + size &gt; totalSizeCap</code>会不成立。。。。</p><p>所以一旦totalSizeCap超过2GB，那么就会导致日志清除失效。</p><p>这个问题有人反馈过 <a href="https://jira.qos.ch/browse/LOGBACK-1231" target="_blank" rel="noopener">issue</a>，而logback也在1.2.0版本修复了这个问题，<a href="https://github.com/qos-ch/logback/commit/2240d4253c9f3fa28497e74f34be3bb1edc73078#diff-cb428af44f615094fb61e87a9ca3411a" target="_blank" rel="noopener">解决方案是修改int为long类型</a>。</p><p><img src="/img/logback-totalSizeCap/commit_2240d42.png" alt="commit_2240d42"></p><p>但是这个并不适用于线上遇到的情况，因为服务中使用的是1.2.3版本，于是继续查询分析。又发现了一个<a href="https://jira.qos.ch/browse/LOGBACK-1500" target="_blank" rel="noopener">类似报告</a>，而且报告的版本也是1.2.3</p><blockquote><p> I’m using SizeAndTimeBasedRollingPolicy.<br> When ‘%i’ file index reaches 999 it stops deleting the old files and totalSizeCap is not respected any more.<br> This soon leads to disk full issues (as logging in my case was fast enough) </p></blockquote><p>这个问题有点相似了，看一下生产环境的日志文件，前几天都是只保留了后缀1000以上的，当天的保留了700以上的，也就是删除了一部分，后面未删除，而且感觉和这个999又很大关系，这个issue里面没有相关回复，那我们自己一边查看源码一般继续Google吧，上面的源码我们已经看到是日志大小回滚的实现，<code>File[] matchingFileArray = getFilesInPeriod(date)</code>，这一步应该是获取相关的文件数组，我们进去继续查看</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">protected</span> File[] getFilesInPeriod(Date dateOfPeriodToClean) &#123;</span><br><span class="line">        File archive0 = <span class="keyword">new</span> File(fileNamePattern.convertMultipleArguments(dateOfPeriodToClean, <span class="number">0</span>));</span><br><span class="line">        File parentDir = getParentDir(archive0);</span><br><span class="line">        String stemRegex = createStemRegex(dateOfPeriodToClean);</span><br><span class="line">        File[] matchingFileArray = FileFilterUtil.filesInFolderMatchingStemRegex(parentDir, stemRegex);</span><br><span class="line">        <span class="keyword">return</span> matchingFileArray;</span><br><span class="line">    &#125;</span><br><span class="line"> <span class="function"><span class="keyword">private</span> String <span class="title">createStemRegex</span><span class="params">(<span class="keyword">final</span> Date dateOfPeriodToClean)</span> </span>&#123;</span><br><span class="line">        String regex = fileNamePattern.toRegexForFixedDate(dateOfPeriodToClean);</span><br><span class="line">        <span class="keyword">return</span> FileFilterUtil.afterLastSlash(regex);</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure><p>上面这一块的逻辑是生成一个正则去和日志目录下的日志匹配获取日志文件，下面的代码就是具体拼接正则的实现<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment">     * Given date, convert this instance to a regular expression.</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * Used to compute sub-regex when the pattern has both %d and %i, and the</span></span><br><span class="line"><span class="comment">     * date is known.</span></span><br><span class="line"><span class="comment">     * </span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> date - known date</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> String <span class="title">toRegexForFixedDate</span><span class="params">(Date date)</span> </span>&#123;</span><br><span class="line">        StringBuilder buf = <span class="keyword">new</span> StringBuilder();</span><br><span class="line">        Converter&lt;Object&gt; p = headTokenConverter;</span><br><span class="line">        <span class="keyword">while</span> (p != <span class="keyword">null</span>) &#123;</span><br><span class="line">            <span class="keyword">if</span> (p <span class="keyword">instanceof</span> LiteralConverter) &#123;</span><br><span class="line">                buf.append(p.convert(<span class="keyword">null</span>));</span><br><span class="line">            &#125; <span class="keyword">else</span> <span class="keyword">if</span> (p <span class="keyword">instanceof</span> IntegerTokenConverter) &#123;</span><br><span class="line">                buf.append(<span class="string">"(\\d&#123;1,3&#125;)"</span>);</span><br><span class="line">            &#125; <span class="keyword">else</span> <span class="keyword">if</span> (p <span class="keyword">instanceof</span> DateTokenConverter) &#123;</span><br><span class="line">                buf.append(p.convert(date));</span><br><span class="line">            &#125;</span><br><span class="line">            p = p.getNext();</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> buf.toString();</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure></p><p>看到这一块果然发现一个问题<code>buf.append(&quot;(\\d{1,3})&quot;)</code>，这个正则是匹配1位到3位的数字，这不刚好，只能识别日志文件后缀1-999，999以上就无法匹配了。</p><p>同样我们也找到了相关的<a href="https://jira.qos.ch/browse/LOGBACK-1297" target="_blank" rel="noopener">issue</a></p><blockquote><p>I found cause.</p><p>Check the <strong>toRegexForFixedDate()</strong> method in <strong>ch.qos.logback.core.rolling.helper.FileNamePattern.java</strong></p><p>Regular expression hardcoded like this:</p><p><strong><em>buf.append(“(\ \d{1,3})”);</em></strong></p><p>So, files indexed more than 3-digit number are not visible to delete…</p><p>I don’t know why the expression hardcoded.</p><p>Anyway, you’d better modify the source.</p></blockquote><p>有人提到了现在已经修复</p><blockquote><p>this issue fixed in 1.3.0-alpha1</p><p><a href="https://jira.qos.ch/browse/LOGBACK-1175" target="_blank" rel="noopener">https://jira.qos.ch/browse/LOGBACK-1175</a></p></blockquote><p>果然这里存在问题，<a href="https://github.com/qos-ch/logback/commit/f264607fb450" target="_blank" rel="noopener">1.3.0-alpha1版本修复这个</a></p><p><img src="/img/logback-totalSizeCap/commit_f264607.png" alt="commit_f264607"></p><p>但是为什么出现了前几天日志文件保留了后缀999以上，当天存在部分低于999的呢？其实这个也很好理解，还是上面源码，先正则匹配到文件之后循环累加大小，maxFileSize =20MB,totalSizeCap =5GB= 5120MB，等于256个文件，256不能被999整除，还会剩余231，也就是说第一天清理了768个文件后，剩余  231 * 20MB&lt;5120MB ,所以不会清理掉，第二天才会继续累加这部分，然后删除上一天剩余的后缀小于1000的日志文件，这样就导致每天可能剩余一定量的后缀小于1000的日志文件，直到第二天被清理，这和我们的实际情况是相符的。</p><p>顺便吐槽一下，从GitHub的文件history来看，2012年作者已经改过一次这个正则了，把<a href="https://github.com/qos-ch/logback/commit/608ed4c58717296b8182006e35aff52ca2ecb598#diff-80c6c7b6b5e9e46b2e5afd6ec7eb07fd" target="_blank" rel="noopener">d{1,2}改成 d{1,3}</a> ，真是醉了┑(￣Д ￣)┍</p><p><img src="/img/logback-totalSizeCap/commit_608ed4c.png" alt="commit_608ed4c"></p><h3 id="问题解决："><a href="#问题解决：" class="headerlink" title="问题解决："></a>问题解决：</h3><p>问题已经找出，那么解决就很简单，升级logback版本就行，当然我觉得首先这个日志打印问题就很大，一天打印上千个日志文件，大小几十G，这根本没有日志的意义了，纯属浪费资源。</p><blockquote class="question"><br>参考📚：<br><i class="fa fa-hand-o-right" aria-hidden="true"></i>   <a href="https://tidyko.com/posts/589711b0.html" target="_blank" rel="noopener">https://tidyko.com/posts/589711b0.html</a><br> <i class="fa fa-hand-o-right" aria-hidden="true"></i>   <a href="https://jira.qos.ch/browse/LOGBACK-1500" target="_blank" rel="noopener">https://jira.qos.ch/browse/LOGBACK-1500</a><br> <i class="fa fa-hand-o-right" aria-hidden="true"></i>   <a href="https://jira.qos.ch/browse/LOGBACK-1297" target="_blank" rel="noopener">https://jira.qos.ch/browse/LOGBACK-1297</a><br></blockquote>]]></content>
    
    <summary type="html">
    
      &lt;h3 id=&quot;问题描述：&quot;&gt;&lt;a href=&quot;#问题描述：&quot; class=&quot;headerlink&quot; title=&quot;问题描述：&quot;&gt;&lt;/a&gt;问题描述：&lt;/h3&gt;&lt;p&gt;线上的一个数据接收服务最近数据量比较大，日志也打的很频繁，但是却出现了totalSizeCap配置不生效，无法删除日志文件的问题，每天日志一度回滚累计到几千个 。&lt;br&gt;
    
    </summary>
    
      <category term="Java" scheme="https://www.elietio.xyz/categories/Java/"/>
    
    
      <category term="排错" scheme="https://www.elietio.xyz/tags/%E6%8E%92%E9%94%99/"/>
    
      <category term="Logback" scheme="https://www.elietio.xyz/tags/Logback/"/>
    
      <category term="totalSizeCap" scheme="https://www.elietio.xyz/tags/totalSizeCap/"/>
    
      <category term="999" scheme="https://www.elietio.xyz/tags/999/"/>
    
  </entry>
  
  <entry>
    <title>如何重定向一个正在执行的程序的标准输出</title>
    <link href="https://www.elietio.xyz/posts/b1ce4d2d.html"/>
    <id>https://www.elietio.xyz/posts/b1ce4d2d.html</id>
    <published>2020-02-22T02:17:54.000Z</published>
    <updated>2020-06-13T08:18:25.218Z</updated>
    
    <content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/assets/css/APlayer.min.css"><script src="/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><script class="meting-secondary-script-marker" src="/assets/js/Meting.min.js"></script><p>如何在Linux系统中更改一个正在执行的程序的标准输出重定向到其他文件？事情的场景是这样的，由于同事的疏忽，忘了关闭一个springboot微服务的控制台日志输出，而这个进程启动后又会把标准输出和标准错误输出写到一个process.log的日志文件中，由于控制台信息输出太多，导致长时间日志磁盘占用过大，这时候又来了一个骚操作，直接把这个日志文件删除掉了😓。<br><a id="more"></a><br>直接删除文件这个肯定是没用的，通过rm删除文件将会从文件系统的目录结构上解除链接(unlink)，然而如果文件是被打开的（有进程正在使用），那么进程将仍然可以读取该文件，磁盘空间也一直被占用，这样就会导致删除了文件，但是磁盘空间却未被释放，大量的文件句柄无法释放。<br>执行 <code>lsof|grep [pid]|grep deleted</code> 查看，确实出现了文件句柄泄露😲<br><img src="/img/redirect-stdout/lsof.png" alt="lsof"><br>然后查看对应进程的文件文件描述符（fd：file descriptor）<br><code>ls -l /proc/[pid]/fd</code><br><img src="/img/redirect-stdout/proc.png" alt="proc"><br>可以看到文件描述符1和2都指向了被删除的日志文件<br>那么如何解决这个问题呢？杀掉进程是最简单的方法，但如果不重启呢？从上面fd表上我们看到1和2指向删除文件，那么我们能不能更改这个指向，重定向到/dev/null丢掉输出呢❔<br>答案是有的，主要依赖于Linux 的 close()、open()、dup2()函数。（open函数也可用creat函数替换）<br>close函数用于关闭一个已打开的文件，函数原型如下：<br><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">close</span><span class="params">(<span class="keyword">int</span> filedes)</span></span>;</span><br><span class="line">返回值：若成功则返回<span class="number">0</span>，出错则返回<span class="number">-1</span></span><br><span class="line">参数：filedes是文件描述符。</span><br></pre></td></tr></table></figure></p><p>open函数可以打开或创建一个文件，函数原型如下：<br><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">open</span><span class="params">(<span class="keyword">const</span> <span class="keyword">char</span> *pathname, <span class="keyword">int</span> flags)</span></span>;</span><br><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">open</span><span class="params">(<span class="keyword">const</span> <span class="keyword">char</span> *pathname, <span class="keyword">int</span> flags, <span class="keyword">mode_t</span> mode)</span></span>;</span><br><span class="line">返回值：成功返回新分配的文件描述符，出错返回<span class="number">-1</span>并设置errno</span><br><span class="line">参数：pathname参数是要打开或创建的文件名，flags参数有一系列常数值，具体不在此处介绍</span><br></pre></td></tr></table></figure></p><p>dup2函数可以复制一个文件的描述符，函数原型如下：<br><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">dup2</span><span class="params">( <span class="keyword">int</span> oldfd, <span class="keyword">int</span> targetfd )</span></span></span><br><span class="line">返回值：目标的文件描述符</span><br><span class="line">参数：oldfd源描述符，targetfd目标描述符</span><br></pre></td></tr></table></figure></p><p>简单来说，close关闭一个文件描述符，open打开一个文件并返回文件描述符，dup2将目标文件描述符变成源文件描述符的一个复制，即两个文件描述符都指向了源文件描述符指向的文件上去。所以我们可以关闭掉标准输出fd1，然后open /dev/null获得一个新的文件描述符，再将fd1指向/dev/null，这样就完成了重定向一个标准输出。<br>OK，我们用GDB尝试一下，首先<code>gdb -p [pid]</code>进入gdb调试💻<br><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">(gdb) p close(1)</span><br><span class="line"><span class="variable">$1</span> = 0</span><br><span class="line">(gdb) p dup2(open(<span class="string">"/dev/null"</span>, 2), 1)    //2表示O_RDWR 0x0002</span><br><span class="line"><span class="variable">$2</span> = 1</span><br><span class="line">(gdb) quit</span><br></pre></td></tr></table></figure></p><p>这样就完成标准输出重定向到/dev/null，标准错误输出依此类推<br>再次查看对应进程的文件描述符列表，1和2都成功指向了/dev/null，并且文件句柄也被释放掉✌<br><img src="/img/redirect-stdout/proc1.png" alt="proc1"><br>不仅仅适用于这个场景，这个方法也可以把一个忘记了nohup启动的进程放置到后台运行等等其他方面。</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;如何在Linux系统中更改一个正在执行的程序的标准输出重定向到其他文件？事情的场景是这样的，由于同事的疏忽，忘了关闭一个springboot微服务的控制台日志输出，而这个进程启动后又会把标准输出和标准错误输出写到一个process.log的日志文件中，由于控制台信息输出太多，导致长时间日志磁盘占用过大，这时候又来了一个骚操作，直接把这个日志文件删除掉了😓。&lt;br&gt;
    
    </summary>
    
      <category term="Linux" scheme="https://www.elietio.xyz/categories/Linux/"/>
    
    
      <category term="linux" scheme="https://www.elietio.xyz/tags/linux/"/>
    
      <category term="gdb" scheme="https://www.elietio.xyz/tags/gdb/"/>
    
      <category term="重定向标准输出，文件描述符" scheme="https://www.elietio.xyz/tags/%E9%87%8D%E5%AE%9A%E5%90%91%E6%A0%87%E5%87%86%E8%BE%93%E5%87%BA%EF%BC%8C%E6%96%87%E4%BB%B6%E6%8F%8F%E8%BF%B0%E7%AC%A6/"/>
    
  </entry>
  
  <entry>
    <title>一次-Djava.ext.dir配置不当的问题</title>
    <link href="https://www.elietio.xyz/posts/e1ec9d49.html"/>
    <id>https://www.elietio.xyz/posts/e1ec9d49.html</id>
    <published>2020-01-07T12:26:54.000Z</published>
    <updated>2020-06-13T08:18:25.217Z</updated>
    
    <content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/assets/css/APlayer.min.css"><script src="/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><script class="meting-secondary-script-marker" src="/assets/js/Meting.min.js"></script><h3 id="问题描述："><a href="#问题描述：" class="headerlink" title="问题描述："></a>问题描述：</h3><p>服务新增一个加解密模块后,本地调试OK部署到测试环境启动开始功能测试，却发现相关功能异常，查看服务日志发现以下异常<br><a id="more"></a></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">java.security.NoSuchAlgorithmException: Algorithm HmacSHA256 not available</span><br><span class="line">        at javax.crypto.Mac.getInstance(Mac.java:<span class="number">181</span>)</span><br><span class="line">        ......</span><br></pre></td></tr></table></figure><h3 id="问题排查："><a href="#问题排查：" class="headerlink" title="问题排查："></a>问题排查：</h3><p>服务是springboot的项目，直接打包成jar包，启动脚本中使用<code>java -jar</code>的形式启动。由于用到了一些公司内部封装的依赖jar包，而这些jar必须外部加载不能打进jar里面启动，所以使用了<code>-Djava.ext.dirs</code>去加载外部依赖jar包，这时一个陷阱就出现了，<code>-Djava.ext.dirs</code>会覆盖掉java本身的ext设置，<code>java.ext.dirs</code>指定的目录由ExtClassLoader加载器加载，如果没有指定该系统属性，那么该加载器默认加载<code>$JAVA_HOME/jre/lib/ext</code>目录下的所有jar文件</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">-rwxr-xr-x 1    3860502 Mar 15  2017 cldrdata.jar</span><br><span class="line">-rwxr-xr-x 1       8286 Mar 15  2017 dnsns.jar</span><br><span class="line">-rwxr-xr-x 1      44516 Mar 15  2017 jaccess.jar</span><br><span class="line">-rwxr-xr-x 1   18490072 Mar 15  2017 jfxrt.jar</span><br><span class="line">-rwxr-xr-x 1    1179093 Mar 15  2017 localedata.jar</span><br><span class="line">-rwxr-xr-x 1       1269 Mar 15  2017 meta-index</span><br><span class="line">-rwxr-xr-x 1    2022531 Mar 15  2017 nashorn.jar</span><br><span class="line">-rwxr-xr-x 1      42154 Mar 15  2017 sunec.jar</span><br><span class="line">-rwxr-xr-x 1     280161 Mar 15  2017 sunjce_provider.jar</span><br><span class="line">-rwxr-xr-x 1     251327 Mar 15  2017 sunpkcs11.jar</span><br><span class="line">-rwxr-xr-x 1      68924 Mar 15  2017 zipfs.jar</span><br></pre></td></tr></table></figure><p>所以，只单单指定了额外依赖的jar包后，就会导致ext目录下的jar包无法加载，而这次我们新增的加解密模块使用了<code>HmacSHA256</code>算法，依赖于<code>sunjce_provider.jar</code>包的内容，当我们在本地环境调试时，直接IDEA启动，没有出现依赖加载错误的问题，而当在测试环境使用启动脚本启动并且指定了<code>-Djava.ext.dirs</code>就导致了依赖出错。</p><h3 id="问题解决："><a href="#问题解决：" class="headerlink" title="问题解决："></a>问题解决：</h3><p>问题已经找出，那么解决就很简单，<code>-Djava.ext.dirs</code>引入多个路径加入Java自带ext路径即可</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">java  -Djava.ext.dirs=../lib:$JAVA_HOME/jre/lib/ext -jar</span><br></pre></td></tr></table></figure>]]></content>
    
    <summary type="html">
    
      &lt;h3 id=&quot;问题描述：&quot;&gt;&lt;a href=&quot;#问题描述：&quot; class=&quot;headerlink&quot; title=&quot;问题描述：&quot;&gt;&lt;/a&gt;问题描述：&lt;/h3&gt;&lt;p&gt;服务新增一个加解密模块后,本地调试OK部署到测试环境启动开始功能测试，却发现相关功能异常，查看服务日志发现以下异常&lt;br&gt;
    
    </summary>
    
      <category term="Java" scheme="https://www.elietio.xyz/categories/Java/"/>
    
    
      <category term="异常" scheme="https://www.elietio.xyz/tags/%E5%BC%82%E5%B8%B8/"/>
    
      <category term="排错" scheme="https://www.elietio.xyz/tags/%E6%8E%92%E9%94%99/"/>
    
      <category term="java.ext.dirs" scheme="https://www.elietio.xyz/tags/java-ext-dirs/"/>
    
  </entry>
  
  <entry>
    <title>Mybatis一对多、多对多查询时数据覆盖问题</title>
    <link href="https://www.elietio.xyz/posts/3f3f23ec.html"/>
    <id>https://www.elietio.xyz/posts/3f3f23ec.html</id>
    <published>2019-12-21T13:25:23.000Z</published>
    <updated>2020-06-14T02:27:33.017Z</updated>
    
    <content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/assets/css/APlayer.min.css"><script src="/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><script class="meting-secondary-script-marker" src="/assets/js/Meting.min.js"></script><h3 id="问题描述："><a href="#问题描述：" class="headerlink" title="问题描述："></a>问题描述：</h3><p>在使用mybatis查询一对多结果返回对象的场景中，当主表关联的多条数据完全一致时，返回的对象只有第一条数据<br><a id="more"></a></p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">select</span></span><br><span class="line">  A.ID,</span><br><span class="line">  A.USER_ID,</span><br><span class="line">  B.RESULT_ID,</span><br><span class="line">  B.RATE</span><br><span class="line"><span class="keyword">from</span> TABLEA A</span><br><span class="line">  <span class="keyword">left</span> <span class="keyword">join</span> TABLEAB B</span><br><span class="line">    <span class="keyword">on</span> A.ID = B.ID</span><br></pre></td></tr></table></figure><table><thead><tr><th>ID</th><th>USER_ID</th><th>RESULT_ID</th><th>RATE</th></tr></thead><tbody><tr><td>100000000000001892</td><td>abc</td><td>10000001</td><td>111</td></tr><tr><td>100000000000001892</td><td>abc</td><td>10000001</td><td>111</td></tr><tr><td>100000000000001892</td><td>abc</td><td>10000001</td><td>111</td></tr></tbody></table><p>映射的xml如下</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">resultMap</span> <span class="attr">id</span>=<span class="string">"xxx"</span> <span class="attr">type</span>=<span class="string">"xxx"</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">result</span> <span class="attr">column</span>=<span class="string">"ID"</span> <span class="attr">property</span>=<span class="string">"id"</span>/&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">result</span> <span class="attr">column</span>=<span class="string">"USER_ID"</span> <span class="attr">property</span>=<span class="string">"userID"</span>/&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">collection</span> <span class="attr">property</span>=<span class="string">"resultList"</span> <span class="attr">ofType</span>=<span class="string">"xxx"</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">result</span> <span class="attr">column</span>=<span class="string">"RESULT_ID"</span> <span class="attr">property</span>=<span class="string">"resultID"</span> /&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">result</span> <span class="attr">column</span>=<span class="string">"RATE"</span> <span class="attr">property</span>=<span class="string">"rate"</span>/&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">collection</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">resultMap</span>&gt;</span></span><br></pre></td></tr></table></figure><p>按照设想，返回的 ID=100000000000001892的对象里面的resultList应该包含三条完全一样的数据，但是实际的结果却是只有一条</p><h3 id="问题排查："><a href="#问题排查：" class="headerlink" title="问题排查："></a>问题排查：</h3><p>resultMap中如果不定义类似主键之类的能够区分每一条结果集的字段的话，当数据完全一致的时候会引起后面一条数据覆盖前面一条数据的现象</p><h3 id="问题解决："><a href="#问题解决：" class="headerlink" title="问题解决："></a>问题解决：</h3><p>查询时将一对多的多表的自增主键ID也查询出来，这样resultMap映射的数据就不会完全一致，避免了这个问题，所以在使用mybatis查询的时候最好将表的主键ID查询出来，如果不需要返回ID字段，可以在代码层面实体类转换JSON返回屏蔽掉。</p>]]></content>
    
    <summary type="html">
    
      &lt;h3 id=&quot;问题描述：&quot;&gt;&lt;a href=&quot;#问题描述：&quot; class=&quot;headerlink&quot; title=&quot;问题描述：&quot;&gt;&lt;/a&gt;问题描述：&lt;/h3&gt;&lt;p&gt;在使用mybatis查询一对多结果返回对象的场景中，当主表关联的多条数据完全一致时，返回的对象只有第一条数据&lt;br&gt;
    
    </summary>
    
      <category term="Java" scheme="https://www.elietio.xyz/categories/Java/"/>
    
    
      <category term="排错" scheme="https://www.elietio.xyz/tags/%E6%8E%92%E9%94%99/"/>
    
      <category term="mybatis" scheme="https://www.elietio.xyz/tags/mybatis/"/>
    
  </entry>
  
  <entry>
    <title>keepAlive解决druid空闲连接socket timeout 15分钟</title>
    <link href="https://www.elietio.xyz/posts/fd295ff1.html"/>
    <id>https://www.elietio.xyz/posts/fd295ff1.html</id>
    <published>2019-12-06T12:26:54.000Z</published>
    <updated>2020-06-13T08:18:25.217Z</updated>
    
    <content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/assets/css/APlayer.min.css"><script src="/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><script class="meting-secondary-script-marker" src="/assets/js/Meting.min.js"></script><h3 id="问题描述："><a href="#问题描述：" class="headerlink" title="问题描述："></a>问题描述：</h3><p>测试环境发现了一个很奇怪的现象，一台服务器出现了请求卡顿15分钟然后才执行SQL返回结果的现象<br><a id="more"></a></p><h3 id="问题排查："><a href="#问题排查：" class="headerlink" title="问题排查："></a>问题排查：</h3><p>最开始我们认为是网络问题，因为这台服务器网络环境比较特殊，正常我们环境的应用服务器和数据库服务器是同一网段的，而这台服务器和数据库服务器并不在同一网段，存在防火墙策略，但是后来发现能够稳定复现，并且每次都是发生在服务空闲一段时间后第一次请求，由于我们使用的是druid，于是查询了相关问题，果然网络上已经有了很多相关的描述</p><p><a href="https://my.oschina.net/u/3434392/blog/3017866" target="_blank" rel="noopener">druid下莫名其妙hold15分钟+。疑是socket timeout超时15分钟后，重建了新连接导致</a></p><p><a href="https://github.com/alibaba/druid/issues/2905" target="_blank" rel="noopener">从连接池中获取到失效连接，在检验连接有效性时出现长时间等待，大概15分钟 #2905</a></p><p>大致原因是这样，当应用服务器和数据库服务器直接存在防火墙策略时，如果服务空闲时间过长，会被防火墙主动断开数据库连接，但是此时druid并未感知，此时当有请求过来时，如果druid配置了<code>testWhileIdle</code>（申请连接的时候检测，如果空闲时间大于<code>timeBetweenEvictionRunsMillis</code>，执行<code>validationQuery</code>检测连接是否有效。），那么将进行一次检测，检测的方式也是根据配置的策略，一般是<code>select 1 from dual</code> ，由于这个连接已经被防火墙断掉，根本到达不了数据库，而druid这边则在一直等待，而这个等待的超时受到<code>socket timeout</code>限制，而我们服务器本身的系统<code>socket timeout</code>设置就是15分钟，所以druid会一直hold15分钟，直到触发超时重新建立连接。所以问题就出现了druid无法主动恢复防火墙主动断开的连接，只有当触发超时才能进行重建连接。</p><h3 id="问题解决："><a href="#问题解决：" class="headerlink" title="问题解决："></a>问题解决：</h3><p>问题解决的思路有两种，第一钟思路，更改jdbc的<code>socke timeout</code>，但是不建议，因为如果要能够快速重连，那么这个<code>socket timeout</code>就需要配置很短，但是这个时间很短会导致执行过长的SQL无法返回结果，<code>socket timeout</code>必须大于<code>statement timeout</code>，否则<code>socket timeout</code>先生效则<code>statement timeout</code>毫无意义，所以即使配置几分钟还是会出现请求hold的现象，因此不合适。</p><p>第二种思路，既然长时间空闲后连接会被防火墙断开，那么维持一个心跳，不让连接被防火墙断开即可，因此，需要引入druid的<code>keepAlive</code></p><p>引入<a href="https://github.com/alibaba/druid/wiki/KeepAlive_cn" target="_blank" rel="noopener">druid GitHub</a>上对此配置的解释</p><blockquote><p>在Druid-1.0.27之前的版本，DruidDataSource建议使用TestWhileIdle来保证连接的有效性，但仍有很多场景需要对连接进行保活处理。在1.0.28版本之后，新加入keepAlive配置，缺省关闭。使用keepAlive功能，建议使用1.1.16或者更高版本</p><h1 id="打开KeepAlive之后的效果"><a href="#打开KeepAlive之后的效果" class="headerlink" title="打开KeepAlive之后的效果"></a>打开KeepAlive之后的效果</h1><ol><li>初始化连接池时会填充到minIdle数量。</li><li>连接池中的minIdle数量以内的连接，空闲时间超过minEvictableIdleTimeMillis，则会执行keepAlive操作。</li><li>当网络断开等原因产生的由ExceptionSorter检测出来的死连接被清除后，自动补充连接到minIdle数量。</li></ol></blockquote><p>开启连接保活配置<code>keepAlive</code>,对于<code>minIdle</code>以内的连接，按照<code>timeBetweenEvictionRunsMillis</code>间隔进行保活检测，当空闲时间大于<code>minEvictableIdleTimeMillis</code>，发送心跳保持连接活跃，只要发送保活心跳的时间间隔小于防火墙断开空闲连接的时间即可。对于下面配置，只有1个空闲连接在空闲状态下会在5分钟左右进行心跳，保持长时间存活。</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">minIdle:</span> <span class="number">1</span></span><br><span class="line"><span class="attr">timeBetweenEvictionRunsMillis:</span> <span class="number">60000</span></span><br><span class="line"><span class="attr">minEvictableIdleTimeMillis:</span> <span class="number">300000</span></span><br><span class="line"><span class="attr">validationQuery:</span> <span class="string">SELECT</span> <span class="number">1</span> <span class="string">FROM</span> <span class="string">DUAL</span></span><br><span class="line"><span class="attr">validationQueryTimeout:</span> <span class="number">10</span></span><br><span class="line"><span class="attr">keepAlive:</span> <span class="literal">true</span></span><br><span class="line"><span class="attr">testWhileIdle:</span> <span class="literal">true</span></span><br><span class="line"><span class="string">....</span></span><br></pre></td></tr></table></figure>]]></content>
    
    <summary type="html">
    
      &lt;h3 id=&quot;问题描述：&quot;&gt;&lt;a href=&quot;#问题描述：&quot; class=&quot;headerlink&quot; title=&quot;问题描述：&quot;&gt;&lt;/a&gt;问题描述：&lt;/h3&gt;&lt;p&gt;测试环境发现了一个很奇怪的现象，一台服务器出现了请求卡顿15分钟然后才执行SQL返回结果的现象&lt;br&gt;
    
    </summary>
    
      <category term="Java" scheme="https://www.elietio.xyz/categories/Java/"/>
    
    
      <category term="socketRead0" scheme="https://www.elietio.xyz/tags/socketRead0/"/>
    
      <category term="keepAlive" scheme="https://www.elietio.xyz/tags/keepAlive/"/>
    
      <category term="druid" scheme="https://www.elietio.xyz/tags/druid/"/>
    
      <category term="socket timeout" scheme="https://www.elietio.xyz/tags/socket-timeout/"/>
    
  </entry>
  
  <entry>
    <title>谷歌SLOT A/B机制下安卓刷入TWRP和Magisk以及OTA的一些方法</title>
    <link href="https://www.elietio.xyz/posts/d1df635e.html"/>
    <id>https://www.elietio.xyz/posts/d1df635e.html</id>
    <published>2019-10-12T14:14:49.000Z</published>
    <updated>2020-06-13T08:18:25.217Z</updated>
    
    <content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/assets/css/APlayer.min.css"><script src="/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><script class="meting-secondary-script-marker" src="/assets/js/Meting.min.js"></script><p>A/B 系统分区是 Google 在 Android 7.0 时代引入的新机制，采用这个机制的设备拥有 A、B 两套系统分区，用户数据则能够在这两套系统分区之间共用。<br><a id="more"></a><br>这种分区机制带来的最大好处是无缝系统更新（seemless updates），当我们在 A 系统中进行 OTA 更新时，而实际更新的是另个一并未启用的 B 系统。手机重启后，系统分区从 A 切换到 B新系统，介于此机制，我们可以实现OTA升级后仍然保留Magisk的Root权限。由于使用了 A/B 分区，因此没有独立的 Recovery 分区；Recovery 现在是 Boot 的一部分。所以要通过 fastboot boot 实现临时从指定镜像启动，从而进入 TWRP，并通过刷入 twrp-installer 实现对 TWRP 的持久化。</p><h2 id="解锁Bootloader"><a href="#解锁Bootloader" class="headerlink" title="解锁Bootloader"></a>解锁Bootloader</h2><ol><li>在「设置 - 关于手机」中点击 5 次「版本号」启用开发者选项,前往「设置 - 系统 - 开发者选项」，分别启用「OEM 解锁」和「高级重启」 </li><li>长按电源键，从电源菜单中选择「引导器模式」，手机将会自动重启进入 Bootloader 模式。</li><li><p>连接电脑，在cmd窗口内输入adb命令：</p><figure class="highlight cmd"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">fastboot devices # 检查设备是否连接</span><br><span class="line">fastboot oem unlock</span><br></pre></td></tr></table></figure><p>回车，手机即出现解锁确认界面，音量键进行选择-「UNLOCK THE BOOTLOADER」，按电源键确认，手机重启开始解锁</p></li></ol><h2 id="刷入-TWRP"><a href="#刷入-TWRP" class="headerlink" title="刷入 TWRP"></a>刷入 TWRP</h2><p>需要如下：</p><ul><li>下载 <a href="https://twrp.me/Devices/" target="_blank" rel="noopener">TWRP</a></li><li>需要 twrp.img 和 twrp-installer.zip 两个文件</li></ul><p>执行以下操作：</p><ol><li>执行 <code>adb reboot bootloader</code> 进入 Bootloader 界面</li><li>执行 <code>fastboot boot twrp.img</code> 进入临时 TWRP</li><li>在TWRP中刷入twrp-installer.zip使TWRP持久化，安装包会在 AB 两个分区中都安装一次 TWRP，这样无论手机从哪个分区启动都可以进入 TWRP。</li></ol><h2 id="刷入Magisk获取Root"><a href="#刷入Magisk获取Root" class="headerlink" title="刷入Magisk获取Root"></a>刷入Magisk获取Root</h2><h3 id="安装-Magisk（使用固化TWRP）"><a href="#安装-Magisk（使用固化TWRP）" class="headerlink" title="安装 Magisk（使用固化TWRP）"></a>安装 Magisk（使用固化TWRP）</h3><p>需要如下：</p><ul><li>下载<a href="https://forum.xda-developers.com/apps/magisk/official-magisk-v7-universal-systemless-t3473445" target="_blank" rel="noopener">Magisk-vX.X.zip 刷机包及 Magisk Manager.apk</a></li></ul><p>执行以下操作：</p><ol><li>重启进去TERP</li><li>在 TWRP 中刷入你下载的 Magisk 安装包，成功后重新启动手机</li><li>安装 Magisk Manager 的 apk，确认 Magisk 已成功激活</li></ol><h3 id="安装-Magisk（使用临时-TWRP）"><a href="#安装-Magisk（使用临时-TWRP）" class="headerlink" title="安装 Magisk（使用临时 TWRP）"></a>安装 Magisk（使用临时 TWRP）</h3><p>需要如下：</p><ul><li>下载<a href="https://forum.xda-developers.com/apps/magisk/official-magisk-v7-universal-systemless-t3473445" target="_blank" rel="noopener">Magisk-vX.X.zip 刷机包及 Magisk Manager.apk</a></li><li>下载 <a href="https://twrp.me/Devices/" target="_blank" rel="noopener">TWRP</a></li></ul><p>执行以下操作：</p><ol><li>执行 <code>adb reboot bootloader</code> 进入 Bootloader 界面</li><li>执行 <code>fastboot boot TWRP.img</code> 进入临时 TWRP</li><li>在 TWRP 中刷入你下载的 Magisk 安装包<br>可选择Sideload 模式，TWRP 中输入正确密码解密分区，并选择 Advanced -&gt; ADB Sideload。<br>执行 <code>adb sideload Magisk-vX.X.zip</code></li><li><code>fastboot reboot</code>重新启动手机</li><li>安装 Magisk Manager 的 apk，确认 Magisk 已成功激活</li></ol><h3 id="安装-Magisk（免-TWRP）"><a href="#安装-Magisk（免-TWRP）" class="headerlink" title="安装 Magisk（免 TWRP）"></a>安装 Magisk（免 TWRP）</h3><p>需要如下：</p><ul><li>下载或提取ROM 所对应的 boot.img</li><li>下载<a href="https://forum.xda-developers.com/apps/magisk/official-magisk-v7-universal-systemless-t3473445" target="_blank" rel="noopener">Magisk Manager.apk</a></li></ul><p>执行以下操作：</p><ol><li>安装 Magisk Manager 的 apk。</li><li>用 Magisk Manager 给 boot.img 手动补丁（<code>install —— 修补 boot 镜像文件</code>），将获得的 <code>magisk_patched.img</code> 传回电脑。</li><li><p>再次进入 Bootloader，输入</p><figure class="highlight cmd"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">fastboot boot magisk_patched.img</span><br><span class="line">fastboot flash boot magisk_patched.img #如果上面命令不生效可以使用这个，Android <span class="number">10</span></span><br></pre></td></tr></table></figure><p>来加载生成后的 boot 分区文件获取<strong>临时 root</strong> </p><ul><li>可使用<code>fastboot flash recovery magisk_patched.img</code>修复recovery image</li></ul></li><li>重启开机后，Magisk Manager 暂时有了 Root 权限，此时可以在 Magisk Manager 中正式安装 Magisk（<code>安装（install）——install——Direct Install（直接安装）</code>）。</li></ol><h2 id="OTA时的操作"><a href="#OTA时的操作" class="headerlink" title="OTA时的操作"></a>OTA时的操作</h2><h3 id="保留-TWRP"><a href="#保留-TWRP" class="headerlink" title="保留 TWRP"></a>保留 TWRP</h3><p>在 Magisk Manager 中下载并安装插件 TWRP A/B Retenion Script</p><h3 id="保留-Root"><a href="#保留-Root" class="headerlink" title="保留 Root"></a>保留 Root</h3><ul><li>能检测到 OTA 更新后，点击进入 Magisk Manager 应用，找到位于主界面的「卸载 Magisk」选项，然后点击「还原原厂镜像」，不要重启直接OTA(或许不需要还原这一步？但是magisk<a href="https://topjohnwu.github.io/Magisk/tutorials.html#ota-installation" target="_blank" rel="noopener">官方文档</a>有提及)</li></ul><ol><li>使用 OTA 更新并安装（自动下载全量包），安装完成后不要重启</li><li>在 Magisk Manager 中「安装 Magisk」</li><li>选择「安装到未使用的槽位 (OTA 后)」(<code>Install to Inactive Slot (After OTA)</code>)</li><li>写入完成后重启即可</li></ol>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;A/B 系统分区是 Google 在 Android 7.0 时代引入的新机制，采用这个机制的设备拥有 A、B 两套系统分区，用户数据则能够在这两套系统分区之间共用。&lt;br&gt;
    
    </summary>
    
      <category term="技术" scheme="https://www.elietio.xyz/categories/%E6%8A%80%E6%9C%AF/"/>
    
    
      <category term="安卓" scheme="https://www.elietio.xyz/tags/%E5%AE%89%E5%8D%93/"/>
    
      <category term="ROOT" scheme="https://www.elietio.xyz/tags/ROOT/"/>
    
  </entry>
  
  <entry>
    <title>socketRead0阻塞导致线程卡死问题分析排查</title>
    <link href="https://www.elietio.xyz/posts/774f1877.html"/>
    <id>https://www.elietio.xyz/posts/774f1877.html</id>
    <published>2019-10-11T12:26:54.000Z</published>
    <updated>2020-06-13T08:18:25.217Z</updated>
    
    <content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/assets/css/APlayer.min.css"><script src="/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><script class="meting-secondary-script-marker" src="/assets/js/Meting.min.js"></script><h3 id="问题描述：Consumer进程无法处理消息"><a href="#问题描述：Consumer进程无法处理消息" class="headerlink" title="问题描述：Consumer进程无法处理消息"></a>问题描述：Consumer进程无法处理消息</h3><p>模拟环境中我们发现一台主备的Consumer进程无法处理消息，首先我们查看服务订阅情况，发现服务订阅正常，日志显示正常收到消息，但是Listener收到消息后业务代码未执行，导致触发消息超时反馈。<br><a id="more"></a></p><h3 id="问题排查："><a href="#问题排查：" class="headerlink" title="问题排查："></a>问题排查：</h3><p>为什么业务代码不执行呢？查看业务代码日志，发现在一次处理中，执行了一次Rest请求后，剩余代码未执行，整个线程hold住了，是否由于该线程阻塞导致Listener收到消息后无法处理呢？梳理了一下代码逻辑，我们采用的是Guava EventBus的事件监听和发布订阅模式，业务方法使用@Subscribe订阅到消息后进行处理，而订阅者对象在处理事件时是使用了synchronized同步锁，所以是因为锁一直未释放导致其他消息无法订阅处理吗？为什么线程会卡住呢？</p><p>接下来我们使用JDK的jstack工具打印出服务的线程堆栈信息，确实发现有线程Blocked并且等待锁的情况。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="string">"IMT_328b84d5-3364-46cf-acfe-5e78de9f9cce_BL"</span> prio=<span class="number">10</span> tid=<span class="number">0xada04400</span> nid=<span class="number">0x7816</span> waiting <span class="keyword">for</span> monitor entry [<span class="number">0x9d1fe000</span>]</span><br><span class="line">   java.lang.Thread.State: BLOCKED (on object monitor)</span><br><span class="line">at com.google.common.eventbus.Subscriber$SynchronizedSubscriber.invokeSubscriberMethod(Subscriber.java:<span class="number">150</span>)</span><br><span class="line">- waiting to lock &lt;<span class="number">0xb73e5c58</span>&gt; (a com.google.common.eventbus.Subscriber$SynchronizedSubscriber)</span><br><span class="line">at com.google.common.eventbus.Subscriber$<span class="number">1</span>.run(Subscriber.java:<span class="number">76</span>)</span><br><span class="line">    ......</span><br></pre></td></tr></table></figure><p><code>waiting to lock &lt;0xb73e5c58&gt; (a com.google.common.eventbus.Subscriber$SynchronizedSubscriber)</code><br>从这个信息看确实是Subscribe的同步锁，那么继续寻找当前占有锁的线程，发现如下</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line"><span class="string">"IMT_0945732d-7dc7-4d12-8e5f-c4f7e9f9f295_BL"</span> prio=<span class="number">10</span> tid=<span class="number">0xa7d02c00</span> nid=<span class="number">0x68ce</span> runnable [<span class="number">0x9e9fc000</span>]</span><br><span class="line">   java.lang.Thread.State: RUNNABLE</span><br><span class="line">at java.net.SocketInputStream.socketRead0(Native Method)</span><br><span class="line">at java.net.SocketInputStream.read(SocketInputStream.java:<span class="number">152</span>)</span><br><span class="line">at java.net.SocketInputStream.read(SocketInputStream.java:<span class="number">122</span>)</span><br><span class="line">at org.apache.http.impl.io.AbstractSessionInputBuffer.fillBuffer(AbstractSessionInputBuffer.java:<span class="number">158</span>)</span><br><span class="line">at org.apache.http.impl.io.SocketInputBuffer.fillBuffer(SocketInputBuffer.java:<span class="number">82</span>)</span><br><span class="line">at org.apache.http.impl.io.AbstractSessionInputBuffer.readLine(AbstractSessionInputBuffer.java:<span class="number">271</span>)</span><br><span class="line">at org.apache.http.impl.conn.DefaultHttpResponseParser.parseHead(DefaultHttpResponseParser.java:<span class="number">140</span>)</span><br><span class="line">at org.apache.http.impl.conn.DefaultHttpResponseParser.parseHead(DefaultHttpResponseParser.java:<span class="number">57</span>)</span><br><span class="line">at org.apache.http.impl.io.AbstractMessageParser.parse(AbstractMessageParser.java:<span class="number">259</span>)</span><br><span class="line">at org.apache.http.impl.AbstractHttpClientConnection.receiveResponseHeader(AbstractHttpClientConnection.java:<span class="number">281</span>)</span><br><span class="line">at org.apache.http.impl.conn.DefaultClientConnection.receiveResponseHeader(DefaultClientConnection.java:<span class="number">259</span>)</span><br><span class="line">at org.apache.http.impl.conn.ManagedClientConnectionImpl.receiveResponseHeader(ManagedClientConnectionImpl.java:<span class="number">209</span>)</span><br><span class="line">at org.apache.http.protocol.HttpRequestExecutor.doReceiveResponse(HttpRequestExecutor.java:<span class="number">273</span>)</span><br><span class="line">at org.apache.http.protocol.HttpRequestExecutor.execute(HttpRequestExecutor.java:<span class="number">125</span>)</span><br><span class="line">at org.apache.http.impl.client.DefaultRequestDirector.tryExecute(DefaultRequestDirector.java:<span class="number">686</span>)</span><br><span class="line">at org.apache.http.impl.client.DefaultRequestDirector.execute(DefaultRequestDirector.java:<span class="number">488</span>)</span><br><span class="line">at org.apache.http.impl.client.AbstractHttpClient.doExecute(AbstractHttpClient.java:<span class="number">884</span>)</span><br><span class="line">at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:<span class="number">82</span>)</span><br><span class="line">at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:<span class="number">107</span>)</span><br><span class="line">at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:<span class="number">55</span>)</span><br><span class="line">at com.netflix.loadbalancer.PingUrl.isAlive(PingUrl.java:<span class="number">126</span>)</span><br><span class="line">at com.netflix.loadbalancer.BaseLoadBalancer$SerialPingStrategy.pingServers(BaseLoadBalancer.java:<span class="number">902</span>)</span><br><span class="line">at com.netflix.loadbalancer.BaseLoadBalancer$Pinger.runPinger(BaseLoadBalancer.java:<span class="number">672</span>)</span><br><span class="line">at com.netflix.loadbalancer.BaseLoadBalancer.forceQuickPing(BaseLoadBalancer.java:<span class="number">814</span>)</span><br><span class="line">at com.netflix.loadbalancer.DynamicServerListLoadBalancer.updateAllServerList(DynamicServerListLoadBalancer.java:<span class="number">268</span>)</span><br><span class="line">at com.netflix.loadbalancer.DynamicServerListLoadBalancer.updateListOfServers(DynamicServerListLoadBalancer.java:<span class="number">250</span>)</span><br><span class="line">at com.netflix.loadbalancer.DynamicServerListLoadBalancer.restOfInit(DynamicServerListLoadBalancer.java:<span class="number">144</span>)</span><br><span class="line">at com.netflix.loadbalancer.DynamicServerListLoadBalancer.&lt;init&gt;(DynamicServerListLoadBalancer.java:<span class="number">95</span>)</span><br><span class="line">at com.netflix.loadbalancer.ZoneAwareLoadBalancer.&lt;init&gt;(ZoneAwareLoadBalancer.java:<span class="number">82</span>)</span><br><span class="line">at org.springframework.cloud.netflix.ribbon.RibbonClientConfiguration.ribbonLoadBalancer(RibbonClientConfiguration.java:<span class="number">140</span>)</span><br><span class="line">......</span><br></pre></td></tr></table></figure><p>从以上信息可以看到，线程一直runnable状态，锁释放不了，代码最后执行到<code>java.net.SocketInputStream.socketRead0</code> 发生阻塞，继续向下看，由Apache Httpclient调用，再往后是Ribbon。而我们的服务确实是通过Ribbon管理目标服务IP发送Rest请求，由于本服务器网络特殊，并未注册到Eureka管理，而是直接把多活的目标服务列表写在配置中维护，加上之前日志的分析，线程确实是卡在Rest请求上，这就很奇怪了，Connection Timeout和Read Timeout我们都有设置，为何未生效？而且这个问题只在本环境发生，并且重启后依然稳定复现。首先对访问的目标服务器端口telnet，一切正常，而执行netstat查看服务器的网络连接，发现本机与一个目标服务端口一直存在一个ESTABLISHED的tcp连接，是否这个一直阻塞着？</p><p>查阅了一下socketRead0阻塞的问题，从网上资料看，这个问题确实有存在，并且从一篇博客<a href="https://suclogger.me/a-not-running-runnable/" target="_blank" rel="noopener">从锁死的RUNNABLE线程谈UNIX的I/O模型</a>中发现这样写道，JAVA IO库（java version “1.8.0_131”）的一个坑。</p><blockquote><p>由于某些请求的TCP包传输过程中出现异常导致<code>poll</code>在没有真实可读数据情况下返回可读标识，使得阻塞的<code>recv</code>方法永远阻塞下去，从而使得当前线程一直处于<code>RUNNABLE</code>，当线程池的核心线程都被这种线程占据之后，就再也无法处理新提交的任务了。</p></blockquote><p>现在<a href="https://bugs.openjdk.java.net/browse/JDK-8075484" target="_blank" rel="noopener">open jdk</a>已经修复了这一bug <code>SocketInputStream.socketRead0 can hang even with soTimeout set</code></p><p>但是从这个来看，发生的概率应该很低的，而我们服务却是百分百发生，重启后依旧，所以继续从堆栈信息上分析。</p><p>从堆栈信息上分析可知Ribbon在更新服务列表updateAllServerList，然后执行了PingUrl的isAlive，调用了HttpClient发生了阻塞。默认情况下服务启动后Ribbon并不会直接加载服务列表，而是当第一次Rest请求调用时，Ribbon会去加载服务列表，并且执行设置的PingUrl方法判断服务节点是否存在，加载好服务列表之后根据设置的Loadbalance策略调用服务节点，该线程应该是hold在PingUrl这一步上。</p><p>PingUrl是服务中Ribbon默认设置的ribbonPing实现，用于检测服务IP列表。翻看PingUrl中isAlive方法的源码，大致如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">String urlStr   = <span class="string">""</span>;</span><br><span class="line"><span class="keyword">if</span> (isSecure)&#123;</span><br><span class="line">       urlStr = <span class="string">"https://"</span>;</span><br><span class="line">&#125;<span class="keyword">else</span>&#123;</span><br><span class="line">       urlStr = <span class="string">"http://"</span>;</span><br><span class="line">&#125;</span><br><span class="line">urlStr += server.getId();</span><br><span class="line">urlStr += getPingAppendString();</span><br><span class="line"><span class="keyword">boolean</span> isAlive = <span class="keyword">false</span>;</span><br><span class="line">HttpClient httpClient = <span class="keyword">new</span> DefaultHttpClient();</span><br><span class="line">HttpUriRequest getRequest = <span class="keyword">new</span> HttpGet(urlStr);</span><br><span class="line">String content=<span class="keyword">null</span>;</span><br><span class="line">httpResponse response = httpClient.execute(getRequest);</span><br></pre></td></tr></table></figure><p>可以看出实际上就是调用httpClinet发送了一次GET请求，请求的URL就是服务列表的每一个服务端口+指定的URL。既然如此，我们可以直接模拟同样的GET请求，执行curl命令</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">curl -X GET 'http://server:port'</span><br></pre></td></tr></table></figure><p>果然，有一台目标服务器没有响应，一直卡住，这台服务器正是上面tcp一直处于ESTABLISHED状态的服务器。看来问题应该是由于这台服务器导致的，登陆这台服务器之后，df -h直接卡住不显示结果，怀疑用户目录下共享存储出问题了，cd到用户目录下，执行ls依然卡住未返回，找运维同学处理后，再次执行curl后正常，而服务还是阻塞状态，重启后ESTABLISHED状态tcp连接释放，服务恢复，正常处理消息。</p><h3 id="问题还原："><a href="#问题还原：" class="headerlink" title="问题还原："></a>问题还原：</h3><p>由于一台服务器共享存储挂了，导致服务器上tomcat服务无法处理远程的请求，没有任何返回，连接一直处于ESTABLISHED状态未释放,虽然telnet服务端口是正常的，但是http请求会hold住无响应。而consumer服务使用Ribbon管理目标服务节点，当服务重启后，第一次执行Rest请求，Ribbon会去加载服务列表并进行ribbonPing检测所有的目标节点是否异常，默认是通过一个GET请求，因此当检测到故障的目标节点后，连接释放不了，线程一直hold，而Subscribe的同步锁也一直被线程占用无法释放，导致其他消息过来时无法处理从而超时，而重启后依然重现这一过程导致问题依旧。</p>]]></content>
    
    <summary type="html">
    
      &lt;h3 id=&quot;问题描述：Consumer进程无法处理消息&quot;&gt;&lt;a href=&quot;#问题描述：Consumer进程无法处理消息&quot; class=&quot;headerlink&quot; title=&quot;问题描述：Consumer进程无法处理消息&quot;&gt;&lt;/a&gt;问题描述：Consumer进程无法处理消息&lt;/h3&gt;&lt;p&gt;模拟环境中我们发现一台主备的Consumer进程无法处理消息，首先我们查看服务订阅情况，发现服务订阅正常，日志显示正常收到消息，但是Listener收到消息后业务代码未执行，导致触发消息超时反馈。&lt;br&gt;
    
    </summary>
    
      <category term="Java" scheme="https://www.elietio.xyz/categories/Java/"/>
    
    
      <category term="异常" scheme="https://www.elietio.xyz/tags/%E5%BC%82%E5%B8%B8/"/>
    
      <category term="排错" scheme="https://www.elietio.xyz/tags/%E6%8E%92%E9%94%99/"/>
    
      <category term="socketRead0" scheme="https://www.elietio.xyz/tags/socketRead0/"/>
    
  </entry>
  
  <entry>
    <title>Oracle批量插入数据异常 java.lang.ArrayIndexOutOfBoundsException</title>
    <link href="https://www.elietio.xyz/posts/e6c1ebcb.html"/>
    <id>https://www.elietio.xyz/posts/e6c1ebcb.html</id>
    <published>2019-08-28T12:14:49.000Z</published>
    <updated>2020-06-14T02:32:54.322Z</updated>
    
    <content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/assets/css/APlayer.min.css"><script src="/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><script class="meting-secondary-script-marker" src="/assets/js/Meting.min.js"></script><p>今天测试过程中一个Oracle mybatis批量插入数据的代码报出了一个异常<br> <code>Caused by: java.lang.ArrayIndexOutOfBoundsException: -32768</code><br><a id="more"></a><br>具体异常堆栈信息如下：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">Caused by: java.lang.ArrayIndexOutOfBoundsException: -<span class="number">32768</span></span><br><span class="line">        at oracle.jdbc.driver.OraclePreparedStatement.setupBindBuffers(OraclePreparedStatement.java:<span class="number">2673</span>)</span><br><span class="line">        at oracle.jdbc.driver.OraclePreparedStatement.processCompletedBindRow(OraclePreparedStatement.java:<span class="number">2206</span>)</span><br><span class="line">        at oracle.jdbc.driver.OraclePreparedStatement.executeInternal(OraclePreparedStatement.java:<span class="number">3365</span>)</span><br><span class="line">        at oracle.jdbc.driver.OraclePreparedStatement.execute(OraclePreparedStatement.java:<span class="number">3476</span>)</span><br><span class="line">        at com.alibaba.druid.filter.FilterChainImpl.preparedStatement_execute(FilterChainImpl.java:<span class="number">3409</span>)</span><br><span class="line">        at com.alibaba.druid.wall.WallFilter.preparedStatement_execute(WallFilter.java:<span class="number">619</span>)</span><br><span class="line">        at com.alibaba.druid.filter.FilterChainImpl.preparedStatement_execute(FilterChainImpl.java:<span class="number">3407</span>)</span><br><span class="line">        at com.alibaba.druid.filter.FilterAdapter.preparedStatement_execute(FilterAdapter.java:<span class="number">1080</span>)</span><br><span class="line">        at com.alibaba.druid.filter.FilterChainImpl.preparedStatement_execute(FilterChainImpl.java:<span class="number">3407</span>)</span><br><span class="line">        at com.alibaba.druid.filter.FilterEventAdapter.preparedStatement_execute(FilterEventAdapter.java:<span class="number">440</span>)</span><br><span class="line">        at com.alibaba.druid.filter.FilterChainImpl.preparedStatement_execute(FilterChainImpl.java:<span class="number">3407</span>)</span><br><span class="line">        at com.alibaba.druid.proxy.jdbc.PreparedStatementProxyImpl.execute(PreparedStatementProxyImpl.java:<span class="number">167</span>)</span><br><span class="line">        at com.alibaba.druid.pool.DruidPooledPreparedStatement.execute(DruidPooledPreparedStatement.java:<span class="number">498</span>)</span><br></pre></td></tr></table></figure></p><p>感觉很奇怪，查看日志SQL打印，这个方也就拼接了400条SQL，参数也不是很多，于是从日志里面copy了一下接口入参在本地用Postman debug了一下，结果第一次居然入库成功，再执行一次，出现了同样的错误，再执行，又成功……反复如此，顿时懵逼。看异常信息，执行ojdbc包内的<code>oracle.jdbc.driver.OraclePreparedStatement.setupBindBuffers</code>方法数组下标越界，好吧，放Google搜一下，发现一段这样描述</p><blockquote><p>The 10g driver apparently keeps a global serialnumber for all parameters in the entire batch, with a “short”variable. So you can have at most 32768 parameters in the batch. I was havingthe same exception because I have a INSERT statement with 42 parameters and mybatches can be as big as 1000 records, so 42000 &gt; 32768 and this overflowsto a negative index. I reduced the batch factor to 100 to be safe, and all iswell. I guess your update DML should have a larger number of parameters perrecord, right? (My diagnostic of the bug is just deduction from the symptoms)<br><a href="https://community.oracle.com/thread/599441?start=15&amp;tstart=0&gt;" target="_blank" rel="noopener">https://community.oracle.com/thread/599441?start=15&amp;tstart=0&gt;</a></p></blockquote><p>说是10g driver statement最大允许参数个数为32768，超过会报错。似乎有点类似，但是我只插入了400条啊，而且每个SQL参数只有9个，也就是3600个参数，远小于32768。<br>还有另外一个说法</p><blockquote><p>In Oracle Metalink (Oracle’s support site - Note ID 736273.1) I found that this is a bug in JDBC adapter (version 10.2.0.0.0 to 11.1.0.7.0) that when you call preparedStatement with more than 7 positional parameters then JDBC will throw this error.<br><a href="https://stackoverflow.com/questions/277744/jdbc-oracle-arrayindexoutofboundsexception" target="_blank" rel="noopener">https://stackoverflow.com/questions/277744/jdbc-oracle-arrayindexoutofboundsexception</a></p></blockquote><p>感觉也不符合，但是从搜索的结果看，10g 的 ojdbc似乎确实有些问题，于是看了下pom，乖乖<br><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="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>ojdbc14<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>ojdbc14<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>10.2.0.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></p><p>那么换个版本吧，我们数据库是11.2g的，于是换了个ojdbc6<br><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="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>com.oracle<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>ojdbc6<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>11.2.0.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></p><p>嗯，这次很顺利，没有再出现异常，看来ojdbc14确实有些问题，但是还是比较疑惑，单单400条数据，每条9个参数就已经超过限制了吗？<br><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">insert</span> <span class="attr">id</span>=<span class="string">"insertBatch"</span> <span class="attr">parameterType</span>=<span class="string">"java.util.List"</span>&gt;</span></span><br><span class="line">        INSERT INTO table (a, b, c, d ......</span><br><span class="line">        )</span><br><span class="line">        SELECT SEQ.nextval,A.* FROM (</span><br><span class="line">        <span class="tag">&lt;<span class="name">foreach</span> <span class="attr">collection</span>=<span class="string">"list"</span> <span class="attr">item</span>=<span class="string">"item"</span> <span class="attr">index</span>=<span class="string">"index"</span> <span class="attr">separator</span>=<span class="string">"union all"</span>&gt;</span></span><br><span class="line">            SELECT</span><br><span class="line">            #&#123;item.a&#125;,#&#123;item.b&#125;,#&#123;item.c&#125;,#&#123;item.d&#125;......</span><br><span class="line">            FROM dual</span><br><span class="line">        <span class="tag">&lt;/<span class="name">foreach</span>&gt;</span>) A</span><br><span class="line">    <span class="tag">&lt;/<span class="name">insert</span>&gt;</span></span><br></pre></td></tr></table></figure></p><p>拼接下来实际SQL如下，类似于insert  into tableA select * from tableB<br><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">INSERT</span> <span class="keyword">INTO</span> <span class="keyword">table</span> (a, b, c, d ......)</span><br><span class="line">  <span class="keyword">SELECT</span></span><br><span class="line">    SEQ.nextval,</span><br><span class="line">    A.*</span><br><span class="line">  <span class="keyword">FROM</span></span><br><span class="line">( <span class="keyword">SELECT</span> ?, ?, ?, ?, ?, ?, ?, ? <span class="keyword">FROM</span> dual </span><br><span class="line"> <span class="keyword">union</span> all </span><br><span class="line">   <span class="keyword">SELECT</span> ?, ?, ?, ?, ?, ?, ?, ? <span class="keyword">FROM</span> dual </span><br><span class="line"> <span class="keyword">union</span> all </span><br><span class="line">      <span class="keyword">SELECT</span> ?, ?, ?, ?, ?, ?, ?, ? <span class="keyword">FROM</span> dual</span><br><span class="line"> <span class="keyword">union</span> all </span><br><span class="line"> ......</span><br><span class="line">    ) A</span><br></pre></td></tr></table></figure></p><p>实际debug了一下，确实出异常的时候OraclePreparedStatement.setupBindBuffers方法short数组 bindIndicators大小超过了32768，换成ojdbc6的时候该方法未调用。</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;今天测试过程中一个Oracle mybatis批量插入数据的代码报出了一个异常&lt;br&gt; &lt;code&gt;Caused by: java.lang.ArrayIndexOutOfBoundsException: -32768&lt;/code&gt;&lt;br&gt;
    
    </summary>
    
      <category term="Java" scheme="https://www.elietio.xyz/categories/Java/"/>
    
    
      <category term="Java" scheme="https://www.elietio.xyz/tags/Java/"/>
    
      <category term="Oracle" scheme="https://www.elietio.xyz/tags/Oracle/"/>
    
      <category term="异常" scheme="https://www.elietio.xyz/tags/%E5%BC%82%E5%B8%B8/"/>
    
  </entry>
  
  <entry>
    <title>惨痛，笔记本机械硬盘突然跪了</title>
    <link href="https://www.elietio.xyz/posts/8f7990e0.html"/>
    <id>https://www.elietio.xyz/posts/8f7990e0.html</id>
    <published>2019-08-14T12:37:06.000Z</published>
    <updated>2020-06-13T08:18:25.217Z</updated>
    
    <content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/assets/css/APlayer.min.css"><script src="/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><script class="meting-secondary-script-marker" src="/assets/js/Meting.min.js"></script><p>感觉可能昨天都跪了，一直没注意，今天晚上才发现只剩一个固态C盘了，设备管理器也找不到机械硬盘，拔了重插也不转，心塞，明明这块1T日立也就2年多啊，通电也就6000h，我对日立还特有好感的说，买了好几块了，<br>想到好多东西也没备份就蛋疼</p> <a id="more"></a><p>从各个论坛收集的歌曲、图包、漫画，还有各种软件、文档、配置等等，虽然一部分网盘有备份，但是全部拖下来也得费不少功夫，而且有些东西完全想不起了。。。唯一庆幸的是博客的之前备份了一份到Git上，<a href="https://www.elietio.xyz/2019/04/hexo-themes-subtree.html">hexo和主题多设备同步</a>，o(︶︿︶)o 唉，以后还是多做备份吧。</p><p>新买的2T西数蓝盘，空空如也。</p><p><img src="/img/hdd/hdd.png" alt="hdd"></p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;感觉可能昨天都跪了，一直没注意，今天晚上才发现只剩一个固态C盘了，设备管理器也找不到机械硬盘，拔了重插也不转，心塞，明明这块1T日立也就2年多啊，通电也就6000h，我对日立还特有好感的说，买了好几块了，&lt;br&gt;想到好多东西也没备份就蛋疼&lt;/p&gt;
    
    </summary>
    
      <category term="随笔" scheme="https://www.elietio.xyz/categories/%E9%9A%8F%E7%AC%94/"/>
    
    
      <category term="硬盘" scheme="https://www.elietio.xyz/tags/%E7%A1%AC%E7%9B%98/"/>
    
  </entry>
  
  <entry>
    <title>Jedis Unexpected end of stream 异常</title>
    <link href="https://www.elietio.xyz/posts/ab6869b3.html"/>
    <id>https://www.elietio.xyz/posts/ab6869b3.html</id>
    <published>2019-05-12T11:37:06.000Z</published>
    <updated>2020-06-14T02:29:54.279Z</updated>
    
    <content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/assets/css/APlayer.min.css"><script src="/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><script class="meting-secondary-script-marker" src="/assets/js/Meting.min.js"></script><p>这周末版本升级遇到一个Jedis异常，其中一步是从Mysql中的临时表查找数据然后拼装key从Redis中查找对应的缓存数据并修改。然而升级过程中修数程序却抛出一个异常<code>Unexpected end of stream</code>意外停止。<br><a id="more"></a></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">redis.clients.jedis.exceptions.JedisConnectionException: Unexpected end of stream.</span><br><span class="line">    at redis.clients.util.RedisInputStream.ensureFill(RedisInputStream.java:<span class="number">199</span>)</span><br><span class="line">    at redis.clients.util.RedisInputStream.readByte(RedisInputStream.java:<span class="number">40</span>)</span><br><span class="line">    at redis.clients.jedis.Protocol.process(Protocol.java:<span class="number">151</span>)</span><br><span class="line">......</span><br></pre></td></tr></table></figure><p>Redis采用的是codis搭建的集群，我们立即Telnet访问的端口，并在服务器使用Redis-cli直接连接，结果都未发现异常。于是立即去网上查找相关资料，网上都这样描述此异常。</p><p>客户端缓冲区异常<br>这个异常是客户端缓冲区异常，产生这个问题可能有三个原因：</p><ol><li><p>多个线程使用一个Jedis连接。</p></li><li><p>客户端缓冲区满了,Redis有三种客户端缓冲区：<br> 普通客户端缓冲区(normal)：用于接受普通的命令，例如get、set、mset、hgetall、zrange等。<br> slave客户端缓冲区(slave)：用于同步master节点的写命令，完成复制。<br> 发布订阅缓冲区(pubsub)：pubsub不是普通的命令，因此有单独的缓冲区。<br> Redis客户端缓冲区配置的格式是：</p> <figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="string">client-output-buffer-limit</span> <span class="string">&lt;class&gt;</span> <span class="string">&lt;hard</span> <span class="string">limit&gt;</span> <span class="string">&lt;soft</span> <span class="string">limit&gt;</span> <span class="string">&lt;soft</span> <span class="string">seconds&gt;</span></span><br></pre></td></tr></table></figure><p>  class: 客户端类型：可选值为normal、slave和pubsub。<br>  hard limit: 如果客户端使用的输出缓冲区大于hard limit，客户端会被立即关闭，单位为秒。<br>  soft limit和soft seconds: 如果客户端使用的输出缓冲区超过了soft limit并且持续了soft limit秒，客户端会被立即关闭，单位为秒。</p></li><li><p>长时间闲置连接会被服务端主动断开，可以查询timeout配置的设置以及自身连接池配置确定是否需要做空闲检测。</p></li></ol><p>于是我们立即对配置进行了检查，并未发现有相关问题，timeout默认设置为0，客户端缓冲区临时修改为不限制也未见生效。</p><p>这下犯愁了，由于这个程序几乎没有日志，代码也不是我们编写，而是一位外地的同事提供的。只能一波人紧急分析代码，一波人继续查询错误日志，从异常的堆栈中我们发现异常是从执行redis的一个get方法抛出的，难道是get某个key的时候出现了异常？由于日志中没有打印具体的key信息，所以也不清楚具体情况，难道是某个key的体积过大，导致查询的时候超过了限制？于是立即使用bigkeys查询了一下Redis的大体积key，结果最大的也只有十几kb，很显然这也不是原因。大家正在一筹莫展，准备把程序加上详细的日志再具体分析，但是生产环境做紧急变更交付物是很困难的，之前我们已经在模拟环境同步生产数据测了好几轮，都未发现问题，为何生产就出现问题了呢？</p><p>继续看codis porxy的日志，发现了一个特殊的地方，客户端建立的连接每次都是经过60s后被断开，显示EOF错误，代表客户端客户端主动断开，于是我们立即查找相关配置，是否存在60s的配置，这时候运维同学提到了一件事，codis-porxy使用了nginx做负载均衡代理，nginx应该做了超时配置，于是我们翻看了nginx配置，果然存在一个60s的超时配置，而这时候我们再去翻看代码逻辑，发现了一个问题，首先程序使用jedis建立一个redis连接，然后从MySQL从查找临时表所有数据，然后修改redis缓存。而我们的临时表数据过于庞大，而且缺乏索引，所以查询这一步花费了很长时间，已经超过60s，等数据查询完毕，再执行redis操作，此时一直空闲的连接已经被nginx当作超时给断开了。为何之前模拟环境并未出现这个问题？应该是最近待修的数据又增加了很多，刚好超过60s，导致这个问题并未在模拟阶段发现。</p><p>于是我们在模拟环境复现该问题，并临时把nginx配置修改为600s，于是修数程序正常执行，没有异常，问题到此解决，于是生产同步操作，最终完成了升级。</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;这周末版本升级遇到一个Jedis异常，其中一步是从Mysql中的临时表查找数据然后拼装key从Redis中查找对应的缓存数据并修改。然而升级过程中修数程序却抛出一个异常&lt;code&gt;Unexpected end of stream&lt;/code&gt;意外停止。&lt;br&gt;
    
    </summary>
    
      <category term="Java" scheme="https://www.elietio.xyz/categories/Java/"/>
    
    
      <category term="Redis" scheme="https://www.elietio.xyz/tags/Redis/"/>
    
  </entry>
  
  <entry>
    <title>Nintendo Switch更换内存卡</title>
    <link href="https://www.elietio.xyz/posts/9f2531c3.html"/>
    <id>https://www.elietio.xyz/posts/9f2531c3.html</id>
    <published>2019-04-30T14:39:27.000Z</published>
    <updated>2020-06-13T08:18:25.216Z</updated>
    
    <content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/assets/css/APlayer.min.css"><script src="/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><script class="meting-secondary-script-marker" src="/assets/js/Meting.min.js"></script><p>NS入手一年多了，一直拿张32G的microSD卡凑合着，因为入手游戏不多，又都是实体版，所以下载几个DLC也完全够了，不过考虑到实体版换卡还是麻烦，而且今年感兴趣的游戏也不少，有些不打算买实体了，所以一直想找个机会换张大一点的microSD卡，正好狗东200G闪迪卡活动195，价格还算OK，于是入手换之。<br><a id="more"></a> </p><h4 id="准备："><a href="#准备：" class="headerlink" title="准备："></a>准备：</h4><ul><li>Nintendo Switch</li><li>闪迪（SanDisk）200GB TF（MicroSD）存储卡 U1 C10 A1</li><li>高速读卡器一枚</li><li>PC</li></ul><h4 id="换卡流程："><a href="#换卡流程：" class="headerlink" title="换卡流程："></a>换卡流程：</h4><p><span id="inline-toc">1.</span> 取卡：NS 关机（Power Off），取出原来的 MicroSD 卡，并把MicroSD卡内容备份到PC<br><span id="inline-toc">2.</span> NS插入新卡（可能需要更新系统）<br><span id="inline-toc">3.</span> 格式化新卡（建议）：设置 - 系统设置 - 格式化选项 - 格式化 microSD 卡，格式化新卡。<br><span id="inline-toc">4.</span> 拷贝游戏文件：将新卡里的 Nintendo 文件夹删除，同时将旧卡备份到PC里的 Nintendo 文件夹复制到新卡里<br><span id="inline-toc">5.</span> 插入新卡：在关机的状态下插入新卡，然后开机，检查下载内容是否存在。</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;NS入手一年多了，一直拿张32G的microSD卡凑合着，因为入手游戏不多，又都是实体版，所以下载几个DLC也完全够了，不过考虑到实体版换卡还是麻烦，而且今年感兴趣的游戏也不少，有些不打算买实体了，所以一直想找个机会换张大一点的microSD卡，正好狗东200G闪迪卡活动195，价格还算OK，于是入手换之。&lt;br&gt;
    
    </summary>
    
      <category term="电子产品" scheme="https://www.elietio.xyz/categories/%E7%94%B5%E5%AD%90%E4%BA%A7%E5%93%81/"/>
    
    
      <category term="电子产品" scheme="https://www.elietio.xyz/tags/%E7%94%B5%E5%AD%90%E4%BA%A7%E5%93%81/"/>
    
      <category term="Nintendo Switch" scheme="https://www.elietio.xyz/tags/Nintendo-Switch/"/>
    
      <category term="NS" scheme="https://www.elietio.xyz/tags/NS/"/>
    
  </entry>
  
  <entry>
    <title>TCP的三次握手和四次挥手</title>
    <link href="https://www.elietio.xyz/posts/b86735db.html"/>
    <id>https://www.elietio.xyz/posts/b86735db.html</id>
    <published>2019-04-29T14:14:49.000Z</published>
    <updated>2020-06-13T08:18:25.216Z</updated>
    
    <content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/assets/css/APlayer.min.css"><script src="/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><script class="meting-secondary-script-marker" src="/assets/js/Meting.min.js"></script><p><code>TCP（Transmission Control Protoco）</code>，是一种基于字节流面向连接的传输层协议。数据的传输需要通信双方建立一个连接，TCP协议采用三次握手建立一个连接，采用 4 次挥手来关闭一个连接。每一个TCP连接都有两个端点，叫作套接字（socket），它的定义为IP地址+端口号拼接。</p><a id="more"></a><h3 id="TCP-IP协议概况"><a href="#TCP-IP协议概况" class="headerlink" title="TCP/IP协议概况"></a>TCP/IP协议概况</h3><ul><li>IPv4: 网际协议版本4(Internet Protocol version 4), 使用32位地址</li><li>IPv6: 网际协议版本6(Internet Protocol version 6), 使用128位地址，是IPv4替代品，通常把它两者称为”IP“</li><li>TCP: 传输控制协议(Transmission Control Protocol),TCP是一个<strong>面向连接</strong>的协议,为用户进程提供<strong>可靠</strong>的<strong>全双工字节流</strong>， TCP套接字是一种<strong>流套接字(stream sockte)</strong>, 关心<strong>确认</strong>, <strong>超时</strong>, <strong>重传</strong>等细节</li><li>UDP: 用户数据报协议(User Datagram Protocol), UDP是一个<strong>无连接</strong>协议,UDP套接字是一种<strong>数据报套接字(datagram socket)</strong></li><li>SCTP：流控制传输协议(Stream Control Transmission Protocol),SCTP是一个提供<strong>可靠全双工关联的面向连接的协议</strong></li><li>ICMP：网际控制消息协议(Internet Control Message Protocol),处理在路由器和主机之间流通的错误和控制消息</li><li>ICMPv6：网际控制消息协议版本6</li><li>IGMP: 网际组管理协议,用于多播</li><li>ARP: 地址解析协议(Address Resolution Protocol),把IPv4地址映射成一个硬件地址(如以太网地址)</li><li>RARP: 反地址解析协议(Reverse..), 将硬件地址映射成IPv4地址</li><li>BPF: BSD分组过滤器</li></ul><h3 id="TCP通信三部曲"><a href="#TCP通信三部曲" class="headerlink" title="TCP通信三部曲"></a>TCP通信三部曲</h3><ul><li>建立：三次握手</li><li>传输：超时重传、快速重传、流量控制、拥塞控制等</li><li>断开：四次挥手</li></ul><h4 id="TCP服务模型"><a href="#TCP服务模型" class="headerlink" title="TCP服务模型"></a>TCP服务模型</h4><p>一个 TCP 连接由一个 4 元组构成，分别是两个 IP 地址和两个端口号。一个 TCP 连接通常分为三个阶段：启动、数据传输、关闭。</p><p>当 TCP 接收到另一端的数据时，它会发送一个确认，但这个确认不会立即发送，一般会延迟一会儿。ACK 是累积的，一个确认字节号 N 的 ACK 表示所有直到 N 的字节（不包括 N）已经成功被接收了。这样的好处是如果一个 ACK 丢失，很可能后续的 ACK 就足以确认前面的报文段了。</p><p>一个完整的 TCP 连接是双向和对称的，数据可以在两个方向上平等地流动。给上层应用程序提供一种双工服务。一旦建立了一个连接，这个连接的一个方向上的每个 TCP 报文段都包含了相反方向上的报文段的一个 ACK。</p><p>序列号的作用是使得一个 TCP 接收端可丢弃重复的报文段，记录以杂乱次序到达的报文段。因为 TCP 使用 IP 来传输报文段，而 IP 不提供重复消除或者保证次序正确的功能。另一方面，TCP 是一个字节流协议，绝不会以杂乱的次序给上层程序发送数据。因此 TCP 接收端会被迫先保持大序列号的数据不交给应用程序，直到缺失的小序列号的报文段被填满。</p><h4 id="TCP报文头部"><a href="#TCP报文头部" class="headerlink" title="TCP报文头部"></a>TCP报文头部</h4><p><img src="/img/tcp-handshake-wave/TCP-Header.jpg" alt="TCP-Header"></p><ul><li>源端口和目的端口，各占2个字节；</li><li>序号，占4个字节，TCP连接中传送的字节流中的每个字节都按顺序编号；</li><li>确认号，占4个字节，是期望收到对方下一个报文的第一个数据字节的序号，即最后被成功接收的数据字节序列号加 1，这个字段只有在 ACK 位被启用的时候才有效；</li><li>数据偏移，占4位，它指出TCP报文的数据距离TCP报文段的起始处有多远；</li><li>保留，占6位，保留今后使用，但目前应都位0；</li><li>紧急URG，当URG=1，表明紧急指针字段有效。告诉系统此报文段中有紧急数据；</li><li>确认ACK，仅当ACK=1时，确认号字段才有效。TCP规定，在连接建立后所有报文的传输都必须把ACK置1；</li><li>推送PSH，当两个应用进程进行交互式通信时，有时在一端的应用进程希望在键入一个命令后立即就能收到对方的响应，这时候就将PSH=1；</li><li>复位RST，当RST=1，表明TCP连接中出现严重差错，必须释放连接，然后再重新建立连接；</li><li>同步SYN，在连接建立时用来同步序号。当SYN=1，ACK=0，表明是连接请求报文，若同意连接，则响应报文中应该使SYN=1，ACK=1；</li><li>终止FIN，用来释放连接。当FIN=1，表明此报文的发送方的数据已经发送完毕，并且要求释放；</li><li>窗口，占2字节，指的是通知接收方，发送本报文你需要有多大的空间来接受；</li><li>检验和，占2字节，校验首部和数据这两部分；</li><li>紧急指针，占2字节，指出本报文段中的紧急数据的字节数；</li></ul><h4 id="TCP状态转换"><a href="#TCP状态转换" class="headerlink" title="TCP状态转换"></a>TCP状态转换</h4><table><thead><tr><th>状 态</th><th style="text-align:left">描 述</th></tr></thead><tbody><tr><td>CLOSED</td><td style="text-align:left">关闭状态，没有连接活动或正在进行</td></tr><tr><td>LISTEN</td><td style="text-align:left">监听状态，服务器正在等待连接进入</td></tr><tr><td>SYN_RCVD</td><td style="text-align:left">收到一个连接请求，尚未确认</td></tr><tr><td>SYN_SENT</td><td style="text-align:left">已经发出连接请求，等待确认</td></tr><tr><td>ESTABLISHED</td><td style="text-align:left">连接建立，正常数据传输状态</td></tr><tr><td>FIN_WAIT_1</td><td style="text-align:left">（主动关闭）已经发送关闭请求，等待确认</td></tr><tr><td>FIN_WAIT_2</td><td style="text-align:left">（主动关闭）收到对方关闭确认，等待对方关闭请求</td></tr><tr><td>TIMED_WAIT</td><td style="text-align:left">完成双向关闭，等待所有分组死掉</td></tr><tr><td>CLOSING</td><td style="text-align:left">双方同时尝试关闭，等待对方确认</td></tr><tr><td>CLOSE_WAIT</td><td style="text-align:left">（被动关闭）收到对方关闭请求，已经确认</td></tr><tr><td>LAST_ACK</td><td style="text-align:left">（被动关闭）等待最后一个关闭确认，并等待所有分组死掉</td></tr></tbody></table><p><img src="/img/tcp-handshake-wave/tcp-ip-handshark.png" alt="tcp-ip-handshark"></p><p>那么状态转换为什么要经历三次握手和四次挥手呢？</p><h4 id="TCP三次握手"><a href="#TCP三次握手" class="headerlink" title="TCP三次握手"></a>TCP三次握手</h4><ol><li>服务器进程先创建传输控制块TCB，时刻准备接受客户进程的连接请求，此时服务器就进入了LISTEN（监听）状态；</li><li>客户端先创建传输控制块TCB，然后向服务器发出连接请求报文，SYN=1，表示请求建立连接，同时选择一个初始序列号 seq=x ，此时，TCP客户端进程进入了 SYN_SENT（同步已发送状态）状态。TCP规定，SYN报文段（SYN=1的报文段）不能携带数据，但需要消耗掉一个序号。</li><li>服务器收到请求报文后，如果同意连接，则发出确认报文。确认报文中应该 ACK=1，SYN=1，确认号是ack=x+1，同时也要为自己初始化一个序列号 seq=y，此时，TCP服务器进程进入了SYN_RCVD（同步收到）状态。这个报文也不能携带数据，但是同样要消耗一个序号。</li><li>T客户端收到确认后，还要向服务器给出确认。确认报文的ACK=1，ack=y+1，自己的序列号seq=x+1，此时，TCP连接建立，客户端进入ESTABLISHED（已建立连接）状态。TCP规定，ACK报文段可以携带数据，但是如果不携带数据则不消耗序号。</li><li>当服务器收到客户端的确认后也进入ESTABLISHED状态，三次握手结束，连接建立。</li></ol><p><img src="/img/tcp-handshake-wave/tcp-three-handshake.gif" alt="tcp-three-handshake"></p><p><strong>为什么需要第三次客户端ACK？</strong></p><div class="note info no-icon">            <p><code>三次握手</code>的目的是“<strong>为了防止已失效的连接请求报文段突然又传送到了服务端，因而产生错误</strong>。</p><p>例如网络出现抖动导致第一次SYN=1并没有及时到达服务端，从而客户端无法收到服务端的ACK，会触发客户端的重试。如果只有两次握手，第二次重发建立连接后，第一次延迟的请求刚好到达服务端，服务端会再次回包ACK，这样本来无效的请求又会建立新的连接。如果是三次握手，客户端收到服务端ACK并不会再次发出确认，这样就不会有新的连接建立。</p>          </div><h4 id="TCP四次挥手"><a href="#TCP四次挥手" class="headerlink" title="TCP四次挥手"></a>TCP四次挥手</h4><p>以客户端主动关闭为例，服务器端也可以主动关闭，方向与下面相反。</p><p><img src="/img/tcp-handshake-wave/tcp-four-wave.gif" alt="tcp-four-wave"></p><ol><li>客户端进程发出连接释放报文，并且停止发送数据。释放数据报文首部，FIN=1，其序列号为seq=u（等于前面已经传送过来的数据的最后一个字节的序号加1），此时，客户端进入FIN_WAIT_1（终止等待1）状态。 TCP规定，FIN报文段即使不携带数据，也要消耗一个序号。</li><li>服务器收到连接释放报文，发出确认报文，ACK=1，ack=u+1，并且带上自己的序列号seq=v，此时，服务端就进入了CLOSE_WAIT（关闭等待）状态。TCP服务器通知高层的应用进程，客户端向服务器的方向就释放了，这时候处于半关闭状态，即客户端已经没有数据要发送了，但是服务器若发送数据，客户端依然要接受。这个状态还要持续一段时间，也就是整个CLOSE_WAIT状态持续的时间。</li><li>客户端收到服务器的确认请求后，此时，客户端就进入FIN_WAIT_2（终止等待2）状态，等待服务器发送连接释放报文（在这之前还需要接受服务器发送的最后的数据）。</li><li>服务器将最后的数据发送完毕后，就向客户端发送连接释放报文，FIN=1，ack=u+1，由于在半关闭状态，服务器很可能又发送了一些数据，假定此时的序列号为seq=w，此时，服务器就进入了LAST_ACK（最后确认）状态，等待客户端的确认。</li><li>客户端收到服务器的连接释放报文后，必须发出确认，ACK=1，ack=w+1，而自己的序列号是seq=u+1，此时，客户端就进入了TIME_WAIT（时间等待）状态。注意此时TCP连接还没有释放，必须经过2*MSL（最长报文段寿命）的时间后，当客户端撤销相应的TCB后，才进入CLOSED状态。</li><li>服务器只要收到了客户端发出的确认，立即进入CLOSED状态，撤销TCB结束TCP连接。</li></ol><p><strong>为什么主动关闭连接方最后需要等待2MSL？</strong></p><div class="note info no-icon">            <p> MSL（Maximum Segment Lifetime），报文最大生存时间。</p><ol><li>保证客户端发送的最后一个ACK报文能够到达服务器，如果因为网络原因导致服务器没收到，服务器会重新发送一次FIN+ack请求关闭连接，客户端就能收到这个重传的报文再次ACK，并且重启2MSL计时器。</li><li>防止类似与“三次握手”中提到了的“已经失效的连接请求报文段突然又传送到了服务端，因而产生错误”。客户端发送完最后一个确认报文后，在这个2MSL时间中，就可以使本连接持续的时间内所产生的所有报文段都从网络中消失，新的连接中不会出现旧连接的请求报文。</li><li>需要注意的是在TIME_WAIT状态 时两端的端口不能使用，客户端要等到2MSL时间结束才可继续使用。</li></ol>          </div><p><strong>为什么关闭连接需要四次挥手，比建立连接多一次呢？</strong></p><div class="note info no-icon">            <p>建立连接的时候， 服务器在LISTEN状态下，收到建立连接请求的SYN报文后，把ACK和SYN放在一个报文里发送给客户端。<br>而关闭连接时，ACK和SYN是分开发送的，服务器收到对方的FIN报文时，仅仅表示客户端请求关闭但是还能接收数据，而且服务器的数据可能也没有发送完毕，所以服务器可以选择立即关闭FIN也可以再发送一部分数据然后再发送FIN报文给客户端关闭连接，由于服务器ACK和FIN分开发送，所以多了一次。</p>          </div><p><strong>如果通信双方同时请求连接或同时请求释放连接？</strong></p><div class="note info no-icon">            <p>这种情况虽然发生的可能性极小，但是是确实存在的，TCP也特意设计了相关机制，使得在这种情况下双方仅建立一条连接。</p><ul><li>双方同时请求连接的情况下，双方同时发出请求连接报文，并进入SYN_SENT状态；当收到对方的请求连接报文后，会再次发送请求连接报文，确认号为对方的SYN+1，并进入SYN_RCVD状态；当收到对方第二次发出的携带确认号的请求报文之后，会进入ESTABLISHED状态。 </li><li>双方同时请求释放连接也是同样的，双方同时发出连接释放报文，并进入FIN_WAIT_1状态；在收到对方的报文之后，发送确认报文，并进入CLOSING状态；在收到对方的确认报文后，进入TIME_WAIT状态，等待2MSL之后关闭连接。需要注意的是，这个时候虽然不用再次发送确认报文并确认对方收到，双方仍需等待2MSL之后再关闭连接，是为了防止“已失效的连接请求报文段”的影响。</li></ul>          </div><p><strong>TIME_WAIT状态</strong><br><div class="note warning no-icon">            <p>首先TIME_WAIT状态是执行主动关闭的那一端产生的，从上面2MSL中我们了解TIME_WAIT状态有两个存在的理由:</p><ol><li>可靠地实现TCP全双工连接的终止，即最后一次ACK如果丢失，可以重新发送FIN并再次ACK；</li><li>允许老的重复分节在网络中消逝，新的连接中不会出现旧连接的请求报文；</li></ol><p>在高并发短连接的TCP服务器上，当服务器处理完请求后立刻按照主动正常关闭连接。这个场景下，会出现大量socket处于TIME_WAIT状态。如果客户端的并发量持续很高，大量端口处于TIME_WAIT状态，无法正常使用，此时部分客户端就会显示连接不上，所以需要引起重视。</p>          </div></p><blockquote class="question"><br>参考📚：<br><i class="fa fa-hand-o-right" aria-hidden="true"></i><a href="https://blog.csdn.net/qzcsu/article/details/72861891" target="_blank" rel="noopener">TCP的三次握手与四次挥手</a><br></blockquote>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;&lt;code&gt;TCP（Transmission Control Protoco）&lt;/code&gt;，是一种基于字节流面向连接的传输层协议。数据的传输需要通信双方建立一个连接，TCP协议采用三次握手建立一个连接，采用 4 次挥手来关闭一个连接。每一个TCP连接都有两个端点，叫作套接字（socket），它的定义为IP地址+端口号拼接。&lt;/p&gt;
    
    </summary>
    
      <category term="技术" scheme="https://www.elietio.xyz/categories/%E6%8A%80%E6%9C%AF/"/>
    
    
      <category term="TCP" scheme="https://www.elietio.xyz/tags/TCP/"/>
    
      <category term="网络" scheme="https://www.elietio.xyz/tags/%E7%BD%91%E7%BB%9C/"/>
    
  </entry>
  
  <entry>
    <title>hexo和主题多设备同步</title>
    <link href="https://www.elietio.xyz/posts/d00638d8.html"/>
    <id>https://www.elietio.xyz/posts/d00638d8.html</id>
    <published>2019-04-12T11:39:27.000Z</published>
    <updated>2020-06-13T08:18:25.216Z</updated>
    
    <content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/assets/css/APlayer.min.css"><script src="/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><script class="meting-secondary-script-marker" src="/assets/js/Meting.min.js"></script><p>使用hexo可以生成静态网页部署到GitHub和VPS上搭建个人博客，但是hexo的部署都是在本地，如果换了一套环境如何也能够编辑发布自己的博客网站呢？<br><a id="more"></a> </p><h2 id="同步方案"><a href="#同步方案" class="headerlink" title="同步方案"></a>同步方案</h2><p>由于部署博客已经使用了github仓库托管网页代码，我们可以考虑使用这个来做hexo部署发布管理的版本控制，由于部署的网站默认使用了master分支，因此我们可以使用一个新的分支hexo或者新建一个仓库来管理。</p><p>下面步骤默认已经安装好了hexo并且已经成功部署网站，首先切换到hexo主目录，git init进行初始化，如果已经纳入git管理并且关联了远程仓库，可能需要删除重新关联。</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#</span><span class="bash"> git 初始化</span></span><br><span class="line"><span class="meta">$</span><span class="bash"> git init</span></span><br><span class="line"><span class="meta">#</span><span class="bash"> 查看关联的远程仓库</span></span><br><span class="line"><span class="meta">$</span><span class="bash"> git remote</span></span><br><span class="line"><span class="meta">#</span><span class="bash"> 删除</span></span><br><span class="line"><span class="meta">$</span><span class="bash"> git remote rm origin</span></span><br><span class="line"><span class="meta">#</span><span class="bash"> 设置新的远程仓库</span></span><br><span class="line"><span class="meta">$</span><span class="bash"> git remote add origin git@github.com:Elietio/Elietio.github.io.git</span></span><br><span class="line"><span class="meta">#</span><span class="bash"> master分支会作为网站部署分支，因此我们切换一个新的分支hexo</span></span><br><span class="line"><span class="meta">$</span><span class="bash"> git checkout -b hexo</span></span><br><span class="line"><span class="meta">#</span><span class="bash"> 推送远程</span></span><br><span class="line"><span class="meta">$</span><span class="bash"> git add .</span></span><br><span class="line"><span class="meta">$</span><span class="bash"> git commit -m <span class="string">""</span></span></span><br><span class="line"><span class="meta">$</span><span class="bash"> git push -u origin hexo</span></span><br></pre></td></tr></table></figure><p>到这一步似乎已经大功告成了，然而，如果你使用了第三方主题，并且是直接git clone，你会发现一个问题，上传的themes/下面主题是空目录，因为git无法直接管理这样的嵌套模块，那么该怎么做呢？最暴力，直接取消主题模块的git管理，但是这样后续主题模块的更新就是一个问题了，所以不推荐，好在我们可以通过git的submodule或subtree来实现，对于git clone安装的其它主题和插件都可以按照这个思路。</p><h2 id="git-submodule-vs-git-subtree"><a href="#git-submodule-vs-git-subtree" class="headerlink" title="git submodule vs git subtree"></a>git submodule vs git subtree</h2><p>简单来说，submodule 和 subtree 最大的区别是，submodule 保存的是子仓库的 link，而 subtree 保存的是子仓库的 copy。</p><h3 id="git-submodule"><a href="#git-submodule" class="headerlink" title="git submodule"></a>git submodule</h3><p>child 目录被当做一个独立的 Git 仓库，所有的 Git 命令都可以在 child 目录以及上层项目下独立工作。尽管 child 是子目录，当你不在 child 目录时并不记录它的内容。而当你在那个子目录里修改并提交时，子项目会通知那里的 HEAD 已经发生变更并记录你当前正在工作的那个提交。而此时上层项目会显示 child 目录下的改动，将它记录成来自那个仓库的一个特殊的提交。</p><p>若他人要克隆该项目，会发现 child 目录为空。这时需要执行 git submodule init 来初始化你的本地配置文件，以及 git submodule update 拉取数据并切换到合适的提交。而后每次从主项目拉取子模块的变更时，由于主项目只更新了子模块提交的引用而没有更新子模块目录下的代码，必须执行 git submodule update 来更新子模块代码。</p><h3 id="git-subtree"><a href="#git-subtree" class="headerlink" title="git subtree"></a>git subtree</h3><p>不同于 git submodule，此时的 child 仅仅是含有相关代码的普通目录，而不是一个独立的 Git 仓库。因此当在 child 进行修改时，上层项目会立刻记录其改动，而不是像之前那样先在子项目中提交才能进行记录。克隆上层仓库时 child 目录也不再为空。但同时，child 也不能再执行独立的 Git 命令，只有 git subtree 相关的操作。</p><p>进行操作前请先备份已有的next主题目录，根据不同情况操作中可能需要删除并且重新clone下来</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line">首先在自己的github上fork一份next源码</span><br><span class="line">git@github.com:Elietio/hexo-theme-next.git     </span><br><span class="line"></span><br><span class="line"><span class="meta">#</span><span class="bash"> 为hexo添加远程仓库 </span></span><br><span class="line"><span class="meta">#</span><span class="bash"> git remote add -f &lt;子仓库名&gt; &lt;子仓库地址&gt;</span></span><br><span class="line"><span class="meta">$</span><span class="bash"> git remote add -f next git@github.com:Elietio/hexo-theme-next.git</span></span><br><span class="line"><span class="meta">#</span><span class="bash"> 添加subtree</span></span><br><span class="line"><span class="meta">#</span><span class="bash"> git subtree add --prefix=&lt;子目录名&gt; &lt;子仓库名&gt; &lt;分支&gt; squash意思是把subtree的改动合并成一次commit</span></span><br><span class="line"><span class="meta">$</span><span class="bash"> git subtree add --prefix=themes/next next master --squash</span></span><br><span class="line"><span class="meta">#</span><span class="bash"> 更新子项目</span></span><br><span class="line"><span class="meta">$</span><span class="bash"> git fetch next master</span></span><br><span class="line"><span class="meta">$</span><span class="bash"> git subtree pull --prefix=themes/next next master --squash</span></span><br><span class="line">整个项目的pull、push同样会对子项目起作用</span><br><span class="line"></span><br><span class="line">而对next子项目进行pull、push操作需要使用subtree</span><br><span class="line"><span class="meta">#</span><span class="bash"> git subtree push --prefix=&lt;子目录名&gt; &lt;远程分支名&gt; 分支</span></span><br><span class="line"><span class="meta">$</span><span class="bash"> git subtree push --prefix=themes/next next master  </span></span><br><span class="line"></span><br><span class="line"><span class="meta">#</span><span class="bash"> git subtree pull --prefix=&lt;子目录名&gt; &lt;远程分支名&gt; 分支</span></span><br><span class="line"><span class="meta">$</span><span class="bash"> git subtree pull --prefix=themes/yilia yilia master --squash</span></span><br></pre></td></tr></table></figure><h2 id="其它设备同步"><a href="#其它设备同步" class="headerlink" title="其它设备同步"></a>其它设备同步</h2><p>上面操作已经把hexo的源目录同步到Git，因此我们只需要clone，并且安装node.js和hexo环境</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">$</span><span class="bash"> git <span class="built_in">clone</span> -b hexo git@github.com:Elietio/Elietio.github.io.git</span></span><br><span class="line"><span class="meta">#</span><span class="bash"> 安装 hexo</span></span><br><span class="line"><span class="meta">$</span><span class="bash"> npm install hexo(npm install hexo-cli -g)</span></span><br><span class="line"><span class="meta">#</span><span class="bash"> 安装依赖库</span></span><br><span class="line"><span class="meta">$</span><span class="bash"> npm install </span></span><br><span class="line"><span class="meta">#</span><span class="bash"> 安装部署相关配置</span></span><br><span class="line"><span class="meta">$</span><span class="bash"> npm install hexo-deployer-git</span></span><br></pre></td></tr></table></figure>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;使用hexo可以生成静态网页部署到GitHub和VPS上搭建个人博客，但是hexo的部署都是在本地，如果换了一套环境如何也能够编辑发布自己的博客网站呢？&lt;br&gt;
    
    </summary>
    
      <category term="hexo" scheme="https://www.elietio.xyz/categories/hexo/"/>
    
    
      <category term="linux" scheme="https://www.elietio.xyz/tags/linux/"/>
    
      <category term="hexo" scheme="https://www.elietio.xyz/tags/hexo/"/>
    
      <category term="git" scheme="https://www.elietio.xyz/tags/git/"/>
    
      <category term="subtree" scheme="https://www.elietio.xyz/tags/subtree/"/>
    
  </entry>
  
  <entry>
    <title>linux的awk命令</title>
    <link href="https://www.elietio.xyz/posts/5bafe779.html"/>
    <id>https://www.elietio.xyz/posts/5bafe779.html</id>
    <published>2019-04-10T11:39:27.000Z</published>
    <updated>2020-06-13T08:18:25.216Z</updated>
    
    <content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/assets/css/APlayer.min.css"><script src="/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><script class="meting-secondary-script-marker" src="/assets/js/Meting.min.js"></script><p>awk是一个强大的文本分析工具，相对于grep的查找，sed的编辑，awk在其对数据分析并生成报告时，显得尤为强大。简单来说awk就是把文件逐行的读入，以空格为默认分隔符将每行切片，切开的部分再进行各种分析处理。<br><a id="more"></a><br>awk 命令和 sed 命令结构相同，通常情况下，awk 将每个输入行解释为一条记录而每一行中的内容（由空格或者制表符分隔）解释为每一个字段，一个或者多个连续空格或者制表符看做定界符。awk 中 <code>$0</code> 代表整个记录。</p><h2 id="linux的awk命令"><a href="#linux的awk命令" class="headerlink" title="linux的awk命令"></a>linux的awk命令</h2><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">awk ' /MA/ &#123; print $1 &#125;' list</span><br></pre></td></tr></table></figure><p>解释：打印包含 MA 的行中的第一个单词。再举一个具体的例子，比如</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">echo 'this is one world\nthat is another world' | awk '&#123;print $1&#125;'</span><br></pre></td></tr></table></figure><p>那么输出就是 awk 处理之后的每一行第一个字符也就是</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">this</span><br><span class="line">that</span><br></pre></td></tr></table></figure><h3 id="基本格式"><a href="#基本格式" class="headerlink" title="基本格式"></a>基本格式</h3><p>awk 命令的基本格式</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">awk [options] <span class="string">'script'</span> file</span><br></pre></td></tr></table></figure><p><code>options</code> 这个表示一些可选的参数选项，<code>script</code> 表示 awk 的可执行脚本代码（一般被<code>{}</code> 花括号包围），这个是必须的。<code>file</code> 这个表示 awk 需要处理的文件，注意需要是纯文本文件（意味着 awk 能够处理）。</p><h3 id="awk-自定义分隔符"><a href="#awk-自定义分隔符" class="headerlink" title="awk 自定义分隔符"></a>awk 自定义分隔符</h3><p>之前提到的awk 默认的分割符为<strong>空格和制表符</strong>，awk 会根据这个默认的分隔符将每一行分为若干字段，依次用 <code>$1</code>, <code>$2</code>,<code>$3</code> 来表示，可以使用 <code>-F</code> 参数来指定分隔符</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">awk -F <span class="string">':'</span> <span class="string">'&#123;print $1&#125;'</span> /etc/passwd</span><br></pre></td></tr></table></figure><p>解释：使用 <code>-F</code> 来改变分隔符为 <code>:</code> ，比如上面的命令将 /etc/passwd 文件中的每一行用冒号 <code>:</code> 分割成多个字段，然后用 print 将第 1 列字段的内容打印输出</p><p>在 awk 中同时指定多个分隔符，比如现在有这样一个文件 some.log 文件内容如下</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">Grape(100g)1980</span><br><span class="line">raisins(500g)1990</span><br><span class="line">plum(240g)1997</span><br><span class="line">apricot(180g)2005</span><br><span class="line">nectarine(200g)2008</span><br></pre></td></tr></table></figure><p>现在我们想将上面的 some.log 文件中按照 “水果名称（重量）年份” 来进行分割</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">$ awk -F <span class="string">'[()]'</span> <span class="string">'&#123;print $1, $2, $3&#125;'</span> some.log</span><br><span class="line">Grape 100g 1980</span><br><span class="line">raisins 500g 1990</span><br><span class="line">plum 240g 1997</span><br><span class="line">apricot 180g 2005</span><br><span class="line">nectarine 200g 2008</span><br></pre></td></tr></table></figure><p>在 <code>-F</code> 参数中使用一对方括号来指定多个分隔符，awk 处理 some.log 文件时就会使用 “(“ 或者 “)” 来对文件的每一行进行分割。</p><h3 id="awk-内置变量的使用"><a href="#awk-内置变量的使用" class="headerlink" title="awk 内置变量的使用"></a>awk 内置变量的使用</h3><p>awk 除了 <code>$</code> 和数字表示字段还有一些其他的内置变量：</p><ul><li>$0 这个表示文本处理时的当前行，$1 表示文本行被分隔后的第 1 个字段列，$2 表示文本行被分割后的第 2 个字段列，$3 表示文本行被分割后的第 3 个字段列，$n 表示文本行被分割后的第 n 个字段列</li><li>NR 表示文件中的行号，表示当前是第几行</li><li>NF 表示文件中的当前行被分割的列数，可以理解为 MySQL 数据表里面每一条记录有多少个字段，所以 $NF 就表示最后一个字段，$(NF-1) 就表示倒数第二个字段</li><li>FS 表示 awk 的输入分隔符，默认分隔符为空格和制表符，可以对其进行自定义设置</li><li>OFS 表示 awk 的输出分隔符，默认为空格，也可以对其进行自定义设置</li><li>FILENAME 表示当前文件的文件名称，如果同时处理多个文件，它也表示当前文件名称</li><li>RS 行分隔符，用于分割行，默认为换行符</li><li>ORS 输出记录的分隔符，默认为换行符</li></ul><p>比如我们有这么一个文本文件 fruit.txt 内容如下，用它来演示如何使用 awk 命令工具</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">peach    100   Mar  1997   China</span><br><span class="line">Lemon    150   Jan  1986   America</span><br><span class="line">Pear     240   Mar  1990   Janpan</span><br><span class="line">avocado  120   Feb  2008   china</span><br><span class="line"></span><br><span class="line">awk <span class="string">'&#123;print $0&#125;'</span> fruit.txt   <span class="comment"># 表示打印输出文件的每一整行的内容</span></span><br><span class="line">awk <span class="string">'&#123;print $1&#125;'</span> fruit.txt   <span class="comment"># 表示打印输出文件的每一行的第 1 列内容</span></span><br><span class="line">awk <span class="string">'&#123;print $1, $2&#125;'</span> fruit.txt</span><br></pre></td></tr></table></figure><p>文件的每一行的每一列的内容除了可以用 print 命令打印输出以外，还可以对其进行赋值</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">awk <span class="string">'&#123;$2 = "***"; print $0&#125;'</span> fruit.txt</span><br></pre></td></tr></table></figure><p>上面的例子就是表示通过对 <code>$2</code> 变量进行重新赋值，来隐藏每一行的第 2 列内容，并且用星号 <code>*</code> 来代替其输出</p><p>在参数列表中加入一些字符串或者转义字符之类的东东</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">awk <span class="string">'&#123;print $1 "\t" $2 "\t" $3&#125;'</span> fruit.txt</span><br></pre></td></tr></table></figure><p>像上面这样，你可以在 print 的参数列表中加入一些字符串或者转义字符之类的东东，让输出的内容格式更漂亮，但一定要记住要使用双引号。</p><p>awk 内置 NR 变量表示每一行的行号</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">awk <span class="string">'&#123;print NR "\t" $0&#125;'</span> fruit.txt</span><br><span class="line"></span><br><span class="line">1   peach    100   Mar  1997   China</span><br><span class="line">2   Lemon    150   Jan  1986   America</span><br><span class="line">3   Pear     240   Mar  1990   Janpan</span><br><span class="line">4   avocado  120   Feb  2008   china</span><br></pre></td></tr></table></figure><p>awk 内置 <code>NF</code> 变量表示每一行的列数</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">awk <span class="string">'&#123;print NF "\t" $0&#125;'</span> fruit.txt</span><br><span class="line"></span><br><span class="line">5   peach    100   Mar  1997   China</span><br><span class="line">5   Lemon    150   Jan  1986   America</span><br><span class="line">5   Pear     240   Mar  1990   Janpan</span><br><span class="line">5   avocado  120   Feb  2008   china</span><br></pre></td></tr></table></figure><p>awk 中 <code>$NF</code> 变量的使用</p><pre><code>awk &apos;{print $NF}&apos; fruit.txt</code></pre><p>上面这个 <code>$NF</code> 就表示每一行的最后一列，因为 <code>NF</code> 表示一行的总列数，在这个文件里表示有 5 列，然后在其前面加上 <code>$</code> 符号，就变成了 <code>$5</code> ，表示第 5 列</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">awk <span class="string">'&#123;print $(NF - 1)&#125;'</span> fruit.txt</span><br><span class="line"></span><br><span class="line">1997</span><br><span class="line">1986</span><br><span class="line">1990</span><br><span class="line">2008</span><br></pre></td></tr></table></figure><p>上面 <code>$(NF-1)</code> 表示倒数第 2 列， <code>$(NF-2)</code> 表示倒数第 3 列，依次类推。</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">awk <span class="string">'NR % 6'</span>        <span class="comment"># 打印出了 6 倍数行之外的其他行</span></span><br><span class="line">awk <span class="string">'NR &gt; 5'</span>        <span class="comment"># 打印第 5 行之后内容，类似 `tail -n +6` 或者 `sed '1,5d'`</span></span><br><span class="line">awk <span class="string">'NF &gt;= 6'</span>       <span class="comment"># 打印大于等于 6 列的行</span></span><br><span class="line">awk <span class="string">'/foo/ &amp;&amp; /bar/'</span>    <span class="comment"># 打印匹配 `/foo/` 和 `/bar/` 的行</span></span><br><span class="line">awk <span class="string">'/foo/ &amp;&amp; !/bar/'</span>   <span class="comment"># 打印包含 `/foo/` 不包含 `/bar/` 的行</span></span><br><span class="line">awk <span class="string">'/foo/ || /bar/'</span>    <span class="comment"># 或</span></span><br><span class="line">awk <span class="string">'/foo/,/bar/'</span>       <span class="comment"># 打印从匹配 `/foo/` 开始的行到 `/bar/` 的行，包含这两行</span></span><br></pre></td></tr></table></figure><h3 id="awk-内置函数"><a href="#awk-内置函数" class="headerlink" title="awk 内置函数"></a>awk 内置函数</h3><p>awk 还提供了一些内置函数，比如</p><ul><li><code>toupper()</code> 用于将字符转为大写</li><li><code>tolower()</code> 将字符转为小写</li><li><code>length()</code> 长度</li><li><code>substr()</code> 子字符串</li><li><code>sin()</code> 正弦</li><li><code>cos()</code> 余弦</li><li><code>sqrt()</code> 平方根</li><li><code>rand()</code> 随机数</li></ul><p>更多的方法可以参考 <code>man awk</code></p><h3 id="awk-同时处理多个文件"><a href="#awk-同时处理多个文件" class="headerlink" title="awk 同时处理多个文件"></a>awk 同时处理多个文件</h3><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">awk <span class="string">'&#123;print FILENAME "\t" $0&#125;'</span> demo1.txt demo2.txt</span><br></pre></td></tr></table></figure><p>当你使用 awk 同时处理多个文件的时候，它会将多个文件合并处理，变量<code>FILENAME</code> 就表示当前文本行所在的文件名称。</p><h3 id="BEGIN-关键字的使用"><a href="#BEGIN-关键字的使用" class="headerlink" title="BEGIN 关键字的使用"></a>BEGIN 关键字的使用</h3><p>在脚本代码段前面使用 BEGIN 关键字时，它会在开始读取一个文件之前，运行一次 <code>BEGIN</code>关键字后面的脚本代码段， BEGIN 后面的脚本代码段只会执行一次，执行完之后 awk 程序就会退出</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">awk <span class="string">'BEGIN &#123;print "Start read file"&#125;'</span> /etc/passwd</span><br></pre></td></tr></table></figure><p>awk 脚本中可以用多个花括号来执行多个脚本代码，就像下面这样</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">awk <span class="string">'BEGIN &#123;print "Start read file"&#125; &#123;print $0&#125;'</span> /etc/passwd</span><br></pre></td></tr></table></figure><h3 id="END-关键字使用方法"><a href="#END-关键字使用方法" class="headerlink" title="END 关键字使用方法"></a>END 关键字使用方法</h3><p>awk 的 END 指令和 BEGIN 恰好相反，在 awk 读取并且处理完文件的所有内容行之后，才会执行<code>END</code>后面的脚本代码段</p><pre><code>awk &apos;END {print &quot;End file&quot;}&apos; /etc/passwdawk &apos;BEGIN {print &quot;Start read file&quot;} {print $0} END {print &quot;End file&quot;}&apos; /etc/passwd</code></pre><h3 id="在-awk-中使用变量"><a href="#在-awk-中使用变量" class="headerlink" title="在 awk 中使用变量"></a>在 awk 中使用变量</h3><p>可以在 awk 脚本中声明和使用变量</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">awk <span class="string">'&#123;msg="hello world"; print msg&#125;'</span> /etc/passwd</span><br></pre></td></tr></table></figure><p>awk 声明的变量可以在任何多个花括号脚本中使用</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">awk <span class="string">'BEGIN &#123;msg="hello world"&#125; &#123;print msg&#125;'</span> /etc/passwd</span><br></pre></td></tr></table></figure><p>在 awk 中使用数学运算，在 awk 中，像其他编程语言一样，它也支持一些基本的数学运算操作</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">awk <span class="string">'&#123;a = 12; b = 24; print a + b&#125;'</span> company.txt</span><br></pre></td></tr></table></figure><p>上面这段脚本表示，先声明两个变量 a = 12 和 b = 24，然后用 print 打印出 a 加上 b 的结果。</p><p>请记住 awk 是针对文件的每一行来执行一次单引号 里面的脚本代码，每读取到一行就会执行一次，文件里面有多少行就会执行多少次，但 BEGIN 和 END 关键字后脚本代码除外，如果被处理的文件中什么都没有，那 awk 就一次都不会执行。</p><p>awk 还支持其他的数学运算符</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">+ 加法运算符</span><br><span class="line">- 减法运算符</span><br><span class="line">* 乘法运算符</span><br><span class="line">/ 除法运算符</span><br><span class="line">% 取余运算符</span><br></pre></td></tr></table></figure><p>在 awk 中使用条件判断</p><p>比如有一个文件 company.txt 内容如下</p><pre><code>yahoo   100 4500google  150 7500apple   180 8000twitter 120 5000</code></pre><p>如果要判断文件的第 3 列数据，也就是平均工资小于 5500 的公司，然后将其打印输出</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">awk <span class="string">'$3 &lt; 5500 &#123;print $0&#125;'</span> company.txt</span><br></pre></td></tr></table></figure><p>上面的命令结果就是平均工资小于 5500 的公司名单，<code>$3 &lt; 5500</code> 表示当第 3 列字段的内容小于 5500 的时候才会执行后面的 {print $0} 代码块</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">awk <span class="string">'$1 == "yahoo" &#123;print $0&#125;'</span> company.txt</span><br></pre></td></tr></table></figure><p>awk 还有一些其他的条件操作符如下</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">&lt;</td><td style="text-align:left">小于</td></tr><tr><td style="text-align:left">&lt;=</td><td style="text-align:left">小于或等于</td></tr><tr><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></tr><tr><td style="text-align:left">&gt;</td><td style="text-align:left">大于</td></tr><tr><td style="text-align:left">&gt;=</td><td style="text-align:left">大于或等于</td></tr><tr><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></tr></tbody></table><p>使用 if 指令判断来实现上面同样的效果</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">awk <span class="string">'&#123;if ($3 &lt; 5500) print $0&#125;'</span> company.txt</span><br></pre></td></tr></table></figure><p>上面表示如果第 3 列字段小于 5500 的时候就会执行后面的 print $0</p><h3 id="在-awk-中使用正则表达式"><a href="#在-awk-中使用正则表达式" class="headerlink" title="在 awk 中使用正则表达式"></a>在 awk 中使用正则表达式</h3><p>比如现在我们有这么一个文件 poetry.txt 内容如下：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">This above all: to thine self be <span class="literal">true</span></span><br><span class="line">There is nothing either good or bad, but thinking makes it so</span><br><span class="line">There’s a special providence <span class="keyword">in</span> the fall of a sparrow</span><br><span class="line">No matter how dark long, may eventually <span class="keyword">in</span> the day arrival</span><br></pre></td></tr></table></figure><p>使用正则表达式匹配字符串 “There” ，将包含这个字符串的行打印并输出</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">awk <span class="string">'/There/&#123;print $0&#125;'</span> poetry.txt</span><br><span class="line"></span><br><span class="line">There is nothing either good or bad, but thinking makes it so</span><br><span class="line">There’s a special providence <span class="keyword">in</span> the fall of a sparrow</span><br></pre></td></tr></table></figure><p>使用正则表达式配一个包含字母 t 和字母 e ，并且 t 和 e 中间只能有任意单个字符的行</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">awk <span class="string">'/t.e/&#123;print $0&#125;'</span> poetry.txt</span><br><span class="line"></span><br><span class="line">There is nothing either good or bad, but thinking makes it so</span><br><span class="line">There’s a special providence <span class="keyword">in</span> the fall of a sparrow</span><br><span class="line">No matter how dark long, may eventually <span class="keyword">in</span> the day arrival</span><br></pre></td></tr></table></figure><p>如果只想匹配单纯的字符串 “t.e”， 那正则表达式就是这样的 /t.e/ ，用反斜杠来转义 . 符号 因为 . 在正则表达式里面表示任意单个字符。</p><p>使用正则表达式来匹配所有以 “The” 字符串开头的行</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">awk <span class="string">'/^The/&#123;print $0&#125;'</span> poetry.txt</span><br></pre></td></tr></table></figure><p>在正则表达式中 <code>^</code>表示以某某字符或者字符串开头。</p><p>使用正则表达式来匹配所有以 “true” 字符串结尾的行</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">awk <span class="string">'/true$/&#123;print $0&#125;'</span> poetry.txt</span><br></pre></td></tr></table></figure><p>在正则表达式中 $ 表示以某某字符或者字符串结尾。</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">awk <span class="string">'/m[a]t/&#123;print $0&#125;'</span> poetry.txt</span><br><span class="line"></span><br><span class="line">No matter how dark long, may eventually <span class="keyword">in</span> the day arrival</span><br></pre></td></tr></table></figure><p>上面这个正则表达式 <code>/m[a]t/</code> 表示匹配包含字符 m ，然后接着后面包含中间方括号中表示的单个字符 a ，最后包含字符 t 的行，输出结果中只有单词 “matter” 符合这个正则表达式的匹配。因为正则表达式 [a] 方括号中表示匹配里面的任意单个字符。</p><p>继续上面的一个新例子如下</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">awk <span class="string">'/^Th[ie]/&#123;print $0&#125;'</span> poetry.txt</span><br></pre></td></tr></table></figure><p>这个例子中的正则表达式 /^Th[ie]/ 表示匹配以字符串 “Thi” 或者 “The” 开头的行，正则表达式方括号中表示匹配其中的任意单个字符。</p><p>再继续上面的新的用法</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">awk <span class="string">'/s[a-z]/&#123;print $0&#125;'</span> poetry.txt</span><br></pre></td></tr></table></figure><p>正则表达式 <code>/s[a-z]/</code> 表示匹配包含字符 s 然后后面跟着任意 a 到 z 之间的单个字符的字符串，比如 “se”, “so”, “sp” 等等。</p><p>正则表达式 [] 方括号中还有一些其他用法比如下面这些</p><pre><code>[a-zA-Z]  表示匹配小写的 a 到 z 之间的单个字符，或者大写的 A 到 Z 之间的单个字符[^a-z]    符号 `^` 在方括号里面表示取反，也就是非的意思，表示匹配任何非 a 到 z 之间的单个字符</code></pre><p>正则表达式中的星号 <code>*</code> 和加号 <code>+</code> 的使用方法，<code>*</code> 表示匹配星号前字符串 0 次或者多次，<code>+</code> 和星号原理差不多，只是加号表示任意 1 个或者 1 个以上，也就是必须至少要出现一次。</p><p>正则表达式问号 ? 的使用方法，正则中的问号 <code>?</code> 表示它前面的字符只能出现 0 次 或者 1 次。</p><p>正则表达式中的 {} 花括号用法，花括号 {} 表示规定它前面的字符必须出现的次数，像这个 /go{2}d/ 就表示只匹配字符串 “good”，也就是中间的字母 “o” 必须要出现 2 次。</p><p>正则表达式中的花括号还有一些其他的用法如下</p><pre><code>/go{2,10}d/   表示字母 &quot;o&quot; 只能可以出现 2 次，3 次，4 次，5 次，6 次 ... 一直到 10 次/go{2,}d/     表示字母 &quot;o&quot; 必须至少出现 2 次或着 2 次以上</code></pre><p>正则表达式中的圆括号表示将多个字符当成一个完整的对象来看待。比如 /th(in){1}king/ 就表示其中字符串 “in” 必须出现 1 次。而如果不加圆括号就变成了 /thin{1}king/ 这个就表示其中字符 “n” 必须出现 1 次。</p><h3 id="一些组合使用"><a href="#一些组合使用" class="headerlink" title="一些组合使用"></a>一些组合使用</h3><p>使用 awk 过滤 history 输出，找到最常用的命令<br><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">history</span> | awk <span class="string">'&#123;a[$2]++&#125;END&#123;for(i in a)&#123;print a[i] " " i&#125;&#125;'</span> | sort -rn | head</span><br></pre></td></tr></table></figure></p><p>过滤文件中重复行<br><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">awk <span class="string">'!x[$0]++'</span> &lt;file&gt;</span><br></pre></td></tr></table></figure></p><p>将一行长度超过 72 字符的行打印<br><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">awk <span class="string">'length&gt;72'</span> file</span><br></pre></td></tr></table></figure></p><p>查看最近哪些用户使用系统<br><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">last | grep -v <span class="string">"^$"</span> | awk <span class="string">'&#123; print $1 &#125;'</span> | sort -nr | uniq -c</span><br></pre></td></tr></table></figure></p><p>假设有一个文本，每一行都是一个 int 数值，想要计算这个文件每一行的和，可以使用<br><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">awk <span class="string">'&#123;s+=$1&#125; ENG &#123;printf "%.0f", s&#125;'</span> /path/to/file</span><br></pre></td></tr></table></figure></p><h2 id="reference"><a href="#reference" class="headerlink" title="reference"></a>reference</h2><blockquote><ul><li><a href="http://www.ruanyifeng.com/blog/2018/11/awk.html" target="_blank" rel="noopener">http://www.ruanyifeng.com/blog/2018/11/awk.html</a></li></ul></blockquote>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;awk是一个强大的文本分析工具，相对于grep的查找，sed的编辑，awk在其对数据分析并生成报告时，显得尤为强大。简单来说awk就是把文件逐行的读入，以空格为默认分隔符将每行切片，切开的部分再进行各种分析处理。&lt;br&gt;
    
    </summary>
    
      <category term="linux" scheme="https://www.elietio.xyz/categories/linux/"/>
    
      <category term="shell" scheme="https://www.elietio.xyz/categories/linux/shell/"/>
    
    
      <category term="linux" scheme="https://www.elietio.xyz/tags/linux/"/>
    
      <category term="shell" scheme="https://www.elietio.xyz/tags/shell/"/>
    
      <category term="awk" scheme="https://www.elietio.xyz/tags/awk/"/>
    
  </entry>
  
  <entry>
    <title>对于含有泛型的类,如何在类中获取泛型的class对象？</title>
    <link href="https://www.elietio.xyz/posts/eb59d595.html"/>
    <id>https://www.elietio.xyz/posts/eb59d595.html</id>
    <published>2018-05-04T16:00:00.000Z</published>
    <updated>2020-06-13T08:18:25.216Z</updated>
    
    <content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/assets/css/APlayer.min.css"><script src="/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><script class="meting-secondary-script-marker" src="/assets/js/Meting.min.js"></script><p>对于含有泛型的类,如何在类中获取泛型的class对象？</p><p>如果子类继承该类的时候传递了泛型，所以编译期该类的泛型其实已经指定了。那么在类中定义一个Class对象，然后通过构造代码块，this指向的是当前调用的子类</p><a id="more"></a><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> Class&lt;T&gt; clazz;</span><br><span class="line">&#123;</span><br><span class="line">   <span class="comment">// 获取当前类上的泛型(this指向当前子类)</span></span><br><span class="line">   ParameterizedType type =(ParameterizedType)<span class="keyword">this</span>.getClass().getGenericSuperclass();</span><br><span class="line">   clazz = (Class&lt;T&gt;) type.getActualTypeArguments()[<span class="number">0</span>];</span><br><span class="line"> &#125;</span><br></pre></td></tr></table></figure><p>对于没有继承该类的子类，可以采用new的时候有参构造函数传递</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> Class&lt;T&gt; clazz;</span><br><span class="line"><span class="comment">//通过构造函数指定元素类型</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="title">classname</span><span class="params">(Class&lt;T&gt; clazz)</span> </span>&#123;</span><br><span class="line">      <span class="keyword">super</span>();</span><br><span class="line">      <span class="keyword">this</span>.clazz = clazz;</span><br><span class="line"> &#125;</span><br></pre></td></tr></table></figure>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;对于含有泛型的类,如何在类中获取泛型的class对象？&lt;/p&gt;
&lt;p&gt;如果子类继承该类的时候传递了泛型，所以编译期该类的泛型其实已经指定了。那么在类中定义一个Class对象，然后通过构造代码块，this指向的是当前调用的子类&lt;/p&gt;
    
    </summary>
    
      <category term="Java" scheme="https://www.elietio.xyz/categories/Java/"/>
    
    
      <category term="Java" scheme="https://www.elietio.xyz/tags/Java/"/>
    
  </entry>
  
</feed>
