<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Raye Blog</title>
  
  <subtitle>你必须去做，你不得不做的事</subtitle>
  <link href="https://raye.wang/atom.xml" rel="self"/>
  
  <link href="https://raye.wang/"/>
  <updated>2026-03-10T06:59:04.571Z</updated>
  <id>https://raye.wang/</id>
  
  <author>
    <name>Raye Wang</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>ubuntu通过源码编译的方式升级openssh版本到9.9p2</title>
    <link href="https://raye.wang/2025/04/01/ubuntu%E9%80%9A%E8%BF%87%E6%BA%90%E7%A0%81%E7%BC%96%E8%AF%91%E7%9A%84%E6%96%B9%E5%BC%8F%E5%8D%87%E7%BA%A7openssh%E7%89%88%E6%9C%AC%E5%88%B09.9p2/"/>
    <id>https://raye.wang/2025/04/01/ubuntu%E9%80%9A%E8%BF%87%E6%BA%90%E7%A0%81%E7%BC%96%E8%AF%91%E7%9A%84%E6%96%B9%E5%BC%8F%E5%8D%87%E7%BA%A7openssh%E7%89%88%E6%9C%AC%E5%88%B09.9p2/</id>
    <published>2025-03-31T16:00:00.000Z</published>
    <updated>2026-03-10T06:59:04.571Z</updated>
    
    <content type="html"><![CDATA[<h4 id="确认源文件目录"><a href="#确认源文件目录" class="headerlink" title="确认源文件目录"></a>确认源文件目录</h4><p>文中所使用的源文件目录使用的是&#x2F;data&#x2F;software，到时候真实更新时所有&#x2F;data&#x2F;software开头的目录需要切换到自己的真实目录,并且文中所有解压源码操作默认是在&#x2F;data&#x2F;software目录下操作</p><h4 id="下载安装包"><a href="#下载安装包" class="headerlink" title="下载安装包"></a>下载安装包</h4><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></pre></td><td class="code"><pre><span class="line">cd /data/software</span><br><span class="line">wget https://www.zlib.net/zlib-1.3.1.tar.gz</span><br><span class="line">wget https://www.openssl.org/source/openssl-3.2.1.tar.gz</span><br><span class="line">wget https://mirrors.aliyun.com/pub/OpenBSD/OpenSSH/portable/openssh-9.9p2.tar.gz</span><br></pre></td></tr></table></figure><h4 id="备份旧版本以及相关配置"><a href="#备份旧版本以及相关配置" class="headerlink" title="备份旧版本以及相关配置"></a>备份旧版本以及相关配置</h4><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></pre></td><td class="code"><pre><span class="line">sudo cp -rf /etc/ssh /etc/ssh.old # 备份旧的ssh配置文件</span><br><span class="line">sudo cp -rf /usr/bin/openssl /usr/bin/openssl.old # 备份旧版本openssl</span><br><span class="line">sudo cp -rf /etc/pam.d /etc/pam.d.old # 备份旧的pam模块配置</span><br></pre></td></tr></table></figure><h4 id="安装zlib模块"><a href="#安装zlib模块" class="headerlink" title="安装zlib模块"></a>安装zlib模块</h4><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></pre></td><td class="code"><pre><span class="line">tar -zxvf zlib-1.3.1.tar.gz # 解压zlib模块源码</span><br><span class="line">cd zlib-1.3.1/</span><br><span class="line">./configure --prefix=/data/software/zlib # 配置编译 以及安装目录</span><br><span class="line">make &amp;&amp; make install # 编译安装</span><br></pre></td></tr></table></figure><h4 id="安装openssl"><a href="#安装openssl" class="headerlink" title="安装openssl"></a>安装openssl</h4><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></pre></td><td class="code"><pre><span class="line">tar -zxvf openssl-3.2.1.tar.gz # 解压openssl</span><br><span class="line">cd openssl-3.2.1/</span><br><span class="line">./config --prefix=/data/software/openssl # 配置编译 以及安装目录</span><br><span class="line">make -j  &amp;&amp; make install # 编译安装，这里编译时间比较长，建议根据自己服务器实际配置设置编译并行的任务数</span><br><span class="line">sudo mv /usr/bin/openssl /usr/bin/openssl.old # 备份并删除旧版的openssl</span><br><span class="line">sudo ln -s /data/software/openssl/bin/openssl /usr/bin/openssl #创建新版本的链接</span><br><span class="line">sudo ln -s /data/software/openssl/lib64/libssl.so.3 /usr/lib/libssl.so.3 # 如果系统有/usr/lib64需要放这个目录下</span><br><span class="line">sudo ln -s /data/software/openssl/lib64/libcrypto.so.3 /usr/lib/libcrypto.so.3 # 如果系统有/usr/lib64需要放这个目录下</span><br></pre></td></tr></table></figure><h4 id="安装openssh"><a href="#安装openssh" class="headerlink" title="安装openssh"></a>安装openssh</h4><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></pre></td><td class="code"><pre><span class="line">tar -zxvf openssh-9.9p2.tar.gz #解压openssh</span><br><span class="line">cd openssh-9.9p2/</span><br><span class="line">./configure --prefix=/data/software/openssh --with-zlib=/data/software/zlib --with-ssl-dir=/data/software/openssl --with-pam   --sysconfdir=/etc/ssh --mandir=/usr/share/man # 配置编译 以及安装目录</span><br><span class="line">sudo rm -rf /etc/ssh # 删除旧版配置文件，前面已经备份过了</span><br><span class="line">make &amp;&amp; make install #编译安装</span><br><span class="line">sudo mv /usr/bin/ssh /usr/bin/ssh.old # 备份旧版</span><br><span class="line">sudo mv /usr/sbin/sshd /usr/sbin/sshd.old # 备份旧版</span><br><span class="line">sudo vi /etc/ssh/sshd_config # 根据实际情况配置调整配置文件，如果需要root登录，一定要配置PermitRootLogin yes</span><br><span class="line">sudo ln -s /data/software/openssh/bin/ssh /usr/bin/ssh # 创建符号链接</span><br><span class="line">sudo ln -s /data/software/openssh/sbin/sshd /usr/sbin/sshd # 创建符号链接</span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">重载配置并启用服务</span></span><br><span class="line">sudo systemctl daemon-reload</span><br><span class="line">sudo systemctl enable sshd # 这里可能会抛Failed to enable unit: Refusing to operate on linked unit file sshd.service，这个不用管，是因为之前的旧版本有，需要覆盖</span><br><span class="line">sudo systemctl restart sshd</span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">重新创建ssh链接</span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">查看版本</span></span><br><span class="line">ssh -V</span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">正常应该输出  OpenSSH_9.9p2, OpenSSL 3.2.1</span></span><br></pre></td></tr></table></figure>]]></content>
    
    
      
      
    <summary type="html">&lt;h4 id=&quot;确认源文件目录&quot;&gt;&lt;a href=&quot;#确认源文件目录&quot; class=&quot;headerlink&quot; title=&quot;确认源文件目录&quot;&gt;&lt;/a&gt;确认源文件目录&lt;/h4&gt;&lt;p&gt;文中所使用的源文件目录使用的是&amp;#x2F;data&amp;#x2F;software，到时候真实更新时所有</summary>
      
    
    
    
    
    <category term="Linux" scheme="https://raye.wang/tags/Linux/"/>
    
  </entry>
  
  <entry>
    <title>KafkaStream Local Store和Global Store区别和用法</title>
    <link href="https://raye.wang/2024/06/04/KafkaStream%20State%20Store%E5%92%8CGlobal%20Store%E7%94%A8%E6%B3%95%E5%92%8C%E5%8C%BA%E5%88%AB/"/>
    <id>https://raye.wang/2024/06/04/KafkaStream%20State%20Store%E5%92%8CGlobal%20Store%E7%94%A8%E6%B3%95%E5%92%8C%E5%8C%BA%E5%88%AB/</id>
    <published>2024-06-04T14:00:16.000Z</published>
    <updated>2026-03-10T06:59:04.570Z</updated>
    
    <content type="html"><![CDATA[<h4 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h4><p>使用kafkaStream进行流式计算时，如果需要对数据进行状态处理，那么常用的会遇到kafkaStream的store，而store也有Local Store以及Global Store，当然也可以使用其他方案的来进行状态保存，文本主要理清楚kafkaStream中的Local Store以及Global Store之间的区别和用法，以及什么时候选择何种store和当store无法满足我们需求时，应该如何使用其他方案来进行数据的状态保存</p><p>本文所有方法和代码皆只针对kafka-streams的3.7.0版本，pom如下：</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></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>org.apache.kafka<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>kafka-streams<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">version</span>&gt;</span>3.7.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>由于不同版本的KafkaStream在使用上有较大区别，也因为KafkaStream不同版本API改动较大，所以如果版本不一致，使用方法甚至是一些核心概念都会跟本文讲述有所出入，并且KafkaStream由于相对小众，文档也很少，官网的文档也只是一些简单介绍，所以需要注意避坑</p><h4 id="Local-Store和Global-Store的共同点和区别点"><a href="#Local-Store和Global-Store的共同点和区别点" class="headerlink" title="Local Store和Global Store的共同点和区别点"></a>Local Store和Global Store的共同点和区别点</h4><h5 id="共同点："><a href="#共同点：" class="headerlink" title="共同点："></a>共同点：</h5><p>1、都是用于流式计算中进行状态存储的</p><p>2、具体结构类似，使用的都是如：KeyValueStore，SessionStore等类</p><p>3、实际机制类似，会通过内存、本地目录和kafka Topic的变更记录等方式来进行缓存数据更新和恢复</p><h5 id="不同点"><a href="#不同点" class="headerlink" title="不同点"></a>不同点</h5><p>1、适用场景不同</p><p><strong>Local Store</strong>  适合用于单个实例的状态管理，适合处理单个分区的数据，并且缓存数据不会多个实例共享</p><p><strong>Global Store</strong>  适用于跨实例共享数据状态，多个实例通过Topic中的更新记录来跟新进程中的数据</p><p>2、使用方法不同</p><p><strong>Local Store</strong>  可以直接在代码中调用对应类型存储（如：KeyValueStore）的put方法进行更新数据，不需要考虑数据一致性（因为可见性只有单个实例）</p><p><strong>Global Store</strong>  不能直接调用对应的put和delete方法，所有更新和删除缓存都需要通过发送数据到Global 配置的topic中，然后自行实现Topic数据消费者（实现：org.apache.kafka.streams.processor.api.Processor类），在消费者类中进行数据更新等操作，同时因为需要自己实现更新实例中的数据逻辑，数据一致性也需要开发者自行处理，虽然正常来说利用Kafka本身的特性很少出现数据一致性问题，但是如果多实例之间性能差异和网络环境等差异，容易将数据不一致的时长延长，如果要求Store一致性强且容忍数据不一致时限短，则需要注意考虑Store更新数据消费者的处理能力</p><p>3、扩展性</p><p><strong>Local Store</strong>：可以通过增加输入主题的分区数来扩展处理能力，但每个实例仍然独立运行。</p><p><strong>Global Store</strong>：需要在多个实例之间共享状态，因此在设计时需要考虑如何高效地管理和同步状态。</p><h4 id="常见的Store-类型"><a href="#常见的Store-类型" class="headerlink" title="常见的Store 类型"></a>常见的Store 类型</h4><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">org.apache.kafka.streams.state.KeyValueStore</span><br><span class="line">org.apache.kafka.streams.state.SessionStore</span><br><span class="line">org.apache.kafka.streams.state.TimestampedKeyValueStore</span><br><span class="line">org.apache.kafka.streams.state.VersionedKeyValueStore</span><br><span class="line">org.apache.kafka.streams.state.WindowStore</span><br></pre></td></tr></table></figure><p>需要根据实际使用场景选择合适的状态存储类</p><h4 id="用法"><a href="#用法" class="headerlink" title="用法"></a>用法</h4><h5 id="Local-Store"><a href="#Local-Store" class="headerlink" title="Local Store"></a>Local Store</h5><p>第一步，先生成对应类型的StoreBuilder对象，如我需要用KeyValueStore,然后状态存储的名字是：testLocalStore（这个名字不能重复，因为会根据消费者id加储存名称创建对应的Topic，当然如果是不同的KafkaStream程序，消费者id不一致，那么重复就没有关系了）,因为是KeyValue类型的储存，所以需要设定对应的Key和Value数据的序列化对象，具体代码如下：</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">StoreBuilder&lt;KeyValueStore&lt;String, String&gt;&gt; kvBuilder = Stores.keyValueStoreBuilder(Stores.persistentKeyValueStore(<span class="string">&quot;testLocalStore&quot;</span>), Serdes.String(), Serdes.String());</span><br></pre></td></tr></table></figure><p>其中Stores.persistentKeyValueStore代表的我得存储是持久化的，正常都是会用持久化，当然也有存储一些不重要或者程序重启丢失也无所谓的状态数据，可以使用Stores.inMemoryKeyValueStore以及基于LRU淘汰机制的储存Stores.lruMap，第二个参数Serdes.String()代表存储数据的key是字符串，第三个参数同理，如果是要存储一些对象，也可以使用自定义的序列化类，实现</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">org.apache.kafka.common.serialization.Serializer</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></pre></td><td class="code"><pre><span class="line">org.apache.kafka.common.serialization.Deserializer</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">new</span> <span class="title class_">Serdes</span>.WrapperSerde&lt;&gt;(<span class="keyword">new</span> <span class="title class_">KryoSerializer</span>&lt;&gt;(TestStoreBean.class),</span><br><span class="line">                <span class="keyword">new</span> <span class="title class_">KryoDeserializer</span>&lt;&gt;(TestStoreBean.class)</span><br></pre></td></tr></table></figure><p>其中KryoSerializer和KryoDeserializer是我自定义的使用Kryo序列化Java对象的类，TestStoreBean是我保存的状态的数据封装bean</p><p>KryoSerializer代码如下：</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> com.esotericsoftware.kryo.Kryo;</span><br><span class="line"><span class="keyword">import</span> com.esotericsoftware.kryo.io.Output;</span><br><span class="line"><span class="keyword">import</span> org.apache.kafka.common.serialization.Serializer;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> java.io.ByteArrayOutputStream;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * kryo序列化类</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@author</span> Raye</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@since</span> 2024-6-4</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">KryoSerializer</span>&lt;T&gt; <span class="keyword">implements</span> <span class="title class_">Serializer</span>&lt;T&gt; &#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> ThreadLocal&lt;Kryo&gt; KRYO_LOCAL = <span class="keyword">new</span> <span class="title class_">ThreadLocal</span>&lt;Kryo&gt;() &#123;</span><br><span class="line">        <span class="meta">@Override</span></span><br><span class="line">        <span class="keyword">protected</span> Kryo <span class="title function_">initialValue</span><span class="params">()</span> &#123;</span><br><span class="line">            <span class="type">Kryo</span> <span class="variable">kryo</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Kryo</span>();</span><br><span class="line"></span><br><span class="line">            <span class="comment">/**</span></span><br><span class="line"><span class="comment">             * 不要轻易改变这里的配置！更改之后，序列化的格式就会发生变化，</span></span><br><span class="line"><span class="comment">             * 上线的同时就必须清除 Redis 里的所有缓存，</span></span><br><span class="line"><span class="comment">             * 否则那些缓存再回来反序列化的时候，就会报错</span></span><br><span class="line"><span class="comment">             */</span></span><br><span class="line">            <span class="comment">//支持对象循环引用（否则会栈溢出）</span></span><br><span class="line">            kryo.setReferences(<span class="literal">true</span>); <span class="comment">//默认值就是 true，添加此行的目的是为了提醒维护者，不要改变这个配置</span></span><br><span class="line"></span><br><span class="line">            <span class="comment">//不强制要求注册类（注册行为无法保证多个 JVM 内同一个类的注册编号相同；而且业务系统中大量的 Class 也难以一一注册）</span></span><br><span class="line">            kryo.setRegistrationRequired(<span class="literal">false</span>); <span class="comment">//默认值就是 false，添加此行的目的是为了提醒维护者，不要改变这个配置</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">            <span class="keyword">return</span> kryo;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;;</span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 获得当前线程的 Kryo 实例</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span> 当前线程的 Kryo 实例</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> Kryo <span class="title function_">getInstance</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> KRYO_LOCAL.get();</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">private</span> Class&lt;T&gt; clz;</span><br><span class="line">    <span class="keyword">public</span> <span class="title function_">KryoSerializer</span><span class="params">(Class&lt;T&gt; clz)</span> &#123;</span><br><span class="line">        <span class="built_in">this</span>.clz = clz;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="type">byte</span>[] serialize(String s, T t) &#123;</span><br><span class="line">        <span class="keyword">if</span>(t == <span class="literal">null</span>)&#123;</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="type">ByteArrayOutputStream</span> <span class="variable">byteArrayOutputStream</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ByteArrayOutputStream</span>();</span><br><span class="line">        <span class="type">Output</span> <span class="variable">output</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Output</span>(byteArrayOutputStream);</span><br><span class="line"></span><br><span class="line">        <span class="type">Kryo</span> <span class="variable">kryo</span> <span class="operator">=</span> getInstance();</span><br><span class="line">        kryo.writeObjectOrNull(output, t,clz);</span><br><span class="line">        output.flush();</span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> byteArrayOutputStream.toByteArray();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>KryoDeserializer代码如下：</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> com.esotericsoftware.kryo.Kryo;</span><br><span class="line"><span class="keyword">import</span> com.esotericsoftware.kryo.io.Input;</span><br><span class="line"><span class="keyword">import</span> org.apache.kafka.common.serialization.Deserializer;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> java.io.ByteArrayInputStream;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * kryo反序列化类</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@author</span> Raye</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@since</span> 2024-6-4</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">KryoDeserializer</span>&lt;T&gt; <span class="keyword">implements</span> <span class="title class_">Deserializer</span>&lt;T&gt; &#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> ThreadLocal&lt;Kryo&gt; KRYO_LOCAL = <span class="keyword">new</span> <span class="title class_">ThreadLocal</span>&lt;Kryo&gt;() &#123;</span><br><span class="line">        <span class="meta">@Override</span></span><br><span class="line">        <span class="keyword">protected</span> Kryo <span class="title function_">initialValue</span><span class="params">()</span> &#123;</span><br><span class="line">            <span class="type">Kryo</span> <span class="variable">kryo</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Kryo</span>();</span><br><span class="line"></span><br><span class="line">            <span class="comment">/**</span></span><br><span class="line"><span class="comment">             * 不要轻易改变这里的配置！更改之后，序列化的格式就会发生变化，</span></span><br><span class="line"><span class="comment">             * 上线的同时就必须清除 Redis 里的所有缓存，</span></span><br><span class="line"><span class="comment">             * 否则那些缓存再回来反序列化的时候，就会报错</span></span><br><span class="line"><span class="comment">             */</span></span><br><span class="line">            <span class="comment">//支持对象循环引用（否则会栈溢出）</span></span><br><span class="line">            kryo.setReferences(<span class="literal">true</span>); <span class="comment">//默认值就是 true，添加此行的目的是为了提醒维护者，不要改变这个配置</span></span><br><span class="line"></span><br><span class="line">            <span class="comment">//不强制要求注册类（注册行为无法保证多个 JVM 内同一个类的注册编号相同；而且业务系统中大量的 Class 也难以一一注册）</span></span><br><span class="line">            kryo.setRegistrationRequired(<span class="literal">false</span>); <span class="comment">//默认值就是 false，添加此行的目的是为了提醒维护者，不要改变这个配置</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">            <span class="keyword">return</span> kryo;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;;</span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 获得当前线程的 Kryo 实例</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span> 当前线程的 Kryo 实例</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> Kryo <span class="title function_">getInstance</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> KRYO_LOCAL.get();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> Class&lt;T&gt; clz;</span><br><span class="line">    <span class="keyword">public</span> <span class="title function_">KryoDeserializer</span><span class="params">(Class&lt;T&gt; clz)</span> &#123;</span><br><span class="line">        <span class="built_in">this</span>.clz = clz;</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="keyword">public</span> T <span class="title function_">deserialize</span><span class="params">(String s, <span class="type">byte</span>[] bytes)</span> &#123;</span><br><span class="line">        <span class="keyword">if</span>(bytes == <span class="literal">null</span>)&#123;</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="type">ByteArrayInputStream</span> <span class="variable">byteArrayInputStream</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ByteArrayInputStream</span>(bytes);</span><br><span class="line">        <span class="type">Input</span> <span class="variable">input</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Input</span>(byteArrayInputStream);</span><br><span class="line"></span><br><span class="line">        <span class="type">Kryo</span> <span class="variable">kryo</span> <span class="operator">=</span> getInstance();</span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            <span class="keyword">return</span> kryo.readObjectOrNull(input, clz);</span><br><span class="line">        &#125;<span class="keyword">catch</span> (Exception e)&#123;</span><br><span class="line">            e.printStackTrace();</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line"></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>同理，使用LocalStore时，可以将代码替换成以下内容：</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">StoreBuilder&lt;KeyValueStore&lt;String, TestStoreBean&gt;&gt; kvBuilder = Stores.keyValueStoreBuilder(Stores.persistentKeyValueStore(<span class="string">&quot;testLocalStore&quot;</span>), Serdes.String(), <span class="keyword">new</span> <span class="title class_">Serdes</span>.WrapperSerde&lt;&gt;(<span class="keyword">new</span> <span class="title class_">KryoSerializer</span>&lt;&gt;(TestStoreBean.class),</span><br><span class="line">                <span class="keyword">new</span> <span class="title class_">KryoDeserializer</span>&lt;&gt;(TestStoreBean.class));</span><br></pre></td></tr></table></figure><p>有了StoreBuilder对象之后，直接在StreamsBuilder对象中添加即可</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">streamsBuilder.addStateStore(kvBuilder);</span><br></pre></td></tr></table></figure><p>需要使用时，先在处理数据的Processor类中的init方法获取对应的状态存储对象</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="built_in">this</span>.testLocalStore = context.getStateStore(<span class="string">&quot;testLocalStore&quot;</span>);</span><br></pre></td></tr></table></figure><p>然后就可以在process方法中调用testLocalStore的get、put、delete等方法操作状态存储数据了，具体代码如下</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></pre></td><td class="code"><pre><span class="line"><span class="meta">@Slf4j</span></span><br><span class="line">   <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">class</span> <span class="title class_">StreamProcessor</span> <span class="keyword">implements</span> <span class="title class_">Processor</span>&lt;String,String,String,String&gt; &#123;</span><br><span class="line"></span><br><span class="line">       <span class="keyword">private</span> KeyValueStore&lt;String,String&gt; testLocalStore;</span><br><span class="line">       <span class="keyword">private</span> ProcessorContext context;</span><br><span class="line">       </span><br><span class="line">       <span class="keyword">private</span> String toTopic;</span><br><span class="line">       <span class="meta">@Override</span></span><br><span class="line">       <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">init</span><span class="params">(ProcessorContext context)</span> &#123;</span><br><span class="line">           <span class="built_in">this</span>.context = context;</span><br><span class="line">           <span class="built_in">this</span>.testLocalStore = context.getStateStore(<span class="string">&quot;testLocalStore&quot;</span>);</span><br><span class="line">       &#125;</span><br><span class="line"></span><br><span class="line">       <span class="keyword">public</span> <span class="title function_">StreamProcessor</span><span class="params">(String toTopic)</span> &#123;</span><br><span class="line">           <span class="built_in">this</span>.toTopic = toTopic;</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="keyword">public</span> <span class="keyword">void</span> <span class="title function_">process</span><span class="params">(Record&lt;String, String&gt; record)</span> &#123;</span><br><span class="line">           testLocalStore.put(<span class="string">&quot;key1&quot;</span>,<span class="string">&quot;testValue1&quot;</span>);</span><br><span class="line">           log.info(<span class="string">&quot;testLocalStore key1 : &#123;&#125;&quot;</span>,testLocalStore.get(<span class="string">&quot;key1&quot;</span>));</span><br><span class="line">           testLocalStore.delete(<span class="string">&quot;key1&quot;</span>);</span><br><span class="line">           context.forward(record,toTopic);</span><br><span class="line">       &#125;</span><br><span class="line">   &#125;</span><br></pre></td></tr></table></figure><p>其中实现的Processor类全称是：org.apache.kafka.streams.processor.api.Processor，上面代码只是在数据处理流程中简单保存了数据，然后获取出来以及删除，没有对流数据做任何处理，就直接发送到输出的topic了</p><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><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Bean</span></span><br><span class="line">   <span class="keyword">public</span> KStream&lt;String,String&gt; <span class="title function_">kStreamTestStore</span><span class="params">(StreamsBuilder streamsBuilder)</span>&#123;</span><br><span class="line">       log.info(<span class="string">&quot;init kStreamTestStore&quot;</span>);</span><br><span class="line">       StoreBuilder&lt;KeyValueStore&lt;String, String&gt;&gt; kvBuilder = Stores.keyValueStoreBuilder(Stores.persistentKeyValueStore(<span class="string">&quot;testLocalStore&quot;</span>), Serdes.String(), Serdes.String());</span><br><span class="line">       streamsBuilder.addStateStore(kvBuilder);</span><br><span class="line">       KStream&lt;String, String&gt; stream = streamsBuilder.stream(fromTopic);</span><br><span class="line">       stream.process(()-&gt;<span class="keyword">new</span> <span class="title class_">StreamProcessor</span>(toTopic), Named.as(fromTopic),<span class="string">&quot;testLocalStore&quot;</span>);</span><br><span class="line">       streamsBuilder.build().addSink(toTopic,toTopic,fromTopic);</span><br><span class="line">       <span class="keyword">return</span> stream;</span><br><span class="line">   &#125;</span><br></pre></td></tr></table></figure><p>注意：由于使用Store需要通过ProcessorContext对象来获取Store对象，所以在KafkaStream常用的一些map,mapValue,flatMapValues这些流式计算方法中是没办法使用的，只能在一些更底层的Api中去使用，如process</p><h5 id="Global-Store"><a href="#Global-Store" class="headerlink" title="Global Store"></a>Global Store</h5><p>同Local Store一样，需要先生成对应类型的StoreBuilder对象，代码跟Local Store一样</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">StoreBuilder&lt;KeyValueStore&lt;String, String&gt;&gt; kvBuilder = Stores.keyValueStoreBuilder(Stores.persistentKeyValueStore(<span class="string">&quot;testGlobalStore&quot;</span>), Serdes.String(), Serdes.String());</span><br></pre></td></tr></table></figure><p>然后定义处理状态更新日志的Processor类，在这个类中，可以对缓存数据进行更新和删除操作（其他地方都是不能直接修改Global Store的）</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">GlobalStoreHandleProcessor</span>&lt;K, V&gt; <span class="keyword">implements</span> <span class="title class_">Processor</span>&lt;K,V,Void,Void&gt; &#123;</span><br><span class="line">    <span class="keyword">private</span> KeyValueStore&lt;K, V&gt; store;</span><br><span class="line">    <span class="keyword">private</span> String storeName;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="title function_">GlobalStoreHandleProcessor</span><span class="params">(String storeName)</span> &#123;</span><br><span class="line">        <span class="built_in">this</span>.storeName = storeName;</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="keyword">public</span> <span class="keyword">void</span> <span class="title function_">process</span><span class="params">(Record&lt;K,V&gt; record)</span> &#123;</span><br><span class="line">        <span class="keyword">if</span>(record == <span class="literal">null</span> || record.value() == <span class="literal">null</span>) &#123;</span><br><span class="line">            <span class="keyword">return</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        store.put(record.key(), record.value());</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="keyword">public</span> <span class="keyword">void</span> <span class="title function_">init</span><span class="params">(ProcessorContext context)</span> &#123;</span><br><span class="line">        <span class="built_in">this</span>.store = context.getStateStore(storeName);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>跟KafkaStream的process是一样的，只需要在process方法中对缓存进行更新或者删除操作即可，我这里只是简单put操作，具体逻辑可以根据自己情况进行处理</p><p>在StreamsBuilder对象中添加StoreBuilder对象</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">streamsBuilder.addGlobalStore(kvBuilder,<span class="string">&quot;testGlobalStore&quot;</span>, Consumed.with(Serdes.String(),Serdes.String()),</span><br><span class="line">                ()-&gt;<span class="keyword">new</span> <span class="title class_">GlobalStoreHandleProcessor</span>&lt;&gt;(<span class="string">&quot;testGlobalStore&quot;</span>));</span><br></pre></td></tr></table></figure><p>其中第二个参数testGlobalStore是Global Store绑定的数据变更记录的Topic，如果要更新，则需要通过向这个topic发送数据来进行更新Global Store中的数据</p><p>处理数据的Processor类实例代码</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">class</span> <span class="title class_">StreamProcessor</span> <span class="keyword">implements</span> <span class="title class_">Processor</span>&lt;String,String,String,String&gt; &#123;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">private</span> KeyValueStore&lt;String,String&gt; testGlobalStore;</span><br><span class="line">        <span class="keyword">private</span> ProcessorContext context;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">private</span> String toTopic;</span><br><span class="line"></span><br><span class="line">        <span class="meta">@Override</span></span><br><span class="line">        <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">init</span><span class="params">(ProcessorContext context)</span> &#123;</span><br><span class="line">            <span class="built_in">this</span>.context = context;</span><br><span class="line">            <span class="built_in">this</span>.testGlobalStore = context.getStateStore(<span class="string">&quot;testGlobalStore&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">public</span> <span class="title function_">StreamProcessor</span><span class="params">(String toTopic)</span> &#123;</span><br><span class="line">            <span class="built_in">this</span>.toTopic = toTopic;</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="keyword">public</span> <span class="keyword">void</span> <span class="title function_">process</span><span class="params">(Record&lt;String, String&gt; record)</span> &#123;</span><br><span class="line">           testLocalStore.put(jsonObject.getString(<span class="string">&quot;key&quot;</span>),jsonObject.getString(<span class="string">&quot;value&quot;</span>));</span><br><span class="line">            log.info(<span class="string">&quot;testLocalStore key1 : &#123;&#125;&quot;</span>,testGlobalStore.get(<span class="string">&quot;key1&quot;</span>));</span><br><span class="line">            <span class="comment">//发送更新Global Store的数据</span></span><br><span class="line">            context.forward(<span class="keyword">new</span> <span class="title class_">Record</span>(<span class="string">&quot;testGlobalKey&quot;</span>,<span class="string">&quot;global value&quot;</span>,record.timestamp()),<span class="string">&quot;testGlobalStore&quot;</span>);</span><br><span class="line">            context.forward(record,toTopic);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure><p>与Local Store不同的是，不能在处理数据流的时候，对缓存进行put操作，只能通过将数据发送到Global Store关联的topic中，在GlobalStoreHandleProcessor中去做更新</p><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><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="meta">@Bean</span></span><br><span class="line">   <span class="keyword">public</span> KStream&lt;String,String&gt; <span class="title function_">kStreamTestStore</span><span class="params">(StreamsBuilder streamsBuilder)</span>&#123;</span><br><span class="line">       log.info(<span class="string">&quot;init kStreamTestStore&quot;</span>);</span><br><span class="line">       StoreBuilder&lt;KeyValueStore&lt;String, String&gt;&gt; kvBuilder = Stores.keyValueStoreBuilder(Stores.persistentKeyValueStore(<span class="string">&quot;testGlobalStore&quot;</span>), Serdes.String(), Serdes.String());</span><br><span class="line">       streamsBuilder.addGlobalStore(kvBuilder,<span class="string">&quot;testGlobalStore&quot;</span>, Consumed.with(Serdes.String(),Serdes.String()),</span><br><span class="line">               ()-&gt;<span class="keyword">new</span> <span class="title class_">GlobalStoreHandleProcessor</span>&lt;&gt;(<span class="string">&quot;testGlobalStore&quot;</span>));</span><br><span class="line">       KStream&lt;String, String&gt; stream = streamsBuilder.stream(fromTopic);</span><br><span class="line">       stream.process(()-&gt;<span class="keyword">new</span> <span class="title class_">StreamProcessor</span>(toTopic), Named.as(fromTopic));</span><br><span class="line">       streamsBuilder.build().addSink(toTopic,toTopic,fromTopic);</span><br><span class="line">       streamsBuilder.build().addSink(<span class="string">&quot;testGlobalStore&quot;</span>,<span class="string">&quot;testGlobalStore&quot;</span>,fromTopic);</span><br><span class="line">       <span class="keyword">return</span> stream;</span><br><span class="line">   &#125;</span><br></pre></td></tr></table></figure><p>与Local Store不同点在于，不需要在process方法中添加store的名字，但是因为要从process方法中直接将更新Store的数据发送到topic，所以需要添加一个Global Store绑定的Topic的输出扩展，也就是下面这行代码</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">streamsBuilder.build().addSink(<span class="string">&quot;testGlobalStore&quot;</span>,<span class="string">&quot;testGlobalStore&quot;</span>,fromTopic);</span><br></pre></td></tr></table></figure><h4 id="不适合的场景"><a href="#不适合的场景" class="headerlink" title="不适合的场景"></a>不适合的场景</h4><p>由于KafkaStream Store 没有自动过期数据和过期数据自动删除的概率（可能是有，但是我没有找到对应文档），所以如果我们存储的key集合特别大，并且需要自动过期和自动删除，那么就不适合使用Store来处理了，因为需要我们自行处理删除逻辑，尤其是有些场景中，并不会对过期的key进行访问，所以采用惰性删除基本上不现实，但是定时删除，因为Store会存储到磁盘，如果存储的key很多，删除对应数据的时候耗时很长，尤其是单次删除大量key的时候，可能会直接超时，并且还必须要自己处理定时删除的逻辑，想要更好的去删除，就需要大量时间去开发和优化。</p><p>虽然使用内存的Store能稍微好点，但是毕竟单个进程内存有限，并且正常流处理中，如果需要保存状态，那么肯定是希望进程重启之后，能恢复数据，避免计算出错的，所以如果是有大量不重复key，并且数据需要到期自动删除的话，可以直接使用Redis做状态存储，并且进过我得实际测试，使用Redis并不比Store慢，并且在key量越来越大的情况下，Redis的性能是完全优于Store的（只针对持久化的Store），当然使用Redis，还是会更使用Global  Store一样，需要考虑数据一致性的问题，不过这个问题可以通过将相同key的数据从Kafka Topic就分配到同一个Topic分区中来避免</p>]]></content>
    
    
      
      
    <summary type="html">&lt;h4 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h4&gt;&lt;p&gt;使用kafkaStream进行流式计算时，如果需要对数据进行状态处理，那么常用的会遇到kafkaStream的store，而store也有L</summary>
      
    
    
    
    
    <category term="KafkaStream" scheme="https://raye.wang/tags/KafkaStream/"/>
    
  </entry>
  
  <entry>
    <title>降低Redis内存使用和提升性能的一些方案</title>
    <link href="https://raye.wang/2024/04/16/%E9%99%8D%E4%BD%8ERedis%E5%86%85%E5%AD%98%E4%BD%BF%E7%94%A8%E5%92%8C%E6%8F%90%E5%8D%87%E6%80%A7%E8%83%BD%E7%9A%84%E4%B8%80%E4%BA%9B%E6%96%B9%E6%A1%88/"/>
    <id>https://raye.wang/2024/04/16/%E9%99%8D%E4%BD%8ERedis%E5%86%85%E5%AD%98%E4%BD%BF%E7%94%A8%E5%92%8C%E6%8F%90%E5%8D%87%E6%80%A7%E8%83%BD%E7%9A%84%E4%B8%80%E4%BA%9B%E6%96%B9%E6%A1%88/</id>
    <published>2024-04-16T03:02:50.000Z</published>
    <updated>2026-03-10T06:59:04.572Z</updated>
    
    <content type="html"><![CDATA[<h4 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h4><h4 id="一、前言"><a href="#一、前言" class="headerlink" title="一、前言"></a>一、前言</h4><p>Redis在现在开发中已经成为了一个不可或缺的组件，很多项目都会依赖Redis进行开发，当数据量和请求量以及Redis本身访问率不高的情况下，Redis不会成为性能瓶颈，但是如果本身处于高并发海量数据这些情况下，即便是Redis，也会成为性能瓶颈中的一环，本文就是基于Redis已经成为我项目中的一个性能瓶颈之后，深入研究后产生的，另外本文主要是针对开发过程中如何降低Redis内存使用率以及整体提升代码性能，不涉及到Redis集群搭建相关性能调优</p><h4 id="二、降低内存占用"><a href="#二、降低内存占用" class="headerlink" title="二、降低内存占用"></a>二、降低内存占用</h4><h5 id="a-减少key长度"><a href="#a-减少key长度" class="headerlink" title="a)减少key长度"></a>a)减少key长度</h5><p>首先key本身也是存在redis内存中的，每条数据增加2个字节的长度，成百上千万数据加起来，就占用了不少的内存了，所以需要尽可能的缩减key的长度，能保证key唯一性就好，没有必要把一些前缀设置很长或者使用英文全称</p><h5 id="b-合理的缓存有效期"><a href="#b-合理的缓存有效期" class="headerlink" title="b)合理的缓存有效期"></a>b)合理的缓存有效期</h5><p>让缓存在不被使用时尽快过期，避免占用内存，如果有大量缓存在业务中不会被使用，或者缓存从生效到不被使用只有1小时，但是缓存有效期却设置的1天，那么就会导致大量无用缓存占用大量内存，所以一定要合理评估每一个缓存的生命周期。当内存资源明显紧张超过CPU等资源时，可以采取主动删除明确不使用的缓存，或者针对热点周期进行缓存生命周期配置，并配置好合理的恢复缓存机制</p><p><strong>示例：</strong></p><h6 id="1-主动删除缓存"><a href="#1-主动删除缓存" class="headerlink" title="1.主动删除缓存"></a>1.主动删除缓存</h6><p>假如有以下场景：某商城，下单订单时需要先预览订单，并且生成订单逻辑复杂，且订单量大，所以预览订单时会先生成订单缓存，创建订单后，因为某些业务还需要快速访问订单，如发起支付时获取金额信息以及验证等，并且支付有效期有1天，也就是如果没有支付，一天内发起支付的可能性较大，需要快速获取相关信息。 如果用固定有效期1天的话，那么其实在订单支付之后，访问订单详情概率变很低，也就是说一旦订单支付完成，订单详情基本上不访问，那么就可以采取主动删除缓存的方式。首先创建缓存的时候，设置缓存有效期是1天，同时订单真正创建时候时候，更新缓存为1天（支付有效期1天），在订单被支付之后，则主动删除缓存（如果还有跳转页面并且需要通过缓存获取订单详情的，则设置一个接口完全可以获取到时间）。这样如果订单没有支付，那么一天内还是可以快速访问到这个订单的缓存信息，如果被支付，因为基本上不访问了，缓存也能尽快过期避免占用内存</p><h6 id="2-热点周期模式"><a href="#2-热点周期模式" class="headerlink" title="2.热点周期模式"></a>2.热点周期模式</h6><p>假设还是上面商城，场景稍作变换，订单支付完成之后，其实很多人会很习惯的关注订单进度（会查看订单详情），但是有些人其实不关心，买完就不管，而查看的正常来说每天会关注几次，知道收货之后，那我们可以模拟TTI的方式（Redis本质上是没有TTI的），在我们每次通过Redis获取到缓存之后，重新设置缓存有效期为1天，这样可以让热点数据一直处于相对有效，当没有获取到数据时，就从数据库等其他地方查询并缓存</p><h5 id="c-合理的结构"><a href="#c-合理的结构" class="headerlink" title="c)合理的结构"></a>c)合理的结构</h5><p>存放在Redis的数据结构尽可能缩减，避免无用字段进入缓存，比如常见的直接用数据库对应实体类对象直接存放到Redis，导致很多无用字段也进行了缓存，如一些乐观锁、创建时间等字段，精简缓存到满足业务需求即可。尽可能保证每个字段都是业务能用上的</p><h5 id="d-合理的序列化"><a href="#d-合理的序列化" class="headerlink" title="d)合理的序列化"></a>d)合理的序列化</h5><p>业务中常见的是把对象缓存到Redis中，而缓存对象，就会涉及到序列化，常见的序列化方式有json、默认序列化、kryo等，选择合适的序列化不仅能提升程序运行速度，还能降低内存占用，当然不同的结构也会影响不同的序列化对于内存占用的影响比例，所以具体选择那种序列化也需要根据自己实际情况做选择，但是通常情况下，加入对象中有10个字段，那么大概率内存占用是Java默认序列化 &gt; json &gt; kryo，但是不同的序列化也会产生很多其他问题，比如可读性和通用性，如果这个Redis缓存并不只是一种语言的程序会访问，那么就要考虑采用多种语言都能序列化的方式了</p><h5 id="e-数据压缩"><a href="#e-数据压缩" class="headerlink" title="e)数据压缩"></a>e)数据压缩</h5><p>很多缓存数据可能本身只是一个字符串，或者因为通用性问题最终选择了json格式的序列化，那么也可以通过压缩来降低内存占用，但是目前redis本身是不支持压缩的，所以只能是在客户端进行压缩和解压操作，但是压缩和解压会带来额外的CPU消耗</p><h4 id="三、提升性能"><a href="#三、提升性能" class="headerlink" title="三、提升性能"></a>三、提升性能</h4><h5 id="a-选择合适的客户端"><a href="#a-选择合适的客户端" class="headerlink" title="a)选择合适的客户端"></a>a)选择合适的客户端</h5><p>常见的有Jedis、Lettuce（现在Spring的默认客户端）、Redisson，每个客户端都有自己的优劣，可以根据实际情况进行选择</p><h5 id="b-合适的序列化"><a href="#b-合适的序列化" class="headerlink" title="b)合适的序列化"></a>b)合适的序列化</h5><p>不同序列化方式也是有差异的，虽然差异很小，但是如果是海量操作，哪怕很小的差异，也是能提升不小的时间成本的，但是序列化也会影响内存占用，并且如果访问量很大的情况下，通常缓存的key也不会小，所以要在内存和性能中做平衡，但是相对来说，序列化本身是在客户端实现的，而客户端的CPU资源相对来说更容易扩展，所以大部分情况下序列化还是考虑内存占用为主</p><h5 id="c-合理的压缩和缩减数据"><a href="#c-合理的压缩和缩减数据" class="headerlink" title="c)合理的压缩和缩减数据"></a>c)合理的压缩和缩减数据</h5><p>首选Redis是以服务形式存在的，并且正常来说Redis是会有专门的服务器，所以如果访问量很大，并且数据本身也不少的话，网络IO也会成为性能瓶颈的一环，所以如果保存的内容越小，传输也会更快，更不容易造成网络阻塞</p><h5 id="d-批量操作"><a href="#d-批量操作" class="headerlink" title="d)批量操作"></a>d)批量操作</h5><p>批量操作包括批量保存和批量获取以及一些其他条件性的保存等，如果业务可以通过批量操作的方式来实现，尽可能使用批量操作，首先批量保存和获取是常用客户端已经支持的操作，同时如果是一些原子性的操作或者有条件判断的，可以利用lua脚本来实现，目前常用的客户端也是支持lua脚本的操作的，比如常见的判断某个key1是否存在，如果存在则保存key2的数据，如果不存在则删除key1的数据这些都可以通过lua脚本实现，这样不仅降低了网络占用，同时还能保证当前不会有并发问题，比如判断key1的时候，key1的确不存在，但是当你写入key2的时候，key1却存在了，但是使用lua脚本就不会有这种问题</p>]]></content>
    
    
      
      
    <summary type="html">&lt;h4 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h4&gt;&lt;h4 id=&quot;一、前言&quot;&gt;&lt;a href=&quot;#一、前言&quot; class=&quot;headerlink&quot; title=&quot;一、前言&quot;&gt;&lt;/a&gt;一、前言&lt;/h</summary>
      
    
    
    
    
    <category term="Redis" scheme="https://raye.wang/tags/Redis/"/>
    
    <category term="性能优化" scheme="https://raye.wang/tags/%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/"/>
    
  </entry>
  
  <entry>
    <title>使用阿里云效自动打包生成hexo静态页面以及部署</title>
    <link href="https://raye.wang/2023/11/10/%E4%BD%BF%E7%94%A8%E9%98%BF%E9%87%8C%E4%BA%91%E6%95%88%E8%87%AA%E5%8A%A8%E6%89%93%E5%8C%85%E7%94%9F%E6%88%90hexo%E9%9D%99%E6%80%81%E9%A1%B5%E9%9D%A2%E4%BB%A5%E5%8F%8A%E9%83%A8%E7%BD%B2/"/>
    <id>https://raye.wang/2023/11/10/%E4%BD%BF%E7%94%A8%E9%98%BF%E9%87%8C%E4%BA%91%E6%95%88%E8%87%AA%E5%8A%A8%E6%89%93%E5%8C%85%E7%94%9F%E6%88%90hexo%E9%9D%99%E6%80%81%E9%A1%B5%E9%9D%A2%E4%BB%A5%E5%8F%8A%E9%83%A8%E7%BD%B2/</id>
    <published>2023-11-10T07:05:00.000Z</published>
    <updated>2026-03-10T06:59:04.571Z</updated>
    
    <content type="html"><![CDATA[<h4 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h4><p>博客系统有很多，但是在我看来从本质上区分大致可以分为2类：动态和静态</p><h5 id="动态博客"><a href="#动态博客" class="headerlink" title="动态博客"></a>动态博客</h5><p>有完整的后台可以直接在后台编辑文章发布，使用起来很方便，缺点是运行需要时时刻刻占用服务器资源，内存和CPU等（也有很多出色的博客系统占用资源很小，如：<a href="https://typecho.org/">typecho</a>），同时如果一个博客本身依赖的主键不少的话，部署起来难度也不低，当然大部分博客系统可以一键部署和docker部署，使用起来还是很方便的</p><h5 id="静态博客"><a href="#静态博客" class="headerlink" title="静态博客"></a>静态博客</h5><p>在电脑上写好文章，通过命令生成静态页面或者直接传到服务器，服务器上面也运行着博客程序，但是没有对应的后台，当文章文件到了服务器之后由服务器上面的博客系统自动生成对应的静态页面，有些博客系统也自带了一个http服务，如我现在使用和文章介绍的hexo就是这种，静态博客的好处是占用服务器资源少，因为生成的纯静态的HTML，所以只要服务器上面有http服务器就可以了，但是缺点也很明显，使用不方便，虽然直接在电脑上面安装还算方便（相对于静态博客来说）</p><p>当然也有一些博客系统本身就可以2种方式都支持（如：<a href="https://gitee.com/dl88250/solo">solo</a>），本身就是动态博客，但是也可以生成静态页面以减少资源的占用，毕竟对于大多数人来说，博客只是一个记录自己成长过程的地方，每学到一个新的东西，写一篇文章，加深一下自己的印象，或者解决了某个问题，写一篇文章记录，当下次遇到了就可以快速解决，同时把博客内容放到网站上，可以加快别人的学习和解决同样的问题。所以如果一个博客如果本身运行占用了不少服务器资源，对于个人来说，如果服务器资源足够好，那还无所谓，但是大部分的个人服务器都是承载着自己做测试的一些环境，所以资源本身存在缺乏的情况，同时如果只是想搞一个自己的网站，记录自己的成长，就要因此付出不小的服务器开支，会劝退不少人（静态博客可以直接部署到网站空间中，价格比服务器便宜很多），而且很多人会把文章发布到 GitHub Pages，所以我想这才是静态博客出现的原因吧</p><h4 id="起因"><a href="#起因" class="headerlink" title="起因"></a>起因</h4><h5 id="ghost的痛点"><a href="#ghost的痛点" class="headerlink" title="ghost的痛点"></a>ghost的痛点</h5><p>一开始我也是用到动态博客，因为毕竟网站后台直接写文章还是很方便的，起初我一直是用的<a href="https://ghost.org/">ghost</a> ,但是因为需要迁移服务器，所以需要重新安装，结果发现在我新服务器上面没办法安装最新版的nodejs（centos 7.9）因为系统某些组件版本太久，但是ghost又必须要用最新版的nodejs才能安装（讲真，ghost更新很频繁，nodejs更新也很频繁，但是不通版本之间兼容性真的好差啊）。我新的服务器上面又有一堆的东西了，又不想重装系统，加上新版的ghost的编辑器越改越不好用（个人感觉），所以干脆打算换一个博客系统</p><h5 id="寻找替代"><a href="#寻找替代" class="headerlink" title="寻找替代"></a>寻找替代</h5><p>当时也看了很多博客系统，从动态的到静态的，很多动态的我很喜欢，但是要么部署麻烦，要么资源占用不少（主要是Java的，因为我本身就是Java开发），虽然有<a href="https://typecho.org/">typecho</a>这种，但是我又不想在搞一套PHP环境，索性选择了静态博客，但是静态博客每次写完都必须要在本地电脑上执行命令生成HTML，然后把HTML复制到服务器上面去发布（hexo有专门的部署命令，但是是否支持部署到自己服务器以及指定目录就没有研究过了），这样很麻烦</p><h5 id="一个猜想"><a href="#一个猜想" class="headerlink" title="一个猜想"></a>一个猜想</h5><p>想起来了之前看过的阿里云效，可以自动做单元测试，编译打包和部署，相当于一个云端的<a href="https://www.jenkins.io/">Jenkins</a>，只是相对于Jenkins来说，更加简单，于是便开始研究能否利用阿里云效给我自动生成HTML和部署到服务器，便有了此文，购买服务器以及服务器上安装http服务器就不用说了，我的http服务器是用的nginx，因为本身自己测试也需要用到nginx给我做反向代理这些</p><h4 id="整体思路"><a href="#整体思路" class="headerlink" title="整体思路"></a>整体思路</h4><p>本地电脑进行文章编辑，编辑完成上传到阿里云效的git服务中，然后由阿里云效执行hexo命令生成HTML，打包HTML发送到服务器指定目录，在服务器执行解压命令</p><p><img src="/./img/2023-11-13/20231110170129.png" alt="流程图"></p><h4 id="第一步：本地环境"><a href="#第一步：本地环境" class="headerlink" title="第一步：本地环境"></a>第一步：本地环境</h4><p>首先在电脑上面安装hexo和git，其实不安装hexo也行，只要有博客网站的代码就行了，这种方式下，hexo主要就是创建博客网站的目录（新写文章时，可以直接在目录里面创建文章的md文件即可），git安装方式就没有啥好说的了，至于hexo，因为是基于nodejs的，所以需要先安装nodejs，hexo的安装详细可以参考<a href="https://hexo.io/zh-cn/docs/">hexo 文档</a></p><h4 id="第二步：阿里云"><a href="#第二步：阿里云" class="headerlink" title="第二步：阿里云"></a>第二步：阿里云</h4><h5 id="1-创建账号开通服务"><a href="#1-创建账号开通服务" class="headerlink" title="1 创建账号开通服务"></a>1 创建账号开通服务</h5><p>首先要创建阿里云的账号，然后开通云效服务，服务用免费的就完全满足需求了</p><h5 id="2-创建codeup项目并上传"><a href="#2-创建codeup项目并上传" class="headerlink" title="2 创建codeup项目并上传"></a>2 创建codeup项目并上传</h5><p>在云效中的codeup中创建一个博客的项目，创建好之后把本地博客网站的目录上传到codeup中</p><h5 id="3-创建云效DevOps项目"><a href="#3-创建云效DevOps项目" class="headerlink" title="3 创建云效DevOps项目"></a>3 创建云效DevOps项目</h5><p>进入云效DevOps中，创建一个DevOps项目，如果直接就有就不用创建了，点进去就行了（不清楚其他环境，因为我账号开通云效时间比较久，我进入到DevOps的时候就已经有一个项目了）</p><h5 id="创建流水线"><a href="#创建流水线" class="headerlink" title="创建流水线"></a>创建流水线</h5><p>在DevOps项目点击添加流水线，选择新建，选择任意Node.Js模版，我这里为了简单就直接选择了【Node.js · 测试、构建、部署到阿里云ECS&#x2F;自有主机】模板（这个可能会随着时间变化而改名），选择了其他模板也无所谓，反正这个主要是靠自己配置</p><p><img src="/./img/2023-11-13/20231110171812.png" alt="选择模版"></p><p>点击创建按钮创建流水线</p><h5 id="5-配置流水线"><a href="#5-配置流水线" class="headerlink" title="5 配置流水线"></a>5 配置流水线</h5><h6 id="a-代码源配置"><a href="#a-代码源配置" class="headerlink" title="a) 代码源配置"></a>a) 代码源配置</h6><p>首先配置流水线源，选择codeup，当然如果不是用的codeup也可以选择其他，只是codeup即免费也更方便而已</p><p><img src="/./img/2023-11-13/1699608823902.png" alt="选择数据源"></p><p>选择对应的仓库，同事开启代码源触发</p><h6 id="b-删除测试"><a href="#b-删除测试" class="headerlink" title="b) 删除测试"></a>b) 删除测试</h6><p><img src="/./img/2023-11-13/1699608977628.png" alt="删除测试环节"></p><p>需要将流水线中的测试环节删除，因为过不了会导致流水线不会继续执行，而且就算过了也没有必要，会浪费时间（免费版每个月有1800分钟打包时间，相当于每天1个小时）。点击其中一项在右边弹出来的窗口中有删除图标</p><h6 id="c-配置Node-js构建"><a href="#c-配置Node-js构建" class="headerlink" title="c) 配置Node.js构建"></a>c) 配置Node.js构建</h6><p>点击Node.js构建，进入配置页面，选择Node.js版本（需要根据当前hexo适配的版本选择）我这里选择的是16.8版本，如果没有推荐的版本，建议选择附近高版本，只能往高选，不要选低版本，否则容易出现兼容问题</p><p>选择版本之后在构建命令中输入</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></pre></td><td class="code"><pre><span class="line">npm install hexo</span><br><span class="line">npm install</span><br><span class="line">npm run deploy</span><br></pre></td></tr></table></figure><p>为了减少出错可能，加上了<strong>npm install hexo</strong>这里是不支持全局安装的，所以只能执行局部安装命令，另外项目本身其实执行npm install的时候也是会安装hexo的，只是我这里配置如果不添加<strong>npm install hexo</strong> 会抛异常，所以添加了，反正添加也没有什么影响，配置图如下：</p><p><img src="/./img/2023-11-13/20231113095112.png"></p><p>Node.Js构建配置好之后，点下面的构建物上传，上传方式选择<strong>归档至云效公共存储空间</strong>,制品名称可以选择默认，打包路径<strong>public&#x2F;</strong>,如下图</p><p><img src="/./img/2023-11-13/20231113095404.png"></p><h6 id="d-主机部署配置"><a href="#d-主机部署配置" class="headerlink" title="d)主机部署配置"></a>d)主机部署配置</h6><p>点击主机部署进入主机部署界面，首先选择制品，就刚刚的，比如我得是<strong>Artifacts_${PIPELINE_ID}</strong></p><p>然后选择主机组，这里如果一开始没有创建过主机组需要点击新建主机组，新建过程中根据提示和实际情况选择创建就好了</p><p>部署配置主要配置下载的压缩文件到服务器的什么目录，执行命令的用户，已经执行的脚本，这里可以直接输入脚本命令，比如我得配置，下载路径是**&#x2F;home&#x2F;blog&#x2F;package.tgz<strong>，执行用户是</strong>root** 部署的脚本因为我只需要一个解压命令，所以直接在脚本里面输入了解压命令，各位可以根据自己的实际情况选择，我的配置如下图：</p><p><img src="/./img/2023-11-13/20231113100041.png"></p><h6 id="e-保存并运行"><a href="#e-保存并运行" class="headerlink" title="e) 保存并运行"></a>e) 保存并运行</h6><p>配置完成之后，点击保存并运行，看看有没有问题，如果有问题，可以看到底哪一步出了问题（云效在某个步骤出错了会有提示，并且也有日志可以追踪）</p><h4 id="第三步：服务器配置"><a href="#第三步：服务器配置" class="headerlink" title="第三步：服务器配置"></a>第三步：服务器配置</h4><p>需要在服务器上面安装http服务器软件以及将域名指向到服务器，这里我是用的Nginx，各位可以根据实际情况选择自己的服务器软件（静态服务器软件即可）</p><h4 id="第四步：写作"><a href="#第四步：写作" class="headerlink" title="第四步：写作"></a>第四步：写作</h4><p>全部搞好之后就可以开始写作了，可以通过hexo命令创建一篇文章，也可以直接在本机网站程序的<strong>\source_posts</strong>目录创建文章（.md格式），写好之后上传到git，就可以自动打包并且部署了</p>]]></content>
    
    
      
      
    <summary type="html">&lt;h4 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h4&gt;&lt;p&gt;博客系统有很多，但是在我看来从本质上区分大致可以分为2类：动态和静态&lt;/p&gt;
&lt;h5 id=&quot;动态博客&quot;&gt;&lt;a href=&quot;#动态博客&quot; c</summary>
      
    
    
    
    
    <category term="Hexo" scheme="https://raye.wang/tags/Hexo/"/>
    
    <category term="自动部署" scheme="https://raye.wang/tags/%E8%87%AA%E5%8A%A8%E9%83%A8%E7%BD%B2/"/>
    
  </entry>
  
  <entry>
    <title>记一次MySQL链接异常排查与解决</title>
    <link href="https://raye.wang/2023/09/19/%E8%AE%B0%E4%B8%80%E6%AC%A1MySQL%E9%93%BE%E6%8E%A5%E5%BC%82%E5%B8%B8%E6%8E%92%E6%9F%A5%E4%B8%8E%E8%A7%A3%E5%86%B3/"/>
    <id>https://raye.wang/2023/09/19/%E8%AE%B0%E4%B8%80%E6%AC%A1MySQL%E9%93%BE%E6%8E%A5%E5%BC%82%E5%B8%B8%E6%8E%92%E6%9F%A5%E4%B8%8E%E8%A7%A3%E5%86%B3/</id>
    <published>2023-09-19T06:33:28.000Z</published>
    <updated>2026-03-10T06:59:04.572Z</updated>
    
    <content type="html"><![CDATA[<p><strong>起因</strong>程序代码中链接MySQL数据库时，提示异常，提示内容如下：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><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></pre></td><td class="code"><pre><span class="line">2023-09-14 11:05:30.207 ERROR 74928 --- [eate-2053481312] com.alibaba.druid.pool.DruidDataSource   : create connection SQLException, url: jdbc:mysql://xxxx:3306/xxx?useUnicode=true&amp;characterEncoding=utf8&amp;useTimezone=true&amp;serverTimezone=GMT%2B8&amp;requireSSL=false&amp;&amp;autoReconnect=true&amp;allowMultiQueries=true&amp;connectTimeout=20000&amp;rewriteBatchedStatements=true, errorCode 0, state 08001</span><br><span class="line"></span><br><span class="line">java.sql.SQLNonTransientConnectionException: Could not create connection to database server. Attempted reconnect 3 times. Giving up.</span><br><span class="line">    at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:110) ~[mysql-connector-java-8.0.17.jar:8.0.17]</span><br><span class="line">    at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:97) ~[mysql-connector-java-8.0.17.jar:8.0.17]</span><br><span class="line">    at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:89) ~[mysql-connector-java-8.0.17.jar:8.0.17]</span><br><span class="line">    at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:63) ~[mysql-connector-java-8.0.17.jar:8.0.17]</span><br><span class="line">    at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:73) ~[mysql-connector-java-8.0.17.jar:8.0.17]</span><br><span class="line">    at com.mysql.cj.jdbc.ConnectionImpl.connectWithRetries(ConnectionImpl.java:897) ~[mysql-connector-java-8.0.17.jar:8.0.17]</span><br><span class="line">    at com.mysql.cj.jdbc.ConnectionImpl.createNewIO(ConnectionImpl.java:822) ~[mysql-connector-java-8.0.17.jar:8.0.17]</span><br><span class="line">    at com.mysql.cj.jdbc.ConnectionImpl.&lt;init&gt;(ConnectionImpl.java:447) ~[mysql-connector-java-8.0.17.jar:8.0.17]</span><br><span class="line">    at com.mysql.cj.jdbc.ConnectionImpl.getInstance(ConnectionImpl.java:237) ~[mysql-connector-java-8.0.17.jar:8.0.17]</span><br><span class="line">    at com.mysql.cj.jdbc.NonRegisteringDriver.connect(NonRegisteringDriver.java:199) ~[mysql-connector-java-8.0.17.jar:8.0.17]</span><br><span class="line">    at com.alibaba.druid.filter.FilterChainImpl.connection_connect(FilterChainImpl.java:156) ~[druid-1.1.21.jar:1.1.21]</span><br><span class="line">    at com.alibaba.druid.filter.stat.StatFilter.connection_connect(StatFilter.java:218) ~[druid-1.1.21.jar:1.1.21]</span><br><span class="line">    at com.alibaba.druid.filter.FilterChainImpl.connection_connect(FilterChainImpl.java:150) ~[druid-1.1.21.jar:1.1.21]</span><br><span class="line">    at com.alibaba.druid.filter.FilterAdapter.connection_connect(FilterAdapter.java:787) ~[druid-1.1.21.jar:1.1.21]</span><br><span class="line">    at com.alibaba.druid.filter.FilterEventAdapter.connection_connect(FilterEventAdapter.java:38) ~[druid-1.1.21.jar:1.1.21]</span><br><span class="line">    at com.alibaba.druid.filter.FilterChainImpl.connection_connect(FilterChainImpl.java:150) ~[druid-1.1.21.jar:1.1.21]</span><br><span class="line">    at com.alibaba.druid.pool.DruidAbstractDataSource.createPhysicalConnection(DruidAbstractDataSource.java:1646) ~[druid-1.1.21.jar:1.1.21]</span><br><span class="line">    at com.alibaba.druid.pool.DruidAbstractDataSource.createPhysicalConnection(DruidAbstractDataSource.java:1710) ~[druid-1.1.21.jar:1.1.21]</span><br><span class="line">    at com.alibaba.druid.pool.DruidDataSource$CreateConnectionThread.run(DruidDataSource.java:2753) ~[druid-1.1.21.jar:1.1.21]</span><br><span class="line">Caused by: com.mysql.cj.exceptions.CJCommunicationsException: Communications link failure</span><br><span class="line"></span><br><span class="line">The last packet sent successfully to the server was 0 milliseconds ago. The driver has not received any packets from the server.</span><br><span class="line">    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) ~[na:1.8.0_201]</span><br><span class="line">    at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62) ~[na:1.8.0_201]</span><br><span class="line">    at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) ~[na:1.8.0_201]</span><br><span class="line">    at java.lang.reflect.Constructor.newInstance(Constructor.java:423) ~[na:1.8.0_201]</span><br><span class="line">    at com.mysql.cj.exceptions.ExceptionFactory.createException(ExceptionFactory.java:61) ~[mysql-connector-java-8.0.17.jar:8.0.17]</span><br><span class="line">    at com.mysql.cj.exceptions.ExceptionFactory.createException(ExceptionFactory.java:105) ~[mysql-connector-java-8.0.17.jar:8.0.17]</span><br><span class="line">    at com.mysql.cj.exceptions.ExceptionFactory.createException(ExceptionFactory.java:151) ~[mysql-connector-java-8.0.17.jar:8.0.17]</span><br><span class="line">    at com.mysql.cj.exceptions.ExceptionFactory.createCommunicationsException(ExceptionFactory.java:167) ~[mysql-connector-java-8.0.17.jar:8.0.17]</span><br><span class="line">    at com.mysql.cj.protocol.a.NativeProtocol.negotiateSSLConnection(NativeProtocol.java:340) ~[mysql-connector-java-8.0.17.jar:8.0.17]</span><br><span class="line">    at com.mysql.cj.protocol.a.NativeAuthenticationProvider.negotiateSSLConnection(NativeAuthenticationProvider.java:777) ~[mysql-connector-java-8.0.17.jar:8.0.17]</span><br><span class="line">    at com.mysql.cj.protocol.a.NativeAuthenticationProvider.proceedHandshakeWithPluggableAuthentication(NativeAuthenticationProvider.java:486) ~[mysql-connector-java-8.0.17.jar:8.0.17]</span><br><span class="line">    at com.mysql.cj.protocol.a.NativeAuthenticationProvider.connect(NativeAuthenticationProvider.java:202) ~[mysql-connector-java-8.0.17.jar:8.0.17]</span><br><span class="line">    at com.mysql.cj.protocol.a.NativeProtocol.connect(NativeProtocol.java:1348) ~[mysql-connector-java-8.0.17.jar:8.0.17]</span><br><span class="line">    at com.mysql.cj.NativeSession.connect(NativeSession.java:163) ~[mysql-connector-java-8.0.17.jar:8.0.17]</span><br><span class="line">    at com.mysql.cj.jdbc.ConnectionImpl.connectWithRetries(ConnectionImpl.java:841) ~[mysql-connector-java-8.0.17.jar:8.0.17]</span><br><span class="line">    ... 13 common frames omitted</span><br><span class="line">Caused by: javax.net.ssl.SSLException: Received fatal alert: internal_error</span><br><span class="line">    at sun.security.ssl.Alerts.getSSLException(Alerts.java:208) ~[na:1.8.0_201]</span><br><span class="line">    at sun.security.ssl.Alerts.getSSLException(Alerts.java:154) ~[na:1.8.0_201]</span><br><span class="line">    at sun.security.ssl.SSLSocketImpl.recvAlert(SSLSocketImpl.java:2020) ~[na:1.8.0_201]</span><br><span class="line">    at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:1127) ~[na:1.8.0_201]</span><br><span class="line">    at sun.security.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1367) ~[na:1.8.0_201]</span><br><span class="line">    at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1395) ~[na:1.8.0_201]</span><br><span class="line">    at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1379) ~[na:1.8.0_201]</span><br><span class="line">    at com.mysql.cj.protocol.ExportControlled.performTlsHandshake(ExportControlled.java:316) ~[mysql-connector-java-8.0.17.jar:8.0.17]</span><br><span class="line">    at com.mysql.cj.protocol.StandardSocketFactory.performTlsHandshake(StandardSocketFactory.java:188) ~[mysql-connector-java-8.0.17.jar:8.0.17]</span><br><span class="line">    at com.mysql.cj.protocol.a.NativeSocketConnection.performTlsHandshake(NativeSocketConnection.java:99) ~[mysql-connector-java-8.0.17.jar:8.0.17]</span><br><span class="line">    at com.mysql.cj.protocol.a.NativeProtocol.negotiateSSLConnection(NativeProtocol.java:331) ~[mysql-connector-java-8.0.17.jar:8.0.17]</span><br><span class="line">    ... 19 common frames omitted</span><br></pre></td></tr></table></figure><p>网上搜索了一圈，要么说防火墙问题，要么说服务没有打开或者连接数问题的，试了一圈下来，并没有解决，于是开始排查</p><p><strong>排查点一</strong></p><p>首先可以确定数据库服务本身是没有问题的，因为使用工具是能正常链接的</p><p><strong>排查点二</strong></p><p>项目代码问题，因为工具能正常链接，修改了URL链接到另外一个数据库，测试结果发现执行结果成功，说明跟代码中的jar包和代码本身没有太大关系</p><p><strong>排查点三</strong></p><p>上面排查了代码本身，但是忽略了一个问题就是2个MySQL本身的版本问题，于是查看版本，能链接的MySQL版本是：5.7.32  ，不能链接的版本是：5.7.43，2个版本相差并不大，并且我用的驱动版本是8的，按道理来说也不关驱动的问题</p><p><strong>排查点四</strong></p><p>由于其他方面都没有找到具体原因，最终还是回到错误日志本身来找原因，其他的错误日志本身看不出来任何问题，唯独其中一行日志：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Caused by: javax.net.ssl.SSLException: Received fatal alert: internal_error</span><br></pre></td></tr></table></figure><p>通过查询这行日志是因为证书原因导致的，但是数据库链接本身并没有强制使用SSL（requireSSL&#x3D;false），所以这个日志也很奇怪，于是找了MySQL链接参数相关SSL配置的参数，尝试配置，发现加上配置：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">enabledTLSProtocols=TLSv1.2</span><br></pre></td></tr></table></figure><p>后链接正常。但是至于为什么就可以正常使用了，目前还不清楚具体原因，因为如果是2个MySQL版本本身支持的TLS版本不一致导致的其中一个可以链接上另外一个不行，或者其中一个指定了TLS版本导致无法链接都可以理解，但是其实在2个MySQL上面执行</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">SHOW</span> VARIABLES <span class="keyword">LIKE</span> <span class="string">&#x27;tls_version&#x27;</span></span><br></pre></td></tr></table></figure><p>返回的结果都是：TLSv1,TLSv1.1,TLSv1.2也就是2个版本本身对TLS版本支持是一致的，并且2个版本的配置文件本身都没有对TLS相关内容进行配置，所以实际上具体原因并不清楚，但是问题却解决了，只能有空仔细研究一下了</p><p><strong>解决方案</strong></p><p>在链接的URL上面加上参数enabledTLSProtocols&#x3D;TLSv1.2则可以解决问题，并且目前没有发现因为这个配置产生的其他问题或者其他异常情况</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;&lt;strong&gt;&amp;#x8D77;&amp;#x56E0;&lt;/strong&gt;&amp;#x7A0B;&amp;#x5E8F;&amp;#x4EE3;&amp;#x7801;&amp;#x4E2D;&amp;#x94FE;&amp;#x63A5;MySQL&amp;#x6570;&amp;#x636E;&amp;#x5E93;&amp;#x65F6;&amp;#xFF0C;&amp;#x63D0;&amp;#x793A;&amp;#x5F02;&amp;#x5E38;&amp;#xFF0C;&amp;#x63D0;&amp;#x793A;&amp;#x5185;&amp;#x5BB9;&amp;#x5982;&amp;#x4E0B;&amp;#xFF1A;&lt;/p&gt;&lt;pre&gt;&lt;code&gt;2023-09-14 11:05:30.207 ERROR 74928 --- [eate-2053481312] com.alibaba.druid.pool.DruidDataSource   : create connection SQLException, url: jdbc:mysql://xxxx:3306/xxx?&lt;/code&gt;&lt;/pre&gt;</summary>
    
    
    
    <category term="MySQL" scheme="https://raye.wang/categories/MySQL/"/>
    
    
  </entry>
  
  <entry>
    <title>MySQL 慢查询发现以及增删改查SQL优化</title>
    <link href="https://raye.wang/2023/04/23/MySQL-%E6%85%A2%E6%9F%A5%E8%AF%A2%E5%8F%91%E7%8E%B0%E4%BB%A5%E5%8F%8A%E5%A2%9E%E5%88%A0%E6%94%B9%E6%9F%A5SQL%E4%BC%98%E5%8C%96/"/>
    <id>https://raye.wang/2023/04/23/MySQL-%E6%85%A2%E6%9F%A5%E8%AF%A2%E5%8F%91%E7%8E%B0%E4%BB%A5%E5%8F%8A%E5%A2%9E%E5%88%A0%E6%94%B9%E6%9F%A5SQL%E4%BC%98%E5%8C%96/</id>
    <published>2023-04-23T14:44:16.000Z</published>
    <updated>2026-03-10T06:59:04.570Z</updated>
    
    <content type="html"><![CDATA[<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>日常工作中，sql语句执行效率经常成为程序的性能瓶颈，如何快速的执行数据库的增删改查则是一个合格的后端程序员必备技能，本文描述了从发现执行慢的sql到如何优化增删改查的sql语句，以及从架构本身优化查询速度的一些方向，本文是我个人的经验之谈，不一定完全正确，如有错误，还望指出</p><h2 id="优化前先了解一些基本的MySQL存储结构和索引基础知识"><a href="#优化前先了解一些基本的MySQL存储结构和索引基础知识" class="headerlink" title="优化前先了解一些基本的MySQL存储结构和索引基础知识"></a>优化前先了解一些基本的MySQL存储结构和索引基础知识</h2><p>在做性能优化之前，首先要对MySQL的数据存储结构要有一定的了解，只有当了解了结构，才能更清楚的知道为什么这样写sql执行会更快，为什么表结构这样设计，在同样的数据行的情况下，查询和写入效率都更高，由于MySQL常用的存储引擎是InnoDB，所以这里介绍的也是基于InnoDB的</p><h3 id="InnoDB的存储结构"><a href="#InnoDB的存储结构" class="headerlink" title="InnoDB的存储结构"></a>InnoDB的存储结构</h3><p>InnoDB存储引擎中，<strong>表记录都是根据主键顺序组织存放，这种存储方式的表称为索引组织表，InnoDB的逻辑结构从大到小是表空间-&gt;段-&gt;区-&gt;页-&gt;行-&gt;列。</strong></p><p>所有数据都被逻辑的存放在表空间中，表空间由各个段组成，常见的段：数据段、索引段、回滚段。</p><p>区是由连续的页组成的，在任何情况下每个区的大小都为1MB。为了保证区中页的连续性，InonoDB存储引擎一次从磁盘申请4-5个区。在默认情况下，InnoDB存储引擎的页的大小为16KB，即一个区中应有64个连续的页。从InnoDB1.0.x开始，可以通过参数KEY_BLOCK_SIZE设置页的大小。</p><p>页（block），是InnoDB磁盘管理的最小单元，其中常见的有数据页、undo页、系统页、事务数据页等。</p><p>行（ROW）InnoDB存储引擎是面向列的（row-oriented），则按行进行存放。每个页存放的行记录也是有硬性定义的，最多允许存放16KB&#x2F;2-200。每页最少存储2行记录，用链表连接起来，否则会失去B+树的意义。数据大的行记录，如：大字符串、TEXT、BLOB对象，都是采用行溢出数据存储。不同的行格式，存储方式不同。</p><h4 id="索引类型"><a href="#索引类型" class="headerlink" title="索引类型"></a>索引类型</h4><p>索引有3个维度：数据结构纬度、物理存储纬度、逻辑维度</p><h5 id="数据结构纬度"><a href="#数据结构纬度" class="headerlink" title="数据结构纬度"></a>数据结构纬度</h5><p><strong>B+树索引</strong>：所有数据存储在叶子节点，复杂度为<code>O(logn)</code>，适合范围查询。（这个索引的结构需要重点了解一下）</p><p>哈希索引: 适合等值查询，检索效率高，一次到位。InnoDB中虽然有用到这个索引，但是是引擎内部自己用的，我们不能手动创建这个类型的索引</p><p>全文索引：<code>MyISAM</code>和<code>InnoDB</code>中都支持使用全文索引，一般在文本类型<code>char,text,varchar</code>类型上创建。</p><p><code>R-Tree</code>索引: 用来对<code>GIS</code>数据类型创建<code>SPATIAL</code>索引</p><h5 id="物理存储维度"><a href="#物理存储维度" class="headerlink" title="物理存储维度"></a>物理存储维度</h5><p>聚集索引：用来确定表数据唯一性的索引，优先选择主键索引，如果没有主键索引会自动选择合适的唯一索引，如果唯一索引也没用，则会使用默认的内部表字段rowId,<strong>聚集索引的叶子节点存储的是行的数据</strong></p><p>非聚集索引：不是聚集索引的都是非聚集索引（有点废话），在叶子节点存储的是索引本身的数据以及聚集索引的值</p><h5 id="逻辑维度"><a href="#逻辑维度" class="headerlink" title="逻辑维度"></a>逻辑维度</h5><p>主键索引：一种特殊的唯一索引，不允许有空值。</p><p>普通索引：<code>MySQL中</code>基本索引类型，允许空值和重复值</p><p>联合索引：多个字段创建的索引，使用时遵循最左前缀原则。</p><p>唯一索引：索引列中的值必须是唯一的，但是允许为空值。</p><p>空间索引：<code>MySQL5.7</code>之后支持空间索引，在空间索引这方面遵循<code>OpenGIS</code>几何数据模型规则</p><p>上面只是大概介绍了一下MySQL的存储引擎和索引的基本知识，要想学好sql优化，光看优化方式是不行的，还需要学习引擎和索引的相关结构、存储、执行逻辑，才能更清楚的了解优化过程中为什么要避免这么做，为什么要那么做，所以建议大家需要看看上面相关的书籍和文章。</p><h3 id="发现慢SQL"><a href="#发现慢SQL" class="headerlink" title="发现慢SQL"></a>发现慢SQL</h3><p>发现慢SQL主要是通过慢查询日志和SHOW PROFILES命令，2种方法各有优劣，2种方法并不是互斥的，给我感觉反而是相互补充的，反正我实际使用中，是2种方法结合起来使用</p><h4 id="慢查询日志"><a href="#慢查询日志" class="headerlink" title="慢查询日志"></a>慢查询日志</h4><p>MySQL 的慢查询日志可以记录执行时间超过指定阈值的 SQL 语句，通过分析慢查询日志可以找出执行时间较长的 SQL 语句，进而进行优化。</p><p>要启用慢查询日志，需要在 MySQL 配置文件中设置相关参数，例如设置执行时间超过 3 秒的 SQL 语句会被记录到慢查询日志：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">slow_query_log = 1</span><br><span class="line">long_query_time = 3</span><br><span class="line">slow_query_log_file = xxxx//慢查询日志文件地址</span><br></pre></td></tr></table></figure><p>这样我们定时的去数据库服务器获取慢查询日志就可以发现哪些sql执行的慢，需要优化了。但是这样并不智能，毕竟不同的表数据量不通，查询的性能指标也肯定不同，比如一个表只有10万条数据，虽然查询只花费了2秒，并没有记录到，但是明显也是需要优化的sql，而有些表数据上亿，查询用了3秒，虽然也需要优化，但是实际上本身才从sql语句上面优化的结果并不会太理想了，所以慢查询日志并不能完全的发现程序中需要优化的sql语句。<strong>注意开启慢查询日志也会因为需要记录日志而占用一定的CPU和硬盘资源，所以如果正式环境MySQL服务器资源已经紧张的情况下，需要谨慎开启</strong></p><h4 id="SHOW-PROFILES"><a href="#SHOW-PROFILES" class="headerlink" title="SHOW PROFILES"></a>SHOW PROFILES</h4><p>SHOW PROFILES是<code>MySQL</code>提供可以用来分析当前会话中<code>SQL</code>语句执行的资源消耗情况。可以用于<code>SQL</code>的调优测量。默认情况下，参数处于关闭状态，并保存最近 15 次的运行结果，首先需要确认当前MySQL是支持的，首先在MySQL中执行</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">SELECT @@have_profiling</span><br></pre></td></tr></table></figure><p>返回的是YES说明当前MySQL版本是支持的，执行</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">SELECT @@profiling</span><br></pre></td></tr></table></figure><p>返回0是关闭的，返回1是开启了，如果返回0，则需要通过执行</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">SET GLOBAL PROFILING =1</span><br></pre></td></tr></table></figure><p>来开启SHOW PROFILES功能，开启后执行</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">SHOW PROFILES;</span><br></pre></td></tr></table></figure><p>可以查看最近15条sql的执行情况，返回有Query_id(查询的id)，Duration（耗时），Query</p><p>(执行的sql语句)，如果想知道某个结果执行的具体耗时，确认id后，通过执行</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">SHOW PROFILE FOR QUERY 指定id</span><br></pre></td></tr></table></figure><p>可以查看sql的具体执行耗时情况，借用此命令，我们可以程序中定时获取MySQL最近15条sql语句的执行情况，然后自己通过自定义规则来判定哪些sql需要优化（需要自行编程实现），通过这种方式，我们可以完全自定义每个表的执行效率规则，来找出需要优化的sql，但是这样也会有一个问题，那就是每次是获取的最近15条sql，有可能每次需要执行的sql语句都刚好没有抓取到，另外就是为了抓取更多的sql，频率太频繁本身也是对数据的一种压力</p><p>实际工作中需要将上面2种方式结合起来使用，一个根据表数据量和应该执行的效率来随机抓取需要优化的sql，一个是获取已经超过执行阀值的sql，这样能消灭系统中绝大部分sql语句性能瓶颈</p><h3 id="查看SQL执行情况"><a href="#查看SQL执行情况" class="headerlink" title="查看SQL执行情况"></a>查看SQL执行情况</h3><h2 id="explain-（desc）"><a href="#explain-（desc）" class="headerlink" title="explain （desc）"></a>explain （desc）</h2><p>当遇到一条sql语句执行慢的缺又没有办法确定准确原因的时候，可以通过explain关键字来查看语句的执行计划，通过执行计划，我们可以确定语句命中了哪个索引，大概扫描了多少行数据，以及关联表的顺序等信息，通过desc关键字效果也是一致的</p><p>返回的结果中有id、select_type、table、partitions、type 、possible_keys、key、key_len、ref、rows、filtered、Extra</p><p>1.id ： 表示当前表的执行操作顺序，id相同，顺序从上往下，id不通，值越大越优先执行</p><p>2.select_type：查询的类型，常见的有SIMPLE、PRIMARY、UNION、SUBQUERY、DERIVED</p><p>3.table：表名（如果取了别名会是别名）</p><p>4.partitions：查询将匹配记录的分区。 对于非分区表，该值为 NULL。</p><p>5.type: 表示链接的类型，性能由好到差是顺序为：NULL-&gt;system-&gt;const-&gt;eq_ref-&gt;ref-&gt;rang-&gt;index-&gt;all</p><p>NULL：MySQL能在优化阶段分解查询语句，在执行阶段不用再去访问表或者索引。正常业务sql不会出现</p><p>system、const：MySQL对查询的某部分进行优化并把其转化成一个常量，system是const的一个特例，表示表里只有一条元组匹配时为system</p><p>eq_ref：主键或唯一键索引被连接使用，最多只会返回一条符合条件的记录</p><p>ref：相比eq_ref，不使用唯一索引，而是使用普通索引或者唯一索引的部分前缀，索引和某个值比较，会找到多个符合条件的行</p><p>range：通常出现在范围查询中，比如in、between、大于、小于等。使用索引来检索给定范围的行。</p><p>index：扫描全索引拿到结果，一般是扫描某个二级索引，二级索引一般比较少，所以通常比ALL快一点</p><p>all：全表扫描</p><p>6.possible_keys：显示当前查询中可能用到的索引，如果为NULL则表示没有相关的索引</p><p>7.key：当前查询中实际用到的索引</p><p>8.key_len：当前命中索引里面使用的字节数（最左原则），值越接近索引本身的长度越好</p><p>9.ref：显示key列记录的索引中，表查找值时使用到的列或常量。常见的有const、字段名</p><p>10.rows：当前查询MySQL预计要读取的行数（不是实际的查询出来的结果行数，而是预计要扫描多少行才能得到正确数据，并且这个行数是预估值，实际上可能会有偏差）</p><p>11.filtered：表示通过查询条件获取的最终记录行数占通过type字段指明的搜索方式搜索出来的记录行数的百分比，其他条件相同的情况下，此值越接近100性能越好</p><p>12.Extra：额外信息列，常见的重要值如下：</p><p>Using index：使用覆盖索引</p><p>Using where：使用 where 语句来处理结果，并且查询的列未被索引覆盖</p><p>Using index condition：查询的列不完全被索引覆盖，where条件中是一个查询的范围</p><p>Using temporary：MySQL需要创建一张临时表来处理查询。出现这种情况一般是要进行优化的</p><p>Using filesort：将使用外部排序而不是索引排序，数据较小时从内存排序，否则需要在磁盘完成排序</p><p>Select tables optimized away：使用某些聚合函数（比如 max、min）来访问存在索引的某个字段时</p><p>上面只是大概介绍了一下explain返回的各个值的基本说明，需要详细了解explain的可以查阅专门介绍explain的文章</p><h3 id="查询语句优化的一些方向"><a href="#查询语句优化的一些方向" class="headerlink" title="查询语句优化的一些方向"></a>查询语句优化的一些方向</h3><p>1.尽可能的避免select *，首先如果一个字段业务根本用不到，刚好这个字段的又是一个很长的字符串，那么查询本身可能会跨页查询（不是我们业务的分页，而是MySQL底层的页），同时也增大了网络和内存开销，当然select * 并非没有好处，那就是如果加了字段只需要业务代码添加对应的属性即可使用，所以如果你的表结构小，字段又几乎是业务全部会用到的，用 *没有太大问题，如果你的表字段多，建议尽量避免</p><p>2.尽量命中索引，如果是联合索引，则尽可能的多命中字段，也就是explain中的key_len尽可能的长，同时也需要尽量避免条件中导致索引失效，关于哪些做法会导致索引失效和怎么高效的命中索引，可以查阅相关文章，内容太多，这里不做详细介绍</p><p>3.小表驱动大表，关联查询中，让优先执行的语句尽可能的是数据量小的表，并且出来的数据量也尽可能的小</p><p>4.能用union all就不用union，对于多次查询结果肯定不可能重复的情况下，尽可能的用union all,因为union会对结果去重，造成性能损失</p><p>5.提前limit，比如关联的表出来只是为了字段返回，本身不会影响主体结果的情况下，先对主体数据提前分页，分页之后再关联需要字段返回的表，而不是关联完了最后分页</p><p>6.尽可能的减少回表，非聚集索引存储的数据是聚集索引的值，也就是说如果命中了非聚集索引，并且查询的字段包含非索引字段，则需要进行回表操作，具体可以查看一些相关文章，回表优化在数据量大的时候能提升不少效率，很多时候甚至会使用手动回表来提升查询效率（通过非聚集索引获取到聚集索引的值，然后通过值查询具体数据，而不是通过MySQL自己的回表来查询）</p><p>7.避免太多表链接查询，适当的时候可以从业务代码中分多次查询，联表查询关联的表越多，性能越低下，有时候还不如多查一次数据库来的快，所以需要根据实际情况下对sql语句禁行拆分，不用非得要求一次查询返回所有数据</p><p>8.如果主键字段是没有规律的，也可以使用避开MySQL自动回表，用主键字段 IN （SELECT 主键字段 FROM 表名 条件 LIMIT 分页参数）的方式也可以提示部分性能（这个方法不一定有用，不同的表结构和数据库版本结果都可能不同，建议先实际测试过之后确认是否采用），另外这2种办法都不适合联合主键的情况</p><p>9.group by 和 order by 的字段如果在索引里面，也是能命中索引的</p><p>10.索引合并查询，当执行语句需要使用多个索引时MySQL可以使用索引合并(<code>Index Merge</code>)来优化查询性能，当explain中出现了Using index merge，则说明使用了索引合并来查询</p><p>11.指定索引查询，当有多个索引可以使用时，MySQL会自动估算并选择合适的索引来进行查询，但是估算结果不一定正确，如果能确定使用其他索引速度肯定比MySQL选择的快，那么也是可以再查询语句中指定索引查询的</p><h3 id="优化索引"><a href="#优化索引" class="headerlink" title="优化索引"></a>优化索引</h3><p>1.优先使用联合索引，并且如果能确定字段组合的结果是唯一的，那么可以把类型设置为唯一或者主键索引。实际业务场景中，where条件肯定不止一个字段（根据主键查询的情况除外），所以联合索引能大大的提高索引的key_len,并且时覆盖索引查询的概率增加</p><p>2.创建合适数量的索引，索引并不是越多越好，每增加一个索引，会增加写入时间损耗，并且查询时也是也会影响MySQL估算成本的</p><p>3.索引字段尽量跟业务强相关，比如高频出现的查询条件所在的字段，尽可能的在索引里面</p><p>4.索引字段顺序跟业务强相关，首先字段顺序要满足一个就是大部分的查询能尽可能的命中索引的字段，同时，索引的字段顺序又要值得到的结果越少越好，比如用户表里面的性别，虽然性别筛选频率较高，但是如果把性别放在第一个就不合适，毕竟除了男就是女或者还有个未知，这样过滤出来的数据体量还是非常大，命中索引意义也不大</p><h3 id="插入的优化"><a href="#插入的优化" class="headerlink" title="插入的优化"></a>插入的优化</h3><ol><li>主键字段最好是有顺序的，这个跟B+树的数据结构有关，如果是乱序的，在每次插入的时候页分裂的时候，耗费性能会比顺序插入更大，建议查阅一下相关资料深入了解一下</li></ol><p>2.数据量大的情况下尽可能批量，MySQL原生支持批量插入，当我们业务数据量很大的时候，我们可以通过业务代码实现定时定量插入，比如当数据累积到500条的时候执行一次操作，当时间过了1秒还没有到500条的时候，也进行一次插入。这里需要自行控制好数量的尺度和时间尺度，当然不是所有数据都适合批量插入，并且批量插入会增加业务代码复杂性，毕竟需要除了数量和时间控制，还需要数据丢失的问题，但是如果你的数据量很大，批量插入也是无法避免的</p><h3 id="更新和删除的优化"><a href="#更新和删除的优化" class="headerlink" title="更新和删除的优化"></a>更新和删除的优化</h3><p>更新和删除的时，主要是需要尽可能的使用聚集索引的值来进行更新或者删除，也就是说最好通过主键来作为条件进行更新和删除</p><h3 id="分页的优化"><a href="#分页的优化" class="headerlink" title="分页的优化"></a>分页的优化</h3><p>分页优化，大数据量情况下，使用limit分页会导致越到后面性能越低下，所以也需要对分页本身做一些优化，</p><p>对排序字段创建索引，每次查询传上一页的最后一条数据得对应的值回来，查询时先where对比上一页最后一条的值，再直接limit 每页大小，如果排序字段是非主键或者是可以重复的，那么还需要带上对应的主键值，排序的时候则先通过排序字段排序，再通过主键字段排序，条件也加上主键的值</p><h3 id="表结构的优化"><a href="#表结构的优化" class="headerlink" title="表结构的优化"></a>表结构的优化</h3><p>1.合适的字段类型：遵循能用数值用数值，字符串长度能小就尽量小</p><p>2.合适的字段：参考数据库三大范式（只是参考，并不是完全符合三大范式），如果有些字段业务使用量很小的情况下，可以考虑拆分成2个表，一个表保存常用字段，一个表保存非常用字段</p><p>3.适当的冗余:很多时候很多数据必须要关联其他表来获取数据，也可以在表中添加部分冗余字段，避免查询时的链表查询</p><p>实际数据库设计中，既要参考三大范式，又要跟三大范式冲突，要结合业务来设计，在数据库存储量、写入性能损耗和操作性能中平衡取舍，没有最好的设计，只有更合适业务的设计，所有设计都应该是为业务服务</p><h3 id="架构的优化"><a href="#架构的优化" class="headerlink" title="架构的优化"></a>架构的优化</h3><p>1.读写分离：数据量大了之后，单机节点难免出现性能瓶颈，通过读写分离分散压力</p><p>2.分库分表：读写分离只能分摊读取压力，对于写没办法分摊（双主从也是没办法分摊的，因为最终所有数据都会写入2个主机），并且一个表数据量过于大之后，查询性能肯定直线下级，所以单库单表数据量已经达到性能临界值之后，就需要分库分表了</p><p>3.采用缓存：对于更新少，读取量很大的情况下，可以采用缓存的方式，降低对数据库的操作</p><p>4.引入搜索引擎（ES）:缓存没办法解决条件搜索，如果一个查询非常复杂，导致数据库搜索性能低下并且优化手段用尽效果也有限的情况下，可以引入ES等搜索引擎，将复杂条件的筛选放搜索引擎中执行从而加速查询速度</p><p>5.适当的归档：如果有些数据（比如一年前的数据），用的已经非常少了，那么可以考虑从数据库中移入其他表或者移入其他可以存储系统，这样可以保证对于热点数据可以快速的通过MySQL查询出来，而对于历史数据，也是可以查询的，只是速度稍微慢点</p>]]></content>
    
    
    <summary type="html">&lt;h2 id=&quot;%E5%89%8D%E8%A8%80&quot;&gt;&amp;#x524D;&amp;#x8A00;&lt;/h2&gt;&lt;p&gt;&amp;#x65E5;&amp;#x5E38;&amp;#x5DE5;&amp;#x4F5C;&amp;#x4E2D;&amp;#xFF0C;sql&amp;#x8BED;&amp;#x53E5;&amp;#x6267;&amp;#x884C;&amp;#x6548;&amp;#x7387;&amp;#x7ECF;&amp;#x5E38;&amp;#x6210;&amp;#x4E3A;&amp;#x7A0B;&amp;#x5E8F;&amp;#x7684;&amp;#x6027;&amp;#x80FD;&amp;#x74F6;&amp;#x9888;&amp;#xFF0C;&amp;#x5982;&amp;#x4F55;&amp;#x5FEB;&amp;#x901F;&amp;#x7684;&amp;#x6267;&amp;#x884C;&amp;#x6570;&amp;#x636E;&amp;#x5E93;&amp;#x7684;&amp;#x589E;&amp;#x5220;&amp;#x6539;&amp;#x67E5;&amp;#x5219;&amp;#x662F;&amp;#x4E00;&amp;#x4E2A;&amp;#x5408;&amp;#x683C;&amp;#x7684;&amp;#x540E;&lt;/p&gt;</summary>
    
    
    
    <category term="MySQL" scheme="https://raye.wang/categories/MySQL/"/>
    
    
  </entry>
  
  <entry>
    <title>简单项目一步一步进化到大量数据、大量并发架构优化方案</title>
    <link href="https://raye.wang/2022/01/11/%E7%AE%80%E5%8D%95%E9%A1%B9%E7%9B%AE%E4%B8%80%E6%AD%A5%E4%B8%80%E6%AD%A5%E8%BF%9B%E5%8C%96%E5%88%B0%E5%A4%A7%E9%87%8F%E6%95%B0%E6%8D%AE%E3%80%81%E5%A4%A7%E9%87%8F%E5%B9%B6%E5%8F%91%E6%9E%B6%E6%9E%84%E4%BC%98%E5%8C%96%E6%96%B9%E6%A1%88/"/>
    <id>https://raye.wang/2022/01/11/%E7%AE%80%E5%8D%95%E9%A1%B9%E7%9B%AE%E4%B8%80%E6%AD%A5%E4%B8%80%E6%AD%A5%E8%BF%9B%E5%8C%96%E5%88%B0%E5%A4%A7%E9%87%8F%E6%95%B0%E6%8D%AE%E3%80%81%E5%A4%A7%E9%87%8F%E5%B9%B6%E5%8F%91%E6%9E%B6%E6%9E%84%E4%BC%98%E5%8C%96%E6%96%B9%E6%A1%88/</id>
    <published>2022-01-11T10:21:00.000Z</published>
    <updated>2026-03-10T06:59:04.572Z</updated>
    
    <content type="html"><![CDATA[<h4 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h4><p>  软件的架构都是从小到大的，除非是一开始就知道软件需要承受载的数据量和并发量，并且有成熟且有经验的团队以及资源来支撑，否则其他的项目都是先做基本架构，做出功能后期再根据实际情况做架构重构优化，相信这是大部分公司的处理办法。毕竟软件前期过度优化，不仅浪费大量时间和资源，还有可能适得其反，搞不好做了一个能承受亿级流量（现在大家都喜欢说亿级流量，好像搞得不管那个公司都能有亿级流量一样，但是我也要蹭蹭热闹）的程序，最后撑死也就服务个百万人的程序，但是仅仅是这套程序的基本运行都需要大量服务器来支撑，最后活活耗死公司，毕竟不是所有公司都有大量时间和资源来烧的。所以个人觉得架构不是越复杂越好，而是要符合产品定位，以及合理的后续优化计划。</p><p>当然合理的架构不代表就是简单的架构，而是要基于可以使用的时间，数据存储量，以及预估的并发量来设计架构，当然所有的架构设计也需要考虑后期的优化或者重构，毕竟如果完全没有考虑，导致后期根本无法扩展或者重构（无法重构是指重构难度已经太大，接近重做的成本或超过重做成本），到时候只能重做那么也不算是合理的架构</p><p>另外，本文只是表达我在工作总结和记录的一些解决方案的思路，没有实际处理方法或者对应的代码，同时后面的步骤可以根据实际情况来处理，不按照顺序或者跳过某些也是可以的，毕竟没有完美和通用的架构，只有更合适的架构</p><h4 id="第一步：简单项目（单体项目-单机部署）"><a href="#第一步：简单项目（单体项目-单机部署）" class="headerlink" title="第一步：简单项目（单体项目+单机部署）"></a>第一步：简单项目（单体项目+单机部署）</h4><p>最简单也是开发最快捷的项目，所有东西都在同一个项目里面，一个基本的数据库，此时所有代码都在同一个项目里面（也有可能分了模块，但是最终打包成一个jar或者一个war包），部署也是单机部署，并发量不大，处理数据只要数据不算太多还是可以处理的，对于大部分公司初始项目，目前这种项目也是可以满足需求的</p><h6 id="可能会产生的问题："><a href="#可能会产生的问题：" class="headerlink" title="可能会产生的问题："></a>可能会产生的问题：</h6><p>1. 首先单体项目常见的问题，随着代码越来越多，编译打包和启动会越来越慢，如果是单机部署，中途停机的时间也更长（当前可以在半夜没啥人用系统的时候更新，降低影响）</p><p>2. 故障会导致停机，因为是单机部署 ，一旦服务器出现什么故障或者项目本身出现灾难性错误导致服务停止，那么所有人都用不了了</p><p>3. 并发量上升会导致体验不好，正常来说，这种架构不会数据量过大的问题，因为先会被并发卡住，一个tomcat默认是150个线程，在传统servlet环境下一个请求会是一个线程，当然可以使用Web Flux来进行开发，这样虽然不能加快相应速度，但是会降低线程使用量，从一定程度降低线程数量</p><h6 id="问题处理办法"><a href="#问题处理办法" class="headerlink" title="问题处理办法"></a>问题处理办法</h6><p>针对问题一没有啥太好的办法，听说新版maven配置个什么东西会构建很快，但是没有试过</p><p>问题二：服务器安装第三方监控软件，可以监控http相应和进程状态，如果有问题可以执行重启服务。</p><p>问题三：可以调大线程（具体上限根据服务器系统和配置不同而不同），但是一旦线程数量超过CPU的承载数量，速度也是非常慢的，毕竟大家也清楚，线程不是一直运行的，在CPU里面也是切换运行的，线程数量太多，会导致CPU大量时间会浪费在线程上下文切换上面，所以线程也不是越多越好，当然如果你服务器有1000个CPU，那么运行1000个线程也是搓搓有余，所以具体线程数量还是要根据服务器实际配置来调。除了线程，还可以配置其他jvm参数，调整堆栈内存大小，通过降低GC频次增加并发数量，当然增加服务器配置肯定也是其中一个办法，还有优化Java代码加快运算速度，以及大部分程序慢的根本原因：慢sql，sql优化也不能专死角，比如一条sql语句，可能关联了一堆的表来查询，当你各种优化索引和语句，发现速度还是提升不上去的时候，也可以考虑通过多次执行来处理。</p><h4 id="第二步：单体负载均衡（单体项目-多机部署）"><a href="#第二步：单体负载均衡（单体项目-多机部署）" class="headerlink" title="第二步：单体负载均衡（单体项目+多机部署）"></a>第二步：单体负载均衡（单体项目+多机部署）</h4><p>当一台服务器不管是增加配置还是优化设置，并发都已经会成为隐患的时候，或者投入的资源跟回报已经完全不成比例的时候，就需要进行第二步了，因为单台服务器已经被压榨到了经济极限的时候，就没有必须要考虑其他方案了。而第二步就是负载均衡，负载均衡能大幅度提升并发，同时也能提高可用性，并且开发效率也不错，所以如果是单体项目需要马上提升并发，可以先实现负载均衡，满足当下要求的同时，再继续改造</p><h6 id="可能会产生的问题：-1"><a href="#可能会产生的问题：-1" class="headerlink" title="可能会产生的问题："></a>可能会产生的问题：</h6><p>1. 定时任务会重复执行</p><p>2. 对于程序内部使用普通Java锁的情况可能会导致异常产生</p><p>3. 如果主键不是使用的数据库自增，且生成算法无法做到几乎不重复的话，会导致主键重复异常</p><h6 id="问题处理办法-1"><a href="#问题处理办法-1" class="headerlink" title="问题处理办法"></a>问题处理办法</h6><p>问题一：可以通过配置控制是否启用定时任务，保证只有其中一个服务启用定时任务，或者为了长期发展，定时任务单独出来，目前可以只是简单得提取定时任务</p><p>问题二：对于业务需要用锁的情况，可以采用数据库乐观锁的方式，当然也可以乘此机会引入redis，使用redis实现分布式锁或者引入zookeeper实现分布式锁（可以根据实际情况选择，但是redis到后期几乎所有项目都会用到，但是zookeeper就不一定会用上了）</p><p>问题三：使用数据库自增在目前是没有问题的，但是如果你的项目以后会有大量数据，可以考虑使用常见的算法来生成id，如雪花算法</p><h4 id="第三步：增加缓存机制"><a href="#第三步：增加缓存机制" class="headerlink" title="第三步：增加缓存机制"></a>第三步：增加缓存机制</h4><p>做了负载均衡，但是并发量大的时候，会有大量查询请求到达数据库，也会引起相应慢导致并发下降以及服务卡顿，这时候需要先降低数据库访问次数，所以需要用到缓存机制。对于经常查询但是不会经常变化的数据存入缓存（不会经常变化不是指不会变化，而是变化的频次跟查询的次数明显不成正比的情况，比如查询10次更新1次这种，具体要根据实际情况处理）。虽然增加缓存能有效降低数据库查询频率，但是缓存也会容易踩坑，以下是使用缓存常见的一些问题</p><h6 id="可能会产生的问题"><a href="#可能会产生的问题" class="headerlink" title="可能会产生的问题"></a>可能会产生的问题</h6><p>1. 缓存一致性，缓存一致性包括缓存与数据库数据一致性以及缓存集群以及多级缓存中出现数据不一致的问题</p><p>2. 缓存雪崩，简单来说就是缓存未命中或者缓存失效瞬间，或者大量缓存同一时间过期，导致大量请求进入到数据库，导致数据库崩溃从而影响服务。当然如果真的已经出现缓存雪崩的时候，那么那时候架构也到达一定程度了，当然也是需要有对应的处理方案，防止后期出现这种问题</p><p>3. 缓存击穿，当被大量的请求缓存不能命中，且数据库也无此数据时，会导致所有请求到数据库中，导致数据库崩溃从而影响服务，跟缓存雪崩不一样的是，缓存击穿情况出现后，除非请求量降下来，否则一直无法改善，因为缓存一直不会生效</p><h6 id="问题处理办法-2"><a href="#问题处理办法-2" class="headerlink" title="问题处理办法"></a>问题处理办法</h6><p>缓存的三个问题造成的原因有很多，不是1,2句话能说清楚的，并且需要根据代码实际情况来处理以及避免，所以相信网上很多文章写的比我详细和清楚，这里我就不写了，毕竟不是本文讨论的内容</p><h4 id="第四步：MySQL表分区"><a href="#第四步：MySQL表分区" class="headerlink" title="第四步：MySQL表分区"></a>第四步：MySQL表分区</h4><p>当并发到达一定程度时，数据库查询虽然通过缓存缓解了很多，但是写入可能会到达一个瓶颈，毕竟每个硬盘都是有一个写入速度的，加上数据越来越大，可能会导致硬盘不足的情况，这时候就需要使用MySQL表分区来缓解了，MySQL表分区之后可以把不同分区的数据放进不同的路径进行保存，不仅可以防止硬盘不够用，还可以将数据放到多个硬盘，增加写入速度。当然服务器不可能无限加硬盘，毕竟写入不仅仅是硬盘的事情 ,分区的规则需要根据实际情况来处理（因为目前个人主要是用MySQL，所以对其他数据库不是很了解，所以以MySQL为代表来写）</p><h6 id="可能会产生的问题-1"><a href="#可能会产生的问题-1" class="headerlink" title="可能会产生的问题"></a>可能会产生的问题</h6><p>使用表分区本身不会影响代码或者其他（可能只是我目前没有发现），说是问题，其实也只能说是MySQL表分区的一些限制吧。</p><p>1. 分区数量限制（1024）</p><p>2. 分区规则相关对来较为固定</p><p>3. 服务CPU和总线压力不能分散</p><p>4. 分区规则会受表索引影响</p><h4 id="第五步：分布式架构或者微服务架构"><a href="#第五步：分布式架构或者微服务架构" class="headerlink" title="第五步：分布式架构或者微服务架构"></a>第五步：分布式架构或者微服务架构</h4><p>如果采用分布式架构，首先团队成员得跟上，因为以前一人天的工作量，在分布式或者微服务的架构上，开发可能最少都要1.5人天，其次分布式也会带来很多新的挑战比如分布式事物，服务治理等等。但是也有好处，比如功能可以单独更新，而不影响其他，甚至可以灰度发布以及可以根据服务实际情况来调整实例数量，比如我觉得前端接口访问量是后台的10倍，那么按照之前，我不管前端接口还是后台接口都要部署多个，因为他们是在一起的，这样不仅浪费资源，而且还会因为前端接口的请求量从而影响到后台接口。这种情况下，我们可以把前后端接口分离成为2个项目，部署的时候也可以后台部署1台服务器，前台的部署5台服务器，不仅不会相互影响，如果某台服务器配置更高一点，还可以提高此服务器的实例的权重，让其处理更多的请求，从而实现资源最大化利用</p><h6 id="分布式还是微服务？"><a href="#分布式还是微服务？" class="headerlink" title="分布式还是微服务？"></a>分布式还是微服务？</h6><p>相信很多团队都会纠结这个问题，或者有些团队直接跟风，微服务火，那就上微服务就对了，为了方便大家做选择，我先说说我个人对于微服务和分布式的理解。首先分布式是为了分散压力，微服务是为了分散能力，微服务本身就是分布式的一种，不过微服务对于能力划分更为细致，常见的微服务正确来说是一个服务只提供一种能力，分布式对于异构支持相对来说较差（不同语言），而微服务对于异构支持能力较好。分布式主要是将服务分布部署，服务不能单独进行使用，而微服务则是将能力分布部署，可以独立使用。当然因为微服务的颗粒度更低，所以相对来说开发时长比分布式会更长一点，但是对于大型团队和大型项目来说，微服务后期的扩展可维护性会好很多</p><h6 id="可能会产生的问题-2"><a href="#可能会产生的问题-2" class="headerlink" title="可能会产生的问题"></a>可能会产生的问题</h6><p>1. 开发效率降低</p><p>2. 分布式事物问题</p><p>3. 分布式锁的问题</p><p>4. 唯一主键问题</p><p>5. 新的技术挑战和踩坑</p><p>6. 问题追踪更复杂，以前出现问题，直接查一个项目的日志就行了，并且日志有连续性，现在可能涉及到多个日志文件，且没有连续性</p><h6 id="问题处理办法-3"><a href="#问题处理办法-3" class="headerlink" title="问题处理办法"></a>问题处理办法</h6><p>问题一：规划设计，规范流程，快速跟进，当然如果你的团队实在不大，如果用了微服务或者分布式，可以按照实际情况来划分项目和服务，颗粒度粗一点，耦合度高一点，虽然都提倡低耦合，但是低耦合的代价是高工作量</p><p>问题二：尽可能的降低分布式生成的概率，比如同一个强事物流程尽可能写进一个服务中，当然如果无法避免，也有很多分布式事物处理方案，需要根据实际情况选择合适并且可以接受的方案，因为目前来说没有完美的分布式事务方案，常见的分布式服务框架有Seata</p><p>问题三和问题四之前已经说过</p><p>问题五可以加强团队内部培训和技术分享，以及采用成熟的技术方案来减轻，记得所有框架最好别用最新版的，毕竟人都是一样，容易写bug啊</p><p>问题六可以使用分布式日志系统以及搭配链路跟踪技术实现，方便排查问题</p><h4 id="第六步：异步处理"><a href="#第六步：异步处理" class="headerlink" title="第六步：异步处理"></a>第六步：异步处理</h4><p>截至目前，我们所有处理都是同步的，哪怕是分布式或者微服务中（当然里面也可以用异步调用），比如我们一个注册流程，除了需要在会员表里面写入一条数据之外，还需要赠送积分，然后赠送的积分又需要写入积分记录表，还需要统计当前时间片段注册人数以及查询配置的新用户送券，如果有配置还需要送券给用户，这就导致了我们的注册流程非常长，如果是微服务划分很细的话，可能涉及到一堆的服务，并且防止有问题，不同服务之间还需要使用分布式事物，并且服务之间相互调用还需要网络成本（内网也是需要的），所以会导致我们的注册时间很长。但是实际上我们注册本身只需要会员，其他都是附带的，那么我们就可以当写入会员成功之后，提交事物，返回成功，然后再异步调用各个服务。可以直接异步调用，也可以使用消息队列的方式实现，但是不论何种方式，一定需要容错机制，比如消息的持久化，异步的调用记录日志，防止其他服务出错时，导致数据最终有误，关键是当服务正常时，能自动恢复数据（因为你不可能回滚会员数据，只能按照规则送积分，送券等）。这样我们的注册流程本身会很快，尤其是当后期服务越来越复杂的时候，当然也肯定会产生一些新的问题</p><h6 id="可能会产生的问题-3"><a href="#可能会产生的问题-3" class="headerlink" title="可能会产生的问题"></a>可能会产生的问题</h6><p>1. 数据不一致，可能前端用户已经获取到了会员信息，但是积分信息还没有生成好</p><p>2. 数据丢失，如果异步调用或者消息队列没有做好持久化，并且服务出现某些不可控的异常的时候，可能会导致消息或事件丢失从而导致数据异常（丢失）</p><p>3. 增加了系统复杂性，从某种程度上来说，会增加系统的可用性，但是却会降低系统可靠性（或者叫准确性），因为注册会员主流程的动作变少了，所以就算积分或者优惠券服务不可用，但是不会影响数据，所以增加了系统可用性，但是一开始告诉别人注册送1000积分或者送100元优惠券，但是人家注册了却没有看到，所以又降低了可靠性，并且当系统的组件越来越多，复杂性也跟着长高了</p><p>4. 问题追踪更复杂</p><h6 id="问题处理办法-4"><a href="#问题处理办法-4" class="headerlink" title="问题处理办法"></a>问题处理办法</h6><p>问题一：加快处理速度，尽可能的再前端获取之前处理完成，同时前端也可以稍微延迟或者判断没有处理成功之前，做一些重新获取的操作，以及前期先隐藏部分数据或其他处理</p><p>问题二：加强持久化处理，比如异步调用则把持久化操作放进写入会员同一个事物中，消息队列则采用持久化队列且要确认送达，否则消息回退到队列中</p><p>问题三：尽量选择成熟稳定且大众化的处理方案，降低学习成本和踩坑</p><p>问题四参考上面</p><h4 id="第七步：读写分离"><a href="#第七步：读写分离" class="headerlink" title="第七步：读写分离"></a>第七步：读写分离</h4><p>虽然很多常用数据已经使用缓存来降低数据库的查询次数，但是不可避免还是会有很多查询进入数据库，以及很多查询可能只能进入数据库查询，长时间来说对于数据库压力也不小，所以采用读写分离，来将大部分查询转入从库，这样就算要做一些耗时的查询，也不用担心影响太大。读写分离可以采用中间件的方式或者代码式，中间件的方式可以在几乎不修改代码的方式下，实现读写分离，而代码式要么使用框架要么自己实现，某些框架也可以实现几乎不用改代码，但是不论如何，代码重新打包是肯定的。而中间件的方式相对来说性能不如代码式，且中间件的性能本身对系统影响颇大，所以具体采用哪种方案根据实际情况来处理</p><h6 id="可能会产生的问题-4"><a href="#可能会产生的问题-4" class="headerlink" title="可能会产生的问题"></a>可能会产生的问题</h6><p>1. 短时间数据不一致，当写入了之后马上查询，可能主库数据还未同步到从库上面，会导致查询到旧的数据或者根本没有查询到数据</p><h6 id="问题处理办法-5"><a href="#问题处理办法-5" class="headerlink" title="问题处理办法"></a>问题处理办法</h6><p>问题一：主从复制速度尽可能快，并且从库配置不要差主库太多，另外对于同一个请求中的写入后马上查询可以使用主库查询，当然对于某些数据，可以在写入后马上对缓存进行处理，但是需要注意缓存一致性，毕竟缓存和数据库操作本身不能依耐于事物处理</p><h4 id="第八步：搜索引擎"><a href="#第八步：搜索引擎" class="headerlink" title="第八步：搜索引擎"></a>第八步：搜索引擎</h4><p>当系统出现复杂搜索或者从大量数据中搜索以及全文检索的场景下，可以引入搜索引擎，当MySQL单表数量到千万级别时，就算有索引查询速度也会下降很多了，尤其是很多复杂场景，语句不一定都能命中索引（索引也不能太多，否则影响写入速度），而引入搜索引擎能近乎完美的处理大量数据查询的问题，同时很多分布式日志方案也是依耐于搜索引擎来实现的</p><h6 id="可能会产生的问题-5"><a href="#可能会产生的问题-5" class="headerlink" title="可能会产生的问题"></a>可能会产生的问题</h6><p>1. 链表查询支持较差，搜索引擎中没有表的概念，只有索引的概念，这里我们就把索引当成表，但是一般搜索引擎中都不支持2个索引关联查询</p><p>2. 数据延迟，常见的做法是在写入数据库之后，异步写入（异步线程或者消息队列）搜索引擎，或者通过读取数据库日志(主从复制的做法)，将数据库最新的数据写入搜索引擎中，这样就必然导致会产生一定的延迟</p><p>3. 学习成本</p><h6 id="问题处理方法"><a href="#问题处理方法" class="headerlink" title="问题处理方法"></a>问题处理方法</h6><p>问题一：常见的一些处理方法有客户端链接、提前聚合写入到搜索引擎等，具体可以查询网上的方法，选择合适自己的方法实现</p><p>问题二：通常做法是，强逻辑业务还是查询数据库，非强逻辑业务查搜索引擎，比如我如果要根据查询出来的数据生成或者执行某些操作，那么对于数据准确性要求非常高的，就是强逻辑业务，那么就从数据库查询。而对于一些数据准确性要求不算太高的，就算数据暂时不准确也不会影响后续操作或者造成不良影响的，属于非强逻辑业务。比如我只是单纯查询当前系统的销售额，虽然数据库里面的更准确，但是只是简单得查看而已，那么从搜索引擎中统计出来就没有关系，因为就算现在不准确，等下也会更新成为正确的，但是如果是双11的活动，我们在直播，要给大家看我们会在什么时候到达上亿营业额，那么就只能查询数据库，因为就算搜索引擎里面延迟1秒，也会造成一些影响（当然不可能真正的查询数据库，除非是不想让别人下单了，这里只是打个比方）。</p><p>问题三跟其他一样，加强内部培训和分享或者聘请专业人士和外援</p><h4 id="第九步：分库分表"><a href="#第九步：分库分表" class="headerlink" title="第九步：分库分表"></a>第九步：分库分表</h4><p>其实大部分公司的架构师一开始设计的时候就做好了分库分表了，之所以放到这里说实话是因为我本身还是非常抵触分库分表的，如果其他技术能满足要求我绝对是不会优先考虑分库分表的（毕竟不是每个软件定位就是服务于千万用户），之所以分库分表我是不到非得不已不考虑的原因就是：分库分表水太深，我怕把控不住，分库分表带来的附带影响实在太多。当然如果你的数据量以及写入量很大，那么分库分表又是必须要做的一步，毕竟一台数据库写入数据终究是有限的。当然不管怎么说，就算一开始不做分库分表，但是正常来说，只要后期有做分库分表的可能性，一般都会在前期表结构设计上，预留用于分库分表的字段，或者结构中某些字段能满足后期分库分表需求。而分库分表的好处也是明显可见的，不管是写入还是查询压力，都能合理的分散到不同服务器中去，当然前提是你的设计是合理的。分库分表跟读写分离一样，常见的方案有中间件式以及框架式，比如MyCat和<a href="https://github.com/go-pg/sharding">sharding</a>-JDBC。而因为分库分表会产生太多问题，以下只是简单罗列一下常见的问题</p><h6 id="可能会产生的问题-6"><a href="#可能会产生的问题-6" class="headerlink" title="可能会产生的问题"></a>可能会产生的问题</h6><p>1. 事物一致性</p><p>2. 唯一主键重复</p><p>3. 跨节点关联查询</p><p>4. 跨节点分页，排序，函数等问题</p><p>5. 迁移扩容</p><p>6. 框架或者技术问题，就我目前采用过mycat和<a href="https://github.com/go-pg/sharding">sharding</a>-JDBC都或多或有些问题或者说局限性</p><h6 id="问题处理方法-1"><a href="#问题处理方法-1" class="headerlink" title="问题处理方法"></a>问题处理方法</h6><p>以上大部分问题都不是一两句话能讲清楚的，建议在做分库分表之前，一定要提前了解能力和局限，以及常见问题的应对方案，另外就算暂时不做分库分表，但是后期可能要处理的，也最好从一开始就预留好相关字段，方便后期处理</p><h4 id="第十步：集群"><a href="#第十步：集群" class="headerlink" title="第十步：集群"></a>第十步：集群</h4><p>集群不是指服务集群，而是整个系统的集群方案，其中包括：服务集群、中间件集群、数据库集群、文件服务集群等。如常见的：zookeeper集群、nacos集群、redis集群、MySQL集群等。</p><p>集群可以能增强系统的处理能力、分散压力、提高可用性，当然集群也会带来部署成本、运维成本、系统复杂性的增加，而选择合适的集群模式也有助于降低成本</p><h4 id="其他细节"><a href="#其他细节" class="headerlink" title="其他细节"></a>其他细节</h4><p>面对大量并发的时候，除了查询数据要快（多级缓存+引擎+数据库分库分表以及优化），也要防止极端情况，比如大量并发的时候，就算你处理的再快，服务器也不能处理所有请求的时候，就必须要防止服务雪崩了，因为一旦其中一个环节或者服务被拖慢，可能导致整个系统被拖慢，但此时又有大量请求在涌入，从而导致所有人的请求都不能被处理，这时候就也就必须要做对应的处理来防止这种情况。常见的一些处理如下：</p><h6 id="限流熔断降级"><a href="#限流熔断降级" class="headerlink" title="限流熔断降级"></a>限流熔断降级</h6><p>简单来说首先预估服务的最大处理能力，可以通过压测来测试服务器的极限，然后根据极限设置合理的流量限制，但是注意实际情况可能比压测时更复杂，所以最好比测试的结果更低。限制流量后可以选择直接失败或者排队等处理，防止服务器承受超过服务本身能处理的请求量。同时也对服务器产生的异常进行监控，如果发现请求中出现的异常超过一定比例，或者相应速度明显变慢或慢响应达到一定比例，那么防止更多异常和卡顿，对服务进行熔断降级处理，可以直接一段时间对此服务器停止访问或者降低权重，使进入的流量降低，等恢复正常之后再恢复，防止服务雪崩。而sentinel可以实现上面的这些需求，但是真心吐槽一下，感觉阿里虽然开源了这个框架，并且这个框架虽然的确能力不错，但是怀疑它主要的目的是推广阿里的xxx服务，因为不管是官方文档还是demo还是其他版本兼容性，只能说要多简陋有多简陋。虽然限流熔断之后会导致部分用户无法正常使用，但是好过所有人都不能正常使用</p><h6 id="请求排队处理"><a href="#请求排队处理" class="headerlink" title="请求排队处理"></a>请求排队处理</h6><p>对于大量请求，如果处理不过来，但是又不想告诉客户失败，也可以使用消息队列来排队处理，比如之前常见的抢购解决方案，抢购者抢购信息先存入消息队列，就直接返回，然后等消息队列处理之后来查询结果，这样至少给客户的感觉是服务没有奔溃，而且服务本身的确没有奔溃</p><h6 id="加强版负载均衡"><a href="#加强版负载均衡" class="headerlink" title="加强版负载均衡"></a>加强版负载均衡</h6><p>之前虽然讲过部署多台服务器来实现负载均衡，但是在请求量不大的情况下，大部分功能的负载均衡可能都是nignx，或者好一点的是F5，但是即使如此，入口本身也有可能超过承载量，所以对于入口本身（nginx，F5）也需要使用到负载均衡，可以使用DNS负载均衡以及阿里的负载均衡服务来实现自建负载均衡的入口，避免入口服务器崩溃导致所有服务不能使用</p><h6 id="异地多活"><a href="#异地多活" class="headerlink" title="异地多活"></a>异地多活</h6><p>异地多活可以提高访问速度（客户从最近节点访问），也可以提高服务可用性，就算某地机房出现故障或者某机房内服务挂掉，其他机房也不受影响，异地多活中，每个区域的服务都是独立的系统，不依赖于任意其他区域的东西，但是数据又要相互同步以及防止相互冲突</p><h6 id="缓存预热"><a href="#缓存预热" class="headerlink" title="缓存预热"></a>缓存预热</h6><p>当使用了缓存，且缓存较大时，需要有对应的缓存预热方案，毕竟大部分缓存是基于内存的，一旦缓存重启或者发生其他问题，会导致大量缓存失效，从而大量请求落入数据库中，导致数据库奔溃。所以当缓存数据量较大时，一定先预热缓存，也就是当缓存中有一定数据量的时候再提供服务，并且缓存无法使用的时候，需要对数据库请求限流，防止太多请求落入数据库中，这里的数据库限流不是指控制数据库连接数量等，而是超过限制的流量应该直接失败，防止大量请求堆积造成服务雪崩事件。当然如果本身使用了多级缓存，并且缓存也做好了集群，一般来说没有这种极端情况产生，但是最好也要有对应的方案处理</p>]]></content>
    
    
    <summary type="html">&lt;h4 id=&quot;%E5%89%8D%E8%A8%80&quot;&gt;&amp;#x524D;&amp;#x8A00;&lt;/h4&gt;&lt;p&gt; &amp;#xA0; 		&amp;#x8F6F;&amp;#x4EF6;&amp;#x7684;&amp;#x67B6;&amp;#x6784;&amp;#x90FD;&amp;#x662F;&amp;#x4ECE;&amp;#x5C0F;&amp;#x5230;&amp;#x5927;&amp;#x7684;&amp;#xFF0C;&amp;#x9664;&amp;#x975E;&amp;#x662F;&amp;#x4E00;&amp;#x5F00;&amp;#x59CB;&amp;#x5C31;&amp;#x77E5;&amp;#x9053;&amp;#x8F6F;&amp;#x4EF6;&amp;#x9700;&amp;#x8981;&amp;#x627F;&amp;#x53D7;&amp;#x8F7D;&amp;#x7684;&amp;#x6570;&amp;#x636E;&amp;#x91CF;&amp;#x548C;&amp;#x5E76;&amp;#x53D1;&amp;#x91CF;&amp;#xFF0C;&amp;#x5E76;&amp;#x4E14;&amp;#x6709;&amp;#x6210;&amp;#x719F;&amp;#x4E14;&amp;#x6709;&amp;#x7ECF;&amp;#x9A8C;&lt;/p&gt;</summary>
    
    
    
    <category term="架构" scheme="https://raye.wang/categories/%E6%9E%B6%E6%9E%84/"/>
    
    
  </entry>
  
  <entry>
    <title>使用Percona xtrabackup对已在使用的MySQL做主从备份</title>
    <link href="https://raye.wang/2021/06/24/%E4%BD%BF%E7%94%A8Percona-xtrabackup%E5%AF%B9%E5%B7%B2%E5%9C%A8%E4%BD%BF%E7%94%A8%E7%9A%84MySQL%E5%81%9A%E4%B8%BB%E4%BB%8E%E5%A4%87%E4%BB%BD/"/>
    <id>https://raye.wang/2021/06/24/%E4%BD%BF%E7%94%A8Percona-xtrabackup%E5%AF%B9%E5%B7%B2%E5%9C%A8%E4%BD%BF%E7%94%A8%E7%9A%84MySQL%E5%81%9A%E4%B8%BB%E4%BB%8E%E5%A4%87%E4%BB%BD/</id>
    <published>2021-06-24T08:44:21.000Z</published>
    <updated>2026-03-10T06:59:04.571Z</updated>
    
    <content type="html"><![CDATA[<h4 id="主库配置"><a href="#主库配置" class="headerlink" title="主库配置"></a>主库配置</h4><p>首先要配置主库服务器的相关主从备份的配置，比如配置启用binlog日志等，涉及到配置修改如下，修改my.conf文件</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">[mysqld]</span><br><span class="line">## 设置server_id，一般设置为IP,注意要唯一</span><br><span class="line">server_id=100</span><br><span class="line">## 复制过滤：也就是指定哪个数据库不用同步（mysql库一般不同步）</span><br><span class="line">binlog-ignore-db=mysql</span><br><span class="line">## 开启二进制日志功能，可以随便取，最好有含义（关键就是这里了）</span><br><span class="line">log-bin=mysql-bin</span><br><span class="line">## 为每个session 分配的内存，在事务过程中用来存储二进制日志的缓存</span><br><span class="line">binlog_cache_size=1M</span><br><span class="line">## 主从复制的格式（mixed,statement,row，默认格式是statement）</span><br><span class="line">binlog_format=mixed</span><br><span class="line">## 二进制日志自动删除/过期的天数。默认值为0，表示不自动删除。</span><br><span class="line">expire_logs_days=7</span><br><span class="line">## 跳过主从复制中遇到的所有错误或指定类型的错误，避免slave端复制中断。</span><br><span class="line">## 如：1062错误是指一些主键重复，1032错误是因为主从数据库数据不一致</span><br><span class="line">slave_skip_errors=1062</span><br></pre></td></tr></table></figure><p>以上是主库做主从备份需要的一些配置，每个配置的已有详细说明，关于MySQL主从备份的一些基础概念和配置方法，详细可以参考**<a href="http://raye.wang/mysqlzhu-cong-fu-zhi-da-jian-ji-yu-ri-zhi-binlog/">MySQL主从复制搭建，基于日志（binlog）</a>**</p><h4 id="初识Percona-xtrabackup"><a href="#初识Percona-xtrabackup" class="headerlink" title="初识Percona xtrabackup"></a>初识Percona xtrabackup</h4><p>Xtrabackup是由percona开源的免费数据库热备份软件，它能对InnoDB数据库和XtraDB存储引擎的数据库非阻塞地备份（对于MyISAM的备份同样需要加表锁）。</p><p>为什么要用Xtrabackup？因为传统的mysqldump备份方式是采用的逻辑备份，其最大的缺陷是备份和恢复速度较慢，如果数据库很大，mysqldump备份就不太适合，并且mysqldump备份时会对数据库运行造成一定的影响，并且xtrabackup还可以进行增量备份，可以降低备份数据大小和每日备份时间。</p><p>（就算做了增量备份，也一定要定时全量备份）</p><h4 id="安装Percona-xtrabackup"><a href="#安装Percona-xtrabackup" class="headerlink" title="安装Percona xtrabackup"></a>安装Percona xtrabackup</h4><p>进入<a href="https://www.percona.com/downloads/">官网下载页面</a>，选择Percona XtraBackup，根据自己的os选择对应的版本下载，如我是centos7</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">wget https://downloads.percona.com/downloads/Percona-XtraBackup-2.4/Percona-XtraBackup-2.4.22/binary/redhat/7/x86_64/percona-xtrabackup-24-2.4.22-1.el7.x86_64.rpm</span><br></pre></td></tr></table></figure><p>下载完成后使用rpm命令安装，可能会出现缺少依赖无法安装的情况，需要安装相关依赖，如我安装时提示缺少依赖</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">libev.so.4</span><br><span class="line">perl(DBD::mysql)</span><br><span class="line">perl(Digest::MD5)</span><br><span class="line">rsync</span><br></pre></td></tr></table></figure><p>于是首先安装Libev.so依赖，去<a href="http://rpm.pbone.net/index.php3/stat/3/srodzaj/1/search/libev.so.4">http://rpm.pbone.net/index.php3/stat/3/srodzaj/1/search/libev.so.4</a> 下载对应系统的安装包，并安装</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">wget ftp://ftp.pbone.net/mirror/ftp5.gwdg.de/pub/opensuse/repositories/home:/rudi_m/CentOS_7/x86_64/libev4-4.24-8.1.x86_64.rpm</span><br></pre></td></tr></table></figure><p>然后安装，perl相关依赖，以下命令是从网上找的，我也不清楚是不是要安装这么多</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">yum -y install perl perl-devel libaio libaio-devel perl-Time-HiRes perl-DBD-MySQL rsync perl l perl-Digest-MD5</span><br></pre></td></tr></table></figure><p>安装完成之后运行安装Percona xtrabackup命令，安装成功</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">rpm -ivh percona-xtrabackup-24-2.4.22-1.el7.x86_64.rpm</span><br></pre></td></tr></table></figure><h4 id="备份"><a href="#备份" class="headerlink" title="备份"></a>备份</h4><p>执行命令</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">innobackupex --defaults-file=/etc/my.cnf --socket=/var/lib/mysql/mysql.sock --user=数据库用户名 --password=用户密码 --parallel=4 /usr/local/mysql_back</span><br></pre></td></tr></table></figure><p>其中&#x2F;etc&#x2F;my.cnf为当前数据库配置文件地址，&#x2F;var&#x2F;lib&#x2F;mysql&#x2F;mysql.sock  为当前数据库sock文件地址，需要根据其实情况配置，&#x2F;usr&#x2F;local&#x2F;mysql_back为备份文件存放目录，–parallel&#x3D;4为开启4个子进程并发备份多个数据文件（一个数据文件只有一个进程进行备份）</p><p>通过innobackupex备份MySQL，本质是通过文件复制实现的，但是复制过程中，可能会有事物未提交，所以备份完成之后需要再次将尚未提交的事务或已经提交但尚未同步至数据文件中的事务进行相关处理，执行命令</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">innobackupex --defaults-file=/etc/my.cnf --socket=/var/lib/mysql/mysql.sock --user=数据库用户名 --password=用户密码 --apply-log /usr/local/mysql_back/2021-06-24_13-59-56</span><br></pre></td></tr></table></figure><p>其中后面的2021-06-24_13-59-56是指上个命名备份出来的目录，要根据实际备份出来的文件夹名称修改</p><h4 id="复制备份文件夹"><a href="#复制备份文件夹" class="headerlink" title="复制备份文件夹"></a>复制备份文件夹</h4><p>备份完成之后，需要将备份文件夹复制到从库服务器，使用scp命令可以直接传到从库服务器</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">scp -r /usr/local/mysql_back/2021-06-24_13-59-56 ip:目录</span><br></pre></td></tr></table></figure><p>其中&#x2F;usr&#x2F;local&#x2F;mysql_back&#x2F;2021-06-24_13-59-56是本地目录地址，需要根据实际情况配置，ip是指从库的IP地址，这里用内网和外网IP都可以，目录为从库服务的目录，根据实际情况选择即可</p><p>如果从库没有固定IP或者觉得这一步比较麻烦，可以采用远程备份的方式，直接再从库服务器上面使用innobackupex备份，但是innobackupex不能直接采用异地备份，只能采取ssh密匙认证传输，因为有点麻烦，所以我就直接在主库备份然后传到从库了，有兴趣的同学可以自己研究一下</p><h4 id="恢复备份到从库"><a href="#恢复备份到从库" class="headerlink" title="恢复备份到从库"></a>恢复备份到从库</h4><p>备份文件夹复制完成之后，需要先恢复到从库，首先再从库服务器上面安装好跟主库同版本的MySQL（最好版本一致，防止出现问题），安装完成之后再安装Percona xtrabackup，步骤跟主库上面的步骤一致，从库的MySQL先不启动，直接执行恢复命令</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">innobackupex --defaults-file=/etc/my.cnf --copy-back /备份文件夹目录</span><br></pre></td></tr></table></figure><p>恢复的同时可以去修改从库MySQL的配置文件，尤其是当数据量比较大的时候，恢复也是需要一定时间的，首先在从库my.conf配置以下内容</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line">[mysqld]</span><br><span class="line">## 设置server_id，一般设置为IP,注意要唯一</span><br><span class="line">server_id=101</span><br><span class="line">## 复制过滤：也就是指定哪个数据库不用同步（mysql库一般不同步）</span><br><span class="line">binlog-ignore-db=mysql</span><br><span class="line">## 开启二进制日志功能，以备Slave作为其它Slave的Master时使用</span><br><span class="line">log-bin=edu-mysql-slave1-bin</span><br><span class="line">## 为每个session 分配的内存，在事务过程中用来存储二进制日志的缓存</span><br><span class="line">binlog_cache_size=1M</span><br><span class="line">## 主从复制的格式（mixed,statement,row，默认格式是statement）</span><br><span class="line">binlog_format=mixed</span><br><span class="line">## 二进制日志自动删除/过期的天数。默认值为0，表示不自动删除。</span><br><span class="line">expire_logs_days=7</span><br><span class="line">## 跳过主从复制中遇到的所有错误或指定类型的错误，避免slave端复制中断。</span><br><span class="line">## 如：1062错误是指一些主键重复，1032错误是因为主从数据库数据不一致</span><br><span class="line">slave_skip_errors=1062</span><br><span class="line">## relay_log配置中继日志</span><br><span class="line">relay_log=edu-mysql-relay-bin</span><br><span class="line">## log_slave_updates表示slave将复制事件写进自己的二进制日志</span><br><span class="line">log_slave_updates=1</span><br><span class="line">## 防止改变数据(除了特殊的线程)</span><br><span class="line">read_only=1</span><br></pre></td></tr></table></figure><p>恢复后重启MySQL，注意先检查MySQL的文件目录权限问题，因为我是用的root用户恢复，所以文件夹里面的文件都归属root，导致MySQL启动失败失败,需要先把MySQL的数据目录所属分组分配给MySQL用户，执行命令</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">chown -R mysql:mysql MySQL数据目录</span><br></pre></td></tr></table></figure><h4 id="创建主库复制账号"><a href="#创建主库复制账号" class="headerlink" title="创建主库复制账号"></a>创建主库复制账号</h4><p>需要先再主库上面创建一个从库用来复制数据的账号</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">grant replication slave,replication client on *.* to slaveuser@&#x27;从库IP&#x27; identified by &#x27;密码&#x27;;</span><br><span class="line">flush privileges;</span><br></pre></td></tr></table></figure><p>这里我们创建的用户名是slaveuser，指定了只有从库能链接，如果多个从库，并且从库在同一个网段，可以IP最后一个.后面跟%分号，如果不是同一网段，可以针对每个从库创建一个同用户名但是不用Host的用户，建议密码都一直方便维护，不建议用%，测试环境和自己做测试用%没有关系，但是生产环境最好别用%来授权，防止数据泄露</p><h4 id="开启主从复制"><a href="#开启主从复制" class="headerlink" title="开启主从复制"></a>开启主从复制</h4><p>首先去备份文件去看当前的备份的日志偏移量，复制需要根据偏移量来复制，常规情况下（从数据库还未正式使用的情况下），我们会从主库去获取这个偏移量，但是由于我们现在是主库还是一直在运行这，业务也一直在正常的写入和更新，所以主库的偏移量已经不适用我们的从库，所以要从备份文件里面去看，进入到备份文件夹目录（有年月日那个）,执行命令</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">more xtrabackup_binlog_info</span><br></pre></td></tr></table></figure><p>会返回日志名字和偏移量类似于下面这种</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">mysql-bin.xxxx        411xxxx</span><br></pre></td></tr></table></figure><p>其中mysql-bin.xxxx是日志文件的名字，411xxxx是我们偏移量</p><p>有了日志文件名字和偏移量之后，再从库MySQL里面执行</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">change master to master_host=&#x27;主库ip&#x27;, master_user=&#x27;复制用户&#x27;, master_password=&#x27;复制密码&#x27;, master_port=3306, master_log_file=&#x27;mysql-bin.xxxx&#x27;, master_log_pos=411xxxx, master_connect_retry=30;</span><br></pre></td></tr></table></figure><p>上面执行的命令的解释：</p><p>master_host&#x3D;’主库ip’                  ## Master的IP地址</p><p>master_user&#x3D;’复制用户’                             ## 用于同步数据的用户（在Master中授权的用户）</p><p>master_password&#x3D;’复制密码’     ## 同步数据用户的密码</p><p>master_port&#x3D;3306                              ## Master数据库服务的端口</p><p>master_log_file&#x3D;’mysql-bin.xxxx’        ##指定Slave从哪个日志文件开始读复制数据（Master上执行命令的结果的File字段）</p><p>master_log_pos&#x3D;411xxxx                         ## 从哪个POSITION号开始读（Master上执行命令的结果的Position字段）</p><p>master_connect_retry&#x3D;30  ##当重新建立主从连接时，如果连接建立失败，间隔多久后重试。单位为秒，默认设置为60秒，同步延迟调优参数。</p><p>成功之后继续执行</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">start slave;</span><br></pre></td></tr></table></figure><p>成功之后可以执行</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">show slave status;</span><br></pre></td></tr></table></figure><p>查看当前复制状态，也可以查询不同表的数据对比复制是否正常，如果数据库较大，做从库时间过长，那么需要等一段时间才能做对比，因为一般数据库很大的情况下，数据写入也非常大，从库要同步过来本来也就需要不少时间，尤其是一般从库性能跟主库还差了几个量级的情况下，会造成一定的延迟，可以通过查看复制状态返回的Relay_Master_Log_File和Exec_Master_Log_Pos来判断当前复制到了那里，以及估算复制速度</p>]]></content>
    
    
    <summary type="html">&lt;h4 id=&quot;%E4%B8%BB%E5%BA%93%E9%85%8D%E7%BD%AE&quot;&gt;&amp;#x4E3B;&amp;#x5E93;&amp;#x914D;&amp;#x7F6E;&lt;/h4&gt;&lt;p&gt;	&amp;#x9996;&amp;#x5148;&amp;#x8981;&amp;#x914D;&amp;#x7F6E;&amp;#x4E3B;&amp;#x5E93;&amp;#x670D;&amp;#x52A1;&amp;#x5668;&amp;#x7684;&amp;#x76F8;&amp;#x5173;&amp;#x4E3B;&amp;#x4ECE;&amp;#x5907;&amp;#x4EFD;&amp;#x7684;&amp;#x914D;&amp;#x7F6E;&amp;#xFF0C;&amp;#x6BD4;&amp;#x5982;&amp;#x914D;&amp;#x7F6E;&amp;#x542F;&amp;#x7528;binlog&amp;#x65E5;&amp;#x5FD7;&amp;#x7B49;&amp;#xFF0C;&amp;#x6D89;&amp;#x53CA;&amp;#x5230;&amp;#x914D;&amp;#x7F6E;&amp;#x4FEE;&amp;#x6539;&amp;#x5982;&amp;#x4E0B;&amp;#xFF0C;&amp;#x4FEE;&amp;#x6539;my.conf&amp;&lt;/p&gt;</summary>
    
    
    
    <category term="MySQL" scheme="https://raye.wang/categories/MySQL/"/>
    
    
    <category term="Mysql" scheme="https://raye.wang/tags/Mysql/"/>
    
  </entry>
  
  <entry>
    <title>Springcloud+Seata+nacos 分布式事务项目搭建 AT模式</title>
    <link href="https://raye.wang/2021/02/26/Springcloud-Seata-nacos-%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1%E9%A1%B9%E7%9B%AE%E6%90%AD%E5%BB%BA-AT%E6%A8%A1%E5%BC%8F/"/>
    <id>https://raye.wang/2021/02/26/Springcloud-Seata-nacos-%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1%E9%A1%B9%E7%9B%AE%E6%90%AD%E5%BB%BA-AT%E6%A8%A1%E5%BC%8F/</id>
    <published>2021-02-26T08:26:00.000Z</published>
    <updated>2026-03-10T06:59:04.571Z</updated>
    
    <content type="html"><![CDATA[<h4 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h4><p>本文要求对Springcloud有一定了解，对分布式事务本身有一定认知，如果不了解微服务，建议先看看<a href="http://raye.wang/spring-cloudde-ji-ben-ren-shi-he-shi-yong-spring-cloudde-ji-ben-jiao-cheng/">Spring Cloud的基本认识和使用Spring Cloud的基本教程</a>,再回头学习本文</p><h4 id="为什么会出现分布式事务"><a href="#为什么会出现分布式事务" class="headerlink" title="为什么会出现分布式事务"></a>为什么会出现分布式事务</h4><p>开发过程中，随着项目模块的增加以及分库分表的出现，传统事务已经无法满足业务需求，如分库，由于有多个数据源，而数据库事务又是基于数据库层，所以如果只用数据库原生事务，会导致数据库A成功提交，数据库B回滚，导致数据不一致，又比如多模块下，常见的订单流程，订单服务成功提交订单，调用库存服务扣减库存，由于是链式调用，库存成功扣减，然后回到订单服务时，出现异常，导致订单回滚，但是此时库存却未回滚，也会导致数据不一致，所以这些情况都需要分布式事务来解决这个问题（当然一般开发中，我们常用的做法是能避免就尽量避免，实在避免不了才使用分布式事务，因为分布式事务不管怎么样，性能，一致性，原子性等都会收到影响）</p><h4 id="分布式事务目前的几种方案"><a href="#分布式事务目前的几种方案" class="headerlink" title="分布式事务目前的几种方案"></a>分布式事务目前的几种方案</h4><p><strong>2PC（二阶段提交）</strong></p><p><strong>3PC（三阶段提交）</strong></p><p><strong>TCC(Try - Confirm - Cancel)</strong></p><p><strong>最终一致性（消息队列等方式）</strong></p><p><strong>最大努力通知（数据不要求强一致）</strong></p><p>每种方案都各有优劣，具体采用何种方案还需要根据实际业务场景来使用</p><h4 id="初识Seata"><a href="#初识Seata" class="headerlink" title="初识Seata"></a>初识Seata</h4><p>Seata 是一款阿里巴巴开源的分布式事务解决方案，致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式，为用户打造一站式的分布式解决方案。详情可以去查看<a href="http://seata.io/zh-cn/docs/overview/what-is-seata.html">seata官方文档</a></p><h4 id="初始Nacos"><a href="#初始Nacos" class="headerlink" title="初始Nacos"></a>初始Nacos</h4><p>Nacos 是一款阿里巴巴开源的服务注册中心，配置中心，管理中心，以下是nacos官网简介：</p><p>Nacos 致力于帮助您发现、配置和管理微服务。Nacos 提供了一组简单易用的特性集，帮助您快速实现动态服务发现、服务配置、服务元数据及流量管理。</p><p>Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。 Nacos 是构建以“服务”为中心的现代应用架构 (例如微服务范式、云原生范式) 的服务基础设施。</p><p>我们可以把nacos当作Eureka +Srpingcloud config的结合体，详情可以查去查看<a href="https://nacos.io/zh-cn/docs/what-is-nacos.html">nacos官方文档</a></p><h4 id="下载并部署Nacos"><a href="#下载并部署Nacos" class="headerlink" title="下载并部署Nacos"></a>下载并部署Nacos</h4><p>Nacos跟Eureka 不一样，Eureka是创建一个Eureka 的项目即可，但是Nacos需要专门下载Nacos的服务端，可以从<a href="https://github.com/alibaba/nacos/releases">下载地址</a>去下载最新的稳定版，目前编写本文时最新版为2.0.0-ALPHA.2，下载好之后解压，进入bin目录，并根据系统执行相关的脚本启动服务，记得脚本后面加上参数</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">-m standalone</span><br></pre></td></tr></table></figure><p>standalone代表着单机模式运行，非集群模式,由于本文只讲解单机模式，所以不用集群模式，集群模式还要配置其他东西,启动成功后浏览器访问<a href="http://localhost:8848/nacos">http://localhost:8848/nacos</a>,能看到登录界面，输入默认账号密码nacos（账号密码都是nacos），能进入首页，如下图  </p><p><img src="https://upload-images.jianshu.io/upload_images/1783810-f2b212437a0a1a88.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="nacos图片"></p><h4 id="下载并部署Seata"><a href="#下载并部署Seata" class="headerlink" title="下载并部署Seata"></a>下载并部署Seata</h4><p><strong>下载并配置</strong></p><p>从 <a href="https://github.com/seata/seata/releases">https://github.com/seata/seata/releases</a>,下载服务器软件包，将其解压缩,本文编写时最新版为1.4.0,进入conf目录，编辑registry.conf，配置Seata的registry和config为nacos模式，同时配置nacos相关配置，首先修改registry(注册中心)节点下的type值为nacos，然后配置register节点下的nacos节点。</p><p>旧配置：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">nacos &#123;</span><br><span class="line">  application = &quot;seata-server&quot;</span><br><span class="line">  serverAddr = &quot;127.0.0.1:8848&quot;</span><br><span class="line">  group = &quot;SEATA_GROUP&quot;</span><br><span class="line">  namespace = &quot;&quot;</span><br><span class="line">  cluster = &quot;default&quot;</span><br><span class="line">  username = &quot;&quot;</span><br><span class="line">  password = &quot;&quot;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>新配置</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">nacos &#123;</span><br><span class="line">  application = &quot;seata-server&quot;</span><br><span class="line">  serverAddr = &quot;127.0.0.1:8848&quot;</span><br><span class="line">  group = &quot;SEATA_GROUP&quot;</span><br><span class="line">  namespace = &quot;public&quot;</span><br><span class="line">  cluster = &quot;default&quot;</span><br><span class="line">  username = &quot;nacos&quot;</span><br><span class="line">  password = &quot;nacos&quot;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>application 为Seata-server注册到nacos的应用名称，serverAddr 为nacos的地址，group为注册到nacos的应用分组，要使用seata，必须要seata-server跟其他使用使用seata分布式事务的应用在同一分组才行，namespace 为命名空间，我们使用默认的public，当然也可以创建其他命名空间来使用，username和password对应的事nacos的账号密码</p><p>修改config（配置中心）节点下type为nacos，同时修改config节点下的nacos节点</p><p>修改前配置</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">nacos &#123;</span><br><span class="line">    serverAddr = &quot;127.0.0.1:8848&quot;</span><br><span class="line">    namespace = &quot;&quot;</span><br><span class="line">    group = &quot;SEATA_GROUP&quot;</span><br><span class="line">    username = &quot;&quot;</span><br><span class="line">    password = &quot;&quot;</span><br><span class="line">  &#125;</span><br></pre></td></tr></table></figure><p>修改后</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">nacos &#123;</span><br><span class="line">  serverAddr = &quot;127.0.0.1:8848&quot;</span><br><span class="line">  namespace = &quot;public&quot;</span><br><span class="line">  group = &quot;SEATA_GROUP&quot;</span><br><span class="line">  username = &quot;nacos&quot;</span><br><span class="line">  password = &quot;nacos&quot;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>添加Seata配置</strong><br>进入nacos里面，添加一个事务分组的配置（常规来说要添加很多配置，不过只要添加了事务分组配置，就可以满足基本运行要求）</p><p>点击添加按钮新增一个配置</p><p><img src="https://upload-images.jianshu.io/upload_images/1783810-5c3a83ac82ec2378.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="添加按钮"></p><p>添加如下配置：  </p><p><img src="https://upload-images.jianshu.io/upload_images/1783810-85c8901b663b6864.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="添加配置"></p><p>其中配置名称</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">service.vgroupMapping.my_test_tx_group</span><br></pre></td></tr></table></figure><p>中的my_test_tx_group是我们自定义的名称，到时候需要配置项目中，可以根据实际情况配置，Group一定要跟之前Seata中配置的group对应上，内容default要跟之前Seata中的register节点下的cluster对应上，点击发布保存配置</p><h4 id="完整Seata配置（此步骤非必须）"><a href="#完整Seata配置（此步骤非必须）" class="headerlink" title="完整Seata配置（此步骤非必须）"></a>完整Seata配置（此步骤非必须）</h4><p>以上配置只是实现分布式事务的最基本的配置，如果想要配置所有支持的参数配置，可以从<a href="https://github.com/seata/seata/blob/develop/script/config-center/config.txt">github下载</a>配置文件config.txt,或者创建config.txt文件并复制写入下面代码（下面代码不是实时更新，所以最好还是去github下载）</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br></pre></td><td class="code"><pre><span class="line">transport.type=TCP</span><br><span class="line">transport.server=NIO</span><br><span class="line">transport.heartbeat=true</span><br><span class="line">transport.enableClientBatchSendRequest=false</span><br><span class="line">transport.threadFactory.bossThreadPrefix=NettyBoss</span><br><span class="line">transport.threadFactory.workerThreadPrefix=NettyServerNIOWorker</span><br><span class="line">transport.threadFactory.serverExecutorThreadPrefix=NettyServerBizHandler</span><br><span class="line">transport.threadFactory.shareBossWorker=false</span><br><span class="line">transport.threadFactory.clientSelectorThreadPrefix=NettyClientSelector</span><br><span class="line">transport.threadFactory.clientSelectorThreadSize=1</span><br><span class="line">transport.threadFactory.clientWorkerThreadPrefix=NettyClientWorkerThread</span><br><span class="line">transport.threadFactory.bossThreadSize=1</span><br><span class="line">transport.threadFactory.workerThreadSize=default</span><br><span class="line">transport.shutdown.wait=3</span><br><span class="line">service.vgroupMapping.my_test_tx_group=default</span><br><span class="line">service.default.grouplist=127.0.0.1:8091</span><br><span class="line">service.enableDegrade=false</span><br><span class="line">service.disableGlobalTransaction=false</span><br><span class="line">client.rm.asyncCommitBufferLimit=10000</span><br><span class="line">client.rm.lock.retryInterval=10</span><br><span class="line">client.rm.lock.retryTimes=30</span><br><span class="line">client.rm.lock.retryPolicyBranchRollbackOnConflict=true</span><br><span class="line">client.rm.reportRetryCount=5</span><br><span class="line">client.rm.tableMetaCheckEnable=false</span><br><span class="line">client.rm.tableMetaCheckerInterval=60000</span><br><span class="line">client.rm.sqlParserType=druid</span><br><span class="line">client.rm.reportSuccessEnable=false</span><br><span class="line">client.rm.sagaBranchRegisterEnable=false</span><br><span class="line">client.tm.commitRetryCount=5</span><br><span class="line">client.tm.rollbackRetryCount=5</span><br><span class="line">client.tm.defaultGlobalTransactionTimeout=60000</span><br><span class="line">client.tm.degradeCheck=false</span><br><span class="line">client.tm.degradeCheckAllowTimes=10</span><br><span class="line">client.tm.degradeCheckPeriod=2000</span><br><span class="line">store.mode=file</span><br><span class="line">store.publicKey=</span><br><span class="line">store.file.dir=file_store/data</span><br><span class="line">store.file.maxBranchSessionSize=16384</span><br><span class="line">store.file.maxGlobalSessionSize=512</span><br><span class="line">store.file.fileWriteBufferCacheSize=16384</span><br><span class="line">store.file.flushDiskMode=async</span><br><span class="line">store.file.sessionReloadReadSize=100</span><br><span class="line">store.db.datasource=druid</span><br><span class="line">store.db.dbType=mysql</span><br><span class="line">store.db.driverClassName=com.mysql.jdbc.Driver</span><br><span class="line">store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true&amp;rewriteBatchedStatements=true</span><br><span class="line">store.db.user=username</span><br><span class="line">store.db.password=password</span><br><span class="line">store.db.minConn=5</span><br><span class="line">store.db.maxConn=30</span><br><span class="line">store.db.globalTable=global_table</span><br><span class="line">store.db.branchTable=branch_table</span><br><span class="line">store.db.queryLimit=100</span><br><span class="line">store.db.lockTable=lock_table</span><br><span class="line">store.db.maxWait=5000</span><br><span class="line">store.redis.mode=single</span><br><span class="line">store.redis.single.host=127.0.0.1</span><br><span class="line">store.redis.single.port=6379</span><br><span class="line">store.redis.maxConn=10</span><br><span class="line">store.redis.minConn=1</span><br><span class="line">store.redis.maxTotal=100</span><br><span class="line">store.redis.database=0</span><br><span class="line">store.redis.password=</span><br><span class="line">store.redis.queryLimit=100</span><br><span class="line">server.recovery.committingRetryPeriod=1000</span><br><span class="line">server.recovery.asynCommittingRetryPeriod=1000</span><br><span class="line">server.recovery.rollbackingRetryPeriod=1000</span><br><span class="line">server.recovery.timeoutRetryPeriod=1000</span><br><span class="line">server.maxCommitRetryTimeout=-1</span><br><span class="line">server.maxRollbackRetryTimeout=-1</span><br><span class="line">server.rollbackRetryTimeoutUnlockEnable=false</span><br><span class="line">client.undo.dataValidation=true</span><br><span class="line">client.undo.logSerialization=jackson</span><br><span class="line">client.undo.onlyCareUpdateColumns=true</span><br><span class="line">server.undo.logSaveDays=7</span><br><span class="line">server.undo.logDeletePeriod=86400000</span><br><span class="line">client.undo.logTable=undo_log</span><br><span class="line">client.undo.compress.enable=true</span><br><span class="line">client.undo.compress.type=zip</span><br><span class="line">client.undo.compress.threshold=64k</span><br><span class="line">log.exceptionRate=100</span><br><span class="line">transport.serialization=seata</span><br><span class="line">transport.compressor=none</span><br><span class="line">metrics.enabled=false</span><br><span class="line">metrics.registryType=compact</span><br><span class="line">metrics.exporterList=prometheus</span><br><span class="line">metrics.exporterPrometheusPort=9898</span><br></pre></td></tr></table></figure><p>以上配置含义建议百度或者查看官方文档确定每个配置的含义，创建好配置文件后，需要导入到nacos中，目前官方支持2种脚本导入方式，一种是python，另外一种是linux脚本，可以去github下载或者直接复制自己创建，下载地址如下：<a href="https://github.com/seata/seata/blob/develop/script/config-center/nacos/nacos-config.py">python导入脚本</a>和   <a href="https://github.com/seata/seata/blob/develop/script/config-center/nacos/nacos-config.sh">linux导入脚本</a></p><p>其中linux导入脚本命令如下</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">nacos-config.sh -h 127.0.0.1 -p 8848  -g SEATA_GROUP -u nacos -w nacos</span><br></pre></td></tr></table></figure><p>nacos-config.sh为脚本名字，如果有修改需要修改成对应的名称，-h后面是nacos的服务器地址，-p为nacos的端口地址，-u后面为nacos的用户名，-w后面为nacos的用户名密码，-g后面为配置的分组，也就是我们之前手动创建配置时填的Group对应的值。</p><p><strong>注意：分组必须要在同一个分组内，也就是说Seata配置的分组和需要分布式事务的程序的分组以及Seata-server的分组必须要在同一个分组内</strong></p><p>以上是完整配置的，但是如果只是想实现基本的分布式事务，可以忽略上面一步</p><h4 id="创建表"><a href="#创建表" class="headerlink" title="创建表"></a>创建表</h4><p>首先Seata要处理事务，需要创建一个Seata事务的日志表（UNDO_LOG），所有库里面都需要这个表（如果每个项目连接的库不一致，那么每个项目连接的库中则都需要此表）</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">CREATE TABLE `undo_log` (</span><br><span class="line">  `id` bigint(20) NOT NULL AUTO_INCREMENT,</span><br><span class="line">  `branch_id` bigint(20) NOT NULL,</span><br><span class="line">  `xid` varchar(100) NOT NULL,</span><br><span class="line">  `context` varchar(128) NOT NULL,</span><br><span class="line">  `rollback_info` longblob NOT NULL,</span><br><span class="line">  `log_status` int(11) NOT NULL,</span><br><span class="line">  `log_created` datetime NOT NULL,</span><br><span class="line">  `log_modified` datetime NOT NULL,</span><br><span class="line">  `ext` varchar(100) DEFAULT NULL,</span><br><span class="line">  PRIMARY KEY (`id`),</span><br><span class="line">  UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)</span><br><span class="line">) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;</span><br></pre></td></tr></table></figure><p>其次创建我们的业务表，这里为了方便演示，我创建2个非常简单的表，一个用户表，一个订单表，我们保存用户时，就调用订单服务保存一个订单，当用户id为偶数时，用户服务抛异常，触发回滚(表结构非常简陋且不符合逻辑，只为演示作)</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">CREATE TABLE `user` (</span><br><span class="line">  `id` int NOT NULL AUTO_INCREMENT,</span><br><span class="line">  `name` varchar(32) NOT NULL,</span><br><span class="line">  `passwords` varchar(32) NOT NULL,</span><br><span class="line">  PRIMARY KEY (`id`)</span><br><span class="line">) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8;</span><br></pre></td></tr></table></figure><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">CREATE TABLE `trade` (</span><br><span class="line">  `id` int NOT NULL AUTO_INCREMENT,</span><br><span class="line">  `userid` int NOT NULL,</span><br><span class="line">  `value` int NOT NULL,</span><br><span class="line">  PRIMARY KEY (`id`)</span><br><span class="line">) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8;</span><br></pre></td></tr></table></figure><h4 id="创建项目"><a href="#创建项目" class="headerlink" title="创建项目"></a>创建项目</h4><p>创建一个user和trade的maven项目，其中pom依赖一致</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br></pre></td><td class="code"><pre><span class="line">  &lt;properties&gt;</span><br><span class="line">       &lt;java.version&gt;1.8&lt;/java.version&gt;</span><br><span class="line">       &lt;spring-cloud.version&gt;Hoxton.SR9&lt;/spring-cloud.version&gt;</span><br><span class="line">       &lt;spring.cloud.alibaba.version&gt;2.2.3.RELEASE&lt;/spring.cloud.alibaba.version&gt;</span><br><span class="line">       &lt;seata.seata-all.version&gt;1.1.0&lt;/seata.seata-all.version&gt;</span><br><span class="line">       &lt;spring-cloud-alibaba-seata.version&gt;2.2.0.RELEASE&lt;/spring-cloud-alibaba-seata.version&gt;</span><br><span class="line">   &lt;/properties&gt;</span><br><span class="line">&lt;dependencies&gt;</span><br><span class="line">       &lt;dependency&gt;</span><br><span class="line">           &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;</span><br><span class="line">           &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt;</span><br><span class="line">       &lt;/dependency&gt;</span><br><span class="line">       &lt;dependency&gt;</span><br><span class="line">           &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;</span><br><span class="line">           &lt;artifactId&gt;spring-boot-starter-jdbc&lt;/artifactId&gt;</span><br><span class="line">       &lt;/dependency&gt;</span><br><span class="line">       &lt;dependency&gt;</span><br><span class="line">           &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;</span><br><span class="line">           &lt;artifactId&gt;spring-boot-starter-aop&lt;/artifactId&gt;</span><br><span class="line">       &lt;/dependency&gt;</span><br><span class="line"></span><br><span class="line">       &lt;dependency&gt;</span><br><span class="line">           &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;</span><br><span class="line">           &lt;artifactId&gt;spring-boot-starter-actuator&lt;/artifactId&gt;</span><br><span class="line">       &lt;/dependency&gt;</span><br><span class="line">       &lt;!-- druid阿里巴巴数据库连接池 --&gt;</span><br><span class="line">       &lt;dependency&gt;</span><br><span class="line">           &lt;groupId&gt;com.alibaba&lt;/groupId&gt;</span><br><span class="line">           &lt;artifactId&gt;druid-spring-boot-starter&lt;/artifactId&gt;</span><br><span class="line">           &lt;version&gt;1.1.10&lt;/version&gt;</span><br><span class="line">       &lt;/dependency&gt;</span><br><span class="line">       &lt;!-- Mybatis --&gt;</span><br><span class="line">       &lt;dependency&gt;</span><br><span class="line">           &lt;groupId&gt;org.mybatis.spring.boot&lt;/groupId&gt;</span><br><span class="line">           &lt;artifactId&gt;mybatis-spring-boot-starter&lt;/artifactId&gt;</span><br><span class="line">           &lt;version&gt;1.3.2&lt;/version&gt;</span><br><span class="line">       &lt;/dependency&gt;</span><br><span class="line">       &lt;!-- MySql数据库驱动 --&gt;</span><br><span class="line">       &lt;dependency&gt;</span><br><span class="line">           &lt;groupId&gt;mysql&lt;/groupId&gt;</span><br><span class="line">           &lt;artifactId&gt;mysql-connector-java&lt;/artifactId&gt;</span><br><span class="line">           &lt;version&gt;8.0.15&lt;/version&gt;</span><br><span class="line">       &lt;/dependency&gt;</span><br><span class="line"></span><br><span class="line">       &lt;dependency&gt;</span><br><span class="line">           &lt;groupId&gt;com.alibaba.cloud&lt;/groupId&gt;</span><br><span class="line">           &lt;artifactId&gt;spring-cloud-starter-alibaba-nacos-discovery&lt;/artifactId&gt;</span><br><span class="line">           &lt;version&gt;$&#123;spring.cloud.alibaba.version&#125;&lt;/version&gt;</span><br><span class="line">       &lt;/dependency&gt;</span><br><span class="line">       &lt;dependency&gt;</span><br><span class="line">           &lt;groupId&gt;org.springframework.cloud&lt;/groupId&gt;</span><br><span class="line">           &lt;artifactId&gt;spring-cloud-starter-openfeign&lt;/artifactId&gt;</span><br><span class="line">       &lt;/dependency&gt;</span><br><span class="line"></span><br><span class="line">       &lt;dependency&gt;</span><br><span class="line">           &lt;groupId&gt;com.alibaba.cloud&lt;/groupId&gt;</span><br><span class="line">           &lt;artifactId&gt;spring-cloud-alibaba-seata&lt;/artifactId&gt;</span><br><span class="line">           &lt;version&gt;$&#123;spring-cloud-alibaba-seata.version&#125;&lt;/version&gt;</span><br><span class="line">       &lt;/dependency&gt;</span><br><span class="line">       &lt;!-- 添加 seata starter ，与服务端保持一致--&gt;</span><br><span class="line">       &lt;dependency&gt;</span><br><span class="line">           &lt;groupId&gt;io.seata&lt;/groupId&gt;</span><br><span class="line">           &lt;artifactId&gt;seata-spring-boot-starter&lt;/artifactId&gt;</span><br><span class="line">           &lt;version&gt;1.4.0&lt;/version&gt;</span><br><span class="line">       &lt;/dependency&gt;</span><br><span class="line">   &lt;/dependencies&gt;</span><br><span class="line">   &lt;dependencyManagement&gt;</span><br><span class="line">       &lt;dependencies&gt;</span><br><span class="line">           &lt;dependency&gt;</span><br><span class="line">               &lt;groupId&gt;org.springframework.cloud&lt;/groupId&gt;</span><br><span class="line">               &lt;artifactId&gt;spring-cloud-dependencies&lt;/artifactId&gt;</span><br><span class="line">               &lt;version&gt;$&#123;spring-cloud.version&#125;&lt;/version&gt;</span><br><span class="line">               &lt;type&gt;pom&lt;/type&gt;</span><br><span class="line">               &lt;scope&gt;import&lt;/scope&gt;</span><br><span class="line">           &lt;/dependency&gt;</span><br><span class="line">           &lt;dependency&gt;</span><br><span class="line">               &lt;groupId&gt;com.alibaba.cloud&lt;/groupId&gt;</span><br><span class="line">               &lt;artifactId&gt;spring-cloud-alibaba-dependencies&lt;/artifactId&gt;</span><br><span class="line">               &lt;version&gt;$&#123;spring.cloud.alibaba.version&#125;&lt;/version&gt;</span><br><span class="line">               &lt;type&gt;pom&lt;/type&gt;</span><br><span class="line">               &lt;scope&gt;import&lt;/scope&gt;</span><br><span class="line">           &lt;/dependency&gt;</span><br><span class="line">       &lt;/dependencies&gt;</span><br><span class="line">   &lt;/dependencyManagement&gt;</span><br></pre></td></tr></table></figure><p>配置trade项目application.yml</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line">server:</span><br><span class="line">  port: 8083</span><br><span class="line">spring:</span><br><span class="line">  application:</span><br><span class="line">    name: trade</span><br><span class="line">  datasource:</span><br><span class="line">    druid:</span><br><span class="line">      url: jdbc:mysql://localhost/test?useUnicode=true&amp;characterEncoding=UTF-8&amp;zeroDateTimeBehavior=convertToNull&amp;useSSL=false&amp;serverTimezone=CTT&amp;allowPublicKeyRetrieval=true</span><br><span class="line">      username: root</span><br><span class="line">      password: rayewang.</span><br><span class="line">      driver-class-name: com.mysql.cj.jdbc.Driver</span><br><span class="line"></span><br><span class="line">      max-active: 20</span><br><span class="line">      initial-size: 1</span><br><span class="line">      min-idle: 3</span><br><span class="line">      max-wait: 60000</span><br><span class="line">      pool-prepared-statements: true</span><br><span class="line">      test-while-idle: true</span><br><span class="line">      time-between-eviction-runs-millis: 60000</span><br><span class="line">      min-evictable-idle-time-millis: 300000</span><br><span class="line">      filters: stat,wall,stat,slf4j,default</span><br><span class="line">      web-stat-filter:</span><br><span class="line">        enabled: true</span><br><span class="line">        url-pattern: /*</span><br><span class="line">        exclusions: &quot;*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*&quot;</span><br><span class="line">      stat-view-servlet:</span><br><span class="line">        enabled: true</span><br><span class="line">        url-pattern: /druid/*</span><br><span class="line">        reset-enable: true</span><br><span class="line">        login-username: Raye</span><br><span class="line">        login-password: 123456</span><br><span class="line">      filter:</span><br><span class="line">        slf4j:</span><br><span class="line">          enabled: true</span><br><span class="line">          statement-create-after-log-enabled: false</span><br><span class="line">          statement-log-enabled: false</span><br><span class="line">          statement-executable-sql-log-enable: true</span><br><span class="line">          statement-log-error-enabled: true</span><br><span class="line">          result-set-log-enabled: false</span><br><span class="line">  servlet:</span><br><span class="line">    multipart:</span><br><span class="line">      max-file-size: 10MB</span><br><span class="line">      max-request-size: 100MB</span><br><span class="line">  cloud:</span><br><span class="line">    nacos:</span><br><span class="line">      discovery:</span><br><span class="line">        server-addr: 127.0.0.1:8848</span><br><span class="line">        group: SEATA_GROUP</span><br><span class="line">feign:</span><br><span class="line">  client:</span><br><span class="line">    config:</span><br><span class="line">      default:</span><br><span class="line">        connect-timeout: 10000 #单位毫秒</span><br><span class="line">        read-timeout : 10000 #单位毫秒</span><br><span class="line">  okhttp:</span><br><span class="line">    enabled: true</span><br><span class="line">  httpclient:</span><br><span class="line">    enabled: true</span><br><span class="line">debug: false</span><br><span class="line">logging:</span><br><span class="line">  level:</span><br><span class="line">    druid:</span><br><span class="line">      sql:</span><br><span class="line">        Statement: DEBUG</span><br><span class="line">nacos:</span><br><span class="line">  group: SEATA_GROUP</span><br><span class="line">  namespace: public</span><br><span class="line">  # 配置中心地址</span><br><span class="line">  server-addr: 127.0.0.1:8848</span><br><span class="line">  seata:</span><br><span class="line">    application: seata-server</span><br><span class="line">    tx-service-group: my_test_tx_group</span><br><span class="line"></span><br><span class="line">seata:</span><br><span class="line">  enabled: true</span><br><span class="line">  application-id: $&#123;spring.application.name&#125;</span><br><span class="line">  tx-service-group: $&#123;nacos.seata.tx-service-group&#125;</span><br><span class="line">  enable-auto-data-source-proxy: true</span><br><span class="line">  config:</span><br><span class="line">    # 指明类型</span><br><span class="line">    type: nacos</span><br><span class="line">    nacos:</span><br><span class="line">      server-addr: $&#123;nacos.server-addr&#125;</span><br><span class="line">      namespace: $&#123;nacos.namespace&#125;</span><br><span class="line">      group: $&#123;nacos.group&#125;</span><br><span class="line">      username: &quot;nacos&quot;</span><br><span class="line">      password: &quot;nacos&quot;</span><br><span class="line">  registry:</span><br><span class="line">    type: nacos</span><br><span class="line">    nacos:</span><br><span class="line">      application: $&#123;nacos.seata.application&#125;</span><br><span class="line">      server-addr: $&#123;nacos.server-addr&#125;</span><br><span class="line">      namespace: $&#123;nacos.namespace&#125;</span><br><span class="line">      group: $&#123;nacos.group&#125;</span><br><span class="line">      username: &quot;nacos&quot;</span><br><span class="line">      password: &quot;nacos&quot;</span><br></pre></td></tr></table></figure><p>其中分布式事务核心配置为nacos节点和seata节点的内容，seata节点内容配置大致跟seata-server的配置一致，其中tx-service-group的值就是我们之前手动添加的配置的节点最后一个名称，user项目的application.yml跟trade基本一致，除了spring.application.name和端口不同</p><p>Springcloud的核心配置为spring.cloud节点下的内容，主要是配置nacos为注册发现中心以及应用的分组，要跟Seata配置的分组一致</p><h4 id="业务流程"><a href="#业务流程" class="headerlink" title="业务流程"></a>业务流程</h4><p>项目创建好之后，我们先梳理整体流程，首先调用user项目的服务保存用户，然后在保存用户服务中开启分布式事务，保存用户后调用trade服务保存订单，订单保存完毕之后，判断用户的id是否是偶数，如果是偶数则抛出一个异常，看保存的用户和订单信息是否正常回滚，如果是奇数则看数据是否正常保存</p><h4 id="编写trade项目相关代码"><a href="#编写trade项目相关代码" class="headerlink" title="编写trade项目相关代码"></a>编写trade项目相关代码</h4><p>以下只列出逻辑相关代码</p><p><strong>TradeMapper.java</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">package wang.raye.nacos;</span><br><span class="line"></span><br><span class="line">import org.apache.ibatis.annotations.Insert;</span><br><span class="line">public interface TradeMapper &#123;</span><br><span class="line"></span><br><span class="line">    @Insert(&#123;&quot;insert into trade(userid,`value`) values(#&#123;userid&#125;,#&#123;value&#125;)&quot;&#125;)</span><br><span class="line">    int insertTrade(int userid,int value);</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p><strong>TradeService.java</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line">package wang.raye.nacos;</span><br><span class="line"></span><br><span class="line">import org.springframework.beans.factory.annotation.Autowired;</span><br><span class="line">import org.springframework.stereotype.Service;</span><br><span class="line">import org.springframework.transaction.annotation.Transactional;</span><br><span class="line"></span><br><span class="line">@Service</span><br><span class="line">public class TradeService &#123;</span><br><span class="line"></span><br><span class="line">    @Autowired</span><br><span class="line">    private TradeMapper mapper;</span><br><span class="line"></span><br><span class="line">    @Transactional(rollbackFor = Exception.class)</span><br><span class="line">    public boolean saveTrade(int userid,int value)&#123;</span><br><span class="line">        return mapper.insertTrade(userid,value) &gt; 0;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>TradeController.java</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line">package wang.raye.nacos;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">import org.springframework.beans.factory.annotation.Autowired;</span><br><span class="line">import org.springframework.web.bind.annotation.GetMapping;</span><br><span class="line">import org.springframework.web.bind.annotation.RequestMapping;</span><br><span class="line">import org.springframework.web.bind.annotation.RestController;</span><br><span class="line">@RestController</span><br><span class="line">public class TradeController &#123;</span><br><span class="line"></span><br><span class="line">    @Autowired</span><br><span class="line">    private TradeService tradeService;</span><br><span class="line"></span><br><span class="line">    @RequestMapping(&quot;trade/save&quot;)</span><br><span class="line">    public boolean save(int userid,int value)&#123;</span><br><span class="line">        return tradeService.saveTrade(userid,value);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="编写user相关代码"><a href="#编写user相关代码" class="headerlink" title="编写user相关代码"></a>编写user相关代码</h4><p><strong>UserMapper.java</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">package wang.raye.nacos;</span><br><span class="line"></span><br><span class="line">import org.apache.ibatis.annotations.Insert;</span><br><span class="line"></span><br><span class="line">public interface UserMapper &#123;</span><br><span class="line"></span><br><span class="line">    @Insert(&#123;&quot;insert user(id,name,passwords) values(#&#123;id&#125;,#&#123;name&#125;,#&#123;passwords&#125;)&quot;&#125;)</span><br><span class="line">    int insert(int id,String name,String passwords);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>UserService.java</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line">package wang.raye.nacos;</span><br><span class="line"></span><br><span class="line">import io.seata.spring.annotation.GlobalTransactional;</span><br><span class="line">import org.springframework.beans.factory.annotation.Autowired;</span><br><span class="line">import org.springframework.stereotype.Service;</span><br><span class="line">import org.springframework.transaction.annotation.Transactional;</span><br><span class="line"></span><br><span class="line">@Service</span><br><span class="line">public class UserService &#123;</span><br><span class="line">    @Autowired</span><br><span class="line">    private TradeService service;</span><br><span class="line">    @Autowired</span><br><span class="line">    private UserMapper mapper;</span><br><span class="line"></span><br><span class="line">    @GlobalTransactional</span><br><span class="line">    @Transactional(rollbackFor = Exception.class)</span><br><span class="line">    public boolean save(int userid,String name,String passwords,int value)&#123;</span><br><span class="line">        mapper.insert(userid,name,passwords);</span><br><span class="line">        service.save(userid,value);</span><br><span class="line">        if(userid % 2 == 0)&#123;</span><br><span class="line">            throw new RuntimeException(&quot;不给保存双数id&quot;);</span><br><span class="line">        &#125;</span><br><span class="line">        return true;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>其中这里主要用注解@GlobalTransactional来开启分布式事务，代码</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">if(userid % 2 == 0)&#123;</span><br><span class="line">    throw new RuntimeException(&quot;不给保存双数id&quot;);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>只是为了模拟一下发生异常的情况，看是否会正常回滚，TradeService为Springcloud调用trade项目的</p><p><strong>TradeService.java</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">package wang.raye.nacos;</span><br><span class="line"></span><br><span class="line">import org.springframework.cloud.openfeign.FeignClient;</span><br><span class="line">import org.springframework.web.bind.annotation.PostMapping;</span><br><span class="line">import org.springframework.web.bind.annotation.RequestParam;</span><br><span class="line"></span><br><span class="line">@FeignClient(&quot;trade&quot;)</span><br><span class="line">public interface TradeService &#123;</span><br><span class="line"></span><br><span class="line">    @RequestMapping(&quot;trade/save&quot;)</span><br><span class="line">    boolean save(@RequestParam(&quot;userid&quot;) int userid,@RequestParam(&quot;value&quot;) int value);</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p><strong>UserController.java</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line">package wang.raye.nacos;</span><br><span class="line"></span><br><span class="line">import org.springframework.beans.factory.annotation.Autowired;</span><br><span class="line">import org.springframework.web.bind.annotation.GetMapping;</span><br><span class="line">import org.springframework.web.bind.annotation.PathVariable;</span><br><span class="line">import org.springframework.web.bind.annotation.RestController;</span><br><span class="line"></span><br><span class="line">@RestController</span><br><span class="line">public class UserController &#123;</span><br><span class="line">    @Autowired</span><br><span class="line">    private UserService service;</span><br><span class="line"></span><br><span class="line">    @GetMapping(&quot;save&quot;)</span><br><span class="line">    public String save(int id,String name,String passwords,int value)&#123;</span><br><span class="line">        try &#123;</span><br><span class="line">            if (service.save(id, name, passwords, value)) &#123;</span><br><span class="line">                return &quot;保存成功&quot;;</span><br><span class="line">            &#125;</span><br><span class="line">            return &quot;保存失败&quot;;</span><br><span class="line">        &#125;catch (Exception e)&#123;</span><br><span class="line">            return e.getMessage();</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><h4 id="测试"><a href="#测试" class="headerlink" title="测试"></a>测试</h4><p>至此业务代码已经完成，跟没用分布式事务的代码只有一个注解的差异，所以应该还是很好理解的，启动2个项目，我们来测试一下，访问<a href="http://localhost:8082/save?id=16&name=raye&passwords=1234566&value=100">http://localhost:8082/save?id=16&name=raye&passwords=1234566&value=100</a>（其中8082为user项目的端口），会提示不给保存双数id，并且保存的订单数据也会被删除掉，可以跟踪执行的sql，发现trade是先插入，然后再删除的,而用户是插入后回滚的，利用了本地事务,而将id改成奇数，发现能正常保存，说明AT模式的分布式事务已经搭建成功</p>]]></content>
    
    
    <summary type="html">&lt;h4 id=&quot;-&quot;&gt;&amp;#x524D;&amp;#x8A00;&lt;/h4&gt;&lt;p&gt;&amp;#x672C;&amp;#x6587;&amp;#x8981;&amp;#x6C42;&amp;#x5BF9;Springcloud&amp;#x6709;&amp;#x4E00;&amp;#x5B9A;&amp;#x4E86;&amp;#x89E3;&amp;#xFF0C;&amp;#x5BF9;&amp;#x5206;&amp;#x5E03;&amp;#x5F0F;&amp;#x4E8B;&amp;#x52A1;&amp;#x672C;&amp;#x8EAB;&amp;#x6709;&amp;#x4E00;&amp;#x5B9A;&amp;#x8BA4;&amp;#x77E5;&amp;#xFF0C;&amp;#x5982;&amp;#x679C;&amp;#x4E0D;&amp;#x4E86;&amp;#x89E3;&amp;#x5FAE;&amp;#x670D;&amp;#x52A1;&amp;#xFF0C;&amp;#x5EFA;&amp;#x8BAE;&amp;#x5148;&amp;#x770B;&amp;#x770B;&lt;a href=&quot;http://raye.wang/spring-cloudde-ji-ben-ren-shi-he-shi-yong-spring-cloudde-ji-ben-jiao-cheng/&quot;&gt;Spring Cloud&amp;#x7684;&amp;#x57FA;&amp;#x672C;&amp;#x8BA4;&amp;#x8BC6;&amp;#x548C;&lt;/a&gt;&lt;/p&gt;</summary>
    
    
    
    <category term="Java" scheme="https://raye.wang/categories/Java/"/>
    
    
    <category term="JavaEE" scheme="https://raye.wang/tags/JavaEE/"/>
    
    <category term="Springboot" scheme="https://raye.wang/tags/Springboot/"/>
    
    <category term="分布式事务" scheme="https://raye.wang/tags/%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1/"/>
    
  </entry>
  
  <entry>
    <title>Springboot Druid 自定义加密数据库密码</title>
    <link href="https://raye.wang/2020/12/21/Springboot-Druid-%E8%87%AA%E5%AE%9A%E4%B9%89%E5%8A%A0%E5%AF%86%E6%95%B0%E6%8D%AE%E5%BA%93%E5%AF%86%E7%A0%81/"/>
    <id>https://raye.wang/2020/12/21/Springboot-Druid-%E8%87%AA%E5%AE%9A%E4%B9%89%E5%8A%A0%E5%AF%86%E6%95%B0%E6%8D%AE%E5%BA%93%E5%AF%86%E7%A0%81/</id>
    <published>2020-12-20T16:00:00.000Z</published>
    <updated>2026-03-10T06:59:04.571Z</updated>
    
    <content type="html"><![CDATA[<h4 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h4><p>开发过程中，配置的数据库密码通常是明文形式，这样首先第一个安全性不好（相对来说），不符合一个开发规范（如项目中不能出现明文账号密码），其实就是当出现特殊需求时，比如要对非运维人员开方服务器部分权限，但是又涉及项目部署的目录时，容易泄漏数据库密码，虽然一般生产环境中，数据库往往放入内网，访问只能通过内网访问，但是不管怎么说账号密码直接让人知道总归不好，甚至有些项目需要部署到客户环境中，但是可能共用一个公共数据库（数据库只向指定服务器开放外网端口或组建内网环境），这样的情况下，如果数据库密码再以明文形式存在，就非常危险了，常见的避免这些方式据我了解有2种</p><p><strong>第一种：通过网络获取密码</strong></p><p>这种是让密码通过接口获取，当然接口中可以做加密认证，但是如果是把获取地址和认证放在了配置文件中，那么稍微用心也能获取到密码，不过也可以跟第二种方法配合使用</p><p><strong>第二种：密码加密</strong></p><p>通过对密码加密，使得配置文件中配置的密码为密文，从而提升安全性，当然网络获取密码也可以获取加密后的密码，而国内常用的数据库连接池是Druid，所以此文主要是针对Druid的数据库密码加密方案，Druid默认再带了一个密码加密，所以Druid加密密码也有2种方案</p><h4 id="Druid加解密密码的2种方案"><a href="#Druid加解密密码的2种方案" class="headerlink" title="Druid加解密密码的2种方案"></a>Druid加解密密码的2种方案</h4><p><strong>第一种：自带加密</strong></p><p>优点：简单快捷，配置方便，无需改动代码（只需要改动配置文件即可）</p><p>缺点：自欺欺人式加密，因为密码和公钥都需要配置到配置文件中，只要别人有了配置文件，看出来了你是用的Druid,就可以用druid进行解密</p><p><strong>第二种：自行加解密</strong></p><p>优点：相对更安全，可以自行实现加解密</p><p>缺点：需要改动源码，另外只要有对应源码和配置文件也能解密，所以如果团队中想要控制其他成员不能获取到生产环境中的数据库密码，那么成员就不能有生产环境中的配置文件</p><h4 id="自行加解密实现"><a href="#自行加解密实现" class="headerlink" title="自行加解密实现"></a>自行加解密实现</h4><p>Druid原生支持自行实现加解密，只需要实现解密类，然后配置对应解密类即可，首先编写类继承DruidPasswordCallback,在setProperties方法中解密密码，我的解密代码如下（代码中有伪代码，不能直接复制使用，需要先替换伪代码）：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line">/**</span><br><span class="line"> * 解密数据库密码回调</span><br><span class="line"> * @author Raye</span><br><span class="line"> * @since 2020-12-21</span><br><span class="line"> */</span><br><span class="line">@Slf4j</span><br><span class="line">public class DBPasswordCallback extends DruidPasswordCallback &#123;</span><br><span class="line"> </span><br><span class="line">    @Override</span><br><span class="line">    public void setProperties(Properties properties) &#123;</span><br><span class="line">        super.setProperties(properties);</span><br><span class="line">        //获取配置文件中的已经加密的密码</span><br><span class="line">        String pwd = (String)properties.get(&quot;password&quot;);</span><br><span class="line">        if (StringUtil.isNotEmpty(pwd)) &#123;</span><br><span class="line">            try &#123;</span><br><span class="line">            </span><br><span class="line">                //这里的代码是将密码进行解密，并设置</span><br><span class="line">                String password = （解密密码）</span><br><span class="line">                setPassword(password.toCharArray());</span><br><span class="line">            &#125; catch (Exception e) &#123;</span><br><span class="line">                e.printStackTrace();</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><h4 id="配置解密类"><a href="#配置解密类" class="headerlink" title="配置解密类"></a>配置解密类</h4><p>用了自行加解密之后，还需要在application.yml中配置解密的类，具体添加配置如下</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">spring:</span><br><span class="line">  datasource:</span><br><span class="line">    druid:</span><br><span class="line">      connection-properties: password=$&#123;spring.datasource.druid.password&#125;</span><br><span class="line">      password-callback-class-name: 对应包名.DBPasswordCallback</span><br></pre></td></tr></table></figure><p>注意配置中有伪代码，也就是加2个配置，其中connection-properties是一个map结构的配置，用;分割，可以自行定义其中key和value的值，最终会封装成对象传入DBPasswordCallback 类的setProperties方法中，password-callback-class-name则是配置解密回调的类，也就是我们的DBPasswordCallback 类，需要跟包名一起设置，上面代码中报名用伪代码代替了，需要替换</p><p>以上就是Springboot 配合Druid实现自定义加解密数据库密码的配置方法，具体加解密本身的方法采用那种方法根据实际情况选择即可</p>]]></content>
    
    
    <summary type="html">&lt;h4 id=&quot;-&quot;&gt;&amp;#x524D;&amp;#x8A00;&lt;/h4&gt;&lt;p&gt;&amp;#x5F00;&amp;#x53D1;&amp;#x8FC7;&amp;#x7A0B;&amp;#x4E2D;&amp;#xFF0C;&amp;#x914D;&amp;#x7F6E;&amp;#x7684;&amp;#x6570;&amp;#x636E;&amp;#x5E93;&amp;#x5BC6;&amp;#x7801;&amp;#x901A;&amp;#x5E38;&amp;#x662F;&amp;#x660E;&amp;#x6587;&amp;#x5F62;&amp;#x5F0F;&amp;#xFF0C;&amp;#x8FD9;&amp;#x6837;&amp;#x9996;&amp;#x5148;&amp;#x7B2C;&amp;#x4E00;&amp;#x4E2A;&amp;#x5B89;&amp;#x5168;&amp;#x6027;&amp;#x4E0D;&amp;#x597D;&amp;#xFF08;&amp;#x76F8;&amp;#x5BF9;&amp;#x6765;&amp;#x8BF4;&amp;#xFF09;&amp;#xFF0C;&amp;#x4E0D;&amp;#x7B26;&amp;#x5408;&amp;#x4E00;&amp;#x4E2A;&amp;#x5F00;&amp;#x53D1;&lt;/p&gt;</summary>
    
    
    
    <category term="Java" scheme="https://raye.wang/categories/Java/"/>
    
    
    <category term="JavaEE" scheme="https://raye.wang/tags/JavaEE/"/>
    
    <category term="Springboot" scheme="https://raye.wang/tags/Springboot/"/>
    
    <category term="Druid" scheme="https://raye.wang/tags/Druid/"/>
    
  </entry>
  
  <entry>
    <title>Java 浮点型（Double,Float）精度丢失解决方案</title>
    <link href="https://raye.wang/2020/07/11/Java-%E6%B5%AE%E7%82%B9%E5%9E%8B%EF%BC%88Double-Float%EF%BC%89%E7%B2%BE%E5%BA%A6%E4%B8%A2%E5%A4%B1%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/"/>
    <id>https://raye.wang/2020/07/11/Java-%E6%B5%AE%E7%82%B9%E5%9E%8B%EF%BC%88Double-Float%EF%BC%89%E7%B2%BE%E5%BA%A6%E4%B8%A2%E5%A4%B1%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/</id>
    <published>2020-07-10T16:00:00.000Z</published>
    <updated>2026-03-10T06:59:04.570Z</updated>
    
    <content type="html"><![CDATA[<h4 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h4><p>最近公司某小伙子做了一个商城的微信支付相关的接口，其中包含退款，在测试过程中发现部分单据没有退款，微信支付提示退款金额跟支付金额不匹配（大于支付金额），检查数据库和调试过程中，发现商品的单价和手工计算出来的总价是跟订单金额匹配的，实在无法确认问题原因最终bug转向我来排查，于是有了此文</p><h4 id="排查"><a href="#排查" class="headerlink" title="排查"></a>排查</h4><p>由于手工计算出来的金额跟实际支付的金额是能匹配上的，所以一开始我以为是订单已经进行了部分退款，再次全部退款的话肯定会被微信阻止，因为这样的话就会出现超退。</p><p><strong>第一步：是否已经部分退款</strong></p><p>去微信支付平台排查是否有退款，结果发现并没有退款</p><p>由于微信平台没有查询到此单的退款，那么说明不是超退的问题，可能是传入的订单金额有问题，但是大部分单据又可以退款，所以也暂时陷入了僵局，不过为了保险起见，还是去看了一下传参处</p><p><strong>第二步：检查传参</strong></p><p>检查微信退款方法传参时，看了传入参数，由于微信传参的单位是分，所以看到传参处传入退款总金额代码(int)(100)*商品单价*退货数量（这里只是伪代码，多个商品肯定还要相加计算的）,粗略一看没有问题，仔细一看不对，总价是直接double类型的乘以数量在乘以100再强转为int类型传入的，按照理论上来说是没有问题的，因为我们商品价格都是精确到分的，但是只是理论上，实际上就是Java的double类型有个大坑（不只是Java，JavaScript，c#等很多语言都有这个问题），最终在代码中进行总价计算，发现确定是因为这个原因，导致计算出来的退款金额比应退金额多了1分钱，所以导致退款失败</p><h4 id="起因"><a href="#起因" class="headerlink" title="起因"></a>起因</h4><p>之所以会发生精度丢失，是因为编程语言在十进制和二进制相互转换时，会出现除不断的问题，就导致了精度丢失  </p><p><img src="https://upload-images.jianshu.io/upload_images/1783810-adf7d680a4ff0131.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="image"></p><p>详情可以查看知乎原文，我觉得解释的非常不错</p><p><a href="https://www.zhihu.com/question/42024389">如何理解double精度丢失问题？</a></p><h4 id="解决办法"><a href="#解决办法" class="headerlink" title="解决办法"></a>解决办法</h4><p>解决办法有2种：</p><p>第一种是电商行业通常的做法，将所有价格用分来表示，数据库中保存int类型，因为通常情况下，商品价格只需要精确到分，分也是正常电商的最小单位，前端显示时通过将分转化成元来显示，但是后端计算都是分来计算，也就是int型来计算，这样就不会出现精度丢失的问题，当然也可能会出现int最大值的问题，当然一般情况下也可以忽略这个问题毕竟没有几个商品价格或者订单能超过int的最大值了，当然系统订单总价可能会超过，这么单独处理一下就行了</p><p>第二种就是使用BigDecimal类进行计算了，由于我们系统一开始就是用元来表示，所以只能用DigDecimal来计算避免精度丢失了，具体BigDecimal的用法可以百度</p><h4 id="结尾"><a href="#结尾" class="headerlink" title="结尾"></a>结尾</h4><p>通过这次问题，首先要注意就是代码中规范好，所有浮点类型计算的都使用BigDecimal进行计算，并且要写入开发规范文档，其次就是类似于价格这类的，以后最好还是用分来进行处理</p>]]></content>
    
    
    <summary type="html">&lt;h4 id=&quot;-&quot;&gt;&amp;#x524D;&amp;#x8A00;&lt;/h4&gt;&lt;p&gt;&amp;#x6700;&amp;#x8FD1;&amp;#x516C;&amp;#x53F8;&amp;#x67D0;&amp;#x5C0F;&amp;#x4F19;&amp;#x5B50;&amp;#x505A;&amp;#x4E86;&amp;#x4E00;&amp;#x4E2A;&amp;#x5546;&amp;#x57CE;&amp;#x7684;&amp;#x5FAE;&amp;#x4FE1;&amp;#x652F;&amp;#x4ED8;&amp;#x76F8;&amp;#x5173;&amp;#x7684;&amp;#x63A5;&amp;#x53E3;&amp;#xFF0C;&amp;#x5176;&amp;#x4E2D;&amp;#x5305;&amp;#x542B;&amp;#x9000;&amp;#x6B3E;&amp;#xFF0C;&amp;#x5728;&amp;#x6D4B;&amp;#x8BD5;&amp;#x8FC7;&amp;#x7A0B;&amp;#x4E2D;&amp;#x53D1;&amp;#x73B0;&amp;#x90E8;&amp;#x5206;&amp;#x5355;&amp;#x636E;&amp;#x6CA1;&amp;#x6709;&amp;#x9000;&amp;#x6B3E;&lt;/p&gt;</summary>
    
    
    
    <category term="Java" scheme="https://raye.wang/categories/Java/"/>
    
    
    <category term="JavaEE" scheme="https://raye.wang/tags/JavaEE/"/>
    
  </entry>
  
  <entry>
    <title>一次抢口罩的负载优化</title>
    <link href="https://raye.wang/2020/03/23/%E4%B8%80%E6%AC%A1%E6%8A%A2%E5%8F%A3%E7%BD%A9%E7%9A%84%E8%B4%9F%E8%BD%BD%E4%BC%98%E5%8C%96/"/>
    <id>https://raye.wang/2020/03/23/%E4%B8%80%E6%AC%A1%E6%8A%A2%E5%8F%A3%E7%BD%A9%E7%9A%84%E8%B4%9F%E8%BD%BD%E4%BC%98%E5%8C%96/</id>
    <published>2020-03-22T16:00:00.000Z</published>
    <updated>2026-03-10T06:59:04.571Z</updated>
    
    <content type="html"><![CDATA[<h4 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h4><p>由于这次令人讨厌的疫情，导致口罩一罩难求，虽然各大厂商都在努力生产，但是毕竟僧多粥少，也幸好疫情快要结束了，也希望早点结束。因为口罩实在难以购买，所以很多地方的口罩采用统一调配，但是如何把口罩发放到大众手里，也成为了一个问题，所以预约购买成了最为安全的购买方式，购买后可以邮寄到家或者根据选择的时间段到店自提，但是由于购买人数过多，也带来了一个新的问题，服务器并发的问题，至少我在二月份参与了很多预约购买口罩的平台，没有一个平台不崩溃的，大多都是10几分钟了，就没有正常请求过服务器一次，不过幸好现在大部分平台已经调整为抽签的形式，而不是之前的先到先得规则，这样相对于先到先得更加公平，不受限于网络和运气，因为我们公司也做了相应的预约平台，不过由于客户要求先到先得原则，所以了本文。不过由于使用我们平台的用户并没有一线城市那么夸张，动不动几百万人同时抢购，而且我们平台的服务器也没有那么好的配置，所以本文的方式只是在几万人同时抢购的情况下测试过，当然其中思路还是可以参考的一下的，毕竟没有万金油，负载优化肯定是要根据实际情况来处理的</p><h4 id="要求"><a href="#要求" class="headerlink" title="要求"></a>要求</h4><p>首先第一个要求是不能超发，第二个要求是尽可能提交成功的失败率不会太高，也就是说不要100人都提交成功了，但是最后只有一个人抢到了口罩，第三就是要求服务器不要崩溃，尽可能的不要出现请求服务器失败的情况。</p><p>首先一个要求解决方案：因为考虑到大并发的情况的，而且还做了负载均衡，所以要么采用分布式锁或者队列，</p><h5 id="分布式锁"><a href="#分布式锁" class="headerlink" title="分布式锁"></a>分布式锁</h5><p>分布式锁的优点是可以实时知道抢购结果，坏处就是性能低下，处理不好容易造成服务器压力过大。</p><p>分布式锁可以采用zookeeper，当然还有别的分布式中间件或者框架，千万别直接用语言自带的锁，做了负载均衡后，这个锁是不适用的，当然如果采用分布式锁的方式，一定要注意处理时间和预判，也就是说比如库存10个，那么理论上来说你有10请求到了，后面请求就应该直接说没有库存了，不然又有锁，又不预判，最终结果就是服务器被拖死，所有请求都需要到锁里面去执行逻辑。</p><h5 id="队列"><a href="#队列" class="headerlink" title="队列"></a>队列</h5><p>队列的好处就是你把请求扔进队列里面，就可以给客户反馈了，最终处理交给队列的消费者来处理就好，坏处就是不能实时知道处理结果，另外消费者也只能配置一个，注意查看一下所用的消息队列的文档，如果队列消费者是有支持多线程消费的，也要加分布式锁，如果需要配置多个消费者一样，需要在消费者中使用分布式锁，也可以直接用数据库乐观锁，不过大并发情况下到底是分布式锁效率高还是乐观锁效率高，还是要根据时间情况来分析，当然仅仅是分析是没用的，还要进行测试。当然用队列的方式也最好加一个库存预判，不然会出现很多人提交成功，但是却只有少数人能抢购到，如果有预判，首先可以降低消息数量，降低消费者运行时间，第二也不用让人一直期待</p><p>第二个要求解决方案就是加库存预判，我们在redis里面也保存了一分库存数据，每次有人提交先判断redis库存，如果有就马上扣减然后扔队列，当然因为redis的锁一直有问题，所以预判的库存肯定不会准确，但是影响不大，因为最终成功是在消息队列里面处理，不会出现超发</p><p>第三个就是简单来说就是尽可能的增加服务器并发量，当然前面2个问题的处理方式已经一定程度上增加了并发量，但是却远远不够，所以为了增加并发量，还要继续优化代码和做负载均衡</p><h4 id="代码优化"><a href="#代码优化" class="headerlink" title="代码优化"></a>代码优化</h4><h5 id="前端"><a href="#前端" class="headerlink" title="前端"></a>前端</h5><p>首先前端因为是网页的（小程序更新版本审核太麻烦，不符合我们快速迭代要求）</p><ul><li>CDN</li></ul><p>首先可以使用cdn，不然所有请求都到我们服务器，宽带能否承受变成了一个问题，毕竟一个网页请求的数据量跟普通接口数据量差别还是挺大的</p><ul><li>代码压缩</li></ul><p>第二就是代码压缩，压缩之后能减少传输的数据量，虽然看起来可能只少了几十K，但是把这几十K放大几万倍也是不小的网络开销了（压缩包括图片，字体等各种前端资源）</p><ul><li>本地缓存</li></ul><p>第三就是不常更新的数据做本地缓存，比如用户信息等，这样就不用每次都请求接口了，不过要注意缓存失效，如果是放在localStorage里面一定要主动做失效控制，否则数据会导致数据一直不更新</p><p>由于不是专业前端，所以就只能想到这几个优化办法（因为处理这块的时候还没有开始上班，而且前端页面不复杂，所以就由我开发了）</p><h5 id="后端"><a href="#后端" class="headerlink" title="后端"></a>后端</h5><ul><li>缓存</li></ul><p>首先数据能放缓存就尽量放缓存，避免经常查询数据库，但是也要注意缓存失效的问题，如果数据库有更新，一定要更新缓存，另外还要注意缓存击穿的问题，当然有些缓存数据不用一定准确，比如之前提到的抢购的时候的库存预判，为了性能只能让缓存跟数据库不同步，只要最终处理结果正确就行</p><ul><li>计算优化</li></ul><p>这个优化程度倒是不是特别多，当然如果懂就更好了，写出更符合jvm优化的代码，不过一般代码规划好的工程师写出来的代码也差不多符合这些要求，不过也要检查是否有可以优化的地方</p><ul><li>接口合并</li></ul><p>有时候一个页面需要请求多个接口获取数据，可以考虑同一个接口返回多个接口返回的数据，这样虽然单次请求时间会延长，但是确能降低请求量，也不失为一种增加并发的方法</p><ul><li>配置优化</li></ul><p>主要是数据库、消息队列、缓存等连接池线程数量的优化，比如你配置的线程池最大只有10个线程，那么不管怎么样，你同一时间段都只能处理10个人的请求（这里的请求只是针对需要用到这些线程的），当然这些配置也不是越大越好，首先考虑一个切换成本，第二就是有了负载后，数据库这些本身的最大连接数，比如你数据库本身最大连接数是100，你给程序数据库连接池配置了1000，那么也没用，因为数据库只能接收你100个连接，最终也只有100个连接才能正常使用</p><h5 id="中间件优化"><a href="#中间件优化" class="headerlink" title="中间件优化"></a>中间件优化</h5><ul><li>Tomcat优化</li></ul><p>优化jvm参数（堆栈初始化大小等），优化了tomcat的线程池配置</p><ul><li>数据库优化</li></ul><p>启用MySQL缓存（如果是读明显大于写很多的情况，可以用这个，可以加快查询数量，如果是写跟读差不多，那也要考虑开启后带来的负面影响，增加了写入成本），增加最多连接数量等</p><p>由于中间件采用的各不相同，所以只能根据实际情况和硬件配置等来具体优化，切记不能千篇一律的照抄，毕竟别人的配置不一定就适合你的服务器，比如别人机器64核，所以给nginx开64个工作线程，但是你的机器只有16核，你也去开64个线程，这样不会更快，反而更慢</p><h4 id="Nginx"><a href="#Nginx" class="headerlink" title="Nginx"></a>Nginx</h4><p>由于Nginx是主要的负载软件，所以讲一下Nginx的相关配置，本文只设计到配置理论，不会出具体配置，还是那句话，具体情况下具体配置，没有万金油</p><ul><li>Nginx负载策略</li></ul><p>Nginx 常用的负载策略有</p><ul><li>轮询（默认方式）</li><li>weight(权重)</li><li>ip_hash(根据IP分配)</li><li>least_conn（最少连接方式）</li><li>fair(响应时间方式 第三方)</li><li>url_hash(根据URL分配  第三方)</li></ul><p>由于服务器配置参差不齐，所以轮询、 ip-hash 、 least_conn这几种策略明显不使用，毕竟这样会造成配置高的服务器接收请求少，而配置差的服务器反而接收了更多请求。urlhash也不符合我们使用场景，主要因为这样会造成不同的服务器处理不同的接口，无法有效把压力分散，毕竟所有压力都集中在1,2个接口上，所以不适合</p><p>最终适合我们的负载策略只有weight和fair，理论上fair是更适合的，因为响应时间才能反应一台服务器真正的压力，如果响应快，那么理所应当接收更多负载，毕竟很多时候一台服务器不只是一个服务，但是因为fair是第三方的，首先有效性还有待考证，其次就是这样的话nginx需要重新编译，而我们nginx已经编译了太多模块，上次编译的命令无法找回，导致担心重新编译会有问题（所以以后这些需要自行编译的软件，一定要记录编译命令，否则万一哪天需要更改，麻烦就来了），所以最终选择了weight，然后根据服务器的配置以及所运行的其他服务来设置了权重</p><ul><li>Nginx其他配置优化</li></ul><p>工作线程数量优化、worker_connections和worker_rlimit_nofile 优化</p><h4 id="多层负载"><a href="#多层负载" class="headerlink" title="多层负载"></a>多层负载</h4><p>Nginx只是软件负载，但是一台服务器的再强，并发也是有限的，所以就需要用到多层负载。如果是真机，那么可以使用硬件负载，如F5、Array等硬件（由于不是专业运营，没有接触过这些高端玩意，也就知道这2个），如果是阿里云的服务器，也可以购买阿里云的负载均衡服务（其他云我不知道有没有，因为没用过），当然这2种办法都是需要增加成本的，所以还有一种免费的方法，就是DNS负载，配置简单，还不受限服务器配置，唯独一点不好的就是DNS负载不会监控服务健康状态，简单来说，假如你这台服务器挂了，那么请求该来的还是会来。多层负载用了之后的结果就是，可以有效的把请求分发到多台nginx服务器，可以避免单台nginx卡死，导致所有服务不可用</p><h4 id="出现过的问题"><a href="#出现过的问题" class="headerlink" title="出现过的问题"></a>出现过的问题</h4><ul><li>Nginx单线程能处理的最大并发数问题</li></ul><p>由于当时时间紧急，所以没有截图，原因是因为worker_connections设置的太小，之前同事直接网上复制了一份配置，上面设置的是2048，由于之前系统没有很大的并发，所以也没有怎么关注这个配置，直到出了问题百度才知道，这个是单个工作线程能处理的最大并发量，比如我设置了10个工作线程，worker_connections设置的2048，那么我nginx最大并发也就是20480，超过这个并发nginx就会抛异常提示，另外worker_connections不能超过worker_rlimit_nofile，具体可以百度相关关键词来配置</p><ul><li>Nginx主机CPU 100%导致全部卡住</li></ul><p>这个问题一开始没有出现，后面抢购直接由县城升级到开放全市抢购的时候，问题来了，主Nginx的服务器CPU 100%一直不降，所有服务器都没有请求进来，说明nginx已经没有进行请求分发了，htop命令查询服务器状态，发现Nginx工作线程占用CPU都很高，还有其他一个服务，这2个程序已经吧CPU全部占用了，但是尽管Nginx占用了CPU，但是却没有执行分发，最后只能kill掉nginx，重启（restart命令已经没用了），原因是服务器配置明显不够（也有可能是nginx本身的版本的问题，因为版本比较老，理论上来说那么点并发是没有多大问题的），再加上另外一个服务也很占用资源，所以第一步是把另外一个服务迁移到其他服务器上，第二就是启用了DNS负载，当时也看了阿里云的负载服务，不过DNS负载能满足我们的需求，虽然没有阿里云的负载那么好用和智能，但是好在DNS负载是免费的[笑着哭]，而且配置也简单，这样从单台nginx变成多台，nginx就再也没有卡死了（虽然是因为并发也不够大，如果几千万的并发，我都不用想就知道，瞬间嘣了）</p><h4 id="结尾"><a href="#结尾" class="headerlink" title="结尾"></a>结尾</h4><p>虽然没有很夸张的动辄几千万，几个亿的负载优化，但是我想所有开发都是一步一步来的，没有谁没有经历过几十万的负载，就去坐几百万的负载优化，纸上谈兵谁都会，但是实验才是检验真理的唯一准则，也希望此文能给阅读的你带来一点帮助</p>]]></content>
    
    
    <summary type="html">&lt;h4 id=&quot;-&quot;&gt;&amp;#x524D;&amp;#x8A00;&lt;/h4&gt;&lt;p&gt;&amp;#x7531;&amp;#x4E8E;&amp;#x8FD9;&amp;#x6B21;&amp;#x4EE4;&amp;#x4EBA;&amp;#x8BA8;&amp;#x538C;&amp;#x7684;&amp;#x75AB;&amp;#x60C5;&amp;#xFF0C;&amp;#x5BFC;&amp;#x81F4;&amp;#x53E3;&amp;#x7F69;&amp;#x4E00;&amp;#x7F69;&amp;#x96BE;&amp;#x6C42;&amp;#xFF0C;&amp;#x867D;&amp;#x7136;&amp;#x5404;&amp;#x5927;&amp;#x5382;&amp;#x5546;&amp;#x90FD;&amp;#x5728;&amp;#x52AA;&amp;#x529B;&amp;#x751F;&amp;#x4EA7;&amp;#xFF0C;&amp;#x4F46;&amp;#x662F;&amp;#x6BD5;&amp;#x7ADF;&amp;#x50E7;&amp;#x591A;&amp;#x7CA5;&amp;#x5C11;&amp;#xFF0C;&amp;#x4E5F;&amp;#x5E78;&amp;#x597D;&amp;#x75AB;&amp;#x60C5;&lt;/p&gt;</summary>
    
    
    
    <category term="架构" scheme="https://raye.wang/categories/%E6%9E%B6%E6%9E%84/"/>
    
    
    <category term="服务器" scheme="https://raye.wang/tags/%E6%9C%8D%E5%8A%A1%E5%99%A8/"/>
    
    <category term="Nginx" scheme="https://raye.wang/tags/Nginx/"/>
    
  </entry>
  
  <entry>
    <title>基于IDEA热部署更新服务器Tomcat类，服务器Tomcat热更新</title>
    <link href="https://raye.wang/2019/12/12/%E5%9F%BA%E4%BA%8EIDEA%E7%83%AD%E9%83%A8%E7%BD%B2%E6%9B%B4%E6%96%B0%E6%9C%8D%E5%8A%A1%E5%99%A8Tomcat%E7%B1%BB%EF%BC%8C%E6%9C%8D%E5%8A%A1%E5%99%A8Tomcat%E7%83%AD%E6%9B%B4%E6%96%B0/"/>
    <id>https://raye.wang/2019/12/12/%E5%9F%BA%E4%BA%8EIDEA%E7%83%AD%E9%83%A8%E7%BD%B2%E6%9B%B4%E6%96%B0%E6%9C%8D%E5%8A%A1%E5%99%A8Tomcat%E7%B1%BB%EF%BC%8C%E6%9C%8D%E5%8A%A1%E5%99%A8Tomcat%E7%83%AD%E6%9B%B4%E6%96%B0/</id>
    <published>2019-12-11T16:00:00.000Z</published>
    <updated>2026-03-10T06:59:04.572Z</updated>
    
    <content type="html"><![CDATA[<h4 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h4><p>在开发过程中，如果我们是使用的IDEA，就会知道IDEA有一个热更新的功能，何为热更新？就是在不重启Tomcat的情况下让服务器中的代码变更为最新的。这样既能快速的更新代码，又不用担心Tomcat重启session过期的问题，如同更新jsp一般的方便，当然IDEA里面的热更新还是没有更新jsp那么方便，具体限制本文后面有提（只是我目前了解到的限制）。如果说本地更新还算快捷，那么服务器更新就更加麻烦了，一般我们更新服务器上面的程序有2种情况，一是全量更新，就是打包war包去更新，二是部分更新，就是修改了那个class，就去服务器替换某个class文件，然后重启服务器（jsp文件不算，因为更新jsp是不需要重启Tomcat的，具体原因自己行百度）。这样常规的更新有2个弊端，第一：耗费时间长，不管是文件上传的时间，还是重启服务的时间，尤其是在项目很大启动很慢的时候，有时候只是更新一个简单的东西，却要等待数分钟，而且这数分钟还不是一个人的时间，是所有当时在用这台服务器的人的时间，第二：session失效的问题，服务器一旦重启，所有登录的用户就需要重新登录（当然如果session不是保存在Tomcat中的就另当别论了）。</p><p>基于以上种种，如果更新服务器能像我们本地IDEA一样方便，那么会节省我们很多的时间，加快各方人马的开发进度。幸好这种方式是有的，可以实现在IDEA中直接更新服务器代码，此方式是基于Tomcat远程调试的（不清楚远程调试的同学建议百度，也是一个挺好用的功能），而且不需要做其他配置，所以直接进入配置环节</p><h4 id="Tomcat远程调试配置"><a href="#Tomcat远程调试配置" class="headerlink" title="Tomcat远程调试配置"></a>Tomcat远程调试配置</h4><p>因为只要配置好了IDEA的远程调试，就会自动生成服务器相关的Tomcat配置，所以可以先从IDEA开始配置</p><h5 id="IDEA配置"><a href="#IDEA配置" class="headerlink" title="IDEA配置"></a>IDEA配置</h5><p>首先点击启动项，在弹出的界面中点击Edit Configurations…，如下图所示  </p><p><img src="http://image.raye.wang/image/e/43/38272b9f743de4e18c0b7644e0d72.png" alt="Edit Configurations..."></p><p>然后点击加号添加一个远程服务器  </p><p><img src="http://image.raye.wang/image/7/48/8dea5ed730ce03cd4d3cb0e52d63e.png" alt="添加远程服务器"></p><p>在弹出来的列表中选中Remote添加  </p><p><img src="http://image.raye.wang/image/5/8b/9a649e6a287743873b7965e5a008f.png" alt="Remote"></p><p>然后在右边的界面中配置启动项的名称，服务器的地址和端口  </p><p><img src="http://image.raye.wang/image/3/85/2fd8c371524b22037a41f1f142e92.png" alt="配置地址和端口"></p><p>端口需要服务器中没有被占用的端口，一旦正确配置了服务器的地址和端口，Command line arguments for remote JVM中自动生成的服务器JVM配置就是正确的了，复制好里面的内容，然后保存刚才的配置</p><h5 id="Tomcat配置"><a href="#Tomcat配置" class="headerlink" title="Tomcat配置"></a>Tomcat配置</h5><p>进入Tomcat目录下bin目录修改catalina.sh文件（Linux），如果是Windows则修改catalina.bat文件，在前面加入export JAVA_OPTS&#x3D;’xxx’,其中xxx是刚刚Command line arguments for remote JVM中复制的内容，然后重启服务器就可以了</p><h4 id="使用"><a href="#使用" class="headerlink" title="使用"></a>使用</h4><p>配置完成后，需要热更新的时候，就先从启动项中选中配置的远程服务器启动，然后修改代码，点击编译按钮，编译完成后会提示是否更新，点击确定就能更新，更新完成无论成功还是失败都会有相应的提示，跟IDEA在本地热更新是一样的，当然这样热更新也又诸多限制，代码修改限制跟IDEA本地热更新限制相同</p><h4 id="限制"><a href="#限制" class="headerlink" title="限制"></a>限制</h4><p>1：只能更新到方法体的修改，如果类本身添加了属性或者方法以及方法修改了参数又或者新增了类是无法更新的（IDEA本身可以通过JRebel插件热更新这些不能更新的东西）<br>2：想要热更新就必须要保证本地未修改前的代码跟服务器的代码是一致的，否则会导致更新不成功<br>3：热更新后并不会覆盖class文件，也就是说重启Tomcat后，热更新的代码就失效了，所以重启Tomcat时需要覆盖被热更新的class</p>]]></content>
    
    
    <summary type="html">&lt;h4 id=&quot;-&quot;&gt;&amp;#x524D;&amp;#x8A00;&lt;/h4&gt;&lt;p&gt;&amp;#x5728;&amp;#x5F00;&amp;#x53D1;&amp;#x8FC7;&amp;#x7A0B;&amp;#x4E2D;&amp;#xFF0C;&amp;#x5982;&amp;#x679C;&amp;#x6211;&amp;#x4EEC;&amp;#x662F;&amp;#x4F7F;&amp;#x7528;&amp;#x7684;IDEA&amp;#xFF0C;&amp;#x5C31;&amp;#x4F1A;&amp;#x77E5;&amp;#x9053;IDEA&amp;#x6709;&amp;#x4E00;&amp;#x4E2A;&amp;#x70ED;&amp;#x66F4;&amp;#x65B0;&amp;#x7684;&amp;#x529F;&amp;#x80FD;&amp;#xFF0C;&amp;#x4F55;&amp;#x4E3A;&amp;#x70ED;&amp;#x66F4;&amp;#x65B0;&amp;#xFF1F;&amp;#x5C31;&amp;#x662F;&amp;#x5728;&amp;#x4E0D;&amp;#x91CD;&amp;#x542F;Tomcat&amp;#x7684;&amp;#x60C5;&amp;#x51B5;&lt;/p&gt;</summary>
    
    
    
    <category term="Java" scheme="https://raye.wang/categories/Java/"/>
    
    
    <category term="JavaEE" scheme="https://raye.wang/tags/JavaEE/"/>
    
  </entry>
  
  <entry>
    <title>Springboot Druid 使用Slf4j输出可执行SQL</title>
    <link href="https://raye.wang/2019/06/28/Springboot-Druid-%E4%BD%BF%E7%94%A8Slf4j%E8%BE%93%E5%87%BA%E5%8F%AF%E6%89%A7%E8%A1%8CSQL/"/>
    <id>https://raye.wang/2019/06/28/Springboot-Druid-%E4%BD%BF%E7%94%A8Slf4j%E8%BE%93%E5%87%BA%E5%8F%AF%E6%89%A7%E8%A1%8CSQL/</id>
    <published>2019-06-27T16:00:00.000Z</published>
    <updated>2026-03-10T06:59:04.571Z</updated>
    
    <content type="html"><![CDATA[<h4 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h4><p>在开发中，为了数据安全，所有SQL语句肯定是用占位符的，但是在实际开发中，为了方便追踪问题，经常需要查看具体执行的SQL语句内容，而用了占位符之后，每次真实执行的语句只能靠自己根据参数拼接出来，耗时耗力，所以需要一个方法输出每次执行的SQL语句，并且还是有参数的（替换了占位符）,而Druid就支持此功能（Druid的主要功能可不是这个）,所以此文主要讲Springboot环境下使用Druid输出可执行SQL，并且是使用的Springboot的默认日志配置格式，因为一旦修改了Springboot的日志配置格式，输出来的日志一般是没有颜色区分的，如下：  </p><p><img src="http://image.raye.wang/image/7/29/93eeba4e0eae7223ca0841999a4d2.png" alt="修改后的日志图片"></p><p>而springboot默认配置的日志如下：  </p><p><img src="http://image.raye.wang/image/4/d8/a75d1b2804e805e5c782095c2deeb.png" alt="默认配置图片"></p><p>当然也可以自己配置出来颜色，只是我个人觉得springboot的默认配置已经很好看了，不用再配置了</p><p>当然Springboot配置Druid输出可执行的SQL文档太多了，只是他们都需要配置日志本身，也就是配置后日志输出的样子我不喜欢，所以各种研究后，才有了此文（之前百度是真没有找到用默认配置输出的方法）</p><h4 id="Druid-介绍"><a href="#Druid-介绍" class="headerlink" title="Druid 介绍"></a>Druid 介绍</h4><p>Druid是Java语言中最好的数据库连接池。Druid能够提供强大的监控和扩展功能。以上是官方的自我介绍，也侧面说明了Druid是一个功能强大，自带监控扩展的数据库连接池，具体想了解Druid的话建议去<a href="https://github.com/alibaba/druid/wiki/%E9%A6%96%E9%A1%B5">全球最大的同性交友基地</a>了解</p><h4 id="配置"><a href="#配置" class="headerlink" title="配置"></a>配置</h4><p>使用Springboot默认的日志配置文件，意味着就没有log4j.properties之类的文件给我们配置了，那就只有在application.yml或者application.properties中配置springboot告诉它我们哪些地方需要打印日志，需要打印什么级别的日志</p><p>首先要配置连接池为Druid</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">spring:</span><br><span class="line">  application:</span><br><span class="line">    name: admin-managee</span><br><span class="line">  datasource:</span><br><span class="line">    druid:</span><br><span class="line">      url: jdbc:mysql://localhost/test?useUnicode=true&amp;characterEncoding=UTF-8&amp;zeroDateTimeBehavior=convertToNull&amp;useSSL=false&amp;serverTimezone=CTT</span><br><span class="line">      username: test</span><br><span class="line">      password: test</span><br><span class="line">      driver-class-name: com.mysql.cj.jdbc.Driver</span><br></pre></td></tr></table></figure><p>使用Druid连接池跟之前不同的是多了一层druid，而其他的配置则是一样的</p><p>而之前用代码或者xml配置druid都在druid这个层级下，包括输出可执行SQL如下：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line">filters: stat,wall,stat,slf4j,default</span><br><span class="line">web-stat-filter:</span><br><span class="line">  enabled: true</span><br><span class="line">  url-pattern: /*</span><br><span class="line">  exclusions: &quot;*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*&quot;</span><br><span class="line">stat-view-servlet:</span><br><span class="line">  enabled: true</span><br><span class="line">  url-pattern: /druid/*</span><br><span class="line">  reset-enable: true</span><br><span class="line">  login-username: Raye</span><br><span class="line">  login-password: 123456</span><br><span class="line">filter:</span><br><span class="line">  slf4j:</span><br><span class="line">    enabled: true</span><br><span class="line">    statement-create-after-log-enabled: false</span><br><span class="line">    statement-log-enabled: false</span><br><span class="line">    statement-executable-sql-log-enable: true</span><br><span class="line">    statement-log-error-enabled: true</span><br><span class="line">    result-set-log-enabled: false</span><br></pre></td></tr></table></figure><p>其中只要statement-executable-sql-log-enable配置为true，则可以输出可执行SQL，当然如果不想用slf4j输出日志，用log4j只需要吧slf4j替换成log4j即可，不过还是推荐使用slf4j，至于为什么使用slf4j，百度很多人都已经说的很好了，另外druid的其他配置说明也请移步专业的druid文章，他们写的更加详细</p><p>完整配置如下：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line">spring:</span><br><span class="line">  application:</span><br><span class="line">    name: admin-managee</span><br><span class="line">  datasource:</span><br><span class="line">    druid:</span><br><span class="line">      url: jdbc:mysql://localhost/test?useUnicode=true&amp;characterEncoding=UTF-8&amp;zeroDateTimeBehavior=convertToNull&amp;useSSL=false&amp;serverTimezone=CTT</span><br><span class="line">      username: test</span><br><span class="line">      password: test</span><br><span class="line">      driver-class-name: com.mysql.cj.jdbc.Driver</span><br><span class="line">      filters: stat,wall,stat,slf4j,default</span><br><span class="line">      web-stat-filter:</span><br><span class="line">        enabled: true</span><br><span class="line">        url-pattern: /*</span><br><span class="line">        exclusions: &quot;*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*&quot;</span><br><span class="line">      stat-view-servlet:</span><br><span class="line">        enabled: true</span><br><span class="line">        url-pattern: /druid/*</span><br><span class="line">        reset-enable: true</span><br><span class="line">        login-username: test</span><br><span class="line">        login-password: 123456</span><br><span class="line">      filter:</span><br><span class="line">        slf4j:</span><br><span class="line">          enabled: true</span><br><span class="line">          statement-create-after-log-enabled: false</span><br><span class="line">          statement-log-enabled: false</span><br><span class="line">          statement-executable-sql-log-enable: true</span><br><span class="line">          statement-log-error-enabled: true</span><br><span class="line">          result-set-log-enabled: false</span><br><span class="line">logging:</span><br><span class="line">  level:</span><br><span class="line">    druid:</span><br><span class="line">      sql:</span><br><span class="line">        Statement: DEBUG</span><br></pre></td></tr></table></figure><h4 id="the-end"><a href="#the-end" class="headerlink" title="the end"></a>the end</h4>]]></content>
    
    
    <summary type="html">&lt;h4 id=&quot;-&quot;&gt;&amp;#x524D;&amp;#x8A00;&lt;/h4&gt;&lt;p&gt;&amp;#x5728;&amp;#x5F00;&amp;#x53D1;&amp;#x4E2D;&amp;#xFF0C;&amp;#x4E3A;&amp;#x4E86;&amp;#x6570;&amp;#x636E;&amp;#x5B89;&amp;#x5168;&amp;#xFF0C;&amp;#x6240;&amp;#x6709;SQL&amp;#x8BED;&amp;#x53E5;&amp;#x80AF;&amp;#x5B9A;&amp;#x662F;&amp;#x7528;&amp;#x5360;&amp;#x4F4D;&amp;#x7B26;&amp;#x7684;&amp;#xFF0C;&amp;#x4F46;&amp;#x662F;&amp;#x5728;&amp;#x5B9E;&amp;#x9645;&amp;#x5F00;&amp;#x53D1;&amp;#x4E2D;&amp;#xFF0C;&amp;#x4E3A;&amp;#x4E86;&amp;#x65B9;&amp;#x4FBF;&amp;#x8FFD;&amp;#x8E2A;&amp;#x95EE;&amp;#x9898;&amp;#xFF0C;&amp;#x7ECF;&amp;#x5E38;&amp;#x9700;&amp;#x8981;&lt;/p&gt;</summary>
    
    
    
    <category term="Java" scheme="https://raye.wang/categories/Java/"/>
    
    
    <category term="JavaEE" scheme="https://raye.wang/tags/JavaEE/"/>
    
    <category term="Druid" scheme="https://raye.wang/tags/Druid/"/>
    
  </entry>
  
  <entry>
    <title>SpringMVC,SpringBoot快速验证参数是否完整  基于@Valid注解</title>
    <link href="https://raye.wang/2019/05/07/SpringMVC-SpringBoot%E5%BF%AB%E9%80%9F%E9%AA%8C%E8%AF%81%E5%8F%82%E6%95%B0%E6%98%AF%E5%90%A6%E5%AE%8C%E6%95%B4-%E5%9F%BA%E4%BA%8E-Valid%E6%B3%A8%E8%A7%A3/"/>
    <id>https://raye.wang/2019/05/07/SpringMVC-SpringBoot%E5%BF%AB%E9%80%9F%E9%AA%8C%E8%AF%81%E5%8F%82%E6%95%B0%E6%98%AF%E5%90%A6%E5%AE%8C%E6%95%B4-%E5%9F%BA%E4%BA%8E-Valid%E6%B3%A8%E8%A7%A3/</id>
    <published>2019-05-06T16:00:00.000Z</published>
    <updated>2026-03-10T06:59:04.571Z</updated>
    
    <content type="html"><![CDATA[<h4 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h4><p>在接口开发中，经常是需要对传入参数完整性进行验证的，或者对手机号格式进行验证等，而一般不了解@Valid注解之前，都是通过自己封装方法来验证，都是一堆if else集合，这样判断对于代码结构很不友好，开发效率也不高，使用@Valid注解可以快速的对参数进行验证，并且可以通过抓取全局异常，从而实现统一返回</p><h4 id="引用"><a href="#引用" class="headerlink" title="引用"></a>引用</h4><p>@Valid是使用hibernate validation的时候使用,其中java的jsr303声明了这类接口，hibernate-validator对其进行了实现</p><p>在进行maven整合时，需要引入的依赖</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"> &lt;dependency&gt;</span><br><span class="line">    &lt;groupId&gt;org.hibernate.validator&lt;/groupId&gt;</span><br><span class="line">    &lt;artifactId&gt;hibernate-validator&lt;/artifactId&gt;</span><br><span class="line">    &lt;version&gt;6.0.13.Final&lt;/version&gt;</span><br><span class="line">&lt;/dependency&gt;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>一般情况下早已被其他框架引入了，只是在不存在的时候需要进行引用一下，@Valid只是验证参数是否符合要求，但是具体参数需要符合什么要求，还是需要其他注解来标志验证规则</p><h4 id="验证规则标签"><a href="#验证规则标签" class="headerlink" title="验证规则标签"></a>验证规则标签</h4><p>@Null       验证对象是否为null</p><p>@NotNull    验证对象是否不为null, 无法查检长度为0的字符串</p><p>@NotBlank 检查约束字符串是不是Null还有被Trim的长度是否大于0,只对字符串,且会去掉前后空格.</p><p>@NotEmpty 检查约束元素是否为NULL或者是EMPTY.</p><p>@AssertTrue     验证 Boolean 对象是否为 true</p><p>@AssertFalse    验证 Boolean 对象是否为 false</p><p>@Size(min&#x3D;, max&#x3D;) 验证对象（Array,Collection,Map,String）长度是否在给定的范围之内</p><p>@Length(min&#x3D;, max&#x3D;) 验证字符串长度是否在给定的范围之内</p><p>@Past           验证 Date 和 Calendar 对象是否在当前时间之前</p><p>@Future     验证 Date 和 Calendar 对象是否在当前时间之后</p><p>@Pattern    验证 String 对象是否符合正则表达式的规则</p><p>@Min            验证 Number 和 String 对象是否大等于指定的值</p><p>@Max            验证 Number 和 String 对象是否小等于指定的值</p><p>@DecimalMax 被标注的值必须不大于约束中指定的最大值. 这个约束的参数是一个通过BigDecimal定义的最大值的字符串表示.小数存在精度</p><p>@DecimalMin 被标注的值必须不小于约束中指定的最小值. 这个约束的参数是一个通过BigDecimal定义的最小值的字符串表示.小数存在精度</p><p>@Digits     验证 Number 和 String 的构成是否合法</p><p>@Digits(integer&#x3D;,fraction&#x3D;) 验证字符串是否是符合指定格式的数字，interger指定整数精度，fraction指定小数精度。</p><p>@Range(min&#x3D;, max&#x3D;) 验证元素是否在合适的范围内</p><p>@Email  验证是否是邮件地址，如果为null,不进行验证，算通过验证。</p><h4 id="使用"><a href="#使用" class="headerlink" title="使用"></a>使用</h4><p>在需要验证的类属性上添加相应的注解，如：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">import lombok.Data;</span><br><span class="line">import org.hibernate.validator.constraints.NotBlank;</span><br><span class="line"></span><br><span class="line">@Data</span><br><span class="line">public class LoginVO &#123;</span><br><span class="line"></span><br><span class="line">    @NotBlank</span><br><span class="line">    private String username;</span><br><span class="line">    @NotBlank</span><br><span class="line">    private String password;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>在SpringMVC控制层中使用@Valid注解进行验证,并判断是否有异常</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">@PostMapping(&quot;login&quot;)</span><br><span class="line">public String login(@Valid LoginVO vo, Errors errors)&#123;</span><br><span class="line">    if (errors.hasErrors()) &#123;</span><br><span class="line">        List&lt;FieldError&gt; list = errors.getFieldErrors();</span><br><span class="line">        StringBuffer sb = new StringBuffer(&quot;参数：&quot;);</span><br><span class="line">        for(FieldError error : list)&#123;</span><br><span class="line">             sb.append(error.getField()).append(&quot;,&quot;);</span><br><span class="line">         &#125;</span><br><span class="line">         sb.append(&quot;不符合要求&quot;);</span><br><span class="line">         return sb.toString();</span><br><span class="line">     &#125;</span><br><span class="line">     return &quot;验证通过&quot;;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>当然上面Controller层的代码非常容易重复，毕竟差不多的方法都需要验证参数，所以为了方面和简洁代码，可以通过抓取全局异常的方式返回提示信息</p><h4 id="全局异常处理"><a href="#全局异常处理" class="headerlink" title="全局异常处理"></a>全局异常处理</h4><p>通过配置全局异常处理方法，抓取BindException，返回异常提示信息</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line">import com.wxkingbos.patrol.model.WebResult;</span><br><span class="line">import org.springframework.validation.BindException;</span><br><span class="line">import org.springframework.web.bind.annotation.ControllerAdvice;</span><br><span class="line">import org.springframework.web.bind.annotation.ExceptionHandler;</span><br><span class="line">import org.springframework.web.bind.annotation.ResponseBody;</span><br><span class="line"></span><br><span class="line">/**</span><br><span class="line"> * 全局异常处理器</span><br><span class="line"> * Created by Raye on 2019-04-15</span><br><span class="line"> */</span><br><span class="line">@ControllerAdvice</span><br><span class="line">public class ExceptionControllerAdvice &#123;</span><br><span class="line"></span><br><span class="line">    @ExceptionHandler(BindException.class)</span><br><span class="line">    @ResponseBody</span><br><span class="line">    public String bindExceptionHandler(BindException ex)&#123;</span><br><span class="line">        if(ex.hasErrors())&#123;</span><br><span class="line">            List&lt;FieldError&gt; list = ex.getFieldErrors();</span><br><span class="line">            StringBuffer sb = new StringBuffer(&quot;参数：&quot;);</span><br><span class="line">            for(FieldError error : list)&#123;</span><br><span class="line">                sb.append(error.getField()).append(&quot;,&quot;);</span><br><span class="line">            &#125;</span><br><span class="line">            sb.append(&quot;不符合要求&quot;);</span><br><span class="line">            return sb.toString();</span><br><span class="line">        &#125;</span><br><span class="line">        return ex.getMessage();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>同理，也可以采用此方法抓取其他全局异常，进行异常日志捕捉获取其他异常提示，另外以上全局异常配置只有在Springboot项目上运行过，因为现在项目差不多都是基于Springboot构建的了，所以也没有在非Springboot上面测试过可行性</p>]]></content>
    
    
    <summary type="html">&lt;h4 id=&quot;-&quot;&gt;&amp;#x524D;&amp;#x8A00;&lt;/h4&gt;&lt;p&gt;&amp;#x5728;&amp;#x63A5;&amp;#x53E3;&amp;#x5F00;&amp;#x53D1;&amp;#x4E2D;&amp;#xFF0C;&amp;#x7ECF;&amp;#x5E38;&amp;#x662F;&amp;#x9700;&amp;#x8981;&amp;#x5BF9;&amp;#x4F20;&amp;#x5165;&amp;#x53C2;&amp;#x6570;&amp;#x5B8C;&amp;#x6574;&amp;#x6027;&amp;#x8FDB;&amp;#x884C;&amp;#x9A8C;&amp;#x8BC1;&amp;#x7684;&amp;#xFF0C;&amp;#x6216;&amp;#x8005;&amp;#x5BF9;&amp;#x624B;&amp;#x673A;&amp;#x53F7;&amp;#x683C;&amp;#x5F0F;&amp;#x8FDB;&amp;#x884C;&amp;#x9A8C;&amp;#x8BC1;&amp;#x7B49;&amp;#xFF0C;&amp;#x800C;&amp;#x4E00;&amp;#x822C;&amp;#x4E0D;&amp;#x4E86;&amp;#x89E3;@Valid&amp;#x6CE8;&lt;/p&gt;</summary>
    
    
    
    <category term="Java" scheme="https://raye.wang/categories/Java/"/>
    
    
    <category term="JavaEE" scheme="https://raye.wang/tags/JavaEE/"/>
    
    <category term="Springboot" scheme="https://raye.wang/tags/Springboot/"/>
    
  </entry>
  
  <entry>
    <title>Linux配置Tomcat日志轮转（切割）</title>
    <link href="https://raye.wang/2019/02/26/Linux%E9%85%8D%E7%BD%AETomcat%E6%97%A5%E5%BF%97%E8%BD%AE%E8%BD%AC%EF%BC%88%E5%88%87%E5%89%B2%EF%BC%89/"/>
    <id>https://raye.wang/2019/02/26/Linux%E9%85%8D%E7%BD%AETomcat%E6%97%A5%E5%BF%97%E8%BD%AE%E8%BD%AC%EF%BC%88%E5%88%87%E5%89%B2%EF%BC%89/</id>
    <published>2019-02-25T16:00:00.000Z</published>
    <updated>2026-03-10T06:59:04.570Z</updated>
    
    <content type="html"><![CDATA[<p>服务器每天都会产生很多各种各样的日志，尤其是生产环境，因为很多日志需要保留一段时间，防止这段时间内出现了问题却没有找到日志文件,当然为了更好的查询日志和定位错误，推荐搭建EFK(ELK)日志分析系统（<a href="http://raye.wang/da-jian-elkfen-bu-shi-ri-zhi-huan-jing/">centos7搭建EFK日志分析系统</a>），但是就算搭建了EFK，也还是需要定时删除原始的日志文件，而如果没有对原始日志文件进行分割的话，一旦删除就会把整个原始日志文件删除掉，万一某一部分日志还未写入EFK中，那么就会导致日志丢失，而且如果日志没有分割，需要查看原始日志的时候也是一件痛苦的事情，毕竟生产环境每天都能参数几个G的日志，如此庞大的日志，使用Linux的命令查看搜索都是耗时耗力的，所以日志分割不管是对于测试环境还是生产环境来说，都是有必要的，而分割日志也叫日志轮转，可以自己写脚本处理，当然也可以使用Linux本来就有的服务：<strong>logrotate</strong></p><h4 id="logrotate简介"><a href="#logrotate简介" class="headerlink" title="logrotate简介"></a>logrotate简介</h4><p>logrotate程序是一个日志文件管理工具，它可以自动对日志进行截断（或轮循）、压缩以及删除旧的日志文件，并创建新的日志文件，起到“转储”作用。它是基于CRON来运行的，其脚本是「&#x2F;etc&#x2F;cron.daily&#x2F;logrotate」。主流Linux发行版上都默认安装有logrotate包</p><h4 id="配置文件介绍"><a href="#配置文件介绍" class="headerlink" title="配置文件介绍"></a>配置文件介绍</h4><p>Linux系统默认安装logrotate工具，它默认的配置文件在：</p><p>&#x2F;etc&#x2F;logrotate.conf</p><p>&#x2F;etc&#x2F;logrotate.d&#x2F;</p><p>logrotate.conf 才主要的配置文件，logrotate.d 是一个目录，该目录里的所有文件都会被主动的读入&#x2F;etc&#x2F;logrotate.conf中执行。<br>另外，如果 &#x2F;etc&#x2F;logrotate.d&#x2F; 里面的文件中没有设定一些细节，则会以&#x2F;etc&#x2F;logrotate.conf这个文件的设定来作为默认值。</p><p>虽然可以将所有的配置都写入&#x2F;etc&#x2F;logrotate.conf ，但是这样一来这个文件就实在是太复杂了，尤其是当使用很多的服务在系统上面时， 每个服务都要去修改&#x2F;etc&#x2F;logrotate.conf的设定也似乎不太合理了。<br>所以，如果独立出来一个目录，那么每个要切割日志的服务， 就可以独自成为一个文件，并且放置到 &#x2F;etc&#x2F;logrotate.d&#x2F; 当中</p><h4 id="logrotate的Tomcat配置"><a href="#logrotate的Tomcat配置" class="headerlink" title="logrotate的Tomcat配置"></a>logrotate的Tomcat配置</h4><p>首先进入&#x2F;etc&#x2F;logrotate.d&#x2F;目录创建一个文件名为Tomcat的文件，文件名可以自由定义，但是为了方便理解和维护，最好和需要轮转日志的服务名相同</p><p>logrotate.conf默认配置：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line">weekly</span><br><span class="line">rotate 4</span><br><span class="line">create</span><br><span class="line">dateext</span><br><span class="line">include /etc/logrotate.d</span><br><span class="line">/var/log/wtmp &#123;</span><br><span class="line">    monthly</span><br><span class="line">    create 0664 root utmp</span><br><span class="line">        minsize 1M</span><br><span class="line">    rotate 1</span><br><span class="line">&#125;</span><br><span class="line">/var/log/btmp &#123;</span><br><span class="line">    missingok</span><br><span class="line">    monthly</span><br><span class="line">    create 0600 root utmp</span><br><span class="line">    rotate 1</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>weekly          默认每一周执行一次rotate轮转工作<br>rotate 4       保留多少个日志文件(轮转几次).默认保留四个.就是指定日志文件删除之前轮转的次数，0 指没有备份<br>create        自动创建新的日志文件，新的日志文件具有和原来的文件相同的权限；因为日志被改名,因此要创建一个新的来继续存储之前的日志<br>dateext       这个参数很重要！就是切割后的日志文件以当前日期为格式结尾，如xxx.log-20131216这样,如果注释掉,切割出来是按数字递增,即前面说的 xxx.log-1这种格式<br>&#x2F;var&#x2F;log&#x2F;wtmp和&#x2F;var&#x2F;log&#x2F;btmp是对默认2个日志进行轮转的配置</p><p>以下是我对于Tomcat日志的轮转的配置</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">/vdb1/tomcat/catalina.out&#123;</span><br><span class="line">    copytruncate</span><br><span class="line">    daily</span><br><span class="line">    rotate 70</span><br><span class="line">    missingok</span><br><span class="line">    size 1G</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>首先&#x2F;vdb1&#x2F;tomcat&#x2F;catalina.out是需要被轮转的日志文件的路径</p><p>copytruncate用于还在打开中的日志文件，把当前日志备份并截断；是先拷贝再清空的方式，拷贝和清空之间有一个时间差，可能会丢失部分日志数据。但是由于Tomcat的特殊性，一旦直接删除日志可能会导致日志输出流有问题，一旦发生轮转后，新的日志不会写入到日志文件，所以这里用copytruncate。</p><p>daily 指定转储周期为每天。</p><p>rotate 70 指定日志文件删除之前转储的次数，0 指没有备份，70代表有70个备份。<br>missingok  如果日志丢失，不报错继续滚动下一个日志。<br>size 1G 当日志文件到达指定的大小时才转储，log-size能指定bytes(缺省)及KB 或MB者G。这个条件跟daily是并且关系，也就是说虽然每天会轮转，但是如果日志没有到达1g也不会执行轮转计划</p><p>以上配置能实现每天检查日志，如果到达了1g则进行日志轮转</p><h4 id="logrotate-主要参数说明"><a href="#logrotate-主要参数说明" class="headerlink" title="logrotate 主要参数说明"></a>logrotate 主要参数说明</h4><p>compress                                   通过gzip 压缩转储以后的日志<br>nocompress                                不做gzip压缩处理<br>copytruncate                              用于还在打开中的日志文件，把当前日志备份并截断；是先拷贝再清空的方式，拷贝和清空之间有一个时间差，可能会丢失部分日志数据。<br>nocopytruncate                           备份日志文件不过不截断<br>create mode owner group             轮转时指定创建新文件的属性，如create 0777 nobody nobody<br>nocreate                                    不建立新的日志文件<br>delaycompress                           和compress 一起使用时，转储的日志文件到下一次转储时才压缩<br>nodelaycompress                        覆盖 delaycompress 选项，转储同时压缩。<br>missingok                                 如果日志丢失，不报错继续滚动下一个日志<br>errors address                           专储时的错误信息发送到指定的Email 地址<br>ifempty                                    即使日志文件为空文件也做轮转，这个是logrotate的缺省选项。<br>notifempty                               当日志文件为空时，不进行轮转<br>mail address                             把转储的日志文件发送到指定的E-mail 地址<br>nomail                                     转储时不发送日志文件<br>olddir directory                         转储后的日志文件放入指定的目录，必须和当前日志文件在同一个文件系统<br>noolddir                                   转储后的日志文件和当前日志文件放在同一个目录下<br>sharedscripts                           运行postrotate脚本，作用是在所有日志都轮转后统一执行一次脚本。如果没有配置这个，那么每个日志轮转后都会执行一次脚本<br>prerotate                                 在logrotate转储之前需要执行的指令，例如修改文件的属性等动作；必须独立成行<br>postrotate                               在logrotate转储之后需要执行的指令，例如重新启动 (kill -HUP) 某个服务！必须独立成行<br>daily                                       指定转储周期为每天<br>weekly                                    指定转储周期为每周<br>monthly                                  指定转储周期为每月<br>rotate count                            指定日志文件删除之前转储的次数，0 指没有备份，5 指保留5 个备份<br>dateext                                  使用当期日期作为命名格式<br>dateformat .%s                       配合dateext使用，紧跟在下一行出现，定义文件切割后的文件名，必须配合dateext使用，只支持 %Y %m %d %s 这四个参数<br>size(或minsize) log-size            当日志文件到达指定的大小时才转储，log-size能指定bytes(缺省)及KB 或MB者G<br>当日志文件 &gt;&#x3D; log-size 的时候就转储。 以下为合法格式：（其他格式的单位大小写没有试过）<br>size &#x3D; 5 或 size 5 （&gt;&#x3D; 5 个字节就转储）<br>size &#x3D; 100k 或 size 100k<br>size &#x3D; 100M 或 size 100M<br>size &#x3D; 100G 或 size 100G</p><h4 id="其他命令和关键点"><a href="#其他命令和关键点" class="headerlink" title="其他命令和关键点"></a>其他命令和关键点</h4><p>手动轮转日志</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"># /usr/sbin/logrotate -f /etc/logrotate.conf</span><br></pre></td></tr></table></figure><p>不过正式执行前最好通过Debug选项来验证一下（-d参数），这对调试也很重要</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"># /usr/sbin/logrotate -d -f /etc/logrotate.conf</span><br></pre></td></tr></table></figure><p>另外由于logrotate 是基于CRON来运行的，所以一定要保证CRON是开启状态才能确保logrotate 运行，另外logrotate 的执行时间也是由CRON控制的，具体可以查询CRON的配置文件&#x2F;etc&#x2F;anacrontab（老版本的文件是&#x2F;etc&#x2F;crontab）来确认logrotate的执行时间</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;&amp;#x670D;&amp;#x52A1;&amp;#x5668;&amp;#x6BCF;&amp;#x5929;&amp;#x90FD;&amp;#x4F1A;&amp;#x4EA7;&amp;#x751F;&amp;#x5F88;&amp;#x591A;&amp;#x5404;&amp;#x79CD;&amp;#x5404;&amp;#x6837;&amp;#x7684;&amp;#x65E5;&amp;#x5FD7;&amp;#xFF0C;&amp;#x5C24;&amp;#x5176;&amp;#x662F;&amp;#x751F;&amp;#x4EA7;&amp;#x73AF;&amp;#x5883;&amp;#xFF0C;&amp;#x56E0;&amp;#x4E3A;&amp;#x5F88;&amp;#x591A;&amp;#x65E5;&amp;#x5FD7;&amp;#x9700;&amp;#x8981;&amp;#x4FDD;&amp;#x7559;&amp;#x4E00;&amp;#x6BB5;&amp;#x65F6;&amp;#x95F4;&amp;#xFF0C;&amp;#x9632;&amp;#x6B62;&amp;#x8FD9;&amp;#x6BB5;&amp;#x65F6;&amp;#x95F4;&amp;#x5185;&amp;#x51FA;&lt;/p&gt;</summary>
    
    
    
    <category term="服务器" scheme="https://raye.wang/categories/%E6%9C%8D%E5%8A%A1%E5%99%A8/"/>
    
    
    <category term="服务器" scheme="https://raye.wang/tags/%E6%9C%8D%E5%8A%A1%E5%99%A8/"/>
    
  </entry>
  
  <entry>
    <title>定时同步数据的任务优化</title>
    <link href="https://raye.wang/2018/11/27/%E5%AE%9A%E6%97%B6%E5%90%8C%E6%AD%A5%E6%95%B0%E6%8D%AE%E7%9A%84%E4%BB%BB%E5%8A%A1%E4%BC%98%E5%8C%96/"/>
    <id>https://raye.wang/2018/11/27/%E5%AE%9A%E6%97%B6%E5%90%8C%E6%AD%A5%E6%95%B0%E6%8D%AE%E7%9A%84%E4%BB%BB%E5%8A%A1%E4%BC%98%E5%8C%96/</id>
    <published>2018-11-26T16:00:00.000Z</published>
    <updated>2026-03-10T06:59:04.572Z</updated>
    
    <content type="html"><![CDATA[<h4 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h4><p>定时任务在系统中并不少见，主要目的是用于需要定时处理数据或者执行某个操作的情况下，如定时关闭订单，或者定时备份。而常见的定时任务分为2种，第一种：固定时间执行，如：每分钟执行一次，每天执行一次。第二种：延时多久执行，就是当发生一件事情后，根据这件时间发生的时间定时多久后执行任务，如：15分钟后关闭订单付款状态，24小时候后关闭订单并且释放库存，而由于第二种一般都是单一数据的处理（主要是指数据量不大，一般情况下只有一个主体处理对象，如：一个订单以及订单中的N个商品），所以一般情况下第二种出现性能问题的几率不大（不代表没有），所以本文主要是针对第一种定时任务来进行优化，而且主要是针对数据同步或者传递数据来进行优化，而优化的方式也是根据实际项目中的情况在不同阶段进行优化的</p><h4 id="第一阶段"><a href="#第一阶段" class="headerlink" title="第一阶段"></a>第一阶段</h4><p>第一阶段属于原始阶段，逻辑也最为简单，由于同步分为数据同步和传递数据，而且2种的需求各不一致（主要是在于是否允许丢失），所以分开分析</p><h6 id="第一种类型：传递数据"><a href="#第一种类型：传递数据" class="headerlink" title="第一种类型：传递数据"></a>第一种类型：传递数据</h6><p>由于传递数据可以允许丢失，常见的场景如调用凭证推送（常见于接口需要暴露给第三方，为了安全性，可以定时推送调用凭证来保证接口安全性），消息推送（订单消费成功后推送消息，由于可能推送失败，所以需要进入定时任务进行重试，但是因为消息实时性，所以重试到一定次数后放弃重试）</p><p>传递数据在第一阶段设计非常简单，定时推送，有限的错误次数，同步成功后修改状态，同步失败后对失败次数+1，一旦超过错误次数，就不在继续尝试</p><h6 id="第二种类型：同步数据"><a href="#第二种类型：同步数据" class="headerlink" title="第二种类型：同步数据"></a>第二种类型：同步数据</h6><p>同步数据跟传递数据不同点在于同步数据一定需要保证数据能投递成功，否则就要一直进行重试，比如2个系统间的订单同步，会员信息同步等</p><p>同步数据再第一阶段也非常简单，定时同步数据，失败就设置同步状态为同步失败，每次同步就只查询状态为未同步和同步失败记录</p><h4 id="第二阶段"><a href="#第二阶段" class="headerlink" title="第二阶段"></a>第二阶段</h4><p>一开始需要传递或者同步的系统很少，数据也少，所以没有什么问题，但是第二阶段不一样了，数据量稍微有所新增，但是增量不大，主要是需要同步的系统多了，打个比方，连锁商店，总部需要把数据下传到所有门店去，这样门店就不用每次去总部获取数据，这样太耗费时间了，当然门店每次从总部获取到数据可以缓存到本地，不过跟本文内容关系不大，所以这里不再讨论。由于需要同步的系统太多，所以延伸出另外一个问题，一旦一个系统的网络环境不好，会影响其他系统数据同步，所以在第二阶段，引入了黑名单机制，由于黑名单机制对于传递数据和同步数据大致相同，所以这里就不分开描述，有差异性的地方也会指出</p><h6 id="黑名单具体处理机制"><a href="#黑名单具体处理机制" class="headerlink" title="黑名单具体处理机制"></a>黑名单具体处理机制</h6><p>黑名单分二级：</p><p>第一级用于控制本次定时任务，当本次运行定时任务时，不同的接受数据服务器可能有0-N条数据需要同步，所以一旦进入第一级黑名单后，本次后面都不会向接受数据服务器发起请求，而是直接失败；</p><p>第二级用于控制多长时间内不进入重试，是控制整个的，从查询需要同步的数据时候就直接过滤并且设置为同步失败状态（传递消息需要对失败次数加1）</p><p>首先第一级，当请求不到接受数据的服务器的时候（链接失败，或者链接超时），会再重试2次（传递数据由于及时性要求，所以不会重试，并且超时时间也会合理的减少），如果2次都同步失败，这判断本条数据同步失败，并且进入第一级黑名单，并且判断一定时间内进入了几次第一级黑名单，具体使用redis控制，首先是否进入第一级黑名单直接程序中存储就好，一定时间段内进入了几次黑名单，就使用有序集合保存，排序的分值就存储当前时间戳</p><p>进入第一级黑名单后，使用一定时间内进入几次的限制条件，来判断是否进入第二级黑名单，比如5分钟进入3次第一级黑名单，就进入第二级黑名单，那么就查询分值大于5分钟前时间戳的数据集合，如果集合结果有3条或以上数据了，那么就进入第二级黑名单，同时清理掉redis中关于第一级黑名单存储的数据，如果没有3条数据，那么就删除分值小于5分钟前的时间戳的数据，避免垃圾数据过多</p><p>使用黑名单机制，可以有效避免一些因为服务本来不可访问导致一直还重试的问题，并且由于有二级黑名单，所以也一定程度上避免了因为暂时网络波动，导致数据长久无法同步的问题</p><h4 id="第三阶段"><a href="#第三阶段" class="headerlink" title="第三阶段"></a>第三阶段</h4><p>由于需要传递的数据和需要同步数据的服务越来越多，并且由于各种问题导致很多数据不能一次性同步成功，所以每次定时任务都需要同步大量数据，这样就导致及时性很差了，比如几千条数据同步下来，就算一条只需要几十毫秒，从开始到最后一条数据同步成功也是几十秒之后了，所以需要再次对定时任务进行优化，数据量大而导致同步慢原因很简单，是由于单个线程串行同步的，也就是说必须要上一条数据处理了才能处理下一条数据，所以可以使用多线程来优化，提高硬件使用率</p><h6 id="多线程的定时任务"><a href="#多线程的定时任务" class="headerlink" title="多线程的定时任务"></a>多线程的定时任务</h6><p>当然肯定不可能给每条数据创建一个线程，先不说得创建多少条线程，仅仅是创建线程的消耗就已经很大了，而且线程数量太多，频繁切换线程上下文也会导致性能损耗，所以最合适的就是将数据分配到机器CPU核心数量的线程，或者核心数量*2的线程上去处理更合适，当然具体情况具体分析，最好还是具体测试得出合适的线程数量，同时由于肯定是会存在多个定时任务，所以可以多个定时任务使用同一个线程池，但是每个任务只使用合适线程数量来处理</p><p><strong>线程数据分配原则</strong></p><p>同一个被接受调用的数据的服务器的数据肯定是分配到一个线程中去处理，比如要分配8个线程来处理，那么可以创建8个集合，先保存查询出来需要被同步的数据，同时查询出来的数据根据被接受数据的服务器标识排序，用接受数据的服务器标识的hash值来%8来确定放入哪个集合，或者使用轮询的方式放入指定集合，分配好之后则创建8个runable放入线程池中去执行</p><p><strong>防止定时任务叠加</strong></p><p>开启多线程处理后，由于主线程在把任务放入线程池中运行的时候就会返回了，所以一定需要防止定时任务叠加，比如任务是10秒执行一次的，每次定时任务本身的线程只执行了1秒，下次定时任务的时候会发现定时任务已经处理完成，但是实际上真正同步数据的8个线程都没有执行完成，就会出现一条数据重复同步，或者把数据累加到上次任务的集合中去（看具体的处理方式导致不同的结果），最后就跟滚雪球一样，整个服务就算不崩溃，也会出现各种问题，或者就是浪费大量资源去做重复同步，所以为了防止任务叠加，需要使用闭锁来防止定时任务本身返回的情况，同时使用闭锁也要注意处理异常的情况，防止发生异常后，闭锁没有执行操作，导致定时任务一直不能返回</p><p><strong>闭锁</strong></p><p>使用闭锁防止定时任务返回，8个线程的情况下创建闭锁</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">CountDownLatch latch = new CountDownLatch(8);</span><br></pre></td></tr></table></figure><p>每个线程执行完数据后需要countDown方法来通知，或者叫关闭一个栅栏吧，创建闭锁的传入的8我们可以看成创建了8个栅栏</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">latch.countDown();</span><br></pre></td></tr></table></figure><p>同时在定时任务的线程中，需要等待所有栅栏关闭才能继续执行，所以需要调用方法</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">latch.await();</span><br></pre></td></tr></table></figure><p>这样只有所有线程执行完成后，定时任务的线程才会继续执行，防止任务叠加</p><p>使用多线程了，一定要注意多线程的一些线程安全以及其他的一些问题，如果对闭锁和多线程本身不够了解的话，可以自行去查阅一些相关资料</p><h4 id="第四阶段"><a href="#第四阶段" class="headerlink" title="第四阶段"></a>第四阶段</h4><p>数据量非常大，接受数据的服务也非常多</p><p>一台服务器的硬件资源始终有限，尤其是网络资源，由于接受数据的服务不一定是内网服务，加上各种问题导致链接失败，所以数据量大的情况下，就算使用了多线程，还是会造成数据延迟很久才同步成功（主要延迟原因是网络问题），这时候就需要使用多台服务器了，而使用多台服务器定时执行就存在一个问题，数据分片，简单来说怎么保证一条数据只能被一台服务器处理，数据分片有2种方式，第一种：不同服务器处理不同的表的数据。第二种：数据本身主键或者某种标志分配处理</p><p>2种处理方式有各自的优缺点</p><p><strong>第一种：</strong></p><p>优点：简单，只需要简单拆分或者配置即可</p><p>缺点：无法扩展更多，最多只能可能扩展到数据表数量台服务器，并且对于热点数据无法更优处理，比如订单这些热点数据，始终都在一台服务器</p><p><strong>第二种：</strong></p><p>优点：理论上可无限扩展，可以针对热点数据专门扩展</p><p>缺点：配置麻烦，每次新增服务器需要重新配置</p><h6 id="实现分片定时任务"><a href="#实现分片定时任务" class="headerlink" title="实现分片定时任务"></a>实现分片定时任务</h6><p>由于第一种配置简单，而且扩展性不强，所以本文主要讲述第二种方式的实现；</p><p>如果所有数据有生成都有自增型主键id，那么最简单也最公平的就是给每台服务器配置一个从0开始连续的服务器id，每台服务器查询数据的时候加一个条件id%服务器台数&#x3D;当前服务器id，注意这样会导致id列的索引可能无法命中（根据数据库不同，是否命中情况不一致），这样配置的好处就是绝对公平，每台服务器分配到的数据量是平等的，坏处就是一台服务器可能会给所有接受数据服务发起请求，无法更好的利用链接复用，另外也无法针对服务器配置来增加或者降低权重（当然可以一个服务器配置2个id的方式来实现，但是这样也不友好）</p><p>如果为了更好的利用链接复用，可以使用先计算出接受数据服务标志的hashcode值，然后跟进hashcode值%服务器台数&#x3D;当前服务器id的形式，这样就可以将接受数据服务分组式的配置到某个服务器上去处理，当然如果接受数据服务本身存在很大的数据量差异，就不推荐这种方式了，毕竟这样容易把大量数据堆积到某台服务器上去处理</p><p>当然还有其他多种分片的配置方式，比如采用表配置的方式来配置哪台服务器处理哪些数据，也可以使用上面种方式的结合体，可以根据具体情况分析到底怎么样才能更适合的进行数据分片处理，当然常规情况下，采用id%服务器的台数是能满足大部分需求的</p><h4 id="其他优化"><a href="#其他优化" class="headerlink" title="其他优化"></a>其他优化</h4><p>当系统针对性能优化到一定程度的时候，就可以考虑从业务或者其他方面进行优化了，比如一旦有系统进入二级黑名单了，就发出警告通知，或者没有进入二级黑名单，但是却经常进入一级黑名单，也提出一个报警，这样可以让人去排查原因，确认是程序问题还是网络本身的问题。另外也可以设置一个阈值，某个接受数据的服务一直响应很慢，或者经常响应时间超过某个阈值的时候，可以考虑进行降权处理，或者排查程序已经网络相关的原因</p>]]></content>
    
    
    <summary type="html">&lt;h4 id=&quot;-&quot;&gt;&amp;#x524D;&amp;#x8A00;&lt;/h4&gt;&lt;p&gt;&amp;#x5B9A;&amp;#x65F6;&amp;#x4EFB;&amp;#x52A1;&amp;#x5728;&amp;#x7CFB;&amp;#x7EDF;&amp;#x4E2D;&amp;#x5E76;&amp;#x4E0D;&amp;#x5C11;&amp;#x89C1;&amp;#xFF0C;&amp;#x4E3B;&amp;#x8981;&amp;#x76EE;&amp;#x7684;&amp;#x662F;&amp;#x7528;&amp;#x4E8E;&amp;#x9700;&amp;#x8981;&amp;#x5B9A;&amp;#x65F6;&amp;#x5904;&amp;#x7406;&amp;#x6570;&amp;#x636E;&amp;#x6216;&amp;#x8005;&amp;#x6267;&amp;#x884C;&amp;#x67D0;&amp;#x4E2A;&amp;#x64CD;&amp;#x4F5C;&amp;#x7684;&amp;#x60C5;&amp;#x51B5;&amp;#x4E0B;&amp;#xFF0C;&amp;#x5982;&amp;#x5B9A;&amp;#x65F6;&amp;#x5173;&amp;#x95ED;&amp;#x8BA2;&amp;#x5355;&lt;/p&gt;</summary>
    
    
    
    <category term="Java" scheme="https://raye.wang/categories/Java/"/>
    
    
    <category term="JavaEE" scheme="https://raye.wang/tags/JavaEE/"/>
    
  </entry>
  
  <entry>
    <title>centos7搭建EFK日志分析系统</title>
    <link href="https://raye.wang/2018/09/14/centos7%E6%90%AD%E5%BB%BAEFK%E6%97%A5%E5%BF%97%E5%88%86%E6%9E%90%E7%B3%BB%E7%BB%9F/"/>
    <id>https://raye.wang/2018/09/14/centos7%E6%90%AD%E5%BB%BAEFK%E6%97%A5%E5%BF%97%E5%88%86%E6%9E%90%E7%B3%BB%E7%BB%9F/</id>
    <published>2018-09-13T16:00:00.000Z</published>
    <updated>2026-03-10T06:59:04.571Z</updated>
    
    <content type="html"><![CDATA[<h4 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h4><p>EFK可能都不熟悉，实际上EFK是大名鼎鼎的日志系统ELK的一个变种</p><p>在没有分布式日志的时候，每次出问题了需要查询日志的时候，需要登录到Linux服务器，使用命令cat -n xxxx|grep xxxx 搜索出日志在哪一行，然后cat -n xxx|tail -n +n行|head -n 显示多少行，这样不仅效率低下，而且对于程序异常也不方便查询，日志少还好，一旦整合出来的日志达到几个G或者几十G的时候，仅仅是搜索都会搜索很长时间了，当然如果知道是哪天什么时候发生的问题当然也方便查询，但是实际上很多时候有问题的时候，是不知道到底什么时候出的问题，所以就必须要在聚合日志中去搜索（一般日志是按照天来分文件的，聚合日志就是把很多天的日志合并在一起，这样方便查询），而搭建EFK日志分析系统的目的就是将日志聚合起来，达到快速查看快速分析的目的，使用EFK不仅可以快速的聚合出每天的日志，还能将不同项目的日志聚合起来，对于微服务和分布式架构来说，查询日志尤为方便，而且因为日志保存在Elasticsearch中，所以查询速度非常之快</p><h4 id="认识EFK"><a href="#认识EFK" class="headerlink" title="认识EFK"></a>认识EFK</h4><p>EFK不是一个软件，而是一套解决方案，并且都是开源软件，之间互相配合使用，完美衔接，高效的满足了很多场合的应用，是目前主流的一种日志系统。EFK是三个开源软件的缩写，分别表示：Elasticsearch , FileBeat, Kibana , 其中ELasticsearch负责日志保存和搜索，FileBeat负责收集日志，Kibana 负责界面,当然EFK和大名鼎鼎的ELK只有一个区别，那就是EFK把ELK的Logstash替换成了FileBeat，因为Filebeat相对于Logstash来说有2个好处：<br>1、侵入低，无需修改程序目前任何代码和配置<br>2、相对于Logstash来说性能高，Logstash对于IO占用很大</p><p>当然FileBeat也并不是完全好过Logstash，毕竟Logstash对于日志的格式化这些相对FileBeat好很多，FileBeat只是将日志从日志文件中读取出来，当然如果你日志本身是有一定格式的，FileBeat也可以格式化，但是相对于Logstash来说，还是差一点</p><p><strong>Elasticsearch</strong></p><p>Elasticsearch是个开源分布式搜索引擎，提供搜集、分析、存储数据三大功能。它的特点有：分布式，零配置，自动发现，索引自动分片，索引副本机制，restful风格接口，多数据源，自动搜索负载等。</p><p><strong>FileBeat</strong></p><p>Filebeat隶属于Beats。目前Beats包含六种工具：<br>Packetbeat（搜集网络流量数据）<br>Metricbeat（搜集系统、进程和文件系统级别的 CPU 和内存使用情况等数据）<br>Filebeat（搜集文件数据）<br>Winlogbeat（搜集 Windows 事件日志数据）<br>Auditbeat（ 轻量型审计日志采集器）<br>Heartbeat（轻量级服务器健康采集器）</p><p><strong>Kibana</strong></p><p>Kibana可以为 Logstash 、Beats和 ElasticSearch 提供的日志分析友好的 Web 界面，可以帮助汇总、分析和搜索重要数据日志。</p><p><strong>EFK架构图</strong>  </p><p><img src="http://image.raye.wang/image/1/9e/dfdc767e41e8bf82b298883e95007.png" alt="EFK架构图"></p><h4 id="安装Java-JDK"><a href="#安装Java-JDK" class="headerlink" title="安装Java JDK"></a>安装Java JDK</h4><p>Elasticsearch需要运行在Java 8 及以上，所以需要先安装Java8，下载JavaJDK</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">wget http://download.oracle.com/otn-pub/java/jdk/8u181-b13/96a7b8442fe848ef90c96a2fad6ed6d1/jdk-8u181-linux-x64.tar.gz?AuthParam=1536892035_945cb24c750d0971b8c5b1925cc723a9</span><br></pre></td></tr></table></figure><p>当然如果要安装其他Java版本，可以将wget后面的URL替换为其他版本的URL，只要版本不低于Java8就行，下载完成后，需要查看下载的文件是后缀是tar.gz（只针对tar.gz，如果是RPM或者其他格式的，请自行安装），如果文件后缀tar.gz还跟了一串代码，则重命名文件</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">mv jdk-8u181-linux-x64.tar.gz\?AuthParam\=1536892035_945cb24c750d0971b8c5b1925cc723a9 jdk-8u181-linux-x64.tar.gz</span><br></pre></td></tr></table></figure><p>解压</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">tar -zxvf jdk-8u181-linux-x64.tar.gz</span><br></pre></td></tr></table></figure><p>编辑环境变量</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">vi /etc/profile</span><br></pre></td></tr></table></figure><p>在文件末尾加上</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">JAVA_HOME=/usr/local/jdk1.8.0_181/</span><br><span class="line">JRE_HOME=$JAVA_HOME/jre</span><br><span class="line">CLASS_PATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar:$JRE_HOME/lib</span><br><span class="line">PATH=$PATH:$JAVA_HOME/bin:$JRE_HOME/bin</span><br><span class="line">export JAVA_HOME JRE_HOME CLASS_PATH PATH</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>其中&#x2F;usr&#x2F;local&#x2F;jdk1.8.0_181&#x2F;替换成自己Java jdk解压出来的主目录，wq保存退出，使配置生效</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">source /etc/profile</span><br></pre></td></tr></table></figure><p>查看是否安装成功</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">java -version</span><br></pre></td></tr></table></figure><p>如果正常输出Java版本则安装成功</p><h4 id="安装Elasticsearch"><a href="#安装Elasticsearch" class="headerlink" title="安装Elasticsearch"></a>安装Elasticsearch</h4><p>下载Elasticsearch，本文以Elasticsearch6.2.4为例，当前Elasticsearch最新版本为Elasticsearch6.4.0，注意Elasticsearch、Kibana、FileBeat一定要使用相同的版本</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-6.2.4.tar.gz</span><br></pre></td></tr></table></figure><p>解压</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">tar -zxvf elasticsearch-6.2.4.tar.gz</span><br></pre></td></tr></table></figure><p>进入Elasticsearch主目录，修改配置</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">vi config/elasticsearch.yml</span><br></pre></td></tr></table></figure><p>添加以下配置，或者将对应的配置注释取消修改</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">network.host: 0.0.0.0 </span><br><span class="line">http.port: 9200</span><br></pre></td></tr></table></figure><p>由于Elasticsearch不能使用root用户打开，所以需要专门创建一个用户来启动Elasticsearch</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">$ adduser elastic</span><br><span class="line">#设置密码</span><br><span class="line">$ passwd elastic</span><br><span class="line">#需要输入2次密码</span><br><span class="line">#授权</span><br><span class="line">$ chmod -R 777 /usr/local/elasticsearch-6.2.4</span><br><span class="line">#切换用户</span><br><span class="line">$ su elastic</span><br></pre></td></tr></table></figure><p>创建的用户名为elastic，其中&#x2F;usr&#x2F;local&#x2F;elasticsearch-6.2.4为解压出来的Elasticsearch主目录</p><p>启动Elasticsearch</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">./bin/elasticsearch</span><br></pre></td></tr></table></figure><p>如果遇到错误：max file descriptors [65535] for elasticsearch process is too low, increase to at least [65536]</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">vi /etc/security/limits.conf  </span><br></pre></td></tr></table></figure><p>如果有 * soft nofile 65535 * hard nofile 65535 则将65535修改为65536，如果没有则在后面添加，注意此处的65535对应descriptors [65535]中的65535，修改后的值65536对应increase to at least [65536]，所以当提示不一致时，需要根据具体的错误提示具体修改</p><p>如果遇到错误： max virtual memory areas vm.max_map_count [65530] is too low, increase to at least [262144]</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">vi /etc/sysctl.conf</span><br></pre></td></tr></table></figure><p>添加配置</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">vm.max_map_count=262144</span><br></pre></td></tr></table></figure><p>并执行命令</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sysctl -p</span><br></pre></td></tr></table></figure><p>以上2个修改需要在root用户权限修改，如果是使用xshell开两个窗口的话修改完成之后一定要断开重新登录一下，启动成功用执行命令</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">curl 127.0.0.1:9200  </span><br></pre></td></tr></table></figure><p>会得到类似以下json</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  &quot;name&quot; : &quot;dQIO4Ad&quot;,</span><br><span class="line">  &quot;cluster_name&quot; : &quot;elasticsearch&quot;,</span><br><span class="line">  &quot;cluster_uuid&quot; : &quot;P8KtmO3vQdactRW1jX9JnQ&quot;,</span><br><span class="line">  &quot;version&quot; : &#123;</span><br><span class="line">    &quot;number&quot; : &quot;6.2.4&quot;,</span><br><span class="line">    &quot;build_hash&quot; : &quot;ccec39f&quot;,</span><br><span class="line">    &quot;build_date&quot; : &quot;2018-04-12T20:37:28.497551Z&quot;,</span><br><span class="line">    &quot;build_snapshot&quot; : false,</span><br><span class="line">    &quot;lucene_version&quot; : &quot;7.2.1&quot;,</span><br><span class="line">    &quot;minimum_wire_compatibility_version&quot; : &quot;5.6.0&quot;,</span><br><span class="line">    &quot;minimum_index_compatibility_version&quot; : &quot;5.0.0&quot;</span><br><span class="line">  &#125;,</span><br><span class="line">  &quot;tagline&quot; : &quot;You Know, for Search&quot;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="安装Kibana"><a href="#安装Kibana" class="headerlink" title="安装Kibana"></a>安装Kibana</h4><p>下载Kibana</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">wget https://artifacts.elastic.co/downloads/kibana/kibana-6.2.4-linux-x86_64.tar.gz</span><br></pre></td></tr></table></figure><p>解压</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">tar -zxvf kibana-6.2.4-linux-x86_64.tar.gz</span><br></pre></td></tr></table></figure><p>进入主目录，修改配置</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">vi config/kibana.yml</span><br></pre></td></tr></table></figure><p>添加以下配置或者取消注释并修改</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">elasticsearch.url: &quot;http://localhost:9200&quot;</span><br><span class="line">server.host: &quot;0.0.0.0&quot;</span><br><span class="line">kibana.index: &quot;.kibana&quot;</span><br></pre></td></tr></table></figure><p>其中elasticsearch.url为Elasticsearch的地址，server.host默认是localhost，如果只是本地访问可以默认localhost，如果需要外网访问，可以设置为0.0.0.0<br>启动Kibana</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">./bin/kibana</span><br></pre></td></tr></table></figure><h4 id="安装FileBeat"><a href="#安装FileBeat" class="headerlink" title="安装FileBeat"></a>安装FileBeat</h4><p>下载FileBeat</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">wget https://artifacts.elastic.co/downloads/beats/filebeat/filebeat-6.2.4-linux-x86_64.tar.gz</span><br></pre></td></tr></table></figure><p>解压</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">tar -zxvf filebeat-6.2.4-linux-x86_64.tar.gz</span><br></pre></td></tr></table></figure><p>进入主目录，修改配置</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">vi filebeat.yml</span><br></pre></td></tr></table></figure><p>找到类似以下的配置并修改</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line">filebeat.prospectors:</span><br><span class="line">- type: log</span><br><span class="line">  enabled: true</span><br><span class="line">  paths:</span><br><span class="line">    - /var/xxx/*.log</span><br><span class="line">    - /var/xxx/*.out</span><br><span class="line">  multiline.pattern: ^\[</span><br><span class="line">  multiline.negate: true</span><br><span class="line">  multiline.match: after</span><br><span class="line">setup.kibana:</span><br><span class="line">  host: &quot;localhost:5601&quot;</span><br><span class="line">output.elasticsearch:</span><br><span class="line">  hosts: [&quot;localhost:9200&quot;]</span><br></pre></td></tr></table></figure><p>配置一定要注意格式，是以2个空格为子级，里面的配置都在配置文件中，列出来的只是要修改的部分，enabled默认为false，需要改成true才会收集日志。其中&#x2F;var&#x2F;xxx&#x2F;*.log修改为自己的日志路径，注意-后面有一个空格，<br>如果多个路径则添加一行，一定要注意新行前面的4个空格，multiline开头的几个配置取消注释就行了，是为了兼容多行日志的情况，setup.kibana中的host取消注释，根据实际情况配置地址，output.elasticsearch中的host也一样，根据实际情况配置</p><p>启动FileBeat</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">./filebeat -c /usr/local/filebeat/filebeat.yml</span><br></pre></td></tr></table></figure><p>&#x2F;usr&#x2F;local&#x2F;filebeat&#x2F;filebeat.yml为filebeat 的配置文件地址，需要根据实际情况修改，启动后FileBeat就会自动收集日志了</p><h4 id="配置Kibana"><a href="#配置Kibana" class="headerlink" title="配置Kibana"></a>配置Kibana</h4><p>打开浏览器进入<a href="http://127.0.0.1:5601/">http://127.0.0.1:5601</a>，会出现如下页面：  </p><p><img src="http://image.raye.wang/image/e/81/71a4ed278c9cf7229f85535d874b3.png" alt="Kibana首页"></p><p>点击Management进入配置  </p><p><img src="http://image.raye.wang/image/7/18/720c58c04a3d9b83e7016a9c979c1.png" alt="Kibana Management页面"></p><p>点击进入Index Patterns  </p><p><img src="http://image.raye.wang/image/d/12/8798affec03a33df7b9ffb6a0ada2.png" alt="Kibana  Index Patterns"></p><p>如图所示，FileBeat默认创建的Elasticsearch索引格式为filebeat-版本号-日期，所以需要在第一个红框的输入框中输入</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">filebeat-6.2.4-*</span><br></pre></td></tr></table></figure><p>能匹配到Elasticsearch的索引，第二个红框出会显示出Elasticsearch中已有的索引，点击Next step进入下一步  </p><p><img src="http://image.raye.wang/image/f/d4/a45f2c971e21b865798e137a102cb.png" alt="Kibana 选择时间属性"></p><p>点击Create index pattern完成配置，配置完成后点击 Discover就能查看日志了，还能搜索，如下图  </p><p><img src="http://image.raye.wang/image/3/6a/9b4b3e26621b94c6b716f118154f8.png" alt="Kibana效果图"></p><p>至此EFK就搭建完毕了</p>]]></content>
    
    
    <summary type="html">&lt;h4 id=&quot;-&quot;&gt;&amp;#x524D;&amp;#x8A00;&lt;/h4&gt;&lt;p&gt;EFK&amp;#x53EF;&amp;#x80FD;&amp;#x90FD;&amp;#x4E0D;&amp;#x719F;&amp;#x6089;&amp;#xFF0C;&amp;#x5B9E;&amp;#x9645;&amp;#x4E0A;EFK&amp;#x662F;&amp;#x5927;&amp;#x540D;&amp;#x9F0E;&amp;#x9F0E;&amp;#x7684;&amp;#x65E5;&amp;#x5FD7;&amp;#x7CFB;&amp;#x7EDF;ELK&amp;#x7684;&amp;#x4E00;&amp;#x4E2A;&amp;#x53D8;&amp;#x79CD;&lt;/p&gt;&lt;p&gt;&amp;#x5728;&amp;#x6CA1;&amp;#x6709;&amp;#x5206;&amp;#x5E03;&amp;#x5F0F;&amp;#x65E5;&amp;#x5FD7;&amp;#x7684;&amp;#x65F6;&amp;#x5019;&amp;#xFF0C;&amp;#x6BCF;&amp;#x6B21;&amp;#x51FA;&amp;#x95EE;&amp;#x9898;&amp;#x4E86;&amp;#x9700;&amp;#x8981;&lt;/p&gt;</summary>
    
    
    
    <category term="服务器" scheme="https://raye.wang/categories/%E6%9C%8D%E5%8A%A1%E5%99%A8/"/>
    
    
    <category term="服务器" scheme="https://raye.wang/tags/%E6%9C%8D%E5%8A%A1%E5%99%A8/"/>
    
    <category term="日志" scheme="https://raye.wang/tags/%E6%97%A5%E5%BF%97/"/>
    
  </entry>
  
  <entry>
    <title>记一次MySQL迁移并从MySQL5.6升级到5.7后查询慢了几十倍的问题</title>
    <link href="https://raye.wang/2018/09/13/%E8%AE%B0%E4%B8%80%E6%AC%A1MySQL%E8%BF%81%E7%A7%BB%E5%B9%B6%E4%BB%8EMySQL5-6%E5%8D%87%E7%BA%A7%E5%88%B05-7%E5%90%8E%E6%9F%A5%E8%AF%A2%E6%85%A2%E4%BA%86%E5%87%A0%E5%8D%81%E5%80%8D%E7%9A%84%E9%97%AE%E9%A2%98/"/>
    <id>https://raye.wang/2018/09/13/%E8%AE%B0%E4%B8%80%E6%AC%A1MySQL%E8%BF%81%E7%A7%BB%E5%B9%B6%E4%BB%8EMySQL5-6%E5%8D%87%E7%BA%A7%E5%88%B05-7%E5%90%8E%E6%9F%A5%E8%AF%A2%E6%85%A2%E4%BA%86%E5%87%A0%E5%8D%81%E5%80%8D%E7%9A%84%E9%97%AE%E9%A2%98/</id>
    <published>2018-09-12T16:00:00.000Z</published>
    <updated>2026-03-10T06:59:04.572Z</updated>
    
    <content type="html"><![CDATA[<h4 id="起因"><a href="#起因" class="headerlink" title="起因"></a>起因</h4><p>因为生产环境数据量越来越大，客户越来越多，项目功能也越来越多，项目本身也越来越多，导致之前的服务器内存、硬盘都已经渐渐的不够用了，当时出现了2种解决方案，增加服务器配置和新购服务器，但是就算是新增硬盘，也需要对数据库进行迁移，所以就采用了新购服务器的方案，并且因为之前是高效云盘，出现过IO占满的情况，所以对于新购的服务器采用了SSD硬盘，理论上速度会飞起来了，实际上我在新服务器上安装MySQL5.7，因为听说MySQL5.7性能提升了N倍，还支持json（虽然对我们没什么用），但是毕竟MySQL8已经出来了，说明MySQL5.7也肯定稳定好了。so，就找了个夜深人静的晚上偷偷的吧数据库迁移过来了，然后开启慢查询日志，限制为5秒，于是开始各种测试，然后查看了一下慢查询日志，一堆慢查询日志，所以有了这篇文章，为什么会出现这么多慢查询，以及如何解决</p><h4 id="开始排查"><a href="#开始排查" class="headerlink" title="开始排查"></a>开始排查</h4><p>将慢查询SQL拿出来，发现主要的慢查询SQL都是链表查询的语句，也就是说查询语句本身非常复杂，所以就把SQL语句放回之前的数据库执行，发现之前的数据库都是不到1秒就查询出来了，而在新的数据库上最慢能达到140多秒，这明显就不正常了，毕竟新的MySQL服务器无论是CPU、内存、还是硬盘相对于以前的MySQL服务器来说，都是好了不止一星半点，如果说性能差不多还能接受，但是一下子慢了这么多，就明显是有异常了，于是开始挨个排查</p><h5 id="排查第一步：配置问题"><a href="#排查第一步：配置问题" class="headerlink" title="排查第一步：配置问题"></a>排查第一步：配置问题</h5><p>首先2边服务器的配置文件是一模一样的，因为就是从旧服务器吧配置文件复制过来的，但是因为MySQL的版本不一致，所以怀疑是因为新版有些配置不一样，所以导致的，于是百度了MySQL5.7的优化配置，同时根据机器的内存CPU等硬件情况调整了部分配置，重启MySQL，执行语句，效果不明显，平均下来能快1秒左右，但是这1秒本身也可以认为是查询波动，所以不是配置的问题</p><h5 id="排查第二步：硬件问题"><a href="#排查第二步：硬件问题" class="headerlink" title="排查第二步：硬件问题"></a>排查第二步：硬件问题</h5><p>首先CPU和内存应该不会有什么问题，唯一可能性就是SSD硬盘的问题，因为之前看到过因为SSD硬盘导致K,V键值存储性能低下，跟机械硬盘完全不在同一个等级上，所以怀疑SSD是否需要开启什么特别的配置才行，于是百度，发现虽然有针对SSD的优化配置，但是也没有因为用了SSD导致速度非常慢的情况，于是针对SSD进行了优化配置，重启MySQL，执行语句，效果微乎其微</p><h5 id="排查第三步：语句问题"><a href="#排查第三步：语句问题" class="headerlink" title="排查第三步：语句问题"></a>排查第三步：语句问题</h5><p>也是实在没办法了，才想到这个问题，但是我自己都觉得不大可能，而且语句本身也优化的差不多了，小结果集驱动大结果集，索引根据where条件创建等。毕竟就算MySQL升级也不会说改变SQL语法之类的，最多就是在优化SQL的进行了一些特殊处理，所以先查看一下SQL语句的索引执行情况于是desc  sql语句查看，跟旧库上面差别</p><p><strong>旧库（MySQL5.6）</strong>  </p><p><img src="http://image.raye.wang/image/c/0f/6658193cfe8c66521c515ecdc592e.png" alt="mysql5.6语句索引使用情况"></p><p><strong>新库（MySQL5.7）</strong>  </p><p><img src="http://image.raye.wang/image/4/ea/76d47aaafd4497a183b2d34a821a1.png"></p><p>在2个库数据，索引相同的情况下，居然会出现索引引用和命中不一样的情况，所以怀疑是否是迁移数据库的时候导致索引数据被破坏，于是百度去找，还真的发现了一个例子，也是迁移数据库后查询非常慢，后面重建索引之后恢复了，于是准备重建索引，由于表非常多，所以写了一个工具类来重建索引（唯一索引和普通索引，不包含主键索引），核心代码如下：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br></pre></td><td class="code"><pre><span class="line">List&lt;HashMap&gt; list = mapper.select1();</span><br><span class="line">        HashMap&lt;String,HashMap&lt;String,Object&gt;&gt; temp = new HashMap&lt;&gt;();</span><br><span class="line">        for(HashMap map : list)&#123;</span><br><span class="line">            String tableName = map.get(&quot;TABLE_NAME&quot;).toString();</span><br><span class="line">            String indexName = map.get(&quot;INDEX_NAME&quot;).toString();</span><br><span class="line">            String nonUnique = map.get(&quot;NON_UNIQUE&quot;).toString();</span><br><span class="line">            String columnName = map.get(&quot;COLUMN_NAME&quot;).toString();</span><br><span class="line">            if(temp.containsKey(tableName+&quot;|&quot;+indexName))&#123;</span><br><span class="line">                HashMap&lt;String,Object&gt; value = temp.get(tableName+&quot;|&quot;+indexName);</span><br><span class="line">                List&lt;String&gt; columns = (List&lt;String&gt;) value.get(&quot;columns&quot;);</span><br><span class="line">                columns.add(columnName);</span><br><span class="line">            &#125;else&#123;</span><br><span class="line">                HashMap&lt;String,Object&gt; value = new HashMap&lt;&gt;();</span><br><span class="line">                value.put(&quot;nonUnique&quot;,nonUnique);</span><br><span class="line">                List&lt;String&gt; columns = new ArrayList&lt;&gt;();</span><br><span class="line">                columns.add(columnName);</span><br><span class="line">                value.put(&quot;columns&quot;,columns);</span><br><span class="line">                value.put(&quot;indexName&quot;,indexName);</span><br><span class="line">                value.put(&quot;tableName&quot;,tableName);</span><br><span class="line">                temp.put(tableName+&quot;|&quot;+indexName,value);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        List&lt;String&gt; creates = new ArrayList&lt;&gt;();</span><br><span class="line">        List&lt;String&gt; drops = new ArrayList&lt;&gt;();</span><br><span class="line">        for(Map.Entry&lt;String,HashMap&lt;String,Object&gt;&gt; entry:temp.entrySet())&#123;</span><br><span class="line">            String create = null;</span><br><span class="line">            String tableName = entry.getValue().get(&quot;tableName&quot;).toString();</span><br><span class="line">            String indexName = entry.getValue().get(&quot;indexName&quot;).toString();</span><br><span class="line">            String nonUnique = entry.getValue().get(&quot;nonUnique&quot;).toString();</span><br><span class="line">            List&lt;String&gt; columns = (List&lt;String&gt;) entry.getValue().get(&quot;columns&quot;);</span><br><span class="line">            drops.add(&quot;DROP INDEX &quot;+indexName+&quot; ON &quot;+tableName+&quot;;&quot;);</span><br><span class="line">            if(&quot;0&quot;.equals(nonUnique))&#123;</span><br><span class="line">                //唯一键索引</span><br><span class="line">                create = &quot;CREATE UNIQUE INDEX &quot;+indexName+&quot; ON &quot;+tableName+&quot; (&quot;;</span><br><span class="line">            &#125;else&#123;</span><br><span class="line">                //创建普通索引</span><br><span class="line">                create = &quot;CREATE INDEX &quot;+indexName+&quot; ON &quot;+tableName+&quot; (&quot;;</span><br><span class="line">            &#125;</span><br><span class="line">            for(int i = 0;i &lt; columns.size();i++)&#123;</span><br><span class="line">                if(i == columns.size() - 1)&#123;</span><br><span class="line">                    create += columns.get(i)+&quot;);&quot;;</span><br><span class="line">                &#125;else&#123;</span><br><span class="line">                    create += columns.get(i)+&quot;,&quot;;</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">            creates.add(create);</span><br><span class="line">        &#125;</span><br><span class="line">        for(String str : drops)&#123;</span><br><span class="line">            System.out.println(str);</span><br><span class="line">        &#125;</span><br><span class="line">        for(String str : creates)&#123;</span><br><span class="line">            System.out.println(str);</span><br><span class="line">        &#125;</span><br></pre></td></tr></table></figure><p>查询所有索引SQL代码如下：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">select * from information_schema.statistics WHERE INDEX_SCHEMA=&#x27;xxxx&#x27; AND INDEX_NAME&lt;&gt;&#x27;PRIMARY&#x27;</span><br></pre></td></tr></table></figure><p>其中xxxx是数据库实例名，代码执行完成后将打印出来的SQL语句放进SQL里面执行即可，当然也可以在使用Java调用SQL执行，不过我为了随时观察状况，所以把SQL复制出来执行</p><p>重建索引完成后执行SQL语句，发现速度还是没有明显变化，说明不是因为索引数据异常的问题。</p><h5 id="检查MySQL5-7新特性"><a href="#检查MySQL5-7新特性" class="headerlink" title="检查MySQL5.7新特性"></a>检查MySQL5.7新特性</h5><p>百度查看MySQL5.7有没有更新什么新特性，看到了derived_merge特性，因为derived_merge是MySQL5.7的新的SQL优化方式，所以试着将derived_merge关闭，执行SQL</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">set GLOBAL optimizer_switch=&#x27;derived_merge=off&#x27;</span><br></pre></td></tr></table></figure><p>执行SQL，发现速度比旧服务器还快，然后用desc查看SQL索引使用情况，跟旧服务器也一样，于是问题解决</p><h5 id="关闭derived-merge后的新问题"><a href="#关闭derived-merge后的新问题" class="headerlink" title="关闭derived_merge后的新问题"></a>关闭derived_merge后的新问题</h5><p>本来以为关闭derived_merge后就万事大吉了，但是服务器的CPU占满却说明事情没有那么简单，top命令查看服务器CPU占满的原因发现是因为MySQL（肯定是MySQL，毕竟服务器就这一个软件），执行命令：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">show full processlist;</span><br></pre></td></tr></table></figure><p>查看卡住的链接信息，发现有大量的视图查询卡住，于是把SQL语句复制出来，发现只是查询单条数据，理论上不会这样慢，为了找出原因，停止测试，重启MySQL，执行视图SQL语句，发现完全卡住几分钟都不能执行完成，强行停止，检查视图的SQL是否有异常，发现视图的SQL也是普通的SQL（4个表的关联查询），理论上来说不会耗费这么久的时间，把创建视图的SQL语句拿出来跟执行的视图的SQL条件拼接起来，用desc查看，发现索引正常命中，于是试着执行一次SQL，结果非常意外，速度非常快，所以以为是服务器发疯，但是为了测试好，就又执行一下视图的SQL，结果为卡死。也就是说视图本身的SQL执行没有任何问题，但是使用视图查询，就会进入卡死状态。于是使用desc 查看视图SQL索引命中情况，发现结果跟直接的SQL不同，下面是对比图：</p><p><strong>视图</strong>  </p><p><img src="http://image.raye.wang/image/9/4c/b377e70947ca4572a176723286c0b.png" alt="视图索引命中情况"></p><p><strong>视图SQL</strong></p><p><img src="http://image.raye.wang/image/0/e6/b5fc74a84271b59ebe14feee9b760.png" alt="视图SQL索引命中情况"></p><p>视图的索引命中情况明显比视图SQL索引命中多了一个索引，但是为什么会造成卡死呢，原因就在多的那个索引身上，仔细看可以看到，索引命中的行有83141272975行，11位数，上百亿，难怪会卡死，索引命中了上百亿的数据，那跟没有命中索引也没有区别了，而且最为关键的是，我们整个库所有表加起来应该也没有上百亿的数据啊，毕竟目前最大的表数据量也才近千万，所以这个索引肯定是有问题，数据也有问题，但是具体什么问题，就不是我了解的了，毕竟不是专业搞数据库的，所以也希望了解的能帮我解答一下。</p><p>知道问题后，感觉解决就简单了，百度搜索了一下MySQL5.7对视图是否进行了优化，但是不管是百度还是谷歌都没有找到合适的答案，毕竟视图本身也只是存储了一个SQL语句而已，并没有保存实际数据，也就是说就算优化也是针对SQL语句本身进行优化，但是SQL语句本身执行又没有任何问题，而且心想MySQL不可能将这么大个bug放出来吧，于是回想之前调过的参数，是否是因为修改了配置导致的，因为之前主要修改特性的配置就derived_merge，所以怀疑是因为derived_merge导致的，于是又打开derived_merge</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">set GLOBAL optimizer_switch=&#x27;derived_merge=on&#x27;</span><br></pre></td></tr></table></figure><p>执行视图，一切正常</p><h4 id="排查第四步：索引命中问题"><a href="#排查第四步：索引命中问题" class="headerlink" title="排查第四步：索引命中问题"></a>排查第四步：索引命中问题</h4><p>由于关闭了derived_merge会导致视图查询问题，而系统中用到了很多视图，所以如果不用视图的话需要对系统进行大的改动导致关闭derived_merge不现实，也就只能另想他法了，查询之所以慢的原因主要还是因为索引没有命中导致的，也就是说解决了索引命中的问题，就能解决查询慢的问题，先对比2个库命中的索引，发现主要是链表查询的时候ON后面跟的条件在新库上面没有命中索引，ON后面的条件在主表是跟其他列有组成联合索引的，而被链接的表有部分表是跟其他列组合成联合索引，有些表的列则没有任何索引，于是尝试着在被链接的表创建ON后面的字段单独的索引，创建之后，速度明显快了一倍，但是还是有部分索引没有命中，所以又在主表对ON后面的字段单独创建索引（如果ON后面有几个条件，就创建联合索引），创建完成后，执行语句，秒查询出来，问题解决</p><h4 id="尝试在旧库上优化索引"><a href="#尝试在旧库上优化索引" class="headerlink" title="尝试在旧库上优化索引"></a>尝试在旧库上优化索引</h4><p>由于新库创建了索引后速度上明细比旧库快了很多，当然跟配置本身也有关系，于是相到再旧库上也创建同样的索引会不会更快了，于是在旧库上创建了跟新库相同的索引，执行SQL语句，比未创建索引之前慢了一倍，查看索引命中情况，虽然命中了更多的索引，但是也导致了命中的索引的行数增加</p><h4 id="感悟"><a href="#感悟" class="headerlink" title="感悟"></a>感悟</h4><p>MySQL不同的版本有不同的SQL优化器，而且不同的版本可能会出现索引命中规则不同，另外索引并不是越多查询就真的能更快，不合理的索引创建不仅会导致插入慢，还会导致查询变慢，所以了解MySQL索引命中规则和了解所用的MySQL的SQL优化器是有必要的，以及不要轻易更新版本，天知道会出现什么莫名其妙的问题。。。。。</p>]]></content>
    
    
    <summary type="html">&lt;h4 id=&quot;-&quot;&gt;&amp;#x8D77;&amp;#x56E0;&lt;/h4&gt;&lt;p&gt;&amp;#x56E0;&amp;#x4E3A;&amp;#x751F;&amp;#x4EA7;&amp;#x73AF;&amp;#x5883;&amp;#x6570;&amp;#x636E;&amp;#x91CF;&amp;#x8D8A;&amp;#x6765;&amp;#x8D8A;&amp;#x5927;&amp;#xFF0C;&amp;#x5BA2;&amp;#x6237;&amp;#x8D8A;&amp;#x6765;&amp;#x8D8A;&amp;#x591A;&amp;#xFF0C;&amp;#x9879;&amp;#x76EE;&amp;#x529F;&amp;#x80FD;&amp;#x4E5F;&amp;#x8D8A;&amp;#x6765;&amp;#x8D8A;&amp;#x591A;&amp;#xFF0C;&amp;#x9879;&amp;#x76EE;&amp;#x672C;&amp;#x8EAB;&amp;#x4E5F;&amp;#x8D8A;&amp;#x6765;&amp;#x8D8A;&amp;#x591A;&amp;#xFF0C;&amp;#x5BFC;&amp;#x81F4;&amp;#x4E4B;&amp;#x524D;&amp;#x7684;&amp;#x670D;&amp;#x52A1;&lt;/p&gt;</summary>
    
    
    
    <category term="MySQL" scheme="https://raye.wang/categories/MySQL/"/>
    
    
    <category term="服务器" scheme="https://raye.wang/tags/%E6%9C%8D%E5%8A%A1%E5%99%A8/"/>
    
    <category term="Mysql" scheme="https://raye.wang/tags/Mysql/"/>
    
  </entry>
  
  <entry>
    <title>使用monit监控服务进程和硬盘以及自动重启进程</title>
    <link href="https://raye.wang/2018/07/23/%E4%BD%BF%E7%94%A8monit%E7%9B%91%E6%8E%A7%E6%9C%8D%E5%8A%A1%E8%BF%9B%E7%A8%8B%E5%92%8C%E7%A1%AC%E7%9B%98%E4%BB%A5%E5%8F%8A%E8%87%AA%E5%8A%A8%E9%87%8D%E5%90%AF%E8%BF%9B%E7%A8%8B/"/>
    <id>https://raye.wang/2018/07/23/%E4%BD%BF%E7%94%A8monit%E7%9B%91%E6%8E%A7%E6%9C%8D%E5%8A%A1%E8%BF%9B%E7%A8%8B%E5%92%8C%E7%A1%AC%E7%9B%98%E4%BB%A5%E5%8F%8A%E8%87%AA%E5%8A%A8%E9%87%8D%E5%90%AF%E8%BF%9B%E7%A8%8B/</id>
    <published>2018-07-22T16:00:00.000Z</published>
    <updated>2026-03-10T06:59:04.571Z</updated>
    
    <content type="html"><![CDATA[<h4 id="需求"><a href="#需求" class="headerlink" title="需求"></a>需求</h4><p>需要服务器运行的程序在被意外杀死时，能自动重启，同时提醒我程序发生过重启。在服务器硬盘使用量达到一定比例的时候，提醒迁移数据或者清理无用的日志文件，所以需要一个服务器的守护程序（能在centos上运行），要求轻量简单占用少，根据上面的需求，找到了supervisor和monit2款软件</p><h4 id="supervisor-和-monit的对比"><a href="#supervisor-和-monit的对比" class="headerlink" title="supervisor 和 monit的对比"></a>supervisor 和 monit的对比</h4><p>因为主要是要满足以上我们的需求，所以我们根据实际需求来对比，而不是完全对比软件本身</p><h5 id="进程守护"><a href="#进程守护" class="headerlink" title="进程守护"></a>进程守护</h5><p>supervisor：<br>优点：采用托管的方式守护，一旦进程异常能马上检查出来，可以指定目录，对启动命令本身修改不大</p><p>缺点：只能启动前台进程，一旦supervisor发生重启会再次启动进程，导致进程再次被启动（不知道有没有办法避免，）</p><p>monit：<br>优点：有基于pid文件和正则匹配的检查进程，不会导致进程重复启动</p><p>缺点：通过定时检查的方式检查进程，存在一定延时，另外启动命令必须要包含完整的路径</p><h5 id="提醒"><a href="#提醒" class="headerlink" title="提醒"></a>提醒</h5><p>supervisor：需要使用第三方插件才能实现邮件提醒功能，本身并不支持邮件提醒<br>monit：原生支持邮件提醒功能</p><h5 id="磁盘监控"><a href="#磁盘监控" class="headerlink" title="磁盘监控"></a>磁盘监控</h5><p>supervisor：本身只是为了守护进程，所以需要第三方扩展才可以监控磁盘<br>monit：原生支持磁盘监控</p><h5 id="轻量级占用少"><a href="#轻量级占用少" class="headerlink" title="轻量级占用少"></a>轻量级占用少</h5><p>supervisor：基于Python开发，轻量级<br>monit：轻量级</p><h5 id="其他"><a href="#其他" class="headerlink" title="其他"></a>其他</h5><p>2款软件都提供了网页管理的功能，monit能在页面中直接对进程进行重启，停止和启动操作，supervisor能直接在页面中查看软件输出的日志</p><p>综合以上的对比，最后觉得还是monit适合我们，所以我们选择了monit</p><h4 id="安装和配置monit"><a href="#安装和配置monit" class="headerlink" title="安装和配置monit"></a>安装和配置monit</h4><p>在centos中，可以直接通过yum命令安装</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sudo yum install -y monit</span><br></pre></td></tr></table></figure><p>安装完成后执行命令</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sudo vi /etc/monitrc</span><br></pre></td></tr></table></figure><p>编辑配置文件</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line">set daemon  30              # 每隔30秒检测一次</span><br><span class="line">set log syslog              # 日志配置</span><br><span class="line">set mailserver smtp.office365.com port 587   # 邮箱的smtp服务器和端口</span><br><span class="line">username &quot;xxxxxx@outlook.com&quot; password &quot;xxxxx&quot;  # 邮箱的账号和密码</span><br><span class="line">using tlsv1, STARTTLS       # 是否使用ssl和加密串</span><br><span class="line">set alert xxxxxx@qq.com     # 接受提醒的邮箱，可以配置多条</span><br><span class="line"># 邮箱的模板配置</span><br><span class="line"> set mail-format &#123;</span><br><span class="line">   from:    xxxxxx@outlook.com  # 同上面的发件邮箱账号相同即可</span><br><span class="line">   subject: monit alert --  $EVENT $SERVICE</span><br><span class="line">   message: $EVENT Service $SERVICE</span><br><span class="line">                 Date:        $DATE</span><br><span class="line">                 Action:      $ACTION</span><br><span class="line">                 Host:        $HOST</span><br><span class="line">                 Description: $DESCRIPTION</span><br><span class="line"></span><br><span class="line">            Your faithful employee,</span><br><span class="line">            Monit</span><br><span class="line"> &#125;</span><br><span class="line">set httpd port 2812 and # http服务的端口</span><br><span class="line">   use address localhost # 只能通过localhost访问，这里是指的URL只能是localhost:2812,如果不用，去掉或者注释此条</span><br><span class="line">   allow localhost # 只允许本机访问，如果要其他IP能访问，注释此条    </span><br><span class="line">   allow username:password      # 访问页面的用户密码</span><br><span class="line">include /etc/monit.d/*   # 导入配置文件，我们将具体的软件守护和磁盘监控配置到这里的文件夹里面</span><br></pre></td></tr></table></figure><h5 id="服务类型"><a href="#服务类型" class="headerlink" title="服务类型"></a>服务类型</h5><p>首先需要理解在monit里什么是服务(service)。看监控语法：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">check &lt;类型&gt; &lt;服务名&gt; [PATH &lt;path&gt;] [ADDRESS &lt;host address&gt;]</span><br></pre></td></tr></table></figure><p>其中类型是monit支持的监控类型，一共有：system、file、process、fifo、filesystem、directory、host、network、program。<br>服务名必需是英文且唯一，不可以出现重复！<br>后面的带[]是根据类型需要添加的。</p><h6 id="进程"><a href="#进程" class="headerlink" title="进程"></a>进程</h6><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">CHECK PROCESS &lt;unique name&gt; &lt;PIDFILE &lt;path&gt; | MATCHING &lt;regex&gt;&gt;</span><br></pre></td></tr></table></figure><p>进程有2种判断模式，一种是pid文件，一种是名称匹配<br>pidfile 后面的是pid文件的绝对路径。pid文件是一个包含进程唯一ID的文件。如果pid文件不存在或不包含正在运行的进程的PID编号，则Monit将调用该条目的start方法（如果已定义）</p><p>如果进程本身并不存在pid文件，那么就可以使用名称匹配。名称匹配会选择具有最长正常运行时间的最顶部匹配的父级，因此如果进程名称是唯一的，则此检查形式是最有用的。应该尽可能使用Pid文件，因为它定义了预期的PID。您可以测试一个进程是否匹配来自命令行使用的模式monit procmatch “regex-pattern”。这将列出匹配的所有进程</p><h5 id="监控程序"><a href="#监控程序" class="headerlink" title="监控程序"></a>监控程序</h5><p>配置程序监控，以Tomcat为例，在&#x2F;etc&#x2F;monit.d&#x2F;文件夹中创建tomcat文件，文件内容如下：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">#tomcat</span><br><span class="line">check process tomcat with matching &quot;tomcat&quot;</span><br><span class="line">start program = &quot;/usr/local/tomcat/bin/startup.sh&quot;</span><br><span class="line">stop program = &quot;/usr/local/tomcat/bin/shutdown.sh&quot;</span><br></pre></td></tr></table></figure><p>第一行代码是注释，第二行代码是判断进程tomcat是否在运行,是通过名称匹配的，第三行是配置的启动脚本路径，第四行是配置的停止脚本路径</p><p>另外需要注意的是，如果在启动或者停止脚本的时候需要用到其他软件或者包含路径的时候，比如nohup等，一定要使用绝对路径，否则可能会出错导致无法启动</p><h6 id="磁盘"><a href="#磁盘" class="headerlink" title="磁盘"></a>磁盘</h6><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">CHECK DEVICE &lt;unique name&gt; PATH &lt;path&gt;</span><br></pre></td></tr></table></figure><p>在服务器中，很多时候日志文件除了会写入数据库、搜索引擎外，Tomcat的日志本身也会保留，所以也需要注意服务器的磁盘剩余空间占用</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">#vda1</span><br><span class="line">check device vda1 with path /dev/vda1</span><br><span class="line">   if space usage &gt; 80% then alert</span><br></pre></td></tr></table></figure><p>这里是判断如果vda1这个磁盘使用空间大于80%就发邮件提醒</p><h4 id="其他监控"><a href="#其他监控" class="headerlink" title="其他监控"></a>其他监控</h4><h6 id="文件"><a href="#文件" class="headerlink" title="文件"></a>文件</h6><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">CHECK FILE &lt;unique name&gt; PATH &lt;path&gt;</span><br></pre></td></tr></table></figure><p>是文件的绝对路径。如果文件不存在，Monit将调用该条目的start方法（如果已定义），如果不指向常规文件类型（例如目录），Monit将禁用此条目的监视。如果Monit在被动模式下运行或者没有定义start方法，Monit只会在错误时发送警报。</p><h6 id="Fifo"><a href="#Fifo" class="headerlink" title="Fifo"></a>Fifo</h6><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">CHECK FIFO &lt;unique name&gt; PATH &lt;path&gt;</span><br></pre></td></tr></table></figure><p>是fifo的绝对路径。如果fifo不存在，Monit将定义调用该条目的start方法，如果没有指向fifo类型（例如目录），Monit将禁用对该条目的监视。如果Monit在被动模式下运行或者没有定义start方法，Monit只会在错误时发送警报。</p><h6 id="文件系统"><a href="#文件系统" class="headerlink" title="文件系统"></a>文件系统</h6><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">CHECK FILESYSTEM &lt;unique name&gt; PATH &lt;path&gt;</span><br></pre></td></tr></table></figure><p>是设备&#x2F;磁盘，安装点，文件或作为文件系统一部分的目录的路径。建议直接使用块特殊文件（例如Linux上的&#x2F;dev&#x2F;vda1或Solaris上的&#x2F;dev&#x2F;dsk&#x2F;c0t0d0s1等）如果使用挂载点（例如&#x2F;data），请注意文件系统是卸载的测试仍然是真的，因为挂载点存在。</p><p>如果文件系统不可用，Monit将调用该条目的start方法（如果已定义）。如果不指向文件系统，Monit将禁用对此条目的监视。如果Monit在被动模式下运行或者没有定义start方法，Monit只会在错误时发送警报。</p><h6 id="目录"><a href="#目录" class="headerlink" title="目录"></a>目录</h6><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">CHECK DIRECTORY &lt;unique name&gt; PATH &lt;path&gt;</span><br></pre></td></tr></table></figure><p>是目录的绝对路径。如果目录不存在，Monit将调用该条目的start方法（如果已定义）。如果不指向目录，monit将禁用对此条目的监视。如果Monit在被动模式下运行或者没有定义启动方法，Monit只会在错误时发送警报。</p><h4 id="monit常用命令"><a href="#monit常用命令" class="headerlink" title="monit常用命令"></a>monit常用命令</h4><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><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">monit -t # 配置文件检测</span><br><span class="line">monit # 启动monit daemon</span><br><span class="line">monit -c /var/monit/monitrc # 启动monit daemon时指定配置文件</span><br><span class="line">monit reload # 当更新了配置文件需要重载</span><br><span class="line">monit status # 查看所有服务状态</span><br><span class="line">monit status xxx # 查看xxx服务状态(xxx)为配置的&lt;unique name&gt;</span><br><span class="line">monit stop all # 停止所有服务</span><br><span class="line">monit stop xxx # 停止xxx服务</span><br><span class="line">monit start all # 启动所有服务</span><br><span class="line">monit start xxx # 启动xxx服务</span><br><span class="line">monit -V # 查看版本</span><br></pre></td></tr></table></figure><h4 id="其他-1"><a href="#其他-1" class="headerlink" title="其他"></a>其他</h4><p>当然monit远不止上面介绍的功能，比如还有远程主机、系统、网络以及自定义一些监控，另外monit还支持检查时间和提醒的相关自定义定制，因为本文主要是介绍使用monit监控服务进程和硬盘以及自动重启进程，所以没有详细介绍其他用法，有兴趣的可以自行了解</p>]]></content>
    
    
    <summary type="html">&lt;h4 id=&quot;-&quot;&gt;&amp;#x9700;&amp;#x6C42;&lt;/h4&gt;&lt;p&gt;&amp;#x9700;&amp;#x8981;&amp;#x670D;&amp;#x52A1;&amp;#x5668;&amp;#x8FD0;&amp;#x884C;&amp;#x7684;&amp;#x7A0B;&amp;#x5E8F;&amp;#x5728;&amp;#x88AB;&amp;#x610F;&amp;#x5916;&amp;#x6740;&amp;#x6B7B;&amp;#x65F6;&amp;#xFF0C;&amp;#x80FD;&amp;#x81EA;&amp;#x52A8;&amp;#x91CD;&amp;#x542F;&amp;#xFF0C;&amp;#x540C;&amp;#x65F6;&amp;#x63D0;&amp;#x9192;&amp;#x6211;&amp;#x7A0B;&amp;#x5E8F;&amp;#x53D1;&amp;#x751F;&amp;#x8FC7;&amp;#x91CD;&amp;#x542F;&amp;#x3002;&amp;#x5728;&amp;#x670D;&amp;#x52A1;&amp;#x5668;&amp;#x786C;&amp;#x76D8;&amp;#x4F7F;&amp;#x7528;&amp;#x91CF;&amp;#x8FBE;&amp;#x5230;&lt;/p&gt;</summary>
    
    
    
    <category term="服务器" scheme="https://raye.wang/categories/%E6%9C%8D%E5%8A%A1%E5%99%A8/"/>
    
    
    <category term="服务器" scheme="https://raye.wang/tags/%E6%9C%8D%E5%8A%A1%E5%99%A8/"/>
    
  </entry>
  
</feed>
