<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <author>
    <name>KeepStayTrue</name>
  </author>
  <generator uri="https://hexo.io/">Hexo</generator>
  <id>https://keepstaytrue.github.io/</id>
  <link href="https://keepstaytrue.github.io/" rel="alternate"/>
  <link href="https://keepstaytrue.github.io/atom.xml" rel="self"/>
  <rights>All rights reserved 2026, KeepStayTrue</rights>
  <subtitle>Stay positive</subtitle>
  <title>KeepStayTrue</title>
  <updated>2026-05-23T13:33:22.632Z</updated>
  <entry>
    <author>
      <name>KeepStayTrue</name>
    </author>
    <category term="Java后端求职学习体系" scheme="https://keepstaytrue.github.io/categories/Java%E5%90%8E%E7%AB%AF%E6%B1%82%E8%81%8C%E5%AD%A6%E4%B9%A0%E4%BD%93%E7%B3%BB/"/>
    <category term="后端框架" scheme="https://keepstaytrue.github.io/categories/Java%E5%90%8E%E7%AB%AF%E6%B1%82%E8%81%8C%E5%AD%A6%E4%B9%A0%E4%BD%93%E7%B3%BB/%E5%90%8E%E7%AB%AF%E6%A1%86%E6%9E%B6/"/>
    <category term="JavaWeb" scheme="https://keepstaytrue.github.io/categories/Java%E5%90%8E%E7%AB%AF%E6%B1%82%E8%81%8C%E5%AD%A6%E4%B9%A0%E4%BD%93%E7%B3%BB/%E5%90%8E%E7%AB%AF%E6%A1%86%E6%9E%B6/JavaWeb/"/>
    <category term="JavaWeb" scheme="https://keepstaytrue.github.io/tags/JavaWeb/"/>
    <content>
      <![CDATA[<h1>JavaWeb 课程概述</h1><p>课程中的项目 基于springBoot3+ vue3 技术进行开发,开发模式选用前后端分离模式</p><ol><li><p>需求分析 -&gt; 页面原型和需求文档</p></li><li><p>项目设计^1^</p></li><li><p>数据库设计^1^</p><blockquote><p>基于页面原型和需求文档设计对应的表结构和功能接口</p></blockquote></li><li><p>前后端程序开发</p></li><li><p>前后端程序联调测试</p></li><li><p>项目部署</p></li></ol><h2 id="Part1-前端Web基础-2天">Part1: 前端Web基础 (2天)</h2><ul><li>HTML(网页的结构)</li><li>CSS(网页的表现)</li><li>JS(网页的行为)</li><li>Ajax / Axios</li></ul><h2 id="Part2-前端Web实战-4天">Part2: 前端Web实战 (4天)</h2><ul><li>Vue工程化</li><li>ElementPlus</li><li>Tlias案例</li></ul><h2 id="Part3-后端Web基础-4天">Part3: 后端Web基础 (4天)</h2><ul><li>Maven</li><li>HTTP</li><li>IOC</li><li>DI</li><li>MySQL</li><li>JDBC</li><li>Mybatis</li></ul><h2 id="Part4-后端Web实战-6天">Part4: 后端Web实战 (6天)</h2><ul><li>部门管理</li><li>员工管理</li><li>班级管理</li><li>学员管理</li><li>报表统计</li><li>登录认证</li></ul><h2 id="Part5-后端Web进阶-2天">Part5: 后端Web进阶 (2天)</h2><ul><li>AOP</li><li>SpringBoot原理</li><li>自定义Starter</li><li>Maven高级</li></ul><h2 id="Part6-部署-2天">Part6: 部署 (2天)</h2><ul><li>Linux</li><li>Docker</li></ul>]]>
    </content>
    <id>https://keepstaytrue.github.io/2025/05/23/JavaWeb/</id>
    <link href="https://keepstaytrue.github.io/2025/05/23/JavaWeb/"/>
    <published>2025-05-23T13:00:00.000Z</published>
    <summary>
      <![CDATA[<h1>JavaWeb 课程概述</h1>
<p>课程中的项目 基于springBoot3+ vue3 技术进行开发,开发模式选用前后端分离模式</p>
<ol>
<li>
<p>需求分析 -&gt; 页面原型和需求文档</p>
</li>
<li>
<p>项目设计^1^</p>]]>
    </summary>
    <title>JavaWeb</title>
    <updated>2026-05-23T13:33:22.632Z</updated>
  </entry>
  <entry>
    <author>
      <name>KeepStayTrue</name>
    </author>
    <category term="Java后端求职学习体系" scheme="https://keepstaytrue.github.io/categories/Java%E5%90%8E%E7%AB%AF%E6%B1%82%E8%81%8C%E5%AD%A6%E4%B9%A0%E4%BD%93%E7%B3%BB/"/>
    <category term="工具" scheme="https://keepstaytrue.github.io/categories/Java%E5%90%8E%E7%AB%AF%E6%B1%82%E8%81%8C%E5%AD%A6%E4%B9%A0%E4%BD%93%E7%B3%BB/%E5%B7%A5%E5%85%B7/"/>
    <category term="开发&amp;运维工具" scheme="https://keepstaytrue.github.io/categories/Java%E5%90%8E%E7%AB%AF%E6%B1%82%E8%81%8C%E5%AD%A6%E4%B9%A0%E4%BD%93%E7%B3%BB/%E5%B7%A5%E5%85%B7/%E5%BC%80%E5%8F%91-%E8%BF%90%E7%BB%B4%E5%B7%A5%E5%85%B7/"/>
    <category term="开发效率工具" scheme="https://keepstaytrue.github.io/categories/Java%E5%90%8E%E7%AB%AF%E6%B1%82%E8%81%8C%E5%AD%A6%E4%B9%A0%E4%BD%93%E7%B3%BB/%E5%B7%A5%E5%85%B7/%E5%BC%80%E5%8F%91-%E8%BF%90%E7%BB%B4%E5%B7%A5%E5%85%B7/%E5%BC%80%E5%8F%91%E6%95%88%E7%8E%87%E5%B7%A5%E5%85%B7/"/>
    <category term="Web平台" scheme="https://keepstaytrue.github.io/categories/Java%E5%90%8E%E7%AB%AF%E6%B1%82%E8%81%8C%E5%AD%A6%E4%B9%A0%E4%BD%93%E7%B3%BB/%E5%B7%A5%E5%85%B7/%E5%BC%80%E5%8F%91-%E8%BF%90%E7%BB%B4%E5%B7%A5%E5%85%B7/%E5%BC%80%E5%8F%91%E6%95%88%E7%8E%87%E5%B7%A5%E5%85%B7/Web%E5%B9%B3%E5%8F%B0/"/>
    <category term="OpenResty" scheme="https://keepstaytrue.github.io/tags/OpenResty/"/>
    <category term="nginx" scheme="https://keepstaytrue.github.io/tags/nginx/"/>
    <category term="Lua" scheme="https://keepstaytrue.github.io/tags/Lua/"/>
    <category term="多级缓存" scheme="https://keepstaytrue.github.io/tags/%E5%A4%9A%E7%BA%A7%E7%BC%93%E5%AD%98/"/>
    <content>
      <![CDATA[<h1>安装OpenResty</h1><h1>1.安装</h1><p>首先你的Linux虚拟机必须联网</p><h2 id="1）安装开发库"><strong>1）安装开发库</strong></h2><p>首先要安装OpenResty的依赖开发库，执行命令：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">yum install -y pcre-devel openssl-devel gcc --skip-broken</span><br></pre></td></tr></table></figure><h2 id="2）安装OpenResty仓库"><strong>2）安装OpenResty仓库</strong></h2><p>你可以在你的 CentOS 系统中添加 <code>openresty</code> 仓库，这样就可以便于未来安装或更新我们的软件包（通过 <code>yum check-update</code> 命令）。运行下面的命令就可以添加我们的仓库：</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-config-manager --add-repo https://openresty.org/package/centos/openresty.repo</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">yum install -y yum-utils </span><br></pre></td></tr></table></figure><p>然后再重复上面的命令</p><h2 id="3）安装OpenResty"><strong>3）安装OpenResty</strong></h2><p>然后就可以像下面这样安装软件包，比如 <code>openresty</code>：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">yum install -y openresty</span><br></pre></td></tr></table></figure><h2 id="4）安装opm工具"><strong>4）安装opm工具</strong></h2><p>opm是OpenResty的一个管理工具，可以帮助我们安装一个第三方的Lua模块。</p><p>如果你想安装命令行工具 <code>opm</code>，那么可以像下面这样安装 <code>openresty-opm</code> 包：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">yum install -y openresty-opm</span><br></pre></td></tr></table></figure><h2 id="5）目录结构"><strong>5）目录结构</strong></h2><p>默认情况下，OpenResty安装的目录是：/usr/local/openresty</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20200310225539214.png" alt="image-20200310225539214"></p><p>看到里面的nginx目录了吗，OpenResty就是在Nginx基础上集成了一些Lua模块。</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20260522173638768.png" alt="image-20260522173638768"></p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20260522173748108.png" alt="image-20260522173748108"></p><p>一般情况下openResty都是当成nginx来用的</p><h2 id="6）配置nginx的环境变量">6）配置nginx的环境变量</h2><p>打开配置文件：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">vi /etc/profile</span><br></pre></td></tr></table></figure><p>在最下面加入两行：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">export</span> NGINX_HOME=/usr/local/openresty/nginx</span><br><span class="line"><span class="built_in">export</span> PATH=<span class="variable">$&#123;NGINX_HOME&#125;</span>/sbin:<span class="variable">$PATH</span></span><br></pre></td></tr></table></figure><p>NGINX_HOME：后面是OpenResty安装目录下的nginx的目录</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">source /etc/profile</span><br></pre></td></tr></table></figure><h1>2.启动和运行</h1><p>OpenResty底层是基于Nginx的，查看OpenResty目录的nginx目录，结构与windows中安装的nginx基本一致：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210811100653291.png" alt="image-20210811100653291"></p><p>所以运行方式与nginx基本一致：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 启动nginx</span></span><br><span class="line">nginx</span><br><span class="line"><span class="comment"># 重新加载配置</span></span><br><span class="line">nginx -s reload</span><br><span class="line"><span class="comment"># 停止</span></span><br><span class="line">nginx -s stop</span><br></pre></td></tr></table></figure><p>nginx的默认配置文件注释太多，影响后续我们的编辑，这里将nginx.conf中的注释部分删除，保留有效部分。</p><p>修改<code>/usr/local/openresty/nginx/conf/nginx.conf</code>文件，内容如下：</p><div class="tabs"><div class="nav-tabs"><button type="button" class="tab active">简化配置</button><button type="button" class="tab">原配置带注释信息</button></div><div class="tab-contents"><div class="tab-item-content active"><figure class="highlight nginx"><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></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="comment">#user  nobody;</span></span><br><span class="line"><span class="attribute">worker_processes</span>  <span class="number">1</span>;</span><br><span class="line"><span class="attribute">error_log</span>  logs/<span class="literal">error</span>.log;</span><br><span class="line"></span><br><span class="line"><span class="section">events</span> &#123;</span><br><span class="line">    <span class="attribute">worker_connections</span>  <span class="number">1024</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="section">http</span> &#123;</span><br><span class="line">    <span class="attribute">include</span>       mime.types;</span><br><span class="line">    <span class="attribute">default_type</span>  application/octet-stream;</span><br><span class="line">    <span class="attribute">sendfile</span>        <span class="literal">on</span>;</span><br><span class="line">    <span class="attribute">keepalive_timeout</span>  <span class="number">65</span>;</span><br><span class="line"></span><br><span class="line">    <span class="section">server</span> &#123;</span><br><span class="line">        <span class="attribute">listen</span>       <span class="number">8081</span>;</span><br><span class="line">        <span class="attribute">server_name</span>  localhost;</span><br><span class="line">        <span class="section">location</span> / &#123;</span><br><span class="line">            <span class="attribute">root</span>   html;</span><br><span class="line">            <span class="attribute">index</span>  index.html index.htm;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="attribute">error_page</span>   <span class="number">500</span> <span class="number">502</span> <span class="number">503</span> <span class="number">504</span>  /50x.html;</span><br><span class="line">        <span class="section">location</span> = /50x.html &#123;</span><br><span class="line">            <span class="attribute">root</span>   html;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div><div class="tab-item-content"><figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="comment">#user  nobody;</span></span><br><span class="line"><span class="attribute">worker_processes</span>  <span class="number">1</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">#error_log  logs/error.log;</span></span><br><span class="line"><span class="comment">#error_log  logs/error.log  notice;</span></span><br><span class="line"><span class="comment">#error_log  logs/error.log  info;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">#pid        logs/nginx.pid;</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="section">events</span> &#123;</span><br><span class="line">    <span class="attribute">worker_connections</span>  <span class="number">1024</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="section">http</span> &#123;</span><br><span class="line">    <span class="attribute">include</span>       mime.types;</span><br><span class="line">    <span class="attribute">default_type</span>  application/octet-stream;</span><br><span class="line"></span><br><span class="line">    <span class="comment">#log_format  main  &#x27;$remote_addr - $remote_user [$time_local] &quot;$request&quot; &#x27;</span></span><br><span class="line">    <span class="comment">#                  &#x27;$status $body_bytes_sent &quot;$http_referer&quot; &#x27;</span></span><br><span class="line">    <span class="comment">#                  &#x27;&quot;$http_user_agent&quot; &quot;$http_x_forwarded_for&quot;&#x27;;</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">#access_log  logs/access.log  main;</span></span><br><span class="line"></span><br><span class="line">    <span class="attribute">sendfile</span>        <span class="literal">on</span>;</span><br><span class="line">    <span class="comment">#tcp_nopush     on;</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">#keepalive_timeout  0;</span></span><br><span class="line">    <span class="attribute">keepalive_timeout</span>  <span class="number">65</span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">#gzip  on;</span></span><br><span class="line"></span><br><span class="line">    <span class="section">server</span> &#123;</span><br><span class="line">        <span class="attribute">listen</span>       <span class="number">80</span>;</span><br><span class="line">        <span class="attribute">server_name</span>  localhost;</span><br><span class="line"></span><br><span class="line">        <span class="comment">#access_log  logs/host.access.log  main;</span></span><br><span class="line"></span><br><span class="line">        <span class="section">location</span> / &#123;</span><br><span class="line">            <span class="attribute">root</span>   html;</span><br><span class="line">            <span class="attribute">index</span>  index.html index.htm;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">#error_page  404              /404.html;</span></span><br><span class="line"></span><br><span class="line">        <span class="comment"># redirect server error pages to the static page /50x.html</span></span><br><span class="line">        <span class="comment">#</span></span><br><span class="line">        <span class="attribute">error_page</span>   <span class="number">500</span> <span class="number">502</span> <span class="number">503</span> <span class="number">504</span>  /50x.html;</span><br><span class="line">        <span class="section">location</span> = /50x.html &#123;</span><br><span class="line">            <span class="attribute">root</span>   html;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment"># proxy the PHP scripts to Apache listening on 127.0.0.1:80</span></span><br><span class="line">        <span class="comment">#</span></span><br><span class="line">        <span class="comment">#location ~ \.php$ &#123;</span></span><br><span class="line">        <span class="comment">#    proxy_pass   http://127.0.0.1;</span></span><br><span class="line">        <span class="comment">#&#125;</span></span><br><span class="line"></span><br><span class="line">        <span class="comment"># pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000</span></span><br><span class="line">        <span class="comment">#</span></span><br><span class="line">        <span class="comment">#location ~ \.php$ &#123;</span></span><br><span class="line">        <span class="comment">#    root           html;</span></span><br><span class="line">        <span class="comment">#    fastcgi_pass   127.0.0.1:9000;</span></span><br><span class="line">        <span class="comment">#    fastcgi_index  index.php;</span></span><br><span class="line">        <span class="comment">#    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;</span></span><br><span class="line">        <span class="comment">#    include        fastcgi_params;</span></span><br><span class="line">        <span class="comment">#&#125;</span></span><br><span class="line"></span><br><span class="line">        <span class="comment"># deny access to .htaccess files, if Apache&#x27;s document root</span></span><br><span class="line">        <span class="comment"># concurs with nginx&#x27;s one</span></span><br><span class="line">        <span class="comment">#</span></span><br><span class="line">        <span class="comment">#location ~ /\.ht &#123;</span></span><br><span class="line">        <span class="comment">#    deny  all;</span></span><br><span class="line">        <span class="comment">#&#125;</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    <span class="comment"># another virtual host using mix of IP-, name-, and port-based configuration</span></span><br><span class="line">    <span class="comment">#</span></span><br><span class="line">    <span class="comment">#server &#123;</span></span><br><span class="line">    <span class="comment">#    listen       8000;</span></span><br><span class="line">    <span class="comment">#    listen       somename:8080;</span></span><br><span class="line">    <span class="comment">#    server_name  somename  alias  another.alias;</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">#    location / &#123;</span></span><br><span class="line">    <span class="comment">#        root   html;</span></span><br><span class="line">    <span class="comment">#        index  index.html index.htm;</span></span><br><span class="line">    <span class="comment">#    &#125;</span></span><br><span class="line">    <span class="comment">#&#125;</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    <span class="comment"># HTTPS server</span></span><br><span class="line">    <span class="comment">#</span></span><br><span class="line">    <span class="comment">#server &#123;</span></span><br><span class="line">    <span class="comment">#    listen       443 ssl;</span></span><br><span class="line">    <span class="comment">#    server_name  localhost;</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">#    ssl_certificate      cert.pem;</span></span><br><span class="line">    <span class="comment">#    ssl_certificate_key  cert.key;</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">#    ssl_session_cache    shared:SSL:1m;</span></span><br><span class="line">    <span class="comment">#    ssl_session_timeout  5m;</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">#    ssl_ciphers  HIGH:!aNULL:!MD5;</span></span><br><span class="line">    <span class="comment">#    ssl_prefer_server_ciphers  on;</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">#    location / &#123;</span></span><br><span class="line">    <span class="comment">#        root   html;</span></span><br><span class="line">    <span class="comment">#        index  index.html index.htm;</span></span><br><span class="line">    <span class="comment">#    &#125;</span></span><br><span class="line">    <span class="comment">#&#125;</span></span><br><span class="line"></span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure></div></div><div class="tab-to-top"><button type="button" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div><p>在Linux的控制台输入命令以启动nginx：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">nginx</span><br></pre></td></tr></table></figure><p>然后访问页面：<a href="http://192.168.150.101:8081">http://192.168.150.101:8081</a>，注意ip地址替换为你自己的虚拟机IP：</p><h1>3.备注</h1><p>加载OpenResty的lua模块：</p><figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">#lua 模块</span></span><br><span class="line"><span class="attribute">lua_package_path</span> <span class="string">&quot;/usr/local/openresty/lualib/?.lua;;&quot;</span>;</span><br><span class="line"><span class="comment">#c模块     </span></span><br><span class="line"><span class="attribute">lua_package_cpath</span> <span class="string">&quot;/usr/local/openresty/lualib/?.so;;&quot;</span>;  </span><br></pre></td></tr></table></figure><p>common.lua</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 封装函数，发送http请求，并解析响应</span></span><br><span class="line"><span class="keyword">local</span> <span class="function"><span class="keyword">function</span> <span class="title">read_http</span><span class="params">(path, params)</span></span></span><br><span class="line">    <span class="keyword">local</span> resp = ngx.location.capture(<span class="built_in">path</span>,&#123;</span><br><span class="line">        method = ngx.HTTP_GET,</span><br><span class="line">        args = params,</span><br><span class="line">    &#125;)</span><br><span class="line">    <span class="keyword">if</span> <span class="keyword">not</span> resp <span class="keyword">then</span></span><br><span class="line">        <span class="comment">-- 记录错误信息，返回404</span></span><br><span class="line">        ngx.<span class="built_in">log</span>(ngx.ERR, <span class="string">&quot;http not found, path: &quot;</span>, <span class="built_in">path</span> , <span class="string">&quot;, args: &quot;</span>, args)</span><br><span class="line">        ngx.<span class="built_in">exit</span>(<span class="number">404</span>)</span><br><span class="line">    <span class="keyword">end</span></span><br><span class="line">    <span class="keyword">return</span> resp.body</span><br><span class="line"><span class="keyword">end</span></span><br><span class="line"><span class="comment">-- 将方法导出</span></span><br><span class="line"><span class="keyword">local</span> _M = &#123;  </span><br><span class="line">    read_http = read_http</span><br><span class="line">&#125;  </span><br><span class="line"><span class="keyword">return</span> _M</span><br></pre></td></tr></table></figure><p>释放Redis连接API：</p><figure class="highlight lua"><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"><span class="comment">-- 关闭redis连接的工具方法，其实是放入连接池</span></span><br><span class="line"><span class="keyword">local</span> <span class="function"><span class="keyword">function</span> <span class="title">close_redis</span><span class="params">(red)</span></span></span><br><span class="line">    <span class="keyword">local</span> pool_max_idle_time = <span class="number">10000</span> <span class="comment">-- 连接的空闲时间，单位是毫秒</span></span><br><span class="line">    <span class="keyword">local</span> pool_size = <span class="number">100</span> <span class="comment">--连接池大小</span></span><br><span class="line">    <span class="keyword">local</span> ok, err = red:set_keepalive(pool_max_idle_time, pool_size)</span><br><span class="line">    <span class="keyword">if</span> <span class="keyword">not</span> ok <span class="keyword">then</span></span><br><span class="line">        ngx.<span class="built_in">log</span>(ngx.ERR, <span class="string">&quot;放入redis连接池失败: &quot;</span>, err)</span><br><span class="line">    <span class="keyword">end</span></span><br><span class="line"><span class="keyword">end</span></span><br></pre></td></tr></table></figure><p>读取Redis数据的API：</p><figure class="highlight lua"><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="comment">-- 查询redis的方法 ip和port是redis地址，key是查询的key</span></span><br><span class="line"><span class="keyword">local</span> <span class="function"><span class="keyword">function</span> <span class="title">read_redis</span><span class="params">(ip, port, key)</span></span></span><br><span class="line">    <span class="comment">-- 获取一个连接</span></span><br><span class="line">    <span class="keyword">local</span> ok, err = red:connect(ip, port)</span><br><span class="line">    <span class="keyword">if</span> <span class="keyword">not</span> ok <span class="keyword">then</span></span><br><span class="line">        ngx.<span class="built_in">log</span>(ngx.ERR, <span class="string">&quot;连接redis失败 : &quot;</span>, err)</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line">    <span class="keyword">end</span></span><br><span class="line">    <span class="comment">-- 查询redis</span></span><br><span class="line">    <span class="keyword">local</span> resp, err = red:get(key)</span><br><span class="line">    <span class="comment">-- 查询失败处理</span></span><br><span class="line">    <span class="keyword">if</span> <span class="keyword">not</span> resp <span class="keyword">then</span></span><br><span class="line">        ngx.<span class="built_in">log</span>(ngx.ERR, <span class="string">&quot;查询Redis失败: &quot;</span>, err, <span class="string">&quot;, key = &quot;</span> , key)</span><br><span class="line">    <span class="keyword">end</span></span><br><span class="line">    <span class="comment">--得到的数据为空处理</span></span><br><span class="line">    <span class="keyword">if</span> resp == ngx.null <span class="keyword">then</span></span><br><span class="line">        resp = <span class="literal">nil</span></span><br><span class="line">        ngx.<span class="built_in">log</span>(ngx.ERR, <span class="string">&quot;查询Redis数据为空, key = &quot;</span>, key)</span><br><span class="line">    <span class="keyword">end</span></span><br><span class="line">    close_redis(red)</span><br><span class="line">    <span class="keyword">return</span> resp</span><br><span class="line"><span class="keyword">end</span></span><br></pre></td></tr></table></figure><p>开启共享词典：</p><figure class="highlight nginx"><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="comment"># 共享字典，也就是本地缓存，名称叫做：item_cache，大小150m</span></span><br><span class="line"><span class="attribute">lua_shared_dict</span> item_cache <span class="number">150m</span>; </span><br></pre></td></tr></table></figure>]]>
    </content>
    <id>https://keepstaytrue.github.io/2025/05/22/%E5%AE%89%E8%A3%85OpenResty/</id>
    <link href="https://keepstaytrue.github.io/2025/05/22/%E5%AE%89%E8%A3%85OpenResty/"/>
    <published>2025-05-22T09:00:00.000Z</published>
    <summary>
      <![CDATA[<h1>安装OpenResty</h1>
<h1>1.安装</h1>
<p>首先你的Linux虚拟机必须联网</p>
<h2 id="1）安装开发库"><strong>1）安装开发库</strong></h2>
<p>首先要安装OpenResty的依赖开发库，执行命令：</p>]]>
    </summary>
    <title>安装OpenResty</title>
    <updated>2026-05-23T13:34:34.894Z</updated>
  </entry>
  <entry>
    <author>
      <name>KeepStayTrue</name>
    </author>
    <category term="Java后端求职学习体系" scheme="https://keepstaytrue.github.io/categories/Java%E5%90%8E%E7%AB%AF%E6%B1%82%E8%81%8C%E5%AD%A6%E4%B9%A0%E4%BD%93%E7%B3%BB/"/>
    <category term="中间件" scheme="https://keepstaytrue.github.io/categories/Java%E5%90%8E%E7%AB%AF%E6%B1%82%E8%81%8C%E5%AD%A6%E4%B9%A0%E4%BD%93%E7%B3%BB/%E4%B8%AD%E9%97%B4%E4%BB%B6/"/>
    <category term="redis" scheme="https://keepstaytrue.github.io/categories/Java%E5%90%8E%E7%AB%AF%E6%B1%82%E8%81%8C%E5%AD%A6%E4%B9%A0%E4%BD%93%E7%B3%BB/%E4%B8%AD%E9%97%B4%E4%BB%B6/redis/"/>
    <category term="缓存架构" scheme="https://keepstaytrue.github.io/categories/Java%E5%90%8E%E7%AB%AF%E6%B1%82%E8%81%8C%E5%AD%A6%E4%B9%A0%E4%BD%93%E7%B3%BB/%E4%B8%AD%E9%97%B4%E4%BB%B6/redis/%E7%BC%93%E5%AD%98%E6%9E%B6%E6%9E%84/"/>
    <category term="多级缓存" scheme="https://keepstaytrue.github.io/categories/Java%E5%90%8E%E7%AB%AF%E6%B1%82%E8%81%8C%E5%AD%A6%E4%B9%A0%E4%BD%93%E7%B3%BB/%E4%B8%AD%E9%97%B4%E4%BB%B6/redis/%E7%BC%93%E5%AD%98%E6%9E%B6%E6%9E%84/%E5%A4%9A%E7%BA%A7%E7%BC%93%E5%AD%98/"/>
    <category term="Redis" scheme="https://keepstaytrue.github.io/tags/Redis/"/>
    <category term="数据库" scheme="https://keepstaytrue.github.io/tags/%E6%95%B0%E6%8D%AE%E5%BA%93/"/>
    <category term="多级缓存" scheme="https://keepstaytrue.github.io/tags/%E5%A4%9A%E7%BA%A7%E7%BC%93%E5%AD%98/"/>
    <content>
      <![CDATA[<h1>多级缓存</h1><h1>1.什么是多级缓存</h1><p>传统的缓存策略一般是请求到达Tomcat后，先查询Redis，如果未命中则查询数据库，如图：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210821075259137.png" alt="image-20210821075259137"></p><p>存在下面的问题：</p><p>•请求要经过Tomcat处理，Tomcat的性能成为整个系统的瓶颈</p><p>•Redis缓存失效时，会对数据库产生冲击</p><p>多级缓存就是充分利用请求处理的每个环节，分别添加缓存，减轻Tomcat压力，提升服务性能：</p><ul><li>浏览器访问静态资源时，优先读取浏览器本地缓存 <strong>(如果本地有直接返回304状态码)</strong></li><li>访问非静态资源（ajax查询数据）时，访问服务端</li><li>请求到达Nginx后，优先读取Nginx本地缓存</li><li>如果Nginx本地缓存未命中，则去直接查询Redis（不经过Tomcat）</li><li>如果Redis查询未命中，则查询Tomcat</li><li>请求进入Tomcat后，优先查询JVM进程缓存</li><li>如果JVM进程缓存未命中，则查询数据库</li></ul><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210821075558137.png" alt="image-20210821075558137"></p><p>在多级缓存架构中，Nginx内部需要编写本地缓存查询、Redis查询、Tomcat查询的业务逻辑，因此这样的nginx服务不再是一个<strong>反向代理服务器</strong>，而是一个编写<strong>业务的Web服务器了</strong>。</p><p>因此这样的业务Nginx服务也需要搭建集群来提高并发，<strong>再有专门的nginx服务来做反向代理</strong>，如图：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210821080511581.png" alt="image-20210821080511581"></p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20260521135642907.png" alt="image-20260521135642907"></p><p>另外，我们的Tomcat服务将来也会部署为集群模式：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210821080954947.png" alt="image-20210821080954947"></p><p>可见，多级缓存的关键有两个：</p><ul><li><p>一个是在nginx中编写业务，实现nginx本地缓存、Redis、Tomcat的查询</p></li><li><p>另一个就是在Tomcat中实现JVM进程缓存</p></li></ul><p>其中Nginx编程则会用到OpenResty框架结合Lua这样的语言。</p><p>这也是今天课程的难点和重点。</p><h1>2.JVM进程缓存</h1><p>为了演示多级缓存的案例，我们先准备一个商品查询的业务。</p><h2 id="2-1-导入案例">2.1.导入案例</h2><p>参考课前资料的：《<a href="http://xn--fsqy8bm2rxoklxcuz1f.md">案例导入说明.md</a>》</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210821081418456.png" alt="image-20210821081418456"></p><h2 id="2-2-初识Caffeine">2.2.初识Caffeine</h2><p>缓存在日常开发中启动至关重要的作用，由于是存储在内存中，数据的读取速度是非常快的，能大量减少对数据库的访问，减少数据库的压力。我们把缓存分为两类：</p><ul><li>分布式缓存，例如Redis：<ul><li>优点：存储容量更大、可靠性更好、可以在集群间共享</li><li>缺点：访问缓存有网络开销</li><li>场景：缓存数据量较大、可靠性要求较高、需要在集群间共享</li></ul></li><li>进程本地缓存，例如HashMap、GuavaCache：<ul><li>优点：读取本地内存，没有网络开销，速度更快</li><li>缺点：存储容量有限、可靠性较低、无法共享</li><li>场景：性能要求较高，缓存数据量较小</li></ul></li></ul><p>我们今天会利用Caffeine框架来实现JVM进程缓存。</p><p><strong>Caffeine</strong>是一个基于Java8开发的，提供了近乎最佳命中率的高性能的本地缓存库。目前Spring内部的缓存使用的就是Caffeine。GitHub地址：<a href="https://github.com/ben-manes/caffeine">https://github.com/ben-manes/caffeine</a></p><p>Caffeine的性能非常好，下图是官方给出的性能对比：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210821081826399.png" alt="image-20210821081826399"></p><p>可以看到Caffeine的性能遥遥领先！</p><p>缓存使用的基本API：</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></pre></td><td class="code"><pre><span class="line"><span class="meta">@Test</span></span><br><span class="line"><span class="keyword">void</span> <span class="title function_">testBasicOps</span><span class="params">()</span> &#123;</span><br><span class="line">      <span class="comment">//newBuilder是一个工厂模式</span></span><br><span class="line">    <span class="comment">// 构建cache对象</span></span><br><span class="line">    Cache&lt;String, String&gt; cache = Caffeine.newBuilder().build();</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 存数据</span></span><br><span class="line">    cache.put(<span class="string">&quot;gf&quot;</span>, <span class="string">&quot;迪丽热巴&quot;</span>);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 取数据</span></span><br><span class="line">    <span class="type">String</span> <span class="variable">gf</span> <span class="operator">=</span> cache.getIfPresent(<span class="string">&quot;gf&quot;</span>);</span><br><span class="line">    System.out.println(<span class="string">&quot;gf = &quot;</span> + gf);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 取数据，包含两个参数：</span></span><br><span class="line">    <span class="comment">// 参数一：缓存的key</span></span><br><span class="line">    <span class="comment">// 参数二：Lambda表达式，表达式参数就是缓存的key，方法体是查询数据库的逻辑</span></span><br><span class="line">    <span class="comment">// 优先根据key查询JVM缓存，如果未命中，则执行参数二的Lambda表达式(一般是去数据库查)</span></span><br><span class="line">    <span class="type">String</span> <span class="variable">defaultGF</span> <span class="operator">=</span> cache.get(<span class="string">&quot;defaultGF&quot;</span>, key -&gt; &#123;</span><br><span class="line">        <span class="comment">// 根据key去数据库查询数据</span></span><br><span class="line">        <span class="keyword">return</span> <span class="string">&quot;柳岩&quot;</span>;</span><br><span class="line">    &#125;);</span><br><span class="line">    System.out.println(<span class="string">&quot;defaultGF = &quot;</span> + defaultGF);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>Caffeine既然是缓存的一种，肯定需要有缓存的清除策略，不然的话内存总会有耗尽的时候。</p><p>Caffeine提供了三种<strong>缓存驱逐策略</strong>：</p><ul><li><p><strong>基于容量</strong>：设置缓存的数量上限</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 创建缓存对象</span></span><br><span class="line">Cache&lt;String, String&gt; cache = Caffeine.newBuilder()</span><br><span class="line">    .maximumSize(<span class="number">1</span>) <span class="comment">// 设置缓存大小上限为 1,达到阈值就可以去做内存的清理,清理的时候采用的是lRU策略和redis一样</span></span><br><span class="line">    .build();</span><br></pre></td></tr></table></figure></li><li><p><strong>基于时间</strong>：设置缓存的有效时间(写之后开始计时)</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 创建缓存对象</span></span><br><span class="line">Cache&lt;String, String&gt; cache = Caffeine.newBuilder()</span><br><span class="line">    <span class="comment">// 设置缓存有效期为 10 秒，从最后一次写入开始计时 </span></span><br><span class="line">    .expireAfterWrite(Duration.ofSeconds(<span class="number">10</span>)) </span><br><span class="line">    .build();</span><br><span class="line"></span><br></pre></td></tr></table></figure></li><li><p><strong>基于引用</strong>：设置缓存为软引用或弱引用，利用GC(垃圾回收机制,如果一个对象没有人引用那么将来就可以被回收)来回收缓存数据。性能较差，不建议使用。</p></li></ul><blockquote><p><strong>注意</strong>：在默认情况下，当一个缓存元素过期的时候，Caffeine<strong>不会自动立即将其清理和驱逐</strong>。而是在一次读或写操作后，或者在空闲时间完成对失效数据的驱逐。</p></blockquote><blockquote><p>企业开发中,可以把缓存提前定义好非静态工具类也行,也可以定义成一个静态工具类,将来,cache对外暴露让别人可以使用往里面做数据读取即可.</p></blockquote><h2 id="2-3-实现JVM进程缓存">2.3.实现JVM进程缓存</h2><h3 id="2-3-1-需求">2.3.1.需求</h3><p>利用Caffeine实现下列需求：</p><ul><li>给根据id查询商品的业务添加缓存，缓存未命中时查询数据库</li><li>给根据id查询商品库存的业务添加缓存，缓存未命中时查询数据库</li><li>缓存初始大小为100</li><li>缓存上限为10000</li></ul><h3 id="2-3-2-实现">2.3.2.实现</h3><p>首先，我们需要定义两个Caffeine的缓存对象，分别保存商品、库存的缓存数据。</p><p>在item-service的<code>com.heima.item.config</code>包下定义<code>CaffeineConfig</code>类：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.heima.item.config;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> com.github.benmanes.caffeine.cache.Cache;</span><br><span class="line"><span class="keyword">import</span> com.github.benmanes.caffeine.cache.Caffeine;</span><br><span class="line"><span class="keyword">import</span> com.heima.item.pojo.Item;</span><br><span class="line"><span class="keyword">import</span> com.heima.item.pojo.ItemStock;</span><br><span class="line"><span class="keyword">import</span> org.springframework.context.annotation.Bean;</span><br><span class="line"><span class="keyword">import</span> org.springframework.context.annotation.Configuration;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Configuration</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">CaffeineConfig</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Bean</span></span><br><span class="line">    <span class="keyword">public</span> Cache&lt;Long, Item&gt; <span class="title function_">itemCache</span><span class="params">()</span>&#123;</span><br><span class="line">        <span class="keyword">return</span> Caffeine.newBuilder()</span><br><span class="line">                .initialCapacity(<span class="number">100</span>)</span><br><span class="line">                .maximumSize(<span class="number">10_000</span>)</span><br><span class="line">                .build();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Bean</span></span><br><span class="line">    <span class="keyword">public</span> Cache&lt;Long, ItemStock&gt; <span class="title function_">stockCache</span><span class="params">()</span>&#123;</span><br><span class="line">        <span class="keyword">return</span> Caffeine.newBuilder()</span><br><span class="line">                .initialCapacity(<span class="number">100</span>)</span><br><span class="line">                .maximumSize(<span class="number">10_000</span>)</span><br><span class="line">                .build();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>然后，修改item-service中的<code>com.heima.item.web</code>包下的ItemController类，添加缓存逻辑：</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></pre></td><td class="code"><pre><span class="line"><span class="meta">@RestController</span></span><br><span class="line"><span class="meta">@RequestMapping(&quot;item&quot;)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">ItemController</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> IItemService itemService;</span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> IItemStockService stockService;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> Cache&lt;Long, Item&gt; itemCache;</span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> Cache&lt;Long, ItemStock&gt; stockCache;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// ...其它略</span></span><br><span class="line">    </span><br><span class="line">    <span class="meta">@GetMapping(&quot;/&#123;id&#125;&quot;)</span></span><br><span class="line">    <span class="keyword">public</span> Item <span class="title function_">findById</span><span class="params">(<span class="meta">@PathVariable(&quot;id&quot;)</span> Long id)</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> itemCache.get(id, key -&gt; itemService.query()</span><br><span class="line">                .ne(<span class="string">&quot;status&quot;</span>, <span class="number">3</span>).eq(<span class="string">&quot;id&quot;</span>, key)</span><br><span class="line">                .one()</span><br><span class="line">        );</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@GetMapping(&quot;/stock/&#123;id&#125;&quot;)</span></span><br><span class="line">    <span class="keyword">public</span> ItemStock <span class="title function_">findStockById</span><span class="params">(<span class="meta">@PathVariable(&quot;id&quot;)</span> Long id)</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> stockCache.get(id, key -&gt; stockService.getById(key));</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h1>3.Lua语法入门</h1><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20260522143331840.png" alt="image-20260522143331840"></p><p>Nginx编程需要用到Lua语言，因此我们必须先入门Lua的基本语法。</p><h2 id="3-1-初识Lua">3.1.初识Lua</h2><p>Lua 是一种轻量小巧的脚本语言，用标准C语言编写并以源代码形式开放， 其设计目的是为了嵌入应用程序中，从而为应用程序提供灵活的扩展和定制功能。官网：<a href="https://www.lua.org/">https://www.lua.org/</a></p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210821091437975.png" alt="image-20210821091437975"></p><p>Lua经常嵌入到C语言开发的程序中，例如游戏开发、游戏插件等。</p><p>Nginx本身也是C语言开发，因此也允许基于Lua做拓展。</p><h2 id="3-1-HelloWorld">3.1.HelloWorld</h2><p>CentOS7默认已经安装了Lua语言环境，所以可以直接运行Lua代码。</p><p>1）在Linux虚拟机的任意目录下，新建一个hello.lua文件</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210821091621308.png" alt="image-20210821091621308"></p><p>2）添加下面的内容</p><figure class="highlight lua"><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">print</span>(<span class="string">&quot;Hello World!&quot;</span>)  </span><br></pre></td></tr></table></figure><p>3）运行</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210821091638140.png" alt="image-20210821091638140"></p><h2 id="3-2-变量和循环">3.2.变量和循环</h2><p>学习任何语言必然离不开变量，而变量的声明必须先知道数据的类型。</p><h3 id="3-2-1-Lua的数据类型">3.2.1.Lua的数据类型</h3><p>Lua中支持的常见数据类型包括：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210821091835406.png" alt="image-20210821091835406"></p><p><strong>table 相当于java中的map (key,value结构),但是他既可以表示数组(key是数字的情况下),也可以表示map(key是字符串的情况下)</strong></p><p>另外，Lua提供了type()函数来判断一个变量的数据类型：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210821091904332.png" alt="image-20210821091904332"></p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20260522144409422.png" alt="image-20260522144409422"></p><h3 id="3-2-2-声明变量">3.2.2.声明变量</h3><p>Lua声明变量的时候无需指定数据类型，而是用local来声明变量为局部变量：</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 声明字符串，可以用单引号或双引号，</span></span><br><span class="line"><span class="keyword">local</span> str = <span class="string">&#x27;hello&#x27;</span></span><br><span class="line"><span class="comment">-- 字符串拼接可以使用 ..</span></span><br><span class="line"><span class="keyword">local</span> str2 = <span class="string">&#x27;hello&#x27;</span> .. <span class="string">&#x27;world&#x27;</span></span><br><span class="line"><span class="comment">-- 声明数字</span></span><br><span class="line"><span class="keyword">local</span> num = <span class="number">21</span></span><br><span class="line"><span class="comment">-- 声明布尔类型</span></span><br><span class="line"><span class="keyword">local</span> flag = <span class="literal">true</span></span><br></pre></td></tr></table></figure><p>Lua中的table类型既可以作为数组，又可以作为Java中的map来使用。数组就是特殊的table，key是数组角标而已：</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 声明数组 ，key为角标的 table</span></span><br><span class="line"><span class="keyword">local</span> arr = &#123;<span class="string">&#x27;java&#x27;</span>, <span class="string">&#x27;python&#x27;</span>, <span class="string">&#x27;lua&#x27;</span>&#125;</span><br><span class="line"><span class="comment">-- 声明table，类似java的map</span></span><br><span class="line"><span class="keyword">local</span> map =  &#123;name=<span class="string">&#x27;Jack&#x27;</span>, age=<span class="number">21</span>&#125;</span><br></pre></td></tr></table></figure><p>Lua中的<strong>数组角标是从1开始</strong>，访问的时候与Java中类似：</p><figure class="highlight lua"><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="comment">-- 访问数组，lua数组的角标从1开始</span></span><br><span class="line"><span class="built_in">print</span>(arr[<span class="number">1</span>])</span><br></pre></td></tr></table></figure><p>Lua中的table可以用key来访问：</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 访问table</span></span><br><span class="line"><span class="built_in">print</span>(map[<span class="string">&#x27;name&#x27;</span>])</span><br><span class="line"><span class="built_in">print</span>(map.name)</span><br></pre></td></tr></table></figure><blockquote><p>Lua中字符串拼接用 …  拼接的</p></blockquote><h3 id="3-2-3-循环">3.2.3.循环</h3><p>对于table，我们可以利用for循环来遍历。不过数组和普通table遍历略有差异。</p><p>遍历数组：</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 声明数组 key为索引的 table</span></span><br><span class="line"><span class="keyword">local</span> arr = &#123;<span class="string">&#x27;java&#x27;</span>, <span class="string">&#x27;python&#x27;</span>, <span class="string">&#x27;lua&#x27;</span>&#125;</span><br><span class="line"><span class="comment">-- 遍历数组</span></span><br><span class="line"><span class="keyword">for</span> index,value <span class="keyword">in</span> <span class="built_in">ipairs</span>(arr) <span class="keyword">do</span></span><br><span class="line">    <span class="built_in">print</span>(index, value) </span><br><span class="line"><span class="keyword">end</span></span><br><span class="line"><span class="comment">-- do表示for循环的开始 , end 表示for循环的结束</span></span><br><span class="line"><span class="comment">-- ipairs表示要解析的数组,解析完后形成键值对,键 是index 值是value,在数组中键就是索引</span></span><br></pre></td></tr></table></figure><p>遍历普通table</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 声明map，也就是table</span></span><br><span class="line"><span class="keyword">local</span> map = &#123;name=<span class="string">&#x27;Jack&#x27;</span>, age=<span class="number">21</span>&#125;</span><br><span class="line"><span class="comment">-- 遍历table</span></span><br><span class="line"><span class="keyword">for</span> key,value <span class="keyword">in</span> <span class="built_in">pairs</span>(map) <span class="keyword">do</span></span><br><span class="line">   <span class="built_in">print</span>(key, value) </span><br><span class="line"><span class="keyword">end</span></span><br><span class="line"><span class="comment">-- pairs表示要解析的table</span></span><br></pre></td></tr></table></figure><h2 id="3-3-条件控制、函数">3.3.条件控制、函数</h2><p>Lua中的条件控制和函数声明与Java类似。</p><h3 id="3-3-1-函数">3.3.1.函数</h3><p>定义函数的语法：</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> 函数名<span class="params">( argument1, argument2..., argumentn)</span></span></span><br><span class="line">    <span class="comment">-- 函数体</span></span><br><span class="line">    <span class="keyword">return</span> 返回值</span><br><span class="line"><span class="keyword">end</span></span><br></pre></td></tr></table></figure><p>例如，定义一个函数，用来打印数组：</p><figure class="highlight lua"><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="function"><span class="keyword">function</span> <span class="title">printArr</span><span class="params">(arr)</span></span></span><br><span class="line">    <span class="keyword">for</span> index, value <span class="keyword">in</span> <span class="built_in">ipairs</span>(arr) <span class="keyword">do</span></span><br><span class="line">        <span class="built_in">print</span>(value)</span><br><span class="line">    <span class="keyword">end</span></span><br><span class="line"><span class="keyword">end</span></span><br></pre></td></tr></table></figure><h3 id="3-3-2-条件控制">3.3.2.条件控制</h3><p>类似Java的条件控制，例如if、else语法：</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span>(布尔表达式)</span><br><span class="line"><span class="keyword">then</span></span><br><span class="line">   <span class="comment">--[ 布尔表达式为 true 时执行该语句块 --]</span></span><br><span class="line"><span class="keyword">else</span></span><br><span class="line">   <span class="comment">--[ 布尔表达式为 false 时执行该语句块 --]</span></span><br><span class="line"><span class="keyword">end</span></span><br><span class="line"></span><br></pre></td></tr></table></figure><p>与java不同，布尔表达式中的逻辑运算是基于英文单词：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210821092657918.png" alt="image-20210821092657918"></p><h3 id="3-3-3-案例">3.3.3.案例</h3><p>需求：自定义一个函数，可以打印table，当参数为nil时，打印错误信息</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">printArr</span><span class="params">(arr)</span></span></span><br><span class="line">    <span class="keyword">if</span> <span class="keyword">not</span> arr <span class="keyword">then</span></span><br><span class="line">        <span class="built_in">print</span>(<span class="string">&#x27;数组不能为空！&#x27;</span>)</span><br><span class="line">    <span class="keyword">end</span></span><br><span class="line">    <span class="keyword">for</span> index, value <span class="keyword">in</span> <span class="built_in">ipairs</span>(arr) <span class="keyword">do</span></span><br><span class="line">        <span class="built_in">print</span>(value)</span><br><span class="line">    <span class="keyword">end</span></span><br><span class="line"><span class="keyword">end</span></span><br></pre></td></tr></table></figure><h1>4.实现多级缓存</h1><p>多级缓存的实现离不开Nginx编程，而Nginx编程又离不开OpenResty。</p><h2 id="4-1-安装OpenResty">4.1.安装OpenResty</h2><p>OpenResty® 是一个基于 Nginx的高性能 Web 平台，用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。具备下列特点：</p><ul><li>具备Nginx的完整功能</li><li>基于Lua语言进行扩展，集成了大量精良的 Lua 库、第三方模块</li><li>允许使用Lua<strong>自定义业务逻辑</strong>、<strong>自定义库</strong></li></ul><p>官方网站： <a href="https://openresty.org/cn/">https://openresty.org/cn/</a></p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210821092902946.png" alt="image-20210821092902946"></p><p>安装Lua可以参考课前资料提供的《<a href="http://xn--OpenResty-ui4qu490b.md">安装OpenResty.md</a>》：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210821092941139.png" alt="image-20210821092941139"></p><h2 id="4-2-OpenResty快速入门">4.2.OpenResty快速入门</h2><p>我们希望达到的多级缓存架构如图：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/yeVDlwtfMx.png" alt="yeVDlwtfMx"></p><p>其中：</p><ul><li><p>windows上的nginx用来做反向代理服务，将前端的查询商品的ajax请求代理到OpenResty集群</p></li><li><p>OpenResty集群用来编写多级缓存业务</p></li></ul><h3 id="4-2-1-反向代理流程">4.2.1.反向代理流程</h3><p>现在，商品详情页使用的是假的商品数据。不过在浏览器中，可以看到页面有发起ajax请求查询真实商品数据。</p><p>这个请求如下：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210821093144700.png" alt="image-20210821093144700"></p><p>请求地址是localhost，端口是80，就被windows上安装的Nginx服务给接收到了。然后代理给了OpenResty集群：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210821094447709.png" alt="image-20210821094447709"></p><p>我们需要在OpenResty中编写业务，查询商品数据并返回到浏览器。</p><p>但是这次，我们先在OpenResty接收请求，返回假的商品数据。</p><h3 id="4-2-2-OpenResty监听请求">4.2.2.OpenResty监听请求</h3><p>OpenResty的很多功能都依赖于其目录下的Lua库，需要在nginx.conf中指定依赖库的目录，并导入依赖：</p><p>1）添加对OpenResty的Lua模块的加载</p><p>修改<code>/usr/local/openresty/nginx/conf/nginx.conf</code>文件，在其中的http下面，添加下面代码：</p><figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">#lua 模块</span></span><br><span class="line"><span class="attribute">lua_package_path</span> <span class="string">&quot;/usr/local/openresty/lualib/?.lua;;&quot;</span>;</span><br><span class="line"><span class="comment">#c模块     </span></span><br><span class="line"><span class="attribute">lua_package_cpath</span> <span class="string">&quot;/usr/local/openresty/lualib/?.so;;&quot;</span>;  </span><br></pre></td></tr></table></figure><p>2）监听/api/item路径</p><p>修改<code>/usr/local/openresty/nginx/conf/nginx.conf</code>文件，在nginx.conf的server下面，添加对/api/item这个路径的监听：</p><figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">location</span>  /api/item &#123;</span><br><span class="line">    <span class="comment"># 默认的响应类型</span></span><br><span class="line">    <span class="attribute">default_type</span> application/json;</span><br><span class="line">    <span class="comment"># 响应结果由lua/item.lua文件来决定</span></span><br><span class="line">    <span class="attribute">content_by_lua_file</span> lua/item.lua;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这个监听，就类似于SpringMVC中的<code>@GetMapping(&quot;/api/item&quot;)</code>做路径映射。</p><p>而<code>content_by_lua_file lua/item.lua</code>则相当于调用item.lua这个文件，执行其中的业务，把结果返回给用户。相当于java中调用service。</p><h3 id="4-2-3-编写item-lua">4.2.3.编写item.lua</h3><p>1）在<code>/usr/loca/openresty/nginx</code>目录创建文件夹：lua</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210821100755080.png" alt="image-20210821100755080"></p><p>2）在<code>/usr/loca/openresty/nginx/lua</code>文件夹下，新建文件：item.lua</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210821100801756.png" alt="image-20210821100801756"></p><p>3）编写item.lua，返回假数据</p><p>item.lua中，利用ngx.say()函数返回数据到Response中</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ngx.say(<span class="string">&#x27;&#123;&quot;id&quot;:10001,&quot;name&quot;:&quot;SALSA AIR&quot;,&quot;title&quot;:&quot;RIMOWA 21寸托运箱拉杆箱 SALSA AIR系列果绿色 820.70.36.4&quot;,&quot;price&quot;:17900,&quot;image&quot;:&quot;https://m.360buyimg.com/mobilecms/s720x720_jfs/t6934/364/1195375010/84676/e9f2c55f/597ece38N0ddcbc77.jpg!q70.jpg.webp&quot;,&quot;category&quot;:&quot;拉杆箱&quot;,&quot;brand&quot;:&quot;RIMOWA&quot;,&quot;spec&quot;:&quot;&quot;,&quot;status&quot;:1,&quot;createTime&quot;:&quot;2019-04-30T16:00:00.000+00:00&quot;,&quot;updateTime&quot;:&quot;2019-04-30T16:00:00.000+00:00&quot;,&quot;stock&quot;:2999,&quot;sold&quot;:31290&#125;&#x27;</span>)</span><br></pre></td></tr></table></figure><p>4）重新加载配置</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">nginx -s reload</span><br></pre></td></tr></table></figure><p>刷新商品页面：<a href="http://localhost/item.html?id=1001%EF%BC%8C%E5%8D%B3%E5%8F%AF%E7%9C%8B%E5%88%B0%E6%95%88%E6%9E%9C%EF%BC%9A">http://localhost/item.html?id=1001，即可看到效果：</a></p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210821101217089.png" alt="image-20210821101217089"></p><h2 id="4-3-请求参数处理">4.3.请求参数处理</h2><p>上一节中，我们在OpenResty接收前端请求，但是返回的是假数据。</p><p>要返回真实数据，必须根据前端传递来的商品id，查询商品信息才可以。</p><p>那么如何获取前端传递的商品参数呢？</p><h3 id="4-3-1-获取参数的API">4.3.1.获取参数的API</h3><p>OpenResty中提供了一些API用来获取不同类型的前端请求参数：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210821101433528.png" alt="image-20210821101433528"></p><h3 id="4-3-2-获取参数并返回">4.3.2.获取参数并返回</h3><p>在前端发起的ajax请求如图：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210821101721649.png" alt="image-20210821101721649"></p><p>可以看到商品id是以路径占位符方式传递的，因此可以利用正则表达式匹配的方式来获取ID</p><p>1）获取商品id</p><p>修改<code>/usr/loca/openresty/nginx/nginx.conf</code>文件中监听/api/item的代码，利用正则表达式获取ID：</p><figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">location</span> <span class="regexp">~ /api/item/(\d+)</span> &#123;</span><br><span class="line">    <span class="comment"># 默认的响应类型</span></span><br><span class="line">    <span class="attribute">default_type</span> application/json;</span><br><span class="line">    <span class="comment"># 响应结果由lua/item.lua文件来决定</span></span><br><span class="line">    <span class="attribute">content_by_lua_file</span> lua/item.lua;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>2）拼接ID并返回</p><p>修改<code>/usr/loca/openresty/nginx/lua/item.lua</code>文件，获取id并拼接到结果中返回：</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 获取商品id</span></span><br><span class="line"><span class="keyword">local</span> id = ngx.var[<span class="number">1</span>]</span><br><span class="line"><span class="comment">-- 拼接并返回</span></span><br><span class="line">ngx.say(<span class="string">&#x27;&#123;&quot;id&quot;:&#x27;</span> .. id .. <span class="string">&#x27;,&quot;name&quot;:&quot;SALSA AIR&quot;,&quot;title&quot;:&quot;RIMOWA 21寸托运箱拉杆箱 SALSA AIR系列果绿色 820.70.36.4&quot;,&quot;price&quot;:17900,&quot;image&quot;:&quot;https://m.360buyimg.com/mobilecms/s720x720_jfs/t6934/364/1195375010/84676/e9f2c55f/597ece38N0ddcbc77.jpg!q70.jpg.webp&quot;,&quot;category&quot;:&quot;拉杆箱&quot;,&quot;brand&quot;:&quot;RIMOWA&quot;,&quot;spec&quot;:&quot;&quot;,&quot;status&quot;:1,&quot;createTime&quot;:&quot;2019-04-30T16:00:00.000+00:00&quot;,&quot;updateTime&quot;:&quot;2019-04-30T16:00:00.000+00:00&quot;,&quot;stock&quot;:2999,&quot;sold&quot;:31290&#125;&#x27;</span>)</span><br></pre></td></tr></table></figure><p>3）重新加载并测试</p><p>运行命令以重新加载OpenResty配置：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">nginx -s reload</span><br></pre></td></tr></table></figure><p>刷新页面可以看到结果中已经带上了ID：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210821102235467.png" alt="image-20210821102235467"></p><h2 id="4-4-查询Tomcat">4.4.查询Tomcat</h2><p>拿到商品ID后，本应去缓存中查询商品信息，不过目前我们还未建立nginx、redis缓存。因此，这里我们先根据商品id去tomcat查询商品信息。我们实现如图部分：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210821102610167.png" alt="image-20210821102610167"></p><p>需要注意的是，我们的OpenResty是在虚拟机，Tomcat是在Windows电脑上。两者IP一定不要搞错了。</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210821102959829.png" alt="image-20210821102959829"></p><h3 id="4-4-1-发送http请求的API">4.4.1.发送http请求的API</h3><p>nginx提供了内部API用以发送http请求：</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">local</span> resp = ngx.location.capture(<span class="string">&quot;/path&quot;</span>,&#123;</span><br><span class="line">    method = ngx.HTTP_GET,   <span class="comment">-- 请求方式</span></span><br><span class="line">    args = &#123;a=<span class="number">1</span>,b=<span class="number">2</span>&#125;,  <span class="comment">-- get方式传参数</span></span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><p>返回的响应内容包括：</p><ul><li>resp.status：响应状态码</li><li>resp.header：响应头，是一个table</li><li>resp.body：响应体，就是响应数据</li></ul><p>注意：这里的path是路径，并不包含IP和端口。这个请求会被nginx内部的server监听并处理。</p><p>但是我们希望这个请求发送到Tomcat服务器，所以还需要编写一个server来对这个路径做反向代理：</p><figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">location</span> /path &#123;</span><br><span class="line">    <span class="comment"># 这里是windows电脑的ip和Java服务端口，需要确保windows防火墙处于关闭状态</span></span><br><span class="line">    <span class="attribute">proxy_pass</span> http://192.168.150.1:8081; </span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>原理如图：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210821104149061.png" alt="image-20210821104149061"></p><h3 id="4-4-2-封装http工具">4.4.2.封装http工具</h3><p>下面，我们封装一个发送Http请求的工具，基于ngx.location.capture来实现查询tomcat。</p><p>1）添加反向代理，到windows的Java服务</p><p>因为item-service中的接口都是/item开头，所以我们监听/item路径，代理到windows上的tomcat服务。</p><p>修改 <code>/usr/local/openresty/nginx/conf/nginx.conf</code>文件，添加一个location：</p><figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">location</span> /item &#123;</span><br><span class="line">    <span class="attribute">proxy_pass</span> http://192.168.150.1:8081;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>以后，只要我们调用<code>ngx.location.capture(&quot;/item&quot;)</code>，就一定能发送请求到windows的tomcat服务。</p><p>2）封装工具类</p><p>之前我们说过，OpenResty启动时会加载以下两个目录中的工具文件：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210821104857413.png" alt="image-20210821104857413"></p><p>所以，自定义的http工具也需要放到这个目录下。</p><p>在<code>/usr/local/openresty/lualib</code>目录下，新建一个common.lua文件：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">vi /usr/local/openresty/lualib/common.lua</span><br></pre></td></tr></table></figure><p>内容如下:</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 封装函数，发送http请求，并解析响应</span></span><br><span class="line"><span class="keyword">local</span> <span class="function"><span class="keyword">function</span> <span class="title">read_http</span><span class="params">(path, params)</span></span></span><br><span class="line">    <span class="keyword">local</span> resp = ngx.location.capture(<span class="built_in">path</span>,&#123;</span><br><span class="line">        method = ngx.HTTP_GET,</span><br><span class="line">        args = params,</span><br><span class="line">    &#125;)</span><br><span class="line">    <span class="keyword">if</span> <span class="keyword">not</span> resp <span class="keyword">then</span></span><br><span class="line">        <span class="comment">-- 记录错误信息，返回404</span></span><br><span class="line">        ngx.<span class="built_in">log</span>(ngx.ERR, <span class="string">&quot;http请求查询失败, path: &quot;</span>, <span class="built_in">path</span> , <span class="string">&quot;, args: &quot;</span>, args)</span><br><span class="line">        ngx.<span class="built_in">exit</span>(<span class="number">404</span>)</span><br><span class="line">    <span class="keyword">end</span></span><br><span class="line">    <span class="keyword">return</span> resp.body</span><br><span class="line"><span class="keyword">end</span></span><br><span class="line"><span class="comment">-- 将方法导出</span></span><br><span class="line"><span class="keyword">local</span> _M = &#123;  </span><br><span class="line">    read_http = read_http</span><br><span class="line">&#125;  </span><br><span class="line"><span class="keyword">return</span> _M</span><br></pre></td></tr></table></figure><p>这个工具将read_http函数封装到_M这个table类型的变量中，并且返回，这类似于导出。</p><p>使用的时候，可以利用<code>require('common')</code>来导入该函数库，这里的common是函数库的文件名。</p><p>3）实现商品查询</p><p>最后，我们修改<code>/usr/local/openresty/lua/item.lua</code>文件，利用刚刚封装的函数库实现对tomcat的查询：</p><figure class="highlight lua"><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="comment">-- 引入自定义common工具模块，返回值是common中返回的 _M</span></span><br><span class="line"><span class="keyword">local</span> common = <span class="built_in">require</span>(<span class="string">&quot;common&quot;</span>)</span><br><span class="line"><span class="comment">-- 从 common中获取read_http这个函数</span></span><br><span class="line"><span class="keyword">local</span> read_http = common.read_http</span><br><span class="line"><span class="comment">-- 获取路径参数</span></span><br><span class="line"><span class="keyword">local</span> id = ngx.var[<span class="number">1</span>]</span><br><span class="line"><span class="comment">-- 根据id查询商品</span></span><br><span class="line"><span class="keyword">local</span> itemJSON = read_http(<span class="string">&quot;/item/&quot;</span>.. id, <span class="literal">nil</span>)</span><br><span class="line"><span class="comment">-- 根据id查询商品库存</span></span><br><span class="line"><span class="keyword">local</span> itemStockJSON = read_http(<span class="string">&quot;/item/stock/&quot;</span>.. id, <span class="literal">nil</span>)</span><br></pre></td></tr></table></figure><p>这里查询到的结果是json字符串，并且包含商品、库存两个json字符串，页面最终需要的是把两个json拼接为一个json：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210821110441222.png" alt="image-20210821110441222"></p><p>这就需要我们先把JSON变为lua的table，完成数据整合后，再转为JSON。</p><h3 id="4-4-3-CJSON工具类">4.4.3.CJSON工具类</h3><p>OpenResty提供了一个cjson的模块用来处理JSON的序列化和反序列化。</p><p>官方地址： <a href="https://github.com/openresty/lua-cjson/">https://github.com/openresty/lua-cjson/</a></p><p>1）引入cjson模块：</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">local</span> cjson = <span class="built_in">require</span> <span class="string">&quot;cjson&quot;</span></span><br></pre></td></tr></table></figure><p>2）序列化：</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">local</span> obj = &#123;</span><br><span class="line">    name = <span class="string">&#x27;jack&#x27;</span>,</span><br><span class="line">    age = <span class="number">21</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">-- 把 table 序列化为 json</span></span><br><span class="line"><span class="keyword">local</span> json = cjson.encode(obj)</span><br></pre></td></tr></table></figure><p>3）反序列化：</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">local</span> json = <span class="string">&#x27;&#123;&quot;name&quot;: &quot;jack&quot;, &quot;age&quot;: 21&#125;&#x27;</span></span><br><span class="line"><span class="comment">-- 反序列化 json为 table</span></span><br><span class="line"><span class="keyword">local</span> obj = cjson.decode(json);</span><br><span class="line"><span class="built_in">print</span>(obj.name)</span><br></pre></td></tr></table></figure><h3 id="4-4-4-实现Tomcat查询">4.4.4.实现Tomcat查询</h3><p>下面，我们修改之前的item.lua中的业务，添加json处理功能：</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 导入common函数库</span></span><br><span class="line"><span class="keyword">local</span> common = <span class="built_in">require</span>(<span class="string">&#x27;common&#x27;</span>)</span><br><span class="line"><span class="keyword">local</span> read_http = common.read_http</span><br><span class="line"><span class="comment">-- 导入cjson库</span></span><br><span class="line"><span class="keyword">local</span> cjson = <span class="built_in">require</span>(<span class="string">&#x27;cjson&#x27;</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 获取路径参数</span></span><br><span class="line"><span class="keyword">local</span> id = ngx.var[<span class="number">1</span>]</span><br><span class="line"><span class="comment">-- 根据id查询商品</span></span><br><span class="line"><span class="keyword">local</span> itemJSON = read_http(<span class="string">&quot;/item/&quot;</span>.. id, <span class="literal">nil</span>)</span><br><span class="line"><span class="comment">-- 根据id查询商品库存</span></span><br><span class="line"><span class="keyword">local</span> itemStockJSON = read_http(<span class="string">&quot;/item/stock/&quot;</span>.. id, <span class="literal">nil</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment">-- JSON转化为lua的table</span></span><br><span class="line"><span class="keyword">local</span> item = cjson.decode(itemJSON)</span><br><span class="line"><span class="keyword">local</span> stock = cjson.decode(stockJSON)</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 组合数据</span></span><br><span class="line">item.stock = stock.stock</span><br><span class="line">item.sold = stock.sold</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 把item序列化为json 返回结果</span></span><br><span class="line">ngx.say(cjson.encode(item))</span><br></pre></td></tr></table></figure><h3 id="4-4-5-基于ID负载均衡">4.4.5.基于ID负载均衡</h3><p>刚才的代码中，我们的tomcat是单机部署。而实际开发中，tomcat一定是集群模式：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210821111023255.png" alt="image-20210821111023255"></p><p>因此，OpenResty需要对tomcat集群做负载均衡。</p><p>而默认的负载均衡规则是轮询模式，当我们查询/item/10001时：</p><ul><li>第一次会访问8081端口的tomcat服务，在该服务内部就形成了JVM进程缓存</li><li>第二次会访问8082端口的tomcat服务，该服务内部没有JVM缓存（因为JVM缓存无法共享），会查询数据库</li><li>…</li></ul><p>你看，因为轮询的原因，第一次查询8081形成的JVM缓存并未生效，直到下一次再次访问到8081时才可以生效，缓存命中率太低了。</p><p>怎么办？</p><p>如果能让同一个商品，每次查询时都访问同一个tomcat服务，那么JVM缓存就一定能生效了。</p><p>也就是说，我们需要根据商品id做负载均衡，而不是轮询。</p><h4 id="1）原理">1）原理</h4><p>nginx提供了基于请求路径做负载均衡的算法：</p><p>nginx根据请求路径做hash运算，把得到的数值对tomcat服务的数量取余，余数是几，就访问第几个服务，实现负载均衡。</p><p>例如：</p><ul><li>我们的请求路径是 /item/10001</li><li>tomcat总数为2台（8081、8082）</li><li>对请求路径/item/1001做hash运算求余的结果为1</li><li>则访问第一个tomcat服务，也就是8081</li></ul><p>只要id不变，每次hash运算结果也不会变，那就可以保证同一个商品，一直访问同一个tomcat服务，确保JVM缓存生效。</p><h4 id="2）实现">2）实现</h4><p>修改<code>/usr/local/openresty/nginx/conf/nginx.conf</code>文件，实现基于ID做负载均衡。</p><p>首先，定义tomcat集群，并设置基于路径做负载均衡：</p><figure class="highlight nginx"><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="section">upstream</span> tomcat-cluster &#123;</span><br><span class="line">    <span class="attribute">hash</span> <span class="variable">$request_uri</span>;</span><br><span class="line">    <span class="attribute">server</span> <span class="number">192.168.150.1:8081</span>;</span><br><span class="line">    <span class="attribute">server</span> <span class="number">192.168.150.1:8082</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>然后，修改对tomcat服务的反向代理，目标指向tomcat集群：</p><figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">location</span> /item &#123;</span><br><span class="line">    <span class="attribute">proxy_pass</span> http://tomcat-cluster;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>重新加载OpenResty</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">nginx -s reload</span><br></pre></td></tr></table></figure><h4 id="3）测试">3）测试</h4><p>启动两台tomcat服务：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210821112420464.png" alt="image-20210821112420464"></p><p>同时启动：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210821112444482.png" alt="image-20210821112444482"></p><p>清空日志后，再次访问页面，可以看到不同id的商品，访问到了不同的tomcat服务：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210821112559965.png" alt="image-20210821112559965"></p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210821112637430.png" alt="image-20210821112637430"></p><h2 id="4-5-Redis缓存预热">4.5.Redis缓存预热</h2><p>Redis缓存会面临冷启动问题：</p><p><strong>冷启动</strong>：服务刚刚启动时，Redis中并没有缓存，如果所有商品数据都在第一次查询时添加缓存，可能会给数据库带来较大压力。</p><p><strong>缓存预热</strong>：在实际开发中，我们可以利用大数据统计用户访问的热点数据，在项目启动时将这些热点数据提前查询并保存到Redis中。</p><p>我们数据量较少，并且没有数据统计相关功能，目前可以在启动时将所有数据都放入缓存中。</p><p>1）利用Docker安装Redis</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker run --name redis -p 6379:6379 -d redis redis-server --appendonly <span class="built_in">yes</span></span><br></pre></td></tr></table></figure><p>2）在item-service服务中引入Redis依赖</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></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.springframework.boot<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-boot-starter-data-redis<span class="tag">&lt;/<span class="name">artifactId</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>3）配置Redis地址</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">spring:</span></span><br><span class="line">  <span class="attr">redis:</span></span><br><span class="line">    <span class="attr">host:</span> <span class="number">192.168</span><span class="number">.150</span><span class="number">.101</span></span><br></pre></td></tr></table></figure><p>4）编写初始化类</p><p>缓存预热需要在项目启动时完成，并且必须是拿到RedisTemplate之后。</p><p>这里我们利用InitializingBean接口来实现，因为InitializingBean可以在对象被Spring创建并且成员变量全部注入后执行。</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.heima.item.config;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> com.fasterxml.jackson.core.JsonProcessingException;</span><br><span class="line"><span class="keyword">import</span> com.fasterxml.jackson.databind.ObjectMapper;</span><br><span class="line"><span class="keyword">import</span> com.heima.item.pojo.Item;</span><br><span class="line"><span class="keyword">import</span> com.heima.item.pojo.ItemStock;</span><br><span class="line"><span class="keyword">import</span> com.heima.item.service.IItemService;</span><br><span class="line"><span class="keyword">import</span> com.heima.item.service.IItemStockService;</span><br><span class="line"><span class="keyword">import</span> org.springframework.beans.factory.InitializingBean;</span><br><span class="line"><span class="keyword">import</span> org.springframework.beans.factory.annotation.Autowired;</span><br><span class="line"><span class="keyword">import</span> org.springframework.data.redis.core.StringRedisTemplate;</span><br><span class="line"><span class="keyword">import</span> org.springframework.stereotype.Component;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> java.util.List;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">RedisHandler</span> <span class="keyword">implements</span> <span class="title class_">InitializingBean</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> StringRedisTemplate redisTemplate;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> IItemService itemService;</span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> IItemStockService stockService;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">ObjectMapper</span> <span class="variable">MAPPER</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ObjectMapper</span>();</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_">afterPropertiesSet</span><span class="params">()</span> <span class="keyword">throws</span> Exception &#123;</span><br><span class="line">        <span class="comment">// 初始化缓存</span></span><br><span class="line">        <span class="comment">// 1.查询商品信息</span></span><br><span class="line">        List&lt;Item&gt; itemList = itemService.list();</span><br><span class="line">        <span class="comment">// 2.放入缓存</span></span><br><span class="line">        <span class="keyword">for</span> (Item item : itemList) &#123;</span><br><span class="line">            <span class="comment">// 2.1.item序列化为JSON</span></span><br><span class="line">            <span class="type">String</span> <span class="variable">json</span> <span class="operator">=</span> MAPPER.writeValueAsString(item);</span><br><span class="line">            <span class="comment">// 2.2.存入redis</span></span><br><span class="line">            redisTemplate.opsForValue().set(<span class="string">&quot;item:id:&quot;</span> + item.getId(), json);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 3.查询商品库存信息</span></span><br><span class="line">        List&lt;ItemStock&gt; stockList = stockService.list();</span><br><span class="line">        <span class="comment">// 4.放入缓存</span></span><br><span class="line">        <span class="keyword">for</span> (ItemStock stock : stockList) &#123;</span><br><span class="line">            <span class="comment">// 2.1.item序列化为JSON</span></span><br><span class="line">            <span class="type">String</span> <span class="variable">json</span> <span class="operator">=</span> MAPPER.writeValueAsString(stock);</span><br><span class="line">            <span class="comment">// 2.2.存入redis</span></span><br><span class="line">            redisTemplate.opsForValue().set(<span class="string">&quot;item:stock:id:&quot;</span> + stock.getId(), json);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="4-6-查询Redis缓存">4.6.查询Redis缓存</h2><p>现在，Redis缓存已经准备就绪，我们可以再OpenResty中实现查询Redis的逻辑了。如下图红框所示：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210821113340111.png" alt="image-20210821113340111"></p><p>当请求进入OpenResty之后：</p><ul><li>优先查询Redis缓存</li><li>如果Redis缓存未命中，再查询Tomcat</li></ul><h3 id="4-6-1-封装Redis工具">4.6.1.封装Redis工具</h3><p>OpenResty提供了操作Redis的模块，我们只要引入该模块就能直接使用。但是为了方便，我们将Redis操作封装到之前的common.lua工具库中。</p><p>修改<code>/usr/local/openresty/lualib/common.lua</code>文件：</p><p>1）引入Redis模块，并初始化Redis对象</p><figure class="highlight lua"><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="comment">-- 导入redis</span></span><br><span class="line"><span class="keyword">local</span> redis = <span class="built_in">require</span>(<span class="string">&#x27;resty.redis&#x27;</span>)</span><br><span class="line"><span class="comment">-- 初始化redis</span></span><br><span class="line"><span class="keyword">local</span> red = redis:new()</span><br><span class="line">red:set_timeouts(<span class="number">1000</span>, <span class="number">1000</span>, <span class="number">1000</span>)</span><br></pre></td></tr></table></figure><p>2）封装函数，用来释放Redis连接，其实是放入连接池</p><figure class="highlight lua"><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"><span class="comment">-- 关闭redis连接的工具方法，其实是放入连接池</span></span><br><span class="line"><span class="keyword">local</span> <span class="function"><span class="keyword">function</span> <span class="title">close_redis</span><span class="params">(red)</span></span></span><br><span class="line">    <span class="keyword">local</span> pool_max_idle_time = <span class="number">10000</span> <span class="comment">-- 连接的空闲时间，单位是毫秒</span></span><br><span class="line">    <span class="keyword">local</span> pool_size = <span class="number">100</span> <span class="comment">--连接池大小</span></span><br><span class="line">    <span class="keyword">local</span> ok, err = red:set_keepalive(pool_max_idle_time, pool_size)</span><br><span class="line">    <span class="keyword">if</span> <span class="keyword">not</span> ok <span class="keyword">then</span></span><br><span class="line">        ngx.<span class="built_in">log</span>(ngx.ERR, <span class="string">&quot;放入redis连接池失败: &quot;</span>, err)</span><br><span class="line">    <span class="keyword">end</span></span><br><span class="line"><span class="keyword">end</span></span><br></pre></td></tr></table></figure><p>3）封装函数，根据key查询Redis数据</p><figure class="highlight lua"><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="comment">-- 查询redis的方法 ip和port是redis地址，key是查询的key</span></span><br><span class="line"><span class="keyword">local</span> <span class="function"><span class="keyword">function</span> <span class="title">read_redis</span><span class="params">(ip, port, key)</span></span></span><br><span class="line">    <span class="comment">-- 获取一个连接</span></span><br><span class="line">    <span class="keyword">local</span> ok, err = red:connect(ip, port)</span><br><span class="line">    <span class="keyword">if</span> <span class="keyword">not</span> ok <span class="keyword">then</span></span><br><span class="line">        ngx.<span class="built_in">log</span>(ngx.ERR, <span class="string">&quot;连接redis失败 : &quot;</span>, err)</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line">    <span class="keyword">end</span></span><br><span class="line">    <span class="comment">-- 查询redis</span></span><br><span class="line">    <span class="keyword">local</span> resp, err = red:get(key)</span><br><span class="line">    <span class="comment">-- 查询失败处理</span></span><br><span class="line">    <span class="keyword">if</span> <span class="keyword">not</span> resp <span class="keyword">then</span></span><br><span class="line">        ngx.<span class="built_in">log</span>(ngx.ERR, <span class="string">&quot;查询Redis失败: &quot;</span>, err, <span class="string">&quot;, key = &quot;</span> , key)</span><br><span class="line">    <span class="keyword">end</span></span><br><span class="line">    <span class="comment">--得到的数据为空处理</span></span><br><span class="line">    <span class="keyword">if</span> resp == ngx.null <span class="keyword">then</span></span><br><span class="line">        resp = <span class="literal">nil</span></span><br><span class="line">        ngx.<span class="built_in">log</span>(ngx.ERR, <span class="string">&quot;查询Redis数据为空, key = &quot;</span>, key)</span><br><span class="line">    <span class="keyword">end</span></span><br><span class="line">    close_redis(red)</span><br><span class="line">    <span class="keyword">return</span> resp</span><br><span class="line"><span class="keyword">end</span></span><br></pre></td></tr></table></figure><p>4）导出</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 将方法导出</span></span><br><span class="line"><span class="keyword">local</span> _M = &#123;  </span><br><span class="line">    read_http = read_http,</span><br><span class="line">    read_redis = read_redis</span><br><span class="line">&#125;  </span><br><span class="line"><span class="keyword">return</span> _M</span><br></pre></td></tr></table></figure><p>完整的common.lua：</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 导入redis</span></span><br><span class="line"><span class="keyword">local</span> redis = <span class="built_in">require</span>(<span class="string">&#x27;resty.redis&#x27;</span>)</span><br><span class="line"><span class="comment">-- 初始化redis</span></span><br><span class="line"><span class="keyword">local</span> red = redis:new()</span><br><span class="line">red:set_timeouts(<span class="number">1000</span>, <span class="number">1000</span>, <span class="number">1000</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 关闭redis连接的工具方法，其实是放入连接池</span></span><br><span class="line"><span class="keyword">local</span> <span class="function"><span class="keyword">function</span> <span class="title">close_redis</span><span class="params">(red)</span></span></span><br><span class="line">    <span class="keyword">local</span> pool_max_idle_time = <span class="number">10000</span> <span class="comment">-- 连接的空闲时间，单位是毫秒</span></span><br><span class="line">    <span class="keyword">local</span> pool_size = <span class="number">100</span> <span class="comment">--连接池大小</span></span><br><span class="line">    <span class="keyword">local</span> ok, err = red:set_keepalive(pool_max_idle_time, pool_size)</span><br><span class="line">    <span class="keyword">if</span> <span class="keyword">not</span> ok <span class="keyword">then</span></span><br><span class="line">        ngx.<span class="built_in">log</span>(ngx.ERR, <span class="string">&quot;放入redis连接池失败: &quot;</span>, err)</span><br><span class="line">    <span class="keyword">end</span></span><br><span class="line"><span class="keyword">end</span></span><br><span class="line"></span><br><span class="line"><span class="comment">-- 查询redis的方法 ip和port是redis地址，key是查询的key</span></span><br><span class="line"><span class="keyword">local</span> <span class="function"><span class="keyword">function</span> <span class="title">read_redis</span><span class="params">(ip, port, key)</span></span></span><br><span class="line">    <span class="comment">-- 获取一个连接</span></span><br><span class="line">    <span class="keyword">local</span> ok, err = red:connect(ip, port)</span><br><span class="line">    <span class="keyword">if</span> <span class="keyword">not</span> ok <span class="keyword">then</span></span><br><span class="line">        ngx.<span class="built_in">log</span>(ngx.ERR, <span class="string">&quot;连接redis失败 : &quot;</span>, err)</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line">    <span class="keyword">end</span></span><br><span class="line">    <span class="comment">-- 查询redis</span></span><br><span class="line">    <span class="keyword">local</span> resp, err = red:get(key)</span><br><span class="line">    <span class="comment">-- 查询失败处理</span></span><br><span class="line">    <span class="keyword">if</span> <span class="keyword">not</span> resp <span class="keyword">then</span></span><br><span class="line">        ngx.<span class="built_in">log</span>(ngx.ERR, <span class="string">&quot;查询Redis失败: &quot;</span>, err, <span class="string">&quot;, key = &quot;</span> , key)</span><br><span class="line">    <span class="keyword">end</span></span><br><span class="line">    <span class="comment">--得到的数据为空处理</span></span><br><span class="line">    <span class="keyword">if</span> resp == ngx.null <span class="keyword">then</span></span><br><span class="line">        resp = <span class="literal">nil</span></span><br><span class="line">        ngx.<span class="built_in">log</span>(ngx.ERR, <span class="string">&quot;查询Redis数据为空, key = &quot;</span>, key)</span><br><span class="line">    <span class="keyword">end</span></span><br><span class="line">    close_redis(red)</span><br><span class="line">    <span class="keyword">return</span> resp</span><br><span class="line"><span class="keyword">end</span></span><br><span class="line"></span><br><span class="line"><span class="comment">-- 封装函数，发送http请求，并解析响应</span></span><br><span class="line"><span class="keyword">local</span> <span class="function"><span class="keyword">function</span> <span class="title">read_http</span><span class="params">(path, params)</span></span></span><br><span class="line">    <span class="keyword">local</span> resp = ngx.location.capture(<span class="built_in">path</span>,&#123;</span><br><span class="line">        method = ngx.HTTP_GET,</span><br><span class="line">        args = params,</span><br><span class="line">    &#125;)</span><br><span class="line">    <span class="keyword">if</span> <span class="keyword">not</span> resp <span class="keyword">then</span></span><br><span class="line">        <span class="comment">-- 记录错误信息，返回404</span></span><br><span class="line">        ngx.<span class="built_in">log</span>(ngx.ERR, <span class="string">&quot;http查询失败, path: &quot;</span>, <span class="built_in">path</span> , <span class="string">&quot;, args: &quot;</span>, args)</span><br><span class="line">        ngx.<span class="built_in">exit</span>(<span class="number">404</span>)</span><br><span class="line">    <span class="keyword">end</span></span><br><span class="line">    <span class="keyword">return</span> resp.body</span><br><span class="line"><span class="keyword">end</span></span><br><span class="line"><span class="comment">-- 将方法导出</span></span><br><span class="line"><span class="keyword">local</span> _M = &#123;  </span><br><span class="line">    read_http = read_http,</span><br><span class="line">    read_redis = read_redis</span><br><span class="line">&#125;  </span><br><span class="line"><span class="keyword">return</span> _M</span><br></pre></td></tr></table></figure><h3 id="4-6-2-实现Redis查询">4.6.2.实现Redis查询</h3><p>接下来，我们就可以去修改item.lua文件，实现对Redis的查询了。</p><p>查询逻辑是：</p><ul><li>根据id查询Redis</li><li>如果查询失败则继续查询Tomcat</li><li>将查询结果返回</li></ul><p>1）修改<code>/usr/local/openresty/lua/item.lua</code>文件，添加一个查询函数：</p><figure class="highlight lua"><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"><span class="comment">-- 导入common函数库</span></span><br><span class="line"><span class="keyword">local</span> common = <span class="built_in">require</span>(<span class="string">&#x27;common&#x27;</span>)</span><br><span class="line"><span class="keyword">local</span> read_http = common.read_http</span><br><span class="line"><span class="keyword">local</span> read_redis = common.read_redis</span><br><span class="line"><span class="comment">-- 封装查询函数</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">read_data</span><span class="params">(key, path, params)</span></span></span><br><span class="line">    <span class="comment">-- 查询本地缓存</span></span><br><span class="line">    <span class="keyword">local</span> val = read_redis(<span class="string">&quot;127.0.0.1&quot;</span>, <span class="number">6379</span>, key)</span><br><span class="line">    <span class="comment">-- 判断查询结果</span></span><br><span class="line">    <span class="keyword">if</span> <span class="keyword">not</span> val <span class="keyword">then</span></span><br><span class="line">        ngx.<span class="built_in">log</span>(ngx.ERR, <span class="string">&quot;redis查询失败，尝试查询http， key: &quot;</span>, key)</span><br><span class="line">        <span class="comment">-- redis查询失败，去查询http</span></span><br><span class="line">        val = read_http(<span class="built_in">path</span>, params)</span><br><span class="line">    <span class="keyword">end</span></span><br><span class="line">    <span class="comment">-- 返回数据</span></span><br><span class="line">    <span class="keyword">return</span> val</span><br><span class="line"><span class="keyword">end</span></span><br></pre></td></tr></table></figure><p>2）而后修改商品查询、库存查询的业务：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210821114528954.png" alt="image-20210821114528954"></p><p>3）完整的item.lua代码：</p><figure class="highlight lua"><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></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 导入common函数库</span></span><br><span class="line"><span class="keyword">local</span> common = <span class="built_in">require</span>(<span class="string">&#x27;common&#x27;</span>)</span><br><span class="line"><span class="keyword">local</span> read_http = common.read_http</span><br><span class="line"><span class="keyword">local</span> read_redis = common.read_redis</span><br><span class="line"><span class="comment">-- 导入cjson库</span></span><br><span class="line"><span class="keyword">local</span> cjson = <span class="built_in">require</span>(<span class="string">&#x27;cjson&#x27;</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 封装查询函数</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">read_data</span><span class="params">(key, path, params)</span></span></span><br><span class="line">    <span class="comment">-- 查询本地缓存</span></span><br><span class="line">    <span class="keyword">local</span> val = read_redis(<span class="string">&quot;127.0.0.1&quot;</span>, <span class="number">6379</span>, key)</span><br><span class="line">    <span class="comment">-- 判断查询结果</span></span><br><span class="line">    <span class="keyword">if</span> <span class="keyword">not</span> val <span class="keyword">then</span></span><br><span class="line">        ngx.<span class="built_in">log</span>(ngx.ERR, <span class="string">&quot;redis查询失败，尝试查询http， key: &quot;</span>, key)</span><br><span class="line">        <span class="comment">-- redis查询失败，去查询http</span></span><br><span class="line">        val = read_http(<span class="built_in">path</span>, params)</span><br><span class="line">    <span class="keyword">end</span></span><br><span class="line">    <span class="comment">-- 返回数据</span></span><br><span class="line">    <span class="keyword">return</span> val</span><br><span class="line"><span class="keyword">end</span></span><br><span class="line"></span><br><span class="line"><span class="comment">-- 获取路径参数</span></span><br><span class="line"><span class="keyword">local</span> id = ngx.var[<span class="number">1</span>]</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 查询商品信息</span></span><br><span class="line"><span class="keyword">local</span> itemJSON = read_data(<span class="string">&quot;item:id:&quot;</span> .. id,  <span class="string">&quot;/item/&quot;</span> .. id, <span class="literal">nil</span>)</span><br><span class="line"><span class="comment">-- 查询库存信息</span></span><br><span class="line"><span class="keyword">local</span> stockJSON = read_data(<span class="string">&quot;item:stock:id:&quot;</span> .. id, <span class="string">&quot;/item/stock/&quot;</span> .. id, <span class="literal">nil</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment">-- JSON转化为lua的table</span></span><br><span class="line"><span class="keyword">local</span> item = cjson.decode(itemJSON)</span><br><span class="line"><span class="keyword">local</span> stock = cjson.decode(stockJSON)</span><br><span class="line"><span class="comment">-- 组合数据</span></span><br><span class="line">item.stock = stock.stock</span><br><span class="line">item.sold = stock.sold</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 把item序列化为json 返回结果</span></span><br><span class="line">ngx.say(cjson.encode(item))</span><br></pre></td></tr></table></figure><h2 id="4-7-Nginx本地缓存">4.7.Nginx本地缓存</h2><p>现在，整个多级缓存中只差最后一环，也就是nginx的本地缓存了。如图：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210821114742950.png" alt="image-20210821114742950"></p><h3 id="4-7-1-本地缓存API">4.7.1.本地缓存API</h3><p>OpenResty为Nginx提供了<strong>shard dict</strong>的功能，可以在nginx的多个worker之间共享数据，实现缓存功能。</p><p>1）开启共享字典，在nginx.conf的http下添加配置：</p><figure class="highlight nginx"><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="comment"># 共享字典，也就是本地缓存，名称叫做：item_cache，大小150m</span></span><br><span class="line"><span class="attribute">lua_shared_dict</span> item_cache <span class="number">150m</span>; </span><br></pre></td></tr></table></figure><p>2）操作共享字典：</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 获取本地缓存对象</span></span><br><span class="line"><span class="keyword">local</span> item_cache = ngx.shared.item_cache</span><br><span class="line"><span class="comment">-- 存储, 指定key、value、过期时间，单位s，默认为0代表永不过期</span></span><br><span class="line">item_cache:set(<span class="string">&#x27;key&#x27;</span>, <span class="string">&#x27;value&#x27;</span>, <span class="number">1000</span>)</span><br><span class="line"><span class="comment">-- 读取</span></span><br><span class="line"><span class="keyword">local</span> val = item_cache:get(<span class="string">&#x27;key&#x27;</span>)</span><br></pre></td></tr></table></figure><h3 id="4-7-2-实现本地缓存查询">4.7.2.实现本地缓存查询</h3><p>1）修改<code>/usr/local/openresty/lua/item.lua</code>文件，修改read_data查询函数，添加本地缓存逻辑：</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 导入共享词典，本地缓存</span></span><br><span class="line"><span class="keyword">local</span> item_cache = ngx.shared.item_cache</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 封装查询函数</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">read_data</span><span class="params">(key, expire, path, params)</span></span></span><br><span class="line">    <span class="comment">-- 查询本地缓存</span></span><br><span class="line">    <span class="keyword">local</span> val = item_cache:get(key)</span><br><span class="line">    <span class="keyword">if</span> <span class="keyword">not</span> val <span class="keyword">then</span></span><br><span class="line">        ngx.<span class="built_in">log</span>(ngx.ERR, <span class="string">&quot;本地缓存查询失败，尝试查询Redis， key: &quot;</span>, key)</span><br><span class="line">        <span class="comment">-- 查询redis</span></span><br><span class="line">        val = read_redis(<span class="string">&quot;127.0.0.1&quot;</span>, <span class="number">6379</span>, key)</span><br><span class="line">        <span class="comment">-- 判断查询结果</span></span><br><span class="line">        <span class="keyword">if</span> <span class="keyword">not</span> val <span class="keyword">then</span></span><br><span class="line">            ngx.<span class="built_in">log</span>(ngx.ERR, <span class="string">&quot;redis查询失败，尝试查询http， key: &quot;</span>, key)</span><br><span class="line">            <span class="comment">-- redis查询失败，去查询http</span></span><br><span class="line">            val = read_http(<span class="built_in">path</span>, params)</span><br><span class="line">        <span class="keyword">end</span></span><br><span class="line">    <span class="keyword">end</span></span><br><span class="line">    <span class="comment">-- 查询成功，把数据写入本地缓存</span></span><br><span class="line">    item_cache:set(key, val, expire)</span><br><span class="line">    <span class="comment">-- 返回数据</span></span><br><span class="line">    <span class="keyword">return</span> val</span><br><span class="line"><span class="keyword">end</span></span><br></pre></td></tr></table></figure><p>2）修改item.lua中查询商品和库存的业务，实现最新的read_data函数：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210821115108528.png" alt="image-20210821115108528"></p><p>其实就是多了缓存时间参数，过期后nginx缓存会自动删除，下次访问即可更新缓存。</p><p>这里给商品基本信息设置超时时间为30分钟，库存为1分钟。</p><p>因为库存更新频率较高，如果缓存时间过长，可能与数据库差异较大。</p><p>3）完整的item.lua文件：</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 导入common函数库</span></span><br><span class="line"><span class="keyword">local</span> common = <span class="built_in">require</span>(<span class="string">&#x27;common&#x27;</span>)</span><br><span class="line"><span class="keyword">local</span> read_http = common.read_http</span><br><span class="line"><span class="keyword">local</span> read_redis = common.read_redis</span><br><span class="line"><span class="comment">-- 导入cjson库</span></span><br><span class="line"><span class="keyword">local</span> cjson = <span class="built_in">require</span>(<span class="string">&#x27;cjson&#x27;</span>)</span><br><span class="line"><span class="comment">-- 导入共享词典，本地缓存</span></span><br><span class="line"><span class="keyword">local</span> item_cache = ngx.shared.item_cache</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 封装查询函数</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">read_data</span><span class="params">(key, expire, path, params)</span></span></span><br><span class="line">    <span class="comment">-- 查询本地缓存</span></span><br><span class="line">    <span class="keyword">local</span> val = item_cache:get(key)</span><br><span class="line">    <span class="keyword">if</span> <span class="keyword">not</span> val <span class="keyword">then</span></span><br><span class="line">        ngx.<span class="built_in">log</span>(ngx.ERR, <span class="string">&quot;本地缓存查询失败，尝试查询Redis， key: &quot;</span>, key)</span><br><span class="line">        <span class="comment">-- 查询redis</span></span><br><span class="line">        val = read_redis(<span class="string">&quot;127.0.0.1&quot;</span>, <span class="number">6379</span>, key)</span><br><span class="line">        <span class="comment">-- 判断查询结果</span></span><br><span class="line">        <span class="keyword">if</span> <span class="keyword">not</span> val <span class="keyword">then</span></span><br><span class="line">            ngx.<span class="built_in">log</span>(ngx.ERR, <span class="string">&quot;redis查询失败，尝试查询http， key: &quot;</span>, key)</span><br><span class="line">            <span class="comment">-- redis查询失败，去查询http</span></span><br><span class="line">            val = read_http(<span class="built_in">path</span>, params)</span><br><span class="line">        <span class="keyword">end</span></span><br><span class="line">    <span class="keyword">end</span></span><br><span class="line">    <span class="comment">-- 查询成功，把数据写入本地缓存</span></span><br><span class="line">    item_cache:set(key, val, expire)</span><br><span class="line">    <span class="comment">-- 返回数据</span></span><br><span class="line">    <span class="keyword">return</span> val</span><br><span class="line"><span class="keyword">end</span></span><br><span class="line"></span><br><span class="line"><span class="comment">-- 获取路径参数</span></span><br><span class="line"><span class="keyword">local</span> id = ngx.var[<span class="number">1</span>]</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 查询商品信息</span></span><br><span class="line"><span class="keyword">local</span> itemJSON = read_data(<span class="string">&quot;item:id:&quot;</span> .. id, <span class="number">1800</span>,  <span class="string">&quot;/item/&quot;</span> .. id, <span class="literal">nil</span>)</span><br><span class="line"><span class="comment">-- 查询库存信息</span></span><br><span class="line"><span class="keyword">local</span> stockJSON = read_data(<span class="string">&quot;item:stock:id:&quot;</span> .. id, <span class="number">60</span>, <span class="string">&quot;/item/stock/&quot;</span> .. id, <span class="literal">nil</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment">-- JSON转化为lua的table</span></span><br><span class="line"><span class="keyword">local</span> item = cjson.decode(itemJSON)</span><br><span class="line"><span class="keyword">local</span> stock = cjson.decode(stockJSON)</span><br><span class="line"><span class="comment">-- 组合数据</span></span><br><span class="line">item.stock = stock.stock</span><br><span class="line">item.sold = stock.sold</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 把item序列化为json 返回结果</span></span><br><span class="line">ngx.say(cjson.encode(item))</span><br></pre></td></tr></table></figure><h1>5.缓存同步</h1><p>大多数情况下，浏览器查询到的都是缓存数据，如果缓存数据与数据库数据存在较大差异，可能会产生比较严重的后果。</p><p>所以我们必须保证数据库数据、缓存数据的一致性，这就是缓存与数据库的同步。</p><h2 id="5-1-数据同步策略">5.1.数据同步策略</h2><p>缓存数据同步的常见方式有三种：</p><p><strong>设置有效期</strong>：给缓存设置有效期，到期后自动删除。再次查询时更新</p><ul><li>优势：简单、方便</li><li>缺点：时效性差，缓存过期之前可能不一致</li><li>场景：更新频率较低，时效性要求低的业务</li></ul><p><strong>同步双写</strong>：在修改数据库的同时，直接修改缓存</p><ul><li>优势：时效性强，缓存与数据库强一致</li><li>缺点：有代码侵入，耦合度高；</li><li>场景：对一致性、时效性要求较高的缓存数据</li></ul><p>**异步通知：**修改数据库时发送事件通知，相关服务监听到通知后修改缓存数据</p><ul><li>优势：低耦合，可以同时通知多个缓存服务</li><li>缺点：时效性一般，可能存在中间不一致状态</li><li>场景：时效性要求一般，有多个服务需要同步</li></ul><p>而异步实现又可以基于MQ或者Canal来实现：</p><p>1）基于MQ的异步通知：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210821115552327.png" alt="image-20210821115552327"></p><p>解读：</p><ul><li>商品服务完成对数据的修改后，只需要发送一条消息到MQ中。</li><li>缓存服务监听MQ消息，然后完成对缓存的更新</li></ul><p>依然有少量的代码侵入。</p><p>2）基于Canal的通知</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210821115719363.png" alt="image-20210821115719363"></p><p>解读：</p><ul><li>商品服务完成商品修改后，业务直接结束，没有任何代码侵入</li><li>Canal监听MySQL变化，当发现变化后，立即通知缓存服务</li><li>缓存服务接收到canal通知，更新缓存</li></ul><p>代码零侵入</p><h2 id="5-2-安装Canal">5.2.安装Canal</h2><h3 id="5-2-1-认识Canal">5.2.1.认识Canal</h3><p><strong>Canal [kə’næl]</strong>，译意为水道/管道/沟渠，canal是阿里巴巴旗下的一款开源项目，基于Java开发。基于数据库增量日志解析，提供增量数据订阅&amp;消费。GitHub的地址：<a href="https://github.com/alibaba/canal">https://github.com/alibaba/canal</a></p><p>Canal是基于mysql的主从同步来实现的，MySQL主从同步的原理如下：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210821115914748.png" alt="image-20210821115914748"></p><ul><li>1）MySQL master 将数据变更写入二进制日志( binary log），其中记录的数据叫做binary log events</li><li>2）MySQL slave 将 master 的 binary log events拷贝到它的中继日志(relay log)</li><li>3）MySQL slave 重放 relay log 中事件，将数据变更反映它自己的数据</li></ul><p>而Canal就是把自己伪装成MySQL的一个slave节点，从而监听master的binary log变化。再把得到的变化信息通知给Canal的客户端，进而完成对其它数据库的同步。</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210821115948395.png" alt="image-20210821115948395"></p><h3 id="5-2-2-安装Canal">5.2.2.安装Canal</h3><p>安装和配置Canal参考课前资料文档：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210821120017324.png" alt="image-20210821120017324"></p><h2 id="5-3-监听Canal">5.3.监听Canal</h2><p>Canal提供了各种语言的客户端，当Canal监听到binlog变化时，会通知Canal的客户端。</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210821120049024.png" alt="image-20210821120049024"></p><p>我们可以利用Canal提供的Java客户端，监听Canal通知消息。当收到变化的消息时，完成对缓存的更新。</p><p>不过这里我们会使用GitHub上的第三方开源的canal-starter客户端。地址：<a href="https://github.com/NormanGyllenhaal/canal-client">https://github.com/NormanGyllenhaal/canal-client</a></p><p>与SpringBoot完美整合，自动装配，比官方客户端要简单好用很多。</p><h3 id="5-3-1-引入依赖：">5.3.1.引入依赖：</h3><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>top.javatool<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>canal-spring-boot-starter<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">version</span>&gt;</span>1.2.1-RELEASE<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure><h3 id="5-3-2-编写配置：">5.3.2.编写配置：</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">canal:</span></span><br><span class="line">  <span class="attr">destination:</span> <span class="string">heima</span> <span class="comment"># canal的集群名字，要与安装canal时设置的名称一致</span></span><br><span class="line">  <span class="attr">server:</span> <span class="number">192.168</span><span class="number">.150</span><span class="number">.101</span><span class="string">:11111</span> <span class="comment"># canal服务地址</span></span><br></pre></td></tr></table></figure><h3 id="5-3-3-修改Item实体类">5.3.3.修改Item实体类</h3><p>通过@Id、@Column、等注解完成Item与数据库表字段的映射：</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.heima.item.pojo;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> com.baomidou.mybatisplus.annotation.IdType;</span><br><span class="line"><span class="keyword">import</span> com.baomidou.mybatisplus.annotation.TableField;</span><br><span class="line"><span class="keyword">import</span> com.baomidou.mybatisplus.annotation.TableId;</span><br><span class="line"><span class="keyword">import</span> com.baomidou.mybatisplus.annotation.TableName;</span><br><span class="line"><span class="keyword">import</span> lombok.Data;</span><br><span class="line"><span class="keyword">import</span> org.springframework.data.annotation.Id;</span><br><span class="line"><span class="keyword">import</span> org.springframework.data.annotation.Transient;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> javax.persistence.Column;</span><br><span class="line"><span class="keyword">import</span> java.util.Date;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Data</span></span><br><span class="line"><span class="meta">@TableName(&quot;tb_item&quot;)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Item</span> &#123;</span><br><span class="line">    <span class="meta">@TableId(type = IdType.AUTO)</span></span><br><span class="line">    <span class="meta">@Id</span></span><br><span class="line">    <span class="keyword">private</span> Long id;<span class="comment">//商品id</span></span><br><span class="line">    <span class="meta">@Column(name = &quot;name&quot;)</span></span><br><span class="line">    <span class="keyword">private</span> String name;<span class="comment">//商品名称</span></span><br><span class="line">    <span class="keyword">private</span> String title;<span class="comment">//商品标题</span></span><br><span class="line">    <span class="keyword">private</span> Long price;<span class="comment">//价格（分）</span></span><br><span class="line">    <span class="keyword">private</span> String image;<span class="comment">//商品图片</span></span><br><span class="line">    <span class="keyword">private</span> String category;<span class="comment">//分类名称</span></span><br><span class="line">    <span class="keyword">private</span> String brand;<span class="comment">//品牌名称</span></span><br><span class="line">    <span class="keyword">private</span> String spec;<span class="comment">//规格</span></span><br><span class="line">    <span class="keyword">private</span> Integer status;<span class="comment">//商品状态 1-正常，2-下架</span></span><br><span class="line">    <span class="keyword">private</span> Date createTime;<span class="comment">//创建时间</span></span><br><span class="line">    <span class="keyword">private</span> Date updateTime;<span class="comment">//更新时间</span></span><br><span class="line">    <span class="meta">@TableField(exist = false)</span></span><br><span class="line">    <span class="meta">@Transient</span></span><br><span class="line">    <span class="keyword">private</span> Integer stock;</span><br><span class="line">    <span class="meta">@TableField(exist = false)</span></span><br><span class="line">    <span class="meta">@Transient</span></span><br><span class="line">    <span class="keyword">private</span> Integer sold;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="5-3-4-编写监听器">5.3.4.编写监听器</h3><p>通过实现<code>EntryHandler&lt;T&gt;</code>接口编写监听器，监听Canal消息。注意两点：</p><ul><li>实现类通过<code>@CanalTable(&quot;tb_item&quot;)</code>指定监听的表信息</li><li>EntryHandler的泛型是与表对应的实体类</li></ul><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.heima.item.canal;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> com.github.benmanes.caffeine.cache.Cache;</span><br><span class="line"><span class="keyword">import</span> com.heima.item.config.RedisHandler;</span><br><span class="line"><span class="keyword">import</span> com.heima.item.pojo.Item;</span><br><span class="line"><span class="keyword">import</span> org.springframework.beans.factory.annotation.Autowired;</span><br><span class="line"><span class="keyword">import</span> org.springframework.stereotype.Component;</span><br><span class="line"><span class="keyword">import</span> top.javatool.canal.client.annotation.CanalTable;</span><br><span class="line"><span class="keyword">import</span> top.javatool.canal.client.handler.EntryHandler;</span><br><span class="line"></span><br><span class="line"><span class="meta">@CanalTable(&quot;tb_item&quot;)</span></span><br><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">ItemHandler</span> <span class="keyword">implements</span> <span class="title class_">EntryHandler</span>&lt;Item&gt; &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> RedisHandler redisHandler;</span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> Cache&lt;Long, Item&gt; itemCache;</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_">insert</span><span class="params">(Item item)</span> &#123;</span><br><span class="line">        <span class="comment">// 写数据到JVM进程缓存</span></span><br><span class="line">        itemCache.put(item.getId(), item);</span><br><span class="line">        <span class="comment">// 写数据到redis</span></span><br><span class="line">        redisHandler.saveItem(item);</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_">update</span><span class="params">(Item before, Item after)</span> &#123;</span><br><span class="line">        <span class="comment">// 写数据到JVM进程缓存</span></span><br><span class="line">        itemCache.put(after.getId(), after);</span><br><span class="line">        <span class="comment">// 写数据到redis</span></span><br><span class="line">        redisHandler.saveItem(after);</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_">delete</span><span class="params">(Item item)</span> &#123;</span><br><span class="line">        <span class="comment">// 删除数据到JVM进程缓存</span></span><br><span class="line">        itemCache.invalidate(item.getId());</span><br><span class="line">        <span class="comment">// 删除数据到redis</span></span><br><span class="line">        redisHandler.deleteItemById(item.getId());</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在这里对Redis的操作都封装到了RedisHandler这个对象中，是我们之前做缓存预热时编写的一个类，内容如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.heima.item.config;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> com.fasterxml.jackson.core.JsonProcessingException;</span><br><span class="line"><span class="keyword">import</span> com.fasterxml.jackson.databind.ObjectMapper;</span><br><span class="line"><span class="keyword">import</span> com.heima.item.pojo.Item;</span><br><span class="line"><span class="keyword">import</span> com.heima.item.pojo.ItemStock;</span><br><span class="line"><span class="keyword">import</span> com.heima.item.service.IItemService;</span><br><span class="line"><span class="keyword">import</span> com.heima.item.service.IItemStockService;</span><br><span class="line"><span class="keyword">import</span> org.springframework.beans.factory.InitializingBean;</span><br><span class="line"><span class="keyword">import</span> org.springframework.beans.factory.annotation.Autowired;</span><br><span class="line"><span class="keyword">import</span> org.springframework.data.redis.core.StringRedisTemplate;</span><br><span class="line"><span class="keyword">import</span> org.springframework.stereotype.Component;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> java.util.List;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">RedisHandler</span> <span class="keyword">implements</span> <span class="title class_">InitializingBean</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> StringRedisTemplate redisTemplate;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> IItemService itemService;</span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> IItemStockService stockService;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">ObjectMapper</span> <span class="variable">MAPPER</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ObjectMapper</span>();</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_">afterPropertiesSet</span><span class="params">()</span> <span class="keyword">throws</span> Exception &#123;</span><br><span class="line">        <span class="comment">// 初始化缓存</span></span><br><span class="line">        <span class="comment">// 1.查询商品信息</span></span><br><span class="line">        List&lt;Item&gt; itemList = itemService.list();</span><br><span class="line">        <span class="comment">// 2.放入缓存</span></span><br><span class="line">        <span class="keyword">for</span> (Item item : itemList) &#123;</span><br><span class="line">            <span class="comment">// 2.1.item序列化为JSON</span></span><br><span class="line">            <span class="type">String</span> <span class="variable">json</span> <span class="operator">=</span> MAPPER.writeValueAsString(item);</span><br><span class="line">            <span class="comment">// 2.2.存入redis</span></span><br><span class="line">            redisTemplate.opsForValue().set(<span class="string">&quot;item:id:&quot;</span> + item.getId(), json);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 3.查询商品库存信息</span></span><br><span class="line">        List&lt;ItemStock&gt; stockList = stockService.list();</span><br><span class="line">        <span class="comment">// 4.放入缓存</span></span><br><span class="line">        <span class="keyword">for</span> (ItemStock stock : stockList) &#123;</span><br><span class="line">            <span class="comment">// 2.1.item序列化为JSON</span></span><br><span class="line">            <span class="type">String</span> <span class="variable">json</span> <span class="operator">=</span> MAPPER.writeValueAsString(stock);</span><br><span class="line">            <span class="comment">// 2.2.存入redis</span></span><br><span class="line">            redisTemplate.opsForValue().set(<span class="string">&quot;item:stock:id:&quot;</span> + stock.getId(), json);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">saveItem</span><span class="params">(Item item)</span> &#123;</span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            <span class="type">String</span> <span class="variable">json</span> <span class="operator">=</span> MAPPER.writeValueAsString(item);</span><br><span class="line">            redisTemplate.opsForValue().set(<span class="string">&quot;item:id:&quot;</span> + item.getId(), json);</span><br><span class="line">        &#125; <span class="keyword">catch</span> (JsonProcessingException e) &#123;</span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">RuntimeException</span>(e);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">deleteItemById</span><span class="params">(Long id)</span> &#123;</span><br><span class="line">        redisTemplate.delete(<span class="string">&quot;item:id:&quot;</span> + id);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>]]>
    </content>
    <id>https://keepstaytrue.github.io/2025/05/21/%E5%A4%9A%E7%BA%A7%E7%BC%93%E5%AD%98/</id>
    <link href="https://keepstaytrue.github.io/2025/05/21/%E5%A4%9A%E7%BA%A7%E7%BC%93%E5%AD%98/"/>
    <published>2025-05-21T06:00:00.000Z</published>
    <summary>
      <![CDATA[<h1>多级缓存</h1>
<h1>1.什么是多级缓存</h1>
<p>传统的缓存策略一般是请求到达Tomcat后，先查询Redis，如果未命中则查询数据库，如图：</p>
<p><img src="https://gcore.jsdelivr.net/gh/keepstaytr]]>
    </summary>
    <title>多级缓存</title>
    <updated>2026-05-23T13:34:43.583Z</updated>
  </entry>
  <entry>
    <author>
      <name>KeepStayTrue</name>
    </author>
    <category term="Java后端求职学习体系" scheme="https://keepstaytrue.github.io/categories/Java%E5%90%8E%E7%AB%AF%E6%B1%82%E8%81%8C%E5%AD%A6%E4%B9%A0%E4%BD%93%E7%B3%BB/"/>
    <category term="工具" scheme="https://keepstaytrue.github.io/categories/Java%E5%90%8E%E7%AB%AF%E6%B1%82%E8%81%8C%E5%AD%A6%E4%B9%A0%E4%BD%93%E7%B3%BB/%E5%B7%A5%E5%85%B7/"/>
    <category term="文档&amp;笔记工具" scheme="https://keepstaytrue.github.io/categories/Java%E5%90%8E%E7%AB%AF%E6%B1%82%E8%81%8C%E5%AD%A6%E4%B9%A0%E4%BD%93%E7%B3%BB/%E5%B7%A5%E5%85%B7/%E6%96%87%E6%A1%A3-%E7%AC%94%E8%AE%B0%E5%B7%A5%E5%85%B7/"/>
    <category term="Markdown" scheme="https://keepstaytrue.github.io/tags/Markdown/"/>
    <category term="typora" scheme="https://keepstaytrue.github.io/tags/typora/"/>
    <content>
      <![CDATA[<h1>typora学习手册</h1><h2 id="一-、概述">一 、概述</h2><h3 id="1、简介">1、简介</h3><p>typora是一款轻量级但功能强大的markdown编辑器，支持实时预览、多种格式导出、自定义主题等功能。可以用来记笔记，写技术类的文档。不仅仅适用于程序员，同时也是一款好用的码字、写材料的工具。</p><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ps：写小说也可以用哦</span><br></pre></td></tr></tbody></table><h3 id="2、下载及安装">2、下载及安装</h3><p>搜索typora中文官网：<a href="https://typoraio.cn/">Typora 官方中文站</a>可根据自身系统进行下载。有米的请支持正版，也可参照以下两个网站步骤进行安装（方法引至CSDN，感谢各位）。</p><ul><li>激活方法（目前版本1.9.5以前可用）：<a href="https://blog.csdn.net/qq_61621323/article/details/141036982">Typora 免费安装教程（已支持最新版 1.9.5）_typora免费安装-CSDN博客</a></li><li>不激活通过修改配置文件方法（各版本均可用）：<a href="https://blog.csdn.net/weixin_45320660/article/details/135482861">typora 序列号（不使用也可以激活）_typora离线激活-CSDN博客</a></li></ul><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ps:不缺这三瓜两枣的尽量购买</span><br></pre></td></tr></tbody></table><h2 id="二-、使用手册">二 、使用手册</h2><h3 id="1、基本设置">1、基本设置</h3><p>以下是typora中常见的设置，其余可以保持默认</p><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">#1 点开文件--&gt;偏好设置--&gt;文件选项--&gt;保存&amp;恢复中勾选“自动保存”</span><br><span class="line">#2 点开文件--&gt;偏好设置--&gt;markdown选项--&gt;markdown语法偏好--&gt;勾选“严格模式”</span><br><span class="line">#3 点开文件--&gt;偏好设置--&gt;markdown选项--&gt;markdown扩展语法--&gt;全部勾选</span><br></pre></td></tr></tbody></table><h3 id="2、文档编辑相关">2、文档编辑相关</h3><h4 id="2-1-标题">2.1 标题</h4><p>标题会在目录于大纲分级显示，可以跳转。在typora中开启严格模式，即不应为#标题，应为# 标题，设置方法如下：数个“#”+空格 前置</p><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"># 一级标题</span><br><span class="line">## 二级标题</span><br><span class="line">### 三级标题</span><br><span class="line">#### 四级标题</span><br><span class="line">##### 五级标题</span><br><span class="line">###### 六级标题</span><br></pre></td></tr></tbody></table><h4 id="2-2-强调">2.2 强调</h4><p>用“**”或“—”包围，或者选中要强调的文字按下ctrl+B.</p><table><tbody><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">#1  **我喜欢学习typora**</span><br><span class="line">#2  __我喜欢学习typora__</span><br></pre></td></tr></tbody></table><p>示例：</p><p><strong>我喜欢学习typora</strong></p><h4 id="2-3-斜体">2.3 斜体</h4><p>用“*”或“—”包围，或者选中文字按下ctrl+I.</p><table><tbody><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">#1 *我喜欢学习typora*</span><br><span class="line">#2 _我喜欢学习typora_</span><br></pre></td></tr></tbody></table><p>示例：</p><p><em>我喜欢学习typora</em></p><p><em><strong>如果需要斜体并强调</strong></em>，【则加三个“*”或者三个“—”.】</p><h4 id="2-4-删除线">2.4 删除线</h4><p>用“~~”包围，或者shift+alt+5</p><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">~~学习治好了我的焦虑~~</span><br></pre></td></tr></tbody></table><p>示例：</p><p>~学习治好了我的焦虑~</p><p>~学习使我快乐~</p><p><s>测试一下</s></p><h4 id="2-5-高亮">2.5 高亮</h4><p>用“==”包围</p><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">==每月坚持学习一项新东西==</span><br></pre></td></tr></tbody></table><p>示例：</p><p>==每月坚持学习一项新东西==</p><h4 id="2-6-代码">2.6 代码</h4><p>用“`”包围</p><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">`print("hello, world!")`</span><br></pre></td></tr></tbody></table><p>示例：</p><p><code>print(&quot;hello, world!&quot;)</code></p><h4 id="2-7-代码块">2.7 代码块</h4><p>按三个“`”并敲回车，要想显示行数，需要在typora的设置里进行勾选，另外代码块里可以选择语言，其会根据语言（如python）来自动高亮各个语句。</p><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">python</span><br></pre></td></tr></tbody></table><h4 id="2-8-引用">2.8 引用</h4><p>用“&gt;” 前置，需要嵌套使用，则多加一个“&gt;”</p><table><tbody><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">&gt;引用就是在前面加竖线</span><br><span class="line">&gt;&gt;这个就是在嵌套</span><br><span class="line">&gt;&gt;&gt;三重嵌套，过瘾把</span><br></pre></td></tr></tbody></table><p>示例：</p><blockquote><p>引用就是在前面加竖线</p><blockquote><p>这个就是在嵌套</p><blockquote><p>三重嵌套，过瘾把</p></blockquote></blockquote></blockquote><h4 id="2-9-无序列表">2.9 无序列表</h4><p>用“-”或“+”+<strong>空格</strong> 前置，回车后按下‘tab“键后，会自动缩进一级。按下”ctrl+[“,取消缩进，回到行首。</p><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">- 相当于在文字前加实心点</span><br><span class="line">+ 一样的效果</span><br><span class="line">* 也行，不过因为一般会自动补全为两个*，所以不常用</span><br></pre></td></tr></tbody></table><p>示例：</p><ul><li>相当于在文字前加实心点<ul><li>按下tab键后会自动缩进一级<ul><li>再按tab,在缩进一级<ul><li>再按继续缩进</li></ul></li></ul></li></ul></li></ul><h4 id="2-10-有序列表">2.10 有序列表</h4><p>数字+”.“+空格 前置，回车会自动补全，回车后按下tab键后会缩进一级</p><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">1. 我讲三点，这是第一点</span><br><span class="line">2. 这是第二点</span><br><span class="line">3. 这是第三点</span><br></pre></td></tr></tbody></table><p>示例：</p><ol><li>我讲三点，这是第一点</li><li>这是第二点</li><li>这是第三点<ol><li>回车后按tab会缩进<ol><li>再按在缩进</li></ol></li></ol></li></ol><h4 id="2-11-上标">2.11 上标</h4><p>用”^“包围</p><table><tbody><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">如2^10^=1024</span><br><span class="line">参考文献^[1][2]^</span><br></pre></td></tr></tbody></table><p>示例：</p><p>2^10^=1024</p><p>参考文献^[1][2]^</p><h4 id="2-12-下标">2.12 下标</h4><p>用“~”包围</p><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">co~2~灭火器</span><br></pre></td></tr></tbody></table><p>示例：</p><p>co~2~灭火器</p><h4 id="2-13-下划线">2.13 下划线</h4><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&lt;u&gt;+要下划线的文字+&lt;/u&gt;</span><br></pre></td></tr></tbody></table><p>示例：</p><p>下划线</p><h4 id="2-14-注释">2.14 注释</h4><p>“[^]”后置</p><table><tbody><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">&gt;难忘今宵，明年在相会！[^1]</span><br><span class="line"></span><br><span class="line">1:春节联欢晚会</span><br></pre></td></tr></tbody></table><blockquote><p>难忘今宵，明年在相会！[^1]</p></blockquote><p>1:春节联欢晚会</p><h4 id="2-15-链接">2.15 链接</h4><p>常用“[]”+“()”分别包围文本与链接</p><table><tbody><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">#1 外链（需按住ctrl）：[大家可以访问typora官方中文站](https://typoraio.cn/)</span><br><span class="line">#2 文内跳转（需按住ctrl）：[跳转到 2.3 斜体](#2.3 斜体)</span><br></pre></td></tr></tbody></table><p>示例：</p><p><a href="https://typoraio.cn/">大家可以访问typora官方中文站</a></p><p>[跳转到 2.3 斜体](#2.3 斜体)</p><h4 id="2-16-任务列表">2.16 任务列表</h4><p>采用“- [ ]”+空格 前置或快捷键ctrl+shift+x</p><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">今天的任务：</span><br><span class="line">- [ ] 送小孩上学</span><br><span class="line">- [ ] 学习typora</span><br><span class="line">- [ ] 接小孩</span><br><span class="line">- [x] 吃饭</span><br></pre></td></tr></tbody></table><p>示例：</p><p>今天的任务：</p><ul><li>[ ]  送小孩上学</li><li>[ ]  学习typora</li><li>[ ]  接小孩</li><li>[x]  吃饭</li></ul><h4 id="2-17-表格">2.17 表格</h4><p>直接右键——&gt;插入——&gt;表格</p><p>示例：</p><table><thead><tr><th>本周计划</th><th>内容</th><th>要求</th></tr></thead><tbody><tr><td>周一</td><td>学习typora</td><td>对学习情况进行记录</td></tr><tr><td>周二</td><td>运动</td><td>消耗2000卡</td></tr><tr><td>周三</td><td>看哪吒2</td><td>二刷，并安利身边至少5个人</td></tr></tbody></table><h4 id="2-18-图片">2.18 图片</h4><p>直接拖进来或者复制粘贴，设置中可以更改图片的存储方式，更进一步的的学习，可以参照如下链接（引用自CSDN）：</p><p><a href="https://blog.csdn.net/young2415/article/details/112750161">Typora使用技巧之插入图片及图片上传_typora插入图片-CSDN博客</a></p><h4 id="2-19-表情">2.19 表情</h4><p>“：”+表情英文+“：”，具体表情的英文可以参考CSDN中这篇链接：<a href="https://blog.csdn.net/weixin_42395140/article/details/111642339">Typora中的emoji表情_typora emoji-CSDN博客</a></p><table><tbody><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">:smile:</span><br><span class="line">:laughing:</span><br><span class="line">:heart_eyes:</span><br></pre></td></tr></tbody></table><p>示例：</p><p>:smile:<br>:laughing:<br>:heart_eyes:</p><h2 id="三-、其他功能">三 、其他功能</h2><h3 id="1、目录">1、目录</h3><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">[TOC]参见本文档开头</span><br></pre></td></tr></tbody></table><h3 id="2、数学公式">2、数学公式</h3><p>选择段落——&gt;公式块，采用“$”包围，关于常见的公式语法，可参见CSDN，链接如下：<a href="https://blog.csdn.net/qq_43444349/article/details/105400052">全面整理Typora的Latex数学公式语法_latex数学公式中间回车-CSDN博客</a></p><p>示例：</p><p><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>s</mi><mi>i</mi><msup><mi>n</mi><mn>2</mn></msup><mo>+</mo><mi>c</mi><mi>o</mi><msup><mi>s</mi><mn>2</mn></msup><mo>=</mo><mn>1</mn></mrow><annotation encoding="application/x-tex">sin^2+cos^2=1  </annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.9474em;vertical-align:-0.0833em;"></span><span class="mord mathnormal">s</span><span class="mord mathnormal">i</span><span class="mord"><span class="mord mathnormal">n</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8641em;"><span style="top:-3.113em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">2</span></span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:0.8641em;"></span><span class="mord mathnormal">co</span><span class="mord"><span class="mord mathnormal">s</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8641em;"><span style="top:-3.113em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">2</span></span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:0.6444em;"></span><span class="mord">1</span></span></span></span></span></p><h3 id="3、主题样式">3、主题样式</h3><p>在 Typora 中自定义主题可以按照以下步骤进行操作：</p><ol><li><p><strong>找到主题文件夹</strong> 打开 Typora 软件，点击菜单栏中的 “文件”，选择 “偏好设置”（在 Mac 系统中是 “Typora” -&gt; “偏好设置”）。在打开的偏好设置窗口中，点击左侧的 “外观” 选项。在右侧找到 “打开主题文件夹” 按钮并点击，这会打开 Typora 的主题文件夹。</p></li><li><p><strong>选择基础主题（可选）</strong> 在主题文件夹中，你会看到许多已经存在的主题文件（.css 格式）。你可以选择一个现有的主题作为基础来进行修改，这样可以减少一些基础样式的编写工作。例如，复制一份 <code>github.css</code> 文件，并将复制后的文件重命名为你想要的新主题名称，比如 <code>my_custom_theme.css</code>。</p></li><li><p><strong>编辑主题 CSS 文件</strong> 使用文本编辑器（如 Notepad++、Sublime Text、Visual Studio Code 等）打开你刚才复制并重命名的 CSS 文件（<code>my_custom_theme.css</code>）。在这个 CSS 文件中，你可以根据自己的喜好修改各种样式。以下是一些常见的样式修改示例：</p><ul><li><strong>修改字体</strong>：<ul><li><code>css body { font-family: Arial, sans-serif; /* 这里可以换成你喜欢的字体 */ font-size: 16px; /* 修改字体大小 */ }</code></li></ul></li><li><strong>修改标题样式</strong>：<ul><li><code>css h1 { color: #333; /* 修改一级标题颜色 */ border-bottom: 2px solid #ccc; /* 在一级标题下方添加边框 */ padding-bottom: 5px; /* 标题下方内边距 */ }</code></li></ul></li><li><strong>修改代码块样式</strong>：<ul><li><code>css code, pre { background-color: #f4f4f4; /* 修改代码块和行内代码的背景颜色 */ border: 1px solid #ccc; /* 添加边框 */ border-radius: 3px; /* 圆角 */ padding: 2px 4px; /* 内边距 */ }</code></li></ul></li></ul></li><li><p><strong>保存并应用主题：</strong></p><p>完成 CSS 文件的修改后，保存文件。然后回到 Typora 的偏好设置窗口，在 “外观” 选项中，点击 “主题” 下拉菜单，你应该能看到你自定义的主题名称，选择它即可应用你刚刚自定义的主题。 通过以上步骤，你就可以在 Typora 中创建并使用自己的自定义主题了。你可以不断调整 CSS 样式，直到达到你满意的效果。</p></li></ol><h3 id="4、画图">4、画图</h3><p>采用typora可进行画图操作，常见流程图、甘特图、时序图、饼图等等，均可通过代码实现。前提在文件——&gt;偏好设置——&gt;markdown——&gt;markdown扩展语法——&gt;勾选图表</p><table><tbody><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">```mermaid</span><br><span class="line">graph LR;</span><br><span class="line">a--&gt;b</span><br><span class="line">b--&gt;c</span><br><span class="line">c--&gt;d</span><br><span class="line">d--&gt;a</span><br><span class="line">```</span><br></pre></td></tr></tbody></table><table><tbody><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">graph LR;</span><br><span class="line">a--&gt;b</span><br><span class="line">b--&gt;c</span><br><span class="line">c--&gt;d</span><br><span class="line">d--&gt;a</span><br></pre></td></tr></tbody></table><h2 id="四、常用快捷键">四、常用快捷键</h2><p>以下是typora中常见的快捷键：</p><table><thead><tr><th style="text-align:center">按键</th><th style="text-align:center">效果</th><th style="text-align:center">按键</th><th style="text-align:center">效果</th></tr></thead><tbody><tr><td style="text-align:center"><code>Ctrl</code> + <code>D</code></td><td style="text-align:center">选中当前词</td><td style="text-align:center"><code>Ctrl</code> + <code>L</code></td><td style="text-align:center">选中当前句/行</td></tr><tr><td style="text-align:center"><code>Ctrl</code> + <code>E</code></td><td style="text-align:center">选中当前区块</td><td style="text-align:center"><code>Ctrl</code> + <code>F</code></td><td style="text-align:center">搜索当前选中</td></tr><tr><td style="text-align:center"><code>Ctrl</code> + <code>B</code></td><td style="text-align:center">加粗当前选中</td><td style="text-align:center"><code>Ctrl</code> + <code>H</code></td><td style="text-align:center">替换当前选中</td></tr><tr><td style="text-align:center"><code>Ctrl</code> + <code>I</code></td><td style="text-align:center">倾斜当前选中</td><td style="text-align:center"><code>Ctrl</code> + <code>U</code></td><td style="text-align:center">下划当前选中</td></tr><tr><td style="text-align:center"><code>Ctrl</code> + <code>K</code></td><td style="text-align:center">将当前选中生成链接</td><td style="text-align:center"><code>Ctrl</code> + <code>J</code></td><td style="text-align:center">滚动屏幕将选中滚至顶部</td></tr><tr><td style="text-align:center"><code>Ctrl</code> + <code>W</code></td><td style="text-align:center">关闭当前窗口</td><td style="text-align:center"><code>Ctrl</code> + <code>N</code></td><td style="text-align:center">打开新窗口</td></tr><tr><td style="text-align:center"><code>Ctrl</code> + <code>O</code></td><td style="text-align:center">打开文件</td><td style="text-align:center"><code>Ctrl</code> + <code>P</code></td><td style="text-align:center">搜索文件并打开</td></tr><tr><td style="text-align:center"><code>Ctrl</code> + <code>回车</code></td><td style="text-align:center">表格下方插入行</td><td style="text-align:center"><code>Ctrl</code> + <code>,</code></td><td style="text-align:center">打开偏好设置</td></tr><tr><td style="text-align:center"><code>Ctrl</code> + <code>.</code></td><td style="text-align:center">切换全角/半角标点</td><td style="text-align:center"><code>Ctrl</code> + <code>/</code></td><td style="text-align:center">切换正常/源代码视图</td></tr><tr><td style="text-align:center"><code>Ctrl</code> + <code>Shift</code> + <code>-</code></td><td style="text-align:center">缩小视图缩放</td><td style="text-align:center"><code>Ctrl</code> + <code>Shift</code> + <code>+</code></td><td style="text-align:center">放大视图缩放</td></tr></tbody></table><h2 id="五、总结">五、总结</h2><p>typora是一款开源的markdown编辑器，给了人们很大的自由创作空间，且非常高效、简洁，相比word更加纯粹，另一方面长期使用typora，可培养程序化的思维，不管工作还是生活，都大有裨益。最后还可以用它写文章，也是一种相当不错的体验。:smile:</p><p><a href="https://mjtao1111.github.io/2025/03/11/hexo%E6%90%AD%E5%BB%BA%E5%8D%9A%E5%AE%A2/" title="hexo搭建个人博客">hexo搭建个人博客</a></p>]]>
    </content>
    <id>https://keepstaytrue.github.io/2025/05/20/typora%E5%AD%A6%E4%B9%A0%E6%89%8B%E5%86%8C/</id>
    <link href="https://keepstaytrue.github.io/2025/05/20/typora%E5%AD%A6%E4%B9%A0%E6%89%8B%E5%86%8C/"/>
    <published>2025-05-20T15:00:00.000Z</published>
    <summary>
      <![CDATA[<h1>typora学习手册</h1>
<h2 id="一-、概述">一 、概述</h2>
<h3 id="1、简介">1、简介</h3>
<p>typora是一款轻量级但功能强大的markdown编辑器，支持实时预览、多种格式导出、自定义主题等功能。可以用来记笔记，写技术类的文]]>
    </summary>
    <title>typora学习手册</title>
    <updated>2026-05-25T15:26:39.555Z</updated>
  </entry>
  <entry>
    <author>
      <name>KeepStayTrue</name>
    </author>
    <category term="Java后端求职学习体系" scheme="https://keepstaytrue.github.io/categories/Java%E5%90%8E%E7%AB%AF%E6%B1%82%E8%81%8C%E5%AD%A6%E4%B9%A0%E4%BD%93%E7%B3%BB/"/>
    <category term="中间件" scheme="https://keepstaytrue.github.io/categories/Java%E5%90%8E%E7%AB%AF%E6%B1%82%E8%81%8C%E5%AD%A6%E4%B9%A0%E4%BD%93%E7%B3%BB/%E4%B8%AD%E9%97%B4%E4%BB%B6/"/>
    <category term="redis" scheme="https://keepstaytrue.github.io/categories/Java%E5%90%8E%E7%AB%AF%E6%B1%82%E8%81%8C%E5%AD%A6%E4%B9%A0%E4%BD%93%E7%B3%BB/%E4%B8%AD%E9%97%B4%E4%BB%B6/redis/"/>
    <category term="集群部署(Redis集群)" scheme="https://keepstaytrue.github.io/categories/Java%E5%90%8E%E7%AB%AF%E6%B1%82%E8%81%8C%E5%AD%A6%E4%B9%A0%E4%BD%93%E7%B3%BB/%E4%B8%AD%E9%97%B4%E4%BB%B6/redis/%E9%9B%86%E7%BE%A4%E9%83%A8%E7%BD%B2-Redis%E9%9B%86%E7%BE%A4/"/>
    <category term="Redis" scheme="https://keepstaytrue.github.io/tags/Redis/"/>
    <category term="数据库" scheme="https://keepstaytrue.github.io/tags/%E6%95%B0%E6%8D%AE%E5%BA%93/"/>
    <category term="Redis集群" scheme="https://keepstaytrue.github.io/tags/Redis%E9%9B%86%E7%BE%A4/"/>
    <content>
      <![CDATA[<h1>Redis集群</h1><p>本章是基于CentOS7下的Redis集群教程，包括：</p><ul><li>单机安装Redis</li><li>Redis主从</li><li>Redis分片集群</li></ul><h1>1.单机安装Redis</h1><p>首先需要安装Redis所需要的依赖：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">yum install -y gcc tcl</span><br></pre></td></tr></table></figure><p>然后将课前资料提供的Redis安装包上传到虚拟机的任意目录：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210629114325516.png" alt="image-20210629114325516"></p><p>例如，我放到了/tmp目录：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210629114830642.png" alt="image-20210629114830642"></p><p>解压缩：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">tar -xzf redis-6.2.4.tar.gz</span><br></pre></td></tr></table></figure><p>解压后：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210629114941810.png" alt="image-20210629114941810"></p><p>进入redis目录：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">cd</span> redis-6.2.4</span><br></pre></td></tr></table></figure><p>运行编译命令：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">make &amp;&amp; make install</span><br></pre></td></tr></table></figure><p>如果没有出错，应该就安装成功了。</p><p>然后修改redis.conf文件中的一些配置：</p><figure class="highlight properties"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 绑定地址，默认是127.0.0.1，会导致只能在本地访问。修改为0.0.0.0则可以在任意IP访问</span></span><br><span class="line"><span class="attr">bind</span> <span class="string">0.0.0.0</span></span><br><span class="line"><span class="comment"># 保护模式，关闭保护模式</span></span><br><span class="line"><span class="attr">protected-mode</span> <span class="string">no</span></span><br><span class="line"><span class="comment"># 数据库数量，设置为1</span></span><br><span class="line"><span class="attr">databases</span> <span class="string">1</span></span><br></pre></td></tr></table></figure><p>启动Redis：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">redis-server redis.conf</span><br></pre></td></tr></table></figure><p>停止redis服务：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">redis-cli shutdown</span><br></pre></td></tr></table></figure><blockquote><p>redis日志输出自定义位置</p></blockquote><p>只要在redis.conf中，将 daemonize no 修改为 daemonize yes 即可。</p><p>当以后台运行的方式启动redis，redis会生成一个pid文件，redis.conf中可以设置pid文件的位置。 pidfile /var/run/redis_6379.pid 当redis进程关闭时，redis会删除这个pid文件。</p><p>2、指定日志文件和日志级别<br>在redis.conf中，将 logfile “” 修改为  logfile /var/log/redis.log</p><h1>2.Redis主从集群</h1><h2 id="2-1-集群结构">2.1.集群结构</h2><p>我们搭建的主从集群结构如图：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210630111505799.png" alt="image-20210630111505799"></p><p>共包含三个节点，一个主节点，两个从节点。</p><p>这里我们会在同一台虚拟机中开启3个redis实例，模拟主从集群，信息如下：</p><table><thead><tr><th style="text-align:center">IP</th><th style="text-align:center">PORT</th><th style="text-align:center">角色</th></tr></thead><tbody><tr><td style="text-align:center">192.168.150.101</td><td style="text-align:center">7001</td><td style="text-align:center">master</td></tr><tr><td style="text-align:center">192.168.150.101</td><td style="text-align:center">7002</td><td style="text-align:center">slave</td></tr><tr><td style="text-align:center">192.168.150.101</td><td style="text-align:center">7003</td><td style="text-align:center">slave</td></tr></tbody></table><h2 id="2-2-准备实例和配置">2.2.准备实例和配置</h2><p>要在同一台虚拟机开启3个实例，必须准备三份不同的配置文件和目录，配置文件所在目录也就是工作目录。</p><p>1）创建目录</p><p>我们创建三个文件夹，名字分别叫7001、7002、7003：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 进入/tmp目录</span></span><br><span class="line"><span class="built_in">cd</span> /tmp</span><br><span class="line"><span class="comment"># 创建目录</span></span><br><span class="line"><span class="built_in">mkdir</span> 7001 7002 7003</span><br></pre></td></tr></table></figure><p>如图：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210630113929868.png" alt="image-20210630113929868"></p><p>2）恢复原始配置</p><p>修改redis-6.2.4/redis.conf文件，将其中的持久化模式改为默认的RDB模式，AOF保持关闭状态。</p><figure class="highlight properties"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 开启RDB</span></span><br><span class="line"><span class="comment"># save &quot;&quot;</span></span><br><span class="line"><span class="attr">save</span> <span class="string">3600 1</span></span><br><span class="line"><span class="attr">save</span> <span class="string">300 100</span></span><br><span class="line"><span class="attr">save</span> <span class="string">60 10000</span></span><br><span class="line"><span class="comment"></span></span><br><span class="line"><span class="comment"># 关闭AOF</span></span><br><span class="line"><span class="attr">appendonly</span> <span class="string">no</span></span><br></pre></td></tr></table></figure><p>3）拷贝配置文件到每个实例目录</p><p>然后将redis-6.2.4/redis.conf文件拷贝到三个目录中（在/tmp目录执行下列命令）：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 方式一：逐个拷贝</span></span><br><span class="line"><span class="built_in">cp</span> redis-6.2.4/redis.conf 7001</span><br><span class="line"><span class="built_in">cp</span> redis-6.2.4/redis.conf 7002</span><br><span class="line"><span class="built_in">cp</span> redis-6.2.4/redis.conf 7003</span><br><span class="line"></span><br><span class="line"><span class="comment"># 方式二：管道组合命令，一键拷贝</span></span><br><span class="line"><span class="built_in">echo</span> 7001 7002 7003 | xargs -t -n 1 <span class="built_in">cp</span> redis-6.2.4/redis.conf</span><br></pre></td></tr></table></figure><p>4）修改每个实例的端口、工作目录</p><p>修改每个文件夹内的配置文件，将端口分别修改为7001、7002、7003，将rdb文件保存位置都修改为自己所在目录（在/tmp目录执行下列命令）：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">sed -i -e <span class="string">&#x27;s/6379/7001/g&#x27;</span> -e <span class="string">&#x27;s/dir .\//dir \/tmp\/7001\//g&#x27;</span> 7001/redis.conf</span><br><span class="line">sed -i -e <span class="string">&#x27;s/6379/7002/g&#x27;</span> -e <span class="string">&#x27;s/dir .\//dir \/tmp\/7002\//g&#x27;</span> 7002/redis.conf</span><br><span class="line">sed -i -e <span class="string">&#x27;s/6379/7003/g&#x27;</span> -e <span class="string">&#x27;s/dir .\//dir \/tmp\/7003\//g&#x27;</span> 7003/redis.conf</span><br></pre></td></tr></table></figure><p>5）修改每个实例的声明IP</p><p>虚拟机本身有多个IP，为了避免将来混乱，我们需要在redis.conf文件中指定每一个实例的绑定ip信息，格式如下：</p><figure class="highlight properties"><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="comment"># redis实例的声明 IP</span></span><br><span class="line"><span class="attr">replica-announce-ip</span> <span class="string">192.168.150.101</span></span><br></pre></td></tr></table></figure><p>每个目录都要改，我们一键完成修改（在/tmp目录执行下列命令）：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 逐一执行</span></span><br><span class="line">sed -i <span class="string">&#x27;1a replica-announce-ip 192.168.150.101&#x27;</span> 7001/redis.conf</span><br><span class="line">sed -i <span class="string">&#x27;1a replica-announce-ip 192.168.150.101&#x27;</span> 7002/redis.conf</span><br><span class="line">sed -i <span class="string">&#x27;1a replica-announce-ip 192.168.150.101&#x27;</span> 7003/redis.conf</span><br><span class="line"></span><br><span class="line"><span class="comment"># 或者一键修改</span></span><br><span class="line"><span class="built_in">printf</span> <span class="string">&#x27;%s\n&#x27;</span> 7001 7002 7003 | xargs -I&#123;&#125; -t sed -i <span class="string">&#x27;1a replica-announce-ip 192.168.150.101&#x27;</span> &#123;&#125;/redis.conf</span><br></pre></td></tr></table></figure><h2 id="2-3-启动">2.3.启动</h2><p>为了方便查看日志，我们打开3个ssh窗口，分别启动3个redis实例，启动命令：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 第1个</span></span><br><span class="line">redis-server 7001/redis.conf</span><br><span class="line"><span class="comment"># 第2个</span></span><br><span class="line">redis-server 7002/redis.conf</span><br><span class="line"><span class="comment"># 第3个</span></span><br><span class="line">redis-server 7003/redis.conf</span><br></pre></td></tr></table></figure><p>启动后：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210630183914491.png" alt="image-20210630183914491"></p><p>如果要一键停止，可以运行下面命令：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">printf</span> <span class="string">&#x27;%s\n&#x27;</span> 7001 7002 7003 | xargs -I&#123;&#125; -t redis-cli -p &#123;&#125; shutdown</span><br></pre></td></tr></table></figure><h2 id="2-4-开启主从关系">2.4.开启主从关系</h2><p>现在三个实例还没有任何关系，要配置主从可以使用replicaof 或者slaveof（5.0以前）命令。</p><p>有临时和永久两种模式：</p><ul><li><p>修改配置文件（永久生效）</p><ul><li>在redis.conf中添加一行配置：<code>slaveof &lt;masterip&gt; &lt;masterport&gt;</code></li></ul></li><li><p>使用redis-cli客户端连接到redis服务，执行slaveof命令（重启后失效）：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">slaveof &lt;masterip&gt; &lt;masterport&gt;</span><br></pre></td></tr></table></figure></li></ul><p><strong><font color="red">注意</font></strong>：在5.0以后新增命令replicaof，与salveof效果一致。</p><p>这里我们为了演示方便，使用方式二。</p><p>通过redis-cli命令连接7002，执行下面命令：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 连接 7002</span></span><br><span class="line">redis-cli -p 7002</span><br><span class="line"><span class="comment"># 执行slaveof</span></span><br><span class="line">slaveof 192.168.150.101 7001</span><br></pre></td></tr></table></figure><p>通过redis-cli命令连接7003，执行下面命令：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 连接 7003</span></span><br><span class="line">redis-cli -p 7003</span><br><span class="line"><span class="comment"># 执行slaveof</span></span><br><span class="line">slaveof 192.168.150.101 7001</span><br></pre></td></tr></table></figure><p>然后连接 7001节点，查看集群状态：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 连接 7001</span></span><br><span class="line">redis-cli -p 7001</span><br><span class="line"><span class="comment"># 查看状态</span></span><br><span class="line">info replication</span><br></pre></td></tr></table></figure><p>结果：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210630201258802.png" alt="image-20210630201258802"></p><h2 id="2-5-测试">2.5.测试</h2><p>执行下列操作以测试：</p><ul><li><p>利用redis-cli连接7001，执行<code>set num 123</code></p></li><li><p>利用redis-cli连接7002，执行<code>get num</code>，再执行<code>set num 666</code></p></li><li><p>利用redis-cli连接7003，执行<code>get num</code>，再执行<code>set num 888</code></p></li></ul><p>可以发现，只有在7001这个master节点上可以执行写操作，7002和7003这两个slave节点只能执行读操作。</p><h1>3.搭建哨兵集群</h1><h2 id="3-1-集群结构">3.1.集群结构</h2><p>这里我们搭建一个三节点形成的Sentinel集群，来监管之前的Redis主从集群。如图：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210701215227018.png" alt="image-20210701215227018"></p><p>三个sentinel实例信息如下：</p><table><thead><tr><th>节点</th><th style="text-align:center">IP</th><th style="text-align:center">PORT</th></tr></thead><tbody><tr><td>s1</td><td style="text-align:center">192.168.150.101</td><td style="text-align:center">27001</td></tr><tr><td>s2</td><td style="text-align:center">192.168.150.101</td><td style="text-align:center">27002</td></tr><tr><td>s3</td><td style="text-align:center">192.168.150.101</td><td style="text-align:center">27003</td></tr></tbody></table><h2 id="3-2-准备实例和配置">3.2.准备实例和配置</h2><p>要在同一台虚拟机开启3个实例，必须准备三份不同的配置文件和目录，配置文件所在目录也就是工作目录。</p><p>我们创建三个文件夹，名字分别叫s1、s2、s3：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 进入/tmp目录</span></span><br><span class="line"><span class="built_in">cd</span> /tmp</span><br><span class="line"><span class="comment"># 创建目录</span></span><br><span class="line"><span class="built_in">mkdir</span> s1 s2 s3</span><br></pre></td></tr></table></figure><p>如图：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210701215534714.png" alt="image-20210701215534714"></p><p>然后我们在s1目录创建一个sentinel.conf文件，添加下面的内容：</p><figure class="highlight ini"><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">port 27001</span><br><span class="line">sentinel announce-ip 192.168.150.101</span><br><span class="line">sentinel monitor mymaster 192.168.150.101 7001 2</span><br><span class="line">sentinel down-after-milliseconds mymaster 5000</span><br><span class="line">sentinel failover-timeout mymaster 60000</span><br><span class="line">dir &quot;/tmp/s1&quot;</span><br></pre></td></tr></table></figure><p>解读：</p><ul><li><code>port 27001</code>：是当前sentinel实例的端口</li><li><code>sentinel monitor mymaster 192.168.150.101 7001 2</code>：指定主节点信息<ul><li><code>mymaster</code>：主节点名称，自定义，任意写</li><li><code>192.168.150.101 7001</code>：主节点的ip和端口</li><li><code>2</code>：选举master时的quorum值</li></ul></li></ul><p>然后将s1/sentinel.conf文件拷贝到s2、s3两个目录中（在/tmp目录执行下列命令）：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 方式一：逐个拷贝</span></span><br><span class="line"><span class="built_in">cp</span> s1/sentinel.conf s2</span><br><span class="line"><span class="built_in">cp</span> s1/sentinel.conf s3</span><br><span class="line"><span class="comment"># 方式二：管道组合命令，一键拷贝</span></span><br><span class="line"><span class="built_in">echo</span> s2 s3 | xargs -t -n 1 <span class="built_in">cp</span> s1/sentinel.conf</span><br></pre></td></tr></table></figure><p>修改s2、s3两个文件夹内的配置文件，将端口分别修改为27002、27003：</p><figure class="highlight sh"><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">sed -i -e <span class="string">&#x27;s/27001/27002/g&#x27;</span> -e <span class="string">&#x27;s/s1/s2/g&#x27;</span> s2/sentinel.conf</span><br><span class="line">sed -i -e <span class="string">&#x27;s/27001/27003/g&#x27;</span> -e <span class="string">&#x27;s/s1/s3/g&#x27;</span> s3/sentinel.conf</span><br></pre></td></tr></table></figure><h2 id="3-3-启动">3.3.启动</h2><p>为了方便查看日志，我们打开3个ssh窗口，分别启动3个redis实例，启动命令：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 第1个</span></span><br><span class="line">redis-sentinel s1/sentinel.conf</span><br><span class="line"><span class="comment"># 第2个</span></span><br><span class="line">redis-sentinel s2/sentinel.conf</span><br><span class="line"><span class="comment"># 第3个</span></span><br><span class="line">redis-sentinel s3/sentinel.conf</span><br></pre></td></tr></table></figure><p>启动后：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210701220714104.png" alt="image-20210701220714104"></p><h2 id="3-4-测试">3.4.测试</h2><p>尝试让master节点7001宕机，查看sentinel日志：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210701222857997.png" alt="image-20210701222857997"></p><p>查看7003的日志：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210701223025709.png" alt="image-20210701223025709"></p><p>查看7002的日志：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210701223131264.png" alt="image-20210701223131264"></p><h1>4.搭建分片集群</h1><h2 id="4-1-集群结构">4.1.集群结构</h2><p>分片集群需要的节点数量较多，这里我们搭建一个最小的分片集群，包含3个master节点，每个master包含一个slave节点，结构如下：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210702164116027.png" alt="image-20210702164116027"></p><p>这里我们会在同一台虚拟机中开启6个redis实例，模拟分片集群，信息如下：</p><table><thead><tr><th style="text-align:center">IP</th><th style="text-align:center">PORT</th><th style="text-align:center">角色</th></tr></thead><tbody><tr><td style="text-align:center">192.168.150.101</td><td style="text-align:center">7001</td><td style="text-align:center">master</td></tr><tr><td style="text-align:center">192.168.150.101</td><td style="text-align:center">7002</td><td style="text-align:center">master</td></tr><tr><td style="text-align:center">192.168.150.101</td><td style="text-align:center">7003</td><td style="text-align:center">master</td></tr><tr><td style="text-align:center">192.168.150.101</td><td style="text-align:center">8001</td><td style="text-align:center">slave</td></tr><tr><td style="text-align:center">192.168.150.101</td><td style="text-align:center">8002</td><td style="text-align:center">slave</td></tr><tr><td style="text-align:center">192.168.150.101</td><td style="text-align:center">8003</td><td style="text-align:center">slave</td></tr></tbody></table><h2 id="4-2-准备实例和配置">4.2.准备实例和配置</h2><p>删除之前的7001、7002、7003这几个目录，重新创建出7001、7002、7003、8001、8002、8003目录：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 进入/tmp目录</span></span><br><span class="line"><span class="built_in">cd</span> /tmp</span><br><span class="line"><span class="comment"># 删除旧的，避免配置干扰</span></span><br><span class="line"><span class="built_in">rm</span> -rf 7001 7002 7003</span><br><span class="line"><span class="comment"># 创建目录</span></span><br><span class="line"><span class="built_in">mkdir</span> 7001 7002 7003 8001 8002 8003</span><br></pre></td></tr></table></figure><p>在/tmp下准备一个新的redis.conf文件，内容如下：</p><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line">port 6379</span><br><span class="line"><span class="comment"># 开启集群功能</span></span><br><span class="line">cluster-enabled yes</span><br><span class="line"><span class="comment"># 集群的配置文件名称，不需要我们创建，由redis自己维护</span></span><br><span class="line">cluster-config-file /tmp/6379/nodes.conf</span><br><span class="line"><span class="comment"># 节点心跳失败的超时时间</span></span><br><span class="line">cluster-node-timeout 5000</span><br><span class="line"><span class="comment"># 持久化文件存放目录</span></span><br><span class="line">dir /tmp/6379</span><br><span class="line"><span class="comment"># 绑定地址</span></span><br><span class="line">bind 0.0.0.0</span><br><span class="line"><span class="comment"># 让redis后台运行</span></span><br><span class="line">daemonize yes</span><br><span class="line"><span class="comment"># 注册的实例ip</span></span><br><span class="line">replica-announce-ip 192.168.150.101</span><br><span class="line"><span class="comment"># 保护模式</span></span><br><span class="line">protected-mode no</span><br><span class="line"><span class="comment"># 数据库数量</span></span><br><span class="line">databases 1</span><br><span class="line"><span class="comment"># 日志</span></span><br><span class="line">logfile /tmp/6379/run.log</span><br></pre></td></tr></table></figure><p>将这个文件拷贝到每个目录下：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 进入/tmp目录</span></span><br><span class="line"><span class="built_in">cd</span> /tmp</span><br><span class="line"><span class="comment"># 执行拷贝</span></span><br><span class="line"><span class="built_in">echo</span> 7001 7002 7003 8001 8002 8003 | xargs -t -n 1 <span class="built_in">cp</span> redis.conf</span><br></pre></td></tr></table></figure><p>修改每个目录下的redis.conf，将其中的6379修改为与所在目录一致：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 进入/tmp目录</span></span><br><span class="line"><span class="built_in">cd</span> /tmp</span><br><span class="line"><span class="comment"># 修改配置文件</span></span><br><span class="line"><span class="built_in">printf</span> <span class="string">&#x27;%s\n&#x27;</span> 7001 7002 7003 8001 8002 8003 | xargs -I&#123;&#125; -t sed -i <span class="string">&#x27;s/6379/&#123;&#125;/g&#x27;</span> &#123;&#125;/redis.conf</span><br></pre></td></tr></table></figure><h2 id="4-3-启动">4.3.启动</h2><p>因为已经配置了后台启动模式，所以可以直接启动服务：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 进入/tmp目录</span></span><br><span class="line"><span class="built_in">cd</span> /tmp</span><br><span class="line"><span class="comment"># 一键启动所有服务</span></span><br><span class="line"><span class="built_in">printf</span> <span class="string">&#x27;%s\n&#x27;</span> 7001 7002 7003 8001 8002 8003 | xargs -I&#123;&#125; -t redis-server &#123;&#125;/redis.conf</span><br></pre></td></tr></table></figure><p>通过ps查看状态：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ps -ef | grep redis</span><br></pre></td></tr></table></figure><p>发现服务都已经正常启动：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210702174255799.png" alt="image-20210702174255799"></p><p>如果要关闭所有进程，可以执行命令：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ps -ef | grep redis | awk <span class="string">&#x27;&#123;print $2&#125;&#x27;</span> | xargs <span class="built_in">kill</span></span><br></pre></td></tr></table></figure><p>或者（推荐这种方式）：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">printf</span> <span class="string">&#x27;%s\n&#x27;</span> 7001 7002 7003 8001 8002 8003 | xargs -I&#123;&#125; -t redis-cli -p &#123;&#125; shutdown</span><br></pre></td></tr></table></figure><h2 id="4-4-创建集群">4.4.创建集群</h2><p>虽然服务启动了，但是目前每个服务之间都是独立的，没有任何关联。</p><p>我们需要执行命令来创建集群，在Redis5.0之前创建集群比较麻烦，5.0之后集群管理命令都集成到了redis-cli中。</p><p>1）Redis5.0之前</p><p>Redis5.0之前集群命令都是用redis安装包下的src/redis-trib.rb来实现的。因为redis-trib.rb是有ruby语言编写的所以需要安装ruby环境。</p> <figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 安装依赖</span></span><br><span class="line">yum -y install zlib ruby rubygems</span><br><span class="line">gem install redis</span><br></pre></td></tr></table></figure><p>然后通过命令来管理集群：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 进入redis的src目录</span></span><br><span class="line"><span class="built_in">cd</span> /tmp/redis-6.2.4/src</span><br><span class="line"><span class="comment"># 创建集群</span></span><br><span class="line">./redis-trib.rb create --replicas 1 192.168.150.101:7001 192.168.150.101:7002 192.168.150.101:7003 192.168.150.101:8001 192.168.150.101:8002 192.168.150.101:8003</span><br></pre></td></tr></table></figure><p>2）Redis5.0以后</p><p>我们使用的是Redis6.2.4版本，集群管理以及集成到了redis-cli中，格式如下：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">redis-cli --cluster create --cluster-replicas 1 192.168.150.101:7001 192.168.150.101:7002 192.168.150.101:7003 192.168.150.101:8001 192.168.150.101:8002 192.168.150.101:8003</span><br></pre></td></tr></table></figure><p>命令说明：</p><ul><li><code>redis-cli --cluster</code>或者<code>./redis-trib.rb</code>：代表集群操作命令</li><li><code>create</code>：代表是创建集群</li><li><code>--replicas 1</code>或者<code>--cluster-replicas 1</code> ：指定集群中每个master的副本个数为1，此时<code>节点总数 ÷ (replicas + 1)</code> 得到的就是master的数量。因此节点列表中的前n个就是master，其它节点都是slave节点，随机分配到不同master</li></ul><p>运行后的样子：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210702181101969.png" alt="image-20210702181101969"></p><p>这里输入yes，则集群开始创建：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210702181215705.png" alt="image-20210702181215705"></p><p>通过命令可以查看集群状态：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">redis-cli -p 7001 cluster nodes</span><br></pre></td></tr></table></figure><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210702181922809.png" alt="image-20210702181922809"></p><h2 id="4-5-测试">4.5.测试</h2><p>尝试连接7001节点，存储一个数据：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 连接</span></span><br><span class="line">redis-cli -p 7001</span><br><span class="line"><span class="comment"># 存储数据</span></span><br><span class="line"><span class="built_in">set</span> num 123</span><br><span class="line"><span class="comment"># 读取数据</span></span><br><span class="line">get num</span><br><span class="line"><span class="comment"># 再次存储</span></span><br><span class="line"><span class="built_in">set</span> a 1</span><br></pre></td></tr></table></figure><p>结果悲剧了：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210702182343979.png" alt="image-20210702182343979"></p><p>集群操作时，需要给<code>redis-cli</code>加上<code>-c</code>参数才可以：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">redis-cli -c -p 7001</span><br></pre></td></tr></table></figure><p>这次可以了：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210702182602145.png" alt="image-20210702182602145"></p>]]>
    </content>
    <id>https://keepstaytrue.github.io/2025/05/18/Redis%E9%9B%86%E7%BE%A4/</id>
    <link href="https://keepstaytrue.github.io/2025/05/18/Redis%E9%9B%86%E7%BE%A4/"/>
    <published>2025-05-18T15:00:00.000Z</published>
    <summary>
      <![CDATA[<h1>Redis集群</h1>
<p>本章是基于CentOS7下的Redis集群教程，包括：</p>
<ul>
<li>单机安装Redis</li>
<li>Redis主从</li>
<li>Redis分片集群</li>
</ul>
<h1>1.单机安装Redis</h1>
<]]>
    </summary>
    <title>Redis集群</title>
    <updated>2026-05-23T13:33:57.729Z</updated>
  </entry>
  <entry>
    <author>
      <name>KeepStayTrue</name>
    </author>
    <category term="Java后端求职学习体系" scheme="https://keepstaytrue.github.io/categories/Java%E5%90%8E%E7%AB%AF%E6%B1%82%E8%81%8C%E5%AD%A6%E4%B9%A0%E4%BD%93%E7%B3%BB/"/>
    <category term="中间件" scheme="https://keepstaytrue.github.io/categories/Java%E5%90%8E%E7%AB%AF%E6%B1%82%E8%81%8C%E5%AD%A6%E4%B9%A0%E4%BD%93%E7%B3%BB/%E4%B8%AD%E9%97%B4%E4%BB%B6/"/>
    <category term="redis" scheme="https://keepstaytrue.github.io/categories/Java%E5%90%8E%E7%AB%AF%E6%B1%82%E8%81%8C%E5%AD%A6%E4%B9%A0%E4%BD%93%E7%B3%BB/%E4%B8%AD%E9%97%B4%E4%BB%B6/redis/"/>
    <category term="缓存架构" scheme="https://keepstaytrue.github.io/categories/Java%E5%90%8E%E7%AB%AF%E6%B1%82%E8%81%8C%E5%AD%A6%E4%B9%A0%E4%BD%93%E7%B3%BB/%E4%B8%AD%E9%97%B4%E4%BB%B6/redis/%E7%BC%93%E5%AD%98%E6%9E%B6%E6%9E%84/"/>
    <category term="分布式缓存" scheme="https://keepstaytrue.github.io/categories/Java%E5%90%8E%E7%AB%AF%E6%B1%82%E8%81%8C%E5%AD%A6%E4%B9%A0%E4%BD%93%E7%B3%BB/%E4%B8%AD%E9%97%B4%E4%BB%B6/redis/%E7%BC%93%E5%AD%98%E6%9E%B6%E6%9E%84/%E5%88%86%E5%B8%83%E5%BC%8F%E7%BC%93%E5%AD%98/"/>
    <category term="Redis" scheme="https://keepstaytrue.github.io/tags/Redis/"/>
    <category term="数据库" scheme="https://keepstaytrue.github.io/tags/%E6%95%B0%E6%8D%AE%E5%BA%93/"/>
    <category term="分布式缓存" scheme="https://keepstaytrue.github.io/tags/%E5%88%86%E5%B8%83%E5%BC%8F%E7%BC%93%E5%AD%98/"/>
    <content>
      <![CDATA[<h1>分布式缓存</h1><p>– 基于Redis集群解决单机Redis存在的问题</p><p>单机的Redis存在四大问题：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210725144240631.png" alt="image-20210725144240631"></p><h1>0.学习目标</h1><h1>1.Redis持久化</h1><p>Redis有两种持久化方案：</p><ul><li>RDB持久化</li><li>AOF持久化</li></ul><h2 id="1-1-RDB持久化">1.1.RDB持久化</h2><blockquote><p>RDB全称Redis Database Backup file（Redis数据备份文件），也被叫做Redis数据快照。简单来说就是把内存中的所有数据都记录到磁盘中。当Redis实例故障重启后，从磁盘读取快照文件，恢复数据。快照文件称为RDB文件，默认是保存在当前运行目录。</p></blockquote><h3 id="1-1-1-执行时机">1.1.1.执行时机</h3><p>RDB持久化在四种情况下会执行：</p><ul><li>执行save命令</li><li>执行bgsave命令</li><li>Redis停机时</li><li>触发RDB条件时</li></ul><p><strong>1）save命令</strong></p><p>执行下面的命令，可以立即执行一次RDB：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210725144536958.png" alt="image-20210725144536958"></p><p>save命令会导致主进程执行RDB，这个过程中其它所有命令都会被阻塞。只有在数据迁移时可能用到。</p><p><strong>2）bgsave命令</strong></p><p>下面的命令可以异步执行RDB：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210725144725943.png" alt="image-20210725144725943"></p><p>这个命令执行后会<strong>开启独立进程</strong>完成RDB，主进程可以持续处理用户请求，不受影响。</p><p><strong>3）停机时</strong></p><p>Redis停机时会执行一次save命令，实现RDB持久化。</p><p><strong>4）触发RDB条件</strong></p><p>Redis内部有触发RDB的机制，可以在redis.conf文件中找到，格式如下：</p><figure class="highlight properties"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 900秒内，如果至少有1个key被修改，则执行bgsave ， 如果是save &quot;&quot; 则表示禁用RDB</span></span><br><span class="line"><span class="attr">save</span> <span class="string">900 1  </span></span><br><span class="line"><span class="attr">save</span> <span class="string">300 10  </span></span><br><span class="line"><span class="attr">save</span> <span class="string">60 10000 </span></span><br></pre></td></tr></table></figure><p>RDB的其它配置也可以在redis.conf文件中设置：</p><figure class="highlight properties"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 是否压缩 ,建议不开启，压缩也会消耗cpu，磁盘的话不值钱</span></span><br><span class="line"><span class="attr">rdbcompression</span> <span class="string">yes</span></span><br><span class="line"><span class="comment"></span></span><br><span class="line"><span class="comment"># RDB文件名称</span></span><br><span class="line"><span class="attr">dbfilename</span> <span class="string">dump.rdb  </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="attr">dir</span> <span class="string">./ </span></span><br></pre></td></tr></table></figure><blockquote><details class="toggle"><summary class="toggle-button" style>启动日志设置后台运行的方法</summary><div class="toggle-content"><blockquote><p>只要在redis.conf中，将 daemonize no 修改为 daemonize yes 即可。</p><p>当以后台运行的方式启动redis，redis会生成一个pid文件，redis.conf中可以设置pid文件的位置。 pidfile /var/run/redis_6379.pid 当redis进程关闭时，redis会删除这个pid文件。</p><p>2、指定日志文件和日志级别<br>在redis.conf中，将 logfile “” 修改为  logfile /var/log/redis.log</p></blockquote></div></details></blockquote><h3 id="1-1-2-RDB原理">1.1.2.RDB原理</h3><p>bgsave开始时会fork主进程得到子进程，子进程共享主进程的内存数据。完成fork后读取内存数据并写入 RDB 文件。</p><p>fork采用的是<strong>copy-on-write</strong>技术：</p><ul><li>当主进程执行读操作时，访问共享内存；</li><li>当主进程执行写操作时，则会拷贝一份数据，执行写操作。</li></ul><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210725151319695.png" alt="image-20210725151319695"></p><h3 id="1-1-3-小结">1.1.3.小结</h3><p>RDB方式bgsave的基本流程？</p><ul><li>fork主进程得到一个子进程，共享内存空间 (共享页表速度比较快)</li><li>子进程读取内存数据并异步写入新的RDB文件</li><li>用新RDB文件替换旧的RDB文件</li></ul><p>RDB会在什么时候执行？save 60 1000代表什么含义？</p><ul><li>默认是服务停止时(save)</li><li>代表60秒内至少执行1000次修改则触发RDB(bgsave)</li></ul><p>RDB的缺点？</p><ul><li>RDB执行间隔时间长，两次RDB之间写入数据有丢失的风险</li><li>fork子进程、压缩、写出RDB文件都比较耗时</li></ul><h2 id="1-2-AOF持久化">1.2.AOF持久化</h2><h3 id="1-2-1-AOF原理">1.2.1.AOF原理</h3><p>AOF全称为Append Only File（追加文件）。Redis处理的每一个写命令都会记录在AOF文件，可以看做是命令日志文件。</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210725151543640.png" alt="image-20210725151543640"></p><ul><li>$ 符记录的是字符串的长度</li></ul><h3 id="1-2-2-AOF配置">1.2.2.AOF配置</h3><p>AOF默认是关闭的，需要修改redis.conf配置文件来开启AOF：</p><figure class="highlight properties"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 是否开启AOF功能，默认是no</span></span><br><span class="line"><span class="attr">appendonly</span> <span class="string">yes</span></span><br><span class="line"><span class="comment"># AOF文件的名称</span></span><br><span class="line"><span class="attr">appendfilename</span> <span class="string">&quot;appendonly.aof&quot;</span></span><br></pre></td></tr></table></figure><p>AOF的命令记录的频率也可以通过redis.conf文件来配：</p><figure class="highlight properties"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 表示每执行一次写命令，立即记录到AOF文件</span></span><br><span class="line"><span class="attr">appendfsync</span> <span class="string">always </span></span><br><span class="line"><span class="comment"># 写命令执行完先放入AOF缓冲区，然后表示每隔1秒将缓冲区数据写到AOF文件，是默认方案</span></span><br><span class="line"><span class="attr">appendfsync</span> <span class="string">everysec </span></span><br><span class="line"><span class="comment"># 写命令执行完先放入AOF缓冲区，由操作系统决定何时将缓冲区内容写回磁盘</span></span><br><span class="line"><span class="attr">appendfsync</span> <span class="string">no</span></span><br></pre></td></tr></table></figure><p>三种策略对比：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210725151654046.png" alt="image-20210725151654046"></p><h3 id="1-2-3-AOF文件重写">1.2.3.AOF文件重写</h3><p>因为是记录命令，AOF文件会比RDB文件大的多。而且AOF会记录对同一个key的多次写操作，但只有最后一次写操作才有意义。通过执行<strong>bgrewriteaof</strong>命令，可以让AOF文件执行重写功能，用最少的命令达到相同效果。</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210725151729118.png" alt="image-20210725151729118"></p><p>如图，AOF原本有三个命令，但是<code>set num 123 和 set num 666</code>都是对num的操作，第二次会覆盖第一次的值，因此第一个命令记录下来没有意义。</p><p>所以重写命令后，AOF文件内容就是：<code>mset name jack num 666</code></p><p>Redis也会在触发阈值时自动去重写AOF文件。阈值也可以在redis.conf中配置：</p><figure class="highlight properties"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># AOF文件比上次文件 增长超过多少百分比则触发重写</span></span><br><span class="line"><span class="attr">auto-aof-rewrite-percentage</span> <span class="string">100</span></span><br><span class="line"><span class="comment"># AOF文件体积最小多大以上才触发重写 </span></span><br><span class="line"><span class="attr">auto-aof-rewrite-min-size</span> <span class="string">64mb </span></span><br></pre></td></tr></table></figure><h2 id="1-3-RDB与AOF对比">1.3.RDB与AOF对比</h2><p>RDB和AOF各有自己的优缺点，如果对数据安全性要求较高，在实际开发中往往会<strong>结合</strong>两者来使用。</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210725151940515.png" alt="image-20210725151940515"></p><h1>2.Redis主从</h1><h2 id="2-1-搭建主从架构">2.1.搭建主从架构</h2><p>单节点Redis的并发能力是有上限的，要进一步提高Redis的并发能力，就需要搭建主从集群，实现读写分离。</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210725152037611.png" alt="image-20210725152037611"></p><p>具体搭建流程参考课前资料《<a href="http://xn--Redis-2o3nn90k.md">Redis集群.md</a>》：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210725152052501.png" alt="image-20210725152052501"></p><p>假设有A、 B两个Redis实例，如何让B作为A的slaVe节点？</p><p><strong>在B节点执行命令： replicaof/slaveof A的lP A的port</strong></p><h2 id="2-2-主从数据同步原理">2.2.主从数据同步原理</h2><h3 id="2-2-1-全量同步">2.2.1.全量同步</h3><p>主从第一次建立连接时，会执行<strong>全量同步</strong>，将master节点的所有数据都拷贝给slave节点，流程：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210725152222497.png" alt="image-20210725152222497"></p><p>这里不是repl_baklog_buffer 应该是replication_buffer</p><p>repl_baklog_buffer 是增量复制的时候用的</p><p>这里有一个问题，master如何得知salve是第一次来连接呢？？</p><p>有几个概念，可以作为判断依据：</p><ul><li><strong>Replication Id</strong>：简称replid，是数据集的标记，id一致则说明是同一数据集。每一个master都有唯一的replid，slave则会继承master节点的replid</li><li><strong>offset</strong>：偏移量，随着记录在repl_baklog中的数据增多而逐渐增大。slave完成同步时也会记录当前同步的offset。如果slave的offset小于master的offset，说明slave数据落后于master，需要更新。</li></ul><p>因此slave做数据同步，必须向master声明自己的replication id 和offset，master才可以判断到底需要同步哪些数据。</p><p>因为slave原本也是一个master，有自己的replid和offset，当第一次变成slave，与master建立连接时，发送的replid和offset是自己的replid和offset。</p><p>master判断发现slave发送来的replid与自己的不一致，说明这是一个全新的slave，就知道要做全量同步了。</p><p>master会将自己的replid和offset都发送给这个slave，slave保存这些信息。以后slave的replid就与master一致了。</p><p>因此，<strong>master判断一个节点是否是第一次同步的依据，就是看replid是否一致</strong>。</p><p>如图：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210725152700914.png" alt="image-20210725152700914"></p><p><strong>完整流程描述：</strong></p><ul><li>slave节点请求增量同步</li><li>master节点判断replid，发现不一致，拒绝增量同步</li><li>master将完整内存数据生成RDB，发送RDB到slave</li><li>slave清空本地数据，加载master的RDB</li><li>master将RDB期间的命令记录在repl_baklog，并持续将log中的命令发送给slave</li><li>slave执行接收到的命令，保持与master之间的同步</li></ul><h3 id="2-2-2-增量同步">2.2.2.增量同步</h3><p>全量同步需要先做RDB，然后将RDB文件通过网络传输个slave，成本太高了。因此除了第一次做全量同步，其它大多数时候slave与master都是做<strong>增量同步</strong>。</p><p>什么是增量同步？就是只更新slave与master存在差异的部分数据。如图：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210725153201086.png" alt="image-20210725153201086"></p><p>那么master怎么知道slave与自己的数据差异在哪里呢?</p><h3 id="2-2-3-repl-backlog原理">2.2.3.repl_backlog原理</h3><p>master怎么知道slave与自己的数据差异在哪里呢?</p><p>这就要说到全量同步时的repl_baklog文件了。</p><p>这个文件是一个固定大小的数组，只不过数组是环形，也就是说<strong>角标到达数组末尾后，会再次从0开始读写</strong>，这样数组头部的数据就会被覆盖。</p><p>repl_baklog中会记录Redis处理过的命令日志及offset，包括master当前的offset，和slave已经拷贝到的offset：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210725153359022.png" alt="image-20210725153359022"></p><p>slave与master的offset之间的差异，就是salve需要增量拷贝的数据了。</p><p>随着不断有数据写入，master的offset逐渐变大，slave也不断的拷贝，追赶master的offset：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210725153524190.png" alt="image-20210725153524190"></p><p>直到数组被填满：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210725153715910.png" alt="image-20210725153715910"></p><p>此时，如果有新的数据写入，就会覆盖数组中的旧数据。不过，旧的数据只要是绿色的，说明是已经被同步到slave的数据，即便被覆盖了也没什么影响。因为未同步的仅仅是红色部分。</p><p>但是，如果slave出现网络阻塞，导致master的offset远远超过了slave的offset：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210725153937031.png" alt="image-20210725153937031"></p><p>如果master继续写入新数据，其offset就会覆盖旧的数据，直到将slave现在的offset也覆盖：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210725154155984.png" alt="image-20210725154155984"></p><p>棕色框中的红色部分，就是尚未同步，但是却已经被覆盖的数据。此时如果slave恢复，需要同步，却发现自己的offset都没有了，无法完成增量同步了。只能做全量同步。</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210725154216392.png" alt="image-20210725154216392"></p><h2 id="2-3-主从同步优化">2.3.主从同步优化</h2><p>主从同步可以保证主从数据的一致性，非常重要。</p><p>可以从以下几个方面来优化Redis主从就集群：</p><ul><li>在master中配置repl-diskless-sync yes启用无磁盘复制，避免全量同步时的磁盘IO。</li><li>Redis单节点上的内存占用不要太大，减少RDB导致的过多磁盘IO</li><li>适当提高repl_baklog的大小，发现slave宕机时尽快实现故障恢复，尽可能避免全量同步</li><li>限制一个master上的slave节点数量，如果实在是太多slave，则可以采用主-从-从链式结构，减少master压力</li></ul><p>主从从架构图：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210725154405899.png" alt="image-20210725154405899"></p><h2 id="2-4-小结">2.4.小结</h2><p>简述全量同步和增量同步区别？</p><ul><li>全量同步：master将完整内存数据生成RDB，发送RDB到slave。后续命令则记录在repl_baklog，逐个发送给slave。</li><li>增量同步：slave提交自己的offset到master，master获取repl_baklog中从offset之后的命令给slave</li></ul><p>什么时候执行全量同步？</p><ul><li>slave节点第一次连接master节点时</li><li>slave节点断开时间太久，repl_baklog中的offset已经被覆盖时</li></ul><p>什么时候执行增量同步？</p><ul><li>slave节点断开又恢复，并且在repl_baklog中能找到offset时</li></ul><h1>3.Redis哨兵</h1><p>Redis提供了哨兵（Sentinel）机制来实现主从集群的自动故障恢复。</p><h2 id="3-1-哨兵原理">3.1.哨兵原理</h2><h3 id="3-1-1-集群结构和作用">3.1.1.集群结构和作用</h3><p>哨兵的结构如图：Sentinel也是一个集群</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210725154528072.png" alt="image-20210725154528072"></p><p>哨兵的作用如下：</p><ul><li><strong>监控</strong>：Sentinel 会不断检查您的master和slave是否按预期工作</li><li><strong>自动故障恢复</strong>：如果master故障，Sentinel会将一个slave提升为master。当故障实例恢复后也以新的master为主</li><li><strong>通知</strong>：Sentinel充当Redis客户端的服务发现来源，当集群发生故障转移时，会将最新信息推送给Redis的客户端</li></ul><h3 id="3-1-2-集群监控原理">3.1.2.集群监控原理</h3><p>Sentinel基于<strong>心跳机制</strong>监测服务状态，每隔1秒向集群的每个实例发送ping命令：</p><p>•主观下线：如果某sentinel节点发现某实例未在规定时间响应，则认为该实例<strong>主观下线</strong>。</p><p>•客观下线：若超过指定数量（quorum）的sentinel都认为该实例主观下线，则该实例<strong>客观下线</strong>(客观就是事实)。quorum值最好超过Sentinel实例数量的一半。</p><blockquote><p>ps: quorum和down-after-milliseconds * 10可以在redis.config文件中配置</p></blockquote><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210725154632354.png" alt="image-20210725154632354"></p><h3 id="3-1-3-集群故障恢复原理">3.1.3.集群故障恢复原理</h3><p>一旦发现master故障，sentinel需要在salve中选择一个作为新的master，选择依据是这样的：</p><ul><li>首先会判断slave节点与master节点断开时间长短，如果超过指定值（down-after-milliseconds * 10）则会排除该slave节点</li><li>然后判断slave节点的slave-priority值，越小优先级越高，如果是0则永不参与选举</li><li>如果slave-prority一样，则判断slave节点的offset值，越大说明数据越新，优先级越高</li><li>最后是判断slave节点的运行id大小，越小优先级越高。</li></ul><p>==如何实现故障转移==</p><p>当选出一个新的master后，该如何实现切换呢？</p><p>流程如下：</p><ul><li>sentinel给备选的slave1节点发送slaveof no one命令，让该节点成为master</li><li>sentinel给所有其它slave发送slaveof 192.168.150.101 7002 命令，让这些slave成为新master的从节点，开始从新的master上同步数据。</li><li>最后，sentinel将故障节点标记为slave，当故障节点恢复后会自动成为新的master的slave节点</li></ul><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210725154816841.png" alt="image-20210725154816841"></p><h3 id="3-1-4-小结">3.1.4.小结</h3><p>Sentinel的三个作用是什么？</p><ul><li>监控</li><li>故障转移</li><li>通知</li></ul><p>Sentinel如何判断一个redis实例是否健康？</p><ul><li>每隔1秒发送一次ping命令，如果超过一定时间没有相向则认为是主观下线</li><li>如果大多数sentinel都认为实例主观下线，则判定服务下线</li></ul><p>故障转移步骤有哪些？</p><ul><li>首先选定一个slave作为新的master，执行slaveof no one</li><li>然后让所有节点都执行slaveof 新master</li><li>修改故障节点配置，添加slaveof 新master</li></ul><h2 id="3-2-搭建哨兵集群">3.2.搭建哨兵集群</h2><p>具体搭建流程参考课前资料《<a href="http://xn--Redis-2o3nn90k.md">Redis集群.md</a>》：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210725155019276.png" alt="image-20210725155019276"></p><h2 id="3-3-RedisTemplate">3.3.RedisTemplate</h2><p>在Sentinel集群监管下的Redis主从集群，其节点会因为自动故障转移而发生变化，Redis的客户端必须感知这种变化，及时更新连接信息。Spring的RedisTemplate底层利用lettuce实现了节点的感知和自动切换。</p><p>下面，我们通过一个测试来实现RedisTemplate集成哨兵机制。</p><h3 id="3-3-1-导入Demo工程">3.3.1.导入Demo工程</h3><p>首先，我们引入课前资料提供的Demo工程：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210725155124958.png" alt="image-20210725155124958"></p><h3 id="3-3-2-引入依赖">3.3.2.引入依赖</h3><p>在项目的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></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.springframework.boot<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-boot-starter-data-redis<span class="tag">&lt;/<span class="name">artifactId</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><h3 id="3-3-3-配置Redis地址">3.3.3.配置Redis地址</h3><p>然后在配置文件application.yml中指定redis的sentinel相关信息：</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></pre></td><td class="code"><pre><span class="line">spring:</span><br><span class="line">  redis:</span><br><span class="line">    sentinel:</span><br><span class="line">      master: mymaster</span><br><span class="line">      nodes:</span><br><span class="line">        - <span class="number">192.168</span><span class="number">.150</span><span class="number">.101</span>:<span class="number">27001</span></span><br><span class="line">        - <span class="number">192.168</span><span class="number">.150</span><span class="number">.101</span>:<span class="number">27002</span></span><br><span class="line">        - <span class="number">192.168</span><span class="number">.150</span><span class="number">.101</span>:<span class="number">27003</span></span><br></pre></td></tr></table></figure><h3 id="3-3-4-配置读写分离">3.3.4.配置<strong>读写分离</strong></h3><p>在项目的启动类中，添加一个新的bean：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Bean</span></span><br><span class="line"><span class="keyword">public</span> LettuceClientConfigurationBuilderCustomizer <span class="title function_">clientConfigurationBuilderCustomizer</span><span class="params">()</span>&#123;</span><br><span class="line">    <span class="keyword">return</span> clientConfigurationBuilder -&gt; clientConfigurationBuilder.readFrom(ReadFrom.REPLICA_PREFERRED);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20260520224603675.png" alt="image-20260520224603675"></p><p><strong>也可以使用匿名内部类的形式构建 Alt + Insert 可以快速替换lambda表达式</strong></p><p>这个bean中配置的就是读写策略，包括四种：</p><ul><li><p>MASTER：从主节点读取</p></li><li><p>MASTER_PREFERRED：优先从master节点读取，master不可用才读取replica</p></li><li><p>REPLICA：从slave（replica）节点读取</p></li><li><p>REPLICA _PREFERRED：优先从slave（replica）节点读取，所有的slave都不可用才读取master</p></li><li><p>我们要做的就是配置文件配置节点的信息主要是针对于哨兵的配置,配置好后节点宕机主从之间的切换以及故障转移都是全自动完成的.</p></li></ul><h1>4.Redis分片集群</h1><h2 id="4-1-搭建分片集群">4.1.搭建分片集群</h2><p>主从和哨兵可以解决高可用、高并发读的问题。但是依然有两个问题没有解决：</p><ul><li><p>海量数据存储问题</p></li><li><p>高并发写的问题</p></li></ul><p>使用分片集群可以解决上述问题，如图:</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210725155747294.png" alt="image-20210725155747294"></p><p>分片集群特征：</p><ul><li><p>集群中有多个master，每个master保存不同数据</p></li><li><p>每个master都可以有多个slave节点</p></li><li><p>master之间通过ping监测彼此健康状态</p></li><li><p>客户端请求可以访问集群任意节点，最终都会被转发到正确节点</p></li></ul><p>具体搭建流程参考课前资料《<a href="http://xn--Redis-2o3nn90k.md">Redis集群.md</a>》：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210725155806288.png" alt="image-20210725155806288"></p><h2 id="4-2-散列插槽">4.2.散列插槽</h2><h3 id="4-2-1-插槽原理">4.2.1.插槽原理</h3><p>Redis会把每一个master节点映射到0~16383共16384个插槽（hash slot）上，查看集群信息时就能看到：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210725155820320.png" alt="image-20210725155820320"></p><p>数据key不是与节点绑定，而是与插槽绑定。redis会根据key的有效部分计算插槽值，分两种情况：</p><ul><li>key中包含&quot;{}&quot;，且“{}”中至少包含1个字符，“{}”中的部分是有效部分</li><li>key中不包含“{}”，整个key都是有效部分</li></ul><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20260521014501243.png" alt="image-20260521014501243"></p><p>例如：key是num，那么就根据num计算，如果是{itcast}num，则根据itcast计算。计算方式是利用CRC16算法得到一个hash值，然后对16384取余，得到的结果就是slot值。</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210725155850200.png" alt="image-20210725155850200"></p><p>如图，在7001这个节点执行set a 1时，对a做hash运算，对16384取余，得到的结果是15495，因此要存储到7003节点。</p><p>到了7003后，执行<code>get num</code>时，对num做hash运算，对16384取余，得到的结果是2765，因此需要切换到7001节点</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20260521014501243.png" alt></p><p><strong>key是和插槽绑定的,操作任意一个key 会先计算插槽值在判断在哪个节点 然后完成一个请求的路由或者叫重定向</strong></p><h3 id="4-2-1-小结">4.2.1.小结</h3><p>Redis如何判断某个key应该在哪个实例？</p><ul><li>将16384个插槽分配到不同的实例</li><li>根据key的有效部分计算哈希值，对16384取余</li><li>余数作为插槽，寻找插槽所在实例即可</li></ul><p>如何将同一类数据固定的保存在同一个Redis实例？</p><ul><li>这一类数据使用相同的有效部分，例如key都以{typeId}为前缀</li></ul><h2 id="4-3-集群伸缩">4.3.集群伸缩</h2><p>redis-cli --cluster提供了很多操作集群的命令，可以通过下面方式查看：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210725160138290.png" alt="image-20210725160138290"></p><p>比如，添加节点的命令：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210725160448139.png" alt="image-20210725160448139"></p><div class="tabs"><div class="nav-tabs"><button type="button" class="tab active">添加新节点到redis</button><button type="button" class="tab">删除集群中的一个节点</button></div><div class="tab-contents"><div class="tab-item-content active"><h3 id="4-3-1-需求分析">4.3.1.需求分析</h3><p>需求：向集群中添加一个新的master节点，并向其中存储 num = 10</p><ul><li>启动一个新的redis实例，端口为7004</li><li>添加7004到之前的集群，并作为一个master节点</li><li>给7004节点分配插槽，使得num这个key可以存储到7004实例</li></ul><p>这里需要两个新的功能：</p><ul><li>添加一个节点到集群中</li><li>将部分插槽分配到新插槽</li></ul><h3 id="4-3-2-创建新的redis实例">4.3.2.创建新的redis实例</h3><p>创建一个文件夹：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">mkdir</span> 7004</span><br></pre></td></tr></table></figure><p>拷贝配置文件：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">cp</span> redis.conf /7004</span><br></pre></td></tr></table></figure><p>修改配置文件：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sed -i /s/6379/7004/g 7004/redis.conf</span><br></pre></td></tr></table></figure><p>启动</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">redis-server 7004/redis.conf</span><br></pre></td></tr></table></figure><p>查看7004端口是否已经开启</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ps -ef | grep redis</span><br></pre></td></tr></table></figure><h3 id="4-3-3-添加新节点到redis">4.3.3.添加新节点到redis</h3><p>添加节点的语法如下：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210725160448139.png" alt="image-20210725160448139"></p><p>执行命令：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">redis-cli --cluster add-node  192.168.150.101:7004 192.168.150.101:7001</span><br></pre></td></tr></table></figure><p>通过命令查看集群状态：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">redis-cli -p 7001 cluster nodes</span><br></pre></td></tr></table></figure><p>如图，7004加入了集群，并且默认是一个master节点：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210725161007099.png" alt="image-20210725161007099"></p><p>但是，可以看到7004节点的插槽数量为0，因此没有任何数据可以存储到7004上</p><p>连接redis集群中的某一个端口</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">redis-cli -c -p 7001</span><br></pre></td></tr></table></figure><h3 id="4-3-4-转移插槽">4.3.4.转移插槽</h3><p>我们要将num存储到7004节点，因此需要先看看num的插槽是多少：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210725161241793.png" alt="image-20210725161241793"></p><p>如上图所示，num的插槽为2765.</p><p>我们可以将0~3000的插槽从7001转移到7004，命令格式如下：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210725161401925.png" alt="image-20210725161401925"></p><p><strong>reshard 就重新分片的意思</strong></p><p>具体命令如下：</p><p>建立连接：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210725161506241.png" alt="image-20210725161506241"></p><p>得到下面的反馈：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210725161540841.png" alt="image-20210725161540841"></p><p>询问要移动多少个插槽，我们计划是3000个：</p><p>新的问题来了：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210725161637152.png" alt="image-20210725161637152"></p><p>那个node来接收这些插槽？？</p><p>显然是7004，那么7004节点的id是多少呢？</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210725161731738.png" alt="image-20210725161731738"></p><p>复制这个id，然后拷贝到刚才的控制台后：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210725161817642.png" alt="image-20210725161817642"></p><p>这里询问，你的插槽是从哪里移动过来的？</p><ul><li>all：代表全部，也就是三个节点各转移一部分</li><li>具体的id：目标节点的id</li><li>done：没有了</li></ul><p>这里我们要从7001获取，因此填写7001的id：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210725162030478.png" alt="image-20210725162030478"></p><p>填完后，点击done，这样插槽转移就准备好了：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210725162101228.png" alt="image-20210725162101228"></p><p>确认要转移吗？输入yes：</p><p>然后，通过命令查看结果：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210725162145497.png" alt="image-20210725162145497"></p><p>可以看到：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210725162224058.png" alt="image-20210725162224058"></p><p>目的达成。</p><blockquote><p>数据是跟着插槽走的</p></blockquote></div><div class="tab-item-content"><blockquote><p>删除 集群中的 7004  待实现 TODO</p></blockquote><ul><li>先学消息队列，再学微服务，再学JVM、JUC，或者你可以一边看消息队列和微服务，一边学JVM、JUC，学这两个有JAVA语言基础就行</li></ul></div></div><div class="tab-to-top"><button type="button" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div><h2 id="4-4-故障转移">4.4.故障转移</h2><p>集群初识状态是这样的：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210727161152065.png" alt="image-20210727161152065"></p><p>其中7001、7002、7003都是master，我们计划让7002宕机。</p><p>通过watch的方式监控集群</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">watch redis-cli -p 7001 cluster nodes</span><br></pre></td></tr></table></figure><h3 id="4-4-1-自动故障转移">4.4.1.自动故障转移</h3><p>当集群中有一个master宕机会发生什么呢？</p><p>直接停止一个redis实例，例如7002：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">redis-cli -p 7002 shutdown</span><br></pre></td></tr></table></figure><p>1）首先是该实例与其它实例失去连接</p><p>2）然后是疑似宕机：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210725162319490.png" alt="image-20210725162319490"></p><p>3）最后是确定下线，自动提升一个slave为新的master：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210725162408979.png" alt="image-20210725162408979"></p><p>4）当7002再次启动，就会变为一个slave节点了：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210727160803386.png" alt="image-20210727160803386"></p><blockquote><p>由此可以看出来redis分片集群不需要哨兵(通过心跳监测机制),会实现自动的主从故障切换</p></blockquote><h3 id="4-4-2-手动故障转移-可以无感知升级机器">4.4.2.手动故障转移(可以无感知升级机器)</h3><p>利用cluster failover命令可以手动让集群中的某个master宕机，切换到执行cluster failover命令的这个slave节点，实现无感知的<strong>数据迁移</strong>。其流程如下：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210725162441407.png" alt="image-20210725162441407"></p><p>这种failover命令可以指定三种模式：</p><ul><li>缺省：默认的流程，如图1~6歩</li><li>force：省略了对offset的一致性校验</li><li>takeover：直接执行第5歩，忽略数据一致性、忽略master状态和其它master的意见</li></ul><p><strong>案例需求</strong>：在7002这个slave节点执行手动故障转移，重新夺回master地位</p><p>步骤如下：</p><p>1）利用redis-cli连接7002这个节点</p><p>2）执行cluster failover命令</p><p>如图：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210727160037766.png" alt="image-20210727160037766"></p><p>效果：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20210727161152065.png" alt="image-20210727161152065"></p><h2 id="4-5-RedisTemplate访问分片集群">4.5.RedisTemplate访问分片集群</h2><p>RedisTemplate底层同样基于<strong>lettuce</strong>实现了分片集群的支持，而使用的步骤与哨兵模式基本一致：</p><p>1）引入redis的starter依赖</p><p>2）配置分片集群地址</p><p>3）配置读写分离</p><p>与哨兵模式相比，其中只有分片集群的配置方式略有差异，如下：</p><div class="tabs"><div class="nav-tabs"><button type="button" class="tab active">分片集群的配置方式</button><button type="button" class="tab">哨兵的配置方式</button></div><div class="tab-contents"><div class="tab-item-content active"><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">spring:</span></span><br><span class="line">  <span class="attr">redis:</span></span><br><span class="line">    <span class="attr">cluster:</span></span><br><span class="line">      <span class="attr">nodes:</span></span><br><span class="line">        <span class="bullet">-</span> <span class="number">192.168</span><span class="number">.150</span><span class="number">.101</span><span class="string">:7001</span></span><br><span class="line">        <span class="bullet">-</span> <span class="number">192.168</span><span class="number">.150</span><span class="number">.101</span><span class="string">:7002</span></span><br><span class="line">        <span class="bullet">-</span> <span class="number">192.168</span><span class="number">.150</span><span class="number">.101</span><span class="string">:7003</span></span><br><span class="line">        <span class="bullet">-</span> <span class="number">192.168</span><span class="number">.150</span><span class="number">.101</span><span class="string">:8001</span></span><br><span class="line">        <span class="bullet">-</span> <span class="number">192.168</span><span class="number">.150</span><span class="number">.101</span><span class="string">:8002</span></span><br><span class="line">        <span class="bullet">-</span> <span class="number">192.168</span><span class="number">.150</span><span class="number">.101</span><span class="string">:8003</span></span><br></pre></td></tr></table></figure></div><div class="tab-item-content"><figure class="highlight yml"><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"><span class="attr">spring:</span></span><br><span class="line">  <span class="attr">redis:</span></span><br><span class="line">    <span class="attr">sentinel:</span></span><br><span class="line">      <span class="attr">master:</span> <span class="string">mymaster</span></span><br><span class="line">      <span class="attr">nodes:</span></span><br><span class="line">        <span class="bullet">-</span> <span class="number">192.168</span><span class="number">.150</span><span class="number">.101</span><span class="string">:27001</span></span><br><span class="line">        <span class="bullet">-</span> <span class="number">192.168</span><span class="number">.150</span><span class="number">.101</span><span class="string">:27002</span></span><br><span class="line">        <span class="bullet">-</span> <span class="number">192.168</span><span class="number">.150</span><span class="number">.101</span><span class="string">:27003</span></span><br><span class="line">      </span><br></pre></td></tr></table></figure></div></div><div class="tab-to-top"><button type="button" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div><blockquote><p>分片集群是为了解决单个内存大小上限的问题,并不是为了解决高并发读写的问题,分片集群也要读写分离</p></blockquote>]]>
    </content>
    <id>https://keepstaytrue.github.io/2025/05/18/Redis%E9%AB%98%E7%BA%A7-%E5%88%86%E5%B8%83%E5%BC%8F%E7%BC%93%E5%AD%98/</id>
    <link href="https://keepstaytrue.github.io/2025/05/18/Redis%E9%AB%98%E7%BA%A7-%E5%88%86%E5%B8%83%E5%BC%8F%E7%BC%93%E5%AD%98/"/>
    <published>2025-05-18T15:00:00.000Z</published>
    <summary>
      <![CDATA[<h1>分布式缓存</h1>
<p>– 基于Redis集群解决单机Redis存在的问题</p>
<p>单机的Redis存在四大问题：</p>
<p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@m]]>
    </summary>
    <title>分布式缓存</title>
    <updated>2026-05-23T13:34:01.348Z</updated>
  </entry>
  <entry>
    <author>
      <name>KeepStayTrue</name>
    </author>
    <category term="Java后端求职学习体系" scheme="https://keepstaytrue.github.io/categories/Java%E5%90%8E%E7%AB%AF%E6%B1%82%E8%81%8C%E5%AD%A6%E4%B9%A0%E4%BD%93%E7%B3%BB/"/>
    <category term="中间件" scheme="https://keepstaytrue.github.io/categories/Java%E5%90%8E%E7%AB%AF%E6%B1%82%E8%81%8C%E5%AD%A6%E4%B9%A0%E4%BD%93%E7%B3%BB/%E4%B8%AD%E9%97%B4%E4%BB%B6/"/>
    <category term="redis" scheme="https://keepstaytrue.github.io/categories/Java%E5%90%8E%E7%AB%AF%E6%B1%82%E8%81%8C%E5%AD%A6%E4%B9%A0%E4%BD%93%E7%B3%BB/%E4%B8%AD%E9%97%B4%E4%BB%B6/redis/"/>
    <category term="基础入门(实战篇)" scheme="https://keepstaytrue.github.io/categories/Java%E5%90%8E%E7%AB%AF%E6%B1%82%E8%81%8C%E5%AD%A6%E4%B9%A0%E4%BD%93%E7%B3%BB/%E4%B8%AD%E9%97%B4%E4%BB%B6/redis/%E5%9F%BA%E7%A1%80%E5%85%A5%E9%97%A8-%E5%AE%9E%E6%88%98%E7%AF%87/"/>
    <category term="Redis" scheme="https://keepstaytrue.github.io/tags/Redis/"/>
    <category term="数据库" scheme="https://keepstaytrue.github.io/tags/%E6%95%B0%E6%8D%AE%E5%BA%93/"/>
    <content>
      <![CDATA[<h1>实战篇Redis</h1><h2 id="开篇导读">开篇导读</h2><p>在此非常感谢黑马程序员提供的课程资料!!!</p><ul><li>短信登录</li></ul><p>这一块我们会使用redis共享session来实现</p><ul><li>商户查询缓存</li></ul><p>通过本章节，我们会理解缓存击穿，缓存穿透，缓存雪崩等问题，让小伙伴的对于这些概念的理解不仅仅是停留在概念上，更是能在代码中看到对应的内容</p><ul><li>优惠卷秒杀</li></ul><p>通过本章节，我们可以学会Redis的计数器功能， 结合Lua完成高性能的redis操作，同时学会Redis分布式锁的原理，包括Redis的三种消息队列</p><ul><li>附近的商户</li></ul><p>我们利用Redis的GEOHash来完成对于地理坐标的操作</p><ul><li>UV统计</li></ul><p>主要是使用Redis来完成统计功能</p><ul><li>用户签到</li></ul><p>使用Redis的BitMap数据统计功能</p><ul><li>好友关注</li></ul><p>基于Set集合的关注、取消关注，共同关注等等功能，这一块知识咱们之前就讲过，这次我们在项目中来使用一下</p><ul><li>打人探店</li></ul><p>基于List来完成点赞列表的操作，同时基于SortedSet来完成点赞的排行榜功能</p><p>以上这些内容咱们统统都会给小伙伴们讲解清楚，让大家充分理解如何使用Redis</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653056228879.png" alt></p><h2 id="1、短信登录">1、短信登录</h2><h3 id="1-1、导入黑马点评项目">1.1、导入黑马点评项目</h3><h4 id="1-1-1-、导入SQL">1.1.1 、导入SQL</h4><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653057872536.png" alt></p><h4 id="1-1-2、有关当前模型">1.1.2、有关当前模型</h4><p>手机或者app端发起请求，请求我们的nginx服务器，nginx基于七层模型走的事HTTP协议，可以实现基于Lua直接绕开tomcat访问redis，也可以作为静态资源服务器，轻松扛下上万并发， 负载均衡到下游tomcat服务器，打散流量，我们都知道一台4核8G的tomcat，在优化和处理简单业务的加持下，大不了就处理1000左右的并发， 经过nginx的负载均衡分流后，利用集群支撑起整个项目，同时nginx在部署了前端项目后，更是可以做到动静分离，进一步降低tomcat服务的压力，这些功能都得靠nginx起作用，所以nginx是整个项目中重要的一环。</p><p>在tomcat支撑起并发流量后，我们如果让tomcat直接去访问Mysql，根据经验Mysql企业级服务器只要上点并发，一般是16或32 核心cpu，32 或64G内存，像企业级mysql加上固态硬盘能够支撑的并发，大概就是4000起~7000左右，上万并发， 瞬间就会让Mysql服务器的cpu，硬盘全部打满，容易崩溃，所以我们在高并发场景下，会选择使用mysql集群，同时为了进一步降低Mysql的压力，同时增加访问的性能，我们也会加入Redis，同时使用Redis集群使得Redis对外提供更好的服务。</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653059409865.png" alt></p><h4 id="1-1-3、导入后端项目">1.1.3、导入后端项目</h4><p>在资料中提供了一个项目源码：</p><p><img src alt></p><h4 id="1-1-4、导入前端工程">1.1.4、导入前端工程</h4><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653060337562.png" alt></p><h4 id="1-1-5-运行前端项目">1.1.5 运行前端项目</h4><p><img src alt></p><h3 id="1-2-、基于Session实现登录流程">1.2 、基于Session实现登录流程</h3><p><strong>发送验证码：</strong></p><p>用户在提交手机号后，会校验手机号是否合法，如果不合法，则要求用户重新输入手机号</p><p>如果手机号合法，后台此时生成对应的验证码，同时将验证码进行保存，然后再通过短信的方式将验证码发送给用户</p><p><strong>短信验证码登录、注册：</strong></p><p>用户将验证码和手机号进行输入，后台从session中拿到当前验证码，然后和用户输入的验证码进行校验，如果不一致，则无法通过校验，如果一致，则后台根据手机号查询用户，如果用户不存在，则为用户创建账号信息，保存到数据库，无论是否存在，都会将用户信息保存到session中，方便后续获得当前登录信息</p><p><strong>校验登录状态:</strong></p><p>用户在请求时候，会从cookie中携带者JsessionId到后台，后台通过JsessionId从session中拿到用户信息，如果没有session信息，则进行拦截，如果有session信息，则将用户信息保存到threadLocal中，并且放行</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653066208144.png" alt></p><h3 id="1-3-、实现发送短信验证码功能">1.3 、实现发送短信验证码功能</h3><p><strong>页面流程</strong></p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653067054461.png" alt></p><p><strong>具体代码如下</strong></p><p><strong>贴心小提示：</strong></p><p>具体逻辑上文已经分析，我们仅仅只需要按照提示的逻辑写出代码即可。</p><ul><li>发送验证码</li></ul><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></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">sendCode</span><span class="params">(String phone, HttpSession session)</span> &#123;</span><br><span class="line">    <span class="comment">// 1.校验手机号</span></span><br><span class="line">    <span class="keyword">if</span> (RegexUtils.isPhoneInvalid(phone)) &#123;</span><br><span class="line">        <span class="comment">// 2.如果不符合，返回错误信息</span></span><br><span class="line">        <span class="keyword">return</span> Result.fail(<span class="string">&quot;手机号格式错误！&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 3.符合，生成验证码</span></span><br><span class="line">    <span class="type">String</span> <span class="variable">code</span> <span class="operator">=</span> RandomUtil.randomNumbers(<span class="number">6</span>);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 4.保存验证码到 session</span></span><br><span class="line">    session.setAttribute(<span class="string">&quot;code&quot;</span>,code);</span><br><span class="line">    <span class="comment">// 5.发送验证码</span></span><br><span class="line">    log.debug(<span class="string">&quot;发送短信验证码成功，验证码：&#123;&#125;&quot;</span>, code);</span><br><span class="line">    <span class="comment">// 返回ok</span></span><br><span class="line">    <span class="keyword">return</span> Result.ok();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>登录</li></ul><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></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">login</span><span class="params">(LoginFormDTO loginForm, HttpSession session)</span> &#123;</span><br><span class="line">    <span class="comment">// 1.校验手机号</span></span><br><span class="line">    <span class="type">String</span> <span class="variable">phone</span> <span class="operator">=</span> loginForm.getPhone();</span><br><span class="line">    <span class="keyword">if</span> (RegexUtils.isPhoneInvalid(phone)) &#123;</span><br><span class="line">        <span class="comment">// 2.如果不符合，返回错误信息</span></span><br><span class="line">        <span class="keyword">return</span> Result.fail(<span class="string">&quot;手机号格式错误！&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 3.校验验证码</span></span><br><span class="line">    <span class="type">Object</span> <span class="variable">cacheCode</span> <span class="operator">=</span> session.getAttribute(<span class="string">&quot;code&quot;</span>);</span><br><span class="line">    <span class="type">String</span> <span class="variable">code</span> <span class="operator">=</span> loginForm.getCode();</span><br><span class="line">    <span class="keyword">if</span>(cacheCode == <span class="literal">null</span> || !cacheCode.toString().equals(code))&#123;</span><br><span class="line">         <span class="comment">//3.不一致，报错</span></span><br><span class="line">        <span class="keyword">return</span> Result.fail(<span class="string">&quot;验证码错误&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">//一致，根据手机号查询用户</span></span><br><span class="line">    <span class="type">User</span> <span class="variable">user</span> <span class="operator">=</span> query().eq(<span class="string">&quot;phone&quot;</span>, phone).one();</span><br><span class="line"></span><br><span class="line">    <span class="comment">//5.判断用户是否存在</span></span><br><span class="line">    <span class="keyword">if</span>(user == <span class="literal">null</span>)&#123;</span><br><span class="line">        <span class="comment">//不存在，则创建</span></span><br><span class="line">        user =  createUserWithPhone(phone);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">//7.保存用户信息到session中</span></span><br><span class="line">    session.setAttribute(<span class="string">&quot;user&quot;</span>,user);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> Result.ok();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="1-4、实现登录拦截功能">1.4、实现登录拦截功能</h3><p><strong>温馨小贴士：tomcat的运行原理</strong></p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653068196656.png" alt></p><p>当用户发起请求时，会访问我们像tomcat注册的端口，任何程序想要运行，都需要有一个线程对当前端口号进行监听，tomcat也不例外，当监听线程知道用户想要和tomcat连接连接时，那会由监听线程创建socket连接，socket都是成对出现的，用户通过socket像互相传递数据，当tomcat端的socket接受到数据后，此时监听线程会从tomcat的线程池中取出一个线程执行用户请求，在我们的服务部署到tomcat后，线程会找到用户想要访问的工程，然后用这个线程转发到工程中的controller，service，dao中，并且访问对应的DB，在用户执行完请求后，再统一返回，再找到tomcat端的socket，再将数据写回到用户端的socket，完成请求和响应</p><p>通过以上讲解，我们可以得知 每个用户其实对应都是去找tomcat线程池中的一个线程来完成工作的， 使用完成后再进行回收，既然每个请求都是独立的，所以在每个用户去访问我们的工程时，我们可以使用threadlocal来做到线程隔离，每个线程操作自己的一份数据</p><p><strong>温馨小贴士：关于threadlocal</strong></p><p>如果小伙伴们看过threadLocal的源码，你会发现在threadLocal中，无论是他的put方法和他的get方法， 都是先从获得当前用户的线程，然后从线程中取出线程的成员变量map，只要线程不一样，map就不一样，所以可以通过这种方式来做到线程隔离</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653068874258.png" alt></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><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">LoginInterceptor</span> <span class="keyword">implements</span> <span class="title class_">HandlerInterceptor</span> &#123;</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="type">boolean</span> <span class="title function_">preHandle</span><span class="params">(HttpServletRequest request, HttpServletResponse response, Object handler)</span> <span class="keyword">throws</span> Exception &#123;</span><br><span class="line">       <span class="comment">//1.获取session</span></span><br><span class="line">        <span class="type">HttpSession</span> <span class="variable">session</span> <span class="operator">=</span> request.getSession();</span><br><span class="line">        <span class="comment">//2.获取session中的用户</span></span><br><span class="line">        <span class="type">Object</span> <span class="variable">user</span> <span class="operator">=</span> session.getAttribute(<span class="string">&quot;user&quot;</span>);</span><br><span class="line">        <span class="comment">//3.判断用户是否存在</span></span><br><span class="line">        <span class="keyword">if</span>(user == <span class="literal">null</span>)&#123;</span><br><span class="line">              <span class="comment">//4.不存在，拦截，返回401状态码</span></span><br><span class="line">              response.setStatus(<span class="number">401</span>);</span><br><span class="line">              <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">//5.存在，保存用户信息到Threadlocal</span></span><br><span class="line">        UserHolder.saveUser((User)user);</span><br><span class="line">        <span class="comment">//6.放行</span></span><br><span class="line">        <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>让拦截器生效</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Configuration</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">MvcConfig</span> <span class="keyword">implements</span> <span class="title class_">WebMvcConfigurer</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Resource</span></span><br><span class="line">    <span class="keyword">private</span> StringRedisTemplate stringRedisTemplate;</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_">addInterceptors</span><span class="params">(InterceptorRegistry registry)</span> &#123;</span><br><span class="line">        <span class="comment">// 登录拦截器</span></span><br><span class="line">        registry.addInterceptor(<span class="keyword">new</span> <span class="title class_">LoginInterceptor</span>())</span><br><span class="line">                .excludePathPatterns(</span><br><span class="line">                        <span class="string">&quot;/shop/**&quot;</span>,</span><br><span class="line">                        <span class="string">&quot;/voucher/**&quot;</span>,</span><br><span class="line">                        <span class="string">&quot;/shop-type/**&quot;</span>,</span><br><span class="line">                        <span class="string">&quot;/upload/**&quot;</span>,</span><br><span class="line">                        <span class="string">&quot;/blog/hot&quot;</span>,</span><br><span class="line">                        <span class="string">&quot;/user/code&quot;</span>,</span><br><span class="line">                        <span class="string">&quot;/user/login&quot;</span></span><br><span class="line">                ).order(<span class="number">1</span>);</span><br><span class="line">        <span class="comment">// token刷新的拦截器</span></span><br><span class="line">        registry.addInterceptor(<span class="keyword">new</span> <span class="title class_">RefreshTokenInterceptor</span>(stringRedisTemplate)).addPathPatterns(<span class="string">&quot;/**&quot;</span>).order(<span class="number">0</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="1-5、隐藏用户敏感信息">1.5、隐藏用户敏感信息</h3><p>我们通过浏览器观察到此时用户的全部信息都在，这样极为不靠谱，所以我们应当在返回用户信息之前，将用户的敏感信息进行隐藏，采用的核心思路就是书写一个UserDto对象，这个UserDto对象就没有敏感信息了，我们在返回前，将有用户敏感信息的User对象转化成没有敏感信息的UserDto对象，那么就能够避免这个尴尬的问题了</p><p><strong>在登录方法处修改</strong></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="comment">//7.保存用户信息到session中</span></span><br><span class="line">session.setAttribute(<span class="string">&quot;user&quot;</span>, BeanUtils.copyProperties(user,UserDTO.class));</span><br></pre></td></tr></table></figure><p><strong>在拦截器处：</strong></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="comment">//5.存在，保存用户信息到Threadlocal</span></span><br><span class="line">UserHolder.saveUser((UserDTO) user);</span><br></pre></td></tr></table></figure><p><strong>在UserHolder处：将user对象换成UserDTO</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">UserHolder</span> &#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> ThreadLocal&lt;UserDTO&gt; tl = <span class="keyword">new</span> <span class="title class_">ThreadLocal</span>&lt;&gt;();</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">saveUser</span><span class="params">(UserDTO user)</span>&#123;</span><br><span class="line">        tl.set(user);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> UserDTO <span class="title function_">getUser</span><span class="params">()</span>&#123;</span><br><span class="line">        <span class="keyword">return</span> tl.get();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">removeUser</span><span class="params">()</span>&#123;</span><br><span class="line">        tl.remove();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="1-6、session共享问题">1.6、session共享问题</h3><p><strong>核心思路分析：</strong></p><p>每个tomcat中都有一份属于自己的session,假设用户第一次访问第一台tomcat，并且把自己的信息存放到第一台服务器的session中，但是第二次这个用户访问到了第二台tomcat，那么在第二台服务器上，肯定没有第一台服务器存放的session，所以此时 整个登录拦截功能就会出现问题，我们能如何解决这个问题呢？早期的方案是session拷贝，就是说虽然每个tomcat上都有不同的session，但是每当任意一台服务器的session修改时，都会同步给其他的Tomcat服务器的session，这样的话，就可以实现session的共享了</p><p>但是这种方案具有两个大问题</p><p>1、每台服务器中都有完整的一份session数据，服务器压力过大。</p><p>2、session拷贝数据时，可能会出现延迟</p><p>所以咱们后来采用的方案都是基于redis来完成，我们把session换成redis，redis数据本身就是共享的，就可以避免session共享的问题了</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653069893050.png" alt></p><h3 id="1-7-Redis代替session的业务流程">1.7 Redis代替session的业务流程</h3><h4 id="1-7-1、设计key的结构">1.7.1、设计key的结构</h4><p>首先我们要思考一下利用redis来存储数据，那么到底使用哪种结构呢？由于存入的数据比较简单，我们可以考虑使用String，或者是使用哈希，如下图，如果使用String，同学们注意他的value，用多占用一点空间，如果使用哈希，则他的value中只会存储他数据本身，如果不是特别在意内存，其实使用String就可以啦。</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653319261433.png" alt></p><h4 id="1-7-2、设计key的具体细节">1.7.2、设计key的具体细节</h4><p>所以我们可以使用String结构，就是一个简单的key，value键值对的方式，但是关于key的处理，session他是每个用户都有自己的session，但是redis的key是共享的，咱们就不能使用code了</p><p>在设计这个key的时候，我们之前讲过需要满足两点</p><p>1、key要具有唯一性</p><p>2、key要方便携带</p><p>如果我们采用phone：手机号这个的数据来存储当然是可以的，但是如果把这样的敏感数据存储到redis中并且从页面中带过来毕竟不太合适，所以我们在后台生成一个随机串token，然后让前端带来这个token就能完成我们的整体逻辑了</p><h4 id="1-7-3、整体访问流程">1.7.3、整体访问流程</h4><p>当注册完成后，用户去登录会去校验用户提交的手机号和验证码，是否一致，如果一致，则根据手机号查询用户信息，不存在则新建，最后将用户数据保存到redis，并且生成token作为redis的key，当我们校验用户是否登录时，会去携带着token进行访问，从redis中取出token对应的value，判断是否存在这个数据，如果没有则拦截，如果存在则将其保存到threadLocal中，并且放行。</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653319474181.png" alt></p><h3 id="1-8-基于Redis实现短信登录">1.8 基于Redis实现短信登录</h3><p>这里具体逻辑就不分析了，之前咱们已经重点分析过这个逻辑啦。</p><p><strong>UserServiceImpl代码</strong></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></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">login</span><span class="params">(LoginFormDTO loginForm, HttpSession session)</span> &#123;</span><br><span class="line">    <span class="comment">// 1.校验手机号</span></span><br><span class="line">    <span class="type">String</span> <span class="variable">phone</span> <span class="operator">=</span> loginForm.getPhone();</span><br><span class="line">    <span class="keyword">if</span> (RegexUtils.isPhoneInvalid(phone)) &#123;</span><br><span class="line">        <span class="comment">// 2.如果不符合，返回错误信息</span></span><br><span class="line">        <span class="keyword">return</span> Result.fail(<span class="string">&quot;手机号格式错误！&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 3.从redis获取验证码并校验</span></span><br><span class="line">    <span class="type">String</span> <span class="variable">cacheCode</span> <span class="operator">=</span> stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);</span><br><span class="line">    <span class="type">String</span> <span class="variable">code</span> <span class="operator">=</span> loginForm.getCode();</span><br><span class="line">    <span class="keyword">if</span> (cacheCode == <span class="literal">null</span> || !cacheCode.equals(code)) &#123;</span><br><span class="line">        <span class="comment">// 不一致，报错</span></span><br><span class="line">        <span class="keyword">return</span> Result.fail(<span class="string">&quot;验证码错误&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 4.一致，根据手机号查询用户 select * from tb_user where phone = ?</span></span><br><span class="line">    <span class="type">User</span> <span class="variable">user</span> <span class="operator">=</span> query().eq(<span class="string">&quot;phone&quot;</span>, phone).one();</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 5.判断用户是否存在</span></span><br><span class="line">    <span class="keyword">if</span> (user == <span class="literal">null</span>) &#123;</span><br><span class="line">        <span class="comment">// 6.不存在，创建新用户并保存</span></span><br><span class="line">        user = createUserWithPhone(phone);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 7.保存用户信息到 redis中</span></span><br><span class="line">    <span class="comment">// 7.1.随机生成token，作为登录令牌</span></span><br><span class="line">    <span class="type">String</span> <span class="variable">token</span> <span class="operator">=</span> UUID.randomUUID().toString(<span class="literal">true</span>);</span><br><span class="line">    <span class="comment">// 7.2.将User对象转为HashMap存储</span></span><br><span class="line">    <span class="type">UserDTO</span> <span class="variable">userDTO</span> <span class="operator">=</span> BeanUtil.copyProperties(user, UserDTO.class);</span><br><span class="line">    Map&lt;String, Object&gt; userMap = BeanUtil.beanToMap(userDTO, <span class="keyword">new</span> <span class="title class_">HashMap</span>&lt;&gt;(),</span><br><span class="line">            CopyOptions.create()</span><br><span class="line">                    .setIgnoreNullValue(<span class="literal">true</span>)</span><br><span class="line">                    .setFieldValueEditor((fieldName, fieldValue) -&gt; fieldValue.toString()));</span><br><span class="line">    <span class="comment">// 7.3.存储</span></span><br><span class="line">    <span class="type">String</span> <span class="variable">tokenKey</span> <span class="operator">=</span> LOGIN_USER_KEY + token;</span><br><span class="line">    stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);</span><br><span class="line">    <span class="comment">// 7.4.设置token有效期</span></span><br><span class="line">    stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 8.返回token</span></span><br><span class="line">    <span class="keyword">return</span> Result.ok(token);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="1-9-解决状态登录刷新问题">1.9 解决状态登录刷新问题</h3><h4 id="1-9-1-初始方案思路总结：">1.9.1 初始方案思路总结：</h4><p>在这个方案中，他确实可以使用对应路径的拦截，同时刷新登录token令牌的存活时间，但是现在这个拦截器他只是拦截需要被拦截的路径，假设当前用户访问了一些不需要拦截的路径，那么这个拦截器就不会生效，所以此时令牌刷新的动作实际上就不会执行，所以这个方案他是存在问题的</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653320822964.png" alt></p><h4 id="1-9-2-优化方案">1.9.2 优化方案</h4><p>既然之前的拦截器无法对不需要拦截的路径生效，那么我们可以添加一个拦截器，在第一个拦截器中拦截所有的路径，把第二个拦截器做的事情放入到第一个拦截器中，同时刷新令牌，因为第一个拦截器有了threadLocal的数据，所以此时第二个拦截器只需要判断拦截器中的user对象是否存在即可，完成整体刷新功能。</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653320764547.png" alt></p><h4 id="1-9-3-代码">1.9.3 代码</h4><p><strong>RefreshTokenInterceptor</strong></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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">RefreshTokenInterceptor</span> <span class="keyword">implements</span> <span class="title class_">HandlerInterceptor</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> StringRedisTemplate stringRedisTemplate;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="title function_">RefreshTokenInterceptor</span><span class="params">(StringRedisTemplate stringRedisTemplate)</span> &#123;</span><br><span class="line">        <span class="built_in">this</span>.stringRedisTemplate = stringRedisTemplate;</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="type">boolean</span> <span class="title function_">preHandle</span><span class="params">(HttpServletRequest request, HttpServletResponse response, Object handler)</span> <span class="keyword">throws</span> Exception &#123;</span><br><span class="line">        <span class="comment">// 1.获取请求头中的token</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">token</span> <span class="operator">=</span> request.getHeader(<span class="string">&quot;authorization&quot;</span>);</span><br><span class="line">        <span class="keyword">if</span> (StrUtil.isBlank(token)) &#123;</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">// 2.基于TOKEN获取redis中的用户</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">key</span>  <span class="operator">=</span> LOGIN_USER_KEY + token;</span><br><span class="line">        Map&lt;Object, Object&gt; userMap = stringRedisTemplate.opsForHash().entries(key);</span><br><span class="line">        <span class="comment">// 3.判断用户是否存在</span></span><br><span class="line">        <span class="keyword">if</span> (userMap.isEmpty()) &#123;</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">// 5.将查询到的hash数据转为UserDTO</span></span><br><span class="line">        <span class="type">UserDTO</span> <span class="variable">userDTO</span> <span class="operator">=</span> BeanUtil.fillBeanWithMap(userMap, <span class="keyword">new</span> <span class="title class_">UserDTO</span>(), <span class="literal">false</span>);</span><br><span class="line">        <span class="comment">// 6.存在，保存用户信息到 ThreadLocal</span></span><br><span class="line">        UserHolder.saveUser(userDTO);</span><br><span class="line">        <span class="comment">// 7.刷新token有效期</span></span><br><span class="line">        stringRedisTemplate.expire(key, LOGIN_USER_TTL, TimeUnit.MINUTES);</span><br><span class="line">        <span class="comment">// 8.放行</span></span><br><span class="line">        <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">afterCompletion</span><span class="params">(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)</span> <span class="keyword">throws</span> Exception &#123;</span><br><span class="line">        <span class="comment">// 移除用户</span></span><br><span class="line">        UserHolder.removeUser();</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><strong>LoginInterceptor</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">LoginInterceptor</span> <span class="keyword">implements</span> <span class="title class_">HandlerInterceptor</span> &#123;</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="type">boolean</span> <span class="title function_">preHandle</span><span class="params">(HttpServletRequest request, HttpServletResponse response, Object handler)</span> <span class="keyword">throws</span> Exception &#123;</span><br><span class="line">        <span class="comment">// 1.判断是否需要拦截（ThreadLocal中是否有用户）</span></span><br><span class="line">        <span class="keyword">if</span> (UserHolder.getUser() == <span class="literal">null</span>) &#123;</span><br><span class="line">            <span class="comment">// 没有，需要拦截，设置状态码</span></span><br><span class="line">            response.setStatus(<span class="number">401</span>);</span><br><span class="line">            <span class="comment">// 拦截</span></span><br><span class="line">            <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">// 有用户，则放行</span></span><br><span class="line">        <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="2、商户查询缓存">2、商户查询缓存</h2><h3 id="2-1-什么是缓存">2.1 什么是缓存?</h3><p><strong>前言</strong>:<strong>什么是缓存?</strong></p><p>就像自行车,越野车的避震器<img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/%E9%81%BF%E9%9C%87%E5%99%A8.gif" alt></p><p>举个例子:越野车,山地自行车,都拥有&quot;避震器&quot;,<strong>防止</strong>车体加速后因惯性,在酷似&quot;U&quot;字母的地形上飞跃,硬着陆导致的<strong>损害</strong>,像个弹簧一样;</p><p>同样,实际开发中,系统也需要&quot;避震器&quot;,防止过高的数据访问猛冲系统,导致其操作线程无法及时处理信息而瘫痪;</p><p>这在实际开发中对企业讲,对产品口碑,用户评价都是致命的;所以企业非常重视缓存技术;</p><p><strong>缓存(<strong>Cache),就是数据交换的</strong>缓冲区</strong>,俗称的缓存就是<strong>缓冲区内的数据</strong>,一般从数据库中获取,存储于本地代码(例如:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">例<span class="number">1</span>:Static <span class="keyword">final</span> ConcurrentHashMap&lt;K,V&gt; map = <span class="keyword">new</span> <span class="title class_">ConcurrentHashMap</span>&lt;&gt;(); 本地用于高并发</span><br><span class="line"></span><br><span class="line">例<span class="number">2</span>:<span class="keyword">static</span> <span class="keyword">final</span> Cache&lt;K,V&gt; USER_CACHE = CacheBuilder.newBuilder().build(); 用于redis等缓存</span><br><span class="line"></span><br><span class="line">例<span class="number">3</span>:Static <span class="keyword">final</span> Map&lt;K,V&gt; map =  <span class="keyword">new</span> <span class="title class_">HashMap</span>(); 本地缓存</span><br></pre></td></tr></table></figure><p>由于其被<strong>Static</strong>修饰,所以随着类的加载而被加载到<strong>内存之中</strong>,作为本地缓存,由于其又被<strong>final</strong>修饰,所以其引用(例3:map)和对象(例3:new HashMap())之间的关系是固定的,不能改变,因此不用担心赋值(=)导致缓存失效;</p><h4 id="2-1-1-为什么要使用缓存">2.1.1 为什么要使用缓存</h4><p>一句话:因为<strong>速度快,好用</strong></p><p>缓存数据存储于代码中,而代码运行在内存中,内存的读写性能远高于磁盘,缓存可以大大降低<strong>用户访问并发量带来的</strong>服务器读写压力</p><p>实际开发过程中,企业的数据量,少则几十万,多则几千万,这么大数据量,如果没有缓存来作为&quot;避震器&quot;,系统是几乎撑不住的,所以企业会大量运用到缓存技术;</p><p>但是缓存也会增加代码复杂度和运营的成本:</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20220523214414123.png" alt></p><h4 id="2-1-2-如何使用缓存">2.1.2 如何使用缓存</h4><p>实际开发中,会构筑多级缓存来使系统运行速度进一步提升,例如:本地缓存与redis中的缓存并发使用</p><p><strong>浏览器缓存</strong>：主要是存在于浏览器端的缓存</p><p>**应用层缓存：**可以分为tomcat本地缓存，比如之前提到的map，或者是使用redis作为缓存</p><p>**数据库缓存：**在数据库中有一片空间是 buffer pool，增改查数据都会先加载到mysql的缓存中</p><p>**CPU缓存：**当代计算机最大的问题是 cpu性能提升了，但内存读写速度没有跟上，所以为了适应当下的情况，增加了cpu的L1，L2，L3级的缓存</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/image-20220523212915666.png" alt></p><h3 id="2-2-添加商户缓存">2.2 添加商户缓存</h3><p>在我们查询商户信息时，我们是直接操作从数据库中去进行查询的，大致逻辑是这样，直接查询数据库那肯定慢咯，所以我们需要增加缓存</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">@GetMapping(&quot;/&#123;id&#125;&quot;)</span><br><span class="line">public Result queryShopById(@PathVariable(&quot;id&quot;) Long id) &#123;</span><br><span class="line">    //这里是直接查询数据库</span><br><span class="line">    return shopService.queryById(id);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="2-2-1-、缓存模型和思路">2.2.1 、缓存模型和思路</h4><p>标准的操作方式就是查询数据库之前先查询缓存，如果缓存数据存在，则直接从缓存中返回，如果缓存数据不存在，再查询数据库，然后将数据存入redis。</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653322097736.png" alt></p><h4 id="2-1-2、代码如下">2.1.2、代码如下</h4><p>代码思路：如果缓存有，则直接返回，如果缓存不存在，则查询数据库，然后存入redis。</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653322190155.png" alt></p><h3 id="2-3-缓存更新策略">2.3 缓存更新策略</h3><p>缓存更新是redis为了节约内存而设计出来的一个东西，主要是因为内存数据宝贵，当我们向redis插入太多数据，此时就可能会导致缓存中的数据过多，所以redis会对部分数据进行更新，或者把他叫为淘汰更合适。</p><p>**内存淘汰：**redis自动进行，当redis内存达到咱们设定的max-memery的时候，会自动触发淘汰机制，淘汰掉一些不重要的数据(可以自己设置策略方式)</p><p>**超时剔除：**当我们给redis设置了过期时间ttl之后，redis会将超时的数据进行删除，方便咱们继续使用缓存</p><p>**主动更新：**我们可以手动调用方法把缓存删掉，通常用于解决缓存和数据库不一致问题</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653322506393.png" alt></p><h4 id="2-3-1-、数据库缓存不一致解决方案：">2.3.1 、数据库缓存不一致解决方案：</h4><p>由于我们的<strong>缓存的数据源来自于数据库</strong>,而数据库的<strong>数据是会发生变化的</strong>,因此,如果当数据库中<strong>数据发生变化,而缓存却没有同步</strong>,此时就会有<strong>一致性问题存在</strong>,其后果是:</p><p>用户使用缓存中的过时数据,就会产生类似多线程数据安全问题,从而影响业务,产品口碑等;怎么解决呢？有如下几种方案</p><p>Cache Aside Pattern 人工编码方式：缓存调用者在更新完数据库后再去更新缓存，也称之为双写方案</p><p>Read/Write Through Pattern : 由系统本身完成，数据库与缓存的问题交由系统本身去处理</p><p>Write Behind Caching Pattern ：调用者只操作缓存，其他线程去异步处理数据库，实现最终一致</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653322857620.png" alt></p><h4 id="2-3-2-、数据库和缓存不一致采用什么方案">2.3.2 、数据库和缓存不一致采用什么方案</h4><p>综合考虑使用方案一，但是方案一调用者如何处理呢？这里有几个问题</p><p>操作缓存和数据库时有三个问题需要考虑：</p><p>如果采用第一个方案，那么假设我们每次操作数据库后，都操作缓存，但是中间如果没有人查询，那么这个更新动作实际上只有最后一次生效，中间的更新动作意义并不大，我们可以把缓存删除，等待再次查询时，将缓存中的数据加载出来</p><ul><li><p>删除缓存还是更新缓存？</p><ul><li>更新缓存：每次更新数据库都更新缓存，无效写操作较多</li><li>删除缓存：更新数据库时让缓存失效，查询时再更新缓存</li></ul></li><li><p>如何保证缓存与数据库的操作的同时成功或失败？</p><ul><li>单体系统，将缓存与数据库操作放在一个事务</li><li>分布式系统，利用TCC等分布式事务方案</li></ul></li></ul><p>应该具体操作缓存还是操作数据库，我们应当是先操作数据库，再删除缓存，原因在于，如果你选择第一种方案，在两个线程并发来访问时，假设线程1先来，他先把缓存删了，此时线程2过来，他查询缓存数据并不存在，此时他写入缓存，当他写入缓存后，线程1再执行更新动作时，实际上写入的就是旧的数据，新的数据被旧数据覆盖了。</p><ul><li>先操作缓存还是先操作数据库？<ul><li>先删除缓存，再操作数据库</li><li>先操作数据库，再删除缓存</li></ul></li></ul><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653323595206.png" alt></p><h3 id="2-4-实现商铺和缓存与数据库双写一致">2.4 实现商铺和缓存与数据库双写一致</h3><p>核心思路如下：</p><p>修改ShopController中的业务逻辑，满足下面的需求：</p><p>根据id查询店铺时，如果缓存未命中，则查询数据库，将数据库结果写入缓存，并设置超时时间</p><p>根据id修改店铺时，先修改数据库，再删除缓存</p><p><strong>修改重点代码1</strong>：修改<strong>ShopServiceImpl</strong>的queryById方法</p><p><strong>设置redis缓存时添加过期时间</strong></p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653325871232.png" alt></p><p><strong>修改重点代码2</strong></p><p>代码分析：通过之前的淘汰，我们确定了采用删除策略，来解决双写问题，当我们修改了数据之后，然后把缓存中的数据进行删除，查询时发现缓存中没有数据，则会从mysql中加载最新的数据，从而避免数据库和缓存不一致的问题</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653325929549.png" alt></p><h3 id="2-5-缓存穿透问题的解决思路">2.5 缓存穿透问题的解决思路</h3><p>缓存穿透 ：缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在，这样缓存永远不会生效，这些请求都会打到数据库。</p><p>常见的解决方案有两种：</p><ul><li>缓存空对象<ul><li>优点：实现简单，维护方便</li><li>缺点：<ul><li>额外的内存消耗</li><li>可能造成短期的不一致</li></ul></li></ul></li><li>布隆过滤<ul><li>优点：内存占用较少，没有多余key</li><li>缺点：<ul><li>实现复杂</li><li>存在误判可能</li></ul></li></ul></li></ul><p>**缓存空对象思路分析：**当我们客户端访问不存在的数据时，先请求redis，但是此时redis中没有数据，此时会访问到数据库，但是数据库中也没有数据，这个数据穿透了缓存，直击数据库，我们都知道数据库能够承载的并发不如redis这么高，如果大量的请求同时过来访问这种不存在的数据，这些请求就都会访问到数据库，简单的解决方案就是哪怕这个数据在数据库中也不存在，我们也把这个数据存入到redis中去，这样，下次用户过来访问这个不存在的数据，那么在redis中也能找到这个数据就不会进入到缓存了</p><p>**布隆过滤：**布隆过滤器其实采用的是哈希思想来解决这个问题，通过一个庞大的二进制数组，走哈希思想去判断当前这个要查询的这个数据是否存在，如果布隆过滤器判断存在，则放行，这个请求会去访问redis，哪怕此时redis中的数据过期了，但是数据库中一定存在这个数据，在数据库中查询出来这个数据后，再将其放入到redis中，</p><p>假设布隆过滤器判断这个数据不存在，则直接返回</p><p>这种方式优点在于节约内存空间，存在误判，误判原因在于：布隆过滤器走的是哈希思想，只要哈希思想，就可能存在哈希冲突</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653326156516.png" alt></p><h3 id="2-6-编码解决商品查询的缓存穿透问题：">2.6 编码解决商品查询的缓存穿透问题：</h3><p>核心思路如下：</p><p>在原来的逻辑中，我们如果发现这个数据在mysql中不存在，直接就返回404了，这样是会存在缓存穿透问题的</p><p>现在的逻辑中：如果这个数据不存在，我们不会返回404 ，还是会把这个数据写入到Redis中，并且将value设置为空，欧当再次发起查询时，我们如果发现命中之后，判断这个value是否是null，如果是null，则是之前写入的数据，证明是缓存穿透数据，如果不是，则直接返回数据。</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653327124561.png" alt></p><p><strong>小总结：</strong></p><p>缓存穿透产生的原因是什么？</p><ul><li>用户请求的数据在缓存中和数据库中都不存在，不断发起这样的请求，给数据库带来巨大压力</li></ul><p>缓存穿透的解决方案有哪些？</p><ul><li>缓存null值</li><li>布隆过滤</li><li>增强id的复杂度，避免被猜测id规律</li><li>做好数据的基础格式校验</li><li>加强用户权限校验</li><li>做好热点参数的限流</li></ul><h3 id="2-7-缓存雪崩问题及解决思路">2.7 缓存雪崩问题及解决思路</h3><p>缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机，导致大量请求到达数据库，带来巨大压力。</p><p>解决方案：</p><ul><li>给不同的Key的TTL添加随机值</li><li>利用Redis集群提高服务的可用性</li><li>给缓存业务添加降级限流策略</li><li>给业务添加多级缓存</li></ul><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653327884526.png" alt></p><h3 id="2-8-缓存击穿问题及解决思路">2.8 缓存击穿问题及解决思路</h3><p>缓存击穿问题也叫热点Key问题，就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了，无数的请求访问会在瞬间给数据库带来巨大的冲击。</p><p>常见的解决方案有两种：</p><ul><li>互斥锁</li><li>逻辑过期</li></ul><p>逻辑分析：假设线程1在查询缓存之后，本来应该去查询数据库，然后把这个数据重新加载到缓存的，此时只要线程1走完这个逻辑，其他线程就都能从缓存中加载这些数据了，但是假设在线程1没有走完的时候，后续的线程2，线程3，线程4同时过来访问当前这个方法， 那么这些线程都不能从缓存中查询到数据，那么他们就会同一时刻来访问查询缓存，都没查到，接着同一时间去访问数据库，同时的去执行数据库代码，对数据库访问压力过大</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653328022622.png" alt></p><p>解决方案一、使用锁来解决：</p><p>因为锁能实现互斥性。假设线程过来，只能一个人一个人的来访问数据库，从而避免对于数据库访问压力过大，但这也会影响查询的性能，因为此时会让查询的性能从并行变成了串行，我们可以采用tryLock方法 + double check来解决这样的问题。</p><p>假设现在线程1过来访问，他查询缓存没有命中，但是此时他获得到了锁的资源，那么线程1就会一个人去执行逻辑，假设现在线程2过来，线程2在执行过程中，并没有获得到锁，那么线程2就可以进行到休眠，直到线程1把锁释放后，线程2获得到锁，然后再来执行逻辑，此时就能够从缓存中拿到数据了。</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653328288627.png" alt></p><p>解决方案二、逻辑过期方案</p><p>方案分析：我们之所以会出现这个缓存击穿问题，主要原因是在于我们对key设置了过期时间，假设我们不设置过期时间，其实就不会有缓存击穿的问题，但是不设置过期时间，这样数据不就一直占用我们内存了吗，我们可以采用逻辑过期方案。</p><p>我们把过期时间设置在 redis的value中，注意：这个过期时间并不会直接作用于redis，而是我们后续通过逻辑去处理。假设线程1去查询缓存，然后从value中判断出来当前的数据已经过期了，此时线程1去获得互斥锁，那么其他线程会进行阻塞，获得了锁的线程他会开启一个 线程去进行 以前的重构数据的逻辑，直到新开的线程完成这个逻辑后，才释放锁， 而线程1直接进行返回，假设现在线程3过来访问，由于线程线程2持有着锁，所以线程3无法获得锁，线程3也直接返回数据，只有等到新开的线程2把重建数据构建完后，其他线程才能走返回正确的数据。</p><p>这种方案巧妙在于，异步的构建缓存，缺点在于在构建完缓存之前，返回的都是脏数据。</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653328663897.png" alt></p><p>进行对比</p><p>**互斥锁方案：**由于保证了互斥性，所以数据一致，且实现简单，因为仅仅只需要加一把锁而已，也没其他的事情需要操心，所以没有额外的内存消耗，缺点在于有锁就有死锁问题的发生，且只能串行执行性能肯定受到影响</p><p><strong>逻辑过期方案：</strong> 线程读取过程中不需要等待，性能好，有一个额外的线程持有锁去进行重构数据，但是在重构数据完成前，其他的线程只能返回之前的数据，且实现起来麻烦</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653357522914.png" alt></p><h3 id="2-9-利用互斥锁解决缓存击穿问题">2.9 利用互斥锁解决缓存击穿问题</h3><p>核心思路：相较于原来从缓存中查询不到数据后直接查询数据库而言，现在的方案是 进行查询之后，如果从缓存没有查询到数据，则进行互斥锁的获取，获取互斥锁后，判断是否获得到了锁，如果没有获得到，则休眠，过一会再进行尝试，直到获取到锁为止，才能进行查询</p><p>如果获取到了锁的线程，再去进行查询，查询后将数据写入redis，再释放锁，返回数据，利用互斥锁就能保证只有一个线程去执行操作数据库的逻辑，防止缓存击穿</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653357860001.png" alt></p><p><strong>操作锁的代码：</strong></p><p>核心思路就是利用redis的setnx方法来表示获取锁，该方法含义是redis中如果没有这个key，则插入成功，返回1，在stringRedisTemplate中返回true，  如果有这个key则插入失败，则返回0，在stringRedisTemplate返回false，我们可以通过true，或者是false，来表示是否有线程成功插入key，成功插入的key的线程我们认为他就是获得到锁的线程。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="keyword">private</span> <span class="type">boolean</span> <span class="title function_">tryLock</span><span class="params">(String key)</span> &#123;</span><br><span class="line">    <span class="type">Boolean</span> <span class="variable">flag</span> <span class="operator">=</span> stringRedisTemplate.opsForValue().setIfAbsent(key, <span class="string">&quot;1&quot;</span>, <span class="number">10</span>, TimeUnit.SECONDS);</span><br><span class="line">    <span class="keyword">return</span> BooleanUtil.isTrue(flag);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title function_">unlock</span><span class="params">(String key)</span> &#123;</span><br><span class="line">    stringRedisTemplate.delete(key);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>操作代码：</strong></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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> Shop <span class="title function_">queryWithMutex</span><span class="params">(Long id)</span>  &#123;</span><br><span class="line">       <span class="type">String</span> <span class="variable">key</span> <span class="operator">=</span> CACHE_SHOP_KEY + id;</span><br><span class="line">       <span class="comment">// 1、从redis中查询商铺缓存</span></span><br><span class="line">       <span class="type">String</span> <span class="variable">shopJson</span> <span class="operator">=</span> stringRedisTemplate.opsForValue().get(<span class="string">&quot;key&quot;</span>);</span><br><span class="line">       <span class="comment">// 2、判断是否存在</span></span><br><span class="line">       <span class="keyword">if</span> (StrUtil.isNotBlank(shopJson)) &#123;</span><br><span class="line">           <span class="comment">// 存在,直接返回</span></span><br><span class="line">           <span class="keyword">return</span> JSONUtil.toBean(shopJson, Shop.class);</span><br><span class="line">       &#125;</span><br><span class="line">       <span class="comment">//判断命中的值是否是空值</span></span><br><span class="line">       <span class="keyword">if</span> (shopJson != <span class="literal">null</span>) &#123;</span><br><span class="line">           <span class="comment">//返回一个错误信息</span></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="comment">// 4.实现缓存重构</span></span><br><span class="line">       <span class="comment">//4.1 获取互斥锁</span></span><br><span class="line">       <span class="type">String</span> <span class="variable">lockKey</span> <span class="operator">=</span> <span class="string">&quot;lock:shop:&quot;</span> + id;</span><br><span class="line">       <span class="type">Shop</span> <span class="variable">shop</span> <span class="operator">=</span> <span class="literal">null</span>;</span><br><span class="line">       <span class="keyword">try</span> &#123;</span><br><span class="line">           <span class="type">boolean</span> <span class="variable">isLock</span> <span class="operator">=</span> tryLock(lockKey);</span><br><span class="line">           <span class="comment">// 4.2 判断否获取成功</span></span><br><span class="line">           <span class="keyword">if</span>(!isLock)&#123;</span><br><span class="line">               <span class="comment">//4.3 失败，则休眠重试</span></span><br><span class="line">               Thread.sleep(<span class="number">50</span>);</span><br><span class="line">               <span class="keyword">return</span> queryWithMutex(id);</span><br><span class="line">           &#125;</span><br><span class="line">           <span class="comment">//4.4 成功，根据id查询数据库</span></span><br><span class="line">            shop = getById(id);</span><br><span class="line">           <span class="comment">// 5.不存在，返回错误</span></span><br><span class="line">           <span class="keyword">if</span>(shop == <span class="literal">null</span>)&#123;</span><br><span class="line">                <span class="comment">//将空值写入redis</span></span><br><span class="line">               stringRedisTemplate.opsForValue().set(key,<span class="string">&quot;&quot;</span>,CACHE_NULL_TTL,TimeUnit.MINUTES);</span><br><span class="line">               <span class="comment">//返回错误信息</span></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="comment">//6.写入redis</span></span><br><span class="line">           stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),CACHE_NULL_TTL,TimeUnit.MINUTES);</span><br><span class="line"></span><br><span class="line">       &#125;<span class="keyword">catch</span> (Exception e)&#123;</span><br><span class="line">           <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">RuntimeException</span>(e);</span><br><span class="line">       &#125;</span><br><span class="line">       <span class="keyword">finally</span> &#123;</span><br><span class="line">           <span class="comment">//7.释放互斥锁</span></span><br><span class="line">           unlock(lockKey);</span><br><span class="line">       &#125;</span><br><span class="line">       <span class="keyword">return</span> shop;</span><br><span class="line">   &#125;</span><br></pre></td></tr></table></figure><h3 id="3-0-、利用逻辑过期解决缓存击穿问题">3.0 、利用逻辑过期解决缓存击穿问题</h3><p><strong>需求：修改根据id查询商铺的业务，基于逻辑过期方式来解决缓存击穿问题</strong></p><p>思路分析：当用户开始查询redis时，判断是否命中，如果没有命中则直接返回空数据，不查询数据库，而一旦命中后，将value取出，判断value中的过期时间是否满足，如果没有过期，则直接返回redis中的数据，如果过期，则在开启独立线程后直接返回之前的数据，独立线程去重构数据，重构完成后释放互斥锁。</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653360308731.png" alt></p><p>如果封装数据：因为现在redis中存储的数据的value需要带上过期时间，此时要么你去修改原来的实体类，要么你</p><p><strong>步骤一、</strong></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></pre></td><td class="code"><pre><span class="line">@Data</span><br><span class="line">public class RedisData &#123;</span><br><span class="line">    private LocalDateTime expireTime;</span><br><span class="line">    private Object data;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>步骤二、</strong></p><p>在<strong>ShopServiceImpl</strong> 新增此方法，利用单元测试进行缓存预热</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653360807133.png" alt></p><p><strong>在测试类中</strong></p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653360864839.png" alt></p><p>步骤三：正式代码</p><p><strong>ShopServiceImpl</strong></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></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">ExecutorService</span> <span class="variable">CACHE_REBUILD_EXECUTOR</span> <span class="operator">=</span> Executors.newFixedThreadPool(<span class="number">10</span>);</span><br><span class="line"><span class="keyword">public</span> Shop <span class="title function_">queryWithLogicalExpire</span><span class="params">( Long id )</span> &#123;</span><br><span class="line">    <span class="type">String</span> <span class="variable">key</span> <span class="operator">=</span> CACHE_SHOP_KEY + id;</span><br><span class="line">    <span class="comment">// 1.从redis查询商铺缓存</span></span><br><span class="line">    <span class="type">String</span> <span class="variable">json</span> <span class="operator">=</span> stringRedisTemplate.opsForValue().get(key);</span><br><span class="line">    <span class="comment">// 2.判断是否存在</span></span><br><span class="line">    <span class="keyword">if</span> (StrUtil.isBlank(json)) &#123;</span><br><span class="line">        <span class="comment">// 3.存在，直接返回</span></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="comment">// 4.命中，需要先把json反序列化为对象</span></span><br><span class="line">    <span class="type">RedisData</span> <span class="variable">redisData</span> <span class="operator">=</span> JSONUtil.toBean(json, RedisData.class);</span><br><span class="line">    <span class="type">Shop</span> <span class="variable">shop</span> <span class="operator">=</span> JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);</span><br><span class="line">    <span class="type">LocalDateTime</span> <span class="variable">expireTime</span> <span class="operator">=</span> redisData.getExpireTime();</span><br><span class="line">    <span class="comment">// 5.判断是否过期</span></span><br><span class="line">    <span class="keyword">if</span>(expireTime.isAfter(LocalDateTime.now())) &#123;</span><br><span class="line">        <span class="comment">// 5.1.未过期，直接返回店铺信息</span></span><br><span class="line">        <span class="keyword">return</span> shop;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 5.2.已过期，需要缓存重建</span></span><br><span class="line">    <span class="comment">// 6.缓存重建</span></span><br><span class="line">    <span class="comment">// 6.1.获取互斥锁</span></span><br><span class="line">    <span class="type">String</span> <span class="variable">lockKey</span> <span class="operator">=</span> LOCK_SHOP_KEY + id;</span><br><span class="line">    <span class="type">boolean</span> <span class="variable">isLock</span> <span class="operator">=</span> tryLock(lockKey);</span><br><span class="line">    <span class="comment">// 6.2.判断是否获取锁成功</span></span><br><span class="line">    <span class="keyword">if</span> (isLock)&#123;</span><br><span class="line">        CACHE_REBUILD_EXECUTOR.submit( ()-&gt;&#123;</span><br><span class="line"></span><br><span class="line">            <span class="keyword">try</span>&#123;</span><br><span class="line">                <span class="comment">//重建缓存</span></span><br><span class="line">                <span class="built_in">this</span>.saveShop2Redis(id,<span class="number">20L</span>);</span><br><span class="line">            &#125;<span class="keyword">catch</span> (Exception e)&#123;</span><br><span class="line">                <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">RuntimeException</span>(e);</span><br><span class="line">            &#125;<span class="keyword">finally</span> &#123;</span><br><span class="line">                unlock(lockKey);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 6.4.返回过期的商铺信息</span></span><br><span class="line">    <span class="keyword">return</span> shop;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="3-1、封装Redis工具类">3.1、封装Redis工具类</h3><p>基于StringRedisTemplate封装一个缓存工具类，满足下列需求：</p><ul><li>方法1：将任意Java对象序列化为json并存储在string类型的key中，并且可以设置TTL过期时间</li><li>方法2：将任意Java对象序列化为json并存储在string类型的key中，并且可以设置逻辑过期时间，用于处理缓</li></ul><p>存击穿问题</p><ul><li>方法3：根据指定的key查询缓存，并反序列化为指定类型，利用缓存空值的方式解决缓存穿透问题</li><li>方法4：根据指定的key查询缓存，并反序列化为指定类型，需要利用逻辑过期解决缓存击穿问题</li></ul><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><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br><span class="line">133</span><br><span class="line">134</span><br><span class="line">135</span><br><span class="line">136</span><br><span class="line">137</span><br><span class="line">138</span><br><span class="line">139</span><br><span class="line">140</span><br><span class="line">141</span><br><span class="line">142</span><br><span class="line">143</span><br><span class="line">144</span><br><span class="line">145</span><br><span class="line">146</span><br><span class="line">147</span><br><span class="line">148</span><br><span class="line">149</span><br><span class="line">150</span><br><span class="line">151</span><br><span class="line">152</span><br><span class="line">153</span><br><span class="line">154</span><br><span class="line">155</span><br><span class="line">156</span><br><span class="line">157</span><br><span class="line">158</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Slf4j</span></span><br><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">CacheClient</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> StringRedisTemplate stringRedisTemplate;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">ExecutorService</span> <span class="variable">CACHE_REBUILD_EXECUTOR</span> <span class="operator">=</span> Executors.newFixedThreadPool(<span class="number">10</span>);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="title function_">CacheClient</span><span class="params">(StringRedisTemplate stringRedisTemplate)</span> &#123;</span><br><span class="line">        <span class="built_in">this</span>.stringRedisTemplate = stringRedisTemplate;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">set</span><span class="params">(String key, Object value, Long time, TimeUnit unit)</span> &#123;</span><br><span class="line">        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">setWithLogicalExpire</span><span class="params">(String key, Object value, Long time, TimeUnit unit)</span> &#123;</span><br><span class="line">        <span class="comment">// 设置逻辑过期</span></span><br><span class="line">        <span class="type">RedisData</span> <span class="variable">redisData</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">RedisData</span>();</span><br><span class="line">        redisData.setData(value);</span><br><span class="line">        redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));</span><br><span class="line">        <span class="comment">// 写入Redis</span></span><br><span class="line">        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> &lt;R,ID&gt; R <span class="title function_">queryWithPassThrough</span><span class="params">(</span></span><br><span class="line"><span class="params">            String keyPrefix, ID id, Class&lt;R&gt; type, Function&lt;ID, R&gt; dbFallback, Long time, TimeUnit unit)</span>&#123;</span><br><span class="line">        <span class="type">String</span> <span class="variable">key</span> <span class="operator">=</span> keyPrefix + id;</span><br><span class="line">        <span class="comment">// 1.从redis查询商铺缓存</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">json</span> <span class="operator">=</span> stringRedisTemplate.opsForValue().get(key);</span><br><span class="line">        <span class="comment">// 2.判断是否存在</span></span><br><span class="line">        <span class="keyword">if</span> (StrUtil.isNotBlank(json)) &#123;</span><br><span class="line">            <span class="comment">// 3.存在，直接返回</span></span><br><span class="line">            <span class="keyword">return</span> JSONUtil.toBean(json, type);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">// 判断命中的是否是空值</span></span><br><span class="line">        <span class="keyword">if</span> (json != <span class="literal">null</span>) &#123;</span><br><span class="line">            <span class="comment">// 返回一个错误信息</span></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><br><span class="line">        <span class="comment">// 4.不存在，根据id查询数据库</span></span><br><span class="line">        <span class="type">R</span> <span class="variable">r</span> <span class="operator">=</span> dbFallback.apply(id);</span><br><span class="line">        <span class="comment">// 5.不存在，返回错误</span></span><br><span class="line">        <span class="keyword">if</span> (r == <span class="literal">null</span>) &#123;</span><br><span class="line">            <span class="comment">// 将空值写入redis</span></span><br><span class="line">            stringRedisTemplate.opsForValue().set(key, <span class="string">&quot;&quot;</span>, CACHE_NULL_TTL, TimeUnit.MINUTES);</span><br><span class="line">            <span class="comment">// 返回错误信息</span></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="comment">// 6.存在，写入redis</span></span><br><span class="line">        <span class="built_in">this</span>.set(key, r, time, unit);</span><br><span class="line">        <span class="keyword">return</span> r;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> &lt;R, ID&gt; R <span class="title function_">queryWithLogicalExpire</span><span class="params">(</span></span><br><span class="line"><span class="params">            String keyPrefix, ID id, Class&lt;R&gt; type, Function&lt;ID, R&gt; dbFallback, Long time, TimeUnit unit)</span> &#123;</span><br><span class="line">        <span class="type">String</span> <span class="variable">key</span> <span class="operator">=</span> keyPrefix + id;</span><br><span class="line">        <span class="comment">// 1.从redis查询商铺缓存</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">json</span> <span class="operator">=</span> stringRedisTemplate.opsForValue().get(key);</span><br><span class="line">        <span class="comment">// 2.判断是否存在</span></span><br><span class="line">        <span class="keyword">if</span> (StrUtil.isBlank(json)) &#123;</span><br><span class="line">            <span class="comment">// 3.存在，直接返回</span></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="comment">// 4.命中，需要先把json反序列化为对象</span></span><br><span class="line">        <span class="type">RedisData</span> <span class="variable">redisData</span> <span class="operator">=</span> JSONUtil.toBean(json, RedisData.class);</span><br><span class="line">        <span class="type">R</span> <span class="variable">r</span> <span class="operator">=</span> JSONUtil.toBean((JSONObject) redisData.getData(), type);</span><br><span class="line">        <span class="type">LocalDateTime</span> <span class="variable">expireTime</span> <span class="operator">=</span> redisData.getExpireTime();</span><br><span class="line">        <span class="comment">// 5.判断是否过期</span></span><br><span class="line">        <span class="keyword">if</span>(expireTime.isAfter(LocalDateTime.now())) &#123;</span><br><span class="line">            <span class="comment">// 5.1.未过期，直接返回店铺信息</span></span><br><span class="line">            <span class="keyword">return</span> r;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">// 5.2.已过期，需要缓存重建</span></span><br><span class="line">        <span class="comment">// 6.缓存重建</span></span><br><span class="line">        <span class="comment">// 6.1.获取互斥锁</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">lockKey</span> <span class="operator">=</span> LOCK_SHOP_KEY + id;</span><br><span class="line">        <span class="type">boolean</span> <span class="variable">isLock</span> <span class="operator">=</span> tryLock(lockKey);</span><br><span class="line">        <span class="comment">// 6.2.判断是否获取锁成功</span></span><br><span class="line">        <span class="keyword">if</span> (isLock)&#123;</span><br><span class="line">            <span class="comment">// 6.3.成功，开启独立线程，实现缓存重建</span></span><br><span class="line">            CACHE_REBUILD_EXECUTOR.submit(() -&gt; &#123;</span><br><span class="line">                <span class="keyword">try</span> &#123;</span><br><span class="line">                    <span class="comment">// 查询数据库</span></span><br><span class="line">                    <span class="type">R</span> <span class="variable">newR</span> <span class="operator">=</span> dbFallback.apply(id);</span><br><span class="line">                    <span class="comment">// 重建缓存</span></span><br><span class="line">                    <span class="built_in">this</span>.setWithLogicalExpire(key, newR, time, unit);</span><br><span class="line">                &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">                    <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">RuntimeException</span>(e);</span><br><span class="line">                &#125;<span class="keyword">finally</span> &#123;</span><br><span class="line">                    <span class="comment">// 释放锁</span></span><br><span class="line">                    unlock(lockKey);</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">// 6.4.返回过期的商铺信息</span></span><br><span class="line">        <span class="keyword">return</span> r;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> &lt;R, ID&gt; R <span class="title function_">queryWithMutex</span><span class="params">(</span></span><br><span class="line"><span class="params">            String keyPrefix, ID id, Class&lt;R&gt; type, Function&lt;ID, R&gt; dbFallback, Long time, TimeUnit unit)</span> &#123;</span><br><span class="line">        <span class="type">String</span> <span class="variable">key</span> <span class="operator">=</span> keyPrefix + id;</span><br><span class="line">        <span class="comment">// 1.从redis查询商铺缓存</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">shopJson</span> <span class="operator">=</span> stringRedisTemplate.opsForValue().get(key);</span><br><span class="line">        <span class="comment">// 2.判断是否存在</span></span><br><span class="line">        <span class="keyword">if</span> (StrUtil.isNotBlank(shopJson)) &#123;</span><br><span class="line">            <span class="comment">// 3.存在，直接返回</span></span><br><span class="line">            <span class="keyword">return</span> JSONUtil.toBean(shopJson, type);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">// 判断命中的是否是空值</span></span><br><span class="line">        <span class="keyword">if</span> (shopJson != <span class="literal">null</span>) &#123;</span><br><span class="line">            <span class="comment">// 返回一个错误信息</span></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><br><span class="line">        <span class="comment">// 4.实现缓存重建</span></span><br><span class="line">        <span class="comment">// 4.1.获取互斥锁</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">lockKey</span> <span class="operator">=</span> LOCK_SHOP_KEY + id;</span><br><span class="line">        <span class="type">R</span> <span class="variable">r</span> <span class="operator">=</span> <span class="literal">null</span>;</span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            <span class="type">boolean</span> <span class="variable">isLock</span> <span class="operator">=</span> tryLock(lockKey);</span><br><span class="line">            <span class="comment">// 4.2.判断是否获取成功</span></span><br><span class="line">            <span class="keyword">if</span> (!isLock) &#123;</span><br><span class="line">                <span class="comment">// 4.3.获取锁失败，休眠并重试</span></span><br><span class="line">                Thread.sleep(<span class="number">50</span>);</span><br><span class="line">                <span class="keyword">return</span> queryWithMutex(keyPrefix, id, type, dbFallback, time, unit);</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="comment">// 4.4.获取锁成功，根据id查询数据库</span></span><br><span class="line">            r = dbFallback.apply(id);</span><br><span class="line">            <span class="comment">// 5.不存在，返回错误</span></span><br><span class="line">            <span class="keyword">if</span> (r == <span class="literal">null</span>) &#123;</span><br><span class="line">                <span class="comment">// 将空值写入redis</span></span><br><span class="line">                stringRedisTemplate.opsForValue().set(key, <span class="string">&quot;&quot;</span>, CACHE_NULL_TTL, TimeUnit.MINUTES);</span><br><span class="line">                <span class="comment">// 返回错误信息</span></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="comment">// 6.存在，写入redis</span></span><br><span class="line">            <span class="built_in">this</span>.set(key, r, time, unit);</span><br><span class="line">        &#125; <span class="keyword">catch</span> (InterruptedException e) &#123;</span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">RuntimeException</span>(e);</span><br><span class="line">        &#125;<span class="keyword">finally</span> &#123;</span><br><span class="line">            <span class="comment">// 7.释放锁</span></span><br><span class="line">            unlock(lockKey);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">// 8.返回</span></span><br><span class="line">        <span class="keyword">return</span> r;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="type">boolean</span> <span class="title function_">tryLock</span><span class="params">(String key)</span> &#123;</span><br><span class="line">        <span class="type">Boolean</span> <span class="variable">flag</span> <span class="operator">=</span> stringRedisTemplate.opsForValue().setIfAbsent(key, <span class="string">&quot;1&quot;</span>, <span class="number">10</span>, TimeUnit.SECONDS);</span><br><span class="line">        <span class="keyword">return</span> BooleanUtil.isTrue(flag);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">void</span> <span class="title function_">unlock</span><span class="params">(String key)</span> &#123;</span><br><span class="line">        stringRedisTemplate.delete(key);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在ShopServiceImpl 中</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></pre></td><td class="code"><pre><span class="line"><span class="meta">@Resource</span></span><br><span class="line"><span class="keyword">private</span> CacheClient cacheClient;</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> Result <span class="title function_">queryById</span><span class="params">(Long id)</span> &#123;</span><br><span class="line">        <span class="comment">// 解决缓存穿透</span></span><br><span class="line">        <span class="type">Shop</span> <span class="variable">shop</span> <span class="operator">=</span> cacheClient</span><br><span class="line">                .queryWithPassThrough(CACHE_SHOP_KEY, id, Shop.class, <span class="built_in">this</span>::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 互斥锁解决缓存击穿</span></span><br><span class="line">        <span class="comment">// Shop shop = cacheClient</span></span><br><span class="line">        <span class="comment">//         .queryWithMutex(CACHE_SHOP_KEY, id, Shop.class, this::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES);</span></span><br><span class="line"></span><br><span class="line">        <span class="comment">// 逻辑过期解决缓存击穿</span></span><br><span class="line">        <span class="comment">// Shop shop = cacheClient</span></span><br><span class="line">        <span class="comment">//         .queryWithLogicalExpire(CACHE_SHOP_KEY, id, Shop.class, this::getById, 20L, TimeUnit.SECONDS);</span></span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> (shop == <span class="literal">null</span>) &#123;</span><br><span class="line">            <span class="keyword">return</span> Result.fail(<span class="string">&quot;店铺不存在！&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">// 7.返回</span></span><br><span class="line">        <span class="keyword">return</span> Result.ok(shop);</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure><h2 id="3、优惠卷秒杀">3、优惠卷秒杀</h2><h3 id="3-1-全局唯一ID">3.1 -全局唯一ID</h3><p>每个店铺都可以发布优惠券：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653362612286.png" alt></p><p>当用户抢购时，就会生成订单并保存到tb_voucher_order这张表中，而订单表如果使用数据库自增ID就存在一些问题：</p><ul><li>id的规律性太明显</li><li>受单表数据量的限制</li></ul><p>场景分析：如果我们的id具有太明显的规则，用户或者说商业对手很容易猜测出来我们的一些敏感信息，比如商城在一天时间内，卖出了多少单，这明显不合适。</p><p>场景分析二：随着我们商城规模越来越大，mysql的单表的容量不宜超过500W，数据量过大之后，我们要进行拆库拆表，但拆分表了之后，他们从逻辑上讲他们是同一张表，所以他们的id是不能一样的， 于是乎我们需要保证id的唯一性。</p><p><strong>全局ID生成器</strong>，是一种在分布式系统下用来生成全局唯一ID的工具，一般要满足下列特性：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653363100502.png" alt></p><p>为了增加ID的安全性，我们可以不直接使用Redis自增的数值，而是拼接一些其它信息：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653363172079.png" alt>ID的组成部分：符号位：1bit，永远为0</p><p>时间戳：31bit，以秒为单位，可以使用69年</p><p>序列号：32bit，秒内的计数器，支持每秒产生2^32个不同ID</p><h3 id="3-2-Redis实现全局唯一Id">3.2 -Redis实现全局唯一Id</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">RedisIdWorker</span> &#123;</span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 开始时间戳</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">long</span> <span class="variable">BEGIN_TIMESTAMP</span> <span class="operator">=</span> <span class="number">1640995200L</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">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">int</span> <span class="variable">COUNT_BITS</span> <span class="operator">=</span> <span class="number">32</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> StringRedisTemplate stringRedisTemplate;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="title function_">RedisIdWorker</span><span class="params">(StringRedisTemplate stringRedisTemplate)</span> &#123;</span><br><span class="line">        <span class="built_in">this</span>.stringRedisTemplate = stringRedisTemplate;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="type">long</span> <span class="title function_">nextId</span><span class="params">(String keyPrefix)</span> &#123;</span><br><span class="line">        <span class="comment">// 1.生成时间戳</span></span><br><span class="line">        <span class="type">LocalDateTime</span> <span class="variable">now</span> <span class="operator">=</span> LocalDateTime.now();</span><br><span class="line">        <span class="type">long</span> <span class="variable">nowSecond</span> <span class="operator">=</span> now.toEpochSecond(ZoneOffset.UTC);</span><br><span class="line">        <span class="type">long</span> <span class="variable">timestamp</span> <span class="operator">=</span> nowSecond - BEGIN_TIMESTAMP;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 2.生成序列号</span></span><br><span class="line">        <span class="comment">// 2.1.获取当前日期，精确到天</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">date</span> <span class="operator">=</span> now.format(DateTimeFormatter.ofPattern(<span class="string">&quot;yyyy:MM:dd&quot;</span>));</span><br><span class="line">        <span class="comment">// 2.2.自增长</span></span><br><span class="line">        <span class="type">long</span> <span class="variable">count</span> <span class="operator">=</span> stringRedisTemplate.opsForValue().increment(<span class="string">&quot;icr:&quot;</span> + keyPrefix + <span class="string">&quot;:&quot;</span> + date);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 3.拼接并返回</span></span><br><span class="line">        <span class="keyword">return</span> timestamp &lt;&lt; COUNT_BITS | count;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>测试类</p><p>知识小贴士：关于countdownlatch</p><p>countdownlatch名为信号枪：主要的作用是同步协调在多线程的等待于唤醒问题</p><p>我们如果没有CountDownLatch ，那么由于程序是异步的，当异步程序没有执行完时，主线程就已经执行完了，然后我们期望的是分线程全部走完之后，主线程再走，所以我们此时需要使用到CountDownLatch</p><p>CountDownLatch 中有两个最重要的方法</p><p>1、countDown</p><p>2、await</p><p>await 方法 是阻塞方法，我们担心分线程没有执行完时，main线程就先执行，所以使用await可以让main线程阻塞，那么什么时候main线程不再阻塞呢？当CountDownLatch  内部维护的 变量变为0时，就不再阻塞，直接放行，那么什么时候CountDownLatch   维护的变量变为0 呢，我们只需要调用一次countDown ，内部变量就减少1，我们让分线程和变量绑定， 执行完一个分线程就减少一个变量，当分线程全部走完，CountDownLatch 维护的变量就是0，此时await就不再阻塞，统计出来的时间也就是所有分线程执行完后的时间。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Test</span></span><br><span class="line"><span class="keyword">void</span> <span class="title function_">testIdWorker</span><span class="params">()</span> <span class="keyword">throws</span> InterruptedException &#123;</span><br><span class="line">    <span class="type">CountDownLatch</span> <span class="variable">latch</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">CountDownLatch</span>(<span class="number">300</span>);</span><br><span class="line"></span><br><span class="line">    <span class="type">Runnable</span> <span class="variable">task</span> <span class="operator">=</span> () -&gt; &#123;</span><br><span class="line">        <span class="keyword">for</span> (<span class="type">int</span> <span class="variable">i</span> <span class="operator">=</span> <span class="number">0</span>; i &lt; <span class="number">100</span>; i++) &#123;</span><br><span class="line">            <span class="type">long</span> <span class="variable">id</span> <span class="operator">=</span> redisIdWorker.nextId(<span class="string">&quot;order&quot;</span>);</span><br><span class="line">            System.out.println(<span class="string">&quot;id = &quot;</span> + id);</span><br><span class="line">        &#125;</span><br><span class="line">        latch.countDown();</span><br><span class="line">    &#125;;</span><br><span class="line">    <span class="type">long</span> <span class="variable">begin</span> <span class="operator">=</span> System.currentTimeMillis();</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> <span class="variable">i</span> <span class="operator">=</span> <span class="number">0</span>; i &lt; <span class="number">300</span>; i++) &#123;</span><br><span class="line">        es.submit(task);</span><br><span class="line">    &#125;</span><br><span class="line">    latch.await();</span><br><span class="line">    <span class="type">long</span> <span class="variable">end</span> <span class="operator">=</span> System.currentTimeMillis();</span><br><span class="line">    System.out.println(<span class="string">&quot;time = &quot;</span> + (end - begin));</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="3-3-添加优惠卷">3.3 添加优惠卷</h3><p>每个店铺都可以发布优惠券，分为平价券和特价券。平价券可以任意购买，而特价券需要秒杀抢购：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653365145124.png" alt></p><p>tb_voucher：优惠券的基本信息，优惠金额、使用规则等<br>tb_seckill_voucher：优惠券的库存、开始抢购时间，结束抢购时间。特价优惠券才需要填写这些信息</p><p>平价卷由于优惠力度并不是很大，所以是可以任意领取</p><p>而代金券由于优惠力度大，所以像第二种卷，就得限制数量，从表结构上也能看出，特价卷除了具有优惠卷的基本信息以外，还具有库存，抢购时间，结束时间等等字段</p><p>**新增普通卷代码：  **VoucherController</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@PostMapping</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">addVoucher</span><span class="params">(<span class="meta">@RequestBody</span> Voucher voucher)</span> &#123;</span><br><span class="line">    voucherService.save(voucher);</span><br><span class="line">    <span class="keyword">return</span> Result.ok(voucher.getId());</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>新增秒杀卷代码：</strong></p><p><strong>VoucherController</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@PostMapping(&quot;seckill&quot;)</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">addSeckillVoucher</span><span class="params">(<span class="meta">@RequestBody</span> Voucher voucher)</span> &#123;</span><br><span class="line">    voucherService.addSeckillVoucher(voucher);</span><br><span class="line">    <span class="keyword">return</span> Result.ok(voucher.getId());</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>VoucherServiceImpl</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="meta">@Transactional</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">addSeckillVoucher</span><span class="params">(Voucher voucher)</span> &#123;</span><br><span class="line">    <span class="comment">// 保存优惠券</span></span><br><span class="line">    save(voucher);</span><br><span class="line">    <span class="comment">// 保存秒杀信息</span></span><br><span class="line">    <span class="type">SeckillVoucher</span> <span class="variable">seckillVoucher</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">SeckillVoucher</span>();</span><br><span class="line">    seckillVoucher.setVoucherId(voucher.getId());</span><br><span class="line">    seckillVoucher.setStock(voucher.getStock());</span><br><span class="line">    seckillVoucher.setBeginTime(voucher.getBeginTime());</span><br><span class="line">    seckillVoucher.setEndTime(voucher.getEndTime());</span><br><span class="line">    seckillVoucherService.save(seckillVoucher);</span><br><span class="line">    <span class="comment">// 保存秒杀库存到Redis中</span></span><br><span class="line">    stringRedisTemplate.opsForValue().set(SECKILL_STOCK_KEY + voucher.getId(), voucher.getStock().toString());</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="3-4-实现秒杀下单">3.4 实现秒杀下单</h3><p>下单核心思路：当我们点击抢购时，会触发右侧的请求，我们只需要编写对应的controller即可</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653365839526.png" alt></p><p>秒杀下单应该思考的内容：</p><p>下单时需要判断两点：</p><ul><li>秒杀是否开始或结束，如果尚未开始或已经结束则无法下单</li><li>库存是否充足，不足则无法下单</li></ul><p>下单核心逻辑分析：</p><p>当用户开始进行下单，我们应当去查询优惠卷信息，查询到优惠卷信息，判断是否满足秒杀条件</p><p>比如时间是否充足，如果时间充足，则进一步判断库存是否足够，如果两者都满足，则扣减库存，创建订单，然后返回订单id，如果有一个条件不满足则直接结束。</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653366238564.png" alt></p><p>VoucherOrderServiceImpl</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></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">seckillVoucher</span><span class="params">(Long voucherId)</span> &#123;</span><br><span class="line">    <span class="comment">// 1.查询优惠券</span></span><br><span class="line">    <span class="type">SeckillVoucher</span> <span class="variable">voucher</span> <span class="operator">=</span> seckillVoucherService.getById(voucherId);</span><br><span class="line">    <span class="comment">// 2.判断秒杀是否开始</span></span><br><span class="line">    <span class="keyword">if</span> (voucher.getBeginTime().isAfter(LocalDateTime.now())) &#123;</span><br><span class="line">        <span class="comment">// 尚未开始</span></span><br><span class="line">        <span class="keyword">return</span> Result.fail(<span class="string">&quot;秒杀尚未开始！&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 3.判断秒杀是否已经结束</span></span><br><span class="line">    <span class="keyword">if</span> (voucher.getEndTime().isBefore(LocalDateTime.now())) &#123;</span><br><span class="line">        <span class="comment">// 尚未开始</span></span><br><span class="line">        <span class="keyword">return</span> Result.fail(<span class="string">&quot;秒杀已经结束！&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 4.判断库存是否充足</span></span><br><span class="line">    <span class="keyword">if</span> (voucher.getStock() &lt; <span class="number">1</span>) &#123;</span><br><span class="line">        <span class="comment">// 库存不足</span></span><br><span class="line">        <span class="keyword">return</span> Result.fail(<span class="string">&quot;库存不足！&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">//5，扣减库存</span></span><br><span class="line">    <span class="type">boolean</span> <span class="variable">success</span> <span class="operator">=</span> seckillVoucherService.update()</span><br><span class="line">            .setSql(<span class="string">&quot;stock= stock -1&quot;</span>)</span><br><span class="line">            .eq(<span class="string">&quot;voucher_id&quot;</span>, voucherId).update();</span><br><span class="line">    <span class="keyword">if</span> (!success) &#123;</span><br><span class="line">        <span class="comment">//扣减库存</span></span><br><span class="line">        <span class="keyword">return</span> Result.fail(<span class="string">&quot;库存不足！&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">//6.创建订单</span></span><br><span class="line">    <span class="type">VoucherOrder</span> <span class="variable">voucherOrder</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">VoucherOrder</span>();</span><br><span class="line">    <span class="comment">// 6.1.订单id</span></span><br><span class="line">    <span class="type">long</span> <span class="variable">orderId</span> <span class="operator">=</span> redisIdWorker.nextId(<span class="string">&quot;order&quot;</span>);</span><br><span class="line">    voucherOrder.setId(orderId);</span><br><span class="line">    <span class="comment">// 6.2.用户id</span></span><br><span class="line">    <span class="type">Long</span> <span class="variable">userId</span> <span class="operator">=</span> UserHolder.getUser().getId();</span><br><span class="line">    voucherOrder.setUserId(userId);</span><br><span class="line">    <span class="comment">// 6.3.代金券id</span></span><br><span class="line">    voucherOrder.setVoucherId(voucherId);</span><br><span class="line">    save(voucherOrder);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> Result.ok(orderId);</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="3-5-库存超卖问题分析">3.5 库存超卖问题分析</h3><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="keyword">if</span> (voucher.getStock() &lt; <span class="number">1</span>) &#123;</span><br><span class="line">       <span class="comment">// 库存不足</span></span><br><span class="line">       <span class="keyword">return</span> Result.fail(<span class="string">&quot;库存不足！&quot;</span>);</span><br><span class="line">   &#125;</span><br><span class="line">   <span class="comment">//5，扣减库存</span></span><br><span class="line">   <span class="type">boolean</span> <span class="variable">success</span> <span class="operator">=</span> seckillVoucherService.update()</span><br><span class="line">           .setSql(<span class="string">&quot;stock= stock -1&quot;</span>)</span><br><span class="line">           .eq(<span class="string">&quot;voucher_id&quot;</span>, voucherId).update();</span><br><span class="line">   <span class="keyword">if</span> (!success) &#123;</span><br><span class="line">       <span class="comment">//扣减库存</span></span><br><span class="line">       <span class="keyword">return</span> Result.fail(<span class="string">&quot;库存不足！&quot;</span>);</span><br><span class="line">   &#125;</span><br></pre></td></tr></table></figure><p>假设线程1过来查询库存，判断出来库存大于1，正准备去扣减库存，但是还没有来得及去扣减，此时线程2过来，线程2也去查询库存，发现这个数量一定也大于1，那么这两个线程都会去扣减库存，最终多个线程相当于一起去扣减库存，此时就会出现库存的超卖问题。</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653368335155.png" alt></p><p>超卖问题是典型的多线程安全问题，针对这一问题的常见解决方案就是加锁：而对于加锁，我们通常有两种解决方案：见下图：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653368562591.png" alt></p><p><strong>悲观锁：</strong></p><p>悲观锁可以实现对于数据的串行化执行，比如syn，和lock都是悲观锁的代表，同时，悲观锁中又可以再细分为公平锁，非公平锁，可重入锁，等等</p><p><strong>乐观锁：</strong></p><p>乐观锁：会有一个版本号，每次操作数据会对版本号+1，再提交回数据时，会去校验是否比之前的版本大1 ，如果大1 ，则进行操作成功，这套机制的核心逻辑在于，如果在操作过程中，版本号只比原来大1 ，那么就意味着操作过程中没有人对他进行过修改，他的操作就是安全的，如果不大1，则数据被修改过，当然乐观锁还有一些变种的处理方式比如cas</p><p>乐观锁的典型代表：就是cas，利用cas进行无锁化机制加锁，var5 是操作前读取的内存值，while中的var1+var2 是预估值，如果预估值 == 内存值，则代表中间没有被人修改过，此时就将新值去替换 内存值</p><p>其中do while 是为了在操作失败时，再次进行自旋操作，即把之前的逻辑再操作一次。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">int</span> var5;</span><br><span class="line"><span class="keyword">do</span> &#123;</span><br><span class="line">    var5 = <span class="built_in">this</span>.getIntVolatile(var1, var2);</span><br><span class="line">&#125; <span class="keyword">while</span>(!<span class="built_in">this</span>.compareAndSwapInt(var1, var2, var5, var5 + var4));</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> var5;</span><br></pre></td></tr></table></figure><p><strong>课程中的使用方式：</strong></p><p>课程中的使用方式是没有像cas一样带自旋的操作，也没有对version的版本号+1 ，他的操作逻辑是在操作时，对版本号进行+1 操作，然后要求version 如果是1 的情况下，才能操作，那么第一个线程在操作后，数据库中的version变成了2，但是他自己满足version=1 ，所以没有问题，此时线程2执行，线程2 最后也需要加上条件version =1 ，但是现在由于线程1已经操作过了，所以线程2，操作时就不满足version=1 的条件了，所以线程2无法执行成功</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653369268550.png" alt></p><h3 id="3-6-乐观锁解决超卖问题">3.6 乐观锁解决超卖问题</h3><p><strong>修改代码方案一、</strong></p><p>VoucherOrderServiceImpl 在扣减库存时，改为：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">boolean</span> <span class="variable">success</span> <span class="operator">=</span> seckillVoucherService.update()</span><br><span class="line">            .setSql(<span class="string">&quot;stock= stock -1&quot;</span>) <span class="comment">//set stock = stock -1</span></span><br><span class="line">            .eq(<span class="string">&quot;voucher_id&quot;</span>, voucherId).eq(<span class="string">&quot;stock&quot;</span>,voucher.getStock()).update(); <span class="comment">//where id = ？ and stock = ?</span></span><br></pre></td></tr></table></figure><p>以上逻辑的核心含义是：只要我扣减库存时的库存和之前我查询到的库存是一样的，就意味着没有人在中间修改过库存，那么此时就是安全的，但是以上这种方式通过测试发现会有很多失败的情况，失败的原因在于：在使用乐观锁过程中假设100个线程同时都拿到了100的库存，然后大家一起去进行扣减，但是100个人中只有1个人能扣减成功，其他的人在处理时，他们在扣减时，库存已经被修改过了，所以此时其他线程都会失败</p><p><strong>修改代码方案二、</strong></p><p>之前的方式要修改前后都保持一致，但是这样我们分析过，成功的概率太低，所以我们的乐观锁需要变一下，改成stock大于0 即可</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">boolean</span> <span class="variable">success</span> <span class="operator">=</span> seckillVoucherService.update()</span><br><span class="line">            .setSql(<span class="string">&quot;stock= stock -1&quot;</span>)</span><br><span class="line">            .eq(<span class="string">&quot;voucher_id&quot;</span>, voucherId).update().gt(<span class="string">&quot;stock&quot;</span>,<span class="number">0</span>); <span class="comment">//where id = ? and stock &gt; 0</span></span><br></pre></td></tr></table></figure><p><strong>知识小扩展：</strong></p><p>针对cas中的自旋压力过大，我们可以使用Longaddr这个类去解决</p><p>Java8 提供的一个对AtomicLong改进后的一个类，LongAdder</p><p>大量线程并发更新一个原子性的时候，天然的问题就是自旋，会导致并发性问题，当然这也比我们直接使用syn来的好</p><p>所以利用这么一个类，LongAdder来进行优化</p><p>如果获取某个值，则会对cell和base的值进行递增，最后返回一个完整的值</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653370271627.png" alt></p><h3 id="3-6-优惠券秒杀-一人一单">3.6 优惠券秒杀-一人一单</h3><p>需求：修改秒杀业务，要求同一个优惠券，一个用户只能下一单</p><p><strong>现在的问题在于：</strong></p><p>优惠卷是为了引流，但是目前的情况是，一个人可以无限制的抢这个优惠卷，所以我们应当增加一层逻辑，让一个用户只能下一个单，而不是让一个用户下多个单</p><p>具体操作逻辑如下：比如时间是否充足，如果时间充足，则进一步判断库存是否足够，然后再根据优惠卷id和用户id查询是否已经下过这个订单，如果下过这个订单，则不再下单，否则进行下单</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653371854389.png" alt></p><p>VoucherOrderServiceImpl</p><p><strong>初步代码：增加一人一单逻辑</strong></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></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">seckillVoucher</span><span class="params">(Long voucherId)</span> &#123;</span><br><span class="line">    <span class="comment">// 1.查询优惠券</span></span><br><span class="line">    <span class="type">SeckillVoucher</span> <span class="variable">voucher</span> <span class="operator">=</span> seckillVoucherService.getById(voucherId);</span><br><span class="line">    <span class="comment">// 2.判断秒杀是否开始</span></span><br><span class="line">    <span class="keyword">if</span> (voucher.getBeginTime().isAfter(LocalDateTime.now())) &#123;</span><br><span class="line">        <span class="comment">// 尚未开始</span></span><br><span class="line">        <span class="keyword">return</span> Result.fail(<span class="string">&quot;秒杀尚未开始！&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 3.判断秒杀是否已经结束</span></span><br><span class="line">    <span class="keyword">if</span> (voucher.getEndTime().isBefore(LocalDateTime.now())) &#123;</span><br><span class="line">        <span class="comment">// 尚未开始</span></span><br><span class="line">        <span class="keyword">return</span> Result.fail(<span class="string">&quot;秒杀已经结束！&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 4.判断库存是否充足</span></span><br><span class="line">    <span class="keyword">if</span> (voucher.getStock() &lt; <span class="number">1</span>) &#123;</span><br><span class="line">        <span class="comment">// 库存不足</span></span><br><span class="line">        <span class="keyword">return</span> Result.fail(<span class="string">&quot;库存不足！&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 5.一人一单逻辑</span></span><br><span class="line">    <span class="comment">// 5.1.用户id</span></span><br><span class="line">    <span class="type">Long</span> <span class="variable">userId</span> <span class="operator">=</span> UserHolder.getUser().getId();</span><br><span class="line">    <span class="type">int</span> <span class="variable">count</span> <span class="operator">=</span> query().eq(<span class="string">&quot;user_id&quot;</span>, userId).eq(<span class="string">&quot;voucher_id&quot;</span>, voucherId).count();</span><br><span class="line">    <span class="comment">// 5.2.判断是否存在</span></span><br><span class="line">    <span class="keyword">if</span> (count &gt; <span class="number">0</span>) &#123;</span><br><span class="line">        <span class="comment">// 用户已经购买过了</span></span><br><span class="line">        <span class="keyword">return</span> Result.fail(<span class="string">&quot;用户已经购买过一次！&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//6，扣减库存</span></span><br><span class="line">    <span class="type">boolean</span> <span class="variable">success</span> <span class="operator">=</span> seckillVoucherService.update()</span><br><span class="line">            .setSql(<span class="string">&quot;stock= stock -1&quot;</span>)</span><br><span class="line">            .eq(<span class="string">&quot;voucher_id&quot;</span>, voucherId).update();</span><br><span class="line">    <span class="keyword">if</span> (!success) &#123;</span><br><span class="line">        <span class="comment">//扣减库存</span></span><br><span class="line">        <span class="keyword">return</span> Result.fail(<span class="string">&quot;库存不足！&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">//7.创建订单</span></span><br><span class="line">    <span class="type">VoucherOrder</span> <span class="variable">voucherOrder</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">VoucherOrder</span>();</span><br><span class="line">    <span class="comment">// 7.1.订单id</span></span><br><span class="line">    <span class="type">long</span> <span class="variable">orderId</span> <span class="operator">=</span> redisIdWorker.nextId(<span class="string">&quot;order&quot;</span>);</span><br><span class="line">    voucherOrder.setId(orderId);</span><br><span class="line"></span><br><span class="line">    voucherOrder.setUserId(userId);</span><br><span class="line">    <span class="comment">// 7.3.代金券id</span></span><br><span class="line">    voucherOrder.setVoucherId(voucherId);</span><br><span class="line">    save(voucherOrder);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> Result.ok(orderId);</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>**存在问题：**现在的问题还是和之前一样，并发过来，查询数据库，都不存在订单，所以我们还是需要加锁，但是乐观锁比较适合更新数据，而现在是插入数据，所以我们需要使用悲观锁操作</p><p>**注意：**在这里提到了非常多的问题，我们需要慢慢的来思考，首先我们的初始方案是封装了一个createVoucherOrder方法，同时为了确保他线程安全，在方法上添加了一把synchronized 锁</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Transactional</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">synchronized</span> Result <span class="title function_">createVoucherOrder</span><span class="params">(Long voucherId)</span> &#123;</span><br><span class="line"></span><br><span class="line"><span class="type">Long</span> <span class="variable">userId</span> <span class="operator">=</span> UserHolder.getUser().getId();</span><br><span class="line">         <span class="comment">// 5.1.查询订单</span></span><br><span class="line">        <span class="type">int</span> <span class="variable">count</span> <span class="operator">=</span> query().eq(<span class="string">&quot;user_id&quot;</span>, userId).eq(<span class="string">&quot;voucher_id&quot;</span>, voucherId).count();</span><br><span class="line">        <span class="comment">// 5.2.判断是否存在</span></span><br><span class="line">        <span class="keyword">if</span> (count &gt; <span class="number">0</span>) &#123;</span><br><span class="line">            <span class="comment">// 用户已经购买过了</span></span><br><span class="line">            <span class="keyword">return</span> Result.fail(<span class="string">&quot;用户已经购买过一次！&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 6.扣减库存</span></span><br><span class="line">        <span class="type">boolean</span> <span class="variable">success</span> <span class="operator">=</span> seckillVoucherService.update()</span><br><span class="line">                .setSql(<span class="string">&quot;stock = stock - 1&quot;</span>) <span class="comment">// set stock = stock - 1</span></span><br><span class="line">                .eq(<span class="string">&quot;voucher_id&quot;</span>, voucherId).gt(<span class="string">&quot;stock&quot;</span>, <span class="number">0</span>) <span class="comment">// where id = ? and stock &gt; 0</span></span><br><span class="line">                .update();</span><br><span class="line">        <span class="keyword">if</span> (!success) &#123;</span><br><span class="line">            <span class="comment">// 扣减失败</span></span><br><span class="line">            <span class="keyword">return</span> Result.fail(<span class="string">&quot;库存不足！&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 7.创建订单</span></span><br><span class="line">        <span class="type">VoucherOrder</span> <span class="variable">voucherOrder</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">VoucherOrder</span>();</span><br><span class="line">        <span class="comment">// 7.1.订单id</span></span><br><span class="line">        <span class="type">long</span> <span class="variable">orderId</span> <span class="operator">=</span> redisIdWorker.nextId(<span class="string">&quot;order&quot;</span>);</span><br><span class="line">        voucherOrder.setId(orderId);</span><br><span class="line">        <span class="comment">// 7.2.用户id</span></span><br><span class="line">        voucherOrder.setUserId(userId);</span><br><span class="line">        <span class="comment">// 7.3.代金券id</span></span><br><span class="line">        voucherOrder.setVoucherId(voucherId);</span><br><span class="line">        save(voucherOrder);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 7.返回订单id</span></span><br><span class="line">        <span class="keyword">return</span> Result.ok(orderId);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>，但是这样添加锁，锁的粒度太粗了，在使用锁过程中，控制<strong>锁粒度</strong> 是一个非常重要的事情，因为如果锁的粒度太大，会导致每个线程进来都会锁住，所以我们需要去控制锁的粒度，以下这段代码需要修改为：<br>intern() 这个方法是从常量池中拿到数据，如果我们直接使用userId.toString() 他拿到的对象实际上是不同的对象，new出来的对象，我们使用锁必须保证锁必须是同一把，所以我们需要使用intern()方法</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></pre></td><td class="code"><pre><span class="line"><span class="meta">@Transactional</span></span><br><span class="line"><span class="keyword">public</span>  Result <span class="title function_">createVoucherOrder</span><span class="params">(Long voucherId)</span> &#123;</span><br><span class="line"><span class="type">Long</span> <span class="variable">userId</span> <span class="operator">=</span> UserHolder.getUser().getId();</span><br><span class="line"><span class="keyword">synchronized</span>(userId.toString().intern())&#123;</span><br><span class="line">         <span class="comment">// 5.1.查询订单</span></span><br><span class="line">        <span class="type">int</span> <span class="variable">count</span> <span class="operator">=</span> query().eq(<span class="string">&quot;user_id&quot;</span>, userId).eq(<span class="string">&quot;voucher_id&quot;</span>, voucherId).count();</span><br><span class="line">        <span class="comment">// 5.2.判断是否存在</span></span><br><span class="line">        <span class="keyword">if</span> (count &gt; <span class="number">0</span>) &#123;</span><br><span class="line">            <span class="comment">// 用户已经购买过了</span></span><br><span class="line">            <span class="keyword">return</span> Result.fail(<span class="string">&quot;用户已经购买过一次！&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 6.扣减库存</span></span><br><span class="line">        <span class="type">boolean</span> <span class="variable">success</span> <span class="operator">=</span> seckillVoucherService.update()</span><br><span class="line">                .setSql(<span class="string">&quot;stock = stock - 1&quot;</span>) <span class="comment">// set stock = stock - 1</span></span><br><span class="line">                .eq(<span class="string">&quot;voucher_id&quot;</span>, voucherId).gt(<span class="string">&quot;stock&quot;</span>, <span class="number">0</span>) <span class="comment">// where id = ? and stock &gt; 0</span></span><br><span class="line">                .update();</span><br><span class="line">        <span class="keyword">if</span> (!success) &#123;</span><br><span class="line">            <span class="comment">// 扣减失败</span></span><br><span class="line">            <span class="keyword">return</span> Result.fail(<span class="string">&quot;库存不足！&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 7.创建订单</span></span><br><span class="line">        <span class="type">VoucherOrder</span> <span class="variable">voucherOrder</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">VoucherOrder</span>();</span><br><span class="line">        <span class="comment">// 7.1.订单id</span></span><br><span class="line">        <span class="type">long</span> <span class="variable">orderId</span> <span class="operator">=</span> redisIdWorker.nextId(<span class="string">&quot;order&quot;</span>);</span><br><span class="line">        voucherOrder.setId(orderId);</span><br><span class="line">        <span class="comment">// 7.2.用户id</span></span><br><span class="line">        voucherOrder.setUserId(userId);</span><br><span class="line">        <span class="comment">// 7.3.代金券id</span></span><br><span class="line">        voucherOrder.setVoucherId(voucherId);</span><br><span class="line">        save(voucherOrder);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 7.返回订单id</span></span><br><span class="line">        <span class="keyword">return</span> Result.ok(orderId);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>但是以上代码还是存在问题，问题的原因在于当前方法被spring的事务控制，如果你在方法内部加锁，可能会导致当前方法事务还没有提交，但是锁已经释放也会导致问题，所以我们选择将当前方法整体包裹起来，确保事务不会出现问题：如下：</p><p>在seckillVoucher 方法中，添加以下逻辑，这样就能保证事务的特性，同时也控制了锁的粒度</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653373434815.png" alt></p><p>但是以上做法依然有问题，因为你调用的方法，其实是this.的方式调用的，事务想要生效，还得利用代理来生效，所以这个地方，我们需要获得原始的事务对象， 来操作事务</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653383810643.png" alt></p><h3 id="3-7-集群环境下的并发问题">3.7 集群环境下的并发问题</h3><p>通过加锁可以解决在单机情况下的一人一单安全问题，但是在集群模式下就不行了。</p><p>1、我们将服务启动两份，端口分别为8081和8082：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653373887844.png" alt></p><p>2、然后修改nginx的conf目录下的nginx.conf文件，配置反向代理和负载均衡：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653373908620.png" alt></p><p><strong>具体操作(略)</strong></p><p><strong>有关锁失效原因分析</strong></p><p>由于现在我们部署了多个tomcat，每个tomcat都有一个属于自己的jvm，那么假设在服务器A的tomcat内部，有两个线程，这两个线程由于使用的是同一份代码，那么他们的锁对象是同一个，是可以实现互斥的，但是如果现在是服务器B的tomcat内部，又有两个线程，但是他们的锁对象写的虽然和服务器A一样，但是锁对象却不是同一个，所以线程3和线程4可以实现互斥，但是却无法和线程1和线程2实现互斥，这就是 集群环境下，syn锁失效的原因，在这种情况下，我们就需要使用分布式锁来解决这个问题。</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653374044740.png" alt></p><h2 id="4、分布式锁">4、分布式锁</h2><h3 id="4-1-、基本原理和实现方式对比">4.1 、基本原理和实现方式对比</h3><p>分布式锁：满足分布式系统或集群模式下多进程可见并且互斥的锁。</p><p>分布式锁的核心思想就是让大家都使用同一把锁，只要大家使用的是同一把锁，那么我们就能锁住线程，不让线程进行，让程序串行执行，这就是分布式锁的核心思路</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653374296906.png" alt></p><p>那么分布式锁他应该满足一些什么样的条件呢？</p><p>可见性：多个线程都能看到相同的结果，注意：这个地方说的可见性并不是并发编程中指的内存可见性，只是说多个进程之间都能感知到变化的意思</p><p>互斥：互斥是分布式锁的最基本的条件，使得程序串行执行</p><p>高可用：程序不易崩溃，时时刻刻都保证较高的可用性</p><p>高性能：由于加锁本身就让性能降低，所有对于分布式锁本身需要他就较高的加锁性能和释放锁性能</p><p>安全性：安全也是程序中必不可少的一环</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653381992018.png" alt></p><p>常见的分布式锁有三种</p><p>Mysql：mysql本身就带有锁机制，但是由于mysql性能本身一般，所以采用分布式锁的情况下，其实使用mysql作为分布式锁比较少见</p><p>Redis：redis作为分布式锁是非常常见的一种使用方式，现在企业级开发中基本都使用redis或者zookeeper作为分布式锁，利用setnx这个方法，如果插入key成功，则表示获得到了锁，如果有人插入成功，其他人插入失败则表示无法获得到锁，利用这套逻辑来实现分布式锁</p><p>Zookeeper：zookeeper也是企业级开发中较好的一个实现分布式锁的方案，由于本套视频并不讲解zookeeper的原理和分布式锁的实现，所以不过多阐述</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653382219377.png" alt></p><h3 id="4-2-、Redis分布式锁的实现核心思路">4.2 、Redis分布式锁的实现核心思路</h3><p>实现分布式锁时需要实现的两个基本方法：</p><ul><li><p>获取锁：</p><ul><li>互斥：确保只能有一个线程获取锁</li><li>非阻塞：尝试一次，成功返回true，失败返回false</li></ul></li><li><p>释放锁：</p><ul><li>手动释放</li><li>超时释放：获取锁时添加一个超时时间</li></ul><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653382669900.png" alt></p></li></ul><p>核心思路：</p><p>我们利用redis 的setNx 方法，当有多个线程进入时，我们就利用该方法，第一个线程进入时，redis 中就有这个key 了，返回了1，如果结果是1，则表示他抢到了锁，那么他去执行业务，然后再删除锁，退出锁逻辑，没有抢到锁的哥们，等待一定时间后重试即可</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653382830810.png" alt></p><h3 id="4-3-实现分布式锁版本一">4.3 实现分布式锁版本一</h3><ul><li>加锁逻辑</li></ul><p><strong>锁的基本接口</strong></p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1656079017728.png" alt="1656079017728"></p><p><strong>SimpleRedisLock</strong></p><p>利用setnx方法进行加锁，同时增加过期时间，防止死锁，此方法可以保证加锁和增加过期时间具有原子性</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="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> String KEY_PREFIX=<span class="string">&quot;lock:&quot;</span></span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> <span class="type">boolean</span> <span class="title function_">tryLock</span><span class="params">(<span class="type">long</span> timeoutSec)</span> &#123;</span><br><span class="line">    <span class="comment">// 获取线程标示</span></span><br><span class="line">    <span class="type">String</span> <span class="variable">threadId</span> <span class="operator">=</span> Thread.currentThread().getId()</span><br><span class="line">    <span class="comment">// 获取锁</span></span><br><span class="line">    <span class="type">Boolean</span> <span class="variable">success</span> <span class="operator">=</span> stringRedisTemplate.opsForValue()</span><br><span class="line">            .setIfAbsent(KEY_PREFIX + name, threadId + <span class="string">&quot;&quot;</span>, timeoutSec, TimeUnit.SECONDS);</span><br><span class="line">    <span class="keyword">return</span> Boolean.TRUE.equals(success);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>释放锁逻辑</li></ul><p>SimpleRedisLock</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">unlock</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="comment">//通过del删除锁</span></span><br><span class="line">    stringRedisTemplate.delete(KEY_PREFIX + name);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>修改业务代码</li></ul><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></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line">  <span class="keyword">public</span> Result <span class="title function_">seckillVoucher</span><span class="params">(Long voucherId)</span> &#123;</span><br><span class="line">      <span class="comment">// 1.查询优惠券</span></span><br><span class="line">      <span class="type">SeckillVoucher</span> <span class="variable">voucher</span> <span class="operator">=</span> seckillVoucherService.getById(voucherId);</span><br><span class="line">      <span class="comment">// 2.判断秒杀是否开始</span></span><br><span class="line">      <span class="keyword">if</span> (voucher.getBeginTime().isAfter(LocalDateTime.now())) &#123;</span><br><span class="line">          <span class="comment">// 尚未开始</span></span><br><span class="line">          <span class="keyword">return</span> Result.fail(<span class="string">&quot;秒杀尚未开始！&quot;</span>);</span><br><span class="line">      &#125;</span><br><span class="line">      <span class="comment">// 3.判断秒杀是否已经结束</span></span><br><span class="line">      <span class="keyword">if</span> (voucher.getEndTime().isBefore(LocalDateTime.now())) &#123;</span><br><span class="line">          <span class="comment">// 尚未开始</span></span><br><span class="line">          <span class="keyword">return</span> Result.fail(<span class="string">&quot;秒杀已经结束！&quot;</span>);</span><br><span class="line">      &#125;</span><br><span class="line">      <span class="comment">// 4.判断库存是否充足</span></span><br><span class="line">      <span class="keyword">if</span> (voucher.getStock() &lt; <span class="number">1</span>) &#123;</span><br><span class="line">          <span class="comment">// 库存不足</span></span><br><span class="line">          <span class="keyword">return</span> Result.fail(<span class="string">&quot;库存不足！&quot;</span>);</span><br><span class="line">      &#125;</span><br><span class="line">      <span class="type">Long</span> <span class="variable">userId</span> <span class="operator">=</span> UserHolder.getUser().getId();</span><br><span class="line">      <span class="comment">//创建锁对象(新增代码)</span></span><br><span class="line">      <span class="type">SimpleRedisLock</span> <span class="variable">lock</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">SimpleRedisLock</span>(<span class="string">&quot;order:&quot;</span> + userId, stringRedisTemplate);</span><br><span class="line">      <span class="comment">//获取锁对象</span></span><br><span class="line">      <span class="type">boolean</span> <span class="variable">isLock</span> <span class="operator">=</span> lock.tryLock(<span class="number">1200</span>);</span><br><span class="line"><span class="comment">//加锁失败</span></span><br><span class="line">      <span class="keyword">if</span> (!isLock) &#123;</span><br><span class="line">          <span class="keyword">return</span> Result.fail(<span class="string">&quot;不允许重复下单&quot;</span>);</span><br><span class="line">      &#125;</span><br><span class="line">      <span class="keyword">try</span> &#123;</span><br><span class="line">          <span class="comment">//获取代理对象(事务)</span></span><br><span class="line">          <span class="type">IVoucherOrderService</span> <span class="variable">proxy</span> <span class="operator">=</span> (IVoucherOrderService) AopContext.currentProxy();</span><br><span class="line">          <span class="keyword">return</span> proxy.createVoucherOrder(voucherId);</span><br><span class="line">      &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">          <span class="comment">//释放锁</span></span><br><span class="line">          lock.unlock();</span><br><span class="line">      &#125;</span><br><span class="line">  &#125;</span><br></pre></td></tr></table></figure><h3 id="4-4-Redis分布式锁误删情况说明">4.4 Redis分布式锁误删情况说明</h3><p>逻辑说明：</p><p>持有锁的线程在锁的内部出现了阻塞，导致他的锁自动释放，这时其他线程，线程2来尝试获得锁，就拿到了这把锁，然后线程2在持有锁执行过程中，线程1反应过来，继续执行，而线程1执行过程中，走到了删除锁逻辑，此时就会把本应该属于线程2的锁进行删除，这就是误删别人锁的情况说明</p><p>解决方案：解决方案就是在每个线程释放锁的时候，去判断一下当前这把锁是否属于自己，如果属于自己，则不进行锁的删除，假设还是上边的情况，线程1卡顿，锁自动释放，线程2进入到锁的内部执行逻辑，此时线程1反应过来，然后删除锁，但是线程1，一看当前这把锁不是属于自己，于是不进行删除锁逻辑，当线程2走到删除锁逻辑时，如果没有卡过自动释放锁的时间点，则判断当前这把锁是属于自己的，于是删除这把锁。</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653385920025.png" alt></p><h3 id="4-5-解决Redis分布式锁误删问题">4.5 解决Redis分布式锁误删问题</h3><p>需求：修改之前的分布式锁实现，满足：在获取锁时存入线程标示（可以用UUID表示）<br>在释放锁时先获取锁中的线程标示，判断是否与当前线程标示一致</p><ul><li>如果一致则释放锁</li><li>如果不一致则不释放锁</li></ul><p>核心逻辑：在存入锁时，放入自己线程的标识，在删除锁时，判断当前这把锁的标识是不是自己存入的，如果是，则进行删除，如果不是，则不进行删除。</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653387398820.png" alt></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="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">String</span> <span class="variable">ID_PREFIX</span> <span class="operator">=</span> UUID.randomUUID().toString(<span class="literal">true</span>) + <span class="string">&quot;-&quot;</span>;</span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> <span class="type">boolean</span> <span class="title function_">tryLock</span><span class="params">(<span class="type">long</span> timeoutSec)</span> &#123;</span><br><span class="line">   <span class="comment">// 获取线程标示</span></span><br><span class="line">   <span class="type">String</span> <span class="variable">threadId</span> <span class="operator">=</span> ID_PREFIX + Thread.currentThread().getId();</span><br><span class="line">   <span class="comment">// 获取锁</span></span><br><span class="line">   <span class="type">Boolean</span> <span class="variable">success</span> <span class="operator">=</span> stringRedisTemplate.opsForValue()</span><br><span class="line">                .setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);</span><br><span class="line">   <span class="keyword">return</span> Boolean.TRUE.equals(success);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>释放锁</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">unlock</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="comment">// 获取线程标示</span></span><br><span class="line">    <span class="type">String</span> <span class="variable">threadId</span> <span class="operator">=</span> ID_PREFIX + Thread.currentThread().getId();</span><br><span class="line">    <span class="comment">// 获取锁中的标示</span></span><br><span class="line">    <span class="type">String</span> <span class="variable">id</span> <span class="operator">=</span> stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);</span><br><span class="line">    <span class="comment">// 判断标示是否一致</span></span><br><span class="line">    <span class="keyword">if</span>(threadId.equals(id)) &#123;</span><br><span class="line">        <span class="comment">// 释放锁</span></span><br><span class="line">        stringRedisTemplate.delete(KEY_PREFIX + name);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>有关代码实操说明：</strong></p><p>在我们修改完此处代码后，我们重启工程，然后启动两个线程，第一个线程持有锁后，手动释放锁，第二个线程 此时进入到锁内部，再放行第一个线程，此时第一个线程由于锁的value值并非是自己，所以不能释放锁，也就无法删除别人的锁，此时第二个线程能够正确释放锁，通过这个案例初步说明我们解决了锁误删的问题。</p><h3 id="4-6-分布式锁的原子性问题">4.6 分布式锁的原子性问题</h3><p>更为极端的误删逻辑说明：</p><p>线程1现在持有锁之后，在执行业务逻辑过程中，他正准备删除锁，而且已经走到了条件判断的过程中，比如他已经拿到了当前这把锁确实是属于他自己的，正准备删除锁，但是此时他的锁到期了，那么此时线程2进来，但是线程1他会接着往后执行，当他卡顿结束后，他直接就会执行删除锁那行代码，相当于条件判断并没有起到作用，这就是删锁时的原子性问题，之所以有这个问题，是因为线程1的拿锁，比锁，删锁，实际上并不是原子性的，我们要防止刚才的情况发生，</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653387764938.png" alt></p><h3 id="4-7-Lua脚本解决多条命令原子性问题">4.7 Lua脚本解决多条命令原子性问题</h3><p>Redis提供了Lua脚本功能，在一个脚本中编写多条Redis命令，确保多条命令执行时的原子性。Lua是一种编程语言，它的基本语法大家可以参考网站：<a href="https://www.runoob.com/lua/lua-tutorial.html%EF%BC%8C%E8%BF%99%E9%87%8C%E9%87%8D%E7%82%B9%E4%BB%8B%E7%BB%8DRedis%E6%8F%90%E4%BE%9B%E7%9A%84%E8%B0%83%E7%94%A8%E5%87%BD%E6%95%B0%EF%BC%8C%E6%88%91%E4%BB%AC%E5%8F%AF%E4%BB%A5%E4%BD%BF%E7%94%A8lua%E5%8E%BB%E6%93%8D%E4%BD%9Credis%EF%BC%8C%E5%8F%88%E8%83%BD%E4%BF%9D%E8%AF%81%E4%BB%96%E7%9A%84%E5%8E%9F%E5%AD%90%E6%80%A7%EF%BC%8C%E8%BF%99%E6%A0%B7%E5%B0%B1%E5%8F%AF%E4%BB%A5%E5%AE%9E%E7%8E%B0%E6%8B%BF%E9%94%81%E6%AF%94%E9%94%81%E5%88%A0%E9%94%81%E6%98%AF%E4%B8%80%E4%B8%AA%E5%8E%9F%E5%AD%90%E6%80%A7%E5%8A%A8%E4%BD%9C%E4%BA%86%EF%BC%8C%E4%BD%9C%E4%B8%BAJava%E7%A8%8B%E5%BA%8F%E5%91%98%E8%BF%99%E4%B8%80%E5%9D%97%E5%B9%B6%E4%B8%8D%E4%BD%9C%E4%B8%80%E4%B8%AA%E7%AE%80%E5%8D%95%E8%A6%81%E6%B1%82%EF%BC%8C%E5%B9%B6%E4%B8%8D%E9%9C%80%E8%A6%81%E5%A4%A7%E5%AE%B6%E8%BF%87%E4%BA%8E%E7%B2%BE%E9%80%9A%EF%BC%8C%E5%8F%AA%E9%9C%80%E8%A6%81%E7%9F%A5%E9%81%93%E4%BB%96%E6%9C%89%E4%BB%80%E4%B9%88%E4%BD%9C%E7%94%A8%E5%8D%B3%E5%8F%AF%E3%80%82">https://www.runoob.com/lua/lua-tutorial.html，这里重点介绍Redis提供的调用函数，我们可以使用lua去操作redis，又能保证他的原子性，这样就可以实现拿锁比锁删锁是一个原子性动作了，作为Java程序员这一块并不作一个简单要求，并不需要大家过于精通，只需要知道他有什么作用即可。</a></p><p>这里重点介绍Redis提供的调用函数，语法如下：</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">redis.call(<span class="string">&#x27;命令名称&#x27;</span>, <span class="string">&#x27;key&#x27;</span>, <span class="string">&#x27;其它参数&#x27;</span>, ...)</span><br></pre></td></tr></table></figure><p>例如，我们要执行set name jack，则脚本是这样：</p><figure class="highlight lua"><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"># 执行 set name jack</span><br><span class="line">redis.call(<span class="string">&#x27;set&#x27;</span>, <span class="string">&#x27;name&#x27;</span>, <span class="string">&#x27;jack&#x27;</span>)</span><br></pre></td></tr></table></figure><p>例如，我们要先执行set name Rose，再执行get name，则脚本如下：</p><figure class="highlight lua"><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"># 先执行 set name jack</span><br><span class="line">redis.call(<span class="string">&#x27;set&#x27;</span>, <span class="string">&#x27;name&#x27;</span>, <span class="string">&#x27;Rose&#x27;</span>)</span><br><span class="line"># 再执行 get name</span><br><span class="line"><span class="keyword">local</span> name = redis.call(<span class="string">&#x27;get&#x27;</span>, <span class="string">&#x27;name&#x27;</span>)</span><br><span class="line"># 返回</span><br><span class="line"><span class="keyword">return</span> name</span><br></pre></td></tr></table></figure><p>写好脚本以后，需要用Redis命令来调用脚本，调用脚本的常见命令如下：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653392181413.png" alt></p><p>例如，我们要执行 redis.call(‘set’, ‘name’, ‘jack’) 这个脚本，语法如下：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653392218531.png" alt></p><p>如果脚本中的key、value不想写死，可以作为参数传递。key类型参数会放入KEYS数组，其它参数会放入ARGV数组，在脚本中可以从KEYS和ARGV数组获取这些参数：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653392438917.png" alt></p><p>接下来我们来回一下我们释放锁的逻辑：</p><p>释放锁的业务流程是这样的</p><p>​1、获取锁中的线程标示</p><p>​2、判断是否与指定的标示（当前线程标示）一致</p><p>​3、如果一致则释放锁（删除）</p><p>​4、如果不一致则什么都不做</p><p>如果用Lua脚本来表示则是这样的：</p><p>最终我们操作redis的拿锁比锁删锁的lua脚本就会变成这样</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 这里的 KEYS[1] 就是锁的key，这里的ARGV[1] 就是当前线程标示</span></span><br><span class="line"><span class="comment">-- 获取锁中的标示，判断是否与当前线程标示一致</span></span><br><span class="line"><span class="keyword">if</span> (redis.call(<span class="string">&#x27;GET&#x27;</span>, KEYS[<span class="number">1</span>]) == ARGV[<span class="number">1</span>]) <span class="keyword">then</span></span><br><span class="line">  <span class="comment">-- 一致，则删除锁</span></span><br><span class="line">  <span class="keyword">return</span> redis.call(<span class="string">&#x27;DEL&#x27;</span>, KEYS[<span class="number">1</span>])</span><br><span class="line"><span class="keyword">end</span></span><br><span class="line"><span class="comment">-- 不一致，则直接返回</span></span><br><span class="line"><span class="keyword">return</span> <span class="number">0</span></span><br></pre></td></tr></table></figure><h3 id="4-8-利用Java代码调用Lua脚本改造分布式锁">4.8 利用Java代码调用Lua脚本改造分布式锁</h3><p>lua脚本本身并不需要大家花费太多时间去研究，只需要知道如何调用，大致是什么意思即可，所以在笔记中并不会详细的去解释这些lua表达式的含义。</p><p>我们的RedisTemplate中，可以利用execute方法去执行lua脚本，参数对应关系就如下图股</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653393304844.png" alt></p><p><strong>Java代码</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> DefaultRedisScript&lt;Long&gt; UNLOCK_SCRIPT;</span><br><span class="line">    <span class="keyword">static</span> &#123;</span><br><span class="line">        UNLOCK_SCRIPT = <span class="keyword">new</span> <span class="title class_">DefaultRedisScript</span>&lt;&gt;();</span><br><span class="line">        UNLOCK_SCRIPT.setLocation(<span class="keyword">new</span> <span class="title class_">ClassPathResource</span>(<span class="string">&quot;unlock.lua&quot;</span>));</span><br><span class="line">        UNLOCK_SCRIPT.setResultType(Long.class);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">unlock</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="comment">// 调用lua脚本</span></span><br><span class="line">    stringRedisTemplate.execute(</span><br><span class="line">            UNLOCK_SCRIPT,</span><br><span class="line">            Collections.singletonList(KEY_PREFIX + name),</span><br><span class="line">            ID_PREFIX + Thread.currentThread().getId());</span><br><span class="line">&#125;</span><br><span class="line">经过以上代码改造后，我们就能够实现 拿锁比锁删锁的原子性动作了~</span><br></pre></td></tr></table></figure><p>小总结：</p><p>基于Redis的分布式锁实现思路：</p><ul><li>利用set nx ex获取锁，并设置过期时间，保存线程标示</li><li>释放锁时先判断线程标示是否与自己一致，一致则删除锁<ul><li>特性：<ul><li>利用set nx满足互斥性</li><li>利用set ex保证故障时锁依然能释放，避免死锁，提高安全性</li><li>利用Redis集群保证高可用和高并发特性</li></ul></li></ul></li></ul><p>笔者总结：我们一路走来，利用添加过期时间，防止死锁问题的发生，但是有了过期时间之后，可能出现误删别人锁的问题，这个问题我们开始是利用删之前 通过拿锁，比锁，删锁这个逻辑来解决的，也就是删之前判断一下当前这把锁是否是属于自己的，但是现在还有原子性问题，也就是我们没法保证拿锁比锁删锁是一个原子性的动作，最后通过lua表达式来解决这个问题</p><p>但是目前还剩下一个问题锁不住，什么是锁不住呢，你想一想，如果当过期时间到了之后，我们可以给他续期一下，比如续个30s，就好像是网吧上网， 网费到了之后，然后说，来，网管，再给我来10块的，是不是后边的问题都不会发生了，那么续期问题怎么解决呢，可以依赖于我们接下来要学习redission啦</p><p><strong>测试逻辑：</strong></p><p>第一个线程进来，得到了锁，手动删除锁，模拟锁超时了，其他线程会执行lua来抢锁，当第一天线程利用lua删除锁时，lua能保证他不能删除他的锁，第二个线程删除锁时，利用lua同样可以保证不会删除别人的锁，同时还能保证原子性。</p><h2 id="5、分布式锁-redission">5、分布式锁-redission</h2><h3 id="5-1-分布式锁-redission功能介绍">5.1 分布式锁-redission功能介绍</h3><p>基于setnx实现的分布式锁存在下面的问题：</p><p><strong>重入问题</strong>：重入问题是指 获得锁的线程可以再次进入到相同的锁的代码块中，可重入锁的意义在于防止死锁，比如HashTable这样的代码中，他的方法都是使用synchronized修饰的，假如他在一个方法内，调用另一个方法，那么此时如果是不可重入的，不就死锁了吗？所以可重入锁他的主要意义是防止死锁，我们的synchronized和Lock锁都是可重入的。</p><p><strong>不可重试</strong>：是指目前的分布式只能尝试一次，我们认为合理的情况是：当线程在获得锁失败后，他应该能再次尝试获得锁。</p><p>**超时释放：**我们在加锁时增加了过期时间，这样的我们可以防止死锁，但是如果卡顿的时间超长，虽然我们采用了lua表达式防止删锁的时候，误删别人的锁，但是毕竟没有锁住，有安全隐患</p><p><strong>主从一致性：</strong> 如果Redis提供了主从集群，当我们向集群写数据时，主机需要异步的将数据同步给从机，而万一在同步过去之前，主机宕机了，就会出现死锁问题。</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653546070602.png" alt></p><p>那么什么是Redission呢</p><p>Redisson是一个在Redis的基础上实现的Java驻内存数据网格（In-Memory Data Grid）。它不仅提供了一系列的分布式的Java常用对象，还提供了许多分布式服务，其中就包含了各种分布式锁的实现。</p><p>Redission提供了分布式锁的多种多样的功能</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653546736063.png" alt></p><h3 id="5-2-分布式锁-Redission快速入门">5.2 分布式锁-Redission快速入门</h3><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></pre></td><td class="code"><pre><span class="line">&lt;dependency&gt;</span><br><span class="line">&lt;groupId&gt;org.redisson&lt;/groupId&gt;</span><br><span class="line">&lt;artifactId&gt;redisson&lt;/artifactId&gt;</span><br><span class="line">&lt;version&gt;<span class="number">3.13</span><span class="number">.6</span>&lt;/version&gt;</span><br><span class="line">&lt;/dependency&gt;</span><br></pre></td></tr></table></figure><p>配置Redisson客户端：</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></pre></td><td class="code"><pre><span class="line"><span class="meta">@Configuration</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">RedissonConfig</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Bean</span></span><br><span class="line">    <span class="keyword">public</span> RedissonClient <span class="title function_">redissonClient</span><span class="params">()</span>&#123;</span><br><span class="line">        <span class="comment">// 配置</span></span><br><span class="line">        <span class="type">Config</span> <span class="variable">config</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Config</span>();</span><br><span class="line">        config.useSingleServer().setAddress(<span class="string">&quot;redis://192.168.150.101:6379&quot;</span>)</span><br><span class="line">            .setPassword(<span class="string">&quot;123321&quot;</span>);</span><br><span class="line">        <span class="comment">// 创建RedissonClient对象</span></span><br><span class="line">        <span class="keyword">return</span> Redisson.create(config);</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>如何使用Redission的分布式锁</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></pre></td><td class="code"><pre><span class="line"><span class="meta">@Resource</span></span><br><span class="line"><span class="keyword">private</span> RedissionClient redissonClient;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Test</span></span><br><span class="line"><span class="keyword">void</span> <span class="title function_">testRedisson</span><span class="params">()</span> <span class="keyword">throws</span> Exception&#123;</span><br><span class="line">    <span class="comment">//获取锁(可重入)，指定锁的名称</span></span><br><span class="line">    <span class="type">RLock</span> <span class="variable">lock</span> <span class="operator">=</span> redissonClient.getLock(<span class="string">&quot;anyLock&quot;</span>);</span><br><span class="line">    <span class="comment">//尝试获取锁，参数分别是：获取锁的最大等待时间(期间会重试)，锁自动释放时间，时间单位</span></span><br><span class="line">    <span class="type">boolean</span> <span class="variable">isLock</span> <span class="operator">=</span> lock.tryLock(<span class="number">1</span>,<span class="number">10</span>,TimeUnit.SECONDS);</span><br><span class="line">    <span class="comment">//判断获取锁成功</span></span><br><span class="line">    <span class="keyword">if</span>(isLock)&#123;</span><br><span class="line">        <span class="keyword">try</span>&#123;</span><br><span class="line">            System.out.println(<span class="string">&quot;执行业务&quot;</span>);          </span><br><span class="line">        &#125;<span class="keyword">finally</span>&#123;</span><br><span class="line">            <span class="comment">//释放锁</span></span><br><span class="line">            lock.unlock();</span><br><span class="line">        &#125;</span><br><span class="line">        </span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    </span><br><span class="line">    </span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在 VoucherOrderServiceImpl</p><p>注入RedissonClient</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></pre></td><td class="code"><pre><span class="line"><span class="meta">@Resource</span></span><br><span class="line"><span class="keyword">private</span> RedissonClient redissonClient;</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> Result <span class="title function_">seckillVoucher</span><span class="params">(Long voucherId)</span> &#123;</span><br><span class="line">        <span class="comment">// 1.查询优惠券</span></span><br><span class="line">        <span class="type">SeckillVoucher</span> <span class="variable">voucher</span> <span class="operator">=</span> seckillVoucherService.getById(voucherId);</span><br><span class="line">        <span class="comment">// 2.判断秒杀是否开始</span></span><br><span class="line">        <span class="keyword">if</span> (voucher.getBeginTime().isAfter(LocalDateTime.now())) &#123;</span><br><span class="line">            <span class="comment">// 尚未开始</span></span><br><span class="line">            <span class="keyword">return</span> Result.fail(<span class="string">&quot;秒杀尚未开始！&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">// 3.判断秒杀是否已经结束</span></span><br><span class="line">        <span class="keyword">if</span> (voucher.getEndTime().isBefore(LocalDateTime.now())) &#123;</span><br><span class="line">            <span class="comment">// 尚未开始</span></span><br><span class="line">            <span class="keyword">return</span> Result.fail(<span class="string">&quot;秒杀已经结束！&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">// 4.判断库存是否充足</span></span><br><span class="line">        <span class="keyword">if</span> (voucher.getStock() &lt; <span class="number">1</span>) &#123;</span><br><span class="line">            <span class="comment">// 库存不足</span></span><br><span class="line">            <span class="keyword">return</span> Result.fail(<span class="string">&quot;库存不足！&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="type">Long</span> <span class="variable">userId</span> <span class="operator">=</span> UserHolder.getUser().getId();</span><br><span class="line">        <span class="comment">//创建锁对象 这个代码不用了，因为我们现在要使用分布式锁</span></span><br><span class="line">        <span class="comment">//SimpleRedisLock lock = new SimpleRedisLock(&quot;order:&quot; + userId, stringRedisTemplate);</span></span><br><span class="line">        <span class="type">RLock</span> <span class="variable">lock</span> <span class="operator">=</span> redissonClient.getLock(<span class="string">&quot;lock:order:&quot;</span> + userId);</span><br><span class="line">        <span class="comment">//获取锁对象</span></span><br><span class="line">        <span class="type">boolean</span> <span class="variable">isLock</span> <span class="operator">=</span> lock.tryLock();</span><br><span class="line">       </span><br><span class="line"><span class="comment">//加锁失败</span></span><br><span class="line">        <span class="keyword">if</span> (!isLock) &#123;</span><br><span class="line">            <span class="keyword">return</span> Result.fail(<span class="string">&quot;不允许重复下单&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            <span class="comment">//获取代理对象(事务)</span></span><br><span class="line">            <span class="type">IVoucherOrderService</span> <span class="variable">proxy</span> <span class="operator">=</span> (IVoucherOrderService) AopContext.currentProxy();</span><br><span class="line">            <span class="keyword">return</span> proxy.createVoucherOrder(voucherId);</span><br><span class="line">        &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">            <span class="comment">//释放锁</span></span><br><span class="line">            lock.unlock();</span><br><span class="line">        &#125;</span><br><span class="line"> &#125;</span><br></pre></td></tr></table></figure><h3 id="5-3-分布式锁-redission可重入锁原理">5.3 分布式锁-redission可重入锁原理</h3><p>在Lock锁中，他是借助于底层的一个voaltile的一个state变量来记录重入的状态的，比如当前没有人持有这把锁，那么state=0，假如有人持有这把锁，那么state=1，如果持有这把锁的人再次持有这把锁，那么state就会+1 ，如果是对于synchronized而言，他在c语言代码中会有一个count，原理和state类似，也是重入一次就加一，释放一次就-1 ，直到减少成0 时，表示当前这把锁没有被人持有。</p><p>在redission中，我们的也支持支持可重入锁</p><p>在分布式锁中，他采用hash结构用来存储锁，其中大key表示表示这把锁是否存在，用小key表示当前这把锁被哪个线程持有，所以接下来我们一起分析一下当前的这个lua表达式</p><p>这个地方一共有3个参数</p><p><strong>KEYS[1] ： 锁名称</strong></p><p><strong>ARGV[1]：  锁失效时间</strong></p><p><strong>ARGV[2]：  id + “:” + threadId; 锁的小key</strong></p><p>exists: 判断数据是否存在  name：是lock是否存在,如果==0，就表示当前这把锁不存在</p><p>redis.call(‘hset’, KEYS[1], ARGV[2], 1);此时他就开始往redis里边去写数据 ，写成一个hash结构</p><p>Lock{</p><p>​    id + <strong>“:”</strong> + threadId :  1</p><p>}</p><p>如果当前这把锁存在，则第一个条件不满足，再判断</p><p>redis.call(‘hexists’, KEYS[1], ARGV[2]) == 1</p><p>此时需要通过大key+小key判断当前这把锁是否是属于自己的，如果是自己的，则进行</p><p>redis.call(‘hincrby’, KEYS[1], ARGV[2], 1)</p><p>将当前这个锁的value进行+1 ，redis.call(‘pexpire’, KEYS[1], ARGV[1]); 然后再对其设置过期时间，如果以上两个条件都不满足，则表示当前这把锁抢锁失败，最后返回pttl，即为当前这把锁的失效时间</p><p>如果小伙帮们看了前边的源码， 你会发现他会去判断当前这个方法的返回值是否为null，如果是null，则对应则前两个if对应的条件，退出抢锁逻辑，如果返回的不是null，即走了第三个分支，在源码处会进行while(true)的自旋抢锁。</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="string">&quot;if (redis.call(&#x27;exists&#x27;, KEYS[1]) == 0) then &quot;</span> +</span><br><span class="line">                  <span class="string">&quot;redis.call(&#x27;hset&#x27;, KEYS[1], ARGV[2], 1); &quot;</span> +</span><br><span class="line">                  <span class="string">&quot;redis.call(&#x27;pexpire&#x27;, KEYS[1], ARGV[1]); &quot;</span> +</span><br><span class="line">                  <span class="string">&quot;return nil; &quot;</span> +</span><br><span class="line">              <span class="string">&quot;end; &quot;</span> +</span><br><span class="line">              <span class="string">&quot;if (redis.call(&#x27;hexists&#x27;, KEYS[1], ARGV[2]) == 1) then &quot;</span> +</span><br><span class="line">                  <span class="string">&quot;redis.call(&#x27;hincrby&#x27;, KEYS[1], ARGV[2], 1); &quot;</span> +</span><br><span class="line">                  <span class="string">&quot;redis.call(&#x27;pexpire&#x27;, KEYS[1], ARGV[1]); &quot;</span> +</span><br><span class="line">                  <span class="string">&quot;return nil; &quot;</span> +</span><br><span class="line">              <span class="string">&quot;end; &quot;</span> +</span><br><span class="line">              <span class="string">&quot;return redis.call(&#x27;pttl&#x27;, KEYS[1]);&quot;</span></span><br></pre></td></tr></table></figure><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653548087334.png" alt></p><h3 id="5-4-分布式锁-redission锁重试和WatchDog机制">5.4 分布式锁-redission锁重试和WatchDog机制</h3><p><strong>说明</strong>：由于课程中已经说明了有关tryLock的源码解析以及其看门狗原理，所以笔者在这里给大家分析lock()方法的源码解析，希望大家在学习过程中，能够掌握更多的知识</p><p>抢锁过程中，获得当前线程，通过tryAcquire进行抢锁，该抢锁逻辑和之前逻辑相同</p><p>1、先判断当前这把锁是否存在，如果不存在，插入一把锁，返回null</p><p>2、判断当前这把锁是否是属于当前线程，如果是，则返回null</p><p>所以如果返回是null，则代表着当前这哥们已经抢锁完毕，或者可重入完毕，但是如果以上两个条件都不满足，则进入到第三个条件，返回的是锁的失效时间，同学们可以自行往下翻一点点，你能发现有个while( true) 再次进行tryAcquire进行抢锁</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">long</span> <span class="variable">threadId</span> <span class="operator">=</span> Thread.currentThread().getId();</span><br><span class="line"><span class="type">Long</span> <span class="variable">ttl</span> <span class="operator">=</span> tryAcquire(-<span class="number">1</span>, leaseTime, unit, threadId);</span><br><span class="line"><span class="comment">// lock acquired</span></span><br><span class="line"><span class="keyword">if</span> (ttl == <span class="literal">null</span>) &#123;</span><br><span class="line">    <span class="keyword">return</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>接下来会有一个条件分支，因为lock方法有重载方法，一个是带参数，一个是不带参数，如果带带参数传入的值是-1，如果传入参数，则leaseTime是他本身，所以如果传入了参数，此时leaseTime != -1 则会进去抢锁，抢锁的逻辑就是之前说的那三个逻辑</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (leaseTime != -<span class="number">1</span>) &#123;</span><br><span class="line">    <span class="keyword">return</span> tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>如果是没有传入时间，则此时也会进行抢锁， 而且抢锁时间是默认看门狗时间 commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout()</p><p>ttlRemainingFuture.onComplete((ttlRemaining, e) 这句话相当于对以上抢锁进行了监听，也就是说当上边抢锁完毕后，此方法会被调用，具体调用的逻辑就是去后台开启一个线程，进行续约逻辑，也就是看门狗线程</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></pre></td><td class="code"><pre><span class="line">RFuture&lt;Long&gt; ttlRemainingFuture = tryLockInnerAsync(waitTime,</span><br><span class="line">                                        commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(),</span><br><span class="line">                                        TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);</span><br><span class="line">ttlRemainingFuture.onComplete((ttlRemaining, e) -&gt; &#123;</span><br><span class="line">    <span class="keyword">if</span> (e != <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"></span><br><span class="line">    <span class="comment">// lock acquired</span></span><br><span class="line">    <span class="keyword">if</span> (ttlRemaining == <span class="literal">null</span>) &#123;</span><br><span class="line">        scheduleExpirationRenewal(threadId);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;);</span><br><span class="line"><span class="keyword">return</span> ttlRemainingFuture;</span><br></pre></td></tr></table></figure><p>此逻辑就是续约逻辑，注意看commandExecutor.getConnectionManager().newTimeout（） 此方法</p><p>Method(  <strong>new</strong> TimerTask() {},参数2 ，参数3  )</p><p>指的是：通过参数2，参数3 去描述什么时候去做参数1的事情，现在的情况是：10s之后去做参数一的事情</p><p>因为锁的失效时间是30s，当10s之后，此时这个timeTask 就触发了，他就去进行续约，把当前这把锁续约成30s，如果操作成功，那么此时就会递归调用自己，再重新设置一个timeTask()，于是再过10s后又再设置一个timerTask，完成不停的续约</p><p>那么大家可以想一想，假设我们的线程出现了宕机他还会续约吗？当然不会，因为没有人再去调用renewExpiration这个方法，所以等到时间之后自然就释放了。</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title function_">renewExpiration</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="type">ExpirationEntry</span> <span class="variable">ee</span> <span class="operator">=</span> EXPIRATION_RENEWAL_MAP.get(getEntryName());</span><br><span class="line">    <span class="keyword">if</span> (ee == <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">    </span><br><span class="line">    <span class="type">Timeout</span> <span class="variable">task</span> <span class="operator">=</span> commandExecutor.getConnectionManager().newTimeout(<span class="keyword">new</span> <span class="title class_">TimerTask</span>() &#123;</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_">run</span><span class="params">(Timeout timeout)</span> <span class="keyword">throws</span> Exception &#123;</span><br><span class="line">            <span class="type">ExpirationEntry</span> <span class="variable">ent</span> <span class="operator">=</span> EXPIRATION_RENEWAL_MAP.get(getEntryName());</span><br><span class="line">            <span class="keyword">if</span> (ent == <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">            <span class="type">Long</span> <span class="variable">threadId</span> <span class="operator">=</span> ent.getFirstThreadId();</span><br><span class="line">            <span class="keyword">if</span> (threadId == <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">            </span><br><span class="line">            RFuture&lt;Boolean&gt; future = renewExpirationAsync(threadId);</span><br><span class="line">            future.onComplete((res, e) -&gt; &#123;</span><br><span class="line">                <span class="keyword">if</span> (e != <span class="literal">null</span>) &#123;</span><br><span class="line">                    log.error(<span class="string">&quot;Can&#x27;t update lock &quot;</span> + getName() + <span class="string">&quot; expiration&quot;</span>, e);</span><br><span class="line">                    <span class="keyword">return</span>;</span><br><span class="line">                &#125;</span><br><span class="line">                </span><br><span class="line">                <span class="keyword">if</span> (res) &#123;</span><br><span class="line">                    <span class="comment">// reschedule itself</span></span><br><span class="line">                    renewExpiration();</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;, internalLockLeaseTime / <span class="number">3</span>, TimeUnit.MILLISECONDS);</span><br><span class="line">    </span><br><span class="line">    ee.setTimeout(task);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="5-5-分布式锁-redission锁的MutiLock原理">5.5 分布式锁-redission锁的MutiLock原理</h3><p>为了提高redis的可用性，我们会搭建集群或者主从，现在以主从为例</p><p>此时我们去写命令，写在主机上， 主机会将数据同步给从机，但是假设在主机还没有来得及把数据写入到从机去的时候，此时主机宕机，哨兵会发现主机宕机，并且选举一个slave变成master，而此时新的master中实际上并没有锁信息，此时锁信息就已经丢掉了。</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653553998403.png" alt></p><p>为了解决这个问题，redission提出来了MutiLock锁，使用这把锁咱们就不使用主从了，每个节点的地位都是一样的， 这把锁加锁的逻辑需要写入到每一个主丛节点上，只有所有的服务器都写入成功，此时才是加锁成功，假设现在某个节点挂了，那么他去获得锁的时候，只要有一个节点拿不到，都不能算是加锁成功，就保证了加锁的可靠性。</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653554055048.png" alt></p><p>那么MutiLock 加锁原理是什么呢？笔者画了一幅图来说明</p><p>当我们去设置了多个锁时，redission会将多个锁添加到一个集合中，然后用while循环去不停去尝试拿锁，但是会有一个总共的加锁时间，这个时间是用需要加锁的个数 * 1500ms ，假设有3个锁，那么时间就是4500ms，假设在这4500ms内，所有的锁都加锁成功， 那么此时才算是加锁成功，如果在4500ms有线程加锁失败，则会再次去进行重试.</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653553093967.png" alt></p><h2 id="6、秒杀优化">6、秒杀优化</h2><h3 id="6-1-秒杀优化-异步秒杀思路">6.1 秒杀优化-异步秒杀思路</h3><p>我们来回顾一下下单流程</p><p>当用户发起请求，此时会请求nginx，nginx会访问到tomcat，而tomcat中的程序，会进行串行操作，分成如下几个步骤</p><p>1、查询优惠卷</p><p>2、判断秒杀库存是否足够</p><p>3、查询订单</p><p>4、校验是否是一人一单</p><p>5、扣减库存</p><p>6、创建订单</p><p>在这六步操作中，又有很多操作是要去操作数据库的，而且还是一个线程串行执行， 这样就会导致我们的程序执行的很慢，所以我们需要异步程序执行，那么如何加速呢？</p><p>在这里笔者想给大家分享一下课程内没有的思路，看看有没有小伙伴这么想，比如，我们可以不可以使用异步编排来做，或者说我开启N多线程，N多个线程，一个线程执行查询优惠卷，一个执行判断扣减库存，一个去创建订单等等，然后再统一做返回，这种做法和课程中有哪种好呢？答案是课程中的好，因为如果你采用我刚说的方式，如果访问的人很多，那么线程池中的线程可能一下子就被消耗完了，而且你使用上述方案，最大的特点在于，你觉得时效性会非常重要，但是你想想是吗？并不是，比如我只要确定他能做这件事，然后我后边慢慢做就可以了，我并不需要他一口气做完这件事，所以我们应当采用的是课程中，类似消息队列的方式来完成我们的需求，而不是使用线程池或者是异步编排的方式来完成这个需求</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653560986599.png" alt></p><p>优化方案：我们将耗时比较短的逻辑判断放入到redis中，比如是否库存足够，比如是否一人一单，这样的操作，只要这种逻辑可以完成，就意味着我们是一定可以下单完成的，我们只需要进行快速的逻辑判断，根本就不用等下单逻辑走完，我们直接给用户返回成功， 再在后台开一个线程，后台线程慢慢的去执行queue里边的消息，这样程序不就超级快了吗？而且也不用担心线程池消耗殆尽的问题，因为这里我们的程序中并没有手动使用任何线程池，当然这里边有两个难点</p><p>第一个难点是我们怎么在redis中去快速校验一人一单，还有库存判断</p><p>第二个难点是由于我们校验和tomct下单是两个线程，那么我们如何知道到底哪个单他最后是否成功，或者是下单完成，为了完成这件事我们在redis操作完之后，我们会将一些信息返回给前端，同时也会把这些信息丢到异步queue中去，后续操作中，可以通过这个id来查询我们tomcat中的下单逻辑是否完成了。</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653561657295.png" alt></p><p>我们现在来看看整体思路：当用户下单之后，判断库存是否充足只需要导redis中去根据key找对应的value是否大于0即可，如果不充足，则直接结束，如果充足，继续在redis中判断用户是否可以下单，如果set集合中没有这条数据，说明他可以下单，如果set集合中没有这条记录，则将userId和优惠卷存入到redis中，并且返回0，整个过程需要保证是原子性的，我们可以使用lua来操作</p><p>当以上判断逻辑走完之后，我们可以判断当前redis中返回的结果是否是0 ，如果是0，则表示可以下单，则将之前说的信息存入到到queue中去，然后返回，然后再来个线程异步的下单，前端可以通过返回的订单id来判断是否下单成功。</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653562234886.png" alt></p><h3 id="6-2-秒杀优化-Redis完成秒杀资格判断">6.2 秒杀优化-Redis完成秒杀资格判断</h3><p>需求：</p><ul><li><p>新增秒杀优惠券的同时，将优惠券信息保存到Redis中</p></li><li><p>基于Lua脚本，判断秒杀库存、一人一单，决定用户是否抢购成功</p></li><li><p>如果抢购成功，将优惠券id和用户id封装后存入阻塞队列</p></li><li><p>开启线程任务，不断从阻塞队列中获取信息，实现异步下单功能</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1656080546603.png" alt="1656080546603"></p></li></ul><p>VoucherServiceImpl</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></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="meta">@Transactional</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">addSeckillVoucher</span><span class="params">(Voucher voucher)</span> &#123;</span><br><span class="line">    <span class="comment">// 保存优惠券</span></span><br><span class="line">    save(voucher);</span><br><span class="line">    <span class="comment">// 保存秒杀信息</span></span><br><span class="line">    <span class="type">SeckillVoucher</span> <span class="variable">seckillVoucher</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">SeckillVoucher</span>();</span><br><span class="line">    seckillVoucher.setVoucherId(voucher.getId());</span><br><span class="line">    seckillVoucher.setStock(voucher.getStock());</span><br><span class="line">    seckillVoucher.setBeginTime(voucher.getBeginTime());</span><br><span class="line">    seckillVoucher.setEndTime(voucher.getEndTime());</span><br><span class="line">    seckillVoucherService.save(seckillVoucher);</span><br><span class="line">    <span class="comment">// 保存秒杀库存到Redis中</span></span><br><span class="line">    <span class="comment">//SECKILL_STOCK_KEY 这个变量定义在RedisConstans中</span></span><br><span class="line">    <span class="comment">//private static final String SECKILL_STOCK_KEY =&quot;seckill:stock:&quot;</span></span><br><span class="line">    stringRedisTemplate.opsForValue().set(SECKILL_STOCK_KEY + voucher.getId(), voucher.getStock().toString());</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>完整lua表达式</p><figure class="highlight lua"><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></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 1.参数列表</span></span><br><span class="line"><span class="comment">-- 1.1.优惠券id</span></span><br><span class="line"><span class="keyword">local</span> voucherId = ARGV[<span class="number">1</span>]</span><br><span class="line"><span class="comment">-- 1.2.用户id</span></span><br><span class="line"><span class="keyword">local</span> userId = ARGV[<span class="number">2</span>]</span><br><span class="line"><span class="comment">-- 1.3.订单id</span></span><br><span class="line"><span class="keyword">local</span> orderId = ARGV[<span class="number">3</span>]</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 2.数据key</span></span><br><span class="line"><span class="comment">-- 2.1.库存key</span></span><br><span class="line"><span class="keyword">local</span> stockKey = <span class="string">&#x27;seckill:stock:&#x27;</span> .. voucherId</span><br><span class="line"><span class="comment">-- 2.2.订单key</span></span><br><span class="line"><span class="keyword">local</span> orderKey = <span class="string">&#x27;seckill:order:&#x27;</span> .. voucherId</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 3.脚本业务</span></span><br><span class="line"><span class="comment">-- 3.1.判断库存是否充足 get stockKey</span></span><br><span class="line"><span class="keyword">if</span>(<span class="built_in">tonumber</span>(redis.call(<span class="string">&#x27;get&#x27;</span>, stockKey)) &lt;= <span class="number">0</span>) <span class="keyword">then</span></span><br><span class="line">    <span class="comment">-- 3.2.库存不足，返回1</span></span><br><span class="line">    <span class="keyword">return</span> <span class="number">1</span></span><br><span class="line"><span class="keyword">end</span></span><br><span class="line"><span class="comment">-- 3.2.判断用户是否下单 SISMEMBER orderKey userId</span></span><br><span class="line"><span class="keyword">if</span>(redis.call(<span class="string">&#x27;sismember&#x27;</span>, orderKey, userId) == <span class="number">1</span>) <span class="keyword">then</span></span><br><span class="line">    <span class="comment">-- 3.3.存在，说明是重复下单，返回2</span></span><br><span class="line">    <span class="keyword">return</span> <span class="number">2</span></span><br><span class="line"><span class="keyword">end</span></span><br><span class="line"><span class="comment">-- 3.4.扣库存 incrby stockKey -1</span></span><br><span class="line">redis.call(<span class="string">&#x27;incrby&#x27;</span>, stockKey, <span class="number">-1</span>)</span><br><span class="line"><span class="comment">-- 3.5.下单（保存用户）sadd orderKey userId</span></span><br><span class="line">redis.call(<span class="string">&#x27;sadd&#x27;</span>, orderKey, userId)</span><br><span class="line"><span class="comment">-- 3.6.发送消息到队列中， XADD stream.orders * k1 v1 k2 v2 ...</span></span><br><span class="line">redis.call(<span class="string">&#x27;xadd&#x27;</span>, <span class="string">&#x27;stream.orders&#x27;</span>, <span class="string">&#x27;*&#x27;</span>, <span class="string">&#x27;userId&#x27;</span>, userId, <span class="string">&#x27;voucherId&#x27;</span>, voucherId, <span class="string">&#x27;id&#x27;</span>, orderId)</span><br><span class="line"><span class="keyword">return</span> <span class="number">0</span></span><br></pre></td></tr></table></figure><p>当以上lua表达式执行完毕后，剩下的就是根据步骤3,4来执行我们接下来的任务了</p><p>VoucherOrderServiceImpl</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></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">seckillVoucher</span><span class="params">(Long voucherId)</span> &#123;</span><br><span class="line">    <span class="comment">//获取用户</span></span><br><span class="line">    <span class="type">Long</span> <span class="variable">userId</span> <span class="operator">=</span> UserHolder.getUser().getId();</span><br><span class="line">    <span class="type">long</span> <span class="variable">orderId</span> <span class="operator">=</span> redisIdWorker.nextId(<span class="string">&quot;order&quot;</span>);</span><br><span class="line">    <span class="comment">// 1.执行lua脚本</span></span><br><span class="line">    <span class="type">Long</span> <span class="variable">result</span> <span class="operator">=</span> stringRedisTemplate.execute(</span><br><span class="line">            SECKILL_SCRIPT,</span><br><span class="line">            Collections.emptyList(),</span><br><span class="line">            voucherId.toString(), userId.toString(), String.valueOf(orderId)</span><br><span class="line">    );</span><br><span class="line">    <span class="type">int</span> <span class="variable">r</span> <span class="operator">=</span> result.intValue();</span><br><span class="line">    <span class="comment">// 2.判断结果是否为0</span></span><br><span class="line">    <span class="keyword">if</span> (r != <span class="number">0</span>) &#123;</span><br><span class="line">        <span class="comment">// 2.1.不为0 ，代表没有购买资格</span></span><br><span class="line">        <span class="keyword">return</span> Result.fail(r == <span class="number">1</span> ? <span class="string">&quot;库存不足&quot;</span> : <span class="string">&quot;不能重复下单&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">//TODO 保存阻塞队列</span></span><br><span class="line">    <span class="comment">// 3.返回订单id</span></span><br><span class="line">    <span class="keyword">return</span> Result.ok(orderId);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="6-3-秒杀优化-基于阻塞队列实现秒杀优化">6.3 秒杀优化-基于阻塞队列实现秒杀优化</h3><p>VoucherOrderServiceImpl</p><p>修改下单动作，现在我们去下单时，是通过lua表达式去原子执行判断逻辑，如果判断我出来不为0 ，则要么是库存不足，要么是重复下单，返回错误信息，如果是0，则把下单的逻辑保存到队列中去，然后异步执行</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//异步处理线程池</span></span><br><span class="line"><span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">ExecutorService</span> <span class="variable">SECKILL_ORDER_EXECUTOR</span> <span class="operator">=</span> Executors.newSingleThreadExecutor();</span><br><span class="line"></span><br><span class="line"><span class="comment">//在类初始化之后执行，因为当这个类初始化好了之后，随时都是有可能要执行的</span></span><br><span class="line"><span class="meta">@PostConstruct</span></span><br><span class="line"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title function_">init</span><span class="params">()</span> &#123;</span><br><span class="line">   SECKILL_ORDER_EXECUTOR.submit(<span class="keyword">new</span> <span class="title class_">VoucherOrderHandler</span>());</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">// 用于线程池处理的任务</span></span><br><span class="line"><span class="comment">// 当初始化完毕后，就会去从对列中去拿信息</span></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">class</span> <span class="title class_">VoucherOrderHandler</span> <span class="keyword">implements</span> <span class="title class_">Runnable</span>&#123;</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_">run</span><span class="params">()</span> &#123;</span><br><span class="line">            <span class="keyword">while</span> (<span class="literal">true</span>)&#123;</span><br><span class="line">                <span class="keyword">try</span> &#123;</span><br><span class="line">                    <span class="comment">// 1.获取队列中的订单信息</span></span><br><span class="line">                    <span class="type">VoucherOrder</span> <span class="variable">voucherOrder</span> <span class="operator">=</span> orderTasks.take();</span><br><span class="line">                    <span class="comment">// 2.创建订单</span></span><br><span class="line">                    handleVoucherOrder(voucherOrder);</span><br><span class="line">                &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">                    log.error(<span class="string">&quot;处理订单异常&quot;</span>, e);</span><br><span class="line">                &#125;</span><br><span class="line">           &#125;</span><br><span class="line">        &#125;</span><br><span class="line">     </span><br><span class="line">       <span class="keyword">private</span> <span class="keyword">void</span> <span class="title function_">handleVoucherOrder</span><span class="params">(VoucherOrder voucherOrder)</span> &#123;</span><br><span class="line">            <span class="comment">//1.获取用户</span></span><br><span class="line">            <span class="type">Long</span> <span class="variable">userId</span> <span class="operator">=</span> voucherOrder.getUserId();</span><br><span class="line">            <span class="comment">// 2.创建锁对象</span></span><br><span class="line">            <span class="type">RLock</span> <span class="variable">redisLock</span> <span class="operator">=</span> redissonClient.getLock(<span class="string">&quot;lock:order:&quot;</span> + userId);</span><br><span class="line">            <span class="comment">// 3.尝试获取锁</span></span><br><span class="line">            <span class="type">boolean</span> <span class="variable">isLock</span> <span class="operator">=</span> redisLock.lock();</span><br><span class="line">            <span class="comment">// 4.判断是否获得锁成功</span></span><br><span class="line">            <span class="keyword">if</span> (!isLock) &#123;</span><br><span class="line">                <span class="comment">// 获取锁失败，直接返回失败或者重试</span></span><br><span class="line">                log.error(<span class="string">&quot;不允许重复下单！&quot;</span>);</span><br><span class="line">                <span class="keyword">return</span>;</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">try</span> &#123;</span><br><span class="line"><span class="comment">//注意：由于是spring的事务是放在threadLocal中，此时的是多线程，事务会失效</span></span><br><span class="line">                proxy.createVoucherOrder(voucherOrder);</span><br><span class="line">            &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">                <span class="comment">// 释放锁</span></span><br><span class="line">                redisLock.unlock();</span><br><span class="line">            &#125;</span><br><span class="line">    &#125;</span><br><span class="line">     <span class="comment">//a</span></span><br><span class="line"><span class="keyword">private</span> BlockingQueue&lt;VoucherOrder&gt; orderTasks =<span class="keyword">new</span>  <span class="title class_">ArrayBlockingQueue</span>&lt;&gt;(<span class="number">1024</span> * <span class="number">1024</span>);</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> Result <span class="title function_">seckillVoucher</span><span class="params">(Long voucherId)</span> &#123;</span><br><span class="line">        <span class="type">Long</span> <span class="variable">userId</span> <span class="operator">=</span> UserHolder.getUser().getId();</span><br><span class="line">        <span class="type">long</span> <span class="variable">orderId</span> <span class="operator">=</span> redisIdWorker.nextId(<span class="string">&quot;order&quot;</span>);</span><br><span class="line">        <span class="comment">// 1.执行lua脚本</span></span><br><span class="line">        <span class="type">Long</span> <span class="variable">result</span> <span class="operator">=</span> stringRedisTemplate.execute(</span><br><span class="line">                SECKILL_SCRIPT,</span><br><span class="line">                Collections.emptyList(),</span><br><span class="line">                voucherId.toString(), userId.toString(), String.valueOf(orderId)</span><br><span class="line">        );</span><br><span class="line">        <span class="type">int</span> <span class="variable">r</span> <span class="operator">=</span> result.intValue();</span><br><span class="line">        <span class="comment">// 2.判断结果是否为0</span></span><br><span class="line">        <span class="keyword">if</span> (r != <span class="number">0</span>) &#123;</span><br><span class="line">            <span class="comment">// 2.1.不为0 ，代表没有购买资格</span></span><br><span class="line">            <span class="keyword">return</span> Result.fail(r == <span class="number">1</span> ? <span class="string">&quot;库存不足&quot;</span> : <span class="string">&quot;不能重复下单&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="type">VoucherOrder</span> <span class="variable">voucherOrder</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">VoucherOrder</span>();</span><br><span class="line">        <span class="comment">// 2.3.订单id</span></span><br><span class="line">        <span class="type">long</span> <span class="variable">orderId</span> <span class="operator">=</span> redisIdWorker.nextId(<span class="string">&quot;order&quot;</span>);</span><br><span class="line">        voucherOrder.setId(orderId);</span><br><span class="line">        <span class="comment">// 2.4.用户id</span></span><br><span class="line">        voucherOrder.setUserId(userId);</span><br><span class="line">        <span class="comment">// 2.5.代金券id</span></span><br><span class="line">        voucherOrder.setVoucherId(voucherId);</span><br><span class="line">        <span class="comment">// 2.6.放入阻塞队列</span></span><br><span class="line">        orderTasks.add(voucherOrder);</span><br><span class="line">        <span class="comment">//3.获取代理对象</span></span><br><span class="line">         proxy = (IVoucherOrderService)AopContext.currentProxy();</span><br><span class="line">        <span class="comment">//4.返回订单id</span></span><br><span class="line">        <span class="keyword">return</span> Result.ok(orderId);</span><br><span class="line">    &#125;</span><br><span class="line">     </span><br><span class="line">      <span class="meta">@Transactional</span></span><br><span class="line">    <span class="keyword">public</span>  <span class="keyword">void</span> <span class="title function_">createVoucherOrder</span><span class="params">(VoucherOrder voucherOrder)</span> &#123;</span><br><span class="line">        <span class="type">Long</span> <span class="variable">userId</span> <span class="operator">=</span> voucherOrder.getUserId();</span><br><span class="line">        <span class="comment">// 5.1.查询订单</span></span><br><span class="line">        <span class="type">int</span> <span class="variable">count</span> <span class="operator">=</span> query().eq(<span class="string">&quot;user_id&quot;</span>, userId).eq(<span class="string">&quot;voucher_id&quot;</span>, voucherOrder.getVoucherId()).count();</span><br><span class="line">        <span class="comment">// 5.2.判断是否存在</span></span><br><span class="line">        <span class="keyword">if</span> (count &gt; <span class="number">0</span>) &#123;</span><br><span class="line">            <span class="comment">// 用户已经购买过了</span></span><br><span class="line">           log.error(<span class="string">&quot;用户已经购买过了&quot;</span>);</span><br><span class="line">           <span class="keyword">return</span> ;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 6.扣减库存</span></span><br><span class="line">        <span class="type">boolean</span> <span class="variable">success</span> <span class="operator">=</span> seckillVoucherService.update()</span><br><span class="line">                .setSql(<span class="string">&quot;stock = stock - 1&quot;</span>) <span class="comment">// set stock = stock - 1</span></span><br><span class="line">                .eq(<span class="string">&quot;voucher_id&quot;</span>, voucherOrder.getVoucherId()).gt(<span class="string">&quot;stock&quot;</span>, <span class="number">0</span>) <span class="comment">// where id = ? and stock &gt; 0</span></span><br><span class="line">                .update();</span><br><span class="line">        <span class="keyword">if</span> (!success) &#123;</span><br><span class="line">            <span class="comment">// 扣减失败</span></span><br><span class="line">            log.error(<span class="string">&quot;库存不足&quot;</span>);</span><br><span class="line">            <span class="keyword">return</span> ;</span><br><span class="line">        &#125;</span><br><span class="line">        save(voucherOrder);</span><br><span class="line"> </span><br><span class="line">    &#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p><strong>小总结：</strong></p><p>秒杀业务的优化思路是什么？</p><ul><li>先利用Redis完成库存余量、一人一单判断，完成抢单业务</li><li>再将下单业务放入阻塞队列，利用独立线程异步下单</li><li>基于阻塞队列的异步秒杀存在哪些问题？<ul><li>内存限制问题</li><li>数据安全问题</li></ul></li></ul><h2 id="7、Redis消息队列">7、Redis消息队列</h2><h3 id="7-1-Redis消息队列-认识消息队列">7.1 Redis消息队列-认识消息队列</h3><p>什么是消息队列：字面意思就是存放消息的队列。最简单的消息队列模型包括3个角色：</p><ul><li>消息队列：存储和管理消息，也被称为消息代理（Message Broker）</li><li>生产者：发送消息到消息队列</li><li>消费者：从消息队列获取消息并处理消息</li></ul><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653574849336.png" alt></p><p>使用队列的好处在于 **解耦：**所谓解耦，举一个生活中的例子就是：快递员(生产者)把快递放到快递柜里边(Message Queue)去，我们(消费者)从快递柜里边去拿东西，这就是一个异步，如果耦合，那么这个快递员相当于直接把快递交给你，这事固然好，但是万一你不在家，那么快递员就会一直等你，这就浪费了快递员的时间，所以这种思想在我们日常开发中，是非常有必要的。</p><p>这种场景在我们秒杀中就变成了：我们下单之后，利用redis去进行校验下单条件，再通过队列把消息发送出去，然后再启动一个线程去消费这个消息，完成解耦，同时也加快我们的响应速度。</p><p>这里我们可以使用一些现成的mq，比如kafka，rabbitmq等等，但是呢，如果没有安装mq，我们也可以直接使用redis提供的mq方案，降低我们的部署和学习成本。</p><h3 id="7-2-Redis消息队列-基于List实现消息队列">7.2 Redis消息队列-基于List实现消息队列</h3><p><strong>基于List结构模拟消息队列</strong></p><p>消息队列（Message Queue），字面意思就是存放消息的队列。而Redis的list数据结构是一个双向链表，很容易模拟出队列效果。</p><p>队列是入口和出口不在一边，因此我们可以利用：LPUSH 结合 RPOP、或者 RPUSH 结合 LPOP来实现。<br>不过要注意的是，当队列中没有消息时RPOP或LPOP操作会返回null，并不像JVM的阻塞队列那样会阻塞并等待消息。因此这里应该使用BRPOP或者BLPOP来实现阻塞效果。</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653575176451.png" alt></p><p>基于List的消息队列有哪些优缺点？<br>优点：</p><ul><li>利用Redis存储，不受限于JVM内存上限</li><li>基于Redis的持久化机制，数据安全性有保证</li><li>可以满足消息有序性</li></ul><p>缺点：</p><ul><li>无法避免消息丢失</li><li>只支持单消费者</li></ul><h3 id="7-3-Redis消息队列-基于PubSub的消息队列">7.3 Redis消息队列-基于PubSub的消息队列</h3><p>PubSub（发布订阅）是Redis2.0版本引入的消息传递模型。顾名思义，消费者可以订阅一个或多个channel，生产者向对应channel发送消息后，所有订阅者都能收到相关消息。</p><p>SUBSCRIBE channel [channel] ：订阅一个或多个频道<br>PUBLISH channel msg ：向一个频道发送消息<br>PSUBSCRIBE pattern[pattern] ：订阅与pattern格式匹配的所有频道</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653575506373.png" alt></p><p>基于PubSub的消息队列有哪些优缺点？<br>优点：</p><ul><li>采用发布订阅模型，支持多生产、多消费</li></ul><p>缺点：</p><ul><li>不支持数据持久化</li><li>无法避免消息丢失</li><li>消息堆积有上限，超出时数据丢失</li></ul><h3 id="7-4-Redis消息队列-基于Stream的消息队列">7.4 Redis消息队列-基于Stream的消息队列</h3><p>Stream 是 Redis 5.0 引入的一种新数据类型，可以实现一个功能非常完善的消息队列。</p><p>发送消息的命令：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653577301737.png" alt></p><p>例如：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653577349691.png" alt></p><p>读取消息的方式之一：XREAD</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653577445413.png" alt></p><p>例如，使用XREAD读取第一个消息：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653577643629.png" alt></p><p>XREAD阻塞方式，读取最新的消息：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653577659166.png" alt></p><p>在业务开发中，我们可以循环的调用XREAD阻塞方式来查询最新消息，从而实现持续监听队列的效果，伪代码如下</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653577689129.png" alt></p><p>注意：当我们指定起始ID为$时，代表读取最新的消息，如果我们处理一条消息的过程中，又有超过1条以上的消息到达队列，则下次获取时也只能获取到最新的一条，会出现漏读消息的问题</p><p>STREAM类型消息队列的XREAD命令特点：</p><ul><li>消息可回溯</li><li>一个消息可以被多个消费者读取</li><li>可以阻塞读取</li><li>有消息漏读的风险</li></ul><h3 id="7-5-Redis消息队列-基于Stream的消息队列-消费者组">7.5 Redis消息队列-基于Stream的消息队列-消费者组</h3><p>消费者组（Consumer Group）：将多个消费者划分到一个组中，监听同一个队列。具备下列特点：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653577801668.png" alt></p><p>创建消费者组：<br><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653577984924.png" alt><br>key：队列名称<br>groupName：消费者组名称<br>ID：起始ID标示，$代表队列中最后一个消息，0则代表队列中第一个消息<br>MKSTREAM：队列不存在时自动创建队列<br>其它常见命令：</p><p><strong>删除指定的消费者组</strong></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">XGROUP DESTORY key groupName</span><br></pre></td></tr></table></figure><p><strong>给指定的消费者组添加消费者</strong></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">XGROUP CREATECONSUMER key groupname consumername</span><br></pre></td></tr></table></figure><p><strong>删除消费者组中的指定消费者</strong></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">XGROUP DELCONSUMER key groupname consumername</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">XREADGROUP GROUP group consumer [COUNT count] [BLOCK milliseconds] [NOACK] STREAMS key [key ...] ID [ID ...]</span><br></pre></td></tr></table></figure><ul><li>group：消费组名称</li><li>consumer：消费者名称，如果消费者不存在，会自动创建一个消费者</li><li>count：本次查询的最大数量</li><li>BLOCK milliseconds：当没有消息时最长等待时间</li><li>NOACK：无需手动ACK，获取到消息后自动确认</li><li>STREAMS key：指定队列名称</li><li>ID：获取消息的起始ID：</li></ul><p>“&gt;”：从下一个未消费的消息开始<br>其它：根据指定id从pending-list中获取已消费但未确认的消息，例如0，是从pending-list中的第一个消息开始</p><p>消费者监听消息的基本思路：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653578211854.png" alt>STREAM类型消息队列的XREADGROUP命令特点：</p><ul><li>消息可回溯</li><li>可以多消费者争抢消息，加快消费速度</li><li>可以阻塞读取</li><li>没有消息漏读的风险</li><li>有消息确认机制，保证消息至少被消费一次</li></ul><p>最后我们来个小对比</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653578560691.png" alt></p><h3 id="7-6-基于Redis的Stream结构作为消息队列，实现异步秒杀下单">7.6 基于Redis的Stream结构作为消息队列，实现异步秒杀下单</h3><p>需求：</p><ul><li>创建一个Stream类型的消息队列，名为stream.orders</li><li>修改之前的秒杀下单Lua脚本，在认定有抢购资格后，直接向stream.orders中添加消息，内容包含voucherId、userId、orderId</li><li>项目启动时，开启一个线程任务，尝试获取stream.orders中的消息，完成下单\</li></ul><p>修改lua表达式,新增3.6</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1656082824939.png" alt="1656082824939"></p><p>VoucherOrderServiceImpl</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">class</span> <span class="title class_">VoucherOrderHandler</span> <span class="keyword">implements</span> <span class="title class_">Runnable</span> &#123;</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_">run</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">while</span> (<span class="literal">true</span>) &#123;</span><br><span class="line">            <span class="keyword">try</span> &#123;</span><br><span class="line">                <span class="comment">// 1.获取消息队列中的订单信息 XREADGROUP GROUP g1 c1 COUNT 1 BLOCK 2000 STREAMS s1 &gt;</span></span><br><span class="line">                List&lt;MapRecord&lt;String, Object, Object&gt;&gt; list = stringRedisTemplate.opsForStream().read(</span><br><span class="line">                    Consumer.from(<span class="string">&quot;g1&quot;</span>, <span class="string">&quot;c1&quot;</span>),</span><br><span class="line">                    StreamReadOptions.empty().count(<span class="number">1</span>).block(Duration.ofSeconds(<span class="number">2</span>)),</span><br><span class="line">                    StreamOffset.create(<span class="string">&quot;stream.orders&quot;</span>, ReadOffset.lastConsumed())</span><br><span class="line">                );</span><br><span class="line">                <span class="comment">// 2.判断订单信息是否为空</span></span><br><span class="line">                <span class="keyword">if</span> (list == <span class="literal">null</span> || list.isEmpty()) &#123;</span><br><span class="line">                    <span class="comment">// 如果为null，说明没有消息，继续下一次循环</span></span><br><span class="line">                    <span class="keyword">continue</span>;</span><br><span class="line">                &#125;</span><br><span class="line">                <span class="comment">// 解析数据</span></span><br><span class="line">                MapRecord&lt;String, Object, Object&gt; record = list.get(<span class="number">0</span>);</span><br><span class="line">                Map&lt;Object, Object&gt; value = record.getValue();</span><br><span class="line">                <span class="type">VoucherOrder</span> <span class="variable">voucherOrder</span> <span class="operator">=</span> BeanUtil.fillBeanWithMap(value, <span class="keyword">new</span> <span class="title class_">VoucherOrder</span>(), <span class="literal">true</span>);</span><br><span class="line">                <span class="comment">// 3.创建订单</span></span><br><span class="line">                createVoucherOrder(voucherOrder);</span><br><span class="line">                <span class="comment">// 4.确认消息 XACK</span></span><br><span class="line">                stringRedisTemplate.opsForStream().acknowledge(<span class="string">&quot;s1&quot;</span>, <span class="string">&quot;g1&quot;</span>, record.getId());</span><br><span class="line">            &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">                log.error(<span class="string">&quot;处理订单异常&quot;</span>, e);</span><br><span class="line">                <span class="comment">//处理异常消息</span></span><br><span class="line">                handlePendingList();</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">void</span> <span class="title function_">handlePendingList</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">while</span> (<span class="literal">true</span>) &#123;</span><br><span class="line">            <span class="keyword">try</span> &#123;</span><br><span class="line">                <span class="comment">// 1.获取pending-list中的订单信息 XREADGROUP GROUP g1 c1 COUNT 1 BLOCK 2000 STREAMS s1 0</span></span><br><span class="line">                List&lt;MapRecord&lt;String, Object, Object&gt;&gt; list = stringRedisTemplate.opsForStream().read(</span><br><span class="line">                    Consumer.from(<span class="string">&quot;g1&quot;</span>, <span class="string">&quot;c1&quot;</span>),</span><br><span class="line">                    StreamReadOptions.empty().count(<span class="number">1</span>),</span><br><span class="line">                    StreamOffset.create(<span class="string">&quot;stream.orders&quot;</span>, ReadOffset.from(<span class="string">&quot;0&quot;</span>))</span><br><span class="line">                );</span><br><span class="line">                <span class="comment">// 2.判断订单信息是否为空</span></span><br><span class="line">                <span class="keyword">if</span> (list == <span class="literal">null</span> || list.isEmpty()) &#123;</span><br><span class="line">                    <span class="comment">// 如果为null，说明没有异常消息，结束循环</span></span><br><span class="line">                    <span class="keyword">break</span>;</span><br><span class="line">                &#125;</span><br><span class="line">                <span class="comment">// 解析数据</span></span><br><span class="line">                MapRecord&lt;String, Object, Object&gt; record = list.get(<span class="number">0</span>);</span><br><span class="line">                Map&lt;Object, Object&gt; value = record.getValue();</span><br><span class="line">                <span class="type">VoucherOrder</span> <span class="variable">voucherOrder</span> <span class="operator">=</span> BeanUtil.fillBeanWithMap(value, <span class="keyword">new</span> <span class="title class_">VoucherOrder</span>(), <span class="literal">true</span>);</span><br><span class="line">                <span class="comment">// 3.创建订单</span></span><br><span class="line">                createVoucherOrder(voucherOrder);</span><br><span class="line">                <span class="comment">// 4.确认消息 XACK</span></span><br><span class="line">                stringRedisTemplate.opsForStream().acknowledge(<span class="string">&quot;s1&quot;</span>, <span class="string">&quot;g1&quot;</span>, record.getId());</span><br><span class="line">            &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">                log.error(<span class="string">&quot;处理pendding订单异常&quot;</span>, e);</span><br><span class="line">                <span class="keyword">try</span>&#123;</span><br><span class="line">                    Thread.sleep(<span class="number">20</span>);</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">            &#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><h2 id="8、达人探店">8、达人探店</h2><h3 id="8-1、达人探店-发布探店笔记">8.1、达人探店-发布探店笔记</h3><p>发布探店笔记</p><p>探店笔记类似点评网站的评价，往往是图文结合。对应的表有两个：<br>tb_blog：探店笔记表，包含笔记中的标题、文字、图片等<br>tb_blog_comments：其他用户对探店笔记的评价</p><p><strong>具体发布流程</strong></p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653578992639.png" alt></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><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Slf4j</span></span><br><span class="line"><span class="meta">@RestController</span></span><br><span class="line"><span class="meta">@RequestMapping(&quot;upload&quot;)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">UploadController</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@PostMapping(&quot;blog&quot;)</span></span><br><span class="line">    <span class="keyword">public</span> Result <span class="title function_">uploadImage</span><span class="params">(<span class="meta">@RequestParam(&quot;file&quot;)</span> MultipartFile image)</span> &#123;</span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            <span class="comment">// 获取原始文件名称</span></span><br><span class="line">            <span class="type">String</span> <span class="variable">originalFilename</span> <span class="operator">=</span> image.getOriginalFilename();</span><br><span class="line">            <span class="comment">// 生成新文件名</span></span><br><span class="line">            <span class="type">String</span> <span class="variable">fileName</span> <span class="operator">=</span> createNewFileName(originalFilename);</span><br><span class="line">            <span class="comment">// 保存文件</span></span><br><span class="line">            image.transferTo(<span class="keyword">new</span> <span class="title class_">File</span>(SystemConstants.IMAGE_UPLOAD_DIR, fileName));</span><br><span class="line">            <span class="comment">// 返回结果</span></span><br><span class="line">            log.debug(<span class="string">&quot;文件上传成功，&#123;&#125;&quot;</span>, fileName);</span><br><span class="line">            <span class="keyword">return</span> Result.ok(fileName);</span><br><span class="line">        &#125; <span class="keyword">catch</span> (IOException e) &#123;</span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">RuntimeException</span>(<span class="string">&quot;文件上传失败&quot;</span>, e);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>注意：同学们在操作时，需要修改SystemConstants.IMAGE_UPLOAD_DIR 自己图片所在的地址，在实际开发中图片一般会放在nginx上或者是云存储上。</p><p>BlogController</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></pre></td><td class="code"><pre><span class="line"><span class="meta">@RestController</span></span><br><span class="line"><span class="meta">@RequestMapping(&quot;/blog&quot;)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">BlogController</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Resource</span></span><br><span class="line">    <span class="keyword">private</span> IBlogService blogService;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@PostMapping</span></span><br><span class="line">    <span class="keyword">public</span> Result <span class="title function_">saveBlog</span><span class="params">(<span class="meta">@RequestBody</span> Blog blog)</span> &#123;</span><br><span class="line">        <span class="comment">//获取登录用户</span></span><br><span class="line">        <span class="type">UserDTO</span> <span class="variable">user</span> <span class="operator">=</span> UserHolder.getUser();</span><br><span class="line">        blog.setUpdateTime(user.getId());</span><br><span class="line">        <span class="comment">//保存探店博文</span></span><br><span class="line">        blogService.saveBlog(blog);</span><br><span class="line">        <span class="comment">//返回id</span></span><br><span class="line">        <span class="keyword">return</span> Result.ok(blog.getId());</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="8-2-达人探店-查看探店笔记">8.2 达人探店-查看探店笔记</h3><p>实现查看发布探店笔记的接口</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653579931626.png" alt></p><p>实现代码：</p><p>BlogServiceImpl</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">@Override</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">queryBlogById</span><span class="params">(Long id)</span> &#123;</span><br><span class="line">    <span class="comment">// 1.查询blog</span></span><br><span class="line">    <span class="type">Blog</span> <span class="variable">blog</span> <span class="operator">=</span> getById(id);</span><br><span class="line">    <span class="keyword">if</span> (blog == <span class="literal">null</span>) &#123;</span><br><span class="line">        <span class="keyword">return</span> Result.fail(<span class="string">&quot;笔记不存在！&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 2.查询blog有关的用户</span></span><br><span class="line">    queryBlogUser(blog);</span><br><span class="line">  </span><br><span class="line">    <span class="keyword">return</span> Result.ok(blog);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="8-3-达人探店-点赞功能">8.3 达人探店-点赞功能</h3><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></pre></td><td class="code"><pre><span class="line"><span class="meta">@GetMapping(&quot;/likes/&#123;id&#125;&quot;)</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">queryBlogLikes</span><span class="params">(<span class="meta">@PathVariable(&quot;id&quot;)</span> Long id)</span> &#123;</span><br><span class="line">    <span class="comment">//修改点赞数量</span></span><br><span class="line">    blogService.update().setSql(<span class="string">&quot;liked = liked +1 &quot;</span>).eq(<span class="string">&quot;id&quot;</span>,id).update();</span><br><span class="line">    <span class="keyword">return</span> Result.ok();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>问题分析：这种方式会导致一个用户无限点赞，明显是不合理的</p><p>造成这个问题的原因是，我们现在的逻辑，发起请求只是给数据库+1，所以才会出现这个问题</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653581590453.png" alt></p><p>完善点赞功能</p><p>需求：</p><ul><li>同一个用户只能点赞一次，再次点击则取消点赞</li><li>如果当前用户已经点赞，则点赞按钮高亮显示（前端已实现，判断字段Blog类的isLike属性）</li></ul><p>实现步骤：</p><ul><li>给Blog类中添加一个isLike字段，标示是否被当前用户点赞</li><li>修改点赞功能，利用Redis的set集合判断是否点赞过，未点赞过则点赞数+1，已点赞过则点赞数-1</li><li>修改根据id查询Blog的业务，判断当前登录用户是否点赞过，赋值给isLike字段</li><li>修改分页查询Blog业务，判断当前登录用户是否点赞过，赋值给isLike字段</li></ul><p>为什么采用set集合：</p><p>因为我们的数据是不能重复的，当用户操作过之后，无论他怎么操作，都是</p><p>具体步骤：</p><p>1、在Blog 添加一个字段</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="meta">@TableField(exist = false)</span></span><br><span class="line"><span class="keyword">private</span> Boolean isLike;</span><br></pre></td></tr></table></figure><p>2、修改代码</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line">   <span class="keyword">public</span> Result <span class="title function_">likeBlog</span><span class="params">(Long id)</span>&#123;</span><br><span class="line">       <span class="comment">// 1.获取登录用户</span></span><br><span class="line">       <span class="type">Long</span> <span class="variable">userId</span> <span class="operator">=</span> UserHolder.getUser().getId();</span><br><span class="line">       <span class="comment">// 2.判断当前登录用户是否已经点赞</span></span><br><span class="line">       <span class="type">String</span> <span class="variable">key</span> <span class="operator">=</span> BLOG_LIKED_KEY + id;</span><br><span class="line">       <span class="type">Boolean</span> <span class="variable">isMember</span> <span class="operator">=</span> stringRedisTemplate.opsForSet().isMember(key, userId.toString());</span><br><span class="line">       <span class="keyword">if</span>(BooleanUtil.isFalse(isMember))&#123;</span><br><span class="line">            <span class="comment">//3.如果未点赞，可以点赞</span></span><br><span class="line">           <span class="comment">//3.1 数据库点赞数+1</span></span><br><span class="line">           <span class="type">boolean</span> <span class="variable">isSuccess</span> <span class="operator">=</span> update().setSql(<span class="string">&quot;liked = liked + 1&quot;</span>).eq(<span class="string">&quot;id&quot;</span>, id).update();</span><br><span class="line">           <span class="comment">//3.2 保存用户到Redis的set集合</span></span><br><span class="line">           <span class="keyword">if</span>(isSuccess)&#123;</span><br><span class="line">               stringRedisTemplate.opsForSet().add(key,userId.toString());</span><br><span class="line">           &#125;</span><br><span class="line">       &#125;<span class="keyword">else</span>&#123;</span><br><span class="line">            <span class="comment">//4.如果已点赞，取消点赞</span></span><br><span class="line">           <span class="comment">//4.1 数据库点赞数-1</span></span><br><span class="line">           <span class="type">boolean</span> <span class="variable">isSuccess</span> <span class="operator">=</span> update().setSql(<span class="string">&quot;liked = liked - 1&quot;</span>).eq(<span class="string">&quot;id&quot;</span>, id).update();</span><br><span class="line">           <span class="comment">//4.2 把用户从Redis的set集合移除</span></span><br><span class="line">           <span class="keyword">if</span>(isSuccess)&#123;</span><br><span class="line">               stringRedisTemplate.opsForSet().remove(key,userId.toString());</span><br><span class="line">           &#125;</span><br><span class="line">       &#125;</span><br></pre></td></tr></table></figure><h3 id="8-4-达人探店-点赞排行榜">8.4 达人探店-点赞排行榜</h3><p>在探店笔记的详情页面，应该把给该笔记点赞的人显示出来，比如最早点赞的TOP5，形成点赞排行榜：</p><p>之前的点赞是放到set集合，但是set集合是不能排序的，所以这个时候，咱们可以采用一个可以排序的set集合，就是咱们的sortedSet</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653805077118.png" alt></p><p>我们接下来来对比一下这些集合的区别是什么</p><p>所有点赞的人，需要是唯一的，所以我们应当使用set或者是sortedSet</p><p>其次我们需要排序，就可以直接锁定使用sortedSet啦</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653805203758.png" alt></p><p>修改代码</p><p>BlogServiceImpl</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><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">public</span> Result <span class="title function_">likeBlog</span><span class="params">(Long id)</span> &#123;</span><br><span class="line">     <span class="comment">// 1.获取登录用户</span></span><br><span class="line">     <span class="type">Long</span> <span class="variable">userId</span> <span class="operator">=</span> UserHolder.getUser().getId();</span><br><span class="line">     <span class="comment">// 2.判断当前登录用户是否已经点赞</span></span><br><span class="line">     <span class="type">String</span> <span class="variable">key</span> <span class="operator">=</span> BLOG_LIKED_KEY + id;</span><br><span class="line">     <span class="type">Double</span> <span class="variable">score</span> <span class="operator">=</span> stringRedisTemplate.opsForZSet().score(key, userId.toString());</span><br><span class="line">     <span class="keyword">if</span> (score == <span class="literal">null</span>) &#123;</span><br><span class="line">         <span class="comment">// 3.如果未点赞，可以点赞</span></span><br><span class="line">         <span class="comment">// 3.1.数据库点赞数 + 1</span></span><br><span class="line">         <span class="type">boolean</span> <span class="variable">isSuccess</span> <span class="operator">=</span> update().setSql(<span class="string">&quot;liked = liked + 1&quot;</span>).eq(<span class="string">&quot;id&quot;</span>, id).update();</span><br><span class="line">         <span class="comment">// 3.2.保存用户到Redis的set集合  zadd key value score</span></span><br><span class="line">         <span class="keyword">if</span> (isSuccess) &#123;</span><br><span class="line">             stringRedisTemplate.opsForZSet().add(key, userId.toString(), System.currentTimeMillis());</span><br><span class="line">         &#125;</span><br><span class="line">     &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">         <span class="comment">// 4.如果已点赞，取消点赞</span></span><br><span class="line">         <span class="comment">// 4.1.数据库点赞数 -1</span></span><br><span class="line">         <span class="type">boolean</span> <span class="variable">isSuccess</span> <span class="operator">=</span> update().setSql(<span class="string">&quot;liked = liked - 1&quot;</span>).eq(<span class="string">&quot;id&quot;</span>, id).update();</span><br><span class="line">         <span class="comment">// 4.2.把用户从Redis的set集合移除</span></span><br><span class="line">         <span class="keyword">if</span> (isSuccess) &#123;</span><br><span class="line">             stringRedisTemplate.opsForZSet().remove(key, userId.toString());</span><br><span class="line">         &#125;</span><br><span class="line">     &#125;</span><br><span class="line">     <span class="keyword">return</span> Result.ok();</span><br><span class="line"> &#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">void</span> <span class="title function_">isBlogLiked</span><span class="params">(Blog blog)</span> &#123;</span><br><span class="line">     <span class="comment">// 1.获取登录用户</span></span><br><span class="line">     <span class="type">UserDTO</span> <span class="variable">user</span> <span class="operator">=</span> UserHolder.getUser();</span><br><span class="line">     <span class="keyword">if</span> (user == <span class="literal">null</span>) &#123;</span><br><span class="line">         <span class="comment">// 用户未登录，无需查询是否点赞</span></span><br><span class="line">         <span class="keyword">return</span>;</span><br><span class="line">     &#125;</span><br><span class="line">     <span class="type">Long</span> <span class="variable">userId</span> <span class="operator">=</span> user.getId();</span><br><span class="line">     <span class="comment">// 2.判断当前登录用户是否已经点赞</span></span><br><span class="line">     <span class="type">String</span> <span class="variable">key</span> <span class="operator">=</span> <span class="string">&quot;blog:liked:&quot;</span> + blog.getId();</span><br><span class="line">     <span class="type">Double</span> <span class="variable">score</span> <span class="operator">=</span> stringRedisTemplate.opsForZSet().score(key, userId.toString());</span><br><span class="line">     blog.setIsLike(score != <span class="literal">null</span>);</span><br><span class="line"> &#125;</span><br></pre></td></tr></table></figure><p>点赞列表查询列表</p><p>BlogController</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@GetMapping(&quot;/likes/&#123;id&#125;&quot;)</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">queryBlogLikes</span><span class="params">(<span class="meta">@PathVariable(&quot;id&quot;)</span> Long id)</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> blogService.queryBlogLikes(id);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>BlogService</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></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">queryBlogLikes</span><span class="params">(Long id)</span> &#123;</span><br><span class="line">    <span class="type">String</span> <span class="variable">key</span> <span class="operator">=</span> BLOG_LIKED_KEY + id;</span><br><span class="line">    <span class="comment">// 1.查询top5的点赞用户 zrange key 0 4</span></span><br><span class="line">    Set&lt;String&gt; top5 = stringRedisTemplate.opsForZSet().range(key, <span class="number">0</span>, <span class="number">4</span>);</span><br><span class="line">    <span class="keyword">if</span> (top5 == <span class="literal">null</span> || top5.isEmpty()) &#123;</span><br><span class="line">        <span class="keyword">return</span> Result.ok(Collections.emptyList());</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 2.解析出其中的用户id</span></span><br><span class="line">    List&lt;Long&gt; ids = top5.stream().map(Long::valueOf).collect(Collectors.toList());</span><br><span class="line">    <span class="type">String</span> <span class="variable">idStr</span> <span class="operator">=</span> StrUtil.join(<span class="string">&quot;,&quot;</span>, ids);</span><br><span class="line">    <span class="comment">// 3.根据用户id查询用户 WHERE id IN ( 5 , 1 ) ORDER BY FIELD(id, 5, 1)</span></span><br><span class="line">    List&lt;UserDTO&gt; userDTOS = userService.query()</span><br><span class="line">            .in(<span class="string">&quot;id&quot;</span>, ids).last(<span class="string">&quot;ORDER BY FIELD(id,&quot;</span> + idStr + <span class="string">&quot;)&quot;</span>).list()</span><br><span class="line">            .stream()</span><br><span class="line">            .map(user -&gt; BeanUtil.copyProperties(user, UserDTO.class))</span><br><span class="line">            .collect(Collectors.toList());</span><br><span class="line">    <span class="comment">// 4.返回</span></span><br><span class="line">    <span class="keyword">return</span> Result.ok(userDTOS);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="9、好友关注">9、好友关注</h2><h3 id="9-1-好友关注-关注和取消关注">9.1 好友关注-关注和取消关注</h3><p>针对用户的操作：可以对用户进行关注和取消关注功能。</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653806140822.png" alt></p><p>实现思路：</p><p>需求：基于该表数据结构，实现两个接口：</p><ul><li>关注和取关接口</li><li>判断是否关注的接口</li></ul><p>关注是User之间的关系，是博主与粉丝的关系，数据库中有一张tb_follow表来标示：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653806253817.png" alt></p><p>注意: 这里需要把主键修改为自增长，简化开发。</p><p>FollowController</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="comment">//关注</span></span><br><span class="line"><span class="meta">@PutMapping(&quot;/&#123;id&#125;/&#123;isFollow&#125;&quot;)</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">follow</span><span class="params">(<span class="meta">@PathVariable(&quot;id&quot;)</span> Long followUserId, <span class="meta">@PathVariable(&quot;isFollow&quot;)</span> Boolean isFollow)</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> followService.follow(followUserId, isFollow);</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">//取消关注</span></span><br><span class="line"><span class="meta">@GetMapping(&quot;/or/not/&#123;id&#125;&quot;)</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">isFollow</span><span class="params">(<span class="meta">@PathVariable(&quot;id&quot;)</span> Long followUserId)</span> &#123;</span><br><span class="line">      <span class="keyword">return</span> followService.isFollow(followUserId);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>FollowService</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line">取消关注service</span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">isFollow</span><span class="params">(Long followUserId)</span> &#123;</span><br><span class="line">        <span class="comment">// 1.获取登录用户</span></span><br><span class="line">        <span class="type">Long</span> <span class="variable">userId</span> <span class="operator">=</span> UserHolder.getUser().getId();</span><br><span class="line">        <span class="comment">// 2.查询是否关注 select count(*) from tb_follow where user_id = ? and follow_user_id = ?</span></span><br><span class="line">        <span class="type">Integer</span> <span class="variable">count</span> <span class="operator">=</span> query().eq(<span class="string">&quot;user_id&quot;</span>, userId).eq(<span class="string">&quot;follow_user_id&quot;</span>, followUserId).count();</span><br><span class="line">        <span class="comment">// 3.判断</span></span><br><span class="line">        <span class="keyword">return</span> Result.ok(count &gt; <span class="number">0</span>);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line"> 关注service</span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> Result <span class="title function_">follow</span><span class="params">(Long followUserId, Boolean isFollow)</span> &#123;</span><br><span class="line">        <span class="comment">// 1.获取登录用户</span></span><br><span class="line">        <span class="type">Long</span> <span class="variable">userId</span> <span class="operator">=</span> UserHolder.getUser().getId();</span><br><span class="line">        <span class="type">String</span> <span class="variable">key</span> <span class="operator">=</span> <span class="string">&quot;follows:&quot;</span> + userId;</span><br><span class="line">        <span class="comment">// 1.判断到底是关注还是取关</span></span><br><span class="line">        <span class="keyword">if</span> (isFollow) &#123;</span><br><span class="line">            <span class="comment">// 2.关注，新增数据</span></span><br><span class="line">            <span class="type">Follow</span> <span class="variable">follow</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Follow</span>();</span><br><span class="line">            follow.setUserId(userId);</span><br><span class="line">            follow.setFollowUserId(followUserId);</span><br><span class="line">            <span class="type">boolean</span> <span class="variable">isSuccess</span> <span class="operator">=</span> save(follow);</span><br><span class="line"></span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            <span class="comment">// 3.取关，删除 delete from tb_follow where user_id = ? and follow_user_id = ?</span></span><br><span class="line">            remove(<span class="keyword">new</span> <span class="title class_">QueryWrapper</span>&lt;Follow&gt;()</span><br><span class="line">                    .eq(<span class="string">&quot;user_id&quot;</span>, userId).eq(<span class="string">&quot;follow_user_id&quot;</span>, followUserId));</span><br><span class="line"></span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> Result.ok();</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure><h3 id="9-2-好友关注-共同关注">9.2 好友关注-共同关注</h3><p>想要去看共同关注的好友，需要首先进入到这个页面，这个页面会发起两个请求</p><p>1、去查询用户的详情</p><p>2、去查询用户的笔记</p><p>以上两个功能和共同关注没有什么关系，大家可以自行将笔记中的代码拷贝到idea中就可以实现这两个功能了，我们的重点在于共同关注功能。</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653806706296.png" alt></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="comment">// UserController 根据id查询用户</span></span><br><span class="line"><span class="meta">@GetMapping(&quot;/&#123;id&#125;&quot;)</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">queryUserById</span><span class="params">(<span class="meta">@PathVariable(&quot;id&quot;)</span> Long userId)</span>&#123;</span><br><span class="line"><span class="comment">// 查询详情</span></span><br><span class="line"><span class="type">User</span> <span class="variable">user</span> <span class="operator">=</span> userService.getById(userId);</span><br><span class="line"><span class="keyword">if</span> (user == <span class="literal">null</span>) &#123;</span><br><span class="line"><span class="keyword">return</span> Result.ok();</span><br><span class="line">&#125;</span><br><span class="line"><span class="type">UserDTO</span> <span class="variable">userDTO</span> <span class="operator">=</span> BeanUtil.copyProperties(user, UserDTO.class);</span><br><span class="line"><span class="comment">// 返回</span></span><br><span class="line"><span class="keyword">return</span> Result.ok(userDTO);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// BlogController  根据id查询博主的探店笔记</span></span><br><span class="line"><span class="meta">@GetMapping(&quot;/of/user&quot;)</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">queryBlogByUserId</span><span class="params">(</span></span><br><span class="line"><span class="params"><span class="meta">@RequestParam(value = &quot;current&quot;, defaultValue = &quot;1&quot;)</span> Integer current,</span></span><br><span class="line"><span class="params"><span class="meta">@RequestParam(&quot;id&quot;)</span> Long id)</span> &#123;</span><br><span class="line"><span class="comment">// 根据用户查询</span></span><br><span class="line">Page&lt;Blog&gt; page = blogService.query()</span><br><span class="line">.eq(<span class="string">&quot;user_id&quot;</span>, id).page(<span class="keyword">new</span> <span class="title class_">Page</span>&lt;&gt;(current, SystemConstants.MAX_PAGE_SIZE));</span><br><span class="line"><span class="comment">// 获取当前页数据</span></span><br><span class="line">List&lt;Blog&gt; records = page.getRecords();</span><br><span class="line"><span class="keyword">return</span> Result.ok(records);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>接下来我们来看看共同关注如何实现：</p><p>需求：利用Redis中恰当的数据结构，实现共同关注功能。在博主个人页面展示出当前用户与博主的共同关注呢。</p><p>当然是使用我们之前学习过的set集合咯，在set集合中，有交集并集补集的api，我们可以把两人的关注的人分别放入到一个set集合中，然后再通过api去查看这两个set集合中的交集数据。</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653806973212.png" alt></p><p>我们先来改造当前的关注列表</p><p>改造原因是因为我们需要在用户关注了某位用户后，需要将数据放入到set集合中，方便后续进行共同关注，同时当取消关注时，也需要从set集合中进行删除</p><p>FollowServiceImpl</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></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">follow</span><span class="params">(Long followUserId, Boolean isFollow)</span> &#123;</span><br><span class="line">    <span class="comment">// 1.获取登录用户</span></span><br><span class="line">    <span class="type">Long</span> <span class="variable">userId</span> <span class="operator">=</span> UserHolder.getUser().getId();</span><br><span class="line">    <span class="type">String</span> <span class="variable">key</span> <span class="operator">=</span> <span class="string">&quot;follows:&quot;</span> + userId;</span><br><span class="line">    <span class="comment">// 1.判断到底是关注还是取关</span></span><br><span class="line">    <span class="keyword">if</span> (isFollow) &#123;</span><br><span class="line">        <span class="comment">// 2.关注，新增数据</span></span><br><span class="line">        <span class="type">Follow</span> <span class="variable">follow</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Follow</span>();</span><br><span class="line">        follow.setUserId(userId);</span><br><span class="line">        follow.setFollowUserId(followUserId);</span><br><span class="line">        <span class="type">boolean</span> <span class="variable">isSuccess</span> <span class="operator">=</span> save(follow);</span><br><span class="line">        <span class="keyword">if</span> (isSuccess) &#123;</span><br><span class="line">            <span class="comment">// 把关注用户的id，放入redis的set集合 sadd userId followerUserId</span></span><br><span class="line">            stringRedisTemplate.opsForSet().add(key, followUserId.toString());</span><br><span class="line">        &#125;</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        <span class="comment">// 3.取关，删除 delete from tb_follow where user_id = ? and follow_user_id = ?</span></span><br><span class="line">        <span class="type">boolean</span> <span class="variable">isSuccess</span> <span class="operator">=</span> remove(<span class="keyword">new</span> <span class="title class_">QueryWrapper</span>&lt;Follow&gt;()</span><br><span class="line">                .eq(<span class="string">&quot;user_id&quot;</span>, userId).eq(<span class="string">&quot;follow_user_id&quot;</span>, followUserId));</span><br><span class="line">        <span class="keyword">if</span> (isSuccess) &#123;</span><br><span class="line">            <span class="comment">// 把关注用户的id从Redis集合中移除</span></span><br><span class="line">            stringRedisTemplate.opsForSet().remove(key, followUserId.toString());</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> Result.ok();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>具体的关注代码：</strong></p><p>FollowServiceImpl</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></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">followCommons</span><span class="params">(Long id)</span> &#123;</span><br><span class="line">    <span class="comment">// 1.获取当前用户</span></span><br><span class="line">    <span class="type">Long</span> <span class="variable">userId</span> <span class="operator">=</span> UserHolder.getUser().getId();</span><br><span class="line">    <span class="type">String</span> <span class="variable">key</span> <span class="operator">=</span> <span class="string">&quot;follows:&quot;</span> + userId;</span><br><span class="line">    <span class="comment">// 2.求交集</span></span><br><span class="line">    <span class="type">String</span> <span class="variable">key2</span> <span class="operator">=</span> <span class="string">&quot;follows:&quot;</span> + id;</span><br><span class="line">    Set&lt;String&gt; intersect = stringRedisTemplate.opsForSet().intersect(key, key2);</span><br><span class="line">    <span class="keyword">if</span> (intersect == <span class="literal">null</span> || intersect.isEmpty()) &#123;</span><br><span class="line">        <span class="comment">// 无交集</span></span><br><span class="line">        <span class="keyword">return</span> Result.ok(Collections.emptyList());</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 3.解析id集合</span></span><br><span class="line">    List&lt;Long&gt; ids = intersect.stream().map(Long::valueOf).collect(Collectors.toList());</span><br><span class="line">    <span class="comment">// 4.查询用户</span></span><br><span class="line">    List&lt;UserDTO&gt; users = userService.listByIds(ids)</span><br><span class="line">            .stream()</span><br><span class="line">            .map(user -&gt; BeanUtil.copyProperties(user, UserDTO.class))</span><br><span class="line">            .collect(Collectors.toList());</span><br><span class="line">    <span class="keyword">return</span> Result.ok(users);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="9-3-好友关注-Feed流实现方案">9.3 好友关注-Feed流实现方案</h3><p>当我们关注了用户后，这个用户发了动态，那么我们应该把这些数据推送给用户，这个需求，其实我们又把他叫做Feed流，关注推送也叫做Feed流，直译为投喂。为用户持续的提供“沉浸式”的体验，通过无限下拉刷新获取新的信息。</p><p>对于传统的模式的内容解锁：我们是需要用户去通过搜索引擎或者是其他的方式去解锁想要看的内容</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653808641260.png" alt></p><p>对于新型的Feed流的的效果：不需要我们用户再去推送信息，而是系统分析用户到底想要什么，然后直接把内容推送给用户，从而使用户能够更加的节约时间，不用主动去寻找。</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653808993693.png" alt></p><p>Feed流的实现有两种模式：</p><p>Feed流产品有两种常见模式：<br>Timeline：不做内容筛选，简单的按照内容发布时间排序，常用于好友或关注。例如朋友圈</p><ul><li>优点：信息全面，不会有缺失。并且实现也相对简单</li><li>缺点：信息噪音较多，用户不一定感兴趣，内容获取效率低</li></ul><p>智能排序：利用智能算法屏蔽掉违规的、用户不感兴趣的内容。推送用户感兴趣信息来吸引用户</p><ul><li>优点：投喂用户感兴趣信息，用户粘度很高，容易沉迷</li><li>缺点：如果算法不精准，可能起到反作用<br>本例中的个人页面，是基于关注的好友来做Feed流，因此采用Timeline的模式。该模式的实现方案有三种：</li></ul><p>我们本次针对好友的操作，采用的就是Timeline的方式，只需要拿到我们关注用户的信息，然后按照时间排序即可</p><p>，因此采用Timeline的模式。该模式的实现方案有三种：</p><ul><li>拉模式</li><li>推模式</li><li>推拉结合</li></ul><p><strong>拉模式</strong>：也叫做读扩散</p><p>该模式的核心含义就是：当张三和李四和王五发了消息后，都会保存在自己的邮箱中，假设赵六要读取信息，那么他会从读取他自己的收件箱，此时系统会从他关注的人群中，把他关注人的信息全部都进行拉取，然后在进行排序</p><p>优点：比较节约空间，因为赵六在读信息时，并没有重复读取，而且读取完之后可以把他的收件箱进行清楚。</p><p>缺点：比较延迟，当用户读取数据时才去关注的人里边去读取数据，假设用户关注了大量的用户，那么此时就会拉取海量的内容，对服务器压力巨大。</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653809450816.png" alt></p><p><strong>推模式</strong>：也叫做写扩散。</p><p>推模式是没有写邮箱的，当张三写了一个内容，此时会主动的把张三写的内容发送到他的粉丝收件箱中去，假设此时李四再来读取，就不用再去临时拉取了</p><p>优点：时效快，不用临时拉取</p><p>缺点：内存压力大，假设一个大V写信息，很多人关注他， 就会写很多分数据到粉丝那边去</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653809875208.png" alt></p><p><strong>推拉结合模式</strong>：也叫做读写混合，兼具推和拉两种模式的优点。</p><p>推拉模式是一个折中的方案，站在发件人这一段，如果是个普通的人，那么我们采用写扩散的方式，直接把数据写入到他的粉丝中去，因为普通的人他的粉丝关注量比较小，所以这样做没有压力，如果是大V，那么他是直接将数据先写入到一份到发件箱里边去，然后再直接写一份到活跃粉丝收件箱里边去，现在站在收件人这端来看，如果是活跃粉丝，那么大V和普通的人发的都会直接写入到自己收件箱里边来，而如果是普通的粉丝，由于他们上线不是很频繁，所以等他们上线时，再从发件箱里边去拉信息。</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653812346852.png" alt></p><h3 id="9-4-好友关注-推送到粉丝收件箱">9.4 好友关注-推送到粉丝收件箱</h3><p>需求：</p><ul><li>修改新增探店笔记的业务，在保存blog到数据库的同时，推送到粉丝的收件箱</li><li>收件箱满足可以根据时间戳排序，必须用Redis的数据结构实现</li><li>查询收件箱数据时，可以实现分页查询</li></ul><p>Feed流中的数据会不断更新，所以数据的角标也在变化，因此不能采用传统的分页模式。</p><p>传统了分页在feed流是不适用的，因为我们的数据会随时发生变化</p><p>假设在t1 时刻，我们去读取第一页，此时page = 1 ，size = 5 ，那么我们拿到的就是10~6 这几条记录，假设现在t2时候又发布了一条记录，此时t3 时刻，我们来读取第二页，读取第二页传入的参数是page=2 ，size=5 ，那么此时读取到的第二页实际上是从6 开始，然后是6~2 ，那么我们就读取到了重复的数据，所以feed流的分页，不能采用原始方案来做。</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653813047671.png" alt></p><p>Feed流的滚动分页</p><p>我们需要记录每次操作的最后一条，然后从这个位置开始去读取数据</p><p>举个例子：我们从t1时刻开始，拿第一页数据，拿到了10~6，然后记录下当前最后一次拿取的记录，就是6，t2时刻发布了新的记录，此时这个11放到最顶上，但是不会影响我们之前记录的6，此时t3时刻来拿第二页，第二页这个时候拿数据，还是从6后一点的5去拿，就拿到了5-1的记录。我们这个地方可以采用sortedSet来做，可以进行范围查询，并且还可以记录当前获取数据时间戳最小值，就可以实现滚动分页了</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653813462834.png" alt></p><p>核心的意思：就是我们在保存完探店笔记后，获得到当前笔记的粉丝，然后把数据推送到粉丝的redis中去。</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></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">saveBlog</span><span class="params">(Blog blog)</span> &#123;</span><br><span class="line">    <span class="comment">// 1.获取登录用户</span></span><br><span class="line">    <span class="type">UserDTO</span> <span class="variable">user</span> <span class="operator">=</span> UserHolder.getUser();</span><br><span class="line">    blog.setUserId(user.getId());</span><br><span class="line">    <span class="comment">// 2.保存探店笔记</span></span><br><span class="line">    <span class="type">boolean</span> <span class="variable">isSuccess</span> <span class="operator">=</span> save(blog);</span><br><span class="line">    <span class="keyword">if</span>(!isSuccess)&#123;</span><br><span class="line">        <span class="keyword">return</span> Result.fail(<span class="string">&quot;新增笔记失败!&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 3.查询笔记作者的所有粉丝 select * from tb_follow where follow_user_id = ?</span></span><br><span class="line">    List&lt;Follow&gt; follows = followService.query().eq(<span class="string">&quot;follow_user_id&quot;</span>, user.getId()).list();</span><br><span class="line">    <span class="comment">// 4.推送笔记id给所有粉丝</span></span><br><span class="line">    <span class="keyword">for</span> (Follow follow : follows) &#123;</span><br><span class="line">        <span class="comment">// 4.1.获取粉丝id</span></span><br><span class="line">        <span class="type">Long</span> <span class="variable">userId</span> <span class="operator">=</span> follow.getUserId();</span><br><span class="line">        <span class="comment">// 4.2.推送</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">key</span> <span class="operator">=</span> FEED_KEY + userId;</span><br><span class="line">        stringRedisTemplate.opsForZSet().add(key, blog.getId().toString(), System.currentTimeMillis());</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 5.返回id</span></span><br><span class="line">    <span class="keyword">return</span> Result.ok(blog.getId());</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="9-5好友关注-实现分页查询收邮箱">9.5好友关注-实现分页查询收邮箱</h3><p>需求：在个人主页的“关注”卡片中，查询并展示推送的Blog信息：</p><p>具体操作如下：</p><p>1、每次查询完成后，我们要分析出查询出数据的最小时间戳，这个值会作为下一次查询的条件</p><p>2、我们需要找到与上一次查询相同的查询个数作为偏移量，下次查询时，跳过这些查询过的数据，拿到我们需要的数据</p><p>综上：我们的请求参数中就需要携带 lastId：上一次查询的最小时间戳 和偏移量这两个参数。</p><p>这两个参数第一次会由前端来指定，以后的查询就根据后台结果作为条件，再次传递到后台。</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653819821591.png" alt></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></pre></td><td class="code"><pre><span class="line"><span class="meta">@Data</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">ScrollResult</span> &#123;</span><br><span class="line">    <span class="keyword">private</span> List&lt;?&gt; list;</span><br><span class="line">    <span class="keyword">private</span> Long minTime;</span><br><span class="line">    <span class="keyword">private</span> Integer offset;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>BlogController</p><p>注意：RequestParam 表示接受url地址栏传参的注解，当方法上参数的名称和url地址栏不相同时，可以通过RequestParam 来进行指定</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@GetMapping(&quot;/of/follow&quot;)</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">queryBlogOfFollow</span><span class="params">(</span></span><br><span class="line"><span class="params">    <span class="meta">@RequestParam(&quot;lastId&quot;)</span> Long max, <span class="meta">@RequestParam(value = &quot;offset&quot;, defaultValue = &quot;0&quot;)</span> Integer offset)</span>&#123;</span><br><span class="line">    <span class="keyword">return</span> blogService.queryBlogOfFollow(max, offset);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>BlogServiceImpl</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></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">queryBlogOfFollow</span><span class="params">(Long max, Integer offset)</span> &#123;</span><br><span class="line">    <span class="comment">// 1.获取当前用户</span></span><br><span class="line">    <span class="type">Long</span> <span class="variable">userId</span> <span class="operator">=</span> UserHolder.getUser().getId();</span><br><span class="line">    <span class="comment">// 2.查询收件箱 ZREVRANGEBYSCORE key Max Min LIMIT offset count</span></span><br><span class="line">    <span class="type">String</span> <span class="variable">key</span> <span class="operator">=</span> FEED_KEY + userId;</span><br><span class="line">    Set&lt;ZSetOperations.TypedTuple&lt;String&gt;&gt; typedTuples = stringRedisTemplate.opsForZSet()</span><br><span class="line">        .reverseRangeByScoreWithScores(key, <span class="number">0</span>, max, offset, <span class="number">2</span>);</span><br><span class="line">    <span class="comment">// 3.非空判断</span></span><br><span class="line">    <span class="keyword">if</span> (typedTuples == <span class="literal">null</span> || typedTuples.isEmpty()) &#123;</span><br><span class="line">        <span class="keyword">return</span> Result.ok();</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 4.解析数据：blogId、minTime（时间戳）、offset</span></span><br><span class="line">    List&lt;Long&gt; ids = <span class="keyword">new</span> <span class="title class_">ArrayList</span>&lt;&gt;(typedTuples.size());</span><br><span class="line">    <span class="type">long</span> <span class="variable">minTime</span> <span class="operator">=</span> <span class="number">0</span>; <span class="comment">// 2</span></span><br><span class="line">    <span class="type">int</span> <span class="variable">os</span> <span class="operator">=</span> <span class="number">1</span>; <span class="comment">// 2</span></span><br><span class="line">    <span class="keyword">for</span> (ZSetOperations.TypedTuple&lt;String&gt; tuple : typedTuples) &#123; <span class="comment">// 5 4 4 2 2</span></span><br><span class="line">        <span class="comment">// 4.1.获取id</span></span><br><span class="line">        ids.add(Long.valueOf(tuple.getValue()));</span><br><span class="line">        <span class="comment">// 4.2.获取分数(时间戳）</span></span><br><span class="line">        <span class="type">long</span> <span class="variable">time</span> <span class="operator">=</span> tuple.getScore().longValue();</span><br><span class="line">        <span class="keyword">if</span>(time == minTime)&#123;</span><br><span class="line">            os++;</span><br><span class="line">        &#125;<span class="keyword">else</span>&#123;</span><br><span class="line">            minTime = time;</span><br><span class="line">            os = <span class="number">1</span>;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">os = minTime == max ? os : os + offset;</span><br><span class="line">    <span class="comment">// 5.根据id查询blog</span></span><br><span class="line">    <span class="type">String</span> <span class="variable">idStr</span> <span class="operator">=</span> StrUtil.join(<span class="string">&quot;,&quot;</span>, ids);</span><br><span class="line">    List&lt;Blog&gt; blogs = query().in(<span class="string">&quot;id&quot;</span>, ids).last(<span class="string">&quot;ORDER BY FIELD(id,&quot;</span> + idStr + <span class="string">&quot;)&quot;</span>).list();</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> (Blog blog : blogs) &#123;</span><br><span class="line">        <span class="comment">// 5.1.查询blog有关的用户</span></span><br><span class="line">        queryBlogUser(blog);</span><br><span class="line">        <span class="comment">// 5.2.查询blog是否被点赞</span></span><br><span class="line">        isBlogLiked(blog);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 6.封装并返回</span></span><br><span class="line">    <span class="type">ScrollResult</span> <span class="variable">r</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ScrollResult</span>();</span><br><span class="line">    r.setList(blogs);</span><br><span class="line">    r.setOffset(os);</span><br><span class="line">    r.setMinTime(minTime);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> Result.ok(r);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="10、附近商户">10、附近商户</h2><h3 id="10-1、附近商户-GEO数据结构的基本用法">10.1、附近商户-GEO数据结构的基本用法</h3><p>GEO就是Geolocation的简写形式，代表地理坐标。Redis在3.2版本中加入了对GEO的支持，允许存储地理坐标信息，帮助我们根据经纬度来检索数据。常见的命令有：</p><ul><li>GEOADD：添加一个地理空间信息，包含：经度（longitude）、纬度（latitude）、值（member）</li><li>GEODIST：计算指定的两个点之间的距离并返回</li><li>GEOHASH：将指定member的坐标转为hash字符串形式并返回</li><li>GEOPOS：返回指定member的坐标</li><li>GEORADIUS：指定圆心、半径，找到该圆内包含的所有member，并按照与圆心之间的距离排序后返回。6.以后已废弃</li><li>GEOSEARCH：在指定范围内搜索member，并按照与指定点之间的距离排序后返回。范围可以是圆形或矩形。6.2.新功能</li><li>GEOSEARCHSTORE：与GEOSEARCH功能一致，不过可以把结果存储到一个指定的key。 6.2.新功能</li></ul><h3 id="10-2、-附近商户-导入店铺数据到GEO">10.2、 附近商户-导入店铺数据到GEO</h3><p>具体场景说明：</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653822036941.png" alt></p><p>当我们点击美食之后，会出现一系列的商家，商家中可以按照多种排序方式，我们此时关注的是距离，这个地方就需要使用到我们的GEO，向后台传入当前app收集的地址(我们此处是写死的) ，以当前坐标作为圆心，同时绑定相同的店家类型type，以及分页信息，把这几个条件传入后台，后台查询出对应的数据再返回。</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653822021827.png" alt></p><p>我们要做的事情是：将数据库表中的数据导入到redis中去，redis中的GEO，GEO在redis中就一个menber和一个经纬度，我们把x和y轴传入到redis做的经纬度位置去，但我们不能把所有的数据都放入到menber中去，毕竟作为redis是一个内存级数据库，如果存海量数据，redis还是力不从心，所以我们在这个地方存储他的id即可。</p><p>但是这个时候还有一个问题，就是在redis中并没有存储type，所以我们无法根据type来对数据进行筛选，所以我们可以按照商户类型做分组，类型相同的商户作为同一组，以typeId为key存入同一个GEO集合中即可</p><p>代码</p><p>HmDianPingApplicationTests</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">@Test</span></span><br><span class="line"><span class="keyword">void</span> <span class="title function_">loadShopData</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="comment">// 1.查询店铺信息</span></span><br><span class="line">    List&lt;Shop&gt; list = shopService.list();</span><br><span class="line">    <span class="comment">// 2.把店铺分组，按照typeId分组，typeId一致的放到一个集合</span></span><br><span class="line">    Map&lt;Long, List&lt;Shop&gt;&gt; map = list.stream().collect(Collectors.groupingBy(Shop::getTypeId));</span><br><span class="line">    <span class="comment">// 3.分批完成写入Redis</span></span><br><span class="line">    <span class="keyword">for</span> (Map.Entry&lt;Long, List&lt;Shop&gt;&gt; entry : map.entrySet()) &#123;</span><br><span class="line">        <span class="comment">// 3.1.获取类型id</span></span><br><span class="line">        <span class="type">Long</span> <span class="variable">typeId</span> <span class="operator">=</span> entry.getKey();</span><br><span class="line">        <span class="type">String</span> <span class="variable">key</span> <span class="operator">=</span> SHOP_GEO_KEY + typeId;</span><br><span class="line">        <span class="comment">// 3.2.获取同类型的店铺的集合</span></span><br><span class="line">        List&lt;Shop&gt; value = entry.getValue();</span><br><span class="line">        List&lt;RedisGeoCommands.GeoLocation&lt;String&gt;&gt; locations = <span class="keyword">new</span> <span class="title class_">ArrayList</span>&lt;&gt;(value.size());</span><br><span class="line">        <span class="comment">// 3.3.写入redis GEOADD key 经度 纬度 member</span></span><br><span class="line">        <span class="keyword">for</span> (Shop shop : value) &#123;</span><br><span class="line">            <span class="comment">// stringRedisTemplate.opsForGeo().add(key, new Point(shop.getX(), shop.getY()), shop.getId().toString());</span></span><br><span class="line">            locations.add(<span class="keyword">new</span> <span class="title class_">RedisGeoCommands</span>.GeoLocation&lt;&gt;(</span><br><span class="line">                    shop.getId().toString(),</span><br><span class="line">                    <span class="keyword">new</span> <span class="title class_">Point</span>(shop.getX(), shop.getY())</span><br><span class="line">            ));</span><br><span class="line">        &#125;</span><br><span class="line">        stringRedisTemplate.opsForGeo().add(key, locations);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="10-3-附近商户-实现附近商户功能">10.3 附近商户-实现附近商户功能</h3><p>SpringDataRedis的2.3.9版本并不支持Redis 6.2提供的GEOSEARCH命令，因此我们需要提示其版本，修改自己的POM</p><p>第一步：导入pom</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line">&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-data-redis&lt;/artifactId&gt;</span><br><span class="line">    &lt;exclusions&gt;</span><br><span class="line">        &lt;exclusion&gt;</span><br><span class="line">            &lt;artifactId&gt;spring-data-redis&lt;/artifactId&gt;</span><br><span class="line">            &lt;groupId&gt;org.springframework.data&lt;/groupId&gt;</span><br><span class="line">        &lt;/exclusion&gt;</span><br><span class="line">        &lt;exclusion&gt;</span><br><span class="line">            &lt;artifactId&gt;lettuce-core&lt;/artifactId&gt;</span><br><span class="line">            &lt;groupId&gt;io.lettuce&lt;/groupId&gt;</span><br><span class="line">        &lt;/exclusion&gt;</span><br><span class="line">    &lt;/exclusions&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.data&lt;/groupId&gt;</span><br><span class="line">    &lt;artifactId&gt;spring-data-redis&lt;/artifactId&gt;</span><br><span class="line">    &lt;version&gt;<span class="number">2.6</span><span class="number">.2</span>&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;io.lettuce&lt;/groupId&gt;</span><br><span class="line">    &lt;artifactId&gt;lettuce-core&lt;/artifactId&gt;</span><br><span class="line">    &lt;version&gt;<span class="number">6.1</span><span class="number">.6</span>.RELEASE&lt;/version&gt;</span><br><span class="line">&lt;/dependency&gt;</span><br></pre></td></tr></table></figure><p>第二步：</p><p>ShopController</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@GetMapping(&quot;/of/type&quot;)</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">queryShopByType</span><span class="params">(</span></span><br><span class="line"><span class="params">        <span class="meta">@RequestParam(&quot;typeId&quot;)</span> Integer typeId,</span></span><br><span class="line"><span class="params">        <span class="meta">@RequestParam(value = &quot;current&quot;, defaultValue = &quot;1&quot;)</span> Integer current,</span></span><br><span class="line"><span class="params">        <span class="meta">@RequestParam(value = &quot;x&quot;, required = false)</span> Double x,</span></span><br><span class="line"><span class="params">        <span class="meta">@RequestParam(value = &quot;y&quot;, required = false)</span> Double y</span></span><br><span class="line"><span class="params">)</span> &#123;</span><br><span class="line">   <span class="keyword">return</span> shopService.queryShopByType(typeId, current, x, y);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>ShopServiceImpl</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></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> Result <span class="title function_">queryShopByType</span><span class="params">(Integer typeId, Integer current, Double x, Double y)</span> &#123;</span><br><span class="line">        <span class="comment">// 1.判断是否需要根据坐标查询</span></span><br><span class="line">        <span class="keyword">if</span> (x == <span class="literal">null</span> || y == <span class="literal">null</span>) &#123;</span><br><span class="line">            <span class="comment">// 不需要坐标查询，按数据库查询</span></span><br><span class="line">            Page&lt;Shop&gt; page = query()</span><br><span class="line">                    .eq(<span class="string">&quot;type_id&quot;</span>, typeId)</span><br><span class="line">                    .page(<span class="keyword">new</span> <span class="title class_">Page</span>&lt;&gt;(current, SystemConstants.DEFAULT_PAGE_SIZE));</span><br><span class="line">            <span class="comment">// 返回数据</span></span><br><span class="line">            <span class="keyword">return</span> Result.ok(page.getRecords());</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 2.计算分页参数</span></span><br><span class="line">        <span class="type">int</span> <span class="variable">from</span> <span class="operator">=</span> (current - <span class="number">1</span>) * SystemConstants.DEFAULT_PAGE_SIZE;</span><br><span class="line">        <span class="type">int</span> <span class="variable">end</span> <span class="operator">=</span> current * SystemConstants.DEFAULT_PAGE_SIZE;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 3.查询redis、按照距离排序、分页。结果：shopId、distance</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">key</span> <span class="operator">=</span> SHOP_GEO_KEY + typeId;</span><br><span class="line">        GeoResults&lt;RedisGeoCommands.GeoLocation&lt;String&gt;&gt; results = stringRedisTemplate.opsForGeo() <span class="comment">// GEOSEARCH key BYLONLAT x y BYRADIUS 10 WITHDISTANCE</span></span><br><span class="line">                .search(</span><br><span class="line">                        key,</span><br><span class="line">                        GeoReference.fromCoordinate(x, y),</span><br><span class="line">                        <span class="keyword">new</span> <span class="title class_">Distance</span>(<span class="number">5000</span>),</span><br><span class="line">                        RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs().includeDistance().limit(end)</span><br><span class="line">                );</span><br><span class="line">        <span class="comment">// 4.解析出id</span></span><br><span class="line">        <span class="keyword">if</span> (results == <span class="literal">null</span>) &#123;</span><br><span class="line">            <span class="keyword">return</span> Result.ok(Collections.emptyList());</span><br><span class="line">        &#125;</span><br><span class="line">        List&lt;GeoResult&lt;RedisGeoCommands.GeoLocation&lt;String&gt;&gt;&gt; list = results.getContent();</span><br><span class="line">        <span class="keyword">if</span> (list.size() &lt;= from) &#123;</span><br><span class="line">            <span class="comment">// 没有下一页了，结束</span></span><br><span class="line">            <span class="keyword">return</span> Result.ok(Collections.emptyList());</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">// 4.1.截取 from ~ end的部分</span></span><br><span class="line">        List&lt;Long&gt; ids = <span class="keyword">new</span> <span class="title class_">ArrayList</span>&lt;&gt;(list.size());</span><br><span class="line">        Map&lt;String, Distance&gt; distanceMap = <span class="keyword">new</span> <span class="title class_">HashMap</span>&lt;&gt;(list.size());</span><br><span class="line">        list.stream().skip(from).forEach(result -&gt; &#123;</span><br><span class="line">            <span class="comment">// 4.2.获取店铺id</span></span><br><span class="line">            <span class="type">String</span> <span class="variable">shopIdStr</span> <span class="operator">=</span> result.getContent().getName();</span><br><span class="line">            ids.add(Long.valueOf(shopIdStr));</span><br><span class="line">            <span class="comment">// 4.3.获取距离</span></span><br><span class="line">            <span class="type">Distance</span> <span class="variable">distance</span> <span class="operator">=</span> result.getDistance();</span><br><span class="line">            distanceMap.put(shopIdStr, distance);</span><br><span class="line">        &#125;);</span><br><span class="line">        <span class="comment">// 5.根据id查询Shop</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">idStr</span> <span class="operator">=</span> StrUtil.join(<span class="string">&quot;,&quot;</span>, ids);</span><br><span class="line">        List&lt;Shop&gt; shops = query().in(<span class="string">&quot;id&quot;</span>, ids).last(<span class="string">&quot;ORDER BY FIELD(id,&quot;</span> + idStr + <span class="string">&quot;)&quot;</span>).list();</span><br><span class="line">        <span class="keyword">for</span> (Shop shop : shops) &#123;</span><br><span class="line">            shop.setDistance(distanceMap.get(shop.getId().toString()).getValue());</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">// 6.返回</span></span><br><span class="line">        <span class="keyword">return</span> Result.ok(shops);</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure><h2 id="11、用户签到">11、用户签到</h2><h4 id="11-1、用户签到-BitMap功能演示">11.1、用户签到-BitMap功能演示</h4><p>我们针对签到功能完全可以通过mysql来完成，比如说以下这张表</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653823145495.png" alt></p><p>用户一次签到，就是一条记录，假如有1000万用户，平均每人每年签到次数为10次，则这张表一年的数据量为 1亿条</p><p>每签到一次需要使用（8 + 8 + 1 + 1 + 3 + 1）共22 字节的内存，一个月则最多需要600多字节</p><p>我们如何能够简化一点呢？其实可以考虑小时候一个挺常见的方案，就是小时候，咱们准备一张小小的卡片，你只要签到就打上一个勾，我最后判断你是否签到，其实只需要到小卡片上看一看就知道了</p><p>我们可以采用类似这样的方案来实现我们的签到需求。</p><p>我们按月来统计用户签到信息，签到记录为1，未签到则记录为0.</p><p>把每一个bit位对应当月的每一天，形成了映射关系。用0和1标示业务状态，这种思路就称为位图（BitMap）。这样我们就用极小的空间，来实现了大量数据的表示</p><p>Redis中是利用string类型数据结构实现BitMap，因此最大上限是512M，转换为bit则是 2^32个bit位。</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653824498278.png" alt></p><p>BitMap的操作命令有：</p><ul><li>SETBIT：向指定位置（offset）存入一个0或1</li><li>GETBIT ：获取指定位置（offset）的bit值</li><li>BITCOUNT ：统计BitMap中值为1的bit位的数量</li><li>BITFIELD ：操作（查询、修改、自增）BitMap中bit数组中的指定位置（offset）的值</li><li>BITFIELD_RO ：获取BitMap中bit数组，并以十进制形式返回</li><li>BITOP ：将多个BitMap的结果做位运算（与 、或、异或）</li><li>BITPOS ：查找bit数组中指定范围内第一个0或1出现的位置</li></ul><h4 id="11-2-、用户签到-实现签到功能">11.2 、用户签到-实现签到功能</h4><p>需求：实现签到接口，将当前用户当天签到信息保存到Redis中</p><p>思路：我们可以把年和月作为bitMap的key，然后保存到一个bitMap中，每次签到就到对应的位上把数字从0变成1，只要对应是1，就表明说明这一天已经签到了，反之则没有签到。</p><p>我们通过接口文档发现，此接口并没有传递任何的参数，没有参数怎么确实是哪一天签到呢？这个很容易，可以通过后台代码直接获取即可，然后到对应的地址上去修改bitMap。</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653833970361.png" alt></p><p><strong>代码</strong></p><p>UserController</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@PostMapping(&quot;/sign&quot;)</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">sign</span><span class="params">()</span>&#123;</span><br><span class="line">   <span class="keyword">return</span> userService.sign();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>UserServiceImpl</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">sign</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="comment">// 1.获取当前登录用户</span></span><br><span class="line">    <span class="type">Long</span> <span class="variable">userId</span> <span class="operator">=</span> UserHolder.getUser().getId();</span><br><span class="line">    <span class="comment">// 2.获取日期</span></span><br><span class="line">    <span class="type">LocalDateTime</span> <span class="variable">now</span> <span class="operator">=</span> LocalDateTime.now();</span><br><span class="line">    <span class="comment">// 3.拼接key</span></span><br><span class="line">    <span class="type">String</span> <span class="variable">keySuffix</span> <span class="operator">=</span> now.format(DateTimeFormatter.ofPattern(<span class="string">&quot;:yyyyMM&quot;</span>));</span><br><span class="line">    <span class="type">String</span> <span class="variable">key</span> <span class="operator">=</span> USER_SIGN_KEY + userId + keySuffix;</span><br><span class="line">    <span class="comment">// 4.获取今天是本月的第几天</span></span><br><span class="line">    <span class="type">int</span> <span class="variable">dayOfMonth</span> <span class="operator">=</span> now.getDayOfMonth();</span><br><span class="line">    <span class="comment">// 5.写入Redis SETBIT key offset 1</span></span><br><span class="line">    stringRedisTemplate.opsForValue().setBit(key, dayOfMonth - <span class="number">1</span>, <span class="literal">true</span>);</span><br><span class="line">    <span class="keyword">return</span> Result.ok();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="11-3-用户签到-签到统计">11.3 用户签到-签到统计</h4><p>**问题1：**什么叫做连续签到天数？<br>从最后一次签到开始向前统计，直到遇到第一次未签到为止，计算总的签到次数，就是连续签到天数。</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653834455899.png" alt></p><p>Java逻辑代码：获得当前这个月的最后一次签到数据，定义一个计数器，然后不停的向前统计，直到获得第一个非0的数字即可，每得到一个非0的数字计数器+1，直到遍历完所有的数据，就可以获得当前月的签到总天数了</p><p>**问题2：**如何得到本月到今天为止的所有签到数据？</p><p>BITFIELD key GET u[dayOfMonth] 0</p><p>假设今天是10号，那么我们就可以从当前月的第一天开始，获得到当前这一天的位数，是10号，那么就是10位，去拿这段时间的数据，就能拿到所有的数据了，那么这10天里边签到了多少次呢？统计有多少个1即可。</p><p><strong>问题3：如何从后向前遍历每个bit位？</strong></p><p>注意：bitMap返回的数据是10进制，哪假如说返回一个数字8，那么我哪儿知道到底哪些是0，哪些是1呢？我们只需要让得到的10进制数字和1做与运算就可以了，因为1只有遇见1 才是1，其他数字都是0 ，我们把签到结果和1进行与操作，每与一次，就把签到结果向右移动一位，依次内推，我们就能完成逐个遍历的效果了。</p><p>需求：实现下面接口，统计当前用户截止当前时间在本月的连续签到天数</p><p>有用户有时间我们就可以组织出对应的key，此时就能找到这个用户截止这天的所有签到记录，再根据这套算法，就能统计出来他连续签到的次数了</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653835784444.png" alt></p><p>代码</p><p><strong>UserController</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@GetMapping(&quot;/sign/count&quot;)</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">signCount</span><span class="params">()</span>&#123;</span><br><span class="line">    <span class="keyword">return</span> userService.signCount();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>UserServiceImpl</strong></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></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">signCount</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="comment">// 1.获取当前登录用户</span></span><br><span class="line">    <span class="type">Long</span> <span class="variable">userId</span> <span class="operator">=</span> UserHolder.getUser().getId();</span><br><span class="line">    <span class="comment">// 2.获取日期</span></span><br><span class="line">    <span class="type">LocalDateTime</span> <span class="variable">now</span> <span class="operator">=</span> LocalDateTime.now();</span><br><span class="line">    <span class="comment">// 3.拼接key</span></span><br><span class="line">    <span class="type">String</span> <span class="variable">keySuffix</span> <span class="operator">=</span> now.format(DateTimeFormatter.ofPattern(<span class="string">&quot;:yyyyMM&quot;</span>));</span><br><span class="line">    <span class="type">String</span> <span class="variable">key</span> <span class="operator">=</span> USER_SIGN_KEY + userId + keySuffix;</span><br><span class="line">    <span class="comment">// 4.获取今天是本月的第几天</span></span><br><span class="line">    <span class="type">int</span> <span class="variable">dayOfMonth</span> <span class="operator">=</span> now.getDayOfMonth();</span><br><span class="line">    <span class="comment">// 5.获取本月截止今天为止的所有的签到记录，返回的是一个十进制的数字 BITFIELD sign:5:202203 GET u14 0</span></span><br><span class="line">    List&lt;Long&gt; result = stringRedisTemplate.opsForValue().bitField(</span><br><span class="line">            key,</span><br><span class="line">            BitFieldSubCommands.create()</span><br><span class="line">                    .get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth)).valueAt(<span class="number">0</span>)</span><br><span class="line">    );</span><br><span class="line">    <span class="keyword">if</span> (result == <span class="literal">null</span> || result.isEmpty()) &#123;</span><br><span class="line">        <span class="comment">// 没有任何签到结果</span></span><br><span class="line">        <span class="keyword">return</span> Result.ok(<span class="number">0</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="type">Long</span> <span class="variable">num</span> <span class="operator">=</span> result.get(<span class="number">0</span>);</span><br><span class="line">    <span class="keyword">if</span> (num == <span class="literal">null</span> || num == <span class="number">0</span>) &#123;</span><br><span class="line">        <span class="keyword">return</span> Result.ok(<span class="number">0</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 6.循环遍历</span></span><br><span class="line">    <span class="type">int</span> <span class="variable">count</span> <span class="operator">=</span> <span class="number">0</span>;</span><br><span class="line">    <span class="keyword">while</span> (<span class="literal">true</span>) &#123;</span><br><span class="line">        <span class="comment">// 6.1.让这个数字与1做与运算，得到数字的最后一个bit位  // 判断这个bit位是否为0</span></span><br><span class="line">        <span class="keyword">if</span> ((num &amp; <span class="number">1</span>) == <span class="number">0</span>) &#123;</span><br><span class="line">            <span class="comment">// 如果为0，说明未签到，结束</span></span><br><span class="line">            <span class="keyword">break</span>;</span><br><span class="line">        &#125;<span class="keyword">else</span> &#123;</span><br><span class="line">            <span class="comment">// 如果不为0，说明已签到，计数器+1</span></span><br><span class="line">            count++;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">// 把数字右移一位，抛弃最后一个bit位，继续下一个bit位</span></span><br><span class="line">        num &gt;&gt;&gt;= <span class="number">1</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> Result.ok(count);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="11-4-额外加餐-关于使用bitmap来解决缓存穿透的方案">11.4 额外加餐-关于使用bitmap来解决缓存穿透的方案</h4><p>回顾<strong>缓存穿透</strong>：</p><p>发起了一个数据库不存在的，redis里边也不存在的数据，通常你可以把他看成一个攻击</p><p>解决方案：</p><ul><li><p>判断id&lt;0</p></li><li><p>如果数据库是空，那么就可以直接往redis里边把这个空数据缓存起来</p></li></ul><p>第一种解决方案：遇到的问题是如果用户访问的是id不存在的数据，则此时就无法生效</p><p>第二种解决方案：遇到的问题是：如果是不同的id那就可以防止下次过来直击数据</p><p>所以我们如何解决呢？</p><p>我们可以将数据库的数据，所对应的id写入到一个list集合中，当用户过来访问的时候，我们直接去判断list中是否包含当前的要查询的数据，如果说用户要查询的id数据并不在list集合中，则直接返回，如果list中包含对应查询的id数据，则说明不是一次缓存穿透数据，则直接放行。</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653836416586.png" alt></p><p>现在的问题是这个主键其实并没有那么短，而是很长的一个 主键</p><p>哪怕你单独去提取这个主键，但是在11年左右，淘宝的商品总量就已经超过10亿个</p><p>所以如果采用以上方案，这个list也会很大，所以我们可以使用bitmap来减少list的存储空间</p><p>我们可以把list数据抽象成一个非常大的bitmap，我们不再使用list，而是将db中的id数据利用哈希思想，比如：</p><p>id % bitmap.size  = 算出当前这个id对应应该落在bitmap的哪个索引上，然后将这个值从0变成1，然后当用户来查询数据时，此时已经没有了list，让用户用他查询的id去用相同的哈希算法， 算出来当前这个id应当落在bitmap的哪一位，然后判断这一位是0，还是1，如果是0则表明这一位上的数据一定不存在，  采用这种方式来处理，需要重点考虑一个事情，就是误差率，所谓的误差率就是指当发生哈希冲突的时候，产生的误差。</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653836578970.png" alt></p><h2 id="12、UV统计">12、UV统计</h2><h3 id="12-1-、UV统计-HyperLogLog">12.1 、UV统计-HyperLogLog</h3><p>首先我们搞懂两个概念：</p><ul><li>UV：全称Unique Visitor，也叫独立访客量，是指通过互联网访问、浏览这个网页的自然人。1天内同一个用户多次访问该网站，只记录1次。</li><li>PV：全称Page View，也叫页面访问量或点击量，用户每访问网站的一个页面，记录1次PV，用户多次打开页面，则记录多次PV。往往用来衡量网站的流量。</li></ul><p>通常来说UV会比PV大很多，所以衡量同一个网站的访问量，我们需要综合考虑很多因素，所以我们只是单纯的把这两个值作为一个参考值</p><p>UV统计在服务端做会比较麻烦，因为要判断该用户是否已经统计过了，需要将统计过的用户信息保存。但是如果每个访问的用户都保存到Redis中，数据量会非常恐怖，那怎么处理呢？</p><p>Hyperloglog(HLL)是从Loglog算法派生的概率算法，用于确定非常大的集合的基数，而不需要存储其所有值。相关算法原理大家可以参考：<a href="https://juejin.cn/post/6844903785744056333#heading-0">https://juejin.cn/post/6844903785744056333#heading-0</a><br>Redis中的HLL是基于string结构实现的，单个HLL的内存<strong>永远小于16kb</strong>，<strong>内存占用低</strong>的令人发指！作为代价，其测量结果是概率性的，<strong>有小于0.81％的误差</strong>。不过对于UV统计来说，这完全可以忽略。</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653837988985.png" alt></p><h3 id="12-2-UV统计-测试百万数据的统计">12.2 UV统计-测试百万数据的统计</h3><p>测试思路：我们直接利用单元测试，向HyperLogLog中添加100万条数据，看看内存占用和统计效果如何</p><p><img src="https://gcore.jsdelivr.net/gh/keepstaytrue/blog_images@master/1653838053608.png" alt></p><p>经过测试：我们会发生他的误差是在允许范围内，并且内存占用极小</p>]]>
    </content>
    <id>https://keepstaytrue.github.io/2025/05/12/Redis%E5%AE%9E%E6%88%98%E7%AF%87/</id>
    <link href="https://keepstaytrue.github.io/2025/05/12/Redis%E5%AE%9E%E6%88%98%E7%AF%87/"/>
    <published>2025-05-12T02:00:00.000Z</published>
    <summary>
      <![CDATA[<h1>实战篇Redis</h1>
<h2 id="开篇导读">开篇导读</h2>
<p>在此非常感谢黑马程序员提供的课程资料!!!</p>
<ul>
<li>短信登录</li>
</ul>
<p>这一块我们会使用redis共享session来实现</p>
<ul>
<li>商户]]>
    </summary>
    <title>实战篇Redis</title>
    <updated>2026-05-23T13:33:53.824Z</updated>
  </entry>
</feed>
