<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>adore🍊</title>
  
  
  <link href="https://blog.adoreorg.cn/atom.xml" rel="self"/>
  
  <link href="https://blog.adoreorg.cn/"/>
  <updated>2026-06-08T11:02:20.177Z</updated>
  <id>https://blog.adoreorg.cn/</id>
  
  <author>
    <name>adore🍊</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>夸克网盘自动转存</title>
    <link href="https://blog.adoreorg.cn/posts/quark-auto-save.html"/>
    <id>https://blog.adoreorg.cn/posts/quark-auto-save.html</id>
    <published>2026-06-05T09:30:00.000Z</published>
    <updated>2026-06-08T11:02:20.177Z</updated>
    
    <content type="html"><![CDATA[<h1>夸克网盘自动转存（QAS）部署与使用教程</h1><blockquote><p>🎯 <strong>一站式解决方案</strong>：夸克网盘签到、自动转存、命名整理、推送提醒和刷新媒体库一条龙。</p></blockquote><p>对于一些持续更新的资源，隔段时间去转存十分麻烦。定期执行本脚本自动转存、文件名整理，配合 <strong>SmartStrm / OpenList / Emby</strong> 可达到自动追更的效果。🥳</p>    <div class="note note-warning">      <div class="note-header">        <i class="note-icon fa-regular fa-circle-dot"></i>        <span class="note-title">重要提醒</span>      </div>      <div class="note-content">        <p>⛔️⛔️⛔️ <strong>资源不会每时每刻更新，严禁设定过高的定时运行频率！</strong> 以免账号风控和给夸克服务器造成不必要的压力。</p><blockquote><p>雪山崩塌，每一片雪花都有责任！</p></blockquote>      </div>    </div>  <hr><h2 id="一、项目介绍">一、项目介绍</h2><p><strong>夸克网盘自动转存（QAS）</strong> 是一款基于 Docker 部署的开源工具，专注于解决网盘资源的手动转存痛点。通过简单的正则表达式和任务配置，即可实现资源的自动追更、文件名整理及媒体库联动刷新。</p><h3 id="核心优势">核心优势</h3><ul><li>🐳 <strong>Docker 一键部署</strong>：提供 WebUI 图形化配置界面</li><li>🔄 <strong>全自动转存</strong>：定时执行，无需人工干预</li><li>📝 <strong>智能命名</strong>：正则表达式批量整理文件名</li><li>📺 <strong>媒体库联动</strong>：自动刷新 Emby / Jellyfin 等媒体库</li><li>🔌 <strong>插件化扩展</strong>：支持模块化插件开发</li></ul><h3 id="项目信息">项目信息</h3><table><thead><tr><th style="text-align:left">项目</th><th style="text-align:left">信息</th></tr></thead><tbody><tr><td style="text-align:left"><strong>项目名称</strong></td><td style="text-align:left">quark-auto-save</td></tr><tr><td style="text-align:left"><strong>镜像名称</strong></td><td style="text-align:left"><code>cp0204/quark-auto-save:latest</code></td></tr><tr><td style="text-align:left"><strong>默认端口</strong></td><td style="text-align:left"><code>5005</code></td></tr><tr><td style="text-align:left"><strong>默认账号</strong></td><td style="text-align:left"><code>admin</code></td></tr><tr><td style="text-align:left"><strong>默认密码</strong></td><td style="text-align:left"><code>admin123</code></td></tr><tr><td style="text-align:left"><strong>国内镜像</strong></td><td style="text-align:left"><code>registry.cn-shenzhen.aliyuncs.com/cp0204/quark-auto-save:latest</code></td></tr></tbody></table><hr><h2 id="二、功能特性">二、功能特性</h2><h3 id="2-1-部署方式">2.1 部署方式</h3><ul><li>✅ 可能兼容青龙</li><li>✅ Docker 部署，提供 WebUI 配置</li></ul><h3 id="2-2-分享链接">2.2 分享链接</h3><ul><li>✅ 支持分享链接的子目录</li><li>✅ 记录失效分享并跳过任务</li><li>✅ 支持需提取码的分享链接</li><li>✅ 智能搜索资源并自动填充</li></ul><h3 id="2-3-文件管理">2.3 文件管理</h3><ul><li>✅ 目标目录不存在时自动新建</li><li>✅ 跳过已转存过的文件</li><li>✅ 正则过滤要转存的文件名</li><li>✅ 转存后文件名整理（正则替换）</li><li>✅ 可选忽略文件后缀</li></ul><h3 id="2-4-任务管理">2.4 任务管理</h3><ul><li>✅ 支持多组任务</li><li>✅ 任务结束期限，期限后不执行此任务</li><li>✅ 可单独指定子任务星期几执行</li></ul><h3 id="2-5-媒体库整合">2.5 媒体库整合</h3><ul><li>✅ 根据任务名搜索 Emby 媒体库</li><li>✅ 追更或整理后自动刷新 Emby 媒体库</li><li>✅ 插件模块化，允许自行开发和挂载插件</li></ul><h3 id="2-6-其它功能">2.6 其它功能</h3><ul><li>✅ 每日签到领空间</li><li>✅ 支持多个通知推送渠道</li><li>✅ 支持多账号（多账号签到，仅首账号转存）</li></ul><hr><h2 id="三、部署教程">三、部署教程</h2><h3 id="3-1-准备工作">3.1 准备工作</h3>    <div class="note note-primary">      <div class="note-header">        <i class="note-icon fa-regular fa-star"></i>        <span class="note-title">部署前准备</span>      </div>      <div class="note-content">        <ol><li>一台运行 <strong>Docker</strong> 的服务器（推荐 Linux / NAS）</li><li>已安装 <strong>Docker</strong> 和 <strong>Docker Compose</strong>（可选）</li><li>服务器开放 <strong>5005</strong> 端口（可自定义）</li><li>一个可用的 <strong>夸克网盘</strong> 账号和 Cookie</li></ol>      </div>    </div>  <h3 id="3-2-部署方式">3.2 部署方式</h3><div class="tabs" id="部署方式"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#部署方式-1">Docker Run</button></li><li class="tab"><button type="button" data-href="#部署方式-2">Docker Compose</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="部署方式-1"><p><strong>使用 <code>docker run</code> 一键部署：</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">docker run -d \</span><br><span class="line">  --name quark-auto-save \</span><br><span class="line">  -p 5005:5005 \</span><br><span class="line">  -e WEBUI_USERNAME=admin \</span><br><span class="line">  -e WEBUI_PASSWORD=admin123 \</span><br><span class="line">  -v ./quark-auto-save/config:/app/config \</span><br><span class="line">  -v ./quark-auto-save/media:/media \</span><br><span class="line">  --network bridge \</span><br><span class="line">  --restart unless-stopped \</span><br><span class="line">  cp0204/quark-auto-save:latest</span><br></pre></td></tr></table></figure><p>注意：<code>-p 5005:5005</code>：冒号<strong>前</strong>的端口可修改（即部署后访问的端口），冒号<strong>后</strong>的端口不可修改。</p><details class="folding-tag" ><summary> 国内用户 </summary>              <div class='content'>              <p>国内用户可使用阿里云镜像加速拉取：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">docker run -d \</span><br><span class="line">  --name quark-auto-save \</span><br><span class="line">  -p 5005:5005 \</span><br><span class="line">  -e WEBUI_USERNAME=admin \</span><br><span class="line">  -e WEBUI_PASSWORD=admin123 \</span><br><span class="line">  -v ./quark-auto-save/config:/app/config \</span><br><span class="line">  -v ./quark-auto-save/media:/media \</span><br><span class="line">  --network bridge \</span><br><span class="line">  --restart unless-stopped \</span><br><span class="line">  registry.cn-shenzhen.aliyuncs.com/cp0204/quark-auto-save:latest</span><br></pre></td></tr></table></figure>              </div>            </details><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="部署方式-2"><p><strong>使用 <code>docker-compose.yml</code> 部署（推荐）：</strong></p><p>新建 <code>docker-compose.yml</code> 文件，写入以下内容：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">name:</span> <span class="string">quark-auto-save</span></span><br><span class="line"><span class="attr">services:</span></span><br><span class="line">  <span class="attr">quark-auto-save:</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">cp0204/quark-auto-save:latest</span></span><br><span class="line">    <span class="attr">container_name:</span> <span class="string">quark-auto-save</span></span><br><span class="line">    <span class="attr">network_mode:</span> <span class="string">bridge</span></span><br><span class="line">    <span class="attr">ports:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="number">5005</span><span class="string">:5005</span></span><br><span class="line">    <span class="attr">restart:</span> <span class="string">unless-stopped</span></span><br><span class="line">    <span class="attr">environment:</span></span><br><span class="line">      <span class="attr">WEBUI_USERNAME:</span> <span class="string">&quot;admin&quot;</span></span><br><span class="line">      <span class="attr">WEBUI_PASSWORD:</span> <span class="string">&quot;admin123&quot;</span></span><br><span class="line">    <span class="attr">volumes:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">./quark-auto-save/config:/app/config</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">./quark-auto-save/media:/media</span></span><br></pre></td></tr></table></figure><p>然后执行：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker-compose up -d</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h3 id="3-3-访问管理后台">3.3 访问管理后台</h3><p>部署完成后，浏览器访问：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">http://yourhost:5005</span><br></pre></td></tr></table></figure><p>将 <code>yourhost</code> 替换为您的服务器 IP 或域名。</p>    <div class="note note-success">      <div class="note-header">        <i class="note-icon fa-regular fa-circle-check"></i>        <span class="note-title">部署成功</span>      </div>      <div class="note-content">        <p>首次访问使用默认账号密码登录后，<strong>请立即修改密码</strong>！</p>      </div>    </div>  <h3 id="3-4-环境变量配置">3.4 环境变量配置</h3><table><thead><tr><th style="text-align:left">环境变量</th><th style="text-align:left">默认值</th><th style="text-align:left">备注</th></tr></thead><tbody><tr><td style="text-align:left"><code>WEBUI_USERNAME</code></td><td style="text-align:left"><code>admin</code></td><td style="text-align:left">管理账号</td></tr><tr><td style="text-align:left"><code>WEBUI_PASSWORD</code></td><td style="text-align:left"><code>admin123</code></td><td style="text-align:left">管理密码</td></tr><tr><td style="text-align:left"><code>PORT</code></td><td style="text-align:left"><code>5005</code></td><td style="text-align:left">管理后台端口</td></tr><tr><td style="text-align:left"><code>PLUGIN_FLAGS</code></td><td style="text-align:left">（空）</td><td style="text-align:left">插件标志，如 <code>-emby,-aria2</code> 禁用某些插件</td></tr><tr><td style="text-align:left"><code>TASK_TIMEOUT</code></td><td style="text-align:left"><code>1800</code></td><td style="text-align:left">任务执行超时时间（秒），超时则任务结束</td></tr></tbody></table><h3 id="3-5-一键更新">3.5 一键更新</h3><p>使用 <strong>Watchtower</strong> 实现自动更新：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">docker run --<span class="built_in">rm</span> \</span><br><span class="line">  -v /var/run/docker.sock:/var/run/docker.sock \</span><br><span class="line">  containrrr/watchtower \</span><br><span class="line">  -cR quark-auto-save</span><br></pre></td></tr></table></figure><h3 id="3-6-WebUI-预览">3.6 WebUI 预览</h3><div class="fj-gallery"><p><img src="https://markpic.adoreorg.cn/2026/06/webui.webp" alt="夸克自动转存 - WebUI 主页"><br><img src="https://markpic.adoreorg.cn/2026/06/startui.webp" alt="夸克自动转存 - 启动界面"></p>          </div><hr><h2 id="四、使用说明">四、使用说明</h2><h3 id="4-1-正则处理示例">4.1 正则处理示例</h3>    <div class="note note-primary">      <div class="note-header">        <i class="note-icon fa-regular fa-star"></i>        <span class="note-title">教程说明</span>      </div>      <div class="note-content">        <p>正则表达式是 QAS 的核心功能之一，通过灵活的 <code>pattern</code> 和 <code>replace</code> 规则，可以实现自动化的文件命名整理。</p>      </div>    </div>  <table><thead><tr><th style="text-align:left">pattern</th><th style="text-align:left">replace</th><th style="text-align:left">效果</th></tr></thead><tbody><tr><td style="text-align:left"><code>.*</code></td><td style="text-align:left">（空）</td><td style="text-align:left">无脑转存所有文件，不整理</td></tr><tr><td style="text-align:left"><code>\.mp4$</code></td><td style="text-align:left">（空）</td><td style="text-align:left">转存所有 <code>.mp4</code> 后缀的文件</td></tr><tr><td style="text-align:left"><code>^【电影TT】花好月圆(\d+)\.(mp4|mkv)</code></td><td style="text-align:left"><code>\1.\2</code></td><td style="text-align:left"><code>【电影TT】花好月圆01.mp4</code> → <code>01.mp4</code><br><code>【电影TT】花好月圆02.mkv</code> → <code>02.mkv</code></td></tr><tr><td style="text-align:left"><code>^(\d+)\.mp4</code></td><td style="text-align:left"><code>S02E\1.mp4</code></td><td style="text-align:left"><code>01.mp4</code> → <code>S02E01.mp4</code><br><code>02.mp4</code> → <code>S02E02.mp4</code></td></tr><tr><td style="text-align:left"><code>$TV</code></td><td style="text-align:left">（空）</td><td style="text-align:left">魔法匹配剧集文件</td></tr><tr><td style="text-align:left"><code>^(\d+)\.mp4</code></td><td style="text-align:left"><code>&#123;TASKNAME&#125;.S02E\1.mp4</code></td><td style="text-align:left"><code>01.mp4</code> → <code>任务名.S02E01.mp4</code></td></tr></tbody></table><p>更多正则使用说明：<a href="https://github.com/cp0204/quark-auto-save/wiki">正则处理教程</a></p><div class="tip info 魔法匹配和魔法变量"><p>在正则处理中，我们定义了一些&quot;魔法匹配&quot;模式。如果 <strong>表达式</strong> 的值以 <code>$</code> 开头且 <strong>替换式</strong> 留空，程序将自动使用预设的正则表达式进行匹配和替换。</p><p>自 <strong>v0.6.0</strong> 开始，支持更多以 <code>{}</code> 包裹的&quot;魔法变量&quot;，可以更灵活地进行重命名。</p><p>更多说明请看：<a href="https://github.com/cp0204/quark-auto-save/wiki">魔法匹配和魔法变量</a></p></div><h3 id="4-2-刷新媒体库">4.2 刷新媒体库</h3><p>在有新转存时，可触发完成相应功能，如自动刷新媒体库、生成 <code>.strm</code> 文件等。配置指南：<a href="https://github.com/cp0204/quark-auto-save/wiki">插件配置</a></p><p>媒体库模块以插件的方式的集成，如果你有兴趣请参考：<a href="https://github.com/cp0204/quark-auto-save/wiki">插件开发指南</a></p><h3 id="4-3-更多使用技巧">4.3 更多使用技巧</h3><p>请参考 Wiki：<a href="https://github.com/cp0204/quark-auto-save/wiki">使用技巧集锦</a></p><hr><h2 id="五、生态项目">五、生态项目</h2><p>以下展示 QAS 生态项目，包括官方项目和第三方项目。</p><h3 id="5-1-官方项目">5.1 官方项目</h3><div class="fj-gallery"><table><thead><tr><th style="text-align:left">项目</th><th style="text-align:left">简介</th></tr></thead><tbody><tr><td style="text-align:left"><strong>QAS 一键推送助手</strong></td><td style="text-align:left">油猴脚本，在夸克网盘分享页面添加推送到 QAS 的按钮</td></tr><tr><td style="text-align:left"><strong>SmartStrm</strong></td><td style="text-align:left">STRM 文件生成工具，用于转存后处理，媒体免下载入库播放</td></tr></tbody></table>          </div><h3 id="5-2-第三方开源项目">5.2 第三方开源项目</h3>    <div class="note note-warning">      <div class="note-header">        <i class="note-icon fa-regular fa-circle-dot"></i>        <span class="note-title">风险提示</span>      </div>      <div class="note-content">        <p>以下第三方开源项目均由社区开发并保持开源，与 QAS 作者无直接关联。在部署到生产环境前，请自行评估相关风险。</p><p>如果您有新的项目没有在此列出，可以通过 <strong>Issues</strong> 提交。</p>      </div>    </div>  <table><thead><tr><th style="text-align:left">项目</th><th style="text-align:left">简介</th></tr></thead><tbody><tr><td style="text-align:left"><strong>nonebot-plugin-quark-autosave</strong></td><td style="text-align:left">QAS Telegram 机器人，快速管理自动转存任务</td></tr><tr><td style="text-align:left"><strong>Astrbot_plugin_quarksave</strong></td><td style="text-align:left">AstrBot 插件，调用 <code>quark_auto_save</code> 实现自动转存资源到夸克网盘</td></tr><tr><td style="text-align:left"><strong>Telegram 媒体资源管理机器人</strong></td><td style="text-align:left">一个功能丰富的 Telegram 机器人，专注于媒体资源管理、Emby 集成、自动下载和夸克网盘资源管理</td></tr></tbody></table><hr><h2 id="六、常见问题">六、常见问题</h2><details class="folding-tag" ><summary> Q1: 部署后无法访问 WebUI？ </summary>              <div class='content'>              <ol><li>检查服务器防火墙是否开放 <code>5005</code> 端口</li><li>确认容器是否正常运行：<code>docker ps | grep quark-auto-save</code></li><li>查看容器日志：<code>docker logs quark-auto-save</code></li></ol>              </div>            </details><details class="folding-tag" ><summary> Q2: 转存任务失败怎么办？ </summary>              <div class='content'>              <ol><li>检查夸克网盘 Cookie 是否过期</li><li>确认分享链接是否有效</li><li>查看 WebUI 中的任务执行日志</li></ol>              </div>            </details><details class="folding-tag" ><summary> Q3: 如何避免账号被风控？ </summary>              <div class='content'>              <ol><li><strong>不要设置过高的定时频率</strong>（建议 6-12 小时一次）</li><li>不要同时转存过多任务</li><li>定期更换夸克网盘 Cookie</li></ol>              </div>            </details><hr>    <div class="note note-success">      <div class="note-header">        <i class="note-icon fa-regular fa-circle-check"></i>        <span class="note-title">部署完成 🎉</span>      </div>      <div class="note-content">        <p>按照本教程，您已经成功部署了 QAS 自动转存服务。现在可以开始配置您的转存任务，享受自动化带来的便利！</p><blockquote><p>💡 提示：更多高级玩法请参考项目 <a href="https://github.com/cp0204/quark-auto-save/wiki">Wiki</a> 和 <a href="https://github.com/cp0204/quark-auto-save/wiki">使用技巧集锦</a>。</p></blockquote>      </div>    </div>  ]]></content>
    
    
    <summary type="html">夸克网盘签到、自动转存、命名整理、推送提醒和媒体库刷新一站式解决方案</summary>
    
    
    
    <category term="心语随笔" scheme="https://blog.adoreorg.cn/categories/%E5%BF%83%E8%AF%AD%E9%9A%8F%E7%AC%94/"/>
    
    
    <category term="夸克网盘" scheme="https://blog.adoreorg.cn/tags/%E5%A4%B8%E5%85%8B%E7%BD%91%E7%9B%98/"/>
    
    <category term="Docker" scheme="https://blog.adoreorg.cn/tags/Docker/"/>
    
    <category term="教程" scheme="https://blog.adoreorg.cn/tags/%E6%95%99%E7%A8%8B/"/>
    
    <category term="自动转存" scheme="https://blog.adoreorg.cn/tags/%E8%87%AA%E5%8A%A8%E8%BD%AC%E5%AD%98/"/>
    
  </entry>
  
  <entry>
    <title>Codex绕过手机号验证</title>
    <link href="https://blog.adoreorg.cn/posts/971a2d1a.html"/>
    <id>https://blog.adoreorg.cn/posts/971a2d1a.html</id>
    <published>2026-06-01T11:30:00.000Z</published>
    <updated>2026-06-08T11:02:20.177Z</updated>
    
    <content type="html"><![CDATA[<h2 id="0成本绕过Codex手机号验证最新方法（已实测有效）">0成本绕过Codex手机号验证最新方法（已实测有效）</h2><p>最近看到好多文章讲如何绕过 Codex 手机号验证的文章，但这些方法讲来讲去都需要下载插件，有的还要激活码。下载 Chrome 插件本来就比较麻烦，对普通用户尤其不友好。于是就有了这个更简单的方法：无需插件，轻松搞定！</p><p><img src="https://markpic.adoreorg.cn/2026/06/1.webp" alt="手机号验证"></p><h3 id="核心原理">核心原理</h3><p>最新无需插件的方法来了（0成本，纯本地操作）：直接生成 <code>auth.json</code> 文件，放到 <code>.codex</code> 目录下即可绕过验证。</p><h3 id="具体步骤（超简单）">具体步骤（超简单）</h3><ol><li>打开 ChatGPT 网页版，登录你的账号。</li><li>进入设置 → 安全，开启 Google Authenticator 二步验证（如已开启可跳过）,手机下载Authenticator应用，扫描二维码输入验证码即可完成设置。<br><img src="https://markpic.adoreorg.cn/2026/06/2.webp" alt="开启二步验证"></li><li>访问 <a href="https://codexauth.moshushi.xyz/">JSON 生成器</a>，生成 <code>auth.json</code> 文件。</li><li>将生成的 <code>auth.json</code> 文件复制到 Codex 本地目录下的 <code>.codex</code> 隐藏文件夹中。<br><img src="https://markpic.adoreorg.cn/2026/06/4.webp" alt="auth.json 存放位置"></li><li>重启 Codex 客户端，再次登录即可自动跳过手机号验证。</li></ol><h3 id="实测效果">实测效果</h3><ul><li>✅ 之前每次登录都弹验证，现在稳定通过</li><li>✅ 完全免费，不用接码、不用买号</li><li>✅ 适合批量/多账号操作</li></ul><h3 id="注意事项">注意事项</h3><ul><li>⚠️ 请注意：<code>auth.json</code> 文件必须放在 <code>.codex</code> 隐藏文件夹中，位置错误会导致验证失败</li><li>⚠️ 确保生成的 <code>auth.json</code> 文件格式正确，建议使用推荐的生成器</li></ul><p>完成！整个过程就是这么简单，无需插件，一键搞定！</p>]]></content>
    
    
      
      
    <summary type="html">&lt;h2 id=&quot;0成本绕过Codex手机号验证最新方法（已实测有效）&quot;&gt;0成本绕过Codex手机号验证最新方法（已实测有效）&lt;/h2&gt;
&lt;p&gt;最近看到好多文章讲如何绕过 Codex 手机号验证的文章，但这些方法讲来讲去都需要下载插件，有的还要激活码。下载 Chrome 插件本来就</summary>
      
    
    
    
    <category term="笔耕闻道" scheme="https://blog.adoreorg.cn/categories/%E7%AC%94%E8%80%95%E9%97%BB%E9%81%93/"/>
    
    
    <category term="教程" scheme="https://blog.adoreorg.cn/tags/%E6%95%99%E7%A8%8B/"/>
    
    <category term="Codex" scheme="https://blog.adoreorg.cn/tags/Codex/"/>
    
  </entry>
  
  <entry>
    <title>测试标签</title>
    <link href="https://blog.adoreorg.cn/posts/f85ebc99.html"/>
    <id>https://blog.adoreorg.cn/posts/f85ebc99.html</id>
    <published>2026-05-21T16:00:00.000Z</published>
    <updated>2026-06-08T11:02:20.178Z</updated>
    
    <content type="html"><![CDATA[    <div class="note note-warning">      <div class="note-header">        <i class="note-icon fa-regular fa-circle-dot"></i>        <span class="note-title">改前须知</span>      </div>      <div class="note-content">        <p>这个是警告内容展示，里面可以正常使用<code>Markdown</code>的相关渲染格式</p>      </div>    </div>      <div class="note note-info">      <div class="note-header">        <i class="note-icon fa-regular fa-circle-info"></i>        <span class="note-title">这是标题</span>      </div>      <div class="note-content">        <p>这个是信息内容展示，里面可以正常使用<code>Markdown</code>的相关渲染格式</p>      </div>    </div>      <div class="note note-success">      <div class="note-header">        <i class="note-icon fa-regular fa-circle-check"></i>        <span class="note-title">这是标题</span>      </div>      <div class="note-content">        <p>这个是成功内容展示，里面可以正常使用<code>Markdown</code>的相关渲染格式</p>      </div>    </div>      <div class="note note-question">      <div class="note-header">        <i class="note-icon fa-regular fa-circle-question"></i>        <span class="note-title">这是标题</span>      </div>      <div class="note-content">        <p>这个是问题内容展示，里面可以正常使用<code>Markdown</code>的相关渲染格式</p>      </div>    </div>      <div class="note note-primary">      <div class="note-header">        <i class="note-icon fa-regular fa-star"></i>        <span class="note-title">这是标题</span>      </div>      <div class="note-content">        <p>这个是主要内容展示，里面可以正常使用<code>Markdown</code>的相关渲染格式</p>      </div>    </div>      <div class="note note-simple">      <div class="note-header">        <i class="note-icon fa-regular fa-circle-info"></i>        <span class="note-title">这是标题</span>      </div>      <div class="note-content">        <p>这个是简单内容展示，里面可以正常使用<code>Markdown</code>的相关渲染格式</p>      </div>    </div>      <div class="note note-error">      <div class="note-header">        <i class="note-icon fa-regular fa-circle-xmark"></i>        <span class="note-title">这是标题</span>      </div>      <div class="note-content">        <p>这个是错误内容展示，里面可以正常使用<code>Markdown</code>的相关渲染格式</p>      </div>    </div>  ]]></content>
    
    
      
      
    <summary type="html">
    &lt;div class=&quot;note note-warning&quot;&gt;
      &lt;div class=&quot;note-header&quot;&gt;
        &lt;i class=&quot;note-icon fa-regular fa-circle-dot&quot;&gt;&lt;/i&gt;
        &lt;spa</summary>
      
    
    
    
    <category term="笔耕闻道" scheme="https://blog.adoreorg.cn/categories/%E7%AC%94%E8%80%95%E9%97%BB%E9%81%93/"/>
    
    
    <category term="测试标签" scheme="https://blog.adoreorg.cn/tags/%E6%B5%8B%E8%AF%95%E6%A0%87%E7%AD%BE/"/>
    
  </entry>
  
  <entry>
    <title>Redis实战篇</title>
    <link href="https://blog.adoreorg.cn/posts/a6b15897.html"/>
    <id>https://blog.adoreorg.cn/posts/a6b15897.html</id>
    <published>2025-08-19T06:30:00.000Z</published>
    <updated>2025-08-19T06:30:00.000Z</updated>
    
    <content type="html"><![CDATA[<h2 id="0-实战导读">0. 实战导读</h2><blockquote><p>🎯 <strong>学习目标</strong><br>本教程将带你从Redis基础概念到实际项目应用，通过8个实战案例深入理解Redis的强大功能。</p></blockquote><div class="tabs" id="实战案例"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#实战案例-1">短信登录</button></li><li class="tab"><button type="button" data-href="#实战案例-2">商户查询缓存</button></li><li class="tab"><button type="button" data-href="#实战案例-3">优惠券秒杀</button></li><li class="tab"><button type="button" data-href="#实战案例-4">附近商户</button></li><li class="tab"><button type="button" data-href="#实战案例-5">UV统计</button></li><li class="tab"><button type="button" data-href="#实战案例-6">用户签到</button></li><li class="tab"><button type="button" data-href="#实战案例-7">好友关注</button></li><li class="tab"><button type="button" data-href="#实战案例-8">探店点赞</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="实战案例-1"><p><strong>📱 短信登录</strong><br>使用Redis共享Session实现分布式登录系统，解决传统Session跨域问题</p><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="实战案例-2"><p><strong>🔍 商户查询缓存</strong><br>深入理解缓存击穿、缓存穿透、缓存雪崩问题，掌握缓存策略和解决方案</p><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="实战案例-3"><p><strong>🎫 优惠券秒杀</strong><br>Redis计数器 + Lua脚本实现高性能秒杀，分布式锁防止超卖</p><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="实战案例-4"><p><strong>📍 附近商户</strong><br>利用Redis GEOHash实现地理位置查询，支持附近商家搜索</p><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="实战案例-5"><p><strong>📊 UV统计</strong><br>使用HyperLogLog进行海量数据去重统计，内存占用极低</p><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="实战案例-6"><p><strong>✅ 用户签到</strong><br>BitMap实现用户签到功能，节省存储空间</p><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="实战案例-7"><p><strong>👥 好友关注</strong><br>基于Set集合实现关注、取消关注、共同关注等社交功能</p><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="实战案例-8"><p><strong>👍 探店点赞</strong><br>List实现点赞列表，SortedSet实现点赞排行榜</p><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><p><img src="https://markpic.adoreorg.cn/2025/09/Redis/1653056228879.png" alt="1653056228879"></p><h2 id="1-短信登录">1. 短信登录</h2><h3 id="1-1、项目架构分析">1.1、项目架构分析</h3><h4 id="1-1-1-系统架构图">1.1.1 系统架构图</h4><p><img src="https://markpic.adoreorg.cn/2025/09/Redis/1653057872536.png" alt="系统架构图"></p><h4 id="1-1-2-架构设计解析">1.1.2 架构设计解析</h4>    <div class="note note-info">      <div class="note-header">        <i class="note-icon fa-regular fa-circle-info"></i>        <span class="note-title">提示信息</span>      </div>      <div class="note-content">        <p><strong>技术栈</strong></p><ul><li>负载均衡：Nginx</li><li>应用服务：Tomcat集群</li><li>数据存储：MySQL + Redis</li><li>缓存策略：多级缓存架构</li></ul>      </div>    </div>  <p><strong>请求流程分析：</strong></p><ol><li><p><strong>客户端请求</strong> → <strong>Nginx负载均衡</strong></p><ul><li>Nginx基于七层模型处理HTTP协议</li><li>支持Lua脚本直接访问Redis</li><li>静态资源服务，轻松扛下上万并发</li></ul></li><li><p><strong>负载均衡</strong> → <strong>Tomcat集群</strong></p><ul><li>4核8G Tomcat约处理1000并发</li><li>Nginx负载均衡分流，集群支撑高并发</li><li>动静分离降低Tomcat压力</li></ul></li><li><p><strong>数据访问层</strong></p><ul><li>MySQL企业级：16-32核CPU，32-64G内存</li><li>并发能力：4000-7000 QPS</li><li>Redis集群缓存，降低数据库压力</li></ul></li></ol><p><img src="https://markpic.adoreorg.cn/2025/09/Redis/1653059409865.png" alt="架构详解"></p><h4 id="1-1-3-项目导入步骤">1.1.3 项目导入步骤</h4><div class="tabs" id="redis"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#redis-1">后端项目</button></li><li class="tab"><button type="button" data-href="#redis-2">前端项目</button></li><li class="tab"><button type="button" data-href="#redis-3">运行项目</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="redis-1"><p><strong>🖥️ 后端项目导入</strong></p><p><img src="https://markpic.adoreorg.cn/2025/09/Redis/1653060237073.png" alt="后端项目"></p><ol><li>解压后端项目源码</li><li>配置数据库连接</li><li>导入SQL脚本</li><li>启动项目</li></ol><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="redis-2"><p><strong>🌐 前端项目导入</strong></p><p><img src="https://markpic.adoreorg.cn/2025/09/Redis/1653060337562.png" alt="前端项目"></p><ol><li>解压前端工程</li><li>安装依赖</li><li>配置API接口地址</li><li>启动前端服务</li></ol><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="redis-3"><p><strong>🚀 项目运行</strong></p><p><img src="https://markpic.adoreorg.cn/2025/09/Redis/1653060588190.png" alt="运行项目"></p><p>访问地址：<code>http://localhost:8080</code></p><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h3 id="1-2-登录流程设计">1.2 登录流程设计</h3><h4 id="1-2-1-登录流程图">1.2.1 登录流程图</h4><p><img src="https://markpic.adoreorg.cn/2025/09/Redis/1653066208144.png" alt="登录流程"></p><h4 id="1-2-2-详细流程解析">1.2.2 详细流程解析</h4><div class="timeline "><div class='timeline-item'><div class='timeline-item-title'><div class='item-circle'><p>发送验证码</p></div></div><div class='timeline-item-content'><p><strong>📤 发送验证码</strong></p><ol><li><p><strong>手机号校验</strong></p><ul><li>前端提交手机号</li><li>后端验证手机号格式</li><li>格式错误：返回错误信息</li></ul></li><li><p><strong>验证码生成与发送</strong></p><ul><li>生成6位随机验证码</li><li>保存验证码到Session</li><li>调用短信服务发送验证码</li></ul></li></ol></div></div><div class='timeline-item'><div class='timeline-item-title'><div class='item-circle'><p>用户登录</p></div></div><div class='timeline-item-content'><p><strong>🔐 用户登录</strong></p><ol><li><p><strong>参数校验</strong></p><ul><li>校验手机号格式</li><li>校验验证码正确性</li></ul></li><li><p><strong>用户处理</strong></p><ul><li>根据手机号查询用户</li><li>用户不存在：创建新用户</li><li>用户存在：更新登录信息</li></ul></li><li><p><strong>Session管理</strong></p><ul><li>用户信息存入Session</li><li>返回登录成功标识</li></ul></li></ol></div></div><div class='timeline-item'><div class='timeline-item-title'><div class='item-circle'><p>状态校验</p></div></div><div class='timeline-item-content'><p><strong>✅ 状态校验</strong></p><ol><li><p><strong>请求拦截</strong></p><ul><li>从Cookie获取JsessionId</li><li>根据SessionId获取用户信息</li></ul></li><li><p><strong>权限控制</strong></p><ul><li>用户不存在：返回401未授权</li><li>用户存在：信息存入ThreadLocal</li><li>请求放行</li></ul></li></ol></div></div></div><p><img src="https://markpic.adoreorg.cn/2025/09/Redis/1653066208144.png" alt="1653066208144"></p><h3 id="1-3-核心代码实现">1.3 核心代码实现</h3><h4 id="1-3-1-页面交互流程">1.3.1 页面交互流程</h4><p><img src="https://markpic.adoreorg.cn/2025/09/Redis/1653067054461.png" alt="页面流程"></p><h4 id="1-3-2-发送验证码功能">1.3.2 发送验证码功能</h4>    <div class="note note-success">      <div class="note-header">        <i class="note-icon fa-regular fa-circle-check"></i>        <span class="note-title">提示信息</span>      </div>      <div class="note-content">        <p><strong>功能说明</strong>：验证手机号格式，生成6位随机验证码并保存到Session</p>      </div>    </div>  <figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Service</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">UserServiceImpl</span> <span class="keyword">implements</span> <span class="title class_">UserService</span> &#123;</span><br><span class="line">    </span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> Result <span class="title function_">sendCode</span><span class="params">(String phone, HttpSession session)</span> &#123;</span><br><span class="line">        <span class="comment">// 1. 校验手机号格式</span></span><br><span class="line">        <span class="keyword">if</span> (RegexUtils.isPhoneInvalid(phone)) &#123;</span><br><span class="line">            <span class="keyword">return</span> Result.fail(<span class="string">&quot;手机号格式错误！&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 2. 生成6位随机验证码</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">code</span> <span class="operator">=</span> RandomUtil.randomNumbers(<span class="number">6</span>);</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 3. 保存验证码到Session</span></span><br><span class="line">        session.setAttribute(<span class="string">&quot;code&quot;</span>, code);</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 4. 发送验证码（实际项目中调用短信服务）</span></span><br><span class="line">        log.debug(<span class="string">&quot;发送短信验证码成功，验证码：&#123;&#125;&quot;</span>, code);</span><br><span class="line">        </span><br><span class="line">        <span class="keyword">return</span> Result.ok();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="1-3-3-用户登录功能">1.3.3 用户登录功能</h4>    <div class="note note-warning">      <div class="note-header">        <i class="note-icon fa-regular fa-circle-dot"></i>        <span class="note-title">提示信息</span>      </div>      <div class="note-content">        <p><strong>安全提醒</strong>：验证码校验通过后，需要根据手机号查询或创建用户，并将用户信息保存到Session</p>      </div>    </div>  <figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Service</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">UserServiceImpl</span> <span class="keyword">implements</span> <span class="title class_">UserService</span> &#123;</span><br><span class="line">    </span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> Result <span class="title function_">login</span><span class="params">(LoginFormDTO loginForm, HttpSession session)</span> &#123;</span><br><span class="line">        <span class="comment">// 1. 校验手机号格式</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">phone</span> <span class="operator">=</span> loginForm.getPhone();</span><br><span class="line">        <span class="keyword">if</span> (RegexUtils.isPhoneInvalid(phone)) &#123;</span><br><span class="line">            <span class="keyword">return</span> Result.fail(<span class="string">&quot;手机号格式错误！&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 2. 校验验证码</span></span><br><span class="line">        <span class="type">Object</span> <span class="variable">cacheCode</span> <span class="operator">=</span> session.getAttribute(<span class="string">&quot;code&quot;</span>);</span><br><span class="line">        <span class="type">String</span> <span class="variable">code</span> <span class="operator">=</span> loginForm.getCode();</span><br><span class="line">        <span class="keyword">if</span> (cacheCode == <span class="literal">null</span> || !cacheCode.toString().equals(code)) &#123;</span><br><span class="line">            <span class="keyword">return</span> Result.fail(<span class="string">&quot;验证码错误&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 3. 根据手机号查询用户</span></span><br><span class="line">        <span class="type">User</span> <span class="variable">user</span> <span class="operator">=</span> query().eq(<span class="string">&quot;phone&quot;</span>, phone).one();</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 4. 用户不存在则创建新用户</span></span><br><span class="line">        <span class="keyword">if</span> (user == <span class="literal">null</span>) &#123;</span><br><span class="line">            user = createUserWithPhone(phone);</span><br><span class="line">        &#125;</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 5. 保存用户信息到Session</span></span><br><span class="line">        session.setAttribute(<span class="string">&quot;user&quot;</span>, user);</span><br><span class="line">        </span><br><span class="line">        <span class="keyword">return</span> Result.ok();</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">private</span> User <span class="title function_">createUserWithPhone</span><span class="params">(String phone)</span> &#123;</span><br><span class="line">        <span class="type">User</span> <span class="variable">user</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">User</span>();</span><br><span class="line">        user.setPhone(phone);</span><br><span class="line">        user.setNickName(<span class="string">&quot;user_&quot;</span> + RandomUtil.randomString(<span class="number">6</span>));</span><br><span class="line">        userService.save(user);</span><br><span class="line">        <span class="keyword">return</span> user;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="1-4-登录拦截器实现">1.4 登录拦截器实现</h3><h4 id="1-4-1-Tomcat运行原理">1.4.1 Tomcat运行原理</h4><p><img src="https://markpic.adoreorg.cn/2025/09/Redis/1653068196656.png" alt="Tomcat原理"></p>    <div class="note note-info">      <div class="note-header">        <i class="note-icon fa-regular fa-circle-info"></i>        <span class="note-title">提示信息</span>      </div>      <div class="note-content">        <p><strong>Tomcat线程模型解析</strong></p><ol><li><strong>监听线程</strong>：监听端口，接收客户端连接</li><li><strong>Socket连接</strong>：每对请求-响应创建独立Socket</li><li><strong>线程池</strong>：从线程池获取线程处理请求</li><li><strong>请求处理</strong>：线程转发到Controller→Service→DAO→DB</li><li><strong>响应返回</strong>：处理完成后数据写回客户端Socket</li></ol>      </div>    </div>  <h4 id="1-4-2-ThreadLocal线程隔离">1.4.2 ThreadLocal线程隔离</h4>    <div class="note note-warning">      <div class="note-header">        <i class="note-icon fa-regular fa-circle-dot"></i>        <span class="note-title">提示信息</span>      </div>      <div class="note-content">        <p><strong>ThreadLocal使用要点</strong></p><p>每个用户请求对应Tomcat线程池中的一个线程，通过ThreadLocal实现线程间的数据隔离，确保每个线程操作自己的数据副本。</p>      </div>    </div>  <p><img src="https://markpic.adoreorg.cn/2025/09/Redis/1653068874258.png" alt="ThreadLocal原理"></p><h4 id="1-4-3-登录拦截器代码">1.4.3 登录拦截器代码</h4><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">LoginInterceptor</span> <span class="keyword">implements</span> <span class="title class_">HandlerInterceptor</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="type">boolean</span> <span class="title function_">preHandle</span><span class="params">(HttpServletRequest request, </span></span><br><span class="line"><span class="params">                           HttpServletResponse response, </span></span><br><span class="line"><span class="params">                           Object handler)</span> <span class="keyword">throws</span> Exception &#123;</span><br><span class="line">        <span class="comment">// 1. 获取Session</span></span><br><span class="line">        <span class="type">HttpSession</span> <span class="variable">session</span> <span class="operator">=</span> request.getSession();</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 2. 获取Session中的用户信息</span></span><br><span class="line">        <span class="type">Object</span> <span class="variable">user</span> <span class="operator">=</span> session.getAttribute(<span class="string">&quot;user&quot;</span>);</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 3. 判断用户是否登录</span></span><br><span class="line">        <span class="keyword">if</span> (user == <span class="literal">null</span>) &#123;</span><br><span class="line">            <span class="comment">// 4. 用户未登录，返回401状态码</span></span><br><span class="line">            response.setStatus(<span class="number">401</span>);</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 5. 用户已登录，保存用户信息到ThreadLocal</span></span><br><span class="line">        UserHolder.saveUser((User) user);</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 6. 放行请求</span></span><br><span class="line">        <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="1-4-4-拦截器配置">1.4.4 拦截器配置</h4>    <div class="note note-primary">      <div class="note-header">        <i class="note-icon fa-regular fa-star"></i>        <span class="note-title">提示信息</span>      </div>      <div class="note-content">        <p><strong>配置说明</strong></p><ul><li><code>order(0)</code>：Token刷新拦截器，优先级最高</li><li><code>order(1)</code>：登录验证拦截器，优先级次之</li><li><code>excludePathPatterns</code>：配置不需要拦截的路径</li></ul>      </div>    </div>  <figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Configuration</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">MvcConfig</span> <span class="keyword">implements</span> <span class="title class_">WebMvcConfigurer</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Resource</span></span><br><span class="line">    <span class="keyword">private</span> StringRedisTemplate stringRedisTemplate;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">addInterceptors</span><span class="params">(InterceptorRegistry registry)</span> &#123;</span><br><span class="line">        <span class="comment">// Token刷新拦截器 - 拦截所有路径</span></span><br><span class="line">        registry.addInterceptor(<span class="keyword">new</span> <span class="title class_">RefreshTokenInterceptor</span>(stringRedisTemplate))</span><br><span class="line">                .addPathPatterns(<span class="string">&quot;/**&quot;</span>)</span><br><span class="line">                .order(<span class="number">0</span>);</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 登录拦截器 - 拦截需要登录的路径</span></span><br><span class="line">        registry.addInterceptor(<span class="keyword">new</span> <span class="title class_">LoginInterceptor</span>())</span><br><span class="line">                .excludePathPatterns(</span><br><span class="line">                        <span class="string">&quot;/shop/**&quot;</span>,           <span class="comment">// 店铺信息</span></span><br><span class="line">                        <span class="string">&quot;/voucher/**&quot;</span>,        <span class="comment">// 优惠券</span></span><br><span class="line">                        <span class="string">&quot;/shop-type/**&quot;</span>,      <span class="comment">// 店铺类型</span></span><br><span class="line">                        <span class="string">&quot;/upload/**&quot;</span>,        <span class="comment">// 文件上传</span></span><br><span class="line">                        <span class="string">&quot;/blog/hot&quot;</span>,         <span class="comment">// 热门博客</span></span><br><span class="line">                        <span class="string">&quot;/user/code&quot;</span>,        <span class="comment">// 发送验证码</span></span><br><span class="line">                        <span class="string">&quot;/user/login&quot;</span>        <span class="comment">// 用户登录</span></span><br><span class="line">                )</span><br><span class="line">                .order(<span class="number">1</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="1-5-用户信息安全处理">1.5 用户信息安全处理</h3><h4 id="1-5-1-敏感信息隐藏的必要性">1.5.1 敏感信息隐藏的必要性</h4>    <div class="note note-warning">      <div class="note-header">        <i class="note-icon fa-regular fa-circle-dot"></i>        <span class="note-title">提示信息</span>      </div>      <div class="note-content">        <p><strong>安全风险警告</strong></p><p>直接返回完整的User实体对象会暴露用户敏感信息（如密码、手机号、邮箱等），存在严重的安全隐患。必须通过DTO对象进行数据脱敏。</p>      </div>    </div>  <h4 id="1-5-2-解决方案：UserDTO数据传输对象">1.5.2 解决方案：UserDTO数据传输对象</h4><p><strong>UserDTO定义：</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Data</span></span><br><span class="line"><span class="meta">@NoArgsConstructor</span></span><br><span class="line"><span class="meta">@AllArgsConstructor</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">UserDTO</span> &#123;</span><br><span class="line">    <span class="keyword">private</span> Long id;</span><br><span class="line">    <span class="keyword">private</span> String nickName;</span><br><span class="line">    <span class="keyword">private</span> String icon;</span><br><span class="line">    <span class="comment">// 注意：不包含敏感信息如password、phone、email等</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="1-5-3-代码修改">1.5.3 代码修改</h4><p><strong>1. 登录方法修改：</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 保存用户信息到Session（使用DTO脱敏）</span></span><br><span class="line">session.setAttribute(<span class="string">&quot;user&quot;</span>, BeanUtil.copyProperties(user, UserDTO.class));</span><br></pre></td></tr></table></figure><p><strong>2. 拦截器修改：</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 保存用户信息到ThreadLocal（使用DTO）</span></span><br><span class="line">UserHolder.saveUser((UserDTO) user);</span><br></pre></td></tr></table></figure><p><strong>3. UserHolder工具类：</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">UserHolder</span> &#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> ThreadLocal&lt;UserDTO&gt; tl = <span class="keyword">new</span> <span class="title class_">ThreadLocal</span>&lt;&gt;();</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">saveUser</span><span class="params">(UserDTO user)</span> &#123;</span><br><span class="line">        tl.set(user);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> UserDTO <span class="title function_">getUser</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> tl.get();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">removeUser</span><span class="params">()</span> &#123;</span><br><span class="line">        tl.remove();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="1-5-4-安全效果对比">1.5.4 安全效果对比</h4><table><thead><tr><th>返回方式</th><th>包含字段</th><th>安全性</th><th>推荐程度</th></tr></thead><tbody><tr><td>User实体</td><td>id, phone, password, email…</td><td>❌ 危险</td><td>禁止使用</td></tr><tr><td>UserDTO</td><td>id, nickName, icon</td><td>✅ 安全</td><td>强烈推荐</td></tr></tbody></table><h3 id="1-6-Session共享问题">1.6 Session共享问题</h3><h4 id="1-6-1-分布式Session问题分析">1.6.1 分布式Session问题分析</h4>    <div class="note note-warning">      <div class="note-header">        <i class="note-icon fa-regular fa-circle-dot"></i>        <span class="note-title">提示信息</span>      </div>      <div class="note-content">        <p><strong>集群环境下的Session一致性挑战</strong></p><p>在分布式系统中，每个Tomcat服务器都有自己的Session存储。当用户请求被分发到不同服务器时，会导致Session数据不一致，用户需要重复登录。</p>      </div>    </div>  <p><strong>典型问题场景：</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">用户请求 → Nginx负载均衡 → Tomcat1 (保存Session)</span><br><span class="line">                    ↓</span><br><span class="line">                   Tomcat2 (无Session数据)</span><br></pre></td></tr></table></figure><p><strong>早期解决方案对比：</strong></p><table><thead><tr><th>方案</th><th>原理</th><th>优点</th><th>缺点</th></tr></thead><tbody><tr><td>Session复制</td><td>各服务器间同步Session</td><td>实现简单</td><td>网络开销大，性能差</td></tr><tr><td>Session粘连</td><td>固定用户到某台服务器</td><td>无需额外开发</td><td>负载不均，单点故障</td></tr><tr><td>Session集中存储</td><td>使用Redis统一管理</td><td>性能高，可扩展</td><td>需要额外组件</td></tr></tbody></table><h4 id="1-6-2-Redis解决方案">1.6.2 Redis解决方案</h4>    <div class="note note-success">      <div class="note-header">        <i class="note-icon fa-regular fa-circle-check"></i>        <span class="note-title">提示信息</span>      </div>      <div class="note-content">        <p><strong>推荐方案：Redis集中存储</strong></p><p>使用Redis作为Session的集中存储，所有服务器共享同一份Session数据，彻底解决分布式环境下的Session一致性问题。</p>      </div>    </div>  <p><strong>架构设计：</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">用户请求 → Nginx负载均衡 → Tomcat1</span><br><span class="line">                    ↓              ↓</span><br><span class="line">                   Redis ←------ Tomcat2</span><br><span class="line">                   (集中Session存储)</span><br></pre></td></tr></table></figure><p><strong>核心优势：</strong></p><ol><li><strong>数据共享</strong>：所有服务器访问同一份Session数据</li><li><strong>高性能</strong>：Redis内存存储，读写速度快</li><li><strong>可扩展</strong>：支持集群部署，水平扩展</li><li><strong>持久化</strong>：支持数据持久化，防止数据丢失</li></ol><p><img src="https://markpic.adoreorg.cn/2025/09/Redis/1653069893050.png" alt="1653069893050"></p><h3 id="1-7-Redis代替Session的业务流程">1.7 Redis代替Session的业务流程</h3><h4 id="1-7-1-Redis数据结构选择">1.7.1 Redis数据结构选择</h4>    <div class="note note-info">      <div class="note-header">        <i class="note-icon fa-regular fa-circle-info"></i>        <span class="note-title">提示信息</span>      </div>      <div class="note-content">        <p><strong>数据结构对比分析</strong></p><p>在Redis中存储用户登录信息时，需要根据数据特性和使用场景选择合适的数据结构。</p>      </div>    </div>  <p><strong>String vs Hash结构对比：</strong></p><table><thead><tr><th>对比维度</th><th>String结构</th><th>Hash结构</th></tr></thead><tbody><tr><td>内存占用</td><td>较高（序列化开销）</td><td>较低（字段独立存储）</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><p><strong>选择建议：</strong></p><ul><li>如果用户信息简单，使用String结构</li><li>如果用户信息复杂，需要频繁更新部分字段，使用Hash结构</li></ul><p><img src="https://markpic.adoreorg.cn/2025/09/Redis/1653319261433.png" alt="1653319261433"></p><h4 id="1-7-2-Key设计策略">1.7.2 Key设计策略</h4>    <div class="note note-warning">      <div class="note-header">        <i class="note-icon fa-regular fa-circle-dot"></i>        <span class="note-title">提示信息</span>      </div>      <div class="note-content">        <p><strong>Key设计原则</strong></p><p>Redis的Key需要满足唯一性和可携带性，同时避免暴露用户敏感信息。</p>      </div>    </div>  <p><strong>Key设计要点：</strong></p><ol><li><strong>唯一性</strong>：确保每个用户的Key都是唯一的</li><li><strong>可携带性</strong>：Key需要方便在前后端之间传递</li><li><strong>安全性</strong>：避免使用手机号等敏感信息作为Key</li></ol><p><strong>推荐方案：</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">Key格式：login:token:&#123;随机token&#125;</span><br><span class="line">示例：login:token:a3f2b8c9d1e4f5g6</span><br></pre></td></tr></table></figure><p><strong>Token生成策略：</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 使用UUID生成随机token</span></span><br><span class="line"><span class="type">String</span> <span class="variable">token</span> <span class="operator">=</span> UUID.randomUUID().toString(<span class="literal">true</span>);</span><br><span class="line"><span class="type">String</span> <span class="variable">key</span> <span class="operator">=</span> LOGIN_USER_KEY + token;</span><br></pre></td></tr></table></figure><h4 id="1-7-3-整体访问流程">1.7.3 整体访问流程</h4><div class="timeline "><div class='timeline-item'><div class='timeline-item-title'><div class='item-circle'></div></div><div class='timeline-item-content'><p>📱 <strong>步骤1：用户登录验证</strong></p><p>用户提交手机号和验证码，系统验证信息一致性</p><p>🔍 <strong>步骤2：用户信息查询</strong></p><p>根据手机号查询用户信息，不存在则创建新用户</p><p>💾 <strong>步骤3：Redis数据存储</strong></p><p>将用户信息保存到Redis，设置合理的过期时间</p><p>🔑 <strong>步骤4：Token返回</strong></p><p>生成随机token作为登录凭证，返回给前端</p><p>🔐 <strong>步骤5：登录状态校验</strong></p><p>后续请求携带token，系统验证Redis中是否存在对应数据</p></div></div></div><h3 id="1-8-基于Redis实现短信登录">1.8 基于Redis实现短信登录</h3><h4 id="1-8-1-实现思路回顾">1.8.1 实现思路回顾</h4>    <div class="note note-primary">      <div class="note-header">        <i class="note-icon fa-regular fa-star"></i>        <span class="note-title">提示信息</span>      </div>      <div class="note-content">        <p><strong>Redis登录方案核心</strong></p><p>将传统的Session存储替换为Redis存储，通过Token机制实现无状态的分布式登录认证。</p>      </div>    </div>  <p><strong>核心改进点：</strong></p><ol><li><strong>存储位置</strong>：从服务器内存 → Redis缓存</li><li><strong>认证方式</strong>：从SessionID → 随机Token</li><li><strong>数据格式</strong>：从完整对象 → 精简DTO</li><li><strong>有效期管理</strong>：支持灵活的过期时间设置</li></ol><h4 id="1-8-2-核心代码实现">1.8.2 核心代码实现</h4><div class="tabs" id="改进"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#改进-1">登录方法</button></li><li class="tab"><button type="button" data-href="#改进-2">常量定义</button></li><li class="tab"><button type="button" data-href="#改进-3">前端调用</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="改进-1"><p><strong>UserServiceImpl登录方法：</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Service</span></span><br><span class="line"><span class="meta">@Slf4j</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">UserServiceImpl</span> <span class="keyword">extends</span> <span class="title class_">ServiceImpl</span>&lt;UserMapper, User&gt; <span class="keyword">implements</span> <span class="title class_">IUserService</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Resource</span></span><br><span class="line">    <span class="keyword">private</span> StringRedisTemplate stringRedisTemplate;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> Result <span class="title function_">login</span><span class="params">(LoginFormDTO loginForm, HttpSession session)</span> &#123;</span><br><span class="line">        <span class="comment">// 1. 校验手机号格式</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">phone</span> <span class="operator">=</span> loginForm.getPhone();</span><br><span class="line">        <span class="keyword">if</span> (RegexUtils.isPhoneInvalid(phone)) &#123;</span><br><span class="line">            <span class="keyword">return</span> Result.fail(<span class="string">&quot;手机号格式错误！&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 2. 从Redis获取验证码并校验</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">cacheCode</span> <span class="operator">=</span> stringRedisTemplate.opsForValue()</span><br><span class="line">            .get(LOGIN_CODE_KEY + phone);</span><br><span class="line">        <span class="type">String</span> <span class="variable">code</span> <span class="operator">=</span> loginForm.getCode();</span><br><span class="line">        </span><br><span class="line">        <span class="keyword">if</span> (cacheCode == <span class="literal">null</span> || !cacheCode.equals(code)) &#123;</span><br><span class="line">            <span class="keyword">return</span> Result.fail(<span class="string">&quot;验证码错误&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 3. 根据手机号查询用户</span></span><br><span class="line">        <span class="type">User</span> <span class="variable">user</span> <span class="operator">=</span> query().eq(<span class="string">&quot;phone&quot;</span>, phone).one();</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 4. 用户不存在则创建新用户</span></span><br><span class="line">        <span class="keyword">if</span> (user == <span class="literal">null</span>) &#123;</span><br><span class="line">            user = createUserWithPhone(phone);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 5. 保存用户信息到Redis</span></span><br><span class="line">        <span class="comment">// 5.1 随机生成token作为登录令牌</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">token</span> <span class="operator">=</span> UUID.randomUUID().toString(<span class="literal">true</span>);</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 5.2 将User对象转为HashMap存储</span></span><br><span class="line">        <span class="type">UserDTO</span> <span class="variable">userDTO</span> <span class="operator">=</span> BeanUtil.copyProperties(user, UserDTO.class);</span><br><span class="line">        Map&lt;String, Object&gt; userMap = BeanUtil.beanToMap(userDTO, <span class="keyword">new</span> <span class="title class_">HashMap</span>&lt;&gt;(),</span><br><span class="line">                CopyOptions.create()</span><br><span class="line">                        .setIgnoreNullValue(<span class="literal">true</span>)</span><br><span class="line">                        .setFieldValueEditor((fieldName, fieldValue) -&gt; </span><br><span class="line">                            fieldValue.toString()));</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 5.3 存储到Redis</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">tokenKey</span> <span class="operator">=</span> LOGIN_USER_KEY + token;</span><br><span class="line">        stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 5.4 设置token有效期（30分钟）</span></span><br><span class="line">        stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 6. 返回token</span></span><br><span class="line">        <span class="keyword">return</span> Result.ok(token);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="改进-2"><p><strong>Redis常量配置：</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">RedisConstants</span> &#123;</span><br><span class="line">    <span class="comment">// 验证码相关</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">String</span> <span class="variable">LOGIN_CODE_KEY</span> <span class="operator">=</span> <span class="string">&quot;login:code:&quot;</span>;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">Long</span> <span class="variable">LOGIN_CODE_TTL</span> <span class="operator">=</span> <span class="number">2L</span>; <span class="comment">// 验证码有效期2分钟</span></span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 用户登录相关</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">String</span> <span class="variable">LOGIN_USER_KEY</span> <span class="operator">=</span> <span class="string">&quot;login:token:&quot;</span>;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">Long</span> <span class="variable">LOGIN_USER_TTL</span> <span class="operator">=</span> <span class="number">30L</span>; <span class="comment">// 用户登录有效期30分钟</span></span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 用户签到相关</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">String</span> <span class="variable">USER_SIGN_KEY</span> <span class="operator">=</span> <span class="string">&quot;sign:&quot;</span>;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 商户信息相关</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">String</span> <span class="variable">CACHE_SHOP_KEY</span> <span class="operator">=</span> <span class="string">&quot;cache:shop:&quot;</span>;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">Long</span> <span class="variable">CACHE_SHOP_TTL</span> <span class="operator">=</span> <span class="number">30L</span>; <span class="comment">// 商户信息缓存30分钟</span></span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 互斥锁相关</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">String</span> <span class="variable">LOCK_SHOP_KEY</span> <span class="operator">=</span> <span class="string">&quot;lock:shop:&quot;</span>;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">Long</span> <span class="variable">LOCK_SHOP_TTL</span> <span class="operator">=</span> <span class="number">10L</span>; <span class="comment">// 锁有效期10秒</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="改进-3"><p><strong>前端登录调用：</strong></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 发送验证码</span></span><br><span class="line"><span class="keyword">async</span> <span class="title function_">sendCode</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="keyword">const</span> result = <span class="keyword">await</span> request.<span class="title function_">post</span>(<span class="string">&#x27;/user/code&#x27;</span>, &#123;</span><br><span class="line">        <span class="attr">phone</span>: <span class="variable language_">this</span>.<span class="property">phone</span></span><br><span class="line">    &#125;);</span><br><span class="line">    <span class="keyword">if</span> (result.<span class="property">code</span> === <span class="number">200</span>) &#123;</span><br><span class="line">        <span class="variable language_">this</span>.<span class="property">$message</span>.<span class="title function_">success</span>(<span class="string">&#x27;验证码发送成功&#x27;</span>);</span><br><span class="line">        <span class="comment">// 开始倒计时</span></span><br><span class="line">        <span class="variable language_">this</span>.<span class="title function_">startCountdown</span>();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;,</span><br><span class="line"></span><br><span class="line"><span class="comment">// 用户登录</span></span><br><span class="line"><span class="keyword">async</span> <span class="title function_">login</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="keyword">const</span> result = <span class="keyword">await</span> request.<span class="title function_">post</span>(<span class="string">&#x27;/user/login&#x27;</span>, &#123;</span><br><span class="line">        <span class="attr">phone</span>: <span class="variable language_">this</span>.<span class="property">phone</span>,</span><br><span class="line">        <span class="attr">code</span>: <span class="variable language_">this</span>.<span class="property">code</span></span><br><span class="line">    &#125;);</span><br><span class="line">    <span class="keyword">if</span> (result.<span class="property">code</span> === <span class="number">200</span>) &#123;</span><br><span class="line">        <span class="comment">// 保存token到localStorage</span></span><br><span class="line">        <span class="variable language_">localStorage</span>.<span class="title function_">setItem</span>(<span class="string">&#x27;token&#x27;</span>, result.<span class="property">data</span>);</span><br><span class="line">        <span class="comment">// 设置请求头</span></span><br><span class="line">        request.<span class="property">defaults</span>.<span class="property">headers</span>.<span class="property">common</span>[<span class="string">&#x27;authorization&#x27;</span>] = result.<span class="property">data</span>;</span><br><span class="line">        <span class="variable language_">this</span>.<span class="property">$message</span>.<span class="title function_">success</span>(<span class="string">&#x27;登录成功&#x27;</span>);</span><br><span class="line">        <span class="variable language_">this</span>.<span class="property">$router</span>.<span class="title function_">push</span>(<span class="string">&#x27;/&#x27;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h3 id="1-9-登录状态刷新优化">1.9 登录状态刷新优化</h3><h4 id="1-9-1-初始方案问题分析">1.9.1 初始方案问题分析</h4>    <div class="note note-warning">      <div class="note-header">        <i class="note-icon fa-regular fa-circle-dot"></i>        <span class="note-title">提示信息</span>      </div>      <div class="note-content">        <p><strong>初始方案缺陷</strong></p><p>原方案中拦截器只拦截需要登录验证的路径，导致用户访问无需拦截的路径时，Token无法得到刷新，可能造成Token过期失效。</p>      </div>    </div>  <p><strong>问题场景：</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">用户访问首页 → 无需拦截 → Token不刷新 → Token过期</span><br><span class="line">用户访问个人中心 → 需要拦截 → Token刷新 → 正常访问</span><br></pre></td></tr></table></figure><p><strong>影响分析：</strong></p><ul><li>用户活跃状态下仍可能因Token过期被强制登出</li><li>用户体验差，需要频繁重新登录</li><li>无法准确反映用户的真实活跃状态</li></ul><p><img src="https://markpic.adoreorg.cn/2025/09/Redis/1653320822964.png" alt="1653320822964"></p><h4 id="1-9-2-双拦截器优化方案">1.9.2 双拦截器优化方案</h4>    <div class="note note-success">      <div class="note-header">        <i class="note-icon fa-regular fa-circle-check"></i>        <span class="note-title">提示信息</span>      </div>      <div class="note-content">        <p><strong>优化思路</strong></p><p>采用双拦截器模式：</p><ul><li><strong>刷新拦截器</strong>：拦截所有路径，负责Token刷新和用户信息加载</li><li><strong>登录拦截器</strong>：只拦截需要登录的路径，负责登录状态校验</li></ul>      </div>    </div>  <p><strong>架构设计：</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">请求进入</span><br><span class="line">    ↓</span><br><span class="line">刷新拦截器（所有路径）→ 刷新Token有效期 → 加载用户信息到ThreadLocal</span><br><span class="line">    ↓</span><br><span class="line">登录拦截器（需登录路径）→ 校验ThreadLocal中用户信息 → 决定是否放行</span><br><span class="line">    ↓</span><br><span class="line">业务处理</span><br></pre></td></tr></table></figure><p><strong>优势分析：</strong></p><ul><li>所有请求都能刷新Token，避免意外过期</li><li>职责分离，代码更清晰</li><li>性能影响最小化</li></ul><p><img src="https://markpic.adoreorg.cn/2025/09/Redis/1653320764547.png" alt="1653320764547"></p><h4 id="1-9-3-代码实现">1.9.3 代码实现</h4><div class="tabs" id="refresh-token-interceptor-刷新拦截器"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#refresh-token-interceptor-刷新拦截器-1">刷新拦截器</button></li><li class="tab"><button type="button" data-href="#refresh-token-interceptor-刷新拦截器-2">登录拦截器</button></li><li class="tab"><button type="button" data-href="#refresh-token-interceptor-刷新拦截器-3">拦截器配置</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="refresh-token-interceptor-刷新拦截器-1"><p><strong>RefreshTokenInterceptor：</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">RefreshTokenInterceptor</span> <span class="keyword">implements</span> <span class="title class_">HandlerInterceptor</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> StringRedisTemplate stringRedisTemplate;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="title function_">RefreshTokenInterceptor</span><span class="params">(StringRedisTemplate stringRedisTemplate)</span> &#123;</span><br><span class="line">        <span class="built_in">this</span>.stringRedisTemplate = stringRedisTemplate;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="type">boolean</span> <span class="title function_">preHandle</span><span class="params">(HttpServletRequest request, </span></span><br><span class="line"><span class="params">                           HttpServletResponse response, </span></span><br><span class="line"><span class="params">                           Object handler)</span> <span class="keyword">throws</span> Exception &#123;</span><br><span class="line">        <span class="comment">// 1. 获取请求头中的token</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">token</span> <span class="operator">=</span> request.getHeader(<span class="string">&quot;authorization&quot;</span>);</span><br><span class="line">        <span class="keyword">if</span> (StrUtil.isBlank(token)) &#123;</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">true</span>; <span class="comment">// 无token直接放行</span></span><br><span class="line">        &#125;</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 2. 基于token获取Redis中的用户信息</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">key</span> <span class="operator">=</span> LOGIN_USER_KEY + token;</span><br><span class="line">        Map&lt;Object, Object&gt; userMap = stringRedisTemplate.opsForHash()</span><br><span class="line">            .entries(key);</span><br><span class="line">            </span><br><span class="line">        <span class="comment">// 3. 判断用户是否存在</span></span><br><span class="line">        <span class="keyword">if</span> (userMap.isEmpty()) &#123;</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">true</span>; <span class="comment">// 用户不存在直接放行</span></span><br><span class="line">        &#125;</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 4. 将查询到的hash数据转为UserDTO</span></span><br><span class="line">        <span class="type">UserDTO</span> <span class="variable">userDTO</span> <span class="operator">=</span> BeanUtil.fillBeanWithMap(userMap, </span><br><span class="line">            <span class="keyword">new</span> <span class="title class_">UserDTO</span>(), <span class="literal">false</span>);</span><br><span class="line">            </span><br><span class="line">        <span class="comment">// 5. 保存用户信息到ThreadLocal</span></span><br><span class="line">        UserHolder.saveUser(userDTO);</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 6. 刷新token有效期</span></span><br><span class="line">        stringRedisTemplate.expire(key, LOGIN_USER_TTL, TimeUnit.MINUTES);</span><br><span class="line">        </span><br><span class="line">        <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">afterCompletion</span><span class="params">(HttpServletRequest request, </span></span><br><span class="line"><span class="params">                              HttpServletResponse response, </span></span><br><span class="line"><span class="params">                              Object handler, Exception ex)</span> <span class="keyword">throws</span> Exception &#123;</span><br><span class="line">        <span class="comment">// 移除用户，防止内存泄漏</span></span><br><span class="line">        UserHolder.removeUser();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="refresh-token-interceptor-刷新拦截器-2"><p><strong>LoginInterceptor：</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">LoginInterceptor</span> <span class="keyword">implements</span> <span class="title class_">HandlerInterceptor</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="type">boolean</span> <span class="title function_">preHandle</span><span class="params">(HttpServletRequest request, </span></span><br><span class="line"><span class="params">                           HttpServletResponse response, </span></span><br><span class="line"><span class="params">                           Object handler)</span> <span class="keyword">throws</span> Exception &#123;</span><br><span class="line">        <span class="comment">// 1. 判断是否需要拦截（ThreadLocal中是否有用户）</span></span><br><span class="line">        <span class="keyword">if</span> (UserHolder.getUser() == <span class="literal">null</span>) &#123;</span><br><span class="line">            <span class="comment">// 2. 没有登录，设置状态码</span></span><br><span class="line">            response.setStatus(<span class="number">401</span>);</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">false</span>; <span class="comment">// 拦截</span></span><br><span class="line">        &#125;</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 3. 有用户，放行</span></span><br><span class="line">        <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="refresh-token-interceptor-刷新拦截器-3"><p><strong>MvcConfig配置：</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Configuration</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">MvcConfig</span> <span class="keyword">implements</span> <span class="title class_">WebMvcConfigurer</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Resource</span></span><br><span class="line">    <span class="keyword">private</span> RefreshTokenInterceptor refreshTokenInterceptor;</span><br><span class="line">    </span><br><span class="line">    <span class="meta">@Resource</span></span><br><span class="line">    <span class="keyword">private</span> LoginInterceptor loginInterceptor;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">addInterceptors</span><span class="params">(InterceptorRegistry registry)</span> &#123;</span><br><span class="line">        <span class="comment">// 1. 刷新拦截器 - 拦截所有路径，用于刷新token</span></span><br><span class="line">        registry.addInterceptor(refreshTokenInterceptor)</span><br><span class="line">                .addPathPatterns(<span class="string">&quot;/**&quot;</span>)</span><br><span class="line">                .order(<span class="number">0</span>); <span class="comment">// 最高优先级</span></span><br><span class="line">                </span><br><span class="line">        <span class="comment">// 2. 登录拦截器 - 只拦截需要登录的路径</span></span><br><span class="line">        registry.addInterceptor(loginInterceptor)</span><br><span class="line">                .excludePathPatterns(</span><br><span class="line">                    <span class="string">&quot;/user/code&quot;</span>,</span><br><span class="line">                    <span class="string">&quot;/user/login&quot;</span>, </span><br><span class="line">                    <span class="string">&quot;/blog/hot&quot;</span>,</span><br><span class="line">                    <span class="string">&quot;/shop/**&quot;</span>,</span><br><span class="line">                    <span class="string">&quot;/shop-type/**&quot;</span>,</span><br><span class="line">                    <span class="string">&quot;/upload/**&quot;</span>,</span><br><span class="line">                    <span class="string">&quot;/voucher/**&quot;</span></span><br><span class="line">                )</span><br><span class="line">                .order(<span class="number">1</span>); <span class="comment">// 第二优先级</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h2 id="2-商户查询缓存">2. 商户查询缓存</h2><h3 id="2-1-缓存基础概念">2.1 缓存基础概念</h3><h4 id="2-1-1-什么是缓存？">2.1.1 什么是缓存？</h4>    <div class="note note-info">      <div class="note-header">        <i class="note-icon fa-regular fa-circle-info"></i>        <span class="note-title">提示信息</span>      </div>      <div class="note-content">        <p><strong>缓存的本质</strong></p><p>缓存就像避震器一样，为系统提供缓冲保护，防止高频访问对数据库造成冲击。</p>      </div>    </div>  <p><strong>生活类比：</strong></p><p><img src="https://markpic.adoreorg.cn/2025/09/Redis/%E9%81%BF%E9%9C%87%E5%99%A8.gif" alt="避震器"></p><p>就像越野车的避震器，在崎岖地形中为车体提供保护：</p><ul><li><strong>保护作用</strong>：防止硬着陆对车体造成损害</li><li><strong>缓冲作用</strong>：吸收冲击力，提供平稳体验</li><li><strong>延长寿命</strong>：减少系统组件的磨损</li></ul><p>同样，在实际开发中，系统也需要&quot;避震器&quot;，防止过高的数据访问量冲击系统，导致操作线程无法及时处理信息而瘫痪。</p><h4 id="2-1-2-缓存的技术定义">2.1.2 缓存的技术定义</h4><p><strong>缓存（Cache）</strong>，就是数据交换的<strong>缓冲区</strong>，俗称的缓存就是<strong>缓冲区内的数据</strong>，一般从数据库中获取，存储于本地代码中。</p><div class="tabs" id="缓存技术"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#缓存技术-1">本地缓存</button></li><li class="tab"><button type="button" data-href="#缓存技术-2">分布式缓存</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="缓存技术-1"><p><strong>本地缓存实现：</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 高并发场景：ConcurrentHashMap</span></span><br><span class="line"><span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> ConcurrentHashMap&lt;String, Object&gt; LOCAL_CACHE = </span><br><span class="line">    <span class="keyword">new</span> <span class="title class_">ConcurrentHashMap</span>&lt;&gt;();</span><br><span class="line"></span><br><span class="line"><span class="comment">// 普通场景：HashMap  </span></span><br><span class="line"><span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> Map&lt;String, Object&gt; SIMPLE_CACHE = <span class="keyword">new</span> <span class="title class_">HashMap</span>&lt;&gt;();</span><br><span class="line"></span><br><span class="line"><span class="comment">// Guava缓存：功能更完善</span></span><br><span class="line"><span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> Cache&lt;String, Object&gt; GUAVA_CACHE = </span><br><span class="line">    CacheBuilder.newBuilder()</span><br><span class="line">        .maximumSize(<span class="number">1000</span>)</span><br><span class="line">        .expireAfterWrite(<span class="number">10</span>, TimeUnit.MINUTES)</span><br><span class="line">        .build();</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="缓存技术-2"><p><strong>Redis缓存实现：</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Autowired</span></span><br><span class="line"><span class="keyword">private</span> RedisTemplate&lt;String, Object&gt; redisTemplate;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 缓存操作</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">setCache</span><span class="params">(String key, Object value, <span class="type">long</span> timeout)</span> &#123;</span><br><span class="line">    redisTemplate.opsForValue().set(key, value, timeout, TimeUnit.MINUTES);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> Object <span class="title function_">getCache</span><span class="params">(String key)</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> redisTemplate.opsForValue().get(key);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><p><strong>缓存特性：</strong></p><ul><li><strong>Static修饰</strong>：随着类加载而加载到内存中</li><li><strong>Final修饰</strong>：引用关系固定，不用担心赋值导致缓存失效</li><li><strong>内存存储</strong>：读写性能远高于磁盘存储</li></ul><h4 id="2-1-3-为什么要使用缓存？">2.1.3 为什么要使用缓存？</h4>    <div class="note note-success">      <div class="note-header">        <i class="note-icon fa-regular fa-circle-check"></i>        <span class="note-title">提示信息</span>      </div>      <div class="note-content">        <p><strong>缓存的核心价值</strong></p><p>缓存数据存储于内存中，而内存的读写性能远高于磁盘，可以大大降低高并发访问带来的服务器读写压力。</p>      </div>    </div>  <p><strong>性能对比：</strong></p><table><thead><tr><th>存储介质</th><th>读取速度</th><th>并发能力</th><th>成本</th></tr></thead><tbody><tr><td><strong>内存缓存</strong></td><td>纳秒级</td><td>10万+QPS</td><td>较高</td></tr><tr><td><strong>SSD磁盘</strong></td><td>微秒级</td><td>1万QPS</td><td>中等</td></tr><tr><td><strong>机械磁盘</strong></td><td>毫秒级</td><td>1千QPS</td><td>较低</td></tr></tbody></table><p><strong>业务价值：</strong></p><ol><li><strong>用户体验提升</strong>：页面响应从秒级→毫秒级</li><li><strong>系统稳定性</strong>：防止高并发冲垮数据库</li><li><strong>成本优化</strong>：减少数据库服务器压力</li><li><strong>扩展能力</strong>：支持更高的并发访问量</li></ol><p><strong>数据规模挑战：</strong></p><p>实际开发中，企业数据量从几十万到几千万不等，如果没有缓存作为&quot;避震器&quot;，系统几乎无法承受高并发访问。</p>    <div class="note note-warning">      <div class="note-header">        <i class="note-icon fa-regular fa-circle-dot"></i>        <span class="note-title">提示信息</span>      </div>      <div class="note-content">        <p><strong>缓存的代价</strong></p><p>缓存技术虽然强大，但也会增加代码复杂度和运维成本，需要权衡使用。</p>      </div>    </div>  <p><img src="https://markpic.adoreorg.cn/2025/09/Redis/image-20220523214414123.png" alt=""></p><h4 id="2-1-4-如何使用缓存？">2.1.4 如何使用缓存？</h4>    <div class="note note-info">      <div class="note-header">        <i class="note-icon fa-regular fa-circle-info"></i>        <span class="note-title">提示信息</span>      </div>      <div class="note-content">        <p><strong>多级缓存架构</strong></p><p>在实际开发中，会构建多级缓存体系来最大化系统性能，每一级缓存都有其特定的作用和使用场景。</p>      </div>    </div>  <p><strong>多级缓存层次：</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">用户请求</span><br><span class="line">    ↓</span><br><span class="line">浏览器缓存 (客户端缓存)</span><br><span class="line">    ↓</span><br><span class="line">CDN缓存 (边缘节点缓存)</span><br><span class="line">    ↓</span><br><span class="line">应用层缓存 (Tomcat本地缓存 + Redis分布式缓存)</span><br><span class="line">    ↓</span><br><span class="line">数据库缓存 (Buffer Pool)</span><br><span class="line">    ↓</span><br><span class="line">CPU缓存 (L1/L2/L3)</span><br></pre></td></tr></table></figure><div class="tabs" id="缓存使用"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#缓存使用-1">浏览器缓存</button></li><li class="tab"><button type="button" data-href="#缓存使用-2">应用层缓存</button></li><li class="tab"><button type="button" data-href="#缓存使用-3">数据库缓存</button></li><li class="tab"><button type="button" data-href="#缓存使用-4">CPU缓存</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="缓存使用-1"><p><strong>浏览器缓存：</strong></p><ul><li><strong>存储位置</strong>：用户浏览器本地</li><li><strong>控制方式</strong>：HTTP头信息控制</li><li><strong>典型应用</strong>：静态资源缓存（CSS、JS、图片）</li><li><strong>有效期</strong>：通过Cache-Control、Expires设置</li></ul><figure class="highlight http"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="attribute">Cache-Control</span><span class="punctuation">: </span>max-age=3600</span><br><span class="line"><span class="attribute">Expires</span><span class="punctuation">: </span>Wed, 21 Oct 2025 07:28:00 GMT</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="缓存使用-2"><p><strong>应用层缓存：</strong></p><ul><li><strong>本地缓存</strong>：Tomcat JVM内存中的Map结构</li><li><strong>分布式缓存</strong>：Redis集群存储</li><li><strong>使用场景</strong>：热点数据、会话信息、配置数据</li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 本地缓存</span></span><br><span class="line"><span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> Map&lt;String, Object&gt; localCache = <span class="keyword">new</span> <span class="title class_">ConcurrentHashMap</span>&lt;&gt;();</span><br><span class="line"></span><br><span class="line"><span class="comment">// Redis缓存</span></span><br><span class="line"><span class="meta">@Autowired</span></span><br><span class="line"><span class="keyword">private</span> RedisTemplate&lt;String, Object&gt; redisTemplate;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="缓存使用-3"><p><strong>数据库缓存：</strong></p><ul><li><strong>Buffer Pool</strong>：InnoDB存储引擎的缓冲池</li><li><strong>查询缓存</strong>：MySQL查询结果缓存（8.0已废弃）</li><li><strong>作用</strong>：减少磁盘IO，提升查询性能</li></ul><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 查看Buffer Pool状态</span></span><br><span class="line"><span class="keyword">SHOW</span> ENGINE INNODB STATUS\G</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="缓存使用-4"><p><strong>CPU缓存：</strong></p><ul><li><strong>L1缓存</strong>：指令缓存和数据缓存，32-64KB</li><li><strong>L2缓存</strong>：256-512KB，速度次于L1</li><li><strong>L3缓存</strong>：多核心共享，8-32MB</li></ul><p><strong>缓存层级对比：</strong></p><table><thead><tr><th>缓存级别</th><th>容量</th><th>访问延迟</th><th>命中率</th></tr></thead><tbody><tr><td>L1</td><td>32-64KB</td><td>1-2ns</td><td>80%+</td></tr><tr><td>L2</td><td>256-512KB</td><td>3-5ns</td><td>90%+</td></tr><tr><td>L3</td><td>8-32MB</td><td>10-20ns</td><td>95%+</td></tr></tbody></table><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><p><img src="https://markpic.adoreorg.cn/2025/09/Redis/image-20220523212915666.png" alt=""></p><h3 id="2-2-商户缓存实现">2.2 商户缓存实现</h3><h4 id="2-2-1-业务场景分析">2.2.1 业务场景分析</h4>    <div class="note note-info">      <div class="note-header">        <i class="note-icon fa-regular fa-circle-info"></i>        <span class="note-title">提示信息</span>      </div>      <div class="note-content">        <p><strong>性能瓶颈识别</strong></p><p>在查询商户信息时，如果直接操作数据库，当并发量增大时，数据库压力会急剧上升，查询性能会显著下降。</p>      </div>    </div>  <p><strong>原始代码：</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@GetMapping(&quot;/&#123;id&#125;&quot;)</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">queryShopById</span><span class="params">(<span class="meta">@PathVariable(&quot;id&quot;)</span> Long id)</span> &#123;</span><br><span class="line">    <span class="comment">// 直接查询数据库 - 性能瓶颈</span></span><br><span class="line">    <span class="keyword">return</span> shopService.queryById(id);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>性能问题：</strong></p><ul><li>每次查询都要访问数据库</li><li>数据库连接资源有限</li><li>高并发下数据库成为瓶颈</li><li>相同数据重复查询浪费资源</li></ul><h4 id="2-2-2-缓存架构设计">2.2.2 缓存架构设计</h4>    <div class="note note-success">      <div class="note-header">        <i class="note-icon fa-regular fa-circle-check"></i>        <span class="note-title">提示信息</span>      </div>      <div class="note-content">        <p><strong>标准缓存模式</strong></p><p>采用Cache-Aside模式：查询前先查缓存，缓存命中直接返回，未命中查询数据库并写入缓存。</p>      </div>    </div>  <p><strong>缓存流程图：</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">用户请求</span><br><span class="line">    ↓</span><br><span class="line">查询Redis缓存</span><br><span class="line">    ↓</span><br><span class="line">缓存命中？→ 是 → 直接返回缓存数据</span><br><span class="line">    ↓ 否</span><br><span class="line">查询MySQL数据库</span><br><span class="line">    ↓</span><br><span class="line">写入Redis缓存</span><br><span class="line">    ↓</span><br><span class="line">返回查询结果</span><br></pre></td></tr></table></figure><p><img src="https://markpic.adoreorg.cn/2025/09/Redis/1653322097736.png" alt="1653322097736"></p><h4 id="2-2-3-代码实现">2.2.3 代码实现</h4>    <div class="note note-primary">      <div class="note-header">        <i class="note-icon fa-regular fa-star"></i>        <span class="note-title">提示信息</span>      </div>      <div class="note-content">        <p><strong>实现思路</strong></p><p>代码逻辑：如果缓存命中则直接返回，如果缓存未命中则查询数据库，然后将结果写入Redis缓存。</p>      </div>    </div>  <div class="tabs" id="缓存"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#缓存-1">Service层实现</button></li><li class="tab"><button type="button" data-href="#缓存-2">Controller层</button></li><li class="tab"><button type="button" data-href="#缓存-3">Redis常量</button></li><li class="tab"><button type="button" data-href="#缓存-4">测试验证</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="缓存-1"><p><strong>ShopServiceImpl：</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Service</span></span><br><span class="line"><span class="meta">@Slf4j</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">ShopServiceImpl</span> <span class="keyword">extends</span> <span class="title class_">ServiceImpl</span>&lt;ShopMapper, Shop&gt; <span class="keyword">implements</span> <span class="title class_">IShopService</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Resource</span></span><br><span class="line">    <span class="keyword">private</span> StringRedisTemplate stringRedisTemplate;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> Result <span class="title function_">queryShopById</span><span class="params">(Long id)</span> &#123;</span><br><span class="line">        <span class="comment">// 1. 从Redis查询商户缓存</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">key</span> <span class="operator">=</span> CACHE_SHOP_KEY + id;</span><br><span class="line">        <span class="type">String</span> <span class="variable">shopJson</span> <span class="operator">=</span> stringRedisTemplate.opsForValue().get(key);</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 2. 判断是否存在</span></span><br><span class="line">        <span class="keyword">if</span> (StrUtil.isNotBlank(shopJson)) &#123;</span><br><span class="line">            <span class="comment">// 3. 存在，直接返回</span></span><br><span class="line">            <span class="type">Shop</span> <span class="variable">shop</span> <span class="operator">=</span> JSONUtil.toBean(shopJson, Shop.class);</span><br><span class="line">            <span class="keyword">return</span> Result.ok(shop);</span><br><span class="line">        &#125;</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 4. 不存在，根据id查询数据库</span></span><br><span class="line">        <span class="type">Shop</span> <span class="variable">shop</span> <span class="operator">=</span> getById(id);</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 5. 不存在，返回错误</span></span><br><span class="line">        <span class="keyword">if</span> (shop == <span class="literal">null</span>) &#123;</span><br><span class="line">            <span class="keyword">return</span> Result.fail(<span class="string">&quot;店铺不存在！&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 6. 存在，写入Redis</span></span><br><span class="line">        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), </span><br><span class="line">            CACHE_SHOP_TTL, TimeUnit.MINUTES);</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 7. 返回</span></span><br><span class="line">        <span class="keyword">return</span> Result.ok(shop);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="缓存-2"><p><strong>ShopController：</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@RestController</span></span><br><span class="line"><span class="meta">@RequestMapping(&quot;/shop&quot;)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">ShopController</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@GetMapping(&quot;/&#123;id&#125;&quot;)</span></span><br><span class="line">    <span class="keyword">public</span> Result <span class="title function_">queryShop</span><span class="params">(<span class="meta">@PathVariable(&quot;id&quot;)</span> Long id)</span> &#123;</span><br><span class="line">        <span class="comment">// 调用Service层的缓存查询方法</span></span><br><span class="line">        <span class="keyword">return</span> shopService.queryShopById(id);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="缓存-3"><p><strong>缓存常量配置：</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">RedisConstants</span> &#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">String</span> <span class="variable">CACHE_SHOP_KEY</span> <span class="operator">=</span> <span class="string">&quot;cache:shop:&quot;</span>;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">Long</span> <span class="variable">CACHE_SHOP_TTL</span> <span class="operator">=</span> <span class="number">30L</span>; <span class="comment">// 30分钟</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="缓存-4"><p><strong>性能测试对比：</strong></p><table><thead><tr><th>查询方式</th><th>平均响应时间</th><th>QPS</th><th>数据库压力</th></tr></thead><tbody><tr><td>直接查数据库</td><td>150ms</td><td>500</td><td>高</td></tr><tr><td>Redis缓存</td><td>5ms</td><td>10,000+</td><td>低</td></tr><tr><td>性能提升</td><td><strong>30倍</strong></td><td><strong>20倍</strong></td><td><strong>大幅降低</strong></td></tr></tbody></table><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><p><img src="https://markpic.adoreorg.cn/2025/09/Redis/1653322190155.png" alt="1653322190155"></p><h3 id="2-3-缓存更新策略">2.3 缓存更新策略</h3><h4 id="2-3-1-缓存更新策略概述">2.3.1 缓存更新策略概述</h4>    <div class="note note-info">      <div class="note-header">        <i class="note-icon fa-regular fa-circle-info"></i>        <span class="note-title">提示信息</span>      </div>      <div class="note-content">        <p><strong>缓存更新的必要性</strong></p><p>内存资源宝贵，当缓存数据过多时需要合理的更新策略来保证系统性能和数据一致性。</p>      </div>    </div>  <p><strong>三大缓存更新策略：</strong></p><div class="tabs" id="缓存更新策略"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#缓存更新策略-1">内存淘汰策略</button></li><li class="tab"><button type="button" data-href="#缓存更新策略-2">超时剔除策略</button></li><li class="tab"><button type="button" data-href="#缓存更新策略-3">主动更新策略</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="缓存更新策略-1"><p><strong>内存淘汰机制</strong></p><p>当Redis内存达到<code>max-memory</code>设置的上限时，自动触发内存淘汰机制，根据配置的淘汰策略删除部分数据。</p><p><strong>常见淘汰策略：</strong></p><table><thead><tr><th>策略</th><th>描述</th><th>适用场景</th></tr></thead><tbody><tr><td>noeviction</td><td>不淘汰，返回错误</td><td>数据不能丢失</td></tr><tr><td>allkeys-lru</td><td>所有key中淘汰最近最少使用</td><td>缓存应用</td></tr><tr><td>volatile-lru</td><td>设置了过期时间的key中淘汰最近最少使用</td><td>混合数据</td></tr><tr><td>allkeys-random</td><td>随机淘汰所有key</td><td>测试环境</td></tr><tr><td>volatile-ttl</td><td>淘汰即将过期的key</td><td>临时数据</td></tr></tbody></table><p><strong>配置示例：</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Redis配置文件</span></span><br><span class="line">maxmemory 2gb</span><br><span class="line">maxmemory-policy allkeys-lru</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="缓存更新策略-2"><p><strong>TTL过期机制</strong></p><p>为缓存数据设置合理的过期时间（TTL），Redis会自动删除过期的数据。</p><p><strong>TTL设置原则：</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 业务数据缓存 - 30分钟</span></span><br><span class="line">stringRedisTemplate.opsForValue().set(key, value, <span class="number">30</span>, TimeUnit.MINUTES);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 热点数据缓存 - 1小时  </span></span><br><span class="line">stringRedisTemplate.opsForValue().set(key, value, <span class="number">1</span>, TimeUnit.HOURS);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 配置数据缓存 - 24小时</span></span><br><span class="line">stringRedisTemplate.opsForValue().set(key, value, <span class="number">24</span>, TimeUnit.HOURS);</span><br></pre></td></tr></table></figure><p><strong>TTL设计要点：</strong></p><ul><li>热点数据：TTL较短，保证数据新鲜度</li><li>冷数据：TTL较长，减少数据库压力</li><li>业务数据：根据业务需求设置合理TTL</li></ul><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="缓存更新策略-3"><p><strong>手动更新机制</strong></p><p>在数据变更时主动删除或更新缓存，保证缓存与数据库的数据一致性。</p><p><strong>更新时机：</strong></p><ul><li>数据新增时：写入缓存</li><li>数据修改时：删除缓存</li><li>数据删除时：删除缓存</li></ul><p><strong>代码示例：</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 更新商户信息</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">updateShop</span><span class="params">(Shop shop)</span> &#123;</span><br><span class="line">    <span class="type">Long</span> <span class="variable">id</span> <span class="operator">=</span> shop.getId();</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 1. 更新数据库</span></span><br><span class="line">    updateById(shop);</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 2. 删除缓存（主动更新）</span></span><br><span class="line">    <span class="type">String</span> <span class="variable">key</span> <span class="operator">=</span> CACHE_SHOP_KEY + id;</span><br><span class="line">    stringRedisTemplate.delete(key);</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">return</span> Result.ok();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><p><img src="https://markpic.adoreorg.cn/2025/09/Redis/1653322506393.png" alt="1653322506393"></p><h4 id="2-3-2-数据库缓存不一致解决方案">2.3.2 数据库缓存不一致解决方案</h4>    <div class="note note-warning">      <div class="note-header">        <i class="note-icon fa-regular fa-circle-dot"></i>        <span class="note-title">提示信息</span>      </div>      <div class="note-content">        <p><strong>一致性问题分析</strong></p><p>缓存数据源来自数据库，而数据库数据会发生变化。当数据库数据发生变化而缓存未同步时，就会产生一致性问题，用户使用过时数据会影响业务正确性。</p>      </div>    </div>  <p><strong>三大解决方案对比：</strong></p><div class="tabs" id="一致性问题解决方案"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#一致性问题解决方案-1">Cache Aside Pattern</button></li><li class="tab"><button type="button" data-href="#一致性问题解决方案-2">Read/Write Through</button></li><li class="tab"><button type="button" data-href="#一致性问题解决方案-3">Write Behind Caching</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="一致性问题解决方案-1"><p><strong>旁路缓存模式（推荐）</strong></p><p>缓存调用者在更新数据库后手动更新缓存，也称为双写方案。</p><p><strong>实现流程：</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">查询操作：</span><br><span class="line">缓存命中 → 直接返回</span><br><span class="line">缓存未命中 → 查询数据库 → 写入缓存 → 返回结果</span><br><span class="line"></span><br><span class="line">更新操作：</span><br><span class="line">更新数据库 → 删除缓存</span><br></pre></td></tr></table></figure><p><strong>优点：</strong></p><ul><li>✅ 实现简单，控制灵活</li><li>✅ 数据一致性较好</li><li>✅ 适合读多写少场景</li></ul><p><strong>缺点：</strong></p><ul><li>❌ 需要开发者手动维护</li><li>❌ 存在短暂不一致窗口</li></ul><p><strong>适用场景：</strong></p><ul><li>读多写少的业务系统</li><li>对数据一致性要求较高</li><li>开发团队有较强技术能力</li></ul><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="一致性问题解决方案-2"><p><strong>读写穿透模式</strong></p><p>由缓存系统本身完成数据库与缓存的同步操作。</p><p><strong>实现流程：</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">查询操作：</span><br><span class="line">缓存未命中 → 缓存系统查询数据库 → 写入缓存 → 返回结果</span><br><span class="line"></span><br><span class="line">更新操作：</span><br><span class="line">更新缓存 → 缓存系统同步更新数据库</span><br></pre></td></tr></table></figure><p><strong>优点：</strong></p><ul><li>✅ 对应用透明，简化开发</li><li>✅ 缓存系统统一管理</li><li>✅ 数据一致性较好</li></ul><p><strong>缺点：</strong></p><ul><li>❌ 实现复杂，需要缓存系统支持</li><li>❌ 性能开销较大</li><li>❌ 强依赖缓存系统稳定性</li></ul><p><strong>适用场景：</strong></p><ul><li>大型分布式系统</li><li>有专业缓存中间件支持</li><li>对开发简化要求较高</li></ul><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="一致性问题解决方案-3"><p><strong>异步写回模式</strong></p><p>调用者只操作缓存，其他线程异步处理数据库，实现最终一致性。</p><p><strong>实现流程：</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">查询操作：</span><br><span class="line">直接查询缓存</span><br><span class="line"></span><br><span class="line">更新操作：</span><br><span class="line">更新缓存 → 异步线程批量写入数据库</span><br></pre></td></tr></table></figure><p><strong>优点：</strong></p><ul><li>✅ 写入性能极高</li><li>✅ 数据库压力小</li><li>✅ 适合写密集型应用</li></ul><p><strong>缺点：</strong></p><ul><li>❌ 数据一致性差</li><li>❌ 实现复杂度高</li><li>❌ 可能丢失数据</li></ul><p><strong>适用场景：</strong></p><ul><li>写操作远多于读操作</li><li>对数据一致性要求不高</li><li>允许短暂数据丢失</li></ul><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><p><strong>方案选择建议：</strong></p><table><thead><tr><th>方案</th><th>一致性</th><th>性能</th><th>复杂度</th><th>适用场景</th></tr></thead><tbody><tr><td>Cache Aside</td><td>⭐⭐⭐</td><td>⭐⭐⭐</td><td>⭐⭐</td><td>读多写少</td></tr><tr><td>Read/Write Through</td><td>⭐⭐⭐⭐</td><td>⭐⭐</td><td>⭐⭐⭐⭐</td><td>大型系统</td></tr><tr><td>Write Behind</td><td>⭐⭐</td><td>⭐⭐⭐⭐</td><td>⭐⭐⭐⭐⭐</td><td>写密集型</td></tr></tbody></table><p><img src="https://markpic.adoreorg.cn/2025/09/Redis/1653322857620.png" alt="1653322857620"></p><h4 id="2-3-3-最佳实践方案选择">2.3.3 最佳实践方案选择</h4>    <div class="note note-success">      <div class="note-header">        <i class="note-icon fa-regular fa-circle-check"></i>        <span class="note-title">提示信息</span>      </div>      <div class="note-content">        <p><strong>推荐方案：Cache Aside + 删除缓存策略</strong></p><p>综合考虑一致性、性能和实现复杂度，推荐使用Cache Aside模式配合删除缓存策略。</p>      </div>    </div>  <p><strong>核心设计原则：</strong></p><div class="tabs" id="删除缓存-vs-更新缓存"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#删除缓存-vs-更新缓存-1">删除缓存 vs 更新缓存</button></li><li class="tab"><button type="button" data-href="#删除缓存-vs-更新缓存-2">操作顺序选择</button></li><li class="tab"><button type="button" data-href="#删除缓存-vs-更新缓存-3">事务保证</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="删除缓存-vs-更新缓存-1"><p><strong>删除缓存策略（推荐）</strong></p><p>更新数据库时让缓存失效，查询时再更新缓存。</p><p><strong>实现流程：</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">更新操作：</span><br><span class="line">1. 更新数据库</span><br><span class="line">2. 删除缓存</span><br><span class="line">3. 等待下次查询时重新加载</span><br></pre></td></tr></table></figure><p><strong>优势分析：</strong></p><ul><li>✅ <strong>避免无效写操作</strong>：多次更新只需一次删除</li><li>✅ <strong>简化实现逻辑</strong>：无需考虑缓存数据格式转换</li><li>✅ <strong>降低并发问题</strong>：减少缓存与数据库不一致时间窗口</li></ul><p><strong>对比表格：</strong></p><table><thead><tr><th>策略</th><th>写操作次数</th><th>实现复杂度</th><th>一致性风险</th><th>推荐度</th></tr></thead><tbody><tr><td>更新缓存</td><td>每次更新都写缓存</td><td>高（需处理数据转换）</td><td>高（并发更新冲突）</td><td>⭐⭐</td></tr><tr><td>删除缓存</td><td>仅删除，查询时重建</td><td>低（简单删除）</td><td>低（重建时数据最新）</td><td>⭐⭐⭐⭐⭐</td></tr></tbody></table><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="删除缓存-vs-更新缓存-2"><p><strong>先操作数据库，再删除缓存（推荐）</strong></p><p>确保数据库操作成功后再删除缓存，避免缓存删除后数据库更新失败的情况。</p><p><strong>时序分析：</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">线程1：更新数据库 → 删除缓存</span><br><span class="line">线程2：查询缓存未命中 → 查询数据库 → 写入缓存</span><br></pre></td></tr></table></figure><p><strong>并发安全性：</strong></p><ul><li>即使线程2在线程1删除缓存后查询数据库，获取的也是最新的数据</li><li>避免了&quot;删除缓存→数据库更新失败&quot;导致的数据不一致</li></ul><p><strong>对比分析：</strong></p><table><thead><tr><th>操作顺序</th><th>并发风险</th><th>一致性保证</th><th>实现复杂度</th><th>推荐度</th></tr></thead><tbody><tr><td>先删缓存再更新数据库</td><td>高（其他线程可能写入旧数据）</td><td>差</td><td>高（需额外锁机制）</td><td>⭐⭐</td></tr><tr><td>先更新数据库再删缓存</td><td>低（数据库已更新）</td><td>好</td><td>低</td><td>⭐⭐⭐⭐⭐</td></tr></tbody></table><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="删除缓存-vs-更新缓存-3"><p><strong>操作原子性保证</strong></p><p>确保缓存与数据库操作同时成功或失败，避免中间状态。</p><p><strong>单体系统事务：</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Transactional</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">updateShop</span><span class="params">(Shop shop)</span> &#123;</span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">        <span class="comment">// 1. 更新数据库</span></span><br><span class="line">        shopMapper.updateById(shop);</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 2. 删除缓存</span></span><br><span class="line">        stringRedisTemplate.delete(CACHE_SHOP_KEY + shop.getId());</span><br><span class="line">        </span><br><span class="line">        <span class="keyword">return</span> Result.ok();</span><br><span class="line">    &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">        <span class="comment">// 事务回滚，数据库和缓存保持一致</span></span><br><span class="line">        <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">RuntimeException</span>(<span class="string">&quot;更新失败&quot;</span>, e);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>分布式系统方案：</strong></p><ul><li><strong>TCC模式</strong>：Try-Confirm-Cancel三阶段提交</li><li><strong>消息队列</strong>：通过消息确保最终一致性</li><li><strong>分布式锁</strong>：保证操作顺序性</li></ul><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><p><strong>最终推荐方案：</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">🎯 最佳实践组合：</span><br><span class="line">1. 采用Cache Aside模式</span><br><span class="line">2. 使用删除缓存策略</span><br><span class="line">3. 先更新数据库再删除缓存</span><br><span class="line">4. 单体系统使用事务保证</span><br><span class="line">5. 分布式系统使用消息队列保证最终一致性</span><br></pre></td></tr></table></figure><p><img src="https://markpic.adoreorg.cn/2025/09/Redis/1653323595206.png" alt="1653323595206"></p><h3 id="2-4-商户缓存双写一致性实现">2.4 商户缓存双写一致性实现</h3><h4 id="2-4-1-实现思路">2.4.1 实现思路</h4>    <div class="note note-primary">      <div class="note-header">        <i class="note-icon fa-regular fa-star"></i>        <span class="note-title">提示信息</span>      </div>      <div class="note-content">        <p><strong>双写一致性要求</strong></p><p>查询时：缓存未命中则查询数据库并写入缓存，设置合理过期时间<br>更新时：先更新数据库，再删除缓存，确保数据一致性</p>      </div>    </div>  <p><strong>实现流程图：</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">查询流程：</span><br><span class="line">用户请求 → 查询Redis缓存 → 命中？→ 是 → 返回缓存数据</span><br><span class="line">                   ↓ 否</span><br><span class="line">               查询MySQL数据库</span><br><span class="line">                   ↓</span><br><span class="line">               写入Redis缓存（带TTL）</span><br><span class="line">                   ↓</span><br><span class="line">               返回查询结果</span><br><span class="line"></span><br><span class="line">更新流程：</span><br><span class="line">用户请求 → 更新MySQL数据库 → 删除Redis缓存 → 返回更新结果</span><br></pre></td></tr></table></figure><h4 id="2-4-2-代码实现">2.4.2 代码实现</h4><div class="tabs" id="查询方法优化"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#查询方法优化-1">查询方法优化</button></li><li class="tab"><button type="button" data-href="#查询方法优化-2">更新方法实现</button></li><li class="tab"><button type="button" data-href="#查询方法优化-3">Controller层调用</button></li><li class="tab"><button type="button" data-href="#查询方法优化-4">一致性验证</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="查询方法优化-1"><p><strong>ShopServiceImpl查询方法：</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">queryShopById</span><span class="params">(Long id)</span> &#123;</span><br><span class="line">    <span class="comment">// 1. 从Redis查询商户缓存</span></span><br><span class="line">    <span class="type">String</span> <span class="variable">key</span> <span class="operator">=</span> CACHE_SHOP_KEY + id;</span><br><span class="line">    <span class="type">String</span> <span class="variable">shopJson</span> <span class="operator">=</span> stringRedisTemplate.opsForValue().get(key);</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 2. 判断缓存是否存在</span></span><br><span class="line">    <span class="keyword">if</span> (StrUtil.isNotBlank(shopJson)) &#123;</span><br><span class="line">        <span class="comment">// 3. 存在，直接返回</span></span><br><span class="line">        <span class="type">Shop</span> <span class="variable">shop</span> <span class="operator">=</span> JSONUtil.toBean(shopJson, Shop.class);</span><br><span class="line">        <span class="keyword">return</span> Result.ok(shop);</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 4. 缓存未命中，查询数据库</span></span><br><span class="line">    <span class="type">Shop</span> <span class="variable">shop</span> <span class="operator">=</span> getById(id);</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 5. 数据库不存在，返回错误</span></span><br><span class="line">    <span class="keyword">if</span> (shop == <span class="literal">null</span>) &#123;</span><br><span class="line">        <span class="keyword">return</span> Result.fail(<span class="string">&quot;店铺不存在！&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 6. 存在，写入Redis缓存（设置30分钟过期时间）</span></span><br><span class="line">    stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), </span><br><span class="line">        CACHE_SHOP_TTL, TimeUnit.MINUTES);</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 7. 返回结果</span></span><br><span class="line">    <span class="keyword">return</span> Result.ok(shop);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="查询方法优化-2"><p><strong>ShopServiceImpl更新方法：</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="meta">@Transactional</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">updateShop</span><span class="params">(Shop shop)</span> &#123;</span><br><span class="line">    <span class="type">Long</span> <span class="variable">id</span> <span class="operator">=</span> shop.getId();</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 1. 参数校验</span></span><br><span class="line">    <span class="keyword">if</span> (id == <span class="literal">null</span>) &#123;</span><br><span class="line">        <span class="keyword">return</span> Result.fail(<span class="string">&quot;店铺id不能为空！&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 2. 更新数据库</span></span><br><span class="line">    <span class="type">boolean</span> <span class="variable">updated</span> <span class="operator">=</span> updateById(shop);</span><br><span class="line">    <span class="keyword">if</span> (!updated) &#123;</span><br><span class="line">        <span class="keyword">return</span> Result.fail(<span class="string">&quot;店铺更新失败！&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 3. 删除缓存（保证数据一致性）</span></span><br><span class="line">    <span class="type">String</span> <span class="variable">key</span> <span class="operator">=</span> CACHE_SHOP_KEY + id;</span><br><span class="line">    stringRedisTemplate.delete(key);</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 4. 返回成功</span></span><br><span class="line">    <span class="keyword">return</span> Result.ok();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="查询方法优化-3"><p><strong>ShopController：</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@RestController</span></span><br><span class="line"><span class="meta">@RequestMapping(&quot;/shop&quot;)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">ShopController</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@GetMapping(&quot;/&#123;id&#125;&quot;)</span></span><br><span class="line">    <span class="keyword">public</span> Result <span class="title function_">queryShop</span><span class="params">(<span class="meta">@PathVariable(&quot;id&quot;)</span> Long id)</span> &#123;</span><br><span class="line">        <span class="comment">// 查询商户（带缓存）</span></span><br><span class="line">        <span class="keyword">return</span> shopService.queryShopById(id);</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="meta">@PutMapping</span></span><br><span class="line">    <span class="keyword">public</span> Result <span class="title function_">updateShop</span><span class="params">(<span class="meta">@RequestBody</span> Shop shop)</span> &#123;</span><br><span class="line">        <span class="comment">// 更新商户（保证缓存一致性）</span></span><br><span class="line">        <span class="keyword">return</span> shopService.updateShop(shop);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="查询方法优化-4"><p><strong>测试验证：</strong></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><tr><td>数据更新</td><td>更新数据库，删除缓存</td><td>✅</td></tr><tr><td>更新后查询</td><td>重新查询数据库，写入新缓存</td><td>✅</td></tr></tbody></table><p><strong>性能对比：</strong></p><table><thead><tr><th>操作类型</th><th>直接数据库</th><th>带缓存</th><th>性能提升</th></tr></thead><tbody><tr><td>查询操作</td><td>150ms</td><td>5ms</td><td><strong>30倍</strong></td></tr><tr><td>更新操作</td><td>50ms</td><td>55ms</td><td>基本持平</td></tr></tbody></table><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><p><img src="https://markpic.adoreorg.cn/2025/09/Redis/1653325871232.png" alt="1653325871232"></p><p><strong>代码分析：</strong><br>通过采用删除缓存策略解决双写问题，当数据更新后删除缓存，后续查询会从MySQL加载最新数据并重新写入缓存，从而避免数据库和缓存不一致的问题。</p><p><img src="https://markpic.adoreorg.cn/2025/09/Redis/1653325929549.png" alt="1653325929549"></p><h3 id="2-5-缓存穿透问题解决">2.5 缓存穿透问题解决</h3><h4 id="2-5-1-缓存穿透问题分析">2.5.1 缓存穿透问题分析</h4>    <div class="note note-warning">      <div class="note-header">        <i class="note-icon fa-regular fa-circle-dot"></i>        <span class="note-title">提示信息</span>      </div>      <div class="note-content">        <p><strong>缓存穿透定义</strong></p><p>客户端请求的数据在缓存和数据库中都不存在，导致缓存永远无法生效，所有请求都直接打到数据库，可能引发数据库崩溃。</p>      </div>    </div>  <p><strong>问题场景：</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">用户请求ID=99999的商品</span><br><span class="line">    ↓</span><br><span class="line">查询Redis缓存（不存在）</span><br><span class="line">    ↓</span><br><span class="line">查询MySQL数据库（不存在）</span><br><span class="line">    ↓</span><br><span class="line">返回404错误</span><br><span class="line">    ↓</span><br><span class="line">大量恶意请求重复此过程</span><br><span class="line">    ↓</span><br><span class="line">数据库压力剧增，可能崩溃</span><br></pre></td></tr></table></figure><p><strong>风险特征：</strong></p><ul><li>🔴 请求的数据在数据库中不存在</li><li>🔴 缓存无法命中，失去保护作用</li><li>🔴 大量请求直接访问数据库</li><li>🔴 可能被恶意利用进行攻击</li></ul><h4 id="2-5-2-解决方案对比">2.5.2 解决方案对比</h4><p><strong>两大核心解决方案：</strong></p><div class="tabs" id="缓存穿透解决方案"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#缓存穿透解决方案-1">缓存空对象方案</button></li><li class="tab"><button type="button" data-href="#缓存穿透解决方案-2">布隆过滤器方案</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="缓存穿透解决方案-1"><p><strong>实现原理</strong></p><p>当数据库查询结果为空时，仍将空结果缓存到Redis，设置较短的过期时间。</p><p><strong>实现流程：</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">用户请求 → 查询缓存未命中 → 查询数据库为空 → 缓存空值 → 返回结果</span><br><span class="line">下次请求 → 查询缓存命中（空值）→ 直接返回，不再访问数据库</span><br></pre></td></tr></table></figure><p><strong>代码示例：</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> Result <span class="title function_">queryShopById</span><span class="params">(Long id)</span> &#123;</span><br><span class="line">    <span class="type">String</span> <span class="variable">key</span> <span class="operator">=</span> CACHE_SHOP_KEY + id;</span><br><span class="line">    <span class="type">String</span> <span class="variable">shopJson</span> <span class="operator">=</span> stringRedisTemplate.opsForValue().get(key);</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 命中缓存</span></span><br><span class="line">    <span class="keyword">if</span> (StrUtil.isNotBlank(shopJson)) &#123;</span><br><span class="line">        <span class="keyword">if</span> (<span class="string">&quot;null&quot;</span>.equals(shopJson)) &#123;  <span class="comment">// 判断是否为缓存的空值</span></span><br><span class="line">            <span class="keyword">return</span> Result.fail(<span class="string">&quot;店铺不存在！&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="type">Shop</span> <span class="variable">shop</span> <span class="operator">=</span> JSONUtil.toBean(shopJson, Shop.class);</span><br><span class="line">        <span class="keyword">return</span> Result.ok(shop);</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 查询数据库</span></span><br><span class="line">    <span class="type">Shop</span> <span class="variable">shop</span> <span class="operator">=</span> getById(id);</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">if</span> (shop == <span class="literal">null</span>) &#123;</span><br><span class="line">        <span class="comment">// 缓存空值，设置2分钟过期</span></span><br><span class="line">        stringRedisTemplate.opsForValue().set(key, <span class="string">&quot;&quot;</span>, <span class="number">2</span>, TimeUnit.MINUTES);</span><br><span class="line">        <span class="keyword">return</span> Result.fail(<span class="string">&quot;店铺不存在！&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 缓存正常数据</span></span><br><span class="line">    stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);</span><br><span class="line">    <span class="keyword">return</span> Result.ok(shop);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>方案评估：</strong></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>需要额外存储空值，但可设置短TTL</td></tr><tr><td>防护效果</td><td>⭐⭐⭐⭐</td><td>有效阻止重复查询不存在数据</td></tr><tr><td>数据一致性</td><td>⭐⭐⭐</td><td>可能存在短暂不一致</td></tr></tbody></table><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="缓存穿透解决方案-2"><p><strong>实现原理</strong></p><p>使用布隆过滤器在查询前进行预判，过滤掉明显不存在的数据请求。</p><p><strong>数据结构：</strong></p><ul><li>庞大的二进制位数组（BitSet）</li><li>多个哈希函数</li><li>内存占用极低</li></ul><p><strong>实现流程：</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">用户请求 → 布隆过滤器判断 → 不存在 → 直接返回</span><br><span class="line">                ↓ 存在</span><br><span class="line">            查询Redis缓存 → 未命中 → 查询数据库</span><br></pre></td></tr></table></figure><p><strong>代码示例：</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">BloomFilterService</span> &#123;</span><br><span class="line">    </span><br><span class="line">    <span class="meta">@Resource</span></span><br><span class="line">    <span class="keyword">private</span> StringRedisTemplate stringRedisTemplate;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 初始化布隆过滤器</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">initBloomFilter</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="comment">// 将所有商品ID添加到布隆过滤器</span></span><br><span class="line">        List&lt;Long&gt; allShopIds = shopMapper.selectAllIds();</span><br><span class="line">        <span class="keyword">for</span> (Long id : allShopIds) &#123;</span><br><span class="line">            addToBloomFilter(<span class="string">&quot;shop:bloom:&quot;</span>, id);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 添加到布隆过滤器</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">void</span> <span class="title function_">addToBloomFilter</span><span class="params">(String key, Long id)</span> &#123;</span><br><span class="line">        <span class="type">int</span>[] offset = getBitOffset(id);</span><br><span class="line">        <span class="keyword">for</span> (<span class="type">int</span> i : offset) &#123;</span><br><span class="line">            stringRedisTemplate.opsForValue().setBit(key, i, <span class="literal">true</span>);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 判断是否存在</span></span><br><span class="line">    <span class="keyword">public</span> <span class="type">boolean</span> <span class="title function_">mightExist</span><span class="params">(String key, Long id)</span> &#123;</span><br><span class="line">        <span class="type">int</span>[] offset = getBitOffset(id);</span><br><span class="line">        <span class="keyword">for</span> (<span class="type">int</span> i : offset) &#123;</span><br><span class="line">            <span class="keyword">if</span> (!stringRedisTemplate.opsForValue().getBit(key, i)) &#123;</span><br><span class="line">                <span class="keyword">return</span> <span class="literal">false</span>;  <span class="comment">// 一定不存在</span></span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">true</span>;  <span class="comment">// 可能存在（存在误判）</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>方案评估：</strong></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><tr><td>防护效果</td><td>⭐⭐⭐⭐⭐</td><td>完美阻止不存在数据查询</td></tr><tr><td>误判率</td><td>⭐⭐</td><td>存在误判可能，需权衡配置</td></tr></tbody></table><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><p><strong>方案选择建议：</strong></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><tr><td>数据量巨大</td><td>布隆过滤器</td><td>内存占用与数据量无关</td></tr><tr><td>开发资源有限</td><td>缓存空对象</td><td>维护成本低</td></tr></tbody></table><p><img src="https://markpic.adoreorg.cn/2025/09/Redis/1653326156516.png" alt="1653326156516"></p><h3 id="2-6-缓存穿透编码实现">2.6 缓存穿透编码实现</h3><h4 id="2-6-1-实现思路">2.6.1 实现思路</h4>    <div class="note note-warning">      <div class="note-header">        <i class="note-icon fa-regular fa-circle-dot"></i>        <span class="note-title">提示信息</span>      </div>      <div class="note-content">        <p><strong>原始问题</strong></p><p>在原来的逻辑中，如果发现数据在MySQL中不存在，直接返回404，这样会导致缓存穿透问题，大量请求直接到达数据库。</p>      </div>    </div>  <p><strong>优化思路：</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">原逻辑：</span><br><span class="line">查询缓存未命中 → 查询数据库为空 → 直接返回404 → 缓存穿透风险</span><br><span class="line"></span><br><span class="line">新逻辑：</span><br><span class="line">查询缓存未命中 → 查询数据库为空 → 缓存空值 → 返回结果 → 下次直接命中缓存</span><br></pre></td></tr></table></figure><h4 id="2-6-2-代码实现">2.6.2 代码实现</h4><div class="tabs" id="缓存穿透实现"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#缓存穿透实现-1">完整实现</button></li><li class="tab"><button type="button" data-href="#缓存穿透实现-2">工具方法</button></li><li class="tab"><button type="button" data-href="#缓存穿透实现-3">测试验证</button></li><li class="tab"><button type="button" data-href="#缓存穿透实现-4">高级优化</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="缓存穿透实现-1"><p><strong>ShopServiceImpl缓存穿透防护：</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">queryShopById</span><span class="params">(Long id)</span> &#123;</span><br><span class="line">    <span class="comment">// 1. 参数校验</span></span><br><span class="line">    <span class="keyword">if</span> (id == <span class="literal">null</span> || id &lt;= <span class="number">0</span>) &#123;</span><br><span class="line">        <span class="keyword">return</span> Result.fail(<span class="string">&quot;店铺ID非法！&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 2. 从Redis查询商户缓存</span></span><br><span class="line">    <span class="type">String</span> <span class="variable">key</span> <span class="operator">=</span> CACHE_SHOP_KEY + id;</span><br><span class="line">    <span class="type">String</span> <span class="variable">shopJson</span> <span class="operator">=</span> stringRedisTemplate.opsForValue().get(key);</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 3. 判断缓存是否存在</span></span><br><span class="line">    <span class="keyword">if</span> (StrUtil.isNotBlank(shopJson)) &#123;</span><br><span class="line">        <span class="comment">// 4. 命中缓存，判断是否为null值（缓存穿透防护）</span></span><br><span class="line">        <span class="keyword">if</span> (<span class="string">&quot;null&quot;</span>.equals(shopJson)) &#123;</span><br><span class="line">            <span class="keyword">return</span> Result.fail(<span class="string">&quot;店铺不存在！&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">// 5. 正常数据，直接返回</span></span><br><span class="line">        <span class="type">Shop</span> <span class="variable">shop</span> <span class="operator">=</span> JSONUtil.toBean(shopJson, Shop.class);</span><br><span class="line">        <span class="keyword">return</span> Result.ok(shop);</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 6. 缓存未命中，查询数据库</span></span><br><span class="line">    <span class="type">Shop</span> <span class="variable">shop</span> <span class="operator">=</span> getById(id);</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 7. 数据库不存在（缓存穿透防护）</span></span><br><span class="line">    <span class="keyword">if</span> (shop == <span class="literal">null</span>) &#123;</span><br><span class="line">        <span class="comment">// 8. 缓存空值，设置较短的过期时间（2分钟）</span></span><br><span class="line">        stringRedisTemplate.opsForValue().set(key, <span class="string">&quot;&quot;</span>, <span class="number">2</span>, TimeUnit.MINUTES);</span><br><span class="line">        <span class="keyword">return</span> Result.fail(<span class="string">&quot;店铺不存在！&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 9. 数据库存在，写入Redis缓存</span></span><br><span class="line">    stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), </span><br><span class="line">        CACHE_SHOP_TTL, TimeUnit.MINUTES);</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 10. 返回结果</span></span><br><span class="line">    <span class="keyword">return</span> Result.ok(shop);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="缓存穿透实现-2"><p><strong>缓存穿透防护工具类：</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">CachePenetrationProtection</span> &#123;</span><br><span class="line">    </span><br><span class="line">    <span class="meta">@Resource</span></span><br><span class="line">    <span class="keyword">private</span> StringRedisTemplate stringRedisTemplate;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 安全的缓存查询方法</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> key 缓存key</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> type 返回类型</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> dbFallback 数据库查询函数</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> ttl 过期时间</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> unit 时间单位</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span> 查询结果</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">public</span> &lt;T&gt; T <span class="title function_">safeQuery</span><span class="params">(String key, Class&lt;T&gt; type, </span></span><br><span class="line"><span class="params">                          Supplier&lt;T&gt; dbFallback, </span></span><br><span class="line"><span class="params">                          Long ttl, TimeUnit unit)</span> &#123;</span><br><span class="line">        <span class="comment">// 1. 查询缓存</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">json</span> <span class="operator">=</span> stringRedisTemplate.opsForValue().get(key);</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 2. 命中缓存</span></span><br><span class="line">        <span class="keyword">if</span> (StrUtil.isNotBlank(json)) &#123;</span><br><span class="line">            <span class="keyword">if</span> (<span class="string">&quot;null&quot;</span>.equals(json)) &#123;</span><br><span class="line">                <span class="keyword">return</span> <span class="literal">null</span>;  <span class="comment">// 缓存的空值</span></span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">return</span> JSONUtil.toBean(json, type);</span><br><span class="line">        &#125;</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 3. 查询数据库</span></span><br><span class="line">        <span class="type">T</span> <span class="variable">result</span> <span class="operator">=</span> dbFallback.get();</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 4. 缓存结果（包括空值）</span></span><br><span class="line">        <span class="keyword">if</span> (result == <span class="literal">null</span>) &#123;</span><br><span class="line">            stringRedisTemplate.opsForValue().set(key, <span class="string">&quot;&quot;</span>, <span class="number">2</span>, TimeUnit.MINUTES);</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(result), ttl, unit);</span><br><span class="line">        &#125;</span><br><span class="line">        </span><br><span class="line">        <span class="keyword">return</span> result;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="缓存穿透实现-3"><p><strong>防护效果测试：</strong></p><table><thead><tr><th>测试场景</th><th>请求参数</th><th>预期结果</th><th>数据库访问次数</th></tr></thead><tbody><tr><td>正常查询</td><td>id=1</td><td>返回店铺信息</td><td>1次</td></tr><tr><td>不存在查询</td><td>id=99999</td><td>返回&quot;店铺不存在&quot;</td><td>1次</td></tr><tr><td>重复不存在查询</td><td>id=99999</td><td>返回&quot;店铺不存在&quot;</td><td>0次（命中缓存空值）</td></tr><tr><td>非法参数</td><td>id=-1</td><td>返回&quot;店铺ID非法&quot;</td><td>0次（参数校验拦截）</td></tr></tbody></table><p><strong>性能对比：</strong></p><table><thead><tr><th>查询类型</th><th>无防护QPS</th><th>有防护QPS</th><th>数据库压力</th></tr></thead><tbody><tr><td>不存在数据查询</td><td>1000（全部打到DB）</td><td>10000+（缓存拦截）</td><td>降低90%+</td></tr></tbody></table><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="缓存穿透实现-4"><p><strong>多重防护策略：</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@RestController</span></span><br><span class="line"><span class="meta">@RequestMapping(&quot;/shop&quot;)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">ShopController</span> &#123;</span><br><span class="line">    </span><br><span class="line">    <span class="meta">@GetMapping(&quot;/&#123;id&#125;&quot;)</span></span><br><span class="line">    <span class="keyword">public</span> Result <span class="title function_">queryShop</span><span class="params">(<span class="meta">@PathVariable(&quot;id&quot;)</span> String idStr)</span> &#123;</span><br><span class="line">        <span class="comment">// 1. 参数格式校验（第一层防护）</span></span><br><span class="line">        <span class="keyword">if</span> (!idStr.matches(<span class="string">&quot;\\d+&quot;</span>)) &#123;</span><br><span class="line">            <span class="keyword">return</span> Result.fail(<span class="string">&quot;店铺ID格式错误！&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line">        </span><br><span class="line">        <span class="type">Long</span> <span class="variable">id</span> <span class="operator">=</span> Long.valueOf(idStr);</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 2. ID范围校验（第二层防护）</span></span><br><span class="line">        <span class="keyword">if</span> (id &gt; <span class="number">1000000</span>) &#123;  <span class="comment">// 假设最大ID为100万</span></span><br><span class="line">            <span class="keyword">return</span> Result.fail(<span class="string">&quot;店铺ID超出范围！&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 3. 调用带缓存穿透防护的Service方法</span></span><br><span class="line">        <span class="keyword">return</span> shopService.queryShopById(id);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><p><img src="https://markpic.adoreorg.cn/2025/09/Redis/1653327124561.png" alt="1653327124561"></p><h4 id="2-6-3-总结与最佳实践">2.6.3 总结与最佳实践</h4><p><strong>缓存穿透产生原因：</strong></p><ul><li>用户请求的数据在缓存和数据库中都不存在</li><li>大量请求不断发起，给数据库带来巨大压力</li></ul><p><strong>解决方案完整清单：</strong></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>ID范围限制</td><td>⭐⭐</td><td>中级防护</td></tr><tr><td>缓存层</td><td>缓存空对象</td><td>⭐⭐⭐</td><td>核心防护</td></tr><tr><td>架构层</td><td>布隆过滤器</td><td>⭐⭐⭐⭐⭐</td><td>高级防护</td></tr><tr><td>系统层</td><td>限流熔断</td><td>⭐⭐⭐⭐</td><td>兜底防护</td></tr></tbody></table><p><strong>推荐组合：</strong></p><ul><li>中小型系统：参数校验 + 缓存空对象</li><li>大型系统：参数校验 + 布隆过滤器 + 缓存空对象</li><li>超高并发：全部方案组合使用</li></ul><h3 id="2-7-缓存雪崩问题解决">2.7 缓存雪崩问题解决</h3><h4 id="2-7-1-缓存雪崩问题分析">2.7.1 缓存雪崩问题分析</h4>    <div class="note note-warning">      <div class="note-header">        <i class="note-icon fa-regular fa-circle-dot"></i>        <span class="note-title">提示信息</span>      </div>      <div class="note-content">        <p><strong>缓存雪崩</strong>：在同一时段大量的缓存key同时失效或者Redis服务宕机，导致大量请求直接到达数据库，可能瞬间压垮数据库，造成系统级故障。</p>      </div>    </div>  <p><img src="https://markpic.adoreorg.cn/2025/09/Redis/1653327884526.png" alt="1653327884526"><br><strong>问题场景</strong>：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">用户请求 → 缓存失效 → 请求数据库 → 数据库压力过大 → 数据库崩溃 → 服务不可用</span><br></pre></td></tr></table></figure><p><strong>风险特征</strong>：</p><ul><li>突发性：大量key在同一时间失效</li><li>级联性：缓存失效→数据库压力→系统崩溃</li><li>恢复难：系统恢复后可能再次雪崩</li></ul><h4 id="2-7-2-解决方案对比">2.7.2 解决方案对比</h4><div class="tabs" id="cache-avalanche"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#cache-avalanche-1">过期时间随机化</button></li><li class="tab"><button type="button" data-href="#cache-avalanche-2">Redis集群高可用</button></li><li class="tab"><button type="button" data-href="#cache-avalanche-3">降级限流策略</button></li><li class="tab"><button type="button" data-href="#cache-avalanche-4">多级缓存架构</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="cache-avalanche-1"><p><strong>过期时间随机化</strong></p><p><strong>实现原理</strong>：<br>在基础TTL上添加随机值，分散key的过期时间</p><p><strong>代码实现</strong>：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 原TTL为30分钟</span></span><br><span class="line"><span class="type">int</span> <span class="variable">baseTtl</span> <span class="operator">=</span> <span class="number">30</span>;</span><br><span class="line"><span class="comment">// 添加随机值，范围±5分钟</span></span><br><span class="line"><span class="type">int</span> <span class="variable">randomTtl</span> <span class="operator">=</span> baseTtl + ThreadLocalRandom.current().nextInt(-<span class="number">5</span>, <span class="number">6</span>);</span><br><span class="line">stringRedisTemplate.opsForValue().set(key, value, randomTtl, TimeUnit.MINUTES);</span><br></pre></td></tr></table></figure><p><strong>优势</strong>：</p><ul><li>实现简单，成本低</li><li>能有效分散过期时间</li><li>对业务无侵入</li></ul><p><strong>劣势</strong>：</p><ul><li>无法应对Redis宕机</li><li>随机范围需要合理设置</li></ul><p><strong>适用场景</strong>：<br>适合大多数业务场景，作为基础防护手段</p><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="cache-avalanche-2"><p><strong>Redis集群高可用</strong></p><p><strong>实现原理</strong>：<br>通过主从复制、哨兵模式、集群模式提高Redis可用性</p><p><strong>架构对比</strong>：</p><table><thead><tr><th>方案</th><th>可用性</th><th>数据安全</th><th>复杂度</th><th>成本</th></tr></thead><tbody><tr><td>主从复制</td><td>★★☆</td><td>★★☆</td><td>★☆☆</td><td>低</td></tr><tr><td>哨兵模式</td><td>★★★</td><td>★★☆</td><td>★★☆</td><td>中</td></tr><tr><td>Redis Cluster</td><td>★★★</td><td>★★★</td><td>★★★</td><td>高</td></tr></tbody></table><p><strong>配置示例</strong>（哨兵模式）：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># sentinel.conf</span></span><br><span class="line">sentinel monitor mymaster 127.0.0.1 6379 2</span><br><span class="line">sentinel down-after-milliseconds mymaster 5000</span><br><span class="line">sentinel failover-timeout mymaster 10000</span><br></pre></td></tr></table></figure><p><strong>优势</strong>：</p><ul><li>提供故障自动转移</li><li>数据安全性高</li><li>服务可用性强</li></ul><p><strong>劣势</strong>：</p><ul><li>架构复杂，维护成本高</li><li>需要更多资源投入</li></ul><p><strong>适用场景</strong>：<br>对可用性要求高的核心业务</p><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="cache-avalanche-3"><p><strong>降级限流策略</strong></p><p><strong>实现原理</strong>：<br>在缓存失效时，通过降级和限流保护数据库</p><p><strong>降级策略</strong>：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@SentinelResource(value = &quot;getShop&quot;, </span></span><br><span class="line"><span class="meta">    blockHandler = &quot;handleBlock&quot;, </span></span><br><span class="line"><span class="meta">    fallback = &quot;handleFallback&quot;)</span></span><br><span class="line"><span class="keyword">public</span> Shop <span class="title function_">getShop</span><span class="params">(Long id)</span> &#123;</span><br><span class="line">    <span class="comment">// 正常业务逻辑</span></span><br><span class="line">    <span class="keyword">return</span> shopService.getById(id);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 限流处理</span></span><br><span class="line"><span class="keyword">public</span> Shop <span class="title function_">handleBlock</span><span class="params">(Long id, BlockException ex)</span> &#123;</span><br><span class="line">    log.warn(<span class="string">&quot;接口被限流: &#123;&#125;&quot;</span>, id);</span><br><span class="line">    <span class="keyword">return</span> getDefaultShop(id); <span class="comment">// 返回默认值或缓存数据</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 降级处理</span></span><br><span class="line"><span class="keyword">public</span> Shop <span class="title function_">handleFallback</span><span class="params">(Long id, Throwable ex)</span> &#123;</span><br><span class="line">    log.error(<span class="string">&quot;接口降级: &#123;&#125;&quot;</span>, id, ex);</span><br><span class="line">    <span class="keyword">return</span> getDefaultShop(id); <span class="comment">// 返回兜底数据</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>限流算法</strong>：</p><ul><li>令牌桶算法：平滑流量</li><li>漏桶算法：固定速率处理</li><li>计数器算法：简单直接</li></ul><p><strong>优势</strong>：</p><ul><li>保护系统稳定性</li><li>提供用户体验降级方案</li><li>灵活配置</li></ul><p><strong>劣势</strong>：</p><ul><li>可能损失部分功能</li><li>需要合理设置阈值</li></ul><p><strong>适用场景</strong>：<br>高并发、大数据量场景</p><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="cache-avalanche-4"><p><strong>多级缓存架构</strong></p><p><strong>架构设计</strong>：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">用户请求 → CDN缓存 → Nginx缓存 → 本地缓存 → Redis缓存 → 数据库</span><br></pre></td></tr></table></figure><p><strong>实现方案</strong>：</p><table><thead><tr><th>缓存层级</th><th>技术选型</th><th>响应时间</th><th>容量</th><th>成本</th></tr></thead><tbody><tr><td>CDN缓存</td><td>CloudFlare/阿里云CDN</td><td>10-50ms</td><td>巨大</td><td>高</td></tr><tr><td>Nginx缓存</td><td>proxy_cache</td><td>1-5ms</td><td>中等</td><td>低</td></tr><tr><td>本地缓存</td><td>Caffeine/Guava</td><td>0.1-1ms</td><td>小</td><td>极低</td></tr><tr><td>Redis缓存</td><td>Redis Cluster</td><td>1-10ms</td><td>大</td><td>中</td></tr></tbody></table><p><strong>本地缓存实现</strong>（Caffeine）：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 本地缓存配置</span></span><br><span class="line">LoadingCache&lt;String, Shop&gt; localCache = Caffeine.newBuilder()</span><br><span class="line">    .maximumSize(<span class="number">10000</span>)</span><br><span class="line">    .expireAfterWrite(<span class="number">5</span>, TimeUnit.MINUTES)</span><br><span class="line">    .build(key -&gt; &#123;</span><br><span class="line">        <span class="comment">// 缓存未命中时从Redis加载</span></span><br><span class="line">        <span class="keyword">return</span> getFromRedis(key);</span><br><span class="line">    &#125;);</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> Shop <span class="title function_">queryShop</span><span class="params">(Long id)</span> &#123;</span><br><span class="line">    <span class="comment">// 优先查询本地缓存</span></span><br><span class="line">    <span class="keyword">return</span> localCache.get(CACHE_SHOP_KEY + id);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>优势</strong>：</p><ul><li>多层次保护，容错性强</li><li>响应速度快，用户体验好</li><li>各层缓存互补</li></ul><p><strong>劣势</strong>：</p><ul><li>架构复杂，实现难度大</li><li>数据一致性挑战</li><li>运维成本高</li></ul><p><strong>适用场景</strong>：<br>大型互联网应用，对性能和可用性要求极高</p><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h4 id="2-7-3-最佳实践方案">2.7.3 最佳实践方案</h4>    <div class="note note-success">      <div class="note-header">        <i class="note-icon fa-regular fa-circle-check"></i>        <span class="note-title">提示信息</span>      </div>      <div class="note-content">        <p><strong>推荐方案</strong>：过期时间随机化 + Redis集群 + 本地缓存</p>      </div>    </div>  <p><strong>方案组合</strong>：</p><ol><li><strong>基础层</strong>：过期时间随机化（必做）</li><li><strong>可用层</strong>：Redis哨兵模式或Cluster（推荐）</li><li><strong>加速层</strong>：本地缓存（Caffeine/Guava）</li><li><strong>保护层</strong>：降级限流（Sentinel）</li></ol><p><strong>实施步骤</strong>：</p><ol><li>为所有缓存key添加随机TTL</li><li>部署Redis哨兵模式</li><li>集成本地缓存框架</li><li>配置限流降级规则</li><li>监控和告警设置</li></ol><p><strong>效果预期</strong>：</p><ul><li>缓存雪崩概率降低90%+</li><li>系统可用性达到99.9%+</li><li>响应时间提升50%+</li></ul><h3 id="2-8-缓存击穿问题解决">2.8 缓存击穿问题解决</h3><h4 id="2-8-1-缓存击穿问题分析">2.8.1 缓存击穿问题分析</h4>    <div class="note note-warning">      <div class="note-header">        <i class="note-icon fa-regular fa-circle-dot"></i>        <span class="note-title">提示信息</span>      </div>      <div class="note-content">        <p><strong>缓存击穿</strong>：也叫热点Key问题，一个被高并发访问并且缓存重建业务较复杂的key突然失效，无数请求瞬间到达数据库，可能造成数据库崩溃。</p>      </div>    </div>  <p><strong>问题场景</strong>：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">高并发请求 → 热点key失效 → 大量请求数据库 → 数据库压力激增 → 数据库崩溃</span><br></pre></td></tr></table></figure><p><strong>与缓存穿透的区别</strong>：</p><ul><li>缓存穿透：查询不存在的数据</li><li>缓存击穿：热点key突然失效</li><li>缓存雪崩：大量key同时失效</li></ul><h4 id="2-8-2-解决方案对比">2.8.2 解决方案对比</h4><div class="tabs" id="cache-breakdown"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#cache-breakdown-1">互斥锁方案</button></li><li class="tab"><button type="button" data-href="#cache-breakdown-2">逻辑过期方案</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="cache-breakdown-1"><p><strong>互斥锁方案</strong></p><p><strong>实现原理</strong>：<br>通过分布式锁保证只有一个线程去重建缓存，其他线程等待<br><img src="https://markpic.adoreorg.cn/2025/09/Redis/1653328288627.png" alt="1653328288627"><br><strong>流程图</strong>：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">请求1：获取锁 → 查询数据库 → 重建缓存 → 释放锁 → 返回数据</span><br><span class="line">请求2：等待锁 → 从缓存获取 → 返回数据</span><br><span class="line">请求3：等待锁 → 从缓存获取 → 返回数据</span><br></pre></td></tr></table></figure><p><strong>代码实现</strong>：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> Shop <span class="title function_">queryWithMutex</span><span class="params">(Long id)</span> &#123;</span><br><span class="line">    <span class="type">String</span> <span class="variable">key</span> <span class="operator">=</span> CACHE_SHOP_KEY + id;</span><br><span class="line">    <span class="type">String</span> <span class="variable">lockKey</span> <span class="operator">=</span> LOCK_SHOP_KEY + id;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 1. 查询缓存</span></span><br><span class="line">    <span class="type">String</span> <span class="variable">shopJson</span> <span class="operator">=</span> stringRedisTemplate.opsForValue().get(key);</span><br><span class="line">    <span class="keyword">if</span> (StrUtil.isNotBlank(shopJson)) &#123;</span><br><span class="line">        <span class="keyword">return</span> JSONUtil.toBean(shopJson, Shop.class);</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 2. 获取互斥锁</span></span><br><span class="line">    <span class="type">boolean</span> <span class="variable">isLock</span> <span class="operator">=</span> tryLock(lockKey);</span><br><span class="line">    <span class="keyword">if</span> (!isLock) &#123;</span><br><span class="line">        <span class="comment">// 3. 获取锁失败，休眠重试</span></span><br><span class="line">        Thread.sleep(<span class="number">50</span>);</span><br><span class="line">        <span class="keyword">return</span> queryWithMutex(id); <span class="comment">// 递归重试</span></span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">        <span class="comment">// 4. 获取锁成功，再次检查缓存（double check）</span></span><br><span class="line">        shopJson = stringRedisTemplate.opsForValue().get(key);</span><br><span class="line">        <span class="keyword">if</span> (StrUtil.isNotBlank(shopJson)) &#123;</span><br><span class="line">            <span class="keyword">return</span> JSONUtil.toBean(shopJson, Shop.class);</span><br><span class="line">        &#125;</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 5. 查询数据库并重建缓存</span></span><br><span class="line">        <span class="type">Shop</span> <span class="variable">shop</span> <span class="operator">=</span> getById(id);</span><br><span class="line">        <span class="keyword">if</span> (shop == <span class="literal">null</span>) &#123;</span><br><span class="line">            <span class="comment">// 防止缓存穿透</span></span><br><span class="line">            stringRedisTemplate.opsForValue().set(key, <span class="string">&quot;&quot;</span>, CACHE_NULL_TTL, TimeUnit.MINUTES);</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        </span><br><span class="line">        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);</span><br><span class="line">        <span class="keyword">return</span> shop;</span><br><span class="line">        </span><br><span class="line">    &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">        <span class="comment">// 6. 释放锁</span></span><br><span class="line">        unlock(lockKey);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>优势</strong>：</p><ul><li>数据一致性强</li><li>实现相对简单</li><li>无额外内存开销</li></ul><p><strong>劣势</strong>：</p><ul><li>性能损耗（串行化）</li><li>存在死锁风险</li><li>用户体验差（等待）</li></ul><p><strong>适用场景</strong>：<br>对数据一致性要求高的业务场景</p><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="cache-breakdown-2"><p><strong>逻辑过期方案</strong></p><p><strong>实现原理</strong>：<br>不设置Redis过期时间，在value中存储逻辑过期时间，异步重建缓存<br><img src="https://markpic.adoreorg.cn/2025/09/Redis/1653328663897.png" alt="1653328663897"><br><strong>流程图</strong>：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">请求1：发现过期 → 获取锁 → 返回旧数据 + 异步重建</span><br><span class="line">请求2：发现过期 → 等待锁 → 返回旧数据</span><br><span class="line">请求3：发现重建完成 → 返回新数据</span><br></pre></td></tr></table></figure><p><strong>数据结构设计</strong>：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Data</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">RedisData</span> &#123;</span><br><span class="line">    <span class="keyword">private</span> LocalDateTime expireTime;  <span class="comment">// 逻辑过期时间</span></span><br><span class="line">    <span class="keyword">private</span> Object data;                 <span class="comment">// 实际数据</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>代码实现</strong>：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">ExecutorService</span> <span class="variable">CACHE_REBUILD_EXECUTOR</span> <span class="operator">=</span> Executors.newFixedThreadPool(<span class="number">10</span>);</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> Shop <span class="title function_">queryWithLogicalExpire</span><span class="params">(Long id)</span> &#123;</span><br><span class="line">    <span class="type">String</span> <span class="variable">key</span> <span class="operator">=</span> CACHE_SHOP_KEY + id;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 1. 查询缓存</span></span><br><span class="line">    <span class="type">String</span> <span class="variable">json</span> <span class="operator">=</span> stringRedisTemplate.opsForValue().get(key);</span><br><span class="line">    <span class="keyword">if</span> (StrUtil.isBlank(json)) &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">null</span>; <span class="comment">// 缓存未命中直接返回</span></span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 2. 反序列化数据</span></span><br><span class="line">    <span class="type">RedisData</span> <span class="variable">redisData</span> <span class="operator">=</span> JSONUtil.toBean(json, RedisData.class);</span><br><span class="line">    <span class="type">Shop</span> <span class="variable">shop</span> <span class="operator">=</span> JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);</span><br><span class="line">    <span class="type">LocalDateTime</span> <span class="variable">expireTime</span> <span class="operator">=</span> redisData.getExpireTime();</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 3. 判断是否过期</span></span><br><span class="line">    <span class="keyword">if</span> (expireTime.isAfter(LocalDateTime.now())) &#123;</span><br><span class="line">        <span class="comment">// 3.1 未过期，直接返回</span></span><br><span class="line">        <span class="keyword">return</span> shop;</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 3.2 已过期，需要缓存重建</span></span><br><span class="line">    <span class="type">String</span> <span class="variable">lockKey</span> <span class="operator">=</span> LOCK_SHOP_KEY + id;</span><br><span class="line">    <span class="type">boolean</span> <span class="variable">isLock</span> <span class="operator">=</span> tryLock(lockKey);</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">if</span> (isLock) &#123;</span><br><span class="line">        <span class="comment">// 4. 获取锁成功，异步重建缓存</span></span><br><span class="line">        CACHE_REBUILD_EXECUTOR.submit(() -&gt; &#123;</span><br><span class="line">            <span class="keyword">try</span> &#123;</span><br><span class="line">                <span class="comment">// 重建缓存</span></span><br><span class="line">                saveShop2Redis(id, <span class="number">20L</span>); <span class="comment">// 20分钟逻辑过期时间</span></span><br><span class="line">            &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">                log.error(<span class="string">&quot;缓存重建失败&quot;</span>, e);</span><br><span class="line">            &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">                unlock(lockKey);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;);</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 5. 返回旧数据</span></span><br><span class="line">    <span class="keyword">return</span> shop;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title function_">saveShop2Redis</span><span class="params">(Long id, Long expireSeconds)</span> &#123;</span><br><span class="line">    <span class="comment">// 查询数据库</span></span><br><span class="line">    <span class="type">Shop</span> <span class="variable">shop</span> <span class="operator">=</span> getById(id);</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 封装逻辑过期时间</span></span><br><span class="line">    <span class="type">RedisData</span> <span class="variable">redisData</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">RedisData</span>();</span><br><span class="line">    redisData.setData(shop);</span><br><span class="line">    redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 写入Redis（不设TTL）</span></span><br><span class="line">    stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>优势</strong>：</p><ul><li>性能优秀（无需等待）</li><li>用户体验好（立即响应）</li><li>无死锁风险</li></ul><p><strong>劣势</strong>：</p><ul><li>数据一致性弱（可能返回脏数据）</li><li>实现复杂</li><li>占用额外内存</li></ul><p><strong>适用场景</strong>：<br>对性能要求高，能容忍短暂数据不一致的场景</p><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h4 id="2-8-3-方案选择建议">2.8.3 方案选择建议</h4>    <div class="note note-success">      <div class="note-header">        <i class="note-icon fa-regular fa-circle-check"></i>        <span class="note-title">提示信息</span>      </div>      <div class="note-content">        <p><strong>选择建议</strong>：根据业务场景和数据一致性要求选择合适的方案</p>      </div>    </div>  <p><strong>对比分析</strong>：</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><tr><td>实现复杂度</td><td>★★☆☆☆</td><td>★★★★☆</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><p><strong>业务场景推荐</strong>：</p><ul><li><strong>金融支付</strong>：互斥锁方案（强一致性）</li><li><strong>商品详情</strong>：逻辑过期方案（高性能）</li><li><strong>用户资料</strong>：互斥锁方案（数据重要）</li><li><strong>新闻资讯</strong>：逻辑过期方案（容忍延迟）</li></ul><p><strong>混合策略</strong>：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> Shop <span class="title function_">queryShop</span><span class="params">(Long id, <span class="type">boolean</span> requireConsistency)</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (requireConsistency) &#123;</span><br><span class="line">        <span class="keyword">return</span> queryWithMutex(id);      <span class="comment">// 强一致性场景</span></span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> queryWithLogicalExpire(id); <span class="comment">// 高性能场景</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="2-9-互斥锁方案实现">2.9 互斥锁方案实现</h3><h4 id="2-9-1-实现思路">2.9.1 实现思路</h4>    <div class="note note-primary">      <div class="note-header">        <i class="note-icon fa-regular fa-star"></i>        <span class="note-title">提示信息</span>      </div>      <div class="note-content">        <p><strong>核心思路</strong>：在缓存未命中时，通过分布式锁保证只有一个线程去查询数据库并重建缓存，其他线程等待锁释放后重试，避免大量请求同时打到数据库。</p>      </div>    </div>  <p><strong>实现流程</strong>：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">查询缓存 → 缓存未命中 → 获取互斥锁 → 再次检查缓存 → 查询数据库 → 重建缓存 → 释放锁</span><br></pre></td></tr></table></figure><p><strong>设计要点</strong>：</p><ul><li><strong>双重检查</strong>：获取锁后再次检查缓存，防止重复查询数据库</li><li><strong>锁超时</strong>：设置合理的锁过期时间，防止死锁</li><li><strong>异常处理</strong>：确保锁最终能被释放</li><li><strong>重试机制</strong>：获取锁失败时适当休眠后重试</li></ul><h4 id="2-9-2-代码实现">2.9.2 代码实现</h4><div class="tabs" id="mutex-implementation"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#mutex-implementation-1">工具类封装</button></li><li class="tab"><button type="button" data-href="#mutex-implementation-2">Service层实现</button></li><li class="tab"><button type="button" data-href="#mutex-implementation-3">Controller层调用</button></li><li class="tab"><button type="button" data-href="#mutex-implementation-4">测试验证</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="mutex-implementation-1"><p><strong>分布式锁工具类</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">RedisLockUtil</span> &#123;</span><br><span class="line">    </span><br><span class="line">    <span class="meta">@Resource</span></span><br><span class="line">    <span class="keyword">private</span> StringRedisTemplate stringRedisTemplate;</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">String</span> <span class="variable">LOCK_PREFIX</span> <span class="operator">=</span> <span class="string">&quot;lock:&quot;</span>;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">long</span> <span class="variable">DEFAULT_TIMEOUT</span> <span class="operator">=</span> <span class="number">10</span>; <span class="comment">// 默认锁超时时间（秒）</span></span><br><span class="line">    </span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 尝试获取分布式锁</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">public</span> <span class="type">boolean</span> <span class="title function_">tryLock</span><span class="params">(String key, <span class="type">long</span> timeout)</span> &#123;</span><br><span class="line">        <span class="type">String</span> <span class="variable">lockKey</span> <span class="operator">=</span> LOCK_PREFIX + key;</span><br><span class="line">        <span class="type">Boolean</span> <span class="variable">flag</span> <span class="operator">=</span> stringRedisTemplate.opsForValue()</span><br><span class="line">            .setIfAbsent(lockKey, Thread.currentThread().getId() + <span class="string">&quot;&quot;</span>, timeout, TimeUnit.SECONDS);</span><br><span class="line">        <span class="keyword">return</span> BooleanUtil.isTrue(flag);</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 释放分布式锁（安全释放）</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">public</span> <span class="type">boolean</span> <span class="title function_">unlock</span><span class="params">(String key)</span> &#123;</span><br><span class="line">        <span class="type">String</span> <span class="variable">lockKey</span> <span class="operator">=</span> LOCK_PREFIX + key;</span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            <span class="comment">// 获取当前线程ID</span></span><br><span class="line">            <span class="type">String</span> <span class="variable">threadId</span> <span class="operator">=</span> Thread.currentThread().getId() + <span class="string">&quot;&quot;</span>;</span><br><span class="line">            <span class="type">String</span> <span class="variable">value</span> <span class="operator">=</span> stringRedisTemplate.opsForValue().get(lockKey);</span><br><span class="line">            </span><br><span class="line">            <span class="comment">// 确保释放的是自己持有的锁</span></span><br><span class="line">            <span class="keyword">if</span> (threadId.equals(value)) &#123;</span><br><span class="line">                <span class="keyword">return</span> BooleanUtil.isTrue(stringRedisTemplate.delete(lockKey));</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">        &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">            log.error(<span class="string">&quot;释放锁失败: &#123;&#125;&quot;</span>, lockKey, e);</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 尝试获取锁并重试</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">public</span> <span class="type">boolean</span> <span class="title function_">tryLockWithRetry</span><span class="params">(String key, <span class="type">long</span> timeout, <span class="type">int</span> maxRetry, <span class="type">long</span> retryInterval)</span> &#123;</span><br><span class="line">        <span class="keyword">for</span> (<span class="type">int</span> <span class="variable">i</span> <span class="operator">=</span> <span class="number">0</span>; i &lt; maxRetry; i++) &#123;</span><br><span class="line">            <span class="keyword">if</span> (tryLock(key, timeout)) &#123;</span><br><span class="line">                <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">if</span> (i &lt; maxRetry - <span class="number">1</span>) &#123;</span><br><span class="line">                <span class="keyword">try</span> &#123;</span><br><span class="line">                    Thread.sleep(retryInterval);</span><br><span class="line">                &#125; <span class="keyword">catch</span> (InterruptedException e) &#123;</span><br><span class="line">                    Thread.currentThread().interrupt();</span><br><span class="line">                    <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="mutex-implementation-2"><p><strong>Service层完整实现</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Service</span></span><br><span class="line"><span class="meta">@Slf4j</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">ShopServiceImpl</span> <span class="keyword">extends</span> <span class="title class_">ServiceImpl</span>&lt;ShopMapper, Shop&gt; <span class="keyword">implements</span> <span class="title class_">IShopService</span> &#123;</span><br><span class="line">    </span><br><span class="line">    <span class="meta">@Resource</span></span><br><span class="line">    <span class="keyword">private</span> StringRedisTemplate stringRedisTemplate;</span><br><span class="line">    </span><br><span class="line">    <span class="meta">@Resource</span></span><br><span class="line">    <span class="keyword">private</span> RedisLockUtil redisLockUtil;</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">String</span> <span class="variable">CACHE_SHOP_KEY</span> <span class="operator">=</span> <span class="string">&quot;cache:shop:&quot;</span>;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">String</span> <span class="variable">LOCK_SHOP_KEY</span> <span class="operator">=</span> <span class="string">&quot;lock:shop:&quot;</span>;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">Long</span> <span class="variable">CACHE_SHOP_TTL</span> <span class="operator">=</span> <span class="number">30L</span>;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">Long</span> <span class="variable">CACHE_NULL_TTL</span> <span class="operator">=</span> <span class="number">2L</span>;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 使用互斥锁解决缓存击穿</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> Shop <span class="title function_">queryWithMutex</span><span class="params">(Long id)</span> &#123;</span><br><span class="line">        <span class="type">String</span> <span class="variable">key</span> <span class="operator">=</span> CACHE_SHOP_KEY + id;</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 1. 从Redis查询缓存</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">shopJson</span> <span class="operator">=</span> stringRedisTemplate.opsForValue().get(key);</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 2. 缓存命中且不为空值</span></span><br><span class="line">        <span class="keyword">if</span> (StrUtil.isNotBlank(shopJson)) &#123;</span><br><span class="line">            log.debug(<span class="string">&quot;缓存命中: &#123;&#125;&quot;</span>, key);</span><br><span class="line">            <span class="keyword">return</span> JSONUtil.toBean(shopJson, Shop.class);</span><br><span class="line">        &#125;</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 3. 判断是否为缓存穿透的空值</span></span><br><span class="line">        <span class="keyword">if</span> (shopJson != <span class="literal">null</span>) &#123;</span><br><span class="line">            log.debug(<span class="string">&quot;命中空值缓存: &#123;&#125;&quot;</span>, key);</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 4. 缓存未命中，尝试获取互斥锁</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">lockKey</span> <span class="operator">=</span> LOCK_SHOP_KEY + id;</span><br><span class="line">        <span class="type">Shop</span> <span class="variable">shop</span> <span class="operator">=</span> <span class="literal">null</span>;</span><br><span class="line">        </span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            <span class="comment">// 4.1 尝试获取锁（最多重试3次，间隔50ms）</span></span><br><span class="line">            <span class="type">boolean</span> <span class="variable">isLock</span> <span class="operator">=</span> redisLockUtil.tryLockWithRetry(lockKey, <span class="number">10L</span>, <span class="number">3</span>, <span class="number">50L</span>);</span><br><span class="line">            </span><br><span class="line">            <span class="keyword">if</span> (!isLock) &#123;</span><br><span class="line">                log.warn(<span class="string">&quot;获取锁失败，返回兜底数据: &#123;&#125;&quot;</span>, lockKey);</span><br><span class="line">                <span class="keyword">return</span> getDefaultShop(id); <span class="comment">// 返回兜底数据</span></span><br><span class="line">            &#125;</span><br><span class="line">            </span><br><span class="line">            log.debug(<span class="string">&quot;获取锁成功: &#123;&#125;&quot;</span>, lockKey);</span><br><span class="line">            </span><br><span class="line">            <span class="comment">// 4.2 双重检查（防止重复查询数据库）</span></span><br><span class="line">            shopJson = stringRedisTemplate.opsForValue().get(key);</span><br><span class="line">            <span class="keyword">if</span> (StrUtil.isNotBlank(shopJson)) &#123;</span><br><span class="line">                log.debug(<span class="string">&quot;双重检查命中缓存: &#123;&#125;&quot;</span>, key);</span><br><span class="line">                <span class="keyword">return</span> JSONUtil.toBean(shopJson, Shop.class);</span><br><span class="line">            &#125;</span><br><span class="line">            </span><br><span class="line">            <span class="comment">// 4.3 查询数据库</span></span><br><span class="line">            log.debug(<span class="string">&quot;查询数据库: &#123;&#125;&quot;</span>, id);</span><br><span class="line">            shop = getById(id);</span><br><span class="line">            </span><br><span class="line">            <span class="comment">// 5. 数据库中不存在（缓存穿透处理）</span></span><br><span class="line">            <span class="keyword">if</span> (shop == <span class="literal">null</span>) &#123;</span><br><span class="line">                log.debug(<span class="string">&quot;数据不存在，写入空值缓存: &#123;&#125;&quot;</span>, key);</span><br><span class="line">                stringRedisTemplate.opsForValue().set(key, <span class="string">&quot;&quot;</span>, CACHE_NULL_TTL, TimeUnit.MINUTES);</span><br><span class="line">                <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line">            &#125;</span><br><span class="line">            </span><br><span class="line">            <span class="comment">// 6. 写入缓存</span></span><br><span class="line">            log.debug(<span class="string">&quot;写入缓存: &#123;&#125;&quot;</span>, key);</span><br><span class="line">            stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);</span><br><span class="line">            </span><br><span class="line">        &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">            log.error(<span class="string">&quot;查询商户异常: &#123;&#125;&quot;</span>, id, e);</span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">RuntimeException</span>(<span class="string">&quot;查询商户失败&quot;</span>, e);</span><br><span class="line">        &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">            <span class="comment">// 7. 释放锁</span></span><br><span class="line">            redisLockUtil.unlock(lockKey);</span><br><span class="line">            log.debug(<span class="string">&quot;释放锁: &#123;&#125;&quot;</span>, lockKey);</span><br><span class="line">        &#125;</span><br><span class="line">        </span><br><span class="line">        <span class="keyword">return</span> shop;</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 兜底数据获取</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> Shop <span class="title function_">getDefaultShop</span><span class="params">(Long id)</span> &#123;</span><br><span class="line">        <span class="comment">// 可以返回默认数据或从备用数据源获取</span></span><br><span class="line">        <span class="type">Shop</span> <span class="variable">defaultShop</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Shop</span>();</span><br><span class="line">        defaultShop.setId(id);</span><br><span class="line">        defaultShop.setName(<span class="string">&quot;默认商户&quot;</span>);</span><br><span class="line">        defaultShop.setTypeId(<span class="number">1L</span>);</span><br><span class="line">        <span class="comment">// 设置其他默认值...</span></span><br><span class="line">        <span class="keyword">return</span> defaultShop;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="mutex-implementation-3"><p><strong>Controller层调用</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@RestController</span></span><br><span class="line"><span class="meta">@RequestMapping(&quot;/shop&quot;)</span></span><br><span class="line"><span class="meta">@Api(tags = &quot;商户管理&quot;)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">ShopController</span> &#123;</span><br><span class="line">    </span><br><span class="line">    <span class="meta">@Resource</span></span><br><span class="line">    <span class="keyword">private</span> IShopService shopService;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 根据ID查询商户（互斥锁方案）</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@GetMapping(&quot;/&#123;id&#125;&quot;)</span></span><br><span class="line">    <span class="meta">@ApiOperation(&quot;根据ID查询商户&quot;)</span></span><br><span class="line">    <span class="keyword">public</span> Result <span class="title function_">queryShop</span><span class="params">(<span class="meta">@PathVariable(&quot;id&quot;)</span> Long id)</span> &#123;</span><br><span class="line">        <span class="comment">// 参数校验</span></span><br><span class="line">        <span class="keyword">if</span> (id == <span class="literal">null</span> || id &lt;= <span class="number">0</span>) &#123;</span><br><span class="line">            <span class="keyword">return</span> Result.fail(<span class="string">&quot;商户ID不能为空&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line">        </span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            <span class="comment">// 使用互斥锁查询</span></span><br><span class="line">            <span class="type">Shop</span> <span class="variable">shop</span> <span class="operator">=</span> shopService.queryWithMutex(id);</span><br><span class="line">            </span><br><span class="line">            <span class="keyword">if</span> (shop == <span class="literal">null</span>) &#123;</span><br><span class="line">                <span class="keyword">return</span> Result.fail(<span class="string">&quot;商户不存在&quot;</span>);</span><br><span class="line">            &#125;</span><br><span class="line">            </span><br><span class="line">            <span class="keyword">return</span> Result.ok(shop);</span><br><span class="line">            </span><br><span class="line">        &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">            log.error(<span class="string">&quot;查询商户失败: &#123;&#125;&quot;</span>, id, e);</span><br><span class="line">            <span class="keyword">return</span> Result.fail(<span class="string">&quot;查询商户失败，请稍后重试&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="mutex-implementation-4"><p><strong>并发测试验证</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@SpringBootTest</span></span><br><span class="line"><span class="meta">@Slf4j</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">CacheBreakdownTest</span> &#123;</span><br><span class="line">    </span><br><span class="line">    <span class="meta">@Resource</span></span><br><span class="line">    <span class="keyword">private</span> IShopService shopService;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 缓存击穿并发测试</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@Test</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">testCacheBreakdown</span><span class="params">()</span> <span class="keyword">throws</span> InterruptedException &#123;</span><br><span class="line">        <span class="type">Long</span> <span class="variable">shopId</span> <span class="operator">=</span> <span class="number">1L</span>;</span><br><span class="line">        <span class="type">int</span> <span class="variable">threadCount</span> <span class="operator">=</span> <span class="number">100</span>; <span class="comment">// 并发线程数</span></span><br><span class="line">        <span class="type">CountDownLatch</span> <span class="variable">latch</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">CountDownLatch</span>(threadCount);</span><br><span class="line">        <span class="type">AtomicInteger</span> <span class="variable">successCount</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">AtomicInteger</span>(<span class="number">0</span>);</span><br><span class="line">        <span class="type">AtomicInteger</span> <span class="variable">dbQueryCount</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">AtomicInteger</span>(<span class="number">0</span>);</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 先清除缓存，模拟缓存失效场景</span></span><br><span class="line">        <span class="type">StringRedisTemplate</span> <span class="variable">stringRedisTemplate</span> <span class="operator">=</span> </span><br><span class="line">            SpringContextUtil.getBean(StringRedisTemplate.class);</span><br><span class="line">        stringRedisTemplate.delete(<span class="string">&quot;cache:shop:&quot;</span> + shopId);</span><br><span class="line">        </span><br><span class="line">        log.info(<span class="string">&quot;开始并发测试，线程数：&#123;&#125;，商户ID：&#123;&#125;&quot;</span>, threadCount, shopId);</span><br><span class="line">        </span><br><span class="line">        <span class="type">long</span> <span class="variable">startTime</span> <span class="operator">=</span> System.currentTimeMillis();</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 并发查询</span></span><br><span class="line">        <span class="keyword">for</span> (<span class="type">int</span> <span class="variable">i</span> <span class="operator">=</span> <span class="number">0</span>; i &lt; threadCount; i++) &#123;</span><br><span class="line">            <span class="keyword">new</span> <span class="title class_">Thread</span>(() -&gt; &#123;</span><br><span class="line">                <span class="keyword">try</span> &#123;</span><br><span class="line">                    <span class="type">Shop</span> <span class="variable">shop</span> <span class="operator">=</span> shopService.queryWithMutex(shopId);</span><br><span class="line">                    <span class="keyword">if</span> (shop != <span class="literal">null</span>) &#123;</span><br><span class="line">                        successCount.incrementAndGet();</span><br><span class="line">                    &#125;</span><br><span class="line">                &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">                    log.error(<span class="string">&quot;查询异常&quot;</span>, e);</span><br><span class="line">                &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">                    latch.countDown();</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;).start();</span><br><span class="line">        &#125;</span><br><span class="line">        </span><br><span class="line">        latch.await();</span><br><span class="line">        <span class="type">long</span> <span class="variable">endTime</span> <span class="operator">=</span> System.currentTimeMillis();</span><br><span class="line">        </span><br><span class="line">        log.info(<span class="string">&quot;并发测试完成&quot;</span>);</span><br><span class="line">        log.info(<span class="string">&quot;总耗时：&#123;&#125;ms&quot;</span>, (endTime - startTime));</span><br><span class="line">        log.info(<span class="string">&quot;成功查询数：&#123;&#125;&quot;</span>, successCount.get());</span><br><span class="line">        log.info(<span class="string">&quot;QPS：&#123;&#125;&quot;</span>, threadCount * <span class="number">1000</span> / (endTime - startTime));</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 验证缓存是否已重建</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">cachedData</span> <span class="operator">=</span> stringRedisTemplate.opsForValue().get(<span class="string">&quot;cache:shop:&quot;</span> + shopId);</span><br><span class="line">        Assertions.assertNotNull(cachedData, <span class="string">&quot;缓存应该已重建&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h4 id="2-9-3-性能对比与最佳实践">2.9.3 性能对比与最佳实践</h4>    <div class="note note-success">      <div class="note-header">        <i class="note-icon fa-regular fa-circle-check"></i>        <span class="note-title">提示信息</span>      </div>      <div class="note-content">        <p><strong>性能表现</strong>：互斥锁方案在并发测试中表现优异，能有效防止缓存击穿问题</p>      </div>    </div>  <p><strong>测试对比</strong>：</p><table><thead><tr><th>方案</th><th>并发数</th><th>数据库查询次数</th><th>平均响应时间</th><th>数据一致性</th></tr></thead><tbody><tr><td>无保护</td><td>100</td><td>100次</td><td>50ms</td><td>不一致</td></tr><tr><td>互斥锁</td><td>100</td><td>1次</td><td>80ms</td><td>强一致</td></tr><tr><td>逻辑过期</td><td>100</td><td>1次</td><td>10ms</td><td>最终一致</td></tr></tbody></table><p><strong>最佳实践</strong>：</p><ol><li><strong>锁粒度控制</strong>：按业务维度加锁，避免全局锁</li><li><strong>超时设置</strong>：锁超时时间要大于数据库查询时间</li><li><strong>重试机制</strong>：获取锁失败时要有重试和兜底策略</li><li><strong>监控告警</strong>：监控锁等待时间和获取成功率</li><li><strong>性能优化</strong>：结合本地缓存减少Redis访问</li></ol><h3 id="2-10-逻辑过期方案实现">2.10 逻辑过期方案实现</h3><h4 id="2-10-1-实现思路">2.10.1 实现思路</h4>    <div class="note note-primary">      <div class="note-header">        <i class="note-icon fa-regular fa-star"></i>        <span class="note-title">提示信息</span>      </div>      <div class="note-content">        <p><strong>核心思路</strong>：不设置Redis过期时间，在value中存储逻辑过期时间，当数据过期时异步重建缓存，立即返回旧数据，保证用户体验。</p>      </div>    </div>  <p><strong>实现流程</strong>：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">查询缓存 → 命中数据 → 检查逻辑过期时间 → 未过期直接返回 → 过期则异步重建 → 返回旧数据</span><br></pre></td></tr></table></figure><p><strong>设计要点</strong>：</p><ul><li><strong>异步重建</strong>：开启独立线程进行缓存重建，不阻塞用户请求</li><li><strong>逻辑过期</strong>：在value中存储过期时间，不依赖Redis TTL</li><li><strong>锁控制</strong>：通过分布式锁保证只有一个线程进行重建</li><li><strong>线程池管理</strong>：使用线程池管理异步任务，避免线程爆炸</li></ul><h4 id="2-10-2-代码实现">2.10.2 代码实现</h4><div class="tabs" id="logical-expire-implementation"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#logical-expire-implementation-1">数据模型设计</button></li><li class="tab"><button type="button" data-href="#logical-expire-implementation-2">缓存预热工具</button></li><li class="tab"><button type="button" data-href="#logical-expire-implementation-3">Service层实现</button></li><li class="tab"><button type="button" data-href="#logical-expire-implementation-4">测试验证</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="logical-expire-implementation-1"><p><strong>逻辑过期数据模型</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * Redis逻辑过期数据结构</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="meta">@Data</span></span><br><span class="line"><span class="meta">@NoArgsConstructor</span></span><br><span class="line"><span class="meta">@AllArgsConstructor</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">RedisData</span> &#123;</span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 逻辑过期时间</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> LocalDateTime expireTime;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 实际业务数据</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> Object data;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 快速构建方法</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> RedisData <span class="title function_">of</span><span class="params">(Object data, Long expireSeconds)</span> &#123;</span><br><span class="line">        <span class="type">RedisData</span> <span class="variable">redisData</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">RedisData</span>();</span><br><span class="line">        redisData.setData(data);</span><br><span class="line">        redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));</span><br><span class="line">        <span class="keyword">return</span> redisData;</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 判断是否过期</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">public</span> <span class="type">boolean</span> <span class="title function_">isExpired</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> expireTime.isBefore(LocalDateTime.now());</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="logical-expire-implementation-2"><p><strong>缓存预热工具类</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="meta">@Slf4j</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">CachePreheatUtil</span> &#123;</span><br><span class="line">    </span><br><span class="line">    <span class="meta">@Resource</span></span><br><span class="line">    <span class="keyword">private</span> StringRedisTemplate stringRedisTemplate;</span><br><span class="line">    </span><br><span class="line">    <span class="meta">@Resource</span></span><br><span class="line">    <span class="keyword">private</span> IShopService shopService;</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">String</span> <span class="variable">CACHE_SHOP_KEY</span> <span class="operator">=</span> <span class="string">&quot;cache:shop:&quot;</span>;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">Long</span> <span class="variable">LOGICAL_EXPIRE_TIME</span> <span class="operator">=</span> <span class="number">20L</span>; <span class="comment">// 逻辑过期时间（秒）</span></span><br><span class="line">    </span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 预热指定商户数据</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">preheatShop</span><span class="params">(Long shopId, Long expireSeconds)</span> &#123;</span><br><span class="line">        <span class="type">String</span> <span class="variable">key</span> <span class="operator">=</span> CACHE_SHOP_KEY + shopId;</span><br><span class="line">        </span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            <span class="comment">// 查询数据库</span></span><br><span class="line">            <span class="type">Shop</span> <span class="variable">shop</span> <span class="operator">=</span> shopService.getById(shopId);</span><br><span class="line">            <span class="keyword">if</span> (shop == <span class="literal">null</span>) &#123;</span><br><span class="line">                log.warn(<span class="string">&quot;商户不存在，跳过预热: &#123;&#125;&quot;</span>, shopId);</span><br><span class="line">                <span class="keyword">return</span>;</span><br><span class="line">            &#125;</span><br><span class="line">            </span><br><span class="line">            <span class="comment">// 封装逻辑过期数据</span></span><br><span class="line">            <span class="type">RedisData</span> <span class="variable">redisData</span> <span class="operator">=</span> RedisData.of(shop, expireSeconds);</span><br><span class="line">            </span><br><span class="line">            <span class="comment">// 写入Redis（不设TTL）</span></span><br><span class="line">            stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));</span><br><span class="line">            </span><br><span class="line">            log.info(<span class="string">&quot;缓存预热成功: &#123;&#125;, 过期时间: &#123;&#125;秒&quot;</span>, key, expireSeconds);</span><br><span class="line">            </span><br><span class="line">        &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">            log.error(<span class="string">&quot;缓存预热失败: &#123;&#125;&quot;</span>, shopId, e);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 批量预热热门商户</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">preheatHotShops</span><span class="params">(List&lt;Long&gt; shopIds, Long expireSeconds)</span> &#123;</span><br><span class="line">        log.info(<span class="string">&quot;开始批量预热热门商户，数量: &#123;&#125;&quot;</span>, shopIds.size());</span><br><span class="line">        </span><br><span class="line">        <span class="keyword">for</span> (Long shopId : shopIds) &#123;</span><br><span class="line">            <span class="keyword">try</span> &#123;</span><br><span class="line">                preheatShop(shopId, expireSeconds);</span><br><span class="line">                <span class="comment">// 适当休眠，避免对数据库造成压力</span></span><br><span class="line">                Thread.sleep(<span class="number">100</span>);</span><br><span class="line">            &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">                log.error(<span class="string">&quot;预热商户失败: &#123;&#125;&quot;</span>, shopId, e);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        </span><br><span class="line">        log.info(<span class="string">&quot;批量预热完成&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="logical-expire-implementation-3"><p><strong>Service层完整实现</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Service</span></span><br><span class="line"><span class="meta">@Slf4j</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">ShopServiceImpl</span> <span class="keyword">extends</span> <span class="title class_">ServiceImpl</span>&lt;ShopMapper, Shop&gt; <span class="keyword">implements</span> <span class="title class_">IShopService</span> &#123;</span><br><span class="line">    </span><br><span class="line">    <span class="meta">@Resource</span></span><br><span class="line">    <span class="keyword">private</span> StringRedisTemplate stringRedisTemplate;</span><br><span class="line">    </span><br><span class="line">    <span class="meta">@Resource</span></span><br><span class="line">    <span class="keyword">private</span> RedisLockUtil redisLockUtil;</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">String</span> <span class="variable">CACHE_SHOP_KEY</span> <span class="operator">=</span> <span class="string">&quot;cache:shop:&quot;</span>;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">String</span> <span class="variable">LOCK_SHOP_KEY</span> <span class="operator">=</span> <span class="string">&quot;lock:shop:&quot;</span>;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">Long</span> <span class="variable">LOGICAL_EXPIRE_TIME</span> <span class="operator">=</span> <span class="number">20L</span>; <span class="comment">// 逻辑过期时间（秒）</span></span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 缓存重建线程池</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">ExecutorService</span> <span class="variable">CACHE_REBUILD_EXECUTOR</span> <span class="operator">=</span> Executors.newFixedThreadPool(</span><br><span class="line">        Runtime.getRuntime().availableProcessors(),</span><br><span class="line">        r -&gt; &#123;</span><br><span class="line">            <span class="type">Thread</span> <span class="variable">thread</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Thread</span>(r);</span><br><span class="line">            thread.setName(<span class="string">&quot;cache-rebuild-&quot;</span> + thread.getId());</span><br><span class="line">            thread.setDaemon(<span class="literal">true</span>);</span><br><span class="line">            <span class="keyword">return</span> thread;</span><br><span class="line">        &#125;</span><br><span class="line">    );</span><br><span class="line">    </span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 逻辑过期方案解决缓存击穿</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> Shop <span class="title function_">queryWithLogicalExpire</span><span class="params">(Long id)</span> &#123;</span><br><span class="line">        <span class="type">String</span> <span class="variable">key</span> <span class="operator">=</span> CACHE_SHOP_KEY + id;</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 1. 从Redis查询缓存</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">json</span> <span class="operator">=</span> stringRedisTemplate.opsForValue().get(key);</span><br><span class="line">        <span class="keyword">if</span> (StrUtil.isBlank(json)) &#123;</span><br><span class="line">            log.debug(<span class="string">&quot;缓存未命中，直接返回null: &#123;&#125;&quot;</span>, key);</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 2. 命中缓存，反序列化数据</span></span><br><span class="line">        <span class="type">RedisData</span> <span class="variable">redisData</span> <span class="operator">=</span> JSONUtil.toBean(json, RedisData.class);</span><br><span class="line">        <span class="type">Shop</span> <span class="variable">shop</span> <span class="operator">=</span> JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 3. 检查是否过期</span></span><br><span class="line">        <span class="keyword">if</span> (!redisData.isExpired()) &#123;</span><br><span class="line">            log.debug(<span class="string">&quot;缓存未过期，直接返回: &#123;&#125;&quot;</span>, key);</span><br><span class="line">            <span class="keyword">return</span> shop;</span><br><span class="line">        &#125;</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 4. 缓存已过期，需要重建</span></span><br><span class="line">        log.debug(<span class="string">&quot;缓存已过期，开始异步重建: &#123;&#125;&quot;</span>, key);</span><br><span class="line">        <span class="type">String</span> <span class="variable">lockKey</span> <span class="operator">=</span> LOCK_SHOP_KEY + id;</span><br><span class="line">        <span class="type">boolean</span> <span class="variable">isLock</span> <span class="operator">=</span> redisLockUtil.tryLock(lockKey, <span class="number">10L</span>);</span><br><span class="line">        </span><br><span class="line">        <span class="keyword">if</span> (isLock) &#123;</span><br><span class="line">            <span class="comment">// 5. 获取锁成功，提交异步重建任务</span></span><br><span class="line">            CACHE_REBUILD_EXECUTOR.submit(() -&gt; &#123;</span><br><span class="line">                <span class="keyword">try</span> &#123;</span><br><span class="line">                    log.info(<span class="string">&quot;开始异步重建缓存: &#123;&#125;&quot;</span>, key);</span><br><span class="line">                    saveShop2Redis(id, LOGICAL_EXPIRE_TIME);</span><br><span class="line">                    log.info(<span class="string">&quot;异步重建缓存完成: &#123;&#125;&quot;</span>, key);</span><br><span class="line">                &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">                    log.error(<span class="string">&quot;异步重建缓存失败: &#123;&#125;&quot;</span>, key, e);</span><br><span class="line">                &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">                    redisLockUtil.unlock(lockKey);</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;);</span><br><span class="line">        &#125;</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 6. 返回旧数据（无论是否获取到锁）</span></span><br><span class="line">        log.debug(<span class="string">&quot;返回旧数据: &#123;&#125;&quot;</span>, key);</span><br><span class="line">        <span class="keyword">return</span> shop;</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 保存商户到Redis（逻辑过期）</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">saveShop2Redis</span><span class="params">(Long id, Long expireSeconds)</span> &#123;</span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            <span class="comment">// 查询数据库</span></span><br><span class="line">            <span class="type">Shop</span> <span class="variable">shop</span> <span class="operator">=</span> getById(id);</span><br><span class="line">            <span class="keyword">if</span> (shop == <span class="literal">null</span>) &#123;</span><br><span class="line">                log.warn(<span class="string">&quot;商户不存在，跳过缓存重建: &#123;&#125;&quot;</span>, id);</span><br><span class="line">                <span class="keyword">return</span>;</span><br><span class="line">            &#125;</span><br><span class="line">            </span><br><span class="line">            <span class="comment">// 封装逻辑过期数据</span></span><br><span class="line">            <span class="type">RedisData</span> <span class="variable">redisData</span> <span class="operator">=</span> RedisData.of(shop, expireSeconds);</span><br><span class="line">            </span><br><span class="line">            <span class="comment">// 写入Redis（不设TTL）</span></span><br><span class="line">            <span class="type">String</span> <span class="variable">key</span> <span class="operator">=</span> CACHE_SHOP_KEY + id;</span><br><span class="line">            stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));</span><br><span class="line">            </span><br><span class="line">            log.debug(<span class="string">&quot;逻辑过期缓存重建成功: &#123;&#125;, 过期时间: &#123;&#125;秒&quot;</span>, key, expireSeconds);</span><br><span class="line">            </span><br><span class="line">        &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">            log.error(<span class="string">&quot;逻辑过期缓存重建失败: &#123;&#125;&quot;</span>, id, e);</span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">RuntimeException</span>(<span class="string">&quot;缓存重建失败&quot;</span>, e);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="logical-expire-implementation-4"><p><strong>并发测试验证</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@SpringBootTest</span></span><br><span class="line"><span class="meta">@Slf4j</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">LogicalExpireTest</span> &#123;</span><br><span class="line">    </span><br><span class="line">    <span class="meta">@Resource</span></span><br><span class="line">    <span class="keyword">private</span> IShopService shopService;</span><br><span class="line">    </span><br><span class="line">    <span class="meta">@Resource</span></span><br><span class="line">    <span class="keyword">private</span> CachePreheatUtil cachePreheatUtil;</span><br><span class="line">    </span><br><span class="line">    <span class="meta">@Resource</span></span><br><span class="line">    <span class="keyword">private</span> StringRedisTemplate stringRedisTemplate;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 逻辑过期方案性能测试</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@Test</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">testLogicalExpirePerformance</span><span class="params">()</span> <span class="keyword">throws</span> InterruptedException &#123;</span><br><span class="line">        <span class="type">Long</span> <span class="variable">shopId</span> <span class="operator">=</span> <span class="number">1L</span>;</span><br><span class="line">        <span class="type">int</span> <span class="variable">threadCount</span> <span class="operator">=</span> <span class="number">100</span>;</span><br><span class="line">        <span class="type">CountDownLatch</span> <span class="variable">latch</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">CountDownLatch</span>(threadCount);</span><br><span class="line">        <span class="type">AtomicInteger</span> <span class="variable">successCount</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">AtomicInteger</span>(<span class="number">0</span>);</span><br><span class="line">        <span class="type">AtomicInteger</span> <span class="variable">rebuildCount</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">AtomicInteger</span>(<span class="number">0</span>);</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 1. 预热缓存（设置为已过期状态）</span></span><br><span class="line">        cachePreheatUtil.preheatShop(shopId, -<span class="number">1L</span>); <span class="comment">// 设置负值，表示已过期</span></span><br><span class="line">        </span><br><span class="line">        log.info(<span class="string">&quot;开始逻辑过期性能测试，线程数: &#123;&#125;，商户ID: &#123;&#125;&quot;</span>, threadCount, shopId);</span><br><span class="line">        </span><br><span class="line">        <span class="type">long</span> <span class="variable">startTime</span> <span class="operator">=</span> System.currentTimeMillis();</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 2. 并发查询</span></span><br><span class="line">        <span class="keyword">for</span> (<span class="type">int</span> <span class="variable">i</span> <span class="operator">=</span> <span class="number">0</span>; i &lt; threadCount; i++) &#123;</span><br><span class="line">            <span class="keyword">new</span> <span class="title class_">Thread</span>(() -&gt; &#123;</span><br><span class="line">                <span class="keyword">try</span> &#123;</span><br><span class="line">                    <span class="type">long</span> <span class="variable">threadStart</span> <span class="operator">=</span> System.currentTimeMillis();</span><br><span class="line">                    <span class="type">Shop</span> <span class="variable">shop</span> <span class="operator">=</span> shopService.queryWithLogicalExpire(shopId);</span><br><span class="line">                    <span class="type">long</span> <span class="variable">threadEnd</span> <span class="operator">=</span> System.currentTimeMillis();</span><br><span class="line">                    </span><br><span class="line">                    <span class="keyword">if</span> (shop != <span class="literal">null</span>) &#123;</span><br><span class="line">                        successCount.incrementAndGet();</span><br><span class="line">                        log.debug(<span class="string">&quot;线程&#123;&#125;查询成功，耗时: &#123;&#125;ms&quot;</span>, Thread.currentThread().getId(), (threadEnd - threadStart));</span><br><span class="line">                    &#125;</span><br><span class="line">                &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">                    log.error(<span class="string">&quot;查询异常&quot;</span>, e);</span><br><span class="line">                &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">                    latch.countDown();</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;).start();</span><br><span class="line">        &#125;</span><br><span class="line">        </span><br><span class="line">        latch.await();</span><br><span class="line">        <span class="type">long</span> <span class="variable">endTime</span> <span class="operator">=</span> System.currentTimeMillis();</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 3. 等待异步重建完成</span></span><br><span class="line">        Thread.sleep(<span class="number">2000</span>);</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 4. 验证结果</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">cachedData</span> <span class="operator">=</span> stringRedisTemplate.opsForValue().get(<span class="string">&quot;cache:shop:&quot;</span> + shopId);</span><br><span class="line">        Assertions.assertNotNull(cachedData, <span class="string">&quot;缓存应该已重建&quot;</span>);</span><br><span class="line">        </span><br><span class="line">        <span class="type">RedisData</span> <span class="variable">redisData</span> <span class="operator">=</span> JSONUtil.toBean(cachedData, RedisData.class);</span><br><span class="line">        Assertions.assertFalse(redisData.isExpired(), <span class="string">&quot;重建后的缓存应该未过期&quot;</span>);</span><br><span class="line">        </span><br><span class="line">        log.info(<span class="string">&quot;逻辑过期测试完成&quot;</span>);</span><br><span class="line">        log.info(<span class="string">&quot;总耗时: &#123;&#125;ms&quot;</span>, (endTime - startTime));</span><br><span class="line">        log.info(<span class="string">&quot;成功查询数: &#123;&#125;&quot;</span>, successCount.get());</span><br><span class="line">        log.info(<span class="string">&quot;QPS: &#123;&#125;&quot;</span>, threadCount * <span class="number">1000</span> / (endTime - startTime));</span><br><span class="line">        log.info(<span class="string">&quot;平均响应时间: &#123;&#125;ms&quot;</span>, (endTime - startTime) / threadCount);</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 测试缓存重建触发</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@Test</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">testCacheRebuild</span><span class="params">()</span> <span class="keyword">throws</span> InterruptedException &#123;</span><br><span class="line">        <span class="type">Long</span> <span class="variable">shopId</span> <span class="operator">=</span> <span class="number">2L</span>;</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 1. 预热缓存（设置为即将过期）</span></span><br><span class="line">        cachePreheatUtil.preheatShop(shopId, <span class="number">1L</span>); <span class="comment">// 1秒后过期</span></span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 2. 等待缓存过期</span></span><br><span class="line">        Thread.sleep(<span class="number">1500</span>);</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 3. 触发查询（应该触发异步重建）</span></span><br><span class="line">        <span class="type">Shop</span> <span class="variable">shop</span> <span class="operator">=</span> shopService.queryWithLogicalExpire(shopId);</span><br><span class="line">        Assertions.assertNotNull(shop, <span class="string">&quot;应该返回旧数据&quot;</span>);</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 4. 等待异步重建</span></span><br><span class="line">        Thread.sleep(<span class="number">2000</span>);</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 5. 验证重建结果</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">cachedData</span> <span class="operator">=</span> stringRedisTemplate.opsForValue().get(<span class="string">&quot;cache:shop:&quot;</span> + shopId);</span><br><span class="line">        <span class="type">RedisData</span> <span class="variable">redisData</span> <span class="operator">=</span> JSONUtil.toBean(cachedData, RedisData.class);</span><br><span class="line">        </span><br><span class="line">        Assertions.assertFalse(redisData.isExpired(), <span class="string">&quot;缓存应该已重建&quot;</span>);</span><br><span class="line">        log.info(<span class="string">&quot;缓存重建验证成功&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h4 id="2-10-3-性能对比与最佳实践">2.10.3 性能对比与最佳实践</h4>    <div class="note note-success">      <div class="note-header">        <i class="note-icon fa-regular fa-circle-check"></i>        <span class="note-title">提示信息</span>      </div>      <div class="note-content">        <p><strong>性能表现</strong>：逻辑过期方案在高并发场景下表现优异，平均响应时间比互斥锁方案提升80%+</p>      </div>    </div>  <p><strong>性能对比</strong>：</p><table><thead><tr><th>指标</th><th>互斥锁方案</th><th>逻辑过期方案</th><th>提升幅度</th></tr></thead><tbody><tr><td>平均响应时间</td><td>80ms</td><td>12ms</td><td>↓85%</td></tr><tr><td>并发处理能力</td><td>中等</td><td>优秀</td><td>↑300%</td></tr><tr><td>数据一致性</td><td>强一致</td><td>最终一致</td><td>-</td></tr><tr><td>用户体验</td><td>有等待</td><td>无等待</td><td>↑100%</td></tr><tr><td>实现复杂度</td><td>简单</td><td>复杂</td><td>-</td></tr><tr><td>内存占用</td><td>低</td><td>高</td><td>-</td></tr></tbody></table><p><strong>最佳实践</strong>：</p><ol><li><strong>预热策略</strong>：系统启动时预热热门数据，避免冷启动问题</li><li><strong>过期时间</strong>：根据业务特点设置合理的逻辑过期时间</li><li><strong>线程池配置</strong>：根据服务器性能合理配置线程池大小</li><li><strong>监控告警</strong>：监控缓存命中率和重建频率</li><li><strong>降级策略</strong>：异步重建失败时要有降级方案</li></ol><p><strong>适用场景</strong>：</p><ul><li>读多写少的业务场景</li><li>对性能要求极高的应用</li><li>能容忍短暂数据不一致的业务</li><li>高并发查询的热点数据</li></ul><h3 id="2-11-Redis工具类封装">2.11 Redis工具类封装</h3><h4 id="2-11-1-需求分析">2.11.1 需求分析</h4><p>基于StringRedisTemplate封装一个缓存工具类，满足下列需求：</p><div class="tabs" id="cache-client-requirements"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#cache-client-requirements-1">基础功能</button></li><li class="tab"><button type="button" data-href="#cache-client-requirements-2">查询功能</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="cache-client-requirements-1"><p><strong>基础功能</strong></p><ul><li>方法1：将任意Java对象序列化为json并存储在string类型的key中，并且可以设置TTL过期时间</li><li>方法2：将任意Java对象序列化为json并存储在string类型的key中，并且可以设置逻辑过期时间，用于处理缓存击穿问题</li></ul><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="cache-client-requirements-2"><p><strong>查询功能</strong></p><ul><li>方法3：根据指定的key查询缓存，并反序列化为指定类型，利用缓存空值的方式解决缓存穿透问题</li><li>方法4：根据指定的key查询缓存，并反序列化为指定类型，需要利用逻辑过期解决缓存击穿问题</li></ul><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h4 id="2-11-2-代码实现">2.11.2 代码实现</h4><div class="tabs" id="cache-client-implementation"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#cache-client-implementation-1">工具类完整实现</button></li><li class="tab"><button type="button" data-href="#cache-client-implementation-2">逻辑过期查询</button></li><li class="tab"><button type="button" data-href="#cache-client-implementation-3">互斥锁查询</button></li><li class="tab"><button type="button" data-href="#cache-client-implementation-4">工具方法</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="cache-client-implementation-1"><p><strong>CacheClient工具类</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Slf4j</span></span><br><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">CacheClient</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> StringRedisTemplate stringRedisTemplate;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">ExecutorService</span> <span class="variable">CACHE_REBUILD_EXECUTOR</span> <span class="operator">=</span> Executors.newFixedThreadPool(<span class="number">10</span>);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="title function_">CacheClient</span><span class="params">(StringRedisTemplate stringRedisTemplate)</span> &#123;</span><br><span class="line">        <span class="built_in">this</span>.stringRedisTemplate = stringRedisTemplate;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 设置缓存（TTL过期）</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">set</span><span class="params">(String key, Object value, Long time, TimeUnit unit)</span> &#123;</span><br><span class="line">        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 设置缓存（逻辑过期）</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">setWithLogicalExpire</span><span class="params">(String key, Object value, Long time, TimeUnit unit)</span> &#123;</span><br><span class="line">        <span class="type">RedisData</span> <span class="variable">redisData</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">RedisData</span>();</span><br><span class="line">        redisData.setData(value);</span><br><span class="line">        redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));</span><br><span class="line">        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 查询缓存（缓存穿透保护）</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">public</span> &lt;R,ID&gt; R <span class="title function_">queryWithPassThrough</span><span class="params">(</span></span><br><span class="line"><span class="params">            String keyPrefix, ID id, Class&lt;R&gt; type, Function&lt;ID, R&gt; dbFallback, Long time, TimeUnit unit)</span>&#123;</span><br><span class="line">        <span class="type">String</span> <span class="variable">key</span> <span class="operator">=</span> keyPrefix + id;</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 1.从redis查询缓存</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">json</span> <span class="operator">=</span> stringRedisTemplate.opsForValue().get(key);</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 2.缓存命中</span></span><br><span class="line">        <span class="keyword">if</span> (StrUtil.isNotBlank(json)) &#123;</span><br><span class="line">            <span class="keyword">return</span> JSONUtil.toBean(json, type);</span><br><span class="line">        &#125;</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 3.命中空值（缓存穿透保护）</span></span><br><span class="line">        <span class="keyword">if</span> (json != <span class="literal">null</span>) &#123;</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 4.缓存未命中，查询数据库</span></span><br><span class="line">        <span class="type">R</span> <span class="variable">r</span> <span class="operator">=</span> dbFallback.apply(id);</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 5.数据库中不存在</span></span><br><span class="line">        <span class="keyword">if</span> (r == <span class="literal">null</span>) &#123;</span><br><span class="line">            stringRedisTemplate.opsForValue().set(key, <span class="string">&quot;&quot;</span>, CACHE_NULL_TTL, TimeUnit.MINUTES);</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 6.数据库中存在，写入缓存</span></span><br><span class="line">        <span class="built_in">this</span>.set(key, r, time, unit);</span><br><span class="line">        <span class="keyword">return</span> r;</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="cache-client-implementation-2"><p><strong>逻辑过期查询方法</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 查询缓存（逻辑过期方案）</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">public</span> &lt;R, ID&gt; R <span class="title function_">queryWithLogicalExpire</span><span class="params">(</span></span><br><span class="line"><span class="params">        String keyPrefix, ID id, Class&lt;R&gt; type, Function&lt;ID, R&gt; dbFallback, Long time, TimeUnit unit)</span> &#123;</span><br><span class="line">    <span class="type">String</span> <span class="variable">key</span> <span class="operator">=</span> keyPrefix + id;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 1.从redis查询缓存</span></span><br><span class="line">    <span class="type">String</span> <span class="variable">json</span> <span class="operator">=</span> stringRedisTemplate.opsForValue().get(key);</span><br><span class="line">    <span class="keyword">if</span> (StrUtil.isBlank(json)) &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 2.命中，反序列化数据</span></span><br><span class="line">    <span class="type">RedisData</span> <span class="variable">redisData</span> <span class="operator">=</span> JSONUtil.toBean(json, RedisData.class);</span><br><span class="line">    <span class="type">R</span> <span class="variable">r</span> <span class="operator">=</span> JSONUtil.toBean((JSONObject) redisData.getData(), type);</span><br><span class="line">    <span class="type">LocalDateTime</span> <span class="variable">expireTime</span> <span class="operator">=</span> redisData.getExpireTime();</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 3.判断是否过期</span></span><br><span class="line">    <span class="keyword">if</span>(expireTime.isAfter(LocalDateTime.now())) &#123;</span><br><span class="line">        <span class="keyword">return</span> r; <span class="comment">// 未过期，直接返回</span></span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 4.已过期，需要缓存重建</span></span><br><span class="line">    <span class="type">String</span> <span class="variable">lockKey</span> <span class="operator">=</span> LOCK_SHOP_KEY + id;</span><br><span class="line">    <span class="type">boolean</span> <span class="variable">isLock</span> <span class="operator">=</span> tryLock(lockKey);</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">if</span> (isLock) &#123;</span><br><span class="line">        <span class="comment">// 5.获取锁成功，异步重建缓存</span></span><br><span class="line">        CACHE_REBUILD_EXECUTOR.submit(() -&gt; &#123;</span><br><span class="line">            <span class="keyword">try</span> &#123;</span><br><span class="line">                <span class="type">R</span> <span class="variable">newR</span> <span class="operator">=</span> dbFallback.apply(id);</span><br><span class="line">                <span class="built_in">this</span>.setWithLogicalExpire(key, newR, time, unit);</span><br><span class="line">            &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">                <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">RuntimeException</span>(e);</span><br><span class="line">            &#125;<span class="keyword">finally</span> &#123;</span><br><span class="line">                unlock(lockKey);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;);</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 6.返回旧数据</span></span><br><span class="line">    <span class="keyword">return</span> r;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="cache-client-implementation-3"><p><strong>互斥锁查询方法</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 查询缓存（互斥锁方案）</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">public</span> &lt;R, ID&gt; R <span class="title function_">queryWithMutex</span><span class="params">(</span></span><br><span class="line"><span class="params">        String keyPrefix, ID id, Class&lt;R&gt; type, Function&lt;ID, R&gt; dbFallback, Long time, TimeUnit unit)</span> &#123;</span><br><span class="line">    <span class="type">String</span> <span class="variable">key</span> <span class="operator">=</span> keyPrefix + id;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 1.从redis查询缓存</span></span><br><span class="line">    <span class="type">String</span> <span class="variable">shopJson</span> <span class="operator">=</span> stringRedisTemplate.opsForValue().get(key);</span><br><span class="line">    <span class="keyword">if</span> (StrUtil.isNotBlank(shopJson)) &#123;</span><br><span class="line">        <span class="keyword">return</span> JSONUtil.toBean(shopJson, type);</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 2.命中空值（缓存穿透保护）</span></span><br><span class="line">    <span class="keyword">if</span> (shopJson != <span class="literal">null</span>) &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 3.实现缓存重建</span></span><br><span class="line">    <span class="type">String</span> <span class="variable">lockKey</span> <span class="operator">=</span> LOCK_SHOP_KEY + id;</span><br><span class="line">    <span class="type">R</span> <span class="variable">r</span> <span class="operator">=</span> <span class="literal">null</span>;</span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">        <span class="type">boolean</span> <span class="variable">isLock</span> <span class="operator">=</span> tryLock(lockKey);</span><br><span class="line">        <span class="keyword">if</span> (!isLock) &#123;</span><br><span class="line">            Thread.sleep(<span class="number">50</span>); <span class="comment">// 获取锁失败，休眠并重试</span></span><br><span class="line">            <span class="keyword">return</span> queryWithMutex(keyPrefix, id, type, dbFallback, time, unit);</span><br><span class="line">        &#125;</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 4.获取锁成功，查询数据库</span></span><br><span class="line">        r = dbFallback.apply(id);</span><br><span class="line">        <span class="keyword">if</span> (r == <span class="literal">null</span>) &#123;</span><br><span class="line">            stringRedisTemplate.opsForValue().set(key, <span class="string">&quot;&quot;</span>, CACHE_NULL_TTL, TimeUnit.MINUTES);</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 5.写入缓存</span></span><br><span class="line">        <span class="built_in">this</span>.set(key, r, time, unit);</span><br><span class="line">    &#125; <span class="keyword">catch</span> (InterruptedException e) &#123;</span><br><span class="line">        <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">RuntimeException</span>(e);</span><br><span class="line">    &#125;<span class="keyword">finally</span> &#123;</span><br><span class="line">        unlock(lockKey);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> r;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="cache-client-implementation-4"><p><strong>锁工具方法</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 尝试获取分布式锁</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> <span class="type">boolean</span> <span class="title function_">tryLock</span><span class="params">(String key)</span> &#123;</span><br><span class="line">        <span class="type">Boolean</span> <span class="variable">flag</span> <span class="operator">=</span> stringRedisTemplate.opsForValue()</span><br><span class="line">                .setIfAbsent(key, <span class="string">&quot;1&quot;</span>, <span class="number">10</span>, TimeUnit.SECONDS);</span><br><span class="line">        <span class="keyword">return</span> BooleanUtil.isTrue(flag);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 释放分布式锁</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">void</span> <span class="title function_">unlock</span><span class="params">(String key)</span> &#123;</span><br><span class="line">        stringRedisTemplate.delete(key);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h4 id="2-11-3-使用示例">2.11.3 使用示例</h4><div class="tabs" id="cache-client-usage"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#cache-client-usage-1">Service层调用</button></li><li class="tab"><button type="button" data-href="#cache-client-usage-2">方案对比</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="cache-client-usage-1"><p><strong>ShopServiceImpl中的使用</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Resource</span></span><br><span class="line"><span class="keyword">private</span> CacheClient cacheClient;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">queryById</span><span class="params">(Long id)</span> &#123;</span><br><span class="line">    <span class="comment">// 解决缓存穿透</span></span><br><span class="line">    <span class="type">Shop</span> <span class="variable">shop</span> <span class="operator">=</span> cacheClient</span><br><span class="line">            .queryWithPassThrough(CACHE_SHOP_KEY, id, Shop.class, <span class="built_in">this</span>::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 互斥锁解决缓存击穿</span></span><br><span class="line">    <span class="comment">// Shop shop = cacheClient</span></span><br><span class="line">    <span class="comment">//         .queryWithMutex(CACHE_SHOP_KEY, id, Shop.class, this::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES);</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 逻辑过期解决缓存击穿</span></span><br><span class="line">    <span class="comment">// Shop shop = cacheClient</span></span><br><span class="line">    <span class="comment">//         .queryWithLogicalExpire(CACHE_SHOP_KEY, id, Shop.class, this::getById, 20L, TimeUnit.SECONDS);</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (shop == <span class="literal">null</span>) &#123;</span><br><span class="line">        <span class="keyword">return</span> Result.fail(<span class="string">&quot;店铺不存在！&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> Result.ok(shop);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="cache-client-usage-2"><p><strong>三种方案对比</strong></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>高性能要求</td><td>性能优秀，无等待</td><td>实现复杂，可能返回旧数据</td></tr></tbody></table><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h4 id="2-11-4-最佳实践总结">2.11.4 最佳实践总结</h4>    <div class="note note-success">      <div class="note-header">        <i class="note-icon fa-regular fa-circle-check"></i>        <span class="note-title">提示信息</span>      </div>      <div class="note-content">        <p><strong>推荐使用</strong>：CacheClient工具类封装了三种缓存策略，可根据业务场景灵活选择</p>      </div>    </div>  <p><strong>选择建议</strong>：</p><ol><li><strong>常规查询</strong>：优先使用<code>queryWithPassThrough</code>（缓存穿透保护）</li><li><strong>热点数据</strong>：使用<code>queryWithLogicalExpire</code>（逻辑过期方案）</li><li><strong>关键数据</strong>：使用<code>queryWithMutex</code>（互斥锁方案）</li></ol><p><strong>使用优势</strong>：</p><ul><li>✅ 代码复用性高，避免重复实现</li><li>✅ 策略灵活，可根据业务场景选择</li><li>✅ 封装完善，包含异常处理和日志</li><li>✅ 性能优化，支持异步重建和双重检查</li></ul><h2 id="3-优惠券秒杀">3. 优惠券秒杀</h2><h3 id="3-1-redis实现全局唯一ID">3.1 redis实现全局唯一ID</h3><h4 id="3-1-1-全局唯一ID生成">3.1.1 全局唯一ID生成</h4><p>每个店铺都可以发布优惠券，当用户抢购时，就会生成订单并保存到tb_voucher_order表中。</p><div class="tabs" id="id-requirement-analysis"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#id-requirement-analysis-1">问题分析</button></li><li class="tab"><button type="button" data-href="#id-requirement-analysis-2">解决方案</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="id-requirement-analysis-1"><p><strong>数据库自增ID存在的问题</strong></p><table><thead><tr><th>问题</th><th>描述</th><th>影响</th></tr></thead><tbody><tr><td>规律性明显</td><td>ID按顺序递增，容易被猜测</td><td>泄露业务数据</td></tr><tr><td>单表限制</td><td>MySQL单表容量不宜超过500W</td><td>制约业务扩展</td></tr><tr><td>分库分表困难</td><td>分布式环境下ID冲突</td><td>系统复杂度增加</td></tr></tbody></table><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="id-requirement-analysis-2"><p><strong>全局ID生成器要求</strong></p><pre><code>&lt;div class=&quot;note note-primary&quot;&gt;  &lt;div class=&quot;note-header&quot;&gt;    &lt;i class=&quot;note-icon fa-regular fa-star&quot;&gt;&lt;/i&gt;    &lt;span class=&quot;note-title&quot;&gt;提示信息&lt;/span&gt;  &lt;/div&gt;  &lt;div class=&quot;note-content&quot;&gt;    &lt;p&gt;全局ID生成器是一种在分布式系统下用来生成全局唯一ID的工具&lt;/p&gt;  &lt;/div&gt;&lt;/div&gt;</code></pre><table><thead><tr><th>特性</th><th>要求</th><th>实现思路</th></tr></thead><tbody><tr><td>唯一性</td><td>全局唯一</td><td>时间戳+序列号</td></tr><tr><td>高可用</td><td>99.99%可用</td><td>Redis集群</td></tr><tr><td>高性能</td><td>10W+QPS</td><td>内存操作</td></tr><tr><td>递增性</td><td>趋势递增</td><td>时间戳保证</td></tr><tr><td>安全性</td><td>无规则</td><td>拼接随机位</td></tr></tbody></table><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><p><strong>ID结构设计</strong></p>    <div class="note note-info">      <div class="note-header">        <i class="note-icon fa-regular fa-circle-info"></i>        <span class="note-title">提示信息</span>      </div>      <div class="note-content">        <p><strong>ID组成结构</strong>：符号位 + 时间戳 + 序列号，总计64位</p>      </div>    </div>  <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">┌──符号位──┬───────时间戳───────┬───────序列号───────┐</span><br><span class="line">│    1bit   │      31bit       │      32bit       │</span><br><span class="line">│    永远0   │  秒级时间戳(69年)  │  秒内计数器(2^32)  │</span><br><span class="line">└──────────┴─────────────────┴─────────────────┘</span><br></pre></td></tr></table></figure><p><strong>设计优势</strong>：</p><ul><li>✅ <strong>符号位</strong>：1bit，永远为0，保证ID为正数</li><li>✅ <strong>时间戳</strong>：31bit，以秒为单位，可以使用69年</li><li>✅ <strong>序列号</strong>：32bit，秒内的计数器，支持每秒产生2^32个不同ID</li><li>✅ <strong>安全性</strong>：不直接使用Redis自增数值，避免泄露业务量</li></ul><h4 id="3-1-2-Redis实现方案">3.1.2 Redis实现方案</h4><div class="tabs" id="redis-id-implementation"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#redis-id-implementation-1">核心代码</button></li><li class="tab"><button type="button" data-href="#redis-id-implementation-2">代码解析</button></li><li class="tab"><button type="button" data-href="#redis-id-implementation-3">测试验证</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="redis-id-implementation-1"><p><strong>RedisIdWorker工具类</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">RedisIdWorker</span> &#123;</span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 开始时间戳 - 2022-01-01 00:00:00</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">long</span> <span class="variable">BEGIN_TIMESTAMP</span> <span class="operator">=</span> <span class="number">1640995200L</span>;</span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 序列号的位数</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">int</span> <span class="variable">COUNT_BITS</span> <span class="operator">=</span> <span class="number">32</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> StringRedisTemplate stringRedisTemplate;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="title function_">RedisIdWorker</span><span class="params">(StringRedisTemplate stringRedisTemplate)</span> &#123;</span><br><span class="line">        <span class="built_in">this</span>.stringRedisTemplate = stringRedisTemplate;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 生成全局唯一ID</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> keyPrefix 业务前缀</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span> 全局唯一ID</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">public</span> <span class="type">long</span> <span class="title function_">nextId</span><span class="params">(String keyPrefix)</span> &#123;</span><br><span class="line">        <span class="comment">// 1. 生成时间戳</span></span><br><span class="line">        <span class="type">LocalDateTime</span> <span class="variable">now</span> <span class="operator">=</span> LocalDateTime.now();</span><br><span class="line">        <span class="type">long</span> <span class="variable">nowSecond</span> <span class="operator">=</span> now.toEpochSecond(ZoneOffset.UTC);</span><br><span class="line">        <span class="type">long</span> <span class="variable">timestamp</span> <span class="operator">=</span> nowSecond - BEGIN_TIMESTAMP;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 2. 生成序列号 - Redis自增</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">date</span> <span class="operator">=</span> now.format(DateTimeFormatter.ofPattern(<span class="string">&quot;yyyy:MM:dd&quot;</span>));</span><br><span class="line">        <span class="type">Long</span> <span class="variable">count</span> <span class="operator">=</span> stringRedisTemplate.opsForValue()</span><br><span class="line">                .increment(<span class="string">&quot;icr:&quot;</span> + keyPrefix + <span class="string">&quot;:&quot;</span> + date);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 3. 拼接并返回</span></span><br><span class="line">        <span class="keyword">return</span> timestamp &lt;&lt; COUNT_BITS | count;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="redis-id-implementation-2"><p><strong>实现原理解析</strong></p><table><thead><tr><th>步骤</th><th>操作</th><th>说明</th></tr></thead><tbody><tr><td>1</td><td>生成时间戳</td><td>当前时间 - 起始时间(2022-01-01)</td></tr><tr><td>2</td><td>生成序列号</td><td>Redis自增，按天分组避免溢出</td></tr><tr><td>3</td><td>位运算拼接</td><td><code>timestamp &lt;&lt; 32 | count</code></td></tr></tbody></table><p><strong>关键设计</strong>：</p><ul><li>✅ <strong>时间戳基准</strong>：使用2022-01-01作为起始时间，支持69年使用期</li><li>✅ <strong>按天分片</strong>：Redis key包含日期，避免自增数值过大</li><li>✅ <strong>位运算优化</strong>：使用位移和或运算快速拼接ID</li><li>✅ <strong>线程安全</strong>：Redis自增操作保证并发安全</li></ul><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="redis-id-implementation-3"><p><strong>性能测试代码</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Test</span></span><br><span class="line"><span class="keyword">void</span> <span class="title function_">testIdWorker</span><span class="params">()</span> <span class="keyword">throws</span> InterruptedException &#123;</span><br><span class="line">    <span class="type">CountDownLatch</span> <span class="variable">latch</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">CountDownLatch</span>(<span class="number">300</span>);</span><br><span class="line">    <span class="type">Runnable</span> <span class="variable">task</span> <span class="operator">=</span> () -&gt; &#123;</span><br><span class="line">        <span class="keyword">for</span> (<span class="type">int</span> <span class="variable">i</span> <span class="operator">=</span> <span class="number">0</span>; i &lt; <span class="number">100</span>; i++) &#123;</span><br><span class="line">            <span class="type">long</span> <span class="variable">id</span> <span class="operator">=</span> redisIdWorker.nextId(<span class="string">&quot;test&quot;</span>);</span><br><span class="line">            System.out.println(<span class="string">&quot;id = &quot;</span> + id);</span><br><span class="line">        &#125;</span><br><span class="line">        latch.countDown();</span><br><span class="line">    &#125;;</span><br><span class="line">    </span><br><span class="line">    <span class="type">long</span> <span class="variable">begin</span> <span class="operator">=</span> System.currentTimeMillis();</span><br><span class="line">    <span class="comment">// 300个线程，每个线程生成100个ID</span></span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> <span class="variable">i</span> <span class="operator">=</span> <span class="number">0</span>; i &lt; <span class="number">300</span>; i++) &#123;</span><br><span class="line">        es.submit(task);</span><br><span class="line">    &#125;</span><br><span class="line">    latch.await();</span><br><span class="line">    <span class="type">long</span> <span class="variable">end</span> <span class="operator">=</span> System.currentTimeMillis();</span><br><span class="line">    System.out.println(<span class="string">&quot;time = &quot;</span> + (end - begin));</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>测试结果</strong>：</p><ul><li>🚀 <strong>生成速度</strong>：3万个ID仅需几百毫秒</li><li>🚀 <strong>并发安全</strong>：多线程环境下无重复ID</li><li>🚀 <strong>趋势递增</strong>：ID整体呈递增趋势</li></ul><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h4 id="3-1-3-方案对比总结">3.1.3 方案对比总结</h4>    <div class="note note-success">      <div class="note-header">        <i class="note-icon fa-regular fa-circle-check"></i>        <span class="note-title">提示信息</span>      </div>      <div class="note-content">        <p><strong>Redis方案 vs 其他方案</strong></p>      </div>    </div>  <table><thead><tr><th>方案</th><th>实现复杂度</th><th>性能</th><th>可用性</th><th>适用场景</th></tr></thead><tbody><tr><td><strong>Redis方案</strong></td><td>⭐⭐ 简单</td><td>⭐⭐⭐⭐⭐ 10W+QPS</td><td>⭐⭐⭐⭐ 主从架构</td><td>中小型系统</td></tr><tr><td><strong>雪花算法</strong></td><td>⭐⭐⭐ 中等</td><td>⭐⭐⭐⭐⭐ 更高</td><td>⭐⭐ 依赖时钟</td><td>大型分布式系统</td></tr><tr><td><strong>数据库自增</strong></td><td>⭐ 最简单</td><td>⭐⭐ 千级QPS</td><td>⭐⭐ 单点故障</td><td>单机系统</td></tr><tr><td><strong>UUID</strong></td><td>⭐ 简单</td><td>⭐⭐⭐ 中等</td><td>⭐⭐⭐⭐⭐ 完全分布式</td><td>对顺序无要求场景</td></tr></tbody></table><h3 id="3-2-优惠券管理">3.2 优惠券管理</h3><h4 id="3-2-1-优惠券类型设计">3.2.1 优惠券类型设计</h4><div class="tabs" id="voucher-types"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#voucher-types-1">业务场景</button></li><li class="tab"><button type="button" data-href="#voucher-types-2">数据模型</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="voucher-types-1"><p><strong>优惠券业务模型</strong></p><table><thead><tr><th>类型</th><th>特点</th><th>使用场景</th><th>表结构</th></tr></thead><tbody><tr><td><strong>普通券</strong></td><td>优惠力度小，任意领取</td><td>日常促销</td><td>tb_voucher</td></tr><tr><td><strong>秒杀券</strong></td><td>优惠力度大，限时限量</td><td>引流获客</td><td>tb_voucher + tb_seckill_voucher</td></tr></tbody></table><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="voucher-types-2"><p><strong>表结构设计</strong></p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 优惠券基础信息表</span></span><br><span class="line">tb_voucher (</span><br><span class="line">    id <span class="type">bigint</span> <span class="keyword">PRIMARY KEY</span>,          <span class="comment">-- 优惠券ID</span></span><br><span class="line">    shop_id <span class="type">bigint</span>,                 <span class="comment">-- 商铺ID</span></span><br><span class="line">    title <span class="type">varchar</span>(<span class="number">255</span>),             <span class="comment">-- 优惠券标题</span></span><br><span class="line">    sub_title <span class="type">varchar</span>(<span class="number">255</span>),         <span class="comment">-- 副标题</span></span><br><span class="line">    rules <span class="type">varchar</span>(<span class="number">1024</span>),            <span class="comment">-- 使用规则</span></span><br><span class="line">    pay_value <span class="type">bigint</span>,               <span class="comment">-- 支付金额（分）</span></span><br><span class="line">    actual_value <span class="type">bigint</span>,            <span class="comment">-- 抵扣金额（分）</span></span><br><span class="line">    type tinyint,                   <span class="comment">-- 类型：1-普通券，2-秒杀券</span></span><br><span class="line">    status tinyint,                 <span class="comment">-- 状态：1-上架，2-下架，3-过期</span></span><br><span class="line">    create_time datetime,           <span class="comment">-- 创建时间</span></span><br><span class="line">    update_time datetime            <span class="comment">-- 更新时间</span></span><br><span class="line">);</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 秒杀优惠券扩展表</span></span><br><span class="line">tb_seckill_voucher (</span><br><span class="line">    voucher_id <span class="type">bigint</span> <span class="keyword">PRIMARY KEY</span>,  <span class="comment">-- 优惠券ID</span></span><br><span class="line">    stock <span class="type">int</span>,                      <span class="comment">-- 库存</span></span><br><span class="line">    create_time datetime,           <span class="comment">-- 创建时间</span></span><br><span class="line">    begin_time datetime,            <span class="comment">-- 开始抢购时间</span></span><br><span class="line">    end_time datetime               <span class="comment">-- 结束抢购时间</span></span><br><span class="line">);</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h4 id="3-2-2-优惠券发布实现">3.2.2 优惠券发布实现</h4><div class="tabs" id="voucher-implementation"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#voucher-implementation-1">普通券发布</button></li><li class="tab"><button type="button" data-href="#voucher-implementation-2">秒杀券发布</button></li><li class="tab"><button type="button" data-href="#voucher-implementation-3">前端调用服务</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="voucher-implementation-1"><p><strong>Controller层接口</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 新增普通券</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> voucher 优惠券信息</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@return</span> 优惠券id</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="meta">@PostMapping</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">addVoucher</span><span class="params">(<span class="meta">@RequestBody</span> Voucher voucher)</span> &#123;</span><br><span class="line">    voucherService.save(voucher);</span><br><span class="line">    <span class="keyword">return</span> Result.ok(voucher.getId());</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="voucher-implementation-2"><p><strong>Controller层接口</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 新增秒杀券</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> voucher 优惠券信息，包含秒杀信息</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@return</span> 优惠券id</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="meta">@PostMapping(&quot;seckill&quot;)</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">addSeckillVoucher</span><span class="params">(<span class="meta">@RequestBody</span> Voucher voucher)</span> &#123;</span><br><span class="line">    voucherService.addSeckillVoucher(voucher);</span><br><span class="line">    <span class="keyword">return</span> Result.ok(voucher.getId());</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>Service层实现</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="meta">@Transactional</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">addSeckillVoucher</span><span class="params">(Voucher voucher)</span> &#123;</span><br><span class="line">    <span class="comment">// 1. 保存优惠券基础信息</span></span><br><span class="line">    save(voucher);</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 2. 保存秒杀扩展信息</span></span><br><span class="line">    <span class="type">SeckillVoucher</span> <span class="variable">seckillVoucher</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">SeckillVoucher</span>();</span><br><span class="line">    seckillVoucher.setVoucherId(voucher.getId());</span><br><span class="line">    seckillVoucher.setStock(voucher.getStock());</span><br><span class="line">    seckillVoucher.setBeginTime(voucher.getBeginTime());</span><br><span class="line">    seckillVoucher.setEndTime(voucher.getEndTime());</span><br><span class="line">    seckillVoucherService.save(seckillVoucher);</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 3. 同步库存到Redis</span></span><br><span class="line">    stringRedisTemplate.opsForValue()</span><br><span class="line">        .set(SECKILL_STOCK_KEY + voucher.getId(), voucher.getStock().toString());</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>设计要点</strong>：</p><ul><li>✅ <strong>事务控制</strong>：使用<code>@Transactional</code>保证数据一致性</li><li>✅ <strong>Redis同步</strong>：将库存信息缓存到Redis，提升查询性能</li><li>✅ <strong>时间窗口</strong>：设置秒杀开始和结束时间</li><li>✅ <strong>库存隔离</strong>：秒杀券有独立库存管理</li></ul><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="voucher-implementation-3"><p><strong>用POSTMAN模拟发送请求来新增秒杀券，请求路径http://localhost:8081/voucher/seckill， 请求方式POST，JSON数据如下</strong></p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;shopId&quot;</span><span class="punctuation">:</span><span class="number">1</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;title&quot;</span><span class="punctuation">:</span><span class="string">&quot;100元代金券&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;subTitle&quot;</span><span class="punctuation">:</span><span class="string">&quot;周一至周五可用&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;rules&quot;</span><span class="punctuation">:</span><span class="string">&quot;全场通用\\n无需预约\\n可无限叠加&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;payValue&quot;</span><span class="punctuation">:</span><span class="number">8000</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;actualValue&quot;</span><span class="punctuation">:</span><span class="number">10000</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span><span class="number">1</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;stock&quot;</span><span class="punctuation">:</span><span class="number">100</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;beginTime&quot;</span><span class="punctuation">:</span><span class="string">&quot;2022-01-01T00:00:00&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;endTime&quot;</span><span class="punctuation">:</span><span class="string">&quot;2022-10-31T23:59:59&quot;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h3 id="3-3-秒杀下单实现">3.3 秒杀下单实现</h3><h4 id="3-3-1-业务需求分析">3.3.1 业务需求分析</h4><div class="tabs" id="seckill-requirement"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#seckill-requirement-1">业务流程</button></li><li class="tab"><button type="button" data-href="#seckill-requirement-2">前置条件</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="seckill-requirement-1"><p><strong>秒杀下单核心逻辑</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">graph TD</span><br><span class="line">    A[用户点击抢购] --&gt; B[查询优惠券信息]</span><br><span class="line">    B --&gt; C&#123;秒杀是否开始?&#125;</span><br><span class="line">    C --&gt;|未开始| D[返回失败:秒杀尚未开始]</span><br><span class="line">    C --&gt;|已开始| E&#123;秒杀是否结束?&#125;</span><br><span class="line">    E --&gt;|已结束| F[返回失败:秒杀已结束]</span><br><span class="line">    E --&gt;|进行中| G&#123;库存是否充足?&#125;</span><br><span class="line">    G --&gt;|不足| H[返回失败:库存不足]</span><br><span class="line">    G --&gt;|充足| I[扣减库存]</span><br><span class="line">    I --&gt; J[创建订单]</span><br><span class="line">    J --&gt; K[返回订单ID]</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="seckill-requirement-2"><p><strong>下单校验规则</strong></p><table><thead><tr><th>校验项</th><th>判断条件</th><th>失败提示</th></tr></thead><tbody><tr><td>秒杀时间</td><td>beginTime &gt; 当前时间</td><td>秒杀尚未开始！</td></tr><tr><td>秒杀时间</td><td>endTime &lt; 当前时间</td><td>秒杀已结束！</td></tr><tr><td>库存检查</td><td>stock &lt; 1</td><td>库存不足！</td></tr></tbody></table><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h4 id="3-3-2-基础实现方案">3.3.2 基础实现方案</h4><div class="tabs" id="seckill-basic"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#seckill-basic-1">controller层调用</button></li><li class="tab"><button type="button" data-href="#seckill-basic-2">Service</button></li><li class="tab"><button type="button" data-href="#seckill-basic-3">Service层实现</button></li><li class="tab"><button type="button" data-href="#seckill-basic-4">问题分析</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="seckill-basic-1"><p><strong>VoucherController</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@RestController</span></span><br><span class="line"><span class="meta">@RequestMapping(&quot;/voucher-order&quot;)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">VoucherOrderController</span> &#123;</span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> IVoucherOrderService voucherOrderService;</span><br><span class="line">    <span class="meta">@PostMapping(&quot;/seckill/&#123;id&#125;&quot;)</span></span><br><span class="line">    <span class="keyword">public</span> Result <span class="title function_">seckillVoucher</span><span class="params">(<span class="meta">@PathVariable(&quot;id&quot;)</span> Long voucherId)</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> voucherOrderService.seckillVoucher(voucherId);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="seckill-basic-2"><p><strong>VoucherOrderService</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">IVoucherOrderService</span> <span class="keyword">extends</span> <span class="title class_">IService</span>&lt;VoucherOrder&gt; &#123;</span><br><span class="line">    Result <span class="title function_">seckillVoucher</span><span class="params">(Long voucherId)</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="seckill-basic-3"><p><strong>VoucherOrderServiceImpl</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Autowired</span></span><br><span class="line"><span class="keyword">private</span> ISeckillVoucherService seckillVoucherService;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Autowired</span></span><br><span class="line"><span class="keyword">private</span> RedisIdWorker redisIdWorker;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">seckillVoucher</span><span class="params">(Long voucherId)</span> &#123;</span><br><span class="line">    <span class="comment">// 1. 查询优惠券信息</span></span><br><span class="line">    <span class="type">SeckillVoucher</span> <span class="variable">voucher</span> <span class="operator">=</span> seckillVoucherService.getById(voucherId);</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 2. 判断秒杀是否开始</span></span><br><span class="line">    <span class="keyword">if</span> (voucher.getBeginTime().isAfter(LocalDateTime.now())) &#123;</span><br><span class="line">        <span class="keyword">return</span> Result.fail(<span class="string">&quot;秒杀尚未开始！&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 3. 判断秒杀是否已经结束</span></span><br><span class="line">    <span class="keyword">if</span> (voucher.getEndTime().isBefore(LocalDateTime.now())) &#123;</span><br><span class="line">        <span class="keyword">return</span> Result.fail(<span class="string">&quot;秒杀已经结束！&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 4. 判断库存是否充足</span></span><br><span class="line">    <span class="keyword">if</span> (voucher.getStock() &lt; <span class="number">1</span>) &#123;</span><br><span class="line">        <span class="keyword">return</span> Result.fail(<span class="string">&quot;库存不足！&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 5. 扣减库存</span></span><br><span class="line">    <span class="type">boolean</span> <span class="variable">success</span> <span class="operator">=</span> seckillVoucherService.update()</span><br><span class="line">            .setSql(<span class="string">&quot;stock = stock - 1&quot;</span>)</span><br><span class="line">            .eq(<span class="string">&quot;voucher_id&quot;</span>, voucherId)</span><br><span class="line">            .update();</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">if</span> (!success) &#123;</span><br><span class="line">        <span class="keyword">return</span> Result.fail(<span class="string">&quot;库存不足！&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 6. 创建订单</span></span><br><span class="line">    <span class="type">VoucherOrder</span> <span class="variable">voucherOrder</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">VoucherOrder</span>();</span><br><span class="line">    <span class="comment">// 6.1 订单ID</span></span><br><span class="line">    <span class="type">long</span> <span class="variable">orderId</span> <span class="operator">=</span> redisIdWorker.nextId(<span class="string">&quot;order&quot;</span>);</span><br><span class="line">    voucherOrder.setId(orderId);</span><br><span class="line">    <span class="comment">// 6.2 用户ID</span></span><br><span class="line">    <span class="type">Long</span> <span class="variable">userId</span> <span class="operator">=</span> UserHolder.getUser().getId();</span><br><span class="line">    voucherOrder.setUserId(userId);</span><br><span class="line">    <span class="comment">// 6.3 代金券ID</span></span><br><span class="line">    voucherOrder.setVoucherId(voucherId);</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 7. 保存订单</span></span><br><span class="line">    save(voucherOrder);</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 8. 返回订单ID</span></span><br><span class="line">    <span class="keyword">return</span> Result.ok(orderId);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="seckill-basic-4"><p><strong>当前方案存在的问题</strong></p><pre><code>&lt;div class=&quot;note note-warning&quot;&gt;  &lt;div class=&quot;note-header&quot;&gt;    &lt;i class=&quot;note-icon fa-regular fa-circle-dot&quot;&gt;&lt;/i&gt;    &lt;span class=&quot;note-title&quot;&gt;提示信息&lt;/span&gt;  &lt;/div&gt;  &lt;div class=&quot;note-content&quot;&gt;    &lt;p&gt;&lt;strong&gt;高并发下的问题分析&lt;/strong&gt;&lt;/p&gt;  &lt;/div&gt;&lt;/div&gt;</code></pre><table><thead><tr><th>问题</th><th>现象</th><th>原因</th><th>影响</th></tr></thead><tbody><tr><td><strong>超卖问题</strong></td><td>库存为负</td><td>并发扣减库存</td><td>商家损失</td></tr><tr><td><strong>重复下单</strong></td><td>同一用户多单</td><td>并发创建订单</td><td>用户体验差</td></tr><tr><td><strong>性能瓶颈</strong></td><td>响应慢</td><td>数据库压力大</td><td>系统崩溃</td></tr></tbody></table><p><strong>问题根因</strong>：</p><ul><li>❌ <strong>无并发控制</strong>：多个线程同时扣减库存</li><li>❌ <strong>无幂等保障</strong>：同一用户可重复下单</li><li>❌ <strong>数据库压力大</strong>：所有操作都走数据库</li></ul><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h3 id="3-4-库存超卖问题分析">3.4 库存超卖问题分析</h3><div class="tabs" id="oversell-analysis"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#oversell-analysis-1">问题复现</button></li><li class="tab"><button type="button" data-href="#oversell-analysis-2">解决方案对比</button></li><li class="tab"><button type="button" data-href="#oversell-analysis-3">乐观锁优化</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="oversell-analysis-1"><p><strong>并发场景下的超卖问题</strong></p><p>假设库存为1，同时有100个线程并发下单：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">时间点 | 线程1 | 线程2 | 线程3 | ... | 库存</span><br><span class="line">-------|-------|-------|-------|-----|-----</span><br><span class="line">t1     | 查询库存=1 | 查询库存=1 | 查询库存=1 | ... | 1</span><br><span class="line">t2     | 判断库存&gt;0✅ | 判断库存&gt;0✅ | 判断库存&gt;0✅ | ... | 1</span><br><span class="line">t3     | 扣减库存 | 扣减库存 | 扣减库存 | ... | 1→0→-1→-2...</span><br></pre></td></tr></table></figure><p><strong>问题本质</strong>：</p><ul><li>❌ <strong>读-改-写</strong>操作非原子性</li><li>❌ <strong>并发控制缺失</strong>导致数据竞争</li><li>❌ <strong>库存校验</strong>与<strong>库存扣减</strong>分离</li></ul><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="oversell-analysis-2"><p><strong>悲观锁 vs 乐观锁</strong></p><pre><code>&lt;div class=&quot;note note-info&quot;&gt;  &lt;div class=&quot;note-header&quot;&gt;    &lt;i class=&quot;note-icon fa-regular fa-circle-info&quot;&gt;&lt;/i&gt;    &lt;span class=&quot;note-title&quot;&gt;提示信息&lt;/span&gt;  &lt;/div&gt;  &lt;div class=&quot;note-content&quot;&gt;    &lt;p&gt;&lt;strong&gt;悲观锁&lt;/strong&gt;：假定会发生并发冲突，屏蔽一切可能违反数据完整性的操作&lt;/p&gt;  &lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;note note-info&quot;&gt;  &lt;div class=&quot;note-header&quot;&gt;    &lt;i class=&quot;note-icon fa-regular fa-circle-info&quot;&gt;&lt;/i&gt;    &lt;span class=&quot;note-title&quot;&gt;提示信息&lt;/span&gt;  &lt;/div&gt;  &lt;div class=&quot;note-content&quot;&gt;    &lt;p&gt;&lt;strong&gt;乐观锁&lt;/strong&gt;：认为线程安全问题不一定会发生，因此不加锁，只是在更新数据的时候再去判断有没有其他线程对数据进行了修改&lt;/p&gt;  &lt;/div&gt;&lt;/div&gt;</code></pre><table><thead><tr><th>方案</th><th>实现方式</th><th>优点</th><th>缺点</th><th>适用场景</th></tr></thead><tbody><tr><td><strong>悲观锁</strong></td><td><code>synchronized</code>/<code>ReentrantLock</code></td><td>简单易理解</td><td>性能差，阻塞严重</td><td>并发量低的场景</td></tr><tr><td><strong>乐观锁</strong></td><td>版本号机制/CAS</td><td>性能好，无阻塞</td><td>实现复杂，ABA问题</td><td>并发量高的场景</td></tr></tbody></table><p><strong>乐观锁实现原理</strong>：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 版本号机制</span></span><br><span class="line">UPDATE tb_seckill_voucher </span><br><span class="line"><span class="type">SET</span> <span class="variable">stock</span> <span class="operator">=</span> stock - <span class="number">1</span>, version = version + <span class="number">1</span> </span><br><span class="line"><span class="type">WHERE</span> <span class="variable">voucher_id</span> <span class="operator">=</span> ? <span class="type">AND</span> <span class="variable">version</span> <span class="operator">=</span> ?</span><br><span class="line"></span><br><span class="line"><span class="comment">// CAS机制（库存大于0）  </span></span><br><span class="line">UPDATE tb_seckill_voucher </span><br><span class="line"><span class="type">SET</span> <span class="variable">stock</span> <span class="operator">=</span> stock - <span class="number">1</span> </span><br><span class="line"><span class="type">WHERE</span> <span class="variable">voucher_id</span> <span class="operator">=</span> ? AND stock &gt; <span class="number">0</span></span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="oversell-analysis-3"><p><strong>乐观锁方案演进</strong></p><pre><code>&lt;div class=&quot;note note-warning&quot;&gt;  &lt;div class=&quot;note-header&quot;&gt;    &lt;i class=&quot;note-icon fa-regular fa-circle-dot&quot;&gt;&lt;/i&gt;    &lt;span class=&quot;note-title&quot;&gt;提示信息&lt;/span&gt;  &lt;/div&gt;  &lt;div class=&quot;note-content&quot;&gt;    &lt;p&gt;&lt;strong&gt;方案一：版本号控制&lt;/strong&gt;（成功率低）&lt;/p&gt;  &lt;/div&gt;&lt;/div&gt;</code></pre><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">boolean</span> <span class="variable">success</span> <span class="operator">=</span> seckillVoucherService.update()</span><br><span class="line">    .setSql(<span class="string">&quot;stock = stock - 1&quot;</span>)</span><br><span class="line">    .eq(<span class="string">&quot;voucher_id&quot;</span>, voucherId)</span><br><span class="line">    .eq(<span class="string">&quot;stock&quot;</span>, voucher.getStock())  <span class="comment">// 版本号校验</span></span><br><span class="line">    .update();</span><br></pre></td></tr></table></figure><p><strong>问题分析</strong>：</p><ul><li><p>❌ <strong>成功率极低</strong>：100个线程同时拿到stock=100，只有1个能成功</p></li><li><p>❌ <strong>重试压力大</strong>：失败线程需要重新查询版本号再重试</p></li><li><p>❌ <strong>用户体验差</strong>：大量请求返回失败</p>  <div class="note note-success">    <div class="note-header">      <i class="note-icon fa-regular fa-circle-check"></i>      <span class="note-title">提示信息</span>    </div>    <div class="note-content">      <p><strong>方案二：库存大于0控制</strong>（推荐）</p><pre><code>&lt;/div&gt;</code></pre>  </div></li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">boolean</span> <span class="variable">success</span> <span class="operator">=</span> seckillVoucherService.update()</span><br><span class="line">    .setSql(<span class="string">&quot;stock = stock - 1&quot;</span>)</span><br><span class="line">    .eq(<span class="string">&quot;voucher_id&quot;</span>, voucherId)</span><br><span class="line">    .gt(<span class="string">&quot;stock&quot;</span>, <span class="number">0</span>)  <span class="comment">// 只需保证库存大于0</span></span><br><span class="line">    .update();</span><br></pre></td></tr></table></figure><p><strong>优势分析</strong>：</p><ul><li>✅ <strong>成功率高</strong>：只要还有库存就能成功</li><li>✅ <strong>实现简单</strong>：无需维护版本号</li><li>✅ <strong>性能优秀</strong>：无自旋重试，一次操作完成</li></ul><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h3 id="3-5-一人一单问题">3.5 一人一单问题</h3><div class="tabs" id="one-user-one-order"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#one-user-one-order-1">业务需求</button></li><li class="tab"><button type="button" data-href="#one-user-one-order-2">并发问题</button></li><li class="tab"><button type="button" data-href="#one-user-one-order-3">解决方案</button></li><li class="tab"><button type="button" data-href="#one-user-one-order-4">事务问题</button></li><li class="tab"><button type="button" data-href="#one-user-one-order-5">引入配置和依赖</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="one-user-one-order-1"><p><strong>需求分析</strong></p><pre><code>&lt;div class=&quot;note note-primary&quot;&gt;  &lt;div class=&quot;note-header&quot;&gt;    &lt;i class=&quot;note-icon fa-regular fa-star&quot;&gt;&lt;/i&gt;    &lt;span class=&quot;note-title&quot;&gt;提示信息&lt;/span&gt;  &lt;/div&gt;  &lt;div class=&quot;note-content&quot;&gt;    &lt;p&gt;&lt;strong&gt;业务规则&lt;/strong&gt;：同一个优惠券，一个用户只能下一单&lt;/p&gt;  &lt;/div&gt;&lt;/div&gt;</code></pre><p><strong>问题背景</strong>：</p><ul><li>🎯 <strong>营销目的</strong>：优惠券用于引流获客，需要控制成本</li><li>🎯 <strong>公平性</strong>：防止黄牛党恶意刷单</li><li>🎯 <strong>用户体验</strong>：让更多用户享受到优惠</li></ul><p><strong>实现思路</strong>：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">用户下单前 → 查询该用户是否已购买 → 已购买则拒绝 → 未购买则允许下单</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="one-user-one-order-2"><p><strong>并发场景下的问题</strong></p><p>假设用户A同时发起5个请求：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">时间点 | 请求1 | 请求2 | 请求3 | 请求4 | 请求5</span><br><span class="line">-------|-------|-------|-------|-------|------</span><br><span class="line">t1     | 查询订单=0✅ | 查询订单=0✅ | 查询订单=0✅ | 查询订单=0✅ | 查询订单=0✅</span><br><span class="line">t2     | 创建订单 | 创建订单 | 创建订单 | 创建订单 | 创建订单</span><br><span class="line">t3     | 成功 | 成功 | 成功 | 成功 | 成功</span><br></pre></td></tr></table></figure><p><strong>问题本质</strong>：</p><ul><li>❌ <strong>查询-判断-创建</strong>操作非原子性</li><li>❌ <strong>无并发控制</strong>导致重复下单</li><li>❌ <strong>数据库唯一约束</strong>无法防止并发插入</li></ul><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="one-user-one-order-3"><p><strong>悲观锁解决方案</strong></p><pre><code>&lt;div class=&quot;note note-success&quot;&gt;  &lt;div class=&quot;note-header&quot;&gt;    &lt;i class=&quot;note-icon fa-regular fa-circle-check&quot;&gt;&lt;/i&gt;    &lt;span class=&quot;note-title&quot;&gt;提示信息&lt;/span&gt;  &lt;/div&gt;  &lt;div class=&quot;note-content&quot;&gt;    &lt;p&gt;&lt;strong&gt;核心思路&lt;/strong&gt;：对&lt;strong&gt;用户ID&lt;/strong&gt;加锁，保证同一用户并发请求串行处理&lt;/p&gt;  &lt;/div&gt;&lt;/div&gt;</code></pre><p><strong>代码实现</strong>：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">seckillVoucher</span><span class="params">(Long voucherId)</span> &#123;</span><br><span class="line">    <span class="comment">// 1. 查询优惠券信息</span></span><br><span class="line">    <span class="type">SeckillVoucher</span> <span class="variable">voucher</span> <span class="operator">=</span> seckillVoucherService.getById(voucherId);</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 2. 判断秒杀是否开始</span></span><br><span class="line">    <span class="keyword">if</span> (voucher.getBeginTime().isAfter(LocalDateTime.now())) &#123;</span><br><span class="line">        <span class="keyword">return</span> Result.fail(<span class="string">&quot;秒杀尚未开始！&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 3. 判断秒杀是否已经结束</span></span><br><span class="line">    <span class="keyword">if</span> (voucher.getEndTime().isBefore(LocalDateTime.now())) &#123;</span><br><span class="line">        <span class="keyword">return</span> Result.fail(<span class="string">&quot;秒杀已经结束！&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 4. 判断库存是否充足</span></span><br><span class="line">    <span class="keyword">if</span> (voucher.getStock() &lt; <span class="number">1</span>) &#123;</span><br><span class="line">        <span class="keyword">return</span> Result.fail(<span class="string">&quot;库存不足！&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 5. 一人一单控制</span></span><br><span class="line">    <span class="type">Long</span> <span class="variable">userId</span> <span class="operator">=</span> UserHolder.getUser().getId();</span><br><span class="line">    <span class="keyword">synchronized</span> (userId.toString().intern()) &#123;        </span><br><span class="line">        <span class="comment">// 创建订单（包含一人一单校验扣减库存逻辑）</span></span><br><span class="line">        <span class="keyword">return</span> createVoucherOrder(voucherId);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">private</span> Result <span class="title function_">createVoucherOrder</span><span class="params">(Long voucherId)</span> &#123;</span><br><span class="line">    <span class="comment">// 一人一单逻辑</span></span><br><span class="line">    <span class="type">Long</span> <span class="variable">userId</span> <span class="operator">=</span> UserHolder.getUser().getId();</span><br><span class="line">    <span class="type">int</span> <span class="variable">count</span> <span class="operator">=</span> query().eq(<span class="string">&quot;voucher_id&quot;</span>, voucherId).eq(<span class="string">&quot;user_id&quot;</span>, userId).count();</span><br><span class="line">    <span class="keyword">if</span> (count &gt; <span class="number">0</span>) &#123;</span><br><span class="line">        <span class="keyword">return</span> Result.fail(<span class="string">&quot;你已经抢过优惠券了哦&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">//5. 扣减库存</span></span><br><span class="line">    <span class="type">boolean</span> <span class="variable">success</span> <span class="operator">=</span> seckillVoucherService.update()</span><br><span class="line">            .setSql(<span class="string">&quot;stock = stock - 1&quot;</span>)</span><br><span class="line">            .eq(<span class="string">&quot;voucher_id&quot;</span>, voucherId)</span><br><span class="line">            .gt(<span class="string">&quot;stock&quot;</span>, <span class="number">0</span>)</span><br><span class="line">            .update();</span><br><span class="line">    <span class="keyword">if</span> (!success) &#123;</span><br><span class="line">        <span class="keyword">return</span> Result.fail(<span class="string">&quot;库存不足&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">//6. 创建订单</span></span><br><span class="line">    <span class="type">VoucherOrder</span> <span class="variable">voucherOrder</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">VoucherOrder</span>();</span><br><span class="line">    <span class="comment">//6.1 设置订单id</span></span><br><span class="line">    <span class="type">long</span> <span class="variable">orderId</span> <span class="operator">=</span> redisIdWorker.nextId(<span class="string">&quot;order&quot;</span>);</span><br><span class="line">    <span class="comment">//6.2 设置用户id</span></span><br><span class="line">    <span class="type">Long</span> <span class="variable">id</span> <span class="operator">=</span> UserHolder.getUser().getId();</span><br><span class="line">    <span class="comment">//6.3 设置代金券id</span></span><br><span class="line">    voucherOrder.setVoucherId(voucherId);</span><br><span class="line">    voucherOrder.setId(orderId);</span><br><span class="line">    voucherOrder.setUserId(id);</span><br><span class="line">    <span class="comment">//7. 将订单数据保存到表中</span></span><br><span class="line">    save(voucherOrder);</span><br><span class="line">    <span class="comment">//8. 返回订单id</span></span><br><span class="line">    <span class="keyword">return</span> Result.ok(orderId);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>关键设计</strong>：</p><ul><li>✅ <strong>锁粒度</strong>：按用户ID加锁，不同用户互不影响</li><li>✅ <strong>字符串常量池</strong>：使用<code>intern()</code>确保同一把锁</li><li>✅ <strong>事务边界</strong>：锁要包裹整个事务操作</li></ul><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="one-user-one-order-4"><p><strong>事务与锁的协同机制</strong></p><pre><code>&lt;div class=&quot;note note-warning&quot;&gt;  &lt;div class=&quot;note-header&quot;&gt;    &lt;i class=&quot;note-icon fa-regular fa-circle-dot&quot;&gt;&lt;/i&gt;    &lt;span class=&quot;note-title&quot;&gt;提示信息&lt;/span&gt;  &lt;/div&gt;  &lt;div class=&quot;note-content&quot;&gt;    &lt;p&gt;&lt;strong&gt;核心问题&lt;/strong&gt;：Spring事务与JVM锁的生命周期不一致&lt;/p&gt;  &lt;/div&gt;&lt;/div&gt;</code></pre><p><strong>问题根源</strong>：<br>直接通过<code>this</code>调用同类方法会导致Spring事务失效，因为事务是通过AOP代理机制实现的。Spring的事务管理基于动态代理，只有通过代理对象调用的方法才能被事务拦截器处理。</p><p><strong>生命周期冲突</strong>：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Transactional</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">method</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="keyword">synchronized</span>(lock) &#123;</span><br><span class="line">        <span class="comment">// 业务操作</span></span><br><span class="line">    &#125; <span class="comment">// 🔒锁已释放，但事务还未提交</span></span><br><span class="line">&#125; <span class="comment">// 💥事务提交，但其他线程可能已获取锁并读取到脏数据</span></span><br></pre></td></tr></table></figure><p><strong>解决方案</strong>：获取Spring代理对象确保事务生效<br>使用<code>AopContext.currentProxy()</code>获取当前代理对象，通过代理对象调用事务方法，确保锁在事务提交后才释放。</p><p><strong>技术实现要点</strong>：</p><ol><li>启用暴露代理：<code>@EnableAspectJAutoProxy(exposeProxy = true)</code></li><li>接口定义：在<code>IVoucherOrderService</code>接口中声明<code>createVoucherOrder</code>方法</li><li>代理调用：通过代理对象调用确保事务拦截器生效</li></ol><p><strong>解决方案</strong>：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> Result <span class="title function_">seckillVoucher</span><span class="params">(Long voucherId)</span> &#123;</span><br><span class="line">    <span class="comment">// 前置校验方案</span></span><br><span class="line"></span><br><span class="line">    <span class="type">Long</span> <span class="variable">userId</span> <span class="operator">=</span> UserHolder.getUser().getId();</span><br><span class="line">    <span class="keyword">synchronized</span> (userId.toString().intern()) &#123;</span><br><span class="line">        <span class="comment">// 获取代理对象，确保事务生效</span></span><br><span class="line">        <span class="type">IVoucherOrderService</span> <span class="variable">proxy</span> <span class="operator">=</span> (IVoucherOrderService) AopContext.currentProxy();</span><br><span class="line">        <span class="keyword">return</span> proxy.createVoucherOrder(voucherId);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Transactional</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">createVoucherOrder</span><span class="params">(Long voucherId)</span> &#123;</span><br><span class="line">    <span class="comment">// 事务操作</span></span><br><span class="line">    <span class="type">Long</span> <span class="variable">userId</span> <span class="operator">=</span> UserHolder.getUser().getId();</span><br><span class="line"><span class="keyword">synchronized</span>(userId.toString().intern())&#123;</span><br><span class="line">         <span class="comment">// 5.1.查询订单</span></span><br><span class="line">        <span class="type">int</span> <span class="variable">count</span> <span class="operator">=</span> query().eq(<span class="string">&quot;user_id&quot;</span>, userId).eq(<span class="string">&quot;voucher_id&quot;</span>, voucherId).count();</span><br><span class="line">        <span class="comment">// 5.2.判断是否存在</span></span><br><span class="line">        <span class="keyword">if</span> (count &gt; <span class="number">0</span>) &#123;</span><br><span class="line">            <span class="comment">// 用户已经购买过了</span></span><br><span class="line">            <span class="keyword">return</span> Result.fail(<span class="string">&quot;用户已经购买过一次！&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 6.扣减库存</span></span><br><span class="line">        <span class="type">boolean</span> <span class="variable">success</span> <span class="operator">=</span> seckillVoucherService.update()</span><br><span class="line">                .setSql(<span class="string">&quot;stock = stock - 1&quot;</span>) <span class="comment">// set stock = stock - 1</span></span><br><span class="line">                .eq(<span class="string">&quot;voucher_id&quot;</span>, voucherId).gt(<span class="string">&quot;stock&quot;</span>, <span class="number">0</span>) <span class="comment">// where id = ? and stock &gt; 0</span></span><br><span class="line">                .update();</span><br><span class="line">        <span class="keyword">if</span> (!success) &#123;</span><br><span class="line">            <span class="comment">// 扣减失败</span></span><br><span class="line">            <span class="keyword">return</span> Result.fail(<span class="string">&quot;库存不足！&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 7.创建订单</span></span><br><span class="line">        <span class="type">VoucherOrder</span> <span class="variable">voucherOrder</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">VoucherOrder</span>();</span><br><span class="line">        <span class="comment">// 7.1.订单id</span></span><br><span class="line">        <span class="type">long</span> <span class="variable">orderId</span> <span class="operator">=</span> redisIdWorker.nextId(<span class="string">&quot;order&quot;</span>);</span><br><span class="line">        voucherOrder.setId(orderId);</span><br><span class="line">        <span class="comment">// 7.2.用户id</span></span><br><span class="line">        voucherOrder.setUserId(userId);</span><br><span class="line">        <span class="comment">// 7.3.代金券id</span></span><br><span class="line">        voucherOrder.setVoucherId(voucherId);</span><br><span class="line">        save(voucherOrder);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 7.返回订单id</span></span><br><span class="line">        <span class="keyword">return</span> Result.ok(orderId);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="one-user-one-order-5"><p><strong>引入配置和依赖</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 引入aspectjweaver依赖</span></span><br><span class="line">&lt;dependency&gt;</span><br><span class="line">    &lt;groupId&gt;org.aspectj&lt;/groupId&gt;</span><br><span class="line">    &lt;artifactId&gt;aspectjweaver&lt;/artifactId&gt;</span><br><span class="line">&lt;/dependency&gt;</span><br></pre></td></tr></table></figure><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 在启动类上加上@EnableAspectJAutoProxy(exposeProxy = true)注解</span></span><br><span class="line"><span class="meta">@MapperScan(&quot;com.hmdp.mapper&quot;)</span></span><br><span class="line"><span class="meta">@SpringBootApplication</span></span><br><span class="line"><span class="meta">@EnableAspectJAutoProxy(exposeProxy = true)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">HmDianPingApplication</span> &#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> &#123;</span><br><span class="line">        SpringApplication.run(HmDianPingApplication.class, args);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h3 id="3-6-集群环境下的并发问题">3.6 集群环境下的并发问题</h3><div class="tabs" id="cluster-concurrency"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#cluster-concurrency-1">问题分析</button></li><li class="tab"><button type="button" data-href="#cluster-concurrency-2">分布式锁需求</button></li><li class="tab"><button type="button" data-href="#cluster-concurrency-3">Redis分布式锁</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="cluster-concurrency-1"><p><strong>集群模式下的新问题</strong></p><pre><code>&lt;div class=&quot;note note-warning&quot;&gt;  &lt;div class=&quot;note-header&quot;&gt;    &lt;i class=&quot;note-icon fa-regular fa-circle-dot&quot;&gt;&lt;/i&gt;    &lt;span class=&quot;note-title&quot;&gt;提示信息&lt;/span&gt;  &lt;/div&gt;  &lt;div class=&quot;note-content&quot;&gt;    &lt;p&gt;&lt;strong&gt;JVM锁失效&lt;/strong&gt;：集群环境下，每个JVM实例有自己的锁&lt;/p&gt;  &lt;/div&gt;&lt;/div&gt;</code></pre><p><strong>场景复现</strong>：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">用户A的请求 → Nginx负载均衡 → 8081端口（JVM锁生效）</span><br><span class="line">用户A的请求 → Nginx负载均衡 → 8082端口（JVM锁失效）</span><br></pre></td></tr></table></figure><p><strong>问题本质</strong>：</p><ul><li>❌ <strong>JVM锁作用域</strong>：只在单个JVM实例内有效</li><li>❌ <strong>负载均衡</strong>：同一用户的请求可能分发到不同实例</li><li>❌ <strong>分布式环境</strong>：需要<strong>分布式锁</strong>解决方案</li></ul><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="cluster-concurrency-2"><p><strong>分布式锁核心要求</strong></p><pre><code>&lt;div class=&quot;note note-info&quot;&gt;  &lt;div class=&quot;note-header&quot;&gt;    &lt;i class=&quot;note-icon fa-regular fa-circle-info&quot;&gt;&lt;/i&gt;    &lt;span class=&quot;note-title&quot;&gt;提示信息&lt;/span&gt;  &lt;/div&gt;  &lt;div class=&quot;note-content&quot;&gt;    &lt;p&gt;&lt;strong&gt;分布式锁&lt;/strong&gt;：在分布式系统中，所有节点共享的锁机制&lt;/p&gt;  &lt;/div&gt;&lt;/div&gt;</code></pre><table><thead><tr><th>特性</th><th>要求</th><th>实现方案</th></tr></thead><tbody><tr><td><strong>互斥性</strong></td><td>同一时间只有一个客户端能获取锁</td><td>Redis SETNX</td></tr><tr><td><strong>安全性</strong></td><td>锁只能被持有者释放</td><td>唯一标识 + Lua脚本</td></tr><tr><td><strong>死锁避免</strong></td><td>锁必须有超时时间</td><td>Redis EXPIRE</td></tr><tr><td><strong>可用性</strong></td><td>高可用的锁服务</td><td>Redis集群</td></tr><tr><td><strong>可重入性</strong></td><td>同一客户端可重复获取锁</td><td>ThreadLocal + 计数器</td></tr></tbody></table><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="cluster-concurrency-3"><p><strong>Redis分布式锁实现</strong></p><p><strong>基本命令</strong>：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 获取锁（SETNX + EXPIRE）</span></span><br><span class="line">SET lock_key unique_value NX EX 30</span><br><span class="line"></span><br><span class="line"><span class="comment"># 释放锁（Lua脚本保证原子性）</span></span><br><span class="line"><span class="keyword">if</span> redis.call(<span class="string">&quot;get&quot;</span>, KEYS[1]) == ARGV[1] <span class="keyword">then</span></span><br><span class="line">    <span class="built_in">return</span> redis.call(<span class="string">&quot;del&quot;</span>, KEYS[1])</span><br><span class="line"><span class="keyword">else</span></span><br><span class="line">    <span class="built_in">return</span> 0</span><br><span class="line">end</span><br></pre></td></tr></table></figure><p><strong>实现优势</strong>：</p><ul><li>✅ <strong>高性能</strong>：Redis内存操作，10W+QPS</li><li>✅ <strong>高可用</strong>：Redis主从架构，故障自动切换</li><li>✅ <strong>易实现</strong>：命令简单，客户端支持好</li><li>✅ <strong>可扩展</strong>：支持RedLock算法，多Redis实例</li></ul><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div>    <div class="note note-info">      <div class="note-header">        <i class="note-icon fa-regular fa-circle-info"></i>        <span class="note-title">提示信息</span>      </div>      <div class="note-content">        <p><strong>有关锁失效原因分析</strong>：这就是集群环境下，syn锁失效的原因，在这种情况下，我们需要使用分布式锁来解决这个问题，让锁不存在于每个jvm的内部，而是让所有jvm公用外部的一把锁（Redis）</p>      </div>    </div>  <h2 id="4-分布式锁">4. 分布式锁</h2><h3 id="4-1-分布式锁概述">4.1 分布式锁概述</h3><div class="tabs" id="distributed-lock-overview"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#distributed-lock-overview-1">基本概念</button></li><li class="tab"><button type="button" data-href="#distributed-lock-overview-2">核心要求</button></li><li class="tab"><button type="button" data-href="#distributed-lock-overview-3">实现方案对比</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="distributed-lock-overview-1"><p><strong>什么是分布式锁</strong></p><pre><code>&lt;div class=&quot;note note-primary&quot;&gt;  &lt;div class=&quot;note-header&quot;&gt;    &lt;i class=&quot;note-icon fa-regular fa-star&quot;&gt;&lt;/i&gt;    &lt;span class=&quot;note-title&quot;&gt;提示信息&lt;/span&gt;  &lt;/div&gt;  &lt;div class=&quot;note-content&quot;&gt;    &lt;p&gt;&lt;strong&gt;分布式锁&lt;/strong&gt;：在分布式系统或集群模式下，多个进程可见且互斥的锁机制&lt;/p&gt;  &lt;/div&gt;&lt;/div&gt;</code></pre><p><strong>核心思想</strong>：所有节点使用<strong>同一把锁</strong>，确保程序串行执行</p><p><strong>与JVM锁的区别</strong>：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">JVM锁：只在单个JVM实例内有效（synchronized、ReentrantLock）</span><br><span class="line">分布式锁：在多个节点间共享，所有实例都能感知锁状态</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="distributed-lock-overview-2"><p><strong>分布式锁必备特性</strong></p><table><thead><tr><th>特性</th><th>说明</th><th>实现方案</th></tr></thead><tbody><tr><td><strong>互斥性</strong></td><td>同一时间只有一个客户端能获取锁</td><td>Redis SETNX</td></tr><tr><td><strong>可见性</strong></td><td>所有节点都能感知锁状态变化</td><td>Redis发布订阅</td></tr><tr><td><strong>高可用</strong></td><td>锁服务不易崩溃，故障可恢复</td><td>Redis集群/哨兵</td></tr><tr><td><strong>高性能</strong></td><td>加锁/解锁操作响应快</td><td>内存操作</td></tr><tr><td><strong>安全性</strong></td><td>锁只能被持有者释放</td><td>唯一标识验证</td></tr><tr><td><strong>死锁避免</strong></td><td>锁必须有超时时间</td><td>TTL过期机制</td></tr></tbody></table><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="distributed-lock-overview-3"><p><strong>常见分布式锁实现</strong></p><pre><code>&lt;div class=&quot;note note-info&quot;&gt;  &lt;div class=&quot;note-header&quot;&gt;    &lt;i class=&quot;note-icon fa-regular fa-circle-info&quot;&gt;&lt;/i&gt;    &lt;span class=&quot;note-title&quot;&gt;提示信息&lt;/span&gt;  &lt;/div&gt;  &lt;div class=&quot;note-content&quot;&gt;    &lt;p&gt;&lt;strong&gt;三种主流方案对比&lt;/strong&gt;&lt;/p&gt;  &lt;/div&gt;&lt;/div&gt;</code></pre><table><thead><tr><th>方案</th><th>优点</th><th>缺点</th><th>适用场景</th></tr></thead><tbody><tr><td><strong>MySQL</strong></td><td>实现简单，事务支持</td><td>性能差，锁表风险</td><td>低频操作</td></tr><tr><td><strong>Redis</strong></td><td>高性能，10W+QPS</td><td>需要处理锁超时</td><td>高频并发</td></tr><tr><td><strong>Zookeeper</strong></td><td>强一致性，Watch机制</td><td>实现复杂，性能一般</td><td>强一致性要求</td></tr></tbody></table><p><strong>企业级选择</strong>：</p><ul><li>✅ <strong>Redis</strong>：99%场景的首选（性能+可用性平衡）</li><li>✅ <strong>Redisson</strong>：Java生态最成熟的分布式锁框架</li></ul><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h3 id="4-2-Redis分布式锁实现">4.2 Redis分布式锁实现</h3><div class="tabs" id="redis-distributed-lock"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#redis-distributed-lock-1">核心命令</button></li><li class="tab"><button type="button" data-href="#redis-distributed-lock-2">代码实现</button></li><li class="tab"><button type="button" data-href="#redis-distributed-lock-3">业务集成</button></li><li class="tab"><button type="button" data-href="#redis-distributed-lock-4">误删问题</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="redis-distributed-lock-1"><p><strong>Redis锁实现原理</strong></p><p><strong>获取锁</strong>：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 原子性操作：SETNX + EXPIRE</span></span><br><span class="line">SET lock_key unique_value NX EX 30</span><br></pre></td></tr></table></figure><p><strong>参数说明</strong>：</p><ul><li><code>NX</code>：key不存在时才设置（互斥性）</li><li><code>EX 30</code>：30秒自动过期（死锁避免）</li><li><code>unique_value</code>：线程唯一标识（安全性）</li></ul><p><strong>释放锁</strong>：</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 原子性脚本：先验证再删除</span></span><br><span class="line"><span class="keyword">if</span> redis.call(<span class="string">&quot;get&quot;</span>, KEYS[<span class="number">1</span>]) == ARGV[<span class="number">1</span>] <span class="keyword">then</span></span><br><span class="line">    <span class="keyword">return</span> redis.call(<span class="string">&quot;del&quot;</span>, KEYS[<span class="number">1</span>])</span><br><span class="line"><span class="keyword">else</span></span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span></span><br><span class="line"><span class="keyword">end</span></span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="redis-distributed-lock-2"><p><strong>SimpleRedisLock实现</strong></p><p><strong>锁接口定义</strong>：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">ILock</span> &#123;</span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 尝试获取锁</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> timeoutSec 锁的超时时间（秒）</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span> true-获取成功，false-获取失败</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="type">boolean</span> <span class="title function_">tryLock</span><span class="params">(<span class="type">long</span> timeoutSec)</span>;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 释放锁</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">void</span> <span class="title function_">unlock</span><span class="params">()</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>核心实现</strong>：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">SimpleRedisLock</span> <span class="keyword">implements</span> <span class="title class_">ILock</span> &#123;</span><br><span class="line">    <span class="comment">//锁的前缀</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">String</span> <span class="variable">KEY_PREFIX</span> <span class="operator">=</span> <span class="string">&quot;lock:&quot;</span>;</span><br><span class="line">    <span class="comment">//具体业务名称，将前缀和业务名拼接之后当做Key</span></span><br><span class="line">    <span class="keyword">private</span> String name;</span><br><span class="line">    <span class="comment">//这里不是@Autowired注入，采用的是构造器注入，在创建SimpleRedisLock时，将RedisTemplate作为参数传入</span></span><br><span class="line">    <span class="keyword">private</span> StringRedisTemplate stringRedisTemplate;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="title function_">SimpleRedisLock</span><span class="params">(String name, StringRedisTemplate stringRedisTemplate)</span> &#123;</span><br><span class="line">        <span class="built_in">this</span>.name = name;</span><br><span class="line">        <span class="built_in">this</span>.stringRedisTemplate = stringRedisTemplate;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="type">boolean</span> <span class="title function_">tryLock</span><span class="params">(<span class="type">long</span> timeoutSec)</span> &#123;</span><br><span class="line">        <span class="comment">//获取线程标识</span></span><br><span class="line">        <span class="type">long</span> <span class="variable">threadId</span> <span class="operator">=</span> Thread.currentThread().getId();</span><br><span class="line">        <span class="comment">//获取锁，使用SETNX方法进行加锁，同时设置过期时间，防止死锁</span></span><br><span class="line">        <span class="type">Boolean</span> <span class="variable">success</span> <span class="operator">=</span> stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId + <span class="string">&quot;&quot;</span>, timeoutSec, TimeUnit.SECONDS);</span><br><span class="line">        <span class="comment">//自动拆箱可能会出现null，这样写更稳妥</span></span><br><span class="line">        <span class="keyword">return</span> Boolean.TRUE.equals(success);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">unlock</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="comment">//通过DEL来删除锁</span></span><br><span class="line">        stringRedisTemplate.delete(KEY_PREFIX + name);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="redis-distributed-lock-3"><p><strong>秒杀业务改造</strong></p><p><strong>集成分布式锁</strong>：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">seckillVoucher</span><span class="params">(Long voucherId)</span> &#123;</span><br><span class="line">    <span class="comment">// ... 前置校验代码 ...</span></span><br><span class="line">    </span><br><span class="line">    <span class="type">Long</span> <span class="variable">userId</span> <span class="operator">=</span> UserHolder.getUser().getId();</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 创建分布式锁对象</span></span><br><span class="line">    <span class="type">SimpleRedisLock</span> <span class="variable">lock</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">SimpleRedisLock</span>(<span class="string">&quot;order:&quot;</span> + userId, stringRedisTemplate);</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 获取锁</span></span><br><span class="line">    <span class="type">boolean</span> <span class="variable">isLock</span> <span class="operator">=</span> lock.tryLock(<span class="number">1200</span>);</span><br><span class="line">    <span class="keyword">if</span> (!isLock) &#123;</span><br><span class="line">        <span class="keyword">return</span> Result.fail(<span class="string">&quot;不允许重复下单&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">        <span class="comment">// 获取代理对象（事务）</span></span><br><span class="line">        <span class="type">IVoucherOrderService</span> <span class="variable">proxy</span> <span class="operator">=</span> (IVoucherOrderService) AopContext.currentProxy();</span><br><span class="line">        <span class="keyword">return</span> proxy.createVoucherOrder(voucherId);</span><br><span class="line">    &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">        <span class="comment">// 释放锁</span></span><br><span class="line">        lock.unlock();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>使用Jmeter进行压力测试，请求头中携带登录用户的token，最终只能抢到一张优惠券</strong></p><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="redis-distributed-lock-4"><p><strong>锁误删问题分析</strong></p><p><img src="https://markpic.adoreorg.cn/2025/09/Redis/1653385920025.png" alt="1653385920025"></p><pre><code>&lt;div class=&quot;note note-warning&quot;&gt;  &lt;div class=&quot;note-header&quot;&gt;    &lt;i class=&quot;note-icon fa-regular fa-circle-dot&quot;&gt;&lt;/i&gt;    &lt;span class=&quot;note-title&quot;&gt;提示信息&lt;/span&gt;  &lt;/div&gt;  &lt;div class=&quot;note-content&quot;&gt;    &lt;p&gt;&lt;strong&gt;问题场景&lt;/strong&gt;：线程阻塞导致锁超时，其他线程获取锁后，原线程恢复误删锁&lt;/p&gt;  &lt;/div&gt;&lt;/div&gt;</code></pre><p><strong>问题复现</strong>：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">时间线：</span><br><span class="line">t1: 线程A获取锁（锁超时30s）</span><br><span class="line">t2: 线程A业务阻塞（超过30s）</span><br><span class="line">t3: 锁自动过期释放</span><br><span class="line">t4: 线程B获取锁成功</span><br><span class="line">t5: 线程A恢复，执行删锁操作（误删线程B的锁）</span><br></pre></td></tr></table></figure><p><strong>解决方案</strong>：</p><ul><li>✅ <strong>唯一标识</strong>：每个线程使用不同的标识</li><li>✅ <strong>验证机制</strong>：删除前验证锁的持有者</li><li>✅ <strong>原子操作</strong>：使用Lua脚本保证验证+删除的原子性</li></ul><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h3 id="4-3-分布式锁演进">4.3 分布式锁演进</h3><div class="tabs" id="distributed-lock-evolution"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#distributed-lock-evolution-1">基础版本</button></li><li class="tab"><button type="button" data-href="#distributed-lock-evolution-2">标识版本</button></li><li class="tab"><button type="button" data-href="#distributed-lock-evolution-3">Lua脚本版本</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="distributed-lock-evolution-1"><p><strong>版本一：基础实现</strong></p><pre><code>&lt;div class=&quot;note note-primary&quot;&gt;  &lt;div class=&quot;note-header&quot;&gt;    &lt;i class=&quot;note-icon fa-regular fa-star&quot;&gt;&lt;/i&gt;    &lt;span class=&quot;note-title&quot;&gt;提示信息&lt;/span&gt;  &lt;/div&gt;  &lt;div class=&quot;note-content&quot;&gt;    &lt;p&gt;&lt;strong&gt;核心功能&lt;/strong&gt;：SETNX + EXPIRE 原子操作&lt;/p&gt;  &lt;/div&gt;&lt;/div&gt;</code></pre><p><strong>问题</strong>：释放锁时可能误删其他线程的锁</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 获取锁（原子性）</span></span><br><span class="line"><span class="type">Boolean</span> <span class="variable">success</span> <span class="operator">=</span> stringRedisTemplate.opsForValue()</span><br><span class="line">    .setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 释放锁（非原子性）</span></span><br><span class="line">stringRedisTemplate.delete(KEY_PREFIX + name);</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="distributed-lock-evolution-2"><p><strong>版本二：线程标识</strong></p><pre><code>&lt;div class=&quot;note note-warning&quot;&gt;  &lt;div class=&quot;note-header&quot;&gt;    &lt;i class=&quot;note-icon fa-regular fa-circle-dot&quot;&gt;&lt;/i&gt;    &lt;span class=&quot;note-title&quot;&gt;提示信息&lt;/span&gt;  &lt;/div&gt;  &lt;div class=&quot;note-content&quot;&gt;    &lt;p&gt;&lt;strong&gt;改进&lt;/strong&gt;：增加线程唯一标识，防止误删&lt;/p&gt;  &lt;/div&gt;&lt;/div&gt;</code></pre><p><strong>实现逻辑</strong>：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">SimpleRedisLock</span> <span class="keyword">implements</span> <span class="title class_">ILock</span> &#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">String</span> <span class="variable">KEY_PREFIX</span> <span class="operator">=</span> <span class="string">&quot;lock:&quot;</span>;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">String</span> <span class="variable">ID_PREFIX</span> <span class="operator">=</span> UUID.randomUUID().toString(<span class="literal">true</span>) + <span class="string">&quot;-&quot;</span>;</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">private</span> String name;</span><br><span class="line">    <span class="keyword">private</span> StringRedisTemplate stringRedisTemplate;</span><br><span class="line">    </span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="type">boolean</span> <span class="title function_">tryLock</span><span class="params">(<span class="type">long</span> timeoutSec)</span> &#123;</span><br><span class="line">        <span class="comment">// 获取线程唯一标识</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">threadId</span> <span class="operator">=</span> ID_PREFIX + Thread.currentThread().getId();</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 获取锁（原子性操作）</span></span><br><span class="line">        <span class="type">Boolean</span> <span class="variable">success</span> <span class="operator">=</span> stringRedisTemplate.opsForValue()</span><br><span class="line">            .setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);</span><br><span class="line">        </span><br><span class="line">        <span class="keyword">return</span> Boolean.TRUE.equals(success);</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">unlock</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="comment">// 获取线程标识</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">threadId</span> <span class="operator">=</span> ID_PREFIX + Thread.currentThread().getId();</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 获取锁中的标识</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">id</span> <span class="operator">=</span> stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 验证是否为自己的锁</span></span><br><span class="line">        <span class="keyword">if</span> (threadId.equals(id)) &#123;</span><br><span class="line">            <span class="comment">// 释放锁</span></span><br><span class="line">            stringRedisTemplate.delete(KEY_PREFIX + name);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>新问题</strong>：<strong>验证和删除非原子性</strong>，仍存在竞态条件</p><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="distributed-lock-evolution-3"><p><strong>版本三：Lua脚本</strong></p><pre><code>&lt;div class=&quot;note note-success&quot;&gt;  &lt;div class=&quot;note-header&quot;&gt;    &lt;i class=&quot;note-icon fa-regular fa-circle-check&quot;&gt;&lt;/i&gt;    &lt;span class=&quot;note-title&quot;&gt;提示信息&lt;/span&gt;  &lt;/div&gt;  &lt;div class=&quot;note-content&quot;&gt;    &lt;p&gt;&lt;strong&gt;最终方案&lt;/strong&gt;：Lua脚本保证&lt;strong&gt;验证+删除&lt;/strong&gt;的原子性&lt;/p&gt;  &lt;/div&gt;&lt;/div&gt;</code></pre><p><strong>Lua脚本实现</strong>：</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 释放锁脚本（unlock.lua）</span></span><br><span class="line"><span class="keyword">if</span> redis.call(<span class="string">&quot;get&quot;</span>, KEYS[<span class="number">1</span>]) == ARGV[<span class="number">1</span>] <span class="keyword">then</span></span><br><span class="line">    <span class="keyword">return</span> redis.call(<span class="string">&quot;del&quot;</span>, KEYS[<span class="number">1</span>])</span><br><span class="line"><span class="keyword">else</span></span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span></span><br><span class="line"><span class="keyword">end</span></span><br></pre></td></tr></table></figure><p><strong>Java调用</strong>：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> DefaultRedisScript&lt;Long&gt; UNLOCK_SCRIPT;</span><br><span class="line"></span><br><span class="line"><span class="keyword">static</span> &#123;</span><br><span class="line">    UNLOCK_SCRIPT = <span class="keyword">new</span> <span class="title class_">DefaultRedisScript</span>&lt;&gt;();</span><br><span class="line">    UNLOCK_SCRIPT.setLocation(<span class="keyword">new</span> <span class="title class_">ClassPathResource</span>(<span class="string">&quot;unlock.lua&quot;</span>));</span><br><span class="line">    UNLOCK_SCRIPT.setResultType(Long.class);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">unlock</span><span class="params">()</span> &#123;</span><br><span class="line">    stringRedisTemplate.execute(</span><br><span class="line">        UNLOCK_SCRIPT,</span><br><span class="line">        Collections.singletonList(KEY_PREFIX + name),</span><br><span class="line">        ID_PREFIX + Thread.currentThread().getId()</span><br><span class="line">    );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h3 id="4-4-Lua脚本详解">4.4 Lua脚本详解</h3><div class='liushen-tag-link'><a class="tag-Link" target="_blank" href=" https://www.runoob.com/lua/lua-tutorial.html">    <div class="tag-link-tips">🪧引用站外地址，不保证站点的可用性和安全性</div>    <div class="tag-link-bottom">        <div class="tag-link-left" style="background-image: url(https://lsky.adoreorg.cn/i/2026/06/08/6a264792045e8.webp);"></div>        <div class="tag-link-right">            <div class="tag-link-title">Lua脚本详解</div>            <div class="tag-link-sitename"> https://source.adoreorg.cn/webp/icon/66a4632bbf06e.webp</div>        </div>        <i class="fa-solid fa-angle-right"></i>    </div>    </a></div><div class="tabs" id="lua-script-detail"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#lua-script-detail-1">基础语法</button></li><li class="tab"><button type="button" data-href="#lua-script-detail-2">脚本调用</button></li><li class="tab"><button type="button" data-href="#lua-script-detail-3">原子性保证</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="lua-script-detail-1"><p><strong>Redis Lua脚本基础</strong></p><pre><code>&lt;div class=&quot;note note-info&quot;&gt;  &lt;div class=&quot;note-header&quot;&gt;    &lt;i class=&quot;note-icon fa-regular fa-circle-info&quot;&gt;&lt;/i&gt;    &lt;span class=&quot;note-title&quot;&gt;提示信息&lt;/span&gt;  &lt;/div&gt;  &lt;div class=&quot;note-content&quot;&gt;    &lt;p&gt;&lt;strong&gt;Lua脚本&lt;/strong&gt;：在Redis服务器端执行的脚本，保证&lt;strong&gt;多条命令原子性&lt;/strong&gt;&lt;/p&gt;  &lt;/div&gt;&lt;/div&gt;</code></pre><p><strong>基本语法</strong>：</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- Redis命令调用</span></span><br><span class="line">redis.call(<span class="string">&#x27;命令名称&#x27;</span>, <span class="string">&#x27;key&#x27;</span>, <span class="string">&#x27;其它参数&#x27;</span>, ...)</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 示例：set name jack</span></span><br><span class="line">redis.call(<span class="string">&#x27;set&#x27;</span>, <span class="string">&#x27;name&#x27;</span>, <span class="string">&#x27;jack&#x27;</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 示例：先set再get</span></span><br><span class="line">redis.call(<span class="string">&#x27;set&#x27;</span>, <span class="string">&#x27;name&#x27;</span>, <span class="string">&#x27;Rose&#x27;</span>)</span><br><span class="line"><span class="keyword">local</span> name = redis.call(<span class="string">&#x27;get&#x27;</span>, <span class="string">&#x27;name&#x27;</span>)</span><br><span class="line"><span class="keyword">return</span> name</span><br></pre></td></tr></table></figure><p><strong>参数传递</strong>：</p><ul><li><code>KEYS数组</code>：接收key类型参数</li><li><code>ARGV数组</code>：接收其他参数</li></ul><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="lua-script-detail-2"><p><strong>Redis脚本调用</strong></p><p><strong>命令行调用</strong>：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 执行脚本</span></span><br><span class="line">EVAL <span class="string">&quot;redis.call(&#x27;set&#x27;, &#x27;name&#x27;, &#x27;jack&#x27;)&quot;</span> 0</span><br><span class="line"></span><br><span class="line"><span class="comment"># 带参数调用</span></span><br><span class="line">EVAL <span class="string">&quot;redis.call(&#x27;set&#x27;, KEYS[1], ARGV[1])&quot;</span> 1 name Rose</span><br></pre></td></tr></table></figure><p><strong>Java调用</strong>：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 脚本定义</span></span><br><span class="line">DefaultRedisScript&lt;Long&gt; script = <span class="keyword">new</span> <span class="title class_">DefaultRedisScript</span>&lt;&gt;();</span><br><span class="line">script.setScriptText(<span class="string">&quot;redis.call(&#x27;set&#x27;, KEYS[1], ARGV[1])&quot;</span>);</span><br><span class="line">script.setResultType(Long.class);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 执行脚本</span></span><br><span class="line">redisTemplate.execute(script, </span><br><span class="line">    Collections.singletonList(<span class="string">&quot;name&quot;</span>), </span><br><span class="line">    <span class="string">&quot;Jack&quot;</span>);</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="lua-script-detail-3"><p><strong>原子性保证机制</strong></p><pre><code>&lt;div class=&quot;note note-success&quot;&gt;  &lt;div class=&quot;note-header&quot;&gt;    &lt;i class=&quot;note-icon fa-regular fa-circle-check&quot;&gt;&lt;/i&gt;    &lt;span class=&quot;note-title&quot;&gt;提示信息&lt;/span&gt;  &lt;/div&gt;  &lt;div class=&quot;note-content&quot;&gt;    &lt;p&gt;&lt;strong&gt;Redis单线程模型&lt;/strong&gt;：Lua脚本执行期间，Redis不会执行其他命令&lt;/p&gt;  &lt;/div&gt;&lt;/div&gt;</code></pre><p><strong>原子性验证</strong>：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">线程A：执行Lua脚本（验证+删除）</span><br><span class="line">Redis：脚本执行期间，线程B的请求排队等待</span><br><span class="line">结果：保证验证和删除操作的原子性</span><br></pre></td></tr></table></figure><p><strong>性能优势</strong>：</p><ul><li>✅ <strong>网络开销少</strong>：一次交互完成多个操作</li><li>✅ <strong>原子性保证</strong>：Redis单线程执行</li><li>✅ <strong>减少竞态</strong>：避免客户端并发问题</li></ul><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h3 id="4-5-分布式锁总结">4.5 分布式锁总结</h3><div class="tabs" id="distributed-lock-summary"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#distributed-lock-summary-1">实现演进</button></li><li class="tab"><button type="button" data-href="#distributed-lock-summary-2">核心特性</button></li><li class="tab"><button type="button" data-href="#distributed-lock-summary-3">最佳实践</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="distributed-lock-summary-1"><p><strong>分布式锁演进历程</strong></p><pre><code>&lt;div class=&quot;note note-primary&quot;&gt;  &lt;div class=&quot;note-header&quot;&gt;    &lt;i class=&quot;note-icon fa-regular fa-star&quot;&gt;&lt;/i&gt;    &lt;span class=&quot;note-title&quot;&gt;提示信息&lt;/span&gt;  &lt;/div&gt;  &lt;div class=&quot;note-content&quot;&gt;    &lt;p&gt;&lt;strong&gt;从简单到完善的演进过程&lt;/strong&gt;&lt;/p&gt;  &lt;/div&gt;&lt;/div&gt;</code></pre><table><thead><tr><th>版本</th><th>实现方式</th><th>解决问题</th><th>存在问题</th></tr></thead><tbody><tr><td><strong>V1</strong></td><td>SETNX + DEL</td><td>基本互斥</td><td>误删锁、死锁</td></tr><tr><td><strong>V2</strong></td><td>SET NX EX + 线程标识</td><td>死锁避免</td><td>误删问题</td></tr><tr><td><strong>V3</strong></td><td>线程标识 + 验证删除</td><td>防误删</td><td>原子性问题</td></tr><tr><td><strong>V4</strong></td><td>Lua脚本原子操作</td><td>原子性保证</td><td>功能单一</td></tr><tr><td><strong>V5</strong></td><td>Redisson框架</td><td>完整功能</td><td>依赖第三方</td></tr></tbody></table><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="distributed-lock-summary-2"><p><strong>Redis分布式锁核心特性</strong></p><pre><code>&lt;div class=&quot;note note-success&quot;&gt;  &lt;div class=&quot;note-header&quot;&gt;    &lt;i class=&quot;note-icon fa-regular fa-circle-check&quot;&gt;&lt;/i&gt;    &lt;span class=&quot;note-title&quot;&gt;提示信息&lt;/span&gt;  &lt;/div&gt;  &lt;div class=&quot;note-content&quot;&gt;    &lt;p&gt;&lt;strong&gt;企业级实现要求&lt;/strong&gt;&lt;/p&gt;  &lt;/div&gt;&lt;/div&gt;</code></pre><p><strong>基本特性</strong>：</p><ul><li>✅ <strong>互斥性</strong>：SETNX保证同一时间只有一个客户端获取锁</li><li>✅ <strong>死锁避免</strong>：EXPIRE设置超时时间，防止死锁</li><li>✅ <strong>安全性</strong>：线程唯一标识，防止误删</li><li>✅ <strong>原子性</strong>：Lua脚本保证操作原子性</li></ul><p><strong>高级特性</strong>（Redisson提供）：</p><ul><li>🔄 <strong>可重入性</strong>：同一线程可重复获取锁</li><li>⏰ <strong>锁续期</strong>：WatchDog自动续期，防止业务未执行完锁过期</li><li>🔄 <strong>可重试</strong>：获取锁失败可自动重试</li><li>🏗️ <strong>主从一致性</strong>：RedLock算法保证主从一致性</li></ul><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="distributed-lock-summary-3"><p><strong>使用建议</strong></p><pre><code>&lt;div class=&quot;note note-warning&quot;&gt;  &lt;div class=&quot;note-header&quot;&gt;    &lt;i class=&quot;note-icon fa-regular fa-circle-dot&quot;&gt;&lt;/i&gt;    &lt;span class=&quot;note-title&quot;&gt;提示信息&lt;/span&gt;  &lt;/div&gt;  &lt;div class=&quot;note-content&quot;&gt;    &lt;p&gt;&lt;strong&gt;不同场景的选择策略&lt;/strong&gt;&lt;/p&gt;  &lt;/div&gt;&lt;/div&gt;</code></pre><p><strong>简单场景</strong>：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 简单分布式锁（适合低频操作）</span></span><br><span class="line"><span class="type">String</span> <span class="variable">lockKey</span> <span class="operator">=</span> <span class="string">&quot;lock:business:&quot;</span> + userId;</span><br><span class="line"><span class="type">String</span> <span class="variable">threadId</span> <span class="operator">=</span> UUID.randomUUID().toString();</span><br><span class="line"><span class="type">boolean</span> <span class="variable">locked</span> <span class="operator">=</span> redisTemplate.opsForValue()</span><br><span class="line">    .setIfAbsent(lockKey, threadId, <span class="number">30</span>, TimeUnit.SECONDS);</span><br></pre></td></tr></table></figure><p><strong>复杂场景</strong>：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Redisson（适合高频并发）</span></span><br><span class="line"><span class="type">RLock</span> <span class="variable">lock</span> <span class="operator">=</span> redissonClient.getLock(<span class="string">&quot;lock:order:&quot;</span> + userId);</span><br><span class="line"><span class="comment">// 可重入、可续期、可重试</span></span><br><span class="line"><span class="type">boolean</span> <span class="variable">locked</span> <span class="operator">=</span> lock.tryLock(<span class="number">1</span>, <span class="number">30</span>, TimeUnit.SECONDS);</span><br></pre></td></tr></table></figure><p><strong>选择原则</strong>：</p><ul><li><strong>低频简单操作</strong>：手写Redis分布式锁</li><li><strong>高频并发场景</strong>：Redisson框架</li><li><strong>强一致性要求</strong>：Zookeeper分布式锁</li></ul><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h2 id="5-分布式锁-redission">5. 分布式锁-redission</h2><h3 id="5-1-Redisson概述">5.1 Redisson概述</h3><div class="tabs" id="redisson-overview"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#redisson-overview-1">基本介绍</button></li><li class="tab"><button type="button" data-href="#redisson-overview-2">问题解决</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="redisson-overview-1"><p><strong>什么是Redisson</strong></p><pre><code>&lt;div class=&quot;note note-primary&quot;&gt;  &lt;div class=&quot;note-header&quot;&gt;    &lt;i class=&quot;note-icon fa-regular fa-star&quot;&gt;&lt;/i&gt;    &lt;span class=&quot;note-title&quot;&gt;提示信息&lt;/span&gt;  &lt;/div&gt;  &lt;div class=&quot;note-content&quot;&gt;    &lt;p&gt;&lt;strong&gt;Redisson&lt;/strong&gt;：基于Redis的Java驻内存数据网格（In-Memory Data Grid）&lt;/p&gt;  &lt;/div&gt;&lt;/div&gt;</code></pre><p><strong>核心功能</strong>：</p><ul><li>🔒 <strong>分布式锁</strong>：可重入锁、公平锁、联锁、红锁等</li><li>📦 <strong>分布式对象</strong>：Object、List、Set、Map、Queue等</li><li>🔄 <strong>分布式服务</strong>：远程服务、消息服务、执行器服务等</li></ul><p><strong>优势特点</strong>：</p><ul><li>✅ <strong>功能完善</strong>：提供各种分布式锁实现</li><li>✅ <strong>可重入性</strong>：支持同一线程重复获取锁</li><li>✅ <strong>自动续期</strong>：WatchDog机制自动延长锁有效期</li><li>✅ <strong>高可用</strong>：支持主从、哨兵、集群模式</li></ul><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="redisson-overview-2"><p><strong>解决手写Redis锁的问题</strong></p><pre><code>&lt;div class=&quot;note note-warning&quot;&gt;  &lt;div class=&quot;note-header&quot;&gt;    &lt;i class=&quot;note-icon fa-regular fa-circle-dot&quot;&gt;&lt;/i&gt;    &lt;span class=&quot;note-title&quot;&gt;提示信息&lt;/span&gt;  &lt;/div&gt;  &lt;div class=&quot;note-content&quot;&gt;    &lt;p&gt;&lt;strong&gt;手写Redis分布式锁的局限性&lt;/strong&gt;&lt;/p&gt;  &lt;/div&gt;&lt;/div&gt;</code></pre><table><thead><tr><th>问题</th><th>手写Redis锁</th><th>Redisson解决方案</th></tr></thead><tbody><tr><td><strong>不可重入</strong></td><td>同一线程无法重复获取锁</td><td>内置可重入机制</td></tr><tr><td><strong>不可重试</strong></td><td>获取失败只能放弃</td><td>支持获取锁超时重试</td></tr><tr><td><strong>锁续期</strong></td><td>固定过期时间</td><td>WatchDog自动续期</td></tr><tr><td><strong>主从一致性</strong></td><td>主从切换可能丢锁</td><td>RedLock算法</td></tr></tbody></table><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h3 id="5-2-Redisson快速入门">5.2 Redisson快速入门</h3><div class="tabs" id="redisson-quickstart"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#redisson-quickstart-1">依赖配置</button></li><li class="tab"><button type="button" data-href="#redisson-quickstart-2">客户端配置</button></li><li class="tab"><button type="button" data-href="#redisson-quickstart-3">基本使用</button></li><li class="tab"><button type="button" data-href="#redisson-quickstart-4">业务集成</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="redisson-quickstart-1"><p><strong>Maven依赖</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">&lt;dependency&gt;</span><br><span class="line">    &lt;groupId&gt;org.redisson&lt;/groupId&gt;</span><br><span class="line">    &lt;artifactId&gt;redisson&lt;/artifactId&gt;</span><br><span class="line">    &lt;version&gt;<span class="number">3.13</span><span class="number">.6</span>&lt;/version&gt;</span><br><span class="line">&lt;/dependency&gt;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="redisson-quickstart-2"><p><strong>Redisson客户端配置</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Configuration</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">RedissonConfig</span> &#123;</span><br><span class="line">    </span><br><span class="line">    <span class="meta">@Bean</span></span><br><span class="line">    <span class="keyword">public</span> RedissonClient <span class="title function_">redissonClient</span><span class="params">()</span>&#123;</span><br><span class="line">        <span class="comment">// 配置</span></span><br><span class="line">        <span class="type">Config</span> <span class="variable">config</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Config</span>();</span><br><span class="line">        config.useSingleServer()</span><br><span class="line">            .setAddress(<span class="string">&quot;redis://192.168.xxx.101:6379&quot;</span>)</span><br><span class="line">            .setPassword(<span class="string">&quot;123321&quot;</span>);</span><br><span class="line">        <span class="comment">// 创建RedissonClient对象</span></span><br><span class="line">        <span class="keyword">return</span> Redisson.create(config);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="redisson-quickstart-3"><p><strong>分布式锁使用示例</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Resource</span></span><br><span class="line"><span class="keyword">private</span> RedissonClient redissonClient;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Test</span></span><br><span class="line"><span class="keyword">void</span> <span class="title function_">testRedisson</span><span class="params">()</span> <span class="keyword">throws</span> Exception&#123;</span><br><span class="line">    <span class="comment">// 获取锁(可重入)，指定锁的名称</span></span><br><span class="line">    <span class="type">RLock</span> <span class="variable">lock</span> <span class="operator">=</span> redissonClient.getLock(<span class="string">&quot;anyLock&quot;</span>);</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 尝试获取锁，参数分别是：获取锁的最大等待时间(期间会重试)，锁自动释放时间，时间单位</span></span><br><span class="line">    <span class="type">boolean</span> <span class="variable">isLock</span> <span class="operator">=</span> lock.tryLock(<span class="number">1</span>, <span class="number">10</span>, TimeUnit.SECONDS);</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 判断获取锁成功</span></span><br><span class="line">    <span class="keyword">if</span>(isLock)&#123;</span><br><span class="line">        <span class="keyword">try</span>&#123;</span><br><span class="line">            System.out.println(<span class="string">&quot;执行业务&quot;</span>);</span><br><span class="line">        &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">            <span class="comment">// 释放锁</span></span><br><span class="line">            lock.unlock();</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="redisson-quickstart-4"><p><strong>秒杀业务集成Redisson锁</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Service</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">VoucherOrderServiceImpl</span> <span class="keyword">implements</span> <span class="title class_">IVoucherOrderService</span> &#123;</span><br><span class="line">    </span><br><span class="line">    <span class="meta">@Resource</span></span><br><span class="line">    <span class="keyword">private</span> RedissonClient redissonClient;</span><br><span class="line">    </span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> Result <span class="title function_">seckillVoucher</span><span class="params">(Long voucherId)</span> &#123;</span><br><span class="line">        <span class="comment">// 1.查询优惠券信息</span></span><br><span class="line">        <span class="type">SeckillVoucher</span> <span class="variable">voucher</span> <span class="operator">=</span> seckillVoucherService.getById(voucherId);</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 2.判断秒杀是否开始</span></span><br><span class="line">        <span class="keyword">if</span> (voucher.getBeginTime().isAfter(LocalDateTime.now())) &#123;</span><br><span class="line">            <span class="keyword">return</span> Result.fail(<span class="string">&quot;秒杀尚未开始！&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 3.判断秒杀是否已经结束</span></span><br><span class="line">        <span class="keyword">if</span> (voucher.getEndTime().isBefore(LocalDateTime.now())) &#123;</span><br><span class="line">            <span class="keyword">return</span> Result.fail(<span class="string">&quot;秒杀已经结束！&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 4.判断库存是否充足</span></span><br><span class="line">        <span class="keyword">if</span> (voucher.getStock() &lt; <span class="number">1</span>) &#123;</span><br><span class="line">            <span class="keyword">return</span> Result.fail(<span class="string">&quot;库存不足！&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line">        </span><br><span class="line">        <span class="type">Long</span> <span class="variable">userId</span> <span class="operator">=</span> UserHolder.getUser().getId();</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 5.创建锁对象（使用Redisson分布式锁）</span></span><br><span class="line">        <span class="type">RLock</span> <span class="variable">lock</span> <span class="operator">=</span> redissonClient.getLock(<span class="string">&quot;lock:order:&quot;</span> + userId);</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 6.获取锁</span></span><br><span class="line">        <span class="type">boolean</span> <span class="variable">isLock</span> <span class="operator">=</span> lock.tryLock();</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 7.判断是否获取锁成功</span></span><br><span class="line">        <span class="keyword">if</span> (!isLock) &#123;</span><br><span class="line">            <span class="keyword">return</span> Result.fail(<span class="string">&quot;不允许重复下单&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line">        </span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            <span class="comment">// 8.获取代理对象（事务）</span></span><br><span class="line">            <span class="type">IVoucherOrderService</span> <span class="variable">proxy</span> <span class="operator">=</span> (IVoucherOrderService) AopContext.currentProxy();</span><br><span class="line">            <span class="keyword">return</span> proxy.createVoucherOrder(voucherId);</span><br><span class="line">        &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">            <span class="comment">// 9.释放锁</span></span><br><span class="line">            lock.unlock();</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h3 id="5-3-Redisson分布式锁详解">5.3 Redisson分布式锁详解</h3><h4 id="5-3-1-Redisson可重入锁原理">5.3.1 Redisson可重入锁原理</h4><div class="tabs" id="redisson-reentrant"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#redisson-reentrant-1">可重入锁原理</button></li><li class="tab"><button type="button" data-href="#redisson-reentrant-2">数据结构</button></li><li class="tab"><button type="button" data-href="#redisson-reentrant-3">获取锁逻辑</button></li><li class="tab"><button type="button" data-href="#redisson-reentrant-4">释放锁逻辑</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="redisson-reentrant-1"><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Resource</span></span><br><span class="line"><span class="keyword">private</span> RedissonClient redissonClient;</span><br><span class="line"></span><br><span class="line"><span class="keyword">private</span> RLock lock;</span><br><span class="line"></span><br><span class="line"><span class="meta">@BeforeEach</span></span><br><span class="line"><span class="keyword">void</span> <span class="title function_">setUp</span><span class="params">()</span> &#123;</span><br><span class="line">    lock = redissonClient.getLock(<span class="string">&quot;lock&quot;</span>);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Test</span></span><br><span class="line"><span class="keyword">void</span> <span class="title function_">method1</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="type">boolean</span> <span class="variable">success</span> <span class="operator">=</span> lock.tryLock();</span><br><span class="line">    <span class="keyword">if</span> (!success) &#123;</span><br><span class="line">        log.error(<span class="string">&quot;获取锁失败，1&quot;</span>);</span><br><span class="line">        <span class="keyword">return</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">        log.info(<span class="string">&quot;获取锁成功&quot;</span>);</span><br><span class="line">        method2();</span><br><span class="line">    &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">        log.info(<span class="string">&quot;释放锁，1&quot;</span>);</span><br><span class="line">        lock.unlock();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">void</span> <span class="title function_">method2</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="type">RLock</span> <span class="variable">lock</span> <span class="operator">=</span> redissonClient.getLock(<span class="string">&quot;lock&quot;</span>);</span><br><span class="line">    <span class="type">boolean</span> <span class="variable">success</span> <span class="operator">=</span> lock.tryLock();</span><br><span class="line">    <span class="keyword">if</span> (!success) &#123;</span><br><span class="line">        log.error(<span class="string">&quot;获取锁失败，2&quot;</span>);</span><br><span class="line">        <span class="keyword">return</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">        log.info(<span class="string">&quot;获取锁成功，2&quot;</span>);</span><br><span class="line">    &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">        log.info(<span class="string">&quot;释放锁，2&quot;</span>);</span><br><span class="line">        lock.unlock();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>同一线程内方法调用时，若method1已持有锁，method2需获取同一把锁，通过判断线程ID实现可重入：state+1获取锁，state-1释放锁，减至0时真正释放</strong></p><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="redisson-reentrant-2"><p><strong>Redis中的锁存储结构</strong></p><pre><code>&lt;div class=&quot;note note-primary&quot;&gt;  &lt;div class=&quot;note-header&quot;&gt;    &lt;i class=&quot;note-icon fa-regular fa-star&quot;&gt;&lt;/i&gt;    &lt;span class=&quot;note-title&quot;&gt;提示信息&lt;/span&gt;  &lt;/div&gt;  &lt;div class=&quot;note-content&quot;&gt;    &lt;p&gt;&lt;strong&gt;Hash结构存储可重入锁信息&lt;/strong&gt;&lt;/p&gt;  &lt;/div&gt;&lt;/div&gt;</code></pre><p><strong>存储格式</strong>：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">Key: lock_name (锁名称)</span><br><span class="line">Value: Hash结构</span><br><span class="line">├─ field: UUID + &quot;:&quot; + threadId (线程唯一标识)</span><br><span class="line">└─ value: 重入次数 (整数)</span><br></pre></td></tr></table></figure><p><strong>示例</strong>：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">lock:order:12345</span><br><span class="line">├─ &quot;8f3e2a1c:1&quot; : 2  (线程1重入了2次)</span><br><span class="line">└─ &quot;9d4c5b2a:2&quot; : 1  (线程2重入了1次)</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="redisson-reentrant-3"><p><strong>可重入锁获取Lua脚本</strong></p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- KEYS[1]: 锁名称</span></span><br><span class="line"><span class="comment">-- ARGV[1]: 锁失效时间(毫秒)</span></span><br><span class="line"><span class="comment">-- ARGV[2]: 线程标识(UUID + &quot;:&quot; + threadId)</span></span><br><span class="line"></span><br><span class="line"><span class="comment">-- 锁不存在，创建新锁</span></span><br><span class="line"><span class="keyword">if</span> (redis.call(<span class="string">&#x27;exists&#x27;</span>, KEYS[<span class="number">1</span>]) == <span class="number">0</span>) <span class="keyword">then</span></span><br><span class="line">    redis.call(<span class="string">&#x27;hset&#x27;</span>, KEYS[<span class="number">1</span>], ARGV[<span class="number">2</span>], <span class="number">1</span>);</span><br><span class="line">    redis.call(<span class="string">&#x27;pexpire&#x27;</span>, KEYS[<span class="number">1</span>], ARGV[<span class="number">1</span>]);</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">nil</span>;</span><br><span class="line"><span class="keyword">end</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 锁存在且是当前线程持有，重入次数+1</span></span><br><span class="line"><span class="keyword">if</span> (redis.call(<span class="string">&#x27;hexists&#x27;</span>, KEYS[<span class="number">1</span>], ARGV[<span class="number">2</span>]) == <span class="number">1</span>) <span class="keyword">then</span></span><br><span class="line">    redis.call(<span class="string">&#x27;hincrby&#x27;</span>, KEYS[<span class="number">1</span>], ARGV[<span class="number">2</span>], <span class="number">1</span>);</span><br><span class="line">    redis.call(<span class="string">&#x27;pexpire&#x27;</span>, KEYS[<span class="number">1</span>], ARGV[<span class="number">1</span>]);</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">nil</span>;</span><br><span class="line"><span class="keyword">end</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 锁存在但不是当前线程持有，返回锁剩余时间</span></span><br><span class="line"><span class="keyword">return</span> redis.call(<span class="string">&#x27;pttl&#x27;</span>, KEYS[<span class="number">1</span>]);</span><br></pre></td></tr></table></figure><p><strong>脚本逻辑</strong>：</p><ol><li><strong>锁不存在</strong>：创建新锁，重入次数设为1</li><li><strong>当前线程重入</strong>：重入次数+1，重置过期时间</li><li><strong>其他线程持有</strong>：返回锁剩余时间，抢锁失败</li></ol><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="redisson-reentrant-4"><p><strong>可重入锁释放Lua脚本</strong></p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- KEYS[1]: 锁名称</span></span><br><span class="line"><span class="comment">-- ARGV[1]: 线程标识</span></span><br><span class="line"><span class="comment">-- ARGV[2]: 锁失效时间</span></span><br><span class="line"></span><br><span class="line"><span class="comment">-- 锁不存在，直接返回</span></span><br><span class="line"><span class="keyword">if</span> (redis.call(<span class="string">&#x27;hexists&#x27;</span>, KEYS[<span class="number">1</span>], ARGV[<span class="number">1</span>]) == <span class="number">0</span>) <span class="keyword">then</span></span><br><span class="line">    <span class="keyword">return</span> <span class="literal">nil</span>;</span><br><span class="line"><span class="keyword">end</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 重入次数-1</span></span><br><span class="line">counter = redis.call(<span class="string">&#x27;hincrby&#x27;</span>, KEYS[<span class="number">1</span>], ARGV[<span class="number">1</span>], <span class="number">-1</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 重入次数为0，删除锁</span></span><br><span class="line"><span class="keyword">if</span> (counter &gt; <span class="number">0</span>) <span class="keyword">then</span></span><br><span class="line">    redis.call(<span class="string">&#x27;pexpire&#x27;</span>, KEYS[<span class="number">1</span>], ARGV[<span class="number">2</span>]);</span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line"><span class="keyword">else</span></span><br><span class="line">    redis.call(<span class="string">&#x27;del&#x27;</span>, KEYS[<span class="number">1</span>]);</span><br><span class="line">    <span class="keyword">return</span> <span class="number">1</span>;</span><br><span class="line"><span class="keyword">end</span>;</span><br></pre></td></tr></table></figure><p><strong>释放逻辑</strong>：</p><ul><li><strong>重入次数&gt;0</strong>：仅减少重入次数，不删除锁</li><li><strong>重入次数=0</strong>：删除整个锁</li><li><strong>锁不存在</strong>：直接返回，防止误删</li></ul><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h4 id="5-3-2-Redisson锁重试和WatchDog机制">5.3.2 Redisson锁重试和WatchDog机制</h4><div class="tabs" id="redisson-watchdog"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#redisson-watchdog-1">重试机制</button></li><li class="tab"><button type="button" data-href="#redisson-watchdog-2">WatchDog机制</button></li><li class="tab"><button type="button" data-href="#redisson-watchdog-3">使用对比</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="redisson-watchdog-1"><p><strong>锁获取重试机制</strong></p><pre><code>&lt;div class=&quot;note note-primary&quot;&gt;  &lt;div class=&quot;note-header&quot;&gt;    &lt;i class=&quot;note-icon fa-regular fa-star&quot;&gt;&lt;/i&gt;    &lt;span class=&quot;note-title&quot;&gt;提示信息&lt;/span&gt;  &lt;/div&gt;  &lt;div class=&quot;note-content&quot;&gt;    &lt;p&gt;&lt;strong&gt;tryLock方法的重试逻辑&lt;/strong&gt;&lt;/p&gt;  &lt;/div&gt;&lt;/div&gt;</code></pre><p><strong>重试流程</strong>：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 尝试获取锁，最多等待1秒，锁有效期10秒</span></span><br><span class="line"><span class="type">boolean</span> <span class="variable">isLock</span> <span class="operator">=</span> lock.tryLock(<span class="number">1</span>, <span class="number">10</span>, TimeUnit.SECONDS);</span><br></pre></td></tr></table></figure><p><strong>内部实现</strong>：</p><ol><li><strong>首次尝试</strong>：立即执行Lua脚本抢锁</li><li><strong>失败重试</strong>：如果锁被占用，等待锁释放后重试</li><li><strong>超时控制</strong>：在指定等待时间内持续重试</li><li><strong>返回结果</strong>：成功返回true，超时返回false</li></ol><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="redisson-watchdog-2"><p><strong>看门狗自动续期机制</strong></p><pre><code>&lt;div class=&quot;note note-warning&quot;&gt;  &lt;div class=&quot;note-header&quot;&gt;    &lt;i class=&quot;note-icon fa-regular fa-circle-dot&quot;&gt;&lt;/i&gt;    &lt;span class=&quot;note-title&quot;&gt;提示信息&lt;/span&gt;  &lt;/div&gt;  &lt;div class=&quot;note-content&quot;&gt;    &lt;p&gt;&lt;strong&gt;解决业务执行时间超过锁有效期的问题&lt;/strong&gt;&lt;/p&gt;  &lt;/div&gt;&lt;/div&gt;</code></pre><p><strong>续期原理</strong>：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// lock()方法默认开启WatchDog</span></span><br><span class="line">lock.lock(); <span class="comment">// 30秒有效期，WatchDog每10秒续期一次</span></span><br></pre></td></tr></table></figure><p><strong>续期流程</strong>：</p><ol><li><strong>初始有效期</strong>：默认30秒（可配置）</li><li><strong>续期触发</strong>：每<code>有效期/3</code>时间触发一次（默认10秒）</li><li><strong>续期条件</strong>：业务线程仍在运行且持有锁</li><li><strong>续期失败</strong>：线程宕机或锁已释放，停止续期</li></ol><p><strong>代码实现</strong>：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title function_">renewExpiration</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="comment">// 从续约映射中获取当前线程的续约条目</span></span><br><span class="line">    <span class="type">ExpirationEntry</span> <span class="variable">ee</span> <span class="operator">=</span> EXPIRATION_RENEWAL_MAP.get(getEntryName());</span><br><span class="line">    <span class="keyword">if</span> (ee == <span class="literal">null</span>) &#123;</span><br><span class="line">        <span class="keyword">return</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 创建定时任务，internalLockLeaseTime/3后执行</span></span><br><span class="line">    <span class="type">Timeout</span> <span class="variable">task</span> <span class="operator">=</span> commandExecutor.getConnectionManager().newTimeout(</span><br><span class="line">        <span class="keyword">new</span> <span class="title class_">TimerTask</span>() &#123;</span><br><span class="line">            <span class="meta">@Override</span></span><br><span class="line">            <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">run</span><span class="params">(Timeout timeout)</span> <span class="keyword">throws</span> Exception &#123;</span><br><span class="line">                <span class="comment">// 异步续期锁有效期</span></span><br><span class="line">                RFuture&lt;Boolean&gt; future = renewExpirationAsync(threadId);</span><br><span class="line">                future.onComplete((res, e) -&gt; &#123;</span><br><span class="line">                    <span class="keyword">if</span> (res) &#123;</span><br><span class="line">                        <span class="comment">// 续期成功，递归调用继续下一轮续期</span></span><br><span class="line">                        renewExpiration();</span><br><span class="line">                    &#125;</span><br><span class="line">                &#125;);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;, </span><br><span class="line">        internalLockLeaseTime / <span class="number">3</span>,  <span class="comment">// 默认10秒</span></span><br><span class="line">        TimeUnit.MILLISECONDS</span><br><span class="line">    );</span><br><span class="line">    </span><br><span class="line">    ee.setTimeout(task);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="redisson-watchdog-3"><p><strong>不同加锁方式的对比</strong></p><table><thead><tr><th>方法</th><th>是否重试</th><th>是否续期</th><th>适用场景</th></tr></thead><tbody><tr><td><code>tryLock()</code></td><td>❌</td><td>❌</td><td>简单获取，立即返回</td></tr><tr><td><code>tryLock(waitTime, leaseTime, unit)</code></td><td>✅</td><td>❌</td><td>带超时等待，固定有效期</td></tr><tr><td><code>lock()</code></td><td>✅</td><td>✅</td><td>长期持有，自动续期</td></tr><tr><td><code>lock(leaseTime, unit)</code></td><td>✅</td><td>❌</td><td>固定有效期，不续期</td></tr></tbody></table><p><strong>最佳实践</strong>：</p><ul><li><strong>短时操作</strong>：<code>tryLock(1, 10, TimeUnit.SECONDS)</code></li><li><strong>长时操作</strong>：<code>lock()</code> + WatchDog续期</li><li><strong>定时任务</strong>：<code>lock(30, TimeUnit.SECONDS)</code> 手动控制有效期</li></ul><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><p><img src="https://markpic.adoreorg.cn/2025/09/Redis/635d046816f2c2beb1293315.jpg" alt="Redisson MultiLock原理"></p><h4 id="5-3-3-Redisson-MultiLock原理">5.3.3 Redisson MultiLock原理</h4><div class="tabs" id="redisson-multilock"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#redisson-multilock-1">问题背景</button></li><li class="tab"><button type="button" data-href="#redisson-multilock-2">实现原理</button></li><li class="tab"><button type="button" data-href="#redisson-multilock-3">优缺点</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="redisson-multilock-1"><p><strong>Redis主从架构中的分布式锁失效风险分析</strong></p><pre><code>&lt;div class=&quot;note note-warning&quot;&gt;  &lt;div class=&quot;note-header&quot;&gt;    &lt;i class=&quot;note-icon fa-regular fa-circle-dot&quot;&gt;&lt;/i&gt;    &lt;span class=&quot;note-title&quot;&gt;提示信息&lt;/span&gt;  &lt;/div&gt;  &lt;div class=&quot;note-content&quot;&gt;    &lt;p&gt;&lt;strong&gt;主从切换导致的锁信息丢失&lt;/strong&gt;&lt;/p&gt;  &lt;/div&gt;&lt;/div&gt;</code></pre><p><strong>问题场景</strong>：</p><ol><li><strong>主节点写入锁</strong>：客户端在Master节点成功获取锁</li><li><strong>主节点宕机</strong>：数据还未同步到Slave节点</li><li><strong>主从切换</strong>：Slave升级为新的Master</li><li><strong>锁信息丢失</strong>：新Master没有锁信息，其他客户端可重新获取锁</li></ol><p><strong>解决方案</strong>：MultiLock机制，在多个独立Redis实例上同时加锁</p><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="redisson-multilock-2"><p><strong>MultiLock核心原理</strong></p><pre><code>&lt;div class=&quot;note note-primary&quot;&gt;  &lt;div class=&quot;note-header&quot;&gt;    &lt;i class=&quot;note-icon fa-regular fa-star&quot;&gt;&lt;/i&gt;    &lt;span class=&quot;note-title&quot;&gt;提示信息&lt;/span&gt;  &lt;/div&gt;  &lt;div class=&quot;note-content&quot;&gt;    &lt;p&gt;&lt;strong&gt;多数派原则保证锁可靠性&lt;/strong&gt;&lt;/p&gt;  &lt;/div&gt;&lt;/div&gt;</code></pre><p><strong>加锁规则</strong>：</p><ul><li><strong>全部成功</strong>：在所有节点都成功加锁才算成功</li><li><strong>超时控制</strong>：总超时时间 = 节点数量 × 1500ms</li><li><strong>失败处理</strong>：任意节点失败则整个加锁失败</li></ul><p><strong>示例代码</strong>：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Configuration</span></span><br><span class="line"><span class="comment">// 配置多个Redis实例</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">RedissonConfig</span> &#123;</span><br><span class="line">    <span class="meta">@Bean</span></span><br><span class="line">    <span class="keyword">public</span> RedissonClient <span class="title function_">redissonClient</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="type">Config</span> <span class="variable">config</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Config</span>();</span><br><span class="line">        config.useSingleServer().setAddress(<span class="string">&quot;redis://192.168.137.130:6379&quot;</span>)</span><br><span class="line">                .setPassword(<span class="string">&quot;root&quot;</span>);</span><br><span class="line">        <span class="keyword">return</span> Redisson.create(config);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Bean</span></span><br><span class="line">    <span class="keyword">public</span> RedissonClient <span class="title function_">redissonClient2</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="type">Config</span> <span class="variable">config</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Config</span>();</span><br><span class="line">        config.useSingleServer().setAddress(<span class="string">&quot;redis://92.168.137.131:6379&quot;</span>)</span><br><span class="line">                .setPassword(<span class="string">&quot;root&quot;</span>);</span><br><span class="line">        <span class="keyword">return</span> Redisson.create(config);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Bean</span></span><br><span class="line">    <span class="keyword">public</span> RedissonClient <span class="title function_">redissonClient3</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="type">Config</span> <span class="variable">config</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Config</span>();</span><br><span class="line">        config.useSingleServer().setAddress(<span class="string">&quot;redis://92.168.137.132:6379&quot;</span>)</span><br><span class="line">                .setPassword(<span class="string">&quot;root&quot;</span>);</span><br><span class="line">        <span class="keyword">return</span> Redisson.create(config);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Resource</span></span><br><span class="line"><span class="keyword">private</span> RedissonClient redissonClient;</span><br><span class="line"><span class="meta">@Resource</span></span><br><span class="line"><span class="keyword">private</span> RedissonClient redissonClient2;</span><br><span class="line"><span class="meta">@Resource</span></span><br><span class="line"><span class="keyword">private</span> RedissonClient redissonClient3;</span><br><span class="line"></span><br><span class="line"><span class="keyword">private</span> RLock lock;</span><br><span class="line"></span><br><span class="line"><span class="meta">@BeforeEach</span></span><br><span class="line"><span class="keyword">void</span> <span class="title function_">setUp</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="type">RLock</span> <span class="variable">lock1</span> <span class="operator">=</span> redissonClient.getLock(<span class="string">&quot;lock&quot;</span>);</span><br><span class="line">    <span class="type">RLock</span> <span class="variable">lock2</span> <span class="operator">=</span> redissonClient2.getLock(<span class="string">&quot;lock&quot;</span>);</span><br><span class="line">    <span class="type">RLock</span> <span class="variable">lock3</span> <span class="operator">=</span> redissonClient3.getLock(<span class="string">&quot;lock&quot;</span>);</span><br><span class="line">    lock = redissonClient.getMultiLock(lock1, lock2, lock3);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Test</span></span><br><span class="line"><span class="keyword">void</span> <span class="title function_">method1</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="type">boolean</span> <span class="variable">success</span> <span class="operator">=</span> lock.tryLock();</span><br><span class="line">    redissonClient.getMultiLock();</span><br><span class="line">    <span class="keyword">if</span> (!success) &#123;</span><br><span class="line">        log.error(<span class="string">&quot;获取锁失败，1&quot;</span>);</span><br><span class="line">        <span class="keyword">return</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">        log.info(<span class="string">&quot;获取锁成功&quot;</span>);</span><br><span class="line">        method2();</span><br><span class="line">    &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">        log.info(<span class="string">&quot;释放锁，1&quot;</span>);</span><br><span class="line">        lock.unlock();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">void</span> <span class="title function_">method2</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="type">RLock</span> <span class="variable">lock</span> <span class="operator">=</span> redissonClient.getLock(<span class="string">&quot;lock&quot;</span>);</span><br><span class="line">    <span class="type">boolean</span> <span class="variable">success</span> <span class="operator">=</span> lock.tryLock();</span><br><span class="line">    <span class="keyword">if</span> (!success) &#123;</span><br><span class="line">        log.error(<span class="string">&quot;获取锁失败，2&quot;</span>);</span><br><span class="line">        <span class="keyword">return</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">        log.info(<span class="string">&quot;获取锁成功，2&quot;</span>);</span><br><span class="line">    &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">        log.info(<span class="string">&quot;释放锁，2&quot;</span>);</span><br><span class="line">        lock.unlock();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="redisson-multilock-3"><p><strong>MultiLock优缺点分析</strong></p><table><thead><tr><th>特性</th><th>优点</th><th>缺点</th></tr></thead><tbody><tr><td><strong>可靠性</strong></td><td>避免单点故障，主从切换不丢锁</td><td>需要维护多个独立Redis实例</td></tr><tr><td><strong>性能</strong></td><td>并行加锁，性能可接受</td><td>网络开销增加，延迟上升</td></tr><tr><td><strong>复杂度</strong></td><td>使用简单，API统一</td><td>需要额外的Redis实例资源</td></tr><tr><td><strong>一致性</strong></td><td>基于多数派，一致性强</td><td>网络分区时可能出现不可用</td></tr></tbody></table><p><strong>适用场景</strong>：</p><ul><li>✅ <strong>高可靠性要求</strong>：金融交易、订单处理等关键业务</li><li>✅ <strong>主从架构</strong>：Redis主从部署环境</li><li>❌ <strong>简单场景</strong>：单机Redis即可满足需求</li></ul><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h2 id="6-秒杀优化">6. 秒杀优化</h2><h3 id="6-1-异步秒杀思路">6.1 异步秒杀思路</h3><div class="tabs" id="async-seckill"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#async-seckill-1">问题分析</button></li><li class="tab"><button type="button" data-href="#async-seckill-2">优化方案</button></li><li class="tab"><button type="button" data-href="#async-seckill-3">实现难点</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="async-seckill-1"><p><strong>同步秒杀的性能瓶颈</strong></p><pre><code>&lt;div class=&quot;note note-warning&quot;&gt;  &lt;div class=&quot;note-header&quot;&gt;    &lt;i class=&quot;note-icon fa-regular fa-circle-dot&quot;&gt;&lt;/i&gt;    &lt;span class=&quot;note-title&quot;&gt;提示信息&lt;/span&gt;  &lt;/div&gt;  &lt;div class=&quot;note-content&quot;&gt;    &lt;p&gt;&lt;strong&gt;串行操作导致的性能问题&lt;/strong&gt;&lt;/p&gt;  &lt;/div&gt;&lt;/div&gt;</code></pre><p><strong>传统流程</strong>：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">查询优惠券 → 判断库存 → 查询订单 → 一人一单校验 → 扣减库存 → 创建订单</span><br></pre></td></tr></table></figure><p><img src="https://markpic.adoreorg.cn/2025/09/Redis/1653560986599.png" alt="1653560986599"></p><p><strong>性能问题</strong>：</p><ul><li><strong>数据库压力大</strong>：每个请求都要多次访问数据库</li><li><strong>串行执行</strong>：所有操作顺序执行，耗时累积</li><li><strong>线程阻塞</strong>：数据库IO等待导致线程闲置</li><li><strong>并发能力低</strong>：QPS受限于数据库性能</li></ul><p><strong>优化思路</strong>：将耗时短的逻辑判断移到Redis，耗时长的下单操作异步化</p><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="async-seckill-2"><p><strong>异步化优化方案</strong></p><pre><code>&lt;div class=&quot;note note-success&quot;&gt;  &lt;div class=&quot;note-header&quot;&gt;    &lt;i class=&quot;note-icon fa-regular fa-circle-check&quot;&gt;&lt;/i&gt;    &lt;span class=&quot;note-title&quot;&gt;提示信息&lt;/span&gt;  &lt;/div&gt;  &lt;div class=&quot;note-content&quot;&gt;    &lt;p&gt;&lt;strong&gt;Redis缓存 + 消息队列异步处理&lt;/strong&gt;&lt;/p&gt;  &lt;/div&gt;&lt;/div&gt;</code></pre><p><strong>优化流程</strong>：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">graph TD</span><br><span class="line">    A[用户请求] --&gt; B[Redis校验]</span><br><span class="line">    B --&gt; C&#123;校验通过?&#125;</span><br><span class="line">    C --&gt;|是| D[返回成功]</span><br><span class="line">    C --&gt;|否| E[返回失败]</span><br><span class="line">    D --&gt; F[消息队列]</span><br><span class="line">    F --&gt; G[异步下单]</span><br><span class="line">    G --&gt; H[数据库更新]</span><br></pre></td></tr></table></figure><p><img src="https://markpic.adoreorg.cn/2025/09/Redis/1653561657295.png" alt="1653561657295"></p><p><strong>核心思想</strong>：</p><ul><li><strong>快速响应</strong>：Redis内存操作，毫秒级响应</li><li><strong>异步处理</strong>：消息队列削峰填谷，平滑处理</li><li><strong>最终一致性</strong>：保证订单最终创建成功</li></ul><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="async-seckill-3"><p><strong>异步化实现难点</strong></p><pre><code>&lt;div class=&quot;note note-info&quot;&gt;  &lt;div class=&quot;note-header&quot;&gt;    &lt;i class=&quot;note-icon fa-regular fa-circle-info&quot;&gt;&lt;/i&gt;    &lt;span class=&quot;note-title&quot;&gt;提示信息&lt;/span&gt;  &lt;/div&gt;  &lt;div class=&quot;note-content&quot;&gt;    &lt;p&gt;&lt;strong&gt;需要解决的关键问题&lt;/strong&gt;&lt;/p&gt;  &lt;/div&gt;&lt;/div&gt;</code></pre><p><strong>技术难点</strong>：</p><ol><li><strong>Redis原子操作</strong>：如何保证库存扣减和订单记录的原子性？</li><li><strong>一人一单校验</strong>：Redis中如何快速判断用户是否已下单？</li><li><strong>订单状态跟踪</strong>：如何告知用户订单处理结果？</li><li><strong>消息可靠性</strong>：如何保证消息不丢失，正确处理？</li></ol><p><strong>解决方案</strong>：</p><ul><li><strong>Lua脚本</strong>：保证Redis操作的原子性</li><li><strong>Set集合</strong>：使用Redis Set存储已下单用户ID</li><li><strong>订单ID预生成</strong>：提前生成订单ID，用于状态查询</li><li><strong>消息队列</strong>：使用Redis Stream或专业消息队列</li></ul><p><img src="https://markpic.adoreorg.cn/2025/09/Redis/1653562234886.png" alt="1653562234886"></p><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h3 id="6-2-Redis完成秒杀资格判断">6.2 Redis完成秒杀资格判断</h3><div class="tabs" id="redis-qualification"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#redis-qualification-1">实现思路</button></li><li class="tab"><button type="button" data-href="#redis-qualification-2">代码实现</button></li><li class="tab"><button type="button" data-href="#redis-qualification-3">关键点</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="redis-qualification-1"><p><strong>Redis原子操作实现资格判断</strong></p><pre><code>&lt;div class=&quot;note note-success&quot;&gt;  &lt;div class=&quot;note-header&quot;&gt;    &lt;i class=&quot;note-icon fa-regular fa-circle-check&quot;&gt;&lt;/i&gt;    &lt;span class=&quot;note-title&quot;&gt;提示信息&lt;/span&gt;  &lt;/div&gt;  &lt;div class=&quot;note-content&quot;&gt;    &lt;p&gt;&lt;strong&gt;Lua脚本保证原子性&lt;/strong&gt;&lt;/p&gt;  &lt;/div&gt;&lt;/div&gt;</code></pre><p><strong>核心思路</strong>：</p><ol><li><strong>库存预加载</strong>：优惠券信息保存到Redis</li><li><strong>原子校验</strong>：Lua脚本一次性完成库存、一人一单判断</li><li><strong>异步下单</strong>：校验通过后发送消息到队列</li></ol><p><strong>实现步骤</strong>：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Redis预加载 → Lua脚本校验 → 消息队列 → 异步下单</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="redis-qualification-2"><p><strong>优惠券预加载</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="meta">@Transactional</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">addSeckillVoucher</span><span class="params">(Voucher voucher)</span> &#123;</span><br><span class="line">    <span class="comment">// 保存优惠券</span></span><br><span class="line">    save(voucher);</span><br><span class="line">    <span class="comment">// 保存秒杀信息</span></span><br><span class="line">    <span class="type">SeckillVoucher</span> <span class="variable">seckillVoucher</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">SeckillVoucher</span>();</span><br><span class="line">    seckillVoucher.setVoucherId(voucher.getId());</span><br><span class="line">    seckillVoucher.setStock(voucher.getStock());</span><br><span class="line">    seckillVoucher.setBeginTime(voucher.getBeginTime());</span><br><span class="line">    seckillVoucher.setEndTime(voucher.getEndTime());</span><br><span class="line">    seckillVoucherService.save(seckillVoucher);</span><br><span class="line">    <span class="comment">// 保存秒杀库存到Redis中</span></span><br><span class="line">    stringRedisTemplate.opsForValue().set(SECKILL_STOCK_KEY + voucher.getId(), </span><br><span class="line">                                         voucher.getStock().toString());</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>Lua脚本实现</strong></p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 1.参数列表</span></span><br><span class="line"><span class="keyword">local</span> voucherId = ARGV[<span class="number">1</span>]      <span class="comment">-- 优惠券id</span></span><br><span class="line"><span class="keyword">local</span> userId = ARGV[<span class="number">2</span>]       <span class="comment">-- 用户id  </span></span><br><span class="line"><span class="keyword">local</span> orderId = ARGV[<span class="number">3</span>]       <span class="comment">-- 订单id</span></span><br><span class="line"></span><br><span class="line"><span class="comment">-- 2.数据key</span></span><br><span class="line"><span class="keyword">local</span> stockKey = <span class="string">&#x27;seckill:stock:&#x27;</span> .. voucherId    <span class="comment">-- 库存key</span></span><br><span class="line"><span class="keyword">local</span> orderKey = <span class="string">&#x27;seckill:order:&#x27;</span> .. voucherId     <span class="comment">-- 订单key</span></span><br><span class="line"></span><br><span class="line"><span class="comment">-- 3.脚本业务</span></span><br><span class="line"><span class="comment">-- 3.1.判断库存是否充足</span></span><br><span class="line"><span class="keyword">if</span>(<span class="built_in">tonumber</span>(redis.call(<span class="string">&#x27;get&#x27;</span>, stockKey)) &lt;= <span class="number">0</span>) <span class="keyword">then</span></span><br><span class="line">    <span class="keyword">return</span> <span class="number">1</span>  <span class="comment">-- 库存不足</span></span><br><span class="line"><span class="keyword">end</span></span><br><span class="line"></span><br><span class="line"><span class="comment">-- 3.2.判断用户是否已下单</span></span><br><span class="line"><span class="keyword">if</span>(redis.call(<span class="string">&#x27;sismember&#x27;</span>, orderKey, userId) == <span class="number">1</span>) <span class="keyword">then</span></span><br><span class="line">    <span class="keyword">return</span> <span class="number">2</span>  <span class="comment">-- 重复下单</span></span><br><span class="line"><span class="keyword">end</span></span><br><span class="line"></span><br><span class="line"><span class="comment">-- 3.3.扣减库存</span></span><br><span class="line">redis.call(<span class="string">&#x27;incrby&#x27;</span>, stockKey, <span class="number">-1</span>)</span><br><span class="line"><span class="comment">-- 3.4.记录用户已下单</span></span><br><span class="line">redis.call(<span class="string">&#x27;sadd&#x27;</span>, orderKey, userId)</span><br><span class="line"><span class="comment">-- 3.5.发送消息到队列</span></span><br><span class="line">redis.call(<span class="string">&#x27;xadd&#x27;</span>, <span class="string">&#x27;stream.orders&#x27;</span>, <span class="string">&#x27;*&#x27;</span>, </span><br><span class="line">          <span class="string">&#x27;userId&#x27;</span>, userId, <span class="string">&#x27;voucherId&#x27;</span>, voucherId, <span class="string">&#x27;id&#x27;</span>, orderId)</span><br><span class="line"><span class="keyword">return</span> <span class="number">0</span>  <span class="comment">-- 抢购成功</span></span><br></pre></td></tr></table></figure><p><strong>业务层调用</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">seckillVoucher</span><span class="params">(Long voucherId)</span> &#123;</span><br><span class="line">    <span class="type">Long</span> <span class="variable">userId</span> <span class="operator">=</span> UserHolder.getUser().getId();</span><br><span class="line">    <span class="type">long</span> <span class="variable">orderId</span> <span class="operator">=</span> redisIdWorker.nextId(<span class="string">&quot;order&quot;</span>);</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 执行lua脚本</span></span><br><span class="line">    <span class="type">Long</span> <span class="variable">result</span> <span class="operator">=</span> stringRedisTemplate.execute(</span><br><span class="line">        SECKILL_SCRIPT,</span><br><span class="line">        Collections.emptyList(),</span><br><span class="line">        voucherId.toString(),</span><br><span class="line">        userId.toString(), </span><br><span class="line">        String.valueOf(orderId)</span><br><span class="line">    );</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 判断结果</span></span><br><span class="line">    <span class="type">int</span> <span class="variable">r</span> <span class="operator">=</span> result.intValue();</span><br><span class="line">    <span class="keyword">if</span> (r != <span class="number">0</span>) &#123;</span><br><span class="line">        <span class="keyword">return</span> Result.fail(r == <span class="number">1</span> ? <span class="string">&quot;库存不足！&quot;</span> : <span class="string">&quot;不能重复下单！&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">return</span> Result.ok(orderId);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>使用PostMan发送请求，添加优惠券</strong><br><strong>请求路径：<a href="http://localhost:8080/api/voucher/seckill">http://localhost:8080/api/voucher/seckill</a></strong><br><strong>请求方式：POST</strong></p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;shopId&quot;</span><span class="punctuation">:</span><span class="number">1</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;title&quot;</span><span class="punctuation">:</span><span class="string">&quot;9999元代金券&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;subTitle&quot;</span><span class="punctuation">:</span><span class="string">&quot;365*24小时可用&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;rules&quot;</span><span class="punctuation">:</span><span class="string">&quot;全场通用\\nApex猎杀无需预约&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;payValue&quot;</span><span class="punctuation">:</span><span class="number">1000</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;actualValue&quot;</span><span class="punctuation">:</span><span class="number">999900</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span><span class="number">1</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;stock&quot;</span><span class="punctuation">:</span><span class="number">100</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;beginTime&quot;</span><span class="punctuation">:</span><span class="string">&quot;2022-01-01T00:00:00&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;endTime&quot;</span><span class="punctuation">:</span><span class="string">&quot;2022-12-31T23:59:59&quot;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="redis-qualification-3"><p><strong>实现关键点</strong></p><pre><code>&lt;div class=&quot;note note-warning&quot;&gt;  &lt;div class=&quot;note-header&quot;&gt;    &lt;i class=&quot;note-icon fa-regular fa-circle-dot&quot;&gt;&lt;/i&gt;    &lt;span class=&quot;note-title&quot;&gt;提示信息&lt;/span&gt;  &lt;/div&gt;  &lt;div class=&quot;note-content&quot;&gt;    &lt;p&gt;&lt;strong&gt;需要注意的细节问题&lt;/strong&gt;&lt;/p&gt;  &lt;/div&gt;&lt;/div&gt;</code></pre><p><strong>原子性保证</strong>：</p><ul><li>✅ <strong>Lua脚本</strong>：所有Redis操作一次性执行</li><li>✅ <strong>单线程模型</strong>：Redis单线程保证脚本执行不被打断</li><li>❌ <strong>事务</strong>：MULTI/EXEC无法保证库存和订单的原子性</li></ul><p><strong>数据一致性</strong>：</p><ul><li><strong>库存Key</strong>：<code>seckill:stock:{voucherId}</code></li><li><strong>订单Key</strong>：<code>seckill:order:{voucherId}</code></li><li><strong>消息队列</strong>：<code>stream.orders</code></li></ul><p><strong>返回值设计</strong>：</p><ul><li><code>0</code>：抢购成功</li><li><code>1</code>：库存不足</li><li><code>2</code>：重复下单</li></ul><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h3 id="6-3-基于阻塞队列实现秒杀优化">6.3 基于阻塞队列实现秒杀优化</h3><div class="tabs" id="blocking-queue"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#blocking-queue-1">实现思路</button></li><li class="tab"><button type="button" data-href="#blocking-queue-2">代码实现</button></li><li class="tab"><button type="button" data-href="#blocking-queue-3">完整代码</button></li><li class="tab"><button type="button" data-href="#blocking-queue-4">优缺点</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="blocking-queue-1"><p><strong>阻塞队列异步处理方案</strong></p><pre><code>&lt;div class=&quot;note note-info&quot;&gt;  &lt;div class=&quot;note-header&quot;&gt;    &lt;i class=&quot;note-icon fa-regular fa-circle-info&quot;&gt;&lt;/i&gt;    &lt;span class=&quot;note-title&quot;&gt;提示信息&lt;/span&gt;  &lt;/div&gt;  &lt;div class=&quot;note-content&quot;&gt;    &lt;p&gt;&lt;strong&gt;内存队列 + 单线程异步处理&lt;/strong&gt;&lt;/p&gt;  &lt;/div&gt;&lt;/div&gt;</code></pre><p><strong>核心思路</strong>：</p><ol><li><strong>快速响应</strong>：Lua脚本校验通过后立即返回订单ID</li><li><strong>异步处理</strong>：订单信息放入阻塞队列，后台线程慢慢处理</li><li><strong>流量削峰</strong>：阻塞队列缓冲瞬时高并发请求</li></ol><p><strong>实现架构</strong>：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">用户请求 → Lua脚本校验 → 内存队列 → 单线程处理 → 数据库操作</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="blocking-queue-2"><p><strong>线程池配置</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 异步处理线程池</span></span><br><span class="line"><span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">ExecutorService</span> <span class="variable">SECKILL_ORDER_EXECUTOR</span> <span class="operator">=</span> </span><br><span class="line">    Executors.newSingleThreadExecutor();</span><br><span class="line"></span><br><span class="line"><span class="comment">// 类初始化后立即执行</span></span><br><span class="line"><span class="meta">@PostConstruct</span></span><br><span class="line"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title function_">init</span><span class="params">()</span> &#123;</span><br><span class="line">    SECKILL_ORDER_EXECUTOR.submit(<span class="keyword">new</span> <span class="title class_">VoucherOrderHandler</span>());</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>订单处理器</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">class</span> <span class="title class_">VoucherOrderHandler</span> <span class="keyword">implements</span> <span class="title class_">Runnable</span> &#123;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">run</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">while</span> (<span class="literal">true</span>) &#123;</span><br><span class="line">            <span class="keyword">try</span> &#123;</span><br><span class="line">                <span class="comment">// 1.获取队列中的订单信息</span></span><br><span class="line">                <span class="type">VoucherOrder</span> <span class="variable">voucherOrder</span> <span class="operator">=</span> orderTasks.take();</span><br><span class="line">                <span class="comment">// 2.处理订单</span></span><br><span class="line">                handleVoucherOrder(voucherOrder);</span><br><span class="line">            &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">                log.error(<span class="string">&quot;处理订单异常&quot;</span>, e);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">void</span> <span class="title function_">handleVoucherOrder</span><span class="params">(VoucherOrder voucherOrder)</span> &#123;</span><br><span class="line">        <span class="type">Long</span> <span class="variable">userId</span> <span class="operator">=</span> voucherOrder.getUserId();</span><br><span class="line">        <span class="comment">// 创建锁对象，防止重复下单</span></span><br><span class="line">        <span class="type">RLock</span> <span class="variable">redisLock</span> <span class="operator">=</span> redissonClient.getLock(<span class="string">&quot;lock:order:&quot;</span> + userId);</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 尝试获取锁</span></span><br><span class="line">        <span class="type">boolean</span> <span class="variable">isLock</span> <span class="operator">=</span> redisLock.lock();</span><br><span class="line">        <span class="keyword">if</span> (!isLock) &#123;</span><br><span class="line">            log.error(<span class="string">&quot;不允许重复下单！&quot;</span>);</span><br><span class="line">            <span class="keyword">return</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        </span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            <span class="comment">// 注意：Spring事务在ThreadLocal中，多线程环境下会失效</span></span><br><span class="line">            proxy.createVoucherOrder(voucherOrder);</span><br><span class="line">        &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">            <span class="comment">// 释放锁</span></span><br><span class="line">            redisLock.unlock();</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="meta">@Transactional</span></span><br><span class="line">    <span class="keyword">public</span>  <span class="keyword">void</span> <span class="title function_">createVoucherOrder</span><span class="params">(VoucherOrder voucherOrder)</span> &#123;</span><br><span class="line">        <span class="type">Long</span> <span class="variable">userId</span> <span class="operator">=</span> voucherOrder.getUserId();</span><br><span class="line">        <span class="comment">// 5.1.查询订单</span></span><br><span class="line">        <span class="type">int</span> <span class="variable">count</span> <span class="operator">=</span> query().eq(<span class="string">&quot;user_id&quot;</span>, userId).eq(<span class="string">&quot;voucher_id&quot;</span>, voucherOrder.getVoucherId()).count();</span><br><span class="line">        <span class="comment">// 5.2.判断是否存在</span></span><br><span class="line">        <span class="keyword">if</span> (count &gt; <span class="number">0</span>) &#123;</span><br><span class="line">            <span class="comment">// 用户已经购买过了</span></span><br><span class="line">           log.error(<span class="string">&quot;用户已经购买过了&quot;</span>);</span><br><span class="line">           <span class="keyword">return</span> ;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 6.扣减库存</span></span><br><span class="line">        <span class="type">boolean</span> <span class="variable">success</span> <span class="operator">=</span> seckillVoucherService.update()</span><br><span class="line">                .setSql(<span class="string">&quot;stock = stock - 1&quot;</span>) <span class="comment">// set stock = stock - 1</span></span><br><span class="line">                .eq(<span class="string">&quot;voucher_id&quot;</span>, voucherOrder.getVoucherId()).gt(<span class="string">&quot;stock&quot;</span>, <span class="number">0</span>) <span class="comment">// where id = ? and stock &gt; 0</span></span><br><span class="line">                .update();</span><br><span class="line">        <span class="keyword">if</span> (!success) &#123;</span><br><span class="line">            <span class="comment">// 扣减失败</span></span><br><span class="line">            log.error(<span class="string">&quot;库存不足&quot;</span>);</span><br><span class="line">            <span class="keyword">return</span> ;</span><br><span class="line">        &#125;</span><br><span class="line">        save(voucherOrder);</span><br><span class="line"> </span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>阻塞队列定义</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 订单阻塞队列，容量1024*1024</span></span><br><span class="line"><span class="keyword">private</span> BlockingQueue&lt;VoucherOrder&gt; orderTasks = </span><br><span class="line">    <span class="keyword">new</span> <span class="title class_">ArrayBlockingQueue</span>&lt;&gt;(<span class="number">1024</span> * <span class="number">1024</span>);</span><br></pre></td></tr></table></figure><p><strong>业务层修改</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">seckillVoucher</span><span class="params">(Long voucherId)</span> &#123;</span><br><span class="line">    <span class="type">Long</span> <span class="variable">userId</span> <span class="operator">=</span> UserHolder.getUser().getId();</span><br><span class="line">    <span class="type">long</span> <span class="variable">orderId</span> <span class="operator">=</span> redisIdWorker.nextId(<span class="string">&quot;order&quot;</span>);</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 1.执行lua脚本</span></span><br><span class="line">    <span class="type">Long</span> <span class="variable">result</span> <span class="operator">=</span> stringRedisTemplate.execute(</span><br><span class="line">        SECKILL_SCRIPT,</span><br><span class="line">        Collections.emptyList(),</span><br><span class="line">        voucherId.toString(), userId.toString(), String.valueOf(orderId)</span><br><span class="line">    );</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 2.判断结果</span></span><br><span class="line">    <span class="type">int</span> <span class="variable">r</span> <span class="operator">=</span> result.intValue();</span><br><span class="line">    <span class="keyword">if</span> (r != <span class="number">0</span>) &#123;</span><br><span class="line">        <span class="keyword">return</span> Result.fail(r == <span class="number">1</span> ? <span class="string">&quot;库存不足&quot;</span> : <span class="string">&quot;不能重复下单&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 3.创建订单对象</span></span><br><span class="line">    <span class="type">VoucherOrder</span> <span class="variable">voucherOrder</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">VoucherOrder</span>();</span><br><span class="line">    voucherOrder.setId(orderId);</span><br><span class="line">    voucherOrder.setUserId(userId);</span><br><span class="line">    voucherOrder.setVoucherId(voucherId);</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 4.放入阻塞队列</span></span><br><span class="line">    orderTasks.add(voucherOrder);</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 5.返回订单id</span></span><br><span class="line">    <span class="keyword">return</span> Result.ok(orderId);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="blocking-queue-3"><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br><span class="line">133</span><br><span class="line">134</span><br><span class="line">135</span><br><span class="line">136</span><br><span class="line">137</span><br><span class="line">138</span><br><span class="line">139</span><br><span class="line">140</span><br><span class="line">141</span><br><span class="line">142</span><br><span class="line">143</span><br><span class="line">144</span><br><span class="line">145</span><br><span class="line">146</span><br><span class="line">147</span><br><span class="line">148</span><br><span class="line">149</span><br><span class="line">150</span><br><span class="line">151</span><br><span class="line">152</span><br><span class="line">153</span><br><span class="line">154</span><br><span class="line">155</span><br><span class="line">156</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.hmdp.service.impl;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> com.hmdp.dto.Result;</span><br><span class="line"><span class="keyword">import</span> com.hmdp.entity.VoucherOrder;</span><br><span class="line"><span class="keyword">import</span> com.hmdp.mapper.VoucherOrderMapper;</span><br><span class="line"><span class="keyword">import</span> com.hmdp.service.ISeckillVoucherService;</span><br><span class="line"><span class="keyword">import</span> com.hmdp.service.IVoucherOrderService;</span><br><span class="line"><span class="keyword">import</span> com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;</span><br><span class="line"><span class="keyword">import</span> com.hmdp.utils.RedisIdWorker;</span><br><span class="line"><span class="keyword">import</span> com.hmdp.utils.UserHolder;</span><br><span class="line"><span class="keyword">import</span> lombok.extern.slf4j.Slf4j;</span><br><span class="line"><span class="keyword">import</span> org.redisson.api.RLock;</span><br><span class="line"><span class="keyword">import</span> org.redisson.api.RedissonClient;</span><br><span class="line"><span class="keyword">import</span> org.springframework.aop.framework.AopContext;</span><br><span class="line"><span class="keyword">import</span> org.springframework.beans.factory.annotation.Autowired;</span><br><span class="line"><span class="keyword">import</span> org.springframework.core.io.ClassPathResource;</span><br><span class="line"><span class="keyword">import</span> org.springframework.data.redis.core.StringRedisTemplate;</span><br><span class="line"><span class="keyword">import</span> org.springframework.data.redis.core.script.DefaultRedisScript;</span><br><span class="line"><span class="keyword">import</span> org.springframework.stereotype.Service;</span><br><span class="line"><span class="keyword">import</span> org.springframework.transaction.annotation.Transactional;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> javax.annotation.PostConstruct;</span><br><span class="line"><span class="keyword">import</span> javax.annotation.Resource;</span><br><span class="line"><span class="keyword">import</span> java.util.Collections;</span><br><span class="line"><span class="keyword">import</span> java.util.concurrent.ArrayBlockingQueue;</span><br><span class="line"><span class="keyword">import</span> java.util.concurrent.BlockingQueue;</span><br><span class="line"><span class="keyword">import</span> java.util.concurrent.ExecutorService;</span><br><span class="line"><span class="keyword">import</span> java.util.concurrent.Executors;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * &lt;p&gt;</span></span><br><span class="line"><span class="comment"> * 服务实现类</span></span><br><span class="line"><span class="comment"> * &lt;/p&gt;</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@author</span> Kyle</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@since</span> 2022-10-22</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="meta">@Service</span></span><br><span class="line"><span class="meta">@Slf4j</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">VoucherOrderServiceImpl</span> <span class="keyword">extends</span> <span class="title class_">ServiceImpl</span>&lt;VoucherOrderMapper, VoucherOrder&gt; <span class="keyword">implements</span> <span class="title class_">IVoucherOrderService</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> ISeckillVoucherService seckillVoucherService;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> RedisIdWorker redisIdWorker;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Resource</span></span><br><span class="line">    <span class="keyword">private</span> StringRedisTemplate stringRedisTemplate;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Resource</span></span><br><span class="line">    <span class="keyword">private</span> RedissonClient redissonClient;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> IVoucherOrderService proxy;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> DefaultRedisScript&lt;Long&gt; SECKILL_SCRIPT;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">static</span> &#123;</span><br><span class="line">        SECKILL_SCRIPT = <span class="keyword">new</span> <span class="title class_">DefaultRedisScript</span>();</span><br><span class="line">        SECKILL_SCRIPT.setLocation(<span class="keyword">new</span> <span class="title class_">ClassPathResource</span>(<span class="string">&quot;seckill.lua&quot;</span>));</span><br><span class="line">        SECKILL_SCRIPT.setResultType(Long.class);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">ExecutorService</span> <span class="variable">SECKILL_ORDER_EXECUTOR</span> <span class="operator">=</span> Executors.newSingleThreadExecutor();</span><br><span class="line"></span><br><span class="line">    <span class="meta">@PostConstruct</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">void</span> <span class="title function_">init</span><span class="params">()</span> &#123;</span><br><span class="line">        SECKILL_ORDER_EXECUTOR.submit(<span class="keyword">new</span> <span class="title class_">VoucherOrderHandler</span>());</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> BlockingQueue&lt;VoucherOrder&gt; orderTasks = <span class="keyword">new</span> <span class="title class_">ArrayBlockingQueue</span>&lt;&gt;(<span class="number">1024</span> * <span class="number">1024</span>);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">void</span> <span class="title function_">handleVoucherOrder</span><span class="params">(VoucherOrder voucherOrder)</span> &#123;</span><br><span class="line">        <span class="comment">//1. 获取用户</span></span><br><span class="line">        <span class="type">Long</span> <span class="variable">userId</span> <span class="operator">=</span> voucherOrder.getUserId();</span><br><span class="line">        <span class="comment">//2. 创建锁对象，作为兜底方案</span></span><br><span class="line">        <span class="type">RLock</span> <span class="variable">redisLock</span> <span class="operator">=</span> redissonClient.getLock(<span class="string">&quot;order:&quot;</span> + userId);</span><br><span class="line">        <span class="comment">//3. 获取锁</span></span><br><span class="line">        <span class="type">boolean</span> <span class="variable">isLock</span> <span class="operator">=</span> redisLock.tryLock();</span><br><span class="line">        <span class="comment">//4. 判断是否获取锁成功 </span></span><br><span class="line">        <span class="keyword">if</span> (!isLock) &#123;</span><br><span class="line">            log.error(<span class="string">&quot;不允许重复下单!&quot;</span>);</span><br><span class="line">            <span class="keyword">return</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            <span class="comment">//5. 使用代理对象，由于这里是另外一个线程，</span></span><br><span class="line">            proxy.createVoucherOrder(voucherOrder);</span><br><span class="line">        &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">            redisLock.unlock();</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">class</span> <span class="title class_">VoucherOrderHandler</span> <span class="keyword">implements</span> <span class="title class_">Runnable</span> &#123;</span><br><span class="line">        <span class="meta">@Override</span></span><br><span class="line">        <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">run</span><span class="params">()</span> &#123;</span><br><span class="line">            <span class="keyword">while</span> (<span class="literal">true</span>) &#123;</span><br><span class="line">                <span class="keyword">try</span> &#123;</span><br><span class="line">                    <span class="comment">//1. 获取队列中的订单信息</span></span><br><span class="line">                    <span class="type">VoucherOrder</span> <span class="variable">voucherOrder</span> <span class="operator">=</span> orderTasks.take();</span><br><span class="line">                    <span class="comment">//2. 创建订单</span></span><br><span class="line">                    handleVoucherOrder(voucherOrder);</span><br><span class="line">                &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">                    log.error(<span class="string">&quot;订单处理异常&quot;</span>, e);</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> Result <span class="title function_">seckillVoucher</span><span class="params">(Long voucherId)</span> &#123;</span><br><span class="line">        <span class="type">Long</span> <span class="variable">result</span> <span class="operator">=</span> stringRedisTemplate.execute(SECKILL_SCRIPT,</span><br><span class="line">                Collections.emptyList(), voucherId.toString(),</span><br><span class="line">                UserHolder.getUser().getId().toString());</span><br><span class="line">        <span class="keyword">if</span> (result.intValue() != <span class="number">0</span>) &#123;</span><br><span class="line">            <span class="keyword">return</span> Result.fail(result.intValue() == <span class="number">1</span> ? <span class="string">&quot;库存不足&quot;</span> : <span class="string">&quot;不能重复下单&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="type">long</span> <span class="variable">orderId</span> <span class="operator">=</span> redisIdWorker.nextId(<span class="string">&quot;order&quot;</span>);</span><br><span class="line">        <span class="comment">//封装到voucherOrder中</span></span><br><span class="line">        <span class="type">VoucherOrder</span> <span class="variable">voucherOrder</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">VoucherOrder</span>();</span><br><span class="line">        voucherOrder.setVoucherId(voucherId);</span><br><span class="line">        voucherOrder.setUserId(UserHolder.getUser().getId());</span><br><span class="line">        voucherOrder.setId(orderId);</span><br><span class="line">        <span class="comment">//加入到阻塞队列</span></span><br><span class="line">        orderTasks.add(voucherOrder);</span><br><span class="line">        <span class="comment">//主线程获取代理对象</span></span><br><span class="line">        <span class="comment">// proxy = (IVoucherOrderService) AopContext.currentProxy();</span></span><br><span class="line">        <span class="keyword">return</span> Result.ok(orderId);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    <span class="meta">@Transactional</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">createVoucherOrder</span><span class="params">(VoucherOrder voucherOrder)</span> &#123;</span><br><span class="line">        <span class="comment">// 一人一单逻辑</span></span><br><span class="line">        <span class="type">Long</span> <span class="variable">userId</span> <span class="operator">=</span> voucherOrder.getUserId();</span><br><span class="line">        <span class="type">Long</span> <span class="variable">voucherId</span> <span class="operator">=</span> voucherOrder.getVoucherId();</span><br><span class="line">        <span class="keyword">synchronized</span> (userId.toString().intern()) &#123;</span><br><span class="line">            <span class="type">int</span> <span class="variable">count</span> <span class="operator">=</span> query().eq(<span class="string">&quot;voucher_id&quot;</span>, voucherId).eq(<span class="string">&quot;user_id&quot;</span>, userId).count();</span><br><span class="line">            <span class="keyword">if</span> (count &gt; <span class="number">0</span>) &#123;</span><br><span class="line">                log.error(<span class="string">&quot;你已经抢过优惠券了哦&quot;</span>);</span><br><span class="line">                <span class="keyword">return</span>;</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="comment">//5. 扣减库存</span></span><br><span class="line">            <span class="type">boolean</span> <span class="variable">success</span> <span class="operator">=</span> seckillVoucherService.update()</span><br><span class="line">                    .setSql(<span class="string">&quot;stock = stock - 1&quot;</span>)</span><br><span class="line">                    .eq(<span class="string">&quot;voucher_id&quot;</span>, voucherId)</span><br><span class="line">                    .gt(<span class="string">&quot;stock&quot;</span>, <span class="number">0</span>)</span><br><span class="line">                    .update();</span><br><span class="line">            <span class="keyword">if</span> (!success) &#123;</span><br><span class="line">                log.error(<span class="string">&quot;库存不足&quot;</span>);</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="comment">//7. 将订单数据保存到表中</span></span><br><span class="line">            save(voucherOrder);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="blocking-queue-4"><p><strong>阻塞队列方案优缺点</strong></p><pre><code>&lt;div class=&quot;note note-warning&quot;&gt;  &lt;div class=&quot;note-header&quot;&gt;    &lt;i class=&quot;note-icon fa-regular fa-circle-dot&quot;&gt;&lt;/i&gt;    &lt;span class=&quot;note-title&quot;&gt;提示信息&lt;/span&gt;  &lt;/div&gt;  &lt;div class=&quot;note-content&quot;&gt;    &lt;p&gt;&lt;strong&gt;内存队列的局限性&lt;/strong&gt;&lt;/p&gt;  &lt;/div&gt;&lt;/div&gt;</code></pre><p><strong>优点</strong>：</p><ul><li>✅ <strong>实现简单</strong>：JDK原生支持，无需额外依赖</li><li>✅ <strong>性能优秀</strong>：内存操作，延迟极低</li><li>✅ <strong>削峰填谷</strong>：缓冲瞬时高并发请求</li></ul><p><strong>缺点</strong>：</p><ul><li>❌ <strong>内存限制</strong>：队列容量有限，数据可能丢失</li><li>❌ <strong>单点故障</strong>：JVM宕机导致队列数据丢失</li><li>❌ <strong>扩展性差</strong>：无法分布式部署，只能单机处理</li></ul><p><strong>适用场景</strong>：</p><ul><li>✅ <strong>单机部署</strong>：应用部署在单台服务器</li><li>✅ <strong>数据可接受丢失</strong>：秒杀活动数据允许少量丢失</li><li>❌ <strong>分布式部署</strong>：需要多机协同处理</li><li>❌ <strong>数据强一致性</strong>：订单数据不能丢失</li></ul><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><p><strong>小总结：</strong></p><p>秒杀业务的优化思路是什么？</p><ul><li>先利用Redis完成库存余量、一人一单判断，完成抢单业务</li><li>再将下单业务放入阻塞队列，利用独立线程异步下单</li><li>基于阻塞队列的异步秒杀存在哪些问题？<ul><li>内存限制问题</li><li>数据安全问题</li></ul></li></ul><h2 id="7-Redis消息队列">7. Redis消息队列</h2><h3 id="7-1-1-消息队列基础">7.1.1 消息队列基础</h3><div class="tabs" id="mq-basics"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#mq-basics-1">基本概念</button></li><li class="tab"><button type="button" data-href="#mq-basics-2">使用场景</button></li><li class="tab"><button type="button" data-href="#mq-basics-3">Redis实现方案</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="mq-basics-1"><p><strong>什么是消息队列</strong></p><pre><code>&lt;div class=&quot;note note-info&quot;&gt;  &lt;div class=&quot;note-header&quot;&gt;    &lt;i class=&quot;note-icon fa-regular fa-circle-info&quot;&gt;&lt;/i&gt;    &lt;span class=&quot;note-title&quot;&gt;提示信息&lt;/span&gt;  &lt;/div&gt;  &lt;div class=&quot;note-content&quot;&gt;    &lt;p&gt;&lt;strong&gt;消息队列的核心概念&lt;/strong&gt;&lt;/p&gt;  &lt;/div&gt;&lt;/div&gt;</code></pre><p><strong>定义</strong>：消息队列是一种异步通信机制，用于在不同组件或系统之间传递消息。</p><p><strong>三个核心角色</strong>：</p><ul><li><strong>消息队列</strong>：存储和管理消息的消息代理（Message Broker）</li><li><strong>生产者</strong>：发送消息到消息队列的应用或服务</li><li><strong>消费者</strong>：从消息队列获取消息并处理的应用或服务</li></ul><p><strong>通信模型</strong>：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">生产者 → 消息队列 → 消费者</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="mq-basics-2"><p><strong>消息队列的使用场景</strong></p><pre><code>&lt;div class=&quot;note note-success&quot;&gt;  &lt;div class=&quot;note-header&quot;&gt;    &lt;i class=&quot;note-icon fa-regular fa-circle-check&quot;&gt;&lt;/i&gt;    &lt;span class=&quot;note-title&quot;&gt;提示信息&lt;/span&gt;  &lt;/div&gt;  &lt;div class=&quot;note-content&quot;&gt;    &lt;p&gt;&lt;strong&gt;解耦和异步处理&lt;/strong&gt;&lt;/p&gt;  &lt;/div&gt;&lt;/div&gt;</code></pre><p><strong>生活例子</strong>：快递柜系统</p><ul><li><strong>快递员（生产者）</strong>：把快递放入快递柜</li><li><strong>快递柜（消息队列）</strong>：临时存储快递</li><li><strong>用户（消费者）</strong>：从快递柜取快递</li></ul><p><strong>技术优势</strong>：</p><ul><li><strong>解耦</strong>：生产者和消费者不需要直接通信</li><li><strong>异步</strong>：生产者不需要等待消费者处理完成</li><li><strong>削峰填谷</strong>：缓冲瞬时高并发请求</li><li><strong>可靠性</strong>：消息持久化，确保不丢失</li></ul><p><strong>秒杀场景应用</strong>：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">下单校验 → Redis队列 → 异步处理订单</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="mq-basics-3"><p><strong>Redis消息队列方案</strong></p><pre><code>&lt;div class=&quot;note note-warning&quot;&gt;  &lt;div class=&quot;note-header&quot;&gt;    &lt;i class=&quot;note-icon fa-regular fa-circle-dot&quot;&gt;&lt;/i&gt;    &lt;span class=&quot;note-title&quot;&gt;提示信息&lt;/span&gt;  &lt;/div&gt;  &lt;div class=&quot;note-content&quot;&gt;    &lt;p&gt;&lt;strong&gt;Redis提供的三种消息队列实现&lt;/strong&gt;&lt;/p&gt;  &lt;/div&gt;&lt;/div&gt;</code></pre><table><thead><tr><th>实现方式</th><th>数据结构</th><th>特点</th><th>适用场景</th></tr></thead><tbody><tr><td><strong>List队列</strong></td><td>List</td><td>简单可靠，支持阻塞</td><td>简单消息传递</td></tr><tr><td><strong>PubSub</strong></td><td>发布订阅</td><td>实时推送，不支持持久化</td><td>实时通知</td></tr><tr><td><strong>Stream</strong></td><td>Stream</td><td>功能完善，支持持久化</td><td>复杂消息系统</td></tr></tbody></table><p><strong>选择建议</strong>：</p><ul><li>✅ <strong>简单场景</strong>：使用List实现</li><li>✅ <strong>实时通知</strong>：使用PubSub</li><li>✅ <strong>复杂业务</strong>：使用Stream</li><li>❌ <strong>大数据量</strong>：建议使用专业MQ（Kafka、RabbitMQ）</li></ul><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h3 id="7-1-2-基于List实现消息队列">7.1.2 基于List实现消息队列</h3><div class="tabs" id="list-mq"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#list-mq-1">实现原理</button></li><li class="tab"><button type="button" data-href="#list-mq-2">代码示例</button></li><li class="tab"><button type="button" data-href="#list-mq-3">优缺点分析</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="list-mq-1"><p><strong>List结构消息队列原理</strong></p><pre><code>&lt;div class=&quot;note note-info&quot;&gt;  &lt;div class=&quot;note-header&quot;&gt;    &lt;i class=&quot;note-icon fa-regular fa-circle-info&quot;&gt;&lt;/i&gt;    &lt;span class=&quot;note-title&quot;&gt;提示信息&lt;/span&gt;  &lt;/div&gt;  &lt;div class=&quot;note-content&quot;&gt;    &lt;p&gt;&lt;strong&gt;双向链表实现队列效果&lt;/strong&gt;&lt;/p&gt;  &lt;/div&gt;&lt;/div&gt;</code></pre><p><strong>基本原理</strong>：Redis的List是双向链表，天然支持队列操作</p><p><strong>操作命令</strong>：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"># 生产者：从左侧入队</span><br><span class="line">LPUSH queue_name message</span><br><span class="line"></span><br><span class="line"># 消费者：从右侧出队  </span><br><span class="line">RPOP queue_name</span><br><span class="line"></span><br><span class="line"># 阻塞式消费（推荐）</span><br><span class="line">BRPOP queue_name timeout</span><br></pre></td></tr></table></figure><p><strong>队列模型</strong>：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">生产者 → LPUSH → [message3, message2, message1] → RPOP → 消费者</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="list-mq-2"><p><strong>List消息队列实现</strong></p><p><strong>生产者代码</strong>：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 发送消息到队列</span></span><br><span class="line">stringRedisTemplate.opsForList().leftPush(<span class="string">&quot;queue:order&quot;</span>, message);</span><br></pre></td></tr></table></figure><p><strong>消费者代码</strong>：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 阻塞式获取消息（超时5秒）</span></span><br><span class="line"><span class="type">String</span> <span class="variable">message</span> <span class="operator">=</span> stringRedisTemplate.opsForList()</span><br><span class="line">    .rightPop(<span class="string">&quot;queue:order&quot;</span>, <span class="number">5</span>, TimeUnit.SECONDS);</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> (message != <span class="literal">null</span>) &#123;</span><br><span class="line">    <span class="comment">// 处理消息</span></span><br><span class="line">    processMessage(message);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>批量处理</strong>：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 一次获取多条消息</span></span><br><span class="line">List&lt;String&gt; messages = stringRedisTemplate.opsForList()</span><br><span class="line">    .range(<span class="string">&quot;queue:order&quot;</span>, <span class="number">0</span>, <span class="number">99</span>);</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="list-mq-3"><p><strong>List消息队列优缺点</strong></p><pre><code>&lt;div class=&quot;note note-warning&quot;&gt;  &lt;div class=&quot;note-header&quot;&gt;    &lt;i class=&quot;note-icon fa-regular fa-circle-dot&quot;&gt;&lt;/i&gt;    &lt;span class=&quot;note-title&quot;&gt;提示信息&lt;/span&gt;  &lt;/div&gt;  &lt;div class=&quot;note-content&quot;&gt;    &lt;p&gt;&lt;strong&gt;简单但功能有限&lt;/strong&gt;&lt;/p&gt;  &lt;/div&gt;&lt;/div&gt;</code></pre><p><strong>优点</strong>：</p><ul><li>✅ <strong>实现简单</strong>：Redis原生List结构，无需额外配置</li><li>✅ <strong>持久化支持</strong>：基于Redis持久化，数据安全</li><li>✅ <strong>有序性</strong>：消息按入队顺序消费</li><li>✅ <strong>内存大</strong>：不受JVM内存限制</li></ul><p><strong>缺点</strong>：</p><ul><li>❌ <strong>消息丢失</strong>：消费者处理失败时消息已删除</li><li>❌ <strong>单消费者</strong>：一条消息只能被一个消费者处理</li><li>❌ <strong>无ACK机制</strong>：无法确认消息是否成功处理</li><li>❌ <strong>无重试机制</strong>：处理失败的消息无法重新消费</li></ul><p><strong>改进方案</strong>：</p><ul><li><strong>消息确认</strong>：消费后不立即删除，先放入&quot;处理中&quot;列表</li><li><strong>失败重试</strong>：处理失败的消息重新放回队列</li><li><strong>多消费者</strong>：使用多个队列实现消费者组</li></ul><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h3 id="7-1-3-基于PubSub实现消息队列">7.1.3 基于PubSub实现消息队列</h3><div class="tabs" id="pubsub-mq"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#pubsub-mq-1">基本原理</button></li><li class="tab"><button type="button" data-href="#pubsub-mq-2">代码示例</button></li><li class="tab"><button type="button" data-href="#pubsub-mq-3">优缺点分析</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="pubsub-mq-1"><p><strong>PubSub发布订阅模型</strong></p><pre><code>&lt;div class=&quot;note note-info&quot;&gt;  &lt;div class=&quot;note-header&quot;&gt;    &lt;i class=&quot;note-icon fa-regular fa-circle-info&quot;&gt;&lt;/i&gt;    &lt;span class=&quot;note-title&quot;&gt;提示信息&lt;/span&gt;  &lt;/div&gt;  &lt;div class=&quot;note-content&quot;&gt;    &lt;p&gt;&lt;strong&gt;频道(Channel)广播机制&lt;/strong&gt;&lt;/p&gt;  &lt;/div&gt;&lt;/div&gt;</code></pre><p><strong>核心概念</strong>：</p><ul><li><strong>频道(Channel)</strong>：消息发布的通道</li><li><strong>订阅者(Subscriber)</strong>：订阅频道的客户端</li><li><strong>发布者(Publisher)</strong>：向频道发送消息的客户端</li></ul><p><strong>基本命令</strong>：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"># 订阅频道</span><br><span class="line">SUBSCRIBE channel1 channel2</span><br><span class="line"></span><br><span class="line"># 发布消息</span><br><span class="line">PUBLISH channel1 &quot;hello world&quot;</span><br><span class="line"></span><br><span class="line"># 模式订阅（通配符）</span><br><span class="line">PSUBSCRIBE news.*</span><br></pre></td></tr></table></figure><p><strong>消息流</strong>：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">发布者 → PUBLISH → Channel → 广播 → 所有订阅者</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="pubsub-mq-2"><p><strong>PubSub消息队列实现</strong></p><p><strong>消息发布者</strong>：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 发布消息</span></span><br><span class="line">stringRedisTemplate.convertAndSend(<span class="string">&quot;order.channel&quot;</span>, orderMessage);</span><br></pre></td></tr></table></figure><p><strong>消息订阅者</strong>：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">OrderMessageSubscriber</span> <span class="keyword">extends</span> <span class="title class_">KeyspaceEventMessageListener</span> &#123;</span><br><span class="line">    </span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">public</span> <span class="title function_">OrderMessageSubscriber</span><span class="params">(RedisMessageListenerContainer listenerContainer)</span> &#123;</span><br><span class="line">        <span class="built_in">super</span>(listenerContainer);</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">protected</span> <span class="keyword">void</span> <span class="title function_">doHandleMessage</span><span class="params">(Message message)</span> &#123;</span><br><span class="line">        <span class="type">String</span> <span class="variable">channel</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">String</span>(message.getChannel());</span><br><span class="line">        <span class="type">String</span> <span class="variable">body</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">String</span>(message.getBody());</span><br><span class="line">        </span><br><span class="line">        <span class="keyword">if</span> (<span class="string">&quot;order.channel&quot;</span>.equals(channel)) &#123;</span><br><span class="line">            <span class="comment">// 处理订单消息</span></span><br><span class="line">            processOrderMessage(body);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>配置监听器</strong>：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Bean</span></span><br><span class="line"><span class="keyword">public</span> RedisMessageListenerContainer <span class="title function_">container</span><span class="params">(</span></span><br><span class="line"><span class="params">        RedisConnectionFactory connectionFactory,</span></span><br><span class="line"><span class="params">        OrderMessageSubscriber subscriber)</span> &#123;</span><br><span class="line">    </span><br><span class="line">    <span class="type">RedisMessageListenerContainer</span> <span class="variable">container</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">RedisMessageListenerContainer</span>();</span><br><span class="line">    container.setConnectionFactory(connectionFactory);</span><br><span class="line">    container.addMessageListener(subscriber, <span class="keyword">new</span> <span class="title class_">ChannelTopic</span>(<span class="string">&quot;order.channel&quot;</span>));</span><br><span class="line">    <span class="keyword">return</span> container;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="pubsub-mq-3"><p><strong>PubSub消息队列优缺点</strong></p><pre><code>&lt;div class=&quot;note note-warning&quot;&gt;  &lt;div class=&quot;note-header&quot;&gt;    &lt;i class=&quot;note-icon fa-regular fa-circle-dot&quot;&gt;&lt;/i&gt;    &lt;span class=&quot;note-title&quot;&gt;提示信息&lt;/span&gt;  &lt;/div&gt;  &lt;div class=&quot;note-content&quot;&gt;    &lt;p&gt;&lt;strong&gt;实时但不可靠&lt;/strong&gt;&lt;/p&gt;  &lt;/div&gt;&lt;/div&gt;</code></pre><p><strong>优点</strong>：</p><ul><li>✅ <strong>实时性</strong>：消息立即推送给所有订阅者</li><li>✅ <strong>多订阅者</strong>：一个消息可被多个消费者同时接收</li><li>✅ <strong>简单易用</strong>：Redis原生支持，配置简单</li><li>✅ <strong>模式匹配</strong>：支持通配符订阅多个频道</li></ul><p><strong>缺点</strong>：</p><ul><li>❌ <strong>无持久化</strong>：消息不存储，订阅者离线时消息丢失</li><li>❌ <strong>无确认机制</strong>：无法保证消息被成功处理</li><li>❌ <strong>无重试机制</strong>：处理失败的消息无法重新投递</li><li>❌ <strong>内存压力</strong>：大量订阅者可能导致内存问题</li></ul><p><strong>适用场景</strong>：</p><ul><li>✅ <strong>实时通知</strong>：系统状态变更、配置更新等</li><li>✅ <strong>广播消息</strong>：需要多个消费者同时接收的场景</li><li>❌ <strong>重要业务</strong>：订单处理、支付等关键业务</li><li>❌ <strong>离线处理</strong>：消费者可能离线的场景</li></ul><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><p>基于PubSub的消息队列有哪些优缺点？<br>优点：</p><ul><li>采用发布订阅模型，支持多生产、多消费</li></ul><p>缺点：</p><ul><li>不支持数据持久化</li><li>无法避免消息丢失</li><li>消息堆积有上限，超出时数据丢失</li></ul><h3 id="7-1-4-基于Stream实现消息队列">7.1.4 基于Stream实现消息队列</h3><div class="tabs" id="stream-mq"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#stream-mq-1">基本原理</button></li><li class="tab"><button type="button" data-href="#stream-mq-2">代码示例</button></li><li class="tab"><button type="button" data-href="#stream-mq-3">优缺点分析</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="stream-mq-1"><p><strong>实现原理</strong></p><p>Stream 是 Redis 5.0 引入的新数据类型，专门用于实现消息队列功能：</p><ul><li><strong>消息存储</strong>：每个消息都有唯一ID，格式为<code>时间戳-序列号</code></li><li><strong>消费者组</strong>：支持多消费者组同时消费，互不干扰</li><li><strong>ACK机制</strong>：消费者处理完消息后需要确认，保证消息至少消费一次</li></ul><p><strong>核心命令</strong>：</p><ul><li><p><code>XADD</code>：发送消息</p></li><li><p><code>XREAD</code>：读取消息</p></li><li><p><code>XREADGROUP</code>：消费者组读取</p></li><li><p><code>XACK</code>：消息确认</p>  <div class="note note-info">    <div class="note-header">      <i class="note-icon fa-regular fa-circle-info"></i>      <span class="note-title">提示信息</span>    </div>    <div class="note-content">      <p>Stream相比List和PubSub，提供了更完善的消息队列功能，支持消息持久化、消费者组和ACK机制。</p><pre><code>&lt;/div&gt;</code></pre>  </div></li></ul><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="stream-mq-2"><p><strong>代码示例</strong></p><p><strong>生产者发送消息</strong>：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 发送消息到Stream</span></span><br><span class="line">Map&lt;String, Object&gt; message = <span class="keyword">new</span> <span class="title class_">HashMap</span>&lt;&gt;();</span><br><span class="line">message.put(<span class="string">&quot;voucherId&quot;</span>, voucherId);</span><br><span class="line">message.put(<span class="string">&quot;userId&quot;</span>, userId);</span><br><span class="line">message.put(<span class="string">&quot;orderId&quot;</span>, orderId);</span><br><span class="line"></span><br><span class="line"><span class="type">String</span> <span class="variable">recordId</span> <span class="operator">=</span> stringRedisTemplate.opsForStream()</span><br><span class="line">    .add(<span class="string">&quot;stream.orders&quot;</span>, message);</span><br></pre></td></tr></table></figure><p><strong>消费者组消费</strong>：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">class</span> <span class="title class_">VoucherOrderHandler</span> <span class="keyword">implements</span> <span class="title class_">Runnable</span> &#123;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">run</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">while</span> (<span class="literal">true</span>) &#123;</span><br><span class="line">            <span class="keyword">try</span> &#123;</span><br><span class="line">                <span class="comment">// 从消费者组读取消息</span></span><br><span class="line">                List&lt;MapRecord&lt;String, Object, Object&gt;&gt; list = stringRedisTemplate.opsForStream()</span><br><span class="line">                    .read(Consumer.from(<span class="string">&quot;g1&quot;</span>, <span class="string">&quot;c1&quot;</span>),</span><br><span class="line">                          StreamReadOptions.empty().count(<span class="number">1</span>).block(Duration.ofSeconds(<span class="number">2</span>)),</span><br><span class="line">                          StreamOffset.create(<span class="string">&quot;stream.orders&quot;</span>, ReadOffset.lastConsumed()));</span><br><span class="line">                </span><br><span class="line">                <span class="keyword">if</span> (list == <span class="literal">null</span> || list.isEmpty()) &#123;</span><br><span class="line">                    <span class="keyword">continue</span>;</span><br><span class="line">                &#125;</span><br><span class="line">                </span><br><span class="line">                <span class="comment">// 处理消息</span></span><br><span class="line">                MapRecord&lt;String, Object, Object&gt; record = list.get(<span class="number">0</span>);</span><br><span class="line">                Map&lt;Object, Object&gt; value = record.getValue();</span><br><span class="line">                <span class="type">VoucherOrder</span> <span class="variable">voucherOrder</span> <span class="operator">=</span> BeanUtil.fillBeanWithMap(value, <span class="keyword">new</span> <span class="title class_">VoucherOrder</span>(), <span class="literal">true</span>);</span><br><span class="line">                </span><br><span class="line">                <span class="comment">// 创建订单</span></span><br><span class="line">                createVoucherOrder(voucherOrder);</span><br><span class="line">                </span><br><span class="line">                <span class="comment">// 确认消息</span></span><br><span class="line">                stringRedisTemplate.opsForStream().acknowledge(<span class="string">&quot;stream.orders&quot;</span>, <span class="string">&quot;g1&quot;</span>, record.getId());</span><br><span class="line">                </span><br><span class="line">            &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">                log.error(<span class="string">&quot;处理订单异常&quot;</span>, e);</span><br><span class="line">                handlePendingList(); <span class="comment">// 处理异常消息</span></span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 处理pending-list中的异常消息</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">void</span> <span class="title function_">handlePendingList</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">while</span> (<span class="literal">true</span>) &#123;</span><br><span class="line">            <span class="keyword">try</span> &#123;</span><br><span class="line">                List&lt;MapRecord&lt;String, Object, Object&gt;&gt; list = stringRedisTemplate.opsForStream()</span><br><span class="line">                    .read(Consumer.from(<span class="string">&quot;g1&quot;</span>, <span class="string">&quot;c1&quot;</span>),</span><br><span class="line">                          StreamReadOptions.empty().count(<span class="number">1</span>),</span><br><span class="line">                          StreamOffset.create(<span class="string">&quot;stream.orders&quot;</span>, ReadOffset.from(<span class="string">&quot;0&quot;</span>)));</span><br><span class="line">                </span><br><span class="line">                <span class="keyword">if</span> (list == <span class="literal">null</span> || list.isEmpty()) &#123;</span><br><span class="line">                    <span class="keyword">break</span>;</span><br><span class="line">                &#125;</span><br><span class="line">                </span><br><span class="line">                MapRecord&lt;String, Object, Object&gt; record = list.get(<span class="number">0</span>);</span><br><span class="line">                Map&lt;Object, Object&gt; value = record.getValue();</span><br><span class="line">                <span class="type">VoucherOrder</span> <span class="variable">voucherOrder</span> <span class="operator">=</span> BeanUtil.fillBeanWithMap(value, <span class="keyword">new</span> <span class="title class_">VoucherOrder</span>(), <span class="literal">true</span>);</span><br><span class="line">                </span><br><span class="line">                createVoucherOrder(voucherOrder);</span><br><span class="line">                stringRedisTemplate.opsForStream().acknowledge(<span class="string">&quot;stream.orders&quot;</span>, <span class="string">&quot;g1&quot;</span>, record.getId());</span><br><span class="line">                </span><br><span class="line">            &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">                log.error(<span class="string">&quot;处理pending订单异常&quot;</span>, e);</span><br><span class="line">                <span class="keyword">try</span> &#123;</span><br><span class="line">                    Thread.sleep(<span class="number">20</span>);</span><br><span class="line">                &#125; <span class="keyword">catch</span> (Exception ex) &#123;</span><br><span class="line">                    ex.printStackTrace();</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="stream-mq-3"><p><strong>优缺点分析</strong></p><table><thead><tr><th>特性</th><th>Stream</th><th>说明</th></tr></thead><tbody><tr><td>消息持久化</td><td>✅ 支持</td><td>消息保存在内存和磁盘中</td></tr><tr><td>消息回溯</td><td>✅ 支持</td><td>可以读取历史消息</td></tr><tr><td>消费者组</td><td>✅ 支持</td><td>多组消费者独立消费</td></tr><tr><td>ACK机制</td><td>✅ 支持</td><td>保证消息至少消费一次</td></tr><tr><td>消息堆积</td><td>✅ 支持</td><td>内存足够时可堆积大量消息</td></tr><tr><td>实现复杂度</td><td>⭐⭐⭐ 中等</td><td>需要理解消费者组概念</td></tr></tbody></table><p><strong>适用场景</strong>：</p><ul><li>需要消息持久化的业务场景</li><li>多消费者组独立消费</li><li>消息处理可靠性要求高的场景</li><li>复杂的秒杀订单处理</li></ul><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h3 id="7-1-5-消息队列对比总结">7.1.5 消息队列对比总结</h3><div class="tabs" id="mq-compare"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#mq-compare-1">功能特性对比</button></li><li class="tab"><button type="button" data-href="#mq-compare-2">性能对比</button></li><li class="tab"><button type="button" data-href="#mq-compare-3">选择建议</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="mq-compare-1"><p><strong>功能特性对比</strong></p><table><thead><tr><th>功能特性</th><th>List结构</th><th>PubSub</th><th>Stream</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>❌ 不支持</td><td>❌ 不支持</td><td>✅ 支持</td></tr><tr><td>消息确认</td><td>❌ 不支持</td><td>❌ 不支持</td><td>✅ 支持</td></tr><tr><td>阻塞读取</td><td>✅ 支持</td><td>✅ 支持</td><td>✅ 支持</td></tr><tr><td>消息堆积</td><td>受内存限制</td><td>受内存限制</td><td>受内存限制</td></tr></tbody></table><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="mq-compare-2"><p><strong>性能对比</strong></p><table><thead><tr><th>性能指标</th><th>List</th><th>PubSub</th><th>Stream</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>CPU消耗</td><td>低</td><td>最低</td><td>中等</td></tr><tr><td>内存使用</td><td>低</td><td>低</td><td>中等</td></tr></tbody></table><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="mq-compare-3"><p><strong>选择建议</strong></p><ul><li><strong>List队列</strong>：简单的任务队列，对可靠性要求不高</li><li><strong>PubSub</strong>：实时消息推送，允许消息丢失的场景</li><li><strong>Stream</strong>：业务消息队列，需要高可靠性和完整功能</li></ul><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h2 id="8-达人探店">8. 达人探店</h2>    <div class="note note-info">      <div class="note-header">        <i class="note-icon fa-regular fa-circle-info"></i>        <span class="note-title">提示信息</span>      </div>      <div class="note-content">        <p>发布探店笔记</p><p>探店笔记类似点评网站的评价，往往是图文结合。对应的表有两个：<br>tb_blog：探店笔记表，包含笔记中的标题、文字、图片等</p><p><strong>tb_blog表结构：</strong></p><table><thead><tr><th>Field</th><th>Type</th><th>Collation</th><th>Null</th><th>Key</th><th>Default</th><th>Extra</th><th>Comment</th></tr></thead><tbody><tr><td>id</td><td>bigint unsigned</td><td>(NULL)</td><td>NO</td><td>PRI</td><td>(NULL)</td><td>auto_increment</td><td>主键</td></tr><tr><td>shop_id</td><td>bigint</td><td>(NULL)</td><td>NO</td><td></td><td>(NULL)</td><td></td><td>商户id</td></tr><tr><td>user_id</td><td>bigint unsigned</td><td>(NULL)</td><td>NO</td><td></td><td>(NULL)</td><td></td><td>用户id</td></tr><tr><td>title</td><td>varchar(255)</td><td>utf8mb4_unicode_ci</td><td>NO</td><td></td><td>(NULL)</td><td></td><td>标题</td></tr><tr><td>images</td><td>varchar(2048)</td><td>utf8mb4_general_ci</td><td>NO</td><td></td><td>(NULL)</td><td></td><td>探店的照片，最多9张，多张以&quot;,&quot;隔开</td></tr><tr><td>content</td><td>varchar(2048)</td><td>utf8mb4_unicode_ci</td><td>NO</td><td></td><td>(NULL)</td><td></td><td>探店的文字描述</td></tr><tr><td>liked</td><td>int unsigned</td><td>(NULL)</td><td>YES</td><td></td><td>0</td><td></td><td>点赞数量</td></tr><tr><td>comments</td><td>int unsigned</td><td>(NULL)</td><td>YES</td><td></td><td>(NULL)</td><td></td><td>评论数量</td></tr><tr><td>create_time</td><td>timestamp</td><td>(NULL)</td><td>NO</td><td></td><td>CURRENT_TIMESTAMP</td><td>DEFAULT_GENERATED</td><td>创建时间</td></tr><tr><td>update_time</td><td>timestamp</td><td>(NULL)</td><td>NO</td><td></td><td>CURRENT_TIMESTAMP</td><td>DEFAULT_GENERATED on update CURRENT_TIMESTAMP</td><td>更新时间</td></tr></tbody></table><p>tb_blog_comments：其他用户对探店笔记的评价</p><p><strong>tb_blog_comments表结构：</strong></p><table><thead><tr><th>Field</th><th>Type</th><th>Collation</th><th>Null</th><th>Key</th><th>Default</th><th>Extra</th><th>Comment</th></tr></thead><tbody><tr><td>id</td><td>bigint unsigned</td><td>(NULL)</td><td>NO</td><td>PRI</td><td>(NULL)</td><td>auto_increment</td><td>主键</td></tr><tr><td>user_id</td><td>bigint unsigned</td><td>(NULL)</td><td>NO</td><td></td><td>(NULL)</td><td></td><td>用户id</td></tr><tr><td>blog_id</td><td>bigint unsigned</td><td>(NULL)</td><td>NO</td><td></td><td>(NULL)</td><td></td><td>探店id</td></tr><tr><td>parent_id</td><td>bigint unsigned</td><td>(NULL)</td><td>NO</td><td></td><td>(NULL)</td><td></td><td>关联的1级评论id，如果是一级评论，则值为0</td></tr><tr><td>answer_id</td><td>bigint unsigned</td><td>(NULL)</td><td>NO</td><td></td><td>(NULL)</td><td></td><td>回复的评论id</td></tr><tr><td>content</td><td>varchar(255)</td><td>utf8mb4_general_ci</td><td>NO</td><td></td><td>(NULL)</td><td></td><td>回复的内容</td></tr><tr><td>liked</td><td>int unsigned</td><td>(NULL)</td><td>YES</td><td></td><td>(NULL)</td><td></td><td>点赞数</td></tr><tr><td>status</td><td>tinyint unsigned</td><td>(NULL)</td><td>YES</td><td></td><td>(NULL)</td><td></td><td>状态，0：正常，1：被举报，2：禁止查看</td></tr><tr><td>create_time</td><td>timestamp</td><td>(NULL)</td><td>NO</td><td></td><td>CURRENT_TIMESTAMP</td><td>DEFAULT_GENERATED</td><td>创建时间</td></tr><tr><td>update_time</td><td>timestamp</td><td>(NULL)</td><td>NO</td><td></td><td>CURRENT_TIMESTAMP</td><td>DEFAULT_GENERATED on update CURRENT_TIMESTAMP</td><td>更新时间</td></tr></tbody></table><p>达人探店功能允许用户发布探店笔记，包括图片上传、笔记发布、点赞互动等功能。本章节将介绍如何使用Redis优化这些功能的实现。</p>      </div>    </div>  <p><strong>对应的实体类</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Data</span></span><br><span class="line"><span class="meta">@EqualsAndHashCode(callSuper = false)</span></span><br><span class="line"><span class="meta">@Accessors(chain = true)</span></span><br><span class="line"><span class="meta">@TableName(&quot;tb_blog&quot;)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Blog</span> <span class="keyword">implements</span> <span class="title class_">Serializable</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">long</span> <span class="variable">serialVersionUID</span> <span class="operator">=</span> <span class="number">1L</span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 主键</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@TableId(value = &quot;id&quot;, type = IdType.AUTO)</span></span><br><span class="line">    <span class="keyword">private</span> Long id;</span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 商户id</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> Long shopId;</span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 用户id</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> Long userId;</span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 用户图标</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@TableField(exist = false)</span></span><br><span class="line">    <span class="keyword">private</span> String icon;</span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 用户姓名</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@TableField(exist = false)</span></span><br><span class="line">    <span class="keyword">private</span> String name;</span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 是否点赞过了</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@TableField(exist = false)</span></span><br><span class="line">    <span class="keyword">private</span> Boolean isLike;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 标题</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> String title;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 探店的照片，最多9张，多张以&quot;,&quot;隔开</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> String images;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 探店的文字描述</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> String content;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 点赞数量</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> Integer liked;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 评论数量</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> Integer comments;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 创建时间</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> LocalDateTime createTime;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 更新时间</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> LocalDateTime updateTime;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="8-1-图片上传与笔记发布">8.1 图片上传与笔记发布</h3><div class="tabs" id="blog-upload"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#blog-upload-1">功能概述</button></li><li class="tab"><button type="button" data-href="#blog-upload-2">代码实现</button></li><li class="tab"><button type="button" data-href="#blog-upload-3">关键点分析</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="blog-upload-1"><p>探店笔记发布包含两个核心功能：</p><ul><li><strong>图片上传</strong>：支持单张图片上传，生成唯一文件名</li><li><strong>笔记发布</strong>：保存探店博文，关联上传的图片</li></ul><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="blog-upload-2"><p><strong>图片上传控制器</strong>：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Slf4j</span></span><br><span class="line"><span class="meta">@RestController</span></span><br><span class="line"><span class="meta">@RequestMapping(&quot;upload&quot;)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">UploadController</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@PostMapping(&quot;blog&quot;)</span></span><br><span class="line">    <span class="keyword">public</span> Result <span class="title function_">uploadImage</span><span class="params">(<span class="meta">@RequestParam(&quot;file&quot;)</span> MultipartFile image)</span> &#123;</span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            <span class="comment">// 获取原始文件名称</span></span><br><span class="line">            <span class="type">String</span> <span class="variable">originalFilename</span> <span class="operator">=</span> image.getOriginalFilename();</span><br><span class="line">            <span class="comment">// 生成新文件名</span></span><br><span class="line">            <span class="type">String</span> <span class="variable">fileName</span> <span class="operator">=</span> createNewFileName(originalFilename);</span><br><span class="line">            <span class="comment">// 保存文件</span></span><br><span class="line">            image.transferTo(<span class="keyword">new</span> <span class="title class_">File</span>(SystemConstants.IMAGE_UPLOAD_DIR, fileName));</span><br><span class="line">            <span class="comment">// 返回结果</span></span><br><span class="line">            log.debug(<span class="string">&quot;文件上传成功，&#123;&#125;&quot;</span>, fileName);</span><br><span class="line">            <span class="keyword">return</span> Result.ok(fileName);</span><br><span class="line">        &#125; <span class="keyword">catch</span> (IOException e) &#123;</span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">RuntimeException</span>(<span class="string">&quot;文件上传失败&quot;</span>, e);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>笔记发布控制器</strong>：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@RestController</span></span><br><span class="line"><span class="meta">@RequestMapping(&quot;/blog&quot;)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">BlogController</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Resource</span></span><br><span class="line">    <span class="keyword">private</span> IBlogService blogService;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@PostMapping</span></span><br><span class="line">    <span class="keyword">public</span> Result <span class="title function_">saveBlog</span><span class="params">(<span class="meta">@RequestBody</span> Blog blog)</span> &#123;</span><br><span class="line">        <span class="comment">//获取登录用户</span></span><br><span class="line">        <span class="type">UserDTO</span> <span class="variable">user</span> <span class="operator">=</span> UserHolder.getUser();</span><br><span class="line">        blog.setUpdateTime(user.getId());</span><br><span class="line">        <span class="comment">//保存探店博文</span></span><br><span class="line">        blogService.saveBlog(blog);</span><br><span class="line">        <span class="comment">//返回id</span></span><br><span class="line">        <span class="keyword">return</span> Result.ok(blog.getId());</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><pre><code>&lt;div class=&quot;note note-info&quot;&gt;  &lt;div class=&quot;note-header&quot;&gt;    &lt;i class=&quot;note-icon fa-regular fa-circle-info&quot;&gt;&lt;/i&gt;    &lt;span class=&quot;note-title&quot;&gt;提示信息&lt;/span&gt;  &lt;/div&gt;  &lt;div class=&quot;note-content&quot;&gt;    &lt;p&gt;注意：需要修改&lt;code&gt;SystemConstants.IMAGE_UPLOAD_DIR&lt;/code&gt;为实际的图片存储路径，生产环境中建议使用云存储服务。&lt;/p&gt;  &lt;/div&gt;&lt;/div&gt;</code></pre><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="blog-upload-3"><ol><li><strong>文件命名策略</strong>：使用UUID确保文件名唯一性，避免冲突</li><li><strong>异常处理</strong>：捕获IO异常并转换为业务异常</li><li><strong>用户认证</strong>：通过<code>UserHolder</code>获取当前登录用户信息</li><li><strong>返回值设计</strong>：返回生成的文件名，供前端展示和后续业务使用</li></ol><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h3 id="8-2-查看探店笔记">8.2 查看探店笔记</h3><div class="tabs" id="blog-query"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#blog-query-1">功能概述</button></li><li class="tab"><button type="button" data-href="#blog-query-2">代码实现</button></li><li class="tab"><button type="button" data-href="#blog-query-3">关键点分析</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="blog-query-1"><p>查看探店笔记功能需要：</p><ul><li><strong>笔记查询</strong>：根据ID查询笔记详情</li><li><strong>用户信息</strong>：查询笔记作者信息</li><li><strong>点赞状态</strong>：判断当前用户是否点赞</li></ul><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="blog-query-2"><p><strong>controller</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@GetMapping(&quot;/hot&quot;)</span></span><br><span class="line">    <span class="keyword">public</span> Result <span class="title function_">queryHotBlog</span><span class="params">(<span class="meta">@RequestParam(value = &quot;current&quot;, defaultValue = &quot;1&quot;)</span> Integer current)</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> blogService.queryHotBlog(current);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="meta">@GetMapping(&quot;/&#123;id&#125;&quot;)</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">queryById</span><span class="params">(<span class="meta">@PathVariable</span> Integer id)</span>&#123;</span><br><span class="line">    <span class="keyword">return</span> blogService.queryById(id);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>service</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">queryHotBlog</span><span class="params">(Integer current)</span> &#123;</span><br><span class="line">    <span class="comment">// 根据用户查询</span></span><br><span class="line">    Page&lt;Blog&gt; page = query()</span><br><span class="line">            .orderByDesc(<span class="string">&quot;liked&quot;</span>)</span><br><span class="line">            .page(<span class="keyword">new</span> <span class="title class_">Page</span>&lt;&gt;(current, SystemConstants.MAX_PAGE_SIZE));</span><br><span class="line">    <span class="comment">// 获取当前页数据</span></span><br><span class="line">    List&lt;Blog&gt; records = page.getRecords();</span><br><span class="line">    <span class="comment">// 查询用户</span></span><br><span class="line">    records.forEach(<span class="built_in">this</span>::queryBlogUser);</span><br><span class="line">    <span class="keyword">return</span> Result.ok(records);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">queryById</span><span class="params">(Integer id)</span> &#123;</span><br><span class="line">    <span class="type">Blog</span> <span class="variable">blog</span> <span class="operator">=</span> getById(id);</span><br><span class="line">    <span class="keyword">if</span> (blog == <span class="literal">null</span>) &#123;</span><br><span class="line">        <span class="keyword">return</span> Result.fail(<span class="string">&quot;评价不存在或已被删除&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    queryBlogUser(blog);</span><br><span class="line">    <span class="keyword">return</span> Result.ok(blog);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title function_">queryBlogUser</span><span class="params">(Blog blog)</span> &#123;</span><br><span class="line">    <span class="type">Long</span> <span class="variable">userId</span> <span class="operator">=</span> blog.getUserId();</span><br><span class="line">    <span class="type">User</span> <span class="variable">user</span> <span class="operator">=</span> userService.getById(userId);</span><br><span class="line">    blog.setName(user.getNickName());</span><br><span class="line">    blog.setIcon(user.getIcon());</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="blog-query-3"><ol><li><strong>数据完整性</strong>：先查询笔记，再查询关联的用户信息</li><li><strong>异常处理</strong>：笔记不存在时给出友好提示</li><li><strong>数据组装</strong>：<code>queryBlogUser</code>方法负责填充用户相关信息</li></ol><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h3 id="8-3-点赞功能优化">8.3 点赞功能优化</h3><div class="tabs" id="blog-like"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#blog-like-1">功能概述</button></li><li class="tab"><button type="button" data-href="#blog-like-2">需求优化</button></li><li class="tab"><button type="button" data-href="#blog-like-3">代码实现</button></li><li class="tab"><button type="button" data-href="#blog-like-4">发送请求</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="blog-like-1"><p>初始点赞实现存在严重问题：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@GetMapping(&quot;/likes/&#123;id&#125;&quot;)</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">queryBlogLikes</span><span class="params">(<span class="meta">@PathVariable(&quot;id&quot;)</span> Long id)</span> &#123;</span><br><span class="line">    <span class="comment">//修改点赞数量</span></span><br><span class="line">    blogService.update().setSql(<span class="string">&quot;liked = liked +1 &quot;</span>).eq(<span class="string">&quot;id&quot;</span>,id).update();</span><br><span class="line">    <span class="keyword">return</span> Result.ok();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>问题</strong>：用户可以无限点赞，没有任何限制机制</p><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="blog-like-2"><ul><li>同一个用户只能点赞一次，再次点击则取消点赞</li><li>如果当前用户已经点赞，则点赞按钮高亮显示</li><li>使用Redis的Set集合记录点赞用户，保证唯一性</li></ul><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="blog-like-3"><p><strong>1. 修改Blog实体类</strong>：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@TableField(exist = false)</span></span><br><span class="line"><span class="keyword">private</span> Boolean isLike; <span class="comment">// 是否被当前用户点赞</span></span><br></pre></td></tr></table></figure><p><strong>2. 点赞业务逻辑</strong>：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@PutMapping(&quot;/like/&#123;id&#125;&quot;)</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">likeBlog</span><span class="params">(<span class="meta">@PathVariable(&quot;id&quot;)</span> Long id)</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> blogService.likeBlog(id);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">likeBlog</span><span class="params">(Long id)</span>&#123;</span><br><span class="line">    <span class="comment">// 1.获取登录用户</span></span><br><span class="line">    <span class="type">Long</span> <span class="variable">userId</span> <span class="operator">=</span> UserHolder.getUser().getId();</span><br><span class="line">    <span class="comment">// 2.判断当前登录用户是否已经点赞</span></span><br><span class="line">    <span class="type">String</span> <span class="variable">key</span> <span class="operator">=</span> BLOG_LIKED_KEY + id;</span><br><span class="line">    <span class="type">Boolean</span> <span class="variable">isMember</span> <span class="operator">=</span> stringRedisTemplate.opsForSet().isMember(key, userId.toString());</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">if</span>(BooleanUtil.isFalse(isMember))&#123;</span><br><span class="line">        <span class="comment">//3.如果未点赞，可以点赞</span></span><br><span class="line">        <span class="comment">//3.1 数据库点赞数+1</span></span><br><span class="line">        <span class="type">boolean</span> <span class="variable">isSuccess</span> <span class="operator">=</span> update().setSql(<span class="string">&quot;liked = liked + 1&quot;</span>).eq(<span class="string">&quot;id&quot;</span>, id).update();</span><br><span class="line">        <span class="comment">//3.2 保存用户到Redis的set集合</span></span><br><span class="line">        <span class="keyword">if</span>(isSuccess)&#123;</span><br><span class="line">            stringRedisTemplate.opsForSet().add(key,userId.toString());</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;<span class="keyword">else</span>&#123;</span><br><span class="line">        <span class="comment">//4.如果已点赞，取消点赞</span></span><br><span class="line">        <span class="comment">//4.1 数据库点赞数-1</span></span><br><span class="line">        <span class="type">boolean</span> <span class="variable">isSuccess</span> <span class="operator">=</span> update().setSql(<span class="string">&quot;liked = liked - 1&quot;</span>).eq(<span class="string">&quot;id&quot;</span>, id).update();</span><br><span class="line">        <span class="comment">//4.2 把用户从Redis的set集合移除</span></span><br><span class="line">        <span class="keyword">if</span>(isSuccess)&#123;</span><br><span class="line">            stringRedisTemplate.opsForSet().remove(key,userId.toString());</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> Result.ok();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>关键点分析</strong></p><ol><li><strong>Redis Key设计</strong>：<code>blog:liked:{blogId}</code>，每个笔记一个独立的Set</li><li><strong>原子性保证</strong>：先判断再操作，通过Redis Set保证用户唯一性</li><li><strong>数据一致性</strong>：数据库和Redis同步更新，确保点赞数准确</li><li><strong>性能优化</strong>：Redis Set操作O(1)时间复杂度，性能优异</li></ol><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="blog-like-4"><p><strong>点击点赞按钮，查看发送的请求</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">请求网址: http://localhost:8080/api/blog/like/4</span><br><span class="line">请求方法: PUT</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h3 id="8-4-点赞排行榜">8.4 点赞排行榜</h3><div class="tabs" id="blog-ranking"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#blog-ranking-1">功能概述</button></li><li class="tab"><button type="button" data-href="#blog-ranking-2">需求分析</button></li><li class="tab"><button type="button" data-href="#blog-ranking-3">代码实现</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="blog-ranking-1"><p>在探店笔记详情页面需要展示点赞排行榜，显示最早点赞的TOP5用户：</p><ul><li><strong>排序需求</strong>：按点赞时间排序，最早点赞的排在前面</li><li><strong>唯一性</strong>：每个用户只能出现一次</li><li><strong>性能要求</strong>：快速查询TOP5用户</li></ul><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="blog-ranking-2"><p>使用Redis的SortedSet（ZSet）数据结构：</p><ul><li><strong>唯一性</strong>：ZSet保证成员唯一</li><li><strong>排序能力</strong>：根据score值排序，score使用时间戳</li><li><strong>范围查询</strong>：支持按排名范围查询</li></ul><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="blog-ranking-3"><p><strong>1. 修改点赞逻辑（使用ZSet）</strong>：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">likeBlog</span><span class="params">(Long id)</span> &#123;</span><br><span class="line">    <span class="comment">// 1.获取登录用户</span></span><br><span class="line">    <span class="type">Long</span> <span class="variable">userId</span> <span class="operator">=</span> UserHolder.getUser().getId();</span><br><span class="line">    <span class="comment">// 2.判断当前登录用户是否已经点赞</span></span><br><span class="line">    <span class="type">String</span> <span class="variable">key</span> <span class="operator">=</span> BLOG_LIKED_KEY + id;</span><br><span class="line">    <span class="type">Double</span> <span class="variable">score</span> <span class="operator">=</span> stringRedisTemplate.opsForZSet().score(key, userId.toString());</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">if</span> (score == <span class="literal">null</span>) &#123;</span><br><span class="line">        <span class="comment">// 3.如果未点赞，可以点赞</span></span><br><span class="line">        <span class="comment">// 3.1.数据库点赞数 + 1</span></span><br><span class="line">        <span class="type">boolean</span> <span class="variable">isSuccess</span> <span class="operator">=</span> update().setSql(<span class="string">&quot;liked = liked + 1&quot;</span>).eq(<span class="string">&quot;id&quot;</span>, id).update();</span><br><span class="line">        <span class="comment">// 3.2.保存用户到Redis的zset集合，score为当前时间戳</span></span><br><span class="line">        <span class="keyword">if</span> (isSuccess) &#123;</span><br><span class="line">            stringRedisTemplate.opsForZSet().add(key, userId.toString(), System.currentTimeMillis());</span><br><span class="line">        &#125;</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        <span class="comment">// 4.如果已点赞，取消点赞</span></span><br><span class="line">        <span class="comment">// 4.1.数据库点赞数 -1</span></span><br><span class="line">        <span class="type">boolean</span> <span class="variable">isSuccess</span> <span class="operator">=</span> update().setSql(<span class="string">&quot;liked = liked - 1&quot;</span>).eq(<span class="string">&quot;id&quot;</span>, id).update();</span><br><span class="line">        <span class="comment">// 4.2.把用户从Redis的zset集合移除</span></span><br><span class="line">        <span class="keyword">if</span> (isSuccess) &#123;</span><br><span class="line">            stringRedisTemplate.opsForZSet().remove(key, userId.toString());</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> Result.ok();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>2. 查询点赞排行榜</strong>：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">queryBlogLikes</span><span class="params">(Long id)</span> &#123;</span><br><span class="line">    <span class="type">String</span> <span class="variable">key</span> <span class="operator">=</span> BLOG_LIKED_KEY + id;</span><br><span class="line">    <span class="comment">// 1.查询top5的点赞用户 zrange key 0 4</span></span><br><span class="line">    Set&lt;String&gt; top5 = stringRedisTemplate.opsForZSet().range(key, <span class="number">0</span>, <span class="number">4</span>);</span><br><span class="line">    <span class="keyword">if</span> (top5 == <span class="literal">null</span> || top5.isEmpty()) &#123;</span><br><span class="line">        <span class="keyword">return</span> Result.ok(Collections.emptyList());</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 2.解析出其中的用户id</span></span><br><span class="line">    List&lt;Long&gt; ids = top5.stream().map(Long::valueOf).collect(Collectors.toList());</span><br><span class="line">    <span class="type">String</span> <span class="variable">idStr</span> <span class="operator">=</span> StrUtil.join(<span class="string">&quot;,&quot;</span>, ids);</span><br><span class="line">    <span class="comment">// 3.根据用户id查询用户 WHERE id IN ( 5 , 1 ) ORDER BY FIELD(id, 5, 1)</span></span><br><span class="line">    List&lt;UserDTO&gt; userDTOS = userService.query()</span><br><span class="line">            .in(<span class="string">&quot;id&quot;</span>, ids).last(<span class="string">&quot;ORDER BY FIELD(id,&quot;</span> + idStr + <span class="string">&quot;)&quot;</span>).list()</span><br><span class="line">            .stream()</span><br><span class="line">            .map(user -&gt; BeanUtil.copyProperties(user, UserDTO.class))</span><br><span class="line">            .collect(Collectors.toList());</span><br><span class="line">    <span class="comment">// 4.返回</span></span><br><span class="line">    <span class="keyword">return</span> Result.ok(userDTOS);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>关键点分析</strong></p><ol><li><strong>Score设计</strong>：使用时间戳作为score，自然按时间排序</li><li><strong>范围查询</strong>：<code>zrange key 0 4</code>获取前5个用户，时间复杂度O(log(n)+m)</li><li><strong>数据一致性</strong>：点赞和取消点赞时同步更新数据库和Redis</li><li><strong>内存优化</strong>：只存储用户ID，不存储完整用户信息</li></ol><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h2 id="9-好友关注">9. 好友关注</h2><h3 id="9-1-关注和取消关注">9.1 关注和取消关注</h3><p>针对用户的操作：可以对用户进行关注和取消关注功能。</p><div class="tabs" id="follow-basic"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#follow-basic-1">功能概述</button></li><li class="tab"><button type="button" data-href="#follow-basic-2">代码实现</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="follow-basic-1"><p>好友关注功能实现用户之间的关注关系管理：</p><ul><li><strong>关注用户</strong>：用户A关注用户B</li><li><strong>取消关注</strong>：用户A取消对用户B的关注</li><li><strong>关注状态查询</strong>：查询用户A是否关注了用户B</li></ul><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="follow-basic-2"><p><strong>1. 数据库表结构</strong>：</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">CREATE TABLE</span> `tb_follow`  (</span><br><span class="line">  `id` <span class="type">bigint</span>(<span class="number">0</span>) <span class="keyword">NOT NULL</span> AUTO_INCREMENT,</span><br><span class="line">  `user_id` <span class="type">bigint</span>(<span class="number">20</span>) <span class="keyword">NOT NULL</span> COMMENT <span class="string">&#x27;用户id&#x27;</span>,</span><br><span class="line">  `follow_user_id` <span class="type">bigint</span>(<span class="number">20</span>) <span class="keyword">NOT NULL</span> COMMENT <span class="string">&#x27;关联的用户id&#x27;</span>,</span><br><span class="line">  `create_time` <span class="type">timestamp</span> <span class="keyword">NOT NULL</span> <span class="keyword">DEFAULT</span> <span class="built_in">CURRENT_TIMESTAMP</span> COMMENT <span class="string">&#x27;创建时间&#x27;</span>,</span><br><span class="line">  <span class="keyword">PRIMARY KEY</span> (`id`),</span><br><span class="line">  <span class="keyword">UNIQUE</span> INDEX `idx_user_id_follow_user_id`(`user_id`, `follow_user_id`) <span class="keyword">USING</span> BTREE</span><br><span class="line">) ENGINE <span class="operator">=</span> InnoDB <span class="keyword">CHARACTER SET</span> <span class="operator">=</span> utf8mb4 <span class="keyword">COLLATE</span> <span class="operator">=</span> utf8mb4_general_ci COMMENT <span class="operator">=</span> <span class="string">&#x27;用户关注表&#x27;</span>;</span><br></pre></td></tr></table></figure><p><strong>2. 控制器层</strong>：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@RestController</span></span><br><span class="line"><span class="meta">@RequestMapping(&quot;/follow&quot;)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">FollowController</span> &#123;</span><br><span class="line">    </span><br><span class="line">    <span class="meta">@Resource</span></span><br><span class="line">    <span class="keyword">private</span> IFollowService followService;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 关注和取消关注</span></span><br><span class="line">    <span class="meta">@PutMapping(&quot;/&#123;id&#125;/&#123;isFollow&#125;&quot;)</span></span><br><span class="line">    <span class="keyword">public</span> Result <span class="title function_">follow</span><span class="params">(<span class="meta">@PathVariable(&quot;id&quot;)</span> Long followUserId, </span></span><br><span class="line"><span class="params">                        <span class="meta">@PathVariable(&quot;isFollow&quot;)</span> Boolean isFollow)</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> followService.follow(followUserId, isFollow);</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 判断是否关注</span></span><br><span class="line">    <span class="meta">@GetMapping(&quot;/or/not/&#123;id&#125;&quot;)</span></span><br><span class="line">    <span class="keyword">public</span> Result <span class="title function_">isFollow</span><span class="params">(<span class="meta">@PathVariable(&quot;id&quot;)</span> Long followUserId)</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> followService.isFollow(followUserId);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>3. 业务逻辑层</strong>：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">isFollow</span><span class="params">(Long followUserId)</span> &#123;</span><br><span class="line">    <span class="comment">// 1.获取登录用户</span></span><br><span class="line">    <span class="type">Long</span> <span class="variable">userId</span> <span class="operator">=</span> UserHolder.getUser().getId();</span><br><span class="line">    <span class="comment">// 2.查询是否关注 select count(*) from tb_follow where user_id = ? and follow_user_id = ?</span></span><br><span class="line">    <span class="type">Integer</span> <span class="variable">count</span> <span class="operator">=</span> query().eq(<span class="string">&quot;user_id&quot;</span>, userId).eq(<span class="string">&quot;follow_user_id&quot;</span>, followUserId).count();</span><br><span class="line">    <span class="comment">// 3.判断</span></span><br><span class="line">    <span class="keyword">return</span> Result.ok(count &gt; <span class="number">0</span>);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">follow</span><span class="params">(Long followUserId, Boolean isFollow)</span> &#123;</span><br><span class="line">    <span class="comment">// 1.获取登录用户</span></span><br><span class="line">    <span class="type">Long</span> <span class="variable">userId</span> <span class="operator">=</span> UserHolder.getUser().getId();</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">if</span> (isFollow) &#123;</span><br><span class="line">        <span class="comment">// 2.关注，新增数据</span></span><br><span class="line">        <span class="type">Follow</span> <span class="variable">follow</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Follow</span>();</span><br><span class="line">        follow.setUserId(userId);</span><br><span class="line">        follow.setFollowUserId(followUserId);</span><br><span class="line">        <span class="type">boolean</span> <span class="variable">isSuccess</span> <span class="operator">=</span> save(follow);</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        <span class="comment">// 3.取关，删除数据</span></span><br><span class="line">        remove(<span class="keyword">new</span> <span class="title class_">QueryWrapper</span>&lt;Follow&gt;()</span><br><span class="line">                .eq(<span class="string">&quot;user_id&quot;</span>, userId).eq(<span class="string">&quot;follow_user_id&quot;</span>, followUserId));</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> Result.ok();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>关键点分析</strong></p><ol><li><strong>唯一索引</strong>：<code>idx_user_id_follow_user_id</code>防止重复关注</li><li><strong>事务性</strong>：关注操作同时操作数据库，保证数据一致性</li><li><strong>幂等性</strong>：重复关注或取消关注不会产生副作用</li><li><strong>性能优化</strong>：关注状态查询使用count，比查询全表更高效</li></ol><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h3 id="9-2-共同关注">9.2 共同关注</h3><div class="tabs" id="follow-common"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#follow-common-1">需求分析</button></li><li class="tab"><button type="button" data-href="#follow-common-2">技术方案</button></li><li class="tab"><button type="button" data-href="#follow-common-3">代码实现</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="follow-common-1"><p>在博主个人页面展示当前用户与博主的共同关注：</p><ul><li><strong>场景</strong>：用户A访问用户B的个人主页</li><li><strong>需求</strong>：显示A和B都关注的用户列表</li><li><strong>技术选型</strong>：Redis Set集合的交集操作</li></ul><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="follow-common-2"><p>使用Redis Set数据结构存储关注关系：</p><ul><li><strong>Key设计</strong>：<code>follows:{userId}</code> 存储用户关注的所有人</li><li><strong>集合操作</strong>：<code>SINTER</code>命令求两个集合的交集</li><li><strong>性能优势</strong>：Set操作时间复杂度O(N)，比数据库查询更高效</li></ul><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="follow-common-3"><p><strong>1. 改造关注逻辑（添加Redis缓存）</strong>：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">follow</span><span class="params">(Long followUserId, Boolean isFollow)</span> &#123;</span><br><span class="line">    <span class="comment">// 1.获取登录用户</span></span><br><span class="line">    <span class="type">Long</span> <span class="variable">userId</span> <span class="operator">=</span> UserHolder.getUser().getId();</span><br><span class="line">    <span class="type">String</span> <span class="variable">key</span> <span class="operator">=</span> <span class="string">&quot;follows:&quot;</span> + userId;</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">if</span> (isFollow) &#123;</span><br><span class="line">        <span class="comment">// 2.关注，新增数据</span></span><br><span class="line">        <span class="type">Follow</span> <span class="variable">follow</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Follow</span>();</span><br><span class="line">        follow.setUserId(userId);</span><br><span class="line">        follow.setFollowUserId(followUserId);</span><br><span class="line">        <span class="type">boolean</span> <span class="variable">isSuccess</span> <span class="operator">=</span> save(follow);</span><br><span class="line">        <span class="keyword">if</span> (isSuccess) &#123;</span><br><span class="line">            <span class="comment">// 把关注用户的id，放入redis的set集合</span></span><br><span class="line">            stringRedisTemplate.opsForSet().add(key, followUserId.toString());</span><br><span class="line">        &#125;</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        <span class="comment">// 3.取关，删除数据</span></span><br><span class="line">        <span class="type">boolean</span> <span class="variable">isSuccess</span> <span class="operator">=</span> remove(<span class="keyword">new</span> <span class="title class_">QueryWrapper</span>&lt;Follow&gt;()</span><br><span class="line">                .eq(<span class="string">&quot;user_id&quot;</span>, userId).eq(<span class="string">&quot;follow_user_id&quot;</span>, followUserId));</span><br><span class="line">        <span class="keyword">if</span> (isSuccess) &#123;</span><br><span class="line">            <span class="comment">// 把关注用户的id从Redis集合中移除</span></span><br><span class="line">            stringRedisTemplate.opsForSet().remove(key, followUserId.toString());</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> Result.ok();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>2. 共同关注查询</strong>：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">followCommons</span><span class="params">(Long targetUserId)</span> &#123;</span><br><span class="line">    <span class="comment">// 1.获取当前用户</span></span><br><span class="line">    <span class="type">Long</span> <span class="variable">userId</span> <span class="operator">=</span> UserHolder.getUser().getId();</span><br><span class="line">    <span class="type">String</span> <span class="variable">key</span> <span class="operator">=</span> <span class="string">&quot;follows:&quot;</span> + userId;</span><br><span class="line">    <span class="comment">// 2.求交集</span></span><br><span class="line">    <span class="type">String</span> <span class="variable">key2</span> <span class="operator">=</span> <span class="string">&quot;follows:&quot;</span> + targetUserId;</span><br><span class="line">    Set&lt;String&gt; intersect = stringRedisTemplate.opsForSet().intersect(key, key2);</span><br><span class="line">    <span class="keyword">if</span> (intersect == <span class="literal">null</span> || intersect.isEmpty()) &#123;</span><br><span class="line">        <span class="comment">// 无交集</span></span><br><span class="line">        <span class="keyword">return</span> Result.ok(Collections.emptyList());</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 3.解析id集合</span></span><br><span class="line">    List&lt;Long&gt; ids = intersect.stream().map(Long::valueOf).collect(Collectors.toList());</span><br><span class="line">    <span class="comment">// 4.查询用户详情</span></span><br><span class="line">    List&lt;UserDTO&gt; users = userService.listByIds(ids)</span><br><span class="line">            .stream()</span><br><span class="line">            .map(user -&gt; BeanUtil.copyProperties(user, UserDTO.class))</span><br><span class="line">            .collect(Collectors.toList());</span><br><span class="line">    <span class="keyword">return</span> Result.ok(users);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>关键点分析</strong></p><ol><li><strong>数据一致性</strong>：数据库和Redis双写一致性，关注成功才写入Redis</li><li><strong>内存优化</strong>：只存储用户ID，不存储完整用户信息</li><li><strong>异常处理</strong>：交集为空时返回空列表，避免空指针异常</li><li><strong>性能考虑</strong>：使用<code>listByIds</code>批量查询，减少数据库访问次数</li></ol><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h3 id="9-3-Feed流实现方案">9.3 Feed流实现方案</h3><p>当我们关注了用户后，这个用户发了动态，那么我们应该把这些数据推送给用户，这个需求，其实我们又把他叫做Feed流，关注推送也叫做Feed流，直译为投喂。为用户持续的提供“沉浸式”的体验，通过无限下拉刷新获取新的信息。</p><p>对于传统的模式的内容解锁：我们是需要用户去通过搜索引擎或者是其他的方式去解锁想要看的内容</p><p><img src="https://markpic.adoreorg.cn/2025/09/Redis/1653808641260.png" alt="1653808641260"></p><p>对于新型的Feed流的的效果：不需要我们用户再去推送信息，而是系统分析用户到底想要什么，然后直接把内容推送给用户，从而使用户能够更加的节约时间，不用主动去寻找。</p><p><img src="https://markpic.adoreorg.cn/2025/09/Redis/1653808993693.png" alt="1653808993693"></p><p>Feed流的实现有两种模式：</p><p>Feed流产品有两种常见模式：<br>Timeline：不做内容筛选，简单的按照内容发布时间排序，常用于好友或关注。例如朋友圈</p><ul><li>优点：信息全面，不会有缺失。并且实现也相对简单</li><li>缺点：信息噪音较多，用户不一定感兴趣，内容获取效率低</li></ul><p>智能排序：利用智能算法屏蔽掉违规的、用户不感兴趣的内容。推送用户感兴趣信息来吸引用户</p><ul><li>优点：投喂用户感兴趣信息，用户粘度很高，容易沉迷</li><li>缺点：如果算法不精准，可能起到反作用<br>本例中的个人页面，是基于关注的好友来做Feed流，因此采用Timeline的模式。该模式的实现方案有三种：</li></ul><p>我们本次针对好友的操作，采用的就是Timeline的方式，只需要拿到我们关注用户的信息，然后按照时间排序即可</p><p>，因此采用Timeline的模式。该模式的实现方案有三种：</p><ul><li>拉模式</li><li>推模式</li><li>推拉结合</li></ul><p><strong>拉模式</strong>：也叫做读扩散</p><p>该模式的核心含义就是：当张三和李四和王五发了消息后，都会保存在自己的邮箱中，假设赵六要读取信息，那么他会从读取他自己的收件箱，此时系统会从他关注的人群中，把他关注人的信息全部都进行拉取，然后在进行排序</p><p>优点：比较节约空间，因为赵六在读信息时，并没有重复读取，而且读取完之后可以把他的收件箱进行清楚。</p><p>缺点：比较延迟，当用户读取数据时才去关注的人里边去读取数据，假设用户关注了大量的用户，那么此时就会拉取海量的内容，对服务器压力巨大。</p><p><img src="https://markpic.adoreorg.cn/2025/09/Redis/1653809450816.png" alt="1653809450816"></p><p><strong>推模式</strong>：也叫做写扩散。</p><p>推模式是没有写邮箱的，当张三写了一个内容，此时会主动的把张三写的内容发送到他的粉丝收件箱中去，假设此时李四再来读取，就不用再去临时拉取了</p><p>优点：时效快，不用临时拉取</p><p>缺点：内存压力大，假设一个大V写信息，很多人关注他， 就会写很多分数据到粉丝那边去</p><p><img src="https://markpic.adoreorg.cn/2025/09/Redis/1653809875208.png" alt="1653809875208"></p><p><strong>推拉结合模式</strong>：也叫做读写混合，兼具推和拉两种模式的优点。</p><p>发件人端策略：</p><ul><li><p>普通用户：采用&quot;推&quot;模式，直接将消息写入所有粉丝的收件箱（粉丝少，压力小）</p></li><li><p>大V用户：采用&quot;推拉结合&quot;模式，消息先写入自己的发件箱，然后只推送给活跃粉丝<br>收件人端策略：</p></li><li><p>活跃粉丝：无论是大V还是普通用户的消息，都直接推送到收件箱</p></li><li><p>普通粉丝：只在上线时从发件箱中拉取未读消息<br>核心优势：</p></li><li><p>平衡了系统性能和用户体验</p></li><li><p>避免了给所有粉丝推送造成的巨大压力</p></li><li><p>保证了活跃用户的实时性体验</p></li><li><p>降低了存储和计算成本</p></li></ul><p><img src="https://markpic.adoreorg.cn/2025/09/Redis/1653812346852.png" alt="1653812346852"></p><div class="tabs" id="feed-flow"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#feed-flow-1">需求背景</button></li><li class="tab"><button type="button" data-href="#feed-flow-2">技术方案对比</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="feed-flow-1"><p>Feed流（关注推送）为用户提供沉浸式内容消费体验：</p><ul><li><strong>Timeline模式</strong>：按时间排序，信息全面但噪音较多</li><li><strong>智能排序</strong>：算法推荐，用户粘度高但实现复杂</li><li><strong>本例选择</strong>：基于好友关系的Timeline模式</li></ul><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="feed-flow-2"><table><thead><tr><th>模式</th><th>原理</th><th>优点</th><th>缺点</th><th>适用场景</th></tr></thead><tbody><tr><td><strong>拉模式</strong></td><td>读取时从关注列表拉取内容</td><td>节省存储空间</td><td>延迟高，压力大</td><td>小用户量</td></tr><tr><td><strong>推模式</strong></td><td>发布时推送到粉丝收件箱</td><td>时效性好</td><td>内存消耗大</td><td>普通用户</td></tr><tr><td><strong>推拉结合</strong></td><td>普通用户推，大V推拉结合</td><td>平衡性能</td><td>实现复杂</td><td>大用户量</td></tr></tbody></table><p><strong>实现选择</strong></p><p>本例采用<strong>推模式</strong>实现：</p><ul><li>用户发布笔记时，主动推送到所有粉丝的收件箱</li><li>使用Redis SortedSet存储，score为时间戳</li><li>支持按时间排序和分页查询</li></ul><p><strong>关键点分析</strong></p><ol><li><p><strong>数据结构</strong>：<code>ZSet&lt;blogId, timestamp&gt;</code> 天然按时间排序</p></li><li><p><strong>写入策略</strong>：发布时同步写入所有粉丝收件箱</p></li><li><p><strong>读取优化</strong>：支持范围查询和滚动分页</p></li><li><p><strong>存储优化</strong>：只存储blogId，不存储完整内容</p> <div class="note note-info">   <div class="note-header">     <i class="note-icon fa-regular fa-circle-info"></i>     <span class="note-title">提示信息</span>   </div>   <div class="note-content">     <p>推模式适合好友关系场景，时效性好，实现简单。对于大V用户，可后续升级为推拉结合模式。</p>   </div> </div></li></ol><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h3 id="9-4-推送到粉丝收件箱">9.4 推送到粉丝收件箱</h3><div class="tabs" id="feed-push"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#feed-push-1">需求分析</button></li><li class="tab"><button type="button" data-href="#feed-push-2">技术方案</button></li><li class="tab"><button type="button" data-href="#feed-push-3">代码实现</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="feed-push-1"><p>实现笔记发布时的粉丝推送功能：</p><ul><li><strong>触发时机</strong>：用户发布探店笔记时</li><li><strong>推送对象</strong>：笔记作者的所有粉丝</li><li><strong>存储要求</strong>：按时间戳排序，支持分页查询</li></ul><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="feed-push-2"><p>使用Redis SortedSet作为粉丝收件箱：</p><ul><li><strong>Key格式</strong>：<code>feed:{userId}</code> 存储用户的收件箱</li><li><strong>Value格式</strong>：<code>ZSet&lt;blogId, timestamp&gt;</code></li><li><strong>推送逻辑</strong>：遍历所有粉丝，写入对应收件箱</li></ul><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="feed-push-3"><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">saveBlog</span><span class="params">(Blog blog)</span> &#123;</span><br><span class="line">    <span class="comment">// 1.获取登录用户</span></span><br><span class="line">    <span class="type">UserDTO</span> <span class="variable">user</span> <span class="operator">=</span> UserHolder.getUser();</span><br><span class="line">    blog.setUserId(user.getId());</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 2.保存探店笔记到数据库</span></span><br><span class="line">    <span class="type">boolean</span> <span class="variable">isSuccess</span> <span class="operator">=</span> save(blog);</span><br><span class="line">    <span class="keyword">if</span>(!isSuccess)&#123;</span><br><span class="line">        <span class="keyword">return</span> Result.fail(<span class="string">&quot;新增笔记失败!&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 3.查询笔记作者的所有粉丝</span></span><br><span class="line">    List&lt;Follow&gt; follows = followService.query()</span><br><span class="line">            .eq(<span class="string">&quot;follow_user_id&quot;</span>, user.getId()).list();</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 4.推送笔记id给所有粉丝</span></span><br><span class="line">    <span class="keyword">for</span> (Follow follow : follows) &#123;</span><br><span class="line">        <span class="comment">// 4.1.获取粉丝id</span></span><br><span class="line">        <span class="type">Long</span> <span class="variable">userId</span> <span class="operator">=</span> follow.getUserId();</span><br><span class="line">        <span class="comment">// 4.2.推送</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">key</span> <span class="operator">=</span> FEED_KEY + userId;</span><br><span class="line">        stringRedisTemplate.opsForZSet()</span><br><span class="line">                .add(key, blog.getId().toString(), System.currentTimeMillis());</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 5.返回笔记id</span></span><br><span class="line">    <span class="keyword">return</span> Result.ok(blog.getId());</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>关键点分析</strong></p><ol><li><p><strong>原子性</strong>：数据库保存成功后才进行推送</p></li><li><p><strong>批量操作</strong>：遍历所有粉丝，逐个写入收件箱</p></li><li><p><strong>时间戳</strong>：使用当前时间作为score，保证时间排序</p></li><li><p><strong>异常处理</strong>：任一粉丝推送失败不影响其他粉丝</p> <div class="note note-info">   <div class="note-header">     <i class="note-icon fa-regular fa-circle-info"></i>     <span class="note-title">提示信息</span>   </div>   <div class="note-content">     <p>推模式的关键是数据一致性，确保数据库保存成功后才进行Redis推送。</p>   </div> </div></li></ol><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">saveBlog</span><span class="params">(Blog blog)</span> &#123;</span><br><span class="line">    <span class="comment">// 1.获取登录用户</span></span><br><span class="line">    <span class="type">UserDTO</span> <span class="variable">user</span> <span class="operator">=</span> UserHolder.getUser();</span><br><span class="line">    blog.setUserId(user.getId());</span><br><span class="line">    <span class="comment">// 2.保存探店笔记</span></span><br><span class="line">    <span class="type">boolean</span> <span class="variable">isSuccess</span> <span class="operator">=</span> save(blog);</span><br><span class="line">    <span class="keyword">if</span>(!isSuccess)&#123;</span><br><span class="line">        <span class="keyword">return</span> Result.fail(<span class="string">&quot;新增笔记失败!&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 3.查询笔记作者的所有粉丝 select * from tb_follow where follow_user_id = ?</span></span><br><span class="line">    List&lt;Follow&gt; follows = followService.query().eq(<span class="string">&quot;follow_user_id&quot;</span>, user.getId()).list();</span><br><span class="line">    <span class="comment">// 4.推送笔记id给所有粉丝</span></span><br><span class="line">    <span class="keyword">for</span> (Follow follow : follows) &#123;</span><br><span class="line">        <span class="comment">// 4.1.获取粉丝id</span></span><br><span class="line">        <span class="type">Long</span> <span class="variable">userId</span> <span class="operator">=</span> follow.getUserId();</span><br><span class="line">        <span class="comment">// 4.2.推送</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">key</span> <span class="operator">=</span> FEED_KEY + userId;</span><br><span class="line">        stringRedisTemplate.opsForZSet().add(key, blog.getId().toString(), System.currentTimeMillis());</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 5.返回id</span></span><br><span class="line">    <span class="keyword">return</span> Result.ok(blog.getId());</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h3 id="9-5-实现分页查询收件箱">9.5 实现分页查询收件箱</h3><p>需求：在个人主页的“关注”卡片中，查询并展示推送的Blog信息：</p><div class="tabs" id="feed-query"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#feed-query-1">需求分析</button></li><li class="tab"><button type="button" data-href="#feed-query-2">技术方案</button></li><li class="tab"><button type="button" data-href="#feed-query-3">代码实现</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="feed-query-1"><p>Feed流分页查询面临特殊挑战：</p><ul><li><strong>数据动态性</strong>：新数据不断插入，传统分页会重复读取</li><li><strong>滚动分页</strong>：基于时间戳和偏移量实现无重复分页</li><li><strong>性能要求</strong>：需要高效的范围查询和排序</li></ul><p><strong>1 传统分页</strong></p><p>传统了分页在feed流是不适用的，因为我们的数据会随时发生变化</p><p>假设在t1 时刻，我们去读取第一页，此时page = 1 ，size = 5 ，那么我们拿到的就是10~6 这几条记录，假设现在t2时候又发布了一条记录，此时t3 时刻，我们来读取第二页，读取第二页传入的参数是page=2 ，size=5 ，那么此时读取到的第二页实际上是从6 开始，然后是6~2 ，那么我们就读取到了重复的数据，所以feed流的分页，不能采用原始方案来做。</p><p><img src="https://markpic.adoreorg.cn/2025/09/Redis/1653813047671.png" alt="1653813047671"></p><p><strong>2 Feed流的滚动分页</strong></p><p>我们需要记录每次操作的最后一条，然后从这个位置开始去读取数据</p><p>举个例子：我们从t1时刻开始，拿第一页数据，拿到了10~6，然后记录下当前最后一次拿取的记录，就是6，t2时刻发布了新的记录，此时这个11放到最顶上，但是不会影响我们之前记录的6，此时t3时刻来拿第二页，第二页这个时候拿数据，还是从6后一点的5去拿，就拿到了5-1的记录。我们这个地方可以采用sortedSet来做，可以进行范围查询，并且还可以记录当前获取数据时间戳最小值，就可以实现滚动分页了</p><p><img src="https://markpic.adoreorg.cn/2025/09/Redis/1653813462834.png" alt="1653813462834"></p><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="feed-query-2"><p>使用Redis SortedSet的滚动分页：</p><ul><li><strong>查询命令</strong>：<code>ZREVRANGEBYSCORE key Max Min LIMIT offset count</code></li><li><strong>分页参数</strong>：max(最大时间戳)、offset(偏移量)、count(每页数量)</li><li><strong>返回数据</strong>：blogId列表、最小时间戳、新偏移量</li></ul><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="feed-query-3"><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Controller层</span></span><br><span class="line"><span class="meta">@GetMapping(&quot;/of/follow&quot;)</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">queryBlogOfFollow</span><span class="params">(</span></span><br><span class="line"><span class="params">    <span class="meta">@RequestParam(&quot;lastId&quot;)</span> Long max, </span></span><br><span class="line"><span class="params">    <span class="meta">@RequestParam(value = &quot;offset&quot;, defaultValue = &quot;0&quot;)</span> Integer offset)</span>&#123;</span><br><span class="line">    <span class="keyword">return</span> blogService.queryBlogOfFollow(max, offset);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Service层</span></span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">queryBlogOfFollow</span><span class="params">(Long max, Integer offset)</span> &#123;</span><br><span class="line">    <span class="comment">// 1.获取当前用户</span></span><br><span class="line">    <span class="type">Long</span> <span class="variable">userId</span> <span class="operator">=</span> UserHolder.getUser().getId();</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 2.查询收件箱 ZREVRANGEBYSCORE key Max Min LIMIT offset count</span></span><br><span class="line">    <span class="type">String</span> <span class="variable">key</span> <span class="operator">=</span> FEED_KEY + userId;</span><br><span class="line">    Set&lt;ZSetOperations.TypedTuple&lt;String&gt;&gt; typedTuples = stringRedisTemplate.opsForZSet()</span><br><span class="line">        .reverseRangeByScoreWithScores(key, <span class="number">0</span>, max, offset, <span class="number">2</span>);</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 3.非空判断</span></span><br><span class="line">    <span class="keyword">if</span> (typedTuples == <span class="literal">null</span> || typedTuples.isEmpty()) &#123;</span><br><span class="line">        <span class="keyword">return</span> Result.ok();</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 4.解析数据：blogId、minTime（时间戳）、offset</span></span><br><span class="line">    List&lt;Long&gt; ids = <span class="keyword">new</span> <span class="title class_">ArrayList</span>&lt;&gt;(typedTuples.size());</span><br><span class="line">    <span class="type">long</span> <span class="variable">minTime</span> <span class="operator">=</span> <span class="number">0</span>;</span><br><span class="line">    <span class="type">int</span> <span class="variable">os</span> <span class="operator">=</span> <span class="number">1</span>;</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">for</span> (ZSetOperations.TypedTuple&lt;String&gt; tuple : typedTuples) &#123;</span><br><span class="line">        <span class="comment">// 4.1.获取id</span></span><br><span class="line">        ids.add(Long.valueOf(tuple.getValue()));</span><br><span class="line">        <span class="comment">// 4.2.获取分数(时间戳）</span></span><br><span class="line">        <span class="type">long</span> <span class="variable">time</span> <span class="operator">=</span> tuple.getScore().longValue();</span><br><span class="line">        <span class="keyword">if</span>(time == minTime)&#123;</span><br><span class="line">            os++;  <span class="comment">// 相同时间戳，偏移量+1</span></span><br><span class="line">        &#125;<span class="keyword">else</span>&#123;</span><br><span class="line">            minTime = time;  <span class="comment">// 更新时间戳</span></span><br><span class="line">            os = <span class="number">1</span>;  <span class="comment">// 重置偏移量</span></span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 5.根据id查询blog</span></span><br><span class="line">    <span class="type">String</span> <span class="variable">idStr</span> <span class="operator">=</span> StrUtil.join(<span class="string">&quot;,&quot;</span>, ids);</span><br><span class="line">    List&lt;Blog&gt; blogs = query().in(<span class="string">&quot;id&quot;</span>, ids).last(<span class="string">&quot;ORDER BY FIELD(id,&quot;</span> + idStr + <span class="string">&quot;)&quot;</span>).list();</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> (Blog blog : blogs) &#123;</span><br><span class="line">        <span class="comment">// 5.1.查询blog有关的用户</span></span><br><span class="line">        queryBlogUser(blog);</span><br><span class="line">        <span class="comment">// 5.2.查询blog是否被点赞</span></span><br><span class="line">        isBlogLiked(blog);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 6.封装并返回</span></span><br><span class="line">    <span class="type">ScrollResult</span> <span class="variable">r</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ScrollResult</span>();</span><br><span class="line">    r.setList(blogs);</span><br><span class="line">    r.setOffset(os);</span><br><span class="line">    r.setMinTime(minTime);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> Result.ok(r);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>关键点分析</strong></p><ol><li><p><strong>滚动分页</strong>：基于时间戳和偏移量，避免重复数据</p></li><li><p><strong>ZSet查询</strong>：<code>reverseRangeByScoreWithScores</code> 支持范围查询</p></li><li><p><strong>偏移量计算</strong>：处理相同时间戳的多条数据</p></li><li><p><strong>数据完整性</strong>：查询blog详情和点赞状态</p> <div class="note note-info">   <div class="note-header">     <i class="note-icon fa-regular fa-circle-info"></i>     <span class="note-title">提示信息</span>   </div>   <div class="note-content">     <p>滚动分页的核心是记录上次查询的最小时间戳和偏移量，下次查询从该位置继续。</p>   </div> </div></li></ol><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h2 id="10-附近商户">10. 附近商户</h2><h3 id="10-1-GEO数据结构基本用法">10.1 GEO数据结构基本用法</h3><div class="tabs" id="geo-basic"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#geo-basic-1">功能概述</button></li><li class="tab"><button type="button" data-href="#geo-basic-2">应用场景</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="geo-basic-1"><p><strong>功能概述</strong>：存储和查询地理坐标信息</p><p>Redis GEO（地理坐标）支持存储和查询地理坐标信息：</p><ul><li><strong>坐标存储</strong>：经度(longitude)、纬度(latitude)、成员(member)</li><li><strong>距离计算</strong>：两点间的球面距离 单位 m-米，km-千米，mi-英⾥，ft-英尺</li><li><strong>范围查询</strong>：圆形或矩形范围内的成员</li><li><strong>排序返回</strong>：按距离排序查询结果</li></ul><p><strong>常用命令</strong></p><table><thead><tr><th>命令</th><th>说明</th><th>示例</th></tr></thead><tbody><tr><td><code>GEOADD</code></td><td>添加地理坐标</td><td><code>GEOADD cities 116.405 39.905 beijing</code></td></tr><tr><td><code>GEODIST</code></td><td>计算两点距离</td><td><code>GEODIST cities beijing shanghai km</code></td></tr><tr><td><code>GEOPOS</code></td><td>获取成员坐标</td><td><code>GEOPOS cities beijing</code></td></tr><tr><td><code>GEOSEARCH</code></td><td>范围搜索排序</td><td><code>GEOSEARCH cities FROMLONLAT 116 39 BYRADIUS 10 km</code></td></tr></tbody></table><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="geo-basic-2"><p><strong>应用场景</strong></p><ul><li><strong>附近商户搜索</strong>：根据用户当前位置，查询距离最近的商户</li><li><strong>地理位置服务</strong>：提供基于位置的服务，如地图导航、位置提醒</li><li><strong>距离计算和排序</strong>：计算两点之间的距离，支持按距离排序查询结果</li><li><strong>基于位置的内容推荐</strong>：根据用户位置推荐相关内容，如本地美食、景点等</li></ul><p><strong>关键点分析</strong></p><ol><li><p><strong>坐标精度</strong>：支持小数点后6位，约10cm精度</p></li><li><p><strong>距离单位</strong>：支持m、km、mi、ft等单位</p></li><li><p><strong>范围查询</strong>：支持圆形和矩形两种查询方式</p></li><li><p><strong>性能优化</strong>：使用geohash编码，查询效率高</p> <div class="note note-info">   <div class="note-header">     <i class="note-icon fa-regular fa-circle-info"></i>     <span class="note-title">提示信息</span>   </div>   <div class="note-content">     <p>Redis 6.2+ 推荐使用 <code>GEOSEARCH</code> 命令，功能更强大，支持更复杂的查询条件。</p>   </div> </div></li></ol><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h3 id="10-2-导入店铺数据到GEO">10.2 导入店铺数据到GEO</h3><div class="tabs" id="geo-import"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#geo-import-1">需求分析</button></li><li class="tab"><button type="button" data-href="#geo-import-2">代码实现</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="geo-import-1"><p><strong>需求分析</strong></p><p>将数据库中的店铺信息导入Redis GEO，支持按类型分类存储：</p><ul><li><strong>数据分组</strong>：按店铺类型分组，同类店铺存储在同一key下</li><li><strong>坐标存储</strong>：使用店铺ID作为member，经纬度作为坐标</li><li><strong>批量导入</strong>：提高导入效率，减少网络开销</li></ul><p><strong>技术方案</strong></p><p>使用Redis GEOADD命令批量导入：</p><ul><li><strong>Key格式</strong>：<code>shop:geo:{typeId}</code> 按店铺类型分组</li><li><strong>Value格式</strong>：<code>GeoLocation&lt;shopId, Point(x,y)&gt;</code></li><li><strong>批量操作</strong>：<code>opsForGeo().add(key, locations)</code> 一次性导入</li></ul><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="geo-import-2"><p><strong>代码实现</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Test</span></span><br><span class="line"><span class="keyword">void</span> <span class="title function_">loadShopData</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="comment">// 1.查询店铺信息</span></span><br><span class="line">    List&lt;Shop&gt; list = shopService.list();</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 2.把店铺分组，按照typeId分组</span></span><br><span class="line">    Map&lt;Long, List&lt;Shop&gt;&gt; map = list.stream()</span><br><span class="line">        .collect(Collectors.groupingBy(Shop::getTypeId));</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 3.分批完成写入Redis</span></span><br><span class="line">    <span class="keyword">for</span> (Map.Entry&lt;Long, List&lt;Shop&gt;&gt; entry : map.entrySet()) &#123;</span><br><span class="line">        <span class="comment">// 3.1.获取类型id</span></span><br><span class="line">        <span class="type">Long</span> <span class="variable">typeId</span> <span class="operator">=</span> entry.getKey();</span><br><span class="line">        <span class="type">String</span> <span class="variable">key</span> <span class="operator">=</span> SHOP_GEO_KEY + typeId;</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 3.2.获取同类型的店铺的集合</span></span><br><span class="line">        List&lt;Shop&gt; value = entry.getValue();</span><br><span class="line">        List&lt;RedisGeoCommands.GeoLocation&lt;String&gt;&gt; locations = </span><br><span class="line">            <span class="keyword">new</span> <span class="title class_">ArrayList</span>&lt;&gt;(value.size());</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 3.3.构建GeoLocation对象</span></span><br><span class="line">        <span class="keyword">for</span> (Shop shop : value) &#123;</span><br><span class="line">            locations.add(<span class="keyword">new</span> <span class="title class_">RedisGeoCommands</span>.GeoLocation&lt;&gt;(</span><br><span class="line">                shop.getId().toString(),</span><br><span class="line">                <span class="keyword">new</span> <span class="title class_">Point</span>(shop.getX(), shop.getY())</span><br><span class="line">            ));</span><br><span class="line">        &#125;</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 3.4.批量写入redis GEOADD key 经度 纬度 member</span></span><br><span class="line">        stringRedisTemplate.opsForGeo().add(key, locations);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>关键点分析</strong></p><ol><li><strong>数据分组</strong>：按typeId分组，支持按类型筛选</li><li><strong>批量导入</strong>：减少网络请求，提高导入效率</li><li><strong>内存优化</strong>：只存储shopId，不存储完整信息</li><li><strong>坐标格式</strong>：使用RedisGeoCommands.GeoLocation封装</li></ol><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h3 id="10-3-实现附近商户功能">10.3 实现附近商户功能</h3><div class="tabs" id="geo-search"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#geo-search-1">需求分析</button></li><li class="tab"><button type="button" data-href="#geo-search-2">代码实现</button></li><li class="tab"><button type="button" data-href="#geo-search-3">发送请求</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="geo-search-1"><p><strong>需求分析</strong></p><p>实现基于地理位置的商户查询功能：</p><ul><li><strong>坐标查询</strong>：根据用户当前位置查询附近商户</li><li><strong>距离排序</strong>：按距离从近到远排序</li><li><strong>分页支持</strong>：支持分页查询，避免一次性返回大量数据</li><li><strong>类型筛选</strong>：可按商户类型筛选结果</li></ul><p><strong>技术方案</strong></p><p>使用Redis GEOSEARCH命令实现：</p><ul><li><strong>查询命令</strong>：<code>GEOSEARCH key BYLONLAT x y BYRADIUS radius WITHDISTANCE</code></li><li><strong>分页处理</strong>：先查询足够数据，再进行内存分页</li><li><strong>距离计算</strong>：自动计算并返回每个商户的距离</li></ul><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="geo-search-2"><p><strong>引入依赖</strong><br><strong>SpringDataRedis的2.3.9版本并不支持Redis 6.2提供的GEOSEARCH命令，因此我们需要提示其版本，修改自己的pom.xml文件</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line">&lt;dependency&gt;</span><br><span class="line">    &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;</span><br><span class="line">    &lt;artifactId&gt;spring-boot-starter-data-redis&lt;/artifactId&gt;</span><br><span class="line">    &lt;exclusions&gt;</span><br><span class="line">        &lt;exclusion&gt;</span><br><span class="line">            &lt;artifactId&gt;spring-data-redis&lt;/artifactId&gt;</span><br><span class="line">            &lt;groupId&gt;org.springframework.data&lt;/groupId&gt;</span><br><span class="line">        &lt;/exclusion&gt;</span><br><span class="line">        &lt;exclusion&gt;</span><br><span class="line">            &lt;artifactId&gt;lettuce-core&lt;/artifactId&gt;</span><br><span class="line">            &lt;groupId&gt;io.lettuce&lt;/groupId&gt;</span><br><span class="line">        &lt;/exclusion&gt;</span><br><span class="line">    &lt;/exclusions&gt;</span><br><span class="line">&lt;/dependency&gt;</span><br><span class="line">&lt;dependency&gt;</span><br><span class="line">    &lt;groupId&gt;org.springframework.data&lt;/groupId&gt;</span><br><span class="line">    &lt;artifactId&gt;spring-data-redis&lt;/artifactId&gt;</span><br><span class="line">    &lt;version&gt;<span class="number">2.6</span><span class="number">.2</span>&lt;/version&gt;</span><br><span class="line">&lt;/dependency&gt;</span><br><span class="line">&lt;dependency&gt;</span><br><span class="line">    &lt;groupId&gt;io.lettuce&lt;/groupId&gt;</span><br><span class="line">    &lt;artifactId&gt;lettuce-core&lt;/artifactId&gt;</span><br><span class="line">    &lt;version&gt;<span class="number">6.1</span><span class="number">.6</span>.RELEASE&lt;/version&gt;</span><br><span class="line">&lt;/dependency&gt;</span><br></pre></td></tr></table></figure><p><strong>代码实现</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Controller层</span></span><br><span class="line"><span class="meta">@GetMapping(&quot;/of/type&quot;)</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">queryShopByType</span><span class="params">(</span></span><br><span class="line"><span class="params">        <span class="meta">@RequestParam(&quot;typeId&quot;)</span> Integer typeId,</span></span><br><span class="line"><span class="params">        <span class="meta">@RequestParam(value = &quot;current&quot;, defaultValue = &quot;1&quot;)</span> Integer current,</span></span><br><span class="line"><span class="params">        <span class="meta">@RequestParam(value = &quot;x&quot;, required = false)</span> Double x,</span></span><br><span class="line"><span class="params">        <span class="meta">@RequestParam(value = &quot;y&quot;, required = false)</span> Double y</span></span><br><span class="line"><span class="params">)</span> &#123;</span><br><span class="line">   <span class="keyword">return</span> shopService.queryShopByType(typeId, current, x, y);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Service层</span></span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">queryShopByType</span><span class="params">(Integer typeId, Integer current, Double x, Double y)</span> &#123;</span><br><span class="line">    <span class="comment">// 1.判断是否需要根据坐标查询</span></span><br><span class="line">    <span class="keyword">if</span> (x == <span class="literal">null</span> || y == <span class="literal">null</span>) &#123;</span><br><span class="line">        <span class="comment">// 不需要坐标查询，按数据库查询</span></span><br><span class="line">        Page&lt;Shop&gt; page = query()</span><br><span class="line">                .eq(<span class="string">&quot;type_id&quot;</span>, typeId)</span><br><span class="line">                .page(<span class="keyword">new</span> <span class="title class_">Page</span>&lt;&gt;(current, SystemConstants.DEFAULT_PAGE_SIZE));</span><br><span class="line">        <span class="keyword">return</span> Result.ok(page.getRecords());</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 2.计算分页参数</span></span><br><span class="line">    <span class="type">int</span> <span class="variable">from</span> <span class="operator">=</span> (current - <span class="number">1</span>) * SystemConstants.DEFAULT_PAGE_SIZE;</span><br><span class="line">    <span class="type">int</span> <span class="variable">end</span> <span class="operator">=</span> current * SystemConstants.DEFAULT_PAGE_SIZE;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 3.查询redis、按照距离排序、分页</span></span><br><span class="line">    <span class="type">String</span> <span class="variable">key</span> <span class="operator">=</span> SHOP_GEO_KEY + typeId;</span><br><span class="line">    GeoResults&lt;RedisGeoCommands.GeoLocation&lt;String&gt;&gt; results = stringRedisTemplate.opsForGeo()</span><br><span class="line">            .search(</span><br><span class="line">                    key,</span><br><span class="line">                    GeoReference.fromCoordinate(x, y),</span><br><span class="line">                    <span class="keyword">new</span> <span class="title class_">Distance</span>(<span class="number">5000</span>), <span class="comment">// 5km范围内</span></span><br><span class="line">                    RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs()</span><br><span class="line">                            .includeDistance()</span><br><span class="line">                            .limit(end)</span><br><span class="line">            );</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 4.解析出id和距离</span></span><br><span class="line">    <span class="keyword">if</span> (results == <span class="literal">null</span>) &#123;</span><br><span class="line">        <span class="keyword">return</span> Result.ok(Collections.emptyList());</span><br><span class="line">    &#125;</span><br><span class="line">    List&lt;GeoResult&lt;RedisGeoCommands.GeoLocation&lt;String&gt;&gt;&gt; list = results.getContent();</span><br><span class="line">    <span class="keyword">if</span> (list.size() &lt;= from) &#123;</span><br><span class="line">        <span class="comment">// 没有下一页了，结束</span></span><br><span class="line">        <span class="keyword">return</span> Result.ok(Collections.emptyList());</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 4.1.截取 from ~ end的部分</span></span><br><span class="line">    List&lt;Long&gt; ids = <span class="keyword">new</span> <span class="title class_">ArrayList</span>&lt;&gt;(list.size());</span><br><span class="line">    Map&lt;String, Distance&gt; distanceMap = <span class="keyword">new</span> <span class="title class_">HashMap</span>&lt;&gt;(list.size());</span><br><span class="line">    list.stream().skip(from).forEach(result -&gt; &#123;</span><br><span class="line">        <span class="comment">// 4.2.获取店铺id</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">shopIdStr</span> <span class="operator">=</span> result.getContent().getName();</span><br><span class="line">        ids.add(Long.valueOf(shopIdStr));</span><br><span class="line">        <span class="comment">// 4.3.获取距离</span></span><br><span class="line">        <span class="type">Distance</span> <span class="variable">distance</span> <span class="operator">=</span> result.getDistance();</span><br><span class="line">        distanceMap.put(shopIdStr, distance);</span><br><span class="line">    &#125;);</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 5.根据id查询Shop详情</span></span><br><span class="line">    <span class="type">String</span> <span class="variable">idStr</span> <span class="operator">=</span> StrUtil.join(<span class="string">&quot;,&quot;</span>, ids);</span><br><span class="line">    List&lt;Shop&gt; shops = query().in(<span class="string">&quot;id&quot;</span>, ids).last(<span class="string">&quot;ORDER BY FIELD(id,&quot;</span> + idStr + <span class="string">&quot;)&quot;</span>).list();</span><br><span class="line">    <span class="keyword">for</span> (Shop shop : shops) &#123;</span><br><span class="line">        shop.setDistance(distanceMap.get(shop.getId().toString()).getValue());</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 6.返回</span></span><br><span class="line">    <span class="keyword">return</span> Result.ok(shops);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>关键点分析</strong></p><ol><li><strong>坐标判断</strong>：无坐标时回退到数据库查询</li><li><strong>分页计算</strong>：内存分页，避免数据库分页的排序问题</li><li><strong>范围查询</strong>：5km范围内搜索，避免数据量过大</li><li><strong>距离设置</strong>：自动将距离信息设置到店铺对象中</li></ol><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="geo-search-3"><p><strong>请求网址: <a href="http://localhost:8080/api/shop/of/type?typeId=1&amp;current=1&amp;x=120.149993&amp;y=30.334229">http://localhost:8080/api/shop/of/type?typeId=1&amp;current=1&amp;x=120.149993&amp;y=30.334229</a></strong><br><strong>请求方法: GET</strong></p><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h2 id="11-用户签到">11. 用户签到</h2><h3 id="11-1-BitMap功能演示">11.1 BitMap功能演示</h3><div class="tabs" id="bitmap-basic"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#bitmap-basic-1">功能概述</button></li><li class="tab"><button type="button" data-href="#bitmap-basic-2">应用场景</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="bitmap-basic-1"><p><strong>功能概述</strong></p><p>BitMap是Redis中基于String类型实现的位图数据结构：</p><ul><li><strong>存储原理</strong>：每个bit位代表一个状态，0表示未签到，1表示已签到</li><li><strong>空间优势</strong>：相比MySQL存储，内存占用减少99%以上</li><li><strong>性能优势</strong>：位操作时间复杂度为O(1)，查询效率极高</li></ul><p><strong>常用命令</strong></p><table><thead><tr><th>命令</th><th>说明</th><th>示例</th></tr></thead><tbody><tr><td>SETBIT</td><td>设置指定位置的bit值</td><td><code>SETBIT sign:1:202401 15 1</code></td></tr><tr><td>GETBIT</td><td>获取指定位置的bit值</td><td><code>GETBIT sign:1:202401 15</code></td></tr><tr><td>BITCOUNT</td><td>统计1的个数</td><td><code>BITCOUNT sign:1:202401</code></td></tr><tr><td>BITFIELD</td><td>批量操作bit位</td><td><code>BITFIELD sign:1:202401 GET u31 0</code></td></tr></tbody></table><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="bitmap-basic-2"><p><strong>应用场景</strong></p><ul><li><strong>用户签到</strong>：按月存储用户签到状态</li><li><strong>活跃用户统计</strong>：统计某时间段活跃用户</li><li><strong>特征标记</strong>：标记用户是否具有某种特征</li><li><strong>布隆过滤器</strong>：快速判断元素是否存在</li></ul><p><strong>关键点分析</strong></p><ol><li><p><strong>存储上限</strong>：最大支持512MB，约43亿个bit位</p></li><li><p><strong>时间效率</strong>：位操作都是O(1)时间复杂度</p></li><li><p><strong>空间效率</strong>：1亿用户10次签到仅需12MB内存</p></li><li><p><strong>数据格式</strong>：按年+月分组存储，便于统计</p> <div class="note note-info">   <div class="note-header">     <i class="note-icon fa-regular fa-circle-info"></i>     <span class="note-title">simple</span>   </div>   <div class="note-content">     <p>BitMap适合存储大量布尔值状态，内存占用极小，查询效率极高。</p>   </div> </div></li></ol><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h3 id="11-2-实现签到功能">11.2 实现签到功能</h3><div class="tabs" id="sign-save"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#sign-save-1">需求分析</button></li><li class="tab"><button type="button" data-href="#sign-save-2">代码实现</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="sign-save-1"><p><strong>需求分析</strong></p><p>实现用户签到功能，将当天签到信息保存到Redis中：</p><ul><li><strong>签到记录</strong>：记录用户每天的签到状态</li><li><strong>状态存储</strong>：使用BitMap存储，0表示未签到，1表示已签到</li><li><strong>时间维度</strong>：按月维度存储，便于统计和查询</li><li><strong>幂等性</strong>：同一天多次签到只记录一次</li></ul><p><strong>技术方案</strong></p><p>使用Redis BitMap实现签到存储：</p><ul><li><strong>Key格式</strong>：<code>sign:userId:yyyyMM</code></li><li><strong>Bit位置</strong>：用月份中的第几天作为offset（0-30）</li><li><strong>命令选择</strong>：使用SETBIT命令设置签到状态</li><li><strong>时间获取</strong>：通过后台代码获取当前日期</li></ul><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="sign-save-2"><p><strong>代码实现</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Controller层</span></span><br><span class="line"><span class="meta">@PostMapping(&quot;/sign&quot;)</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">sign</span><span class="params">()</span>&#123;</span><br><span class="line">   <span class="keyword">return</span> userService.sign();</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Service层</span></span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">sign</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="comment">// 1.获取当前登录用户</span></span><br><span class="line">    <span class="type">Long</span> <span class="variable">userId</span> <span class="operator">=</span> UserHolder.getUser().getId();</span><br><span class="line">    <span class="comment">// 2.获取当前日期</span></span><br><span class="line">    <span class="type">LocalDateTime</span> <span class="variable">now</span> <span class="operator">=</span> LocalDateTime.now();</span><br><span class="line">    <span class="comment">// 3.拼接key：sign:userId:yyyyMM</span></span><br><span class="line">    <span class="type">String</span> <span class="variable">keySuffix</span> <span class="operator">=</span> now.format(DateTimeFormatter.ofPattern(<span class="string">&quot;:yyyyMM&quot;</span>));</span><br><span class="line">    <span class="type">String</span> <span class="variable">key</span> <span class="operator">=</span> USER_SIGN_KEY + userId + keySuffix;</span><br><span class="line">    <span class="comment">// 4.获取今天是本月的第几天（1-31）</span></span><br><span class="line">    <span class="type">int</span> <span class="variable">dayOfMonth</span> <span class="operator">=</span> now.getDayOfMonth();</span><br><span class="line">    <span class="comment">// 5.写入Redis：SETBIT key offset 1</span></span><br><span class="line">    stringRedisTemplate.opsForValue().setBit(key, dayOfMonth - <span class="number">1</span>, <span class="literal">true</span>);</span><br><span class="line">    <span class="keyword">return</span> Result.ok();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>关键点分析</strong></p><ol><li><p><strong>Key设计</strong>：按用户和月份维度存储，便于查询和统计</p></li><li><p><strong>Offset计算</strong>：使用月份中的第几天减1作为bit位偏移</p></li><li><p><strong>幂等性保证</strong>：SETBIT命令天然幂等，重复签到无影响</p></li><li><p><strong>时间处理</strong>：通过LocalDateTime获取准确的日期信息</p> <div class="note note-success">   <div class="note-header">     <i class="note-icon fa-regular fa-circle-check"></i>     <span class="note-title">simple</span>   </div>   <div class="note-content">     <p>BitMap的SETBIT命令天然幂等，同一天多次签到不会产生重复记录。</p>   </div> </div></li></ol><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h3 id="11-3-签到统计">11.3 签到统计</h3><div class="tabs" id="sign-count"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#sign-count-1">需求分析</button></li><li class="tab"><button type="button" data-href="#sign-count-2">代码实现</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="sign-count-1"><p><strong>需求分析</strong></p><p>统计用户本月的连续签到天数：</p><ul><li><strong>连续签到定义</strong>：从最后一次签到开始向前统计，直到遇到第一次未签到为止</li><li><strong>统计范围</strong>：本月第一天到当前日期</li><li><strong>返回结果</strong>：连续签到天数</li><li><strong>算法思路</strong>：从后向前遍历bit位，遇到0停止</li></ul><p><strong>技术方案</strong></p><p>使用BITFIELD命令获取本月签到数据：</p><ul><li><strong>命令格式</strong>：<code>BITFIELD key GET u[dayOfMonth] 0</code></li><li><strong>数据处理</strong>：获取本月所有签到记录，返回十进制数字</li><li><strong>位运算</strong>：通过与1进行与运算，逐个检查bit位</li><li><strong>遍历逻辑</strong>：从后向前遍历，遇到0停止计数</li></ul><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="sign-count-2"><p><strong>代码实现</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Controller层</span></span><br><span class="line"><span class="meta">@GetMapping(&quot;/sign/count&quot;)</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">signCount</span><span class="params">()</span>&#123;</span><br><span class="line">    <span class="keyword">return</span> userService.signCount();</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Service层</span></span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">signCount</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="comment">// 1.获取当前登录用户</span></span><br><span class="line">    <span class="type">Long</span> <span class="variable">userId</span> <span class="operator">=</span> UserHolder.getUser().getId();</span><br><span class="line">    <span class="comment">// 2.获取当前日期</span></span><br><span class="line">    <span class="type">LocalDateTime</span> <span class="variable">now</span> <span class="operator">=</span> LocalDateTime.now();</span><br><span class="line">    <span class="comment">// 3.拼接key：sign:userId:yyyyMM</span></span><br><span class="line">    <span class="type">String</span> <span class="variable">keySuffix</span> <span class="operator">=</span> now.format(DateTimeFormatter.ofPattern(<span class="string">&quot;:yyyyMM&quot;</span>));</span><br><span class="line">    <span class="type">String</span> <span class="variable">key</span> <span class="operator">=</span> USER_SIGN_KEY + userId + keySuffix;</span><br><span class="line">    <span class="comment">// 4.获取今天是本月的第几天</span></span><br><span class="line">    <span class="type">int</span> <span class="variable">dayOfMonth</span> <span class="operator">=</span> now.getDayOfMonth();</span><br><span class="line">    <span class="comment">// 5.获取本月截止今天为止的所有签到记录</span></span><br><span class="line">    <span class="comment">// BITFIELD sign:1:202401 GET u31 0</span></span><br><span class="line">    List&lt;Long&gt; result = stringRedisTemplate.opsForValue().bitField(</span><br><span class="line">            key,</span><br><span class="line">            BitFieldSubCommands.create()</span><br><span class="line">                    .get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth)).valueAt(<span class="number">0</span>)</span><br><span class="line">    );</span><br><span class="line">    <span class="keyword">if</span> (result == <span class="literal">null</span> || result.isEmpty()) &#123;</span><br><span class="line">        <span class="comment">// 没有任何签到结果</span></span><br><span class="line">        <span class="keyword">return</span> Result.ok(<span class="number">0</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="type">Long</span> <span class="variable">num</span> <span class="operator">=</span> result.get(<span class="number">0</span>);</span><br><span class="line">    <span class="keyword">if</span> (num == <span class="literal">null</span> || num == <span class="number">0</span>) &#123;</span><br><span class="line">        <span class="keyword">return</span> Result.ok(<span class="number">0</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 6.循环遍历统计连续签到天数</span></span><br><span class="line">    <span class="type">int</span> <span class="variable">count</span> <span class="operator">=</span> <span class="number">0</span>;</span><br><span class="line">    <span class="keyword">while</span> (<span class="literal">true</span>) &#123;</span><br><span class="line">        <span class="comment">// 6.1.让这个数字与1做与运算，得到数字的最后一个bit位</span></span><br><span class="line">        <span class="keyword">if</span> ((num &amp; <span class="number">1</span>) == <span class="number">0</span>) &#123;</span><br><span class="line">            <span class="comment">// 如果为0，说明未签到，结束统计</span></span><br><span class="line">            <span class="keyword">break</span>;</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            <span class="comment">// 如果不为0，说明已签到，计数器+1</span></span><br><span class="line">            count++;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">// 把数字右移一位，抛弃最后一个bit位，继续下一个bit位</span></span><br><span class="line">        num &gt;&gt;&gt;= <span class="number">1</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> Result.ok(count);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>关键点分析</strong></p><ol><li><p><strong>连续签到定义</strong>：从最后一次签到向前统计，遇到0停止</p></li><li><p><strong>BITFIELD使用</strong>：一次性获取本月所有签到数据</p></li><li><p><strong>位运算技巧</strong>：通过与1进行与运算判断最低位是否为1</p></li><li><p><strong>遍历方向</strong>：从后向前遍历，符合连续签到定义</p> <div class="note note-info">   <div class="note-header">     <i class="note-icon fa-regular fa-circle-info"></i>     <span class="note-title">simple</span>   </div>   <div class="note-content">     <p>使用无符号右移（&gt;&gt;&gt;&quot;)确保高位补0，避免负数影响统计结果。</p>   </div> </div></li></ol><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h3 id="11-4-BitMap解决缓存穿透">11.4 BitMap解决缓存穿透</h3><div class="tabs" id="bitmap-bloom"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#bitmap-bloom-1">需求分析</button></li><li class="tab"><button type="button" data-href="#bitmap-bloom-2">代码实现</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="bitmap-bloom-1"><p><strong>需求分析</strong></p><p>使用BitMap解决缓存穿透问题：</p><ul><li><strong>缓存穿透</strong>：查询不存在的数据，绕过缓存直达数据库</li><li><strong>传统方案</strong>：使用list存储所有有效id，内存占用大</li><li><strong>优化方案</strong>：使用BitMap作为布隆过滤器，快速判断id是否存在</li><li><strong>误差控制</strong>：通过哈希算法降低冲突概率</li></ul><p><strong>技术方案</strong></p><p>基于BitMap实现布隆过滤器：</p><ul><li><strong>哈希算法</strong>：id % bitmap_size，确定bit位置</li><li><strong>存储方式</strong>：将数据库中所有有效id对应的bit位置为1</li><li><strong>查询逻辑</strong>：用户查询时，用相同算法计算bit位，为0则一定不存在</li><li><strong>误差处理</strong>：哈希冲突可能导致误判，但概率可控</li></ul><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="bitmap-bloom-2"><p><strong>代码实现</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 初始化布隆过滤器</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">initBloomFilter</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="comment">// 1.获取数据库中所有有效id</span></span><br><span class="line">    List&lt;Long&gt; validIds = getAllValidIdsFromDB();</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 2.计算bitmap大小（根据数据量和期望误差率）</span></span><br><span class="line">    <span class="type">int</span> <span class="variable">bitmapSize</span> <span class="operator">=</span> calculateOptimalSize(validIds.size(), <span class="number">0.01</span>);</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 3.将每个id映射到bit位并设置</span></span><br><span class="line">    <span class="keyword">for</span> (Long id : validIds) &#123;</span><br><span class="line">        <span class="type">int</span> <span class="variable">bitIndex</span> <span class="operator">=</span> (<span class="type">int</span>) (id % bitmapSize);</span><br><span class="line">        stringRedisTemplate.opsForValue().setBit(<span class="string">&quot;bloom:filter&quot;</span>, bitIndex, <span class="literal">true</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 查询时判断</span></span><br><span class="line"><span class="keyword">public</span> <span class="type">boolean</span> <span class="title function_">mightExist</span><span class="params">(Long id)</span> &#123;</span><br><span class="line">    <span class="type">int</span> <span class="variable">bitmapSize</span> <span class="operator">=</span> getBitmapSize(); <span class="comment">// 获取之前计算的size</span></span><br><span class="line">    <span class="type">int</span> <span class="variable">bitIndex</span> <span class="operator">=</span> (<span class="type">int</span>) (id % bitmapSize);</span><br><span class="line">    <span class="keyword">return</span> stringRedisTemplate.opsForValue().getBit(<span class="string">&quot;bloom:filter&quot;</span>, bitIndex);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>关键点分析</strong></p><ol><li><strong>内存优化</strong>：相比存储完整id列表，内存占用减少99%以上</li><li><strong>查询效率</strong>：位操作时间复杂度O(1)，查询极快</li><li><strong>误差控制</strong>：通过合理设置bitmap大小，可将误差率控制在1%以内</li><li><strong>数据更新</strong>：数据库新增/删除数据时需要同步更新bitmap</li></ol><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h2 id="12-UV统计">12. UV统计</h2><h3 id="12-1-HyperLogLog原理">12.1 HyperLogLog原理</h3><div class="tabs" id="hyperloglog-basic"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#hyperloglog-basic-1">概念定义</button></li><li class="tab"><button type="button" data-href="#hyperloglog-basic-2">应用场景</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="hyperloglog-basic-1"><p><strong>概念定义</strong></p><ul><li><strong>UV（Unique Visitor）</strong>：独立访客量，同一用户多次访问只计1次</li><li><strong>PV（Page View）</strong>：页面访问量，每次访问都计数</li><li><strong>统计挑战</strong>：UV需要去重，传统Set存储方式内存消耗巨大</li></ul><p><strong>HyperLogLog原理</strong></p><div class='liushen-tag-link'><a class="tag-Link" target="_blank" href=" https://juejin.cn/post/6844903785744056333#heading-0">    <div class="tag-link-tips">🪧引用站外地址，不保证站点的可用性和安全性</div>    <div class="tag-link-bottom">        <div class="tag-link-left" style="background-image: url(https://lsky.adoreorg.cn/i/2026/06/08/6a264792045e8.webp);"></div>        <div class="tag-link-right">            <div class="tag-link-title">HyperLogLog原理</div>            <div class="tag-link-sitename"> https://source.adoreorg.cn/webp/icon/66a4632bbf06e.webp</div>        </div>        <i class="fa-solid fa-angle-right"></i>    </div>    </a></div><p>HyperLogLog是基于概率算法的基数统计方案：</p><ul><li><strong>内存占用</strong>：单个HLL结构小于16KB，与数据量无关</li><li><strong>误差范围</strong>：标准误差小于0.81%，对UV统计可接受</li><li><strong>底层实现</strong>：基于String结构，使用位运算和哈希算法</li><li><strong>适用场景</strong>：大数据量去重统计，如UV、DAU等</li></ul><p><strong>Redis命令</strong></p><table><thead><tr><th>命令</th><th>说明</th><th>示例</th></tr></thead><tbody><tr><td>PFADD</td><td>添加元素</td><td><code>PFADD uv:202401 user1 user2</code></td></tr><tr><td>PFCOUNT</td><td>统计基数</td><td><code>PFCOUNT uv:202401</code></td></tr><tr><td>PFMERGE</td><td>合并多个HLL</td><td><code>PFMERGE uv:total uv:202401 uv:202402</code></td></tr></tbody></table><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="hyperloglog-basic-2"><p><strong>应用场景</strong></p><ul><li><strong>网站UV统计</strong>：统计每日/月独立访客</li><li><strong>APP日活统计</strong>：统计每日活跃用户</li><li><strong>广告点击统计</strong>：统计独立点击用户数</li><li><strong>用户行为分析</strong>：统计参与特定活动的用户数</li></ul><p><strong>关键点分析</strong></p><ol><li><strong>内存优势</strong>：相比Set存储，内存节省99%以上</li><li><strong>误差可控</strong>：0.81%误差对大多数业务场景可接受</li><li><strong>合并支持</strong>：支持多时间段数据合并统计</li><li><strong>性能高效</strong>：添加和查询操作都是O(1)复杂度</li></ol><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h3 id="12-2-百万级数据统计测试">12.2 百万级数据统计测试</h3><div class="tabs" id="hyperloglog-test"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#hyperloglog-test-1">测试目标</button></li><li class="tab"><button type="button" data-href="#hyperloglog-test-2">测试结果</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="hyperloglog-test-1"><p><strong>测试目标</strong></p><p>验证HyperLogLog在百万级数据下的性能表现：</p><ul><li><strong>内存占用</strong>：统计100万条数据的内存消耗</li><li><strong>统计精度</strong>：误差是否在0.81%范围内</li><li><strong>性能表现</strong>：添加和查询操作的耗时</li><li><strong>对比测试</strong>：与Set结构进行内存和性能对比</li></ul><p><strong>测试方案</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Test</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">testHyperLogLog</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="comment">// 1.初始化HyperLogLog</span></span><br><span class="line">    <span class="type">String</span> <span class="variable">key</span> <span class="operator">=</span> <span class="string">&quot;test:uv:&quot;</span> + System.currentTimeMillis();</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 2.添加100万条数据</span></span><br><span class="line">    <span class="type">int</span> <span class="variable">total</span> <span class="operator">=</span> <span class="number">1_000_000</span>;</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> <span class="variable">i</span> <span class="operator">=</span> <span class="number">0</span>; i &lt; total; i++) &#123;</span><br><span class="line">        stringRedisTemplate.opsForHyperLogLog().add(key, <span class="string">&quot;user&quot;</span> + i);</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 3.统计基数</span></span><br><span class="line">    <span class="type">Long</span> <span class="variable">count</span> <span class="operator">=</span> stringRedisTemplate.opsForHyperLogLog().size(key);</span><br><span class="line">    System.out.println(<span class="string">&quot;实际数据量：&quot;</span> + total);</span><br><span class="line">    System.out.println(<span class="string">&quot;统计结果：&quot;</span> + count);</span><br><span class="line">    System.out.println(<span class="string">&quot;误差率：&quot;</span> + Math.abs(count - total) * <span class="number">100.0</span> / total + <span class="string">&quot;%&quot;</span>);</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 4.内存占用估算</span></span><br><span class="line">    <span class="type">Long</span> <span class="variable">memory</span> <span class="operator">=</span> getMemoryUsage(key);</span><br><span class="line">    System.out.println(<span class="string">&quot;内存占用：&quot;</span> + memory + <span class="string">&quot; bytes&quot;</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="hyperloglog-test-2"><p><strong>测试结果</strong></p><p>经过测试发现：</p><ul><li><strong>内存占用</strong>：约12KB，远低于Set结构的几十MB</li><li><strong>统计精度</strong>：误差在0.5%左右，优于官方标称的0.81%</li><li><strong>性能表现</strong>：100万条数据添加耗时约2秒</li><li><strong>空间效率</strong>：相比Set结构，内存节省99%以上</li></ul><p><strong>性能对比</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">| 数据结构 | 内存占用 | 统计精度 | 适用场景 |</span><br><span class="line">|---------|----------|----------|----------|</span><br><span class="line">| Set | 几十MB | 100% | 小数据量精确统计 |</span><br><span class="line">| HyperLogLog | 12KB | 99.2% | 大数据量近似统计 |</span><br></pre></td></tr></table></figure><p><strong>关键点分析</strong></p><ol><li><strong>内存效率</strong>：百万级数据仅需12KB内存，极其高效</li><li><strong>统计精度</strong>：实际误差通常小于理论值，表现优秀</li><li><strong>性能稳定</strong>：数据量增加不会显著影响性能</li><li><strong>生产适用</strong>：完全满足生产环境的UV统计需求</li></ol><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div>]]></content>
    
    
    <summary type="html">详细介绍Redis的基本概念、安装配置、数据类型、常用命令以及实际应用场景，帮助开发者快速掌握这个强大的内存数据库。</summary>
    
    
    
    <category term="墨香实战" scheme="https://blog.adoreorg.cn/categories/%E5%A2%A8%E9%A6%99%E5%AE%9E%E6%88%98/"/>
    
    
    <category term="Redis" scheme="https://blog.adoreorg.cn/tags/Redis/"/>
    
    <category term="缓存" scheme="https://blog.adoreorg.cn/tags/%E7%BC%93%E5%AD%98/"/>
    
    <category term="NoSQL" scheme="https://blog.adoreorg.cn/tags/NoSQL/"/>
    
    <category term="分布式" scheme="https://blog.adoreorg.cn/tags/%E5%88%86%E5%B8%83%E5%BC%8F/"/>
    
    <category term="高性能" scheme="https://blog.adoreorg.cn/tags/%E9%AB%98%E6%80%A7%E8%83%BD/"/>
    
  </entry>
  
  <entry>
    <title>Markdown语法与外挂标签写法汇总</title>
    <link href="https://blog.adoreorg.cn/posts/2013454d.html"/>
    <id>https://blog.adoreorg.cn/posts/2013454d.html</id>
    <published>2025-08-09T10:19:03.000Z</published>
    <updated>2025-08-23T14:00:00.000Z</updated>
    
    <content type="html"><![CDATA[<h1>1.Markdown语法自带格式</h1>    <div class="note note-info">      <div class="note-header">        <i class="note-icon fa-regular fa-circle-info"></i>        <span class="note-title">提示信息</span>      </div>      <div class="note-content">        <p>参考：<a href="https://blog.csdn.net/u014061630/article/details/81359144">Markdown语法图文全面详解(10分钟学会)</a></p>      </div>    </div>      <div class="note note-warning">      <div class="note-header">        <i class="note-icon fa-regular fa-circle-dot"></i>        <span class="note-title">提示信息</span>      </div>      <div class="note-content">        <p>注意：此页面偶尔会存在CSS冲突问题!</p>      </div>    </div>  <h2 id="1-1-代码块">1.1 代码块</h2><div class="tabs" id="分栏"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#分栏-1">示例源码</button></li><li class="tab"><button type="button" data-href="#分栏-2">渲染演示</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="分栏-1"><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">\```shell</span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">VSCode终端</span></span><br><span class="line">hexo clean; hexo s</span><br><span class="line">hexo clean; hexo g; hexo d</span><br><span class="line">git add .; git commit -m &quot;npm publish&quot;; npm version patch; </span><br><span class="line">git push</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">Cmder终端</span></span><br><span class="line">hexo clean &amp;&amp; hexo s</span><br><span class="line">hexo clean &amp;&amp; hexo g &amp;&amp; hexo d</span><br><span class="line">git add . &amp;&amp; git commit -m &quot;npm publish&quot; &amp;&amp; npm version patch</span><br><span class="line">git push</span><br><span class="line">\```</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-2"><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_"># </span><span class="language-bash">VSCode终端</span></span><br><span class="line">hexo clean; hexo s</span><br><span class="line">hexo clean; hexo g; hexo d</span><br><span class="line">git add .; git commit -m &quot;npm publish&quot;; npm version patch; </span><br><span class="line">git push</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">Cmder终端</span></span><br><span class="line">hexo clean &amp;&amp; hexo s</span><br><span class="line">hexo clean &amp;&amp; hexo g &amp;&amp; hexo d</span><br><span class="line">git add . &amp;&amp; git commit -m &quot;npm publish&quot; &amp;&amp; npm version patch</span><br><span class="line">git push</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h2 id="1-2-多级标题">1.2 多级标题</h2><div class="tabs" id="分栏"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#分栏-1">示例源码</button></li><li class="tab"><button type="button" data-href="#分栏-2">渲染演示</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="分栏-1"><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="section"># H1</span></span><br><span class="line"><span class="section">## H2</span></span><br><span class="line"><span class="section">### H3</span></span><br><span class="line"><span class="section">#### H4</span></span><br><span class="line"><span class="section">##### H5</span></span><br><span class="line"><span class="section">###### H6</span></span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-2"><p>见本文章标题!</p><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h2 id="1-3-文字样式">1.3 文字样式</h2><div class="tabs" id="分栏"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#分栏-1">示例源码</button></li><li class="tab"><button type="button" data-href="#分栏-2">渲染演示</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="分栏-1"><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="language-xml"><span class="tag">&lt;<span class="name">u</span>&gt;</span></span>下划线演示<span class="language-xml"><span class="tag">&lt;/<span class="name">u</span>&gt;</span></span></span><br><span class="line"></span><br><span class="line">文字<span class="strong">**加粗**</span>演示</span><br><span class="line"></span><br><span class="line">文字<span class="emphasis">*斜体*</span>演示</span><br><span class="line"></span><br><span class="line">文本<span class="code">`高亮`</span>演示</span><br><span class="line"></span><br><span class="line">文本~~删除~~线演示</span><br><span class="line"></span><br><span class="line"><span class="language-xml"><span class="tag">&lt;<span class="name">font</span> <span class="attr">size</span> = <span class="string">5</span>&gt;</span></span>5号字<span class="language-xml"><span class="tag">&lt;/<span class="name">font</span>&gt;</span></span></span><br><span class="line"><span class="language-xml"><span class="tag">&lt;<span class="name">font</span> <span class="attr">face</span>=<span class="string">&quot;黑体&quot;</span>&gt;</span></span>黑体<span class="language-xml"><span class="tag">&lt;/<span class="name">font</span>&gt;</span></span></span><br><span class="line"><span class="language-xml"><span class="tag">&lt;<span class="name">font</span> <span class="attr">color</span>=<span class="string">blue</span>&gt;</span></span>蓝色<span class="language-xml"><span class="tag">&lt;/<span class="name">font</span>&gt;</span></span></span><br><span class="line"></span><br><span class="line"><span class="language-xml"><span class="tag">&lt;<span class="name">table</span>&gt;</span></span><span class="language-xml"><span class="tag">&lt;<span class="name">tr</span>&gt;</span></span><span class="language-xml"><span class="tag">&lt;<span class="name">td</span> <span class="attr">bgcolor</span>=<span class="string">MistyRose</span>&gt;</span></span>这里的背景色是：MistyRosen，此处输入任意想输入的内容<span class="language-xml"><span class="tag">&lt;/<span class="name">td</span>&gt;</span></span><span class="language-xml"><span class="tag">&lt;/<span class="name">tr</span>&gt;</span></span><span class="language-xml"><span class="tag">&lt;/<span class="name">table</span>&gt;</span></span></span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-2"><p><u>下划线演示</u></p><p>文字<strong>加粗</strong>演示</p><p>文字<em>斜体</em>演示</p><p>文本<code>高亮</code>演示</p><p>文本<s>删除</s>线演示</p><p><font size = 5>5号字</font><br><font face="黑体">黑体</font><br><font color=blue>蓝色</font></p><table><tr><td bgcolor=MistyRose>这里的背景色是：MistyRosen，此处输入任意想输入的内容</td></tr></table><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div>    <div class="note note-info">      <div class="note-header">        <i class="note-icon fa-regular fa-circle-info"></i>        <span class="note-title">提示信息</span>      </div>      <div class="note-content">        <p>上述要点可参考:<a href="https://blog.csdn.net/qq_43732429/article/details/108034518">【Markdown语法】字体颜色大小及文字底色设置</a></p>      </div>    </div>  <h2 id="1-4-引用">1.4 引用</h2><div class="tabs" id="分栏"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#分栏-1">示例源码</button></li><li class="tab"><button type="button" data-href="#分栏-2">渲染演示</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="分栏-1"><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="quote">&gt;  Java</span></span><br><span class="line"><span class="quote">&gt; 二级引用演示</span></span><br><span class="line"><span class="quote">&gt; MySQL</span></span><br><span class="line"><span class="quote">&gt; &gt;外键</span></span><br><span class="line"><span class="quote">&gt; &gt;</span></span><br><span class="line"><span class="quote">&gt; &gt;事务</span></span><br><span class="line"><span class="quote">&gt; &gt;</span></span><br><span class="line"><span class="quote">&gt; &gt;<span class="strong">**行级锁**</span>(引用内部一样可以用格式)</span></span><br><span class="line"><span class="quote">&gt; </span></span><br><span class="line"><span class="quote">&gt; ....</span></span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-2"><blockquote><p>Java<br>二级引用演示<br>MySQL</p><blockquote><p>外键</p><p>事务</p><p><strong>行级锁</strong>(引用内部一样可以用格式)</p></blockquote><p>…</p></blockquote><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h2 id="1-5-分割线">1.5 分割线</h2><div class="tabs" id="分栏"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#分栏-1">示例源码</button></li><li class="tab"><button type="button" data-href="#分栏-2">渲染演示</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="分栏-1"><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">---</span><br><span class="line"><span class="strong">**<span class="emphasis">*</span></span></span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-2"><hr><hr><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h2 id="1-6-列表-跟空格都可以">1.6 列表(*,+,-跟空格都可以)</h2><h3 id="1-6-1-无序列表">1.6.1 无序列表</h3><div class="tabs" id="分栏"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#分栏-1">示例源码</button></li><li class="tab"><button type="button" data-href="#分栏-2">渲染演示</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="分栏-1"><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="bullet">*</span> Java</span><br><span class="line"><span class="bullet">*</span> Python</span><br><span class="line"><span class="bullet">*</span> ...</span><br><span class="line"></span><br><span class="line"><span class="bullet">+</span> Java</span><br><span class="line"><span class="bullet">+</span> Python</span><br><span class="line"><span class="bullet">+</span> ...</span><br><span class="line"></span><br><span class="line"><span class="bullet">-</span> Java</span><br><span class="line"><span class="bullet">-</span> Python</span><br><span class="line"><span class="bullet">-</span> ...</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-2"><ul><li>Java</li><li>Python</li><li>…</li></ul><ul><li>Java</li><li>Python</li><li>…</li></ul><ul><li>Java</li><li>Python</li><li>…</li></ul><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h3 id="1-6-2-有序列表">1.6.2 有序列表</h3><div class="tabs" id="分栏"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#分栏-1">示例源码</button></li><li class="tab"><button type="button" data-href="#分栏-2">渲染演示</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="分栏-1"><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="section"># 注意后面有空格</span></span><br><span class="line"><span class="bullet">1.</span> </span><br><span class="line"><span class="bullet">2.</span> </span><br><span class="line"><span class="bullet">3.</span> </span><br><span class="line"><span class="bullet">4.</span> </span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-2"><ol><li></li><li></li><li></li><li></li></ol><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h2 id="1-7-图片">1.7 图片</h2><div class="tabs" id="分栏"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#分栏-1">示例源码</button></li><li class="tab"><button type="button" data-href="#分栏-2">渲染演示</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="分栏-1"><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="section"># 本地图片</span></span><br><span class="line"><span class="language-xml"><span class="tag">&lt;<span class="name">img</span> <span class="attr">src</span>=<span class="string">&quot;/assets/pusheencode.webp&quot;</span> <span class="attr">alt</span>=<span class="string">&quot;示例图片&quot;</span> <span class="attr">style</span>=<span class="string">&quot;zoom:50%;&quot;</span> /&gt;</span></span></span><br><span class="line"><span class="section"># 在线图片</span></span><br><span class="line">![<span class="string">code</span>](<span class="link">https://cdn.jsdelivr.net/gh/fomalhaut1998/markdown_pic/img/code.png</span>)</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-2"><p>本地图片:<br><img src="/assets/pusheencode.webp" alt="示例图片" style="zoom:50%;" /><br>在线图片:<br><img src="https://cdn.jsdelivr.net/gh/fomalhaut1998/markdown_pic/img/code.png" alt="code"></p><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h2 id="1-8-表格">1.8 表格</h2><div class="tabs" id="分栏"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#分栏-1">示例源码</button></li><li class="tab"><button type="button" data-href="#分栏-2">渲染演示</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="分栏-1"><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">| 项目标号 | 资金     | 备注 |</span><br><span class="line">| -------- | -------- | ---- |</span><br><span class="line">| 1        | 100，000 | 无   |</span><br><span class="line">| 2        | 200，000 | 无   |</span><br><span class="line">| 3        | 300,600  | 重要 |</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-2"><table><thead><tr><th>项目标号</th><th>资金</th><th>备注</th></tr></thead><tbody><tr><td>1</td><td>100，000</td><td>无</td></tr><tr><td>2</td><td>200，000</td><td>无</td></tr><tr><td>3</td><td>300,600</td><td>重要</td></tr></tbody></table><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h2 id="1-9-公式">1.9 公式</h2><div class="tabs" id="分栏"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#分栏-1">示例源码</button></li><li class="tab"><button type="button" data-href="#分栏-2">渲染演示</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="分栏-1"><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">$$</span><br><span class="line">\Gamma(z)=\int<span class="emphasis">_0^\infty t^&#123;z-1&#125;e^&#123;-t&#125;dt.</span></span><br><span class="line"><span class="emphasis">$$</span></span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-2"><p>$$<br>\Gamma(z)=\int_0^\infty t^{z-1}e^{-t}dt.<br>$$</p><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h1>2.Butterfly外挂标签</h1>    <div class="note note-info">      <div class="note-header">        <i class="note-icon fa-regular fa-circle-info"></i>        <span class="note-title">提示信息</span>      </div>      <div class="note-content">        <p>这部分参考安知鱼:<a href="https://anzhiy.cn/posts/7d58.html">基于Butterfly的外挂标签引入</a></p>      </div>    </div>  <h2 id="2-1-行内文本样式-text">2.1 行内文本样式 text</h2><div class="tabs" id="分栏"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#分栏-1">标签语法</button></li><li class="tab"><button type="button" data-href="#分栏-2">示例源码</button></li><li class="tab"><button type="button" data-href="#分栏-3">渲染演示</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="分栏-1"><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">&#123;% u 文本内容 %&#125;</span><br><span class="line">&#123;% emp 文本内容 %&#125;</span><br><span class="line">&#123;% wavy 文本内容 %&#125;</span><br><span class="line">&#123;% del 文本内容 %&#125;</span><br><span class="line">&#123;% kbd 文本内容 %&#125;</span><br><span class="line">&#123;% psw 文本内容 %&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-2"><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="bullet">1.</span> 带 &#123;% u 下划线 %&#125; 的文本</span><br><span class="line"><span class="bullet">2.</span> 带 &#123;% emp 着重号 %&#125; 的文本</span><br><span class="line"><span class="bullet">3.</span> 带 &#123;% wavy 波浪线 %&#125; 的文本</span><br><span class="line"><span class="bullet">4.</span> 带 &#123;% del 删除线 %&#125; 的文本</span><br><span class="line"><span class="bullet">5.</span> 键盘样式的文本 &#123;% kbd command %&#125; + &#123;% kbd D %&#125;</span><br><span class="line"><span class="bullet">6.</span> 密码样式的文本：&#123;% psw 这里没有验证码 %&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-3"><ol><li>带 <u>下划线</u> 的文本</li><li>带 <emp>着重号</emp> 的文本</li><li>带 <wavy>波浪线</wavy> 的文本</li><li>带 <del>删除线</del> 的文本</li><li>键盘样式的文本 <kbd>command</kbd> + <kbd>D</kbd></li><li>密码样式的文本：<psw>这里没有验证码</psw></li></ol><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h2 id="2-2-行内文本-span">2.2 行内文本 span</h2><div class="tabs" id="分栏"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#分栏-1">标签语法</button></li><li class="tab"><button type="button" data-href="#分栏-2">配置参数</button></li><li class="tab"><button type="button" data-href="#分栏-3">示例源码</button></li><li class="tab"><button type="button" data-href="#分栏-4">渲染演示</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="分栏-1"><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&#123;% span 样式参数(参数以空格划分), 文本内容 %&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-2"><ol><li><code>字体</code>: logo, code</li><li><code>颜色</code>: red,yellow,green,cyan,blue,gray</li><li><code>大小</code>: small, h4, h3, h2, h1, large, huge, ultra</li><li><code>对齐方向</code>: left, center, right</li></ol><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-3"><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> 彩色文字</span><br><span class="line">在一段话中方便插入各种颜色的标签，包括：&#123;% span red, 红色 %&#125;、&#123;% span yellow, 黄色 %&#125;、&#123;% span green, 绿色 %&#125;、&#123;% span cyan, 青色 %&#125;、&#123;% span blue, 蓝色 %&#125;、&#123;% span gray, 灰色 %&#125;。</span><br><span class="line"><span class="bullet">-</span> 超大号文字</span><br><span class="line">文档「开始」页面中的标题部分就是超大号文字。</span><br><span class="line">&#123;% span center logo large, Volantis %&#125;</span><br><span class="line">&#123;% span center small, A Wonderful Theme for Hexo %&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-4"><ul><li>彩色文字<br>在一段话中方便插入各种颜色的标签，包括：<span class='p red'>红色</span>、<span class='p yellow'>黄色</span>、<span class='p green'>绿色</span>、<span class='p cyan'>青色</span>、<span class='p blue'>蓝色</span>、<span class='p gray'>灰色</span>。</li><li>超大号文字<br>文档「开始」页面中的标题部分就是超大号文字。<br><span class='p center logo large'>Volantis</span><br><span class='p center small'>A Wonderful Theme for Hexo</span></li></ul><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h2 id="2-3-段落文本-p">2.3 段落文本 p</h2><div class="tabs" id="分栏"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#分栏-1">标签语法</button></li><li class="tab"><button type="button" data-href="#分栏-2">配置参数</button></li><li class="tab"><button type="button" data-href="#分栏-3">示例源码</button></li><li class="tab"><button type="button" data-href="#分栏-4">渲染演示</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="分栏-1"><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&#123;% p 样式参数(参数以空格划分), 文本内容 %&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-2"><ol><li><code>字体</code>: logo, code</li><li><code>颜色</code>: red,yellow,green,cyan,blue,gray</li><li><code>大小</code>: small, h4, h3, h2, h1, large, huge, ultra</li><li><code>对齐方向</code>: left, center, right</li></ol><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-3"><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> 彩色文字</span><br><span class="line">在一段话中方便插入各种颜色的标签，包括：&#123;% p red, 红色 %&#125;、&#123;% p yellow, 黄色 %&#125;、&#123;% p green, 绿色 %&#125;、&#123;% p cyan, 青色 %&#125;、&#123;% p blue, 蓝色 %&#125;、&#123;% p gray, 灰色 %&#125;。</span><br><span class="line"><span class="bullet">-</span> 超大号文字</span><br><span class="line">文档「开始」页面中的标题部分就是超大号文字。</span><br><span class="line">&#123;% p center logo large, Volantis %&#125;</span><br><span class="line">&#123;% p center small, A Wonderful Theme for Hexo %&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-4"><ul><li>彩色文字<br>在一段话中方便插入各种颜色的标签，包括：<p class='p red'>红色</p>、<p class='p yellow'>黄色</p>、<p class='p green'>绿色</p>、<p class='p cyan'>青色</p>、<p class='p blue'>蓝色</p>、<p class='p gray'>灰色</p>。</li><li>超大号文字<br>文档「开始」页面中的标题部分就是超大号文字。</li></ul><p class='p center logo large'>Volantis</p><p class='p center small'>A Wonderful Theme for Hexo</p><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h2 id="2-4-引用note">2.4 引用note</h2><div class="tabs" id="分栏"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#分栏-1">通用配置</button></li><li class="tab"><button type="button" data-href="#分栏-2">语法格式</button></li><li class="tab"><button type="button" data-href="#分栏-3">参数配置</button></li><li class="tab"><button type="button" data-href="#分栏-4">示例源码</button></li><li class="tab"><button type="button" data-href="#分栏-5">渲染演示</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="分栏-1"><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">note:</span><br><span class="line">  # Note tag style values:</span><br><span class="line">  #  - simple    bs-callout old alert style. Default.</span><br><span class="line">  #  - modern    bs-callout new (v2-v3) alert style.</span><br><span class="line">  #  - flat      flat callout style with background, like on Mozilla or StackOverflow.</span><br><span class="line">  #  - disabled  disable all CSS styles import of note tag.</span><br><span class="line">  style: simple</span><br><span class="line">  icons: false</span><br><span class="line">  border<span class="emphasis">_radius: 3</span></span><br><span class="line"><span class="emphasis">  # Offset lighter of background in % for modern and flat styles (modern: -12 | 12; flat: -18 | 6).</span></span><br><span class="line"><span class="emphasis">  # Offset also applied to label tag variables. This option can work with disabled note tag.</span></span><br><span class="line"><span class="emphasis">  light_</span>bg<span class="emphasis">_offset: 0</span></span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-2"><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">&#123;% note 类型 标题（可选） %&#125;</span><br><span class="line">内容</span><br><span class="line">&#123;% endnote %&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-3"><p>支持的类型：</p><table><thead><tr><th>类型</th><th>颜色</th><th>说明</th></tr></thead><tbody><tr><td><code>info</code></td><td>#1890ff</td><td>信息提示</td></tr><tr><td><code>warning</code></td><td>#f0ad4e</td><td>警告提示</td></tr><tr><td><code>error</code></td><td>#d9534f</td><td>错误提示</td></tr><tr><td><code>success</code></td><td>#52c41a</td><td>成功提示</td></tr><tr><td><code>simple</code></td><td>#8c8c8c</td><td>简单信息</td></tr><tr><td><code>primary</code></td><td>#6f42c1</td><td>主要信息</td></tr><tr><td><code>question</code></td><td>#5bc0de</td><td>问题提示</td></tr></tbody></table><p>用法示例：</p><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line">&#123;% note info %&#125;</span><br><span class="line">这是一条信息提示</span><br><span class="line">&#123;% endnote %&#125;</span><br><span class="line"></span><br><span class="line">&#123;% note warning 警告标题 %&#125;</span><br><span class="line">这是一条警告信息</span><br><span class="line">&#123;% endnote %&#125;</span><br><span class="line"></span><br><span class="line">&#123;% note error 错误 %&#125;</span><br><span class="line">这是一个错误提示</span><br><span class="line">&#123;% endnote %&#125;</span><br><span class="line"></span><br><span class="line">&#123;% note success 成功 %&#125;</span><br><span class="line">操作成功！</span><br><span class="line">&#123;% endnote %&#125;</span><br><span class="line"></span><br><span class="line">&#123;% note simple 简单 %&#125;</span><br><span class="line">简单信息提示</span><br><span class="line">&#123;% endnote %&#125;</span><br><span class="line"></span><br><span class="line">&#123;% note primary 主要 %&#125;</span><br><span class="line">主要信息</span><br><span class="line">&#123;% endnote %&#125;</span><br><span class="line"></span><br><span class="line">&#123;% note question 问题 %&#125;</span><br><span class="line">这是一个问题</span><br><span class="line">&#123;% endnote %&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-4"><details class="folding-tag" blue><summary> 提示块示例 </summary>              <div class='content'>              <figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line">&#123;% note info %&#125;</span><br><span class="line">这是一条信息提示</span><br><span class="line">&#123;% endnote %&#125;</span><br><span class="line"></span><br><span class="line">&#123;% note warning 警告标题 %&#125;</span><br><span class="line">这是一条警告信息</span><br><span class="line">&#123;% endnote %&#125;</span><br><span class="line"></span><br><span class="line">&#123;% note error 错误 %&#125;</span><br><span class="line">这是一个错误提示</span><br><span class="line">&#123;% endnote %&#125;</span><br><span class="line"></span><br><span class="line">&#123;% note success 成功 %&#125;</span><br><span class="line">操作成功！</span><br><span class="line">&#123;% endnote %&#125;</span><br><span class="line"></span><br><span class="line">&#123;% note simple 简单 %&#125;</span><br><span class="line">简单信息提示</span><br><span class="line">&#123;% endnote %&#125;</span><br><span class="line"></span><br><span class="line">&#123;% note primary 主要 %&#125;</span><br><span class="line">主要信息</span><br><span class="line">&#123;% endnote %&#125;</span><br><span class="line"></span><br><span class="line">&#123;% note question 问题 %&#125;</span><br><span class="line">这是一个问题</span><br><span class="line">&#123;% endnote %&#125;</span><br></pre></td></tr></table></figure>              </div>            </details><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-5"><details class="folding-tag" blue><summary> 提示块示例 </summary>              <div class='content'>              <div class="note note-info">  <div class="note-header">    <i class="note-icon fa-regular fa-circle-info"></i>    <span class="note-title">提示信息</span>  </div>  <div class="note-content">    <p>这是一条信息提示</p>  </div></div><div class="note note-warning">  <div class="note-header">    <i class="note-icon fa-regular fa-circle-dot"></i>    <span class="note-title">警告标题</span>  </div>  <div class="note-content">    <p>这是一条警告信息</p>  </div></div><div class="note note-error">  <div class="note-header">    <i class="note-icon fa-regular fa-circle-xmark"></i>    <span class="note-title">错误</span>  </div>  <div class="note-content">    <p>这是一个错误提示</p>  </div></div><div class="note note-success">  <div class="note-header">    <i class="note-icon fa-regular fa-circle-check"></i>    <span class="note-title">成功</span>  </div>  <div class="note-content">    <p>操作成功！</p>  </div></div><div class="note note-simple">  <div class="note-header">    <i class="note-icon fa-regular fa-circle-info"></i>    <span class="note-title">简单</span>  </div>  <div class="note-content">    <p>简单信息提示</p>  </div></div><div class="note note-primary">  <div class="note-header">    <i class="note-icon fa-regular fa-star"></i>    <span class="note-title">主要</span>  </div>  <div class="note-content">    <p>主要信息</p>  </div></div><div class="note note-question">  <div class="note-header">    <i class="note-icon fa-regular fa-circle-question"></i>    <span class="note-title">问题</span>  </div>  <div class="note-content">    <p>这是一个问题</p>  </div></div>              </div>            </details><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h2 id="2-5-上标标签-tip">2.5 上标标签 tip</h2><div class="tabs" id="分栏"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#分栏-1">标签语法</button></li><li class="tab"><button type="button" data-href="#分栏-2">配置参数</button></li><li class="tab"><button type="button" data-href="#分栏-3">示例源码</button></li><li class="tab"><button type="button" data-href="#分栏-4">渲染演示</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="分栏-1"><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&#123;% tip [参数，可选] %&#125;文本内容&#123;% endtip %&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-2"><ol><li><code>样式</code>: success,error,warning,bolt,ban,home,sync,cogs,key,bell</li><li><code>自定义图标</code>: 支持fontawesome。</li></ol><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-3"><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">&#123;% tip %&#125;default&#123;% endtip %&#125;</span><br><span class="line">&#123;% tip info %&#125;info&#123;% endtip %&#125;</span><br><span class="line">&#123;% tip success %&#125;success&#123;% endtip %&#125;</span><br><span class="line">&#123;% tip error %&#125;error&#123;% endtip %&#125;</span><br><span class="line">&#123;% tip warning %&#125;warning&#123;% endtip %&#125;</span><br><span class="line">&#123;% tip bolt %&#125;bolt&#123;% endtip %&#125;</span><br><span class="line">&#123;% tip ban %&#125;ban&#123;% endtip %&#125;</span><br><span class="line">&#123;% tip home %&#125;home&#123;% endtip %&#125;</span><br><span class="line">&#123;% tip sync %&#125;sync&#123;% endtip %&#125;</span><br><span class="line">&#123;% tip cogs %&#125;cogs&#123;% endtip %&#125;</span><br><span class="line">&#123;% tip key %&#125;key&#123;% endtip %&#125;</span><br><span class="line">&#123;% tip bell %&#125;bell&#123;% endtip %&#125;</span><br><span class="line">&#123;% tip fa-atom %&#125;自定义font awesome图标&#123;% endtip %&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-4"><div class="tip "><p>default</p></div><div class="tip info"><p>info</p></div><div class="tip success"><p>success</p></div><div class="tip error"><p>error</p></div><div class="tip warning"><p>warning</p></div><div class="tip bolt"><p>bolt</p></div><div class="tip ban"><p>ban</p></div><div class="tip home"><p>home</p></div><div class="tip sync"><p>sync</p></div><div class="tip cogs"><p>cogs</p></div><div class="tip key"><p>key</p></div><div class="tip bell"><p>bell</p></div><div class="tip fa-atom"><p>自定义font awesome图标</p></div><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h2 id="2-6-动态标签-anima">2.6 动态标签 anima</h2><div class="tabs" id="分栏"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#分栏-1">示例源码</button></li><li class="tab"><button type="button" data-href="#分栏-2">配置参数</button></li><li class="tab"><button type="button" data-href="#分栏-3">示例源码</button></li><li class="tab"><button type="button" data-href="#分栏-4">渲染演示</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="分栏-1"><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&#123;% tip [参数，可选] %&#125;文本内容&#123;% endtip %&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-2"><pre><code>&lt;div class=&quot;note note-info&quot;&gt;  &lt;div class=&quot;note-header&quot;&gt;    &lt;i class=&quot;note-icon fa-regular fa-circle-info&quot;&gt;&lt;/i&gt;    &lt;span class=&quot;note-title&quot;&gt;提示信息&lt;/span&gt;  &lt;/div&gt;  &lt;div class=&quot;note-content&quot;&gt;    &lt;ol&gt;</code></pre><li>将所需的CSS类添加到图标（或DOM中的任何元素）。</li><li>对于父级悬停样式，需要给目标元素添加指定CSS类，同时还要给目标元素的父级元素添加CSS类<code>faa-parent animated-hover</code>。（详情见示例及示例源码）<br>You can regulate the speed of the animation by adding the CSS class or . faa-fastfaa-slow</li><li>可以通过给目标元素添加CSS类<code>faa-fast</code>或<code>faa-slow</code>来控制动画快慢。</li></ol><pre><code>  &lt;/div&gt;&lt;/div&gt;</code></pre><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-3"><p>1.On DOM load（当页面加载时显示动画）</p><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">&#123;% tip warning faa-horizontal animated %&#125;warning&#123;% endtip %&#125;</span><br><span class="line">&#123;% tip ban faa-flash animated %&#125;ban&#123;% endtip %&#125;</span><br></pre></td></tr></table></figure><p>2.调整动画速度</p><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">&#123;% tip warning faa-horizontal animated faa-fast %&#125;warning&#123;% endtip %&#125;</span><br><span class="line">&#123;% tip ban faa-flash animated faa-slow %&#125;ban&#123;% endtip %&#125;</span><br></pre></td></tr></table></figure><p>3.On hover（当鼠标悬停时显示动画）</p><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">&#123;% tip warning faa-horizontal animated-hover %&#125;warning&#123;% endtip %&#125;</span><br><span class="line">&#123;% tip ban faa-flash animated-hover %&#125;ban&#123;% endtip %&#125;</span><br></pre></td></tr></table></figure><p>4.On parent hover（当鼠标悬停在父级元素时显示动画）</p><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">&#123;% tip warning faa-parent animated-hover %&#125;<span class="language-xml"><span class="tag">&lt;<span class="name">p</span> <span class="attr">class</span>=<span class="string">&quot;faa-horizontal&quot;</span>&gt;</span></span>warning<span class="language-xml"><span class="tag">&lt;/<span class="name">p</span>&gt;</span></span>&#123;% endtip %&#125;</span><br><span class="line">&#123;% tip ban faa-parent animated-hover %&#125;<span class="language-xml"><span class="tag">&lt;<span class="name">p</span> <span class="attr">class</span>=<span class="string">&quot;faa-flash&quot;</span>&gt;</span></span>ban<span class="language-xml"><span class="tag">&lt;/<span class="name">p</span>&gt;</span></span>&#123;% endtip %&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-4"><p>1.On DOM load（当页面加载时显示动画）</p><div class="tip warning faa-horizontal animated"><p>warning</p></div><div class="tip ban faa-flash animated"><p>ban</p></div>2.调整动画速度<div class="tip warning faa-horizontal animated faa-fast"><p>warning</p></div><div class="tip ban faa-flash animated faa-slow"><p>ban</p></div>3.On hover（当鼠标悬停时显示动画）<div class="tip warning faa-horizontal animated-hover"><p>warning</p></div><div class="tip ban faa-flash animated-hover"><p>ban</p></div>4.On parent hover（当鼠标悬停在父级元素时显示动画）<div class="tip warning faa-parent animated-hover"><p class="faa-horizontal">warning</p></div><div class="tip ban faa-parent animated-hover"><p class="faa-flash">ban</p></div><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h2 id="2-7-复选列表-checkbox">2.7 复选列表 checkbox</h2><div class="tabs" id="分栏"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#分栏-1">标签语法</button></li><li class="tab"><button type="button" data-href="#分栏-2">配置参数</button></li><li class="tab"><button type="button" data-href="#分栏-3">示例源码</button></li><li class="tab"><button type="button" data-href="#分栏-4">渲染演示</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="分栏-1"><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&#123;% checkbox 样式参数（可选）, 文本（支持简单md） %&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-2"><ol><li><code>样式</code>: plus, minus, times</li><li><code>颜色</code>: red,yellow,green,cyan,blue,gray</li><li><code>选中状态</code>: checked</li></ol><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-3"><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">&#123;% checkbox 纯文本测试 %&#125;</span><br><span class="line">&#123;% checkbox checked, 支持简单的 [<span class="string">markdown</span>](<span class="link">https://guides.github.com/features/mastering-markdown/</span>) 语法 %&#125;</span><br><span class="line">&#123;% checkbox red, 支持自定义颜色 %&#125;</span><br><span class="line">&#123;% checkbox green checked, 绿色 + 默认选中 %&#125;</span><br><span class="line">&#123;% checkbox yellow checked, 黄色 + 默认选中 %&#125;</span><br><span class="line">&#123;% checkbox cyan checked, 青色 + 默认选中 %&#125;</span><br><span class="line">&#123;% checkbox blue checked, 蓝色 + 默认选中 %&#125;</span><br><span class="line">&#123;% checkbox plus green checked, 增加 %&#125;</span><br><span class="line">&#123;% checkbox minus yellow checked, 减少 %&#125;</span><br><span class="line">&#123;% checkbox times red checked, 叉 %&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-4"><div class='checkbox'><input type="checkbox" />            <p>纯文本测试</p>            </div><div class='checkbox checked'><input type="checkbox" checked="checked"/>            <p>支持简单的 <a href="https://guides.github.com/features/mastering-markdown/">markdown</a> 语法</p>            </div><div class='checkbox red'><input type="checkbox" />            <p>支持自定义颜色</p>            </div><div class='checkbox green checked'><input type="checkbox" checked="checked"/>            <p>绿色 + 默认选中</p>            </div><div class='checkbox yellow checked'><input type="checkbox" checked="checked"/>            <p>黄色 + 默认选中</p>            </div><div class='checkbox cyan checked'><input type="checkbox" checked="checked"/>            <p>青色 + 默认选中</p>            </div><div class='checkbox blue checked'><input type="checkbox" checked="checked"/>            <p>蓝色 + 默认选中</p>            </div><div class='checkbox plus green checked'><input type="checkbox" checked="checked"/>            <p>增加</p>            </div><div class='checkbox minus yellow checked'><input type="checkbox" checked="checked"/>            <p>减少</p>            </div><div class='checkbox times red checked'><input type="checkbox" checked="checked"/>            <p>叉</p>            </div><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h2 id="2-8-单选列表-radio">2.8 单选列表 radio</h2><div class="tabs" id="分栏"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#分栏-1">标签语法</button></li><li class="tab"><button type="button" data-href="#分栏-2">配置参数</button></li><li class="tab"><button type="button" data-href="#分栏-3">示例源码</button></li><li class="tab"><button type="button" data-href="#分栏-4">渲染演示</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="分栏-1"><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&#123;% radio 样式参数（可选）, 文本（支持简单md） %&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-2"><ol><li><code>颜色</code>: red,yellow,green,cyan,blue,gray</li><li><code>选中状态</code>: checked</li></ol><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-3"><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">&#123;% radio 纯文本测试 %&#125;</span><br><span class="line">&#123;% radio checked, 支持简单的 [<span class="string">markdown</span>](<span class="link">https://guides.github.com/features/mastering-markdown/</span>) 语法 %&#125;</span><br><span class="line">&#123;% radio red, 支持自定义颜色 %&#125;</span><br><span class="line">&#123;% radio green, 绿色 %&#125;</span><br><span class="line">&#123;% radio yellow, 黄色 %&#125;</span><br><span class="line">&#123;% radio cyan, 青色 %&#125;</span><br><span class="line">&#123;% radio blue, 蓝色 %&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-4"><div class='checkbox'><input type="radio" />            <p>纯文本测试</p>            </div><div class='checkbox checked'><input type="radio" checked="checked"/>            <p>支持简单的 <a href="https://guides.github.com/features/mastering-markdown/">markdown</a> 语法</p>            </div><div class='checkbox red'><input type="radio" />            <p>支持自定义颜色</p>            </div><div class='checkbox green'><input type="radio" />            <p>绿色</p>            </div><div class='checkbox yellow'><input type="radio" />            <p>黄色</p>            </div><div class='checkbox cyan'><input type="radio" />            <p>青色</p>            </div><div class='checkbox blue'><input type="radio" />            <p>蓝色</p>            </div><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h2 id="2-9-时间轴-timeline">2.9 时间轴 timeline</h2><div class="tabs" id="分栏"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#分栏-1">标签语法</button></li><li class="tab"><button type="button" data-href="#分栏-2">配置参数</button></li><li class="tab"><button type="button" data-href="#分栏-3">示例源码</button></li><li class="tab"><button type="button" data-href="#分栏-4">渲染演示</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="分栏-1"><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">&#123;% timeline 时间线标题（可选）[,color] %&#125;</span><br><span class="line">&lt;!-- timeline 时间节点（标题） --&gt;</span><br><span class="line">正文内容</span><br><span class="line">&lt;!-- endtimeline --&gt;</span><br><span class="line">&lt;!-- timeline 时间节点（标题） --&gt;</span><br><span class="line">正文内容</span><br><span class="line">&lt;!-- endtimeline --&gt;</span><br><span class="line">&#123;% endtimeline %&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-2"><ol><li><code>title</code>:标题/时间线</li><li><code>color</code>:<code>timeline</code>颜色:default(留空) / blue / pink / red / purple / orange / green</li></ol><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-3"><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line">&#123;% timeline 时间轴样式,blue %&#125;</span><br><span class="line"></span><br><span class="line">&lt;!-- timeline 2020-07-24 [<span class="string">2.6.6 -&gt; 3.0</span>](<span class="link">https://github.com/volantis-x/hexo-theme-volantis/releases</span>) --&gt;</span><br><span class="line"></span><br><span class="line"><span class="bullet">1.</span> 如果有 <span class="code">`hexo-lazyload-image`</span> 插件，需要删除并重新安装最新版本，设置 <span class="code">`lazyload.isSPA: true`</span>。</span><br><span class="line"><span class="bullet">2.</span> 2.x 版本的 css 和 js 不适用于 3.x 版本，如果使用了 <span class="code">`use_cdn: true`</span> 则需要删除。</span><br><span class="line"><span class="bullet">3.</span> 2.x 版本的 fancybox 标签在 3.x 版本中被重命名为 gallery 。</span><br><span class="line"><span class="bullet">4.</span> 2.x 版本的置顶 <span class="code">`top: true`</span> 改为了 <span class="code">`pin: true`</span>，并且同样适用于 <span class="code">`layout: page`</span> 的页面。</span><br><span class="line"><span class="bullet">5.</span> 如果使用了 <span class="code">`hexo-offline`</span> 插件，建议卸载，3.0 版本默认开启了 pjax 服务。</span><br><span class="line"></span><br><span class="line">&lt;!-- endtimeline --&gt;</span><br><span class="line"></span><br><span class="line">&lt;!-- timeline 2020-05-15 [<span class="string">2.6.3 -&gt; 2.6.6</span>](<span class="link">https://github.com/volantis-x/hexo-theme-volantis/releases/tag/2.6.6</span>) --&gt;</span><br><span class="line"></span><br><span class="line">不需要额外处理。</span><br><span class="line"></span><br><span class="line">&lt;!-- endtimeline --&gt;</span><br><span class="line"></span><br><span class="line">&lt;!-- timeline 2020-04-20 [<span class="string">2.6.2 -&gt; 2.6.3</span>](<span class="link">https://github.com/volantis-x/hexo-theme-volantis/releases/tag/2.6.3</span>) --&gt;</span><br><span class="line"></span><br><span class="line"><span class="bullet">1.</span> 全局搜索 <span class="code">`seotitle`</span> 并替换为 <span class="code">`seo_title`</span>。</span><br><span class="line"><span class="bullet">2.</span> group 组件的索引规则有变，使用 group 组件的文章内，<span class="code">`group: group_name`</span> 对应的组件名必须是 <span class="code">`group_name`</span>。</span><br><span class="line"><span class="bullet">2.</span> group 组件的列表名优先显示文章的 <span class="code">`short_title`</span> 其次是 <span class="code">`title`</span>。</span><br><span class="line"></span><br><span class="line">&lt;!-- endtimeline --&gt;</span><br><span class="line"></span><br><span class="line">&#123;% endtimeline %&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-4"><div class="timeline blue"><div class='timeline-item headline'><div class='timeline-item-title'><div class='item-circle'><p>时间轴样式</p></div></div></div><div class='timeline-item'><div class='timeline-item-title'><div class='item-circle'><p>2020-07-24 <a href="https://github.com/volantis-x/hexo-theme-volantis/releases">2.6.6 -&gt; 3.0</a></p></div></div><div class='timeline-item-content'><ol><li>如果有 <code>hexo-lazyload-image</code> 插件，需要删除并重新安装最新版本，设置 <code>lazyload.isSPA: true</code>。</li><li>2.x 版本的 css 和 js 不适用于 3.x 版本，如果使用了 <code>use_cdn: true</code> 则需要删除。</li><li>2.x 版本的 fancybox 标签在 3.x 版本中被重命名为 gallery 。</li><li>2.x 版本的置顶 <code>top: true</code> 改为了 <code>pin: true</code>，并且同样适用于 <code>layout: page</code> 的页面。</li><li>如果使用了 <code>hexo-offline</code> 插件，建议卸载，3.0 版本默认开启了 pjax 服务。</li></ol></div></div><div class='timeline-item'><div class='timeline-item-title'><div class='item-circle'><p>2020-05-15 <a href="https://github.com/volantis-x/hexo-theme-volantis/releases/tag/2.6.6">2.6.3 -&gt; 2.6.6</a></p></div></div><div class='timeline-item-content'><p>不需要额外处理。</p></div></div><div class='timeline-item'><div class='timeline-item-title'><div class='item-circle'><p>2020-04-20 <a href="https://github.com/volantis-x/hexo-theme-volantis/releases/tag/2.6.3">2.6.2 -&gt; 2.6.3</a></p></div></div><div class='timeline-item-content'><ol><li>全局搜索 <code>seotitle</code> 并替换为 <code>seo_title</code>。</li><li>group 组件的索引规则有变，使用 group 组件的文章内，<code>group: group_name</code> 对应的组件名必须是 <code>group_name</code>。</li><li>group 组件的列表名优先显示文章的 <code>short_title</code> 其次是 <code>title</code>。</li></ol></div></div></div><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h2 id="2-10-链接卡片-link">2.10 链接卡片 link</h2><div class="tabs" id="分栏"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#分栏-1">标签语法</button></li><li class="tab"><button type="button" data-href="#分栏-2">示例源码</button></li><li class="tab"><button type="button" data-href="#分栏-3">渲染演示</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="分栏-1"><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&#123;% link 标题, 链接, 图片链接（可选） %&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-2"><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&#123;% link 糖果屋教程贴, https://akilar.top/posts/615e2dec/, https://cdn.cbd.int/akilar-candyassets@1.0.36/image/siteicon/favicon.ico %&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-3"><div class='liushen-tag-link'><a class="tag-Link" target="_blank" href=" https://cdn.cbd.int/akilar-candyassets@1.0.36/image/siteicon/favicon.ico">    <div class="tag-link-tips">🪧引用站外地址，不保证站点的可用性和安全性</div>    <div class="tag-link-bottom">        <div class="tag-link-left" style="background-image: url(https://lsky.adoreorg.cn/i/2026/06/08/6a264792045e8.webp);"></div>        <div class="tag-link-right">            <div class="tag-link-title">糖果屋教程贴</div>            <div class="tag-link-sitename"> https://akilar.top/posts/615e2dec/</div>        </div>        <i class="fa-solid fa-angle-right"></i>    </div>    </a></div><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h2 id="2-11-按钮-btns">2.11 按钮 btns</h2><div class="tabs" id="分栏"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#分栏-1">标签语法</button></li><li class="tab"><button type="button" data-href="#分栏-2">参数配置</button></li><li class="tab"><button type="button" data-href="#分栏-3">示例源码</button></li><li class="tab"><button type="button" data-href="#分栏-4">渲染演示</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="分栏-1"><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">&#123;% btns 样式参数 %&#125;</span><br><span class="line">&#123;% cell 标题, 链接, 图片或者图标 %&#125;</span><br><span class="line">&#123;% cell 标题, 链接, 图片或者图标 %&#125;</span><br><span class="line">&#123;% endbtns %&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-2"><ol><li>圆角样式：rounded, circle</li><li>增加文字样式：可以在容器内增加 <code>&lt;b&gt;</code>标题<code>&lt;/b&gt;</code>和<code>&lt;p&gt;</code>描述文字<code>&lt;/p&gt;</code></li><li>布局方式：<br>默认为自动宽度，适合视野内只有一两个的情况。</li></ol><table><thead><tr><th>参数</th><th>含义</th></tr></thead><tbody><tr><td>wide</td><td>宽一点的按钮</td></tr><tr><td>fill</td><td>填充布局，自动铺满至少一行，多了会换行</td></tr><tr><td>center</td><td>居中，按钮之间是固定间距</td></tr><tr><td>around</td><td>居中分散</td></tr><tr><td>grid2</td><td>等宽最多2列，屏幕变窄会适当减少列数</td></tr><tr><td>grid3</td><td>等宽最多3列，屏幕变窄会适当减少列数</td></tr><tr><td>grid4</td><td>等宽最多4列，屏幕变窄会适当减少列数</td></tr><tr><td>grid5</td><td>等宽最多5列，屏幕变窄会适当减少列数</td></tr></tbody></table><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-3"><p>1.如果需要显示类似「团队成员」之类的一组含有头像的链接</p><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">&#123;% btns circle grid5 %&#125;</span><br><span class="line">&#123;% cell xaoxuu, https://xaoxuu.com, https://cdn.jsdelivr.net/gh/xaoxuu/cdn-assets/avatar/avatar.png %&#125;</span><br><span class="line">&#123;% cell xaoxuu, https://xaoxuu.com, https://cdn.jsdelivr.net/gh/xaoxuu/cdn-assets/avatar/avatar.png %&#125;</span><br><span class="line">&#123;% cell xaoxuu, https://xaoxuu.com, https://cdn.jsdelivr.net/gh/xaoxuu/cdn-assets/avatar/avatar.png %&#125;</span><br><span class="line">&#123;% cell xaoxuu, https://xaoxuu.com, https://cdn.jsdelivr.net/gh/xaoxuu/cdn-assets/avatar/avatar.png %&#125;</span><br><span class="line">&#123;% cell xaoxuu, https://xaoxuu.com, https://cdn.jsdelivr.net/gh/xaoxuu/cdn-assets/avatar/avatar.png %&#125;</span><br><span class="line">&#123;% endbtns %&#125;</span><br></pre></td></tr></table></figure><p>2.或者含有图标的按钮</p><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">&#123;% btns rounded grid5 %&#125;</span><br><span class="line">&#123;% cell 下载源码, /, fas fa-download %&#125;</span><br><span class="line">&#123;% cell 查看文档, /, fas fa-book-open %&#125;</span><br><span class="line">&#123;% endbtns %&#125;</span><br></pre></td></tr></table></figure><p>3.圆形图标 + 标题 + 描述 + 图片 + 网格5列 + 居中</p><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">&#123;% btns circle center grid5 %&#125;</span><br><span class="line"><span class="language-xml"><span class="tag">&lt;<span class="name">a</span> <span class="attr">href</span>=<span class="string">&#x27;https://apps.apple.com/cn/app/heart-mate-pro-hrm-utility/id1463348922?ls=1&#x27;</span>&gt;</span></span></span><br><span class="line">  <span class="language-xml"><span class="tag">&lt;<span class="name">i</span> <span class="attr">class</span>=<span class="string">&#x27;fab fa-apple&#x27;</span>&gt;</span></span><span class="language-xml"><span class="tag">&lt;/<span class="name">i</span>&gt;</span></span></span><br><span class="line">  <span class="language-xml"><span class="tag">&lt;<span class="name">b</span>&gt;</span></span>心率管家<span class="language-xml"><span class="tag">&lt;/<span class="name">b</span>&gt;</span></span></span><br><span class="line">  &#123;% p red, 专业版 %&#125;</span><br><span class="line">  <span class="language-xml"><span class="tag">&lt;<span class="name">img</span> <span class="attr">src</span>=<span class="string">&#x27;https://cdn.jsdelivr.net/gh/fomalhaut1998/cdn-assets/qrcode/heartmate_pro.png&#x27;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml"><span class="tag">&lt;/<span class="name">a</span>&gt;</span></span></span><br><span class="line"><span class="language-xml"><span class="tag">&lt;<span class="name">a</span> <span class="attr">href</span>=<span class="string">&#x27;https://apps.apple.com/cn/app/heart-mate-lite-hrm-utility/id1475747930?ls=1&#x27;</span>&gt;</span></span></span><br><span class="line">  <span class="language-xml"><span class="tag">&lt;<span class="name">i</span> <span class="attr">class</span>=<span class="string">&#x27;fab fa-apple&#x27;</span>&gt;</span></span><span class="language-xml"><span class="tag">&lt;/<span class="name">i</span>&gt;</span></span></span><br><span class="line">  <span class="language-xml"><span class="tag">&lt;<span class="name">b</span>&gt;</span></span>心率管家<span class="language-xml"><span class="tag">&lt;/<span class="name">b</span>&gt;</span></span></span><br><span class="line">  &#123;% p green, 免费版 %&#125;</span><br><span class="line">  <span class="language-xml"><span class="tag">&lt;<span class="name">img</span> <span class="attr">src</span>=<span class="string">&#x27;https://cdn.jsdelivr.net/gh/fomalhaut1998/cdn-assets/qrcode/heartmate_lite.png&#x27;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml"><span class="tag">&lt;/<span class="name">a</span>&gt;</span></span></span><br><span class="line">&#123;% endbtns %&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-4"><p>1.如果需要显示类似「团队成员」之类的一组含有头像的链接</p><div class="btns circle grid5">            <a class="button" href='https://xaoxuu.com' title='xaoxuu'><img src='https://cdn.jsdelivr.net/gh/xaoxuu/cdn-assets/avatar/avatar.png'>xaoxuu</a><a class="button" href='https://xaoxuu.com' title='xaoxuu'><img src='https://cdn.jsdelivr.net/gh/xaoxuu/cdn-assets/avatar/avatar.png'>xaoxuu</a><a class="button" href='https://xaoxuu.com' title='xaoxuu'><img src='https://cdn.jsdelivr.net/gh/xaoxuu/cdn-assets/avatar/avatar.png'>xaoxuu</a><a class="button" href='https://xaoxuu.com' title='xaoxuu'><img src='https://cdn.jsdelivr.net/gh/xaoxuu/cdn-assets/avatar/avatar.png'>xaoxuu</a><a class="button" href='https://xaoxuu.com' title='xaoxuu'><img src='https://cdn.jsdelivr.net/gh/xaoxuu/cdn-assets/avatar/avatar.png'>xaoxuu</a>          </div>2.或者含有图标的按钮<div class="btns rounded grid5">            <a class="button" href='/' title='下载源码'><i class='fas fa-download'></i>下载源码</a><a class="button" href='/' title='查看文档'><i class='fas fa-book-open'></i>查看文档</a>          </div>3.圆形图标 + 标题 + 描述 + 图片 + 网格5列 + 居中<div class="btns circle center grid5">            <a href='https://apps.apple.com/cn/app/heart-mate-pro-hrm-utility/id1463348922?ls=1'>  <i class='fab fa-apple'></i>  <b>心率管家</b>  <p class='p red'>专业版</p>  <img src='https://cdn.jsdelivr.net/gh/fomalhaut1998/cdn-assets/qrcode/heartmate_pro.png'></a><a href='https://apps.apple.com/cn/app/heart-mate-lite-hrm-utility/id1475747930?ls=1'>  <i class='fab fa-apple'></i>  <b>心率管家</b>  <p class='p green'>免费版</p>  <img src='https://cdn.jsdelivr.net/gh/fomalhaut1998/cdn-assets/qrcode/heartmate_lite.png'></a>          </div><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h2 id="2-12-github卡片-ghcard">2.12 github卡片 ghcard</h2><div class="tabs" id="分栏"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#分栏-1">标签语法</button></li><li class="tab"><button type="button" data-href="#分栏-2">参数配置</button></li><li class="tab"><button type="button" data-href="#分栏-3">示例源码</button></li><li class="tab"><button type="button" data-href="#分栏-4">渲染演示</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="分栏-1"><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">&#123;% ghcard 用户名, 其它参数（可选） %&#125;</span><br><span class="line">&#123;% ghcard 用户名/仓库, 其它参数（可选） %&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-2"><p>使用<code>,</code>分割各个参数。写法为：<code>参数名=参数值</code><br>以下只写几个常用参数值。</p><table><thead><tr><th><strong>参数名</strong></th><th>取值</th><th>释义</th></tr></thead><tbody><tr><td>hide</td><td>stars,commits,prs,issues,contribs</td><td>隐藏指定统计</td></tr><tr><td>count_private</td><td>true</td><td>将私人项目贡献添加到总提交计数中</td></tr><tr><td>show_icons</td><td>true</td><td>显示图标</td></tr><tr><td>theme</td><td>查阅:<a href="https://github.com/anuraghazra/github-readme-stats/blob/master/themes/README.md">Available Themes</a></td><td>主题</td></tr></tbody></table><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-3"><p>1.用户信息卡片</p><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">| &#123;% ghcard fomalhaut1998 %&#125; | &#123;% ghcard fomalhaut1998, theme=vue %&#125; |</span><br><span class="line">| -- | -- |</span><br><span class="line">| &#123;% ghcard fomalhaut1998, theme=buefy %&#125; | &#123;% ghcard fomalhaut1998, theme=solarized-light %&#125; |</span><br><span class="line">| &#123;% ghcard fomalhaut1998, theme=onedark %&#125; | &#123;% ghcard fomalhaut1998, theme=solarized-dark %&#125; |</span><br><span class="line">| &#123;% ghcard fomalhaut1998, theme=algolia %&#125; | &#123;% ghcard fomalhaut1998, theme=calm %&#125; |</span><br></pre></td></tr></table></figure><p>2.仓库信息卡片</p><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">| &#123;% ghcard volantis-x/hexo-theme-volantis %&#125; | &#123;% ghcard volantis-x/hexo-theme-volantis, theme=vue %&#125; |</span><br><span class="line">| -- | -- |</span><br><span class="line">| &#123;% ghcard volantis-x/hexo-theme-volantis, theme=buefy %&#125; | &#123;% ghcard volantis-x/hexo-theme-volantis, theme=solarized-light %&#125; |</span><br><span class="line">| &#123;% ghcard volantis-x/hexo-theme-volantis, theme=onedark %&#125; | &#123;% ghcard volantis-x/hexo-theme-volantis, theme=solarized-dark %&#125; |</span><br><span class="line">| &#123;% ghcard volantis-x/hexo-theme-volantis, theme=algolia %&#125; | &#123;% ghcard volantis-x/hexo-theme-volantis, theme=calm %&#125; |</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-4"><p>1.用户信息卡片</p><table><thead><tr><th><a class="ghcard" rel="external nofollow noopener noreferrer" href="https://github.com/fomalhaut1998"><img src="https://github-readme-stats.vercel.app/api/?username=fomalhaut1998&show_owner=true"/></a></th><th><a class="ghcard" rel="external nofollow noopener noreferrer" href="https://github.com/fomalhaut1998"><img src="https://github-readme-stats.vercel.app/api/?username=fomalhaut1998&theme=vue&show_owner=true"/></a></th></tr></thead><tbody><tr><td><a class="ghcard" rel="external nofollow noopener noreferrer" href="https://github.com/fomalhaut1998"><img src="https://github-readme-stats.vercel.app/api/?username=fomalhaut1998&theme=buefy&show_owner=true"/></a></td><td><a class="ghcard" rel="external nofollow noopener noreferrer" href="https://github.com/fomalhaut1998"><img src="https://github-readme-stats.vercel.app/api/?username=fomalhaut1998&theme=solarized-light&show_owner=true"/></a></td></tr><tr><td><a class="ghcard" rel="external nofollow noopener noreferrer" href="https://github.com/fomalhaut1998"><img src="https://github-readme-stats.vercel.app/api/?username=fomalhaut1998&theme=onedark&show_owner=true"/></a></td><td><a class="ghcard" rel="external nofollow noopener noreferrer" href="https://github.com/fomalhaut1998"><img src="https://github-readme-stats.vercel.app/api/?username=fomalhaut1998&theme=solarized-dark&show_owner=true"/></a></td></tr><tr><td><a class="ghcard" rel="external nofollow noopener noreferrer" href="https://github.com/fomalhaut1998"><img src="https://github-readme-stats.vercel.app/api/?username=fomalhaut1998&theme=algolia&show_owner=true"/></a></td><td><a class="ghcard" rel="external nofollow noopener noreferrer" href="https://github.com/fomalhaut1998"><img src="https://github-readme-stats.vercel.app/api/?username=fomalhaut1998&theme=calm&show_owner=true"/></a></td></tr></tbody></table><p>2.仓库信息卡片</p><table><thead><tr><th><a class="ghcard" rel="external nofollow noopener noreferrer" href="https://github.com/volantis-x/hexo-theme-volantis"><img src="https://github-readme-stats.vercel.app/api/pin/?username=volantis-x&repo=hexo-theme-volantis&show_owner=true"/></a></th><th><a class="ghcard" rel="external nofollow noopener noreferrer" href="https://github.com/volantis-x/hexo-theme-volantis"><img src="https://github-readme-stats.vercel.app/api/pin/?username=volantis-x&repo=hexo-theme-volantis&theme=vue&show_owner=true"/></a></th></tr></thead><tbody><tr><td><a class="ghcard" rel="external nofollow noopener noreferrer" href="https://github.com/volantis-x/hexo-theme-volantis"><img src="https://github-readme-stats.vercel.app/api/pin/?username=volantis-x&repo=hexo-theme-volantis&theme=buefy&show_owner=true"/></a></td><td><a class="ghcard" rel="external nofollow noopener noreferrer" href="https://github.com/volantis-x/hexo-theme-volantis"><img src="https://github-readme-stats.vercel.app/api/pin/?username=volantis-x&repo=hexo-theme-volantis&theme=solarized-light&show_owner=true"/></a></td></tr><tr><td><a class="ghcard" rel="external nofollow noopener noreferrer" href="https://github.com/volantis-x/hexo-theme-volantis"><img src="https://github-readme-stats.vercel.app/api/pin/?username=volantis-x&repo=hexo-theme-volantis&theme=onedark&show_owner=true"/></a></td><td><a class="ghcard" rel="external nofollow noopener noreferrer" href="https://github.com/volantis-x/hexo-theme-volantis"><img src="https://github-readme-stats.vercel.app/api/pin/?username=volantis-x&repo=hexo-theme-volantis&theme=solarized-dark&show_owner=true"/></a></td></tr><tr><td><a class="ghcard" rel="external nofollow noopener noreferrer" href="https://github.com/volantis-x/hexo-theme-volantis"><img src="https://github-readme-stats.vercel.app/api/pin/?username=volantis-x&repo=hexo-theme-volantis&theme=algolia&show_owner=true"/></a></td><td><a class="ghcard" rel="external nofollow noopener noreferrer" href="https://github.com/volantis-x/hexo-theme-volantis"><img src="https://github-readme-stats.vercel.app/api/pin/?username=volantis-x&repo=hexo-theme-volantis&theme=calm&show_owner=true"/></a></td></tr></tbody></table><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h2 id="2-13-github徽标-ghbdage">2.13 github徽标 ghbdage</h2><div class="tabs" id="分栏"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#分栏-1">标签语法</button></li><li class="tab"><button type="button" data-href="#分栏-2">配置参数</button></li><li class="tab"><button type="button" data-href="#分栏-3">示例源码</button></li><li class="tab"><button type="button" data-href="#分栏-4">渲染演示</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="分栏-1"><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&#123;% bdage [right],[left],[logo]||[color],[link],[title]||[option] %&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-2"><ol><li><code>left</code>：徽标左边的信息，必选参数。</li><li><code>right</code>: 徽标右边的信息，必选参数，</li><li><code>logo</code>：徽标图标，图标名称详见<a href="https://simpleicons.org/">simpleicons</a>，可选参数。</li><li><code>color</code>：徽标右边的颜色，可选参数。</li><li><code>link</code>：指向的链接，可选参数。</li><li><code>title</code>：徽标的额外信息，可选参数。主要用于优化SEO，但<code>object</code>标签不会像<code>a</code>标签一样在鼠标悬停显示<code>title</code>信息。</li><li><code>option</code>：自定义参数，支持<a href="https://shields.io/">shields.io</a>的全部API参数支持，具体参数可以参看上文中的拓展写法示例。形式为<code>name1=value2&amp;name2=value2</code>。</li></ol><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-3"><p>1.基本参数,定义徽标左右文字和图标</p><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">&#123;% bdage Theme,Butterfly %&#125;</span><br><span class="line">&#123;% bdage Frame,Hexo,hexo %&#125;</span><br></pre></td></tr></table></figure><p>2.信息参数，定义徽标右侧内容背景色，指向链接</p><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">&#123;% bdage CDN,JsDelivr,jsDelivr||abcdef,https://metroui.org.ua/index.html,本站使用JsDelivr为静态资源提供CDN加速 %&#125;</span><br><span class="line">//如果是跨顺序省略可选参数，仍然需要写个逗号,用作分割</span><br><span class="line">&#123;% bdage Source,GitHub,GitHub||,https://github.com/ %&#125;</span><br></pre></td></tr></table></figure><p>3.拓展参数，支持shields的API的全部参数内容</p><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">&#123;% bdage Hosted,Vercel,Vercel||brightgreen,https://vercel.com/,本站采用双线部署，默认线路托管于Vercel||style=social&amp;logoWidth=20 %&#125;</span><br><span class="line">//如果是跨顺序省略可选参数组，仍然需要写双竖线||用作分割</span><br><span class="line">&#123;% bdage Hosted,Vercel,Vercel||||style=social&amp;logoWidth=20&amp;logoColor=violet %&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-4"><p>1.基本参数,定义徽标左右文字和图标</p><p><object class="ghbdage" style="margin-inline:5px" title="" standby="loading..." data="https://img.shields.io/badge/Butterfly-Theme-orange?logo=&color=orange&link=&"></object><br><object class="ghbdage" style="margin-inline:5px" title="" standby="loading..." data="https://img.shields.io/badge/Hexo-Frame-orange?logo=hexo&color=orange&link=&"></object></p><p>2.信息参数，定义徽标右侧内容背景色，指向链接</p><p><object class="ghbdage" style="margin-inline:5px" title="本站使用JsDelivr为静态资源提供CDN加速" standby="loading..." data="https://img.shields.io/badge/JsDelivr-CDN-orange?logo=jsDelivr&color=abcdef&link=https://metroui.org.ua/index.html&"></object><br>//如果是跨顺序省略可选参数，仍然需要写个逗号,用作分割<br><object class="ghbdage" style="margin-inline:5px" title="" standby="loading..." data="https://img.shields.io/badge/GitHub-Source-orange?logo=GitHub&color=orange&link=https://github.com/&"></object></p><p>3.拓展参数，支持shields的API的全部参数内容</p><p><object class="ghbdage" style="margin-inline:5px" title="本站采用双线部署，默认线路托管于Vercel" standby="loading..." data="https://img.shields.io/badge/Vercel-Hosted-orange?logo=Vercel&color=brightgreen&link=https://vercel.com/&style=social&logoWidth=20"></object><br>//如果是跨顺序省略可选参数组，仍然需要写双竖线||用作分割<br><object class="ghbdage" style="margin-inline:5px" title="" standby="loading..." data="https://img.shields.io/badge/Vercel-Hosted-orange?logo=Vercel&color=orange&link=&style=social&logoWidth=20&logoColor=violet"></object></p><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h2 id="2-14-网站卡片-sites">2.14 网站卡片 sites</h2><div class="tabs" id="分栏"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#分栏-1">标签语法</button></li><li class="tab"><button type="button" data-href="#分栏-2">示例源码</button></li><li class="tab"><button type="button" data-href="#分栏-3">渲染演示</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="分栏-1"><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">&#123;% sitegroup %&#125;</span><br><span class="line">&#123;% site 标题, url=链接, screenshot=截图链接, avatar=头像链接（可选）, description=描述（可选） %&#125;</span><br><span class="line">&#123;% site 标题, url=链接, screenshot=截图链接, avatar=头像链接（可选）, description=描述（可选） %&#125;</span><br><span class="line">&#123;% endsitegroup %&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-2"><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">&#123;% sitegroup %&#125;</span><br><span class="line">&#123;% site xaoxuu, url=https://xaoxuu.com, screenshot=https://i.loli.net/2020/08/21/VuSwWZ1xAeUHEBC.jpg, avatar=https://cdn.jsdelivr.net/gh/fomalhaut1998/cdn-assets/avatar/avatar.png, description=简约风格 %&#125;</span><br><span class="line">&#123;% site inkss, url=https://inkss.cn, screenshot=https://i.loli.net/2020/08/21/Vzbu3i8fXs6Nh5Y.jpg, avatar=https://cdn.jsdelivr.net/gh/inkss/common@master/static/web/avatar.jpg, description=这是一段关于这个网站的描述文字 %&#125;</span><br><span class="line">&#123;% site MHuiG, url=https://blog.mhuig.top, screenshot=https://i.loli.net/2020/08/22/d24zpPlhLYWX6D1.png, avatar=https://cdn.jsdelivr.net/gh/MHuiG/imgbed@master/data/p.png, description=这是一段关于这个网站的描述文字 %&#125;</span><br><span class="line">&#123;% site Colsrch, url=https://colsrch.top, screenshot=https://i.loli.net/2020/08/22/dFRWXm52OVu8qfK.png, avatar=https://cdn.jsdelivr.net/gh/Colsrch/images/Colsrch/avatar.jpg, description=这是一段关于这个网站的描述文字 %&#125;</span><br><span class="line">&#123;% site Linhk1606, url=https://linhk1606.github.io, screenshot=https://i.loli.net/2020/08/21/3PmGLCKicnfow1x.png, avatar=https://i.loli.net/2020/02/09/PN7I5RJfFtA93r2.png, description=这是一段关于这个网站的描述文字 %&#125;</span><br><span class="line">&#123;% endsitegroup %&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-3"><div class="site-card-group"><a class="site-card" href="https://fomalhaut1998.com"><div class="img"><img src="https://i.loli.net/2020/08/21/VuSwWZ1xAeUHEBC.jpg"/></div><div class="info"><img src="https://cdn.jsdelivr.net/gh/fomalhaut1998/cdn-assets/avatar/avatar.png"/><span class="title">fomalhaut1998</span><span class="desc">简约风格</span></div></a><a class="site-card" href="https://inkss.cn"><div class="img"><img src="https://i.loli.net/2020/08/21/Vzbu3i8fXs6Nh5Y.jpg"/></div><div class="info"><img src="https://cdn.jsdelivr.net/gh/inkss/common@master/static/web/avatar.jpg"/><span class="title">inkss</span><span class="desc">这是一段关于这个网站的描述文字</span></div></a><a class="site-card" href="https://blog.mhuig.top"><div class="img"><img src="https://i.loli.net/2020/08/22/d24zpPlhLYWX6D1.png"/></div><div class="info"><img src="https://cdn.jsdelivr.net/gh/MHuiG/imgbed@master/data/p.png"/><span class="title">MHuiG</span><span class="desc">这是一段关于这个网站的描述文字</span></div></a><a class="site-card" href="https://colsrch.top"><div class="img"><img src="https://i.loli.net/2020/08/22/dFRWXm52OVu8qfK.png"/></div><div class="info"><img src="https://cdn.jsdelivr.net/gh/Colsrch/images/Colsrch/avatar.jpg"/><span class="title">Colsrch</span><span class="desc">这是一段关于这个网站的描述文字</span></div></a><a class="site-card" href="https://linhk1606.github.io"><div class="img"><img src="https://i.loli.net/2020/08/21/3PmGLCKicnfow1x.png"/></div><div class="info"><img src="https://i.loli.net/2020/02/09/PN7I5RJfFtA93r2.png"/><span class="title">Linhk1606</span><span class="desc">这是一段关于这个网站的描述文字</span></div></a></div><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h2 id="2-15-行内图片-inlineimage">2.15 行内图片 inlineimage</h2><div class="tabs" id="分栏"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#分栏-1">标签语法</button></li><li class="tab"><button type="button" data-href="#分栏-2">参数配置</button></li><li class="tab"><button type="button" data-href="#分栏-3">示例源码</button></li><li class="tab"><button type="button" data-href="#分栏-4">渲染演示</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="分栏-1"><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&#123;% inlineimage 图片链接, height=高度（可选） %&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-2"><ol><li><code>高度</code>：height=20px</li></ol><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-3"><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">这是 &#123;% inlineimage https://cdn.jsdelivr.net/gh/volantis-x/cdn-emoji/aru-l/0000.gif %&#125; 一段话。</span><br><span class="line"></span><br><span class="line">这又是 &#123;% inlineimage https://cdn.jsdelivr.net/gh/volantis-x/cdn-emoji/aru-l/5150.gif, height=40px %&#125; 一段话。</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-4"><p>这是 <img no-lazy class="inline" src="https://cdn.jsdelivr.net/gh/volantis-x/cdn-emoji/aru-l/0000.gif" style="height:1.5em"/> 一段话。</p><p>这又是 <img no-lazy class="inline" src="https://cdn.jsdelivr.net/gh/volantis-x/cdn-emoji/aru-l/5150.gif" style="height:40px;"/> 一段话。</p><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h2 id="2-16-单张图片-image">2.16 单张图片 image</h2><div class="tabs" id="分栏"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#分栏-1">标签语法</button></li><li class="tab"><button type="button" data-href="#分栏-2">参数配置</button></li><li class="tab"><button type="button" data-href="#分栏-3">示例源码</button></li><li class="tab"><button type="button" data-href="#分栏-4">渲染演示</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="分栏-1"><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&#123;% image 链接, width=宽度（可选）, height=高度（可选）, alt=描述（可选）, bg=占位颜色（可选） %&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-2"><ol><li>图片宽度高度：width=300px, height=32px</li><li>图片描述：alt=图片描述（butterfly需要在主题配置文件中开启图片描述）</li><li>占位背景色：bg=#f2f2f2</li></ol><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-3"><p>1.添加描述：</p><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&#123;% image https://cdn.jsdelivr.net/gh/volantis-x/cdn-wallpaper-minimalist/2020/025.jpg, alt=每天下课回宿舍的路，没有什么故事。 %&#125;</span><br></pre></td></tr></table></figure><p>2.指定宽度</p><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&#123;% image https://cdn.jsdelivr.net/gh/volantis-x/cdn-wallpaper-minimalist/2020/025.jpg, width=400px %&#125;</span><br></pre></td></tr></table></figure><p>3.指定宽度并添加描述：</p><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&#123;% image https://cdn.jsdelivr.net/gh/volantis-x/cdn-wallpaper-minimalist/2020/025.jpg, width=400px, alt=每天下课回宿舍的路，没有什么故事。 %&#125;</span><br></pre></td></tr></table></figure><p>4.设置占位背景色：</p><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&#123;% image https://cdn.jsdelivr.net/gh/volantis-x/cdn-wallpaper-minimalist/2020/025.jpg, width=400px, bg=#1D0C04, alt=优化不同宽度浏览的观感 %&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-4"><p>1.添加描述：</p><div class="img-wrap"><div class="img-bg"><img class="img" src="https://cdn.jsdelivr.net/gh/volantis-x/cdn-wallpaper-minimalist/2020/025.jpg" alt="每天下课回宿舍的路，没有什么故事。"/></div><span class="image-caption">每天下课回宿舍的路，没有什么故事。</span></div>2..指定宽度<div class="img-wrap"><div class="img-bg"><img class="img" src="https://cdn.jsdelivr.net/gh/volantis-x/cdn-wallpaper-minimalist/2020/025.jpg" style="width:400px;"/></div></div>3.指定宽度并添加描述：<div class="img-wrap"><div class="img-bg"><img class="img" src="https://cdn.jsdelivr.net/gh/volantis-x/cdn-wallpaper-minimalist/2020/025.jpg" alt="每天下课回宿舍的路，没有什么故事。" style="width:400px;"/></div><span class="image-caption">每天下课回宿舍的路，没有什么故事。</span></div>4.设置占位背景色：<div class="img-wrap"><div class="img-bg" style="background:#1D0C04"><img class="img" src="https://cdn.jsdelivr.net/gh/volantis-x/cdn-wallpaper-minimalist/2020/025.jpg" alt="优化不同宽度浏览的观感" style="width:400px;"/></div><span class="image-caption">优化不同宽度浏览的观感</span></div><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h2 id="2-17-音频-audio">2.17 音频 audio</h2><div class="tabs" id="分栏"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#分栏-1">标签语法</button></li><li class="tab"><button type="button" data-href="#分栏-2">示例源码</button></li><li class="tab"><button type="button" data-href="#分栏-3">渲染演示</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="分栏-1"><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&#123;% audio 音频链接 %&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-2"><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&#123;% audio https://github.com/volantis-x/volantis-docs/releases/download/assets/Lumia1020.mp3 %&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-3"><div class="audio"><audio controls preload><source src='https://github.com/volantis-x/volantis-docs/releases/download/assets/Lumia1020.mp3' type='audio/mp3'>Your browser does not support the audio tag.</audio></div><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h2 id="2-18-视频-video">2.18 视频 video</h2><div class="tabs" id="分栏"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#分栏-1">标签语法</button></li><li class="tab"><button type="button" data-href="#分栏-2">标签语法</button></li><li class="tab"><button type="button" data-href="#分栏-3">示例源码</button></li><li class="tab"><button type="button" data-href="#分栏-4">渲染演示</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="分栏-1"><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&#123;% video 视频链接 %&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-2"><ol><li><code>对齐方向</code>：left, center, right</li><li><code>列数</code>：逗号后面直接写列数，支持 1 ～ 4 列。</li></ol><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-3"><p>1.100%宽度</p><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&#123;% video https://github.com/volantis-x/volantis-docs/releases/download/assets/IMG<span class="emphasis">_0341.mov %&#125;</span></span><br></pre></td></tr></table></figure><p>2.50%宽度</p><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">&#123;% videos, 2 %&#125;</span><br><span class="line">&#123;% video https://github.com/volantis-x/volantis-docs/releases/download/assets/IMG<span class="emphasis">_0341.mov %&#125;</span></span><br><span class="line"><span class="emphasis">&#123;% video https://github.com/volantis-x/volantis-docs/releases/download/assets/IMG_</span>0341.mov %&#125;</span><br><span class="line">&#123;% video https://github.com/volantis-x/volantis-docs/releases/download/assets/IMG<span class="emphasis">_0341.mov %&#125;</span></span><br><span class="line"><span class="emphasis">&#123;% video https://github.com/volantis-x/volantis-docs/releases/download/assets/IMG_</span>0341.mov %&#125;</span><br><span class="line">&#123;% endvideos %&#125;</span><br></pre></td></tr></table></figure><p>3.25%宽度</p><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">&#123;% videos, 4 %&#125;</span><br><span class="line">&#123;% video https://github.com/volantis-x/volantis-docs/releases/download/assets/IMG<span class="emphasis">_0341.mov %&#125;</span></span><br><span class="line"><span class="emphasis">&#123;% video https://github.com/volantis-x/volantis-docs/releases/download/assets/IMG_</span>0341.mov %&#125;</span><br><span class="line">&#123;% video https://github.com/volantis-x/volantis-docs/releases/download/assets/IMG<span class="emphasis">_0341.mov %&#125;</span></span><br><span class="line"><span class="emphasis">&#123;% video https://github.com/volantis-x/volantis-docs/releases/download/assets/IMG_</span>0341.mov %&#125;</span><br><span class="line">&#123;% video https://github.com/volantis-x/volantis-docs/releases/download/assets/IMG<span class="emphasis">_0341.mov %&#125;</span></span><br><span class="line"><span class="emphasis">&#123;% video https://github.com/volantis-x/volantis-docs/releases/download/assets/IMG_</span>0341.mov %&#125;</span><br><span class="line">&#123;% video https://github.com/volantis-x/volantis-docs/releases/download/assets/IMG<span class="emphasis">_0341.mov %&#125;</span></span><br><span class="line"><span class="emphasis">&#123;% video https://github.com/volantis-x/volantis-docs/releases/download/assets/IMG_</span>0341.mov %&#125;</span><br><span class="line">&#123;% endvideos %&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-4"><p>1.100%宽度</p><div class="video"><video controls preload><source src='https://github.com/volantis-x/volantis-docs/releases/download/assets/IMG_0341.mov' type='video/mp4'>Your browser does not support the video tag.</video></div>2.50%宽度<div class="videos" col='2'><div class="video"><video controls preload><source src='https://github.com/volantis-x/volantis-docs/releases/download/assets/IMG_0341.mov' type='video/mp4'>Your browser does not support the video tag.</video></div><div class="video"><video controls preload><source src='https://github.com/volantis-x/volantis-docs/releases/download/assets/IMG_0341.mov' type='video/mp4'>Your browser does not support the video tag.</video></div><div class="video"><video controls preload><source src='https://github.com/volantis-x/volantis-docs/releases/download/assets/IMG_0341.mov' type='video/mp4'>Your browser does not support the video tag.</video></div><div class="video"><video controls preload><source src='https://github.com/volantis-x/volantis-docs/releases/download/assets/IMG_0341.mov' type='video/mp4'>Your browser does not support the video tag.</video></div></div>3.25%宽度<div class="videos" col='4'><div class="video"><video controls preload><source src='https://github.com/volantis-x/volantis-docs/releases/download/assets/IMG_0341.mov' type='video/mp4'>Your browser does not support the video tag.</video></div><div class="video"><video controls preload><source src='https://github.com/volantis-x/volantis-docs/releases/download/assets/IMG_0341.mov' type='video/mp4'>Your browser does not support the video tag.</video></div><div class="video"><video controls preload><source src='https://github.com/volantis-x/volantis-docs/releases/download/assets/IMG_0341.mov' type='video/mp4'>Your browser does not support the video tag.</video></div><div class="video"><video controls preload><source src='https://github.com/volantis-x/volantis-docs/releases/download/assets/IMG_0341.mov' type='video/mp4'>Your browser does not support the video tag.</video></div><div class="video"><video controls preload><source src='https://github.com/volantis-x/volantis-docs/releases/download/assets/IMG_0341.mov' type='video/mp4'>Your browser does not support the video tag.</video></div><div class="video"><video controls preload><source src='https://github.com/volantis-x/volantis-docs/releases/download/assets/IMG_0341.mov' type='video/mp4'>Your browser does not support the video tag.</video></div><div class="video"><video controls preload><source src='https://github.com/volantis-x/volantis-docs/releases/download/assets/IMG_0341.mov' type='video/mp4'>Your browser does not support the video tag.</video></div><div class="video"><video controls preload><source src='https://github.com/volantis-x/volantis-docs/releases/download/assets/IMG_0341.mov' type='video/mp4'>Your browser does not support the video tag.</video></div></div><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h2 id="2-19-相册-gallery">2.19 相册 gallery</h2><div class="tabs" id="分栏"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#分栏-1">标签语法</button></li><li class="tab"><button type="button" data-href="#分栏-2">参数配置</button></li><li class="tab"><button type="button" data-href="#分栏-3">示例源码</button></li><li class="tab"><button type="button" data-href="#分栏-4">渲染演示</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="分栏-1"><p>1.gallerygroup 相册图库</p><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="language-xml"><span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">&quot;gallery-group-main&quot;</span>&gt;</span></span></span><br><span class="line">&#123;% galleryGroup name description link img-url %&#125;</span><br><span class="line">&#123;% galleryGroup name description link img-url %&#125;</span><br><span class="line">&#123;% galleryGroup name description link img-url %&#125;</span><br><span class="line"><span class="language-xml"><span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br></pre></td></tr></table></figure><p>2.gallery 相册</p><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">&#123;% gallery %&#125;</span><br><span class="line">markdown 圖片格式</span><br><span class="line">&#123;% endgallery %&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-2"><ul><li>gallerygroup 相册图库</li></ul><table><thead><tr><th>参数名</th><th>释义</th></tr></thead><tbody><tr><td>name</td><td>图库名字</td></tr><tr><td>description</td><td>图库描述</td></tr><tr><td>link</td><td>链接到对应相册的地址</td></tr><tr><td>img-url</td><td>图库封面</td></tr></tbody></table><ul><li><p>gallery 相册</p><p>区别于旧版的Gallery相册,新的Gallery相册会自动根据图片长度进行排版，书写也更加方便，与markdown格式一样。可根据需要插入到相应的md。无需再自己配置长宽。<strong>建议在粘贴时故意使用长短、大小、横竖不一的图片</strong>，会有更好的效果。（尺寸完全相同的图片只会平铺输出，效果很糟糕）</p></li></ul><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-3"><p>1.gallerygroup 相册图库</p><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="language-xml"><span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">&quot;gallery-group-main&quot;</span>&gt;</span></span></span><br><span class="line">&#123;% galleryGroup MC 在Rikkaの六花服务器里留下的足迹 &#x27;/gallery/MC/&#x27; https://cdn.cbd.int/akilar-candyassets@1.0.36/image/1.jpg %&#125;</span><br><span class="line">&#123;% galleryGroup Gundam 哦咧哇gundam哒！ &#x27;/gallery/Gundam/&#x27; https://cdn.cbd.int/akilar-candyassets@1.0.36/image/20200907110508327.png %&#125;</span><br><span class="line">&#123;% galleryGroup I-am-Akilar 某种意义上也算自拍吧 &#x27;/gallery/I-am-Akilar/&#x27; https://cdn.cbd.int/akilar-candyassets@1.0.36/image/20200907113116651.png %&#125;</span><br><span class="line"><span class="language-xml"><span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br></pre></td></tr></table></figure><p>2.gallery 相册</p><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">&#123;% gallery %&#125;</span><br><span class="line">![](<span class="link">https://i.loli.net/2019/12/25/Fze9jchtnyJXMHN.jpg</span>)</span><br><span class="line">![](<span class="link">https://i.loli.net/2019/12/25/ryLVePaqkYm4TEK.jpg</span>)</span><br><span class="line">&#123;% endgallery %&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-4"><p>1.gallerygroup 相册图库</p><div class="gallery-group-main">  <figure class="gallery-group">  <img class="gallery-group-img no-lightbox" src='https://cdn.cbd.int/akilar-candyassets@1.0.36/image/1.jpg' alt="Group Image Gallery">  <figcaption>  <div class="gallery-group-name">MC</div>  <p>在Rikkaの六花服务器里留下的足迹</p>  <a href='/gallery/MC/'></a>  </figcaption>  </figure>  <figure class="gallery-group">  <img class="gallery-group-img no-lightbox" src='https://cdn.cbd.int/akilar-candyassets@1.0.36/image/20200907110508327.png' alt="Group Image Gallery">  <figcaption>  <div class="gallery-group-name">Gundam</div>  <p>哦咧哇gundam哒！</p>  <a href='/gallery/Gundam/'></a>  </figcaption>  </figure>  <figure class="gallery-group">  <img class="gallery-group-img no-lightbox" src='https://cdn.cbd.int/akilar-candyassets@1.0.36/image/20200907113116651.png' alt="Group Image Gallery">  <figcaption>  <div class="gallery-group-name">I-am-Akilar</div>  <p>某种意义上也算自拍吧</p>  <a href='/gallery/I-am-Akilar/'></a>  </figcaption>  </figure></div>2.gallery 相册<div class="fj-gallery"><p><img src="https://i.loli.net/2019/12/25/Fze9jchtnyJXMHN.jpg" alt=""><br><img src="https://i.loli.net/2019/12/25/ryLVePaqkYm4TEK.jpg" alt=""></p>          </div><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h2 id="2-20-折叠框-folding">2.20 折叠框 folding</h2><div class="tabs" id="分栏"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#分栏-1">标签语法</button></li><li class="tab"><button type="button" data-href="#分栏-2">示例源码</button></li><li class="tab"><button type="button" data-href="#分栏-3">渲染演示</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="分栏-1"><p>1.gallerygroup 相册图库</p><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">&#123;% folding 参数（可选）, 标题 %&#125;</span><br><span class="line">![](<span class="link">https://cdn.jsdelivr.net/gh/volantis-x/cdn-wallpaper/abstract/41F215B9-261F-48B4-80B5-4E86E165259E.jpeg</span>)</span><br><span class="line">&#123;% endfolding %&#125;</span><br></pre></td></tr></table></figure><!-- tab 参数配置 --><ol><li><p><code>颜色</code>：blue, cyan, green, yellow, red</p></li><li><p><code>状态</code>：状态填写 open 代表默认打开。</p></li></ol><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-2"><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line">&#123;% folding 查看图片测试 %&#125;</span><br><span class="line"></span><br><span class="line">![](<span class="link">https://cdn.jsdelivr.net/gh/volantis-x/cdn-wallpaper/abstract/41F215B9-261F-48B4-80B5-4E86E165259E.jpeg</span>)</span><br><span class="line"></span><br><span class="line">&#123;% endfolding %&#125;</span><br><span class="line"></span><br><span class="line">&#123;% folding cyan open, 查看默认打开的折叠框 %&#125;</span><br><span class="line"></span><br><span class="line">这是一个默认打开的折叠框。</span><br><span class="line"></span><br><span class="line">&#123;% endfolding %&#125;</span><br><span class="line"></span><br><span class="line">&#123;% folding green, 查看代码测试 %&#125;</span><br><span class="line">假装这里有代码块（代码块没法嵌套代码块）</span><br><span class="line">&#123;% endfolding %&#125;</span><br><span class="line"></span><br><span class="line">&#123;% folding yellow, 查看列表测试 %&#125;</span><br><span class="line"></span><br><span class="line"><span class="bullet">-</span> haha</span><br><span class="line"><span class="bullet">-</span> hehe</span><br><span class="line"></span><br><span class="line">&#123;% endfolding %&#125;</span><br><span class="line"></span><br><span class="line">&#123;% folding red, 查看嵌套测试 %&#125;</span><br><span class="line"></span><br><span class="line">&#123;% folding blue, 查看嵌套测试2 %&#125;</span><br><span class="line"></span><br><span class="line">&#123;% folding 查看嵌套测试3 %&#125;</span><br><span class="line"></span><br><span class="line">hahaha <span class="language-xml"><span class="tag">&lt;<span class="name">span</span>&gt;</span></span><span class="language-xml"><span class="tag">&lt;<span class="name">img</span> <span class="attr">src</span>=<span class="string">&#x27;https://cdn.jsdelivr.net/gh/volantis-x/cdn-emoji/tieba/%E6%BB%91%E7%A8%BD.png&#x27;</span> <span class="attr">style</span>=<span class="string">&#x27;height:24px&#x27;</span>&gt;</span></span><span class="language-xml"><span class="tag">&lt;/<span class="name">span</span>&gt;</span></span></span><br><span class="line"></span><br><span class="line">&#123;% endfolding %&#125;</span><br><span class="line"></span><br><span class="line">&#123;% endfolding %&#125;</span><br><span class="line"></span><br><span class="line">&#123;% endfolding %&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-3"><details class="folding-tag" ><summary> 查看图片测试 </summary>              <div class='content'>              <p><img src="https://cdn.jsdelivr.net/gh/volantis-x/cdn-wallpaper/abstract/41F215B9-261F-48B4-80B5-4E86E165259E.jpeg" alt=""></p>              </div>            </details><details class="folding-tag" cyan open><summary> 查看默认打开的折叠框 </summary>              <div class='content'>              <p>这是一个默认打开的折叠框。</p>              </div>            </details><details class="folding-tag" green><summary> 查看代码测试 </summary>              <div class='content'>              <p>假装这里有代码块（代码块没法嵌套代码块）</p>              </div>            </details><details class="folding-tag" yellow><summary> 查看列表测试 </summary>              <div class='content'>              <ul><li>haha</li><li>hehe</li></ul>              </div>            </details><details class="folding-tag" red><summary> 查看嵌套测试 </summary>              <div class='content'>              <details class="folding-tag" blue><summary> 查看嵌套测试2 </summary>              <div class='content'>              <details class="folding-tag" ><summary> 查看嵌套测试3 </summary>              <div class='content'>              <p>hahaha <span><img src='https://cdn.jsdelivr.net/gh/volantis-x/cdn-emoji/tieba/%E6%BB%91%E7%A8%BD.png' style='height:24px'></span></p>              </div>            </details>              </div>            </details>              </div>            </details><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h2 id="2-21-分栏-tab">2.21 分栏 tab</h2><div class="tabs" id="分栏"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#分栏-1">标签语法</button></li><li class="tab"><button type="button" data-href="#分栏-2">配置参数</button></li><li class="tab"><button type="button" data-href="#分栏-3">示例源码</button></li><li class="tab"><button type="button" data-href="#分栏-4">渲染演示</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="分栏-1"><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">&#123;% tabs Unique name, [index] %&#125;</span><br><span class="line">&lt;!-- tab [Tab caption] [@icon] --&gt;</span><br><span class="line">Any content (support inline tags too).</span><br><span class="line">&lt;!-- endtab --&gt;</span><br><span class="line">&#123;% endtabs %&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-2"><ol><li><p>Unique name :</p><ul><li><p>选项卡块标签的唯一名称，不带逗号。</p></li><li><p>将在#id中用作每个标签及其索引号的前缀。</p></li><li><p>如果名称中包含空格，则对于生成#id，所有空格将由破折号代替。</p></li><li><p>仅当前帖子/页面的URL必须是唯一的！</p></li></ul></li><li><p>[index]:</p><ul><li><p>活动选项卡的索引号。</p></li><li><p>如果未指定，将选择第一个标签（1）。</p></li><li><p>如果index为-1，则不会选择任何选项卡。</p></li><li><p>可选参数。</p></li></ul></li><li><p>[Tab caption]:</p><ul><li><p>当前选项卡的标题。</p></li><li><p>如果未指定标题，则带有制表符索引后缀的唯一名称将用作制表符的标题。</p></li><li><p>如果未指定标题，但指定了图标，则标题将为空。</p></li><li><p>可选参数。</p></li></ul></li><li><p>[@icon]:</p><ul><li><p>FontAwesome图标名称（全名，看起来像“ fas fa-font”）</p></li><li><p>可以指定带空格或不带空格；</p></li><li><p>例如’Tab caption @icon’ 和 ‘Tab caption@icon’.</p></li><li><p>可选参数。</p></li></ul></li></ol><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-3"><p>1.Demo 1 - 预设选择第一个【默认】</p><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">&#123;% tabs test1 %&#125;</span><br><span class="line">&lt;!-- tab --&gt;</span><br><span class="line"><span class="strong">**This is Tab 1.**</span></span><br><span class="line">&lt;!-- endtab --&gt;</span><br><span class="line"></span><br><span class="line">&lt;!-- tab --&gt;</span><br><span class="line"><span class="strong">**This is Tab 2.**</span></span><br><span class="line">&lt;!-- endtab --&gt;</span><br><span class="line"></span><br><span class="line">&lt;!-- tab --&gt;</span><br><span class="line"><span class="strong">**This is Tab 3.**</span></span><br><span class="line">&lt;!-- endtab --&gt;</span><br><span class="line">&#123;% endtabs %&#125;</span><br></pre></td></tr></table></figure><p>2.Demo 2 - 预设选择tabs</p><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">&#123;% tabs test2, 3 %&#125;</span><br><span class="line">&lt;!-- tab --&gt;</span><br><span class="line"><span class="strong">**This is Tab 1.**</span></span><br><span class="line">&lt;!-- endtab --&gt;</span><br><span class="line"></span><br><span class="line">&lt;!-- tab --&gt;</span><br><span class="line"><span class="strong">**This is Tab 2.**</span></span><br><span class="line">&lt;!-- endtab --&gt;</span><br><span class="line"></span><br><span class="line">&lt;!-- tab --&gt;</span><br><span class="line"><span class="strong">**This is Tab 3.**</span></span><br><span class="line">&lt;!-- endtab --&gt;</span><br><span class="line">&#123;% endtabs %&#125;</span><br></pre></td></tr></table></figure><p>3.Demo 3 - 没有预设值</p><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">&#123;% tabs test3, -1 %&#125;</span><br><span class="line">&lt;!-- tab --&gt;</span><br><span class="line"><span class="strong">**This is Tab 1.**</span></span><br><span class="line">&lt;!-- endtab --&gt;</span><br><span class="line"></span><br><span class="line">&lt;!-- tab --&gt;</span><br><span class="line"><span class="strong">**This is Tab 2.**</span></span><br><span class="line">&lt;!-- endtab --&gt;</span><br><span class="line"></span><br><span class="line">&lt;!-- tab --&gt;</span><br><span class="line"><span class="strong">**This is Tab 3.**</span></span><br><span class="line">&lt;!-- endtab --&gt;</span><br><span class="line">&#123;% endtabs %&#125;</span><br></pre></td></tr></table></figure><p>4.Demo 4 - 自定义Tab名 + 只有icon + icon和Tab名</p><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">&#123;% tabs test4 %&#125;</span><br><span class="line">&lt;!-- tab 第一个Tab --&gt;</span><br><span class="line"><span class="strong">**tab名字为第一个Tab**</span></span><br><span class="line">&lt;!-- endtab --&gt;</span><br><span class="line"></span><br><span class="line">&lt;!-- tab @fab fa-apple-pay --&gt;</span><br><span class="line"><span class="strong">**只有图标 没有Tab名字**</span></span><br><span class="line">&lt;!-- endtab --&gt;</span><br><span class="line"></span><br><span class="line">&lt;!-- tab 炸弹@fas fa-bomb --&gt;</span><br><span class="line"><span class="strong">**名字+icon**</span></span><br><span class="line">&lt;!-- endtab --&gt;</span><br><span class="line">&#123;% endtabs %&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-4"><p>1.Demo 1 - 预设选择第一个【默认】</p><div class="tabs" id="test1"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#test1-1">test1 1</button></li><li class="tab"><button type="button" data-href="#test1-2">test1 2</button></li><li class="tab"><button type="button" data-href="#test1-3">test1 3</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="test1-1"><p><strong>This is Tab 1.</strong></p><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="test1-2"><p><strong>This is Tab 2.</strong></p><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="test1-3"><p><strong>This is Tab 3.</strong></p><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><p>2.Demo 2 - 预设选择tabs</p><div class="tabs" id="test2"><ul class="nav-tabs"><li class="tab"><button type="button" data-href="#test2-1">test2 1</button></li><li class="tab"><button type="button" data-href="#test2-2">test2 2</button></li><li class="tab active"><button type="button" data-href="#test2-3">test2 3</button></li></ul><div class="tab-contents"><div class="tab-item-content" id="test2-1"><p><strong>This is Tab 1.</strong></p><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="test2-2"><p><strong>This is Tab 2.</strong></p><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content active" id="test2-3"><p><strong>This is Tab 3.</strong></p><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><p>3.Demo 3 - 没有预设值</p><div class="tabs" id="test3"><ul class="nav-tabs"><li class="tab"><button type="button" data-href="#test3-1">test3 1</button></li><li class="tab"><button type="button" data-href="#test3-2">test3 2</button></li><li class="tab"><button type="button" data-href="#test3-3">test3 3</button></li></ul><div class="tab-contents"><div class="tab-item-content" id="test3-1"><p><strong>This is Tab 1.</strong></p><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="test3-2"><p><strong>This is Tab 2.</strong></p><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="test3-3"><p><strong>This is Tab 3.</strong></p><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><p>4.Demo 4 - 自定义Tab名 + 只有icon + icon和Tab名</p><div class="tabs" id="test4"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#test4-1">第一个Tab</button></li><li class="tab"><button type="button" data-href="#test4-2"><i class="fab fa-apple-pay" style="text-align: center;"></i></button></li><li class="tab"><button type="button" data-href="#test4-3"><i class="fas fa-bomb"></i>炸弹</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="test4-1"><p><strong>tab名字为第一个Tab</strong></p><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="test4-2"><p><strong>只有图标 没有Tab名字</strong></p><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="test4-3"><p><strong>名字+icon</strong></p><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h2 id="2-22-诗词标签-poem">2.22 诗词标签 poem</h2><div class="tabs" id="分栏"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#分栏-1">参数配置</button></li><li class="tab"><button type="button" data-href="#分栏-2">示例源码</button></li><li class="tab"><button type="button" data-href="#分栏-3">渲染演示</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="分栏-1"><ol><li><code>title</code>：诗词标题</li><li><code>author</code>：作者，可以不写</li></ol><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-2"><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">&#123;% poem 水调歌头,苏轼 %&#125;</span><br><span class="line">丙辰中秋，欢饮达旦，大醉，作此篇，兼怀子由。</span><br><span class="line">明月几时有？把酒问青天。</span><br><span class="line">不知天上宫阙，今夕是何年？</span><br><span class="line">我欲乘风归去，又恐琼楼玉宇，高处不胜寒。</span><br><span class="line">起舞弄清影，何似在人间？</span><br><span class="line"></span><br><span class="line">转朱阁，低绮户，照无眠。</span><br><span class="line">不应有恨，何事长向别时圆？</span><br><span class="line">人有悲欢离合，月有阴晴圆缺，此事古难全。</span><br><span class="line">但愿人长久，千里共婵娟。</span><br><span class="line">&#123;% endpoem %&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-3"><div class='poem'><div class='poem-title'>水调歌头</div><div class='poem-author'>苏轼</div><p>丙辰中秋，欢饮达旦，大醉，作此篇，兼怀子由。<br>明月几时有？把酒问青天。<br>不知天上宫阙，今夕是何年？<br>我欲乘风归去，又恐琼楼玉宇，高处不胜寒。<br>起舞弄清影，何似在人间？</p><p>转朱阁，低绮户，照无眠。<br>不应有恨，何事长向别时圆？<br>人有悲欢离合，月有阴晴圆缺，此事古难全。<br>但愿人长久，千里共婵娟。</p></div><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h2 id="2-23-阿里图标-icon">2.23 阿里图标 icon</h2><div class="tabs" id="分栏"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#分栏-1">标签语法</button></li><li class="tab"><button type="button" data-href="#分栏-2">参数配置</button></li><li class="tab"><button type="button" data-href="#分栏-3">示例源码</button></li><li class="tab"><button type="button" data-href="#分栏-4">渲染演示</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="分栏-1"><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&#123;% icon [icon-xxxx],[font-size] %&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-2"><ol><li><code>icon-xxxx</code>：表示图标<code>font-class</code>,可以在自己的阿里矢量图标库项目的<code>font-class</code>引用方案内查询并复制。</li><li><code>font-size</code>：表示图标大小，直接填写数字即可，单位为<code>em</code>。图标大小默认值为<code>1em</code>。</li></ol><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-3"><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line">&#123;% icon icon-rat<span class="emphasis">_zi %&#125;&#123;% icon icon-rat,2 %&#125;</span></span><br><span class="line"><span class="emphasis"></span></span><br><span class="line"><span class="emphasis">&#123;% icon icon-ox_</span>chou,3 %&#125;&#123;% icon icon-ox,4 %&#125;</span><br><span class="line"></span><br><span class="line">&#123;% icon icon-tiger<span class="emphasis">_yin,5 %&#125;&#123;% icon icon-tiger,6 %&#125;</span></span><br><span class="line"><span class="emphasis"></span></span><br><span class="line"><span class="emphasis">&#123;% icon icon-rabbit_</span>mao,1 %&#125;&#123;% icon icon-rabbit,2 %&#125;</span><br><span class="line"></span><br><span class="line">&#123;% icon icon-dragon<span class="emphasis">_chen,3 %&#125;&#123;% icon icon-dragon,4 %&#125;</span></span><br><span class="line"><span class="emphasis"></span></span><br><span class="line"><span class="emphasis">&#123;% icon icon-snake_</span>si,5 %&#125;&#123;% icon icon-snake,6 %&#125;</span><br><span class="line"></span><br><span class="line">&#123;% icon icon-horse<span class="emphasis">_wu %&#125;&#123;% icon icon-horse,2 %&#125;</span></span><br><span class="line"><span class="emphasis"></span></span><br><span class="line"><span class="emphasis">&#123;% icon icon-goat_</span>wei,3 %&#125;&#123;% icon icon-goat,4 %&#125;</span><br><span class="line"></span><br><span class="line">&#123;% icon icon-monkey<span class="emphasis">_shen,5 %&#125;&#123;% icon icon-monkey,6 %&#125;</span></span><br><span class="line"><span class="emphasis"></span></span><br><span class="line"><span class="emphasis">&#123;% icon icon-rooster_</span>you %&#125;&#123;% icon icon-rooster,2 %&#125;</span><br><span class="line"></span><br><span class="line">&#123;% icon icon-dog<span class="emphasis">_xu,3 %&#125;&#123;% icon icon-dog,4 %&#125;</span></span><br><span class="line"><span class="emphasis"></span></span><br><span class="line"><span class="emphasis">&#123;% icon icon-boar_</span>hai,5 %&#125;&#123;% icon icon-boar,6 %&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-4"><p><svg class="icon" style="width:1em; height:1em" aria-hidden="true"><use xlink:href="#icon-rat_zi"></use></svg><svg class="icon" style="width:2em; height:2em" aria-hidden="true"><use xlink:href="#icon-rat"></use></svg></p><p><svg class="icon" style="width:3em; height:3em" aria-hidden="true"><use xlink:href="#icon-ox_chou"></use></svg><svg class="icon" style="width:4em; height:4em" aria-hidden="true"><use xlink:href="#icon-ox"></use></svg></p><p><svg class="icon" style="width:5em; height:5em" aria-hidden="true"><use xlink:href="#icon-tiger_yin"></use></svg><svg class="icon" style="width:6em; height:6em" aria-hidden="true"><use xlink:href="#icon-tiger"></use></svg></p><p><svg class="icon" style="width:1em; height:1em" aria-hidden="true"><use xlink:href="#icon-rabbit_mao"></use></svg><svg class="icon" style="width:2em; height:2em" aria-hidden="true"><use xlink:href="#icon-rabbit"></use></svg></p><p><svg class="icon" style="width:3em; height:3em" aria-hidden="true"><use xlink:href="#icon-dragon_chen"></use></svg><svg class="icon" style="width:4em; height:4em" aria-hidden="true"><use xlink:href="#icon-dragon"></use></svg></p><p><svg class="icon" style="width:5em; height:5em" aria-hidden="true"><use xlink:href="#icon-snake_si"></use></svg><svg class="icon" style="width:6em; height:6em" aria-hidden="true"><use xlink:href="#icon-snake"></use></svg></p><p><svg class="icon" style="width:1em; height:1em" aria-hidden="true"><use xlink:href="#icon-horse_wu"></use></svg><svg class="icon" style="width:2em; height:2em" aria-hidden="true"><use xlink:href="#icon-horse"></use></svg></p><p><svg class="icon" style="width:3em; height:3em" aria-hidden="true"><use xlink:href="#icon-goat_wei"></use></svg><svg class="icon" style="width:4em; height:4em" aria-hidden="true"><use xlink:href="#icon-goat"></use></svg></p><p><svg class="icon" style="width:5em; height:5em" aria-hidden="true"><use xlink:href="#icon-monkey_shen"></use></svg><svg class="icon" style="width:6em; height:6em" aria-hidden="true"><use xlink:href="#icon-monkey"></use></svg></p><p><svg class="icon" style="width:1em; height:1em" aria-hidden="true"><use xlink:href="#icon-rooster_you"></use></svg><svg class="icon" style="width:2em; height:2em" aria-hidden="true"><use xlink:href="#icon-rooster"></use></svg></p><p><svg class="icon" style="width:3em; height:3em" aria-hidden="true"><use xlink:href="#icon-dog_xu"></use></svg><svg class="icon" style="width:4em; height:4em" aria-hidden="true"><use xlink:href="#icon-dog"></use></svg></p><p><svg class="icon" style="width:5em; height:5em" aria-hidden="true"><use xlink:href="#icon-boar_hai"></use></svg><svg class="icon" style="width:6em; height:6em" aria-hidden="true"><use xlink:href="#icon-boar"></use></svg></p><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h2 id="2-24-特效标签wow">2.24 特效标签wow</h2><div class="tabs" id="分栏"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#分栏-1">示例源码</button></li><li class="tab"><button type="button" data-href="#分栏-2">渲染演示</button></li><li class="tab"><button type="button" data-href="#分栏-3">示例源码</button></li><li class="tab"><button type="button" data-href="#分栏-4">渲染演示</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="分栏-1"><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">&#123;% wow [animete],[duration],[delay],[offset],[iteration] %&#125;</span><br><span class="line">内容</span><br><span class="line">&#123;% endwow %&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-2"><ol><li><code>animate</code>: 动画样式，效果详见<a href="https://animate.style/">animate.css参考文档</a></li><li><code>duration</code>: 选填项，动画持续时间，单位可以是<code>ms</code>也可以是<code>s</code>。例如<code>3s</code>，<code>700ms</code>。</li><li><code>delay</code>: 选填项，动画开始的延迟时间，单位可以是<code>ms</code>也可以是<code>s</code>。例如<code>3s</code>，<code>700ms</code>。</li><li><code>offset</code>: 选填项，开始动画的距离（相对浏览器底部）</li><li><code>iteration</code>: 选填项，动画重复的次数</li></ol><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-3"><p>1.flip动画效果。</p><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">&#123;% wow animate<span class="strong">__zoomIn,5s,5s,100,10 %&#125;</span></span><br><span class="line"><span class="strong">&#123;% note info &#x27;fas fa-bullhorn&#x27;%&#125;</span></span><br><span class="line"><span class="strong">`zoomIn`动画效果，持续`5s`，延时`5s`，离底部`100`距离时启动，重复`10`次</span></span><br><span class="line"><span class="strong">&#123;% endnote %&#125;</span></span><br><span class="line"><span class="strong">&#123;% endwow %&#125;</span></span><br></pre></td></tr></table></figure><p>2.zoomIn动画效果，持续5s，延时5s，离底部100距离时启动，重复10次</p><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">&#123;% wow animate<span class="strong">__zoomIn,5s,5s,100,10 %&#125;</span></span><br><span class="line"><span class="strong">&#123;% note info &#x27;fas fa-bullhorn&#x27;%&#125;</span></span><br><span class="line"><span class="strong">`zoomIn`动画效果，持续`5s`，延时`5s`，离底部`100`距离时启动，重复`10`次</span></span><br><span class="line"><span class="strong">&#123;% endnote %&#125;</span></span><br><span class="line"><span class="strong">&#123;% endwow %&#125;</span></span><br></pre></td></tr></table></figure><p>3.slideInRight动画效果，持续5s，延时5s</p><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">&#123;% wow animate<span class="strong">__slideInRight,5s,5s %&#125;</span></span><br><span class="line"><span class="strong">&#123;% note warning &#x27;fa-car&#x27;%&#125;</span></span><br><span class="line"><span class="strong">`slideInRight`动画效果，持续`5s`，延时`5s`。</span></span><br><span class="line"><span class="strong">&#123;% endnote %&#125;</span></span><br><span class="line"><span class="strong">&#123;% endwow %&#125;</span></span><br></pre></td></tr></table></figure><p>4.heartBeat动画效果，延时5s，重复10次。此处注意不用的参数位置要留空，用逗号间隔。</p><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">&#123;% wow animate<span class="strong">__heartBeat,,5s,,10 %&#125;</span></span><br><span class="line"><span class="strong">&#123;% note error &#x27;fa-battery-half&#x27;%&#125;</span></span><br><span class="line"><span class="strong">`heartBeat`动画效果，延时`5s`，重复`10`次。</span></span><br><span class="line"><span class="strong">&#123;% endnote %&#125;</span></span><br><span class="line"><span class="strong">&#123;% endwow %&#125;</span></span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-4"><p>1.flip动画效果。</p><div class='wow animate__zoomIn' data-wow-duration='5s' data-wow-delay='5s' data-wow-offset='100'  data-wow-iteration='10' ><div class="note note-info">  <div class="note-header">    <i class="note-icon fa-regular fa-circle-info"></i>    <span class="note-title">fas fa-bullhorn</span>  </div>  <div class="note-content">    <p><code>zoomIn</code>动画效果，持续<code>5s</code>，延时<code>5s</code>，离底部<code>100</code>距离时启动，重复<code>10</code>次</p>  </div></div></div><p>2.zoomIn动画效果，持续5s，延时5s，离底部100距离时启动，重复10次</p><div class='wow animate__zoomIn' data-wow-duration='5s' data-wow-delay='5s' data-wow-offset='100'  data-wow-iteration='10' ><div class="note note-info">  <div class="note-header">    <i class="note-icon fa-regular fa-circle-info"></i>    <span class="note-title">fas fa-bullhorn</span>  </div>  <div class="note-content">    <p><code>zoomIn</code>动画效果，持续<code>5s</code>，延时<code>5s</code>，离底部<code>100</code>距离时启动，重复<code>10</code>次</p>  </div></div></div><p>3.slideInRight动画效果，持续5s，延时5s</p><div class='wow animate__slideInRight' data-wow-duration='5s' data-wow-delay='5s' data-wow-offset=''  data-wow-iteration='' ><div class="note note-warning">  <div class="note-header">    <i class="note-icon fa-regular fa-circle-dot"></i>    <span class="note-title">fa-car</span>  </div>  <div class="note-content">    <p><code>slideInRight</code>动画效果，持续<code>5s</code>，延时<code>5s</code>。</p>  </div></div></div><p>4.heartBeat动画效果，延时5s，重复10次。此处注意不用的参数位置要留空，用逗号间隔。</p><div class='wow animate__heartBeat' data-wow-duration='' data-wow-delay='5s' data-wow-offset=''  data-wow-iteration='10' ><div class="note note-error">  <div class="note-header">    <i class="note-icon fa-regular fa-circle-xmark"></i>    <span class="note-title">fa-battery-half</span>  </div>  <div class="note-content">    <p><code>heartBeat</code>动画效果，延时<code>5s</code>，重复<code>10</code>次。</p>  </div></div></div><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h2 id="2-25-进度条-progress">2.25  进度条 progress</h2>    <div class="note note-info">      <div class="note-header">        <i class="note-icon fa-regular fa-circle-info"></i>        <span class="note-title">提示信息</span>      </div>      <div class="note-content">        <p>进度条标签参考<a href="https://rongbuqiu.com/jdt.html">沂佰孜猫-给HEXO文章添加彩色进度条</a>。<br>源样式提取自<a href="https://zwying0814.gitbook.io/cuteen/">Cuteen</a>主题。</p>      </div>    </div>  <div class="tabs" id="分栏"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#分栏-1">参数配置</button></li><li class="tab"><button type="button" data-href="#分栏-2">标签语法</button></li><li class="tab"><button type="button" data-href="#分栏-3">示例源码</button></li><li class="tab"><button type="button" data-href="#分栏-4">渲染演示</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="分栏-1"><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&#123;% progress [width] [color] [text] %&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-2"><ol><li><code>width</code>: 0到100的阿拉伯数字</li><li><code>color</code>: 颜色，取值有red,yellow,green,cyan,blue,gray</li><li><code>text</code>:进度条上的文字内容</li></ol><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-3"><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">&#123;% progress 10 red 进度条样式预览 %&#125;</span><br><span class="line">&#123;% progress 30 yellow 进度条样式预览 %&#125;</span><br><span class="line">&#123;% progress 50 green 进度条样式预览 %&#125;</span><br><span class="line">&#123;% progress 70 cyan 进度条样式预览 %&#125;</span><br><span class="line">&#123;% progress 90 blue 进度条样式预览 %&#125;</span><br><span class="line">&#123;% progress 100 gray 进度条样式预览 %&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-4"><div class="progress"><div class="progress-bar-animated progress-bar progress-bar-striped bg-red"  style="width: 10%" aria-valuenow="10" aria-valuemin="0" aria-valuemax="100"><p>进度条样式预览</p></div></div><div class="progress"><div class="progress-bar-animated progress-bar progress-bar-striped bg-yellow"  style="width: 30%" aria-valuenow="30" aria-valuemin="0" aria-valuemax="100"><p>进度条样式预览</p></div></div><div class="progress"><div class="progress-bar-animated progress-bar progress-bar-striped bg-green"  style="width: 50%" aria-valuenow="50" aria-valuemin="0" aria-valuemax="100"><p>进度条样式预览</p></div></div><div class="progress"><div class="progress-bar-animated progress-bar progress-bar-striped bg-cyan"  style="width: 70%" aria-valuenow="70" aria-valuemin="0" aria-valuemax="100"><p>进度条样式预览</p></div></div><div class="progress"><div class="progress-bar-animated progress-bar progress-bar-striped bg-blue"  style="width: 90%" aria-valuenow="90" aria-valuemin="0" aria-valuemax="100"><p>进度条样式预览</p></div></div><div class="progress"><div class="progress-bar-animated progress-bar progress-bar-striped bg-gray"  style="width: 100%" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100"><p>进度条样式预览</p></div></div><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h2 id="2-26-注释-notation">2.26 注释 notation</h2><div class="tabs" id="分栏"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#分栏-1">标签语法</button></li><li class="tab"><button type="button" data-href="#分栏-2">参数配置</button></li><li class="tab"><button type="button" data-href="#分栏-3">示例源码</button></li><li class="tab"><button type="button" data-href="#分栏-4">渲染演示</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="分栏-1"><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&#123;% nota [label] , [text] %&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-2"><ol><li><p><code>label</code>: 注释词汇</p></li><li><p><code>text</code>: 悬停显示的注解内容</p></li></ol><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-3"><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&#123;% nota 把鼠标移动到我上面试试 ,可以看到注解内容出现在顶栏 %&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-4"><p><span class='nota' data-nota='可以看到注解内容出现在顶栏'>把鼠标移动到我上面试试</span></p><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h2 id="2-27-气泡注释-bubble">2.27 气泡注释 bubble</h2><div class="tabs" id="分栏"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#分栏-1">标签语法</button></li><li class="tab"><button type="button" data-href="#分栏-2">参数配置</button></li><li class="tab"><button type="button" data-href="#分栏-3">示例源码</button></li><li class="tab"><button type="button" data-href="#分栏-4">渲染演示</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="分栏-1"><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&#123;% bubble [content] , [notation] ,[background-color] %&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-2"><ol><li><code>content</code>: 注释词汇</li><li><code>notation</code>: 悬停显示的注解内容</li><li><code>background-color</code>: 可选，气泡背景色。默认为“#71a4e3”</li></ol><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-3"><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">最近我学到了不少新玩意儿（虽然对很多大佬来说这些已经是旧技术了），比如CSS的&#123;% bubble 兄弟相邻选择器,&quot;例如 h1 + p &#123;margin-top:50px;&#125;&quot; %&#125;，&#123;% bubble flex布局,&quot;Flex 是 Flexible Box 的缩写，意为&quot;弹性布局&quot;，用来为盒状模型提供最大的灵活性&quot;,&quot;#ec5830&quot; %&#125;，&#123;% bubble transform变换,&quot;transform 属性向元素应用 2D 或 3D 转换。该属性允许我们对元素进行旋转、缩放、移动或倾斜。&quot;,&quot;#1db675&quot; %&#125;，animation的&#123;% bubble 贝塞尔速度曲线,&quot;贝塞尔曲线(Bézier curve)，又称贝兹曲线或贝济埃曲线，是应用于二维图形应用程序的数学曲线。一般的矢量图形软件通过它来精确画出曲线，贝兹曲线由线段与节点组成，节点是可拖动的支点，线段像可伸缩的皮筋&quot;,&quot;#de4489&quot; %&#125;写法，还有今天刚看到的&#123;% bubble clip-path,&quot;clip-path属性使用裁剪方式创建元素的可显示区域。区域内的部分显示，区域外的隐藏。&quot;,&quot;#868fd7&quot; %&#125;属性。这些对我来说很新颖的概念狠狠的冲击着我以前积累起来的设计思路。</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-4"><p>最近我学到了不少新玩意儿（虽然对很多大佬来说这些已经是旧技术了），比如CSS的<span class="bubble-content">兄弟相邻选择器</span><span class="bubble-notation"><span class="bubble-item" style="background-color:#71a4e3;">例如 h1 + p {margin-top:50px;}</span></span>，<span class="bubble-content">flex布局</span><span class="bubble-notation"><span class="bubble-item" style="background-color:#ec5830;">Flex 是 Flexible Box 的缩写，意为弹性布局&quot;，用来为盒状模型提供最大的灵活性&quot;</span></span>，<span class="bubble-content">transform变换</span><span class="bubble-notation"><span class="bubble-item" style="background-color:#1db675;">transform 属性向元素应用 2D 或 3D 转换。该属性允许我们对元素进行旋转、缩放、移动或倾斜。</span></span>，animation的<span class="bubble-content">贝塞尔速度曲线</span><span class="bubble-notation"><span class="bubble-item" style="background-color:#de4489;">贝塞尔曲线(Bézier curve)，又称贝兹曲线或贝济埃曲线，是应用于二维图形应用程序的数学曲线。一般的矢量图形软件通过它来精确画出曲线，贝兹曲线由线段与节点组成，节点是可拖动的支点，线段像可伸缩的皮筋</span></span>写法，还有今天刚看到的<span class="bubble-content">clip-path</span><span class="bubble-notation"><span class="bubble-item" style="background-color:#868fd7;">clip-path属性使用裁剪方式创建元素的可显示区域。区域内的部分显示，区域外的隐藏。</span></span>属性。这些对我来说很新颖的概念狠狠的冲击着我以前积累起来的设计思路。</p><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h2 id="2-28-引用文献-reference">2.28 引用文献 reference</h2><div class="tabs" id="分栏"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#分栏-1">标签语法</button></li><li class="tab"><button type="button" data-href="#分栏-2">参数配置</button></li><li class="tab"><button type="button" data-href="#分栏-3">示例源码</button></li><li class="tab"><button type="button" data-href="#分栏-4">渲染演示</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="分栏-1"><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">&#123;% referto [id] , [literature] %&#125;</span><br><span class="line">&#123;% referfrom [id] , [literature] , [url] %&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-2"><ol><li><p>referto 引用上标</p><ul><li><p><code>id</code>: 上标序号内容，需与referfrom标签的id对应才能实现跳转</p></li><li><p><code>literature</code>: 引用的参考文献名称</p></li></ul></li><li><p>referfrom 引用出处</p><ul><li><p><code>id</code>: 序号内容，需与referto标签的id对应才能实现 跳转</p></li><li><p><code>literature</code>: 引用的参考文献名称</p></li><li><p><code>url</code>: 引用的参考文献链接，可省略</p></li></ul></li></ol><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-3"><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">Akilarの糖果屋(akilar.top)是一个私人性质的博客&#123;% referto &#x27;[1]&#x27;,&#x27;Akilarの糖果屋群聊简介&#x27; %&#125;，从各类教程至生活点滴，无话不谈。建群的目的是提供一个闲聊的场所。博客采用Hexo框架&#123;% referto &#x27;[2]&#x27;,&#x27;Hexo中文文档&#x27; %&#125;，Butterfly主题&#123;% referto &#x27;[3]&#x27;,&#x27;Butterfly 安装文档(一) 快速开始&#x27; %&#125;</span><br><span class="line"></span><br><span class="line">本项目参考了Volantis&#123;% referto &#x27;[4]&#x27;,&#x27;hexo-theme-volantis 标签插件&#x27; %&#125;的标签样式。引入<span class="code">`[tag].js`</span>，并针对<span class="code">`butterfly`</span>主题修改了相应的<span class="code">`[tag].styl`</span>。在此鸣谢<span class="code">`Volantis`</span>主题众开发者。</span><br><span class="line">主要参考内容包括各个volantis的内置标签插件文档&#123;% referto &#x27;[5]&#x27;,&#x27;Volantis文档:内置标签插件&#x27; %&#125;</span><br><span class="line">Butterfly主题的各个衍生魔改&#123;% referto &#x27;[6]&#x27;,&#x27;Butterfly 安装文档:标签外挂（Tag Plugins&#x27; %&#125;&#123;% referto &#x27;[7]&#x27;,&#x27;小弋の生活馆全样式预览&#x27; %&#125;&#123;% referto &#x27;[8]&#x27;,&#x27;l-lin-font-awesome-animation&#x27; %&#125;&#123;% referto &#x27;[9]&#x27;,&#x27;小康的butterfly主题使用文档&#x27; %&#125;</span><br><span class="line"></span><br><span class="line">&#123;% referfrom &#x27;[1]&#x27;,&#x27;Akilarの糖果屋群聊简介&#x27;,&#x27;https://jq.qq.com/?<span class="emphasis">_wv=1027&amp;k=pGLB2C0N&#x27; %&#125;</span></span><br><span class="line"><span class="emphasis">&#123;% referfrom &#x27;[2]&#x27;,&#x27;Hexo中文文档&#x27;,&#x27;https://hexo.io/zh-cn/docs/&#x27; %&#125;</span></span><br><span class="line"><span class="emphasis">&#123;% referfrom &#x27;[3]&#x27;,&#x27;Butterfly 安装文档(一) 快速开始&#x27;,&#x27;https://butterfly.js.org/posts/21cfbf15/&#x27; %&#125;</span></span><br><span class="line"><span class="emphasis">&#123;% referfrom &#x27;[4]&#x27;,&#x27;hexo-theme-volantis 标签插件&#x27;,&#x27;https://volantis.js.org/v5/tag-plugins/&#x27; %&#125;</span></span><br><span class="line"><span class="emphasis">&#123;% referfrom &#x27;[5]&#x27;,&#x27;Volantis文档:内置标签插件&#x27;,&#x27;https://volantis.js.org/tag-plugins/&#x27; %&#125;</span></span><br><span class="line"><span class="emphasis">&#123;% referfrom &#x27;[6]&#x27;,&#x27;Butterfly 安装文档:标签外挂（Tag Plugins&#x27;,&#x27;https://butterfly.js.org/posts/4aa8abbe/#%E6%A8%99%E7%B1%A4%E5%A4%96%E6%8E%9B%EF%BC%88Tag-Plugins%EF%BC%89&#x27; %&#125;</span></span><br><span class="line"><span class="emphasis">&#123;% referfrom &#x27;[7]&#x27;,&#x27;小弋の生活馆全样式预览&#x27;,&#x27;https://lovelijunyi.gitee.io/posts/c898.html&#x27; %&#125;</span></span><br><span class="line"><span class="emphasis">&#123;% referfrom &#x27;[8]&#x27;,&#x27;l-lin-font-awesome-animation&#x27;,&#x27;https://github.com/l-lin/font-awesome-animation&#x27; %&#125;</span></span><br><span class="line"><span class="emphasis">&#123;% referfrom &#x27;[9]&#x27;,&#x27;小康的butterfly主题使用文档&#x27;,&#x27;https://www.antmoe.com/posts/3b43914f/&#x27; %&#125;</span></span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-4"><p>Akilarの糖果屋(akilar.top)是一个私人性质的博客<span class="hidden-anchor" id="referto_[1]"></span><sup class="reference"><a href="#referfrom_[1]">[1]</a></sup><span class="reference-bubble"><span class="reference-item"><span class="reference-literature">Akilarの糖果屋群聊简介</span><span class="reference-title">参考资料</span></span></span>，从各类教程至生活点滴，无话不谈。建群的目的是提供一个闲聊的场所。博客采用Hexo框架<span class="hidden-anchor" id="referto_[2]"></span><sup class="reference"><a href="#referfrom_[2]">[2]</a></sup><span class="reference-bubble"><span class="reference-item"><span class="reference-literature">Hexo中文文档</span><span class="reference-title">参考资料</span></span></span>，Butterfly主题<span class="hidden-anchor" id="referto_[3]"></span><sup class="reference"><a href="#referfrom_[3]">[3]</a></sup><span class="reference-bubble"><span class="reference-item"><span class="reference-literature">Butterfly 安装文档(一) 快速开始</span><span class="reference-title">参考资料</span></span></span></p><p>本项目参考了Volantis<span class="hidden-anchor" id="referto_[4]"></span><sup class="reference"><a href="#referfrom_[4]">[4]</a></sup><span class="reference-bubble"><span class="reference-item"><span class="reference-literature">hexo-theme-volantis 标签插件</span><span class="reference-title">参考资料</span></span></span>的标签样式。引入<code>[tag].js</code>，并针对<code>butterfly</code>主题修改了相应的<code>[tag].styl</code>。在此鸣谢<code>Volantis</code>主题众开发者。<br>主要参考内容包括各个volantis的内置标签插件文档<span class="hidden-anchor" id="referto_[5]"></span><sup class="reference"><a href="#referfrom_[5]">[5]</a></sup><span class="reference-bubble"><span class="reference-item"><span class="reference-literature">Volantis文档:内置标签插件</span><span class="reference-title">参考资料</span></span></span><br>Butterfly主题的各个衍生魔改<span class="hidden-anchor" id="referto_[6]"></span><sup class="reference"><a href="#referfrom_[6]">[6]</a></sup><span class="reference-bubble"><span class="reference-item"><span class="reference-literature">Butterfly 安装文档:标签外挂（Tag Plugins</span><span class="reference-title">参考资料</span></span></span><span class="hidden-anchor" id="referto_[7]"></span><sup class="reference"><a href="#referfrom_[7]">[7]</a></sup><span class="reference-bubble"><span class="reference-item"><span class="reference-literature">小弋の生活馆全样式预览</span><span class="reference-title">参考资料</span></span></span><span class="hidden-anchor" id="referto_[8]"></span><sup class="reference"><a href="#referfrom_[8]">[8]</a></sup><span class="reference-bubble"><span class="reference-item"><span class="reference-literature">l-lin-font-awesome-animation</span><span class="reference-title">参考资料</span></span></span><span class="hidden-anchor" id="referto_[9]"></span><sup class="reference"><a href="#referfrom_[9]">[9]</a></sup><span class="reference-bubble"><span class="reference-item"><span class="reference-literature">小康的butterfly主题使用文档</span><span class="reference-title">参考资料</span></span></span></p><div class="reference-source"><span class="hidden-anchor" id="referfrom_[1]"></span><a class="reference-anchor" href="#referto_[1]">[1]<div class="reference-anchor-up fa-solid fa-angles-up"></div></a><a class="reference-link" href="https://jq.qq.com/?_wv=1027&k=pGLB2C0N">Akilarの糖果屋群聊简介</a></div><div class="reference-source"><span class="hidden-anchor" id="referfrom_[2]"></span><a class="reference-anchor" href="#referto_[2]">[2]<div class="reference-anchor-up fa-solid fa-angles-up"></div></a><a class="reference-link" href="https://hexo.io/zh-cn/docs/">Hexo中文文档</a></div><div class="reference-source"><span class="hidden-anchor" id="referfrom_[3]"></span><a class="reference-anchor" href="#referto_[3]">[3]<div class="reference-anchor-up fa-solid fa-angles-up"></div></a><a class="reference-link" href="https://butterfly.js.org/posts/21cfbf15/">Butterfly 安装文档(一) 快速开始</a></div><div class="reference-source"><span class="hidden-anchor" id="referfrom_[4]"></span><a class="reference-anchor" href="#referto_[4]">[4]<div class="reference-anchor-up fa-solid fa-angles-up"></div></a><a class="reference-link" href="https://volantis.js.org/v5/tag-plugins/">hexo-theme-volantis 标签插件</a></div><div class="reference-source"><span class="hidden-anchor" id="referfrom_[5]"></span><a class="reference-anchor" href="#referto_[5]">[5]<div class="reference-anchor-up fa-solid fa-angles-up"></div></a><a class="reference-link" href="https://volantis.js.org/tag-plugins/">Volantis文档:内置标签插件</a></div><div class="reference-source"><span class="hidden-anchor" id="referfrom_[6]"></span><a class="reference-anchor" href="#referto_[6]">[6]<div class="reference-anchor-up fa-solid fa-angles-up"></div></a><a class="reference-link" href="https://butterfly.js.org/posts/4aa8abbe/#%E6%A8%99%E7%B1%A4%E5%A4%96%E6%8E%9B%EF%BC%88Tag-Plugins%EF%BC%89">Butterfly 安装文档:标签外挂（Tag Plugins</a></div><div class="reference-source"><span class="hidden-anchor" id="referfrom_[7]"></span><a class="reference-anchor" href="#referto_[7]">[7]<div class="reference-anchor-up fa-solid fa-angles-up"></div></a><a class="reference-link" href="https://lovelijunyi.gitee.io/posts/c898.html">小弋の生活馆全样式预览</a></div><div class="reference-source"><span class="hidden-anchor" id="referfrom_[8]"></span><a class="reference-anchor" href="#referto_[8]">[8]<div class="reference-anchor-up fa-solid fa-angles-up"></div></a><a class="reference-link" href="https://github.com/l-lin/font-awesome-animation">l-lin-font-awesome-animation</a></div><div class="reference-source"><span class="hidden-anchor" id="referfrom_[9]"></span><a class="reference-anchor" href="#referto_[9]">[9]<div class="reference-anchor-up fa-solid fa-angles-up"></div></a><a class="reference-link" href="https://www.antmoe.com/posts/3b43914f/">小康的butterfly主题使用文档</a></div><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h2 id="2-29-PDF展示">2.29 PDF展示</h2><div class="tabs" id="分栏"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#分栏-1">标签语法</button></li><li class="tab"><button type="button" data-href="#分栏-2">参数配置</button></li><li class="tab"><button type="button" data-href="#分栏-3">示例源码</button></li><li class="tab"><button type="button" data-href="#分栏-4">渲染演示</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="分栏-1"><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&#123;% pdf 文件路径 %&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-2"><ol><li><code>文件路径</code>: 可以是相对路径或者是在线链接</li></ol><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-3"><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="section"># 1.本地文件:在md文件路径下创建一个同名文件夹，其内放pdf文件名为xxx.pdf的文件</span></span><br><span class="line">&#123;% pdf xxx.pdf %&#125;</span><br><span class="line"><span class="section"># 2.在线链接</span></span><br><span class="line">&#123;% pdf https://cdn.jsdelivr.net/gh/Justlovesmile/CDN/pdf/小作文讲义.pdf %&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-4"><p>2.在线链接(要放到最外层才能起作用)</p><pre><code>&lt;div class=&quot;row&quot;&gt;&lt;embed src=&quot;https://cdn.jsdelivr.net/gh/Justlovesmile/CDN/pdf/小作文讲义.pdf&quot; width=&quot;100%&quot; height=&quot;550&quot; type=&quot;application/pdf&quot;&gt;&lt;/div&gt;</code></pre><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h2 id="2-30-Hexo-tag-map-插件">2.30 Hexo-tag-map 插件</h2><div class="tabs" id="分栏"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#分栏-1">标签语法</button></li><li class="tab"><button type="button" data-href="#分栏-2">参数配置</button></li><li class="tab"><button type="button" data-href="#分栏-3">示例源码</button></li><li class="tab"><button type="button" data-href="#分栏-4">渲染演示</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="分栏-1"><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&#123;% + 标签值 + 经度 + 纬度 + 文本 + 缩放等级 + 宽 + 高 + 默认图层 + %&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-2"><table><thead><tr><th style="text-align:center">地图名</th><th style="text-align:center">标签值 &lt;必填&gt;</th><th style="text-align:center">宽 (默认 100%) / 高 (默认 360px)</th><th style="text-align:center">缩放等级 (默认 14)</th><th style="text-align:center">宽 (默认 100%) / 高 (默认 360px)</th><th style="text-align:center">默认图层 (默认 1)</th></tr></thead><tbody><tr><td style="text-align:center">混合地图</td><td style="text-align:center">map</td><td style="text-align:center">百分数或具体值 (100% 或 360px)</td><td style="text-align:center">取值 3~18</td><td style="text-align:center">百分数或具体值 (100% 或 360px)</td><td style="text-align:center">取值 1~7</td></tr><tr><td style="text-align:center">谷歌地图</td><td style="text-align:center">googleMap</td><td style="text-align:center">百分数或具体值 (100% 或 360px)</td><td style="text-align:center">取值 1~20</td><td style="text-align:center">百分数或具体值 (100% 或 360px)</td><td style="text-align:center">取值 1~3</td></tr><tr><td style="text-align:center">高德地图</td><td style="text-align:center">gaodeMap</td><td style="text-align:center">百分数或具体值 (100% 或 360px)</td><td style="text-align:center">取值 3~18</td><td style="text-align:center">百分数或具体值 (100% 或 360px)</td><td style="text-align:center">取值 1~3</td></tr><tr><td style="text-align:center">百度地图</td><td style="text-align:center">baiduMap</td><td style="text-align:center">百分数或具体值 (100% 或 360px)</td><td style="text-align:center">取值 4~18</td><td style="text-align:center">百分数或具体值 (100% 或 360px)</td><td style="text-align:center">取值 1~2</td></tr><tr><td style="text-align:center">Geoq 地图</td><td style="text-align:center">geoqMap</td><td style="text-align:center">百分数或具体值 (100% 或 360px)</td><td style="text-align:center">取值 1~18</td><td style="text-align:center">百分数或具体值 (100% 或 360px)</td><td style="text-align:center">取值 1~5</td></tr><tr><td style="text-align:center">openstreet 地图</td><td style="text-align:center">openstreetMap</td><td style="text-align:center">百分数或具体值 (100% 或 360px)</td><td style="text-align:center">取值 1~18</td><td style="text-align:center">百分数或具体值 (100% 或 360px)</td><td style="text-align:center">不支持此参数</td></tr></tbody></table><ol><li>参数之间，用英文逗号相隔</li><li>参数必须按上述事例顺序输入，不得为空</li><li>同一个页面，同一组经纬度值，只能插入一个相同标签值的地图 (若有需要，可以将第二个地图上，经度或纬度末尾删除一两个数)</li><li>参数取值必须在上述范围内</li><li>默认图层：即地图叠加层的值，默认常规地图还是卫星地图，可按地图显示顺序取值</li><li>缩放等级，数字越大，地图比例尺越小，显示的越精细</li><li>除标签值外，其他参数选填，但 每个参数的左边的参数必填</li><li>谷歌地图需要外网才能加载查看</li></ol><p>坐标获取：<a href="https://lbs.amap.com/tools/picker">高德地图坐标拾取系统</a> 、<a href="https://api.map.baidu.com/lbsapi/getpoint/index.html">百度地图坐标拾取系统</a></p><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-3"><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">&#123;% map 120.101101,30.239119 %&#125;</span><br><span class="line">&#123;% googleMap 120.101101,30.239119, 这里是西湖灵隐寺，据说求姻缘很灵验哦！ %&#125;</span><br><span class="line">&#123;% geoqMap 120.101101,30.239119, 这里是西湖灵隐寺，据说求姻缘很灵验哦！, 13, 90%, 320px, 3 %&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-4"><link rel="stylesheet" href="//unpkg.com/hexo-tag-map/lib/leaflet@1.7.1.css"><script data-pjax src="//unpkg.com/hexo-tag-map/lib/leaflet@1.7.1.js"></script><script data-pjax src="//unpkg.com/hexo-tag-map/lib/leaflet.ChineseTmsProviders@1.0.4.js"></script><div class="map-box" style="margin: 0.8rem 0 1.6rem 0;"><div id="map-120.101101-30.239119" style="max-width:100%; height:360px;display: block;margin:0 auto;z-index:1;border-radius: 5px;"></div></div><script type="text/javascript">var normalm=L.tileLayer.chinaProvider('GaoDe.Normal.Map',{maxZoom:20,minZoom:1,attribution:'高德地图'});var imgm=L.tileLayer.chinaProvider('GaoDe.Satellite.Map',{maxZoom:20,minZoom:1,attribution:'高德地图'});var imga=L.tileLayer.chinaProvider('GaoDe.Satellite.Annotion',{maxZoom:20,minZoom:1,attribution:'高德地图'});var normalMap=L.tileLayer.chinaProvider('Google.Normal.Map',{maxZoom:20,minZoom:1,attribution:'Google Maps'}),satelliteMap=L.tileLayer.chinaProvider('Google.Satellite.Map',{maxZoom:21,minZoom:1,attribution:'Google Maps'});routeMap=L.tileLayer.chinaProvider('Google.Satellite.Annotion',{maxZoom:21,minZoom:1});var normalMap=L.tileLayer.chinaProvider('Google.Normal.Map',{maxZoom:21,minZoom:1,attribution:'Google Maps'}),satelliteMap=L.tileLayer.chinaProvider('Google.Satellite.Map',{maxZoom:21,minZoom:1,attribution:'Google Maps'}),routeMap=L.tileLayer.chinaProvider('Google.Satellite.Annotion',{maxZoom:21,minZoom:1,attribution:'Google Maps'});var normalm1=L.tileLayer.chinaProvider('Geoq.Normal.Map',{maxZoom:21,minZoom:1,attribution:'GeoQ'});var normal=L.layerGroup([normalm]),image=L.layerGroup([imgm,imga]);var baseLayers={"高德地图":normal,"智图地图":normalm1,"谷歌地图":normalMap,"高德卫星地图":imgm,"谷歌卫星地图":satelliteMap,"高德卫星标注":image,"谷歌卫星标注":routeMap};var mymap=L.map('map-120.101101-30.239119',{center:[30.239119,120.101101],zoom:14,layers:[normal],zoomControl:false});L.control.layers(baseLayers,null).addTo(mymap);L.control.zoom({zoomInTitle:'放大',zoomOutTitle:'缩小'}).addTo(mymap);</script><br><link rel="stylesheet" href="//unpkg.com/hexo-tag-map/lib/leaflet@1.7.1.css"><script data-pjax src="//unpkg.com/hexo-tag-map/lib/leaflet@1.7.1.js"></script><script data-pjax src="//unpkg.com/hexo-tag-map/lib/leaflet.ChineseTmsProviders@1.0.4.js"></script><div id="googleMap-120.101101-30.239119" style="max-width:100%; height:360px;display: block;margin:0 auto;z-index:1;border-radius: 5px;"></div><script type="text/javascript">var normalMap=L.tileLayer.chinaProvider('Google.Normal.Map',{maxZoom:22,minZoom:1,attribution:'Google Maps'}),satelliteMap=L.tileLayer.chinaProvider('Google.Satellite.Map',{maxZoom:22,minZoom:1,attribution:'Google Maps'}),routeMap=L.tileLayer.chinaProvider('Google.Satellite.Annotion',{maxZoom:22,minZoom:1,attribution:'Google Maps'});var baseLayers={"谷歌地图":normalMap,"谷歌卫星图":satelliteMap,"谷歌卫星标注": routeMap};var overlayLayers={};var mymap=L.map("googleMap-120.101101-30.239119",{center:[30.239119,120.101101],zoom:14,layers:[normalMap],zoomControl:false});L.control.layers(baseLayers,null).addTo(mymap);L.control.zoom({zoomInTitle:'放大',zoomOutTitle:'缩小'}).addTo(mymap);var marker = L.marker(['30.239119','120.101101']).addTo(mymap);marker.bindPopup("这里是西湖灵隐寺，据说求姻缘很灵验哦！").openPopup();</script><br><link rel="stylesheet" href="//unpkg.com/hexo-tag-map/lib/leaflet@1.7.1.css"><script data-pjax src="//unpkg.com/hexo-tag-map/lib/leaflet@1.7.1.js"></script><script data-pjax src="//unpkg.com/hexo-tag-map/lib/leaflet.ChineseTmsProviders@1.0.4.js"></script><div id="geoqMap-120.101101-30.239119" style="max-width:90%; height:320px;display: block;margin:0 auto;z-index:1;border-radius: 5px;"></div><script type="text/javascript">var normalm1=L.tileLayer.chinaProvider('Geoq.Normal.Map',{maxZoom:20,minZoom:1,attribution:'GeoQ'});var normalm2=L.tileLayer.chinaProvider('Geoq.Normal.PurplishBlue',{maxZoom:20,minZoom:1,attribution:'GeoQ'});var normalm3=L.tileLayer.chinaProvider('Geoq.Normal.Gray',{maxZoom:20,minZoom:1,attribution:'GeoQ'});var normalm4=L.tileLayer.chinaProvider('Geoq.Normal.Warm',{maxZoom:20,minZoom:1,attribution:'GeoQ'});var normalm5=L.tileLayer.chinaProvider('Geoq.Theme.Hydro',{maxZoom:20,minZoom:1,attribution:'GeoQ'});var normal=L.layerGroup([normalm1,normalm2,normalm3,normalm4,normalm5]);var baseLayers={"智图地图":normalm1,"午夜蓝":normalm2,"灰色":normalm3,"暖色":normalm4,"水系":normalm5};var mymap=L.map("geoqMap-120.101101-30.239119",{center:[30.239119,120.101101],zoom:13,layers:[normalm3],zoomControl:false});L.control.layers(baseLayers,null).addTo(mymap);L.control.zoom({zoomInTitle:'放大',zoomOutTitle:'缩小'}).addTo(mymap);var marker = L.marker(['30.239119','120.101101']).addTo(mymap);marker.bindPopup("这里是西湖灵隐寺，据说求姻缘很灵验哦！").openPopup();</script><br><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div><h2 id="2-31-隐藏块">2.31 隐藏块</h2><div class="tabs" id="分栏"><ul class="nav-tabs"><li class="tab active"><button type="button" data-href="#分栏-1">标签语法</button></li><li class="tab"><button type="button" data-href="#分栏-2">参数配置</button></li><li class="tab"><button type="button" data-href="#分栏-3">示例源码</button></li><li class="tab"><button type="button" data-href="#分栏-4">渲染演示</button></li></ul><div class="tab-contents"><div class="tab-item-content active" id="分栏-1"><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">&#123;% hideBlock display,bg,color %&#125;</span><br><span class="line">content</span><br><span class="line">&#123;% endhideBlock %&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-2"><ol><li>content：要隐藏的内容</li><li>display：展示前按钮显示的文字（可选）</li><li>bg：按钮的背景颜色（可选）</li><li>color：按钮显示的文字的颜色（可选）</li></ol><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-3"><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">&#123;% hideBlock 点我预览, blue %&#125;</span><br><span class="line">这里有张图片：</span><br><span class="line"><span class="language-xml"><span class="tag">&lt;<span class="name">img</span> <span class="attr">src</span>=<span class="string">&quot;https://s1.vika.cn/space/2022/10/30/b35fce448bc9404a8d65c3ce1e6e46eb&quot;</span> <span class="attr">alt</span>=<span class="string">&quot;image (1)&quot;</span> <span class="attr">style</span>=<span class="string">&quot;zoom:67%;&quot;</span> /&gt;</span></span></span><br><span class="line">&#123;% endhideBlock %&#125;</span><br></pre></td></tr></table></figure><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div><div class="tab-item-content" id="分栏-4"><div class="hide-block"><button type="button" class="hide-button" style="background-color:  blue;">点我预览    </button><div class="hide-content"><p>这里有张图片：<br><img src="https://s1.vika.cn/space/2022/10/30/b35fce448bc9404a8d65c3ce1e6e46eb" alt="image (1)" style="zoom:67%;" /></p></div></div><button type="button" class="tab-to-top" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div></div>]]></content>
    
    
    <summary type="html">🥧本文汇总Markdown格式以及外挂标签在网页端的渲染效果，可作为文档进行查询</summary>
    
    
    
    <category term="书山指路" scheme="https://blog.adoreorg.cn/categories/%E4%B9%A6%E5%B1%B1%E6%8C%87%E8%B7%AF/"/>
    
    
    <category term="Markdown" scheme="https://blog.adoreorg.cn/tags/Markdown/"/>
    
  </entry>
  
  <entry>
    <title>Git使用教程：Hexo博客必备技能</title>
    <link href="https://blog.adoreorg.cn/posts/713beab.html"/>
    <id>https://blog.adoreorg.cn/posts/713beab.html</id>
    <published>2025-08-01T06:30:00.000Z</published>
    <updated>2026-06-08T11:02:20.178Z</updated>
    
    <content type="html"><![CDATA[<h2 id="🎯-为什么博客需要Git？">🎯 为什么博客需要Git？</h2><p>在Hexo博客中使用Git，不仅能实现版本控制，还能配合GitHub Actions实现自动化部署。本教程将带你从零开始掌握博客必备的Git技能。</p><h2 id="📦-基础概念">📦 基础概念</h2><h3 id="Git工作区域">Git工作区域</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">工作区(Working Directory) → 暂存区(Staging Area) → 本地仓库(Repository) → 远程仓库(Remote)</span><br></pre></td></tr></table></figure><h3 id="博客文件状态">博客文件状态</h3><ul><li><strong>已修改(modified)</strong>：修改了文件，还未提交</li><li><strong>已暂存(staged)</strong>：标记了要提交的文件</li><li><strong>已提交(committed)</strong>：文件已安全保存在本地仓库</li></ul><h2 id="🚀-博客日常操作">🚀 博客日常操作</h2><h3 id="1-新建文章并提交">1. 新建文章并提交</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 创建新文章</span></span><br><span class="line">hexo new post <span class="string">&quot;我的新文章&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 编辑完成后</span></span><br><span class="line">git add <span class="built_in">source</span>/_posts/我的新文章.md</span><br><span class="line">git commit -m <span class="string">&quot;发布新文章：我的新文章&quot;</span></span><br><span class="line">git push origin main</span><br></pre></td></tr></table></figure><h3 id="2-修改文章内容">2. 修改文章内容</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 修改文章内容后</span></span><br><span class="line">vim <span class="built_in">source</span>/_posts/我的新文章.md  <span class="comment"># 编辑文章</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 查看修改了什么</span></span><br><span class="line">git diff <span class="built_in">source</span>/_posts/我的新文章.md</span><br><span class="line"></span><br><span class="line"><span class="comment"># 提交修改</span></span><br><span class="line">git add <span class="built_in">source</span>/_posts/我的新文章.md</span><br><span class="line">git commit -m <span class="string">&quot;修正文章格式和错别字&quot;</span></span><br><span class="line">git push origin main</span><br></pre></td></tr></table></figure><h3 id="3-修改网站配置">3. 修改网站配置</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 修改主题配置后</span></span><br><span class="line">vim _config.butterfly.yml</span><br><span class="line"></span><br><span class="line"><span class="comment"># 查看所有修改</span></span><br><span class="line">git status</span><br><span class="line"></span><br><span class="line"><span class="comment"># 选择性提交</span></span><br><span class="line">git add _config.butterfly.yml</span><br><span class="line">git commit -m <span class="string">&quot;更新主题配色方案&quot;</span></span><br><span class="line">git push origin main</span><br></pre></td></tr></table></figure><h2 id="📝-实用命令清单">📝 实用命令清单</h2><h3 id="查看状态和历史">查看状态和历史</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 查看当前状态</span></span><br><span class="line">git status</span><br><span class="line"></span><br><span class="line"><span class="comment"># 查看提交历史</span></span><br><span class="line">git <span class="built_in">log</span> --oneline --graph --decorate</span><br><span class="line"></span><br><span class="line"><span class="comment"># 查看文件修改历史</span></span><br><span class="line">git <span class="built_in">log</span> --oneline <span class="built_in">source</span>/_posts/文章名.md</span><br><span class="line"></span><br><span class="line"><span class="comment"># 查看具体修改内容</span></span><br><span class="line">git diff 文件名</span><br></pre></td></tr></table></figure><h3 id="分支管理（高级用法）">分支管理（高级用法）</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 创建新分支写文章</span></span><br><span class="line">git checkout -b draft-new-post</span><br><span class="line"></span><br><span class="line"><span class="comment"># 在新分支上写文章</span></span><br><span class="line">hexo new post <span class="string">&quot;草稿文章&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 完成后再合并到main分支</span></span><br><span class="line">git checkout main</span><br><span class="line">git merge draft-new-post</span><br><span class="line">git push origin main</span><br></pre></td></tr></table></figure><h2 id="🔄-回滚操作详解">🔄 回滚操作详解</h2><h3 id="场景1：文章内容写错了">场景1：文章内容写错了</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 查看提交历史</span></span><br><span class="line">git <span class="built_in">log</span> --oneline</span><br><span class="line"></span><br><span class="line"><span class="comment"># 回滚到指定版本（保留历史）</span></span><br><span class="line">git revert abc123</span><br><span class="line">git push origin main</span><br><span class="line"></span><br><span class="line"><span class="comment"># 或者彻底回滚（删除历史）</span></span><br><span class="line">git reset --hard abc123</span><br><span class="line">git push --force origin main</span><br></pre></td></tr></table></figure><h3 id="场景2：只回滚单个文件">场景2：只回滚单个文件</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 只回滚文章内容，不影响其他配置</span></span><br><span class="line">git checkout HEAD~1 -- <span class="built_in">source</span>/_posts/文章名.md</span><br><span class="line">git commit -m <span class="string">&quot;回滚文章内容到之前版本&quot;</span></span><br><span class="line">git push origin main</span><br></pre></td></tr></table></figure><h3 id="场景3：使用GitHub网页回滚">场景3：使用GitHub网页回滚</h3><ol><li>访问GitHub仓库 → Commits</li><li>找到要回滚的提交 → 点击&quot;Revert&quot;</li><li>自动创建回滚提交，无需命令行</li></ol><h2 id="⚡-高效工作流">⚡ 高效工作流</h2><h3 id="每日写作流程">每日写作流程</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 1. 开始写作前拉取最新代码</span></span><br><span class="line">git pull origin main</span><br><span class="line"></span><br><span class="line"><span class="comment"># 2. 创建新文章</span></span><br><span class="line">hexo new post <span class="string">&quot;<span class="subst">$(date +%Y-%m-%d)</span>-今日文章&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 3. 编辑文章（用你喜欢的编辑器）</span></span><br><span class="line">code <span class="built_in">source</span>/_posts/$(<span class="built_in">date</span> +%Y-%m-%d)-今日文章.md</span><br><span class="line"></span><br><span class="line"><span class="comment"># 4. 本地预览（可选）</span></span><br><span class="line">hexo server</span><br><span class="line"></span><br><span class="line"><span class="comment"># 5. 提交并推送</span></span><br><span class="line">git add .</span><br><span class="line">git commit -m <span class="string">&quot;发布新文章：<span class="subst">$(date +%Y-%m-%d)</span>-今日文章&quot;</span></span><br><span class="line">git push origin main</span><br></pre></td></tr></table></figure><h3 id="批量操作技巧">批量操作技巧</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 一次性提交多个修改</span></span><br><span class="line">git add .</span><br><span class="line">git commit -m <span class="string">&quot;更新：新增Redis教程，修改主题配置，添加友情链接&quot;</span></span><br><span class="line">git push origin main</span><br><span class="line"></span><br><span class="line"><span class="comment"># 忽略某些文件不提交</span></span><br><span class="line"><span class="built_in">echo</span> <span class="string">&quot;draft-*.md&quot;</span> &gt;&gt; .gitignore</span><br><span class="line">git add .gitignore</span><br><span class="line">git commit -m <span class="string">&quot;忽略草稿文件&quot;</span></span><br></pre></td></tr></table></figure><h2 id="🎨-高级技巧">🎨 高级技巧</h2><h3 id="提交信息规范">提交信息规范</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 好的提交信息示例</span></span><br><span class="line">git commit -m <span class="string">&quot;文章：添加Redis集群配置教程&quot;</span></span><br><span class="line">git commit -m <span class="string">&quot;修复：修正MySQL教程中的语法错误&quot;</span></span><br><span class="line">git commit -m <span class="string">&quot;主题：更新导航栏样式&quot;</span></span><br></pre></td></tr></table></figure><h3 id="备份策略">备份策略</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 创建备份分支</span></span><br><span class="line">git branch backup-$(<span class="built_in">date</span> +%Y%m%d)</span><br><span class="line">git push origin backup-$(<span class="built_in">date</span> +%Y%m%d)</span><br><span class="line"></span><br><span class="line"><span class="comment"># 定期清理旧备份</span></span><br><span class="line">git branch -d backup-20241101</span><br><span class="line">git push origin --delete backup-20241101</span><br></pre></td></tr></table></figure><h2 id="📱-移动设备写作">📱 移动设备写作</h2><h3 id="手机-平板操作">手机/平板操作</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 使用GitHub移动端或第三方App</span></span><br><span class="line"><span class="comment"># 工作流程：</span></span><br><span class="line"><span class="comment"># 1. 手机编辑Markdown文件</span></span><br><span class="line"><span class="comment"># 2. 通过GitHub App提交</span></span><br><span class="line"><span class="comment"># 3. 自动部署完成</span></span><br></pre></td></tr></table></figure><h2 id="🔧-常见问题解决">🔧 常见问题解决</h2><h3 id="推送被拒绝">推送被拒绝</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 先拉取最新更改</span></span><br><span class="line">git pull origin main</span><br><span class="line"><span class="comment"># 解决冲突后再次推送</span></span><br><span class="line">git push origin main</span><br></pre></td></tr></table></figure><h3 id="误提交敏感信息">误提交敏感信息</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 如果提交了密码等敏感信息</span></span><br><span class="line">git reset --hard HEAD~1</span><br><span class="line">git push --force origin main</span><br><span class="line"><span class="comment"># 然后修改文件重新提交</span></span><br></pre></td></tr></table></figure><h2 id="🎯-最佳实践总结">🎯 最佳实践总结</h2><ol><li><strong>小步快跑</strong>：每次只提交一个逻辑修改</li><li><strong>清晰描述</strong>：提交信息要说明修改了什么</li><li><strong>及时备份</strong>：重要修改后立即推送</li><li><strong>分支管理</strong>：复杂修改使用分支</li><li><strong>定期清理</strong>：删除无用分支和旧备份</li></ol><h2 id="📚-学习资源">📚 学习资源</h2><ul><li><a href="https://git-scm.com/doc">Git官方文档</a></li><li><a href="https://lab.github.com">GitHub学习实验室</a></li><li><a href="https://git-scm.com/book/zh/v2">Pro Git中文版</a></li></ul><hr><blockquote><p>💡 <strong>小贴士</strong>：刚开始记不住命令没关系，把常用命令贴在显示器旁边，用几次就熟练了！</p></blockquote><blockquote><p>🚀 <strong>效率提升</strong>：配合GitHub Actions，你的<code>git push origin main</code>就是一键发布的魔法棒！</p></blockquote>]]></content>
    
    
    <summary type="html">详细讲解Git在Hexo博客中的实际应用，从基础命令到高级技巧，配合GitHub Actions实现自动化部署。</summary>
    
    
    
    <category term="笔耕问道" scheme="https://blog.adoreorg.cn/categories/%E7%AC%94%E8%80%95%E9%97%AE%E9%81%93/"/>
    
    
    <category term="教程" scheme="https://blog.adoreorg.cn/tags/%E6%95%99%E7%A8%8B/"/>
    
    <category term="Git" scheme="https://blog.adoreorg.cn/tags/Git/"/>
    
    <category term="Hexo" scheme="https://blog.adoreorg.cn/tags/Hexo/"/>
    
  </entry>
  
</feed>
