<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <id>https://firefly.cuteleaf.cn/</id>
  <title type="text">Columbina</title>
  <subtitle type="text">Columbina</subtitle>
  <updated>2025-08-12T00:00:00.000Z</updated>
  <author><name>幽流啊飘</name></author>
  <link rel="alternate" href="https://firefly.cuteleaf.cn/"/>
  <link rel="self" href="https://firefly.cuteleaf.cn/atom.xml"/>
  <generator uri="https://github.com/CuteLeaf/Firefly">Firefly v6.16.8</generator>
    <entry>
      <id>https://firefly.cuteleaf.cn/posts/learn-redis-ex/</id>
      <title type="text">Redis 实战笔记：短信登录、缓存、秒杀、分布式锁与社交功能</title>
      <published>2025-08-12T00:00:00.000Z</published>
      <updated>2025-08-12T00:00:00.000Z</updated>
      <author><name>幽流啊飘</name></author>
      <link rel="alternate" href="https://firefly.cuteleaf.cn/posts/learn-redis-ex/"/>
      <summary type="text">基于黑马点评项目，系统整理 Redis 在短信登录、商户缓存、优惠券秒杀、分布式锁、达人探店、好友关注、附近商户、签到与 UV 统计等场景中的实战应用。</summary>
      <content type="html"><![CDATA[<section><h1>Redis 实战笔记：短信登录、缓存、秒杀、分布式锁与社交功能<a href="#redis-实战笔记短信登录缓存秒杀分布式锁与社交功能"><span>#</span></a></h1><div><div><div></div><div>阅读提示</div></div><div><p>本文是 Redis 实战项目「黑马点评」的学习笔记，内容覆盖：
<strong>短信登录 → 商户缓存 → 优惠券秒杀 → 分布式锁 → 异步秒杀 → 达人探店 → 好友关注 → 附近商户 → 签到统计 → UV 统计</strong>。
建议先掌握 Redis 五大数据结构、SpringDataRedis 基本用法，再阅读本文。</p></div></div><section><h2>内容概述<a href="#内容概述"><span>#</span></a></h2><p>本项目围绕一个本地生活服务平台展开，主要使用 Redis 解决以下业务问题：</p>

<table><thead><tr><th>模块</th><th>Redis 技术点</th></tr></thead><tbody><tr><td>短信登录</td><td>Redis 共享 Session、Token</td></tr><tr><td>商户查询缓存</td><td>缓存穿透、缓存雪崩、缓存击穿</td></tr><tr><td>优惠券秒杀</td><td>计数器、Lua 脚本、分布式锁、消息队列</td></tr><tr><td>附近的商户</td><td>Redis GEO</td></tr><tr><td>UV 统计</td><td>HyperLogLog</td></tr><tr><td>用户签到</td><td>BitMap</td></tr><tr><td>好友关注</td><td>Set 集合交并差</td></tr><tr><td>达人探店</td><td>List、SortedSet</td></tr></tbody></table><hr /></section><section><h2>一、短信登录<a href="#一短信登录"><span>#</span></a></h2><section><h3>1.1 导入项目<a href="#11-导入项目"><span>#</span></a></h3><p>项目采用前后端分离模式，整体请求链路如下：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>手机/App → Nginx → Tomcat → MySQL / Redis</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>Nginx 可以作为静态资源服务器、反向代理和负载均衡器；Tomcat 负责业务逻辑；MySQL 存储核心数据；Redis 提供高性能缓存与共享数据。</p><p>项目中涉及的主要数据表：</p>

<table><thead><tr><th>表</th><th>说明</th></tr></thead><tbody><tr><td><code>tb_user</code></td><td>用户表</td></tr><tr><td><code>tb_user_info</code></td><td>用户详情表</td></tr><tr><td><code>tb_shop</code></td><td>商户信息表</td></tr><tr><td><code>tb_shop_type</code></td><td>商户类型表</td></tr><tr><td><code>tb_blog</code></td><td>用户日记表（达人探店日记）</td></tr><tr><td><code>tb_follow</code></td><td>用户关注表</td></tr><tr><td><code>tb_voucher</code></td><td>优惠券表</td></tr><tr><td><code>tb_voucher_order</code></td><td>优惠券订单表</td></tr></tbody></table><p>导入后端项目后，修改 MySQL 和 Redis 连接信息并启动。访问：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>http://localhost:8081/shop-type/list</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>如果能看到 JSON 数据，说明后端导入成功。</p><p>前端项目解压后，在 Nginx 目录执行：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>start</span><span> </span><span>nginx.exe</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>访问：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>http://localhost:8080/</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>1.2 基于 Session 实现登录流程<a href="#12-基于-session-实现登录流程"><span>#</span></a></h3><section><h4>发送验证码<a href="#发送验证码"><span>#</span></a></h4><ol>
<li>用户提交手机号/邮箱。</li>
<li>后端校验格式是否合法。</li>
<li>合法则生成验证码并保存。</li>
<li>通过短信或邮件发送验证码。</li>
</ol></section><section><h4>登录/注册<a href="#登录注册"><span>#</span></a></h4><ol>
<li>用户输入账号和验证码。</li>
<li>后端从 Session 中取出验证码进行校验。</li>
<li>校验通过后根据账号查询用户。</li>
<li>用户不存在则创建用户。</li>
<li>将用户信息保存到 Session。</li>
</ol></section><section><h4>校验登录状态<a href="#校验登录状态"><span>#</span></a></h4><ol>
<li>用户请求时携带 Cookie 中的 <code>JsessionId</code>。</li>
<li>后端通过 <code>JsessionId</code> 从 Session 中获取用户信息。</li>
<li>没有用户信息则拦截。</li>
<li>有用户信息则保存到 <code>ThreadLocal</code> 并放行。</li>
</ol></section></section><section><h3>1.3 发送邮箱验证码<a href="#13-发送邮箱验证码"><span>#</span></a></h3><p>黑马点评默认使用短信验证码，但为了实际部署方便，可以改为邮箱验证码。需要导入依赖：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>&lt;</span><span>dependency</span><span>&gt;</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>&lt;</span><span>groupId</span><span>&gt;javax.activation&lt;/</span><span>groupId</span><span>&gt;</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>&lt;</span><span>artifactId</span><span>&gt;activation&lt;/</span><span>artifactId</span><span>&gt;</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>&lt;</span><span>version</span><span>&gt;1.1.1&lt;/</span><span>version</span><span>&gt;</span></div></div><div><div><div>5</div></div><div><span>&lt;/</span><span>dependency</span><span>&gt;</span></div></div><div><div><div>6</div></div><div><span>&lt;</span><span>dependency</span><span>&gt;</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>&lt;</span><span>groupId</span><span>&gt;javax.mail&lt;/</span><span>groupId</span><span>&gt;</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>&lt;</span><span>artifactId</span><span>&gt;mail&lt;/</span><span>artifactId</span><span>&gt;</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>&lt;</span><span>version</span><span>&gt;1.4.7&lt;/</span><span>version</span><span>&gt;</span></div></div><div><div><div>10</div></div><div><span>&lt;/</span><span>dependency</span><span>&gt;</span></div></div><div><div><div>11</div></div><div><span>&lt;</span><span>dependency</span><span>&gt;</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>&lt;</span><span>groupId</span><span>&gt;org.apache.commons&lt;/</span><span>groupId</span><span>&gt;</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>&lt;</span><span>artifactId</span><span>&gt;commons-email&lt;/</span><span>artifactId</span><span>&gt;</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>&lt;</span><span>version</span><span>&gt;1.4&lt;/</span><span>version</span><span>&gt;</span></div></div><div><div><div>15</div></div><div><span>&lt;/</span><span>dependency</span><span>&gt;</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>邮件工具类核心逻辑：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>public</span><span> </span><span>class</span><span><span> </span><span>MailUtils</span><span> </span></span><span>{</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>    </span><span>public</span><span> </span><span>static</span><span> </span><span>void</span><span> </span><span>sendTestMail</span><span>(</span><span>String</span><span><span> </span><span>email</span><span>, </span></span><span>String</span><span><span> </span><span>code</span><span>)</span><span> </span></span><span>throws</span><span> </span><span>MessagingException</span><span><span> </span><span>{</span></span></div></div><div><div><div>4</div></div><div><span>        </span><span>Properties</span><span><span> </span><span>props</span><span> </span></span><span><span>=</span><span> </span></span><span>new</span><span> </span><span>Properties</span><span>();</span></div></div><div><div><div>5</div></div><div><span>        </span><span>props</span><span>.</span><span>put</span><span>(</span><span>"mail.smtp.auth"</span><span>, </span><span>"true"</span><span>);</span></div></div><div><div><div>6</div></div><div><span>        </span><span>props</span><span>.</span><span>put</span><span>(</span><span>"mail.smtp.host"</span><span>, </span><span>"smtp.qq.com"</span><span>);</span></div></div><div><div><div>7</div></div><div><span>        </span><span>props</span><span>.</span><span>put</span><span>(</span><span>"mail.smtp.port"</span><span>, </span><span>"587"</span><span>);</span></div></div><div><div><div>8</div></div><div><span>        </span><span>props</span><span>.</span><span>put</span><span>(</span><span>"mail.user"</span><span>, </span><span>"你的邮箱@qq.com"</span><span>);</span></div></div><div><div><div>9</div></div><div><span>        </span><span>props</span><span>.</span><span>put</span><span>(</span><span>"mail.password"</span><span>, </span><span>"你的SMTP授权码"</span><span>);</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>        </span><span>Authenticator</span><span><span> </span><span>authenticator</span><span> </span></span><span><span>=</span><span> </span></span><span>new</span><span> </span><span>Authenticator</span><span>() {</span></div></div><div><div><div>12</div></div><div><span><span>            </span></span><span>@</span><span>Override</span></div></div><div><div><div>13</div></div><div><span>            </span><span>protected</span><span> </span><span>PasswordAuthentication</span><span> </span><span>getPasswordAuthentication</span><span><span>()</span><span> </span><span>{</span></span></div></div><div><div><div>14</div></div><div><span>                </span><span>String</span><span><span> </span><span>userName</span><span> </span></span><span><span>=</span><span> </span></span><span>props</span><span>.</span><span>getProperty</span><span>(</span><span>"mail.user"</span><span>);</span></div></div><div><div><div>15</div></div><div><span>                </span><span>String</span><span><span> </span><span>password</span><span> </span></span><span><span>=</span><span> </span></span><span>props</span><span>.</span><span>getProperty</span><span>(</span><span>"mail.password"</span><span>);</span></div></div><div><div><div>16</div></div><div><span>                </span><span>return</span><span> </span><span>new</span><span> </span><span>PasswordAuthentication</span><span>(userName, password);</span></div></div><div><div><div>17</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>18</div></div><div><span><span>        </span></span><span>};</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>        </span><span>Session</span><span><span> </span><span>mailSession</span><span> </span></span><span><span>=</span><span> </span></span><span>Session</span><span>.</span><span>getInstance</span><span>(props, authenticator);</span></div></div><div><div><div>21</div></div><div><span>        </span><span>MimeMessage</span><span><span> </span><span>message</span><span> </span></span><span><span>=</span><span> </span></span><span>new</span><span> </span><span>MimeMessage</span><span>(mailSession);</span></div></div><div><div><div>22</div></div><div><span>        </span><span>message</span><span>.</span><span>setFrom</span><span>(</span><span>new</span><span> </span><span>InternetAddress</span><span>(</span><span>props</span><span>.</span><span>getProperty</span><span>(</span><span>"mail.user"</span><span>)));</span></div></div><div><div><div>23</div></div><div><span>        </span><span>message</span><span>.</span><span>setRecipient</span><span>(</span><span>Message</span><span>.</span><span>RecipientType</span><span>.</span><span>TO</span><span>, </span><span>new</span><span> </span><span>InternetAddress</span><span>(email));</span></div></div><div><div><div>24</div></div><div><span>        </span><span>message</span><span>.</span><span>setSubject</span><span>(</span><span>"登录验证码"</span><span>);</span></div></div><div><div><div>25</div></div><div><span>        </span><span>message</span><span>.</span><span>setContent</span><span>(</span><span>"验证码为："</span><span><span> </span><span>+</span><span> code </span><span>+</span><span> </span></span><span>"（有效期一分钟）"</span><span>, </span><span>"text/html;charset=UTF-8"</span><span>);</span></div></div><div><div><div>26</div></div><div><span>        </span><span>Transport</span><span>.</span><span>send</span><span>(message);</span></div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>28</div></div><div>
</div></div><div><div><div>29</div></div><div><span>    </span><span>public</span><span> </span><span>static</span><span> </span><span>String</span><span> </span><span>achieveCode</span><span><span>()</span><span> </span><span>{</span></span></div></div><div><div><div>30</div></div><div><span>        </span><span>String</span><span><span>[] </span><span>beforeShuffle</span><span> </span></span><span><span>=</span><span> </span></span><span>new</span><span> </span><span>String</span><span>[]{</span></div></div><div><div><div>31</div></div><div><span>                </span><span>"2"</span><span>, </span><span>"3"</span><span>, </span><span>"4"</span><span>, </span><span>"5"</span><span>, </span><span>"6"</span><span>, </span><span>"7"</span><span>, </span><span>"8"</span><span>, </span><span>"9"</span><span>,</span></div></div><div><div><div>32</div></div><div><span>                </span><span>"A"</span><span>, </span><span>"B"</span><span>, </span><span>"C"</span><span>, </span><span>"D"</span><span>, </span><span>"E"</span><span>, </span><span>"F"</span><span>, </span><span>"G"</span><span>, </span><span>"H"</span><span>,</span></div></div><div><div><div>33</div></div><div><span>                </span><span>"I"</span><span>, </span><span>"J"</span><span>, </span><span>"K"</span><span>, </span><span>"L"</span><span>, </span><span>"M"</span><span>, </span><span>"N"</span><span>, </span><span>"O"</span><span>, </span><span>"P"</span><span>,</span></div></div><div><div><div>34</div></div><div><span>                </span><span>"Q"</span><span>, </span><span>"R"</span><span>, </span><span>"S"</span><span>, </span><span>"T"</span><span>, </span><span>"U"</span><span>, </span><span>"V"</span><span>, </span><span>"W"</span><span>, </span><span>"X"</span><span>,</span></div></div><div><div><div>35</div></div><div><span>                </span><span>"Y"</span><span>, </span><span>"Z"</span><span>, </span><span>"a"</span><span>, </span><span>"b"</span><span>, </span><span>"c"</span><span>, </span><span>"d"</span><span>, </span><span>"e"</span><span>, </span><span>"f"</span><span>,</span></div></div><div><div><div>36</div></div><div><span>                </span><span>"g"</span><span>, </span><span>"h"</span><span>, </span><span>"i"</span><span>, </span><span>"j"</span><span>, </span><span>"k"</span><span>, </span><span>"l"</span><span>, </span><span>"m"</span><span>, </span><span>"n"</span><span>,</span></div></div><div><div><div>37</div></div><div><span>                </span><span>"o"</span><span>, </span><span>"p"</span><span>, </span><span>"q"</span><span>, </span><span>"r"</span><span>, </span><span>"s"</span><span>, </span><span>"t"</span><span>, </span><span>"u"</span><span>, </span><span>"v"</span><span>,</span></div></div><div><div><div>38</div></div><div><span>                </span><span>"w"</span><span>, </span><span>"x"</span><span>, </span><span>"y"</span><span>, </span><span>"z"</span></div></div><div><div><div>39</div></div><div><span><span>        </span></span><span>};</span></div></div><div><div><div>40</div></div><div><span>        </span><span>List</span><span>&lt;</span><span>String</span><span><span>&gt; </span><span>list</span><span> </span></span><span><span>=</span><span> </span></span><span>Arrays</span><span>.</span><span>asList</span><span>(beforeShuffle);</span></div></div><div><div><div>41</div></div><div><span>        </span><span>Collections</span><span>.</span><span>shuffle</span><span>(list);</span></div></div><div><div><div>42</div></div><div><span>        </span><span>StringBuilder</span><span><span> </span><span>sb</span><span> </span></span><span><span>=</span><span> </span></span><span>new</span><span> </span><span>StringBuilder</span><span>();</span></div></div><div><div><div>43</div></div><div><span>        </span><span>for</span><span> (</span><span>String</span><span><span> </span><span>s</span><span> </span></span><span>:</span><span> list) {</span></div></div><div><div><div>44</div></div><div><span>            </span><span>sb</span><span>.</span><span>append</span><span>(s);</span></div></div><div><div><div>45</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>46</div></div><div><span>        </span><span>return</span><span> </span><span>sb</span><span>.</span><span>substring</span><span>(</span><span>3</span><span>, </span><span>8</span><span>);</span></div></div><div><div><div>47</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>48</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>修改 <code>sendCode</code> 方法：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@</span><span>PostMapping</span><span>(</span><span>"/code"</span><span>)</span></div></div><div><div><div>2</div></div><div><span>public</span><span> </span><span>Result</span><span> </span><span>sendCode</span><span><span>(</span><span>@</span></span><span>RequestParam</span><span>(</span><span>"phone"</span><span>) </span><span>String</span><span><span> phone</span><span>,</span><span> </span></span><span>HttpSession</span><span> session) throws MessagingException {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>if</span><span> (</span><span>RegexUtils</span><span>.</span><span>isEmailInvalid</span><span><span>(phone)</span><span>) {</span></span></div></div><div><div><div>4</div></div><div><span>        </span><span>return</span><span> </span><span>Result</span><span>.</span><span>fail</span><span>(</span><span>"邮箱格式不正确"</span><span>);</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>6</div></div><div><span>    </span><span>String</span><span> code </span><span><span>=</span><span> </span></span><span>MailUtils</span><span>.</span><span>achieveCode</span><span>();</span></div></div><div><div><div>7</div></div><div><span>    </span><span>session</span><span>.</span><span>setAttribute</span><span>(phone, code);</span></div></div><div><div><div>8</div></div><div><span>    </span><span>log</span><span>.</span><span>info</span><span>(</span><span>"发送登录验证码：{}"</span><span>, code);</span></div></div><div><div><div>9</div></div><div><span>    </span><span>MailUtils</span><span>.</span><span>sendTestMail</span><span>(phone, code);</span></div></div><div><div><div>10</div></div><div><span>    </span><span>return</span><span> </span><span>Result</span><span>.</span><span>ok</span><span>();</span></div></div><div><div><div>11</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>修改 <code>login</code> 方法：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@</span><span>PostMapping</span><span>(</span><span>"/login"</span><span>)</span></div></div><div><div><div>2</div></div><div><span>public</span><span> </span><span>Result</span><span> </span><span>login</span><span><span>(</span><span>@</span></span><span>RequestBody</span><span> </span><span>LoginFormDTO</span><span><span> loginForm</span><span>,</span><span> </span></span><span>HttpSession</span><span> session) {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>String</span><span> phone </span><span><span>=</span><span> </span></span><span>loginForm</span><span>.</span><span>getPhone</span><span>();</span></div></div><div><div><div>4</div></div><div><span>    </span><span>String</span><span> code </span><span><span>=</span><span> </span></span><span>loginForm</span><span>.</span><span>getCode</span><span>();</span></div></div><div><div><div>5</div></div><div><span>    </span><span>Object</span><span> cacheCode </span><span><span>=</span><span> </span></span><span>session</span><span>.</span><span>getAttribute</span><span>(phone);</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>    </span><span>if</span><span> (</span><span>RegexUtils</span><span>.</span><span>isEmailInvalid</span><span><span>(phone)</span><span>) {</span></span></div></div><div><div><div>8</div></div><div><span>        </span><span>return</span><span> </span><span>Result</span><span>.</span><span>fail</span><span>(</span><span>"邮箱格式不正确！！"</span><span>);</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>    </span><span>if</span><span><span> (code </span><span>==</span><span> </span></span><span>null</span><span><span> </span><span>||</span><span> cacheCode </span><span>==</span><span> </span></span><span>null</span><span><span> </span><span>||</span><span> </span><span>!</span></span><span>cacheCode</span><span>.</span><span>toString</span><span>().</span><span>equals</span><span><span>(code)</span><span>) {</span></span></div></div><div><div><div>12</div></div><div><span>        </span><span>return</span><span> </span><span>Result</span><span>.</span><span>fail</span><span>(</span><span>"验证码不一致！！"</span><span>);</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>    </span><span>LambdaQueryWrapper</span><span>&lt;</span><span>User</span><span><span>&gt;</span><span> queryWrapper </span></span><span><span>=</span><span> </span></span><span>new</span><span> </span><span>LambdaQueryWrapper</span><span><span>&lt;&gt;</span><span>()</span><span>;</span></span></div></div><div><div><div>16</div></div><div><span>    </span><span>queryWrapper</span><span>.</span><span>eq</span><span>(User</span><span>::</span><span>getPhone, phone);</span></div></div><div><div><div>17</div></div><div><span>    </span><span>User</span><span> user </span><span><span>=</span><span> </span></span><span>userService</span><span>.</span><span>getOne</span><span>(queryWrapper);</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>    </span><span>if</span><span><span> (user </span><span>==</span><span> </span></span><span>null</span><span>) {</span></div></div><div><div><div>20</div></div><div><span><span>        </span></span><span>user </span><span>=</span><span> </span><span>createUserWithPhone</span><span><span>(phone)</span><span>;</span></span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span>    </span><span>session</span><span>.</span><span>setAttribute</span><span>(</span><span>"user"</span><span>, user);</span></div></div><div><div><div>24</div></div><div><span>    </span><span>return</span><span> </span><span>Result</span><span>.</span><span>ok</span><span>();</span></div></div><div><div><div>25</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section><section><h3>1.4 登录拦截与隐藏敏感信息<a href="#14-登录拦截与隐藏敏感信息"><span>#</span></a></h3><p>创建 <code>LoginInterceptor</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>public</span><span> </span><span>class</span><span><span> </span><span>LoginInterceptor</span><span> </span></span><span>implements</span><span><span> </span><span>HandlerInterceptor</span><span> </span></span><span>{</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>@</span><span>Override</span></div></div><div><div><div>4</div></div><div><span>    </span><span>public</span><span> </span><span>boolean</span><span> </span><span>preHandle</span><span>(</span><span>HttpServletRequest</span><span><span> </span><span>request</span><span>, </span></span><span>HttpServletResponse</span><span><span> </span><span>response</span><span>, </span></span><span>Object</span><span><span> </span><span>handler</span><span>)</span><span> </span></span><span>throws</span><span> </span><span>Exception</span><span><span> </span><span>{</span></span></div></div><div><div><div>5</div></div><div><span>        </span><span>HttpSession</span><span><span> </span><span>session</span><span> </span></span><span><span>=</span><span> </span></span><span>request</span><span>.</span><span>getSession</span><span>();</span></div></div><div><div><div>6</div></div><div><span>        </span><span>User</span><span><span> </span><span>user</span><span> </span></span><span><span>=</span><span> (User) </span></span><span>session</span><span>.</span><span>getAttribute</span><span>(</span><span>"user"</span><span>);</span></div></div><div><div><div>7</div></div><div><span>        </span><span>if</span><span><span> (user </span><span>==</span><span> </span></span><span>null</span><span>) {</span></div></div><div><div><div>8</div></div><div><span>            </span><span>response</span><span>.</span><span>setStatus</span><span>(</span><span>401</span><span>);</span></div></div><div><div><div>9</div></div><div><span>            </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>11</div></div><div><span>        </span><span>UserHolder</span><span>.</span><span>saveUser</span><span>(user);</span></div></div><div><div><div>12</div></div><div><span>        </span><span>return</span><span> </span><span>true</span><span>;</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>@</span><span>Override</span></div></div><div><div><div>16</div></div><div><span>    </span><span>public</span><span> </span><span>void</span><span> </span><span>afterCompletion</span><span>(</span><span>HttpServletRequest</span><span><span> </span><span>request</span><span>, </span></span><span>HttpServletResponse</span><span><span> </span><span>response</span><span>, </span></span><span>Object</span><span><span> </span><span>handler</span><span>, </span></span><span>Exception</span><span><span> </span><span>ex</span><span>)</span><span> </span><span>{</span></span></div></div><div><div><div>17</div></div><div><span>        </span><span>UserHolder</span><span>.</span><span>removeUser</span><span>();</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>19</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>为了避免返回用户密码等敏感信息，创建 <code>UserDTO</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@</span><span>Data</span></div></div><div><div><div>2</div></div><div><span>public</span><span> </span><span>class</span><span><span> </span><span>UserDTO</span><span> </span></span><span>{</span></div></div><div><div><div>3</div></div><div><span>    </span><span>private</span><span> </span><span>Long</span><span> id</span><span>;</span></div></div><div><div><div>4</div></div><div><span>    </span><span>private</span><span> </span><span>String</span><span> nickName</span><span>;</span></div></div><div><div><div>5</div></div><div><span>    </span><span>private</span><span> </span><span>String</span><span> icon</span><span>;</span></div></div><div><div><div>6</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>修改登录逻辑，将 <code>User</code> 转为 <code>UserDTO</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>UserDTO</span><span> userDTO </span><span><span>=</span><span> </span></span><span>BeanUtil</span><span>.</span><span>copyProperties</span><span>(user, </span><span>UserDTO</span><span>.</span><span>class</span><span>);</span></div></div><div><div><div>2</div></div><div><span>session</span><span>.</span><span>setAttribute</span><span>(</span><span>"user"</span><span>, userDTO);</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>修改拦截器：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>UserDTO</span><span> user </span><span><span>=</span><span> (UserDTO) </span></span><span>session</span><span>.</span><span>getAttribute</span><span>(</span><span>"user"</span><span>);</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>1.5 Session 共享问题<a href="#15-session-共享问题"><span>#</span></a></h3><p>每个 Tomcat 都有自己独立的 Session。如果用户第一次访问 Tomcat1，第二次被负载均衡到 Tomcat2，Tomcat2 上没有 Session，就会导致登录状态失效。</p><p>早期解决方案是 Session 拷贝，但存在两个问题：</p><ul>
<li>每台服务器都有完整 Session 数据，服务器压力大。</li>
<li>Session 拷贝可能存在延迟。</li>
</ul><p>因此，使用 Redis 替代 Session。Redis 数据本身是共享的。</p></section><section><h3>1.6 基于 Redis 实现短信登录<a href="#16-基于-redis-实现短信登录"><span>#</span></a></h3><p>设计 Key 时需要注意：</p><ul>
<li>Key 要有唯一性。</li>
<li>Key 要方便携带。</li>
<li>不要直接使用手机号等敏感信息作为 Key。</li>
</ul><p>整体流程：</p><ol>
<li>用户提交账号和验证码。</li>
<li>校验验证码。</li>
<li>根据账号查询用户，不存在则创建。</li>
<li>生成随机 Token。</li>
<li>将用户信息以 Hash 结构存入 Redis。</li>
<li>返回 Token 给前端。</li>
<li>后续请求携带 Token，从 Redis 获取用户信息。</li>
</ol><p>修改 <code>sendCode</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@</span><span>PostMapping</span><span>(</span><span>"/code"</span><span>)</span></div></div><div><div><div>2</div></div><div><span>public</span><span> </span><span>Result</span><span> </span><span>sendCode</span><span><span>(</span><span>@</span></span><span>RequestParam</span><span>(</span><span>"phone"</span><span>) </span><span>String</span><span> phone) throws MessagingException {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>if</span><span> (</span><span>RegexUtils</span><span>.</span><span>isEmailInvalid</span><span><span>(phone)</span><span>) {</span></span></div></div><div><div><div>4</div></div><div><span>        </span><span>return</span><span> </span><span>Result</span><span>.</span><span>fail</span><span>(</span><span>"邮箱格式不正确"</span><span>);</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>6</div></div><div><span>    </span><span>String</span><span> code </span><span><span>=</span><span> </span></span><span>MailUtils</span><span>.</span><span>achieveCode</span><span>();</span></div></div><div><div><div>7</div></div><div><span>    </span><span>stringRedisTemplate</span><span>.</span><span>opsForValue</span><span>().</span><span>set</span><span>(</span></div></div><div><div><div>8</div></div><div><span><span>            </span></span><span>LOGIN_CODE_KEY </span><span>+</span><span> phone,</span></div></div><div><div><div>9</div></div><div><span><span>            </span></span><span>code,</span></div></div><div><div><div>10</div></div><div><span><span>            </span></span><span>LOGIN_CODE_TTL,</span></div></div><div><div><div>11</div></div><div><span>            </span><span>TimeUnit</span><span>.</span><span>MINUTES</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>);</span></div></div><div><div><div>13</div></div><div><span>    </span><span>log</span><span>.</span><span>info</span><span>(</span><span>"发送登录验证码：{}"</span><span>, code);</span></div></div><div><div><div>14</div></div><div><span>    </span><span>return</span><span> </span><span>Result</span><span>.</span><span>ok</span><span>();</span></div></div><div><div><div>15</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>修改 <code>login</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@</span><span>PostMapping</span><span>(</span><span>"/login"</span><span>)</span></div></div><div><div><div>2</div></div><div><span>public</span><span> </span><span>Result</span><span> </span><span>login</span><span><span>(</span><span>@</span></span><span>RequestBody</span><span> </span><span>LoginFormDTO</span><span> loginForm) {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>String</span><span> phone </span><span><span>=</span><span> </span></span><span>loginForm</span><span>.</span><span>getPhone</span><span>();</span></div></div><div><div><div>4</div></div><div><span>    </span><span>String</span><span> code </span><span><span>=</span><span> </span></span><span>loginForm</span><span>.</span><span>getCode</span><span>();</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>    </span><span>String</span><span> cacheCode </span><span><span>=</span><span> </span></span><span>stringRedisTemplate</span><span>.</span><span>opsForValue</span><span>().</span><span>get</span><span><span>(LOGIN_CODE_KEY </span><span>+</span><span> phone);</span></span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>    </span><span>if</span><span> (</span><span>RegexUtils</span><span>.</span><span>isEmailInvalid</span><span><span>(phone)</span><span>) {</span></span></div></div><div><div><div>9</div></div><div><span>        </span><span>return</span><span> </span><span>Result</span><span>.</span><span>fail</span><span>(</span><span>"邮箱格式不正确！！"</span><span>);</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>    </span><span>if</span><span><span> (code </span><span>==</span><span> </span></span><span>null</span><span><span> </span><span>||</span><span> cacheCode </span><span>==</span><span> </span></span><span>null</span><span><span> </span><span>||</span><span> </span><span>!</span></span><span>cacheCode</span><span>.</span><span>equals</span><span><span>(code)</span><span>) {</span></span></div></div><div><div><div>13</div></div><div><span>        </span><span>return</span><span> </span><span>Result</span><span>.</span><span>fail</span><span>(</span><span>"验证码不一致！！"</span><span>);</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>    </span><span>LambdaQueryWrapper</span><span>&lt;</span><span>User</span><span><span>&gt;</span><span> queryWrapper </span></span><span><span>=</span><span> </span></span><span>new</span><span> </span><span>LambdaQueryWrapper</span><span><span>&lt;&gt;</span><span>()</span><span>;</span></span></div></div><div><div><div>17</div></div><div><span>    </span><span>queryWrapper</span><span>.</span><span>eq</span><span>(User</span><span>::</span><span>getPhone, phone);</span></div></div><div><div><div>18</div></div><div><span>    </span><span>User</span><span> user </span><span><span>=</span><span> </span></span><span>userService</span><span>.</span><span>getOne</span><span>(queryWrapper);</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>    </span><span>if</span><span><span> (user </span><span>==</span><span> </span></span><span>null</span><span>) {</span></div></div><div><div><div>21</div></div><div><span><span>        </span></span><span>user </span><span>=</span><span> </span><span>createUserWithPhone</span><span><span>(phone)</span><span>;</span></span></div></div><div><div><div>22</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>23</div></div><div>
</div></div><div><div><div>24</div></div><div><span>    </span><span>String</span><span> token </span><span><span>=</span><span> </span></span><span>UUID</span><span>.</span><span>randomUUID</span><span>().</span><span>toString</span><span>();</span></div></div><div><div><div>25</div></div><div><span>    </span><span>UserDTO</span><span> userDTO </span><span><span>=</span><span> </span></span><span>BeanUtil</span><span>.</span><span>copyProperties</span><span>(user, </span><span>UserDTO</span><span>.</span><span>class</span><span>);</span></div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div><span>    </span><span>Map</span><span>&lt;</span><span>String</span><span><span>,</span><span> </span></span><span>Object</span><span><span>&gt;</span><span> userMap </span></span><span><span>=</span><span> </span></span><span>BeanUtil</span><span>.</span><span>beanToMap</span><span>(userDTO, </span><span>new</span><span> </span><span>HashMap</span><span>&lt;&gt;(),</span></div></div><div><div><div>28</div></div><div><span>            </span><span>CopyOptions</span><span>.</span><span>create</span><span>()</span></div></div><div><div><div>29</div></div><div><span><span>                    </span></span><span>.</span><span>setIgnoreNullValue</span><span>(</span><span>true</span><span>)</span></div></div><div><div><div>30</div></div><div><span><span>                    </span></span><span>.</span><span>setFieldValueEditor</span><span>((fieldName, fieldValue) </span><span>-&gt;</span><span> </span><span>fieldValue</span><span>.</span><span>toString</span><span>()));</span></div></div><div><div><div>31</div></div><div>
</div></div><div><div><div>32</div></div><div><span>    </span><span>String</span><span> tokenKey </span><span><span>=</span><span> LOGIN_USER_KEY </span><span>+</span><span> token</span><span>;</span></span></div></div><div><div><div>33</div></div><div><span>    </span><span>stringRedisTemplate</span><span>.</span><span>opsForHash</span><span>().</span><span>putAll</span><span>(tokenKey, userMap);</span></div></div><div><div><div>34</div></div><div><span>    </span><span>stringRedisTemplate</span><span>.</span><span>expire</span><span>(tokenKey, LOGIN_USER_TTL, </span><span>TimeUnit</span><span>.</span><span>MINUTES</span><span>);</span></div></div><div><div><div>35</div></div><div><span>    </span><span>stringRedisTemplate</span><span>.</span><span>delete</span><span><span>(LOGIN_CODE_KEY </span><span>+</span><span> phone);</span></span></div></div><div><div><div>36</div></div><div>
</div></div><div><div><div>37</div></div><div><span>    </span><span>return</span><span> </span><span>Result</span><span>.</span><span>ok</span><span>(token);</span></div></div><div><div><div>38</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section><section><h3>1.7 登录状态刷新<a href="#17-登录状态刷新"><span>#</span></a></h3><p>如果只在需要拦截的路径上刷新 Token TTL，那么用户访问不需要拦截的路径时，Token 不会续期。因此需要两个拦截器：</p><ul>
<li><code>RefreshTokenInterceptor</code>：拦截所有路径，刷新 Token TTL。</li>
<li><code>LoginInterceptor</code>：只负责判断用户是否登录。</li>
</ul><p><code>RefreshTokenInterceptor</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>public</span><span> </span><span>class</span><span><span> </span><span>RefreshTokenInterceptor</span><span> </span></span><span>implements</span><span><span> </span><span>HandlerInterceptor</span><span> </span></span><span>{</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>    </span><span>private</span><span> </span><span>final</span><span> </span><span>StringRedisTemplate</span><span> stringRedisTemplate</span><span>;</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>    </span><span>public</span><span> </span><span>RefreshTokenInterceptor</span><span>(</span><span>StringRedisTemplate</span><span><span> </span><span>stringRedisTemplate</span><span>)</span><span> </span><span>{</span></span></div></div><div><div><div>6</div></div><div><span>        </span><span>this</span><span>.</span><span>stringRedisTemplate</span><span><span> </span><span>=</span><span> stringRedisTemplate;</span></span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>@</span><span>Override</span></div></div><div><div><div>10</div></div><div><span>    </span><span>public</span><span> </span><span>boolean</span><span> </span><span>preHandle</span><span>(</span><span>HttpServletRequest</span><span><span> </span><span>request</span><span>, </span></span><span>HttpServletResponse</span><span><span> </span><span>response</span><span>, </span></span><span>Object</span><span><span> </span><span>handler</span><span>)</span><span> </span><span>{</span></span></div></div><div><div><div>11</div></div><div><span>        </span><span>String</span><span><span> </span><span>token</span><span> </span></span><span><span>=</span><span> </span></span><span>request</span><span>.</span><span>getHeader</span><span>(</span><span>"authorization"</span><span>);</span></div></div><div><div><div>12</div></div><div><span>        </span><span>if</span><span> (</span><span>StrUtil</span><span>.</span><span>isBlank</span><span>(token)) {</span></div></div><div><div><div>13</div></div><div><span>            </span><span>return</span><span> </span><span>true</span><span>;</span></div></div><div><div><div>14</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>        </span><span>String</span><span><span> </span><span>key</span><span> </span></span><span><span>=</span><span> </span></span><span>RedisConstants</span><span>.</span><span>LOGIN_USER_KEY</span><span><span> </span><span>+</span><span> token;</span></span></div></div><div><div><div>17</div></div><div><span>        </span><span>Map</span><span>&lt;</span><span>Object</span><span>, </span><span>Object</span><span><span>&gt; </span><span>userMap</span><span> </span></span><span><span>=</span><span> </span></span><span>stringRedisTemplate</span><span>.</span><span>opsForHash</span><span>().</span><span>entries</span><span>(key);</span></div></div><div><div><div>18</div></div><div><span>        </span><span>if</span><span> (</span><span>userMap</span><span>.</span><span>isEmpty</span><span>()) {</span></div></div><div><div><div>19</div></div><div><span>            </span><span>return</span><span> </span><span>true</span><span>;</span></div></div><div><div><div>20</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>        </span><span>UserDTO</span><span><span> </span><span>userDTO</span><span> </span></span><span><span>=</span><span> </span></span><span>BeanUtil</span><span>.</span><span>fillBeanWithMap</span><span>(userMap, </span><span>new</span><span> </span><span>UserDTO</span><span>(), </span><span>false</span><span>);</span></div></div><div><div><div>23</div></div><div><span>        </span><span>UserHolder</span><span>.</span><span>saveUser</span><span>(userDTO);</span></div></div><div><div><div>24</div></div><div><span>        </span><span>stringRedisTemplate</span><span>.</span><span>expire</span><span>(key, </span><span>RedisConstants</span><span>.</span><span>LOGIN_USER_TTL</span><span>, </span><span>TimeUnit</span><span>.</span><span>MINUTES</span><span>);</span></div></div><div><div><div>25</div></div><div><span>        </span><span>return</span><span> </span><span>true</span><span>;</span></div></div><div><div><div>26</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>27</div></div><div>
</div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>@</span><span>Override</span></div></div><div><div><div>29</div></div><div><span>    </span><span>public</span><span> </span><span>void</span><span> </span><span>afterCompletion</span><span>(</span><span>HttpServletRequest</span><span><span> </span><span>request</span><span>, </span></span><span>HttpServletResponse</span><span><span> </span><span>response</span><span>, </span></span><span>Object</span><span><span> </span><span>handler</span><span>, </span></span><span>Exception</span><span><span> </span><span>ex</span><span>)</span><span> </span><span>{</span></span></div></div><div><div><div>30</div></div><div><span>        </span><span>UserHolder</span><span>.</span><span>removeUser</span><span>();</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>32</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>LoginInterceptor</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>public</span><span> </span><span>class</span><span><span> </span><span>LoginInterceptor</span><span> </span></span><span>implements</span><span><span> </span><span>HandlerInterceptor</span><span> </span></span><span>{</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>@</span><span>Override</span></div></div><div><div><div>4</div></div><div><span>    </span><span>public</span><span> </span><span>boolean</span><span> </span><span>preHandle</span><span>(</span><span>HttpServletRequest</span><span><span> </span><span>request</span><span>, </span></span><span>HttpServletResponse</span><span><span> </span><span>response</span><span>, </span></span><span>Object</span><span><span> </span><span>handler</span><span>)</span><span> </span><span>{</span></span></div></div><div><div><div>5</div></div><div><span>        </span><span>if</span><span> (</span><span>UserHolder</span><span>.</span><span>getUser</span><span><span>() </span><span>==</span><span> </span></span><span>null</span><span>) {</span></div></div><div><div><div>6</div></div><div><span>            </span><span>response</span><span>.</span><span>setStatus</span><span>(</span><span>401</span><span>);</span></div></div><div><div><div>7</div></div><div><span>            </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>8</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>9</div></div><div><span>        </span><span>return</span><span> </span><span>true</span><span>;</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>@</span><span>Override</span></div></div><div><div><div>13</div></div><div><span>    </span><span>public</span><span> </span><span>void</span><span> </span><span>afterCompletion</span><span>(</span><span>HttpServletRequest</span><span><span> </span><span>request</span><span>, </span></span><span>HttpServletResponse</span><span><span> </span><span>response</span><span>, </span></span><span>Object</span><span><span> </span><span>handler</span><span>, </span></span><span>Exception</span><span><span> </span><span>ex</span><span>)</span><span> </span><span>{</span></span></div></div><div><div><div>14</div></div><div><span>        </span><span>UserHolder</span><span>.</span><span>removeUser</span><span>();</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>16</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>配置类：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@</span><span>Configuration</span></div></div><div><div><div>2</div></div><div><span>public</span><span> </span><span>class</span><span><span> </span><span>MvcConfig</span><span> </span></span><span>implements</span><span><span> </span><span>WebMvcConfigurer</span><span> </span></span><span>{</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>@</span><span>Autowired</span></div></div><div><div><div>5</div></div><div><span>    </span><span>private</span><span> </span><span>StringRedisTemplate</span><span> stringRedisTemplate</span><span>;</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>@</span><span>Override</span></div></div><div><div><div>8</div></div><div><span>    </span><span>public</span><span> </span><span>void</span><span> </span><span>addInterceptors</span><span>(</span><span>InterceptorRegistry</span><span><span> </span><span>registry</span><span>)</span><span> </span><span>{</span></span></div></div><div><div><div>9</div></div><div><span>        </span><span>registry</span><span>.</span><span>addInterceptor</span><span>(</span><span>new</span><span> </span><span>LoginInterceptor</span><span>())</span></div></div><div><div><div>10</div></div><div><span><span>                </span></span><span>.</span><span>excludePathPatterns</span><span>(</span></div></div><div><div><div>11</div></div><div><span>                        </span><span>"/user/code"</span><span>,</span></div></div><div><div><div>12</div></div><div><span>                        </span><span>"/user/login"</span><span>,</span></div></div><div><div><div>13</div></div><div><span>                        </span><span>"/blog/hot"</span><span>,</span></div></div><div><div><div>14</div></div><div><span>                        </span><span>"/shop/**"</span><span>,</span></div></div><div><div><div>15</div></div><div><span>                        </span><span>"/shop-type/**"</span><span>,</span></div></div><div><div><div>16</div></div><div><span>                        </span><span>"/upload/**"</span><span>,</span></div></div><div><div><div>17</div></div><div><span>                        </span><span>"/voucher/**"</span></div></div><div><div><div>18</div></div><div><span><span>                </span></span><span>).</span><span>order</span><span>(</span><span>1</span><span>);</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>        </span><span>registry</span><span>.</span><span>addInterceptor</span><span>(</span><span>new</span><span> </span><span>RefreshTokenInterceptor</span><span>(stringRedisTemplate)).</span><span>order</span><span>(</span><span>0</span><span>);</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>22</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><hr /></section></section><section><h2>二、商户查询缓存<a href="#二商户查询缓存"><span>#</span></a></h2><section><h3>2.1 什么是缓存<a href="#21-什么是缓存"><span>#</span></a></h3><p>缓存是数据交换的缓冲区，通常用于降低数据库访问压力、提高读写效率、降低响应时间。</p><p>缓存的作用：</p><ul>
<li>降低后端负载。</li>
<li>提高读写效率，降低响应时间。</li>
</ul><p>缓存的成本：</p><ul>
<li>数据一致性成本。</li>
<li>代码维护成本。</li>
<li>运维成本。</li>
</ul></section><section><h3>2.2 添加商户缓存<a href="#22-添加商户缓存"><span>#</span></a></h3><p>查询商户时，先从 Redis 查询，未命中再查数据库，并将结果写入 Redis。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@</span><span>GetMapping</span><span>(</span><span>"/{id}"</span><span>)</span></div></div><div><div><div>2</div></div><div><span>public</span><span> </span><span>Result</span><span> </span><span>queryShopById</span><span><span>(</span><span>@</span></span><span>PathVariable</span><span>(</span><span>"id"</span><span>) </span><span>Long</span><span> id) {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>return</span><span> </span><span>shopService</span><span>.</span><span>queryById</span><span>(id);</span></div></div><div><div><div>4</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>Service 实现：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@</span><span>Override</span></div></div><div><div><div>2</div></div><div><span>public</span><span> </span><span>Result</span><span> </span><span>queryById</span><span>(</span><span>Long</span><span> id) {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>String</span><span> shopJson </span><span><span>=</span><span> </span></span><span>stringRedisTemplate</span><span>.</span><span>opsForValue</span><span>().</span><span>get</span><span><span>(CACHE_SHOP_KEY </span><span>+</span><span> id);</span></span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>    </span><span>if</span><span> (</span><span>StrUtil</span><span>.</span><span>isNotBlank</span><span><span>(shopJson)</span><span>) {</span></span></div></div><div><div><div>6</div></div><div><span>        </span><span>Shop</span><span> shop </span><span><span>=</span><span> </span></span><span>JSONUtil</span><span>.</span><span>toBean</span><span>(shopJson, </span><span>Shop</span><span>.</span><span>class</span><span>);</span></div></div><div><div><div>7</div></div><div><span>        </span><span>return</span><span> </span><span>Result</span><span>.</span><span>ok</span><span>(shop);</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>    </span><span>Shop</span><span> shop </span><span><span>=</span><span> </span></span><span>getById</span><span><span>(id)</span><span>;</span></span></div></div><div><div><div>11</div></div><div><span>    </span><span>if</span><span><span> (shop </span><span>==</span><span> </span></span><span>null</span><span>) {</span></div></div><div><div><div>12</div></div><div><span>        </span><span>return</span><span> </span><span>Result</span><span>.</span><span>fail</span><span>(</span><span>"店铺不存在！！"</span><span>);</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>    </span><span>String</span><span> jsonStr </span><span><span>=</span><span> </span></span><span>JSONUtil</span><span>.</span><span>toJsonStr</span><span>(shop);</span></div></div><div><div><div>16</div></div><div><span>    </span><span>stringRedisTemplate</span><span>.</span><span>opsForValue</span><span>().</span><span>set</span><span>(</span></div></div><div><div><div>17</div></div><div><span><span>            </span></span><span>CACHE_SHOP_KEY </span><span>+</span><span> id,</span></div></div><div><div><div>18</div></div><div><span><span>            </span></span><span>jsonStr,</span></div></div><div><div><div>19</div></div><div><span><span>            </span></span><span>CACHE_SHOP_TTL,</span></div></div><div><div><div>20</div></div><div><span>            </span><span>TimeUnit</span><span>.</span><span>MINUTES</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>);</span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span>    </span><span>return</span><span> </span><span>Result</span><span>.</span><span>ok</span><span>(shop);</span></div></div><div><div><div>24</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section><section><h3>2.3 缓存更新策略<a href="#23-缓存更新策略"><span>#</span></a></h3>

<table><thead><tr><th>策略</th><th>说明</th><th>一致性</th><th>维护成本</th></tr></thead><tbody><tr><td>内存淘汰</td><td>Redis 内存不足时自动淘汰</td><td>差</td><td>无</td></tr><tr><td>超时剔除</td><td>设置 TTL，到期删除</td><td>一般</td><td>低</td></tr><tr><td>主动更新</td><td>修改数据库时更新或删除缓存</td><td>好</td><td>高</td></tr></tbody></table><p>业务建议：</p><ul>
<li>低一致性需求：使用内存淘汰或超时剔除。</li>
<li>高一致性需求：主动更新，并以超时剔除兜底。</li>
</ul></section><section><h3>2.4 数据库与缓存一致性<a href="#24-数据库与缓存一致性"><span>#</span></a></h3><p>常见方案：</p><ol>
<li><strong>Cache Aside Pattern</strong>：人工编码，更新数据库后删除缓存。</li>
<li><strong>Read/Write Through Pattern</strong>：缓存与数据库整合为一个服务。</li>
<li><strong>Write Behind Caching Pattern</strong>：只操作缓存，异步写数据库。</li>
</ol><p>实际项目中推荐 <strong>Cache Aside Pattern</strong>：先操作数据库，再删除缓存。</p><p>实现商户缓存双写一致：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@</span><span>PutMapping</span></div></div><div><div><div>2</div></div><div><span>public</span><span> </span><span>Result</span><span> </span><span>updateShop</span><span><span>(</span><span>@</span></span><span>RequestBody</span><span> </span><span>Shop</span><span> shop) {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>return</span><span> </span><span>shopService</span><span>.</span><span>update</span><span>(shop);</span></div></div><div><div><div>4</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>Service：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@</span><span>Override</span></div></div><div><div><div>2</div></div><div><span>@</span><span>Transactional</span></div></div><div><div><div>3</div></div><div><span>public</span><span> </span><span>Result</span><span> </span><span>update</span><span>(</span><span>Shop</span><span> shop) {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>Long</span><span> id </span><span><span>=</span><span> </span></span><span>shop</span><span>.</span><span>getId</span><span>();</span></div></div><div><div><div>5</div></div><div><span>    </span><span>if</span><span><span> (id </span><span>==</span><span> </span></span><span>null</span><span>) {</span></div></div><div><div><div>6</div></div><div><span>        </span><span>return</span><span> </span><span>Result</span><span>.</span><span>fail</span><span>(</span><span>"店铺id不能为空"</span><span>);</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>8</div></div><div><span>    </span><span>updateById</span><span><span>(shop)</span><span>;</span></span></div></div><div><div><div>9</div></div><div><span>    </span><span>stringRedisTemplate</span><span>.</span><span>delete</span><span><span>(CACHE_SHOP_KEY </span><span>+</span><span> id);</span></span></div></div><div><div><div>10</div></div><div><span>    </span><span>return</span><span> </span><span>Result</span><span>.</span><span>ok</span><span>();</span></div></div><div><div><div>11</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>2.5 缓存穿透<a href="#25-缓存穿透"><span>#</span></a></h3><p>缓存穿透：请求的数据在缓存和数据库中都不存在，请求会频繁打到数据库。</p><p>解决方案：</p><ul>
<li>缓存空对象。</li>
<li>布隆过滤。</li>
<li>增强 id 复杂度。</li>
<li>做好数据格式校验。</li>
<li>加强用户权限校验。</li>
<li>热点参数限流。</li>
</ul><p>缓存空对象实现：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@</span><span>Override</span></div></div><div><div><div>2</div></div><div><span>public</span><span> </span><span>Result</span><span> </span><span>queryById</span><span>(</span><span>Long</span><span> id) {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>String</span><span> shopJson </span><span><span>=</span><span> </span></span><span>stringRedisTemplate</span><span>.</span><span>opsForValue</span><span>().</span><span>get</span><span><span>(CACHE_SHOP_KEY </span><span>+</span><span> id);</span></span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>    </span><span>if</span><span> (</span><span>StrUtil</span><span>.</span><span>isNotBlank</span><span><span>(shopJson)</span><span>) {</span></span></div></div><div><div><div>6</div></div><div><span>        </span><span>Shop</span><span> shop </span><span><span>=</span><span> </span></span><span>JSONUtil</span><span>.</span><span>toBean</span><span>(shopJson, </span><span>Shop</span><span>.</span><span>class</span><span>);</span></div></div><div><div><div>7</div></div><div><span>        </span><span>return</span><span> </span><span>Result</span><span>.</span><span>ok</span><span>(shop);</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>    </span><span>if</span><span><span> (shopJson </span><span>!=</span><span> </span></span><span>null</span><span>) {</span></div></div><div><div><div>11</div></div><div><span>        </span><span>return</span><span> </span><span>Result</span><span>.</span><span>fail</span><span>(</span><span>"店铺不存在！！"</span><span>);</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>    </span><span>Shop</span><span> shop </span><span><span>=</span><span> </span></span><span>getById</span><span><span>(id)</span><span>;</span></span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>    </span><span>if</span><span><span> (shop </span><span>==</span><span> </span></span><span>null</span><span>) {</span></div></div><div><div><div>17</div></div><div><span>        </span><span>stringRedisTemplate</span><span>.</span><span>opsForValue</span><span>().</span><span>set</span><span>(</span></div></div><div><div><div>18</div></div><div><span><span>                </span></span><span>CACHE_SHOP_KEY </span><span>+</span><span> id,</span></div></div><div><div><div>19</div></div><div><span>                </span><span>""</span><span>,</span></div></div><div><div><div>20</div></div><div><span><span>                </span></span><span>CACHE_NULL_TTL,</span></div></div><div><div><div>21</div></div><div><span>                </span><span>TimeUnit</span><span>.</span><span>MINUTES</span></div></div><div><div><div>22</div></div><div><span><span>        </span></span><span>);</span></div></div><div><div><div>23</div></div><div><span>        </span><span>return</span><span> </span><span>Result</span><span>.</span><span>fail</span><span>(</span><span>"店铺不存在！！"</span><span>);</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div><span>    </span><span>String</span><span> jsonStr </span><span><span>=</span><span> </span></span><span>JSONUtil</span><span>.</span><span>toJsonStr</span><span>(shop);</span></div></div><div><div><div>27</div></div><div><span>    </span><span>stringRedisTemplate</span><span>.</span><span>opsForValue</span><span>().</span><span>set</span><span>(</span></div></div><div><div><div>28</div></div><div><span><span>            </span></span><span>CACHE_SHOP_KEY </span><span>+</span><span> id,</span></div></div><div><div><div>29</div></div><div><span><span>            </span></span><span>jsonStr,</span></div></div><div><div><div>30</div></div><div><span><span>            </span></span><span>CACHE_SHOP_TTL,</span></div></div><div><div><div>31</div></div><div><span>            </span><span>TimeUnit</span><span>.</span><span>MINUTES</span></div></div><div><div><div>32</div></div><div><span><span>    </span></span><span>);</span></div></div><div><div><div>33</div></div><div>
</div></div><div><div><div>34</div></div><div><span>    </span><span>return</span><span> </span><span>Result</span><span>.</span><span>ok</span><span>(shop);</span></div></div><div><div><div>35</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section><section><h3>2.6 缓存雪崩<a href="#26-缓存雪崩"><span>#</span></a></h3><p>缓存雪崩：同一时间段大量缓存 Key 同时失效，或 Redis 服务宕机，导致大量请求到达数据库。</p><p>解决方案：</p><ul>
<li>给不同 Key 的 TTL 添加随机值。</li>
<li>利用 Redis 集群提高可用性。</li>
<li>给缓存业务添加降级限流策略。</li>
<li>给业务添加多级缓存。</li>
</ul></section><section><h3>2.7 缓存击穿<a href="#27-缓存击穿"><span>#</span></a></h3><p>缓存击穿：高并发访问且缓存重建业务复杂的 Key 突然失效，大量请求瞬间打到数据库。</p><p>解决方案：</p>

<table><thead><tr><th>方案</th><th>优点</th><th>缺点</th></tr></thead><tbody><tr><td>互斥锁</td><td>没有额外内存消耗、保证一致性、实现简单</td><td>线程需要等待、性能受影响、可能死锁</td></tr><tr><td>逻辑过期</td><td>线程无需等待、性能较好</td><td>不保证一致性、有额外内存消耗、实现复杂</td></tr></tbody></table><section><h4>互斥锁解决缓存击穿<a href="#互斥锁解决缓存击穿"><span>#</span></a></h4><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>private</span><span> </span><span>boolean</span><span> </span><span>tryLock</span><span>(</span><span>String</span><span> key) {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>Boolean</span><span> flag </span><span><span>=</span><span> </span></span><span>stringRedisTemplate</span><span>.</span><span>opsForValue</span><span>()</span></div></div><div><div><div>3</div></div><div><span><span>            </span></span><span>.</span><span>setIfAbsent</span><span>(key, </span><span>"1"</span><span>, </span><span>10</span><span>, </span><span>TimeUnit</span><span>.</span><span>SECONDS</span><span>);</span></div></div><div><div><div>4</div></div><div><span>    </span><span>return</span><span> </span><span>BooleanUtil</span><span>.</span><span>isTrue</span><span>(flag);</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>private</span><span> </span><span>void</span><span> </span><span>unlock</span><span>(</span><span>String</span><span> key) {</span></div></div><div><div><div>8</div></div><div><span>    </span><span>stringRedisTemplate</span><span>.</span><span>delete</span><span>(key);</span></div></div><div><div><div>9</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>查询逻辑：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@</span><span>Override</span></div></div><div><div><div>2</div></div><div><span>public</span><span> </span><span>Shop</span><span> </span><span>queryWithMutex</span><span>(</span><span>Long</span><span> id) {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>String</span><span> shopJson </span><span><span>=</span><span> </span></span><span>stringRedisTemplate</span><span>.</span><span>opsForValue</span><span>().</span><span>get</span><span><span>(CACHE_SHOP_KEY </span><span>+</span><span> id);</span></span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>    </span><span>if</span><span> (</span><span>StrUtil</span><span>.</span><span>isNotBlank</span><span><span>(shopJson)</span><span>) {</span></span></div></div><div><div><div>6</div></div><div><span>        </span><span>return</span><span> </span><span>JSONUtil</span><span>.</span><span>toBean</span><span>(shopJson, </span><span>Shop</span><span>.</span><span>class</span><span>);</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>    </span><span>if</span><span><span> (shopJson </span><span>!=</span><span> </span></span><span>null</span><span>) {</span></div></div><div><div><div>10</div></div><div><span>        </span><span>return</span><span> </span><span>null</span><span>;</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>    </span><span>String</span><span> lockKey </span><span><span>=</span><span> LOCK_SHOP_KEY </span><span>+</span><span> id</span><span>;</span></span></div></div><div><div><div>14</div></div><div><span>    </span><span>Shop</span><span> shop </span><span><span>=</span><span> </span></span><span>null</span><span>;</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>    </span><span>try</span><span> {</span></div></div><div><div><div>17</div></div><div><span>        </span><span>boolean</span><span> flag </span><span><span>=</span><span> </span></span><span>tryLock</span><span><span>(lockKey)</span><span>;</span></span></div></div><div><div><div>18</div></div><div><span>        </span><span>if</span><span><span> (</span><span>!</span><span>flag) {</span></span></div></div><div><div><div>19</div></div><div><span>            </span><span>Thread</span><span>.</span><span>sleep</span><span>(</span><span>50</span><span>);</span></div></div><div><div><div>20</div></div><div><span>            </span><span>return</span><span> </span><span>queryWithMutex</span><span><span>(id)</span><span>;</span></span></div></div><div><div><div>21</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span><span>        </span></span><span>shop </span><span>=</span><span> </span><span>getById</span><span><span>(id)</span><span>;</span></span></div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div><span>        </span><span>if</span><span><span> (shop </span><span>==</span><span> </span></span><span>null</span><span>) {</span></div></div><div><div><div>26</div></div><div><span>            </span><span>stringRedisTemplate</span><span>.</span><span>opsForValue</span><span>().</span><span>set</span><span>(</span></div></div><div><div><div>27</div></div><div><span><span>                    </span></span><span>CACHE_SHOP_KEY </span><span>+</span><span> id,</span></div></div><div><div><div>28</div></div><div><span>                    </span><span>""</span><span>,</span></div></div><div><div><div>29</div></div><div><span><span>                    </span></span><span>CACHE_NULL_TTL,</span></div></div><div><div><div>30</div></div><div><span>                    </span><span>TimeUnit</span><span>.</span><span>MINUTES</span></div></div><div><div><div>31</div></div><div><span><span>            </span></span><span>);</span></div></div><div><div><div>32</div></div><div><span>            </span><span>return</span><span> </span><span>null</span><span>;</span></div></div><div><div><div>33</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>34</div></div><div>
</div></div><div><div><div>35</div></div><div><span>        </span><span>String</span><span> jsonStr </span><span><span>=</span><span> </span></span><span>JSONUtil</span><span>.</span><span>toJsonStr</span><span>(shop);</span></div></div><div><div><div>36</div></div><div><span>        </span><span>stringRedisTemplate</span><span>.</span><span>opsForValue</span><span>().</span><span>set</span><span>(</span></div></div><div><div><div>37</div></div><div><span><span>                </span></span><span>CACHE_SHOP_KEY </span><span>+</span><span> id,</span></div></div><div><div><div>38</div></div><div><span><span>                </span></span><span>jsonStr,</span></div></div><div><div><div>39</div></div><div><span><span>                </span></span><span>CACHE_SHOP_TTL,</span></div></div><div><div><div>40</div></div><div><span>                </span><span>TimeUnit</span><span>.</span><span>MINUTES</span></div></div><div><div><div>41</div></div><div><span><span>        </span></span><span>);</span></div></div><div><div><div>42</div></div><div><span><span>    </span></span><span>} </span><span>catch</span><span> (</span><span>InterruptedException</span><span><span> </span><span>e</span><span>) {</span></span></div></div><div><div><div>43</div></div><div><span>        </span><span>throw</span><span> </span><span>new</span><span> </span><span>RuntimeException</span><span><span>(e)</span><span>;</span></span></div></div><div><div><div>44</div></div><div><span><span>    </span></span><span>} </span><span>finally</span><span> {</span></div></div><div><div><div>45</div></div><div><span>        </span><span>unlock</span><span><span>(lockKey)</span><span>;</span></span></div></div><div><div><div>46</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>47</div></div><div>
</div></div><div><div><div>48</div></div><div><span>    </span><span>return</span><span><span> shop</span><span>;</span></span></div></div><div><div><div>49</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section><section><h4>逻辑过期解决缓存击穿<a href="#逻辑过期解决缓存击穿"><span>#</span></a></h4><p>封装逻辑过期数据：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@</span><span>Data</span></div></div><div><div><div>2</div></div><div><span>public</span><span> </span><span>class</span><span><span> </span><span>RedisData</span><span>&lt;</span><span>T</span><span>&gt;</span><span> </span></span><span>{</span></div></div><div><div><div>3</div></div><div><span>    </span><span>private</span><span> </span><span>LocalDateTime</span><span> expireTime</span><span>;</span></div></div><div><div><div>4</div></div><div><span>    </span><span>private</span><span> </span><span>T</span><span> data</span><span>;</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>写入逻辑过期数据：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>public</span><span> </span><span>void</span><span> </span><span>saveShop2Redis</span><span>(</span><span>Long</span><span><span> id</span><span>,</span><span> </span></span><span>Long</span><span> expireSeconds) {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>Shop</span><span> shop </span><span><span>=</span><span> </span></span><span>getById</span><span><span>(id)</span><span>;</span></span></div></div><div><div><div>3</div></div><div><span>    </span><span>RedisData</span><span>&lt;</span><span>Shop</span><span><span>&gt;</span><span> redisData </span></span><span><span>=</span><span> </span></span><span>new</span><span> </span><span>RedisData</span><span><span>&lt;&gt;</span><span>()</span><span>;</span></span></div></div><div><div><div>4</div></div><div><span>    </span><span>redisData</span><span>.</span><span>setData</span><span>(shop);</span></div></div><div><div><div>5</div></div><div><span>    </span><span>redisData</span><span>.</span><span>setExpireTime</span><span>(</span><span>LocalDateTime</span><span>.</span><span>now</span><span>().</span><span>plusSeconds</span><span>(expireSeconds));</span></div></div><div><div><div>6</div></div><div><span>    </span><span>stringRedisTemplate</span><span>.</span><span>opsForValue</span><span>().</span><span>set</span><span>(</span></div></div><div><div><div>7</div></div><div><span><span>            </span></span><span>CACHE_SHOP_KEY </span><span>+</span><span> id,</span></div></div><div><div><div>8</div></div><div><span>            </span><span>JSONUtil</span><span>.</span><span>toJsonStr</span><span>(redisData)</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>);</span></div></div><div><div><div>10</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>查询逻辑：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>private</span><span> </span><span>static</span><span> </span><span>final</span><span> </span><span>ExecutorService</span><span> CACHE_REBUILD_EXECUTOR </span><span>=</span></div></div><div><div><div>2</div></div><div><span>        </span><span>Executors</span><span>.</span><span>newFixedThreadPool</span><span>(</span><span>10</span><span>);</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>@</span><span>Override</span></div></div><div><div><div>5</div></div><div><span>public</span><span> </span><span>Shop</span><span> </span><span>queryWithLogicalExpire</span><span>(</span><span>Long</span><span> id) {</span></div></div><div><div><div>6</div></div><div><span>    </span><span>String</span><span> json </span><span><span>=</span><span> </span></span><span>stringRedisTemplate</span><span>.</span><span>opsForValue</span><span>().</span><span>get</span><span><span>(CACHE_SHOP_KEY </span><span>+</span><span> id);</span></span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>    </span><span>if</span><span> (</span><span>StrUtil</span><span>.</span><span>isBlank</span><span><span>(json)</span><span>) {</span></span></div></div><div><div><div>9</div></div><div><span>        </span><span>return</span><span> </span><span>null</span><span>;</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>    </span><span>RedisData</span><span> redisData </span><span><span>=</span><span> </span></span><span>JSONUtil</span><span>.</span><span>toBean</span><span>(json, </span><span>RedisData</span><span>.</span><span>class</span><span>);</span></div></div><div><div><div>13</div></div><div><span>    </span><span>JSONObject</span><span> shopJson </span><span><span>=</span><span> (JSONObject) </span></span><span>redisData</span><span>.</span><span>getData</span><span>();</span></div></div><div><div><div>14</div></div><div><span>    </span><span>Shop</span><span> shop </span><span><span>=</span><span> </span></span><span>JSONUtil</span><span>.</span><span>toBean</span><span>(shopJson, </span><span>Shop</span><span>.</span><span>class</span><span>);</span></div></div><div><div><div>15</div></div><div><span>    </span><span>LocalDateTime</span><span> expireTime </span><span><span>=</span><span> </span></span><span>redisData</span><span>.</span><span>getExpireTime</span><span>();</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>    </span><span>if</span><span> (</span><span>expireTime</span><span>.</span><span>isAfter</span><span>(</span><span>LocalDateTime</span><span>.</span><span>now</span><span><span>())</span><span>) {</span></span></div></div><div><div><div>18</div></div><div><span>        </span><span>return</span><span><span> shop</span><span>;</span></span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span>    </span><span>String</span><span> lockKey </span><span><span>=</span><span> LOCK_SHOP_KEY </span><span>+</span><span> id</span><span>;</span></span></div></div><div><div><div>22</div></div><div><span>    </span><span>boolean</span><span> flag </span><span><span>=</span><span> </span></span><span>tryLock</span><span><span>(lockKey)</span><span>;</span></span></div></div><div><div><div>23</div></div><div>
</div></div><div><div><div>24</div></div><div><span>    </span><span>if</span><span> (flag) {</span></div></div><div><div><div>25</div></div><div><span>        </span><span>CACHE_REBUILD_EXECUTOR</span><span>.</span><span>submit</span><span>(() </span><span>-&gt;</span><span> {</span></div></div><div><div><div>26</div></div><div><span>            </span><span>try</span><span> {</span></div></div><div><div><div>27</div></div><div><span>                </span><span>saveShop2Redis</span><span>(id, </span><span>20L</span><span>);</span></div></div><div><div><div>28</div></div><div><span><span>            </span></span><span>} </span><span>catch</span><span> (</span><span>Exception</span><span><span> </span><span>e</span><span>) {</span></span></div></div><div><div><div>29</div></div><div><span>                </span><span>throw</span><span> </span><span>new</span><span> </span><span>RuntimeException</span><span>(e);</span></div></div><div><div><div>30</div></div><div><span><span>            </span></span><span>} </span><span>finally</span><span> {</span></div></div><div><div><div>31</div></div><div><span>                </span><span>unlock</span><span>(lockKey);</span></div></div><div><div><div>32</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>33</div></div><div><span><span>        </span></span><span>});</span></div></div><div><div><div>34</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>35</div></div><div>
</div></div><div><div><div>36</div></div><div><span>    </span><span>return</span><span><span> shop</span><span>;</span></span></div></div><div><div><div>37</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section></section><section><h3>2.8 封装缓存工具类<a href="#28-封装缓存工具类"><span>#</span></a></h3><p>创建 <code>CacheClient</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@</span><span>Slf4j</span></div></div><div><div><div>2</div></div><div><span>@</span><span>Component</span></div></div><div><div><div>3</div></div><div><span>public</span><span> </span><span>class</span><span><span> </span><span>CacheClient</span><span> </span></span><span>{</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>    </span><span>private</span><span> </span><span>final</span><span> </span><span>StringRedisTemplate</span><span> stringRedisTemplate</span><span>;</span></div></div><div><div><div>6</div></div><div><span>    </span><span>private</span><span> </span><span>static</span><span> </span><span>final</span><span> </span><span>ExecutorService</span><span> CACHE_REBUILD_EXECUTOR </span><span>=</span></div></div><div><div><div>7</div></div><div><span>            </span><span>Executors</span><span>.</span><span>newFixedThreadPool</span><span>(</span><span>10</span><span>);</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>    </span><span>public</span><span> </span><span>CacheClient</span><span>(</span><span>StringRedisTemplate</span><span><span> </span><span>stringRedisTemplate</span><span>)</span><span> </span><span>{</span></span></div></div><div><div><div>10</div></div><div><span>        </span><span>this</span><span>.</span><span>stringRedisTemplate</span><span><span> </span><span>=</span><span> stringRedisTemplate;</span></span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>    </span><span>public</span><span> </span><span>void</span><span> </span><span>set</span><span>(</span><span>String</span><span><span> </span><span>key</span><span>, </span></span><span>Object</span><span><span> </span><span>value</span><span>, </span></span><span>Long</span><span><span> </span><span>time</span><span>, </span></span><span>TimeUnit</span><span><span> </span><span>timeUnit</span><span>)</span><span> </span><span>{</span></span></div></div><div><div><div>14</div></div><div><span>        </span><span>stringRedisTemplate</span><span>.</span><span>opsForValue</span><span>().</span><span>set</span><span>(</span></div></div><div><div><div>15</div></div><div><span><span>                </span></span><span>key,</span></div></div><div><div><div>16</div></div><div><span>                </span><span>JSONUtil</span><span>.</span><span>toJsonStr</span><span>(value),</span></div></div><div><div><div>17</div></div><div><span><span>                </span></span><span>time,</span></div></div><div><div><div>18</div></div><div><span><span>                </span></span><span>timeUnit</span></div></div><div><div><div>19</div></div><div><span><span>        </span></span><span>);</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>    </span><span>public</span><span> </span><span>void</span><span> </span><span>setWithLogicExpire</span><span>(</span><span>String</span><span><span> </span><span>key</span><span>, </span></span><span>Object</span><span><span> </span><span>value</span><span>, </span></span><span>Long</span><span><span> </span><span>time</span><span>, </span></span><span>TimeUnit</span><span><span> </span><span>timeUnit</span><span>)</span><span> </span><span>{</span></span></div></div><div><div><div>23</div></div><div><span>        </span><span>RedisData</span><span>&lt;</span><span>Object</span><span><span>&gt; </span><span>redisData</span><span> </span></span><span><span>=</span><span> </span></span><span>new</span><span> </span><span>RedisData</span><span>&lt;&gt;();</span></div></div><div><div><div>24</div></div><div><span>        </span><span>redisData</span><span>.</span><span>setData</span><span>(value);</span></div></div><div><div><div>25</div></div><div><span>        </span><span>redisData</span><span>.</span><span>setExpireTime</span><span>(</span><span>LocalDateTime</span><span>.</span><span>now</span><span>().</span><span>plusSeconds</span><span>(</span><span>timeUnit</span><span>.</span><span>toSeconds</span><span>(time)));</span></div></div><div><div><div>26</div></div><div><span>        </span><span>stringRedisTemplate</span><span>.</span><span>opsForValue</span><span>().</span><span>set</span><span>(key, </span><span>JSONUtil</span><span>.</span><span>toJsonStr</span><span>(redisData));</span></div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>28</div></div><div>
</div></div><div><div><div>29</div></div><div><span>    </span><span>public</span><span><span> </span><span>&lt;</span></span><span>R</span><span><span>,</span><span> </span></span><span>ID</span><span><span>&gt;</span><span> </span></span><span>R</span><span> </span><span>queryWithPassThrough</span><span>(</span></div></div><div><div><div>30</div></div><div><span>            </span><span>String</span><span><span> </span><span>keyPrefix</span><span>,</span></span></div></div><div><div><div>31</div></div><div><span>            </span><span>ID</span><span><span> </span><span>id</span><span>,</span></span></div></div><div><div><div>32</div></div><div><span>            </span><span>Class</span><span>&lt;</span><span>R</span><span><span>&gt; </span><span>type</span><span>,</span></span></div></div><div><div><div>33</div></div><div><span>            </span><span>Function</span><span>&lt;</span><span>ID</span><span>, </span><span>R</span><span><span>&gt; </span><span>dbFallback</span><span>,</span></span></div></div><div><div><div>34</div></div><div><span>            </span><span>Long</span><span><span> </span><span>time</span><span>,</span></span></div></div><div><div><div>35</div></div><div><span>            </span><span>TimeUnit</span><span><span> </span><span>timeUnit</span></span></div></div><div><div><div>36</div></div><div><span><span>    </span></span><span>)</span><span> </span><span>{</span></div></div><div><div><div>37</div></div><div><span>        </span><span>String</span><span><span> </span><span>key</span><span> </span></span><span><span>=</span><span> keyPrefix </span><span>+</span><span> id;</span></span></div></div><div><div><div>38</div></div><div><span>        </span><span>String</span><span><span> </span><span>json</span><span> </span></span><span><span>=</span><span> </span></span><span>stringRedisTemplate</span><span>.</span><span>opsForValue</span><span>().</span><span>get</span><span>(key);</span></div></div><div><div><div>39</div></div><div>
</div></div><div><div><div>40</div></div><div><span>        </span><span>if</span><span> (</span><span>StrUtil</span><span>.</span><span>isNotBlank</span><span>(json)) {</span></div></div><div><div><div>41</div></div><div><span>            </span><span>return</span><span> </span><span>JSONUtil</span><span>.</span><span>toBean</span><span>(json, type);</span></div></div><div><div><div>42</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>43</div></div><div>
</div></div><div><div><div>44</div></div><div><span>        </span><span>if</span><span><span> (json </span><span>!=</span><span> </span></span><span>null</span><span>) {</span></div></div><div><div><div>45</div></div><div><span>            </span><span>return</span><span> </span><span>null</span><span>;</span></div></div><div><div><div>46</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>47</div></div><div>
</div></div><div><div><div>48</div></div><div><span>        </span><span>R</span><span><span> </span><span>r</span><span> </span></span><span><span>=</span><span> </span></span><span>dbFallback</span><span>.</span><span>apply</span><span>(id);</span></div></div><div><div><div>49</div></div><div>
</div></div><div><div><div>50</div></div><div><span>        </span><span>if</span><span><span> (r </span><span>==</span><span> </span></span><span>null</span><span>) {</span></div></div><div><div><div>51</div></div><div><span>            </span><span>stringRedisTemplate</span><span>.</span><span>opsForValue</span><span>().</span><span>set</span><span>(</span></div></div><div><div><div>52</div></div><div><span><span>                    </span></span><span>key,</span></div></div><div><div><div>53</div></div><div><span>                    </span><span>""</span><span>,</span></div></div><div><div><div>54</div></div><div><span><span>                    </span></span><span>CACHE_NULL_TTL,</span></div></div><div><div><div>55</div></div><div><span>                    </span><span>TimeUnit</span><span>.</span><span>MINUTES</span></div></div><div><div><div>56</div></div><div><span><span>            </span></span><span>);</span></div></div><div><div><div>57</div></div><div><span>            </span><span>return</span><span> </span><span>null</span><span>;</span></div></div><div><div><div>58</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>59</div></div><div>
</div></div><div><div><div>60</div></div><div><span>        </span><span>this</span><span>.</span><span>set</span><span>(key, r, time, timeUnit);</span></div></div><div><div><div>61</div></div><div><span>        </span><span>return</span><span> r;</span></div></div><div><div><div>62</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>63</div></div><div>
</div></div><div><div><div>64</div></div><div><span>    </span><span>public</span><span><span> </span><span>&lt;</span></span><span>R</span><span><span>,</span><span> </span></span><span>ID</span><span><span>&gt;</span><span> </span></span><span>R</span><span> </span><span>queryWithMutex</span><span>(</span></div></div><div><div><div>65</div></div><div><span>            </span><span>String</span><span><span> </span><span>keyPrefix</span><span>,</span></span></div></div><div><div><div>66</div></div><div><span>            </span><span>ID</span><span><span> </span><span>id</span><span>,</span></span></div></div><div><div><div>67</div></div><div><span>            </span><span>Class</span><span>&lt;</span><span>R</span><span><span>&gt; </span><span>type</span><span>,</span></span></div></div><div><div><div>68</div></div><div><span>            </span><span>Function</span><span>&lt;</span><span>ID</span><span>, </span><span>R</span><span><span>&gt; </span><span>dbFallback</span><span>,</span></span></div></div><div><div><div>69</div></div><div><span>            </span><span>Long</span><span><span> </span><span>time</span><span>,</span></span></div></div><div><div><div>70</div></div><div><span>            </span><span>TimeUnit</span><span><span> </span><span>timeUnit</span></span></div></div><div><div><div>71</div></div><div><span><span>    </span></span><span>)</span><span> </span><span>{</span></div></div><div><div><div>72</div></div><div><span>        </span><span>String</span><span><span> </span><span>key</span><span> </span></span><span><span>=</span><span> keyPrefix </span><span>+</span><span> id;</span></span></div></div><div><div><div>73</div></div><div><span>        </span><span>String</span><span><span> </span><span>json</span><span> </span></span><span><span>=</span><span> </span></span><span>stringRedisTemplate</span><span>.</span><span>opsForValue</span><span>().</span><span>get</span><span>(key);</span></div></div><div><div><div>74</div></div><div>
</div></div><div><div><div>75</div></div><div><span>        </span><span>if</span><span> (</span><span>StrUtil</span><span>.</span><span>isNotBlank</span><span>(json)) {</span></div></div><div><div><div>76</div></div><div><span>            </span><span>return</span><span> </span><span>JSONUtil</span><span>.</span><span>toBean</span><span>(json, type);</span></div></div><div><div><div>77</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>78</div></div><div>
</div></div><div><div><div>79</div></div><div><span>        </span><span>if</span><span><span> (json </span><span>!=</span><span> </span></span><span>null</span><span>) {</span></div></div><div><div><div>80</div></div><div><span>            </span><span>return</span><span> </span><span>null</span><span>;</span></div></div><div><div><div>81</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>82</div></div><div>
</div></div><div><div><div>83</div></div><div><span>        </span><span>R</span><span><span> </span><span>r</span><span> </span></span><span><span>=</span><span> </span></span><span>null</span><span>;</span></div></div><div><div><div>84</div></div><div><span>        </span><span>String</span><span><span> </span><span>lockKey</span><span> </span></span><span><span>=</span><span> LOCK_SHOP_KEY </span><span>+</span><span> id;</span></span></div></div><div><div><div>85</div></div><div>
</div></div><div><div><div>86</div></div><div><span>        </span><span>try</span><span> {</span></div></div><div><div><div>87</div></div><div><span>            </span><span>boolean</span><span><span> </span><span>flag</span><span> </span></span><span><span>=</span><span> </span></span><span>tryLock</span><span>(lockKey);</span></div></div><div><div><div>88</div></div><div><span>            </span><span>if</span><span><span> (</span><span>!</span><span>flag) {</span></span></div></div><div><div><div>89</div></div><div><span>                </span><span>Thread</span><span>.</span><span>sleep</span><span>(</span><span>50</span><span>);</span></div></div><div><div><div>90</div></div><div><span>                </span><span>return</span><span> </span><span>queryWithMutex</span><span>(keyPrefix, id, type, dbFallback, time, timeUnit);</span></div></div><div><div><div>91</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>92</div></div><div>
</div></div><div><div><div>93</div></div><div><span><span>            </span></span><span>r </span><span>=</span><span> </span><span>dbFallback</span><span>.</span><span>apply</span><span>(id);</span></div></div><div><div><div>94</div></div><div>
</div></div><div><div><div>95</div></div><div><span>            </span><span>if</span><span><span> (r </span><span>==</span><span> </span></span><span>null</span><span>) {</span></div></div><div><div><div>96</div></div><div><span>                </span><span>stringRedisTemplate</span><span>.</span><span>opsForValue</span><span>().</span><span>set</span><span>(</span></div></div><div><div><div>97</div></div><div><span><span>                        </span></span><span>key,</span></div></div><div><div><div>98</div></div><div><span>                        </span><span>""</span><span>,</span></div></div><div><div><div>99</div></div><div><span><span>                        </span></span><span>CACHE_NULL_TTL,</span></div></div><div><div><div>100</div></div><div><span>                        </span><span>TimeUnit</span><span>.</span><span>MINUTES</span></div></div><div><div><div>101</div></div><div><span><span>                </span></span><span>);</span></div></div><div><div><div>102</div></div><div><span>                </span><span>return</span><span> </span><span>null</span><span>;</span></div></div><div><div><div>103</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>104</div></div><div>
</div></div><div><div><div>105</div></div><div><span>            </span><span>this</span><span>.</span><span>set</span><span>(key, r, time, timeUnit);</span></div></div><div><div><div>106</div></div><div><span><span>        </span></span><span>} </span><span>catch</span><span> (</span><span>InterruptedException</span><span><span> </span><span>e</span><span>) {</span></span></div></div><div><div><div>107</div></div><div><span>            </span><span>throw</span><span> </span><span>new</span><span> </span><span>RuntimeException</span><span>(e);</span></div></div><div><div><div>108</div></div><div><span><span>        </span></span><span>} </span><span>finally</span><span> {</span></div></div><div><div><div>109</div></div><div><span>            </span><span>unlock</span><span>(lockKey);</span></div></div><div><div><div>110</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>111</div></div><div>
</div></div><div><div><div>112</div></div><div><span>        </span><span>return</span><span> r;</span></div></div><div><div><div>113</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>114</div></div><div>
</div></div><div><div><div>115</div></div><div><span>    </span><span>public</span><span><span> </span><span>&lt;</span></span><span>R</span><span><span>,</span><span> </span></span><span>ID</span><span><span>&gt;</span><span> </span></span><span>R</span><span> </span><span>queryWithLogicalExpire</span><span>(</span></div></div><div><div><div>116</div></div><div><span>            </span><span>String</span><span><span> </span><span>keyPrefix</span><span>,</span></span></div></div><div><div><div>117</div></div><div><span>            </span><span>ID</span><span><span> </span><span>id</span><span>,</span></span></div></div><div><div><div>118</div></div><div><span>            </span><span>Class</span><span>&lt;</span><span>R</span><span><span>&gt; </span><span>type</span><span>,</span></span></div></div><div><div><div>119</div></div><div><span>            </span><span>Function</span><span>&lt;</span><span>ID</span><span>, </span><span>R</span><span><span>&gt; </span><span>dbFallback</span><span>,</span></span></div></div><div><div><div>120</div></div><div><span>            </span><span>Long</span><span><span> </span><span>time</span><span>,</span></span></div></div><div><div><div>121</div></div><div><span>            </span><span>TimeUnit</span><span><span> </span><span>timeUnit</span></span></div></div><div><div><div>122</div></div><div><span><span>    </span></span><span>)</span><span> </span><span>{</span></div></div><div><div><div>123</div></div><div><span>        </span><span>String</span><span><span> </span><span>key</span><span> </span></span><span><span>=</span><span> keyPrefix </span><span>+</span><span> id;</span></span></div></div><div><div><div>124</div></div><div><span>        </span><span>String</span><span><span> </span><span>json</span><span> </span></span><span><span>=</span><span> </span></span><span>stringRedisTemplate</span><span>.</span><span>opsForValue</span><span>().</span><span>get</span><span>(key);</span></div></div><div><div><div>125</div></div><div>
</div></div><div><div><div>126</div></div><div><span>        </span><span>if</span><span> (</span><span>StrUtil</span><span>.</span><span>isBlank</span><span>(json)) {</span></div></div><div><div><div>127</div></div><div><span>            </span><span>return</span><span> </span><span>null</span><span>;</span></div></div><div><div><div>128</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>129</div></div><div>
</div></div><div><div><div>130</div></div><div><span>        </span><span>RedisData</span><span><span> </span><span>redisData</span><span> </span></span><span><span>=</span><span> </span></span><span>JSONUtil</span><span>.</span><span>toBean</span><span>(json, </span><span>RedisData</span><span>.</span><span>class</span><span>);</span></div></div><div><div><div>131</div></div><div><span>        </span><span>R</span><span><span> </span><span>r</span><span> </span></span><span><span>=</span><span> </span></span><span>JSONUtil</span><span>.</span><span>toBean</span><span>((JSONObject) </span><span>redisData</span><span>.</span><span>getData</span><span>(), type);</span></div></div><div><div><div>132</div></div><div><span>        </span><span>LocalDateTime</span><span><span> </span><span>expireTime</span><span> </span></span><span><span>=</span><span> </span></span><span>redisData</span><span>.</span><span>getExpireTime</span><span>();</span></div></div><div><div><div>133</div></div><div>
</div></div><div><div><div>134</div></div><div><span>        </span><span>if</span><span> (</span><span>expireTime</span><span>.</span><span>isAfter</span><span>(</span><span>LocalDateTime</span><span>.</span><span>now</span><span>())) {</span></div></div><div><div><div>135</div></div><div><span>            </span><span>return</span><span> r;</span></div></div><div><div><div>136</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>137</div></div><div>
</div></div><div><div><div>138</div></div><div><span>        </span><span>String</span><span><span> </span><span>lockKey</span><span> </span></span><span><span>=</span><span> LOCK_SHOP_KEY </span><span>+</span><span> id;</span></span></div></div><div><div><div>139</div></div><div><span>        </span><span>boolean</span><span><span> </span><span>flag</span><span> </span></span><span><span>=</span><span> </span></span><span>tryLock</span><span>(lockKey);</span></div></div><div><div><div>140</div></div><div>
</div></div><div><div><div>141</div></div><div><span>        </span><span>if</span><span> (flag) {</span></div></div><div><div><div>142</div></div><div><span>            </span><span>CACHE_REBUILD_EXECUTOR</span><span>.</span><span>submit</span><span>(() </span><span>-&gt;</span><span> {</span></div></div><div><div><div>143</div></div><div><span>                </span><span>try</span><span> {</span></div></div><div><div><div>144</div></div><div><span>                    </span><span>R</span><span><span> </span><span>tmp</span><span> </span></span><span><span>=</span><span> </span></span><span>dbFallback</span><span>.</span><span>apply</span><span>(id);</span></div></div><div><div><div>145</div></div><div><span>                    </span><span>this</span><span>.</span><span>setWithLogicExpire</span><span>(key, tmp, time, timeUnit);</span></div></div><div><div><div>146</div></div><div><span><span>                </span></span><span>} </span><span>catch</span><span> (</span><span>Exception</span><span><span> </span><span>e</span><span>) {</span></span></div></div><div><div><div>147</div></div><div><span>                    </span><span>throw</span><span> </span><span>new</span><span> </span><span>RuntimeException</span><span>(e);</span></div></div><div><div><div>148</div></div><div><span><span>                </span></span><span>} </span><span>finally</span><span> {</span></div></div><div><div><div>149</div></div><div><span>                    </span><span>unlock</span><span>(lockKey);</span></div></div><div><div><div>150</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>151</div></div><div><span><span>            </span></span><span>});</span></div></div><div><div><div>152</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>153</div></div><div>
</div></div><div><div><div>154</div></div><div><span>        </span><span>return</span><span> r;</span></div></div><div><div><div>155</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>156</div></div><div>
</div></div><div><div><div>157</div></div><div><span>    </span><span>private</span><span> </span><span>boolean</span><span> </span><span>tryLock</span><span>(</span><span>String</span><span><span> </span><span>key</span><span>)</span><span> </span><span>{</span></span></div></div><div><div><div>158</div></div><div><span>        </span><span>Boolean</span><span><span> </span><span>flag</span><span> </span></span><span><span>=</span><span> </span></span><span>stringRedisTemplate</span><span>.</span><span>opsForValue</span><span>()</span></div></div><div><div><div>159</div></div><div><span><span>                </span></span><span>.</span><span>setIfAbsent</span><span>(key, </span><span>"1"</span><span>, </span><span>10</span><span>, </span><span>TimeUnit</span><span>.</span><span>SECONDS</span><span>);</span></div></div><div><div><div>160</div></div><div><span>        </span><span>return</span><span> </span><span>BooleanUtil</span><span>.</span><span>isTrue</span><span>(flag);</span></div></div><div><div><div>161</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>162</div></div><div>
</div></div><div><div><div>163</div></div><div><span>    </span><span>private</span><span> </span><span>void</span><span> </span><span>unlock</span><span>(</span><span>String</span><span><span> </span><span>key</span><span>)</span><span> </span><span>{</span></span></div></div><div><div><div>164</div></div><div><span>        </span><span>stringRedisTemplate</span><span>.</span><span>delete</span><span>(key);</span></div></div><div><div><div>165</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>166</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><hr /></section></section><section><h2>三、优惠券秒杀<a href="#三优惠券秒杀"><span>#</span></a></h2><section><h3>3.1 Redis 实现全局唯一 ID<a href="#31-redis-实现全局唯一-id"><span>#</span></a></h3><p>数据库自增 ID 存在两个问题：</p><ul>
<li>ID 规律太明显，容易泄露业务信息。</li>
<li>单表数据量受限，分库分表后 ID 不能重复。</li>
</ul><p>全局 ID 生成器需要满足：</p><ul>
<li>唯一性</li>
<li>高可用</li>
<li>高性能</li>
<li>递增性</li>
<li>安全性</li>
</ul><p>ID 结构：</p>

<table><thead><tr><th>部分</th><th>位数</th><th>说明</th></tr></thead><tbody><tr><td>符号位</td><td>1 bit</td><td>永远为 0</td></tr><tr><td>时间戳</td><td>31 bit</td><td>以秒为单位，可用约 69 年</td></tr><tr><td>序列号</td><td>32 bit</td><td>秒内计数器</td></tr></tbody></table><p>实现：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@</span><span>Component</span></div></div><div><div><div>2</div></div><div><span>public</span><span> </span><span>class</span><span><span> </span><span>RedisIdWorker</span><span> </span></span><span>{</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>@</span><span>Autowired</span></div></div><div><div><div>5</div></div><div><span>    </span><span>private</span><span> </span><span>StringRedisTemplate</span><span> stringRedisTemplate</span><span>;</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>    </span><span>public</span><span> </span><span>static</span><span> </span><span>final</span><span> </span><span>Long</span><span> BEGIN_TIMESTAMP </span><span><span>=</span><span> </span></span><span>1640995200L</span><span>;</span></div></div><div><div><div>8</div></div><div><span>    </span><span>public</span><span> </span><span>static</span><span> </span><span>final</span><span> </span><span>Long</span><span> COUNT_BIT </span><span><span>=</span><span> </span></span><span>32L</span><span>;</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>    </span><span>public</span><span> </span><span>long</span><span> </span><span>nextId</span><span>(</span><span>String</span><span><span> </span><span>keyPrefix</span><span>)</span><span> </span><span>{</span></span></div></div><div><div><div>11</div></div><div><span>        </span><span>LocalDateTime</span><span><span> </span><span>now</span><span> </span></span><span><span>=</span><span> </span></span><span>LocalDateTime</span><span>.</span><span>now</span><span>();</span></div></div><div><div><div>12</div></div><div><span>        </span><span>long</span><span><span> </span><span>currentSecond</span><span> </span></span><span><span>=</span><span> </span></span><span>now</span><span>.</span><span>toEpochSecond</span><span>(</span><span>ZoneOffset</span><span>.</span><span>UTC</span><span>);</span></div></div><div><div><div>13</div></div><div><span>        </span><span>long</span><span><span> </span><span>timeStamp</span><span> </span></span><span><span>=</span><span> currentSecond </span><span>-</span><span> BEGIN_TIMESTAMP;</span></span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>        </span><span>String</span><span><span> </span><span>date</span><span> </span></span><span><span>=</span><span> </span></span><span>now</span><span>.</span><span>format</span><span>(</span><span>DateTimeFormatter</span><span>.</span><span>ofPattern</span><span>(</span><span>"yyyy:MM:dd"</span><span>));</span></div></div><div><div><div>16</div></div><div><span>        </span><span>long</span><span><span> </span><span>count</span><span> </span></span><span><span>=</span><span> </span></span><span>stringRedisTemplate</span><span>.</span><span>opsForValue</span><span>()</span></div></div><div><div><div>17</div></div><div><span><span>                </span></span><span>.</span><span>increment</span><span>(</span><span>"inc:"</span><span><span> </span><span>+</span><span> keyPrefix </span><span>+</span><span> </span></span><span>":"</span><span><span> </span><span>+</span><span> date);</span></span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>        </span><span>return</span><span><span> timeStamp </span><span>&lt;&lt;</span><span> COUNT_BIT </span><span>|</span><span> count;</span></span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>21</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section><section><h3>3.2 添加优惠券<a href="#32-添加优惠券"><span>#</span></a></h3><p>优惠券分为平价券和特价券。特价券需要记录库存、开始时间、结束时间。</p><p>新增秒杀券：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@</span><span>PostMapping</span><span>(</span><span>"/seckill"</span><span>)</span></div></div><div><div><div>2</div></div><div><span>public</span><span> </span><span>Result</span><span> </span><span>addSeckillVoucher</span><span><span>(</span><span>@</span></span><span>RequestBody</span><span> </span><span>Voucher</span><span> voucher) {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>voucherService</span><span>.</span><span>addSeckillVoucher</span><span>(voucher);</span></div></div><div><div><div>4</div></div><div><span>    </span><span>return</span><span> </span><span>Result</span><span>.</span><span>ok</span><span>(</span><span>voucher</span><span>.</span><span>getId</span><span>());</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>Service：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@</span><span>Override</span></div></div><div><div><div>2</div></div><div><span>@</span><span>Transactional</span></div></div><div><div><div>3</div></div><div><span>public</span><span> </span><span>void</span><span> </span><span>addSeckillVoucher</span><span>(</span><span>Voucher</span><span> voucher) {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>save</span><span><span>(voucher)</span><span>;</span></span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>    </span><span>SeckillVoucher</span><span> seckillVoucher </span><span><span>=</span><span> </span></span><span>new</span><span> </span><span>SeckillVoucher</span><span><span>()</span><span>;</span></span></div></div><div><div><div>7</div></div><div><span>    </span><span>seckillVoucher</span><span>.</span><span>setVoucherId</span><span>(</span><span>voucher</span><span>.</span><span>getId</span><span>());</span></div></div><div><div><div>8</div></div><div><span>    </span><span>seckillVoucher</span><span>.</span><span>setStock</span><span>(</span><span>voucher</span><span>.</span><span>getStock</span><span>());</span></div></div><div><div><div>9</div></div><div><span>    </span><span>seckillVoucher</span><span>.</span><span>setBeginTime</span><span>(</span><span>voucher</span><span>.</span><span>getBeginTime</span><span>());</span></div></div><div><div><div>10</div></div><div><span>    </span><span>seckillVoucher</span><span>.</span><span>setEndTime</span><span>(</span><span>voucher</span><span>.</span><span>getEndTime</span><span>());</span></div></div><div><div><div>11</div></div><div><span>    </span><span>seckillVoucherService</span><span>.</span><span>save</span><span>(seckillVoucher);</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>    </span><span>stringRedisTemplate</span><span>.</span><span>opsForValue</span><span>().</span><span>set</span><span>(</span></div></div><div><div><div>14</div></div><div><span><span>            </span></span><span>SECKILL_STOCK_KEY </span><span>+</span><span> </span><span>voucher</span><span>.</span><span>getId</span><span>(),</span></div></div><div><div><div>15</div></div><div><span>            </span><span>voucher</span><span>.</span><span>getStock</span><span>().</span><span>toString</span><span>()</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>);</span></div></div><div><div><div>17</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section><section><h3>3.3 实现秒杀下单<a href="#33-实现秒杀下单"><span>#</span></a></h3><p>基础流程：</p><ol>
<li>查询优惠券。</li>
<li>判断秒杀是否开始。</li>
<li>判断秒杀是否结束。</li>
<li>判断库存是否充足。</li>
<li>扣减库存。</li>
<li>创建订单。</li>
<li>返回订单 ID。</li>
</ol><p>基础代码：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@</span><span>Override</span></div></div><div><div><div>2</div></div><div><span>public</span><span> </span><span>Result</span><span> </span><span>seckillVoucher</span><span>(</span><span>Long</span><span> voucherId) {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>LambdaQueryWrapper</span><span>&lt;</span><span>SeckillVoucher</span><span><span>&gt;</span><span> queryWrapper </span></span><span><span>=</span><span> </span></span><span>new</span><span> </span><span>LambdaQueryWrapper</span><span><span>&lt;&gt;</span><span>()</span><span>;</span></span></div></div><div><div><div>4</div></div><div><span>    </span><span>queryWrapper</span><span>.</span><span>eq</span><span>(SeckillVoucher</span><span>::</span><span>getVoucherId, voucherId);</span></div></div><div><div><div>5</div></div><div><span>    </span><span>SeckillVoucher</span><span> seckillVoucher </span><span><span>=</span><span> </span></span><span>seckillVoucherService</span><span>.</span><span>getOne</span><span>(queryWrapper);</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>    </span><span>if</span><span> (</span><span>LocalDateTime</span><span>.</span><span>now</span><span>().</span><span>isBefore</span><span>(</span><span>seckillVoucher</span><span>.</span><span>getBeginTime</span><span><span>())</span><span>) {</span></span></div></div><div><div><div>8</div></div><div><span>        </span><span>return</span><span> </span><span>Result</span><span>.</span><span>fail</span><span>(</span><span>"秒杀还未开始，请耐心等待"</span><span>);</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>    </span><span>if</span><span> (</span><span>LocalDateTime</span><span>.</span><span>now</span><span>().</span><span>isAfter</span><span>(</span><span>seckillVoucher</span><span>.</span><span>getEndTime</span><span><span>())</span><span>) {</span></span></div></div><div><div><div>12</div></div><div><span>        </span><span>return</span><span> </span><span>Result</span><span>.</span><span>fail</span><span>(</span><span>"秒杀已经结束！"</span><span>);</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>    </span><span>if</span><span> (</span><span>seckillVoucher</span><span>.</span><span>getStock</span><span><span>()</span><span> </span><span>&lt;</span><span> </span></span><span>1</span><span>) {</span></div></div><div><div><div>16</div></div><div><span>        </span><span>return</span><span> </span><span>Result</span><span>.</span><span>fail</span><span>(</span><span>"优惠券已被抢光了哦"</span><span>);</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>    </span><span>boolean</span><span> success </span><span><span>=</span><span> </span></span><span>seckillVoucherService</span><span>.</span><span>update</span><span>()</span></div></div><div><div><div>20</div></div><div><span><span>            </span></span><span>.</span><span>setSql</span><span>(</span><span>"stock = stock - 1"</span><span>)</span></div></div><div><div><div>21</div></div><div><span><span>            </span></span><span>.</span><span>eq</span><span>(</span><span>"voucher_id"</span><span>, voucherId)</span></div></div><div><div><div>22</div></div><div><span><span>            </span></span><span>.</span><span>gt</span><span>(</span><span>"stock"</span><span>, </span><span>0</span><span>)</span></div></div><div><div><div>23</div></div><div><span><span>            </span></span><span>.</span><span>update</span><span>();</span></div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div><span>    </span><span>if</span><span><span> (</span><span>!</span><span>success) {</span></span></div></div><div><div><div>26</div></div><div><span>        </span><span>return</span><span> </span><span>Result</span><span>.</span><span>fail</span><span>(</span><span>"库存不足"</span><span>);</span></div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>28</div></div><div>
</div></div><div><div><div>29</div></div><div><span>    </span><span>VoucherOrder</span><span> voucherOrder </span><span><span>=</span><span> </span></span><span>new</span><span> </span><span>VoucherOrder</span><span><span>()</span><span>;</span></span></div></div><div><div><div>30</div></div><div><span>    </span><span>long</span><span> orderId </span><span><span>=</span><span> </span></span><span>redisIdWorker</span><span>.</span><span>nextId</span><span>(</span><span>"order"</span><span>);</span></div></div><div><div><div>31</div></div><div><span>    </span><span>Long</span><span> userId </span><span><span>=</span><span> </span></span><span>UserHolder</span><span>.</span><span>getUser</span><span>().</span><span>getId</span><span>();</span></div></div><div><div><div>32</div></div><div>
</div></div><div><div><div>33</div></div><div><span>    </span><span>voucherOrder</span><span>.</span><span>setId</span><span>(orderId);</span></div></div><div><div><div>34</div></div><div><span>    </span><span>voucherOrder</span><span>.</span><span>setUserId</span><span>(userId);</span></div></div><div><div><div>35</div></div><div><span>    </span><span>voucherOrder</span><span>.</span><span>setVoucherId</span><span>(voucherId);</span></div></div><div><div><div>36</div></div><div>
</div></div><div><div><div>37</div></div><div><span>    </span><span>save</span><span><span>(voucherOrder)</span><span>;</span></span></div></div><div><div><div>38</div></div><div>
</div></div><div><div><div>39</div></div><div><span>    </span><span>return</span><span> </span><span>Result</span><span>.</span><span>ok</span><span>(orderId);</span></div></div><div><div><div>40</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section><section><h3>3.4 超卖问题<a href="#34-超卖问题"><span>#</span></a></h3><p>超卖问题属于多线程安全问题。常见解决方案：</p><ul>
<li><strong>悲观锁</strong>：认为线程安全问题一定会发生，操作前加锁。</li>
<li><strong>乐观锁</strong>：认为线程安全问题不一定发生，更新时判断数据是否被修改。</li>
</ul><p>本项目可以使用库存作为版本号：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>boolean</span><span> success </span><span><span>=</span><span> </span></span><span>seckillVoucherService</span><span>.</span><span>update</span><span>()</span></div></div><div><div><div>2</div></div><div><span><span>        </span></span><span>.</span><span>setSql</span><span>(</span><span>"stock = stock - 1"</span><span>)</span></div></div><div><div><div>3</div></div><div><span><span>        </span></span><span>.</span><span>eq</span><span>(</span><span>"voucher_id"</span><span>, voucherId)</span></div></div><div><div><div>4</div></div><div><span><span>        </span></span><span>.</span><span>gt</span><span>(</span><span>"stock"</span><span>, </span><span>0</span><span>)</span></div></div><div><div><div>5</div></div><div><span><span>        </span></span><span>.</span><span>update</span><span>();</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>只要库存大于 0，就允许扣减，可以避免大量失败。</p></section><section><h3>3.5 一人一单<a href="#35-一人一单"><span>#</span></a></h3><p>需求：同一个优惠券，一个用户只能抢一张。</p><p>初步实现：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Long</span><span> userId </span><span><span>=</span><span> </span></span><span>UserHolder</span><span>.</span><span>getUser</span><span>().</span><span>getId</span><span>();</span></div></div><div><div><div>2</div></div><div><span>int</span><span> count </span><span><span>=</span><span> </span></span><span>query</span><span>()</span></div></div><div><div><div>3</div></div><div><span><span>        </span></span><span>.</span><span>eq</span><span>(</span><span>"voucher_id"</span><span>, voucherId)</span></div></div><div><div><div>4</div></div><div><span><span>        </span></span><span>.</span><span>eq</span><span>(</span><span>"user_id"</span><span>, userId)</span></div></div><div><div><div>5</div></div><div><span><span>        </span></span><span>.</span><span>count</span><span>();</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>if</span><span><span> (count </span><span>&gt;</span><span> </span></span><span>0</span><span>) {</span></div></div><div><div><div>8</div></div><div><span>    </span><span>return</span><span> </span><span>Result</span><span>.</span><span>fail</span><span>(</span><span>"你已经抢过优惠券了哦"</span><span>);</span></div></div><div><div><div>9</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>由于并发情况下仍然可能重复下单，需要使用锁。</p><p>将一人一单逻辑抽取为 <code>createVoucherOrder</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@</span><span>Transactional</span></div></div><div><div><div>2</div></div><div><span>public</span><span> </span><span>Result</span><span> </span><span>createVoucherOrder</span><span>(</span><span>Long</span><span> voucherId) {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>Long</span><span> userId </span><span><span>=</span><span> </span></span><span>UserHolder</span><span>.</span><span>getUser</span><span>().</span><span>getId</span><span>();</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>    </span><span>synchronized</span><span> (</span><span>userId</span><span>.</span><span>toString</span><span>().</span><span>intern</span><span><span>()</span><span>) {</span></span></div></div><div><div><div>6</div></div><div><span>        </span><span>int</span><span> count </span><span><span>=</span><span> </span></span><span>query</span><span>()</span></div></div><div><div><div>7</div></div><div><span><span>                </span></span><span>.</span><span>eq</span><span>(</span><span>"voucher_id"</span><span>, voucherId)</span></div></div><div><div><div>8</div></div><div><span><span>                </span></span><span>.</span><span>eq</span><span>(</span><span>"user_id"</span><span>, userId)</span></div></div><div><div><div>9</div></div><div><span><span>                </span></span><span>.</span><span>count</span><span>();</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>        </span><span>if</span><span><span> (count </span><span>&gt;</span><span> </span></span><span>0</span><span>) {</span></div></div><div><div><div>12</div></div><div><span>            </span><span>return</span><span> </span><span>Result</span><span>.</span><span>fail</span><span>(</span><span>"你已经抢过优惠券了哦"</span><span>);</span></div></div><div><div><div>13</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>        </span><span>boolean</span><span> success </span><span><span>=</span><span> </span></span><span>seckillVoucherService</span><span>.</span><span>update</span><span>()</span></div></div><div><div><div>16</div></div><div><span><span>                </span></span><span>.</span><span>setSql</span><span>(</span><span>"stock = stock - 1"</span><span>)</span></div></div><div><div><div>17</div></div><div><span><span>                </span></span><span>.</span><span>eq</span><span>(</span><span>"voucher_id"</span><span>, voucherId)</span></div></div><div><div><div>18</div></div><div><span><span>                </span></span><span>.</span><span>gt</span><span>(</span><span>"stock"</span><span>, </span><span>0</span><span>)</span></div></div><div><div><div>19</div></div><div><span><span>                </span></span><span>.</span><span>update</span><span>();</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span>        </span><span>if</span><span><span> (</span><span>!</span><span>success) {</span></span></div></div><div><div><div>22</div></div><div><span>            </span><span>return</span><span> </span><span>Result</span><span>.</span><span>fail</span><span>(</span><span>"库存不足"</span><span>);</span></div></div><div><div><div>23</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div><span>        </span><span>VoucherOrder</span><span> voucherOrder </span><span><span>=</span><span> </span></span><span>new</span><span> </span><span>VoucherOrder</span><span><span>()</span><span>;</span></span></div></div><div><div><div>26</div></div><div><span>        </span><span>long</span><span> orderId </span><span><span>=</span><span> </span></span><span>redisIdWorker</span><span>.</span><span>nextId</span><span>(</span><span>"order"</span><span>);</span></div></div><div><div><div>27</div></div><div>
</div></div><div><div><div>28</div></div><div><span>        </span><span>voucherOrder</span><span>.</span><span>setId</span><span>(orderId);</span></div></div><div><div><div>29</div></div><div><span>        </span><span>voucherOrder</span><span>.</span><span>setUserId</span><span>(userId);</span></div></div><div><div><div>30</div></div><div><span>        </span><span>voucherOrder</span><span>.</span><span>setVoucherId</span><span>(voucherId);</span></div></div><div><div><div>31</div></div><div>
</div></div><div><div><div>32</div></div><div><span>        </span><span>save</span><span><span>(voucherOrder)</span><span>;</span></span></div></div><div><div><div>33</div></div><div>
</div></div><div><div><div>34</div></div><div><span>        </span><span>return</span><span> </span><span>Result</span><span>.</span><span>ok</span><span>(orderId);</span></div></div><div><div><div>35</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>36</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>调用时需要通过代理对象调用，确保事务生效：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Long</span><span> userId </span><span><span>=</span><span> </span></span><span>UserHolder</span><span>.</span><span>getUser</span><span>().</span><span>getId</span><span>();</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>synchronized</span><span> (</span><span>userId</span><span>.</span><span>toString</span><span>().</span><span>intern</span><span><span>()</span><span>) {</span></span></div></div><div><div><div>4</div></div><div><span>    </span><span>IVoucherOrderService</span><span> proxy </span><span><span>=</span><span> (IVoucherOrderService) </span></span><span>AopContext</span><span>.</span><span>currentProxy</span><span>();</span></div></div><div><div><div>5</div></div><div><span>    </span><span>return</span><span> </span><span>proxy</span><span>.</span><span>createVoucherOrder</span><span>(voucherId);</span></div></div><div><div><div>6</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>启动类开启：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@</span><span>EnableAspectJAutoProxy</span><span>(</span><span>exposeProxy</span><span><span> </span><span>=</span><span> </span></span><span>true</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>3.6 集群环境下的并发问题<a href="#36-集群环境下的并发问题"><span>#</span></a></h3><p>单机 <code>synchronized</code> 只能锁住当前 JVM。集群部署时，不同 Tomcat 有不同 JVM，锁对象不是同一个，因此会失效。</p><p>解决方案：使用分布式锁。</p><hr /></section></section><section><h2>四、分布式锁<a href="#四分布式锁"><span>#</span></a></h2><section><h3>4.1 分布式锁基本要求<a href="#41-分布式锁基本要求"><span>#</span></a></h3><ul>
<li>可见性：多个线程都能看到相同结果。</li>
<li>互斥：程序串行执行。</li>
<li>高可用：程序不易崩溃。</li>
<li>高性能：加锁和释放锁性能要好。</li>
<li>安全性：异常时能自动释放锁。</li>
</ul><p>常见实现：</p>

<table><thead><tr><th>方案</th><th>互斥</th><th>高可用</th><th>高性能</th><th>安全性</th></tr></thead><tbody><tr><td>MySQL</td><td>数据库锁</td><td>好</td><td>一般</td><td>断开连接自动释放</td></tr><tr><td>Redis</td><td><code>SETNX</code></td><td>好</td><td>好</td><td>超时释放</td></tr><tr><td>Zookeeper</td><td>节点唯一性</td><td>好</td><td>一般</td><td>临时节点自动释放</td></tr></tbody></table></section><section><h3>4.2 Redis 分布式锁核心思路<a href="#42-redis-分布式锁核心思路"><span>#</span></a></h3><p>获取锁：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>SET</span><span> </span><span>lock</span><span> </span><span>thread01</span><span> </span><span>NX</span><span> </span><span>EX</span><span> </span><span>10</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>释放锁：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>DEL</span><span> </span><span>lock</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>实现接口：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>public</span><span> </span><span>interface</span><span><span> </span><span>ILock</span><span> </span></span><span>{</span></div></div><div><div><div>2</div></div><div><span>    </span><span>boolean</span><span> </span><span>tryLock</span><span>(</span><span>long</span><span><span> </span><span>timeoutSec</span><span>);</span></span></div></div><div><div><div>3</div></div><div><span>    </span><span>void</span><span> </span><span>unlock</span><span>();</span></div></div><div><div><div>4</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>实现类：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>public</span><span> </span><span>class</span><span><span> </span><span>SimpleRedisLock</span><span> </span></span><span>implements</span><span><span> </span><span>ILock</span><span> </span></span><span>{</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>    </span><span>private</span><span> </span><span>static</span><span> </span><span>final</span><span> </span><span>String</span><span> KEY_PREFIX </span><span><span>=</span><span> </span></span><span>"lock:"</span><span>;</span></div></div><div><div><div>4</div></div><div><span>    </span><span>private</span><span> </span><span>final</span><span> </span><span>String</span><span> name</span><span>;</span></div></div><div><div><div>5</div></div><div><span>    </span><span>private</span><span> </span><span>final</span><span> </span><span>StringRedisTemplate</span><span> stringRedisTemplate</span><span>;</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>    </span><span>public</span><span> </span><span>SimpleRedisLock</span><span>(</span><span>String</span><span><span> </span><span>name</span><span>, </span></span><span>StringRedisTemplate</span><span><span> </span><span>stringRedisTemplate</span><span>)</span><span> </span><span>{</span></span></div></div><div><div><div>8</div></div><div><span>        </span><span>this</span><span>.</span><span>name</span><span><span> </span><span>=</span><span> name;</span></span></div></div><div><div><div>9</div></div><div><span>        </span><span>this</span><span>.</span><span>stringRedisTemplate</span><span><span> </span><span>=</span><span> stringRedisTemplate;</span></span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>@</span><span>Override</span></div></div><div><div><div>13</div></div><div><span>    </span><span>public</span><span> </span><span>boolean</span><span> </span><span>tryLock</span><span>(</span><span>long</span><span><span> </span><span>timeoutSec</span><span>)</span><span> </span><span>{</span></span></div></div><div><div><div>14</div></div><div><span>        </span><span>long</span><span><span> </span><span>threadId</span><span> </span></span><span><span>=</span><span> </span></span><span>Thread</span><span>.</span><span>currentThread</span><span>().</span><span>getId</span><span>();</span></div></div><div><div><div>15</div></div><div><span>        </span><span>Boolean</span><span><span> </span><span>success</span><span> </span></span><span><span>=</span><span> </span></span><span>stringRedisTemplate</span><span>.</span><span>opsForValue</span><span>()</span></div></div><div><div><div>16</div></div><div><span><span>                </span></span><span>.</span><span>setIfAbsent</span><span><span>(KEY_PREFIX </span><span>+</span><span> name, threadId </span><span>+</span><span> </span></span><span>""</span><span>, timeoutSec, </span><span>TimeUnit</span><span>.</span><span>SECONDS</span><span>);</span></div></div><div><div><div>17</div></div><div><span>        </span><span>return</span><span> </span><span>Boolean</span><span>.</span><span>TRUE</span><span>.</span><span>equals</span><span>(success);</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>@</span><span>Override</span></div></div><div><div><div>21</div></div><div><span>    </span><span>public</span><span> </span><span>void</span><span> </span><span>unlock</span><span><span>()</span><span> </span><span>{</span></span></div></div><div><div><div>22</div></div><div><span>        </span><span>stringRedisTemplate</span><span>.</span><span>delete</span><span><span>(KEY_PREFIX </span><span>+</span><span> name);</span></span></div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>24</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>业务使用：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>SimpleRedisLock</span><span> redisLock </span><span><span>=</span><span> </span></span><span>new</span><span> </span><span>SimpleRedisLock</span><span>(</span><span>"order:"</span><span><span> </span><span>+</span><span> userId</span><span>,</span><span> stringRedisTemplate)</span><span>;</span></span></div></div><div><div><div>2</div></div><div><span>boolean</span><span> isLock </span><span><span>=</span><span> </span></span><span>redisLock</span><span>.</span><span>tryLock</span><span>(</span><span>120</span><span>);</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>if</span><span><span> (</span><span>!</span><span>isLock) {</span></span></div></div><div><div><div>5</div></div><div><span>    </span><span>return</span><span> </span><span>Result</span><span>.</span><span>fail</span><span>(</span><span>"不允许抢多张优惠券"</span><span>);</span></div></div><div><div><div>6</div></div><div><span>}</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>try</span><span> {</span></div></div><div><div><div>9</div></div><div><span>    </span><span>IVoucherOrderService</span><span> proxy </span><span><span>=</span><span> (IVoucherOrderService) </span></span><span>AopContext</span><span>.</span><span>currentProxy</span><span>();</span></div></div><div><div><div>10</div></div><div><span>    </span><span>return</span><span> </span><span>proxy</span><span>.</span><span>createVoucherOrder</span><span>(voucherId);</span></div></div><div><div><div>11</div></div><div><span>} </span><span>finally</span><span> {</span></div></div><div><div><div>12</div></div><div><span>    </span><span>redisLock</span><span>.</span><span>unlock</span><span>();</span></div></div><div><div><div>13</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>4.3 分布式锁误删问题<a href="#43-分布式锁误删问题"><span>#</span></a></h3><p>如果线程 1 阻塞导致锁 TTL 到期，线程 2 获取锁；线程 1 恢复后直接删除锁，就会误删线程 2 的锁。</p><p>解决方案：锁中存入线程标识，释放时判断是否属于自己。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>private</span><span> </span><span>static</span><span> </span><span>final</span><span> </span><span>String</span><span> ID_PREFIX </span><span><span>=</span><span> </span></span><span>UUID</span><span>.</span><span>randomUUID</span><span>().</span><span>toString</span><span>(</span><span>true</span><span><span>)</span><span> </span><span>+</span><span> </span></span><span>"-"</span><span>;</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>@</span><span>Override</span></div></div><div><div><div>4</div></div><div><span>public</span><span> </span><span>boolean</span><span> </span><span>tryLock</span><span>(</span><span>long</span><span> timeoutSec) {</span></div></div><div><div><div>5</div></div><div><span>    </span><span>String</span><span> threadId </span><span><span>=</span><span> ID_PREFIX </span><span>+</span><span> </span></span><span>Thread</span><span>.</span><span>currentThread</span><span>().</span><span>getId</span><span>();</span></div></div><div><div><div>6</div></div><div><span>    </span><span>Boolean</span><span> success </span><span><span>=</span><span> </span></span><span>stringRedisTemplate</span><span>.</span><span>opsForValue</span><span>()</span></div></div><div><div><div>7</div></div><div><span><span>            </span></span><span>.</span><span>setIfAbsent</span><span><span>(KEY_PREFIX </span><span>+</span><span> name, threadId, timeoutSec, </span></span><span>TimeUnit</span><span>.</span><span>SECONDS</span><span>);</span></div></div><div><div><div>8</div></div><div><span>    </span><span>return</span><span> </span><span>Boolean</span><span>.</span><span>TRUE</span><span>.</span><span>equals</span><span>(success);</span></div></div><div><div><div>9</div></div><div><span>}</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>@</span><span>Override</span></div></div><div><div><div>12</div></div><div><span>public</span><span> </span><span>void</span><span> </span><span>unlock</span><span>() {</span></div></div><div><div><div>13</div></div><div><span>    </span><span>String</span><span> threadId </span><span><span>=</span><span> ID_PREFIX </span><span>+</span><span> </span></span><span>Thread</span><span>.</span><span>currentThread</span><span>().</span><span>getId</span><span>();</span></div></div><div><div><div>14</div></div><div><span>    </span><span>String</span><span> id </span><span><span>=</span><span> </span></span><span>stringRedisTemplate</span><span>.</span><span>opsForValue</span><span>().</span><span>get</span><span><span>(KEY_PREFIX </span><span>+</span><span> name);</span></span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>    </span><span>if</span><span> (</span><span>threadId</span><span>.</span><span>equals</span><span><span>(id)</span><span>) {</span></span></div></div><div><div><div>17</div></div><div><span>        </span><span>stringRedisTemplate</span><span>.</span><span>delete</span><span><span>(KEY_PREFIX </span><span>+</span><span> name);</span></span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>19</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section><section><h3>4.4 Lua 脚本保证原子性<a href="#44-lua-脚本保证原子性"><span>#</span></a></h3><p>判断锁标识和删除锁必须是原子操作，否则仍可能误删。</p><p>Lua 脚本 <code>unlock.lua</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>local</span><span> </span><span>key</span><span> = </span><span>KEYS</span><span>[</span><span>1</span><span>]</span></div></div><div><div><div>2</div></div><div><span>local</span><span> </span><span>threadId</span><span> = </span><span>ARGV</span><span>[</span><span>1</span><span>]</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>if</span><span> (</span><span>redis</span><span>.</span><span>call</span><span>(</span><span>'get'</span><span>, </span><span>key</span><span>) == </span><span>threadId</span><span>) </span><span>then</span></div></div><div><div><div>5</div></div><div><span>    </span><span>return</span><span> </span><span>redis</span><span>.</span><span>call</span><span>(</span><span>'del'</span><span>, </span><span>key</span><span>)</span></div></div><div><div><div>6</div></div><div><span>end</span></div></div><div><div><div>7</div></div><div><span>return</span><span> </span><span>0</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>Java 调用：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>private</span><span> </span><span>static</span><span> </span><span>final</span><span> </span><span>DefaultRedisScript</span><span>&lt;</span><span>Long</span><span><span>&gt;</span><span> UNLOCK_SCRIPT</span></span><span>;</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>static</span><span> {</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>UNLOCK_SCRIPT </span><span>=</span><span> </span><span>new</span><span> </span><span>DefaultRedisScript</span><span><span>&lt;&gt;</span><span>()</span><span>;</span></span></div></div><div><div><div>5</div></div><div><span>    </span><span>UNLOCK_SCRIPT</span><span>.</span><span>setLocation</span><span>(</span><span>new</span><span> </span><span>ClassPathResource</span><span>(</span><span>"unlock.lua"</span><span>));</span></div></div><div><div><div>6</div></div><div><span>    </span><span>UNLOCK_SCRIPT</span><span>.</span><span>setResultType</span><span>(</span><span>Long</span><span>.</span><span>class</span><span>);</span></div></div><div><div><div>7</div></div><div><span>}</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>@</span><span>Override</span></div></div><div><div><div>10</div></div><div><span>public</span><span> </span><span>void</span><span> </span><span>unlock</span><span>() {</span></div></div><div><div><div>11</div></div><div><span>    </span><span>stringRedisTemplate</span><span>.</span><span>execute</span><span>(</span></div></div><div><div><div>12</div></div><div><span><span>            </span></span><span>UNLOCK_SCRIPT,</span></div></div><div><div><div>13</div></div><div><span>            </span><span>Collections</span><span>.</span><span>singletonList</span><span><span>(KEY_PREFIX </span><span>+</span><span> name),</span></span></div></div><div><div><div>14</div></div><div><span><span>            </span></span><span>ID_PREFIX </span><span>+</span><span> </span><span>Thread</span><span>.</span><span>currentThread</span><span>().</span><span>getId</span><span>()</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>);</span></div></div><div><div><div>16</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section><section><h3>4.5 Redisson<a href="#45-redisson"><span>#</span></a></h3><p>基于 <code>SETNX</code> 的分布式锁存在以下问题：</p><ul>
<li>不可重入。</li>
<li>不可重试。</li>
<li>超时释放可能导致锁失效。</li>
<li>主从一致性存在问题。</li>
</ul><p>Redisson 提供了完整解决方案。</p><p>导入依赖：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>&lt;</span><span>dependency</span><span>&gt;</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>&lt;</span><span>groupId</span><span>&gt;org.redisson&lt;/</span><span>groupId</span><span>&gt;</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>&lt;</span><span>artifactId</span><span>&gt;redisson&lt;/</span><span>artifactId</span><span>&gt;</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>&lt;</span><span>version</span><span>&gt;3.13.6&lt;/</span><span>version</span><span>&gt;</span></div></div><div><div><div>5</div></div><div><span>&lt;/</span><span>dependency</span><span>&gt;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>配置：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@</span><span>Configuration</span></div></div><div><div><div>2</div></div><div><span>public</span><span> </span><span>class</span><span><span> </span><span>RedissonConfig</span><span> </span></span><span>{</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>@</span><span>Bean</span></div></div><div><div><div>5</div></div><div><span>    </span><span>public</span><span> </span><span>RedissonClient</span><span> </span><span>redissonClient</span><span><span>()</span><span> </span><span>{</span></span></div></div><div><div><div>6</div></div><div><span>        </span><span>Config</span><span><span> </span><span>config</span><span> </span></span><span><span>=</span><span> </span></span><span>new</span><span> </span><span>Config</span><span>();</span></div></div><div><div><div>7</div></div><div><span>        </span><span>config</span><span>.</span><span>useSingleServer</span><span>()</span></div></div><div><div><div>8</div></div><div><span><span>                </span></span><span>.</span><span>setAddress</span><span>(</span><span>"redis://127.0.0.1:6379"</span><span>)</span></div></div><div><div><div>9</div></div><div><span><span>                </span></span><span>.</span><span>setPassword</span><span>(</span><span>"root"</span><span>);</span></div></div><div><div><div>10</div></div><div><span>        </span><span>return</span><span> </span><span>Redisson</span><span>.</span><span>create</span><span>(config);</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>12</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>使用：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@</span><span>Resource</span></div></div><div><div><div>2</div></div><div><span>private</span><span> </span><span>RedissonClient</span><span> redissonClient</span><span>;</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>@</span><span>Test</span></div></div><div><div><div>5</div></div><div><span>void</span><span> </span><span>testRedisson</span><span>() throws InterruptedException {</span></div></div><div><div><div>6</div></div><div><span>    </span><span>RLock</span><span> lock </span><span><span>=</span><span> </span></span><span>redissonClient</span><span>.</span><span>getLock</span><span>(</span><span>"anyLock"</span><span>);</span></div></div><div><div><div>7</div></div><div><span>    </span><span>boolean</span><span> success </span><span><span>=</span><span> </span></span><span>lock</span><span>.</span><span>tryLock</span><span>(</span><span>1</span><span>, </span><span>10</span><span>, </span><span>TimeUnit</span><span>.</span><span>SECONDS</span><span>);</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>    </span><span>if</span><span> (success) {</span></div></div><div><div><div>10</div></div><div><span>        </span><span>try</span><span> {</span></div></div><div><div><div>11</div></div><div><span>            </span><span>System</span><span>.</span><span>out</span><span>.</span><span>println</span><span>(</span><span>"执行业务"</span><span>);</span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>} </span><span>finally</span><span> {</span></div></div><div><div><div>13</div></div><div><span>            </span><span>lock</span><span>.</span><span>unlock</span><span>();</span></div></div><div><div><div>14</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>16</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>替换秒杀业务中的锁：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@</span><span>Resource</span></div></div><div><div><div>2</div></div><div><span>private</span><span> </span><span>RedissonClient</span><span> redissonClient</span><span>;</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>RLock</span><span> redisLock </span><span><span>=</span><span> </span></span><span>redissonClient</span><span>.</span><span>getLock</span><span>(</span><span>"order:"</span><span><span> </span><span>+</span><span> userId);</span></span></div></div><div><div><div>5</div></div><div><span>boolean</span><span> isLock </span><span><span>=</span><span> </span></span><span>redisLock</span><span>.</span><span>tryLock</span><span>();</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>if</span><span><span> (</span><span>!</span><span>isLock) {</span></span></div></div><div><div><div>8</div></div><div><span>    </span><span>return</span><span> </span><span>Result</span><span>.</span><span>fail</span><span>(</span><span>"不允许抢多张优惠券"</span><span>);</span></div></div><div><div><div>9</div></div><div><span>}</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>try</span><span> {</span></div></div><div><div><div>12</div></div><div><span>    </span><span>IVoucherOrderService</span><span> proxy </span><span><span>=</span><span> (IVoucherOrderService) </span></span><span>AopContext</span><span>.</span><span>currentProxy</span><span>();</span></div></div><div><div><div>13</div></div><div><span>    </span><span>return</span><span> </span><span>proxy</span><span>.</span><span>createVoucherOrder</span><span>(voucherId);</span></div></div><div><div><div>14</div></div><div><span>} </span><span>finally</span><span> {</span></div></div><div><div><div>15</div></div><div><span>    </span><span>redisLock</span><span>.</span><span>unlock</span><span>();</span></div></div><div><div><div>16</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>Redisson 可重入锁使用 Hash 结构记录线程标识与重入次数，并通过 WatchDog 机制自动续期。</p><p>Redisson MultiLock 用于主从一致性场景：多个独立 Redis 节点都加锁成功，才算加锁成功。</p><hr /></section></section><section><h2>五、秒杀优化<a href="#五秒杀优化"><span>#</span></a></h2><section><h3>5.1 异步秒杀思路<a href="#51-异步秒杀思路"><span>#</span></a></h3><p>原始下单流程中，很多步骤需要访问数据库，并且串行执行，性能较差。</p><p>优化思路：</p><ol>
<li>将库存判断、一人一单判断放到 Redis 中完成。</li>
<li>使用 Lua 脚本保证判断与扣减库存的原子性。</li>
<li>判断成功后，将下单信息放入消息队列。</li>
<li>后台线程异步消费消息，完成数据库下单。</li>
</ol></section><section><h3>5.2 Redis 完成秒杀资格判断<a href="#52-redis-完成秒杀资格判断"><span>#</span></a></h3><p>Lua 脚本 <code>seckill.lua</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>local</span><span> </span><span>voucherId</span><span> = </span><span>ARGV</span><span>[</span><span>1</span><span>]</span></div></div><div><div><div>2</div></div><div><span>local</span><span> </span><span>userId</span><span> = </span><span>ARGV</span><span>[</span><span>2</span><span>]</span></div></div><div><div><div>3</div></div><div><span>local</span><span> </span><span>id</span><span> = </span><span>ARGV</span><span>[</span><span>3</span><span>]</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>local</span><span> </span><span>stockKey</span><span> = </span><span>'seckill:stock:' </span><span>.. </span><span>voucherId</span></div></div><div><div><div>6</div></div><div><span>local</span><span> </span><span>orderKey</span><span> = </span><span>'seckill:order:' </span><span>.. </span><span>voucherId</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>if</span><span> (</span><span>tonumber</span><span>(</span><span>redis</span><span>.</span><span>call</span><span>(</span><span>'get'</span><span>, </span><span>stockKey</span><span>)) &lt;= </span><span>0</span><span>) </span><span>then</span></div></div><div><div><div>9</div></div><div><span>    </span><span>return</span><span> </span><span>1</span></div></div><div><div><div>10</div></div><div><span>end</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>if</span><span> (</span><span>redis</span><span>.</span><span>call</span><span>(</span><span>'sismember'</span><span>, </span><span>orderKey</span><span>, </span><span>userId</span><span>) == </span><span>1</span><span>) </span><span>then</span></div></div><div><div><div>13</div></div><div><span>    </span><span>return</span><span> </span><span>2</span></div></div><div><div><div>14</div></div><div><span>end</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>redis</span><span>.</span><span>call</span><span>(</span><span>'incrby'</span><span>, </span><span>stockKey</span><span>, -</span><span>1</span><span>)</span></div></div><div><div><div>17</div></div><div><span>redis</span><span>.</span><span>call</span><span>(</span><span>'sadd'</span><span>, </span><span>orderKey</span><span>, </span><span>userId</span><span>)</span></div></div><div><div><div>18</div></div><div><span>redis</span><span>.</span><span>call</span><span>(</span><span>'xadd'</span><span>, </span><span>'stream.orders'</span><span>, </span><span>'*'</span><span>, </span><span>'userId'</span><span>, </span><span>userId</span><span>, </span><span>'voucherId'</span><span>, </span><span>voucherId</span><span>, </span><span>'id'</span><span>, </span><span>id</span><span>)</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>return</span><span> </span><span>0</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>业务逻辑：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@</span><span>Override</span></div></div><div><div><div>2</div></div><div><span>public</span><span> </span><span>Result</span><span> </span><span>seckillVoucher</span><span>(</span><span>Long</span><span> voucherId) {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>long</span><span> orderId </span><span><span>=</span><span> </span></span><span>redisIdWorker</span><span>.</span><span>nextId</span><span>(</span><span>"order"</span><span>);</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>    </span><span>Long</span><span> result </span><span><span>=</span><span> </span></span><span>stringRedisTemplate</span><span>.</span><span>execute</span><span>(</span></div></div><div><div><div>6</div></div><div><span><span>            </span></span><span>SECKILL_SCRIPT,</span></div></div><div><div><div>7</div></div><div><span>            </span><span>Collections</span><span>.</span><span>emptyList</span><span>(),</span></div></div><div><div><div>8</div></div><div><span>            </span><span>voucherId</span><span>.</span><span>toString</span><span>(),</span></div></div><div><div><div>9</div></div><div><span>            </span><span>UserHolder</span><span>.</span><span>getUser</span><span>().</span><span>getId</span><span>().</span><span>toString</span><span>(),</span></div></div><div><div><div>10</div></div><div><span>            </span><span>String</span><span>.</span><span>valueOf</span><span>(orderId)</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>);</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>    </span><span>if</span><span> (</span><span>result</span><span>.</span><span>intValue</span><span><span>()</span><span> </span><span>!=</span><span> </span></span><span>0</span><span>) {</span></div></div><div><div><div>14</div></div><div><span>        </span><span>return</span><span> </span><span>Result</span><span>.</span><span>fail</span><span>(</span><span>result</span><span>.</span><span>intValue</span><span><span>() </span><span>==</span><span> </span></span><span>1</span><span> </span><span>?</span><span> </span><span>"库存不足"</span><span> </span><span>:</span><span> </span><span>"不能重复下单"</span><span>);</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>proxy </span><span>=</span><span> (IVoucherOrderService) </span><span>AopContext</span><span>.</span><span>currentProxy</span><span>();</span></div></div><div><div><div>18</div></div><div><span>    </span><span>return</span><span> </span><span>Result</span><span>.</span><span>ok</span><span>(orderId);</span></div></div><div><div><div>19</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section><section><h3>5.3 基于阻塞队列实现异步秒杀<a href="#53-基于阻塞队列实现异步秒杀"><span>#</span></a></h3><p>创建阻塞队列：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>private</span><span> </span><span>final</span><span> </span><span>BlockingQueue</span><span>&lt;</span><span>VoucherOrder</span><span><span>&gt;</span><span> orderTasks </span></span><span>=</span></div></div><div><div><div>2</div></div><div><span>        </span><span>new</span><span> </span><span>ArrayBlockingQueue</span><span><span>&lt;&gt;</span><span>(</span></span><span>1024</span><span><span> </span><span>*</span><span> </span></span><span>1024</span><span><span>)</span><span>;</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>将订单加入队列：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>VoucherOrder</span><span> voucherOrder </span><span><span>=</span><span> </span></span><span>new</span><span> </span><span>VoucherOrder</span><span><span>()</span><span>;</span></span></div></div><div><div><div>2</div></div><div><span>voucherOrder</span><span>.</span><span>setVoucherId</span><span>(voucherId);</span></div></div><div><div><div>3</div></div><div><span>voucherOrder</span><span>.</span><span>setUserId</span><span>(</span><span>UserHolder</span><span>.</span><span>getUser</span><span>().</span><span>getId</span><span>());</span></div></div><div><div><div>4</div></div><div><span>voucherOrder</span><span>.</span><span>setId</span><span>(orderId);</span></div></div><div><div><div>5</div></div><div><span>orderTasks</span><span>.</span><span>add</span><span>(voucherOrder);</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>线程池消费：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>private</span><span> </span><span>static</span><span> </span><span>final</span><span> </span><span>ExecutorService</span><span> SECKILL_ORDER_EXECUTOR </span><span>=</span></div></div><div><div><div>2</div></div><div><span>        </span><span>Executors</span><span>.</span><span>newSingleThreadExecutor</span><span>();</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>@</span><span>PostConstruct</span></div></div><div><div><div>5</div></div><div><span>private</span><span> </span><span>void</span><span> </span><span>init</span><span>() {</span></div></div><div><div><div>6</div></div><div><span>    </span><span>SECKILL_ORDER_EXECUTOR</span><span>.</span><span>submit</span><span>(</span><span>new</span><span> </span><span>VoucherOrderHandler</span><span>());</span></div></div><div><div><div>7</div></div><div><span>}</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>private</span><span> </span><span>class</span><span><span> </span><span>VoucherOrderHandler</span><span> </span></span><span>implements</span><span><span> </span><span>Runnable</span><span> </span></span><span>{</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>@</span><span>Override</span></div></div><div><div><div>11</div></div><div><span>    </span><span>public</span><span> </span><span>void</span><span> </span><span>run</span><span><span>()</span><span> </span><span>{</span></span></div></div><div><div><div>12</div></div><div><span>        </span><span>while</span><span> (</span><span>true</span><span>) {</span></div></div><div><div><div>13</div></div><div><span>            </span><span>try</span><span> {</span></div></div><div><div><div>14</div></div><div><span>                </span><span>VoucherOrder</span><span><span> </span><span>voucherOrder</span><span> </span></span><span><span>=</span><span> </span></span><span>orderTasks</span><span>.</span><span>take</span><span>();</span></div></div><div><div><div>15</div></div><div><span>                </span><span>handleVoucherOrder</span><span>(voucherOrder);</span></div></div><div><div><div>16</div></div><div><span><span>            </span></span><span>} </span><span>catch</span><span> (</span><span>Exception</span><span><span> </span><span>e</span><span>) {</span></span></div></div><div><div><div>17</div></div><div><span>                </span><span>log</span><span>.</span><span>error</span><span>(</span><span>"订单处理异常"</span><span>, e);</span></div></div><div><div><div>18</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>19</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>21</div></div><div><span>}</span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span>private</span><span> </span><span>void</span><span> </span><span>handleVoucherOrder</span><span>(</span><span>VoucherOrder</span><span> voucherOrder) {</span></div></div><div><div><div>24</div></div><div><span>    </span><span>Long</span><span> userId </span><span><span>=</span><span> </span></span><span>voucherOrder</span><span>.</span><span>getUserId</span><span>();</span></div></div><div><div><div>25</div></div><div><span>    </span><span>RLock</span><span> redisLock </span><span><span>=</span><span> </span></span><span>redissonClient</span><span>.</span><span>getLock</span><span>(</span><span>"order:"</span><span><span> </span><span>+</span><span> userId);</span></span></div></div><div><div><div>26</div></div><div><span>    </span><span>boolean</span><span> isLock </span><span><span>=</span><span> </span></span><span>redisLock</span><span>.</span><span>tryLock</span><span>();</span></div></div><div><div><div>27</div></div><div>
</div></div><div><div><div>28</div></div><div><span>    </span><span>if</span><span><span> (</span><span>!</span><span>isLock) {</span></span></div></div><div><div><div>29</div></div><div><span>        </span><span>log</span><span>.</span><span>error</span><span>(</span><span>"不允许重复下单!"</span><span>);</span></div></div><div><div><div>30</div></div><div><span>        </span><span>return</span><span>;</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>32</div></div><div>
</div></div><div><div><div>33</div></div><div><span>    </span><span>try</span><span> {</span></div></div><div><div><div>34</div></div><div><span>        </span><span>proxy</span><span>.</span><span>createVoucherOrder</span><span>(voucherOrder);</span></div></div><div><div><div>35</div></div><div><span><span>    </span></span><span>} </span><span>finally</span><span> {</span></div></div><div><div><div>36</div></div><div><span>        </span><span>redisLock</span><span>.</span><span>unlock</span><span>();</span></div></div><div><div><div>37</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>38</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div></div><div>阻塞队列的问题</div></div><div><ul>
<li>受 JVM 内存限制，高并发时可能内存溢出。</li>
<li>服务宕机时，队列中的订单可能丢失。</li>
</ul></div></div></section><section><h3>5.4 Redis 消息队列<a href="#54-redis-消息队列"><span>#</span></a></h3>

<table><thead><tr><th>方案</th><th>消息持久化</th><th>阻塞读取</th><th>消息确认</th><th>消息回溯</th></tr></thead><tbody><tr><td>List</td><td>支持</td><td>支持</td><td>不支持</td><td>不支持</td></tr><tr><td>PubSub</td><td>不支持</td><td>支持</td><td>不支持</td><td>不支持</td></tr><tr><td>Stream</td><td>支持</td><td>支持</td><td>支持</td><td>支持</td></tr></tbody></table><section><h4>基于 List<a href="#基于-list"><span>#</span></a></h4><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>LPUSH</span><span> </span><span>queue</span><span> </span><span>msg</span></div></div><div><div><div>2</div></div><div><span>BRPOP</span><span> </span><span>queue</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>优点：</p><ul>
<li>利用 Redis 存储，不受 JVM 内存上限限制。</li>
<li>基于 Redis 持久化机制，数据安全性有保障。</li>
<li>可以满足消息有序性。</li>
</ul><p>缺点：</p><ul>
<li>无法避免消息丢失。</li>
<li>只支持单消费者。</li>
</ul></section><section><h4>基于 PubSub<a href="#基于-pubsub"><span>#</span></a></h4><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>SUBSCRIBE</span><span> </span><span>channel</span></div></div><div><div><div>2</div></div><div><span>PUBLISH</span><span> </span><span>channel</span><span> </span><span>msg</span></div></div><div><div><div>3</div></div><div><span>PSUBSCRIBE</span><span> </span><span>pattern</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>优点：支持多生产、多消费。</p><p>缺点：不支持持久化、可能丢消息、消息堆积有上限。</p></section><section><h4>基于 Stream<a href="#基于-stream"><span>#</span></a></h4><p>发送消息：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>XADD</span><span> </span><span>users</span><span> </span><span>*</span><span> </span><span>name</span><span> </span><span>jack</span><span> </span><span>age</span><span> </span><span>21</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>读取消息：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>XREAD</span><span> </span><span>COUNT</span><span> </span><span>1</span><span> </span><span>BLOCK</span><span> </span><span>2000</span><span> </span><span>STREAMS</span><span> </span><span>users</span><span> $</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>创建消费者组：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>XGROUP</span><span> </span><span>CREATE</span><span> </span><span>stream.orders</span><span> </span><span>g1</span><span> </span><span>0</span><span> </span><span>MKSTREAM</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>消费者组读取：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>XREADGROUP</span><span> </span><span>GROUP</span><span> </span><span>g1</span><span> </span><span>c1</span><span> </span><span>COUNT</span><span> </span><span>1</span><span> </span><span>BLOCK</span><span> </span><span>2000</span><span> </span><span>STREAMS</span><span> </span><span>stream.orders</span><span> &gt;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>确认消息：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>XACK</span><span> </span><span>stream.orders</span><span> </span><span>g1</span><span> </span><span>消息ID</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>Stream 特点：</p><ul>
<li>消息可回溯。</li>
<li>多消费者争抢消息。</li>
<li>可阻塞读取。</li>
<li>无消息漏读风险。</li>
<li>有消息确认机制。</li>
</ul></section></section><section><h3>5.5 基于 Stream 实现异步秒杀<a href="#55-基于-stream-实现异步秒杀"><span>#</span></a></h3><p>Lua 脚本中直接写入 Stream：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>redis</span><span>.</span><span>call</span><span>(</span><span>'xadd'</span><span>, </span><span>'stream.orders'</span><span>, </span><span>'*'</span><span>,</span></div></div><div><div><div>2</div></div><div><span>        </span><span>'userId'</span><span>, </span><span>userId</span><span>,</span></div></div><div><div><div>3</div></div><div><span>        </span><span>'voucherId'</span><span>, </span><span>voucherId</span><span>,</span></div></div><div><div><div>4</div></div><div><span>        </span><span>'id'</span><span>, </span><span>id</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>消费者处理：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>private</span><span> </span><span>class</span><span><span> </span><span>VoucherOrderHandler</span><span> </span></span><span>implements</span><span><span> </span><span>Runnable</span><span> </span></span><span>{</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>@</span><span>Override</span></div></div><div><div><div>3</div></div><div><span>    </span><span>public</span><span> </span><span>void</span><span> </span><span>run</span><span><span>()</span><span> </span><span>{</span></span></div></div><div><div><div>4</div></div><div><span>        </span><span>while</span><span> (</span><span>true</span><span>) {</span></div></div><div><div><div>5</div></div><div><span>            </span><span>try</span><span> {</span></div></div><div><div><div>6</div></div><div><span>                </span><span>List</span><span>&lt;</span><span>MapRecord</span><span>&lt;</span><span>String</span><span>, </span><span>Object</span><span>, </span><span>Object</span><span><span>&gt;&gt; </span><span>list</span><span> </span></span><span>=</span></div></div><div><div><div>7</div></div><div><span>                        </span><span>stringRedisTemplate</span><span>.</span><span>opsForStream</span><span>().</span><span>read</span><span>(</span></div></div><div><div><div>8</div></div><div><span>                                </span><span>Consumer</span><span>.</span><span>from</span><span>(</span><span>"g1"</span><span>, </span><span>"c1"</span><span>),</span></div></div><div><div><div>9</div></div><div><span>                                </span><span>StreamReadOptions</span><span>.</span><span>empty</span><span>().</span><span>count</span><span>(</span><span>1</span><span>).</span><span>block</span><span>(</span><span>Duration</span><span>.</span><span>ofSeconds</span><span>(</span><span>2</span><span>)),</span></div></div><div><div><div>10</div></div><div><span>                                </span><span>StreamOffset</span><span>.</span><span>create</span><span>(</span><span>"stream.orders"</span><span>, </span><span>ReadOffset</span><span>.</span><span>lastConsumed</span><span>())</span></div></div><div><div><div>11</div></div><div><span><span>                        </span></span><span>);</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>                </span><span>if</span><span><span> (list </span><span>==</span><span> </span></span><span>null</span><span><span> </span><span>||</span><span> </span></span><span>list</span><span>.</span><span>isEmpty</span><span>()) {</span></div></div><div><div><div>14</div></div><div><span>                    </span><span>continue</span><span>;</span></div></div><div><div><div>15</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>                </span><span>MapRecord</span><span>&lt;</span><span>String</span><span>, </span><span>Object</span><span>, </span><span>Object</span><span><span>&gt; </span><span>record</span><span> </span></span><span><span>=</span><span> </span></span><span>list</span><span>.</span><span>get</span><span>(</span><span>0</span><span>);</span></div></div><div><div><div>18</div></div><div><span>                </span><span>Map</span><span>&lt;</span><span>Object</span><span>, </span><span>Object</span><span><span>&gt; </span><span>value</span><span> </span></span><span><span>=</span><span> </span></span><span>record</span><span>.</span><span>getValue</span><span>();</span></div></div><div><div><div>19</div></div><div><span>                </span><span>VoucherOrder</span><span><span> </span><span>voucherOrder</span><span> </span></span><span><span>=</span><span> </span></span><span>BeanUtil</span><span>.</span><span>fillBeanWithMap</span><span>(</span></div></div><div><div><div>20</div></div><div><span><span>                        </span></span><span>value,</span></div></div><div><div><div>21</div></div><div><span>                        </span><span>new</span><span> </span><span>VoucherOrder</span><span>(),</span></div></div><div><div><div>22</div></div><div><span>                        </span><span>true</span></div></div><div><div><div>23</div></div><div><span><span>                </span></span><span>);</span></div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div><span>                </span><span>handleVoucherOrder</span><span>(voucherOrder);</span></div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div><span>                </span><span>stringRedisTemplate</span><span>.</span><span>opsForStream</span><span>().</span><span>acknowledge</span><span>(</span></div></div><div><div><div>28</div></div><div><span>                        </span><span>"stream.orders"</span><span>,</span></div></div><div><div><div>29</div></div><div><span>                        </span><span>"g1"</span><span>,</span></div></div><div><div><div>30</div></div><div><span>                        </span><span>record</span><span>.</span><span>getId</span><span>()</span></div></div><div><div><div>31</div></div><div><span><span>                </span></span><span>);</span></div></div><div><div><div>32</div></div><div><span><span>            </span></span><span>} </span><span>catch</span><span> (</span><span>Exception</span><span><span> </span><span>e</span><span>) {</span></span></div></div><div><div><div>33</div></div><div><span>                </span><span>log</span><span>.</span><span>error</span><span>(</span><span>"处理订单异常"</span><span>, e);</span></div></div><div><div><div>34</div></div><div><span>                </span><span>handlePendingList</span><span>();</span></div></div><div><div><div>35</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>36</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>37</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>38</div></div><div>
</div></div><div><div><div>39</div></div><div><span>    </span><span>private</span><span> </span><span>void</span><span> </span><span>handlePendingList</span><span><span>()</span><span> </span><span>{</span></span></div></div><div><div><div>40</div></div><div><span>        </span><span>while</span><span> (</span><span>true</span><span>) {</span></div></div><div><div><div>41</div></div><div><span>            </span><span>try</span><span> {</span></div></div><div><div><div>42</div></div><div><span>                </span><span>List</span><span>&lt;</span><span>MapRecord</span><span>&lt;</span><span>String</span><span>, </span><span>Object</span><span>, </span><span>Object</span><span><span>&gt;&gt; </span><span>list</span><span> </span></span><span>=</span></div></div><div><div><div>43</div></div><div><span>                        </span><span>stringRedisTemplate</span><span>.</span><span>opsForStream</span><span>().</span><span>read</span><span>(</span></div></div><div><div><div>44</div></div><div><span>                                </span><span>Consumer</span><span>.</span><span>from</span><span>(</span><span>"g1"</span><span>, </span><span>"c1"</span><span>),</span></div></div><div><div><div>45</div></div><div><span>                                </span><span>StreamReadOptions</span><span>.</span><span>empty</span><span>().</span><span>count</span><span>(</span><span>1</span><span>),</span></div></div><div><div><div>46</div></div><div><span>                                </span><span>StreamOffset</span><span>.</span><span>create</span><span>(</span><span>"stream.orders"</span><span>, </span><span>ReadOffset</span><span>.</span><span>from</span><span>(</span><span>"0"</span><span>))</span></div></div><div><div><div>47</div></div><div><span><span>                        </span></span><span>);</span></div></div><div><div><div>48</div></div><div>
</div></div><div><div><div>49</div></div><div><span>                </span><span>if</span><span><span> (list </span><span>==</span><span> </span></span><span>null</span><span><span> </span><span>||</span><span> </span></span><span>list</span><span>.</span><span>isEmpty</span><span>()) {</span></div></div><div><div><div>50</div></div><div><span>                    </span><span>break</span><span>;</span></div></div><div><div><div>51</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>52</div></div><div>
</div></div><div><div><div>53</div></div><div><span>                </span><span>MapRecord</span><span>&lt;</span><span>String</span><span>, </span><span>Object</span><span>, </span><span>Object</span><span><span>&gt; </span><span>record</span><span> </span></span><span><span>=</span><span> </span></span><span>list</span><span>.</span><span>get</span><span>(</span><span>0</span><span>);</span></div></div><div><div><div>54</div></div><div><span>                </span><span>Map</span><span>&lt;</span><span>Object</span><span>, </span><span>Object</span><span><span>&gt; </span><span>value</span><span> </span></span><span><span>=</span><span> </span></span><span>record</span><span>.</span><span>getValue</span><span>();</span></div></div><div><div><div>55</div></div><div><span>                </span><span>VoucherOrder</span><span><span> </span><span>voucherOrder</span><span> </span></span><span><span>=</span><span> </span></span><span>BeanUtil</span><span>.</span><span>fillBeanWithMap</span><span>(</span></div></div><div><div><div>56</div></div><div><span><span>                        </span></span><span>value,</span></div></div><div><div><div>57</div></div><div><span>                        </span><span>new</span><span> </span><span>VoucherOrder</span><span>(),</span></div></div><div><div><div>58</div></div><div><span>                        </span><span>true</span></div></div><div><div><div>59</div></div><div><span><span>                </span></span><span>);</span></div></div><div><div><div>60</div></div><div>
</div></div><div><div><div>61</div></div><div><span>                </span><span>handleVoucherOrder</span><span>(voucherOrder);</span></div></div><div><div><div>62</div></div><div>
</div></div><div><div><div>63</div></div><div><span>                </span><span>stringRedisTemplate</span><span>.</span><span>opsForStream</span><span>().</span><span>acknowledge</span><span>(</span></div></div><div><div><div>64</div></div><div><span>                        </span><span>"stream.orders"</span><span>,</span></div></div><div><div><div>65</div></div><div><span>                        </span><span>"g1"</span><span>,</span></div></div><div><div><div>66</div></div><div><span>                        </span><span>record</span><span>.</span><span>getId</span><span>()</span></div></div><div><div><div>67</div></div><div><span><span>                </span></span><span>);</span></div></div><div><div><div>68</div></div><div><span><span>            </span></span><span>} </span><span>catch</span><span> (</span><span>Exception</span><span><span> </span><span>e</span><span>) {</span></span></div></div><div><div><div>69</div></div><div><span>                </span><span>log</span><span>.</span><span>error</span><span>(</span><span>"处理 pending-list 异常"</span><span>, e);</span></div></div><div><div><div>70</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>71</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>72</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>73</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><hr /></section></section><section><h2>六、达人探店<a href="#六达人探店"><span>#</span></a></h2><section><h3>6.1 发布探店笔记<a href="#61-发布探店笔记"><span>#</span></a></h3><p>保存探店笔记：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@</span><span>PostMapping</span></div></div><div><div><div>2</div></div><div><span>public</span><span> </span><span>Result</span><span> </span><span>saveBlog</span><span><span>(</span><span>@</span></span><span>RequestBody</span><span> </span><span>Blog</span><span> blog) {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>UserDTO</span><span> user </span><span><span>=</span><span> </span></span><span>UserHolder</span><span>.</span><span>getUser</span><span>();</span></div></div><div><div><div>4</div></div><div><span>    </span><span>blog</span><span>.</span><span>setUserId</span><span>(</span><span>user</span><span>.</span><span>getId</span><span>());</span></div></div><div><div><div>5</div></div><div><span>    </span><span>blogService</span><span>.</span><span>save</span><span>(blog);</span></div></div><div><div><div>6</div></div><div><span>    </span><span>return</span><span> </span><span>Result</span><span>.</span><span>ok</span><span>(</span><span>blog</span><span>.</span><span>getId</span><span>());</span></div></div><div><div><div>7</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>上传图片：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@</span><span>PostMapping</span><span>(</span><span>"blog"</span><span>)</span></div></div><div><div><div>2</div></div><div><span>public</span><span> </span><span>Result</span><span> </span><span>uploadImage</span><span><span>(</span><span>@</span></span><span>RequestParam</span><span>(</span><span>"file"</span><span>) </span><span>MultipartFile</span><span> image) {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>try</span><span> {</span></div></div><div><div><div>4</div></div><div><span>        </span><span>String</span><span> originalFilename </span><span><span>=</span><span> </span></span><span>image</span><span>.</span><span>getOriginalFilename</span><span>();</span></div></div><div><div><div>5</div></div><div><span>        </span><span>String</span><span> fileName </span><span><span>=</span><span> </span></span><span>createNewFileName</span><span><span>(originalFilename)</span><span>;</span></span></div></div><div><div><div>6</div></div><div><span>        </span><span>image</span><span>.</span><span>transferTo</span><span>(</span><span>new</span><span> </span><span>File</span><span>(</span><span>SystemConstants</span><span>.</span><span>IMAGE_UPLOAD_DIR</span><span>, fileName));</span></div></div><div><div><div>7</div></div><div><span>        </span><span>return</span><span> </span><span>Result</span><span>.</span><span>ok</span><span>(fileName);</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>} </span><span>catch</span><span> (</span><span>IOException</span><span><span> </span><span>e</span><span>) {</span></span></div></div><div><div><div>9</div></div><div><span>        </span><span>throw</span><span> </span><span>new</span><span> </span><span>RuntimeException</span><span>(</span><span>"文件上传失败"</span><span><span>,</span><span> e)</span><span>;</span></span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>11</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>6.2 查看探店笔记<a href="#62-查看探店笔记"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@</span><span>GetMapping</span><span>(</span><span>"/{id}"</span><span>)</span></div></div><div><div><div>2</div></div><div><span>public</span><span> </span><span>Result</span><span> </span><span>queryById</span><span><span>(</span><span>@</span></span><span>PathVariable</span><span> </span><span>Integer</span><span> id) {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>return</span><span> </span><span>blogService</span><span>.</span><span>queryById</span><span>(id);</span></div></div><div><div><div>4</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>Service：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@</span><span>Override</span></div></div><div><div><div>2</div></div><div><span>public</span><span> </span><span>Result</span><span> </span><span>queryById</span><span>(</span><span>Integer</span><span> id) {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>Blog</span><span> blog </span><span><span>=</span><span> </span></span><span>getById</span><span><span>(id)</span><span>;</span></span></div></div><div><div><div>4</div></div><div><span>    </span><span>if</span><span><span> (blog </span><span>==</span><span> </span></span><span>null</span><span>) {</span></div></div><div><div><div>5</div></div><div><span>        </span><span>return</span><span> </span><span>Result</span><span>.</span><span>fail</span><span>(</span><span>"评价不存在或已被删除"</span><span>);</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>7</div></div><div><span>    </span><span>queryBlogUser</span><span><span>(blog)</span><span>;</span></span></div></div><div><div><div>8</div></div><div><span>    </span><span>isBlogLiked</span><span><span>(blog)</span><span>;</span></span></div></div><div><div><div>9</div></div><div><span>    </span><span>return</span><span> </span><span>Result</span><span>.</span><span>ok</span><span>(blog);</span></div></div><div><div><div>10</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>6.3 点赞功能<a href="#63-点赞功能"><span>#</span></a></h3><p>最初实现只是简单给点赞数 +1，会导致用户无限点赞。</p><p>使用 Redis Set 或 SortedSet 判断是否已点赞。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@</span><span>Override</span></div></div><div><div><div>2</div></div><div><span>public</span><span> </span><span>Result</span><span> </span><span>likeBlog</span><span>(</span><span>Long</span><span> id) {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>Long</span><span> userId </span><span><span>=</span><span> </span></span><span>UserHolder</span><span>.</span><span>getUser</span><span>().</span><span>getId</span><span>();</span></div></div><div><div><div>4</div></div><div><span>    </span><span>String</span><span> key </span><span><span>=</span><span> BLOG_LIKED_KEY </span><span>+</span><span> id</span><span>;</span></span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>    </span><span>Double</span><span> score </span><span><span>=</span><span> </span></span><span>stringRedisTemplate</span><span>.</span><span>opsForZSet</span><span>().</span><span>score</span><span>(key, </span><span>userId</span><span>.</span><span>toString</span><span>());</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>    </span><span>if</span><span><span> (score </span><span>==</span><span> </span></span><span>null</span><span>) {</span></div></div><div><div><div>9</div></div><div><span>        </span><span>boolean</span><span> success </span><span><span>=</span><span> </span></span><span>update</span><span>()</span></div></div><div><div><div>10</div></div><div><span><span>                </span></span><span>.</span><span>setSql</span><span>(</span><span>"liked = liked + 1"</span><span>)</span></div></div><div><div><div>11</div></div><div><span><span>                </span></span><span>.</span><span>eq</span><span>(</span><span>"id"</span><span>, id)</span></div></div><div><div><div>12</div></div><div><span><span>                </span></span><span>.</span><span>update</span><span>();</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>        </span><span>if</span><span> (success) {</span></div></div><div><div><div>15</div></div><div><span>            </span><span>stringRedisTemplate</span><span>.</span><span>opsForZSet</span><span>().</span><span>add</span><span>(</span></div></div><div><div><div>16</div></div><div><span><span>                    </span></span><span>key,</span></div></div><div><div><div>17</div></div><div><span>                    </span><span>userId</span><span>.</span><span>toString</span><span>(),</span></div></div><div><div><div>18</div></div><div><span>                    </span><span>System</span><span>.</span><span>currentTimeMillis</span><span>()</span></div></div><div><div><div>19</div></div><div><span><span>            </span></span><span>);</span></div></div><div><div><div>20</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>22</div></div><div><span>        </span><span>boolean</span><span> success </span><span><span>=</span><span> </span></span><span>update</span><span>()</span></div></div><div><div><div>23</div></div><div><span><span>                </span></span><span>.</span><span>setSql</span><span>(</span><span>"liked = liked - 1"</span><span>)</span></div></div><div><div><div>24</div></div><div><span><span>                </span></span><span>.</span><span>eq</span><span>(</span><span>"id"</span><span>, id)</span></div></div><div><div><div>25</div></div><div><span><span>                </span></span><span>.</span><span>update</span><span>();</span></div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div><span>        </span><span>if</span><span> (success) {</span></div></div><div><div><div>28</div></div><div><span>            </span><span>stringRedisTemplate</span><span>.</span><span>opsForZSet</span><span>().</span><span>remove</span><span>(key, </span><span>userId</span><span>.</span><span>toString</span><span>());</span></div></div><div><div><div>29</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>30</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>31</div></div><div>
</div></div><div><div><div>32</div></div><div><span>    </span><span>return</span><span> </span><span>Result</span><span>.</span><span>ok</span><span>();</span></div></div><div><div><div>33</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>判断当前用户是否点赞：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>private</span><span> </span><span>void</span><span> </span><span>isBlogLiked</span><span>(</span><span>Blog</span><span> blog) {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>UserDTO</span><span> userDTO </span><span><span>=</span><span> </span></span><span>UserHolder</span><span>.</span><span>getUser</span><span>();</span></div></div><div><div><div>3</div></div><div><span>    </span><span>if</span><span><span> (userDTO </span><span>==</span><span> </span></span><span>null</span><span>) {</span></div></div><div><div><div>4</div></div><div><span>        </span><span>return</span><span>;</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>6</div></div><div><span>    </span><span>String</span><span> key </span><span><span>=</span><span> BLOG_LIKED_KEY </span><span>+</span><span> </span></span><span>blog</span><span>.</span><span>getId</span><span>();</span></div></div><div><div><div>7</div></div><div><span>    </span><span>Double</span><span> score </span><span><span>=</span><span> </span></span><span>stringRedisTemplate</span><span>.</span><span>opsForZSet</span><span>()</span></div></div><div><div><div>8</div></div><div><span><span>            </span></span><span>.</span><span>score</span><span>(key, </span><span>userDTO</span><span>.</span><span>getId</span><span>().</span><span>toString</span><span>());</span></div></div><div><div><div>9</div></div><div><span>    </span><span>blog</span><span>.</span><span>setIsLike</span><span><span>(score </span><span>!=</span><span> </span></span><span>null</span><span>);</span></div></div><div><div><div>10</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>6.4 点赞排行榜<a href="#64-点赞排行榜"><span>#</span></a></h3><p>查询点赞 Top5：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@</span><span>GetMapping</span><span>(</span><span>"/likes/{id}"</span><span>)</span></div></div><div><div><div>2</div></div><div><span>public</span><span> </span><span>Result</span><span> </span><span>queryBlogLikes</span><span><span>(</span><span>@</span></span><span>PathVariable</span><span> </span><span>Integer</span><span> id) {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>return</span><span> </span><span>blogService</span><span>.</span><span>queryBlogLikes</span><span>(id);</span></div></div><div><div><div>4</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>Service：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@</span><span>Override</span></div></div><div><div><div>2</div></div><div><span>public</span><span> </span><span>Result</span><span> </span><span>queryBlogLikes</span><span>(</span><span>Integer</span><span> id) {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>String</span><span> key </span><span><span>=</span><span> BLOG_LIKED_KEY </span><span>+</span><span> id</span><span>;</span></span></div></div><div><div><div>4</div></div><div><span>    </span><span>Set</span><span>&lt;</span><span>String</span><span><span>&gt;</span><span> top5 </span></span><span><span>=</span><span> </span></span><span>stringRedisTemplate</span><span>.</span><span>opsForZSet</span><span>().</span><span>range</span><span>(key, </span><span>0</span><span>, </span><span>4</span><span>);</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>    </span><span>if</span><span><span> (top5 </span><span>==</span><span> </span></span><span>null</span><span><span> </span><span>||</span><span> </span></span><span>top5</span><span>.</span><span>isEmpty</span><span><span>()</span><span>) {</span></span></div></div><div><div><div>7</div></div><div><span>        </span><span>return</span><span> </span><span>Result</span><span>.</span><span>ok</span><span>(</span><span>Collections</span><span>.</span><span>emptyList</span><span>());</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>    </span><span>List</span><span>&lt;</span><span>Long</span><span><span>&gt;</span><span> ids </span></span><span><span>=</span><span> </span></span><span>top5</span><span>.</span><span>stream</span><span>().</span><span>map</span><span>(Long</span><span>::</span><span>valueOf).</span><span>collect</span><span>(</span><span>Collectors</span><span>.</span><span>toList</span><span>());</span></div></div><div><div><div>11</div></div><div><span>    </span><span>String</span><span> idsStr </span><span><span>=</span><span> </span></span><span>StrUtil</span><span>.</span><span>join</span><span>(</span><span>","</span><span>, ids);</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>    </span><span>List</span><span>&lt;</span><span>UserDTO</span><span><span>&gt;</span><span> userDTOS </span></span><span><span>=</span><span> </span></span><span>userService</span><span>.</span><span>query</span><span>()</span></div></div><div><div><div>14</div></div><div><span><span>            </span></span><span>.</span><span>in</span><span>(</span><span>"id"</span><span>, ids)</span></div></div><div><div><div>15</div></div><div><span><span>            </span></span><span>.</span><span>last</span><span>(</span><span>"order by field(id,"</span><span><span> </span><span>+</span><span> idsStr </span><span>+</span><span> </span></span><span>")"</span><span>)</span></div></div><div><div><div>16</div></div><div><span><span>            </span></span><span>.</span><span>list</span><span>()</span></div></div><div><div><div>17</div></div><div><span><span>            </span></span><span>.</span><span>stream</span><span>()</span></div></div><div><div><div>18</div></div><div><span><span>            </span></span><span>.</span><span>map</span><span>(user </span><span>-&gt;</span><span> </span><span>BeanUtil</span><span>.</span><span>copyProperties</span><span>(user, </span><span>UserDTO</span><span>.</span><span>class</span><span>))</span></div></div><div><div><div>19</div></div><div><span><span>            </span></span><span>.</span><span>collect</span><span>(</span><span>Collectors</span><span>.</span><span>toList</span><span>());</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span>    </span><span>return</span><span> </span><span>Result</span><span>.</span><span>ok</span><span>(userDTOS);</span></div></div><div><div><div>22</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><hr /></section></section><section><h2>七、好友关注与 Feed 流<a href="#七好友关注与-feed-流"><span>#</span></a></h2><section><h3>7.1 关注与取消关注<a href="#71-关注与取消关注"><span>#</span></a></h3><p>判断是否关注：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@</span><span>GetMapping</span><span>(</span><span>"/or/not/{id}"</span><span>)</span></div></div><div><div><div>2</div></div><div><span>public</span><span> </span><span>Result</span><span> </span><span>isFollow</span><span><span>(</span><span>@</span></span><span>PathVariable</span><span>(</span><span>"id"</span><span>) </span><span>Long</span><span> followUserId) {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>return</span><span> </span><span>followService</span><span>.</span><span>isFollow</span><span>(followUserId);</span></div></div><div><div><div>4</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>关注/取关：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@</span><span>PutMapping</span><span>(</span><span>"/{id}/{isFollow}"</span><span>)</span></div></div><div><div><div>2</div></div><div><span>public</span><span> </span><span>Result</span><span> </span><span>follow</span><span>(</span></div></div><div><div><div>3</div></div><div><span><span>        </span></span><span>@</span><span>PathVariable</span><span>(</span><span>"id"</span><span>) </span><span>Long</span><span><span> followUserId</span><span>,</span></span></div></div><div><div><div>4</div></div><div><span><span>        </span></span><span>@</span><span>PathVariable</span><span>(</span><span>"isFollow"</span><span>) </span><span>Boolean</span><span> isFollow</span></div></div><div><div><div>5</div></div><div><span>) {</span></div></div><div><div><div>6</div></div><div><span>    </span><span>return</span><span> </span><span>followService</span><span>.</span><span>follow</span><span>(followUserId, isFollow);</span></div></div><div><div><div>7</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>Service：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@</span><span>Override</span></div></div><div><div><div>2</div></div><div><span>public</span><span> </span><span>Result</span><span> </span><span>isFollow</span><span>(</span><span>Long</span><span> followUserId) {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>Long</span><span> userId </span><span><span>=</span><span> </span></span><span>UserHolder</span><span>.</span><span>getUser</span><span>().</span><span>getId</span><span>();</span></div></div><div><div><div>4</div></div><div><span>    </span><span>LambdaQueryWrapper</span><span>&lt;</span><span>Follow</span><span><span>&gt;</span><span> queryWrapper </span></span><span><span>=</span><span> </span></span><span>new</span><span> </span><span>LambdaQueryWrapper</span><span><span>&lt;&gt;</span><span>()</span><span>;</span></span></div></div><div><div><div>5</div></div><div><span>    </span><span>queryWrapper</span><span>.</span><span>eq</span><span>(Follow</span><span>::</span><span>getUserId, userId)</span></div></div><div><div><div>6</div></div><div><span><span>            </span></span><span>.</span><span>eq</span><span>(Follow</span><span>::</span><span>getFollowUserId, followUserId);</span></div></div><div><div><div>7</div></div><div><span>    </span><span>int</span><span> count </span><span><span>=</span><span> </span></span><span>count</span><span><span>(queryWrapper)</span><span>;</span></span></div></div><div><div><div>8</div></div><div><span>    </span><span>return</span><span> </span><span>Result</span><span>.</span><span>ok</span><span><span>(count </span><span>&gt;</span><span> </span></span><span>0</span><span>);</span></div></div><div><div><div>9</div></div><div><span>}</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>@</span><span>Override</span></div></div><div><div><div>12</div></div><div><span>public</span><span> </span><span>Result</span><span> </span><span>follow</span><span>(</span><span>Long</span><span><span> followUserId</span><span>,</span><span> </span></span><span>Boolean</span><span> isFollow) {</span></div></div><div><div><div>13</div></div><div><span>    </span><span>Long</span><span> userId </span><span><span>=</span><span> </span></span><span>UserHolder</span><span>.</span><span>getUser</span><span>().</span><span>getId</span><span>();</span></div></div><div><div><div>14</div></div><div><span>    </span><span>String</span><span> key </span><span><span>=</span><span> </span></span><span>"follows:"</span><span><span> </span><span>+</span><span> userId</span><span>;</span></span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>    </span><span>if</span><span> (isFollow) {</span></div></div><div><div><div>17</div></div><div><span>        </span><span>Follow</span><span> follow </span><span><span>=</span><span> </span></span><span>new</span><span> </span><span>Follow</span><span><span>()</span><span>;</span></span></div></div><div><div><div>18</div></div><div><span>        </span><span>follow</span><span>.</span><span>setUserId</span><span>(userId);</span></div></div><div><div><div>19</div></div><div><span>        </span><span>follow</span><span>.</span><span>setFollowUserId</span><span>(followUserId);</span></div></div><div><div><div>20</div></div><div><span>        </span><span>boolean</span><span> success </span><span><span>=</span><span> </span></span><span>save</span><span><span>(follow)</span><span>;</span></span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>        </span><span>if</span><span> (success) {</span></div></div><div><div><div>23</div></div><div><span>            </span><span>stringRedisTemplate</span><span>.</span><span>opsForSet</span><span>().</span><span>add</span><span>(key, </span><span>followUserId</span><span>.</span><span>toString</span><span>());</span></div></div><div><div><div>24</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>26</div></div><div><span>        </span><span>LambdaQueryWrapper</span><span>&lt;</span><span>Follow</span><span><span>&gt;</span><span> queryWrapper </span></span><span><span>=</span><span> </span></span><span>new</span><span> </span><span>LambdaQueryWrapper</span><span><span>&lt;&gt;</span><span>()</span><span>;</span></span></div></div><div><div><div>27</div></div><div><span>        </span><span>queryWrapper</span><span>.</span><span>eq</span><span>(Follow</span><span>::</span><span>getUserId, userId)</span></div></div><div><div><div>28</div></div><div><span><span>                </span></span><span>.</span><span>eq</span><span>(Follow</span><span>::</span><span>getFollowUserId, followUserId);</span></div></div><div><div><div>29</div></div><div><span>        </span><span>boolean</span><span> success </span><span><span>=</span><span> </span></span><span>remove</span><span><span>(queryWrapper)</span><span>;</span></span></div></div><div><div><div>30</div></div><div>
</div></div><div><div><div>31</div></div><div><span>        </span><span>if</span><span> (success) {</span></div></div><div><div><div>32</div></div><div><span>            </span><span>stringRedisTemplate</span><span>.</span><span>opsForSet</span><span>().</span><span>remove</span><span>(key, </span><span>followUserId</span><span>.</span><span>toString</span><span>());</span></div></div><div><div><div>33</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>34</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>35</div></div><div>
</div></div><div><div><div>36</div></div><div><span>    </span><span>return</span><span> </span><span>Result</span><span>.</span><span>ok</span><span>();</span></div></div><div><div><div>37</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section><section><h3>7.2 共同关注<a href="#72-共同关注"><span>#</span></a></h3><p>利用 Redis Set 求交集：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@</span><span>GetMapping</span><span>(</span><span>"/common/{id}"</span><span>)</span></div></div><div><div><div>2</div></div><div><span>public</span><span> </span><span>Result</span><span> </span><span>followCommons</span><span><span>(</span><span>@</span></span><span>PathVariable</span><span> </span><span>Long</span><span> id) {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>return</span><span> </span><span>followService</span><span>.</span><span>followCommons</span><span>(id);</span></div></div><div><div><div>4</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>Service：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@</span><span>Override</span></div></div><div><div><div>2</div></div><div><span>public</span><span> </span><span>Result</span><span> </span><span>followCommons</span><span>(</span><span>Long</span><span> id) {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>Long</span><span> userId </span><span><span>=</span><span> </span></span><span>UserHolder</span><span>.</span><span>getUser</span><span>().</span><span>getId</span><span>();</span></div></div><div><div><div>4</div></div><div><span>    </span><span>String</span><span> key1 </span><span><span>=</span><span> </span></span><span>"follows:"</span><span><span> </span><span>+</span><span> userId</span><span>;</span></span></div></div><div><div><div>5</div></div><div><span>    </span><span>String</span><span> key2 </span><span><span>=</span><span> </span></span><span>"follows:"</span><span><span> </span><span>+</span><span> id</span><span>;</span></span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>    </span><span>Set</span><span>&lt;</span><span>String</span><span><span>&gt;</span><span> intersect </span></span><span><span>=</span><span> </span></span><span>stringRedisTemplate</span><span>.</span><span>opsForSet</span><span>().</span><span>intersect</span><span>(key1, key2);</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>    </span><span>if</span><span><span> (intersect </span><span>==</span><span> </span></span><span>null</span><span><span> </span><span>||</span><span> </span></span><span>intersect</span><span>.</span><span>isEmpty</span><span><span>()</span><span>) {</span></span></div></div><div><div><div>10</div></div><div><span>        </span><span>return</span><span> </span><span>Result</span><span>.</span><span>ok</span><span>(</span><span>Collections</span><span>.</span><span>emptyList</span><span>());</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>    </span><span>List</span><span>&lt;</span><span>Long</span><span><span>&gt;</span><span> ids </span></span><span><span>=</span><span> </span></span><span>intersect</span><span>.</span><span>stream</span><span>().</span><span>map</span><span>(Long</span><span>::</span><span>valueOf).</span><span>collect</span><span>(</span><span>Collectors</span><span>.</span><span>toList</span><span>());</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>    </span><span>List</span><span>&lt;</span><span>UserDTO</span><span><span>&gt;</span><span> users </span></span><span><span>=</span><span> </span></span><span>userService</span><span>.</span><span>listByIds</span><span>(ids)</span></div></div><div><div><div>16</div></div><div><span><span>            </span></span><span>.</span><span>stream</span><span>()</span></div></div><div><div><div>17</div></div><div><span><span>            </span></span><span>.</span><span>map</span><span>(user </span><span>-&gt;</span><span> </span><span>BeanUtil</span><span>.</span><span>copyProperties</span><span>(user, </span><span>UserDTO</span><span>.</span><span>class</span><span>))</span></div></div><div><div><div>18</div></div><div><span><span>            </span></span><span>.</span><span>collect</span><span>(</span><span>Collectors</span><span>.</span><span>toList</span><span>());</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>    </span><span>return</span><span> </span><span>Result</span><span>.</span><span>ok</span><span>(users);</span></div></div><div><div><div>21</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section><section><h3>7.3 Feed 流<a href="#73-feed-流"><span>#</span></a></h3><p>Feed 流有两种模式：</p><ul>
<li><strong>Timeline</strong>：按发布时间排序，适合好友/关注场景。</li>
<li><strong>智能排序</strong>：根据算法推荐用户感兴趣的内容。</li>
</ul><p>Timeline 三种实现方案：</p>

<table><thead><tr><th>方案</th><th>说明</th><th>优点</th><th>缺点</th></tr></thead><tbody><tr><td>拉模式</td><td>读时拉取关注人的发件箱</td><td>节约空间</td><td>有延迟，关注多时压力大</td></tr><tr><td>推模式</td><td>写时推送到粉丝收件箱</td><td>时效快</td><td>内存压力大</td></tr><tr><td>推拉结合</td><td>普通人推、大V拉</td><td>折中</td><td>实现复杂</td></tr></tbody></table></section><section><h3>7.4 推送到粉丝收件箱<a href="#74-推送到粉丝收件箱"><span>#</span></a></h3><p>保存笔记后，推送到粉丝的 SortedSet：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@</span><span>Override</span></div></div><div><div><div>2</div></div><div><span>public</span><span> </span><span>Result</span><span> </span><span>saveBlog</span><span>(</span><span>Blog</span><span> blog) {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>UserDTO</span><span> user </span><span><span>=</span><span> </span></span><span>UserHolder</span><span>.</span><span>getUser</span><span>();</span></div></div><div><div><div>4</div></div><div><span>    </span><span>blog</span><span>.</span><span>setUserId</span><span>(</span><span>user</span><span>.</span><span>getId</span><span>());</span></div></div><div><div><div>5</div></div><div><span>    </span><span>save</span><span><span>(blog)</span><span>;</span></span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>    </span><span>LambdaQueryWrapper</span><span>&lt;</span><span>Follow</span><span><span>&gt;</span><span> queryWrapper </span></span><span><span>=</span><span> </span></span><span>new</span><span> </span><span>LambdaQueryWrapper</span><span><span>&lt;&gt;</span><span>()</span><span>;</span></span></div></div><div><div><div>8</div></div><div><span>    </span><span>queryWrapper</span><span>.</span><span>eq</span><span>(Follow</span><span>::</span><span>getFollowUserId, </span><span>user</span><span>.</span><span>getId</span><span>());</span></div></div><div><div><div>9</div></div><div><span>    </span><span>List</span><span>&lt;</span><span>Follow</span><span><span>&gt;</span><span> follows </span></span><span><span>=</span><span> </span></span><span>followService</span><span>.</span><span>list</span><span>(queryWrapper);</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>    </span><span>for</span><span> (</span><span>Follow</span><span> follow </span><span>:</span><span> follows) {</span></div></div><div><div><div>12</div></div><div><span>        </span><span>Long</span><span> userId </span><span><span>=</span><span> </span></span><span>follow</span><span>.</span><span>getUserId</span><span>();</span></div></div><div><div><div>13</div></div><div><span>        </span><span>String</span><span> key </span><span><span>=</span><span> FEED_KEY </span><span>+</span><span> userId</span><span>;</span></span></div></div><div><div><div>14</div></div><div><span>        </span><span>stringRedisTemplate</span><span>.</span><span>opsForZSet</span><span>().</span><span>add</span><span>(</span></div></div><div><div><div>15</div></div><div><span><span>                </span></span><span>key,</span></div></div><div><div><div>16</div></div><div><span>                </span><span>blog</span><span>.</span><span>getId</span><span>().</span><span>toString</span><span>(),</span></div></div><div><div><div>17</div></div><div><span>                </span><span>System</span><span>.</span><span>currentTimeMillis</span><span>()</span></div></div><div><div><div>18</div></div><div><span><span>        </span></span><span>);</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span>    </span><span>return</span><span> </span><span>Result</span><span>.</span><span>ok</span><span>(</span><span>blog</span><span>.</span><span>getId</span><span>());</span></div></div><div><div><div>22</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section><section><h3>7.5 滚动分页查询收件箱<a href="#75-滚动分页查询收件箱"><span>#</span></a></h3><p>Feed 流数据不断更新，不能使用传统分页，需要使用滚动分页。</p><p>请求参数：</p><ul>
<li><code>lastId</code>：上一次查询的最小时间戳。</li>
<li><code>offset</code>：偏移量。</li>
</ul><p>通用结果类：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@</span><span>Data</span></div></div><div><div><div>2</div></div><div><span>public</span><span> </span><span>class</span><span><span> </span><span>ScrollResult</span><span> </span></span><span>{</span></div></div><div><div><div>3</div></div><div><span>    </span><span>private</span><span> </span><span>List</span><span>&lt;</span><span>?</span><span><span>&gt;</span><span> list</span></span><span>;</span></div></div><div><div><div>4</div></div><div><span>    </span><span>private</span><span> </span><span>Long</span><span> minTime</span><span>;</span></div></div><div><div><div>5</div></div><div><span>    </span><span>private</span><span> </span><span>Integer</span><span> offset</span><span>;</span></div></div><div><div><div>6</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>Controller：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@</span><span>GetMapping</span><span>(</span><span>"/of/follow"</span><span>)</span></div></div><div><div><div>2</div></div><div><span>public</span><span> </span><span>Result</span><span> </span><span>queryBlogOfFollow</span><span>(</span></div></div><div><div><div>3</div></div><div><span><span>        </span></span><span>@</span><span>RequestParam</span><span>(</span><span>"lastId"</span><span>) </span><span>Long</span><span><span> max</span><span>,</span></span></div></div><div><div><div>4</div></div><div><span><span>        </span></span><span>@</span><span>RequestParam</span><span>(</span><span>value</span><span><span> </span><span>=</span><span> </span></span><span>"offset"</span><span><span>,</span><span> </span></span><span>defaultValue</span><span><span> </span><span>=</span><span> </span></span><span>"0"</span><span>) </span><span>Integer</span><span> offset</span></div></div><div><div><div>5</div></div><div><span>) {</span></div></div><div><div><div>6</div></div><div><span>    </span><span>return</span><span> </span><span>blogService</span><span>.</span><span>queryBlogOfFollow</span><span>(max, offset);</span></div></div><div><div><div>7</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>Service：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@</span><span>Override</span></div></div><div><div><div>2</div></div><div><span>public</span><span> </span><span>Result</span><span> </span><span>queryBlogOfFollow</span><span>(</span><span>Long</span><span><span> max</span><span>,</span><span> </span></span><span>Integer</span><span> offset) {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>Long</span><span> userId </span><span><span>=</span><span> </span></span><span>UserHolder</span><span>.</span><span>getUser</span><span>().</span><span>getId</span><span>();</span></div></div><div><div><div>4</div></div><div><span>    </span><span>String</span><span> key </span><span><span>=</span><span> FEED_KEY </span><span>+</span><span> userId</span><span>;</span></span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>    </span><span>Set</span><span>&lt;</span><span>ZSetOperations</span><span>.</span><span>TypedTuple</span><span>&lt;</span><span>String</span><span><span>&gt;&gt;</span><span> typedTuples </span></span><span>=</span></div></div><div><div><div>7</div></div><div><span>            </span><span>stringRedisTemplate</span><span>.</span><span>opsForZSet</span><span>()</span></div></div><div><div><div>8</div></div><div><span><span>                    </span></span><span>.</span><span>reverseRangeByScoreWithScores</span><span>(key, </span><span>0</span><span>, max, offset, </span><span>2</span><span>);</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>    </span><span>if</span><span><span> (typedTuples </span><span>==</span><span> </span></span><span>null</span><span><span> </span><span>||</span><span> </span></span><span>typedTuples</span><span>.</span><span>isEmpty</span><span><span>()</span><span>) {</span></span></div></div><div><div><div>11</div></div><div><span>        </span><span>return</span><span> </span><span>Result</span><span>.</span><span>ok</span><span>(</span><span>Collections</span><span>.</span><span>emptyList</span><span>());</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>    </span><span>List</span><span>&lt;</span><span>Long</span><span><span>&gt;</span><span> ids </span></span><span><span>=</span><span> </span></span><span>new</span><span> </span><span>ArrayList</span><span><span>&lt;&gt;</span><span>(</span></span><span>typedTuples</span><span>.</span><span>size</span><span><span>()</span><span>)</span><span>;</span></span></div></div><div><div><div>15</div></div><div><span>    </span><span>long</span><span> minTime </span><span><span>=</span><span> </span></span><span>0</span><span>;</span></div></div><div><div><div>16</div></div><div><span>    </span><span>int</span><span> os </span><span><span>=</span><span> </span></span><span>1</span><span>;</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>    </span><span>for</span><span> (</span><span>ZSetOperations</span><span>.</span><span>TypedTuple</span><span>&lt;</span><span>String</span><span><span>&gt;</span><span> tuple </span></span><span>:</span><span> typedTuples) {</span></div></div><div><div><div>19</div></div><div><span>        </span><span>ids</span><span>.</span><span>add</span><span>(</span><span>Long</span><span>.</span><span>valueOf</span><span>(</span><span>tuple</span><span>.</span><span>getValue</span><span>()));</span></div></div><div><div><div>20</div></div><div><span>        </span><span>long</span><span> time </span><span><span>=</span><span> </span></span><span>tuple</span><span>.</span><span>getScore</span><span>().</span><span>longValue</span><span>();</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>        </span><span>if</span><span><span> (time </span><span>==</span><span> minTime) {</span></span></div></div><div><div><div>23</div></div><div><span><span>            </span></span><span>os</span><span>++;</span></div></div><div><div><div>24</div></div><div><span><span>        </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>25</div></div><div><span><span>            </span></span><span>minTime </span><span>=</span><span> time</span><span>;</span></div></div><div><div><div>26</div></div><div><span><span>            </span></span><span>os </span><span>=</span><span> </span><span>1</span><span>;</span></div></div><div><div><div>27</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>29</div></div><div>
</div></div><div><div><div>30</div></div><div><span>    </span><span>String</span><span> idStr </span><span><span>=</span><span> </span></span><span>StrUtil</span><span>.</span><span>join</span><span>(</span><span>","</span><span>, ids);</span></div></div><div><div><div>31</div></div><div><span>    </span><span>List</span><span>&lt;</span><span>Blog</span><span><span>&gt;</span><span> blogs </span></span><span><span>=</span><span> </span></span><span>query</span><span>()</span></div></div><div><div><div>32</div></div><div><span><span>            </span></span><span>.</span><span>in</span><span>(</span><span>"id"</span><span>, ids)</span></div></div><div><div><div>33</div></div><div><span><span>            </span></span><span>.</span><span>last</span><span>(</span><span>"order by field(id,"</span><span><span> </span><span>+</span><span> idStr </span><span>+</span><span> </span></span><span>")"</span><span>)</span></div></div><div><div><div>34</div></div><div><span><span>            </span></span><span>.</span><span>list</span><span>();</span></div></div><div><div><div>35</div></div><div>
</div></div><div><div><div>36</div></div><div><span>    </span><span>for</span><span> (</span><span>Blog</span><span> blog </span><span>:</span><span> blogs) {</span></div></div><div><div><div>37</div></div><div><span>        </span><span>queryBlogUser</span><span><span>(blog)</span><span>;</span></span></div></div><div><div><div>38</div></div><div><span>        </span><span>isBlogLiked</span><span><span>(blog)</span><span>;</span></span></div></div><div><div><div>39</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>40</div></div><div>
</div></div><div><div><div>41</div></div><div><span>    </span><span>ScrollResult</span><span> r </span><span><span>=</span><span> </span></span><span>new</span><span> </span><span>ScrollResult</span><span><span>()</span><span>;</span></span></div></div><div><div><div>42</div></div><div><span>    </span><span>r</span><span>.</span><span>setList</span><span>(blogs);</span></div></div><div><div><div>43</div></div><div><span>    </span><span>r</span><span>.</span><span>setOffset</span><span>(os);</span></div></div><div><div><div>44</div></div><div><span>    </span><span>r</span><span>.</span><span>setMinTime</span><span>(minTime);</span></div></div><div><div><div>45</div></div><div>
</div></div><div><div><div>46</div></div><div><span>    </span><span>return</span><span> </span><span>Result</span><span>.</span><span>ok</span><span>(r);</span></div></div><div><div><div>47</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><hr /></section></section><section><h2>八、附近商户<a href="#八附近商户"><span>#</span></a></h2><section><h3>8.1 GEO 基本用法<a href="#81-geo-基本用法"><span>#</span></a></h3>

<table><thead><tr><th>命令</th><th>说明</th></tr></thead><tbody><tr><td><code>GEOADD</code></td><td>添加地理坐标</td></tr><tr><td><code>GEODIST</code></td><td>计算两点距离</td></tr><tr><td><code>GEOHASH</code></td><td>返回 Geohash 字符串</td></tr><tr><td><code>GEOPOS</code></td><td>返回坐标</td></tr><tr><td><code>GEORADIUS</code></td><td>根据圆心和半径查询（6.2 后废弃）</td></tr><tr><td><code>GEOSEARCH</code></td><td>在指定范围内搜索（6.2 新功能）</td></tr><tr><td><code>GEOSEARCHSTORE</code></td><td>搜索并存储结果</td></tr></tbody></table><p>示例：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>GEOADD</span><span> </span><span>china</span><span> </span><span>13.361389</span><span> </span><span>38.115556</span><span> </span><span>"shanghai"</span><span> </span><span>15.087269</span><span> </span><span>37.502669</span><span> </span><span>"beijing"</span></div></div><div><div><div>2</div></div><div><span>GEODIST</span><span> </span><span>china</span><span> </span><span>beijing</span><span> </span><span>shanghai</span><span> </span><span>km</span></div></div><div><div><div>3</div></div><div><span>GEOSEARCH</span><span> </span><span>china</span><span> </span><span>FROMLONLAT</span><span> </span><span>15</span><span> </span><span>37</span><span> </span><span>BYRADIUS</span><span> </span><span>200</span><span> </span><span>km</span><span> </span><span>ASC</span><span> </span><span>WITHCOORD</span><span> </span><span>WITHDIST</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>8.2 导入店铺数据到 GEO<a href="#82-导入店铺数据到-geo"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@</span><span>Test</span></div></div><div><div><div>2</div></div><div><span>public</span><span> </span><span>void</span><span> </span><span>loadShopData</span><span>() {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>List</span><span>&lt;</span><span>Shop</span><span><span>&gt;</span><span> shopList </span></span><span><span>=</span><span> </span></span><span>shopService</span><span>.</span><span>list</span><span>();</span></div></div><div><div><div>4</div></div><div><span>    </span><span>Map</span><span>&lt;</span><span>Long</span><span><span>,</span><span> </span></span><span>List</span><span>&lt;</span><span>Shop</span><span><span>&gt;&gt;</span><span> map </span></span><span><span>=</span><span> </span></span><span>shopList</span><span>.</span><span>stream</span><span>()</span></div></div><div><div><div>5</div></div><div><span><span>            </span></span><span>.</span><span>collect</span><span>(</span><span>Collectors</span><span>.</span><span>groupingBy</span><span>(Shop</span><span>::</span><span>getTypeId));</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>    </span><span>for</span><span> (</span><span>Map</span><span>.</span><span>Entry</span><span>&lt;</span><span>Long</span><span><span>,</span><span> </span></span><span>List</span><span>&lt;</span><span>Shop</span><span><span>&gt;&gt;</span><span> entry </span></span><span>:</span><span> </span><span>map</span><span>.</span><span>entrySet</span><span><span>()</span><span>) {</span></span></div></div><div><div><div>8</div></div><div><span>        </span><span>Long</span><span> typeId </span><span><span>=</span><span> </span></span><span>entry</span><span>.</span><span>getKey</span><span>();</span></div></div><div><div><div>9</div></div><div><span>        </span><span>List</span><span>&lt;</span><span>Shop</span><span><span>&gt;</span><span> shops </span></span><span><span>=</span><span> </span></span><span>entry</span><span>.</span><span>getValue</span><span>();</span></div></div><div><div><div>10</div></div><div><span>        </span><span>String</span><span> key </span><span><span>=</span><span> SHOP_GEO_KEY </span><span>+</span><span> typeId</span><span>;</span></span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>        </span><span>List</span><span>&lt;</span><span>RedisGeoCommands</span><span>.</span><span>GeoLocation</span><span>&lt;</span><span>String</span><span><span>&gt;&gt;</span><span> locations </span></span><span>=</span></div></div><div><div><div>13</div></div><div><span>                </span><span>new</span><span> </span><span>ArrayList</span><span><span>&lt;&gt;</span><span>(</span></span><span>shops</span><span>.</span><span>size</span><span><span>()</span><span>)</span><span>;</span></span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>        </span><span>for</span><span> (</span><span>Shop</span><span> shop </span><span>:</span><span> shops) {</span></div></div><div><div><div>16</div></div><div><span>            </span><span>locations</span><span>.</span><span>add</span><span>(</span><span>new</span><span> </span><span>RedisGeoCommands</span><span>.</span><span>GeoLocation</span><span>&lt;&gt;(</span></div></div><div><div><div>17</div></div><div><span>                    </span><span>shop</span><span>.</span><span>getId</span><span>().</span><span>toString</span><span>(),</span></div></div><div><div><div>18</div></div><div><span>                    </span><span>new</span><span> </span><span>Point</span><span>(</span><span>shop</span><span>.</span><span>getX</span><span>(), </span><span>shop</span><span>.</span><span>getY</span><span>())</span></div></div><div><div><div>19</div></div><div><span><span>            </span></span><span>));</span></div></div><div><div><div>20</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>        </span><span>stringRedisTemplate</span><span>.</span><span>opsForGeo</span><span>().</span><span>add</span><span>(key, locations);</span></div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>24</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section><section><h3>8.3 实现附近商户功能<a href="#83-实现附近商户功能"><span>#</span></a></h3><p>SpringDataRedis 2.3.9 不支持 Redis 6.2 的 <code>GEOSEARCH</code>，需要升级：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>&lt;</span><span>dependency</span><span>&gt;</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>&lt;</span><span>groupId</span><span>&gt;org.springframework.boot&lt;/</span><span>groupId</span><span>&gt;</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>&lt;</span><span>artifactId</span><span>&gt;spring-boot-starter-data-redis&lt;/</span><span>artifactId</span><span>&gt;</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>&lt;</span><span>exclusions</span><span>&gt;</span></div></div><div><div><div>5</div></div><div><span><span>        </span></span><span>&lt;</span><span>exclusion</span><span>&gt;</span></div></div><div><div><div>6</div></div><div><span><span>            </span></span><span>&lt;</span><span>artifactId</span><span>&gt;spring-data-redis&lt;/</span><span>artifactId</span><span>&gt;</span></div></div><div><div><div>7</div></div><div><span><span>            </span></span><span>&lt;</span><span>groupId</span><span>&gt;org.springframework.data&lt;/</span><span>groupId</span><span>&gt;</span></div></div><div><div><div>8</div></div><div><span><span>        </span></span><span>&lt;/</span><span>exclusion</span><span>&gt;</span></div></div><div><div><div>9</div></div><div><span><span>        </span></span><span>&lt;</span><span>exclusion</span><span>&gt;</span></div></div><div><div><div>10</div></div><div><span><span>            </span></span><span>&lt;</span><span>artifactId</span><span>&gt;lettuce-core&lt;/</span><span>artifactId</span><span>&gt;</span></div></div><div><div><div>11</div></div><div><span><span>            </span></span><span>&lt;</span><span>groupId</span><span>&gt;io.lettuce&lt;/</span><span>groupId</span><span>&gt;</span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>&lt;/</span><span>exclusion</span><span>&gt;</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>&lt;/</span><span>exclusions</span><span>&gt;</span></div></div><div><div><div>14</div></div><div><span>&lt;/</span><span>dependency</span><span>&gt;</span></div></div><div><div><div>15</div></div><div><span>&lt;</span><span>dependency</span><span>&gt;</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>&lt;</span><span>groupId</span><span>&gt;org.springframework.data&lt;/</span><span>groupId</span><span>&gt;</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>&lt;</span><span>artifactId</span><span>&gt;spring-data-redis&lt;/</span><span>artifactId</span><span>&gt;</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>&lt;</span><span>version</span><span>&gt;2.6.2&lt;/</span><span>version</span><span>&gt;</span></div></div><div><div><div>19</div></div><div><span>&lt;/</span><span>dependency</span><span>&gt;</span></div></div><div><div><div>20</div></div><div><span>&lt;</span><span>dependency</span><span>&gt;</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>&lt;</span><span>groupId</span><span>&gt;io.lettuce&lt;/</span><span>groupId</span><span>&gt;</span></div></div><div><div><div>22</div></div><div><span><span>    </span></span><span>&lt;</span><span>artifactId</span><span>&gt;lettuce-core&lt;/</span><span>artifactId</span><span>&gt;</span></div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>&lt;</span><span>version</span><span>&gt;6.1.6.RELEASE&lt;/</span><span>version</span><span>&gt;</span></div></div><div><div><div>24</div></div><div><span>&lt;/</span><span>dependency</span><span>&gt;</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>Controller：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@</span><span>GetMapping</span><span>(</span><span>"/of/type"</span><span>)</span></div></div><div><div><div>2</div></div><div><span>public</span><span> </span><span>Result</span><span> </span><span>queryShopByType</span><span>(</span></div></div><div><div><div>3</div></div><div><span><span>        </span></span><span>@</span><span>RequestParam</span><span>(</span><span>"typeId"</span><span>) </span><span>Integer</span><span><span> typeId</span><span>,</span></span></div></div><div><div><div>4</div></div><div><span><span>        </span></span><span>@</span><span>RequestParam</span><span>(</span><span>value</span><span><span> </span><span>=</span><span> </span></span><span>"current"</span><span><span>,</span><span> </span></span><span>defaultValue</span><span><span> </span><span>=</span><span> </span></span><span>"1"</span><span>) </span><span>Integer</span><span><span> current</span><span>,</span></span></div></div><div><div><div>5</div></div><div><span><span>        </span></span><span>@</span><span>RequestParam</span><span>(</span><span>value</span><span><span> </span><span>=</span><span> </span></span><span>"x"</span><span><span>,</span><span> </span></span><span>required</span><span><span> </span><span>=</span><span> </span></span><span>false</span><span>) </span><span>Double</span><span><span> x</span><span>,</span></span></div></div><div><div><div>6</div></div><div><span><span>        </span></span><span>@</span><span>RequestParam</span><span>(</span><span>value</span><span><span> </span><span>=</span><span> </span></span><span>"y"</span><span><span>,</span><span> </span></span><span>required</span><span><span> </span><span>=</span><span> </span></span><span>false</span><span>) </span><span>Double</span><span> y</span></div></div><div><div><div>7</div></div><div><span>) {</span></div></div><div><div><div>8</div></div><div><span>    </span><span>return</span><span> </span><span>shopService</span><span>.</span><span>queryShopByType</span><span>(typeId, current, x, y);</span></div></div><div><div><div>9</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>Service：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@</span><span>Override</span></div></div><div><div><div>2</div></div><div><span>public</span><span> </span><span>Result</span><span> </span><span>queryShopByType</span><span>(</span><span>Integer</span><span><span> typeId</span><span>,</span><span> </span></span><span>Integer</span><span><span> current</span><span>,</span><span> </span></span><span>Double</span><span><span> x</span><span>,</span><span> </span></span><span>Double</span><span> y) {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>if</span><span><span> (x </span><span>==</span><span> </span></span><span>null</span><span><span> </span><span>||</span><span> y </span><span>==</span><span> </span></span><span>null</span><span>) {</span></div></div><div><div><div>4</div></div><div><span>        </span><span>Page</span><span>&lt;</span><span>Shop</span><span><span>&gt;</span><span> page </span></span><span><span>=</span><span> </span></span><span>query</span><span>()</span></div></div><div><div><div>5</div></div><div><span><span>                </span></span><span>.</span><span>eq</span><span>(</span><span>"type_id"</span><span>, typeId)</span></div></div><div><div><div>6</div></div><div><span><span>                </span></span><span>.</span><span>page</span><span>(</span><span>new</span><span> </span><span>Page</span><span>&lt;&gt;(current, </span><span>SystemConstants</span><span>.</span><span>DEFAULT_PAGE_SIZE</span><span>));</span></div></div><div><div><div>7</div></div><div><span>        </span><span>return</span><span> </span><span>Result</span><span>.</span><span>ok</span><span>(</span><span>page</span><span>.</span><span>getRecords</span><span>());</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>    </span><span>int</span><span> from </span><span><span>=</span><span> (current </span><span>-</span><span> </span></span><span>1</span><span><span>) </span><span>*</span><span> </span></span><span>SystemConstants</span><span>.</span><span>MAX_PAGE_SIZE</span><span>;</span></div></div><div><div><div>11</div></div><div><span>    </span><span>int</span><span> end </span><span><span>=</span><span> current </span><span>*</span><span> </span></span><span>SystemConstants</span><span>.</span><span>MAX_PAGE_SIZE</span><span>;</span></div></div><div><div><div>12</div></div><div><span>    </span><span>String</span><span> key </span><span><span>=</span><span> SHOP_GEO_KEY </span><span>+</span><span> typeId</span><span>;</span></span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>    </span><span>GeoResults</span><span>&lt;</span><span>RedisGeoCommands</span><span>.</span><span>GeoLocation</span><span>&lt;</span><span>String</span><span><span>&gt;&gt;</span><span> results </span></span><span>=</span></div></div><div><div><div>15</div></div><div><span>            </span><span>stringRedisTemplate</span><span>.</span><span>opsForGeo</span><span>().</span><span>search</span><span>(</span></div></div><div><div><div>16</div></div><div><span><span>                    </span></span><span>key,</span></div></div><div><div><div>17</div></div><div><span>                    </span><span>GeoReference</span><span>.</span><span>fromCoordinate</span><span>(x, y),</span></div></div><div><div><div>18</div></div><div><span>                    </span><span>new</span><span> </span><span>Distance</span><span>(</span><span>5000</span><span>),</span></div></div><div><div><div>19</div></div><div><span>                    </span><span>RedisGeoCommands</span><span>.</span><span>GeoSearchCommandArgs</span><span>.</span><span>newGeoSearchArgs</span><span>()</span></div></div><div><div><div>20</div></div><div><span><span>                            </span></span><span>.</span><span>includeDistance</span><span>()</span></div></div><div><div><div>21</div></div><div><span><span>                            </span></span><span>.</span><span>limit</span><span>(end)</span></div></div><div><div><div>22</div></div><div><span><span>            </span></span><span>);</span></div></div><div><div><div>23</div></div><div>
</div></div><div><div><div>24</div></div><div><span>    </span><span>if</span><span><span> (results </span><span>==</span><span> </span></span><span>null</span><span>) {</span></div></div><div><div><div>25</div></div><div><span>        </span><span>return</span><span> </span><span>Result</span><span>.</span><span>ok</span><span>(</span><span>Collections</span><span>.</span><span>emptyList</span><span>());</span></div></div><div><div><div>26</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>27</div></div><div>
</div></div><div><div><div>28</div></div><div><span>    </span><span>List</span><span>&lt;</span><span>GeoResult</span><span>&lt;</span><span>RedisGeoCommands</span><span>.</span><span>GeoLocation</span><span>&lt;</span><span>String</span><span><span>&gt;&gt;&gt;</span><span> list </span></span><span><span>=</span><span> </span></span><span>results</span><span>.</span><span>getContent</span><span>();</span></div></div><div><div><div>29</div></div><div>
</div></div><div><div><div>30</div></div><div><span>    </span><span>if</span><span> (</span><span>list</span><span>.</span><span>size</span><span><span>()</span><span> </span><span>&lt;</span><span> from) {</span></span></div></div><div><div><div>31</div></div><div><span>        </span><span>return</span><span> </span><span>Result</span><span>.</span><span>ok</span><span>(</span><span>Collections</span><span>.</span><span>emptyList</span><span>());</span></div></div><div><div><div>32</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>33</div></div><div>
</div></div><div><div><div>34</div></div><div><span>    </span><span>List</span><span>&lt;</span><span>Long</span><span><span>&gt;</span><span> ids </span></span><span><span>=</span><span> </span></span><span>new</span><span> </span><span>ArrayList</span><span><span>&lt;&gt;</span><span>(</span></span><span>list</span><span>.</span><span>size</span><span><span>()</span><span>)</span><span>;</span></span></div></div><div><div><div>35</div></div><div><span>    </span><span>Map</span><span>&lt;</span><span>String</span><span><span>,</span><span> </span></span><span>Distance</span><span><span>&gt;</span><span> distanceMap </span></span><span><span>=</span><span> </span></span><span>new</span><span> </span><span>HashMap</span><span><span>&lt;&gt;</span><span>(</span></span><span>list</span><span>.</span><span>size</span><span><span>()</span><span>)</span><span>;</span></span></div></div><div><div><div>36</div></div><div>
</div></div><div><div><div>37</div></div><div><span>    </span><span>list</span><span>.</span><span>stream</span><span>().</span><span>skip</span><span>(from).</span><span>forEach</span><span>(result </span><span>-&gt;</span><span> {</span></div></div><div><div><div>38</div></div><div><span>        </span><span>String</span><span><span> </span><span>shopIdStr</span><span> </span></span><span><span>=</span><span> </span></span><span>result</span><span>.</span><span>getContent</span><span>().</span><span>getName</span><span>();</span></div></div><div><div><div>39</div></div><div><span>        </span><span>ids</span><span>.</span><span>add</span><span>(</span><span>Long</span><span>.</span><span>valueOf</span><span>(shopIdStr));</span></div></div><div><div><div>40</div></div><div><span>        </span><span>distanceMap</span><span>.</span><span>put</span><span>(shopIdStr, </span><span>result</span><span>.</span><span>getDistance</span><span>());</span></div></div><div><div><div>41</div></div><div><span><span>    </span></span><span>});</span></div></div><div><div><div>42</div></div><div>
</div></div><div><div><div>43</div></div><div><span>    </span><span>String</span><span> idsStr </span><span><span>=</span><span> </span></span><span>StrUtil</span><span>.</span><span>join</span><span>(</span><span>","</span><span>, ids);</span></div></div><div><div><div>44</div></div><div><span>    </span><span>List</span><span>&lt;</span><span>Shop</span><span><span>&gt;</span><span> shops </span></span><span><span>=</span><span> </span></span><span>query</span><span>()</span></div></div><div><div><div>45</div></div><div><span><span>            </span></span><span>.</span><span>in</span><span>(</span><span>"id"</span><span>, ids)</span></div></div><div><div><div>46</div></div><div><span><span>            </span></span><span>.</span><span>last</span><span>(</span><span>"ORDER BY FIELD(id,"</span><span><span> </span><span>+</span><span> idsStr </span><span>+</span><span> </span></span><span>")"</span><span>)</span></div></div><div><div><div>47</div></div><div><span><span>            </span></span><span>.</span><span>list</span><span>();</span></div></div><div><div><div>48</div></div><div>
</div></div><div><div><div>49</div></div><div><span>    </span><span>for</span><span> (</span><span>Shop</span><span> shop </span><span>:</span><span> shops) {</span></div></div><div><div><div>50</div></div><div><span>        </span><span>shop</span><span>.</span><span>setDistance</span><span>(</span><span>distanceMap</span><span>.</span><span>get</span><span>(</span><span>shop</span><span>.</span><span>getId</span><span>().</span><span>toString</span><span>()).</span><span>getValue</span><span>());</span></div></div><div><div><div>51</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>52</div></div><div>
</div></div><div><div><div>53</div></div><div><span>    </span><span>return</span><span> </span><span>Result</span><span>.</span><span>ok</span><span>(shops);</span></div></div><div><div><div>54</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><hr /></section></section><section><h2>九、用户签到<a href="#九用户签到"><span>#</span></a></h2><section><h3>9.1 BitMap 介绍<a href="#91-bitmap-介绍"><span>#</span></a></h3><p>BitMap 使用二进制位记录业务状态，签到为 1，未签到为 0。</p><p>Redis 中 BitMap 基于 String 实现，最大 512M，即 <code>2^32</code> 个 bit。</p><p>常用命令：</p>

<table><thead><tr><th>命令</th><th>说明</th></tr></thead><tbody><tr><td><code>SETBIT</code></td><td>向指定位置存入 0 或 1</td></tr><tr><td><code>GETBIT</code></td><td>获取指定位置的 bit 值</td></tr><tr><td><code>BITCOUNT</code></td><td>统计值为 1 的 bit 数量</td></tr><tr><td><code>BITFIELD</code></td><td>操作 bit 数组指定位置</td></tr><tr><td><code>BITOP</code></td><td>多个 BitMap 位运算</td></tr><tr><td><code>BITPOS</code></td><td>查找第一个 0 或 1</td></tr></tbody></table></section><section><h3>9.2 实现签到功能<a href="#92-实现签到功能"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@</span><span>PostMapping</span><span>(</span><span>"/sign"</span><span>)</span></div></div><div><div><div>2</div></div><div><span>public</span><span> </span><span>Result</span><span> </span><span>sign</span><span>() {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>return</span><span> </span><span>userService</span><span>.</span><span>sign</span><span>();</span></div></div><div><div><div>4</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>Service：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@</span><span>Override</span></div></div><div><div><div>2</div></div><div><span>public</span><span> </span><span>Result</span><span> </span><span>sign</span><span>() {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>Long</span><span> userId </span><span><span>=</span><span> </span></span><span>UserHolder</span><span>.</span><span>getUser</span><span>().</span><span>getId</span><span>();</span></div></div><div><div><div>4</div></div><div><span>    </span><span>LocalDateTime</span><span> now </span><span><span>=</span><span> </span></span><span>LocalDateTime</span><span>.</span><span>now</span><span>();</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>    </span><span>String</span><span> keySuffix </span><span><span>=</span><span> </span></span><span>now</span><span>.</span><span>format</span><span>(</span><span>DateTimeFormatter</span><span>.</span><span>ofPattern</span><span>(</span><span>":yyyyMM"</span><span>));</span></div></div><div><div><div>7</div></div><div><span>    </span><span>String</span><span> key </span><span><span>=</span><span> USER_SIGN_KEY </span><span>+</span><span> userId </span><span>+</span><span> keySuffix</span><span>;</span></span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>    </span><span>int</span><span> dayOfMonth </span><span><span>=</span><span> </span></span><span>now</span><span>.</span><span>getDayOfMonth</span><span>();</span></div></div><div><div><div>10</div></div><div><span>    </span><span>stringRedisTemplate</span><span>.</span><span>opsForValue</span><span>().</span><span>setBit</span><span><span>(key, dayOfMonth </span><span>-</span><span> </span></span><span>1</span><span>, </span><span>true</span><span>);</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>    </span><span>return</span><span> </span><span>Result</span><span>.</span><span>ok</span><span>();</span></div></div><div><div><div>13</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>9.3 签到统计<a href="#93-签到统计"><span>#</span></a></h3><p>统计截止当前时间的连续签到天数：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@</span><span>GetMapping</span><span>(</span><span>"/sign/count"</span><span>)</span></div></div><div><div><div>2</div></div><div><span>public</span><span> </span><span>Result</span><span> </span><span>signCount</span><span>() {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>return</span><span> </span><span>userService</span><span>.</span><span>signCount</span><span>();</span></div></div><div><div><div>4</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>Service：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@</span><span>Override</span></div></div><div><div><div>2</div></div><div><span>public</span><span> </span><span>Result</span><span> </span><span>signCount</span><span>() {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>Long</span><span> userId </span><span><span>=</span><span> </span></span><span>UserHolder</span><span>.</span><span>getUser</span><span>().</span><span>getId</span><span>();</span></div></div><div><div><div>4</div></div><div><span>    </span><span>LocalDateTime</span><span> now </span><span><span>=</span><span> </span></span><span>LocalDateTime</span><span>.</span><span>now</span><span>();</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>    </span><span>String</span><span> keySuffix </span><span><span>=</span><span> </span></span><span>now</span><span>.</span><span>format</span><span>(</span><span>DateTimeFormatter</span><span>.</span><span>ofPattern</span><span>(</span><span>":yyyyMM"</span><span>));</span></div></div><div><div><div>7</div></div><div><span>    </span><span>String</span><span> key </span><span><span>=</span><span> USER_SIGN_KEY </span><span>+</span><span> userId </span><span>+</span><span> keySuffix</span><span>;</span></span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>    </span><span>int</span><span> dayOfMonth </span><span><span>=</span><span> </span></span><span>now</span><span>.</span><span>getDayOfMonth</span><span>();</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>    </span><span>List</span><span>&lt;</span><span>Long</span><span><span>&gt;</span><span> result </span></span><span><span>=</span><span> </span></span><span>stringRedisTemplate</span><span>.</span><span>opsForValue</span><span>().</span><span>bitField</span><span>(</span></div></div><div><div><div>12</div></div><div><span><span>            </span></span><span>key,</span></div></div><div><div><div>13</div></div><div><span>            </span><span>BitFieldSubCommands</span><span>.</span><span>create</span><span>()</span></div></div><div><div><div>14</div></div><div><span><span>                    </span></span><span>.</span><span>get</span><span>(</span><span>BitFieldSubCommands</span><span>.</span><span>BitFieldType</span><span>.</span><span>unsigned</span><span>(dayOfMonth))</span></div></div><div><div><div>15</div></div><div><span><span>                    </span></span><span>.</span><span>valueAt</span><span>(</span><span>0</span><span>)</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>);</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>    </span><span>if</span><span><span> (result </span><span>==</span><span> </span></span><span>null</span><span><span> </span><span>||</span><span> </span></span><span>result</span><span>.</span><span>isEmpty</span><span><span>()</span><span>) {</span></span></div></div><div><div><div>19</div></div><div><span>        </span><span>return</span><span> </span><span>Result</span><span>.</span><span>ok</span><span>(</span><span>0</span><span>);</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>    </span><span>int</span><span> count </span><span><span>=</span><span> </span></span><span>0</span><span>;</span></div></div><div><div><div>23</div></div><div><span>    </span><span>Long</span><span> num </span><span><span>=</span><span> </span></span><span>result</span><span>.</span><span>get</span><span>(</span><span>0</span><span>);</span></div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div><span>    </span><span>while</span><span> (</span><span>true</span><span>) {</span></div></div><div><div><div>26</div></div><div><span>        </span><span>if</span><span><span> ((num </span><span>&amp;</span><span> </span></span><span>1</span><span><span>) </span><span>==</span><span> </span></span><span>0</span><span>) {</span></div></div><div><div><div>27</div></div><div><span>            </span><span>break</span><span>;</span></div></div><div><div><div>28</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>29</div></div><div><span><span>        </span></span><span>count</span><span>++;</span></div></div><div><div><div>30</div></div><div><span><span>        </span></span><span>num </span><span>&gt;&gt;&gt;=</span><span> </span><span>1</span><span>;</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>32</div></div><div>
</div></div><div><div><div>33</div></div><div><span>    </span><span>return</span><span> </span><span>Result</span><span>.</span><span>ok</span><span>(count);</span></div></div><div><div><div>34</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><hr /></section></section><section><h2>十、UV 统计<a href="#十uv-统计"><span>#</span></a></h2><section><h3>10.1 HyperLogLog<a href="#101-hyperloglog"><span>#</span></a></h3><ul>
<li><strong>UV</strong>：Unique Visitor，独立访客量，同一用户一天内多次访问只记录一次。</li>
<li><strong>PV</strong>：Page View，页面访问量，每访问一次记录一次。</li>
</ul><p>HyperLogLog 是概率算法，用于统计大规模集合基数，内存占用极低，单个 HLL 小于 16KB，误差约 0.81%。</p><p>常用命令：</p>

<table><thead><tr><th>命令</th><th>说明</th></tr></thead><tbody><tr><td><code>PFADD key element ...</code></td><td>添加元素</td></tr><tr><td><code>PFCOUNT key ...</code></td><td>统计基数</td></tr><tr><td><code>PFMERGE destkey sourcekey ...</code></td><td>合并多个 HLL</td></tr></tbody></table><p>测试百万数据：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@</span><span>Test</span></div></div><div><div><div>2</div></div><div><span>public</span><span> </span><span>void</span><span> </span><span>testHyperLogLog</span><span>() {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>String</span><span>[] users </span><span><span>=</span><span> </span></span><span>new</span><span> </span><span>String</span><span>[</span><span>1000</span><span><span>]</span><span>;</span></span></div></div><div><div><div>4</div></div><div><span>    </span><span>int</span><span> j </span><span><span>=</span><span> </span></span><span>0</span><span>;</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> i </span><span><span>=</span><span> </span></span><span>0</span><span><span>;</span><span> i </span><span>&lt;</span><span> </span></span><span>1000000</span><span><span>;</span><span> i</span><span>++</span><span>) {</span></span></div></div><div><div><div>7</div></div><div><span><span>        </span></span><span>j </span><span>=</span><span> i </span><span>%</span><span> </span><span>1000</span><span>;</span></div></div><div><div><div>8</div></div><div><span><span>        </span></span><span>users[j] </span><span>=</span><span> </span><span>"user_"</span><span><span> </span><span>+</span><span> i</span><span>;</span></span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>        </span><span>if</span><span><span> (j </span><span>==</span><span> </span></span><span>999</span><span>) {</span></div></div><div><div><div>11</div></div><div><span>            </span><span>stringRedisTemplate</span><span>.</span><span>opsForHyperLogLog</span><span>().</span><span>add</span><span>(</span><span>"HLL"</span><span>, users);</span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>    </span><span>Long</span><span> count </span><span><span>=</span><span> </span></span><span>stringRedisTemplate</span><span>.</span><span>opsForHyperLogLog</span><span>().</span><span>size</span><span>(</span><span>"HLL"</span><span>);</span></div></div><div><div><div>16</div></div><div><span>    </span><span>System</span><span>.</span><span>out</span><span>.</span><span>println</span><span>(</span><span>"count = "</span><span><span> </span><span>+</span><span> count);</span></span></div></div><div><div><div>17</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>百万数据测试结果误差很小，内存占用仅约 12KB，非常适合 UV 统计场景。</p><hr /></section></section><section><h2>总结<a href="#总结"><span>#</span></a></h2><p>本文围绕黑马点评项目，串联了 Redis 在实际业务中的典型用法：</p><ul>
<li>使用 Redis 替代 Session，实现 Token 登录。</li>
<li>使用缓存解决商户查询性能问题，并处理缓存穿透、雪崩、击穿。</li>
<li>使用 Redis 全局 ID、Lua 脚本、分布式锁、消息队列优化优惠券秒杀。</li>
<li>使用 Set、SortedSet 实现点赞、关注、共同关注和 Feed 流。</li>
<li>使用 GEO 实现附近商户查询。</li>
<li>使用 BitMap 实现签到统计。</li>
<li>使用 HyperLogLog 实现 UV 统计。</li>
</ul><div><div><div></div><div>下一步建议</div></div><div><p>可以继续深入学习 Redis 持久化、主从复制、哨兵、分片集群，以及 Redisson 在生产环境中的锁续期、联锁、读写锁等高级特性。</p></div></div></section></section>]]></content>
    </entry>
    <entry>
      <id>https://firefly.cuteleaf.cn/posts/learn-rendis/</id>
      <title type="text">Redis 入门不迷路：从 NoSQL 到 Spring Data Redis 实战笔记</title>
      <published>2025-08-07T00:00:00.000Z</published>
      <updated>2025-08-07T00:00:00.000Z</updated>
      <author><name>幽流啊飘</name></author>
      <link rel="alternate" href="https://firefly.cuteleaf.cn/posts/learn-rendis/"/>
      <summary type="text">一份更轻快、更成体系的 Redis 入门笔记：先建立 NoSQL 地图，再速通五大数据结构与常用命令，最后用 Jedis 和 Spring Data Redis 接入 Java 项目。</summary>
      <content type="html"><![CDATA[<section><h1>Redis 入门不迷路：从 NoSQL 到 Spring Data Redis 实战笔记<a href="#redis-入门不迷路从-nosql-到-spring-data-redis-实战笔记"><span>#</span></a></h1><div><div><div></div><div>阅读姿势</div></div><div><p>这不是一份“命令流水账”，而是一张从概念到项目落地的学习地图。
建议打开 <code>redis-cli</code> 边看边敲；命令别死背，先想清楚它解决什么问题。</p></div></div><section><h2>先把学习路线铺开<a href="#先把学习路线铺开"><span>#</span></a></h2><div><div><div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>NoSQL 是什么</p></span></div><div><span><p>Redis 核心特征</p></span></div><div><span><p>redis-cli 与桌面客户端</p></span></div><div><span><p>五大数据结构</p></span></div><div><span><p>Jedis 直连/连接池</p></span></div><div><span><p>Spring Data Redis</p></span></div><div><span><p>序列化选型</p></span></div>
</div><div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>NoSQL 是什么</p></span></div><div><span><p>Redis 核心特征</p></span></div><div><span><p>redis-cli 与桌面客户端</p></span></div><div><span><p>五大数据结构</p></span></div><div><span><p>Jedis 直连/连接池</p></span></div><div><span><p>Spring Data Redis</p></span></div><div><span><p>序列化选型</p></span></div>
</div></div></div></section><section><h2>初识 Redis：先别急着背命令<a href="#初识-redis先别急着背命令"><span>#</span></a></h2><p>Redis 是一个<strong>键值型 NoSQL 数据库</strong>。这里有两个关键词：</p><ul>
<li><strong>键值型</strong>：数据以 Key-Value 形式存储，Value 可以是字符串、数值、JSON 等。</li>
<li><strong>NoSQL</strong>：它和传统关系型数据库的思路差别很大。</li>
</ul><p>你可以先把 Redis 理解成“内存里一个特别能打的数据结构服务器”。但它不只是大号 Map，后面会看到 String、Hash、List、Set、SortedSet 各有各的脾气。</p></section><section><h2>NoSQL 和 SQL 到底差在哪<a href="#nosql-和-sql-到底差在哪"><span>#</span></a></h2><p>NoSQL 常被解释为 <strong>Not Only SQL</strong>，也可以理解为“非关系型数据库”。它不是来完全替代 SQL 的，而是解决另一类问题。</p><section><h3>结构化 vs 非结构化<a href="#结构化-vs-非结构化"><span>#</span></a></h3><p>关系型数据库是<strong>结构化</strong>的：建表时字段名、类型、约束都定好了，插入数据必须守规矩。</p><p>NoSQL 通常<strong>没有严格约束</strong>，可以是键值型、文档型，甚至图结构。</p></section><section><h3>关联 vs 非关联<a href="#关联-vs-非关联"><span>#</span></a></h3><p>关系型数据库表与表之间常有外键关联。NoSQL 往往<strong>不靠数据库维护关联</strong>，要么靠业务代码，要么靠数据冗余。</p><p>比如要维护“张三和两个手机订单”的关系，可能得把订单信息塞进用户文档里：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{</span></div></div><div><div><div>2</div></div><div><span>  </span><span>"id"</span><span>: </span><span>1</span><span>,</span></div></div><div><div><div>3</div></div><div><span>  </span><span>"name"</span><span>: </span><span>"张三"</span><span>,</span></div></div><div><div><div>4</div></div><div><span>  </span><span>"orders"</span><span>: [</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>{</span></div></div><div><div><div>6</div></div><div><span>      </span><span>"id"</span><span>: </span><span>1</span><span>,</span></div></div><div><div><div>7</div></div><div><span>      </span><span>"item"</span><span>: { </span><span>"id"</span><span>: </span><span>10</span><span>, </span><span>"title"</span><span>: </span><span>"荣耀6"</span><span>, </span><span>"price"</span><span>: </span><span>4999</span><span> }</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>},</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>{</span></div></div><div><div><div>10</div></div><div><span>      </span><span>"id"</span><span>: </span><span>2</span><span>,</span></div></div><div><div><div>11</div></div><div><span>      </span><span>"item"</span><span>: { </span><span>"id"</span><span>: </span><span>20</span><span>, </span><span>"title"</span><span>: </span><span>"小米11"</span><span>, </span><span>"price"</span><span>: </span><span>3999</span><span> }</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>]</span></div></div><div><div><div>14</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这种冗余不够优雅，所以实际项目更推荐用<strong>业务逻辑</strong>维护关联关系。</p></section><section><h3>查询方式不一样<a href="#查询方式不一样"><span>#</span></a></h3><p>SQL 有统一语法：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>SELECT</span><span> id, age </span><span>FROM</span><span> tb_user </span><span>WHERE</span><span><span> id </span><span>=</span><span> </span></span><span>1</span><span>;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>NoSQL 则是“各家有各家的方言”：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Redis:         get user:1</span></div></div><div><div><div>2</div></div><div><span>MongoDB:       db.user.find({_id: 1})</span></div></div><div><div><div>3</div></div><div><span>elasticsearch: GET http://localhost:9200/users/1</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>事务能力不一样<a href="#事务能力不一样"><span>#</span></a></h3><p>关系型数据库通常满足事务 <strong>ACID</strong>：原子性、一致性、隔离性、持久性。</p><p>NoSQL 往往不支持完整事务，或者只能保证基本一致性，更偏向 <strong>BASE</strong>。</p></section><section><h3>一张表快速对比<a href="#一张表快速对比"><span>#</span></a></h3>

<table><thead><tr><th>特性</th><th>SQL</th><th>NoSQL</th></tr></thead><tbody><tr><td>数据结构</td><td>结构化（Structured）</td><td>非结构化</td></tr><tr><td>数据关联</td><td>关联的（Relational）</td><td>无关联或弱关联</td></tr><tr><td>查询方式</td><td>SQL 查询</td><td>非 SQL，方言差异大</td></tr><tr><td>事务特性</td><td>ACID</td><td>BASE</td></tr><tr><td>存储方式</td><td>磁盘为主</td><td>内存为主</td></tr><tr><td>扩展性</td><td>垂直扩展为主</td><td>水平扩展更常见</td></tr><tr><td>使用场景</td><td>数据结构固定、一致性/安全性要求高、关系复杂</td><td>数据结构不固定、性能要求高、可水平扩展、最终一致</td></tr></tbody></table></section><section><h3>存储方式与扩展性<a href="#存储方式与扩展性"><span>#</span></a></h3><p>关系型数据库基于磁盘，磁盘 IO 多，性能会受影响。</p><p>NoSQL 更依赖<strong>内存</strong>，读写速度很快，性能自然更猛。</p><p>扩展性上：</p><ul>
<li><strong>垂直扩展</strong>：关系型数据库常见主从集群，主从数据一致，主要用于备份和读扩展。</li>
<li><strong>水平扩展</strong>：NoSQL 可以把数据拆分到不同机器，存海量数据，解决单机内存有限的问题。</li>
</ul><div><div><div></div><div>别忽略关联的代价</div></div><div><p>关系型数据库表之间有关联，如果硬做水平扩展，查询会变得很麻烦。选型时别只看“能扩”，也要看“扩完好不好查”。</p></div></div></section></section><section><h2>认识 Redis：它到底强在哪<a href="#认识-redis它到底强在哪"><span>#</span></a></h2><p>Redis 诞生于 2009 年，全称 <strong>Remote Dictionary Server</strong>（远程词典服务器），是一个基于内存的键值型 NoSQL 数据库。</p><p>核心特征：</p><ul>
<li>Key-Value 型，Value 支持多种数据结构，功能丰富</li>
<li>单线程模型，每个命令具有原子性</li>
<li>低延迟、速度快：基于内存、IO 多路复用、编码优化</li>
<li>支持数据持久化</li>
<li>支持主从集群、分片集群</li>
<li>支持多语言客户端</li>
</ul><blockquote><p>作者：Antirez<br />
官网：<a href="https://redis.io/" target="_blank">https://redis.io/</a></p></blockquote></section><section><h2>安装与客户端：工具先备齐<a href="#安装与客户端工具先备齐"><span>#</span></a></h2><p>Redis 安装可以参考之前整理的文章：</p><ul>
<li><a href="https://cyborg2077.github.io/2022/10/17/ReggieRedis/" target="_blank">Redis 入门</a></li>
</ul><p>安装完成后，操作 Redis 常见有三类客户端：</p><ul>
<li><strong>命令行客户端</strong></li>
<li><strong>图形化桌面客户端</strong></li>
<li><strong>编程客户端</strong></li>
</ul><section><h3>redis-cli 命令行客户端<a href="#redis-cli-命令行客户端"><span>#</span></a></h3><p>Redis 自带 <code>redis-cli</code>，基本用法：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>redis-cli</span><span> [options] [commands]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>常见 options：</p>

<table><thead><tr><th>参数</th><th>说明</th></tr></thead><tbody><tr><td><code>-h 127.0.0.1</code></td><td>指定 Redis 节点 IP，默认 127.0.0.1</td></tr><tr><td><code>-p 6379</code></td><td>指定 Redis 节点端口，默认 6379</td></tr><tr><td><code>-a 123321</code></td><td>指定访问密码</td></tr></tbody></table><p><code>commands</code> 就是 Redis 操作命令，比如：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>ping</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>服务端正常会返回 <code>pong</code>。</p></section><section><h3>图形化桌面客户端<a href="#图形化桌面客户端"><span>#</span></a></h3><p>下载地址：<a href="https://github.com/lework/RedisDesktopManager-Windows/releases" target="_blank">https://github.com/lework/RedisDesktopManager-Windows/releases</a></p><p>Redis 默认有 <strong>16 个数据库</strong>，编号从 0 到 15。数据库数量可以通过配置文件调整，但数据库名称不能自定义。</p><p>使用 <code>redis-cli</code> 时，可以通过 <code>select</code> 选择数据库：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span># 选择 0 号数据库</span></div></div><div><div><div>2</div></div><div><span>select</span><span> </span><span>0</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section></section><section><h2>五大数据结构速通<a href="#五大数据结构速通"><span>#</span></a></h2><p>Redis 是典型 key-value 数据库，key 一般是字符串，value 则有很多类型。先把通用命令拿下，再逐个击破。</p><section><h3>通用命令：先学会“看家本领”<a href="#通用命令先学会看家本领"><span>#</span></a></h3>

<table><thead><tr><th>命令</th><th>描述</th></tr></thead><tbody><tr><td><code>KEYS pattern</code></td><td>查找所有符合给定模式的 key</td></tr><tr><td><code>EXISTS key</code></td><td>检查给定 key 是否存在</td></tr><tr><td><code>TYPE key</code></td><td>返回 key 所储存值的类型</td></tr><tr><td><code>TTL key</code></td><td>返回 key 剩余生存时间，单位秒</td></tr><tr><td><code>EXPIRE key seconds</code></td><td>给 key 设置有效期</td></tr><tr><td><code>DEL key</code></td><td>key 存在时删除 key</td></tr></tbody></table><p>补充说明：</p><ul>
<li><code>DEL name age</code> 可以一次删除多个 key。</li>
<li><code>EXISTS name</code> 存在返回 <code>1</code>，不存在返回 <code>0</code>。</li>
<li><code>EXPIRE name 20</code> 给 <code>name</code> 设置 20 秒有效期。</li>
<li><code>TTL name</code> 未设置有效期通常返回 <code>-1</code>。</li>
</ul><div><div><div></div><div>生产环境慎用 KEYS</div></div><div><p>Redis 是单线程的，<code>KEYS</code> 做模糊查询时可能阻塞其他命令。数据量大时效率极差，生产环境不建议乱用。</p></div></div></section><section><h3>String：最基础，也最常用<a href="#string最基础也最常用"><span>#</span></a></h3><p>String 是 Redis 中最简单的存储类型。Value 是字符串，但根据格式又可以分为：</p>

<table><thead><tr><th>类型</th><th>说明</th></tr></thead><tbody><tr><td><code>string</code></td><td>普通字符串</td></tr><tr><td><code>int</code></td><td>整数，可自增自减</td></tr><tr><td><code>float</code></td><td>浮点数，可自增自减</td></tr></tbody></table><p>不管哪种格式，底层都是<strong>字节数组</strong>存储，只是编码方式不同。String 类型最大空间不能超过 <strong>512M</strong>。</p><p>常用命令：</p>

<table><thead><tr><th>命令</th><th>描述</th></tr></thead><tbody><tr><td><code>SET</code></td><td>添加或修改 String 类型键值对</td></tr><tr><td><code>GET</code></td><td>根据 key 获取 String 类型 value</td></tr><tr><td><code>MSET</code></td><td>批量添加多个 String 类型键值对</td></tr><tr><td><code>MGET</code></td><td>根据多个 key 获取多个 value</td></tr><tr><td><code>INCR</code></td><td>整型 key 自增 1</td></tr><tr><td><code>INCRBY</code></td><td>整型 key 自增并指定步长，如 <code>INCRBY num 2</code></td></tr><tr><td><code>INCRBYFLOAT</code></td><td>浮点类型数字自增并指定步长</td></tr><tr><td><code>SETNX</code></td><td>key 不存在时才添加</td></tr><tr><td><code>SETEX</code></td><td>添加键值对并指定有效期</td></tr></tbody></table><section><h4>Key 结构：别让 id 打架<a href="#key-结构别让-id-打架"><span>#</span></a></h4><p>Redis 没有 MySQL 中 Table 的概念，那怎么区分不同类型的数据？</p><div><div><div></div><div>场景举例</div></div><div><p>用户 id 是 1，商品 id 也是 1。如果都用 <code>1</code> 当 key，就会冲突。</p></div></div><p>解决办法是给 key 加<strong>前缀</strong>，用 <code>:</code> 分隔形成层级结构：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>项目名:业务名:类型:id</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这个格式不固定，可以按需求增删词条。假设项目名叫 <code>reggie</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>user 相关 key: reggie:user:1</span></div></div><div><div><div>2</div></div><div><span>dish 相关 key: reggie:dish:1</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>如果 Value 是 Java 对象，可以序列化为 JSON 字符串：</p>

<table><thead><tr><th>KEY</th><th>VALUE</th></tr></thead><tbody><tr><td><code>reggie:user:1</code></td><td><code>{"id":1, "name": "Jack", "age": 21}</code></td></tr><tr><td><code>reggie:dish:1</code></td><td><code>{"id":1, "name": "鲟鱼火锅", "price": 4999}</code></td></tr></tbody></table><p>图形化客户端里也会按相同前缀展示层次结构，看起来清爽很多。</p></section></section><section><h3>Hash：对象字段可以单独改<a href="#hash对象字段可以单独改"><span>#</span></a></h3><p>Hash 类型也叫散列，Value 是一个无序字典，类似 Java 的 <code>HashMap</code>。</p><p>String 存对象时，要改某个属性得整体改；Hash 可以把对象每个字段独立存储，针对单个字段做 CRUD。</p>

<table><thead><tr><th>KEY</th><th>field</th><th>value</th></tr></thead><tbody><tr><td><code>reggie:user:1</code></td><td>name</td><td>Jack</td></tr><tr><td><code>reggie:user:1</code></td><td>age</td><td>21</td></tr><tr><td><code>reggie:user:2</code></td><td>name</td><td>Rose</td></tr><tr><td><code>reggie:user:2</code></td><td>age</td><td>18</td></tr></tbody></table><p>常用命令：</p>

<table><thead><tr><th>命令</th><th>描述</th></tr></thead><tbody><tr><td><code>HSET key field value</code></td><td>添加或修改 hash 类型 key 的 field 值</td></tr><tr><td><code>HGET key field</code></td><td>获取 hash 类型 key 的 field 值</td></tr><tr><td><code>HMSET</code></td><td>批量添加多个 field 值</td></tr><tr><td><code>HMGET</code></td><td>批量获取多个 field 值</td></tr><tr><td><code>HGETALL</code></td><td>获取所有 field 和 value</td></tr><tr><td><code>HKEYS</code></td><td>获取所有 field</td></tr><tr><td><code>HINCRBY</code></td><td>hash 字段值自增并指定步长</td></tr><tr><td><code>HSETNX</code></td><td>field 不存在时才添加</td></tr></tbody></table></section><section><h3>List：像 LinkedList 一样排队<a href="#list像-linkedlist-一样排队"><span>#</span></a></h3><p>Redis 的 List 类似 Java 的 <code>LinkedList</code>，可以看成<strong>双向链表</strong>，支持正向和反向检索。</p><p>特点：</p><ul>
<li>有序</li>
<li>元素可重复</li>
<li>插入和删除快</li>
<li>查询速度一般</li>
</ul><p>常用来存有序数据，比如朋友圈点赞列表、评论列表。</p><p>常用命令：</p>

<table><thead><tr><th>命令</th><th>描述</th></tr></thead><tbody><tr><td><code>LPUSH key element ...</code></td><td>向列表左侧插入一个或多个元素</td></tr><tr><td><code>LPOP key</code></td><td>移除并返回列表左侧第一个元素，没有则返回 nil</td></tr><tr><td><code>RPUSH key element ...</code></td><td>向列表右侧插入一个或多个元素</td></tr><tr><td><code>RPOP key</code></td><td>移除并返回列表右侧第一个元素</td></tr><tr><td><code>LRANGE key start end</code></td><td>返回指定角标范围内的元素</td></tr><tr><td><code>BLPOP</code> / <code>BRPOP</code></td><td>类似 <code>LPOP</code>/<code>RPOP</code>，没有元素时等待指定时间</td></tr></tbody></table></section><section><h3>Set：去重和交集的好帮手<a href="#set去重和交集的好帮手"><span>#</span></a></h3><p>Redis 的 Set 类似 Java 的 <code>HashSet</code>，可以理解为 value 为 null 的 HashMap。</p><p>特点：</p><ul>
<li>无序</li>
<li>元素不可重复</li>
<li>查找快</li>
<li>支持交集、并集、差集</li>
</ul><p>常用命令：</p>

<table><thead><tr><th>命令</th><th>描述</th></tr></thead><tbody><tr><td><code>SADD key member ...</code></td><td>向 set 添加一个或多个元素</td></tr><tr><td><code>SREM key member ...</code></td><td>移除指定元素</td></tr><tr><td><code>SCARD key</code></td><td>返回元素个数</td></tr><tr><td><code>SISMEMBER key member</code></td><td>判断元素是否存在</td></tr><tr><td><code>SMEMBERS</code></td><td>获取所有元素</td></tr><tr><td><code>SINTER key1 key2 ...</code></td><td>求交集</td></tr><tr><td><code>SUNION key1 key2 ...</code></td><td>求并集</td></tr><tr><td><code>SDIFF key1 key2 ...</code></td><td>求差集</td></tr></tbody></table><section><h4>小练习：用 Set 维护好友关系<a href="#小练习用-set-维护好友关系"><span>#</span></a></h4><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span># 张三的好友：李四、王五、赵六</span></div></div><div><div><div>2</div></div><div><span>127.0.0.1:6379</span><span>&gt; </span><span>sadd</span><span> </span><span>zhangsan</span><span> </span><span>lisi</span><span> </span><span>wangwu</span><span> </span><span>zhaoliu</span></div></div><div><div><div>3</div></div><div><span>(</span><span>integer</span><span>) </span><span>3</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span># 李四的好友：王五、麻子、二狗</span></div></div><div><div><div>6</div></div><div><span>127.0.0.1:6379</span><span>&gt; </span><span>sadd</span><span> </span><span>lisi</span><span> </span><span>wangwu</span><span> </span><span>mazi</span><span> </span><span>ergou</span></div></div><div><div><div>7</div></div><div><span>(</span><span>integer</span><span>) </span><span>3</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>计算张三的好友人数：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>127.0.0.1:6379</span><span>&gt; </span><span>scard</span><span> </span><span>zhangsan</span></div></div><div><div><div>2</div></div><div><span>(</span><span>integer</span><span>) </span><span>3</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>计算张三和李四的共同好友：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>127.0.0.1:6379</span><span>&gt; </span><span>sinter</span><span> </span><span>zhangsan</span><span> </span><span>lisi</span></div></div><div><div><div>2</div></div><div><span>1</span><span>) </span><span>"wangwu"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>查询是张三好友、但不是李四好友的人：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>127.0.0.1:6379</span><span>&gt; </span><span>sdiff</span><span> </span><span>zhangsan</span><span> </span><span>lisi</span></div></div><div><div><div>2</div></div><div><span>1</span><span>) </span><span>"zhaoliu"</span></div></div><div><div><div>3</div></div><div><span>2</span><span>) </span><span>"lisi"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>查询两人的好友总人数：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>127.0.0.1:6379</span><span>&gt; </span><span>sunion</span><span> </span><span>zhangsan</span><span> </span><span>lisi</span></div></div><div><div><div>2</div></div><div><span>1</span><span>) </span><span>"wangwu"</span></div></div><div><div><div>3</div></div><div><span>2</span><span>) </span><span>"zhaoliu"</span></div></div><div><div><div>4</div></div><div><span>3</span><span>) </span><span>"ergou"</span></div></div><div><div><div>5</div></div><div><span>4</span><span>) </span><span>"lisi"</span></div></div><div><div><div>6</div></div><div><span>5</span><span>) </span><span>"mazi"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>判断李四是否是张三的好友：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>127.0.0.1:6379</span><span>&gt; </span><span>sismember</span><span> </span><span>zhangsan</span><span> </span><span>lisi</span></div></div><div><div><div>2</div></div><div><span>(</span><span>integer</span><span>) </span><span>1</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>判断张三是否是李四的好友：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>127.0.0.1:6379</span><span>&gt; </span><span>sismember</span><span> </span><span>lisi</span><span> </span><span>zhangsan</span></div></div><div><div><div>2</div></div><div><span>(</span><span>integer</span><span>) </span><span>0</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>把李四从张三好友列表移除：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>127.0.0.1:6379</span><span>&gt; </span><span>srem</span><span> </span><span>zhangsan</span><span> </span><span>lisi</span></div></div><div><div><div>2</div></div><div><span>(</span><span>integer</span><span>) </span><span>1</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section></section><section><h3>SortedSet：排行榜专业户<a href="#sortedset排行榜专业户"><span>#</span></a></h3><p>SortedSet 是可排序的 Set，和 Java 的 <code>TreeSet</code> 有点像，但底层差别很大。每个元素带一个 <strong>score</strong>，可基于 score 排序，底层实现是<strong>跳表（SkipList）+ hash 表</strong>。</p><p>特点：</p><ul>
<li>可排序</li>
<li>元素不重复</li>
<li>查询速度快</li>
</ul><p>因为能排序，SortedSet 经常用来做<strong>排行榜</strong>。</p><p>常用命令：</p>

<table><thead><tr><th>命令</th><th>描述</th></tr></thead><tbody><tr><td><code>ZADD key score member</code></td><td>添加一个或多个元素，已存在则更新 score</td></tr><tr><td><code>ZREM key member</code></td><td>删除指定元素</td></tr><tr><td><code>ZSCORE key member</code></td><td>获取指定元素 score</td></tr><tr><td><code>ZRANK key member</code></td><td>获取指定元素排名</td></tr><tr><td><code>ZCARD key</code></td><td>获取元素个数</td></tr><tr><td><code>ZCOUNT key min max</code></td><td>统计 score 在给定范围内的元素个数</td></tr><tr><td><code>ZINCRBY key increment member</code></td><td>指定元素自增，步长为 increment</td></tr><tr><td><code>ZRANGE key min max</code></td><td>按 score 升序获取指定排名范围元素</td></tr><tr><td><code>ZRANGEBYSCORE key min max</code></td><td>按 score 获取指定分数范围元素</td></tr><tr><td><code>ZDIFF</code> / <code>ZINTER</code> / <code>ZUNION</code></td><td>求差集、交集、并集</td></tr></tbody></table><div><div><div></div><div>升序与降序</div></div><div><p>排名默认升序。如果要降序，在命令的 <code>Z</code> 后面加 <code>REV</code>：</p><ul>
<li>升序：<code>ZRANK key member</code></li>
<li>降序：<code>ZREVRANK key member</code></li>
</ul></div></div><section><h4>小练习：用 SortedSet 做班级成绩榜<a href="#小练习用-sortedset-做班级成绩榜"><span>#</span></a></h4><p>存入学生成绩：Jack 85, Lucy 89, Rose 82, Tom 95, Jerry 78, Amy 92, Miles 76</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>127.0.0.1:6379</span><span>&gt; </span><span>zadd</span><span> </span><span>stu</span><span> </span><span>85</span><span> </span><span>Jack</span><span> </span><span>89</span><span> </span><span>Lucy</span><span> </span><span>82</span><span> </span><span>Rose</span><span> </span><span>95</span><span> </span><span>Tom</span><span> </span><span>78</span><span> </span><span>Jerry</span><span> </span><span>92</span><span> </span><span>Amy</span><span> </span><span>76</span><span> </span><span>Miles</span></div></div><div><div><div>2</div></div><div><span>(</span><span>integer</span><span>) </span><span>7</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>删除 Tom：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>127.0.0.1:6379</span><span>&gt; </span><span>zrem</span><span> </span><span>stu</span><span> </span><span>Tom</span></div></div><div><div><div>2</div></div><div><span>(</span><span>integer</span><span>) </span><span>1</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>获取 Amy 的分数：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>127.0.0.1:6379</span><span>&gt; </span><span>zscore</span><span> </span><span>stu</span><span> </span><span>Amy</span></div></div><div><div><div>2</div></div><div><span>"92"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>获取 Rose 的排名：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>127.0.0.1:6379</span><span>&gt; </span><span>zrank</span><span> </span><span>stu</span><span> </span><span>Rose</span></div></div><div><div><div>2</div></div><div><span>(</span><span>integer</span><span>) </span><span>2</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>查询 80 分以下有几个学生：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>127.0.0.1:6379</span><span>&gt; </span><span>zcount</span><span> </span><span>stu</span><span> </span><span>0</span><span> </span><span>80</span></div></div><div><div><div>2</div></div><div><span>(</span><span>integer</span><span>) </span><span>2</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>给 Amy 加 2 分：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>127.0.0.1:6379</span><span>&gt; </span><span>zincrby</span><span> </span><span>stu</span><span> </span><span>2</span><span> </span><span>Amy</span></div></div><div><div><div>2</div></div><div><span>"94"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>查出成绩前 3 名：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>127.0.0.1:6379</span><span>&gt; </span><span>zrange</span><span> </span><span>stu</span><span> </span><span>0</span><span> </span><span>2</span></div></div><div><div><div>2</div></div><div><span>1</span><span>) </span><span>"Miles"</span></div></div><div><div><div>3</div></div><div><span>2</span><span>) </span><span>"Jerry"</span></div></div><div><div><div>4</div></div><div><span>3</span><span>) </span><span>"Rose"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>查出 80 分以下的所有同学：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>127.0.0.1:6379</span><span>&gt; </span><span>zrangebyscore</span><span> </span><span>stu</span><span> </span><span>0</span><span> </span><span>80</span></div></div><div><div><div>2</div></div><div><span>1</span><span>) </span><span>"Miles"</span></div></div><div><div><div>3</div></div><div><span>2</span><span>) </span><span>"Jerry"</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section></section></section><section><h2>Java 客户端：Jedis 与 Spring Data Redis<a href="#java-客户端jedis-与-spring-data-redis"><span>#</span></a></h2><p>主流 Redis Java 客户端可以这样理解：</p>

<table><thead><tr><th>客户端</th><th>说明</th></tr></thead><tbody><tr><td><strong>Jedis / Lettuce</strong></td><td>提供 Redis 命令对应的 API，方便操作 Redis；Spring Data Redis 对它们做了抽象和封装</td></tr><tr><td><strong>Redisson</strong></td><td>在 Redis 基础上实现分布式可伸缩 Java 数据结构（Map、Queue 等），支持跨进程同步机制（Lock、Semaphore 等）</td></tr></tbody></table><div><div><div></div><div>学习建议</div></div><div><p>Spring Data Redis 对 Jedis/Lettuce 都做了封装，后期建议直接以 <strong>Spring Data Redis</strong> 为主。</p></div></div><section><h3>Jedis 快速入门<a href="#jedis-快速入门"><span>#</span></a></h3><section><h4>Step 1：导入 Maven 坐标<a href="#step-1导入-maven-坐标"><span>#</span></a></h4><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>&lt;!--jedis--&gt;</span></div></div><div><div><div>2</div></div><div><span>&lt;</span><span>dependency</span><span>&gt;</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>&lt;</span><span>groupId</span><span>&gt;redis.clients&lt;/</span><span>groupId</span><span>&gt;</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>&lt;</span><span>artifactId</span><span>&gt;jedis&lt;/</span><span>artifactId</span><span>&gt;</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>&lt;</span><span>version</span><span>&gt;3.7.0&lt;/</span><span>version</span><span>&gt;</span></div></div><div><div><div>6</div></div><div><span>&lt;/</span><span>dependency</span><span>&gt;</span></div></div><div><div><div>7</div></div><div><span>&lt;!--单元测试--&gt;</span></div></div><div><div><div>8</div></div><div><span>&lt;</span><span>dependency</span><span>&gt;</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>&lt;</span><span>groupId</span><span>&gt;org.junit.jupiter&lt;/</span><span>groupId</span><span>&gt;</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>&lt;</span><span>artifactId</span><span>&gt;junit-jupiter&lt;/</span><span>artifactId</span><span>&gt;</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>&lt;</span><span>version</span><span>&gt;5.7.0&lt;/</span><span>version</span><span>&gt;</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>&lt;</span><span>scope</span><span>&gt;test&lt;/</span><span>scope</span><span>&gt;</span></div></div><div><div><div>13</div></div><div><span>&lt;/</span><span>dependency</span><span>&gt;</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h4>Step 2：建立连接<a href="#step-2建立连接"><span>#</span></a></h4><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>private</span><span> </span><span>Jedis</span><span> jedis</span><span>;</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>@</span><span>BeforeEach</span></div></div><div><div><div>4</div></div><div><span>void</span><span> </span><span>setUp</span><span>() {</span></div></div><div><div><div>5</div></div><div><span>    </span><span>// 1. 建立连接</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>jedis </span><span>=</span><span> </span><span>new</span><span> </span><span>Jedis</span><span>(</span><span>"101.42.225.160"</span><span><span>,</span><span> </span></span><span>6379</span><span><span>)</span><span>;</span></span></div></div><div><div><div>7</div></div><div><span>    </span><span>// 2. 设置密码</span></div></div><div><div><div>8</div></div><div><span>    </span><span>jedis</span><span>.</span><span>auth</span><span>(</span><span>"root"</span><span>);</span></div></div><div><div><div>9</div></div><div><span>    </span><span>// 3. 选择库</span></div></div><div><div><div>10</div></div><div><span>    </span><span>jedis</span><span>.</span><span>select</span><span>(</span><span>0</span><span>);</span></div></div><div><div><div>11</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h4>Step 3：写两个测试感受一下<a href="#step-3写两个测试感受一下"><span>#</span></a></h4><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@</span><span>Test</span></div></div><div><div><div>2</div></div><div><span>void</span><span> </span><span>testString</span><span>() {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>jedis</span><span>.</span><span>set</span><span>(</span><span>"name"</span><span>, </span><span>"Kyle"</span><span>);</span></div></div><div><div><div>4</div></div><div><span>    </span><span>String</span><span> name </span><span><span>=</span><span> </span></span><span>jedis</span><span>.</span><span>get</span><span>(</span><span>"name"</span><span>);</span></div></div><div><div><div>5</div></div><div><span>    </span><span>System</span><span>.</span><span>out</span><span>.</span><span>println</span><span>(</span><span>"name = "</span><span><span> </span><span>+</span><span> name);</span></span></div></div><div><div><div>6</div></div><div><span>}</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>@</span><span>Test</span></div></div><div><div><div>9</div></div><div><span>void</span><span> </span><span>testHash</span><span>() {</span></div></div><div><div><div>10</div></div><div><span>    </span><span>jedis</span><span>.</span><span>hset</span><span>(</span><span>"reggie:user:1"</span><span>, </span><span>"name"</span><span>, </span><span>"Jack"</span><span>);</span></div></div><div><div><div>11</div></div><div><span>    </span><span>jedis</span><span>.</span><span>hset</span><span>(</span><span>"reggie:user:2"</span><span>, </span><span>"name"</span><span>, </span><span>"Rose"</span><span>);</span></div></div><div><div><div>12</div></div><div><span>    </span><span>jedis</span><span>.</span><span>hset</span><span>(</span><span>"reggie:user:1"</span><span>, </span><span>"age"</span><span>, </span><span>"21"</span><span>);</span></div></div><div><div><div>13</div></div><div><span>    </span><span>jedis</span><span>.</span><span>hset</span><span>(</span><span>"reggie:user:2"</span><span>, </span><span>"age"</span><span>, </span><span>"18"</span><span>);</span></div></div><div><div><div>14</div></div><div><span>    </span><span>Map</span><span>&lt;</span><span>String</span><span><span>,</span><span> </span></span><span>String</span><span><span>&gt;</span><span> map </span></span><span><span>=</span><span> </span></span><span>jedis</span><span>.</span><span>hgetAll</span><span>(</span><span>"reggie:user:1"</span><span>);</span></div></div><div><div><div>15</div></div><div><span>    </span><span>System</span><span>.</span><span>out</span><span>.</span><span>println</span><span>(map);</span></div></div><div><div><div>16</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section><section><h4>Step 4：释放资源<a href="#step-4释放资源"><span>#</span></a></h4><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@</span><span>AfterEach</span></div></div><div><div><div>2</div></div><div><span>void</span><span> </span><span>tearDown</span><span>() {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>if</span><span><span> (jedis </span><span>!=</span><span> </span></span><span>null</span><span>) {</span></div></div><div><div><div>4</div></div><div><span>        </span><span>jedis</span><span>.</span><span>close</span><span>();</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>6</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section></section><section><h3>Jedis 连接池：别频繁创建销毁连接<a href="#jedis-连接池别频繁创建销毁连接"><span>#</span></a></h3><div><div><div></div><div>直连的性能问题</div></div><div><p>Jedis 本身是<strong>线程不安全</strong>的，频繁创建和销毁连接也有性能损耗，推荐使用 <strong>Jedis 连接池</strong>。</p></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>public</span><span> </span><span>class</span><span><span> </span><span>JedisConnectionFactory</span><span> </span></span><span>{</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>    </span><span>private</span><span> </span><span>static</span><span> </span><span>JedisPool</span><span> jedisPool</span><span>;</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>    </span><span>static</span><span> {</span></div></div><div><div><div>6</div></div><div><span>        </span><span>// 配置连接池</span></div></div><div><div><div>7</div></div><div><span>        </span><span>JedisPoolConfig</span><span> poolConfig </span><span><span>=</span><span> </span></span><span>new</span><span> </span><span>JedisPoolConfig</span><span><span>()</span><span>;</span></span></div></div><div><div><div>8</div></div><div><span>        </span><span>poolConfig</span><span>.</span><span>setMaxTotal</span><span>(</span><span>8</span><span>);</span></div></div><div><div><div>9</div></div><div><span>        </span><span>poolConfig</span><span>.</span><span>setMaxIdle</span><span>(</span><span>8</span><span>);</span></div></div><div><div><div>10</div></div><div><span>        </span><span>poolConfig</span><span>.</span><span>setMinIdle</span><span>(</span><span>0</span><span>);</span></div></div><div><div><div>11</div></div><div><span>        </span><span>poolConfig</span><span>.</span><span>setMaxWaitMillis</span><span>(</span><span>1000</span><span>);</span></div></div><div><div><div>12</div></div><div><span>        </span><span>// 创建连接池对象，参数：连接池配置、服务端 IP、服务端端口、超时时间、密码</span></div></div><div><div><div>13</div></div><div><span><span>        </span></span><span>jedisPool </span><span>=</span><span> </span><span>new</span><span> </span><span>JedisPool</span><span><span>(poolConfig</span><span>,</span><span> </span></span><span>"101.42.225.160"</span><span><span>,</span><span> </span></span><span>6379</span><span><span>,</span><span> </span></span><span>1000</span><span><span>,</span><span> </span></span><span>"root"</span><span><span>)</span><span>;</span></span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>    </span><span>public</span><span> </span><span>static</span><span> </span><span>Jedis</span><span> </span><span>getJedis</span><span><span>()</span><span> </span><span>{</span></span></div></div><div><div><div>17</div></div><div><span>        </span><span>return</span><span> </span><span>jedisPool</span><span>.</span><span>getResource</span><span>();</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>19</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>测试类可以改成：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@</span><span>SpringBootTest</span></div></div><div><div><div>2</div></div><div><span>class</span><span><span> </span><span>RedisTestApplicationTests</span><span> </span></span><span>{</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>    </span><span>private</span><span> </span><span>Jedis</span><span> jedis </span><span><span>=</span><span> </span></span><span>JedisConnectionFactory</span><span>.</span><span>getJedis</span><span>();</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>@</span><span>Test</span></div></div><div><div><div>7</div></div><div><span>    </span><span>void</span><span> </span><span>testString</span><span><span>()</span><span> </span><span>{</span></span></div></div><div><div><div>8</div></div><div><span>        </span><span>jedis</span><span>.</span><span>set</span><span>(</span><span>"name"</span><span>, </span><span>"Kyle"</span><span>);</span></div></div><div><div><div>9</div></div><div><span>        </span><span>String</span><span><span> </span><span>name</span><span> </span></span><span><span>=</span><span> </span></span><span>jedis</span><span>.</span><span>get</span><span>(</span><span>"name"</span><span>);</span></div></div><div><div><div>10</div></div><div><span>        </span><span>System</span><span>.</span><span>out</span><span>.</span><span>println</span><span>(</span><span>"name = "</span><span><span> </span><span>+</span><span> name);</span></span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>@</span><span>Test</span></div></div><div><div><div>14</div></div><div><span>    </span><span>void</span><span> </span><span>testHash</span><span><span>()</span><span> </span><span>{</span></span></div></div><div><div><div>15</div></div><div><span>        </span><span>jedis</span><span>.</span><span>hset</span><span>(</span><span>"reggie:user:1"</span><span>, </span><span>"name"</span><span>, </span><span>"Jack"</span><span>);</span></div></div><div><div><div>16</div></div><div><span>        </span><span>jedis</span><span>.</span><span>hset</span><span>(</span><span>"reggie:user:2"</span><span>, </span><span>"name"</span><span>, </span><span>"Rose"</span><span>);</span></div></div><div><div><div>17</div></div><div><span>        </span><span>jedis</span><span>.</span><span>hset</span><span>(</span><span>"reggie:user:3"</span><span>, </span><span>"name"</span><span>, </span><span>"Kyle"</span><span>);</span></div></div><div><div><div>18</div></div><div><span>        </span><span>jedis</span><span>.</span><span>hset</span><span>(</span><span>"reggie:user:1"</span><span>, </span><span>"age"</span><span>, </span><span>"21"</span><span>);</span></div></div><div><div><div>19</div></div><div><span>        </span><span>jedis</span><span>.</span><span>hset</span><span>(</span><span>"reggie:user:2"</span><span>, </span><span>"age"</span><span>, </span><span>"18"</span><span>);</span></div></div><div><div><div>20</div></div><div><span>        </span><span>jedis</span><span>.</span><span>hset</span><span>(</span><span>"reggie:user:3"</span><span>, </span><span>"age"</span><span>, </span><span>"18"</span><span>);</span></div></div><div><div><div>21</div></div><div><span>        </span><span>Map</span><span>&lt;</span><span>String</span><span>, </span><span>String</span><span><span>&gt; </span><span>map</span><span> </span></span><span><span>=</span><span> </span></span><span>jedis</span><span>.</span><span>hgetAll</span><span>(</span><span>"reggie:user:1"</span><span>);</span></div></div><div><div><div>22</div></div><div><span>        </span><span>System</span><span>.</span><span>out</span><span>.</span><span>println</span><span>(map);</span></div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>@</span><span>AfterEach</span></div></div><div><div><div>26</div></div><div><span>    </span><span>void</span><span> </span><span>tearDown</span><span><span>()</span><span> </span><span>{</span></span></div></div><div><div><div>27</div></div><div><span>        </span><span>if</span><span><span> (jedis </span><span>!=</span><span> </span></span><span>null</span><span>) {</span></div></div><div><div><div>28</div></div><div><span>            </span><span>jedis</span><span>.</span><span>close</span><span>();</span></div></div><div><div><div>29</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>30</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>31</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section><section><h3>Spring Data Redis：项目里更常见的选择<a href="#spring-data-redis项目里更常见的选择"><span>#</span></a></h3><p>Spring Data 是 Spring 的数据操作模块，其中对 Redis 的集成模块叫 <strong>Spring Data Redis</strong>。</p><p>官网：<a href="https://spring.io/projects/spring-data-redis" target="_blank">https://spring.io/projects/spring-data-redis</a></p><p>主要能力：</p><ul>
<li>整合不同 Redis 客户端（Lettuce 和 Jedis）</li>
<li>提供 <code>RedisTemplate</code> 统一 API</li>
<li>支持 Redis 发布订阅模型</li>
<li>支持 Redis 哨兵和集群</li>
<li>支持基于 Lettuce 的响应式编程</li>
<li>支持 JDK、JSON、字符串、Spring 对象等序列化方式</li>
<li>支持基于 Redis 的 JDKCollection 实现</li>
</ul><p><code>RedisTemplate</code> 把不同数据类型的操作封装到了不同 API 中：</p>

<table><thead><tr><th>API</th><th>返回值类型</th><th>说明</th></tr></thead><tbody><tr><td><code>redisTemplate.opsForValue()</code></td><td><code>ValueOperations</code></td><td>操作 String 类型数据</td></tr><tr><td><code>redisTemplate.opsForHash()</code></td><td><code>HashOperations</code></td><td>操作 Hash 类型数据</td></tr><tr><td><code>redisTemplate.opsForList()</code></td><td><code>ListOperations</code></td><td>操作 List 类型数据</td></tr><tr><td><code>redisTemplate.opsForSet()</code></td><td><code>SetOperations</code></td><td>操作 Set 类型数据</td></tr><tr><td><code>redisTemplate.opsForZSet()</code></td><td><code>ZSetOperations</code></td><td>操作 SortedSet 类型数据</td></tr><tr><td><code>redisTemplate</code></td><td>—</td><td>通用命令</td></tr></tbody></table><section><h4>Step 1：导入依赖<a href="#step-1导入依赖"><span>#</span></a></h4><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>&lt;!--redis依赖--&gt;</span></div></div><div><div><div>2</div></div><div><span>&lt;</span><span>dependency</span><span>&gt;</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>&lt;</span><span>groupId</span><span>&gt;org.springframework.boot&lt;/</span><span>groupId</span><span>&gt;</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>&lt;</span><span>artifactId</span><span>&gt;spring-boot-starter-data-redis&lt;/</span><span>artifactId</span><span>&gt;</span></div></div><div><div><div>5</div></div><div><span>&lt;/</span><span>dependency</span><span>&gt;</span></div></div><div><div><div>6</div></div><div><span>&lt;!--common-pool--&gt;</span></div></div><div><div><div>7</div></div><div><span>&lt;</span><span>dependency</span><span>&gt;</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>&lt;</span><span>groupId</span><span>&gt;org.apache.commons&lt;/</span><span>groupId</span><span>&gt;</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>&lt;</span><span>artifactId</span><span>&gt;commons-pool2&lt;/</span><span>artifactId</span><span>&gt;</span></div></div><div><div><div>10</div></div><div><span>&lt;/</span><span>dependency</span><span>&gt;</span></div></div><div><div><div>11</div></div><div><span>&lt;!--Jackson依赖--&gt;</span></div></div><div><div><div>12</div></div><div><span>&lt;</span><span>dependency</span><span>&gt;</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>&lt;</span><span>groupId</span><span>&gt;com.fasterxml.jackson.core&lt;/</span><span>groupId</span><span>&gt;</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>&lt;</span><span>artifactId</span><span>&gt;jackson-databind&lt;/</span><span>artifactId</span><span>&gt;</span></div></div><div><div><div>15</div></div><div><span>&lt;/</span><span>dependency</span><span>&gt;</span></div></div><div><div><div>16</div></div><div><span>&lt;!--lombok--&gt;</span></div></div><div><div><div>17</div></div><div><span>&lt;</span><span>dependency</span><span>&gt;</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>&lt;</span><span>groupId</span><span>&gt;org.projectlombok&lt;/</span><span>groupId</span><span>&gt;</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>&lt;</span><span>artifactId</span><span>&gt;lombok&lt;/</span><span>artifactId</span><span>&gt;</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>&lt;</span><span>optional</span><span>&gt;true&lt;/</span><span>optional</span><span>&gt;</span></div></div><div><div><div>21</div></div><div><span>&lt;/</span><span>dependency</span><span>&gt;</span></div></div><div><div><div>22</div></div><div><span>&lt;</span><span>dependency</span><span>&gt;</span></div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>&lt;</span><span>groupId</span><span>&gt;org.springframework.boot&lt;/</span><span>groupId</span><span>&gt;</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>&lt;</span><span>artifactId</span><span>&gt;spring-boot-starter-test&lt;/</span><span>artifactId</span><span>&gt;</span></div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>&lt;</span><span>scope</span><span>&gt;test&lt;/</span><span>scope</span><span>&gt;</span></div></div><div><div><div>26</div></div><div><span>&lt;/</span><span>dependency</span><span>&gt;</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section><section><h4>Step 2：配置 Redis<a href="#step-2配置-redis"><span>#</span></a></h4><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>spring</span><span>:</span></div></div><div><div><div>2</div></div><div><span>  </span><span>redis</span><span>:</span></div></div><div><div><div>3</div></div><div><span>    </span><span>host</span><span>: </span><span>101.42.225.160</span></div></div><div><div><div>4</div></div><div><span>    </span><span>port</span><span>: </span><span>6379</span></div></div><div><div><div>5</div></div><div><span>    </span><span>password</span><span>: </span><span>root</span></div></div><div><div><div>6</div></div><div><span>    </span><span>lettuce</span><span>:</span></div></div><div><div><div>7</div></div><div><span>      </span><span>pool</span><span>:</span></div></div><div><div><div>8</div></div><div><span>        </span><span>max-active</span><span>: </span><span>8</span></div></div><div><div><div>9</div></div><div><span>        </span><span>max-idle</span><span>: </span><span>8</span></div></div><div><div><div>10</div></div><div><span>        </span><span>min-idle</span><span>: </span><span>0</span></div></div><div><div><div>11</div></div><div><span>        </span><span>max-wait</span><span>: </span><span>100ms</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h4>Step 3：注入 RedisTemplate<a href="#step-3注入-redistemplate"><span>#</span></a></h4><p>有了 Spring Boot 自动装配，可以直接拿来用：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@</span><span>Autowired</span></div></div><div><div><div>2</div></div><div><span>private</span><span> </span><span>RedisTemplate</span><span> redisTemplate</span><span>;</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h4>Step 4：写个测试<a href="#step-4写个测试"><span>#</span></a></h4><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@</span><span>Test</span></div></div><div><div><div>2</div></div><div><span>void</span><span> </span><span>stringTest</span><span>() {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>redisTemplate</span><span>.</span><span>opsForValue</span><span>().</span><span>set</span><span>(</span><span>"username"</span><span>, </span><span>"David"</span><span>);</span></div></div><div><div><div>4</div></div><div><span>    </span><span>String</span><span> username </span><span><span>=</span><span> (String) </span></span><span>redisTemplate</span><span>.</span><span>opsForValue</span><span>().</span><span>get</span><span>(</span><span>"username"</span><span>);</span></div></div><div><div><div>5</div></div><div><span>    </span><span>System</span><span>.</span><span>out</span><span>.</span><span>println</span><span>(username);</span></div></div><div><div><div>6</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section></section><section><h3>自定义序列化：默认 JDK 序列化不太好看<a href="#自定义序列化默认-jdk-序列化不太好看"><span>#</span></a></h3><p><code>RedisTemplate</code> 可以接收任意 <code>Object</code> 作为值写入 Redis，但默认会用 <strong>JDK 序列化</strong>，结果可能是这样的：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>\xAC\xED\x00\x05t\x00\x06\xE5\xBC\xA0\xE4\xB8\x89</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div></div><div>JDK 序列化的缺点</div></div><div><ul>
<li>可读性差</li>
<li>内存占用较大</li>
</ul></div></div><p>可以自定义 <code>RedisTemplate</code> 的序列化方式，比如在 <code>com.blog.config</code> 包下配置：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@</span><span>Configuration</span></div></div><div><div><div>2</div></div><div><span>public</span><span> </span><span>class</span><span><span> </span><span>RedisConfig</span><span> </span></span><span>{</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>@</span><span>Bean</span></div></div><div><div><div>5</div></div><div><span>    </span><span>public</span><span> </span><span>RedisTemplate</span><span>&lt;</span><span>String</span><span><span>,</span><span> </span></span><span>Object</span><span><span>&gt;</span><span> </span></span><span>redisTemplate</span><span>(</span><span>RedisConnectionFactory</span><span><span> </span><span>connectionFactory</span><span>)</span><span> </span><span>{</span></span></div></div><div><div><div>6</div></div><div><span>        </span><span>// 创建 RedisTemplate 对象</span></div></div><div><div><div>7</div></div><div><span>        </span><span>RedisTemplate</span><span>&lt;</span><span>String</span><span>, </span><span>Object</span><span><span>&gt; </span><span>template</span><span> </span></span><span><span>=</span><span> </span></span><span>new</span><span> </span><span>RedisTemplate</span><span>&lt;&gt;();</span></div></div><div><div><div>8</div></div><div><span>        </span><span>// 设置连接工厂</span></div></div><div><div><div>9</div></div><div><span>        </span><span>template</span><span>.</span><span>setConnectionFactory</span><span>(connectionFactory);</span></div></div><div><div><div>10</div></div><div><span>        </span><span>// 创建 JSON 序列化工具</span></div></div><div><div><div>11</div></div><div><span>        </span><span>GenericJackson2JsonRedisSerializer</span><span><span> </span><span>jsonRedisSerializer</span><span> </span></span><span>=</span></div></div><div><div><div>12</div></div><div><span>                </span><span>new</span><span> </span><span>GenericJackson2JsonRedisSerializer</span><span>();</span></div></div><div><div><div>13</div></div><div><span>        </span><span>// 设置 Key 的序列化</span></div></div><div><div><div>14</div></div><div><span>        </span><span>template</span><span>.</span><span>setKeySerializer</span><span>(</span><span>RedisSerializer</span><span>.</span><span>string</span><span>());</span></div></div><div><div><div>15</div></div><div><span>        </span><span>template</span><span>.</span><span>setHashKeySerializer</span><span>(</span><span>RedisSerializer</span><span>.</span><span>string</span><span>());</span></div></div><div><div><div>16</div></div><div><span>        </span><span>// 设置 Value 的序列化</span></div></div><div><div><div>17</div></div><div><span>        </span><span>template</span><span>.</span><span>setValueSerializer</span><span>(jsonRedisSerializer);</span></div></div><div><div><div>18</div></div><div><span>        </span><span>template</span><span>.</span><span>setHashValueSerializer</span><span>(jsonRedisSerializer);</span></div></div><div><div><div>19</div></div><div><span>        </span><span>// 返回</span></div></div><div><div><div>20</div></div><div><span>        </span><span>return</span><span> template;</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>22</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>写个 <code>User</code> 类试试：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@</span><span>Data</span></div></div><div><div><div>2</div></div><div><span>@</span><span>AllArgsConstructor</span></div></div><div><div><div>3</div></div><div><span>@</span><span>NoArgsConstructor</span></div></div><div><div><div>4</div></div><div><span>public</span><span> </span><span>class</span><span><span> </span><span>User</span><span> </span></span><span>{</span></div></div><div><div><div>5</div></div><div><span>    </span><span>private</span><span> </span><span>String</span><span> name</span><span>;</span></div></div><div><div><div>6</div></div><div><span>    </span><span>private</span><span> </span><span>Integer</span><span> age</span><span>;</span></div></div><div><div><div>7</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>测试方法：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@</span><span>Test</span></div></div><div><div><div>2</div></div><div><span>void</span><span> </span><span>stringTest</span><span>() {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>redisTemplate</span><span>.</span><span>opsForValue</span><span>().</span><span>set</span><span>(</span><span>"userdata"</span><span>, </span><span>new</span><span> </span><span>User</span><span>(</span><span>"张三"</span><span>, </span><span>18</span><span>));</span></div></div><div><div><div>4</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>采用 JSON 序列化后，结果大致如下：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{</span></div></div><div><div><div>2</div></div><div><span>  </span><span>"@class"</span><span>: </span><span>"com.blog.entity.User"</span><span>,</span></div></div><div><div><div>3</div></div><div><span>  </span><span>"name"</span><span>: </span><span>"张三"</span><span>,</span></div></div><div><div><div>4</div></div><div><span>  </span><span>"age"</span><span>: </span><span>18</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>可读性提升明显，而且能把 Java 对象自动序列化为 JSON，查询时再自动反序列化回 Java 对象。</p><div><div><div></div><div>关于 <code>@class</code> 字段</div></div><div><p>它记录了序列化时对应的 class 名称，目的是查询时能自动反序列化。
但这会带来<strong>额外内存开销</strong>，所以还有更省内存的方案。</p></div></div></section><section><h3>StringRedisTemplate：手动序列化，内存更清爽<a href="#stringredistemplate手动序列化内存更清爽"><span>#</span></a></h3><p>为了节省内存，可以不使用 JSON 序列化器处理 value，而是统一使用 <strong>String 序列化器</strong>。这样只能存 String 类型的 key 和 value；需要存 Java 对象时，<strong>手动完成序列化和反序列化</strong>。</p><p>因为序列化和反序列化都由我们自己实现，Spring Data Redis 就不会把 class 信息写进 Redis 了。</p><p>Spring Data Redis 提供了 <code>RedisTemplate</code> 的子类 <code>StringRedisTemplate</code>，它的 key 和 value 默认就是 String 序列化：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>public</span><span> </span><span>class</span><span><span> </span><span>StringRedisTemplate</span><span> </span></span><span>extends</span><span><span> </span><span>RedisTemplate</span><span>&lt;</span><span>String</span><span>,</span><span> </span><span>String</span><span>&gt;</span><span> </span></span><span>{</span></div></div><div><div><div>2</div></div><div><span>    </span><span>public</span><span> </span><span>StringRedisTemplate</span><span><span>()</span><span> </span><span>{</span></span></div></div><div><div><div>3</div></div><div><span>        </span><span>this</span><span>.</span><span>setKeySerializer</span><span>(</span><span>RedisSerializer</span><span>.</span><span>string</span><span>());</span></div></div><div><div><div>4</div></div><div><span>        </span><span>this</span><span>.</span><span>setValueSerializer</span><span>(</span><span>RedisSerializer</span><span>.</span><span>string</span><span>());</span></div></div><div><div><div>5</div></div><div><span>        </span><span>this</span><span>.</span><span>setHashKeySerializer</span><span>(</span><span>RedisSerializer</span><span>.</span><span>string</span><span>());</span></div></div><div><div><div>6</div></div><div><span>        </span><span>this</span><span>.</span><span>setHashValueSerializer</span><span>(</span><span>RedisSerializer</span><span>.</span><span>string</span><span>());</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>8</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这样就可以省去自定义序列化的步骤，直接使用：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@</span><span>Autowired</span></div></div><div><div><div>2</div></div><div><span>private</span><span> </span><span>StringRedisTemplate</span><span> stringRedisTemplate</span><span>;</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>@</span><span>Autowired</span></div></div><div><div><div>5</div></div><div><span>private</span><span> </span><span>ObjectMapper</span><span> mapper</span><span>;</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>@</span><span>Test</span></div></div><div><div><div>8</div></div><div><span>void</span><span> </span><span>stringTest</span><span>() throws JsonProcessingException {</span></div></div><div><div><div>9</div></div><div><span>    </span><span>// 创建对象</span></div></div><div><div><div>10</div></div><div><span>    </span><span>User</span><span> user </span><span><span>=</span><span> </span></span><span>new</span><span> </span><span>User</span><span>(</span><span>"张三"</span><span><span>,</span><span> </span></span><span>18</span><span><span>)</span><span>;</span></span></div></div><div><div><div>11</div></div><div><span>    </span><span>// 手动序列化</span></div></div><div><div><div>12</div></div><div><span>    </span><span>String</span><span> json </span><span><span>=</span><span> </span></span><span>mapper</span><span>.</span><span>writeValueAsString</span><span>(user);</span></div></div><div><div><div>13</div></div><div><span>    </span><span>// 写入数据</span></div></div><div><div><div>14</div></div><div><span>    </span><span>stringRedisTemplate</span><span>.</span><span>opsForValue</span><span>().</span><span>set</span><span>(</span><span>"userdata"</span><span>, json);</span></div></div><div><div><div>15</div></div><div><span>    </span><span>// 获取数据</span></div></div><div><div><div>16</div></div><div><span>    </span><span>String</span><span> userdata </span><span><span>=</span><span> </span></span><span>stringRedisTemplate</span><span>.</span><span>opsForValue</span><span>().</span><span>get</span><span>(</span><span>"userdata"</span><span>);</span></div></div><div><div><div>17</div></div><div><span>    </span><span>// 手动反序列化</span></div></div><div><div><div>18</div></div><div><span>    </span><span>User</span><span> readValue </span><span><span>=</span><span> </span></span><span>mapper</span><span>.</span><span>readValue</span><span>(userdata, </span><span>User</span><span>.</span><span>class</span><span>);</span></div></div><div><div><div>19</div></div><div><span>    </span><span>System</span><span>.</span><span>out</span><span>.</span><span>println</span><span>(readValue);</span></div></div><div><div><div>20</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>存入 Redis 中是这样的：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{</span></div></div><div><div><div>2</div></div><div><span>  </span><span>"name"</span><span>: </span><span>"张三"</span><span>,</span></div></div><div><div><div>3</div></div><div><span>  </span><span>"age"</span><span>: </span><span>18</span></div></div><div><div><div>4</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div></div><div>序列化选型建议</div></div><div><ul>
<li><strong>不想写序列化代码</strong>、且不介意多存 <code>@class</code> 字段 → 使用 <code>RedisTemplate</code> + <code>GenericJackson2JsonRedisSerializer</code></li>
<li><strong>追求极致内存</strong>、希望 key 清晰易读 → 使用 <code>StringRedisTemplate</code> + 手动 JSON 序列化</li>
</ul></div></div></section></section><section><h2>收个尾：三个阶段完成跃迁<a href="#收个尾三个阶段完成跃迁"><span>#</span></a></h2><p>回顾一下，这篇笔记其实完成了三个阶段的升级：</p><ol>
<li><strong>认知层</strong>：理解 NoSQL 与关系型数据库的核心差异，包括结构化、关联、查询、事务、存储和扩展性。</li>
<li><strong>命令层</strong>：掌握 Redis 五大数据结构（String、Hash、List、Set、SortedSet）以及通用命令。</li>
<li><strong>工程层</strong>：通过 Jedis 与 Spring Data Redis 两种 Java 客户端，把 Redis 接入实际项目，并掌握序列化选型技巧。</li>
</ol><div><div><div></div><div>下一步学什么</div></div><div><p>建议继续深入：<strong>持久化机制（RDB/AOF）→ 主从复制 → 哨兵模式 → 分片集群</strong>，一步步补齐 Redis 生产知识体系。</p></div></div></section></section>]]></content>
    </entry>
</feed>
